// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include #include #include "base/message_loop.h" #include "base/file_util.h" #include "base/path_service.h" #include "base/process_util.h" #include "base/shared_memory.h" #include "base/string_util.h" #include "chrome/browser/browser_thread.h" #include "chrome/browser/visitedlink_master.h" #include "chrome/browser/visitedlink_event_listener.h" #include "chrome/browser/renderer_host/browser_render_process_host.h" #include "chrome/browser/renderer_host/test/test_render_view_host.h" #include "chrome/common/render_messages.h" #include "chrome/renderer/visitedlink_slave.h" #include "chrome/test/testing_profile.h" #include "googleurl/src/gurl.h" #include "testing/gtest/include/gtest/gtest.h" namespace { // a nice long URL that we can append numbers to to get new URLs const char g_test_prefix[] = "http://www.google.com/products/foo/index.html?id=45028640526508376&seq="; const int g_test_count = 1000; // Returns a test URL for index |i| GURL TestURL(int i) { return GURL(StringPrintf("%s%d", g_test_prefix, i)); } std::vector g_slaves; } // namespace class TrackingVisitedLinkEventListener : public VisitedLinkMaster::Listener { public: TrackingVisitedLinkEventListener() : reset_count_(0), add_count_(0) {} virtual void NewTable(base::SharedMemory* table) { if (table) { for (std::vector::size_type i = 0; i < g_slaves.size(); i++) { base::SharedMemoryHandle new_handle = base::SharedMemory::NULLHandle(); table->ShareToProcess(base::GetCurrentProcessHandle(), &new_handle); g_slaves[i]->Init(new_handle); } } } virtual void Add(VisitedLinkCommon::Fingerprint) { add_count_++; } virtual void Reset() { reset_count_++; } void SetUp() { reset_count_ = 0; add_count_ = 0; } int reset_count() const { return reset_count_; } int add_count() const { return add_count_; } private: int reset_count_; int add_count_; }; class VisitedLinkTest : public testing::Test { protected: VisitedLinkTest() : ui_thread_(BrowserThread::UI, &message_loop_), file_thread_(BrowserThread::FILE, &message_loop_) {} // Initialize the history system. This should be called before InitVisited(). bool InitHistory() { history_service_ = new HistoryService; return history_service_->Init(history_dir_, NULL); } // Initializes the visited link objects. Pass in the size that you want a // freshly created table to be. 0 means use the default. // // |suppress_rebuild| is set when we're not testing rebuilding, see // the VisitedLinkMaster constructor. bool InitVisited(int initial_size, bool suppress_rebuild) { // Initialize the visited link system. master_.reset(new VisitedLinkMaster(&listener_, history_service_, suppress_rebuild, visited_file_, initial_size)); return master_->Init(); } // May be called multiple times (some tests will do this to clear things, // and TearDown will do this to make sure eveything is shiny before quitting. void ClearDB() { if (master_.get()) master_.reset(NULL); if (history_service_.get()) { history_service_->SetOnBackendDestroyTask(new MessageLoop::QuitTask); history_service_->Cleanup(); history_service_ = NULL; // Wait for the backend class to terminate before deleting the files and // moving to the next test. Note: if this never terminates, somebody is // probably leaking a reference to the history backend, so it never calls // our destroy task. MessageLoop::current()->Run(); } } // Loads the database from disk and makes sure that the same URLs are present // as were generated by TestIO_Create(). This also checks the URLs with a // slave to make sure it reads the data properly. void Reload() { // Clean up after our caller, who may have left the database open. ClearDB(); ASSERT_TRUE(InitHistory()); ASSERT_TRUE(InitVisited(0, true)); master_->DebugValidate(); // check that the table has the proper number of entries int used_count = master_->GetUsedCount(); ASSERT_EQ(used_count, g_test_count); // Create a slave database. VisitedLinkSlave slave; base::SharedMemoryHandle new_handle = base::SharedMemory::NULLHandle(); master_->shared_memory()->ShareToProcess( base::GetCurrentProcessHandle(), &new_handle); bool success = slave.Init(new_handle); ASSERT_TRUE(success); g_slaves.push_back(&slave); bool found; for (int i = 0; i < g_test_count; i++) { GURL cur = TestURL(i); found = master_->IsVisited(cur); EXPECT_TRUE(found) << "URL " << i << "not found in master."; found = slave.IsVisited(cur); EXPECT_TRUE(found) << "URL " << i << "not found in slave."; } // test some random URL so we know that it returns false sometimes too found = master_->IsVisited(GURL("http://unfound.site/")); ASSERT_FALSE(found); found = slave.IsVisited(GURL("http://unfound.site/")); ASSERT_FALSE(found); master_->DebugValidate(); g_slaves.clear(); } // testing::Test virtual void SetUp() { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); history_dir_ = temp_dir_.path().AppendASCII("VisitedLinkTest"); ASSERT_TRUE(file_util::CreateDirectory(history_dir_)); visited_file_ = history_dir_.Append(FILE_PATH_LITERAL("VisitedLinks")); listener_.SetUp(); } virtual void TearDown() { ClearDB(); } ScopedTempDir temp_dir_; MessageLoop message_loop_; BrowserThread ui_thread_; BrowserThread file_thread_; // Filenames for the services; FilePath history_dir_; FilePath visited_file_; scoped_ptr master_; scoped_refptr history_service_; TrackingVisitedLinkEventListener listener_; }; // This test creates and reads some databases to make sure the data is // preserved throughout those operations. TEST_F(VisitedLinkTest, DatabaseIO) { ASSERT_TRUE(InitHistory()); ASSERT_TRUE(InitVisited(0, true)); for (int i = 0; i < g_test_count; i++) master_->AddURL(TestURL(i)); // Test that the database was written properly Reload(); } // Checks that we can delete things properly when there are collisions. TEST_F(VisitedLinkTest, Delete) { static const int32 kInitialSize = 17; ASSERT_TRUE(InitHistory()); ASSERT_TRUE(InitVisited(kInitialSize, true)); // Add a cluster from 14-17 wrapping around to 0. These will all hash to the // same value. const VisitedLinkCommon::Fingerprint kFingerprint0 = kInitialSize * 0 + 14; const VisitedLinkCommon::Fingerprint kFingerprint1 = kInitialSize * 1 + 14; const VisitedLinkCommon::Fingerprint kFingerprint2 = kInitialSize * 2 + 14; const VisitedLinkCommon::Fingerprint kFingerprint3 = kInitialSize * 3 + 14; const VisitedLinkCommon::Fingerprint kFingerprint4 = kInitialSize * 4 + 14; master_->AddFingerprint(kFingerprint0, false); // @14 master_->AddFingerprint(kFingerprint1, false); // @15 master_->AddFingerprint(kFingerprint2, false); // @16 master_->AddFingerprint(kFingerprint3, false); // @0 master_->AddFingerprint(kFingerprint4, false); // @1 // Deleting 14 should move the next value up one slot (we do not specify an // order). EXPECT_EQ(kFingerprint3, master_->hash_table_[0]); master_->DeleteFingerprint(kFingerprint3, false); VisitedLinkCommon::Fingerprint zero_fingerprint = 0; EXPECT_EQ(zero_fingerprint, master_->hash_table_[1]); EXPECT_NE(zero_fingerprint, master_->hash_table_[0]); // Deleting the other four should leave the table empty. master_->DeleteFingerprint(kFingerprint0, false); master_->DeleteFingerprint(kFingerprint1, false); master_->DeleteFingerprint(kFingerprint2, false); master_->DeleteFingerprint(kFingerprint4, false); EXPECT_EQ(0, master_->used_items_); for (int i = 0; i < kInitialSize; i++) EXPECT_EQ(zero_fingerprint, master_->hash_table_[i]) << "Hash table has values in it."; } // When we delete more than kBigDeleteThreshold we trigger different behavior // where the entire file is rewritten. TEST_F(VisitedLinkTest, BigDelete) { ASSERT_TRUE(InitHistory()); ASSERT_TRUE(InitVisited(16381, true)); // Add the base set of URLs that won't be deleted. // Reload() will test for these. for (int32 i = 0; i < g_test_count; i++) master_->AddURL(TestURL(i)); // Add more URLs than necessary to trigger this case. const int kTestDeleteCount = VisitedLinkMaster::kBigDeleteThreshold + 2; std::set urls_to_delete; for (int32 i = g_test_count; i < g_test_count + kTestDeleteCount; i++) { GURL url(TestURL(i)); master_->AddURL(url); urls_to_delete.insert(url); } master_->DeleteURLs(urls_to_delete); master_->DebugValidate(); Reload(); } TEST_F(VisitedLinkTest, DeleteAll) { ASSERT_TRUE(InitHistory()); ASSERT_TRUE(InitVisited(0, true)); { VisitedLinkSlave slave; base::SharedMemoryHandle new_handle = base::SharedMemory::NULLHandle(); master_->shared_memory()->ShareToProcess( base::GetCurrentProcessHandle(), &new_handle); ASSERT_TRUE(slave.Init(new_handle)); g_slaves.push_back(&slave); // Add the test URLs. for (int i = 0; i < g_test_count; i++) { master_->AddURL(TestURL(i)); ASSERT_EQ(i + 1, master_->GetUsedCount()); } master_->DebugValidate(); // Make sure the slave picked up the adds. for (int i = 0; i < g_test_count; i++) EXPECT_TRUE(slave.IsVisited(TestURL(i))); // Clear the table and make sure the slave picked it up. master_->DeleteAllURLs(); EXPECT_EQ(0, master_->GetUsedCount()); for (int i = 0; i < g_test_count; i++) { EXPECT_FALSE(master_->IsVisited(TestURL(i))); EXPECT_FALSE(slave.IsVisited(TestURL(i))); } // Close the database. g_slaves.clear(); ClearDB(); } // Reopen and validate. ASSERT_TRUE(InitHistory()); ASSERT_TRUE(InitVisited(0, true)); master_->DebugValidate(); EXPECT_EQ(0, master_->GetUsedCount()); for (int i = 0; i < g_test_count; i++) EXPECT_FALSE(master_->IsVisited(TestURL(i))); } // This tests that the master correctly resizes its tables when it gets too // full, notifies its slaves of the change, and updates the disk. TEST_F(VisitedLinkTest, Resizing) { // Create a very small database. const int32 initial_size = 17; ASSERT_TRUE(InitHistory()); ASSERT_TRUE(InitVisited(initial_size, true)); // ...and a slave VisitedLinkSlave slave; base::SharedMemoryHandle new_handle = base::SharedMemory::NULLHandle(); master_->shared_memory()->ShareToProcess( base::GetCurrentProcessHandle(), &new_handle); bool success = slave.Init(new_handle); ASSERT_TRUE(success); g_slaves.push_back(&slave); int32 used_count = master_->GetUsedCount(); ASSERT_EQ(used_count, 0); for (int i = 0; i < g_test_count; i++) { master_->AddURL(TestURL(i)); used_count = master_->GetUsedCount(); ASSERT_EQ(i + 1, used_count); } // Verify that the table got resized sufficiently. int32 table_size; VisitedLinkCommon::Fingerprint* table; master_->GetUsageStatistics(&table_size, &table); used_count = master_->GetUsedCount(); ASSERT_GT(table_size, used_count); ASSERT_EQ(used_count, g_test_count) << "table count doesn't match the # of things we added"; // Verify that the slave got the resize message and has the same // table information. int32 child_table_size; VisitedLinkCommon::Fingerprint* child_table; slave.GetUsageStatistics(&child_table_size, &child_table); ASSERT_EQ(table_size, child_table_size); for (int32 i = 0; i < table_size; i++) { ASSERT_EQ(table[i], child_table[i]); } master_->DebugValidate(); g_slaves.clear(); // This tests that the file is written correctly by reading it in using // a new database. Reload(); } // Tests that if the database doesn't exist, it will be rebuilt from history. TEST_F(VisitedLinkTest, Rebuild) { ASSERT_TRUE(InitHistory()); // Add half of our URLs to history. This needs to be done before we // initialize the visited link DB. int history_count = g_test_count / 2; for (int i = 0; i < history_count; i++) history_service_->AddPage(TestURL(i), history::SOURCE_BROWSED); // Initialize the visited link DB. Since the visited links file doesn't exist // and we don't suppress history rebuilding, this will load from history. ASSERT_TRUE(InitVisited(0, false)); // While the table is rebuilding, add the rest of the URLs to the visited // link system. This isn't guaranteed to happen during the rebuild, so we // can't be 100% sure we're testing the right thing, but in practice is. // All the adds above will generally take some time queuing up on the // history thread, and it will take a while to catch up to actually // processing the rebuild that has queued behind it. We will generally // finish adding all of the URLs before it has even found the first URL. for (int i = history_count; i < g_test_count; i++) master_->AddURL(TestURL(i)); // Add one more and then delete it. master_->AddURL(TestURL(g_test_count)); std::set deleted_urls; deleted_urls.insert(TestURL(g_test_count)); master_->DeleteURLs(deleted_urls); // Wait for the rebuild to complete. The task will terminate the message // loop when the rebuild is done. There's no chance that the rebuild will // complete before we set the task because the rebuild completion message // is posted to the message loop; until we Run() it, rebuild can not // complete. master_->set_rebuild_complete_task(new MessageLoop::QuitTask); MessageLoop::current()->Run(); // Test that all URLs were written to the database properly. Reload(); // Make sure the extra one was *not* written (Reload won't test this). EXPECT_FALSE(master_->IsVisited(TestURL(g_test_count))); } // Test that importing a large number of URLs will work TEST_F(VisitedLinkTest, BigImport) { ASSERT_TRUE(InitHistory()); ASSERT_TRUE(InitVisited(0, false)); // Before the table rebuilds, add a large number of URLs int total_count = VisitedLinkMaster::kDefaultTableSize + 10; for (int i = 0; i < total_count; i++) master_->AddURL(TestURL(i)); // Wait for the rebuild to complete. master_->set_rebuild_complete_task(new MessageLoop::QuitTask); MessageLoop::current()->Run(); // Ensure that the right number of URLs are present int used_count = master_->GetUsedCount(); ASSERT_EQ(used_count, total_count); } TEST_F(VisitedLinkTest, Listener) { ASSERT_TRUE(InitHistory()); ASSERT_TRUE(InitVisited(0, true)); // Add test URLs. for (int i = 0; i < g_test_count; i++) { master_->AddURL(TestURL(i)); ASSERT_EQ(i + 1, master_->GetUsedCount()); } std::set deleted_urls; deleted_urls.insert(TestURL(0)); // Delete an URL. master_->DeleteURLs(deleted_urls); // ... and all of the remaining ones. master_->DeleteAllURLs(); // Verify that VisitedLinkMaster::Listener::Add was called for each added URL. EXPECT_EQ(g_test_count, listener_.add_count()); // Verify that VisitedLinkMaster::Listener::Reset was called both when one and // all URLs are deleted. EXPECT_EQ(2, listener_.reset_count()); } class VisitCountingProfile : public TestingProfile { public: explicit VisitCountingProfile(VisitedLinkEventListener* event_listener) : add_count_(0), add_event_count_(0), reset_event_count_(0), event_listener_(event_listener) {} virtual VisitedLinkMaster* GetVisitedLinkMaster() { if (!visited_link_master_.get()) { visited_link_master_.reset(new VisitedLinkMaster(event_listener_, this)); visited_link_master_->Init(); } return visited_link_master_.get(); } void CountAddEvent(int by) { add_count_ += by; add_event_count_++; } void CountResetEvent() { reset_event_count_++; } VisitedLinkMaster* master() const { return visited_link_master_.get(); } int add_count() const { return add_count_; } int add_event_count() const { return add_event_count_; } int reset_event_count() const { return reset_event_count_; } private: int add_count_; int add_event_count_; int reset_event_count_; VisitedLinkEventListener* event_listener_; scoped_ptr visited_link_master_; }; class VisitCountingRenderProcessHost : public MockRenderProcessHost { public: explicit VisitCountingRenderProcessHost(Profile* profile) : MockRenderProcessHost(profile) {} virtual void AddVisitedLinks( const VisitedLinkCommon::Fingerprints& visited_links) { VisitCountingProfile* counting_profile = static_cast(profile()); counting_profile->CountAddEvent(visited_links.size()); } virtual void ResetVisitedLinks() { VisitCountingProfile* counting_profile = static_cast(profile()); counting_profile->CountResetEvent(); } private: DISALLOW_COPY_AND_ASSIGN(VisitCountingRenderProcessHost); }; // Stub out as little as possible, borrowing from MockRenderProcessHost. class VisitRelayingRenderProcessHost : public BrowserRenderProcessHost { public: explicit VisitRelayingRenderProcessHost(Profile* profile) : BrowserRenderProcessHost(profile) { } virtual ~VisitRelayingRenderProcessHost() { } virtual bool Init() { return true; } virtual void CancelResourceRequests(int render_widget_id) { } virtual void CrossSiteClosePageACK(int new_render_process_host_id, int new_request_id) { } virtual bool WaitForPaintMsg(int render_widget_id, const base::TimeDelta& max_delay, IPC::Message* msg) { return false; } virtual bool Send(IPC::Message* msg) { VisitCountingProfile* counting_profile = static_cast(profile()); if (msg->type() == ViewMsg_VisitedLink_Add::ID) counting_profile->CountAddEvent(1); else if (msg->type() == ViewMsg_VisitedLink_Reset::ID) counting_profile->CountResetEvent(); delete msg; return true; } virtual void SetBackgrounded(bool backgrounded) { backgrounded_ = backgrounded; } private: DISALLOW_COPY_AND_ASSIGN(VisitRelayingRenderProcessHost); }; class VisitedLinkRenderProcessHostFactory : public MockRenderProcessHostFactory { public: VisitedLinkRenderProcessHostFactory() : MockRenderProcessHostFactory(), relay_mode_(false) {} virtual RenderProcessHost* CreateRenderProcessHost(Profile* profile) const { if (relay_mode_) return new VisitRelayingRenderProcessHost(profile); else return new VisitCountingRenderProcessHost(profile); } void set_relay_mode(bool mode) { relay_mode_ = mode; } private: bool relay_mode_; DISALLOW_COPY_AND_ASSIGN(VisitedLinkRenderProcessHostFactory); }; class VisitedLinkEventsTest : public RenderViewHostTestHarness { public: VisitedLinkEventsTest() : RenderViewHostTestHarness(), file_thread_(BrowserThread::FILE, &message_loop_) {} ~VisitedLinkEventsTest() { // This ends up using the file thread to schedule the delete. profile_.reset(); message_loop_.RunAllPending(); } virtual void SetFactoryMode() {} virtual void SetUp() { SetFactoryMode(); event_listener_.reset(new VisitedLinkEventListener()); rvh_factory_.set_render_process_host_factory(&vc_rph_factory_); profile_.reset(new VisitCountingProfile(event_listener_.get())); RenderViewHostTestHarness::SetUp(); } VisitCountingProfile* profile() const { return static_cast(profile_.get()); } void WaitForCoalescense() { // Let the timer fire. MessageLoop::current()->PostDelayedTask(FROM_HERE, new MessageLoop::QuitTask(), 110); MessageLoop::current()->Run(); } protected: VisitedLinkRenderProcessHostFactory vc_rph_factory_; private: scoped_ptr event_listener_; BrowserThread file_thread_; DISALLOW_COPY_AND_ASSIGN(VisitedLinkEventsTest); }; class VisitedLinkRelayTest : public VisitedLinkEventsTest { public: virtual void SetFactoryMode() { vc_rph_factory_.set_relay_mode(true); } }; TEST_F(VisitedLinkEventsTest, Coalescense) { // add some URLs to master. VisitedLinkMaster* master = profile_->GetVisitedLinkMaster(); // Add a few URLs. master->AddURL(GURL("http://acidtests.org/")); master->AddURL(GURL("http://google.com/")); master->AddURL(GURL("http://chromium.org/")); // Just for kicks, add a duplicate URL. This shouldn't increase the resulting master->AddURL(GURL("http://acidtests.org/")); // Make sure that coalescing actually occurs. There should be no links or // events received by the renderer. EXPECT_EQ(0, profile()->add_count()); EXPECT_EQ(0, profile()->add_event_count()); WaitForCoalescense(); // We now should have 3 entries added in 1 event. EXPECT_EQ(3, profile()->add_count()); EXPECT_EQ(1, profile()->add_event_count()); // Test whether the coalescing continues by adding a few more URLs. master->AddURL(GURL("http://google.com/chrome/")); master->AddURL(GURL("http://webkit.org/")); master->AddURL(GURL("http://acid3.acidtests.org/")); WaitForCoalescense(); // We should have 6 entries added in 2 events. EXPECT_EQ(6, profile()->add_count()); EXPECT_EQ(2, profile()->add_event_count()); // Test whether duplicate entries produce add events. master->AddURL(GURL("http://acidtests.org/")); WaitForCoalescense(); // We should have no change in results. EXPECT_EQ(6, profile()->add_count()); EXPECT_EQ(2, profile()->add_event_count()); // Ensure that the coalescing does not resume after resetting. master->AddURL(GURL("http://build.chromium.org/")); master->DeleteAllURLs(); WaitForCoalescense(); // We should have no change in results except for one new reset event. EXPECT_EQ(6, profile()->add_count()); EXPECT_EQ(2, profile()->add_event_count()); EXPECT_EQ(1, profile()->reset_event_count()); } TEST_F(VisitedLinkRelayTest, Basics) { VisitedLinkMaster* master = profile_->GetVisitedLinkMaster(); rvh()->CreateRenderView(string16()); // Add a few URLs. master->AddURL(GURL("http://acidtests.org/")); master->AddURL(GURL("http://google.com/")); master->AddURL(GURL("http://chromium.org/")); WaitForCoalescense(); // We now should have 1 add event. EXPECT_EQ(1, profile()->add_event_count()); EXPECT_EQ(0, profile()->reset_event_count()); master->DeleteAllURLs(); WaitForCoalescense(); // We should have no change in add results, plus one new reset event. EXPECT_EQ(1, profile()->add_event_count()); EXPECT_EQ(1, profile()->reset_event_count()); } TEST_F(VisitedLinkRelayTest, TabVisibility) { VisitedLinkMaster* master = profile_->GetVisitedLinkMaster(); rvh()->CreateRenderView(string16()); // Simulate tab becoming inactive. rvh()->WasHidden(); // Add a few URLs. master->AddURL(GURL("http://acidtests.org/")); master->AddURL(GURL("http://google.com/")); master->AddURL(GURL("http://chromium.org/")); WaitForCoalescense(); // We shouldn't have any events. EXPECT_EQ(0, profile()->add_event_count()); EXPECT_EQ(0, profile()->reset_event_count()); // Simulate the tab becoming active. rvh()->WasRestored(); // We should now have 3 add events, still no reset events. EXPECT_EQ(1, profile()->add_event_count()); EXPECT_EQ(0, profile()->reset_event_count()); // Deactivate the tab again. rvh()->WasHidden(); // Add a bunch of URLs (over 50) to exhaust the link event buffer. for (int i = 0; i < 100; i++) master->AddURL(TestURL(i)); WaitForCoalescense(); // Again, no change in events until tab is active. EXPECT_EQ(1, profile()->add_event_count()); EXPECT_EQ(0, profile()->reset_event_count()); // Activate the tab. rvh()->WasRestored(); // We should have only one more reset event. EXPECT_EQ(1, profile()->add_event_count()); EXPECT_EQ(1, profile()->reset_event_count()); } TEST_F(VisitedLinkRelayTest, WebViewReadiness) { VisitedLinkMaster* master = profile_->GetVisitedLinkMaster(); // Add a few URLs. master->AddURL(GURL("http://acidtests.org/")); master->AddURL(GURL("http://google.com/")); master->AddURL(GURL("http://chromium.org/")); WaitForCoalescense(); std::set deleted_urls; deleted_urls.insert(GURL("http://acidtests.org/")); master->DeleteURLs(deleted_urls); // We shouldn't have any events, because RenderView hasn't been created, // and we ensure that updates are sent until it is. EXPECT_EQ(0, profile()->add_event_count()); EXPECT_EQ(0, profile()->reset_event_count()); rvh()->CreateRenderView(string16()); // We should now have just a reset event: adds are eaten up by a reset // that followed. EXPECT_EQ(0, profile()->add_event_count()); EXPECT_EQ(1, profile()->reset_event_count()); }