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/metrics/field_trial.h"
10 #include "base/strings/string16.h"
11 #include "base/strings/stringprintf.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "base/test/mock_entropy_provider.h"
14 #include "testing/gtest/include/gtest/gtest.h"
15 #include "ui/app_list/app_list_model.h"
16 #include "ui/app_list/search/history_types.h"
17 #include "ui/app_list/search/mixer.h"
18 #include "ui/app_list/search_provider.h"
19 #include "ui/app_list/search_result.h"
24 // Maximum number of results to show in each mixer group.
25 const size_t kMaxAppsGroupResults
= 4;
26 // Ignored unless AppListMixer field trial is "Blended".
27 const size_t kMaxOmniboxResults
= 4;
28 const size_t kMaxWebstoreResults
= 2;
29 const size_t kMaxPeopleResults
= 2;
31 class TestSearchResult
: public SearchResult
{
33 TestSearchResult(const std::string
& id
, double relevance
)
34 : instance_id_(instantiation_count
++) {
36 set_title(base::UTF8ToUTF16(id
));
37 set_relevance(relevance
);
39 ~TestSearchResult() override
{}
41 using SearchResult::set_voice_result
;
43 // SearchResult overrides:
44 void Open(int event_flags
) override
{}
45 void InvokeAction(int action_index
, int event_flags
) override
{}
46 scoped_ptr
<SearchResult
> Duplicate() const override
{
47 return make_scoped_ptr(new TestSearchResult(id(), relevance()));
50 // For reference equality testing. (Addresses cannot be used to test reference
51 // equality because it is possible that an object will be allocated at the
52 // same address as a previously deleted one.)
53 static int GetInstanceId(SearchResult
* result
) {
54 return static_cast<const TestSearchResult
*>(result
)->instance_id_
;
58 static int instantiation_count
;
62 DISALLOW_COPY_AND_ASSIGN(TestSearchResult
);
64 int TestSearchResult::instantiation_count
= 0;
66 class TestSearchProvider
: public SearchProvider
{
68 explicit TestSearchProvider(const std::string
& prefix
)
69 : prefix_(prefix
), count_(0), bad_relevance_range_(false) {}
70 ~TestSearchProvider() override
{}
72 // SearchProvider overrides:
73 void Start(bool is_voice_query
, const base::string16
& query
) override
{
75 for (size_t i
= 0; i
< count_
; ++i
) {
76 const std::string id
=
77 base::StringPrintf("%s%d", prefix_
.c_str(), static_cast<int>(i
));
78 double relevance
= 1.0 - i
/ 10.0;
79 // If bad_relevance_range_, change the relevances to give results outside
80 // of the canonical [0.0, 1.0] range.
81 if (bad_relevance_range_
)
82 relevance
= 10.0 - i
* 10;
83 TestSearchResult
* result
= new TestSearchResult(id
, relevance
);
84 if (voice_result_indices
.find(i
) != voice_result_indices
.end())
85 result
->set_voice_result(true);
86 Add(scoped_ptr
<SearchResult
>(result
).Pass());
89 void Stop() override
{}
91 void set_prefix(const std::string
& prefix
) { prefix_
= prefix
; }
92 void set_count(size_t count
) { count_
= count
; }
93 void set_as_voice_result(size_t index
) { voice_result_indices
.insert(index
); }
94 void set_bad_relevance_range() { bad_relevance_range_
= true; }
99 bool bad_relevance_range_
;
100 // Indices of results that will have the |voice_result| flag set.
101 std::set
<size_t> voice_result_indices
;
103 DISALLOW_COPY_AND_ASSIGN(TestSearchProvider
);
106 // Test is parameterized with bool. True enables the "Blended" field trial.
107 class MixerTest
: public testing::Test
,
108 public testing::WithParamInterface
<bool> {
111 : is_voice_query_(false),
112 field_trial_list_(new base::MockEntropyProvider()) {}
113 ~MixerTest() override
{}
115 // testing::Test overrides:
116 void SetUp() override
{
117 // If the parameter is true, enable the field trial.
118 const char* field_trial_name
= GetParam() ? "Blended" : "default";
119 base::FieldTrialList::CreateFieldTrial("AppListMixer", field_trial_name
);
121 results_
.reset(new AppListModel::SearchResults
);
123 providers_
.push_back(new TestSearchProvider("app"));
124 providers_
.push_back(new TestSearchProvider("omnibox"));
125 providers_
.push_back(new TestSearchProvider("webstore"));
126 providers_
.push_back(new TestSearchProvider("people"));
128 is_voice_query_
= false;
130 mixer_
.reset(new Mixer(results_
.get()));
132 size_t apps_group_id
= mixer_
->AddGroup(kMaxAppsGroupResults
, 3.0, 1.0);
133 size_t omnibox_group_id
=
134 mixer_
->AddOmniboxGroup(kMaxOmniboxResults
, 2.0, 1.0);
135 size_t webstore_group_id
= mixer_
->AddGroup(kMaxWebstoreResults
, 1.0, 0.5);
136 size_t people_group_id
= mixer_
->AddGroup(kMaxPeopleResults
, 0.0, 1.0);
138 mixer_
->AddProviderToGroup(apps_group_id
, providers_
[0]);
139 mixer_
->AddProviderToGroup(omnibox_group_id
, providers_
[1]);
140 mixer_
->AddProviderToGroup(webstore_group_id
, providers_
[2]);
141 mixer_
->AddProviderToGroup(people_group_id
, providers_
[3]);
145 const base::string16 query
;
147 for (size_t i
= 0; i
< providers_
.size(); ++i
) {
148 providers_
[i
]->Start(is_voice_query_
, query
);
149 providers_
[i
]->Stop();
152 mixer_
->MixAndPublish(is_voice_query_
, known_results_
);
155 std::string
GetResults() const {
157 for (size_t i
= 0; i
< results_
->item_count(); ++i
) {
161 result
+= base::UTF16ToUTF8(results_
->GetItemAt(i
)->title());
167 Mixer
* mixer() { return mixer_
.get(); }
168 TestSearchProvider
* app_provider() { return providers_
[0]; }
169 TestSearchProvider
* omnibox_provider() { return providers_
[1]; }
170 TestSearchProvider
* webstore_provider() { return providers_
[2]; }
171 TestSearchProvider
* people_provider() { return providers_
[3]; }
173 // Sets whether test runs should be treated as a voice query.
174 void set_is_voice_query(bool is_voice_query
) {
175 is_voice_query_
= is_voice_query
;
178 void AddKnownResult(const std::string
& id
, KnownResultType type
) {
179 known_results_
[id
] = type
;
183 scoped_ptr
<Mixer
> mixer_
;
184 scoped_ptr
<AppListModel::SearchResults
> results_
;
185 KnownResults known_results_
;
187 bool is_voice_query_
;
189 ScopedVector
<TestSearchProvider
> providers_
;
191 base::FieldTrialList field_trial_list_
;
193 DISALLOW_COPY_AND_ASSIGN(MixerTest
);
196 TEST_P(MixerTest
, Basic
) {
197 // Note: Some cases in |expected_blended| have vastly more results than
198 // others, due to the "at least 6" mechanism. If it gets at least 6 results
199 // from all providers, it stops at 6. If not, it fetches potentially many more
200 // results from all providers. Not ideal, but currently by design.
202 const size_t app_results
;
203 const size_t omnibox_results
;
204 const size_t webstore_results
;
205 const size_t people_results
;
206 const char* expected_default
; // Expected results with trial off.
207 const char* expected_blended
; // Expected results with trial on.
209 {0, 0, 0, 0, "", ""},
214 "app0,app1,app2,app3",
215 "app0,app1,app2,app3,app4,app5,app6,app7,app8,app9"},
220 "webstore0,webstore1",
221 "webstore0,webstore1,webstore2,webstore3,webstore4,webstore5,webstore6,"
222 "webstore7,webstore8,webstore9"},
228 "people0,people1,people2,people3,people4,people5,people6,people7,"
234 "app0,app1,app2,app3,omnibox0,omnibox1",
235 "app0,omnibox0,app1,omnibox1,app2,omnibox2,app3,omnibox3"},
240 "app0,app1,app2,app3,omnibox0,webstore0",
241 "app0,omnibox0,app1,omnibox1,app2,omnibox2,app3,omnibox3,webstore0,"
247 "app0,app1,app2,app3,omnibox0,people0",
248 "app0,omnibox0,people0,app1,omnibox1,people1,app2,omnibox2,app3,"
254 "app0,app1,app2,app3,omnibox0,webstore0",
255 "app0,omnibox0,app1,omnibox1,app2,omnibox2,app3,omnibox3,webstore0,"
261 "omnibox0,omnibox1,omnibox2,omnibox3,omnibox4,omnibox5",
262 "omnibox0,omnibox1,omnibox2,omnibox3,omnibox4,omnibox5,omnibox6,"
263 "omnibox7,omnibox8,omnibox9"},
268 "omnibox0,omnibox1,omnibox2,omnibox3,omnibox4,webstore0",
269 "omnibox0,omnibox1,omnibox2,omnibox3,webstore0,omnibox4,omnibox5,"
270 "omnibox6,omnibox7,omnibox8,omnibox9"},
275 "omnibox0,omnibox1,omnibox2,omnibox3,webstore0,webstore1",
276 "omnibox0,omnibox1,omnibox2,omnibox3,webstore0,webstore1"},
281 "app0,omnibox0,omnibox1,omnibox2,omnibox3,omnibox4",
282 "app0,omnibox0,omnibox1,omnibox2,omnibox3,omnibox4,omnibox5,omnibox6,"
283 "omnibox7,omnibox8,omnibox9"},
288 "app0,app1,omnibox0,omnibox1,omnibox2,omnibox3",
289 "app0,omnibox0,app1,omnibox1,omnibox2,omnibox3"},
294 "app0,app1,omnibox0,omnibox1,omnibox2,webstore0",
295 "app0,omnibox0,app1,omnibox1,omnibox2,omnibox3,webstore0"},
300 "app0,app1,omnibox0,omnibox1,webstore0,webstore1",
301 "app0,omnibox0,app1,omnibox1,omnibox2,omnibox3,webstore0,webstore1"},
306 "app0,app1,webstore0,webstore1",
307 "app0,app1,webstore0,webstore1"},
312 "app0,app1,app2,app3,webstore0,webstore1",
313 "app0,people0,app1,people1,app2,app3,webstore0,webstore1"},
318 "app0,app1,app2,app3,omnibox0,webstore0",
319 "app0,omnibox0,people0,app1,omnibox1,people1,app2,omnibox2,app3,"
320 "omnibox3,webstore0,webstore1"},
321 {0, 0, 0, 0, "", ""},
324 for (size_t i
= 0; i
< arraysize(kTestCases
); ++i
) {
325 app_provider()->set_count(kTestCases
[i
].app_results
);
326 omnibox_provider()->set_count(kTestCases
[i
].omnibox_results
);
327 webstore_provider()->set_count(kTestCases
[i
].webstore_results
);
328 people_provider()->set_count(kTestCases
[i
].people_results
);
331 const char* expected
= GetParam() ? kTestCases
[i
].expected_blended
332 : kTestCases
[i
].expected_default
;
333 EXPECT_EQ(expected
, GetResults()) << "Case " << i
;
337 TEST_P(MixerTest
, RemoveDuplicates
) {
338 const std::string dup
= "dup";
340 // This gives "dup0,dup1,dup2".
341 app_provider()->set_prefix(dup
);
342 app_provider()->set_count(3);
344 // This gives "dup0,dup1".
345 omnibox_provider()->set_prefix(dup
);
346 omnibox_provider()->set_count(2);
348 // This gives "dup0".
349 webstore_provider()->set_prefix(dup
);
350 webstore_provider()->set_count(1);
354 // Only three results with unique id are kept.
355 EXPECT_EQ("dup0,dup1,dup2", GetResults());
358 // Tests that "known results" have priority over others.
359 TEST_P(MixerTest
, KnownResultsPriority
) {
360 // This gives omnibox 0 -- 5.
361 omnibox_provider()->set_count(6);
363 // omnibox 1 -- 4 are "known results".
364 AddKnownResult("omnibox1", PREFIX_SECONDARY
);
365 AddKnownResult("omnibox2", PERFECT_SECONDARY
);
366 AddKnownResult("omnibox3", PREFIX_PRIMARY
);
367 AddKnownResult("omnibox4", PERFECT_PRIMARY
);
371 // omnibox 1 -- 4 should be prioritised over the others. They should be
372 // ordered 4, 3, 2, 1 (in order of match quality).
373 EXPECT_EQ("omnibox4,omnibox3,omnibox2,omnibox1,omnibox0,omnibox5",
377 TEST_P(MixerTest
, VoiceQuery
) {
378 omnibox_provider()->set_count(3);
380 EXPECT_EQ("omnibox0,omnibox1,omnibox2", GetResults());
382 // Set "omnibox1" as a voice result. Do not expect any changes (as this is not
384 omnibox_provider()->set_as_voice_result(1);
386 EXPECT_EQ("omnibox0,omnibox1,omnibox2", GetResults());
388 // Perform a voice query. Expect voice result first.
389 set_is_voice_query(true);
391 EXPECT_EQ("omnibox1,omnibox0,omnibox2", GetResults());
393 // All voice results should appear before non-voice results.
394 omnibox_provider()->set_as_voice_result(2);
396 EXPECT_EQ("omnibox1,omnibox2,omnibox0", GetResults());
399 TEST_P(MixerTest
, BadRelevanceRange
) {
400 // This gives relevance scores: (10.0, 0.0). Even though providers are
401 // supposed to give scores within the range [0.0, 1.0], we cannot rely on
402 // providers to do this, since they retrieve results from disparate and
403 // unreliable sources (like the Google+ API).
404 people_provider()->set_bad_relevance_range();
405 people_provider()->set_count(2);
407 // Give a massive boost to the second result.
408 AddKnownResult("people1", PERFECT_PRIMARY
);
412 // If the results are correctly clamped to the range [0.0, 1.0], the boost to
413 // "people1" will push it over the first result. If not, the massive base
414 // score of "people0" will erroneously keep it on top.
415 EXPECT_EQ("people1,people0", GetResults());
418 TEST_P(MixerTest
, Publish
) {
419 scoped_ptr
<SearchResult
> result1(new TestSearchResult("app1", 0));
420 scoped_ptr
<SearchResult
> result2(new TestSearchResult("app2", 0));
421 scoped_ptr
<SearchResult
> result3(new TestSearchResult("app3", 0));
422 scoped_ptr
<SearchResult
> result3_copy
= result3
->Duplicate();
423 scoped_ptr
<SearchResult
> result4(new TestSearchResult("app4", 0));
424 scoped_ptr
<SearchResult
> result5(new TestSearchResult("app5", 0));
426 AppListModel::SearchResults ui_results
;
428 // Publish the first three results to |ui_results|.
429 Mixer::SortedResults new_results
;
430 new_results
.push_back(Mixer::SortData(result1
.get(), 1.0f
));
431 new_results
.push_back(Mixer::SortData(result2
.get(), 1.0f
));
432 new_results
.push_back(Mixer::SortData(result3
.get(), 1.0f
));
434 Mixer::Publish(new_results
, &ui_results
);
435 EXPECT_EQ(3u, ui_results
.item_count());
436 // The objects in |ui_results| should be new copies because the input results
437 // are owned and |ui_results| needs to own its results as well.
438 EXPECT_NE(TestSearchResult::GetInstanceId(new_results
[0].result
),
439 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(0)));
440 EXPECT_NE(TestSearchResult::GetInstanceId(new_results
[1].result
),
441 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(1)));
442 EXPECT_NE(TestSearchResult::GetInstanceId(new_results
[2].result
),
443 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(2)));
445 // Save the current |ui_results| instance ids for comparison later.
446 std::vector
<int> old_ui_result_ids
;
447 for (size_t i
= 0; i
< ui_results
.item_count(); ++i
) {
448 old_ui_result_ids
.push_back(
449 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(i
)));
452 // Change the first result to a totally new object (with a new ID).
453 new_results
[0] = Mixer::SortData(result4
.get(), 1.0f
);
455 // Change the second result's title, but keep the same id. (The result will
456 // keep the id "app2" but change its title to "New App 2 Title".)
457 const base::string16 kNewAppTitle
= base::UTF8ToUTF16("New App 2 Title");
458 new_results
[1].result
->set_title(kNewAppTitle
);
460 // Change the third result's object address (it points to an object with the
462 new_results
[2] = Mixer::SortData(result3_copy
.get(), 1.0f
);
464 Mixer::Publish(new_results
, &ui_results
);
465 EXPECT_EQ(3u, ui_results
.item_count());
467 // The first result will be a new object, as the ID has changed.
468 EXPECT_NE(old_ui_result_ids
[0],
469 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(0)));
471 // The second result will still use the original object, but have a different
472 // title, since the ID did not change.
473 EXPECT_EQ(old_ui_result_ids
[1],
474 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(1)));
475 EXPECT_EQ(kNewAppTitle
, ui_results
.GetItemAt(1)->title());
477 // The third result will use the original object as the ID did not change.
478 EXPECT_EQ(old_ui_result_ids
[2],
479 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(2)));
481 // Save the current |ui_results| order which should is app4, app2, app3.
482 old_ui_result_ids
.clear();
483 for (size_t i
= 0; i
< ui_results
.item_count(); ++i
) {
484 old_ui_result_ids
.push_back(
485 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(i
)));
488 // Reorder the existing results and add a new one in the second place.
489 new_results
[0] = Mixer::SortData(result2
.get(), 1.0f
);
490 new_results
[1] = Mixer::SortData(result5
.get(), 1.0f
);
491 new_results
[2] = Mixer::SortData(result3
.get(), 1.0f
);
492 new_results
.push_back(Mixer::SortData(result4
.get(), 1.0f
));
494 Mixer::Publish(new_results
, &ui_results
);
495 EXPECT_EQ(4u, ui_results
.item_count());
497 // The reordered results should use the original objects.
498 EXPECT_EQ(old_ui_result_ids
[0],
499 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(3)));
500 EXPECT_EQ(old_ui_result_ids
[1],
501 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(0)));
502 EXPECT_EQ(old_ui_result_ids
[2],
503 TestSearchResult::GetInstanceId(ui_results
.GetItemAt(2)));
506 INSTANTIATE_TEST_CASE_P(MixerTestInstance
, MixerTest
, testing::Bool());
509 } // namespace app_list