// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "components/dom_distiller/core/dom_distiller_service.h" #include "base/bind.h" #include "base/callback.h" #include "base/containers/hash_tables.h" #include "base/message_loop/message_loop.h" #include "base/run_loop.h" #include "base/strings/string_number_conversions.h" #include "components/dom_distiller/core/article_entry.h" #include "components/dom_distiller/core/distilled_page_prefs.h" #include "components/dom_distiller/core/dom_distiller_model.h" #include "components/dom_distiller/core/dom_distiller_store.h" #include "components/dom_distiller/core/dom_distiller_test_util.h" #include "components/dom_distiller/core/fake_distiller.h" #include "components/dom_distiller/core/fake_distiller_page.h" #include "components/dom_distiller/core/task_tracker.h" #include "components/leveldb_proto/testing/fake_db.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" using leveldb_proto::test::FakeDB; using testing::Invoke; using testing::Return; using testing::_; namespace dom_distiller { namespace test { namespace { class FakeViewRequestDelegate : public ViewRequestDelegate { public: virtual ~FakeViewRequestDelegate() {} MOCK_METHOD1(OnArticleReady, void(const DistilledArticleProto* proto)); MOCK_METHOD1(OnArticleUpdated, void(ArticleDistillationUpdate article_update)); }; class MockDistillerObserver : public DomDistillerObserver { public: MOCK_METHOD1(ArticleEntriesUpdated, void(const std::vector&)); virtual ~MockDistillerObserver() {} }; class MockArticleAvailableCallback { public: MOCK_METHOD1(DistillationCompleted, void(bool)); }; DomDistillerService::ArticleAvailableCallback ArticleCallback( MockArticleAvailableCallback* callback) { return base::Bind(&MockArticleAvailableCallback::DistillationCompleted, base::Unretained(callback)); } void RunDistillerCallback(FakeDistiller* distiller, scoped_ptr proto) { distiller->RunDistillerCallback(proto.Pass()); base::RunLoop().RunUntilIdle(); } scoped_ptr CreateArticleWithURL(const std::string& url) { scoped_ptr proto(new DistilledArticleProto); DistilledPageProto* page = proto->add_pages(); page->set_url(url); return proto.Pass(); } scoped_ptr CreateDefaultArticle() { return CreateArticleWithURL("http://www.example.com/default_article_page1") .Pass(); } } // namespace class DomDistillerServiceTest : public testing::Test { public: virtual void SetUp() { main_loop_.reset(new base::MessageLoop()); FakeDB* fake_db = new FakeDB(&db_model_); FakeDB::EntryMap store_model; store_ = test::util::CreateStoreWithFakeDB(fake_db, store_model); distiller_factory_ = new MockDistillerFactory(); distiller_page_factory_ = new MockDistillerPageFactory(); service_.reset(new DomDistillerService( scoped_ptr(store_), scoped_ptr(distiller_factory_), scoped_ptr(distiller_page_factory_), scoped_ptr())); fake_db->InitCallback(true); fake_db->LoadCallback(true); } virtual void TearDown() { base::RunLoop().RunUntilIdle(); store_ = NULL; distiller_factory_ = NULL; service_.reset(); } protected: // store is owned by service_. DomDistillerStoreInterface* store_; MockDistillerFactory* distiller_factory_; MockDistillerPageFactory* distiller_page_factory_; scoped_ptr service_; scoped_ptr main_loop_; FakeDB::EntryMap db_model_; }; TEST_F(DomDistillerServiceTest, TestViewEntry) { FakeDistiller* distiller = new FakeDistiller(false); EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) .WillOnce(Return(distiller)); GURL url("http://www.example.com/p1"); std::string entry_id("id0"); ArticleEntry entry; entry.set_entry_id(entry_id); entry.add_pages()->set_url(url.spec()); store_->AddEntry(entry); FakeViewRequestDelegate viewer_delegate; scoped_ptr handle = service_->ViewEntry( &viewer_delegate, service_->CreateDefaultDistillerPage(gfx::Size()), entry_id); ASSERT_FALSE(distiller->GetArticleCallback().is_null()); scoped_ptr proto = CreateDefaultArticle(); EXPECT_CALL(viewer_delegate, OnArticleReady(proto.get())); RunDistillerCallback(distiller, proto.Pass()); } TEST_F(DomDistillerServiceTest, TestViewUrl) { FakeDistiller* distiller = new FakeDistiller(false); EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) .WillOnce(Return(distiller)); FakeViewRequestDelegate viewer_delegate; GURL url("http://www.example.com/p1"); scoped_ptr handle = service_->ViewUrl( &viewer_delegate, service_->CreateDefaultDistillerPage(gfx::Size()), url); ASSERT_FALSE(distiller->GetArticleCallback().is_null()); EXPECT_EQ(url, distiller->GetUrl()); scoped_ptr proto = CreateDefaultArticle(); EXPECT_CALL(viewer_delegate, OnArticleReady(proto.get())); RunDistillerCallback(distiller, proto.Pass()); } TEST_F(DomDistillerServiceTest, TestMultipleViewUrl) { FakeDistiller* distiller = new FakeDistiller(false); FakeDistiller* distiller2 = new FakeDistiller(false); EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) .WillOnce(Return(distiller)) .WillOnce(Return(distiller2)); FakeViewRequestDelegate viewer_delegate; FakeViewRequestDelegate viewer_delegate2; GURL url("http://www.example.com/p1"); GURL url2("http://www.example.com/a/p1"); scoped_ptr handle = service_->ViewUrl( &viewer_delegate, service_->CreateDefaultDistillerPage(gfx::Size()), url); scoped_ptr handle2 = service_->ViewUrl( &viewer_delegate2, service_->CreateDefaultDistillerPage(gfx::Size()), url2); ASSERT_FALSE(distiller->GetArticleCallback().is_null()); EXPECT_EQ(url, distiller->GetUrl()); scoped_ptr proto = CreateDefaultArticle(); EXPECT_CALL(viewer_delegate, OnArticleReady(proto.get())); RunDistillerCallback(distiller, proto.Pass()); ASSERT_FALSE(distiller2->GetArticleCallback().is_null()); EXPECT_EQ(url2, distiller2->GetUrl()); scoped_ptr proto2 = CreateDefaultArticle(); EXPECT_CALL(viewer_delegate2, OnArticleReady(proto2.get())); RunDistillerCallback(distiller2, proto2.Pass()); } TEST_F(DomDistillerServiceTest, TestViewUrlCancelled) { FakeDistiller* distiller = new FakeDistiller(false); EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) .WillOnce(Return(distiller)); bool distiller_destroyed = false; EXPECT_CALL(*distiller, Die()) .WillOnce(testing::Assign(&distiller_destroyed, true)); FakeViewRequestDelegate viewer_delegate; GURL url("http://www.example.com/p1"); scoped_ptr handle = service_->ViewUrl( &viewer_delegate, service_->CreateDefaultDistillerPage(gfx::Size()), url); ASSERT_FALSE(distiller->GetArticleCallback().is_null()); EXPECT_EQ(url, distiller->GetUrl()); EXPECT_CALL(viewer_delegate, OnArticleReady(_)).Times(0); EXPECT_FALSE(distiller_destroyed); handle.reset(); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(distiller_destroyed); } TEST_F(DomDistillerServiceTest, TestViewUrlDoesNotAddEntry) { FakeDistiller* distiller = new FakeDistiller(false); EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) .WillOnce(Return(distiller)); FakeViewRequestDelegate viewer_delegate; GURL url("http://www.example.com/p1"); scoped_ptr handle = service_->ViewUrl( &viewer_delegate, service_->CreateDefaultDistillerPage(gfx::Size()), url); scoped_ptr proto = CreateArticleWithURL(url.spec()); EXPECT_CALL(viewer_delegate, OnArticleReady(proto.get())); RunDistillerCallback(distiller, proto.Pass()); base::RunLoop().RunUntilIdle(); // The entry should not be added to the store. EXPECT_EQ(0u, store_->GetEntries().size()); } TEST_F(DomDistillerServiceTest, TestAddAndRemoveEntry) { FakeDistiller* distiller = new FakeDistiller(false); EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) .WillOnce(Return(distiller)); GURL url("http://www.example.com/p1"); MockArticleAvailableCallback article_cb; EXPECT_CALL(article_cb, DistillationCompleted(true)); std::string entry_id = service_->AddToList(url, service_->CreateDefaultDistillerPage(gfx::Size()).Pass(), ArticleCallback(&article_cb)); ASSERT_FALSE(distiller->GetArticleCallback().is_null()); EXPECT_EQ(url, distiller->GetUrl()); scoped_ptr proto = CreateArticleWithURL(url.spec()); RunDistillerCallback(distiller, proto.Pass()); ArticleEntry entry; EXPECT_TRUE(store_->GetEntryByUrl(url, &entry)); EXPECT_EQ(entry.entry_id(), entry_id); EXPECT_EQ(1u, store_->GetEntries().size()); service_->RemoveEntry(entry_id); base::RunLoop().RunUntilIdle(); EXPECT_EQ(0u, store_->GetEntries().size()); } TEST_F(DomDistillerServiceTest, TestCancellation) { FakeDistiller* distiller = new FakeDistiller(false); MockDistillerObserver observer; service_->AddObserver(&observer); EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) .WillOnce(Return(distiller)); MockArticleAvailableCallback article_cb; EXPECT_CALL(article_cb, DistillationCompleted(false)); GURL url("http://www.example.com/p1"); std::string entry_id = service_->AddToList(url, service_->CreateDefaultDistillerPage(gfx::Size()).Pass(), ArticleCallback(&article_cb)); // Remove entry will cause the |article_cb| to be called with false value. service_->RemoveEntry(entry_id); base::RunLoop().RunUntilIdle(); } TEST_F(DomDistillerServiceTest, TestMultipleObservers) { FakeDistiller* distiller = new FakeDistiller(false); EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) .WillOnce(Return(distiller)); const int kObserverCount = 5; MockDistillerObserver observers[kObserverCount]; for (int i = 0; i < kObserverCount; ++i) { service_->AddObserver(&observers[i]); } DomDistillerService::ArticleAvailableCallback article_cb; GURL url("http://www.example.com/p1"); std::string entry_id = service_->AddToList( url, service_->CreateDefaultDistillerPage(gfx::Size()).Pass(), article_cb); // Distillation should notify all observers that article is added. std::vector expected_updates; DomDistillerObserver::ArticleUpdate update; update.entry_id = entry_id; update.update_type = DomDistillerObserver::ArticleUpdate::ADD; expected_updates.push_back(update); for (int i = 0; i < kObserverCount; ++i) { EXPECT_CALL(observers[i], ArticleEntriesUpdated( util::HasExpectedUpdates(expected_updates))); } scoped_ptr proto = CreateDefaultArticle(); RunDistillerCallback(distiller, proto.Pass()); // Remove should notify all observers that article is removed. update.update_type = DomDistillerObserver::ArticleUpdate::REMOVE; expected_updates.clear(); expected_updates.push_back(update); for (int i = 0; i < kObserverCount; ++i) { EXPECT_CALL(observers[i], ArticleEntriesUpdated( util::HasExpectedUpdates(expected_updates))); } service_->RemoveEntry(entry_id); base::RunLoop().RunUntilIdle(); } TEST_F(DomDistillerServiceTest, TestMultipleCallbacks) { FakeDistiller* distiller = new FakeDistiller(false); EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) .WillOnce(Return(distiller)); const int kClientsCount = 5; MockArticleAvailableCallback article_cb[kClientsCount]; // Adding a URL and then distilling calls all clients. GURL url("http://www.example.com/p1"); const std::string entry_id = service_->AddToList(url, service_->CreateDefaultDistillerPage(gfx::Size()).Pass(), ArticleCallback(&article_cb[0])); EXPECT_CALL(article_cb[0], DistillationCompleted(true)); for (int i = 1; i < kClientsCount; ++i) { EXPECT_EQ(entry_id, service_->AddToList( url, service_->CreateDefaultDistillerPage(gfx::Size()).Pass(), ArticleCallback(&article_cb[i]))); EXPECT_CALL(article_cb[i], DistillationCompleted(true)); } scoped_ptr proto = CreateArticleWithURL(url.spec()); RunDistillerCallback(distiller, proto.Pass()); // Add the same url again, all callbacks should be called with true. for (int i = 0; i < kClientsCount; ++i) { EXPECT_CALL(article_cb[i], DistillationCompleted(true)); EXPECT_EQ(entry_id, service_->AddToList( url, service_->CreateDefaultDistillerPage(gfx::Size()).Pass(), ArticleCallback(&article_cb[i]))); } base::RunLoop().RunUntilIdle(); } TEST_F(DomDistillerServiceTest, TestMultipleCallbacksOnRemove) { FakeDistiller* distiller = new FakeDistiller(false); EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) .WillOnce(Return(distiller)); const int kClientsCount = 5; MockArticleAvailableCallback article_cb[kClientsCount]; // Adding a URL and remove the entry before distillation. Callback should be // called with false. GURL url("http://www.example.com/p1"); const std::string entry_id = service_->AddToList(url, service_->CreateDefaultDistillerPage(gfx::Size()).Pass(), ArticleCallback(&article_cb[0])); EXPECT_CALL(article_cb[0], DistillationCompleted(false)); for (int i = 1; i < kClientsCount; ++i) { EXPECT_EQ(entry_id, service_->AddToList( url, service_->CreateDefaultDistillerPage(gfx::Size()).Pass(), ArticleCallback(&article_cb[i]))); EXPECT_CALL(article_cb[i], DistillationCompleted(false)); } service_->RemoveEntry(entry_id); base::RunLoop().RunUntilIdle(); } TEST_F(DomDistillerServiceTest, TestMultiplePageArticle) { FakeDistiller* distiller = new FakeDistiller(false); EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) .WillOnce(Return(distiller)); const int kPageCount = 8; std::string base_url("http://www.example.com/p"); GURL pages_url[kPageCount]; for (int page_num = 0; page_num < kPageCount; ++page_num) { pages_url[page_num] = GURL(base_url + base::IntToString(page_num)); } MockArticleAvailableCallback article_cb; EXPECT_CALL(article_cb, DistillationCompleted(true)); std::string entry_id = service_->AddToList( pages_url[0], service_->CreateDefaultDistillerPage(gfx::Size()).Pass(), ArticleCallback(&article_cb)); ArticleEntry entry; ASSERT_FALSE(distiller->GetArticleCallback().is_null()); EXPECT_EQ(pages_url[0], distiller->GetUrl()); // Create the article with pages to pass to the distiller. scoped_ptr proto = CreateArticleWithURL(pages_url[0].spec()); for (int page_num = 1; page_num < kPageCount; ++page_num) { DistilledPageProto* distilled_page = proto->add_pages(); distilled_page->set_url(pages_url[page_num].spec()); } RunDistillerCallback(distiller, proto.Pass()); EXPECT_TRUE(store_->GetEntryByUrl(pages_url[0], &entry)); EXPECT_EQ(kPageCount, entry.pages_size()); // An article should have just one entry. EXPECT_EQ(1u, store_->GetEntries().size()); // All pages should have correct urls. for (int page_num = 0; page_num < kPageCount; ++page_num) { EXPECT_EQ(pages_url[page_num].spec(), entry.pages(page_num).url()); } // Should be able to query article using any of the pages url. for (int page_num = 0; page_num < kPageCount; ++page_num) { EXPECT_TRUE(store_->GetEntryByUrl(pages_url[page_num], &entry)); } service_->RemoveEntry(entry_id); base::RunLoop().RunUntilIdle(); EXPECT_EQ(0u, store_->GetEntries().size()); } } // namespace test } // namespace dom_distiller