shortcuts_provider_unittest.cc revision c2db58bd994c04d98e4ee2cd7565b71548655fe3
1// Copyright (c) 2012 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 "chrome/browser/autocomplete/shortcuts_provider.h"
6
7#include <math.h>
8
9#include <algorithm>
10#include <functional>
11#include <set>
12#include <string>
13#include <vector>
14
15#include "base/memory/ref_counted.h"
16#include "base/message_loop/message_loop.h"
17#include "base/prefs/pref_service.h"
18#include "base/strings/stringprintf.h"
19#include "base/strings/utf_string_conversions.h"
20#include "chrome/browser/autocomplete/autocomplete_input.h"
21#include "chrome/browser/autocomplete/autocomplete_match.h"
22#include "chrome/browser/autocomplete/autocomplete_provider.h"
23#include "chrome/browser/autocomplete/autocomplete_provider_listener.h"
24#include "chrome/browser/autocomplete/autocomplete_result.h"
25#include "chrome/browser/history/history_service.h"
26#include "chrome/browser/history/in_memory_url_index.h"
27#include "chrome/browser/history/shortcuts_backend.h"
28#include "chrome/browser/history/shortcuts_backend_factory.h"
29#include "chrome/browser/history/url_database.h"
30#include "chrome/common/pref_names.h"
31#include "chrome/test/base/testing_profile.h"
32#include "content/public/test/test_browser_thread.h"
33#include "testing/gtest/include/gtest/gtest.h"
34
35using content::BrowserThread;
36using history::ShortcutsBackend;
37
38namespace {
39
40struct TestShortcutInfo {
41  std::string guid;
42  std::string url;
43  std::string title;  // The text that orginally was searched for.
44  std::string contents;
45  std::string contents_class;
46  std::string description;
47  std::string description_class;
48  int typed_count;
49  int days_from_now;
50} shortcut_test_db[] = {
51  { "BD85DBA2-8C29-49F9-84AE-48E1E90880E0",
52    "http://www.google.com/", "goog",
53    "Google", "0,1,4,0", "Google", "0,3,4,1", 100, 1 },
54  { "BD85DBA2-8C29-49F9-84AE-48E1E90880E1",
55    "http://slashdot.org/", "slash",
56    "slashdot.org", "0,3,5,1",
57    "Slashdot - News for nerds, stuff that matters", "0,2,5,0", 100, 0},
58  { "BD85DBA2-8C29-49F9-84AE-48E1E90880E2",
59    "http://slashdot.org/", "news",
60    "slashdot.org", "0,1",
61    "Slashdot - News for nerds, stuff that matters", "0,0,11,2,15,0", 5, 0},
62  { "BD85DBA2-8C29-49F9-84AE-48E1E90880E3",
63    "http://sports.yahoo.com/", "news",
64    "sports.yahoo.com", "0,1",
65    "Yahoo! Sports - Sports News, Scores, Rumors, Fantasy Games, and more",
66    "0,0,23,2,27,0", 5, 2},
67  { "BD85DBA2-8C29-49F9-84AE-48E1E90880E4",
68    "http://www.cnn.com/index.html", "news weather",
69    "www.cnn.com/index.html", "0,1",
70    "CNN.com - Breaking News, U.S., World, Weather, Entertainment & Video",
71    "0,0,19,2,23,0,38,2,45,0", 10, 1},
72  { "BD85DBA2-8C29-49F9-84AE-48E1E90880E5",
73    "http://sports.yahoo.com/", "nhl scores",
74    "sports.yahoo.com", "0,1",
75    "Yahoo! Sports - Sports News, Scores, Rumors, Fantasy Games, and more",
76    "0,0,29,2,35,0", 10, 1},
77  { "BD85DBA2-8C29-49F9-84AE-48E1E90880E6",
78    "http://www.nhl.com/scores/index.html", "nhl scores",
79    "www.nhl.com/scores/index.html", "0,1,4,3,7,1",
80    "January 13, 2010 - NHL.com - Scores", "0,0,19,2,22,0,29,2,35,0", 1, 5},
81  { "BD85DBA2-8C29-49F9-84AE-48E1E90880E7",
82    "http://www.testsite.com/a.html", "just",
83    "www.testsite.com/a.html", "0,1",
84    "Test - site - just a test", "0,0,14,2,18,0", 1, 5},
85  { "BD85DBA2-8C29-49F9-84AE-48E1E90880E8",
86    "http://www.testsite.com/b.html", "just",
87    "www.testsite.com/b.html", "0,1",
88    "Test - site - just a test", "0,0,14,2,18,0", 2, 5},
89  { "BD85DBA2-8C29-49F9-84AE-48E1E90880E9",
90    "http://www.testsite.com/c.html", "just",
91    "www.testsite.com/c.html", "0,1",
92    "Test - site - just a test", "0,0,14,2,18,0", 1, 8},
93  { "BD85DBA2-8C29-49F9-84AE-48E1E90880EA",
94    "http://www.testsite.com/d.html", "just a",
95    "www.testsite.com/d.html", "0,1",
96    "Test - site - just a test", "0,0,14,2,18,0", 1, 12},
97  { "BD85DBA2-8C29-49F9-84AE-48E1E90880EB",
98    "http://www.testsite.com/e.html", "just a t",
99    "www.testsite.com/e.html", "0,1",
100    "Test - site - just a test", "0,0,14,2,18,0", 1, 12},
101  { "BD85DBA2-8C29-49F9-84AE-48E1E90880EC",
102    "http://www.testsite.com/f.html", "just a te",
103    "www.testsite.com/f.html", "0,1",
104    "Test - site - just a test", "0,0,14,2,18,0", 1, 12},
105  { "BD85DBA2-8C29-49F9-84AE-48E1E90880ED",
106    "http://www.daysagotest.com/a.html", "ago",
107    "www.daysagotest.com/a.html", "0,1,8,3,11,1",
108    "Test - site", "0,0", 1, 1},
109  { "BD85DBA2-8C29-49F9-84AE-48E1E90880EE",
110    "http://www.daysagotest.com/b.html", "ago",
111    "www.daysagotest.com/b.html", "0,1,8,3,11,1",
112    "Test - site", "0,0", 1, 2},
113  { "BD85DBA2-8C29-49F9-84AE-48E1E90880EF",
114    "http://www.daysagotest.com/c.html", "ago",
115    "www.daysagotest.com/c.html", "0,1,8,3,11,1",
116    "Test - site", "0,0", 1, 3},
117  { "BD85DBA2-8C29-49F9-84AE-48E1E90880F0",
118    "http://www.daysagotest.com/d.html", "ago",
119    "www.daysagotest.com/d.html", "0,1,8,3,11,1",
120    "Test - site", "0,0", 1, 4},
121};
122
123}  // namespace
124
125class ShortcutsProviderTest : public testing::Test,
126                              public AutocompleteProviderListener {
127 public:
128  ShortcutsProviderTest();
129
130  // AutocompleteProviderListener:
131  virtual void OnProviderUpdate(bool updated_matches) OVERRIDE;
132
133 protected:
134  class SetShouldContain
135      : public std::unary_function<const std::string&, std::set<std::string> > {
136   public:
137    explicit SetShouldContain(const ACMatches& matched_urls);
138
139    void operator()(const std::string& expected);
140    std::set<std::string> Leftovers() const { return matches_; }
141
142   private:
143    std::set<std::string> matches_;
144  };
145
146  virtual void SetUp();
147  virtual void TearDown();
148
149  // Fills test data into the provider.
150  void FillData(TestShortcutInfo* db, size_t db_size);
151
152  // Runs an autocomplete query on |text| and checks to see that the returned
153  // results' destination URLs match those provided. |expected_urls| does not
154  // need to be in sorted order.
155  void RunTest(const string16 text,
156               std::vector<std::string> expected_urls,
157               std::string expected_top_result);
158
159  base::MessageLoopForUI message_loop_;
160  content::TestBrowserThread ui_thread_;
161  content::TestBrowserThread file_thread_;
162
163  TestingProfile profile_;
164
165  ACMatches ac_matches_;  // The resulting matches after running RunTest.
166
167  scoped_refptr<ShortcutsBackend> backend_;
168  scoped_refptr<ShortcutsProvider> provider_;
169};
170
171ShortcutsProviderTest::ShortcutsProviderTest()
172    : ui_thread_(BrowserThread::UI, &message_loop_),
173      file_thread_(BrowserThread::FILE, &message_loop_) {
174}
175
176void ShortcutsProviderTest::OnProviderUpdate(bool updated_matches) {}
177
178void ShortcutsProviderTest::SetUp() {
179  ShortcutsBackendFactory::GetInstance()->SetTestingFactoryAndUse(
180      &profile_, &ShortcutsBackendFactory::BuildProfileNoDatabaseForTesting);
181  backend_ = ShortcutsBackendFactory::GetForProfile(&profile_);
182  ASSERT_TRUE(backend_.get());
183  ASSERT_TRUE(profile_.CreateHistoryService(true, false));
184  provider_ = new ShortcutsProvider(this, &profile_);
185  FillData(shortcut_test_db, arraysize(shortcut_test_db));
186}
187
188void ShortcutsProviderTest::TearDown() {
189  // Run all pending tasks or else some threads hold on to the message loop
190  // and prevent it from being deleted.
191  message_loop_.RunUntilIdle();
192  provider_ = NULL;
193}
194
195void ShortcutsProviderTest::FillData(TestShortcutInfo* db, size_t db_size) {
196  DCHECK(provider_.get());
197  size_t expected_size = backend_->shortcuts_map().size() + db_size;
198  for (size_t i = 0; i < db_size; ++i) {
199    const TestShortcutInfo& cur = db[i];
200    ShortcutsBackend::Shortcut shortcut(cur.guid,
201        ASCIIToUTF16(cur.title), GURL(cur.url), ASCIIToUTF16(cur.contents),
202        AutocompleteMatch::ClassificationsFromString(cur.contents_class),
203        ASCIIToUTF16(cur.description),
204        AutocompleteMatch::ClassificationsFromString(cur.description_class),
205        base::Time::Now() - base::TimeDelta::FromDays(cur.days_from_now),
206        cur.typed_count);
207    backend_->AddShortcut(shortcut);
208  }
209  EXPECT_EQ(expected_size, backend_->shortcuts_map().size());
210}
211
212ShortcutsProviderTest::SetShouldContain::SetShouldContain(
213    const ACMatches& matched_urls) {
214  for (ACMatches::const_iterator iter = matched_urls.begin();
215       iter != matched_urls.end(); ++iter)
216    matches_.insert(iter->destination_url.spec());
217}
218
219void ShortcutsProviderTest::SetShouldContain::operator()(
220    const std::string& expected) {
221  EXPECT_EQ(1U, matches_.erase(expected));
222}
223
224void ShortcutsProviderTest::RunTest(const string16 text,
225                                   std::vector<std::string> expected_urls,
226                                   std::string expected_top_result) {
227  std::sort(expected_urls.begin(), expected_urls.end());
228
229  base::MessageLoop::current()->RunUntilIdle();
230  AutocompleteInput input(text, string16::npos, string16(), GURL(),
231                          AutocompleteInput::INVALID_SPEC, false, false, true,
232                          AutocompleteInput::ALL_MATCHES);
233  provider_->Start(input, false);
234  EXPECT_TRUE(provider_->done());
235
236  ac_matches_ = provider_->matches();
237
238  // We should have gotten back at most AutocompleteProvider::kMaxMatches.
239  EXPECT_LE(ac_matches_.size(), AutocompleteProvider::kMaxMatches);
240
241  // If the number of expected and actual matches aren't equal then we need
242  // test no further, but let's do anyway so that we know which URLs failed.
243  EXPECT_EQ(expected_urls.size(), ac_matches_.size());
244
245  // Verify that all expected URLs were found and that all found URLs
246  // were expected.
247  std::set<std::string> Leftovers =
248      for_each(expected_urls.begin(), expected_urls.end(),
249               SetShouldContain(ac_matches_)).Leftovers();
250  EXPECT_EQ(0U, Leftovers.size());
251
252  // See if we got the expected top scorer.
253  if (!ac_matches_.empty()) {
254    std::partial_sort(ac_matches_.begin(), ac_matches_.begin() + 1,
255                      ac_matches_.end(), AutocompleteMatch::MoreRelevant);
256    EXPECT_EQ(expected_top_result, ac_matches_[0].destination_url.spec());
257  }
258
259  // No shortcuts matches are allowed to be inlined no matter how highly
260  // they score.
261  for (ACMatches::const_iterator it = ac_matches_.begin();
262       it != ac_matches_.end(); ++it)
263    EXPECT_FALSE(it->allowed_to_be_default_match);
264}
265
266TEST_F(ShortcutsProviderTest, SimpleSingleMatch) {
267  string16 text(ASCIIToUTF16("go"));
268  std::string expected_url("http://www.google.com/");
269  std::vector<std::string> expected_urls;
270  expected_urls.push_back(expected_url);
271  RunTest(text, expected_urls, expected_url);
272}
273
274TEST_F(ShortcutsProviderTest, MultiMatch) {
275  string16 text(ASCIIToUTF16("NEWS"));
276  std::vector<std::string> expected_urls;
277  // Scores high because of completion length.
278  expected_urls.push_back("http://slashdot.org/");
279  // Scores high because of visit count.
280  expected_urls.push_back("http://sports.yahoo.com/");
281  // Scores high because of visit count but less match span,
282  // which is more important.
283  expected_urls.push_back("http://www.cnn.com/index.html");
284  RunTest(text, expected_urls, "http://slashdot.org/");
285}
286
287TEST_F(ShortcutsProviderTest, TypedCountMatches) {
288  string16 text(ASCIIToUTF16("just"));
289  std::vector<std::string> expected_urls;
290  expected_urls.push_back("http://www.testsite.com/b.html");
291  expected_urls.push_back("http://www.testsite.com/a.html");
292  expected_urls.push_back("http://www.testsite.com/c.html");
293  RunTest(text, expected_urls, "http://www.testsite.com/b.html");
294}
295
296TEST_F(ShortcutsProviderTest, FragmentLengthMatches) {
297  string16 text(ASCIIToUTF16("just a"));
298  std::vector<std::string> expected_urls;
299  expected_urls.push_back("http://www.testsite.com/d.html");
300  expected_urls.push_back("http://www.testsite.com/e.html");
301  expected_urls.push_back("http://www.testsite.com/f.html");
302  RunTest(text, expected_urls, "http://www.testsite.com/d.html");
303}
304
305TEST_F(ShortcutsProviderTest, DaysAgoMatches) {
306  string16 text(ASCIIToUTF16("ago"));
307  std::vector<std::string> expected_urls;
308  expected_urls.push_back("http://www.daysagotest.com/a.html");
309  expected_urls.push_back("http://www.daysagotest.com/b.html");
310  expected_urls.push_back("http://www.daysagotest.com/c.html");
311  RunTest(text, expected_urls, "http://www.daysagotest.com/a.html");
312}
313
314// Helper class to make running tests of ClassifyAllMatchesInString() more
315// convenient.
316class ClassifyTest {
317 public:
318  ClassifyTest(const string16& text, ACMatchClassifications matches);
319  ~ClassifyTest();
320
321  ACMatchClassifications RunTest(const string16& find_text);
322
323 private:
324  const string16 text_;
325  const ACMatchClassifications matches_;
326};
327
328ClassifyTest::ClassifyTest(const string16& text, ACMatchClassifications matches)
329    : text_(text),
330      matches_(matches) {
331}
332
333ClassifyTest::~ClassifyTest() {
334}
335
336ACMatchClassifications ClassifyTest::RunTest(const string16& find_text) {
337  return ShortcutsProvider::ClassifyAllMatchesInString(find_text,
338      ShortcutsProvider::CreateWordMapForString(find_text), text_, matches_);
339}
340
341TEST_F(ShortcutsProviderTest, ClassifyAllMatchesInString) {
342  ACMatchClassifications matches;
343  matches.push_back(ACMatchClassification(0, ACMatchClassification::NONE));
344  ClassifyTest classify_test(ASCIIToUTF16("A man, a plan, a canal Panama"),
345                             matches);
346
347  ACMatchClassifications spans_a = classify_test.RunTest(ASCIIToUTF16("man"));
348  // ACMatch spans should be: '--MMM------------------------'
349  ASSERT_EQ(3U, spans_a.size());
350  EXPECT_EQ(0U, spans_a[0].offset);
351  EXPECT_EQ(ACMatchClassification::NONE, spans_a[0].style);
352  EXPECT_EQ(2U, spans_a[1].offset);
353  EXPECT_EQ(ACMatchClassification::MATCH, spans_a[1].style);
354  EXPECT_EQ(5U, spans_a[2].offset);
355  EXPECT_EQ(ACMatchClassification::NONE, spans_a[2].style);
356
357  ACMatchClassifications spans_b = classify_test.RunTest(ASCIIToUTF16("man p"));
358  // ACMatch spans should be: '--MMM----M-------------M-----'
359  ASSERT_EQ(7U, spans_b.size());
360  EXPECT_EQ(0U, spans_b[0].offset);
361  EXPECT_EQ(ACMatchClassification::NONE, spans_b[0].style);
362  EXPECT_EQ(2U, spans_b[1].offset);
363  EXPECT_EQ(ACMatchClassification::MATCH, spans_b[1].style);
364  EXPECT_EQ(5U, spans_b[2].offset);
365  EXPECT_EQ(ACMatchClassification::NONE, spans_b[2].style);
366  EXPECT_EQ(9U, spans_b[3].offset);
367  EXPECT_EQ(ACMatchClassification::MATCH, spans_b[3].style);
368  EXPECT_EQ(10U, spans_b[4].offset);
369  EXPECT_EQ(ACMatchClassification::NONE, spans_b[4].style);
370  EXPECT_EQ(23U, spans_b[5].offset);
371  EXPECT_EQ(ACMatchClassification::MATCH, spans_b[5].style);
372  EXPECT_EQ(24U, spans_b[6].offset);
373  EXPECT_EQ(ACMatchClassification::NONE, spans_b[6].style);
374
375  ACMatchClassifications spans_c =
376      classify_test.RunTest(ASCIIToUTF16("man plan panama"));
377  // ACMatch spans should be:'--MMM----MMMM----------MMMMMM'
378  ASSERT_EQ(6U, spans_c.size());
379  EXPECT_EQ(0U, spans_c[0].offset);
380  EXPECT_EQ(ACMatchClassification::NONE, spans_c[0].style);
381  EXPECT_EQ(2U, spans_c[1].offset);
382  EXPECT_EQ(ACMatchClassification::MATCH, spans_c[1].style);
383  EXPECT_EQ(5U, spans_c[2].offset);
384  EXPECT_EQ(ACMatchClassification::NONE, spans_c[2].style);
385  EXPECT_EQ(9U, spans_c[3].offset);
386  EXPECT_EQ(ACMatchClassification::MATCH, spans_c[3].style);
387  EXPECT_EQ(13U, spans_c[4].offset);
388  EXPECT_EQ(ACMatchClassification::NONE, spans_c[4].style);
389  EXPECT_EQ(23U, spans_c[5].offset);
390  EXPECT_EQ(ACMatchClassification::MATCH, spans_c[5].style);
391
392  ClassifyTest classify_test2(ASCIIToUTF16("Yahoo! Sports - Sports News, "
393      "Scores, Rumors, Fantasy Games, and more"), matches);
394
395  ACMatchClassifications spans_d = classify_test2.RunTest(ASCIIToUTF16("ne"));
396  // ACMatch spans should match first two letters of the "news".
397  ASSERT_EQ(3U, spans_d.size());
398  EXPECT_EQ(0U, spans_d[0].offset);
399  EXPECT_EQ(ACMatchClassification::NONE, spans_d[0].style);
400  EXPECT_EQ(23U, spans_d[1].offset);
401  EXPECT_EQ(ACMatchClassification::MATCH, spans_d[1].style);
402  EXPECT_EQ(25U, spans_d[2].offset);
403  EXPECT_EQ(ACMatchClassification::NONE, spans_d[2].style);
404
405  ACMatchClassifications spans_e =
406      classify_test2.RunTest(ASCIIToUTF16("news r"));
407  // ACMatch spans should be the same as original matches.
408  ASSERT_EQ(15U, spans_e.size());
409  EXPECT_EQ(0U, spans_e[0].offset);
410  EXPECT_EQ(ACMatchClassification::NONE, spans_e[0].style);
411  // "r" in "Sports".
412  EXPECT_EQ(10U, spans_e[1].offset);
413  EXPECT_EQ(ACMatchClassification::MATCH, spans_e[1].style);
414  EXPECT_EQ(11U, spans_e[2].offset);
415  EXPECT_EQ(ACMatchClassification::NONE, spans_e[2].style);
416  // "r" in second "Sports".
417  EXPECT_EQ(19U, spans_e[3].offset);
418  EXPECT_EQ(ACMatchClassification::MATCH, spans_e[3].style);
419  EXPECT_EQ(20U, spans_e[4].offset);
420  EXPECT_EQ(ACMatchClassification::NONE, spans_e[4].style);
421  // "News".
422  EXPECT_EQ(23U, spans_e[5].offset);
423  EXPECT_EQ(ACMatchClassification::MATCH, spans_e[5].style);
424  EXPECT_EQ(27U, spans_e[6].offset);
425  EXPECT_EQ(ACMatchClassification::NONE, spans_e[6].style);
426  // "r" in "Scores".
427  EXPECT_EQ(32U, spans_e[7].offset);
428  EXPECT_EQ(ACMatchClassification::MATCH, spans_e[7].style);
429  EXPECT_EQ(33U, spans_e[8].offset);
430  EXPECT_EQ(ACMatchClassification::NONE, spans_e[8].style);
431  // First "r" in "Rumors".
432  EXPECT_EQ(37U, spans_e[9].offset);
433  EXPECT_EQ(ACMatchClassification::MATCH, spans_e[9].style);
434  EXPECT_EQ(38U, spans_e[10].offset);
435  EXPECT_EQ(ACMatchClassification::NONE, spans_e[10].style);
436  // Second "r" in "Rumors".
437  EXPECT_EQ(41U, spans_e[11].offset);
438  EXPECT_EQ(ACMatchClassification::MATCH, spans_e[11].style);
439  EXPECT_EQ(42U, spans_e[12].offset);
440  EXPECT_EQ(ACMatchClassification::NONE, spans_e[12].style);
441  // "r" in "more".
442  EXPECT_EQ(66U, spans_e[13].offset);
443  EXPECT_EQ(ACMatchClassification::MATCH, spans_e[13].style);
444  EXPECT_EQ(67U, spans_e[14].offset);
445  EXPECT_EQ(ACMatchClassification::NONE, spans_e[14].style);
446
447  matches.clear();
448  matches.push_back(ACMatchClassification(0, ACMatchClassification::URL));
449  ClassifyTest classify_test3(ASCIIToUTF16("livescore.goal.com"), matches);
450
451  ACMatchClassifications spans_f = classify_test3.RunTest(ASCIIToUTF16("go"));
452  // ACMatch spans should match first two letters of the "goal".
453  ASSERT_EQ(3U, spans_f.size());
454  EXPECT_EQ(0U, spans_f[0].offset);
455  EXPECT_EQ(ACMatchClassification::URL, spans_f[0].style);
456  EXPECT_EQ(10U, spans_f[1].offset);
457  EXPECT_EQ(ACMatchClassification::URL | ACMatchClassification::MATCH,
458            spans_f[1].style);
459  EXPECT_EQ(12U, spans_f[2].offset);
460  EXPECT_EQ(ACMatchClassification::URL, spans_f[2].style);
461
462  matches.clear();
463  matches.push_back(ACMatchClassification(0, ACMatchClassification::NONE));
464  matches.push_back(ACMatchClassification(13, ACMatchClassification::URL));
465  ClassifyTest classify_test4(ASCIIToUTF16("Email login: mail.somecorp.com"),
466                              matches);
467
468  ACMatchClassifications spans_g = classify_test4.RunTest(ASCIIToUTF16("ail"));
469  ASSERT_EQ(6U, spans_g.size());
470  EXPECT_EQ(0U, spans_g[0].offset);
471  EXPECT_EQ(ACMatchClassification::NONE, spans_g[0].style);
472  EXPECT_EQ(2U, spans_g[1].offset);
473  EXPECT_EQ(ACMatchClassification::MATCH, spans_g[1].style);
474  EXPECT_EQ(5U, spans_g[2].offset);
475  EXPECT_EQ(ACMatchClassification::NONE, spans_g[2].style);
476  EXPECT_EQ(13U, spans_g[3].offset);
477  EXPECT_EQ(ACMatchClassification::URL, spans_g[3].style);
478  EXPECT_EQ(14U, spans_g[4].offset);
479  EXPECT_EQ(ACMatchClassification::URL | ACMatchClassification::MATCH,
480            spans_g[4].style);
481  EXPECT_EQ(17U, spans_g[5].offset);
482  EXPECT_EQ(ACMatchClassification::URL, spans_g[5].style);
483
484  ACMatchClassifications spans_h =
485      classify_test4.RunTest(ASCIIToUTF16("lo log"));
486  ASSERT_EQ(4U, spans_h.size());
487  EXPECT_EQ(0U, spans_h[0].offset);
488  EXPECT_EQ(ACMatchClassification::NONE, spans_h[0].style);
489  EXPECT_EQ(6U, spans_h[1].offset);
490  EXPECT_EQ(ACMatchClassification::MATCH, spans_h[1].style);
491  EXPECT_EQ(9U, spans_h[2].offset);
492  EXPECT_EQ(ACMatchClassification::NONE, spans_h[2].style);
493  EXPECT_EQ(13U, spans_h[3].offset);
494  EXPECT_EQ(ACMatchClassification::URL, spans_h[3].style);
495
496  ACMatchClassifications spans_i =
497      classify_test4.RunTest(ASCIIToUTF16("ail em"));
498  // 'Email' and 'ail' should be matched.
499  ASSERT_EQ(5U, spans_i.size());
500  EXPECT_EQ(0U, spans_i[0].offset);
501  EXPECT_EQ(ACMatchClassification::MATCH, spans_i[0].style);
502  EXPECT_EQ(5U, spans_i[1].offset);
503  EXPECT_EQ(ACMatchClassification::NONE, spans_i[1].style);
504  EXPECT_EQ(13U, spans_i[2].offset);
505  EXPECT_EQ(ACMatchClassification::URL, spans_i[2].style);
506  EXPECT_EQ(14U, spans_i[3].offset);
507  EXPECT_EQ(ACMatchClassification::URL | ACMatchClassification::MATCH,
508            spans_i[3].style);
509  EXPECT_EQ(17U, spans_i[4].offset);
510  EXPECT_EQ(ACMatchClassification::URL, spans_i[4].style);
511
512  // Some web sites do not have a description.  If the string being searched is
513  // empty, the classifications must also be empty: http://crbug.com/148647
514  // Extra parens in the next line hack around C++03's "most vexing parse".
515  class ClassifyTest classify_test5((string16()), ACMatchClassifications());
516  ACMatchClassifications spans_j = classify_test5.RunTest(ASCIIToUTF16("man"));
517  ASSERT_EQ(0U, spans_j.size());
518
519  // Matches which end at beginning of classification merge properly.
520  matches.clear();
521  matches.push_back(ACMatchClassification(0, ACMatchClassification::DIM));
522  matches.push_back(ACMatchClassification(9, ACMatchClassification::NONE));
523  ClassifyTest classify_test6(ASCIIToUTF16("html password example"), matches);
524
525  // Extra space in the next string avoids having the string be a prefix of the
526  // text above, which would allow for two different valid classification sets,
527  // one of which uses two spans (the first of which would mark all of "html
528  // pass" as a match) and one which uses four (which marks the individual words
529  // as matches but not the space between them).  This way only the latter is
530  // valid.
531  ACMatchClassifications spans_k =
532      classify_test6.RunTest(ASCIIToUTF16("html  pass"));
533  ASSERT_EQ(4U, spans_k.size());
534  EXPECT_EQ(0U, spans_k[0].offset);
535  EXPECT_EQ(ACMatchClassification::DIM | ACMatchClassification::MATCH,
536            spans_k[0].style);
537  EXPECT_EQ(4U, spans_k[1].offset);
538  EXPECT_EQ(ACMatchClassification::DIM, spans_k[1].style);
539  EXPECT_EQ(5U, spans_k[2].offset);
540  EXPECT_EQ(ACMatchClassification::DIM | ACMatchClassification::MATCH,
541            spans_k[2].style);
542  EXPECT_EQ(9U, spans_k[3].offset);
543  EXPECT_EQ(ACMatchClassification::NONE, spans_k[3].style);
544
545  // Multiple matches with both beginning and end at beginning of
546  // classifications merge properly.
547  matches.clear();
548  matches.push_back(ACMatchClassification(0, ACMatchClassification::URL));
549  matches.push_back(ACMatchClassification(11, ACMatchClassification::NONE));
550  ClassifyTest classify_test7(ASCIIToUTF16("http://a.co is great"), matches);
551
552  ACMatchClassifications spans_l =
553      classify_test7.RunTest(ASCIIToUTF16("ht co"));
554  ASSERT_EQ(4U, spans_l.size());
555  EXPECT_EQ(0U, spans_l[0].offset);
556  EXPECT_EQ(ACMatchClassification::URL | ACMatchClassification::MATCH,
557            spans_l[0].style);
558  EXPECT_EQ(2U, spans_l[1].offset);
559  EXPECT_EQ(ACMatchClassification::URL, spans_l[1].style);
560  EXPECT_EQ(9U, spans_l[2].offset);
561  EXPECT_EQ(ACMatchClassification::URL | ACMatchClassification::MATCH,
562            spans_l[2].style);
563  EXPECT_EQ(11U, spans_l[3].offset);
564  EXPECT_EQ(ACMatchClassification::NONE, spans_l[3].style);
565}
566
567TEST_F(ShortcutsProviderTest, CalculateScore) {
568  ACMatchClassifications spans_content;
569  spans_content.push_back(ACMatchClassification(0, ACMatchClassification::URL));
570  spans_content.push_back(ACMatchClassification(
571      4, ACMatchClassification::MATCH | ACMatchClassification::URL));
572  spans_content.push_back(ACMatchClassification(8, ACMatchClassification::URL));
573  ACMatchClassifications spans_description;
574  spans_description.push_back(
575      ACMatchClassification(0, ACMatchClassification::NONE));
576  spans_description.push_back(
577      ACMatchClassification(2, ACMatchClassification::MATCH));
578  ShortcutsBackend::Shortcut shortcut(std::string(),
579      ASCIIToUTF16("test"), GURL("http://www.test.com"),
580      ASCIIToUTF16("www.test.com"), spans_content, ASCIIToUTF16("A test"),
581      spans_description, base::Time::Now(), 1);
582
583  // Maximal score.
584  const int max_relevance = AutocompleteResult::kLowestDefaultScore - 1;
585  const int kMaxScore = provider_->CalculateScore(
586      ASCIIToUTF16("test"), shortcut, max_relevance);
587
588  // Score decreases as percent of the match is decreased.
589  int score_three_quarters =
590      provider_->CalculateScore(ASCIIToUTF16("tes"), shortcut, max_relevance);
591  EXPECT_LT(score_three_quarters, kMaxScore);
592  int score_one_half =
593      provider_->CalculateScore(ASCIIToUTF16("te"), shortcut, max_relevance);
594  EXPECT_LT(score_one_half, score_three_quarters);
595  int score_one_quarter =
596      provider_->CalculateScore(ASCIIToUTF16("t"), shortcut, max_relevance);
597  EXPECT_LT(score_one_quarter, score_one_half);
598
599  // Should decay with time - one week.
600  shortcut.last_access_time = base::Time::Now() - base::TimeDelta::FromDays(7);
601  int score_week_old =
602      provider_->CalculateScore(ASCIIToUTF16("test"), shortcut, max_relevance);
603  EXPECT_LT(score_week_old, kMaxScore);
604
605  // Should decay more in two weeks.
606  shortcut.last_access_time = base::Time::Now() - base::TimeDelta::FromDays(14);
607  int score_two_weeks_old =
608      provider_->CalculateScore(ASCIIToUTF16("test"), shortcut, max_relevance);
609  EXPECT_LT(score_two_weeks_old, score_week_old);
610
611  // But not if it was activly clicked on. 2 hits slow decaying power.
612  shortcut.number_of_hits = 2;
613  shortcut.last_access_time = base::Time::Now() - base::TimeDelta::FromDays(14);
614  int score_popular_two_weeks_old =
615      provider_->CalculateScore(ASCIIToUTF16("test"), shortcut, max_relevance);
616  EXPECT_LT(score_two_weeks_old, score_popular_two_weeks_old);
617  // But still decayed.
618  EXPECT_LT(score_popular_two_weeks_old, kMaxScore);
619
620  // 3 hits slow decaying power even more.
621  shortcut.number_of_hits = 3;
622  shortcut.last_access_time = base::Time::Now() - base::TimeDelta::FromDays(14);
623  int score_more_popular_two_weeks_old =
624      provider_->CalculateScore(ASCIIToUTF16("test"), shortcut, max_relevance);
625  EXPECT_LT(score_two_weeks_old, score_more_popular_two_weeks_old);
626  EXPECT_LT(score_popular_two_weeks_old, score_more_popular_two_weeks_old);
627  // But still decayed.
628  EXPECT_LT(score_more_popular_two_weeks_old, kMaxScore);
629}
630
631TEST_F(ShortcutsProviderTest, DeleteMatch) {
632  TestShortcutInfo shortcuts_to_test_delete[3] = {
633    { "BD85DBA2-8C29-49F9-84AE-48E1E90880F1",
634      "http://www.deletetest.com/1.html", "delete",
635      "http://www.deletetest.com/1.html", "0,2",
636      "Erase this shortcut!", "0,0", 1, 1},
637    { "BD85DBA2-8C29-49F9-84AE-48E1E90880F2",
638      "http://www.deletetest.com/1.html", "erase",
639      "http://www.deletetest.com/1.html", "0,2",
640      "Erase this shortcut!", "0,0", 1, 1},
641    { "BD85DBA2-8C29-49F9-84AE-48E1E90880F3",
642      "http://www.deletetest.com/2.html", "delete",
643      "http://www.deletetest.com/2.html", "0,2",
644      "Erase this shortcut!", "0,0", 1, 1},
645  };
646
647  size_t original_shortcuts_count = backend_->shortcuts_map().size();
648
649  FillData(shortcuts_to_test_delete, arraysize(shortcuts_to_test_delete));
650
651  EXPECT_EQ(original_shortcuts_count + 3, backend_->shortcuts_map().size());
652  EXPECT_FALSE(backend_->shortcuts_map().end() ==
653               backend_->shortcuts_map().find(ASCIIToUTF16("delete")));
654  EXPECT_FALSE(backend_->shortcuts_map().end() ==
655               backend_->shortcuts_map().find(ASCIIToUTF16("erase")));
656
657  AutocompleteMatch match(
658      provider_.get(), 1200, true, AutocompleteMatchType::HISTORY_TITLE);
659
660  match.destination_url = GURL(shortcuts_to_test_delete[0].url);
661  match.contents = ASCIIToUTF16(shortcuts_to_test_delete[0].contents);
662  match.description = ASCIIToUTF16(shortcuts_to_test_delete[0].description);
663
664  provider_->DeleteMatch(match);
665
666  // |shortcuts_to_test_delete[0]| and |shortcuts_to_test_delete[1]| should be
667  // deleted, but not |shortcuts_to_test_delete[2]| as it has different url.
668  EXPECT_EQ(original_shortcuts_count + 1, backend_->shortcuts_map().size());
669  EXPECT_FALSE(backend_->shortcuts_map().end() ==
670               backend_->shortcuts_map().find(ASCIIToUTF16("delete")));
671  EXPECT_TRUE(backend_->shortcuts_map().end() ==
672              backend_->shortcuts_map().find(ASCIIToUTF16("erase")));
673
674  match.destination_url = GURL(shortcuts_to_test_delete[2].url);
675  match.contents = ASCIIToUTF16(shortcuts_to_test_delete[2].contents);
676  match.description = ASCIIToUTF16(shortcuts_to_test_delete[2].description);
677
678  provider_->DeleteMatch(match);
679  EXPECT_EQ(original_shortcuts_count, backend_->shortcuts_map().size());
680  EXPECT_TRUE(backend_->shortcuts_map().end() ==
681              backend_->shortcuts_map().find(ASCIIToUTF16("delete")));
682}
683