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
21static const int kBitmapWidth = 100;
22static 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
31class 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
135TEST_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.
143TEST_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
156TEST_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
185TEST(ThumbnailGeneratorSimpleTest, CalculateBoringScore_Empty) {
186  SkBitmap bitmap;
187  EXPECT_DOUBLE_EQ(1.0, ThumbnailGenerator::CalculateBoringScore(&bitmap));
188}
189
190TEST(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
202TEST(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
220TEST(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
236TEST(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
252TEST(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
268TEST(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().
285class 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
321TEST(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