1 // Copyright (c) 2011 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 "base/basictypes.h" 6 #include "base/stringprintf.h" 7 #include "chrome/browser/history/top_sites.h" 8 #include "chrome/browser/tab_contents/thumbnail_generator.h" 9 #include "chrome/common/render_messages.h" 10 #include "chrome/test/testing_profile.h" 11 #include "content/browser/renderer_host/backing_store_manager.h" 12 #include "content/browser/renderer_host/mock_render_process_host.h" 13 #include "content/browser/renderer_host/test_render_view_host.h" 14 #include "content/common/notification_service.h" 15 #include "skia/ext/platform_canvas.h" 16 #include "testing/gtest/include/gtest/gtest.h" 17 #include "third_party/skia/include/core/SkColorPriv.h" 18 #include "ui/gfx/canvas_skia.h" 19 #include "ui/gfx/surface/transport_dib.h" 20 21 static const int kBitmapWidth = 100; 22 static const int kBitmapHeight = 100; 23 24 // TODO(brettw) enable this when GetThumbnailForBackingStore is implemented 25 // for other platforms in thumbnail_generator.cc 26 // #if defined(OS_WIN) 27 // TODO(brettw) enable this on Windows after we clobber a build to see if the 28 // failures of this on the buildbot can be resolved. 29 #if 0 30 31 class ThumbnailGeneratorTest : public testing::Test { 32 public: 33 ThumbnailGeneratorTest() 34 : profile_(), 35 process_(new MockRenderProcessHost(&profile_)), 36 widget_(process_, 1), 37 view_(&widget_) { 38 // Paiting will be skipped if there's no view. 39 widget_.set_view(&view_); 40 41 // Need to send out a create notification for the RWH to get hooked. This is 42 // a little scary in that we don't have a RenderView, but the only listener 43 // will want a RenderWidget, so it works out OK. 44 NotificationService::current()->Notify( 45 NotificationType::RENDER_VIEW_HOST_CREATED_FOR_TAB, 46 Source<RenderViewHostManager>(NULL), 47 Details<RenderViewHost>(reinterpret_cast<RenderViewHost*>(&widget_))); 48 49 transport_dib_.reset(TransportDIB::Create(kBitmapWidth * kBitmapHeight * 4, 50 1)); 51 52 // We don't want to be sensitive to timing. 53 generator_.StartThumbnailing(); 54 generator_.set_no_timeout(true); 55 } 56 57 protected: 58 // Indicates what bitmap should be sent with the paint message. _OTHER will 59 // only be retrned by CheckFirstPixel if the pixel is none of the others. 60 enum TransportType { TRANSPORT_BLACK, TRANSPORT_WHITE, TRANSPORT_OTHER }; 61 62 void SendPaint(TransportType type) { 63 ViewHostMsg_PaintRect_Params params; 64 params.bitmap_rect = gfx::Rect(0, 0, kBitmapWidth, kBitmapHeight); 65 params.view_size = params.bitmap_rect.size(); 66 params.flags = 0; 67 68 scoped_ptr<skia::PlatformCanvas> canvas( 69 transport_dib_->GetPlatformCanvas(kBitmapWidth, kBitmapHeight)); 70 switch (type) { 71 case TRANSPORT_BLACK: 72 canvas->getTopPlatformDevice().accessBitmap(true).eraseARGB( 73 0xFF, 0, 0, 0); 74 break; 75 case TRANSPORT_WHITE: 76 canvas->getTopPlatformDevice().accessBitmap(true).eraseARGB( 77 0xFF, 0xFF, 0xFF, 0xFF); 78 break; 79 case TRANSPORT_OTHER: 80 default: 81 NOTREACHED(); 82 break; 83 } 84 85 params.bitmap = transport_dib_->id(); 86 87 ViewHostMsg_PaintRect msg(1, params); 88 widget_.OnMessageReceived(msg); 89 } 90 91 TransportType ClassifyFirstPixel(const SkBitmap& bitmap) { 92 // Returns the color of the first pixel of the bitmap. The bitmap must be 93 // non-empty. 94 SkAutoLockPixels lock(bitmap); 95 uint32 pixel = *bitmap.getAddr32(0, 0); 96 97 if (SkGetPackedA32(pixel) != 0xFF) 98 return TRANSPORT_OTHER; // All values expect an opqaue alpha channel 99 100 if (SkGetPackedR32(pixel) == 0 && 101 SkGetPackedG32(pixel) == 0 && 102 SkGetPackedB32(pixel) == 0) 103 return TRANSPORT_BLACK; 104 105 if (SkGetPackedR32(pixel) == 0xFF && 106 SkGetPackedG32(pixel) == 0xFF && 107 SkGetPackedB32(pixel) == 0xFF) 108 return TRANSPORT_WHITE; 109 110 EXPECT_TRUE(false) << "Got weird color: " << pixel; 111 return TRANSPORT_OTHER; 112 } 113 114 MessageLoopForUI message_loop_; 115 116 TestingProfile profile_; 117 118 // This will get deleted when the last RHWH associated with it is destroyed. 119 MockRenderProcessHost* process_; 120 121 RenderWidgetHost widget_; 122 TestRenderWidgetHostView view_; 123 ThumbnailGenerator generator_; 124 125 scoped_ptr<TransportDIB> transport_dib_; 126 127 private: 128 // testing::Test implementation. 129 void SetUp() { 130 } 131 void TearDown() { 132 } 133 }; 134 135 TEST_F(ThumbnailGeneratorTest, NoThumbnail) { 136 // This is the case where there is no thumbnail available on the tab and 137 // there is no backing store. There should be no image returned. 138 SkBitmap result = generator_.GetThumbnailForRenderer(&widget_); 139 EXPECT_TRUE(result.isNull()); 140 } 141 142 // Tests basic thumbnail generation when a backing store is discarded. 143 TEST_F(ThumbnailGeneratorTest, DiscardBackingStore) { 144 // First set up a backing store and then discard it. 145 SendPaint(TRANSPORT_BLACK); 146 widget_.WasHidden(); 147 ASSERT_TRUE(BackingStoreManager::ExpireBackingStoreForTest(&widget_)); 148 ASSERT_FALSE(widget_.GetBackingStore(false, false)); 149 150 // The thumbnail generator should have stashed a thumbnail of the page. 151 SkBitmap result = generator_.GetThumbnailForRenderer(&widget_); 152 ASSERT_FALSE(result.isNull()); 153 EXPECT_EQ(TRANSPORT_BLACK, ClassifyFirstPixel(result)); 154 } 155 156 TEST_F(ThumbnailGeneratorTest, QuickShow) { 157 // Set up a hidden widget with a black cached thumbnail and an expired 158 // backing store. 159 SendPaint(TRANSPORT_BLACK); 160 widget_.WasHidden(); 161 ASSERT_TRUE(BackingStoreManager::ExpireBackingStoreForTest(&widget_)); 162 ASSERT_FALSE(widget_.GetBackingStore(false, false)); 163 164 // Now show the widget and paint white. 165 widget_.WasRestored(); 166 SendPaint(TRANSPORT_WHITE); 167 168 // The black thumbnail should still be cached because it hasn't processed the 169 // timer message yet. 170 SkBitmap result = generator_.GetThumbnailForRenderer(&widget_); 171 ASSERT_FALSE(result.isNull()); 172 EXPECT_EQ(TRANSPORT_BLACK, ClassifyFirstPixel(result)); 173 174 // Running the message loop will process the timer, which should expire the 175 // cached thumbnail. Asking again should give us a new one computed from the 176 // backing store. 177 message_loop_.RunAllPending(); 178 result = generator_.GetThumbnailForRenderer(&widget_); 179 ASSERT_FALSE(result.isNull()); 180 EXPECT_EQ(TRANSPORT_WHITE, ClassifyFirstPixel(result)); 181 } 182 183 #endif 184 185 TEST(ThumbnailGeneratorSimpleTest, CalculateBoringScore_Empty) { 186 SkBitmap bitmap; 187 EXPECT_DOUBLE_EQ(1.0, ThumbnailGenerator::CalculateBoringScore(&bitmap)); 188 } 189 190 TEST(ThumbnailGeneratorSimpleTest, CalculateBoringScore_SingleColor) { 191 const SkColor kBlack = SkColorSetRGB(0, 0, 0); 192 const gfx::Size kSize(20, 10); 193 gfx::CanvasSkia canvas(kSize.width(), kSize.height(), true); 194 // Fill all pixesl in black. 195 canvas.FillRectInt(kBlack, 0, 0, kSize.width(), kSize.height()); 196 197 SkBitmap bitmap = canvas.getTopPlatformDevice().accessBitmap(false); 198 // The thumbnail should deserve the highest boring score. 199 EXPECT_DOUBLE_EQ(1.0, ThumbnailGenerator::CalculateBoringScore(&bitmap)); 200 } 201 202 TEST(ThumbnailGeneratorSimpleTest, CalculateBoringScore_TwoColors) { 203 const SkColor kBlack = SkColorSetRGB(0, 0, 0); 204 const SkColor kWhite = SkColorSetRGB(0xFF, 0xFF, 0xFF); 205 const gfx::Size kSize(20, 10); 206 207 gfx::CanvasSkia canvas(kSize.width(), kSize.height(), true); 208 // Fill all pixesl in black. 209 canvas.FillRectInt(kBlack, 0, 0, kSize.width(), kSize.height()); 210 // Fill the left half pixels in white. 211 canvas.FillRectInt(kWhite, 0, 0, kSize.width() / 2, kSize.height()); 212 213 SkBitmap bitmap = canvas.getTopPlatformDevice().accessBitmap(false); 214 ASSERT_EQ(kSize.width(), bitmap.width()); 215 ASSERT_EQ(kSize.height(), bitmap.height()); 216 // The thumbnail should be less boring because two colors are used. 217 EXPECT_DOUBLE_EQ(0.5, ThumbnailGenerator::CalculateBoringScore(&bitmap)); 218 } 219 220 TEST(ThumbnailGeneratorSimpleTest, GetClippedBitmap_TallerThanWide) { 221 // The input bitmap is vertically long. 222 gfx::CanvasSkia canvas(40, 90, true); 223 const SkBitmap bitmap = canvas.getTopPlatformDevice().accessBitmap(false); 224 225 // The desired size is square. 226 ThumbnailGenerator::ClipResult clip_result = ThumbnailGenerator::kNotClipped; 227 SkBitmap clipped_bitmap = ThumbnailGenerator::GetClippedBitmap( 228 bitmap, 10, 10, &clip_result); 229 // The clipped bitmap should be square. 230 EXPECT_EQ(40, clipped_bitmap.width()); 231 EXPECT_EQ(40, clipped_bitmap.height()); 232 // The input was taller than wide. 233 EXPECT_EQ(ThumbnailGenerator::kTallerThanWide, clip_result); 234 } 235 236 TEST(ThumbnailGeneratorSimpleTest, GetClippedBitmap_WiderThanTall) { 237 // The input bitmap is horizontally long. 238 gfx::CanvasSkia canvas(90, 40, true); 239 const SkBitmap bitmap = canvas.getTopPlatformDevice().accessBitmap(false); 240 241 // The desired size is square. 242 ThumbnailGenerator::ClipResult clip_result = ThumbnailGenerator::kNotClipped; 243 SkBitmap clipped_bitmap = ThumbnailGenerator::GetClippedBitmap( 244 bitmap, 10, 10, &clip_result); 245 // The clipped bitmap should be square. 246 EXPECT_EQ(40, clipped_bitmap.width()); 247 EXPECT_EQ(40, clipped_bitmap.height()); 248 // The input was wider than tall. 249 EXPECT_EQ(ThumbnailGenerator::kWiderThanTall, clip_result); 250 } 251 252 TEST(ThumbnailGeneratorSimpleTest, GetClippedBitmap_NotClipped) { 253 // The input bitmap is square. 254 gfx::CanvasSkia canvas(40, 40, true); 255 const SkBitmap bitmap = canvas.getTopPlatformDevice().accessBitmap(false); 256 257 // The desired size is square. 258 ThumbnailGenerator::ClipResult clip_result = ThumbnailGenerator::kNotClipped; 259 SkBitmap clipped_bitmap = ThumbnailGenerator::GetClippedBitmap( 260 bitmap, 10, 10, &clip_result); 261 // The clipped bitmap should be square. 262 EXPECT_EQ(40, clipped_bitmap.width()); 263 EXPECT_EQ(40, clipped_bitmap.height()); 264 // There was no need to clip. 265 EXPECT_EQ(ThumbnailGenerator::kNotClipped, clip_result); 266 } 267 268 TEST(ThumbnailGeneratorSimpleTest, GetClippedBitmap_NonSquareOutput) { 269 // The input bitmap is square. 270 gfx::CanvasSkia canvas(40, 40, true); 271 const SkBitmap bitmap = canvas.getTopPlatformDevice().accessBitmap(false); 272 273 // The desired size is horizontally long. 274 ThumbnailGenerator::ClipResult clip_result = ThumbnailGenerator::kNotClipped; 275 SkBitmap clipped_bitmap = ThumbnailGenerator::GetClippedBitmap( 276 bitmap, 20, 10, &clip_result); 277 // The clipped bitmap should have the same aspect ratio of the desired size. 278 EXPECT_EQ(40, clipped_bitmap.width()); 279 EXPECT_EQ(20, clipped_bitmap.height()); 280 // The input was taller than wide. 281 EXPECT_EQ(ThumbnailGenerator::kTallerThanWide, clip_result); 282 } 283 284 // A mock version of TopSites, used for testing ShouldUpdateThumbnail(). 285 class MockTopSites : public history::TopSites { 286 public: 287 explicit MockTopSites(Profile* profile) 288 : history::TopSites(profile), 289 capacity_(1) { 290 } 291 292 // history::TopSites overrides. 293 virtual bool IsFull() { 294 return known_url_map_.size() >= capacity_; 295 } 296 virtual bool IsKnownURL(const GURL& url) { 297 return known_url_map_.find(url.spec()) != known_url_map_.end(); 298 } 299 virtual bool GetPageThumbnailScore(const GURL& url, ThumbnailScore* score) { 300 std::map<std::string, ThumbnailScore>::const_iterator iter = 301 known_url_map_.find(url.spec()); 302 if (iter == known_url_map_.end()) { 303 return false; 304 } else { 305 *score = iter->second; 306 return true; 307 } 308 } 309 310 // Adds a known URL with the associated thumbnail score. 311 void AddKnownURL(const GURL& url, const ThumbnailScore& score) { 312 known_url_map_[url.spec()] = score; 313 } 314 315 private: 316 virtual ~MockTopSites() {} 317 size_t capacity_; 318 std::map<std::string, ThumbnailScore> known_url_map_; 319 }; 320 321 TEST(ThumbnailGeneratorSimpleTest, ShouldUpdateThumbnail) { 322 const GURL kGoodURL("http://www.google.com/"); 323 const GURL kBadURL("chrome://newtab"); 324 325 // Set up the profile. 326 TestingProfile profile; 327 328 // Set up the top sites service. 329 ScopedTempDir temp_dir; 330 ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); 331 scoped_refptr<MockTopSites> top_sites(new MockTopSites(&profile)); 332 333 // Should be false because it's a bad URL. 334 EXPECT_FALSE(ThumbnailGenerator::ShouldUpdateThumbnail( 335 &profile, top_sites.get(), kBadURL)); 336 337 // Should be true, as it's a good URL. 338 EXPECT_TRUE(ThumbnailGenerator::ShouldUpdateThumbnail( 339 &profile, top_sites.get(), kGoodURL)); 340 341 // Should be false, if it's in the incognito mode. 342 profile.set_incognito(true); 343 EXPECT_FALSE(ThumbnailGenerator::ShouldUpdateThumbnail( 344 &profile, top_sites.get(), kGoodURL)); 345 346 // Should be true again, once turning off the incognito mode. 347 profile.set_incognito(false); 348 EXPECT_TRUE(ThumbnailGenerator::ShouldUpdateThumbnail( 349 &profile, top_sites.get(), kGoodURL)); 350 351 // Add a known URL. This makes the top sites data full. 352 ThumbnailScore bad_score; 353 bad_score.time_at_snapshot = base::Time::UnixEpoch(); // Ancient time stamp. 354 top_sites->AddKnownURL(kGoodURL, bad_score); 355 ASSERT_TRUE(top_sites->IsFull()); 356 357 // Should be false, as the top sites data is full, and the new URL is 358 // not known. 359 const GURL kAnotherGoodURL("http://www.youtube.com/"); 360 EXPECT_FALSE(ThumbnailGenerator::ShouldUpdateThumbnail( 361 &profile, top_sites.get(), kAnotherGoodURL)); 362 363 // Should be true, as the existing thumbnail is bad (i.e need a better one). 364 EXPECT_TRUE(ThumbnailGenerator::ShouldUpdateThumbnail( 365 &profile, top_sites.get(), kGoodURL)); 366 367 // Replace the thumbnail score with a really good one. 368 ThumbnailScore good_score; 369 good_score.time_at_snapshot = base::Time::Now(); // Very new. 370 good_score.at_top = true; 371 good_score.good_clipping = true; 372 good_score.boring_score = 0.0; 373 top_sites->AddKnownURL(kGoodURL, good_score); 374 375 // Should be false, as the existing thumbnail is good enough (i.e. don't 376 // need to replace the existing thumbnail which is new and good). 377 EXPECT_FALSE(ThumbnailGenerator::ShouldUpdateThumbnail( 378 &profile, top_sites.get(), kGoodURL)); 379 } 380