// Copyright (c) 2012 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/bind.h" #include "base/file_util.h" #include "base/message_loop.h" #include "base/message_loop_proxy.h" #include "base/scoped_temp_dir.h" #include "base/threading/sequenced_worker_pool.h" #include "base/time.h" #include "base/utf_string_conversions.h" #include "testing/gtest/include/gtest/gtest.h" #include "webkit/dom_storage/dom_storage_area.h" #include "webkit/dom_storage/dom_storage_task_runner.h" #include "webkit/dom_storage/dom_storage_types.h" namespace dom_storage { class DomStorageAreaTest : public testing::Test { public: DomStorageAreaTest() : kOrigin(GURL("http://dom_storage/")), kKey(ASCIIToUTF16("key")), kValue(ASCIIToUTF16("value")), kKey2(ASCIIToUTF16("key2")), kValue2(ASCIIToUTF16("value2")) { } const GURL kOrigin; const string16 kKey; const string16 kValue; const string16 kKey2; const string16 kValue2; // Method used in the CommitTasks test case. void InjectedCommitSequencingTask(DomStorageArea* area) { // At this point the OnCommitTimer has run. // Verify that it put a commit in flight. EXPECT_EQ(1, area->commit_batches_in_flight_); EXPECT_FALSE(area->commit_batch_.get()); EXPECT_TRUE(area->HasUncommittedChanges()); // Make additional change and verify that a new commit batch // is created for that change. NullableString16 old_value; EXPECT_TRUE(area->SetItem(kKey2, kValue2, &old_value)); EXPECT_TRUE(area->commit_batch_.get()); EXPECT_EQ(1, area->commit_batches_in_flight_); EXPECT_TRUE(area->HasUncommittedChanges()); } // Class used in the CommitChangesAtShutdown test case. class VerifyChangesCommittedDatabase : public DomStorageDatabase { public: VerifyChangesCommittedDatabase() {} virtual ~VerifyChangesCommittedDatabase() { const string16 kKey(ASCIIToUTF16("key")); const string16 kValue(ASCIIToUTF16("value")); ValuesMap values; ReadAllValues(&values); EXPECT_EQ(1u, values.size()); EXPECT_EQ(kValue, values[kKey].string()); } }; private: MessageLoop message_loop_; }; TEST_F(DomStorageAreaTest, DomStorageAreaBasics) { scoped_refptr area( new DomStorageArea(1, std::string(), kOrigin, NULL)); string16 old_value; NullableString16 old_nullable_value; scoped_refptr copy; // We don't focus on the underlying DomStorageMap functionality // since that's covered by seperate unit tests. EXPECT_EQ(kOrigin, area->origin()); EXPECT_EQ(1, area->namespace_id()); EXPECT_EQ(0u, area->Length()); EXPECT_TRUE(area->SetItem(kKey, kValue, &old_nullable_value)); EXPECT_TRUE(area->SetItem(kKey2, kValue2, &old_nullable_value)); EXPECT_FALSE(area->HasUncommittedChanges()); // Verify that a copy shares the same map. copy = area->ShallowCopy(2, std::string()); EXPECT_EQ(kOrigin, copy->origin()); EXPECT_EQ(2, copy->namespace_id()); EXPECT_EQ(area->Length(), copy->Length()); EXPECT_EQ(area->GetItem(kKey).string(), copy->GetItem(kKey).string()); EXPECT_EQ(area->Key(0).string(), copy->Key(0).string()); EXPECT_EQ(copy->map_.get(), area->map_.get()); // But will deep copy-on-write as needed. EXPECT_TRUE(area->RemoveItem(kKey, &old_value)); EXPECT_NE(copy->map_.get(), area->map_.get()); copy = area->ShallowCopy(2, std::string()); EXPECT_EQ(copy->map_.get(), area->map_.get()); EXPECT_TRUE(area->SetItem(kKey, kValue, &old_nullable_value)); EXPECT_NE(copy->map_.get(), area->map_.get()); copy = area->ShallowCopy(2, std::string()); EXPECT_EQ(copy->map_.get(), area->map_.get()); EXPECT_NE(0u, area->Length()); EXPECT_TRUE(area->Clear()); EXPECT_EQ(0u, area->Length()); EXPECT_NE(copy->map_.get(), area->map_.get()); // Verify that once Shutdown(), behaves that way. area->Shutdown(); EXPECT_TRUE(area->is_shutdown_); EXPECT_FALSE(area->map_.get()); EXPECT_EQ(0u, area->Length()); EXPECT_TRUE(area->Key(0).is_null()); EXPECT_TRUE(area->GetItem(kKey).is_null()); EXPECT_FALSE(area->SetItem(kKey, kValue, &old_nullable_value)); EXPECT_FALSE(area->RemoveItem(kKey, &old_value)); EXPECT_FALSE(area->Clear()); } TEST_F(DomStorageAreaTest, BackingDatabaseOpened) { const int64 kSessionStorageNamespaceId = kLocalStorageNamespaceId + 1; ScopedTempDir temp_dir; ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); const FilePath kExpectedOriginFilePath = temp_dir.path().Append( DomStorageArea::DatabaseFileNameFromOrigin(kOrigin)); // No directory, backing should be null. { scoped_refptr area( new DomStorageArea(kOrigin, FilePath(), NULL)); EXPECT_EQ(NULL, area->backing_.get()); EXPECT_TRUE(area->is_initial_import_done_); EXPECT_FALSE(file_util::PathExists(kExpectedOriginFilePath)); } // Valid directory and origin but non-local namespace id. Backing should // be null. { scoped_refptr area( new DomStorageArea(kSessionStorageNamespaceId, std::string(), kOrigin, NULL)); EXPECT_EQ(NULL, area->backing_.get()); EXPECT_TRUE(area->is_initial_import_done_); NullableString16 old_value; EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value)); ASSERT_TRUE(old_value.is_null()); // Check that saving a value has still left us without a backing database. EXPECT_EQ(NULL, area->backing_.get()); EXPECT_FALSE(file_util::PathExists(kExpectedOriginFilePath)); } // This should set up a DomStorageArea that is correctly backed to disk. { scoped_refptr area( new DomStorageArea(kOrigin, temp_dir.path(), new MockDomStorageTaskRunner(base::MessageLoopProxy::current()))); EXPECT_TRUE(area->backing_.get()); EXPECT_FALSE(area->backing_->IsOpen()); EXPECT_FALSE(area->is_initial_import_done_); // Inject an in-memory db to speed up the test. // We will verify that something is written into the database but not // that a file is written to disk - DOMStorageDatabase unit tests cover // that. area->backing_.reset(new DomStorageDatabase()); // Need to write something to ensure that the database is created. NullableString16 old_value; EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value)); ASSERT_TRUE(old_value.is_null()); EXPECT_TRUE(area->is_initial_import_done_); EXPECT_TRUE(area->commit_batch_.get()); EXPECT_EQ(0, area->commit_batches_in_flight_); MessageLoop::current()->RunAllPending(); EXPECT_FALSE(area->commit_batch_.get()); EXPECT_EQ(0, area->commit_batches_in_flight_); EXPECT_TRUE(area->backing_->IsOpen()); EXPECT_EQ(1u, area->Length()); EXPECT_EQ(kValue, area->GetItem(kKey).string()); // Verify the content made it to the in memory database. ValuesMap values; area->backing_->ReadAllValues(&values); EXPECT_EQ(1u, values.size()); EXPECT_EQ(kValue, values[kKey].string()); } } TEST_F(DomStorageAreaTest, CommitTasks) { ScopedTempDir temp_dir; ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); scoped_refptr area( new DomStorageArea(kOrigin, temp_dir.path(), new MockDomStorageTaskRunner(base::MessageLoopProxy::current()))); // Inject an in-memory db to speed up the test. area->backing_.reset(new DomStorageDatabase()); // Unrelated to commits, but while we're here, see that querying Length() // causes the backing database to be opened and presumably read from. EXPECT_FALSE(area->is_initial_import_done_); EXPECT_EQ(0u, area->Length()); EXPECT_TRUE(area->is_initial_import_done_); ValuesMap values; NullableString16 old_value; // See that changes are batched up. EXPECT_FALSE(area->commit_batch_.get()); EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value)); EXPECT_TRUE(area->HasUncommittedChanges()); EXPECT_TRUE(area->commit_batch_.get()); EXPECT_FALSE(area->commit_batch_->clear_all_first); EXPECT_EQ(1u, area->commit_batch_->changed_values.size()); EXPECT_TRUE(area->SetItem(kKey2, kValue2, &old_value)); EXPECT_TRUE(area->commit_batch_.get()); EXPECT_FALSE(area->commit_batch_->clear_all_first); EXPECT_EQ(2u, area->commit_batch_->changed_values.size()); MessageLoop::current()->RunAllPending(); EXPECT_FALSE(area->HasUncommittedChanges()); EXPECT_FALSE(area->commit_batch_.get()); EXPECT_EQ(0, area->commit_batches_in_flight_); // Verify the changes made it to the database. values.clear(); area->backing_->ReadAllValues(&values); EXPECT_EQ(2u, values.size()); EXPECT_EQ(kValue, values[kKey].string()); EXPECT_EQ(kValue2, values[kKey2].string()); // See that clear is handled properly. EXPECT_TRUE(area->Clear()); EXPECT_TRUE(area->commit_batch_.get()); EXPECT_TRUE(area->commit_batch_->clear_all_first); EXPECT_TRUE(area->commit_batch_->changed_values.empty()); MessageLoop::current()->RunAllPending(); EXPECT_FALSE(area->commit_batch_.get()); EXPECT_EQ(0, area->commit_batches_in_flight_); // Verify the changes made it to the database. values.clear(); area->backing_->ReadAllValues(&values); EXPECT_TRUE(values.empty()); // See that if changes accrue while a commit is "in flight" // those will also get committed. EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value)); EXPECT_TRUE(area->HasUncommittedChanges()); // At this point the OnCommitTimer task has been posted. We inject // another task in the queue that will execute after the timer task, // but before the CommitChanges task. From within our injected task, // we'll make an additional SetItem() call. MessageLoop::current()->PostTask( FROM_HERE, base::Bind(&DomStorageAreaTest::InjectedCommitSequencingTask, base::Unretained(this), area)); MessageLoop::current()->RunAllPending(); EXPECT_TRUE(area->HasOneRef()); EXPECT_FALSE(area->HasUncommittedChanges()); // Verify the changes made it to the database. values.clear(); area->backing_->ReadAllValues(&values); EXPECT_EQ(2u, values.size()); EXPECT_EQ(kValue, values[kKey].string()); EXPECT_EQ(kValue2, values[kKey2].string()); } TEST_F(DomStorageAreaTest, CommitChangesAtShutdown) { ScopedTempDir temp_dir; ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); scoped_refptr area( new DomStorageArea(kOrigin, temp_dir.path(), new MockDomStorageTaskRunner(base::MessageLoopProxy::current()))); // Inject an in-memory db to speed up the test and also to verify // the final changes are commited in it's dtor. area->backing_.reset(new VerifyChangesCommittedDatabase()); ValuesMap values; NullableString16 old_value; EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value)); EXPECT_TRUE(area->HasUncommittedChanges()); area->backing_->ReadAllValues(&values); EXPECT_TRUE(values.empty()); // not committed yet area->Shutdown(); MessageLoop::current()->RunAllPending(); EXPECT_TRUE(area->HasOneRef()); EXPECT_FALSE(area->backing_.get()); // The VerifyChangesCommittedDatabase destructor verifies values // were committed. } TEST_F(DomStorageAreaTest, DeleteOrigin) { ScopedTempDir temp_dir; ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); scoped_refptr area( new DomStorageArea(kOrigin, temp_dir.path(), new MockDomStorageTaskRunner(base::MessageLoopProxy::current()))); // This test puts files on disk. FilePath db_file_path = area->backing_->file_path(); FilePath db_journal_file_path = DomStorageDatabase::GetJournalFilePath(db_file_path); // Nothing bad should happen when invoked w/o any files on disk. area->DeleteOrigin(); EXPECT_FALSE(file_util::PathExists(db_file_path)); // Commit something in the database and then delete. NullableString16 old_value; area->SetItem(kKey, kValue, &old_value); MessageLoop::current()->RunAllPending(); EXPECT_TRUE(file_util::PathExists(db_file_path)); area->DeleteOrigin(); EXPECT_EQ(0u, area->Length()); EXPECT_FALSE(file_util::PathExists(db_file_path)); EXPECT_FALSE(file_util::PathExists(db_journal_file_path)); // Put some uncommitted changes to a non-existing database in // and then delete. No file ever gets created in this case. area->SetItem(kKey, kValue, &old_value); EXPECT_TRUE(area->HasUncommittedChanges()); EXPECT_EQ(1u, area->Length()); area->DeleteOrigin(); EXPECT_TRUE(area->HasUncommittedChanges()); EXPECT_EQ(0u, area->Length()); MessageLoop::current()->RunAllPending(); EXPECT_FALSE(area->HasUncommittedChanges()); EXPECT_FALSE(file_util::PathExists(db_file_path)); // Put some uncommitted changes to a an existing database in // and then delete. area->SetItem(kKey, kValue, &old_value); MessageLoop::current()->RunAllPending(); EXPECT_TRUE(file_util::PathExists(db_file_path)); area->SetItem(kKey2, kValue2, &old_value); EXPECT_TRUE(area->HasUncommittedChanges()); EXPECT_EQ(2u, area->Length()); area->DeleteOrigin(); EXPECT_TRUE(area->HasUncommittedChanges()); EXPECT_EQ(0u, area->Length()); MessageLoop::current()->RunAllPending(); EXPECT_FALSE(area->HasUncommittedChanges()); // Since the area had uncommitted changes at the time delete // was called, the file will linger until the shutdown time. EXPECT_TRUE(file_util::PathExists(db_file_path)); area->Shutdown(); MessageLoop::current()->RunAllPending(); EXPECT_FALSE(file_util::PathExists(db_file_path)); } TEST_F(DomStorageAreaTest, PurgeMemory) { ScopedTempDir temp_dir; ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); scoped_refptr area( new DomStorageArea(kOrigin, temp_dir.path(), new MockDomStorageTaskRunner(base::MessageLoopProxy::current()))); // Inject an in-memory db to speed up the test. area->backing_.reset(new DomStorageDatabase()); // Unowned ptrs we use to verify that 'purge' has happened. DomStorageDatabase* original_backing = area->backing_.get(); DomStorageMap* original_map = area->map_.get(); // Should do no harm when called on a newly constructed object. EXPECT_FALSE(area->is_initial_import_done_); area->PurgeMemory(); EXPECT_FALSE(area->is_initial_import_done_); EXPECT_EQ(original_backing, area->backing_.get()); EXPECT_EQ(original_map, area->map_.get()); // Should not do anything when commits are pending. NullableString16 old_value; area->SetItem(kKey, kValue, &old_value); EXPECT_TRUE(area->is_initial_import_done_); EXPECT_TRUE(area->HasUncommittedChanges()); area->PurgeMemory(); EXPECT_TRUE(area->is_initial_import_done_); EXPECT_TRUE(area->HasUncommittedChanges()); EXPECT_EQ(original_backing, area->backing_.get()); EXPECT_EQ(original_map, area->map_.get()); // Commit the changes from above, MessageLoop::current()->RunAllPending(); EXPECT_FALSE(area->HasUncommittedChanges()); EXPECT_EQ(original_backing, area->backing_.get()); EXPECT_EQ(original_map, area->map_.get()); // Should drop caches and reset database connections // when invoked on an area that's loaded up primed. area->PurgeMemory(); EXPECT_FALSE(area->is_initial_import_done_); EXPECT_NE(original_backing, area->backing_.get()); EXPECT_NE(original_map, area->map_.get()); } TEST_F(DomStorageAreaTest, DatabaseFileNames) { struct { const char* origin; const char* file_name; const char* journal_file_name; } kCases[] = { { "https://www.google.com/", "https_www.google.com_0.localstorage", "https_www.google.com_0.localstorage-journal" }, { "http://www.google.com:8080/", "http_www.google.com_8080.localstorage", "http_www.google.com_8080.localstorage-journal" }, { "file:///", "file__0.localstorage", "file__0.localstorage-journal" }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kCases); ++i) { GURL origin = GURL(kCases[i].origin).GetOrigin(); FilePath file_name = FilePath().AppendASCII(kCases[i].file_name); FilePath journal_file_name = FilePath().AppendASCII(kCases[i].journal_file_name); EXPECT_EQ(file_name, DomStorageArea::DatabaseFileNameFromOrigin(origin)); EXPECT_EQ(origin, DomStorageArea::OriginFromDatabaseFileName(file_name)); EXPECT_EQ(journal_file_name, DomStorageDatabase::GetJournalFilePath(file_name)); } // Also test some DomStorageDatabase::GetJournalFilePath cases here. FilePath parent = FilePath().AppendASCII("a").AppendASCII("b"); EXPECT_EQ( parent.AppendASCII("file-journal"), DomStorageDatabase::GetJournalFilePath(parent.AppendASCII("file"))); EXPECT_EQ( FilePath().AppendASCII("-journal"), DomStorageDatabase::GetJournalFilePath(FilePath())); EXPECT_EQ( FilePath().AppendASCII(".extensiononly-journal"), DomStorageDatabase::GetJournalFilePath( FilePath().AppendASCII(".extensiononly"))); } } // namespace dom_storage