| // Copyright 2018 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 <memory> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/test/gtest_util.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "media/capabilities/in_memory_video_decode_stats_db_impl.h" |
| #include "media/capabilities/video_decode_stats_db_impl.h" |
| #include "media/capabilities/video_decode_stats_db_provider.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using testing::_; |
| using testing::Eq; |
| using testing::Pointee; |
| using testing::IsNull; |
| |
| namespace media { |
| |
| static VideoDecodeStatsDB::VideoDescKey kTestKey() { |
| return VideoDecodeStatsDB::VideoDescKey::MakeBucketedKey( |
| VP9PROFILE_PROFILE3, gfx::Size(1024, 768), 60, "com.widevine.alpha", |
| false); |
| } |
| |
| static VideoDecodeStatsDB::DecodeStatsEntry kEmtpyEntry() { |
| return VideoDecodeStatsDB::DecodeStatsEntry(0, 0, 0); |
| } |
| |
| class MockSeedDB : public VideoDecodeStatsDB { |
| public: |
| MockSeedDB() = default; |
| ~MockSeedDB() override = default; |
| |
| MOCK_METHOD1(Initialize, void(InitializeCB init_cb)); |
| MOCK_METHOD3(AppendDecodeStats, |
| void(const VideoDescKey& key, |
| const DecodeStatsEntry& entry, |
| AppendDecodeStatsCB append_done_cb)); |
| MOCK_METHOD2(GetDecodeStats, |
| void(const VideoDescKey& key, GetDecodeStatsCB get_stats_cb)); |
| MOCK_METHOD1(ClearStats, void(base::OnceClosure destroy_done_cb)); |
| }; |
| |
| class MockDBProvider : public VideoDecodeStatsDBProvider { |
| public: |
| MockDBProvider() = default; |
| ~MockDBProvider() override = default; |
| |
| MOCK_METHOD1(GetVideoDecodeStatsDB, void(GetCB get_db_b)); |
| }; |
| |
| template <bool WithSeedDB> |
| class InMemoryDBTestBase : public testing::Test { |
| public: |
| InMemoryDBTestBase() |
| : seed_db_(WithSeedDB ? new MockSeedDB() : nullptr), |
| db_provider_(WithSeedDB ? new MockDBProvider() : nullptr), |
| in_memory_db_(new InMemoryVideoDecodeStatsDBImpl(db_provider_.get())) { |
| // Setup MockDBProvider to provide the seed DB. No need to initialize the |
| // DB here since it too is a Mock. |
| if (db_provider_) { |
| using GetCB = VideoDecodeStatsDBProvider::GetCB; |
| ON_CALL(*db_provider_, GetVideoDecodeStatsDB(_)) |
| .WillByDefault([&](GetCB cb) { std::move(cb).Run(seed_db_.get()); }); |
| } |
| |
| // The InMemoryDB should NEVER modify the seed DB. |
| if (seed_db_) { |
| EXPECT_CALL(*seed_db_, AppendDecodeStats(_, _, _)).Times(0); |
| EXPECT_CALL(*seed_db_, ClearStats(_)).Times(0); |
| } |
| } |
| |
| void InitializeEmptyDB() { |
| if (seed_db_) |
| EXPECT_CALL(*db_provider_, GetVideoDecodeStatsDB(_)); |
| |
| EXPECT_CALL(*this, InitializeCB(true)); |
| |
| in_memory_db_->Initialize(base::BindOnce(&InMemoryDBTestBase::InitializeCB, |
| base::Unretained(this))); |
| scoped_task_environment_.RunUntilIdle(); |
| } |
| |
| MOCK_METHOD1(InitializeCB, void(bool success)); |
| MOCK_METHOD1(AppendDecodeStatsCB, void(bool success)); |
| MOCK_METHOD2( |
| GetDecodeStatsCB, |
| void(bool success, |
| std::unique_ptr<VideoDecodeStatsDB::DecodeStatsEntry> entry)); |
| MOCK_METHOD0(ClearStatsCB, void()); |
| |
| protected: |
| using VideoDescKey = media::VideoDecodeStatsDB::VideoDescKey; |
| using DecodeStatsEntry = media::VideoDecodeStatsDB::DecodeStatsEntry; |
| |
| base::test::ScopedTaskEnvironment scoped_task_environment_; |
| std::unique_ptr<MockSeedDB> seed_db_; |
| std::unique_ptr<MockDBProvider> db_provider_; |
| std::unique_ptr<InMemoryVideoDecodeStatsDBImpl> in_memory_db_; |
| }; |
| |
| // Specialization for tests that have/lack a seed DB. Some tests only make sense |
| // with seed DB, so we separate them. |
| class SeededInMemoryDBTest : public InMemoryDBTestBase<true> {}; |
| class SeedlessInMemoryDBTest : public InMemoryDBTestBase<false> {}; |
| |
| TEST_F(SeedlessInMemoryDBTest, ReadExpectingEmpty) { |
| InitializeEmptyDB(); |
| |
| // Database is empty, seed DB is empty => expect empty stats entry. |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(kEmtpyEntry())))); |
| |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(SeededInMemoryDBTest, ReadExpectingEmpty) { |
| InitializeEmptyDB(); |
| |
| // Make seed DB return null (empty) for this request. |
| EXPECT_CALL(*seed_db_, GetDecodeStats(Eq(kTestKey()), _)) |
| .WillOnce([](const auto& key, auto get_cb) { |
| std::move(get_cb).Run(true, nullptr); |
| }); |
| |
| // Database is empty, seed DB is empty => expect empty stats entry. |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(kEmtpyEntry())))); |
| |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(SeededInMemoryDBTest, ReadExpectingSeedData) { |
| InitializeEmptyDB(); |
| |
| // Setup seed DB to return an entry for the test key. |
| DecodeStatsEntry seed_entry(1000, 2, 10); |
| |
| EXPECT_CALL(*seed_db_, GetDecodeStats(Eq(kTestKey()), _)) |
| .WillOnce([&](const auto& key, auto get_cb) { |
| std::move(get_cb).Run(true, |
| std::make_unique<DecodeStatsEntry>(seed_entry)); |
| }); |
| |
| // Seed DB has a an entry for the test key. Expect it! |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(seed_entry)))); |
| |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| ::testing::Mock::VerifyAndClear(this); |
| |
| // Verify a second GetDecodeStats() call with the same key does not trigger a |
| // second call to the seed DB (we cache it). |
| EXPECT_CALL(*seed_db_, GetDecodeStats(_, _)).Times(0); |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(seed_entry)))); |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(SeededInMemoryDBTest, AppendReadAndClear) { |
| const DecodeStatsEntry seed_entry(1000, 2, 10); |
| const DecodeStatsEntry double_seed_entry(2000, 4, 20); |
| const DecodeStatsEntry triple_seed_entry(3000, 6, 30); |
| |
| InitializeEmptyDB(); |
| |
| // Setup seed DB to always return an entry for the test key. |
| ON_CALL(*seed_db_, GetDecodeStats(Eq(kTestKey()), _)) |
| .WillByDefault([&](const auto& key, auto get_cb) { |
| std::move(get_cb).Run(true, |
| std::make_unique<DecodeStatsEntry>(seed_entry)); |
| }); |
| |
| // First append should trigger a request for the same key from the seed DB. |
| // Simulate a successful read providing seed_entry for that key. |
| EXPECT_CALL(*seed_db_, GetDecodeStats(Eq(kTestKey()), _)); |
| |
| // Append the same seed entry, doubling the stats for this key. |
| EXPECT_CALL(*this, AppendDecodeStatsCB(true)); |
| in_memory_db_->AppendDecodeStats( |
| kTestKey(), seed_entry, |
| base::BindOnce(&InMemoryDBTestBase::AppendDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| ::testing::Mock::VerifyAndClear(this); |
| |
| // Seed DB should not be queried again for this key. |
| EXPECT_CALL(*seed_db_, GetDecodeStats(Eq(kTestKey()), _)).Times(0); |
| |
| // Now verify that the stats were doubled by the append above. |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(double_seed_entry)))); |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| ::testing::Mock::VerifyAndClear(this); |
| |
| // Append the same seed entry again to triple the stats. Additional appends |
| // should not trigger queries the seed DB for this key. |
| EXPECT_CALL(*seed_db_, GetDecodeStats(Eq(kTestKey()), _)).Times(0); |
| in_memory_db_->AppendDecodeStats( |
| kTestKey(), seed_entry, |
| base::BindOnce(&InMemoryDBTestBase::AppendDecodeStatsCB, |
| base::Unretained(this))); |
| |
| // Verify we have 3x the stats. |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(triple_seed_entry)))); |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| // Now destroy the in-memory stats... |
| EXPECT_CALL(*this, ClearStatsCB()); |
| in_memory_db_->ClearStats(base::BindOnce(&InMemoryDBTestBase::ClearStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| ::testing::Mock::VerifyAndClear(this); |
| |
| // With in-memory stats now gone, GetDecodeStats(kTestKey()) should again |
| // trigger a call to the seed DB and return the un-doubled seed stats. |
| EXPECT_CALL(*seed_db_, GetDecodeStats(Eq(kTestKey()), _)); |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(seed_entry)))); |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(SeedlessInMemoryDBTest, AppendReadAndClear) { |
| const DecodeStatsEntry entry(50, 1, 5); |
| const DecodeStatsEntry double_entry(100, 2, 10); |
| |
| InitializeEmptyDB(); |
| |
| // Expect successful append to the empty seedless DB. |
| EXPECT_CALL(*this, AppendDecodeStatsCB(true)); |
| in_memory_db_->AppendDecodeStats( |
| kTestKey(), entry, |
| base::BindOnce(&InMemoryDBTestBase::AppendDecodeStatsCB, |
| base::Unretained(this))); |
| |
| // Verify stats can be read back. |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(entry)))); |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| ::testing::Mock::VerifyAndClear(this); |
| |
| // Append same stats again to test summation. |
| EXPECT_CALL(*this, AppendDecodeStatsCB(true)); |
| in_memory_db_->AppendDecodeStats( |
| kTestKey(), entry, |
| base::BindOnce(&InMemoryDBTestBase::AppendDecodeStatsCB, |
| base::Unretained(this))); |
| |
| // Verify doubled stats can be read back. |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(double_entry)))); |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| ::testing::Mock::VerifyAndClear(this); |
| |
| // Now destroy the in-memory stats... |
| EXPECT_CALL(*this, ClearStatsCB()); |
| in_memory_db_->ClearStats(base::BindOnce(&InMemoryDBTestBase::ClearStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| ::testing::Mock::VerifyAndClear(this); |
| |
| // Verify DB now empty for this key. |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(kEmtpyEntry())))); |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(SeededInMemoryDBTest, ProvidedNullSeedDB) { |
| // DB provider may provide a null seed DB if it encounters some error. |
| EXPECT_CALL(*db_provider_, GetVideoDecodeStatsDB(_)) |
| .WillOnce([](auto get_db_cb) { std::move(get_db_cb).Run(nullptr); }); |
| |
| // Failing to obtain the seed DB is not a show stopper. The in-memory DB |
| // should simply carry on in a seedless fashion. |
| EXPECT_CALL(*this, InitializeCB(true)); |
| in_memory_db_->Initialize(base::BindOnce(&InMemoryDBTestBase::InitializeCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| ::testing::Mock::VerifyAndClear(this); |
| |
| // Writes still succeed. |
| EXPECT_CALL(*this, AppendDecodeStatsCB(true)); |
| const DecodeStatsEntry entry(50, 1, 5); |
| in_memory_db_->AppendDecodeStats( |
| kTestKey(), entry, |
| base::BindOnce(&InMemoryDBTestBase::AppendDecodeStatsCB, |
| base::Unretained(this))); |
| |
| // Reads should still succeed. |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(entry)))); |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(SeededInMemoryDBTest, SeedReadFailureOnGettingStats) { |
| // Everything seems fine at initialization... |
| InitializeEmptyDB(); |
| |
| // But seed DB will repeatedly fail to provide stats. |
| ON_CALL(*seed_db_, GetDecodeStats(_, _)) |
| .WillByDefault([](const auto& key, auto get_cb) { |
| std::move(get_cb).Run(false, nullptr); |
| }); |
| |
| // Reading the in-memory will still try to read the seed DB, and the read |
| // callback will simply report that the DB is empty for this key. |
| EXPECT_CALL(*seed_db_, GetDecodeStats(Eq(kTestKey()), _)); |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(kEmtpyEntry())))); |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(SeededInMemoryDBTest, SeedReadFailureOnAppendingingStats) { |
| // Everything seems fine at initialization... |
| InitializeEmptyDB(); |
| |
| // But seed DB will repeatedly fail to provide stats. |
| ON_CALL(*seed_db_, GetDecodeStats(_, _)) |
| .WillByDefault([](const auto& key, auto get_cb) { |
| std::move(get_cb).Run(false, nullptr); |
| }); |
| |
| // Appending to the in-memory will still try to read the seed DB, and the |
| // append will proceed successfully as if the seed DB were empty. |
| EXPECT_CALL(*seed_db_, GetDecodeStats(Eq(kTestKey()), _)); |
| EXPECT_CALL(*this, AppendDecodeStatsCB(true)); |
| const DecodeStatsEntry entry(50, 1, 5); |
| in_memory_db_->AppendDecodeStats( |
| kTestKey(), entry, |
| base::BindOnce(&InMemoryDBTestBase::AppendDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| ::testing::Mock::VerifyAndClear(this); |
| |
| // Reading the appended data works without issue and does not trigger new |
| // queries to the seed DB. |
| EXPECT_CALL(*seed_db_, GetDecodeStats(Eq(kTestKey()), _)).Times(0); |
| EXPECT_CALL(*this, GetDecodeStatsCB(true, Pointee(Eq(entry)))); |
| in_memory_db_->GetDecodeStats( |
| kTestKey(), base::BindOnce(&InMemoryDBTestBase::GetDecodeStatsCB, |
| base::Unretained(this))); |
| |
| scoped_task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(SeededInMemoryDBTest, SeedDBTearDownRace) { |
| ::testing::FLAGS_gtest_death_test_style = "threadsafe"; |
| |
| // Establish depends-on connection from InMemoryDB to SeedDB. |
| InitializeEmptyDB(); |
| |
| // Clearing the seed-db dependency should trigger a crash. |
| EXPECT_CHECK_DEATH(seed_db_.reset()); |
| } |
| |
| } // namespace media |