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.
8 #include "base/memory/scoped_vector.h"
9 #include "base/strings/string16.h"
10 #include "base/strings/stringprintf.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "testing/gtest/include/gtest/gtest.h"
13 #include "ui/app_list/app_list_model.h"
14 #include "ui/app_list/search/history_types.h"
15 #include "ui/app_list/search/mixer.h"
16 #include "ui/app_list/search_provider.h"
17 #include "ui/app_list/search_result.h"
22 // Maximum number of results to show in each mixer group.
23 const size_t kMaxAppsGroupResults
= 4;
24 const size_t kMaxOmniboxResults
= 0; // Unlimited.
25 const size_t kMaxWebstoreResults
= 2;
26 const size_t kMaxPeopleResults
= 2;
28 class TestSearchResult
: public SearchResult
{
30 TestSearchResult(const std::string
& id
, double relevance
)
31 : instance_id_(instantiation_count
++) {
33 set_title(base::UTF8ToUTF16(id
));
34 set_relevance(relevance
);
36 ~TestSearchResult() override
{}
38 using SearchResult::set_voice_result
;
40 // SearchResult overrides:
41 void Open(int event_flags
) override
{}
42 void InvokeAction(int action_index
, int event_flags
) override
{}
43 scoped_ptr
<SearchResult
> Duplicate() const override
{
44 return make_scoped_ptr(new TestSearchResult(id(), relevance()));
47 // For reference equality testing. (Addresses cannot be used to test reference
48 // equality because it is possible that an object will be allocated at the
49 // same address as a previously deleted one.)
50 static int GetInstanceId(SearchResult
* result
) {
51 return static_cast<const TestSearchResult
*>(result
)->instance_id_
;
55 static int instantiation_count
;
59 DISALLOW_COPY_AND_ASSIGN(TestSearchResult
);
61 int TestSearchResult::instantiation_count
= 0;
63 class TestSearchProvider
: public SearchProvider
{
65 explicit TestSearchProvider(const std::string
& prefix
)
66 : prefix_(prefix
), count_(0), bad_relevance_range_(false) {}
67 ~TestSearchProvider() override
{}
69 // SearchProvider overrides:
70 void Start(bool is_voice_query
, const base::string16
& query
) override
{
72 for (size_t i
= 0; i
< count_
; ++i
) {
73 const std::string id
=
74 base::StringPrintf("%s%d", prefix_
.c_str(), static_cast<int>(i
));
75 double relevance
= 1.0 - i
/ 10.0;
76 // If bad_relevance_range_, change the relevances to give results outside
77 // of the canonical [0.0, 1.0] range.
78 if (bad_relevance_range_
)
79 relevance
= 10.0 - i
* 10;
80 TestSearchResult
* result
= new TestSearchResult(id
, relevance
);
81 if (voice_result_indices
.find(i
) != voice_result_indices
.end())
82 result
->set_voice_result(true);
83 Add(scoped_ptr
<SearchResult
>(result
).Pass());
86 void Stop() override
{}
88 void set_prefix(const std::string
& prefix
) { prefix_
= prefix
; }
89 void set_count(size_t count
) { count_
= count
; }
90 void set_as_voice_result(size_t index
) { voice_result_indices
.insert(index
); }
91 void set_bad_relevance_range() { bad_relevance_range_
= true; }
96 bool bad_relevance_range_
;
97 // Indices of results that will have the |voice_result| flag set.
98 std::set
<size_t> voice_result_indices
;
100 DISALLOW_COPY_AND_ASSIGN(TestSearchProvider
);
103 class MixerTest
: public testing::Test
{
105 MixerTest() : is_voice_query_(false) {}
106 ~MixerTest() override
{}
108 // testing::Test overrides:
109 void SetUp() override
{
110 results_
.reset(new AppListModel::SearchResults
);
112 providers_
.push_back(new TestSearchProvider("app"));
113 providers_
.push_back(new TestSearchProvider("omnibox"));
114 providers_
.push_back(new TestSearchProvider("webstore"));
115 providers_
.push_back(new TestSearchProvider("people"));
117 is_voice_query_
= false;
119 mixer_
.reset(new Mixer(results_
.get()));
121 size_t apps_group_id
= mixer_
->AddGroup(kMaxAppsGroupResults
, 3.0);
122 size_t omnibox_group_id
= mixer_
->AddOmniboxGroup(kMaxOmniboxResults
, 2.0);
123 size_t webstore_group_id
= mixer_
->AddGroup(kMaxWebstoreResults
, 1.0);
124 size_t people_group_id
= mixer_
->AddGroup(kMaxPeopleResults
, 0.0);
126 mixer_
->AddProviderToGroup(apps_group_id
, providers_
[0]);
127 mixer_
->AddProviderToGroup(omnibox_group_id
, providers_
[1]);
128 mixer_
->AddProviderToGroup(webstore_group_id
, providers_
[2]);
129 mixer_
->AddProviderToGroup(people_group_id
, providers_
[3]);
133 const base::string16 query
;
135 for (size_t i
= 0; i
< providers_
.size(); ++i
) {
136 providers_
[i
]->Start(is_voice_query_
, query
);
137 providers_
[i
]->Stop();
140 mixer_
->MixAndPublish(is_voice_query_
, known_results_
);
143 std::string
GetResults() const {
145 for (size_t i
= 0; i
< results_
->item_count(); ++i
) {
149 result
+= base::UTF16ToUTF8(results_
->GetItemAt(i
)->title());
155 Mixer
* mixer() { return mixer_
.get(); }
156 TestSearchProvider
* app_provider() { return providers_
[0]; }
157 TestSearchProvider
* omnibox_provider() { return providers_
[1]; }
158 TestSearchProvider
* webstore_provider() { return providers_
[2]; }
159 TestSearchProvider
* people_provider() { return providers_
[3]; }
161 // Sets whether test runs should be treated as a voice query.
162 void set_is_voice_query(bool is_voice_query
) {
163 is_voice_query_
= is_voice_query
;
166 void AddKnownResult(const std::string
& id
, KnownResultType type
) {
167 known_results_
[id
] = type
;
171 scoped_ptr
<Mixer
> mixer_
;
172 scoped_ptr
<AppListModel::SearchResults
> results_
;
173 KnownResults known_results_
;
175 bool is_voice_query_
;
177 ScopedVector
<TestSearchProvider
> providers_
;
179 DISALLOW_COPY_AND_ASSIGN(MixerTest
);
182 TEST_F(MixerTest
, Basic
) {
184 const size_t app_results
;
185 const size_t omnibox_results
;
186 const size_t webstore_results
;
187 const size_t people_results
;
188 const char* expected
;
191 {10, 0, 0, 0, "app0,app1,app2,app3"},
192 {0, 0, 10, 0, "webstore0,webstore1"},
193 {0, 0, 0, 10, "people0,people1"},
194 {4, 6, 0, 0, "app0,app1,app2,app3,omnibox0,omnibox1"},
195 {4, 6, 2, 0, "app0,app1,app2,app3,omnibox0,webstore0"},
196 {4, 6, 0, 2, "app0,app1,app2,app3,omnibox0,people0"},
197 {10, 10, 10, 0, "app0,app1,app2,app3,omnibox0,webstore0"},
198 {0, 10, 0, 0, "omnibox0,omnibox1,omnibox2,omnibox3,omnibox4,omnibox5"},
199 {0, 10, 1, 0, "omnibox0,omnibox1,omnibox2,omnibox3,omnibox4,webstore0"},
200 {0, 10, 2, 0, "omnibox0,omnibox1,omnibox2,omnibox3,webstore0,webstore1"},
201 {1, 10, 0, 0, "app0,omnibox0,omnibox1,omnibox2,omnibox3,omnibox4"},
202 {2, 10, 0, 0, "app0,app1,omnibox0,omnibox1,omnibox2,omnibox3"},
203 {2, 10, 1, 0, "app0,app1,omnibox0,omnibox1,omnibox2,webstore0"},
204 {2, 10, 2, 0, "app0,app1,omnibox0,omnibox1,webstore0,webstore1"},
205 {2, 0, 2, 0, "app0,app1,webstore0,webstore1"},
206 {10, 0, 10, 10, "app0,app1,app2,app3,webstore0,webstore1"},
207 {10, 10, 10, 10, "app0,app1,app2,app3,omnibox0,webstore0"},
211 for (size_t i
= 0; i
< arraysize(kTestCases
); ++i
) {
212 app_provider()->set_count(kTestCases
[i
].app_results
);
213 omnibox_provider()->set_count(kTestCases
[i
].omnibox_results
);
214 webstore_provider()->set_count(kTestCases
[i
].webstore_results
);
215 people_provider()->set_count(kTestCases
[i
].people_results
);
218 EXPECT_EQ(kTestCases
[i
].expected
, GetResults()) << "Case " << i
;
222 TEST_F(MixerTest
, RemoveDuplicates
) {
223 const std::string dup
= "dup";
225 // This gives "dup0,dup1,dup2".
226 app_provider()->set_prefix(dup
);
227 app_provider()->set_count(3);
229 // This gives "dup0,dup1".
230 omnibox_provider()->set_prefix(dup
);
231 omnibox_provider()->set_count(2);
233 // This gives "dup0".
234 webstore_provider()->set_prefix(dup
);
235 webstore_provider()->set_count(1);
239 // Only three results with unique id are kept.
240 EXPECT_EQ("dup0,dup1,dup2", GetResults());
243 // Tests that "known results" have priority over others.
244 TEST_F(MixerTest
, KnownResultsPriority
) {
245 // This gives omnibox 0 -- 5.
246 omnibox_provider()->set_count(6);
248 // omnibox 1 -- 4 are "known results".
249 AddKnownResult("omnibox1", PREFIX_SECONDARY
);
250 AddKnownResult("omnibox2", PERFECT_SECONDARY
);
251 AddKnownResult("omnibox3", PREFIX_PRIMARY
);
252 AddKnownResult("omnibox4", PERFECT_PRIMARY
);
256 // omnibox 1 -- 4 should be prioritised over the others. They should be
257 // ordered 4, 3, 2, 1 (in order of match quality).
258 EXPECT_EQ("omnibox4,omnibox3,omnibox2,omnibox1,omnibox0,omnibox5",
262 TEST_F(MixerTest
, VoiceQuery
) {
263 omnibox_provider()->set_count(3);
265 EXPECT_EQ("omnibox0,omnibox1,omnibox2", GetResults());
267 // Set "omnibox1" as a voice result. Do not expect any changes (as this is not
269 omnibox_provider()->set_as_voice_result(1);
271 EXPECT_EQ("omnibox0,omnibox1,omnibox2", GetResults());
273 // Perform a voice query. Expect voice result first.
274 set_is_voice_query(true);
276 EXPECT_EQ("omnibox1,omnibox0,omnibox2", GetResults());
278 // All voice results should appear before non-voice results.
279 omnibox_provider()->set_as_voice_result(2);
281 EXPECT_EQ("omnibox1,omnibox2,omnibox0", GetResults());
284 TEST_F(MixerTest
, BadRelevanceRange
) {
285 // This gives relevance scores: (10.0, 0.0). Even though providers are
286 // supposed to give scores within the range [0.0, 1.0], we cannot rely on
287 // providers to do this, since they retrieve results from disparate and
288 // unreliable sources (like the Google+ API).
289 people_provider()->set_bad_relevance_range();
290 people_provider()->set_count(2);
292 // Give a massive boost to the second result.
293 AddKnownResult("people1", PERFECT_PRIMARY
);
297 // If the results are correctly clamped to the range [0.0, 1.0], the boost to
298 // "people1" will push it over the first result. If not, the massive base
299 // score of "people0" will erroneously keep it on top.
300 EXPECT_EQ("people1,people0", GetResults());
303 TEST_F(MixerTest
, Publish
) {
304 scoped_ptr
<SearchResult
> result1(new TestSearchResult("app1", 0));
305 scoped_ptr
<SearchResult
> result2(new TestSearchResult("app2", 0));
306 scoped_ptr
<SearchResult
> result3(new TestSearchResult("app3", 0));
307 scoped_ptr
<SearchResult
> result3_copy
= result3
->Duplicate();
308 scoped_ptr
<SearchResult
> result4(new TestSearchResult("app4", 0));
309 scoped_ptr
<SearchResult
> result5(new TestSearchResult("app5", 0));
311 AppListModel::SearchResults ui_results
;
313 // Publish the first three results to |ui_results|.
314 Mixer::SortedResults new_results
;
315 new_results
.push_back(Mixer::SortData(result1
.get(), 1.0f
));
316 new_results
.push_back(Mixer::SortData(result2
.get(), 1.0f
));
317 new_results
.push_back(Mixer::SortData(result3
.get(), 1.0f
));
319 Mixer::Publish(new_results
, &ui_results
);
320 EXPECT_EQ(3u, ui_results
.item_count());
321 // The objects in |ui_results| should be new copies because the input results
322 // are owned and |ui_results| needs to own its results as well.
323 EXPECT_NE(TestSearchResult::GetInstanceId(new_results
[0].result
),
324 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(0)));
325 EXPECT_NE(TestSearchResult::GetInstanceId(new_results
[1].result
),
326 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(1)));
327 EXPECT_NE(TestSearchResult::GetInstanceId(new_results
[2].result
),
328 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(2)));
330 // Save the current |ui_results| instance ids for comparison later.
331 std::vector
<int> old_ui_result_ids
;
332 for (size_t i
= 0; i
< ui_results
.item_count(); ++i
) {
333 old_ui_result_ids
.push_back(
334 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(i
)));
337 // Change the first result to a totally new object (with a new ID).
338 new_results
[0] = Mixer::SortData(result4
.get(), 1.0f
);
340 // Change the second result's title, but keep the same id. (The result will
341 // keep the id "app2" but change its title to "New App 2 Title".)
342 const base::string16 kNewAppTitle
= base::UTF8ToUTF16("New App 2 Title");
343 new_results
[1].result
->set_title(kNewAppTitle
);
345 // Change the third result's object address (it points to an object with the
347 new_results
[2] = Mixer::SortData(result3_copy
.get(), 1.0f
);
349 Mixer::Publish(new_results
, &ui_results
);
350 EXPECT_EQ(3u, ui_results
.item_count());
352 // The first result will be a new object, as the ID has changed.
353 EXPECT_NE(old_ui_result_ids
[0],
354 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(0)));
356 // The second result will still use the original object, but have a different
357 // title, since the ID did not change.
358 EXPECT_EQ(old_ui_result_ids
[1],
359 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(1)));
360 EXPECT_EQ(kNewAppTitle
, ui_results
.GetItemAt(1)->title());
362 // The third result will use the original object as the ID did not change.
363 EXPECT_EQ(old_ui_result_ids
[2],
364 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(2)));
366 // Save the current |ui_results| order which should is app4, app2, app3.
367 old_ui_result_ids
.clear();
368 for (size_t i
= 0; i
< ui_results
.item_count(); ++i
) {
369 old_ui_result_ids
.push_back(
370 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(i
)));
373 // Reorder the existing results and add a new one in the second place.
374 new_results
[0] = Mixer::SortData(result2
.get(), 1.0f
);
375 new_results
[1] = Mixer::SortData(result5
.get(), 1.0f
);
376 new_results
[2] = Mixer::SortData(result3
.get(), 1.0f
);
377 new_results
.push_back(Mixer::SortData(result4
.get(), 1.0f
));
379 Mixer::Publish(new_results
, &ui_results
);
380 EXPECT_EQ(4u, ui_results
.item_count());
382 // The reordered results should use the original objects.
383 EXPECT_EQ(old_ui_result_ids
[0],
384 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(3)));
385 EXPECT_EQ(old_ui_result_ids
[1],
386 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(0)));
387 EXPECT_EQ(old_ui_result_ids
[2],
388 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(2)));
392 } // namespace app_list