1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "components/dom_distiller/core/dom_distiller_service.h" 6 7 #include "base/bind.h" 8 #include "base/callback.h" 9 #include "base/containers/hash_tables.h" 10 #include "base/message_loop/message_loop.h" 11 #include "base/run_loop.h" 12 #include "base/strings/string_number_conversions.h" 13 #include "components/dom_distiller/core/article_entry.h" 14 #include "components/dom_distiller/core/dom_distiller_model.h" 15 #include "components/dom_distiller/core/dom_distiller_store.h" 16 #include "components/dom_distiller/core/dom_distiller_test_util.h" 17 #include "components/dom_distiller/core/fake_distiller.h" 18 #include "components/dom_distiller/core/fake_distiller_page.h" 19 #include "components/dom_distiller/core/task_tracker.h" 20 #include "components/leveldb_proto/testing/fake_db.h" 21 #include "testing/gmock/include/gmock/gmock.h" 22 #include "testing/gtest/include/gtest/gtest.h" 23 24 using leveldb_proto::test::FakeDB; 25 using testing::Invoke; 26 using testing::Return; 27 using testing::_; 28 29 namespace dom_distiller { 30 namespace test { 31 32 namespace { 33 34 class FakeViewRequestDelegate : public ViewRequestDelegate { 35 public: 36 virtual ~FakeViewRequestDelegate() {} 37 MOCK_METHOD1(OnArticleReady, void(const DistilledArticleProto* proto)); 38 MOCK_METHOD1(OnArticleUpdated, 39 void(ArticleDistillationUpdate article_update)); 40 }; 41 42 class MockDistillerObserver : public DomDistillerObserver { 43 public: 44 MOCK_METHOD1(ArticleEntriesUpdated, void(const std::vector<ArticleUpdate>&)); 45 virtual ~MockDistillerObserver() {} 46 }; 47 48 class MockArticleAvailableCallback { 49 public: 50 MOCK_METHOD1(DistillationCompleted, void(bool)); 51 }; 52 53 DomDistillerService::ArticleAvailableCallback ArticleCallback( 54 MockArticleAvailableCallback* callback) { 55 return base::Bind(&MockArticleAvailableCallback::DistillationCompleted, 56 base::Unretained(callback)); 57 } 58 59 void RunDistillerCallback(FakeDistiller* distiller, 60 scoped_ptr<DistilledArticleProto> proto) { 61 distiller->RunDistillerCallback(proto.Pass()); 62 base::RunLoop().RunUntilIdle(); 63 } 64 65 scoped_ptr<DistilledArticleProto> CreateArticleWithURL(const std::string& url) { 66 scoped_ptr<DistilledArticleProto> proto(new DistilledArticleProto); 67 DistilledPageProto* page = proto->add_pages(); 68 page->set_url(url); 69 return proto.Pass(); 70 } 71 72 scoped_ptr<DistilledArticleProto> CreateDefaultArticle() { 73 return CreateArticleWithURL("http://www.example.com/default_article_page1") 74 .Pass(); 75 } 76 77 } // namespace 78 79 class DomDistillerServiceTest : public testing::Test { 80 public: 81 virtual void SetUp() { 82 main_loop_.reset(new base::MessageLoop()); 83 FakeDB<ArticleEntry>* fake_db = new FakeDB<ArticleEntry>(&db_model_); 84 FakeDB<ArticleEntry>::EntryMap store_model; 85 store_ = 86 test::util::CreateStoreWithFakeDB(fake_db, store_model); 87 distiller_factory_ = new MockDistillerFactory(); 88 distiller_page_factory_ = new MockDistillerPageFactory(); 89 service_.reset(new DomDistillerService( 90 scoped_ptr<DomDistillerStoreInterface>(store_), 91 scoped_ptr<DistillerFactory>(distiller_factory_), 92 scoped_ptr<DistillerPageFactory>(distiller_page_factory_))); 93 fake_db->InitCallback(true); 94 fake_db->LoadCallback(true); 95 } 96 97 virtual void TearDown() { 98 base::RunLoop().RunUntilIdle(); 99 store_ = NULL; 100 distiller_factory_ = NULL; 101 service_.reset(); 102 } 103 104 protected: 105 // store is owned by service_. 106 DomDistillerStoreInterface* store_; 107 MockDistillerFactory* distiller_factory_; 108 MockDistillerPageFactory* distiller_page_factory_; 109 scoped_ptr<DomDistillerService> service_; 110 scoped_ptr<base::MessageLoop> main_loop_; 111 FakeDB<ArticleEntry>::EntryMap db_model_; 112 }; 113 114 TEST_F(DomDistillerServiceTest, TestViewEntry) { 115 FakeDistiller* distiller = new FakeDistiller(false); 116 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 117 .WillOnce(Return(distiller)); 118 119 GURL url("http://www.example.com/p1"); 120 std::string entry_id("id0"); 121 ArticleEntry entry; 122 entry.set_entry_id(entry_id); 123 entry.add_pages()->set_url(url.spec()); 124 125 store_->AddEntry(entry); 126 127 FakeViewRequestDelegate viewer_delegate; 128 scoped_ptr<ViewerHandle> handle = service_->ViewEntry( 129 &viewer_delegate, service_->CreateDefaultDistillerPage(), entry_id); 130 131 ASSERT_FALSE(distiller->GetArticleCallback().is_null()); 132 133 scoped_ptr<DistilledArticleProto> proto = CreateDefaultArticle(); 134 EXPECT_CALL(viewer_delegate, OnArticleReady(proto.get())); 135 136 RunDistillerCallback(distiller, proto.Pass()); 137 } 138 139 TEST_F(DomDistillerServiceTest, TestViewUrl) { 140 FakeDistiller* distiller = new FakeDistiller(false); 141 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 142 .WillOnce(Return(distiller)); 143 144 FakeViewRequestDelegate viewer_delegate; 145 GURL url("http://www.example.com/p1"); 146 scoped_ptr<ViewerHandle> handle = service_->ViewUrl( 147 &viewer_delegate, service_->CreateDefaultDistillerPage(), url); 148 149 ASSERT_FALSE(distiller->GetArticleCallback().is_null()); 150 EXPECT_EQ(url, distiller->GetUrl()); 151 152 scoped_ptr<DistilledArticleProto> proto = CreateDefaultArticle(); 153 EXPECT_CALL(viewer_delegate, OnArticleReady(proto.get())); 154 155 RunDistillerCallback(distiller, proto.Pass()); 156 } 157 158 TEST_F(DomDistillerServiceTest, TestMultipleViewUrl) { 159 FakeDistiller* distiller = new FakeDistiller(false); 160 FakeDistiller* distiller2 = new FakeDistiller(false); 161 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 162 .WillOnce(Return(distiller)) 163 .WillOnce(Return(distiller2)); 164 165 FakeViewRequestDelegate viewer_delegate; 166 FakeViewRequestDelegate viewer_delegate2; 167 168 GURL url("http://www.example.com/p1"); 169 GURL url2("http://www.example.com/a/p1"); 170 171 scoped_ptr<ViewerHandle> handle = service_->ViewUrl( 172 &viewer_delegate, service_->CreateDefaultDistillerPage(), url); 173 scoped_ptr<ViewerHandle> handle2 = service_->ViewUrl( 174 &viewer_delegate2, service_->CreateDefaultDistillerPage(), url2); 175 176 ASSERT_FALSE(distiller->GetArticleCallback().is_null()); 177 EXPECT_EQ(url, distiller->GetUrl()); 178 179 scoped_ptr<DistilledArticleProto> proto = CreateDefaultArticle(); 180 EXPECT_CALL(viewer_delegate, OnArticleReady(proto.get())); 181 182 RunDistillerCallback(distiller, proto.Pass()); 183 184 ASSERT_FALSE(distiller2->GetArticleCallback().is_null()); 185 EXPECT_EQ(url2, distiller2->GetUrl()); 186 187 scoped_ptr<DistilledArticleProto> proto2 = CreateDefaultArticle(); 188 EXPECT_CALL(viewer_delegate2, OnArticleReady(proto2.get())); 189 190 RunDistillerCallback(distiller2, proto2.Pass()); 191 } 192 193 TEST_F(DomDistillerServiceTest, TestViewUrlCancelled) { 194 FakeDistiller* distiller = new FakeDistiller(false); 195 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 196 .WillOnce(Return(distiller)); 197 198 bool distiller_destroyed = false; 199 EXPECT_CALL(*distiller, Die()) 200 .WillOnce(testing::Assign(&distiller_destroyed, true)); 201 202 FakeViewRequestDelegate viewer_delegate; 203 GURL url("http://www.example.com/p1"); 204 scoped_ptr<ViewerHandle> handle = service_->ViewUrl( 205 &viewer_delegate, service_->CreateDefaultDistillerPage(), url); 206 207 ASSERT_FALSE(distiller->GetArticleCallback().is_null()); 208 EXPECT_EQ(url, distiller->GetUrl()); 209 210 EXPECT_CALL(viewer_delegate, OnArticleReady(_)).Times(0); 211 212 EXPECT_FALSE(distiller_destroyed); 213 214 handle.reset(); 215 base::RunLoop().RunUntilIdle(); 216 EXPECT_TRUE(distiller_destroyed); 217 } 218 219 TEST_F(DomDistillerServiceTest, TestViewUrlDoesNotAddEntry) { 220 FakeDistiller* distiller = new FakeDistiller(false); 221 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 222 .WillOnce(Return(distiller)); 223 224 FakeViewRequestDelegate viewer_delegate; 225 GURL url("http://www.example.com/p1"); 226 scoped_ptr<ViewerHandle> handle = service_->ViewUrl( 227 &viewer_delegate, service_->CreateDefaultDistillerPage(), url); 228 229 scoped_ptr<DistilledArticleProto> proto = CreateArticleWithURL(url.spec()); 230 EXPECT_CALL(viewer_delegate, OnArticleReady(proto.get())); 231 232 RunDistillerCallback(distiller, proto.Pass()); 233 base::RunLoop().RunUntilIdle(); 234 // The entry should not be added to the store. 235 EXPECT_EQ(0u, store_->GetEntries().size()); 236 } 237 238 TEST_F(DomDistillerServiceTest, TestAddAndRemoveEntry) { 239 FakeDistiller* distiller = new FakeDistiller(false); 240 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 241 .WillOnce(Return(distiller)); 242 243 GURL url("http://www.example.com/p1"); 244 245 MockArticleAvailableCallback article_cb; 246 EXPECT_CALL(article_cb, DistillationCompleted(true)); 247 248 std::string entry_id = 249 service_->AddToList(url, service_->CreateDefaultDistillerPage().Pass(), 250 ArticleCallback(&article_cb)); 251 252 ASSERT_FALSE(distiller->GetArticleCallback().is_null()); 253 EXPECT_EQ(url, distiller->GetUrl()); 254 255 scoped_ptr<DistilledArticleProto> proto = CreateArticleWithURL(url.spec()); 256 RunDistillerCallback(distiller, proto.Pass()); 257 258 ArticleEntry entry; 259 EXPECT_TRUE(store_->GetEntryByUrl(url, &entry)); 260 EXPECT_EQ(entry.entry_id(), entry_id); 261 EXPECT_EQ(1u, store_->GetEntries().size()); 262 service_->RemoveEntry(entry_id); 263 base::RunLoop().RunUntilIdle(); 264 EXPECT_EQ(0u, store_->GetEntries().size()); 265 } 266 267 TEST_F(DomDistillerServiceTest, TestCancellation) { 268 FakeDistiller* distiller = new FakeDistiller(false); 269 MockDistillerObserver observer; 270 service_->AddObserver(&observer); 271 272 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 273 .WillOnce(Return(distiller)); 274 275 MockArticleAvailableCallback article_cb; 276 EXPECT_CALL(article_cb, DistillationCompleted(false)); 277 278 GURL url("http://www.example.com/p1"); 279 std::string entry_id = 280 service_->AddToList(url, service_->CreateDefaultDistillerPage().Pass(), 281 ArticleCallback(&article_cb)); 282 283 // Remove entry will cause the |article_cb| to be called with false value. 284 service_->RemoveEntry(entry_id); 285 base::RunLoop().RunUntilIdle(); 286 } 287 288 TEST_F(DomDistillerServiceTest, TestMultipleObservers) { 289 FakeDistiller* distiller = new FakeDistiller(false); 290 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 291 .WillOnce(Return(distiller)); 292 293 const int kObserverCount = 5; 294 MockDistillerObserver observers[kObserverCount]; 295 for (int i = 0; i < kObserverCount; ++i) { 296 service_->AddObserver(&observers[i]); 297 } 298 299 DomDistillerService::ArticleAvailableCallback article_cb; 300 GURL url("http://www.example.com/p1"); 301 std::string entry_id = service_->AddToList( 302 url, service_->CreateDefaultDistillerPage().Pass(), article_cb); 303 304 // Distillation should notify all observers that article is added. 305 std::vector<DomDistillerObserver::ArticleUpdate> expected_updates; 306 DomDistillerObserver::ArticleUpdate update; 307 update.entry_id = entry_id; 308 update.update_type = DomDistillerObserver::ArticleUpdate::ADD; 309 expected_updates.push_back(update); 310 311 for (int i = 0; i < kObserverCount; ++i) { 312 EXPECT_CALL(observers[i], ArticleEntriesUpdated( 313 util::HasExpectedUpdates(expected_updates))); 314 } 315 316 scoped_ptr<DistilledArticleProto> proto = CreateDefaultArticle(); 317 RunDistillerCallback(distiller, proto.Pass()); 318 319 // Remove should notify all observers that article is removed. 320 update.update_type = DomDistillerObserver::ArticleUpdate::REMOVE; 321 expected_updates.clear(); 322 expected_updates.push_back(update); 323 for (int i = 0; i < kObserverCount; ++i) { 324 EXPECT_CALL(observers[i], ArticleEntriesUpdated( 325 util::HasExpectedUpdates(expected_updates))); 326 } 327 328 service_->RemoveEntry(entry_id); 329 base::RunLoop().RunUntilIdle(); 330 } 331 332 TEST_F(DomDistillerServiceTest, TestMultipleCallbacks) { 333 FakeDistiller* distiller = new FakeDistiller(false); 334 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 335 .WillOnce(Return(distiller)); 336 337 const int kClientsCount = 5; 338 MockArticleAvailableCallback article_cb[kClientsCount]; 339 // Adding a URL and then distilling calls all clients. 340 GURL url("http://www.example.com/p1"); 341 const std::string entry_id = 342 service_->AddToList(url, service_->CreateDefaultDistillerPage().Pass(), 343 ArticleCallback(&article_cb[0])); 344 EXPECT_CALL(article_cb[0], DistillationCompleted(true)); 345 346 for (int i = 1; i < kClientsCount; ++i) { 347 EXPECT_EQ(entry_id, service_->AddToList( 348 url, service_->CreateDefaultDistillerPage().Pass(), 349 ArticleCallback(&article_cb[i]))); 350 EXPECT_CALL(article_cb[i], DistillationCompleted(true)); 351 } 352 353 scoped_ptr<DistilledArticleProto> proto = CreateArticleWithURL(url.spec()); 354 RunDistillerCallback(distiller, proto.Pass()); 355 356 // Add the same url again, all callbacks should be called with true. 357 for (int i = 0; i < kClientsCount; ++i) { 358 EXPECT_CALL(article_cb[i], DistillationCompleted(true)); 359 EXPECT_EQ(entry_id, service_->AddToList( 360 url, service_->CreateDefaultDistillerPage().Pass(), 361 ArticleCallback(&article_cb[i]))); 362 } 363 364 base::RunLoop().RunUntilIdle(); 365 } 366 367 TEST_F(DomDistillerServiceTest, TestMultipleCallbacksOnRemove) { 368 FakeDistiller* distiller = new FakeDistiller(false); 369 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 370 .WillOnce(Return(distiller)); 371 372 const int kClientsCount = 5; 373 MockArticleAvailableCallback article_cb[kClientsCount]; 374 // Adding a URL and remove the entry before distillation. Callback should be 375 // called with false. 376 GURL url("http://www.example.com/p1"); 377 const std::string entry_id = 378 service_->AddToList(url, service_->CreateDefaultDistillerPage().Pass(), 379 ArticleCallback(&article_cb[0])); 380 381 EXPECT_CALL(article_cb[0], DistillationCompleted(false)); 382 for (int i = 1; i < kClientsCount; ++i) { 383 EXPECT_EQ(entry_id, service_->AddToList( 384 url, service_->CreateDefaultDistillerPage().Pass(), 385 ArticleCallback(&article_cb[i]))); 386 EXPECT_CALL(article_cb[i], DistillationCompleted(false)); 387 } 388 389 service_->RemoveEntry(entry_id); 390 base::RunLoop().RunUntilIdle(); 391 } 392 393 TEST_F(DomDistillerServiceTest, TestMultiplePageArticle) { 394 FakeDistiller* distiller = new FakeDistiller(false); 395 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 396 .WillOnce(Return(distiller)); 397 398 const int kPageCount = 8; 399 400 std::string base_url("http://www.example.com/p"); 401 GURL pages_url[kPageCount]; 402 for (int page_num = 0; page_num < kPageCount; ++page_num) { 403 pages_url[page_num] = GURL(base_url + base::IntToString(page_num)); 404 } 405 406 MockArticleAvailableCallback article_cb; 407 EXPECT_CALL(article_cb, DistillationCompleted(true)); 408 409 std::string entry_id = service_->AddToList( 410 pages_url[0], service_->CreateDefaultDistillerPage().Pass(), 411 ArticleCallback(&article_cb)); 412 413 ArticleEntry entry; 414 ASSERT_FALSE(distiller->GetArticleCallback().is_null()); 415 EXPECT_EQ(pages_url[0], distiller->GetUrl()); 416 417 // Create the article with pages to pass to the distiller. 418 scoped_ptr<DistilledArticleProto> proto = 419 CreateArticleWithURL(pages_url[0].spec()); 420 for (int page_num = 1; page_num < kPageCount; ++page_num) { 421 DistilledPageProto* distilled_page = proto->add_pages(); 422 distilled_page->set_url(pages_url[page_num].spec()); 423 } 424 425 RunDistillerCallback(distiller, proto.Pass()); 426 EXPECT_TRUE(store_->GetEntryByUrl(pages_url[0], &entry)); 427 428 EXPECT_EQ(kPageCount, entry.pages_size()); 429 // An article should have just one entry. 430 EXPECT_EQ(1u, store_->GetEntries().size()); 431 432 // All pages should have correct urls. 433 for (int page_num = 0; page_num < kPageCount; ++page_num) { 434 EXPECT_EQ(pages_url[page_num].spec(), entry.pages(page_num).url()); 435 } 436 437 // Should be able to query article using any of the pages url. 438 for (int page_num = 0; page_num < kPageCount; ++page_num) { 439 EXPECT_TRUE(store_->GetEntryByUrl(pages_url[page_num], &entry)); 440 } 441 442 service_->RemoveEntry(entry_id); 443 base::RunLoop().RunUntilIdle(); 444 EXPECT_EQ(0u, store_->GetEntries().size()); 445 } 446 447 } // namespace test 448 } // namespace dom_distiller 449