// Copyright (c) 2013 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 #include #include "base/files/scoped_temp_dir.h" #include "base/hash.h" #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/pickle.h" #include "base/sha1.h" #include "base/strings/stringprintf.h" #include "base/task_runner.h" #include "base/threading/platform_thread.h" #include "base/time/time.h" #include "net/base/cache_type.h" #include "net/disk_cache/simple/simple_index.h" #include "net/disk_cache/simple/simple_index_delegate.h" #include "net/disk_cache/simple/simple_index_file.h" #include "net/disk_cache/simple/simple_test_util.h" #include "net/disk_cache/simple/simple_util.h" #include "testing/gtest/include/gtest/gtest.h" namespace disk_cache { namespace { const base::Time kTestLastUsedTime = base::Time::UnixEpoch() + base::TimeDelta::FromDays(20); const uint64 kTestEntrySize = 789; } // namespace class EntryMetadataTest : public testing::Test { public: EntryMetadata NewEntryMetadataWithValues() { return EntryMetadata(kTestLastUsedTime, kTestEntrySize); } void CheckEntryMetadataValues(const EntryMetadata& entry_metadata) { EXPECT_LT(kTestLastUsedTime - base::TimeDelta::FromSeconds(2), entry_metadata.GetLastUsedTime()); EXPECT_GT(kTestLastUsedTime + base::TimeDelta::FromSeconds(2), entry_metadata.GetLastUsedTime()); EXPECT_EQ(kTestEntrySize, entry_metadata.GetEntrySize()); } }; class MockSimpleIndexFile : public SimpleIndexFile, public base::SupportsWeakPtr { public: MockSimpleIndexFile() : SimpleIndexFile(NULL, NULL, net::DISK_CACHE, base::FilePath()), load_result_(NULL), load_index_entries_calls_(0), disk_writes_(0) {} void LoadIndexEntries(base::Time cache_last_modified, const base::Closure& callback, SimpleIndexLoadResult* out_load_result) override { load_callback_ = callback; load_result_ = out_load_result; ++load_index_entries_calls_; } void WriteToDisk(const SimpleIndex::EntrySet& entry_set, uint64 cache_size, const base::TimeTicks& start, bool app_on_background, const base::Closure& callback) override { disk_writes_++; disk_write_entry_set_ = entry_set; } void GetAndResetDiskWriteEntrySet(SimpleIndex::EntrySet* entry_set) { entry_set->swap(disk_write_entry_set_); } const base::Closure& load_callback() const { return load_callback_; } SimpleIndexLoadResult* load_result() const { return load_result_; } int load_index_entries_calls() const { return load_index_entries_calls_; } int disk_writes() const { return disk_writes_; } private: base::Closure load_callback_; SimpleIndexLoadResult* load_result_; int load_index_entries_calls_; int disk_writes_; SimpleIndex::EntrySet disk_write_entry_set_; }; class SimpleIndexTest : public testing::Test, public SimpleIndexDelegate { protected: SimpleIndexTest() : hashes_(base::Bind(&HashesInitializer)), doom_entries_calls_(0) {} static uint64 HashesInitializer(size_t hash_index) { return disk_cache::simple_util::GetEntryHashKey( base::StringPrintf("key%d", static_cast(hash_index))); } void SetUp() override { scoped_ptr index_file(new MockSimpleIndexFile()); index_file_ = index_file->AsWeakPtr(); index_.reset( new SimpleIndex(NULL, this, net::DISK_CACHE, index_file.Pass())); index_->Initialize(base::Time()); } void WaitForTimeChange() { const base::Time initial_time = base::Time::Now(); do { base::PlatformThread::YieldCurrentThread(); } while (base::Time::Now() - initial_time < base::TimeDelta::FromSeconds(1)); } // From SimpleIndexDelegate: void DoomEntries(std::vector* entry_hashes, const net::CompletionCallback& callback) override { std::for_each(entry_hashes->begin(), entry_hashes->end(), std::bind1st(std::mem_fun(&SimpleIndex::Remove), index_.get())); last_doom_entry_hashes_ = *entry_hashes; ++doom_entries_calls_; } // Redirect to allow single "friend" declaration in base class. bool GetEntryForTesting(uint64 key, EntryMetadata* metadata) { SimpleIndex::EntrySet::iterator it = index_->entries_set_.find(key); if (index_->entries_set_.end() == it) return false; *metadata = it->second; return true; } void InsertIntoIndexFileReturn(uint64 hash_key, base::Time last_used_time, int entry_size) { index_file_->load_result()->entries.insert(std::make_pair( hash_key, EntryMetadata(last_used_time, entry_size))); } void ReturnIndexFile() { index_file_->load_result()->did_load = true; index_file_->load_callback().Run(); } // Non-const for timer manipulation. SimpleIndex* index() { return index_.get(); } const MockSimpleIndexFile* index_file() const { return index_file_.get(); } const std::vector& last_doom_entry_hashes() const { return last_doom_entry_hashes_; } int doom_entries_calls() const { return doom_entries_calls_; } const simple_util::ImmutableArray hashes_; scoped_ptr index_; base::WeakPtr index_file_; std::vector last_doom_entry_hashes_; int doom_entries_calls_; }; TEST_F(EntryMetadataTest, Basics) { EntryMetadata entry_metadata; EXPECT_EQ(base::Time(), entry_metadata.GetLastUsedTime()); EXPECT_EQ(0U, entry_metadata.GetEntrySize()); entry_metadata = NewEntryMetadataWithValues(); CheckEntryMetadataValues(entry_metadata); const base::Time new_time = base::Time::Now(); entry_metadata.SetLastUsedTime(new_time); EXPECT_LT(new_time - base::TimeDelta::FromSeconds(2), entry_metadata.GetLastUsedTime()); EXPECT_GT(new_time + base::TimeDelta::FromSeconds(2), entry_metadata.GetLastUsedTime()); } TEST_F(EntryMetadataTest, Serialize) { EntryMetadata entry_metadata = NewEntryMetadataWithValues(); Pickle pickle; entry_metadata.Serialize(&pickle); PickleIterator it(pickle); EntryMetadata new_entry_metadata; new_entry_metadata.Deserialize(&it); CheckEntryMetadataValues(new_entry_metadata); } TEST_F(SimpleIndexTest, IndexSizeCorrectOnMerge) { index()->SetMaxSize(100); index()->Insert(hashes_.at<2>()); index()->UpdateEntrySize(hashes_.at<2>(), 2); index()->Insert(hashes_.at<3>()); index()->UpdateEntrySize(hashes_.at<3>(), 3); index()->Insert(hashes_.at<4>()); index()->UpdateEntrySize(hashes_.at<4>(), 4); EXPECT_EQ(9U, index()->cache_size_); { scoped_ptr result(new SimpleIndexLoadResult()); result->did_load = true; index()->MergeInitializingSet(result.Pass()); } EXPECT_EQ(9U, index()->cache_size_); { scoped_ptr result(new SimpleIndexLoadResult()); result->did_load = true; const uint64 new_hash_key = hashes_.at<11>(); result->entries.insert( std::make_pair(new_hash_key, EntryMetadata(base::Time::Now(), 11))); const uint64 redundant_hash_key = hashes_.at<4>(); result->entries.insert(std::make_pair(redundant_hash_key, EntryMetadata(base::Time::Now(), 4))); index()->MergeInitializingSet(result.Pass()); } EXPECT_EQ(2U + 3U + 4U + 11U, index()->cache_size_); } // State of index changes as expected with an insert and a remove. TEST_F(SimpleIndexTest, BasicInsertRemove) { // Confirm blank state. EntryMetadata metadata; EXPECT_EQ(base::Time(), metadata.GetLastUsedTime()); EXPECT_EQ(0U, metadata.GetEntrySize()); // Confirm state after insert. index()->Insert(hashes_.at<1>()); ASSERT_TRUE(GetEntryForTesting(hashes_.at<1>(), &metadata)); base::Time now(base::Time::Now()); EXPECT_LT(now - base::TimeDelta::FromMinutes(1), metadata.GetLastUsedTime()); EXPECT_GT(now + base::TimeDelta::FromMinutes(1), metadata.GetLastUsedTime()); EXPECT_EQ(0U, metadata.GetEntrySize()); // Confirm state after remove. metadata = EntryMetadata(); index()->Remove(hashes_.at<1>()); EXPECT_FALSE(GetEntryForTesting(hashes_.at<1>(), &metadata)); EXPECT_EQ(base::Time(), metadata.GetLastUsedTime()); EXPECT_EQ(0U, metadata.GetEntrySize()); } TEST_F(SimpleIndexTest, Has) { // Confirm the base index has dispatched the request for index entries. EXPECT_TRUE(index_file_.get()); EXPECT_EQ(1, index_file_->load_index_entries_calls()); // Confirm "Has()" always returns true before the callback is called. const uint64 kHash1 = hashes_.at<1>(); EXPECT_TRUE(index()->Has(kHash1)); index()->Insert(kHash1); EXPECT_TRUE(index()->Has(kHash1)); index()->Remove(kHash1); // TODO(rdsmith): Maybe return false on explicitly removed entries? EXPECT_TRUE(index()->Has(kHash1)); ReturnIndexFile(); // Confirm "Has() returns conditionally now. EXPECT_FALSE(index()->Has(kHash1)); index()->Insert(kHash1); EXPECT_TRUE(index()->Has(kHash1)); index()->Remove(kHash1); } TEST_F(SimpleIndexTest, UseIfExists) { // Confirm the base index has dispatched the request for index entries. EXPECT_TRUE(index_file_.get()); EXPECT_EQ(1, index_file_->load_index_entries_calls()); // Confirm "UseIfExists()" always returns true before the callback is called // and updates mod time if the entry was really there. const uint64 kHash1 = hashes_.at<1>(); EntryMetadata metadata1, metadata2; EXPECT_TRUE(index()->UseIfExists(kHash1)); EXPECT_FALSE(GetEntryForTesting(kHash1, &metadata1)); index()->Insert(kHash1); EXPECT_TRUE(index()->UseIfExists(kHash1)); EXPECT_TRUE(GetEntryForTesting(kHash1, &metadata1)); WaitForTimeChange(); EXPECT_TRUE(GetEntryForTesting(kHash1, &metadata2)); EXPECT_EQ(metadata1.GetLastUsedTime(), metadata2.GetLastUsedTime()); EXPECT_TRUE(index()->UseIfExists(kHash1)); EXPECT_TRUE(GetEntryForTesting(kHash1, &metadata2)); EXPECT_LT(metadata1.GetLastUsedTime(), metadata2.GetLastUsedTime()); index()->Remove(kHash1); EXPECT_TRUE(index()->UseIfExists(kHash1)); ReturnIndexFile(); // Confirm "UseIfExists() returns conditionally now EXPECT_FALSE(index()->UseIfExists(kHash1)); EXPECT_FALSE(GetEntryForTesting(kHash1, &metadata1)); index()->Insert(kHash1); EXPECT_TRUE(index()->UseIfExists(kHash1)); EXPECT_TRUE(GetEntryForTesting(kHash1, &metadata1)); WaitForTimeChange(); EXPECT_TRUE(GetEntryForTesting(kHash1, &metadata2)); EXPECT_EQ(metadata1.GetLastUsedTime(), metadata2.GetLastUsedTime()); EXPECT_TRUE(index()->UseIfExists(kHash1)); EXPECT_TRUE(GetEntryForTesting(kHash1, &metadata2)); EXPECT_LT(metadata1.GetLastUsedTime(), metadata2.GetLastUsedTime()); index()->Remove(kHash1); EXPECT_FALSE(index()->UseIfExists(kHash1)); } TEST_F(SimpleIndexTest, UpdateEntrySize) { base::Time now(base::Time::Now()); index()->SetMaxSize(1000); const uint64 kHash1 = hashes_.at<1>(); InsertIntoIndexFileReturn(kHash1, now - base::TimeDelta::FromDays(2), 475); ReturnIndexFile(); EntryMetadata metadata; EXPECT_TRUE(GetEntryForTesting(kHash1, &metadata)); EXPECT_LT( now - base::TimeDelta::FromDays(2) - base::TimeDelta::FromSeconds(1), metadata.GetLastUsedTime()); EXPECT_GT( now - base::TimeDelta::FromDays(2) + base::TimeDelta::FromSeconds(1), metadata.GetLastUsedTime()); EXPECT_EQ(475U, metadata.GetEntrySize()); index()->UpdateEntrySize(kHash1, 600u); EXPECT_TRUE(GetEntryForTesting(kHash1, &metadata)); EXPECT_EQ(600U, metadata.GetEntrySize()); EXPECT_EQ(1, index()->GetEntryCount()); } TEST_F(SimpleIndexTest, GetEntryCount) { EXPECT_EQ(0, index()->GetEntryCount()); index()->Insert(hashes_.at<1>()); EXPECT_EQ(1, index()->GetEntryCount()); index()->Insert(hashes_.at<2>()); EXPECT_EQ(2, index()->GetEntryCount()); index()->Insert(hashes_.at<3>()); EXPECT_EQ(3, index()->GetEntryCount()); index()->Insert(hashes_.at<3>()); EXPECT_EQ(3, index()->GetEntryCount()); index()->Remove(hashes_.at<2>()); EXPECT_EQ(2, index()->GetEntryCount()); index()->Insert(hashes_.at<4>()); EXPECT_EQ(3, index()->GetEntryCount()); index()->Remove(hashes_.at<3>()); EXPECT_EQ(2, index()->GetEntryCount()); index()->Remove(hashes_.at<3>()); EXPECT_EQ(2, index()->GetEntryCount()); index()->Remove(hashes_.at<1>()); EXPECT_EQ(1, index()->GetEntryCount()); index()->Remove(hashes_.at<4>()); EXPECT_EQ(0, index()->GetEntryCount()); } // Confirm that we get the results we expect from a simple init. TEST_F(SimpleIndexTest, BasicInit) { base::Time now(base::Time::Now()); InsertIntoIndexFileReturn(hashes_.at<1>(), now - base::TimeDelta::FromDays(2), 10u); InsertIntoIndexFileReturn(hashes_.at<2>(), now - base::TimeDelta::FromDays(3), 100u); ReturnIndexFile(); EntryMetadata metadata; EXPECT_TRUE(GetEntryForTesting(hashes_.at<1>(), &metadata)); EXPECT_LT( now - base::TimeDelta::FromDays(2) - base::TimeDelta::FromSeconds(1), metadata.GetLastUsedTime()); EXPECT_GT( now - base::TimeDelta::FromDays(2) + base::TimeDelta::FromSeconds(1), metadata.GetLastUsedTime()); EXPECT_EQ(10U, metadata.GetEntrySize()); EXPECT_TRUE(GetEntryForTesting(hashes_.at<2>(), &metadata)); EXPECT_LT( now - base::TimeDelta::FromDays(3) - base::TimeDelta::FromSeconds(1), metadata.GetLastUsedTime()); EXPECT_GT( now - base::TimeDelta::FromDays(3) + base::TimeDelta::FromSeconds(1), metadata.GetLastUsedTime()); EXPECT_EQ(100U, metadata.GetEntrySize()); } // Remove something that's going to come in from the loaded index. TEST_F(SimpleIndexTest, RemoveBeforeInit) { const uint64 kHash1 = hashes_.at<1>(); index()->Remove(kHash1); InsertIntoIndexFileReturn(kHash1, base::Time::Now() - base::TimeDelta::FromDays(2), 10u); ReturnIndexFile(); EXPECT_FALSE(index()->Has(kHash1)); } // Insert something that's going to come in from the loaded index; correct // result? TEST_F(SimpleIndexTest, InsertBeforeInit) { const uint64 kHash1 = hashes_.at<1>(); index()->Insert(kHash1); InsertIntoIndexFileReturn(kHash1, base::Time::Now() - base::TimeDelta::FromDays(2), 10u); ReturnIndexFile(); EntryMetadata metadata; EXPECT_TRUE(GetEntryForTesting(kHash1, &metadata)); base::Time now(base::Time::Now()); EXPECT_LT(now - base::TimeDelta::FromMinutes(1), metadata.GetLastUsedTime()); EXPECT_GT(now + base::TimeDelta::FromMinutes(1), metadata.GetLastUsedTime()); EXPECT_EQ(0U, metadata.GetEntrySize()); } // Insert and Remove something that's going to come in from the loaded index. TEST_F(SimpleIndexTest, InsertRemoveBeforeInit) { const uint64 kHash1 = hashes_.at<1>(); index()->Insert(kHash1); index()->Remove(kHash1); InsertIntoIndexFileReturn(kHash1, base::Time::Now() - base::TimeDelta::FromDays(2), 10u); ReturnIndexFile(); EXPECT_FALSE(index()->Has(kHash1)); } // Insert and Remove something that's going to come in from the loaded index. TEST_F(SimpleIndexTest, RemoveInsertBeforeInit) { const uint64 kHash1 = hashes_.at<1>(); index()->Remove(kHash1); index()->Insert(kHash1); InsertIntoIndexFileReturn(kHash1, base::Time::Now() - base::TimeDelta::FromDays(2), 10u); ReturnIndexFile(); EntryMetadata metadata; EXPECT_TRUE(GetEntryForTesting(kHash1, &metadata)); base::Time now(base::Time::Now()); EXPECT_LT(now - base::TimeDelta::FromMinutes(1), metadata.GetLastUsedTime()); EXPECT_GT(now + base::TimeDelta::FromMinutes(1), metadata.GetLastUsedTime()); EXPECT_EQ(0U, metadata.GetEntrySize()); } // Do all above tests at once + a non-conflict to test for cross-key // interactions. TEST_F(SimpleIndexTest, AllInitConflicts) { base::Time now(base::Time::Now()); index()->Remove(hashes_.at<1>()); InsertIntoIndexFileReturn(hashes_.at<1>(), now - base::TimeDelta::FromDays(2), 10u); index()->Insert(hashes_.at<2>()); InsertIntoIndexFileReturn(hashes_.at<2>(), now - base::TimeDelta::FromDays(3), 100u); index()->Insert(hashes_.at<3>()); index()->Remove(hashes_.at<3>()); InsertIntoIndexFileReturn(hashes_.at<3>(), now - base::TimeDelta::FromDays(4), 1000u); index()->Remove(hashes_.at<4>()); index()->Insert(hashes_.at<4>()); InsertIntoIndexFileReturn(hashes_.at<4>(), now - base::TimeDelta::FromDays(5), 10000u); InsertIntoIndexFileReturn(hashes_.at<5>(), now - base::TimeDelta::FromDays(6), 100000u); ReturnIndexFile(); EXPECT_FALSE(index()->Has(hashes_.at<1>())); EntryMetadata metadata; EXPECT_TRUE(GetEntryForTesting(hashes_.at<2>(), &metadata)); EXPECT_LT(now - base::TimeDelta::FromMinutes(1), metadata.GetLastUsedTime()); EXPECT_GT(now + base::TimeDelta::FromMinutes(1), metadata.GetLastUsedTime()); EXPECT_EQ(0U, metadata.GetEntrySize()); EXPECT_FALSE(index()->Has(hashes_.at<3>())); EXPECT_TRUE(GetEntryForTesting(hashes_.at<4>(), &metadata)); EXPECT_LT(now - base::TimeDelta::FromMinutes(1), metadata.GetLastUsedTime()); EXPECT_GT(now + base::TimeDelta::FromMinutes(1), metadata.GetLastUsedTime()); EXPECT_EQ(0U, metadata.GetEntrySize()); EXPECT_TRUE(GetEntryForTesting(hashes_.at<5>(), &metadata)); EXPECT_GT( now - base::TimeDelta::FromDays(6) + base::TimeDelta::FromSeconds(1), metadata.GetLastUsedTime()); EXPECT_LT( now - base::TimeDelta::FromDays(6) - base::TimeDelta::FromSeconds(1), metadata.GetLastUsedTime()); EXPECT_EQ(100000U, metadata.GetEntrySize()); } TEST_F(SimpleIndexTest, BasicEviction) { base::Time now(base::Time::Now()); index()->SetMaxSize(1000); InsertIntoIndexFileReturn(hashes_.at<1>(), now - base::TimeDelta::FromDays(2), 475u); index()->Insert(hashes_.at<2>()); index()->UpdateEntrySize(hashes_.at<2>(), 475); ReturnIndexFile(); WaitForTimeChange(); index()->Insert(hashes_.at<3>()); // Confirm index is as expected: No eviction, everything there. EXPECT_EQ(3, index()->GetEntryCount()); EXPECT_EQ(0, doom_entries_calls()); EXPECT_TRUE(index()->Has(hashes_.at<1>())); EXPECT_TRUE(index()->Has(hashes_.at<2>())); EXPECT_TRUE(index()->Has(hashes_.at<3>())); // Trigger an eviction, and make sure the right things are tossed. // TODO(rdsmith): This is dependent on the innards of the implementation // as to at exactly what point we trigger eviction. Not sure how to fix // that. index()->UpdateEntrySize(hashes_.at<3>(), 475); EXPECT_EQ(1, doom_entries_calls()); EXPECT_EQ(1, index()->GetEntryCount()); EXPECT_FALSE(index()->Has(hashes_.at<1>())); EXPECT_FALSE(index()->Has(hashes_.at<2>())); EXPECT_TRUE(index()->Has(hashes_.at<3>())); ASSERT_EQ(2u, last_doom_entry_hashes().size()); } // Confirm all the operations queue a disk write at some point in the // future. TEST_F(SimpleIndexTest, DiskWriteQueued) { index()->SetMaxSize(1000); ReturnIndexFile(); EXPECT_FALSE(index()->write_to_disk_timer_.IsRunning()); const uint64 kHash1 = hashes_.at<1>(); index()->Insert(kHash1); EXPECT_TRUE(index()->write_to_disk_timer_.IsRunning()); index()->write_to_disk_timer_.Stop(); EXPECT_FALSE(index()->write_to_disk_timer_.IsRunning()); index()->UseIfExists(kHash1); EXPECT_TRUE(index()->write_to_disk_timer_.IsRunning()); index()->write_to_disk_timer_.Stop(); index()->UpdateEntrySize(kHash1, 20); EXPECT_TRUE(index()->write_to_disk_timer_.IsRunning()); index()->write_to_disk_timer_.Stop(); index()->Remove(kHash1); EXPECT_TRUE(index()->write_to_disk_timer_.IsRunning()); index()->write_to_disk_timer_.Stop(); } TEST_F(SimpleIndexTest, DiskWriteExecuted) { index()->SetMaxSize(1000); ReturnIndexFile(); EXPECT_FALSE(index()->write_to_disk_timer_.IsRunning()); const uint64 kHash1 = hashes_.at<1>(); index()->Insert(kHash1); index()->UpdateEntrySize(kHash1, 20); EXPECT_TRUE(index()->write_to_disk_timer_.IsRunning()); base::Closure user_task(index()->write_to_disk_timer_.user_task()); index()->write_to_disk_timer_.Stop(); EXPECT_EQ(0, index_file_->disk_writes()); user_task.Run(); EXPECT_EQ(1, index_file_->disk_writes()); SimpleIndex::EntrySet entry_set; index_file_->GetAndResetDiskWriteEntrySet(&entry_set); uint64 hash_key = kHash1; base::Time now(base::Time::Now()); ASSERT_EQ(1u, entry_set.size()); EXPECT_EQ(hash_key, entry_set.begin()->first); const EntryMetadata& entry1(entry_set.begin()->second); EXPECT_LT(now - base::TimeDelta::FromMinutes(1), entry1.GetLastUsedTime()); EXPECT_GT(now + base::TimeDelta::FromMinutes(1), entry1.GetLastUsedTime()); EXPECT_EQ(20U, entry1.GetEntrySize()); } TEST_F(SimpleIndexTest, DiskWritePostponed) { index()->SetMaxSize(1000); ReturnIndexFile(); EXPECT_FALSE(index()->write_to_disk_timer_.IsRunning()); index()->Insert(hashes_.at<1>()); index()->UpdateEntrySize(hashes_.at<1>(), 20); EXPECT_TRUE(index()->write_to_disk_timer_.IsRunning()); base::TimeTicks expected_trigger( index()->write_to_disk_timer_.desired_run_time()); WaitForTimeChange(); EXPECT_EQ(expected_trigger, index()->write_to_disk_timer_.desired_run_time()); index()->Insert(hashes_.at<2>()); index()->UpdateEntrySize(hashes_.at<2>(), 40); EXPECT_TRUE(index()->write_to_disk_timer_.IsRunning()); EXPECT_LT(expected_trigger, index()->write_to_disk_timer_.desired_run_time()); index()->write_to_disk_timer_.Stop(); } } // namespace disk_cache