// 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 "base/basictypes.h" #include "base/stringprintf.h" #include "chrome/browser/history/top_sites.h" #include "chrome/browser/tab_contents/thumbnail_generator.h" #include "chrome/common/render_messages.h" #include "chrome/test/testing_profile.h" #include "content/browser/renderer_host/backing_store_manager.h" #include "content/browser/renderer_host/mock_render_process_host.h" #include "content/browser/renderer_host/test_render_view_host.h" #include "content/common/notification_service.h" #include "skia/ext/platform_canvas.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/skia/include/core/SkColorPriv.h" #include "ui/gfx/canvas_skia.h" #include "ui/gfx/surface/transport_dib.h" static const int kBitmapWidth = 100; static const int kBitmapHeight = 100; // TODO(brettw) enable this when GetThumbnailForBackingStore is implemented // for other platforms in thumbnail_generator.cc // #if defined(OS_WIN) // TODO(brettw) enable this on Windows after we clobber a build to see if the // failures of this on the buildbot can be resolved. #if 0 class ThumbnailGeneratorTest : public testing::Test { public: ThumbnailGeneratorTest() : profile_(), process_(new MockRenderProcessHost(&profile_)), widget_(process_, 1), view_(&widget_) { // Paiting will be skipped if there's no view. widget_.set_view(&view_); // Need to send out a create notification for the RWH to get hooked. This is // a little scary in that we don't have a RenderView, but the only listener // will want a RenderWidget, so it works out OK. NotificationService::current()->Notify( NotificationType::RENDER_VIEW_HOST_CREATED_FOR_TAB, Source<RenderViewHostManager>(NULL), Details<RenderViewHost>(reinterpret_cast<RenderViewHost*>(&widget_))); transport_dib_.reset(TransportDIB::Create(kBitmapWidth * kBitmapHeight * 4, 1)); // We don't want to be sensitive to timing. generator_.StartThumbnailing(); generator_.set_no_timeout(true); } protected: // Indicates what bitmap should be sent with the paint message. _OTHER will // only be retrned by CheckFirstPixel if the pixel is none of the others. enum TransportType { TRANSPORT_BLACK, TRANSPORT_WHITE, TRANSPORT_OTHER }; void SendPaint(TransportType type) { ViewHostMsg_PaintRect_Params params; params.bitmap_rect = gfx::Rect(0, 0, kBitmapWidth, kBitmapHeight); params.view_size = params.bitmap_rect.size(); params.flags = 0; scoped_ptr<skia::PlatformCanvas> canvas( transport_dib_->GetPlatformCanvas(kBitmapWidth, kBitmapHeight)); switch (type) { case TRANSPORT_BLACK: canvas->getTopPlatformDevice().accessBitmap(true).eraseARGB( 0xFF, 0, 0, 0); break; case TRANSPORT_WHITE: canvas->getTopPlatformDevice().accessBitmap(true).eraseARGB( 0xFF, 0xFF, 0xFF, 0xFF); break; case TRANSPORT_OTHER: default: NOTREACHED(); break; } params.bitmap = transport_dib_->id(); ViewHostMsg_PaintRect msg(1, params); widget_.OnMessageReceived(msg); } TransportType ClassifyFirstPixel(const SkBitmap& bitmap) { // Returns the color of the first pixel of the bitmap. The bitmap must be // non-empty. SkAutoLockPixels lock(bitmap); uint32 pixel = *bitmap.getAddr32(0, 0); if (SkGetPackedA32(pixel) != 0xFF) return TRANSPORT_OTHER; // All values expect an opqaue alpha channel if (SkGetPackedR32(pixel) == 0 && SkGetPackedG32(pixel) == 0 && SkGetPackedB32(pixel) == 0) return TRANSPORT_BLACK; if (SkGetPackedR32(pixel) == 0xFF && SkGetPackedG32(pixel) == 0xFF && SkGetPackedB32(pixel) == 0xFF) return TRANSPORT_WHITE; EXPECT_TRUE(false) << "Got weird color: " << pixel; return TRANSPORT_OTHER; } MessageLoopForUI message_loop_; TestingProfile profile_; // This will get deleted when the last RHWH associated with it is destroyed. MockRenderProcessHost* process_; RenderWidgetHost widget_; TestRenderWidgetHostView view_; ThumbnailGenerator generator_; scoped_ptr<TransportDIB> transport_dib_; private: // testing::Test implementation. void SetUp() { } void TearDown() { } }; TEST_F(ThumbnailGeneratorTest, NoThumbnail) { // This is the case where there is no thumbnail available on the tab and // there is no backing store. There should be no image returned. SkBitmap result = generator_.GetThumbnailForRenderer(&widget_); EXPECT_TRUE(result.isNull()); } // Tests basic thumbnail generation when a backing store is discarded. TEST_F(ThumbnailGeneratorTest, DiscardBackingStore) { // First set up a backing store and then discard it. SendPaint(TRANSPORT_BLACK); widget_.WasHidden(); ASSERT_TRUE(BackingStoreManager::ExpireBackingStoreForTest(&widget_)); ASSERT_FALSE(widget_.GetBackingStore(false, false)); // The thumbnail generator should have stashed a thumbnail of the page. SkBitmap result = generator_.GetThumbnailForRenderer(&widget_); ASSERT_FALSE(result.isNull()); EXPECT_EQ(TRANSPORT_BLACK, ClassifyFirstPixel(result)); } TEST_F(ThumbnailGeneratorTest, QuickShow) { // Set up a hidden widget with a black cached thumbnail and an expired // backing store. SendPaint(TRANSPORT_BLACK); widget_.WasHidden(); ASSERT_TRUE(BackingStoreManager::ExpireBackingStoreForTest(&widget_)); ASSERT_FALSE(widget_.GetBackingStore(false, false)); // Now show the widget and paint white. widget_.WasRestored(); SendPaint(TRANSPORT_WHITE); // The black thumbnail should still be cached because it hasn't processed the // timer message yet. SkBitmap result = generator_.GetThumbnailForRenderer(&widget_); ASSERT_FALSE(result.isNull()); EXPECT_EQ(TRANSPORT_BLACK, ClassifyFirstPixel(result)); // Running the message loop will process the timer, which should expire the // cached thumbnail. Asking again should give us a new one computed from the // backing store. message_loop_.RunAllPending(); result = generator_.GetThumbnailForRenderer(&widget_); ASSERT_FALSE(result.isNull()); EXPECT_EQ(TRANSPORT_WHITE, ClassifyFirstPixel(result)); } #endif TEST(ThumbnailGeneratorSimpleTest, CalculateBoringScore_Empty) { SkBitmap bitmap; EXPECT_DOUBLE_EQ(1.0, ThumbnailGenerator::CalculateBoringScore(&bitmap)); } TEST(ThumbnailGeneratorSimpleTest, CalculateBoringScore_SingleColor) { const SkColor kBlack = SkColorSetRGB(0, 0, 0); const gfx::Size kSize(20, 10); gfx::CanvasSkia canvas(kSize.width(), kSize.height(), true); // Fill all pixesl in black. canvas.FillRectInt(kBlack, 0, 0, kSize.width(), kSize.height()); SkBitmap bitmap = canvas.getTopPlatformDevice().accessBitmap(false); // The thumbnail should deserve the highest boring score. EXPECT_DOUBLE_EQ(1.0, ThumbnailGenerator::CalculateBoringScore(&bitmap)); } TEST(ThumbnailGeneratorSimpleTest, CalculateBoringScore_TwoColors) { const SkColor kBlack = SkColorSetRGB(0, 0, 0); const SkColor kWhite = SkColorSetRGB(0xFF, 0xFF, 0xFF); const gfx::Size kSize(20, 10); gfx::CanvasSkia canvas(kSize.width(), kSize.height(), true); // Fill all pixesl in black. canvas.FillRectInt(kBlack, 0, 0, kSize.width(), kSize.height()); // Fill the left half pixels in white. canvas.FillRectInt(kWhite, 0, 0, kSize.width() / 2, kSize.height()); SkBitmap bitmap = canvas.getTopPlatformDevice().accessBitmap(false); ASSERT_EQ(kSize.width(), bitmap.width()); ASSERT_EQ(kSize.height(), bitmap.height()); // The thumbnail should be less boring because two colors are used. EXPECT_DOUBLE_EQ(0.5, ThumbnailGenerator::CalculateBoringScore(&bitmap)); } TEST(ThumbnailGeneratorSimpleTest, GetClippedBitmap_TallerThanWide) { // The input bitmap is vertically long. gfx::CanvasSkia canvas(40, 90, true); const SkBitmap bitmap = canvas.getTopPlatformDevice().accessBitmap(false); // The desired size is square. ThumbnailGenerator::ClipResult clip_result = ThumbnailGenerator::kNotClipped; SkBitmap clipped_bitmap = ThumbnailGenerator::GetClippedBitmap( bitmap, 10, 10, &clip_result); // The clipped bitmap should be square. EXPECT_EQ(40, clipped_bitmap.width()); EXPECT_EQ(40, clipped_bitmap.height()); // The input was taller than wide. EXPECT_EQ(ThumbnailGenerator::kTallerThanWide, clip_result); } TEST(ThumbnailGeneratorSimpleTest, GetClippedBitmap_WiderThanTall) { // The input bitmap is horizontally long. gfx::CanvasSkia canvas(90, 40, true); const SkBitmap bitmap = canvas.getTopPlatformDevice().accessBitmap(false); // The desired size is square. ThumbnailGenerator::ClipResult clip_result = ThumbnailGenerator::kNotClipped; SkBitmap clipped_bitmap = ThumbnailGenerator::GetClippedBitmap( bitmap, 10, 10, &clip_result); // The clipped bitmap should be square. EXPECT_EQ(40, clipped_bitmap.width()); EXPECT_EQ(40, clipped_bitmap.height()); // The input was wider than tall. EXPECT_EQ(ThumbnailGenerator::kWiderThanTall, clip_result); } TEST(ThumbnailGeneratorSimpleTest, GetClippedBitmap_NotClipped) { // The input bitmap is square. gfx::CanvasSkia canvas(40, 40, true); const SkBitmap bitmap = canvas.getTopPlatformDevice().accessBitmap(false); // The desired size is square. ThumbnailGenerator::ClipResult clip_result = ThumbnailGenerator::kNotClipped; SkBitmap clipped_bitmap = ThumbnailGenerator::GetClippedBitmap( bitmap, 10, 10, &clip_result); // The clipped bitmap should be square. EXPECT_EQ(40, clipped_bitmap.width()); EXPECT_EQ(40, clipped_bitmap.height()); // There was no need to clip. EXPECT_EQ(ThumbnailGenerator::kNotClipped, clip_result); } TEST(ThumbnailGeneratorSimpleTest, GetClippedBitmap_NonSquareOutput) { // The input bitmap is square. gfx::CanvasSkia canvas(40, 40, true); const SkBitmap bitmap = canvas.getTopPlatformDevice().accessBitmap(false); // The desired size is horizontally long. ThumbnailGenerator::ClipResult clip_result = ThumbnailGenerator::kNotClipped; SkBitmap clipped_bitmap = ThumbnailGenerator::GetClippedBitmap( bitmap, 20, 10, &clip_result); // The clipped bitmap should have the same aspect ratio of the desired size. EXPECT_EQ(40, clipped_bitmap.width()); EXPECT_EQ(20, clipped_bitmap.height()); // The input was taller than wide. EXPECT_EQ(ThumbnailGenerator::kTallerThanWide, clip_result); } // A mock version of TopSites, used for testing ShouldUpdateThumbnail(). class MockTopSites : public history::TopSites { public: explicit MockTopSites(Profile* profile) : history::TopSites(profile), capacity_(1) { } // history::TopSites overrides. virtual bool IsFull() { return known_url_map_.size() >= capacity_; } virtual bool IsKnownURL(const GURL& url) { return known_url_map_.find(url.spec()) != known_url_map_.end(); } virtual bool GetPageThumbnailScore(const GURL& url, ThumbnailScore* score) { std::map<std::string, ThumbnailScore>::const_iterator iter = known_url_map_.find(url.spec()); if (iter == known_url_map_.end()) { return false; } else { *score = iter->second; return true; } } // Adds a known URL with the associated thumbnail score. void AddKnownURL(const GURL& url, const ThumbnailScore& score) { known_url_map_[url.spec()] = score; } private: virtual ~MockTopSites() {} size_t capacity_; std::map<std::string, ThumbnailScore> known_url_map_; }; TEST(ThumbnailGeneratorSimpleTest, ShouldUpdateThumbnail) { const GURL kGoodURL("http://www.google.com/"); const GURL kBadURL("chrome://newtab"); // Set up the profile. TestingProfile profile; // Set up the top sites service. ScopedTempDir temp_dir; ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); scoped_refptr<MockTopSites> top_sites(new MockTopSites(&profile)); // Should be false because it's a bad URL. EXPECT_FALSE(ThumbnailGenerator::ShouldUpdateThumbnail( &profile, top_sites.get(), kBadURL)); // Should be true, as it's a good URL. EXPECT_TRUE(ThumbnailGenerator::ShouldUpdateThumbnail( &profile, top_sites.get(), kGoodURL)); // Should be false, if it's in the incognito mode. profile.set_incognito(true); EXPECT_FALSE(ThumbnailGenerator::ShouldUpdateThumbnail( &profile, top_sites.get(), kGoodURL)); // Should be true again, once turning off the incognito mode. profile.set_incognito(false); EXPECT_TRUE(ThumbnailGenerator::ShouldUpdateThumbnail( &profile, top_sites.get(), kGoodURL)); // Add a known URL. This makes the top sites data full. ThumbnailScore bad_score; bad_score.time_at_snapshot = base::Time::UnixEpoch(); // Ancient time stamp. top_sites->AddKnownURL(kGoodURL, bad_score); ASSERT_TRUE(top_sites->IsFull()); // Should be false, as the top sites data is full, and the new URL is // not known. const GURL kAnotherGoodURL("http://www.youtube.com/"); EXPECT_FALSE(ThumbnailGenerator::ShouldUpdateThumbnail( &profile, top_sites.get(), kAnotherGoodURL)); // Should be true, as the existing thumbnail is bad (i.e need a better one). EXPECT_TRUE(ThumbnailGenerator::ShouldUpdateThumbnail( &profile, top_sites.get(), kGoodURL)); // Replace the thumbnail score with a really good one. ThumbnailScore good_score; good_score.time_at_snapshot = base::Time::Now(); // Very new. good_score.at_top = true; good_score.good_clipping = true; good_score.boring_score = 0.0; top_sites->AddKnownURL(kGoodURL, good_score); // Should be false, as the existing thumbnail is good enough (i.e. don't // need to replace the existing thumbnail which is new and good). EXPECT_FALSE(ThumbnailGenerator::ShouldUpdateThumbnail( &profile, top_sites.get(), kGoodURL)); }