// 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 "chrome/browser/sync/syncable/syncable.h" #include "build/build_config.h" #include <sys/types.h> #include <limits> #include <string> #if !defined(OS_WIN) #define MAX_PATH PATH_MAX #include <ostream> #include <stdio.h> #include <sys/ipc.h> #include <sys/sem.h> #include <sys/times.h> #endif // !defined(OS_WIN) #include "base/file_path.h" #include "base/file_util.h" #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/memory/scoped_temp_dir.h" #include "base/string_util.h" #include "base/threading/platform_thread.h" #include "base/values.h" #include "chrome/browser/sync/engine/syncproto.h" #include "chrome/browser/sync/protocol/bookmark_specifics.pb.h" #include "chrome/browser/sync/syncable/directory_backing_store.h" #include "chrome/browser/sync/syncable/directory_manager.h" #include "chrome/common/deprecated/event_sys-inl.h" #include "chrome/test/sync/engine/test_id_factory.h" #include "chrome/test/sync/engine/test_syncable_utils.h" #include "chrome/test/values_test_util.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/sqlite/sqlite3.h" using browser_sync::TestIdFactory; using test::ExpectBooleanValue; using test::ExpectStringValue; namespace syncable { class SyncableKernelTest : public testing::Test {}; TEST_F(SyncableKernelTest, ToValue) { EntryKernel kernel; scoped_ptr<DictionaryValue> value(kernel.ToValue()); if (value.get()) { // Not much to check without repeating the ToValue() code. EXPECT_TRUE(value->HasKey("isDirty")); // The extra +1 is for "isDirty". EXPECT_EQ(BIT_TEMPS_END - BEGIN_FIELDS + 1, static_cast<int>(value->size())); } else { ADD_FAILURE(); } } namespace { void PutDataAsBookmarkFavicon(WriteTransaction* wtrans, MutableEntry* e, const char* bytes, size_t bytes_length) { sync_pb::EntitySpecifics specifics; specifics.MutableExtension(sync_pb::bookmark)->set_url("http://demo/"); specifics.MutableExtension(sync_pb::bookmark)->set_favicon(bytes, bytes_length); e->Put(SPECIFICS, specifics); } void ExpectDataFromBookmarkFaviconEquals(BaseTransaction* trans, Entry* e, const char* bytes, size_t bytes_length) { ASSERT_TRUE(e->good()); ASSERT_TRUE(e->Get(SPECIFICS).HasExtension(sync_pb::bookmark)); ASSERT_EQ("http://demo/", e->Get(SPECIFICS).GetExtension(sync_pb::bookmark).url()); ASSERT_EQ(std::string(bytes, bytes_length), e->Get(SPECIFICS).GetExtension(sync_pb::bookmark).favicon()); } } // namespace class SyncableGeneralTest : public testing::Test { public: virtual void SetUp() { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); db_path_ = temp_dir_.path().Append( FILE_PATH_LITERAL("SyncableTest.sqlite3")); } virtual void TearDown() { } protected: ScopedTempDir temp_dir_; FilePath db_path_; }; TEST_F(SyncableGeneralTest, General) { Directory dir; dir.Open(db_path_, "SimpleTest"); int64 written_metahandle; const Id id = TestIdFactory::FromNumber(99); std::string name = "Jeff"; // Test simple read operations on an empty DB. { ReadTransaction rtrans(&dir, __FILE__, __LINE__); Entry e(&rtrans, GET_BY_ID, id); ASSERT_FALSE(e.good()); // Hasn't been written yet. Directory::ChildHandles child_handles; dir.GetChildHandles(&rtrans, rtrans.root_id(), &child_handles); EXPECT_TRUE(child_handles.empty()); } // Test creating a new meta entry. { WriteTransaction wtrans(&dir, UNITTEST, __FILE__, __LINE__); MutableEntry me(&wtrans, CREATE, wtrans.root_id(), name); ASSERT_TRUE(me.good()); me.Put(ID, id); me.Put(BASE_VERSION, 1); written_metahandle = me.Get(META_HANDLE); } // Test GetChildHandles after something is now in the DB. // Also check that GET_BY_ID works. { ReadTransaction rtrans(&dir, __FILE__, __LINE__); Entry e(&rtrans, GET_BY_ID, id); ASSERT_TRUE(e.good()); Directory::ChildHandles child_handles; dir.GetChildHandles(&rtrans, rtrans.root_id(), &child_handles); EXPECT_EQ(1u, child_handles.size()); for (Directory::ChildHandles::iterator i = child_handles.begin(); i != child_handles.end(); ++i) { EXPECT_EQ(*i, written_metahandle); } } // Test writing data to an entity. Also check that GET_BY_HANDLE works. static const char s[] = "Hello World."; { WriteTransaction trans(&dir, UNITTEST, __FILE__, __LINE__); MutableEntry e(&trans, GET_BY_HANDLE, written_metahandle); ASSERT_TRUE(e.good()); PutDataAsBookmarkFavicon(&trans, &e, s, sizeof(s)); } // Test reading back the contents that we just wrote. { WriteTransaction trans(&dir, UNITTEST, __FILE__, __LINE__); MutableEntry e(&trans, GET_BY_HANDLE, written_metahandle); ASSERT_TRUE(e.good()); ExpectDataFromBookmarkFaviconEquals(&trans, &e, s, sizeof(s)); } // Verify it exists in the folder. { ReadTransaction rtrans(&dir, __FILE__, __LINE__); EXPECT_EQ(1, CountEntriesWithName(&rtrans, rtrans.root_id(), name)); } // Now delete it. { WriteTransaction trans(&dir, UNITTEST, __FILE__, __LINE__); MutableEntry e(&trans, GET_BY_HANDLE, written_metahandle); e.Put(IS_DEL, true); EXPECT_EQ(0, CountEntriesWithName(&trans, trans.root_id(), name)); } dir.SaveChanges(); } TEST_F(SyncableGeneralTest, ClientIndexRebuildsProperly) { int64 written_metahandle; TestIdFactory factory; const Id id = factory.NewServerId(); std::string name = "cheesepuffs"; std::string tag = "dietcoke"; // Test creating a new meta entry. { Directory dir; dir.Open(db_path_, "IndexTest"); { WriteTransaction wtrans(&dir, UNITTEST, __FILE__, __LINE__); MutableEntry me(&wtrans, CREATE, wtrans.root_id(), name); ASSERT_TRUE(me.good()); me.Put(ID, id); me.Put(BASE_VERSION, 1); me.Put(UNIQUE_CLIENT_TAG, tag); written_metahandle = me.Get(META_HANDLE); } dir.SaveChanges(); } // The DB was closed. Now reopen it. This will cause index regeneration. { Directory dir; dir.Open(db_path_, "IndexTest"); ReadTransaction trans(&dir, __FILE__, __LINE__); Entry me(&trans, GET_BY_CLIENT_TAG, tag); ASSERT_TRUE(me.good()); EXPECT_EQ(me.Get(ID), id); EXPECT_EQ(me.Get(BASE_VERSION), 1); EXPECT_EQ(me.Get(UNIQUE_CLIENT_TAG), tag); EXPECT_EQ(me.Get(META_HANDLE), written_metahandle); } } TEST_F(SyncableGeneralTest, ClientIndexRebuildsDeletedProperly) { TestIdFactory factory; const Id id = factory.NewServerId(); std::string tag = "dietcoke"; // Test creating a deleted, unsynced, server meta entry. { Directory dir; dir.Open(db_path_, "IndexTest"); { WriteTransaction wtrans(&dir, UNITTEST, __FILE__, __LINE__); MutableEntry me(&wtrans, CREATE, wtrans.root_id(), "deleted"); ASSERT_TRUE(me.good()); me.Put(ID, id); me.Put(BASE_VERSION, 1); me.Put(UNIQUE_CLIENT_TAG, tag); me.Put(IS_DEL, true); me.Put(IS_UNSYNCED, true); // Or it might be purged. } dir.SaveChanges(); } // The DB was closed. Now reopen it. This will cause index regeneration. // Should still be present and valid in the client tag index. { Directory dir; dir.Open(db_path_, "IndexTest"); ReadTransaction trans(&dir, __FILE__, __LINE__); Entry me(&trans, GET_BY_CLIENT_TAG, tag); ASSERT_TRUE(me.good()); EXPECT_EQ(me.Get(ID), id); EXPECT_EQ(me.Get(UNIQUE_CLIENT_TAG), tag); EXPECT_TRUE(me.Get(IS_DEL)); EXPECT_TRUE(me.Get(IS_UNSYNCED)); } } TEST_F(SyncableGeneralTest, ToValue) { Directory dir; dir.Open(db_path_, "SimpleTest"); const Id id = TestIdFactory::FromNumber(99); { ReadTransaction rtrans(&dir, __FILE__, __LINE__); Entry e(&rtrans, GET_BY_ID, id); EXPECT_FALSE(e.good()); // Hasn't been written yet. scoped_ptr<DictionaryValue> value(e.ToValue()); ExpectBooleanValue(false, *value, "good"); EXPECT_EQ(1u, value->size()); } // Test creating a new meta entry. { WriteTransaction wtrans(&dir, UNITTEST, __FILE__, __LINE__); MutableEntry me(&wtrans, CREATE, wtrans.root_id(), "new"); ASSERT_TRUE(me.good()); me.Put(ID, id); me.Put(BASE_VERSION, 1); scoped_ptr<DictionaryValue> value(me.ToValue()); ExpectBooleanValue(true, *value, "good"); EXPECT_TRUE(value->HasKey("kernel")); ExpectStringValue("Unspecified", *value, "serverModelType"); ExpectStringValue("Unspecified", *value, "modelType"); ExpectBooleanValue(false, *value, "shouldMaintainPosition"); ExpectBooleanValue(true, *value, "existsOnClientBecauseNameIsNonEmpty"); ExpectBooleanValue(false, *value, "isRoot"); } dir.SaveChanges(); } // A Directory whose backing store always fails SaveChanges by returning false. class TestUnsaveableDirectory : public Directory { public: class UnsaveableBackingStore : public DirectoryBackingStore { public: UnsaveableBackingStore(const std::string& dir_name, const FilePath& backing_filepath) : DirectoryBackingStore(dir_name, backing_filepath) { } virtual bool SaveChanges(const Directory::SaveChangesSnapshot& snapshot) { return false; } }; virtual DirectoryBackingStore* CreateBackingStore( const std::string& dir_name, const FilePath& backing_filepath) { return new UnsaveableBackingStore(dir_name, backing_filepath); } }; // Test suite for syncable::Directory. class SyncableDirectoryTest : public testing::Test { protected: static const FilePath::CharType kFilePath[]; static const char kName[]; static const Id kId; // SetUp() is called before each test case is run. // The sqlite3 DB is deleted before each test is run. virtual void SetUp() { file_path_ = FilePath(kFilePath); file_util::Delete(file_path_, true); dir_.reset(new Directory()); ASSERT_TRUE(dir_.get()); ASSERT_TRUE(OPENED == dir_->Open(file_path_, kName)); ASSERT_TRUE(dir_->good()); } virtual void TearDown() { // This also closes file handles. dir_->SaveChanges(); dir_.reset(); file_util::Delete(file_path_, true); } void ReloadDir() { dir_.reset(new Directory()); ASSERT_TRUE(dir_.get()); ASSERT_TRUE(OPENED == dir_->Open(file_path_, kName)); } void SaveAndReloadDir() { dir_->SaveChanges(); ReloadDir(); } bool IsInDirtyMetahandles(int64 metahandle) { return 1 == dir_->kernel_->dirty_metahandles->count(metahandle); } bool IsInMetahandlesToPurge(int64 metahandle) { return 1 == dir_->kernel_->metahandles_to_purge->count(metahandle); } void CheckPurgeEntriesWithTypeInSucceeded(const ModelTypeSet& types_to_purge, bool before_reload) { SCOPED_TRACE(testing::Message("Before reload: ") << before_reload); { ReadTransaction trans(dir_.get(), __FILE__, __LINE__); MetahandleSet all_set; dir_->GetAllMetaHandles(&trans, &all_set); EXPECT_EQ(3U, all_set.size()); if (before_reload) EXPECT_EQ(4U, dir_->kernel_->metahandles_to_purge->size()); for (MetahandleSet::iterator iter = all_set.begin(); iter != all_set.end(); ++iter) { Entry e(&trans, GET_BY_HANDLE, *iter); if ((types_to_purge.count(e.GetModelType()) || types_to_purge.count(e.GetServerModelType()))) { FAIL() << "Illegal type should have been deleted."; } } } EXPECT_FALSE(dir_->initial_sync_ended_for_type(PREFERENCES)); EXPECT_FALSE(dir_->initial_sync_ended_for_type(AUTOFILL)); EXPECT_TRUE(dir_->initial_sync_ended_for_type(BOOKMARKS)); } scoped_ptr<Directory> dir_; FilePath file_path_; // Creates an empty entry and sets the ID field to the default kId. void CreateEntry(const std::string& entryname) { CreateEntry(entryname, kId); } // Creates an empty entry and sets the ID field to id. void CreateEntry(const std::string& entryname, const int id) { CreateEntry(entryname, TestIdFactory::FromNumber(id)); } void CreateEntry(const std::string& entryname, Id id) { WriteTransaction wtrans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry me(&wtrans, CREATE, wtrans.root_id(), entryname); ASSERT_TRUE(me.good()); me.Put(ID, id); me.Put(IS_UNSYNCED, true); } void ValidateEntry(BaseTransaction* trans, int64 id, bool check_name, const std::string& name, int64 base_version, int64 server_version, bool is_del); }; TEST_F(SyncableDirectoryTest, TakeSnapshotGetsMetahandlesToPurge) { const int metas_to_create = 50; MetahandleSet expected_purges; MetahandleSet all_handles; { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); for (int i = 0; i < metas_to_create; i++) { MutableEntry e(&trans, CREATE, trans.root_id(), "foo"); e.Put(IS_UNSYNCED, true); sync_pb::EntitySpecifics specs; if (i % 2 == 0) { AddDefaultExtensionValue(BOOKMARKS, &specs); expected_purges.insert(e.Get(META_HANDLE)); all_handles.insert(e.Get(META_HANDLE)); } else { AddDefaultExtensionValue(PREFERENCES, &specs); all_handles.insert(e.Get(META_HANDLE)); } e.Put(SPECIFICS, specs); e.Put(SERVER_SPECIFICS, specs); } } ModelTypeSet to_purge; to_purge.insert(BOOKMARKS); dir_->PurgeEntriesWithTypeIn(to_purge); Directory::SaveChangesSnapshot snapshot1; base::AutoLock scoped_lock(dir_->kernel_->save_changes_mutex); dir_->TakeSnapshotForSaveChanges(&snapshot1); EXPECT_TRUE(expected_purges == snapshot1.metahandles_to_purge); to_purge.clear(); to_purge.insert(PREFERENCES); dir_->PurgeEntriesWithTypeIn(to_purge); dir_->HandleSaveChangesFailure(snapshot1); Directory::SaveChangesSnapshot snapshot2; dir_->TakeSnapshotForSaveChanges(&snapshot2); EXPECT_TRUE(all_handles == snapshot2.metahandles_to_purge); } TEST_F(SyncableDirectoryTest, TakeSnapshotGetsAllDirtyHandlesTest) { const int metahandles_to_create = 100; std::vector<int64> expected_dirty_metahandles; { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); for (int i = 0; i < metahandles_to_create; i++) { MutableEntry e(&trans, CREATE, trans.root_id(), "foo"); expected_dirty_metahandles.push_back(e.Get(META_HANDLE)); e.Put(IS_UNSYNCED, true); } } // Fake SaveChanges() and make sure we got what we expected. { Directory::SaveChangesSnapshot snapshot; base::AutoLock scoped_lock(dir_->kernel_->save_changes_mutex); dir_->TakeSnapshotForSaveChanges(&snapshot); // Make sure there's an entry for each new metahandle. Make sure all // entries are marked dirty. ASSERT_EQ(expected_dirty_metahandles.size(), snapshot.dirty_metas.size()); for (OriginalEntries::const_iterator i = snapshot.dirty_metas.begin(); i != snapshot.dirty_metas.end(); ++i) { ASSERT_TRUE(i->is_dirty()); } dir_->VacuumAfterSaveChanges(snapshot); } // Put a new value with existing transactions as well as adding new ones. { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); std::vector<int64> new_dirty_metahandles; for (std::vector<int64>::const_iterator i = expected_dirty_metahandles.begin(); i != expected_dirty_metahandles.end(); ++i) { // Change existing entries to directories to dirty them. MutableEntry e1(&trans, GET_BY_HANDLE, *i); e1.Put(IS_DIR, true); e1.Put(IS_UNSYNCED, true); // Add new entries MutableEntry e2(&trans, CREATE, trans.root_id(), "bar"); e2.Put(IS_UNSYNCED, true); new_dirty_metahandles.push_back(e2.Get(META_HANDLE)); } expected_dirty_metahandles.insert(expected_dirty_metahandles.end(), new_dirty_metahandles.begin(), new_dirty_metahandles.end()); } // Fake SaveChanges() and make sure we got what we expected. { Directory::SaveChangesSnapshot snapshot; base::AutoLock scoped_lock(dir_->kernel_->save_changes_mutex); dir_->TakeSnapshotForSaveChanges(&snapshot); // Make sure there's an entry for each new metahandle. Make sure all // entries are marked dirty. EXPECT_EQ(expected_dirty_metahandles.size(), snapshot.dirty_metas.size()); for (OriginalEntries::const_iterator i = snapshot.dirty_metas.begin(); i != snapshot.dirty_metas.end(); ++i) { EXPECT_TRUE(i->is_dirty()); } dir_->VacuumAfterSaveChanges(snapshot); } } TEST_F(SyncableDirectoryTest, TestPurgeEntriesWithTypeIn) { sync_pb::EntitySpecifics bookmark_specs; sync_pb::EntitySpecifics autofill_specs; sync_pb::EntitySpecifics preference_specs; AddDefaultExtensionValue(BOOKMARKS, &bookmark_specs); AddDefaultExtensionValue(PREFERENCES, &preference_specs); AddDefaultExtensionValue(AUTOFILL, &autofill_specs); dir_->set_initial_sync_ended_for_type(BOOKMARKS, true); dir_->set_initial_sync_ended_for_type(PREFERENCES, true); dir_->set_initial_sync_ended_for_type(AUTOFILL, true); std::set<ModelType> types_to_purge; types_to_purge.insert(PREFERENCES); types_to_purge.insert(AUTOFILL); TestIdFactory id_factory; // Create some items for each type. { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry item1(&trans, CREATE, trans.root_id(), "Item"); ASSERT_TRUE(item1.good()); item1.Put(SPECIFICS, bookmark_specs); item1.Put(SERVER_SPECIFICS, bookmark_specs); item1.Put(IS_UNSYNCED, true); MutableEntry item2(&trans, CREATE_NEW_UPDATE_ITEM, id_factory.NewServerId()); ASSERT_TRUE(item2.good()); item2.Put(SERVER_SPECIFICS, bookmark_specs); item2.Put(IS_UNAPPLIED_UPDATE, true); MutableEntry item3(&trans, CREATE, trans.root_id(), "Item"); ASSERT_TRUE(item3.good()); item3.Put(SPECIFICS, preference_specs); item3.Put(SERVER_SPECIFICS, preference_specs); item3.Put(IS_UNSYNCED, true); MutableEntry item4(&trans, CREATE_NEW_UPDATE_ITEM, id_factory.NewServerId()); ASSERT_TRUE(item4.good()); item4.Put(SERVER_SPECIFICS, preference_specs); item4.Put(IS_UNAPPLIED_UPDATE, true); MutableEntry item5(&trans, CREATE, trans.root_id(), "Item"); ASSERT_TRUE(item5.good()); item5.Put(SPECIFICS, autofill_specs); item5.Put(SERVER_SPECIFICS, autofill_specs); item5.Put(IS_UNSYNCED, true); MutableEntry item6(&trans, CREATE_NEW_UPDATE_ITEM, id_factory.NewServerId()); ASSERT_TRUE(item6.good()); item6.Put(SERVER_SPECIFICS, autofill_specs); item6.Put(IS_UNAPPLIED_UPDATE, true); } dir_->SaveChanges(); { ReadTransaction trans(dir_.get(), __FILE__, __LINE__); MetahandleSet all_set; dir_->GetAllMetaHandles(&trans, &all_set); ASSERT_EQ(7U, all_set.size()); } dir_->PurgeEntriesWithTypeIn(types_to_purge); // We first query the in-memory data, and then reload the directory (without // saving) to verify that disk does not still have the data. CheckPurgeEntriesWithTypeInSucceeded(types_to_purge, true); SaveAndReloadDir(); CheckPurgeEntriesWithTypeInSucceeded(types_to_purge, false); } TEST_F(SyncableDirectoryTest, TakeSnapshotGetsOnlyDirtyHandlesTest) { const int metahandles_to_create = 100; // half of 2 * metahandles_to_create const unsigned int number_changed = 100u; std::vector<int64> expected_dirty_metahandles; { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); for (int i = 0; i < metahandles_to_create; i++) { MutableEntry e(&trans, CREATE, trans.root_id(), "foo"); expected_dirty_metahandles.push_back(e.Get(META_HANDLE)); e.Put(IS_UNSYNCED, true); } } dir_->SaveChanges(); // Put a new value with existing transactions as well as adding new ones. { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); std::vector<int64> new_dirty_metahandles; for (std::vector<int64>::const_iterator i = expected_dirty_metahandles.begin(); i != expected_dirty_metahandles.end(); ++i) { // Change existing entries to directories to dirty them. MutableEntry e1(&trans, GET_BY_HANDLE, *i); ASSERT_TRUE(e1.good()); e1.Put(IS_DIR, true); e1.Put(IS_UNSYNCED, true); // Add new entries MutableEntry e2(&trans, CREATE, trans.root_id(), "bar"); e2.Put(IS_UNSYNCED, true); new_dirty_metahandles.push_back(e2.Get(META_HANDLE)); } expected_dirty_metahandles.insert(expected_dirty_metahandles.end(), new_dirty_metahandles.begin(), new_dirty_metahandles.end()); } dir_->SaveChanges(); // Don't make any changes whatsoever and ensure nothing comes back. { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); for (std::vector<int64>::const_iterator i = expected_dirty_metahandles.begin(); i != expected_dirty_metahandles.end(); ++i) { MutableEntry e(&trans, GET_BY_HANDLE, *i); ASSERT_TRUE(e.good()); // We aren't doing anything to dirty these entries. } } // Fake SaveChanges() and make sure we got what we expected. { Directory::SaveChangesSnapshot snapshot; base::AutoLock scoped_lock(dir_->kernel_->save_changes_mutex); dir_->TakeSnapshotForSaveChanges(&snapshot); // Make sure there are no dirty_metahandles. EXPECT_EQ(0u, snapshot.dirty_metas.size()); dir_->VacuumAfterSaveChanges(snapshot); } { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); bool should_change = false; for (std::vector<int64>::const_iterator i = expected_dirty_metahandles.begin(); i != expected_dirty_metahandles.end(); ++i) { // Maybe change entries by flipping IS_DIR. MutableEntry e(&trans, GET_BY_HANDLE, *i); ASSERT_TRUE(e.good()); should_change = !should_change; if (should_change) { bool not_dir = !e.Get(IS_DIR); e.Put(IS_DIR, not_dir); e.Put(IS_UNSYNCED, true); } } } // Fake SaveChanges() and make sure we got what we expected. { Directory::SaveChangesSnapshot snapshot; base::AutoLock scoped_lock(dir_->kernel_->save_changes_mutex); dir_->TakeSnapshotForSaveChanges(&snapshot); // Make sure there's an entry for each changed metahandle. Make sure all // entries are marked dirty. EXPECT_EQ(number_changed, snapshot.dirty_metas.size()); for (OriginalEntries::const_iterator i = snapshot.dirty_metas.begin(); i != snapshot.dirty_metas.end(); ++i) { EXPECT_TRUE(i->is_dirty()); } dir_->VacuumAfterSaveChanges(snapshot); } } const FilePath::CharType SyncableDirectoryTest::kFilePath[] = FILE_PATH_LITERAL("Test.sqlite3"); const char SyncableDirectoryTest::kName[] = "Foo"; const Id SyncableDirectoryTest::kId(TestIdFactory::FromNumber(-99)); namespace { TEST_F(SyncableDirectoryTest, TestBasicLookupNonExistantID) { ReadTransaction rtrans(dir_.get(), __FILE__, __LINE__); Entry e(&rtrans, GET_BY_ID, kId); ASSERT_FALSE(e.good()); } TEST_F(SyncableDirectoryTest, TestBasicLookupValidID) { CreateEntry("rtc"); ReadTransaction rtrans(dir_.get(), __FILE__, __LINE__); Entry e(&rtrans, GET_BY_ID, kId); ASSERT_TRUE(e.good()); } TEST_F(SyncableDirectoryTest, TestDelete) { std::string name = "peanut butter jelly time"; WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry e1(&trans, CREATE, trans.root_id(), name); ASSERT_TRUE(e1.good()); ASSERT_TRUE(e1.Put(IS_DEL, true)); MutableEntry e2(&trans, CREATE, trans.root_id(), name); ASSERT_TRUE(e2.good()); ASSERT_TRUE(e2.Put(IS_DEL, true)); MutableEntry e3(&trans, CREATE, trans.root_id(), name); ASSERT_TRUE(e3.good()); ASSERT_TRUE(e3.Put(IS_DEL, true)); ASSERT_TRUE(e1.Put(IS_DEL, false)); ASSERT_TRUE(e2.Put(IS_DEL, false)); ASSERT_TRUE(e3.Put(IS_DEL, false)); ASSERT_TRUE(e1.Put(IS_DEL, true)); ASSERT_TRUE(e2.Put(IS_DEL, true)); ASSERT_TRUE(e3.Put(IS_DEL, true)); } TEST_F(SyncableDirectoryTest, TestGetUnsynced) { Directory::UnsyncedMetaHandles handles; int64 handle1, handle2; { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); dir_->GetUnsyncedMetaHandles(&trans, &handles); ASSERT_TRUE(0 == handles.size()); MutableEntry e1(&trans, CREATE, trans.root_id(), "abba"); ASSERT_TRUE(e1.good()); handle1 = e1.Get(META_HANDLE); e1.Put(BASE_VERSION, 1); e1.Put(IS_DIR, true); e1.Put(ID, TestIdFactory::FromNumber(101)); MutableEntry e2(&trans, CREATE, e1.Get(ID), "bread"); ASSERT_TRUE(e2.good()); handle2 = e2.Get(META_HANDLE); e2.Put(BASE_VERSION, 1); e2.Put(ID, TestIdFactory::FromNumber(102)); } dir_->SaveChanges(); { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); dir_->GetUnsyncedMetaHandles(&trans, &handles); ASSERT_TRUE(0 == handles.size()); MutableEntry e3(&trans, GET_BY_HANDLE, handle1); ASSERT_TRUE(e3.good()); e3.Put(IS_UNSYNCED, true); } dir_->SaveChanges(); { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); dir_->GetUnsyncedMetaHandles(&trans, &handles); ASSERT_TRUE(1 == handles.size()); ASSERT_TRUE(handle1 == handles[0]); MutableEntry e4(&trans, GET_BY_HANDLE, handle2); ASSERT_TRUE(e4.good()); e4.Put(IS_UNSYNCED, true); } dir_->SaveChanges(); { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); dir_->GetUnsyncedMetaHandles(&trans, &handles); ASSERT_TRUE(2 == handles.size()); if (handle1 == handles[0]) { ASSERT_TRUE(handle2 == handles[1]); } else { ASSERT_TRUE(handle2 == handles[0]); ASSERT_TRUE(handle1 == handles[1]); } MutableEntry e5(&trans, GET_BY_HANDLE, handle1); ASSERT_TRUE(e5.good()); ASSERT_TRUE(e5.Get(IS_UNSYNCED)); ASSERT_TRUE(e5.Put(IS_UNSYNCED, false)); ASSERT_FALSE(e5.Get(IS_UNSYNCED)); } dir_->SaveChanges(); { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); dir_->GetUnsyncedMetaHandles(&trans, &handles); ASSERT_TRUE(1 == handles.size()); ASSERT_TRUE(handle2 == handles[0]); } } TEST_F(SyncableDirectoryTest, TestGetUnappliedUpdates) { Directory::UnappliedUpdateMetaHandles handles; int64 handle1, handle2; { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); dir_->GetUnappliedUpdateMetaHandles(&trans, &handles); ASSERT_TRUE(0 == handles.size()); MutableEntry e1(&trans, CREATE, trans.root_id(), "abba"); ASSERT_TRUE(e1.good()); handle1 = e1.Get(META_HANDLE); e1.Put(IS_UNAPPLIED_UPDATE, false); e1.Put(BASE_VERSION, 1); e1.Put(ID, TestIdFactory::FromNumber(101)); e1.Put(IS_DIR, true); MutableEntry e2(&trans, CREATE, e1.Get(ID), "bread"); ASSERT_TRUE(e2.good()); handle2 = e2.Get(META_HANDLE); e2.Put(IS_UNAPPLIED_UPDATE, false); e2.Put(BASE_VERSION, 1); e2.Put(ID, TestIdFactory::FromNumber(102)); } dir_->SaveChanges(); { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); dir_->GetUnappliedUpdateMetaHandles(&trans, &handles); ASSERT_TRUE(0 == handles.size()); MutableEntry e3(&trans, GET_BY_HANDLE, handle1); ASSERT_TRUE(e3.good()); e3.Put(IS_UNAPPLIED_UPDATE, true); } dir_->SaveChanges(); { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); dir_->GetUnappliedUpdateMetaHandles(&trans, &handles); ASSERT_TRUE(1 == handles.size()); ASSERT_TRUE(handle1 == handles[0]); MutableEntry e4(&trans, GET_BY_HANDLE, handle2); ASSERT_TRUE(e4.good()); e4.Put(IS_UNAPPLIED_UPDATE, true); } dir_->SaveChanges(); { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); dir_->GetUnappliedUpdateMetaHandles(&trans, &handles); ASSERT_TRUE(2 == handles.size()); if (handle1 == handles[0]) { ASSERT_TRUE(handle2 == handles[1]); } else { ASSERT_TRUE(handle2 == handles[0]); ASSERT_TRUE(handle1 == handles[1]); } MutableEntry e5(&trans, GET_BY_HANDLE, handle1); ASSERT_TRUE(e5.good()); e5.Put(IS_UNAPPLIED_UPDATE, false); } dir_->SaveChanges(); { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); dir_->GetUnappliedUpdateMetaHandles(&trans, &handles); ASSERT_TRUE(1 == handles.size()); ASSERT_TRUE(handle2 == handles[0]); } } TEST_F(SyncableDirectoryTest, DeleteBug_531383) { // Try to evoke a check failure... TestIdFactory id_factory; int64 grandchild_handle, twin_handle; { WriteTransaction wtrans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry parent(&wtrans, CREATE, id_factory.root(), "Bob"); ASSERT_TRUE(parent.good()); parent.Put(IS_DIR, true); parent.Put(ID, id_factory.NewServerId()); parent.Put(BASE_VERSION, 1); MutableEntry child(&wtrans, CREATE, parent.Get(ID), "Bob"); ASSERT_TRUE(child.good()); child.Put(IS_DIR, true); child.Put(ID, id_factory.NewServerId()); child.Put(BASE_VERSION, 1); MutableEntry grandchild(&wtrans, CREATE, child.Get(ID), "Bob"); ASSERT_TRUE(grandchild.good()); grandchild.Put(ID, id_factory.NewServerId()); grandchild.Put(BASE_VERSION, 1); ASSERT_TRUE(grandchild.Put(IS_DEL, true)); MutableEntry twin(&wtrans, CREATE, child.Get(ID), "Bob"); ASSERT_TRUE(twin.good()); ASSERT_TRUE(twin.Put(IS_DEL, true)); ASSERT_TRUE(grandchild.Put(IS_DEL, false)); grandchild_handle = grandchild.Get(META_HANDLE); twin_handle = twin.Get(META_HANDLE); } dir_->SaveChanges(); { WriteTransaction wtrans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry grandchild(&wtrans, GET_BY_HANDLE, grandchild_handle); grandchild.Put(IS_DEL, true); // Used to CHECK fail here. } } static inline bool IsLegalNewParent(const Entry& a, const Entry& b) { return IsLegalNewParent(a.trans(), a.Get(ID), b.Get(ID)); } TEST_F(SyncableDirectoryTest, TestIsLegalNewParent) { TestIdFactory id_factory; WriteTransaction wtrans(dir_.get(), UNITTEST, __FILE__, __LINE__); Entry root(&wtrans, GET_BY_ID, id_factory.root()); ASSERT_TRUE(root.good()); MutableEntry parent(&wtrans, CREATE, root.Get(ID), "Bob"); ASSERT_TRUE(parent.good()); parent.Put(IS_DIR, true); parent.Put(ID, id_factory.NewServerId()); parent.Put(BASE_VERSION, 1); MutableEntry child(&wtrans, CREATE, parent.Get(ID), "Bob"); ASSERT_TRUE(child.good()); child.Put(IS_DIR, true); child.Put(ID, id_factory.NewServerId()); child.Put(BASE_VERSION, 1); MutableEntry grandchild(&wtrans, CREATE, child.Get(ID), "Bob"); ASSERT_TRUE(grandchild.good()); grandchild.Put(ID, id_factory.NewServerId()); grandchild.Put(BASE_VERSION, 1); MutableEntry parent2(&wtrans, CREATE, root.Get(ID), "Pete"); ASSERT_TRUE(parent2.good()); parent2.Put(IS_DIR, true); parent2.Put(ID, id_factory.NewServerId()); parent2.Put(BASE_VERSION, 1); MutableEntry child2(&wtrans, CREATE, parent2.Get(ID), "Pete"); ASSERT_TRUE(child2.good()); child2.Put(IS_DIR, true); child2.Put(ID, id_factory.NewServerId()); child2.Put(BASE_VERSION, 1); MutableEntry grandchild2(&wtrans, CREATE, child2.Get(ID), "Pete"); ASSERT_TRUE(grandchild2.good()); grandchild2.Put(ID, id_factory.NewServerId()); grandchild2.Put(BASE_VERSION, 1); // resulting tree // root // / | // parent parent2 // | | // child child2 // | | // grandchild grandchild2 ASSERT_TRUE(IsLegalNewParent(child, root)); ASSERT_TRUE(IsLegalNewParent(child, parent)); ASSERT_FALSE(IsLegalNewParent(child, child)); ASSERT_FALSE(IsLegalNewParent(child, grandchild)); ASSERT_TRUE(IsLegalNewParent(child, parent2)); ASSERT_TRUE(IsLegalNewParent(child, grandchild2)); ASSERT_FALSE(IsLegalNewParent(parent, grandchild)); ASSERT_FALSE(IsLegalNewParent(root, grandchild)); ASSERT_FALSE(IsLegalNewParent(parent, grandchild)); } TEST_F(SyncableDirectoryTest, TestEntryIsInFolder) { // Create a subdir and an entry. int64 entry_handle; syncable::Id folder_id; syncable::Id entry_id; std::string entry_name = "entry"; { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry folder(&trans, CREATE, trans.root_id(), "folder"); ASSERT_TRUE(folder.good()); EXPECT_TRUE(folder.Put(IS_DIR, true)); EXPECT_TRUE(folder.Put(IS_UNSYNCED, true)); folder_id = folder.Get(ID); MutableEntry entry(&trans, CREATE, folder.Get(ID), entry_name); ASSERT_TRUE(entry.good()); entry_handle = entry.Get(META_HANDLE); entry.Put(IS_UNSYNCED, true); entry_id = entry.Get(ID); } // Make sure we can find the entry in the folder. { ReadTransaction trans(dir_.get(), __FILE__, __LINE__); EXPECT_EQ(0, CountEntriesWithName(&trans, trans.root_id(), entry_name)); EXPECT_EQ(1, CountEntriesWithName(&trans, folder_id, entry_name)); Entry entry(&trans, GET_BY_ID, entry_id); ASSERT_TRUE(entry.good()); EXPECT_EQ(entry_handle, entry.Get(META_HANDLE)); EXPECT_TRUE(entry.Get(NON_UNIQUE_NAME) == entry_name); EXPECT_TRUE(entry.Get(PARENT_ID) == folder_id); } } TEST_F(SyncableDirectoryTest, TestParentIdIndexUpdate) { std::string child_name = "child"; WriteTransaction wt(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry parent_folder(&wt, CREATE, wt.root_id(), "folder1"); parent_folder.Put(IS_UNSYNCED, true); EXPECT_TRUE(parent_folder.Put(IS_DIR, true)); MutableEntry parent_folder2(&wt, CREATE, wt.root_id(), "folder2"); parent_folder2.Put(IS_UNSYNCED, true); EXPECT_TRUE(parent_folder2.Put(IS_DIR, true)); MutableEntry child(&wt, CREATE, parent_folder.Get(ID), child_name); EXPECT_TRUE(child.Put(IS_DIR, true)); child.Put(IS_UNSYNCED, true); ASSERT_TRUE(child.good()); EXPECT_EQ(0, CountEntriesWithName(&wt, wt.root_id(), child_name)); EXPECT_EQ(parent_folder.Get(ID), child.Get(PARENT_ID)); EXPECT_EQ(1, CountEntriesWithName(&wt, parent_folder.Get(ID), child_name)); EXPECT_EQ(0, CountEntriesWithName(&wt, parent_folder2.Get(ID), child_name)); child.Put(PARENT_ID, parent_folder2.Get(ID)); EXPECT_EQ(parent_folder2.Get(ID), child.Get(PARENT_ID)); EXPECT_EQ(0, CountEntriesWithName(&wt, parent_folder.Get(ID), child_name)); EXPECT_EQ(1, CountEntriesWithName(&wt, parent_folder2.Get(ID), child_name)); } TEST_F(SyncableDirectoryTest, TestNoReindexDeletedItems) { std::string folder_name = "folder"; std::string new_name = "new_name"; WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry folder(&trans, CREATE, trans.root_id(), folder_name); ASSERT_TRUE(folder.good()); ASSERT_TRUE(folder.Put(IS_DIR, true)); ASSERT_TRUE(folder.Put(IS_DEL, true)); EXPECT_EQ(0, CountEntriesWithName(&trans, trans.root_id(), folder_name)); MutableEntry deleted(&trans, GET_BY_ID, folder.Get(ID)); ASSERT_TRUE(deleted.good()); ASSERT_TRUE(deleted.Put(PARENT_ID, trans.root_id())); ASSERT_TRUE(deleted.Put(NON_UNIQUE_NAME, new_name)); EXPECT_EQ(0, CountEntriesWithName(&trans, trans.root_id(), folder_name)); EXPECT_EQ(0, CountEntriesWithName(&trans, trans.root_id(), new_name)); } TEST_F(SyncableDirectoryTest, TestCaseChangeRename) { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry folder(&trans, CREATE, trans.root_id(), "CaseChange"); ASSERT_TRUE(folder.good()); EXPECT_TRUE(folder.Put(PARENT_ID, trans.root_id())); EXPECT_TRUE(folder.Put(NON_UNIQUE_NAME, "CASECHANGE")); EXPECT_TRUE(folder.Put(IS_DEL, true)); } TEST_F(SyncableDirectoryTest, TestShareInfo) { dir_->set_initial_sync_ended_for_type(AUTOFILL, true); dir_->set_store_birthday("Jan 31st"); dir_->SetNotificationState("notification_state"); { ReadTransaction trans(dir_.get(), __FILE__, __LINE__); EXPECT_TRUE(dir_->initial_sync_ended_for_type(AUTOFILL)); EXPECT_FALSE(dir_->initial_sync_ended_for_type(BOOKMARKS)); EXPECT_EQ("Jan 31st", dir_->store_birthday()); EXPECT_EQ("notification_state", dir_->GetAndClearNotificationState()); EXPECT_EQ("", dir_->GetAndClearNotificationState()); } dir_->set_store_birthday("April 10th"); dir_->SetNotificationState("notification_state2"); dir_->SaveChanges(); { ReadTransaction trans(dir_.get(), __FILE__, __LINE__); EXPECT_TRUE(dir_->initial_sync_ended_for_type(AUTOFILL)); EXPECT_FALSE(dir_->initial_sync_ended_for_type(BOOKMARKS)); EXPECT_EQ("April 10th", dir_->store_birthday()); EXPECT_EQ("notification_state2", dir_->GetAndClearNotificationState()); EXPECT_EQ("", dir_->GetAndClearNotificationState()); } dir_->SetNotificationState("notification_state2"); // Restore the directory from disk. Make sure that nothing's changed. SaveAndReloadDir(); { ReadTransaction trans(dir_.get(), __FILE__, __LINE__); EXPECT_TRUE(dir_->initial_sync_ended_for_type(AUTOFILL)); EXPECT_FALSE(dir_->initial_sync_ended_for_type(BOOKMARKS)); EXPECT_EQ("April 10th", dir_->store_birthday()); EXPECT_EQ("notification_state2", dir_->GetAndClearNotificationState()); EXPECT_EQ("", dir_->GetAndClearNotificationState()); } } TEST_F(SyncableDirectoryTest, TestSimpleFieldsPreservedDuringSaveChanges) { Id update_id = TestIdFactory::FromNumber(1); Id create_id; EntryKernel create_pre_save, update_pre_save; EntryKernel create_post_save, update_post_save; std::string create_name = "Create"; { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry create(&trans, CREATE, trans.root_id(), create_name); MutableEntry update(&trans, CREATE_NEW_UPDATE_ITEM, update_id); create.Put(IS_UNSYNCED, true); update.Put(IS_UNAPPLIED_UPDATE, true); sync_pb::EntitySpecifics specifics; specifics.MutableExtension(sync_pb::bookmark)->set_favicon("PNG"); specifics.MutableExtension(sync_pb::bookmark)->set_url("http://nowhere"); create.Put(SPECIFICS, specifics); create_pre_save = create.GetKernelCopy(); update_pre_save = update.GetKernelCopy(); create_id = create.Get(ID); } dir_->SaveChanges(); dir_.reset(new Directory()); ASSERT_TRUE(dir_.get()); ASSERT_TRUE(OPENED == dir_->Open(file_path_, kName)); ASSERT_TRUE(dir_->good()); { ReadTransaction trans(dir_.get(), __FILE__, __LINE__); Entry create(&trans, GET_BY_ID, create_id); EXPECT_EQ(1, CountEntriesWithName(&trans, trans.root_id(), create_name)); Entry update(&trans, GET_BY_ID, update_id); create_post_save = create.GetKernelCopy(); update_post_save = update.GetKernelCopy(); } int i = BEGIN_FIELDS; for ( ; i < INT64_FIELDS_END ; ++i) { EXPECT_EQ(create_pre_save.ref((Int64Field)i), create_post_save.ref((Int64Field)i)) << "int64 field #" << i << " changed during save/load"; EXPECT_EQ(update_pre_save.ref((Int64Field)i), update_post_save.ref((Int64Field)i)) << "int64 field #" << i << " changed during save/load"; } for ( ; i < ID_FIELDS_END ; ++i) { EXPECT_EQ(create_pre_save.ref((IdField)i), create_post_save.ref((IdField)i)) << "id field #" << i << " changed during save/load"; EXPECT_EQ(update_pre_save.ref((IdField)i), update_pre_save.ref((IdField)i)) << "id field #" << i << " changed during save/load"; } for ( ; i < BIT_FIELDS_END ; ++i) { EXPECT_EQ(create_pre_save.ref((BitField)i), create_post_save.ref((BitField)i)) << "Bit field #" << i << " changed during save/load"; EXPECT_EQ(update_pre_save.ref((BitField)i), update_post_save.ref((BitField)i)) << "Bit field #" << i << " changed during save/load"; } for ( ; i < STRING_FIELDS_END ; ++i) { EXPECT_EQ(create_pre_save.ref((StringField)i), create_post_save.ref((StringField)i)) << "String field #" << i << " changed during save/load"; EXPECT_EQ(update_pre_save.ref((StringField)i), update_post_save.ref((StringField)i)) << "String field #" << i << " changed during save/load"; } for ( ; i < PROTO_FIELDS_END; ++i) { EXPECT_EQ(create_pre_save.ref((ProtoField)i).SerializeAsString(), create_post_save.ref((ProtoField)i).SerializeAsString()) << "Blob field #" << i << " changed during save/load"; EXPECT_EQ(update_pre_save.ref((ProtoField)i).SerializeAsString(), update_post_save.ref((ProtoField)i).SerializeAsString()) << "Blob field #" << i << " changed during save/load"; } } TEST_F(SyncableDirectoryTest, TestSaveChangesFailure) { int64 handle1 = 0; // Set up an item using a regular, saveable directory. { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry e1(&trans, CREATE, trans.root_id(), "aguilera"); ASSERT_TRUE(e1.good()); EXPECT_TRUE(e1.GetKernelCopy().is_dirty()); handle1 = e1.Get(META_HANDLE); e1.Put(BASE_VERSION, 1); e1.Put(IS_DIR, true); e1.Put(ID, TestIdFactory::FromNumber(101)); EXPECT_TRUE(e1.GetKernelCopy().is_dirty()); EXPECT_TRUE(IsInDirtyMetahandles(handle1)); } ASSERT_TRUE(dir_->SaveChanges()); // Make sure the item is no longer dirty after saving, // and make a modification. { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry aguilera(&trans, GET_BY_HANDLE, handle1); ASSERT_TRUE(aguilera.good()); EXPECT_FALSE(aguilera.GetKernelCopy().is_dirty()); EXPECT_EQ(aguilera.Get(NON_UNIQUE_NAME), "aguilera"); aguilera.Put(NON_UNIQUE_NAME, "overwritten"); EXPECT_TRUE(aguilera.GetKernelCopy().is_dirty()); EXPECT_TRUE(IsInDirtyMetahandles(handle1)); } ASSERT_TRUE(dir_->SaveChanges()); // Now do some operations using a directory for which SaveChanges will // always fail. dir_.reset(new TestUnsaveableDirectory()); ASSERT_TRUE(dir_.get()); ASSERT_TRUE(OPENED == dir_->Open(FilePath(kFilePath), kName)); ASSERT_TRUE(dir_->good()); int64 handle2 = 0; { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry aguilera(&trans, GET_BY_HANDLE, handle1); ASSERT_TRUE(aguilera.good()); EXPECT_FALSE(aguilera.GetKernelCopy().is_dirty()); EXPECT_EQ(aguilera.Get(NON_UNIQUE_NAME), "overwritten"); EXPECT_FALSE(aguilera.GetKernelCopy().is_dirty()); EXPECT_FALSE(IsInDirtyMetahandles(handle1)); aguilera.Put(NON_UNIQUE_NAME, "christina"); EXPECT_TRUE(aguilera.GetKernelCopy().is_dirty()); EXPECT_TRUE(IsInDirtyMetahandles(handle1)); // New item. MutableEntry kids_on_block(&trans, CREATE, trans.root_id(), "kids"); ASSERT_TRUE(kids_on_block.good()); handle2 = kids_on_block.Get(META_HANDLE); kids_on_block.Put(BASE_VERSION, 1); kids_on_block.Put(IS_DIR, true); kids_on_block.Put(ID, TestIdFactory::FromNumber(102)); EXPECT_TRUE(kids_on_block.GetKernelCopy().is_dirty()); EXPECT_TRUE(IsInDirtyMetahandles(handle2)); } // We are using an unsaveable directory, so this can't succeed. However, // the HandleSaveChangesFailure code path should have been triggered. ASSERT_FALSE(dir_->SaveChanges()); // Make sure things were rolled back and the world is as it was before call. { ReadTransaction trans(dir_.get(), __FILE__, __LINE__); Entry e1(&trans, GET_BY_HANDLE, handle1); ASSERT_TRUE(e1.good()); EntryKernel aguilera = e1.GetKernelCopy(); Entry kids(&trans, GET_BY_HANDLE, handle2); ASSERT_TRUE(kids.good()); EXPECT_TRUE(kids.GetKernelCopy().is_dirty()); EXPECT_TRUE(IsInDirtyMetahandles(handle2)); EXPECT_TRUE(aguilera.is_dirty()); EXPECT_TRUE(IsInDirtyMetahandles(handle1)); } } TEST_F(SyncableDirectoryTest, TestSaveChangesFailureWithPurge) { int64 handle1 = 0; // Set up an item using a regular, saveable directory. { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry e1(&trans, CREATE, trans.root_id(), "aguilera"); ASSERT_TRUE(e1.good()); EXPECT_TRUE(e1.GetKernelCopy().is_dirty()); handle1 = e1.Get(META_HANDLE); e1.Put(BASE_VERSION, 1); e1.Put(IS_DIR, true); e1.Put(ID, TestIdFactory::FromNumber(101)); sync_pb::EntitySpecifics bookmark_specs; AddDefaultExtensionValue(BOOKMARKS, &bookmark_specs); e1.Put(SPECIFICS, bookmark_specs); e1.Put(SERVER_SPECIFICS, bookmark_specs); e1.Put(ID, TestIdFactory::FromNumber(101)); EXPECT_TRUE(e1.GetKernelCopy().is_dirty()); EXPECT_TRUE(IsInDirtyMetahandles(handle1)); } ASSERT_TRUE(dir_->SaveChanges()); // Now do some operations using a directory for which SaveChanges will // always fail. dir_.reset(new TestUnsaveableDirectory()); ASSERT_TRUE(dir_.get()); ASSERT_TRUE(OPENED == dir_->Open(FilePath(kFilePath), kName)); ASSERT_TRUE(dir_->good()); ModelTypeSet set; set.insert(BOOKMARKS); dir_->PurgeEntriesWithTypeIn(set); EXPECT_TRUE(IsInMetahandlesToPurge(handle1)); ASSERT_FALSE(dir_->SaveChanges()); EXPECT_TRUE(IsInMetahandlesToPurge(handle1)); } // Create items of each model type, and check that GetModelType and // GetServerModelType return the right value. TEST_F(SyncableDirectoryTest, GetModelType) { TestIdFactory id_factory; for (int i = 0; i < MODEL_TYPE_COUNT; ++i) { ModelType datatype = ModelTypeFromInt(i); SCOPED_TRACE(testing::Message("Testing model type ") << datatype); switch (datatype) { case UNSPECIFIED: case TOP_LEVEL_FOLDER: continue; // Datatype isn't a function of Specifics. default: break; } sync_pb::EntitySpecifics specifics; AddDefaultExtensionValue(datatype, &specifics); WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry folder(&trans, CREATE, trans.root_id(), "Folder"); ASSERT_TRUE(folder.good()); folder.Put(ID, id_factory.NewServerId()); folder.Put(SPECIFICS, specifics); folder.Put(BASE_VERSION, 1); folder.Put(IS_DIR, true); folder.Put(IS_DEL, false); ASSERT_EQ(datatype, folder.GetModelType()); MutableEntry item(&trans, CREATE, trans.root_id(), "Item"); ASSERT_TRUE(item.good()); item.Put(ID, id_factory.NewServerId()); item.Put(SPECIFICS, specifics); item.Put(BASE_VERSION, 1); item.Put(IS_DIR, false); item.Put(IS_DEL, false); ASSERT_EQ(datatype, item.GetModelType()); // It's critical that deletion records retain their datatype, so that // they can be dispatched to the appropriate change processor. MutableEntry deleted_item(&trans, CREATE, trans.root_id(), "Deleted Item"); ASSERT_TRUE(item.good()); deleted_item.Put(ID, id_factory.NewServerId()); deleted_item.Put(SPECIFICS, specifics); deleted_item.Put(BASE_VERSION, 1); deleted_item.Put(IS_DIR, false); deleted_item.Put(IS_DEL, true); ASSERT_EQ(datatype, deleted_item.GetModelType()); MutableEntry server_folder(&trans, CREATE_NEW_UPDATE_ITEM, id_factory.NewServerId()); ASSERT_TRUE(server_folder.good()); server_folder.Put(SERVER_SPECIFICS, specifics); server_folder.Put(BASE_VERSION, 1); server_folder.Put(SERVER_IS_DIR, true); server_folder.Put(SERVER_IS_DEL, false); ASSERT_EQ(datatype, server_folder.GetServerModelType()); MutableEntry server_item(&trans, CREATE_NEW_UPDATE_ITEM, id_factory.NewServerId()); ASSERT_TRUE(server_item.good()); server_item.Put(SERVER_SPECIFICS, specifics); server_item.Put(BASE_VERSION, 1); server_item.Put(SERVER_IS_DIR, false); server_item.Put(SERVER_IS_DEL, false); ASSERT_EQ(datatype, server_item.GetServerModelType()); browser_sync::SyncEntity folder_entity; folder_entity.set_id(id_factory.NewServerId()); folder_entity.set_deleted(false); folder_entity.set_folder(true); folder_entity.mutable_specifics()->CopyFrom(specifics); ASSERT_EQ(datatype, folder_entity.GetModelType()); browser_sync::SyncEntity item_entity; item_entity.set_id(id_factory.NewServerId()); item_entity.set_deleted(false); item_entity.set_folder(false); item_entity.mutable_specifics()->CopyFrom(specifics); ASSERT_EQ(datatype, item_entity.GetModelType()); } } } // namespace void SyncableDirectoryTest::ValidateEntry(BaseTransaction* trans, int64 id, bool check_name, const std::string& name, int64 base_version, int64 server_version, bool is_del) { Entry e(trans, GET_BY_ID, TestIdFactory::FromNumber(id)); ASSERT_TRUE(e.good()); if (check_name) ASSERT_TRUE(name == e.Get(NON_UNIQUE_NAME)); ASSERT_TRUE(base_version == e.Get(BASE_VERSION)); ASSERT_TRUE(server_version == e.Get(SERVER_VERSION)); ASSERT_TRUE(is_del == e.Get(IS_DEL)); } namespace { TEST(SyncableDirectoryManager, TestFileRelease) { DirectoryManager dm(FilePath(FILE_PATH_LITERAL("."))); ASSERT_TRUE(dm.Open("ScopeTest")); { ScopedDirLookup(&dm, "ScopeTest"); } dm.Close("ScopeTest"); ASSERT_TRUE(file_util::Delete(dm.GetSyncDataDatabasePath(), true)); } class ThreadOpenTestDelegate : public base::PlatformThread::Delegate { public: explicit ThreadOpenTestDelegate(DirectoryManager* dm) : directory_manager_(dm) {} DirectoryManager* const directory_manager_; private: // PlatformThread::Delegate methods: virtual void ThreadMain() { CHECK(directory_manager_->Open("Open")); } DISALLOW_COPY_AND_ASSIGN(ThreadOpenTestDelegate); }; TEST(SyncableDirectoryManager, ThreadOpenTest) { DirectoryManager dm(FilePath(FILE_PATH_LITERAL("."))); base::PlatformThreadHandle thread_handle; ThreadOpenTestDelegate test_delegate(&dm); ASSERT_TRUE(base::PlatformThread::Create(0, &test_delegate, &thread_handle)); base::PlatformThread::Join(thread_handle); { ScopedDirLookup dir(&dm, "Open"); ASSERT_TRUE(dir.good()); } dm.Close("Open"); ScopedDirLookup dir(&dm, "Open"); ASSERT_FALSE(dir.good()); } struct Step { Step() : condvar(&mutex), number(0) {} base::Lock mutex; base::ConditionVariable condvar; int number; int64 metahandle; }; class ThreadBugDelegate : public base::PlatformThread::Delegate { public: // a role is 0 or 1, meaning this thread does the odd or event steps. ThreadBugDelegate(int role, Step* step, DirectoryManager* dirman) : role_(role), step_(step), directory_manager_(dirman) {} protected: const int role_; Step* const step_; DirectoryManager* const directory_manager_; // PlatformThread::Delegate methods: virtual void ThreadMain() { const std::string dirname = "ThreadBug1"; base::AutoLock scoped_lock(step_->mutex); while (step_->number < 3) { while (step_->number % 2 != role_) { step_->condvar.Wait(); } switch (step_->number) { case 0: directory_manager_->Open(dirname); break; case 1: { directory_manager_->Close(dirname); directory_manager_->Open(dirname); ScopedDirLookup dir(directory_manager_, dirname); CHECK(dir.good()); WriteTransaction trans(dir, UNITTEST, __FILE__, __LINE__); MutableEntry me(&trans, CREATE, trans.root_id(), "Jeff"); step_->metahandle = me.Get(META_HANDLE); me.Put(IS_UNSYNCED, true); } break; case 2: { ScopedDirLookup dir(directory_manager_, dirname); CHECK(dir.good()); ReadTransaction trans(dir, __FILE__, __LINE__); Entry e(&trans, GET_BY_HANDLE, step_->metahandle); CHECK(e.good()); // Failed due to ThreadBug1 } directory_manager_->Close(dirname); break; } step_->number += 1; step_->condvar.Signal(); } } DISALLOW_COPY_AND_ASSIGN(ThreadBugDelegate); }; TEST(SyncableDirectoryManager, ThreadBug1) { Step step; step.number = 0; DirectoryManager dirman(FilePath(FILE_PATH_LITERAL("."))); ThreadBugDelegate thread_delegate_1(0, &step, &dirman); ThreadBugDelegate thread_delegate_2(1, &step, &dirman); base::PlatformThreadHandle thread_handle_1; base::PlatformThreadHandle thread_handle_2; ASSERT_TRUE( base::PlatformThread::Create(0, &thread_delegate_1, &thread_handle_1)); ASSERT_TRUE( base::PlatformThread::Create(0, &thread_delegate_2, &thread_handle_2)); base::PlatformThread::Join(thread_handle_1); base::PlatformThread::Join(thread_handle_2); } // The in-memory information would get out of sync because a // directory would be closed and re-opened, and then an old // Directory::Kernel with stale information would get saved to the db. class DirectoryKernelStalenessBugDelegate : public ThreadBugDelegate { public: DirectoryKernelStalenessBugDelegate(int role, Step* step, DirectoryManager* dirman) : ThreadBugDelegate(role, step, dirman) {} virtual void ThreadMain() { const char test_bytes[] = "test data"; const std::string dirname = "DirectoryKernelStalenessBug"; base::AutoLock scoped_lock(step_->mutex); const Id jeff_id = TestIdFactory::FromNumber(100); while (step_->number < 4) { while (step_->number % 2 != role_) { step_->condvar.Wait(); } switch (step_->number) { case 0: { // Clean up remnants of earlier test runs. file_util::Delete(directory_manager_->GetSyncDataDatabasePath(), true); // Test. directory_manager_->Open(dirname); ScopedDirLookup dir(directory_manager_, dirname); CHECK(dir.good()); WriteTransaction trans(dir, UNITTEST, __FILE__, __LINE__); MutableEntry me(&trans, CREATE, trans.root_id(), "Jeff"); me.Put(BASE_VERSION, 1); me.Put(ID, jeff_id); PutDataAsBookmarkFavicon(&trans, &me, test_bytes, sizeof(test_bytes)); } { ScopedDirLookup dir(directory_manager_, dirname); CHECK(dir.good()); dir->SaveChanges(); } directory_manager_->Close(dirname); break; case 1: { directory_manager_->Open(dirname); ScopedDirLookup dir(directory_manager_, dirname); CHECK(dir.good()); } break; case 2: { ScopedDirLookup dir(directory_manager_, dirname); CHECK(dir.good()); } break; case 3: { ScopedDirLookup dir(directory_manager_, dirname); CHECK(dir.good()); ReadTransaction trans(dir, __FILE__, __LINE__); Entry e(&trans, GET_BY_ID, jeff_id); ExpectDataFromBookmarkFaviconEquals(&trans, &e, test_bytes, sizeof(test_bytes)); } // Same result as CloseAllDirectories, but more code coverage. directory_manager_->Close(dirname); break; } step_->number += 1; step_->condvar.Signal(); } } DISALLOW_COPY_AND_ASSIGN(DirectoryKernelStalenessBugDelegate); }; TEST(SyncableDirectoryManager, DirectoryKernelStalenessBug) { Step step; DirectoryManager dirman(FilePath(FILE_PATH_LITERAL("."))); DirectoryKernelStalenessBugDelegate thread_delegate_1(0, &step, &dirman); DirectoryKernelStalenessBugDelegate thread_delegate_2(1, &step, &dirman); base::PlatformThreadHandle thread_handle_1; base::PlatformThreadHandle thread_handle_2; ASSERT_TRUE( base::PlatformThread::Create(0, &thread_delegate_1, &thread_handle_1)); ASSERT_TRUE( base::PlatformThread::Create(0, &thread_delegate_2, &thread_handle_2)); base::PlatformThread::Join(thread_handle_1); base::PlatformThread::Join(thread_handle_2); } class StressTransactionsDelegate : public base::PlatformThread::Delegate { public: StressTransactionsDelegate(DirectoryManager* dm, const std::string& dirname, int thread_number) : directory_manager_(dm), dirname_(dirname), thread_number_(thread_number) {} private: DirectoryManager* const directory_manager_; std::string dirname_; const int thread_number_; // PlatformThread::Delegate methods: virtual void ThreadMain() { ScopedDirLookup dir(directory_manager_, dirname_); CHECK(dir.good()); int entry_count = 0; std::string path_name; for (int i = 0; i < 20; ++i) { const int rand_action = rand() % 10; if (rand_action < 4 && !path_name.empty()) { ReadTransaction trans(dir, __FILE__, __LINE__); CHECK(1 == CountEntriesWithName(&trans, trans.root_id(), path_name)); base::PlatformThread::Sleep(rand() % 10); } else { std::string unique_name = StringPrintf("%d.%d", thread_number_, entry_count++); path_name.assign(unique_name.begin(), unique_name.end()); WriteTransaction trans(dir, UNITTEST, __FILE__, __LINE__); MutableEntry e(&trans, CREATE, trans.root_id(), path_name); CHECK(e.good()); base::PlatformThread::Sleep(rand() % 20); e.Put(IS_UNSYNCED, true); if (e.Put(ID, TestIdFactory::FromNumber(rand())) && e.Get(ID).ServerKnows() && !e.Get(ID).IsRoot()) { e.Put(BASE_VERSION, 1); } } } } DISALLOW_COPY_AND_ASSIGN(StressTransactionsDelegate); }; TEST(SyncableDirectory, StressTransactions) { DirectoryManager dirman(FilePath(FILE_PATH_LITERAL("."))); std::string dirname = "stress"; file_util::Delete(dirman.GetSyncDataDatabasePath(), true); dirman.Open(dirname); const int kThreadCount = 7; base::PlatformThreadHandle threads[kThreadCount]; scoped_ptr<StressTransactionsDelegate> thread_delegates[kThreadCount]; for (int i = 0; i < kThreadCount; ++i) { thread_delegates[i].reset( new StressTransactionsDelegate(&dirman, dirname, i)); ASSERT_TRUE(base::PlatformThread::Create( 0, thread_delegates[i].get(), &threads[i])); } for (int i = 0; i < kThreadCount; ++i) { base::PlatformThread::Join(threads[i]); } dirman.Close(dirname); file_util::Delete(dirman.GetSyncDataDatabasePath(), true); } class SyncableClientTagTest : public SyncableDirectoryTest { public: static const int kBaseVersion = 1; const char* test_name_; const char* test_tag_; SyncableClientTagTest() : test_name_("test_name"), test_tag_("dietcoke") {} bool CreateWithDefaultTag(Id id, bool deleted) { return CreateWithTag(test_tag_, id, deleted); } // Attempt to create an entry with a default tag. bool CreateWithTag(const char* tag, Id id, bool deleted) { WriteTransaction wtrans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry me(&wtrans, CREATE, wtrans.root_id(), test_name_); CHECK(me.good()); me.Put(ID, id); if (id.ServerKnows()) { me.Put(BASE_VERSION, kBaseVersion); } me.Put(IS_DEL, deleted); me.Put(IS_UNSYNCED, true); me.Put(IS_DIR, false); return me.Put(UNIQUE_CLIENT_TAG, tag); } // Verify an entry exists with the default tag. void VerifyTag(Id id, bool deleted) { // Should still be present and valid in the client tag index. ReadTransaction trans(dir_.get(), __FILE__, __LINE__); Entry me(&trans, GET_BY_CLIENT_TAG, test_tag_); CHECK(me.good()); EXPECT_EQ(me.Get(ID), id); EXPECT_EQ(me.Get(UNIQUE_CLIENT_TAG), test_tag_); EXPECT_EQ(me.Get(IS_DEL), deleted); EXPECT_EQ(me.Get(IS_UNSYNCED), true); } protected: TestIdFactory factory_; }; TEST_F(SyncableClientTagTest, TestClientTagClear) { Id server_id = factory_.NewServerId(); EXPECT_TRUE(CreateWithDefaultTag(server_id, false)); { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); MutableEntry me(&trans, GET_BY_CLIENT_TAG, test_tag_); EXPECT_TRUE(me.good()); me.Put(UNIQUE_CLIENT_TAG, ""); } { ReadTransaction trans(dir_.get(), __FILE__, __LINE__); Entry by_tag(&trans, GET_BY_CLIENT_TAG, test_tag_); EXPECT_FALSE(by_tag.good()); Entry by_id(&trans, GET_BY_ID, server_id); EXPECT_TRUE(by_id.good()); EXPECT_TRUE(by_id.Get(UNIQUE_CLIENT_TAG).empty()); } } TEST_F(SyncableClientTagTest, TestClientTagIndexServerId) { Id server_id = factory_.NewServerId(); EXPECT_TRUE(CreateWithDefaultTag(server_id, false)); VerifyTag(server_id, false); } TEST_F(SyncableClientTagTest, TestClientTagIndexClientId) { Id client_id = factory_.NewLocalId(); EXPECT_TRUE(CreateWithDefaultTag(client_id, false)); VerifyTag(client_id, false); } TEST_F(SyncableClientTagTest, TestDeletedClientTagIndexClientId) { Id client_id = factory_.NewLocalId(); EXPECT_TRUE(CreateWithDefaultTag(client_id, true)); VerifyTag(client_id, true); } TEST_F(SyncableClientTagTest, TestDeletedClientTagIndexServerId) { Id server_id = factory_.NewServerId(); EXPECT_TRUE(CreateWithDefaultTag(server_id, true)); VerifyTag(server_id, true); } TEST_F(SyncableClientTagTest, TestClientTagIndexDuplicateServer) { EXPECT_TRUE(CreateWithDefaultTag(factory_.NewServerId(), true)); EXPECT_FALSE(CreateWithDefaultTag(factory_.NewServerId(), true)); EXPECT_FALSE(CreateWithDefaultTag(factory_.NewServerId(), false)); EXPECT_FALSE(CreateWithDefaultTag(factory_.NewLocalId(), false)); EXPECT_FALSE(CreateWithDefaultTag(factory_.NewLocalId(), true)); } } // namespace } // namespace syncable