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
TEST(ThumbnailGeneratorSimpleTest,CalculateBoringScore_Empty)185 TEST(ThumbnailGeneratorSimpleTest, CalculateBoringScore_Empty) {
186 SkBitmap bitmap;
187 EXPECT_DOUBLE_EQ(1.0, ThumbnailGenerator::CalculateBoringScore(&bitmap));
188 }
189
TEST(ThumbnailGeneratorSimpleTest,CalculateBoringScore_SingleColor)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
TEST(ThumbnailGeneratorSimpleTest,CalculateBoringScore_TwoColors)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
TEST(ThumbnailGeneratorSimpleTest,GetClippedBitmap_TallerThanWide)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
TEST(ThumbnailGeneratorSimpleTest,GetClippedBitmap_WiderThanTall)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
TEST(ThumbnailGeneratorSimpleTest,GetClippedBitmap_NotClipped)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
TEST(ThumbnailGeneratorSimpleTest,GetClippedBitmap_NonSquareOutput)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:
MockTopSites(Profile * profile)287 explicit MockTopSites(Profile* profile)
288 : history::TopSites(profile),
289 capacity_(1) {
290 }
291
292 // history::TopSites overrides.
IsFull()293 virtual bool IsFull() {
294 return known_url_map_.size() >= capacity_;
295 }
IsKnownURL(const GURL & url)296 virtual bool IsKnownURL(const GURL& url) {
297 return known_url_map_.find(url.spec()) != known_url_map_.end();
298 }
GetPageThumbnailScore(const GURL & url,ThumbnailScore * score)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.
AddKnownURL(const GURL & url,const ThumbnailScore & score)311 void AddKnownURL(const GURL& url, const ThumbnailScore& score) {
312 known_url_map_[url.spec()] = score;
313 }
314
315 private:
~MockTopSites()316 virtual ~MockTopSites() {}
317 size_t capacity_;
318 std::map<std::string, ThumbnailScore> known_url_map_;
319 };
320
TEST(ThumbnailGeneratorSimpleTest,ShouldUpdateThumbnail)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