Commit 92e4afc1 authored by rvargas@google.com's avatar rvargas@google.com

Revert 96974 - Remove mock_ffmpeg and update media unittests.

BUG=92429
TEST=BitstreamConverterTest.*, ChunkDemuxerTest.*, FFmpegDemuxerTest.*, FFmpegGlueTest.*, FFmpegVideoDecoderTest.*, FFmpegH264BitstreamConverterTest.*, FFmpegVideoDecodeEngineTest.*

Review URL: http://codereview.chromium.org/7587012

TBR=acolwell@chromium.org
Review URL: http://codereview.chromium.org/7658017

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@96977 0039d316-1c4b-4281-b951-d872f2087c98
parent 3d143e9b
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
......@@ -25,11 +25,6 @@ namespace media {
// Returns true if everything was successfully initialized, false otherwise.
bool InitializeMediaLibrary(const FilePath& module_dir);
// Helper function for unit tests to avoid boiler plate code everywhere. This
// function will crash if it fails to load the media library. This ensures tests
// fail if the media library is not available.
void InitializeMediaLibraryForTesting();
// Use this if you need to check whether the media library is initialized
// for the this process, without actually trying to initialize it.
bool IsMediaLibraryInitialized();
......
......@@ -92,12 +92,6 @@ bool InitializeMediaLibrary(const FilePath& module_dir) {
return g_media_library_is_initialized;
}
void InitializeMediaLibraryForTesting() {
FilePath file_path;
CHECK(PathService::Get(base::DIR_EXE, &file_path));
CHECK(InitializeMediaLibrary(file_path));
}
bool IsMediaLibraryInitialized() {
return g_media_library_is_initialized;
}
......
......@@ -78,12 +78,6 @@ bool InitializeMediaLibrary(const FilePath& base_path) {
return g_media_library_is_initialized;
}
void InitializeMediaLibraryForTesting() {
FilePath file_path;
CHECK(PathService::Get(base::DIR_EXE, &file_path));
CHECK(InitializeMediaLibrary(file_path));
}
bool IsMediaLibraryInitialized() {
return g_media_library_is_initialized;
}
......
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/base/mock_ffmpeg.h"
#include "base/logging.h"
#include "media/ffmpeg/ffmpeg_common.h"
using ::testing::_;
using ::testing::AtMost;
using ::testing::DoAll;
using ::testing::Return;
using ::testing::SaveArg;
namespace media {
MockFFmpeg* MockFFmpeg::instance_ = NULL;
URLProtocol* MockFFmpeg::protocol_ = NULL;
MockFFmpeg::MockFFmpeg()
: outstanding_packets_(0) {
CHECK(instance_ == NULL) << "Only a single MockFFmpeg instance can exist";
instance_ = this;
// If we haven't assigned our static copy of URLProtocol, set up expectations
// to catch the URLProtocol registered when the singleton instance of
// FFmpegGlue is created.
//
// TODO(scherkus): this feels gross and I need to think of a way to better
// inject/mock singletons.
if (!protocol_) {
EXPECT_CALL(*this, AVLogSetLevel(AV_LOG_QUIET))
.Times(AtMost(1))
.WillOnce(Return());
EXPECT_CALL(*this, AVCodecInit())
.Times(AtMost(1))
.WillOnce(Return());
EXPECT_CALL(*this, AVRegisterProtocol2(_,_))
.Times(AtMost(1))
.WillOnce(DoAll(SaveArg<0>(&protocol_), Return(0)));
EXPECT_CALL(*this, AVRegisterAll())
.Times(AtMost(1))
.WillOnce(Return());
}
}
MockFFmpeg::~MockFFmpeg() {
CHECK(!outstanding_packets_)
<< "MockFFmpeg destroyed with outstanding packets";
CHECK(instance_);
instance_ = NULL;
}
void MockFFmpeg::inc_outstanding_packets() {
++outstanding_packets_;
}
void MockFFmpeg::dec_outstanding_packets() {
CHECK(outstanding_packets_ > 0);
--outstanding_packets_;
}
// static
MockFFmpeg* MockFFmpeg::get() {
return instance_;
}
// static
URLProtocol* MockFFmpeg::protocol() {
return protocol_;
}
// static
void MockFFmpeg::DestructPacket(AVPacket* packet) {
delete [] packet->data;
packet->data = NULL;
packet->size = 0;
}
// FFmpeg stubs that delegate to the FFmpegMock instance.
extern "C" {
void avcodec_init() {
MockFFmpeg::get()->AVCodecInit();
}
int av_register_protocol2(URLProtocol* protocol, int size) {
return MockFFmpeg::get()->AVRegisterProtocol2(protocol, size);
}
void av_register_all() {
MockFFmpeg::get()->AVRegisterAll();
}
int av_lockmgr_register(int (*cb)(void**, enum AVLockOp)) {
// Here |mock| may be NULL when this function is called from ~FFmpegGlue().
if (MockFFmpeg::get()) {
return MockFFmpeg::get()->AVRegisterLockManager(cb);
}
return 0;
}
AVCodec* avcodec_find_decoder(enum CodecID id) {
return MockFFmpeg::get()->AVCodecFindDecoder(id);
}
int avcodec_open(AVCodecContext* avctx, AVCodec* codec) {
return MockFFmpeg::get()->AVCodecOpen(avctx, codec);
}
int avcodec_close(AVCodecContext* avctx) {
return MockFFmpeg::get()->AVCodecClose(avctx);
}
void avcodec_flush_buffers(AVCodecContext* avctx) {
return MockFFmpeg::get()->AVCodecFlushBuffers(avctx);
}
AVCodecContext* avcodec_alloc_context() {
return MockFFmpeg::get()->AVCodecAllocContext();
}
AVFrame* avcodec_alloc_frame() {
return MockFFmpeg::get()->AVCodecAllocFrame();
}
int avcodec_decode_video2(AVCodecContext* avctx, AVFrame* picture,
int* got_picture_ptr, AVPacket* avpkt) {
return MockFFmpeg::get()->
AVCodecDecodeVideo2(avctx, picture, got_picture_ptr, avpkt);
}
AVBitStreamFilterContext* av_bitstream_filter_init(const char* name) {
return MockFFmpeg::get()->AVBitstreamFilterInit(name);
}
int av_bitstream_filter_filter(AVBitStreamFilterContext* bsfc,
AVCodecContext* avctx,
const char* args,
uint8_t** poutbuf,
int* poutbuf_size,
const uint8_t* buf,
int buf_size,
int keyframe) {
return MockFFmpeg::get()->
AVBitstreamFilterFilter(bsfc, avctx, args, poutbuf, poutbuf_size, buf,
buf_size, keyframe);
}
void av_bitstream_filter_close(AVBitStreamFilterContext* bsf) {
return MockFFmpeg::get()->AVBitstreamFilterClose(bsf);
}
int av_open_input_file(AVFormatContext** format, const char* filename,
AVInputFormat* input_format, int buffer_size,
AVFormatParameters* parameters) {
return MockFFmpeg::get()->AVOpenInputFile(format, filename,
input_format, buffer_size,
parameters);
}
void av_close_input_file(AVFormatContext* format) {
MockFFmpeg::get()->AVCloseInputFile(format);
}
int av_find_stream_info(AVFormatContext* format) {
return MockFFmpeg::get()->AVFindStreamInfo(format);
}
int64 av_rescale_q(int64 a, AVRational bq, AVRational cq) {
// Because this is a math function there's little point in mocking it, so we
// implement a cheap version that's capable of overflowing.
int64 num = bq.num * cq.den;
int64 den = cq.num * bq.den;
// Rescale a by num/den. The den / 2 is for rounding.
return (a * num + den / 2) / den;
}
int av_read_frame(AVFormatContext* format, AVPacket* packet) {
return MockFFmpeg::get()->AVReadFrame(format, packet);
}
int av_seek_frame(AVFormatContext *format, int stream_index, int64_t timestamp,
int flags) {
return MockFFmpeg::get()->AVSeekFrame(format, stream_index, timestamp,
flags);
}
void av_init_packet(AVPacket* pkt) {
return MockFFmpeg::get()->AVInitPacket(pkt);
}
int av_new_packet(AVPacket* packet, int size) {
return MockFFmpeg::get()->AVNewPacket(packet, size);
}
void av_free_packet(AVPacket* packet) {
MockFFmpeg::get()->AVFreePacket(packet);
}
void av_free(void* ptr) {
// Freeing NULL pointers are valid, but they aren't interesting from a mock
// perspective.
if (ptr) {
MockFFmpeg::get()->AVFree(ptr);
}
}
int av_dup_packet(AVPacket* packet) {
return MockFFmpeg::get()->AVDupPacket(packet);
}
void av_log_set_level(int level) {
MockFFmpeg::get()->AVLogSetLevel(level);
}
void av_destruct_packet(AVPacket *pkt) {
MockFFmpeg::get()->AVDestructPacket(pkt);
}
} // extern "C"
} // namespace media
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef MEDIA_BASE_MOCK_FFMPEG_H_
#define MEDIA_BASE_MOCK_FFMPEG_H_
// TODO(scherkus): See if we can remove ffmpeg_common from this file.
#include "media/ffmpeg/ffmpeg_common.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace media {
class MockFFmpeg {
public:
MockFFmpeg();
virtual ~MockFFmpeg();
// TODO(ajwong): Organize this class, and make sure that all mock entrypoints
// are still used.
MOCK_METHOD0(AVCodecInit, void());
MOCK_METHOD2(AVRegisterProtocol2, int(URLProtocol* protocol, int size));
MOCK_METHOD0(AVRegisterAll, void());
MOCK_METHOD1(AVRegisterLockManager, int(int (*cb)(void**, enum AVLockOp)));
MOCK_METHOD1(AVCodecFindDecoder, AVCodec*(enum CodecID id));
MOCK_METHOD2(AVCodecOpen, int(AVCodecContext* avctx, AVCodec* codec));
MOCK_METHOD1(AVCodecClose, int(AVCodecContext* avctx));
MOCK_METHOD2(AVCodecThreadInit, int(AVCodecContext* avctx, int threads));
MOCK_METHOD1(AVCodecFlushBuffers, void(AVCodecContext* avctx));
MOCK_METHOD0(AVCodecAllocContext, AVCodecContext*());
MOCK_METHOD0(AVCodecAllocFrame, AVFrame*());
MOCK_METHOD4(AVCodecDecodeVideo2,
int(AVCodecContext* avctx, AVFrame* picture,
int* got_picture_ptr, AVPacket* avpkt));
MOCK_METHOD1(AVBitstreamFilterInit,
AVBitStreamFilterContext*(const char *name));
MOCK_METHOD8(AVBitstreamFilterFilter,
int(AVBitStreamFilterContext* bsfc, AVCodecContext* avctx,
const char* args, uint8_t** poutbuf, int* poutbuf_size,
const uint8_t* buf, int buf_size, int keyframe));
MOCK_METHOD1(AVBitstreamFilterClose, void(AVBitStreamFilterContext* bsf));
MOCK_METHOD1(AVDestructPacket, void(AVPacket* packet));
MOCK_METHOD5(AVOpenInputFile, int(AVFormatContext** format,
const char* filename,
AVInputFormat* input_format,
int buffer_size,
AVFormatParameters* parameters));
MOCK_METHOD1(AVCloseInputFile, void(AVFormatContext* format));
MOCK_METHOD1(AVFindStreamInfo, int(AVFormatContext* format));
MOCK_METHOD2(AVReadFrame, int(AVFormatContext* format, AVPacket* packet));
MOCK_METHOD4(AVSeekFrame, int(AVFormatContext *format,
int stream_index,
int64_t timestamp,
int flags));
MOCK_METHOD1(AVInitPacket, void(AVPacket* pkt));
MOCK_METHOD2(AVNewPacket, int(AVPacket* packet, int size));
MOCK_METHOD1(AVFreePacket, void(AVPacket* packet));
MOCK_METHOD1(AVFree, void(void* ptr));
MOCK_METHOD1(AVDupPacket, int(AVPacket* packet));
MOCK_METHOD1(AVLogSetLevel, void(int level));
// Used for verifying check points during tests.
MOCK_METHOD1(CheckPoint, void(int id));
// Returns the current MockFFmpeg instance.
static MockFFmpeg* get();
// Returns the URLProtocol registered by the FFmpegGlue singleton.
static URLProtocol* protocol();
// AVPacket destructor for packets allocated by av_new_packet().
static void DestructPacket(AVPacket* packet);
// Modifies the number of outstanding packets.
void inc_outstanding_packets();
void dec_outstanding_packets();
private:
static MockFFmpeg* instance_;
static URLProtocol* protocol_;
// Tracks the number of packets allocated by calls to av_read_frame() and
// av_free_packet(). We crash the unit test if this is not zero at time of
// destruction.
int outstanding_packets_;
};
// Used for simulating av_read_frame().
ACTION_P3(CreatePacket, stream_index, data, size) {
// Confirm we're dealing with AVPacket so we can safely const_cast<>.
::testing::StaticAssertTypeEq<AVPacket*, arg1_type>();
memset(arg1, 0, sizeof(*arg1));
arg1->stream_index = stream_index;
arg1->data = const_cast<uint8*>(data);
arg1->size = size;
// Increment number of packets allocated.
MockFFmpeg::get()->inc_outstanding_packets();
return 0;
}
// Used for simulating av_read_frame().
ACTION_P3(CreatePacketNoCount, stream_index, data, size) {
// Confirm we're dealing with AVPacket so we can safely const_cast<>.
::testing::StaticAssertTypeEq<AVPacket*, arg1_type>();
memset(arg1, 0, sizeof(*arg1));
arg1->stream_index = stream_index;
arg1->data = const_cast<uint8*>(data);
arg1->size = size;
return 0;
}
// Used for simulating av_read_frame().
ACTION_P4(CreatePacketTimeNoCount, stream_index, data, size, pts) {
// Confirm we're dealing with AVPacket so we can safely const_cast<>.
::testing::StaticAssertTypeEq<AVPacket*, arg1_type>();
memset(arg1, 0, sizeof(*arg1));
arg1->stream_index = stream_index;
arg1->data = const_cast<uint8*>(data);
arg1->size = size;
arg1->pts = pts;
return 0;
}
// Used for simulating av_new_packet().
ACTION(NewPacket) {
::testing::StaticAssertTypeEq<AVPacket*, arg0_type>();
int size = arg1;
memset(arg0, 0, sizeof(*arg0));
arg0->data = new uint8[size];
arg0->size = size;
arg0->destruct = &MockFFmpeg::DestructPacket;
// Increment number of packets allocated.
MockFFmpeg::get()->inc_outstanding_packets();
return 0;
}
// Used for simulating av_free_packet().
ACTION(FreePacket) {
::testing::StaticAssertTypeEq<AVPacket*, arg0_type>();
// Call the destructor if present, such as the one assigned in NewPacket().
if (arg0->destruct) {
arg0->destruct(arg0);
}
// Decrement number of packets allocated.
MockFFmpeg::get()->dec_outstanding_packets();
}
} // namespace media
#endif // MEDIA_BASE_MOCK_FFMPEG_H_
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/test/test_suite.h"
#include "media/base/media.h"
int main(int argc, char** argv) {
base::TestSuite suite(argc, argv);
media::InitializeMediaLibraryForTesting();
return suite.Run();
return base::TestSuite(argc, argv).Run();
}
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/base/test_data_util.h"
#include "base/file_util.h"
#include "base/logging.h"
#include "base/path_service.h"
namespace media {
void ReadTestDataFile(const std::string& name, scoped_array<uint8>* buffer,
int* size) {
FilePath file_path;
CHECK(PathService::Get(base::DIR_SOURCE_ROOT, &file_path));
file_path = file_path.Append(FILE_PATH_LITERAL("media"))
.Append(FILE_PATH_LITERAL("test"))
.Append(FILE_PATH_LITERAL("data"))
.AppendASCII(name);
int64 tmp = 0;
CHECK(file_util::GetFileSize(file_path, &tmp))
<< "Failed to get file size for '" << name << "'";
int file_size = static_cast<int>(tmp);
buffer->reset(new uint8[file_size]);
CHECK(file_size == file_util::ReadFile(file_path,
reinterpret_cast<char*>(buffer->get()),
file_size))
<< "Failed to read '" << name << "'";
*size = file_size;
}
void ReadTestDataFile(const std::string& name, scoped_refptr<Buffer>* buffer) {
scoped_array<uint8> buf;
int buf_size;
ReadTestDataFile(name, &buf, &buf_size);
*buffer = new DataBuffer(buf.release(), buf_size);
}
} // namespace media
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef MEDIA_BASE_TEST_DATA_UTIL_H_
#define MEDIA_BASE_TEST_DATA_UTIL_H_
#include <string>
#include "base/basictypes.h"
#include "base/scoped_ptr.h"
#include "media/base/data_buffer.h"
namespace media {
// Reads a test file from media/test/data directory and stores it in
// a scoped_array.
//
// |name| - The name of the file.
// |buffer| - The contents of the file.
// |size| - The size of the buffer.
void ReadTestDataFile(const std::string& name,
scoped_array<uint8>* buffer,
int* size);
// Reads a test file from media/test/data directory and stored it in
// a Buffer.
//
// |name| - The name of the file.
// |buffer| - The contents of the file.
void ReadTestDataFile(const std::string& name, scoped_refptr<Buffer>* buffer);
} // namespace media
#endif // MEDIA_BASE_TEST_DATA_UTIL_H_
......@@ -43,8 +43,6 @@ VideoCodec CodecIDToVideoCodec(CodecID codec_id) {
CodecID VideoCodecToCodecID(VideoCodec video_codec) {
switch (video_codec) {
case kUnknown:
return CODEC_ID_NONE;
case kCodecVC1:
return CODEC_ID_VC1;
case kCodecH264:
......
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
......@@ -69,6 +69,11 @@ class FFmpegBitstreamConverter : public BitstreamConverter {
virtual bool ConvertPacket(AVPacket* packet);
private:
FRIEND_TEST_ALL_PREFIXES(BitstreamConverterTest, ConvertPacket_FailedFilter);
FRIEND_TEST_ALL_PREFIXES(BitstreamConverterTest, ConvertPacket_Success);
FRIEND_TEST_ALL_PREFIXES(BitstreamConverterTest,
ConvertPacket_SuccessInPlace);
std::string filter_name_;
AVBitStreamFilterContext* stream_filter_;
AVCodecContext* stream_context_;
......
......@@ -4,92 +4,73 @@
#include <deque>
#include "media/base/mock_ffmpeg.h"
#include "media/ffmpeg/ffmpeg_common.h"
#include "media/filters/bitstream_converter.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace media {
static const char kTestFilterName[] = "test_filter";
static uint8_t kFailData[] = { 3, 2, 1 };
static uint8_t kNewBufferData[] = { 2, 1 };
static uint8_t kInPlaceData[] = { 1 };
static const int kFailSize = 3;
static const int kNewBufferSize = 2;
static const int kInPlaceSize = 1;
// Test filter function that looks for specific input data and changes it's
// behavior based on what is passed to |buf| & |buf_size|. The three behaviors
// simulated are the following:
// - Create a new output buffer. Triggered by |buf| == |kNewBufferData|.
// - Use the existing output buffer. Triggered by |buf| == |kInPlaceData|.
// - Signal an error. Triggered by |buf| == |kFailData|.
static int DoFilter(AVBitStreamFilterContext* bsfc,
AVCodecContext* avctx,
const char* args,
uint8_t** poutbuf,
int* poutbuf_size,
const uint8_t* buf,
int buf_size,
int keyframe) {
if (buf_size == kNewBufferSize &&
!memcmp(buf, kNewBufferData, kNewBufferSize)) {
*poutbuf_size = buf_size + 1;
*poutbuf = static_cast<uint8*>(av_malloc(*poutbuf_size));
*poutbuf[0] = 0;
memcpy((*poutbuf) + 1, buf, buf_size);
return 0;
} else if (buf_size == kInPlaceSize &&
!memcmp(buf, kInPlaceData, kInPlaceSize)) {
return 0;
}
return -1;
}
static void DoClose(AVBitStreamFilterContext* bsfc) {}
using ::testing::DoAll;
using ::testing::Mock;
using ::testing::Return;
using ::testing::ReturnNull;
using ::testing::SetArgumentPointee;
using ::testing::StrEq;
using ::testing::StrictMock;
using ::testing::_;
static AVBitStreamFilter g_stream_filter = {
kTestFilterName,
0, // Private Data Size
DoFilter,
DoClose,
0, // Next filter pointer.
};
namespace media {
class BitstreamConverterTest : public testing::Test {
protected:
BitstreamConverterTest() {
memset(&test_stream_context_, 0, sizeof(test_stream_context_));
memset(&test_filter_, 0, sizeof(test_filter_));
memset(&test_packet_, 0, sizeof(test_packet_));
test_packet_.data = kFailData;
test_packet_.size = kFailSize;
test_packet_.data = kData1;
test_packet_.size = kTestSize1;
}
virtual ~BitstreamConverterTest() {}
static void SetUpTestCase() {
// Register g_stream_filter if it isn't already registered.
if (!g_stream_filter.next)
av_register_bitstream_filter(&g_stream_filter);
}
AVCodecContext test_stream_context_;
AVBitStreamFilterContext test_filter_;
AVPacket test_packet_;
StrictMock<MockFFmpeg> mock_ffmpeg_;
static const char kTestFilterName[];
static uint8_t kData1[];
static uint8_t kData2[];
static const int kTestSize1;
static const int kTestSize2;
private:
DISALLOW_COPY_AND_ASSIGN(BitstreamConverterTest);
};
TEST_F(BitstreamConverterTest, InitializeFailed) {
FFmpegBitstreamConverter converter("BAD_FILTER_NAME", &test_stream_context_);
const char BitstreamConverterTest::kTestFilterName[] = "test_filter";
uint8_t BitstreamConverterTest::kData1[] = { 1 };
uint8_t BitstreamConverterTest::kData2[] = { 2 };
const int BitstreamConverterTest::kTestSize1 = 1;
const int BitstreamConverterTest::kTestSize2 = 2;
TEST_F(BitstreamConverterTest, Initialize) {
FFmpegBitstreamConverter converter(kTestFilterName, &test_stream_context_);
// Test Initialize returns false on a bad initialization, and cleanup is not
// done.
EXPECT_CALL(mock_ffmpeg_, AVBitstreamFilterInit(StrEq(kTestFilterName)))
.WillOnce(ReturnNull());
EXPECT_FALSE(converter.Initialize());
}
TEST_F(BitstreamConverterTest, InitializeSuccess) {
FFmpegBitstreamConverter converter(kTestFilterName, &test_stream_context_);
EXPECT_TRUE(Mock::VerifyAndClearExpectations(&mock_ffmpeg_));
// Test Initialize returns true on successful initialization, and cleanup is
// done. The cleanup will be activated when the converter object goes out of
// scope.
EXPECT_CALL(mock_ffmpeg_, AVBitstreamFilterInit(StrEq(kTestFilterName)))
.WillOnce(Return(&test_filter_));
EXPECT_CALL(mock_ffmpeg_, AVBitstreamFilterClose(&test_filter_));
EXPECT_TRUE(converter.Initialize());
}
......@@ -102,42 +83,81 @@ TEST_F(BitstreamConverterTest, ConvertPacket_NotInitialized) {
TEST_F(BitstreamConverterTest, ConvertPacket_FailedFilter) {
FFmpegBitstreamConverter converter(kTestFilterName, &test_stream_context_);
EXPECT_TRUE(converter.Initialize());
// Inject mock filter instance.
converter.stream_filter_ = &test_filter_;
// Simulate a successful filter call, that allocates a new data buffer.
EXPECT_CALL(mock_ffmpeg_,
AVBitstreamFilterFilter(&test_filter_, &test_stream_context_,
NULL, _, _,
test_packet_.data, test_packet_.size, _))
.WillOnce(Return(AVERROR(EINVAL)));
EXPECT_FALSE(converter.ConvertPacket(&test_packet_));
// Uninject mock filter instance to avoid cleanup code on destruction of
// converter.
converter.stream_filter_ = NULL;
}
TEST_F(BitstreamConverterTest, ConvertPacket_Success) {
FFmpegBitstreamConverter converter(kTestFilterName, &test_stream_context_);
EXPECT_TRUE(converter.Initialize());
// Inject mock filter instance.
converter.stream_filter_ = &test_filter_;
// Ensure our packet doesn't already have a destructor.
ASSERT_TRUE(test_packet_.destruct == NULL);
test_packet_.data = kNewBufferData;
test_packet_.size = kNewBufferSize;
// Simulate a successful filter call, that allocates a new data buffer.
EXPECT_CALL(mock_ffmpeg_,
AVBitstreamFilterFilter(&test_filter_, &test_stream_context_,
NULL, _, _,
test_packet_.data, test_packet_.size, _))
.WillOnce(DoAll(SetArgumentPointee<3>(&kData2[0]),
SetArgumentPointee<4>(kTestSize2),
Return(0)));
EXPECT_CALL(mock_ffmpeg_, AVFreePacket(&test_packet_));
EXPECT_TRUE(converter.ConvertPacket(&test_packet_));
EXPECT_NE(kNewBufferData, test_packet_.data);
EXPECT_EQ(kNewBufferSize + 1, test_packet_.size);
EXPECT_EQ(kData2, test_packet_.data);
EXPECT_EQ(kTestSize2, test_packet_.size);
EXPECT_TRUE(test_packet_.destruct != NULL);
// Uninject mock filter instance to avoid cleanup code on destruction of
// converter.
converter.stream_filter_ = NULL;
}
TEST_F(BitstreamConverterTest, ConvertPacket_SuccessInPlace) {
FFmpegBitstreamConverter converter(kTestFilterName, &test_stream_context_);
EXPECT_TRUE(converter.Initialize());
// Inject mock filter instance.
converter.stream_filter_ = &test_filter_;
// Ensure our packet is in a sane start state.
ASSERT_TRUE(test_packet_.destruct == NULL);
test_packet_.data = kInPlaceData;
test_packet_.size = kInPlaceSize;
ASSERT_EQ(kData1, test_packet_.data);
ASSERT_EQ(kTestSize1, test_packet_.size);
// Simulate a successful filter call, that reuses the input buffer. We should
// not free the packet here or alter the packet's destructor.
EXPECT_CALL(mock_ffmpeg_,
AVBitstreamFilterFilter(&test_filter_, &test_stream_context_,
NULL, _, _,
test_packet_.data, test_packet_.size, _))
.WillOnce(DoAll(SetArgumentPointee<3>(test_packet_.data),
SetArgumentPointee<4>(test_packet_.size),
Return(0)));
EXPECT_TRUE(converter.ConvertPacket(&test_packet_));
EXPECT_EQ(kInPlaceData, test_packet_.data);
EXPECT_EQ(kInPlaceSize, test_packet_.size);
EXPECT_EQ(kData1, test_packet_.data);
EXPECT_EQ(kTestSize1, test_packet_.size);
EXPECT_TRUE(test_packet_.destruct == NULL);
// Uninject mock filter instance to avoid cleanup code on destruction of
// converter.
converter.stream_filter_ = NULL;
}
} // namespace media
......@@ -2,10 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/base_paths.h"
#include "base/bind.h"
#include "base/file_util.h"
#include "base/path_service.h"
#include "media/base/media.h"
#include "media/base/mock_callback.h"
#include "media/base/mock_ffmpeg.h"
#include "media/base/mock_filter_host.h"
#include "media/base/test_data_util.h"
#include "media/filters/chunk_demuxer.h"
#include "media/filters/chunk_demuxer_client.h"
#include "media/webm/cluster_builder.h"
......@@ -54,12 +58,47 @@ class ChunkDemuxerTest : public testing::Test{
ChunkDemuxerTest()
: client_(new MockChunkDemuxerClient()),
demuxer_(new ChunkDemuxer(client_.get())) {
memset(&format_context_, 0, sizeof(format_context_));
memset(&streams_, 0, sizeof(streams_));
memset(&codecs_, 0, sizeof(codecs_));
codecs_[VIDEO].codec_type = AVMEDIA_TYPE_VIDEO;
codecs_[VIDEO].codec_id = CODEC_ID_VP8;
codecs_[VIDEO].width = 320;
codecs_[VIDEO].height = 240;
codecs_[AUDIO].codec_type = AVMEDIA_TYPE_AUDIO;
codecs_[AUDIO].codec_id = CODEC_ID_VORBIS;
codecs_[AUDIO].channels = 2;
codecs_[AUDIO].sample_rate = 44100;
}
virtual ~ChunkDemuxerTest() {
ShutdownDemuxer();
}
void ReadFile(const std::string& name, scoped_array<uint8>* buffer,
int* size) {
FilePath file_path;
EXPECT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &file_path));
file_path = file_path.Append(FILE_PATH_LITERAL("media"))
.Append(FILE_PATH_LITERAL("test"))
.Append(FILE_PATH_LITERAL("data"))
.AppendASCII(name);
int64 tmp = 0;
EXPECT_TRUE(file_util::GetFileSize(file_path, &tmp));
EXPECT_LT(tmp, 32768);
int file_size = static_cast<int>(tmp);
buffer->reset(new uint8[file_size]);
EXPECT_EQ(file_size,
file_util::ReadFile(file_path,
reinterpret_cast<char*>(buffer->get()),
file_size));
*size = file_size;
}
void CreateInfoTracks(bool has_audio, bool has_video,
scoped_array<uint8>* buffer, int* size) {
scoped_array<uint8> info;
......@@ -69,11 +108,11 @@ class ChunkDemuxerTest : public testing::Test{
scoped_array<uint8> video_track_entry;
int video_track_entry_size = 0;
ReadTestDataFile("webm_info_element", &info, &info_size);
ReadTestDataFile("webm_vorbis_track_entry", &audio_track_entry,
&audio_track_entry_size);
ReadTestDataFile("webm_vp8_track_entry", &video_track_entry,
&video_track_entry_size);
ReadFile("webm_info_element", &info, &info_size);
ReadFile("webm_vorbis_track_entry", &audio_track_entry,
&audio_track_entry_size);
ReadFile("webm_vp8_track_entry", &video_track_entry,
&video_track_entry_size);
int tracks_element_size = 0;
......@@ -120,10 +159,24 @@ class ChunkDemuxerTest : public testing::Test{
}
void AppendInfoTracks(bool has_audio, bool has_video) {
EXPECT_CALL(mock_ffmpeg_, AVOpenInputFile(_, _, NULL, 0, NULL))
.WillOnce(DoAll(SetArgumentPointee<0>(&format_context_),
Return(0)));
EXPECT_CALL(mock_ffmpeg_, AVFindStreamInfo(&format_context_))
.WillOnce(Return(0));
EXPECT_CALL(mock_ffmpeg_, AVCloseInputFile(&format_context_));
EXPECT_CALL(mock_ffmpeg_, AVRegisterLockManager(_))
.WillRepeatedly(Return(0));
scoped_array<uint8> info_tracks;
int info_tracks_size = 0;
CreateInfoTracks(has_audio, has_video, &info_tracks, &info_tracks_size);
SetupAVFormatContext(has_audio, has_video);
AppendData(info_tracks.get(), info_tracks_size);
}
......@@ -158,6 +211,12 @@ class ChunkDemuxerTest : public testing::Test{
EXPECT_CALL(*client_, DemuxerClosed());
demuxer_->Shutdown();
}
if (format_context_.streams) {
delete[] format_context_.streams;
format_context_.streams = NULL;
format_context_.nb_streams = 0;
}
}
void AddSimpleBlock(ClusterBuilder* cb, int track_num, int64 timecode) {
......@@ -167,12 +226,41 @@ class ChunkDemuxerTest : public testing::Test{
MOCK_METHOD1(Checkpoint, void(int id));
MockFFmpeg mock_ffmpeg_;
MockFilterHost mock_filter_host_;
AVFormatContext format_context_;
AVCodecContext codecs_[MAX_CODECS_INDEX];
AVStream streams_[MAX_CODECS_INDEX];
scoped_ptr<MockChunkDemuxerClient> client_;
scoped_refptr<ChunkDemuxer> demuxer_;
private:
void SetupAVFormatContext(bool has_audio, bool has_video) {
int i = 0;
format_context_.streams = new AVStream*[MAX_CODECS_INDEX];
if (has_audio) {
format_context_.streams[i] = &streams_[i];
streams_[i].codec = &codecs_[AUDIO];
streams_[i].duration = 100;
streams_[i].time_base.den = base::Time::kMicrosecondsPerSecond;
streams_[i].time_base.num = 1;
i++;
}
if (has_video) {
format_context_.streams[i] = &streams_[i];
streams_[i].codec = &codecs_[VIDEO];
streams_[i].duration = 100;
streams_[i].time_base.den = base::Time::kMicrosecondsPerSecond;
streams_[i].time_base.num = 1;
i++;
}
format_context_.nb_streams = i;
}
DISALLOW_COPY_AND_ASSIGN(ChunkDemuxerTest);
};
......
This diff is collapsed.
......@@ -146,11 +146,6 @@ FFmpegGlue* FFmpegGlue::GetInstance() {
return Singleton<FFmpegGlue>::get();
}
// static
URLProtocol* FFmpegGlue::url_protocol() {
return &kFFmpegURLProtocol;
}
std::string FFmpegGlue::AddProtocol(FFmpegURLProtocol* protocol) {
base::AutoLock auto_lock(lock_);
std::string key = GetProtocolKey(protocol);
......
......@@ -32,15 +32,15 @@
#include "base/memory/singleton.h"
#include "base/synchronization/lock.h"
struct URLProtocol;
namespace media {
class FFmpegURLProtocol {
public:
FFmpegURLProtocol() {}
FFmpegURLProtocol() {
}
virtual ~FFmpegURLProtocol() {}
virtual ~FFmpegURLProtocol() {
}
// Read the given amount of bytes into data, returns the number of bytes read
// if successful, kReadError otherwise.
......@@ -99,9 +99,6 @@ class FFmpegGlue {
typedef std::map<std::string, FFmpegURLProtocol*> ProtocolMap;
ProtocolMap protocols_;
friend class FFmpegGlueTest;
static URLProtocol* url_protocol();
DISALLOW_COPY_AND_ASSIGN(FFmpegGlue);
};
......
......@@ -4,6 +4,7 @@
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "media/base/mock_ffmpeg.h"
#include "media/base/mock_filters.h"
#include "media/ffmpeg/ffmpeg_common.h"
#include "media/filters/ffmpeg_glue.h"
......@@ -35,21 +36,17 @@ class MockProtocol : public FFmpegURLProtocol {
class FFmpegGlueTest : public ::testing::Test {
public:
FFmpegGlueTest() : protocol_(NULL) {}
FFmpegGlueTest() {}
static void SetUpTestCase() {
virtual void SetUp() {
// Singleton should initialize FFmpeg.
CHECK(FFmpegGlue::GetInstance());
}
virtual void SetUp() {
// Assign our static copy of URLProtocol for the rest of the tests.
protocol_ = FFmpegGlue::url_protocol();
protocol_ = MockFFmpeg::protocol();
CHECK(protocol_);
}
MOCK_METHOD1(CheckPoint, void(int val));
// Helper to open a URLContext pointing to the given mocked protocol.
// Callers are expected to close the context at the end of their test.
virtual void OpenContext(MockProtocol* protocol, URLContext* context) {
......@@ -65,12 +62,15 @@ class FFmpegGlueTest : public ::testing::Test {
protected:
// Fixture members.
URLProtocol* protocol_;
MockFFmpeg mock_ffmpeg_;
static URLProtocol* protocol_;
private:
DISALLOW_COPY_AND_ASSIGN(FFmpegGlueTest);
};
URLProtocol* FFmpegGlueTest::protocol_ = NULL;
TEST_F(FFmpegGlueTest, InitializeFFmpeg) {
// Make sure URLProtocol was filled out correctly.
EXPECT_STREQ("http", protocol_->name);
......@@ -118,7 +118,7 @@ TEST_F(FFmpegGlueTest, AddRemoveGetProtocol) {
InSequence s;
EXPECT_CALL(*protocol_a, OnDestroy());
EXPECT_CALL(*protocol_b, OnDestroy());
EXPECT_CALL(*this, CheckPoint(0));
EXPECT_CALL(mock_ffmpeg_, CheckPoint(0));
glue->RemoveProtocol(protocol_a.get());
glue->GetProtocol(key_a, &protocol_c);
......@@ -132,7 +132,7 @@ TEST_F(FFmpegGlueTest, AddRemoveGetProtocol) {
protocol_b.reset();
// Data sources should be deleted by this point.
CheckPoint(0);
mock_ffmpeg_.CheckPoint(0);
}
TEST_F(FFmpegGlueTest, OpenClose) {
......@@ -162,22 +162,22 @@ TEST_F(FFmpegGlueTest, OpenClose) {
// held by FFmpeg. Once we close the URLContext, the protocol should be
// destroyed.
InSequence s;
EXPECT_CALL(*this, CheckPoint(0));
EXPECT_CALL(*this, CheckPoint(1));
EXPECT_CALL(mock_ffmpeg_, CheckPoint(0));
EXPECT_CALL(mock_ffmpeg_, CheckPoint(1));
EXPECT_CALL(*protocol, OnDestroy());
EXPECT_CALL(*this, CheckPoint(2));
EXPECT_CALL(mock_ffmpeg_, CheckPoint(2));
// Remove the protocol from the glue layer, releasing a reference.
glue->RemoveProtocol(protocol.get());
CheckPoint(0);
mock_ffmpeg_.CheckPoint(0);
// Remove our own reference -- URLContext should maintain a reference.
CheckPoint(1);
mock_ffmpeg_.CheckPoint(1);
protocol.reset();
// Close the URLContext, which should release the final reference.
EXPECT_EQ(0, protocol_->url_close(&context));
CheckPoint(2);
mock_ffmpeg_.CheckPoint(2);
}
TEST_F(FFmpegGlueTest, Write) {
......@@ -309,11 +309,11 @@ TEST_F(FFmpegGlueTest, Destroy) {
// We should expect the protocol to get destroyed when the unit test
// exits.
InSequence s;
EXPECT_CALL(*this, CheckPoint(0));
EXPECT_CALL(mock_ffmpeg_, CheckPoint(0));
EXPECT_CALL(*protocol, OnDestroy());
// Remove our own reference, we shouldn't be destroyed yet.
CheckPoint(0);
mock_ffmpeg_.CheckPoint(0);
protocol.reset();
// ~FFmpegGlue() will be called when this unit test finishes execution. By
......
......@@ -2,13 +2,22 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/base/media.h"
#include "media/ffmpeg/ffmpeg_common.h"
#include "media/base/mock_ffmpeg.h"
#include "media/filters/ffmpeg_h264_bitstream_converter.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::_;
using ::testing::Invoke;
using ::testing::Return;
using ::testing::StrictMock;
namespace media {
// Forward declarations for fake FFmpeg packet handling functions.
static void fake_av_destruct_packet(AVPacket* pkt);
static void fake_av_init_packet(AVPacket* pkt);
static int fake_av_new_packet(AVPacket* pkt, int size);
// Test data arrays.
static const uint8 kHeaderDataOkWithFieldLen4[] = {
0x01, 0x42, 0x00, 0x28, 0xFF, 0xE1, 0x00, 0x08, 0x67, 0x42, 0x00, 0x28,
......@@ -259,6 +268,14 @@ static const uint8 kPacketDataOkWithFieldLen4[] = {
class FFmpegH264BitstreamConverterTest : public testing::Test {
protected:
FFmpegH264BitstreamConverterTest() {
// Set up the ffmpeg mock and use our local fake functions to do the
// actual implementation for packet allocation / freeing.
ON_CALL(ffmpeg_mock_, AVInitPacket(_))
.WillByDefault(Invoke(fake_av_init_packet));
ON_CALL(ffmpeg_mock_, AVNewPacket(_, _))
.WillByDefault(Invoke(fake_av_new_packet));
ON_CALL(ffmpeg_mock_, AVDestructPacket(_))
.WillByDefault(Invoke(fake_av_destruct_packet));
// Set up AVCConfigurationRecord correctly for tests.
// It's ok to do const cast here as data in kHeaderDataOkWithFieldLen4 is
// never written to.
......@@ -271,10 +288,16 @@ class FFmpegH264BitstreamConverterTest : public testing::Test {
void CreatePacket(AVPacket* packet, const uint8* data, uint32 data_size) {
// Create new packet sized of |data_size| from |data|.
EXPECT_CALL(ffmpeg_mock_, AVNewPacket(_, _));
EXPECT_CALL(ffmpeg_mock_, AVInitPacket(_));
EXPECT_EQ(av_new_packet(packet, data_size), 0);
memcpy(packet->data, data, data_size);
}
// FFmpeg mock implementation. We want strict mock since we will strictly
// define the order of calls and do not want any extra calls.
StrictMock<MockFFmpeg> ffmpeg_mock_;
// Variable to hold valid dummy context for testing.
AVCodecContext test_context_;
......@@ -294,9 +317,13 @@ TEST_F(FFmpegH264BitstreamConverterTest, Conversion_Success) {
// Try out the actual conversion (should be successful and allocate new
// packet and destroy the old one).
EXPECT_CALL(ffmpeg_mock_, AVNewPacket(_, _));
EXPECT_CALL(ffmpeg_mock_, AVInitPacket(_));
EXPECT_CALL(ffmpeg_mock_, AVDestructPacket(_));
EXPECT_TRUE(converter.ConvertPacket(&test_packet));
// Clean-up the test packet.
EXPECT_CALL(ffmpeg_mock_, AVDestructPacket(_));
av_destruct_packet(&test_packet);
// Converter will be automatically cleaned up.
......@@ -317,9 +344,38 @@ TEST_F(FFmpegH264BitstreamConverterTest, Conversion_SuccessBigPacket) {
// Try out the actual conversion (should be successful and allocate new
// packet and destroy the old one as we do NOT support in place transform).
EXPECT_CALL(ffmpeg_mock_, AVNewPacket(_, _));
EXPECT_CALL(ffmpeg_mock_, AVInitPacket(_));
EXPECT_CALL(ffmpeg_mock_, AVDestructPacket(_));
EXPECT_TRUE(converter.ConvertPacket(&test_packet));
// Clean-up the test packet.
EXPECT_CALL(ffmpeg_mock_, AVDestructPacket(_));
av_destruct_packet(&test_packet);
// Converter will be automatically cleaned up.
}
TEST_F(FFmpegH264BitstreamConverterTest, Conversion_FailureOutOfMem) {
FFmpegH264BitstreamConverter converter(&test_context_);
// Initialization should be always successful.
EXPECT_TRUE(converter.Initialize());
// Create new packet.
AVPacket test_packet;
CreatePacket(&test_packet, kPacketDataOkWithFieldLen4,
sizeof(kPacketDataOkWithFieldLen4));
// Try out the actual conversion (should be successful and allocate new
// packet and destroy the old one).
EXPECT_CALL(ffmpeg_mock_, AVNewPacket(_, _))
.WillOnce(Return(-1));
EXPECT_FALSE(converter.ConvertPacket(&test_packet))
<< "ConvertPacket() did not return expected failure due to out of mem";
// Clean-up the test packet.
EXPECT_CALL(ffmpeg_mock_, AVDestructPacket(_));
av_destruct_packet(&test_packet);
// Converter will be automatically cleaned up.
......@@ -348,9 +404,39 @@ TEST_F(FFmpegH264BitstreamConverterTest, Conversion_FailureNullParams) {
EXPECT_FALSE(converter.ConvertPacket(&test_packet));
// Clean-up the test packet.
EXPECT_CALL(ffmpeg_mock_, AVDestructPacket(_));
av_destruct_packet(&test_packet);
// Converted will be automatically cleaned up.
}
static void fake_av_destruct_packet(AVPacket* pkt) {
free(pkt->data);
pkt->data = NULL;
pkt->size = 0;
}
static void fake_av_init_packet(AVPacket* pkt) {
pkt->pts = AV_NOPTS_VALUE;
pkt->dts = AV_NOPTS_VALUE;
pkt->pos = -1;
pkt->duration = 0;
pkt->convergence_duration = 0;
pkt->flags = 0;
pkt->stream_index = 0;
pkt->destruct= NULL;
}
static int fake_av_new_packet(AVPacket* pkt, int size) {
uint8* data = reinterpret_cast<uint8*>(malloc(size));
av_init_packet(pkt);
pkt->data = data;
pkt->size = size;
pkt->destruct = av_destruct_packet;
if (data == NULL)
return AVERROR(ENOMEM);
return 0;
}
} // namespace media
......@@ -10,6 +10,7 @@
#include "media/base/data_buffer.h"
#include "media/base/filters.h"
#include "media/base/mock_callback.h"
#include "media/base/mock_ffmpeg.h"
#include "media/base/mock_filter_host.h"
#include "media/base/mock_filters.h"
#include "media/base/mock_task.h"
......@@ -219,6 +220,7 @@ class FFmpegVideoDecoderTest : public testing::Test {
AVCodec codec_;
AVFrame yuv_frame_;
scoped_refptr<VideoFrame> video_frame_;
StrictMock<MockFFmpeg> mock_ffmpeg_;
private:
DISALLOW_COPY_AND_ASSIGN(FFmpegVideoDecoderTest);
......
......@@ -417,6 +417,8 @@
'base/djb2_unittest.cc',
'base/filter_collection_unittest.cc',
'base/h264_bitstream_converter_unittest.cc',
'base/mock_ffmpeg.cc',
'base/mock_ffmpeg.h',
'base/mock_reader.h',
'base/mock_task.cc',
'base/mock_task.h',
......@@ -426,8 +428,6 @@
'base/run_all_unittests.cc',
'base/seekable_buffer_unittest.cc',
'base/state_matrix_unittest.cc',
'base/test_data_util.cc',
'base/test_data_util.h',
'base/video_frame_unittest.cc',
'base/video_util_unittest.cc',
'base/yuv_convert_unittest.cc',
......
......@@ -120,7 +120,6 @@ void FFmpegVideoDecodeEngine::Initialize(
kNoTimestamp);
frame_queue_available_.push_back(video_frame);
}
codec_context_->thread_count = decode_threads;
if (codec &&
avcodec_open(codec_context_, codec) >= 0 &&
......@@ -187,6 +186,7 @@ void FFmpegVideoDecodeEngine::DecodeFrame(scoped_refptr<Buffer> buffer) {
av_frame_.get(),
&frame_decoded,
&packet);
// Log the problem if we can't decode a video frame and exit early.
if (result < 0) {
LOG(ERROR) << "Error decoding a video frame with timestamp: "
......@@ -303,6 +303,24 @@ void FFmpegVideoDecodeEngine::ReadInput() {
event_handler_->ProduceVideoSample(NULL);
}
VideoFrame::Format FFmpegVideoDecodeEngine::GetSurfaceFormat() const {
// J (Motion JPEG) versions of YUV are full range 0..255.
// Regular (MPEG) YUV is 16..240.
// For now we will ignore the distinction and treat them the same.
switch (codec_context_->pix_fmt) {
case PIX_FMT_YUV420P:
case PIX_FMT_YUVJ420P:
return VideoFrame::YV12;
case PIX_FMT_YUV422P:
case PIX_FMT_YUVJ422P:
return VideoFrame::YV16;
default:
// TODO(scherkus): More formats here?
break;
}
return VideoFrame::INVALID;
}
} // namespace media
// Disable refcounting for this object because this object only lives
......
......@@ -33,6 +33,8 @@ class FFmpegVideoDecodeEngine : public VideoDecodeEngine {
virtual void Flush();
virtual void Seek();
VideoFrame::Format GetSurfaceFormat() const;
private:
void DecodeFrame(scoped_refptr<Buffer> buffer);
void ReadInput();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment