1// Copyright 2013 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/bind.h"
7#include "base/memory/scoped_ptr.h"
8#include "base/message_loop/message_loop.h"
9#include "base/run_loop.h"
10#include "base/strings/stringprintf.h"
11#include "base/threading/platform_thread.h"
12#include "chrome/browser/ui/app_list/search/common/dictionary_data_store.h"
13#include "chrome/browser/ui/app_list/search/history.h"
14#include "chrome/browser/ui/app_list/search/history_data.h"
15#include "chrome/browser/ui/app_list/search/history_data_observer.h"
16#include "chrome/browser/ui/app_list/search/history_data_store.h"
17#include "chrome/browser/ui/app_list/search/history_factory.h"
18#include "chrome/test/base/testing_profile.h"
19#include "content/public/test/test_browser_thread.h"
20#include "testing/gtest/include/gtest/gtest.h"
21
22namespace app_list {
23namespace test {
24
25namespace {
26
27const size_t kMaxPrimary = 3;
28const size_t kMaxSecondary = 2;
29
30// HistoryDataLoadWaiter waits for give |data| to be loaded from underlying
31// store on the blocking pool. The waiter waits on the main message loop until
32// OnHistoryDataLoadedFromStore() is invoked or the maximum allowed wait time
33// has passed.
34class HistoryDataLoadWaiter : public HistoryDataObserver {
35 public:
36  explicit HistoryDataLoadWaiter(HistoryData* data) : data_(data)  {}
37  virtual ~HistoryDataLoadWaiter() {}
38
39  void Wait() {
40    data_->AddObserver(this);
41
42    run_loop_.reset(new base::RunLoop);
43    run_loop_->Run();
44
45    data_->RemoveObserver(this);
46  }
47
48 private:
49  // HistoryDataObserver overrides:
50  virtual void OnHistoryDataLoadedFromStore() OVERRIDE {
51    run_loop_->Quit();
52  }
53
54  HistoryData* data_;  // Not owned.
55  scoped_ptr<base::RunLoop> run_loop_;
56
57  DISALLOW_COPY_AND_ASSIGN(HistoryDataLoadWaiter);
58};
59
60// StoreFlushWaiter waits for the given |store| to flush its data to disk.
61// The flush and disk write happens on the blocking pool. The waiter waits
62// on the main message loop until the OnFlushed() is invoked or the maximum
63// allowed wait time has passed.
64class StoreFlushWaiter {
65 public:
66  explicit StoreFlushWaiter(HistoryDataStore* store) : store_(store) {}
67  ~StoreFlushWaiter() {}
68
69  void Wait() {
70    store_->Flush(
71        base::Bind(&StoreFlushWaiter::OnFlushed, base::Unretained(this)));
72
73    run_loop_.reset(new base::RunLoop);
74    run_loop_->Run();
75  }
76
77 private:
78  void OnFlushed() {
79    run_loop_->Quit();
80  }
81
82  HistoryDataStore* store_;  // Not owned.
83  scoped_ptr<base::RunLoop> run_loop_;
84
85  DISALLOW_COPY_AND_ASSIGN(StoreFlushWaiter);
86};
87
88}  // namespace
89
90class SearchHistoryTest : public testing::Test {
91 public:
92  SearchHistoryTest()
93      : ui_thread_(content::BrowserThread::UI, &message_loop_) {}
94  virtual ~SearchHistoryTest() {}
95
96  // testing::Test overrides:
97  virtual void SetUp() OVERRIDE {
98    profile_.reset(new TestingProfile);
99    CreateHistory();
100  }
101  virtual void TearDown() OVERRIDE {
102    Flush();
103  }
104
105  void CreateHistory() {
106    const char kStoreDataFileName[] = "app-launcher-test";
107    const base::FilePath data_file =
108        profile_->GetPath().AppendASCII(kStoreDataFileName);
109    scoped_refptr<DictionaryDataStore> dictionary_data_store(
110        new DictionaryDataStore(data_file));
111    history_.reset(new History(scoped_refptr<HistoryDataStore>(
112        new HistoryDataStore(dictionary_data_store))));
113
114    // Replace |data_| with test params.
115    history_->data_->RemoveObserver(history_.get());
116    history_->data_.reset(
117        new HistoryData(history_->store_.get(), kMaxPrimary, kMaxSecondary));
118    history_->data_->AddObserver(history_.get());
119
120    HistoryDataLoadWaiter(history_->data_.get()).Wait();
121    ASSERT_TRUE(history_->IsReady());
122  }
123
124  void Flush() {
125    StoreFlushWaiter(history_->store_.get()).Wait();
126  }
127
128  size_t GetKnownResults(const std::string& query) {
129    known_results_ = history()->GetKnownResults(query).Pass();
130    return known_results_->size();
131  }
132
133  KnownResultType GetResultType(const std::string& result_id) {
134    return known_results_->find(result_id) != known_results_->end()
135               ? (*known_results_.get())[result_id]
136               : UNKNOWN_RESULT;
137  }
138
139  History* history() { return history_.get(); }
140  const HistoryData::Associations& associations() const {
141    return history_->data_->associations();
142  }
143
144 private:
145  base::MessageLoopForUI message_loop_;
146  content::TestBrowserThread ui_thread_;
147  scoped_ptr<TestingProfile> profile_;
148
149  scoped_ptr<History> history_;
150  scoped_ptr<KnownResults> known_results_;
151
152  DISALLOW_COPY_AND_ASSIGN(SearchHistoryTest);
153};
154
155TEST_F(SearchHistoryTest, Persistence) {
156  // Ensure it's empty.
157  EXPECT_EQ(0u, GetKnownResults("cal"));
158
159  // Add one launch event.
160  history()->AddLaunchEvent("cal", "calendar");
161  EXPECT_EQ(1u, GetKnownResults("cal"));
162
163  // Flush and recreate the history object.
164  Flush();
165  CreateHistory();
166
167  // History should be initialized with data just added.
168  EXPECT_EQ(1u, GetKnownResults("cal"));
169}
170
171TEST_F(SearchHistoryTest, PerfectAndPrefixMatch) {
172  const char kQuery[] = "cal";
173  const char kQueryPrefix[] = "c";
174  const char kPrimary[] = "calendar";
175  const char kSecondary[] = "calculator";
176
177  history()->AddLaunchEvent(kQuery, kPrimary);
178  history()->AddLaunchEvent(kQuery, kSecondary);
179
180  EXPECT_EQ(2u, GetKnownResults(kQuery));
181  EXPECT_EQ(PERFECT_PRIMARY, GetResultType(kPrimary));
182  EXPECT_EQ(PERFECT_SECONDARY, GetResultType(kSecondary));
183
184  EXPECT_EQ(2u, GetKnownResults(kQueryPrefix));
185  EXPECT_EQ(PREFIX_PRIMARY, GetResultType(kPrimary));
186  EXPECT_EQ(PREFIX_SECONDARY, GetResultType(kSecondary));
187}
188
189TEST_F(SearchHistoryTest, StickyPrimary) {
190  const char kQuery[] = "cal";
191  const char kPrimary[] = "calendar";
192  const char kSecondary[] = "calculator";
193  const char kOther[] = "other";
194
195  // Add two launch events. kPrimary becomes primary.
196  history()->AddLaunchEvent(kQuery, kPrimary);
197  history()->AddLaunchEvent(kQuery, kSecondary);
198
199  EXPECT_EQ(2u, GetKnownResults(kQuery));
200  EXPECT_EQ(PERFECT_PRIMARY, GetResultType(kPrimary));
201  EXPECT_EQ(PERFECT_SECONDARY, GetResultType(kSecondary));
202
203  // These launch events should not change primary.
204  history()->AddLaunchEvent(kQuery, kPrimary);
205  history()->AddLaunchEvent(kQuery, kSecondary);
206  history()->AddLaunchEvent(kQuery, kPrimary);
207  history()->AddLaunchEvent(kQuery, kSecondary);
208  history()->AddLaunchEvent(kQuery, kPrimary);
209  history()->AddLaunchEvent(kQuery, kSecondary);
210  history()->AddLaunchEvent(kQuery, kOther);
211  history()->AddLaunchEvent(kQuery, kSecondary);
212  history()->AddLaunchEvent(kQuery, kOther);
213  history()->AddLaunchEvent(kQuery, kSecondary);
214  history()->AddLaunchEvent(kQuery, kOther);
215
216  EXPECT_EQ(3u, GetKnownResults(kQuery));
217  EXPECT_EQ(PERFECT_PRIMARY, GetResultType(kPrimary));
218  EXPECT_EQ(PERFECT_SECONDARY, GetResultType(kSecondary));
219  EXPECT_EQ(PERFECT_SECONDARY, GetResultType(kOther));
220}
221
222TEST_F(SearchHistoryTest, PromoteSecondary) {
223  const char kQuery[] = "cal";
224  const char kPrimary[] = "calendar";
225  const char kSecondary[] = "calculator";
226
227  history()->AddLaunchEvent(kQuery, kPrimary);
228  history()->AddLaunchEvent(kQuery, kSecondary);
229
230  EXPECT_EQ(2u, GetKnownResults(kQuery));
231  EXPECT_EQ(PERFECT_PRIMARY, GetResultType(kPrimary));
232  EXPECT_EQ(PERFECT_SECONDARY, GetResultType(kSecondary));
233
234  // The 2nd launch in a row promotes it to be primary.
235  history()->AddLaunchEvent(kQuery, kSecondary);
236
237  EXPECT_EQ(2u, GetKnownResults(kQuery));
238  EXPECT_EQ(PERFECT_PRIMARY, GetResultType(kSecondary));
239  EXPECT_EQ(PERFECT_SECONDARY, GetResultType(kPrimary));
240}
241
242TEST_F(SearchHistoryTest, MaxPrimary) {
243  for (size_t i = 0; i < kMaxPrimary; ++i) {
244    std::string query = base::StringPrintf("%d", static_cast<int>(i));
245    history()->AddLaunchEvent(query, "app");
246  }
247  EXPECT_EQ(kMaxPrimary, associations().size());
248
249  // Oldest entries still exists.
250  EXPECT_TRUE(associations().find("0") != associations().end());
251  EXPECT_TRUE(associations().find("1") != associations().end());
252
253  // Primary entry trimming is based on time. The code could run here too fast
254  // and Time::Now has not changed on some platform. Sleep a bit here to ensure
255  // that some time has passed to get rid of the flake.
256  base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(25));
257
258  // Touches the oldest and 2nd oldest becomes oldest now.
259  history()->AddLaunchEvent("0", "app");
260
261  // Adds one more
262  history()->AddLaunchEvent("extra", "app");
263
264  // Number of entries are capped to kMaxPrimary.
265  EXPECT_EQ(kMaxPrimary, associations().size());
266
267  // Oldest entry is trimmed.
268  EXPECT_FALSE(associations().find("1") != associations().end());
269
270  // The touched oldest survived.
271  EXPECT_TRUE(associations().find("0") != associations().end());
272}
273
274TEST_F(SearchHistoryTest, MaxSecondary) {
275  const char kQuery[] = "query";
276  history()->AddLaunchEvent(kQuery, "primary");
277  for (size_t i = 0; i < kMaxSecondary; ++i) {
278    std::string result_id = base::StringPrintf("%d", static_cast<int>(i));
279    history()->AddLaunchEvent(kQuery, result_id);
280  }
281
282  EXPECT_EQ(kMaxSecondary + 1, GetKnownResults(kQuery));
283  EXPECT_EQ(PERFECT_SECONDARY, GetResultType("0"));
284  EXPECT_EQ(PERFECT_SECONDARY, GetResultType("1"));
285
286  // Touches the oldest secondary.
287  history()->AddLaunchEvent(kQuery, "0");
288
289  // Adds one more.
290  history()->AddLaunchEvent(kQuery, "extra");
291
292  // Total number of results is capped.
293  EXPECT_EQ(kMaxSecondary + 1, GetKnownResults(kQuery));
294
295  // The oldest secondary is gone.
296  EXPECT_EQ(UNKNOWN_RESULT, GetResultType("1"));
297
298  // Touched oldest survived.
299  EXPECT_EQ(PERFECT_SECONDARY, GetResultType("0"));
300}
301
302}  // namespace test
303}  // namespace app_list
304