• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.quicksearchbox;
17 
18 import com.android.quicksearchbox.util.MockExecutor;
19 import com.android.quicksearchbox.util.Util;
20 
21 import org.json.JSONArray;
22 
23 import android.app.SearchManager;
24 import android.content.Intent;
25 import android.test.AndroidTestCase;
26 import android.test.MoreAsserts;
27 import android.test.suitebuilder.annotation.MediumTest;
28 import android.util.Log;
29 
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Collection;
33 import java.util.Collections;
34 import java.util.Comparator;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Map.Entry;
38 
39 /**
40  * Abstract base class for tests of  {@link ShortcutRepository}
41  * implementations.  Most importantly, verifies the
42  * stuff we are doing with sqlite works how we expect it to.
43  *
44  * Attempts to test logic independent of the (sql) details of the implementation, so these should
45  * be useful even in the face of a schema change.
46  */
47 @MediumTest
48 public class ShortcutRepositoryTest extends AndroidTestCase {
49 
50     private static final String TAG = "ShortcutRepositoryTest";
51 
52     static final long NOW = 1239841162000L; // millis since epoch. some time in 2009
53 
54     static final Source APP_SOURCE = new MockSource("com.example.app/.App");
55 
56     static final Source APP_SOURCE_V2 = new MockSource("com.example.app/.App", 2);
57 
58     static final Source CONTACTS_SOURCE = new MockSource("com.android.contacts/.Contacts");
59 
60     static final Source BOOKMARKS_SOURCE = new MockSource("com.android.browser/.Bookmarks");
61 
62     static final Source HISTORY_SOURCE = new MockSource("com.android.browser/.History");
63 
64     static final Source MUSIC_SOURCE = new MockSource("com.android.music/.Music");
65 
66     static final Source MARKET_SOURCE = new MockSource("com.android.vending/.Market");
67 
68     static final Corpus APP_CORPUS = new MockCorpus(APP_SOURCE);
69 
70     static final Corpus CONTACTS_CORPUS = new MockCorpus(CONTACTS_SOURCE);
71 
72     static final Corpus WEB_CORPUS = new MockCorpus(MockSource.WEB_SOURCE);
73 
74     static final int MAX_SHORTCUTS = 8;
75 
76     protected Config mConfig;
77     protected MockCorpora mCorpora;
78     protected MockExecutor mLogExecutor;
79     protected ShortcutRefresher mRefresher;
80 
81     protected List<Corpus> mAllowedCorpora;
82 
83     protected ShortcutRepositoryImplLog mRepo;
84 
85     protected ListSuggestionCursor mAppSuggestions;
86     protected ListSuggestionCursor mContactSuggestions;
87 
88     protected SuggestionData mApp1;
89     protected SuggestionData mApp2;
90     protected SuggestionData mApp3;
91 
92     protected SuggestionData mContact1;
93     protected SuggestionData mContact2;
94 
95     protected SuggestionData mWeb1;
96 
createShortcutRepository()97     protected ShortcutRepositoryImplLog createShortcutRepository() {
98         return new ShortcutRepositoryImplLog(getContext(), mConfig, mCorpora,
99                 mRefresher, new MockHandler(), mLogExecutor,
100                 "test-shortcuts-log.db");
101     }
102 
103     @Override
setUp()104     protected void setUp() throws Exception {
105         super.setUp();
106 
107         mConfig = new Config(getContext());
108         mCorpora = new MockCorpora();
109         mCorpora.addCorpus(APP_CORPUS);
110         mCorpora.addCorpus(CONTACTS_CORPUS);
111         mCorpora.addCorpus(WEB_CORPUS);
112         mRefresher = new MockShortcutRefresher();
113         mLogExecutor = new MockExecutor();
114         mRepo = createShortcutRepository();
115 
116         mAllowedCorpora = new ArrayList<Corpus>(mCorpora.getAllCorpora());
117 
118         mApp1 = makeApp("app1");
119         mApp2 = makeApp("app2");
120         mApp3 = makeApp("app3");
121         mAppSuggestions = new ListSuggestionCursor("foo", mApp1, mApp2, mApp3);
122 
123         mContact1 = new SuggestionData(CONTACTS_SOURCE)
124                 .setText1("Joe Blow")
125                 .setIntentAction("view")
126                 .setIntentData("contacts/joeblow")
127                 .setShortcutId("j-blow");
128         mContact2 = new SuggestionData(CONTACTS_SOURCE)
129                 .setText1("Mike Johnston")
130                 .setIntentAction("view")
131                 .setIntentData("contacts/mikeJ")
132                 .setShortcutId("mo-jo");
133 
134         mWeb1 = new SuggestionData(MockSource.WEB_SOURCE)
135                 .setText1("foo")
136                 .setIntentAction(Intent.ACTION_WEB_SEARCH)
137                 .setSuggestionQuery("foo");
138 
139         mContactSuggestions = new ListSuggestionCursor("foo", mContact1, mContact2);
140     }
141 
makeApp(String name)142     private SuggestionData makeApp(String name) {
143         return new SuggestionData(APP_SOURCE)
144                 .setText1(name)
145                 .setIntentAction("view")
146                 .setIntentData("apps/" + name)
147                 .setShortcutId("shorcut_" + name);
148     }
149 
makeContact(String name)150     private SuggestionData makeContact(String name) {
151         return new SuggestionData(CONTACTS_SOURCE)
152                 .setText1(name)
153                 .setIntentAction("view")
154                 .setIntentData("contacts/" + name)
155                 .setShortcutId("shorcut_" + name);
156     }
157 
158     @Override
tearDown()159     protected void tearDown() throws Exception {
160         super.tearDown();
161         mRepo.deleteRepository();
162     }
163 
testHasHistory()164     public void testHasHistory() {
165         assertHasHistory(false);
166         reportClickAtTime(mAppSuggestions, 0, NOW);
167         assertHasHistory(true);
168         mRepo.clearHistory();
169         mLogExecutor.runNext();
170         assertHasHistory(false);
171     }
172 
testRemoveFromHistory()173     public void testRemoveFromHistory() {
174         SuggestionData john = new SuggestionData(CONTACTS_SOURCE)
175                 .setText1("john doe")
176                 .setIntentAction("view")
177                 .setIntentData("john_doe");
178         SuggestionData jane = new SuggestionData(CONTACTS_SOURCE)
179                 .setText1("jane doe")
180                 .setIntentAction("view")
181                 .setIntentData("jane_doe");
182         reportClick("j", john);
183         reportClick("j", john);
184         reportClick("j", jane);
185         assertShortcuts("j", john, jane);
186         removeFromHistory(new ListSuggestionCursor("j", jane, john), 1);
187         assertShortcuts("j", jane);
188     }
189 
testRemoveFromHistoryNonExisting()190     public void testRemoveFromHistoryNonExisting() {
191         SuggestionData john = new SuggestionData(CONTACTS_SOURCE)
192                 .setText1("john doe")
193                 .setIntentAction("view")
194                 .setIntentData("john_doe");
195         SuggestionData jane = new SuggestionData(CONTACTS_SOURCE)
196                 .setText1("jane doe")
197                 .setIntentAction("view")
198                 .setIntentData("jane_doe");
199         reportClick("j", john);
200         assertShortcuts("j", john);
201         removeFromHistory(new ListSuggestionCursor("j", jane), 0);
202         assertShortcuts("j", john);
203     }
204 
testNoMatch()205     public void testNoMatch() {
206         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
207                 .setText1("bob smith")
208                 .setIntentAction("action")
209                 .setIntentData("data");
210 
211         reportClick("bob smith", clicked);
212         assertNoShortcuts("joe");
213     }
214 
testFullPackingUnpacking()215     public void testFullPackingUnpacking() {
216         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
217                 .setFormat("<i>%s</i>")
218                 .setText1("title")
219                 .setText2("description")
220                 .setText2Url("description_url")
221                 .setIcon1("android.resource://system/drawable/foo")
222                 .setIcon2("content://test/bar")
223                 .setIntentAction("action")
224                 .setIntentData("data")
225                 .setSuggestionQuery("query")
226                 .setIntentExtraData("extradata")
227                 .setShortcutId("idofshortcut")
228                 .setSuggestionLogType("logtype");
229         reportClick("q", clicked);
230 
231         assertShortcuts("q", clicked);
232         assertShortcuts("", clicked);
233     }
234 
testSpinnerWhileRefreshing()235     public void testSpinnerWhileRefreshing() {
236         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
237                 .setText1("title")
238                 .setText2("description")
239                 .setIcon2("icon2")
240                 .setSuggestionQuery("query")
241                 .setIntentExtraData("extradata")
242                 .setShortcutId("idofshortcut")
243                 .setSpinnerWhileRefreshing(true);
244 
245         reportClick("q", clicked);
246 
247         String spinnerUri = Util.getResourceUri(mContext, R.drawable.search_spinner).toString();
248         SuggestionData expected = new SuggestionData(CONTACTS_SOURCE)
249                 .setText1("title")
250                 .setText2("description")
251                 .setIcon2(spinnerUri)
252                 .setSuggestionQuery("query")
253                 .setIntentExtraData("extradata")
254                 .setShortcutId("idofshortcut")
255                 .setSpinnerWhileRefreshing(true);
256 
257         assertShortcuts("q", expected);
258     }
259 
testPrefixesMatch()260     public void testPrefixesMatch() {
261         assertNoShortcuts("bob");
262 
263         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
264                 .setText1("bob smith the third")
265                 .setIntentAction("action")
266                 .setIntentData("intentdata");
267 
268         reportClick("bob smith", clicked);
269 
270         assertShortcuts("bob smith", clicked);
271         assertShortcuts("bob s", clicked);
272         assertShortcuts("b", clicked);
273     }
274 
testMatchesOneAndNotOthers()275     public void testMatchesOneAndNotOthers() {
276         SuggestionData bob = new SuggestionData(CONTACTS_SOURCE)
277                 .setText1("bob smith the third")
278                 .setIntentAction("action")
279                 .setIntentData("intentdata/bob");
280 
281         reportClick("bob", bob);
282 
283         SuggestionData george = new SuggestionData(CONTACTS_SOURCE)
284                 .setText1("george jones")
285                 .setIntentAction("action")
286                 .setIntentData("intentdata/george");
287         reportClick("geor", george);
288 
289         assertShortcuts("b for bob", "b", bob);
290         assertShortcuts("g for george", "g", george);
291     }
292 
testDifferentPrefixesMatchSameEntity()293     public void testDifferentPrefixesMatchSameEntity() {
294         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
295                 .setText1("bob smith the third")
296                 .setIntentAction("action")
297                 .setIntentData("intentdata");
298 
299         reportClick("bob", clicked);
300         reportClick("smith", clicked);
301         assertShortcuts("b", clicked);
302         assertShortcuts("s", clicked);
303     }
304 
testMoreClicksWins()305     public void testMoreClicksWins() {
306         reportClick("app", mApp1);
307         reportClick("app", mApp2);
308         reportClick("app", mApp1);
309 
310         assertShortcuts("expected app1 to beat app2 since it has more hits",
311                 "app", mApp1, mApp2);
312 
313         reportClick("app", mApp2);
314         reportClick("app", mApp2);
315 
316         assertShortcuts("query 'app': expecting app2 to beat app1 since it has more hits",
317                 "app", mApp2, mApp1);
318         assertShortcuts("query 'a': expecting app2 to beat app1 since it has more hits",
319                 "a", mApp2, mApp1);
320     }
321 
testMostRecentClickWins()322     public void testMostRecentClickWins() {
323         // App 1 has 3 clicks
324         reportClick("app", mApp1, NOW - 5);
325         reportClick("app", mApp1, NOW - 5);
326         reportClick("app", mApp1, NOW - 5);
327         // App 2 has 2 clicks
328         reportClick("app", mApp2, NOW - 2);
329         reportClick("app", mApp2, NOW - 2);
330         // App 3 only has 1, but it's most recent
331         reportClick("app", mApp3, NOW - 1);
332 
333         assertShortcuts("expected app3 to beat app1 and app2 because it's clicked last",
334                 "app", mApp3, mApp1, mApp2);
335 
336         reportClick("app", mApp2, NOW);
337 
338         assertShortcuts("query 'app': expecting app2 to beat app1 since it's clicked last",
339                 "app", mApp2, mApp1, mApp3);
340         assertShortcuts("query 'a': expecting app2 to beat app1 since it's clicked last",
341                 "a", mApp2, mApp1, mApp3);
342         assertShortcuts("query '': expecting app2 to beat app1 since it's clicked last",
343                 "", mApp2, mApp1, mApp3);
344     }
345 
testMostRecentClickWinsOnEmptyQuery()346     public void testMostRecentClickWinsOnEmptyQuery() {
347         reportClick("app", mApp1, NOW - 3);
348         reportClick("app", mApp1, NOW - 2);
349         reportClick("app", mApp2, NOW - 1);
350 
351         assertShortcuts("expected app2 to beat app1 since it's clicked last", "",
352                 mApp2, mApp1);
353     }
354 
testMostRecentClickWinsEvenWithMoreThanLimitShortcuts()355     public void testMostRecentClickWinsEvenWithMoreThanLimitShortcuts() {
356         for (int i = 0; i < MAX_SHORTCUTS; i++) {
357             SuggestionData app = makeApp("TestApp" + i);
358             // Each of these shortcuts has two clicks
359             reportClick("app", app, NOW - 2);
360             reportClick("app", app, NOW - 1);
361         }
362 
363         // mApp1 has only one click, but is more recent
364         reportClick("app", mApp1, NOW);
365 
366         assertShortcutAtPosition(
367             "expecting app1 to beat all others since it's clicked last",
368             "app", 0, mApp1);
369     }
370 
371     /**
372      * similar to {@link #testMoreClicksWins()} but clicks are reported with prefixes of the
373      * original query.  we want to make sure a match on query 'a' updates the stats for the
374      * entry it matched against, 'app'.
375      */
testPrefixMatchUpdatesSameEntry()376     public void testPrefixMatchUpdatesSameEntry() {
377         reportClick("app", mApp1, NOW);
378         reportClick("app", mApp2, NOW);
379         reportClick("app", mApp1, NOW);
380 
381         assertShortcuts("expected app1 to beat app2 since it has more hits",
382                 "app", mApp1, mApp2);
383     }
384 
385     private static final long DAY_MILLIS = 86400000L; // just ask the google
386     private static final long HOUR_MILLIS = 3600000L;
387 
testMoreRecentlyClickedWins()388     public void testMoreRecentlyClickedWins() {
389         reportClick("app", mApp1, NOW - DAY_MILLIS*2);
390         reportClick("app", mApp2, NOW);
391         reportClick("app", mApp3, NOW - DAY_MILLIS*4);
392 
393         assertShortcuts("expecting more recently clicked app to rank higher",
394                 "app", mApp2, mApp1, mApp3);
395     }
396 
testMoreRecentlyClickedWinsSeconds()397     public void testMoreRecentlyClickedWinsSeconds() {
398         reportClick("app", mApp1, NOW - 10000);
399         reportClick("app", mApp2, NOW - 5000);
400         reportClick("app", mApp3, NOW);
401 
402         assertShortcuts("expecting more recently clicked app to rank higher",
403                 "app", mApp3, mApp2, mApp1);
404     }
405 
testRecencyOverridesClicks()406     public void testRecencyOverridesClicks() {
407 
408         // 5 clicks, most recent half way through age limit
409         long halfWindow = mConfig.getMaxStatAgeMillis() / 2;
410         reportClick("app", mApp1, NOW - halfWindow);
411         reportClick("app", mApp1, NOW - halfWindow);
412         reportClick("app", mApp1, NOW - halfWindow);
413         reportClick("app", mApp1, NOW - halfWindow);
414         reportClick("app", mApp1, NOW - halfWindow);
415 
416         // 3 clicks, the most recent very recent
417         reportClick("app", mApp2, NOW - HOUR_MILLIS);
418         reportClick("app", mApp2, NOW - HOUR_MILLIS);
419         reportClick("app", mApp2, NOW - HOUR_MILLIS);
420 
421         assertShortcuts("expecting 3 recent clicks to beat 5 clicks long ago",
422                 "app", mApp2, mApp1);
423     }
424 
testEntryOlderThanAgeLimitFiltered()425     public void testEntryOlderThanAgeLimitFiltered() {
426         reportClick("app", mApp1);
427 
428         long pastWindow = mConfig.getMaxStatAgeMillis() + 1000;
429         reportClick("app", mApp2, NOW - pastWindow);
430 
431         assertShortcuts("expecting app2 not clicked on recently enough to be filtered",
432                 "app", mApp1);
433     }
434 
testZeroQueryResults_MoreClicksWins()435     public void testZeroQueryResults_MoreClicksWins() {
436         reportClick("app", mApp1);
437         reportClick("app", mApp1);
438         reportClick("foo", mApp2);
439 
440         assertShortcuts("", mApp1, mApp2);
441 
442         reportClick("foo", mApp2);
443         reportClick("foo", mApp2);
444 
445         assertShortcuts("", mApp2, mApp1);
446     }
447 
testZeroQueryResults_DifferentQueryhitsCreditSameShortcut()448     public void testZeroQueryResults_DifferentQueryhitsCreditSameShortcut() {
449         reportClick("app", mApp1);
450         reportClick("foo", mApp2);
451         reportClick("bar", mApp2);
452 
453         assertShortcuts("hits for 'foo' and 'bar' on app2 should have combined to rank it " +
454                 "ahead of app1, which only has one hit.",
455                 "", mApp2, mApp1);
456 
457         reportClick("z", mApp1);
458         reportClick("2", mApp1);
459 
460         assertShortcuts("", mApp1, mApp2);
461     }
462 
testZeroQueryResults_zeroQueryHitCounts()463     public void testZeroQueryResults_zeroQueryHitCounts() {
464         reportClick("app", mApp1);
465         reportClick("", mApp2);
466         reportClick("", mApp2);
467 
468         assertShortcuts("hits for '' on app2 should have combined to rank it " +
469                 "ahead of app1, which only has one hit.",
470                 "", mApp2, mApp1);
471 
472         reportClick("", mApp1);
473         reportClick("", mApp1);
474 
475         assertShortcuts("zero query hits for app1 should have made it higher than app2.",
476                 "", mApp1, mApp2);
477 
478         assertShortcuts("query for 'a' should only match app1.",
479                 "a", mApp1);
480     }
481 
testRefreshShortcut()482     public void testRefreshShortcut() {
483         final SuggestionData app1 = new SuggestionData(APP_SOURCE)
484                 .setFormat("format")
485                 .setText1("app1")
486                 .setText2("cool app")
487                 .setShortcutId("app1_id");
488 
489         reportClick("app", app1);
490 
491         final SuggestionData updated = new SuggestionData(APP_SOURCE)
492                 .setFormat("format (updated)")
493                 .setText1("app1 (updated)")
494                 .setText2("cool app")
495                 .setShortcutId("app1_id");
496 
497         refreshShortcut(APP_SOURCE, "app1_id", updated);
498 
499         assertShortcuts("expected updated properties in match",
500                 "app", updated);
501     }
502 
testRefreshShortcutChangedIntent()503     public void testRefreshShortcutChangedIntent() {
504 
505         final SuggestionData app1 = new SuggestionData(APP_SOURCE)
506                 .setIntentData("data")
507                 .setFormat("format")
508                 .setText1("app1")
509                 .setText2("cool app")
510                 .setShortcutId("app1_id");
511 
512         reportClick("app", app1);
513 
514         final SuggestionData updated = new SuggestionData(APP_SOURCE)
515                 .setIntentData("data-updated")
516                 .setFormat("format (updated)")
517                 .setText1("app1 (updated)")
518                 .setText2("cool app")
519                 .setShortcutId("app1_id");
520 
521         refreshShortcut(APP_SOURCE, "app1_id", updated);
522 
523         assertShortcuts("expected updated properties in match",
524                 "app", updated);
525     }
526 
testInvalidateShortcut()527     public void testInvalidateShortcut() {
528         final SuggestionData app1 = new SuggestionData(APP_SOURCE)
529                 .setText1("app1")
530                 .setText2("cool app")
531                 .setShortcutId("app1_id");
532 
533         reportClick("app", app1);
534 
535         invalidateShortcut(APP_SOURCE, "app1_id");
536 
537         assertNoShortcuts("should be no matches since shortcut is invalid.", "app");
538     }
539 
testInvalidateShortcut_sameIdDifferentSources()540     public void testInvalidateShortcut_sameIdDifferentSources() {
541         final String sameid = "same_id";
542         final SuggestionData app = new SuggestionData(APP_SOURCE)
543                 .setText1("app1")
544                 .setText2("cool app")
545                 .setShortcutId(sameid);
546         reportClick("app", app);
547         assertShortcuts("app should be there", "", app);
548 
549         final SuggestionData contact = new SuggestionData(CONTACTS_SOURCE)
550                 .setText1("joe blow")
551                 .setText2("a good pal")
552                 .setShortcutId(sameid);
553         reportClick("joe", contact);
554         reportClick("joe", contact);
555         assertShortcuts("app and contact should be there.", "", contact, app);
556 
557         refreshShortcut(APP_SOURCE, sameid, null);
558         assertNoShortcuts("app should not be there.", "app");
559         assertShortcuts("contact with same shortcut id should still be there.",
560                 "joe", contact);
561         assertShortcuts("contact with same shortcut id should still be there.",
562                 "", contact);
563     }
564 
testNeverMakeShortcut()565     public void testNeverMakeShortcut() {
566         final SuggestionData contact = new SuggestionData(CONTACTS_SOURCE)
567                 .setText1("unshortcuttable contact")
568                 .setText2("you didn't want to call them again anyway")
569                 .setShortcutId(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
570         reportClick("unshortcuttable", contact);
571         assertNoShortcuts("never-shortcutted suggestion should not be there.", "unshortcuttable");
572     }
573 
testCountResetAfterShortcutDeleted()574     public void testCountResetAfterShortcutDeleted() {
575         reportClick("app", mApp1);
576         reportClick("app", mApp1);
577         reportClick("app", mApp1);
578         reportClick("app", mApp1);
579 
580         reportClick("app", mApp2);
581         reportClick("app", mApp2);
582 
583         // app1 wins 4 - 2
584         assertShortcuts("app", mApp1, mApp2);
585 
586         // reset to 1
587         invalidateShortcut(APP_SOURCE, mApp1.getShortcutId());
588         reportClick("app", mApp1);
589 
590         // app2 wins 2 - 1
591         assertShortcuts("expecting app1's click count to reset after being invalidated.",
592                 "app", mApp2, mApp1);
593     }
594 
testShortcutsAllowedCorpora()595     public void testShortcutsAllowedCorpora() {
596         reportClick("a", mApp1);
597         reportClick("a", mContact1);
598 
599         assertShortcuts("only allowed shortcuts should be returned",
600                 "a", Arrays.asList(APP_CORPUS), mApp1);
601     }
602 
603     //
604     // SOURCE RANKING TESTS BELOW
605     //
606 
testSourceRanking_moreClicksWins()607     public void testSourceRanking_moreClicksWins() {
608         assertCorpusRanking("expected no ranking");
609 
610         int minClicks = mConfig.getMinClicksForSourceRanking();
611 
612         // click on an app
613         for (int i = 0; i < minClicks + 1; i++) {
614             reportClick("a", mApp1);
615         }
616         // fewer clicks on a contact
617         for (int i = 0; i < minClicks; i++) {
618             reportClick("a", mContact1);
619         }
620 
621         assertCorpusRanking("expecting apps to rank ahead of contacts (more clicks)",
622                 APP_CORPUS, CONTACTS_CORPUS);
623 
624         // more clicks on a contact
625         reportClick("a", mContact1);
626         reportClick("a", mContact1);
627 
628         assertCorpusRanking("expecting contacts to rank ahead of apps (more clicks)",
629                 CONTACTS_CORPUS, APP_CORPUS);
630     }
631 
testOldSourceStatsDontCount()632     public void testOldSourceStatsDontCount() {
633         // apps were popular back in the day
634         final long toOld = mConfig.getMaxStatAgeMillis() + 1;
635         int minClicks = mConfig.getMinClicksForSourceRanking();
636         for (int i = 0; i < minClicks; i++) {
637             reportClick("app", mApp1, NOW - toOld);
638         }
639 
640         // and contacts is 1/2
641         for (int i = 0; i < minClicks; i++) {
642             reportClick("bob", mContact1, NOW);
643         }
644 
645         assertCorpusRanking("old clicks for apps shouldn't count.",
646                 CONTACTS_CORPUS);
647     }
648 
649 
testSourceRanking_filterSourcesWithInsufficientData()650     public void testSourceRanking_filterSourcesWithInsufficientData() {
651         int minClicks = mConfig.getMinClicksForSourceRanking();
652         // not enough
653         for (int i = 0; i < minClicks - 1; i++) {
654             reportClick("app", mApp1);
655         }
656         // just enough
657         for (int i = 0; i < minClicks; i++) {
658             reportClick("bob", mContact1);
659         }
660 
661         assertCorpusRanking(
662                 "ordering should only include sources with at least " + minClicks + " clicks.",
663                 CONTACTS_CORPUS);
664     }
665 
666     // App upgrade tests
667 
testAppUpgradeClearsShortcuts()668     public void testAppUpgradeClearsShortcuts() {
669         reportClick("a", mApp1);
670         reportClick("add", mApp1);
671         reportClick("a", mContact1);
672 
673         assertShortcuts("all shortcuts should be returned",
674                 "a", mAllowedCorpora, mApp1, mContact1);
675 
676         // Upgrade an existing corpus
677         MockCorpus upgradedCorpus = new MockCorpus(APP_SOURCE_V2);
678         mCorpora.addCorpus(upgradedCorpus);
679 
680         List<Corpus> newAllowedCorpora = new ArrayList<Corpus>(mCorpora.getAllCorpora());
681         assertShortcuts("app shortcuts should be removed when the source was upgraded",
682                 "a", newAllowedCorpora, mContact1);
683     }
684 
testAppUpgradePromotesLowerRanked()685     public void testAppUpgradePromotesLowerRanked() {
686 
687         ListSuggestionCursor expected = new ListSuggestionCursor("a");
688         for (int i = 0; i < MAX_SHORTCUTS + 1; i++) {
689             reportClick("app", mApp1, NOW);
690         }
691         expected.add(mApp1);
692 
693         // Enough contact clicks to make one more shortcut than getMaxShortcutsReturned()
694         for (int i = 0; i < MAX_SHORTCUTS; i++) {
695             SuggestionData contact = makeContact("andy" + i);
696             int numClicks = MAX_SHORTCUTS - i;  // use click count to get shortcuts in order
697             for (int j = 0; j < numClicks; j++) {
698                 reportClick("and", contact, NOW);
699             }
700             expected.add(contact);
701         }
702 
703         // Expect the app, and then all contacts
704         assertShortcuts("app and all contacts should be returned",
705                 "a", mAllowedCorpora, expected);
706 
707         // Upgrade app corpus
708         MockCorpus upgradedCorpus = new MockCorpus(APP_SOURCE_V2);
709         mCorpora.addCorpus(upgradedCorpus);
710 
711         // Expect all contacts
712         List<Corpus> newAllowedCorpora = new ArrayList<Corpus>(mCorpora.getAllCorpora());
713         assertShortcuts("app shortcuts should be removed when the source was upgraded "
714                 + "and a contact should take its place",
715                 "a", newAllowedCorpora, SuggestionCursorUtil.slice(expected, 1));
716     }
717 
testIrrelevantAppUpgrade()718     public void testIrrelevantAppUpgrade() {
719         reportClick("a", mApp1);
720         reportClick("add", mApp1);
721         reportClick("a", mContact1);
722 
723         assertShortcuts("all shortcuts should be returned",
724                 "a", mAllowedCorpora, mApp1, mContact1);
725 
726         // Fire a corpus set update that affect no shortcuts corpus
727         MockCorpus newCorpus = new MockCorpus(new MockSource("newsource"));
728         mCorpora.addCorpus(newCorpus);
729 
730         assertShortcuts("all shortcuts should be returned",
731                 "a", mAllowedCorpora, mApp1, mContact1);
732     }
733 
testAllowWebSearchShortcuts()734     public void testAllowWebSearchShortcuts() {
735         reportClick("a", mApp1);
736         reportClick("a", mApp1);
737         reportClick("a", mWeb1);
738         assertShortcuts("web shortcuts should be included", "a",
739                 mAllowedCorpora, true, mApp1, mWeb1);
740         assertShortcuts("web shortcuts should not be included", "a",
741                 mAllowedCorpora, false, mApp1);
742     }
743 
testExtraDataNull()744     public void testExtraDataNull() {
745         assertExtra("Null extra", "extra_null", null);
746     }
747 
testExtraDataString()748     public void testExtraDataString() {
749         assertExtra("String extra", "extra_string", "stringy-stringy-string");
750     }
751 
testExtraDataInteger()752     public void testExtraDataInteger() {
753         assertExtra("Integer extra", "extra_int", new Integer(42));
754     }
755 
testExtraDataFloat()756     public void testExtraDataFloat() {
757         assertExtra("Float extra", "extra_float", new Float(Math.PI));
758     }
759 
testExtraDataStringWithDodgyChars()760     public void testExtraDataStringWithDodgyChars() {
761         assertExtra("String extra with newlines", "extra_string", "line\nline\nline\n");
762         JSONArray a = new JSONArray();
763         a.put(true);
764         a.put(42);
765         a.put("hello");
766         a.put("hello \"again\"");
767         assertExtra("String extra with JSON", "extra_string", a.toString());
768         assertExtra("String extra with control chars", "extra_string", "\0\b\t\f\r");
769     }
770 
771     // Utilities
772 
makeCursor(String query, SuggestionData... suggestions)773     protected ListSuggestionCursor makeCursor(String query, SuggestionData... suggestions) {
774         ListSuggestionCursor cursor = new ListSuggestionCursor(query);
775         for (SuggestionData suggestion : suggestions) {
776             cursor.add(suggestion);
777         }
778         return cursor;
779     }
780 
reportClick(String query, SuggestionData suggestion)781     protected void reportClick(String query, SuggestionData suggestion) {
782         reportClick(new ListSuggestionCursor(query, suggestion), 0);
783     }
784 
reportClick(String query, SuggestionData suggestion, long now)785     protected void reportClick(String query, SuggestionData suggestion, long now) {
786         reportClickAtTime(new ListSuggestionCursor(query, suggestion), 0, now);
787     }
788 
reportClick(SuggestionCursor suggestions, int position)789     protected void reportClick(SuggestionCursor suggestions, int position) {
790         reportClickAtTime(suggestions, position, NOW);
791     }
792 
reportClickAtTime(SuggestionCursor suggestions, int position, long now)793     protected void reportClickAtTime(SuggestionCursor suggestions, int position, long now) {
794         mRepo.reportClickAtTime(suggestions, position, now);
795         mLogExecutor.runNext();
796     }
797 
removeFromHistory(SuggestionCursor suggestions, int position)798     protected void removeFromHistory(SuggestionCursor suggestions, int position) {
799         mRepo.removeFromHistory(suggestions, position);
800         mLogExecutor.runNext();
801     }
802 
invalidateShortcut(Source source, String shortcutId)803     protected void invalidateShortcut(Source source, String shortcutId) {
804         refreshShortcut(source, shortcutId, null);
805     }
806 
refreshShortcut(Source source, String shortcutId, SuggestionData suggestion)807     protected void refreshShortcut(Source source, String shortcutId, SuggestionData suggestion) {
808         SuggestionCursor refreshed =
809                 suggestion == null ? null : new ListSuggestionCursor(null, suggestion);
810         mRepo.refreshShortcut(source, shortcutId, refreshed);
811         mLogExecutor.runNext();
812     }
813 
sourceImpressions(Source source, int clicks, int impressions)814     protected void sourceImpressions(Source source, int clicks, int impressions) {
815         if (clicks > impressions) throw new IllegalArgumentException("ya moran!");
816 
817         for (int i = 0; i < impressions; i++, clicks--) {
818             sourceImpression(source, clicks > 0);
819         }
820     }
821 
822     /**
823      * Simulate an impression, and optionally a click, on a source.
824      *
825      * @param source The name of the source.
826      * @param click Whether to register a click in addition to the impression.
827      */
sourceImpression(Source source, boolean click)828     protected void sourceImpression(Source source, boolean click) {
829         sourceImpression(source, click, NOW);
830     }
831 
sourceSuggestion(Source source)832     protected SuggestionData sourceSuggestion(Source source) {
833         return new SuggestionData(source)
834             .setIntentAction("view")
835             .setIntentData("data/id")
836             .setShortcutId("shortcutid");
837     }
838 
839     /**
840      * Simulate an impression, and optionally a click, on a source.
841      *
842      * @param source The name of the source.
843      * @param click Whether to register a click in addition to the impression.
844      */
sourceImpression(Source source, boolean click, long now)845     protected void sourceImpression(Source source, boolean click, long now) {
846         SuggestionData suggestionClicked = !click ?
847                 null : sourceSuggestion(source);
848 
849         reportClick("a", suggestionClicked);
850     }
851 
assertNoShortcuts(String query)852     void assertNoShortcuts(String query) {
853         assertNoShortcuts("", query);
854     }
855 
assertNoShortcuts(String message, String query)856     void assertNoShortcuts(String message, String query) {
857         SuggestionCursor cursor = getShortcuts(query, mAllowedCorpora);
858         try {
859             assertNull(message + ", got shortcuts", cursor);
860         } finally {
861             if (cursor != null) cursor.close();
862         }
863     }
864 
assertShortcuts(String query, SuggestionData... expected)865     void assertShortcuts(String query, SuggestionData... expected) {
866         assertShortcuts("", query, expected);
867     }
868 
assertShortcutAtPosition(String message, String query, int position, SuggestionData expected)869     void assertShortcutAtPosition(String message, String query,
870             int position, SuggestionData expected) {
871         SuggestionCursor cursor = getShortcuts(query, mAllowedCorpora);
872         try {
873             SuggestionCursor expectedCursor = new ListSuggestionCursor(query, expected);
874             SuggestionCursorUtil.assertSameSuggestion(message, position, expectedCursor, cursor);
875         } finally {
876             if (cursor != null) cursor.close();
877         }
878     }
879 
assertShortcutCount(String message, String query, int expectedCount)880     void assertShortcutCount(String message, String query, int expectedCount) {
881         SuggestionCursor cursor = getShortcuts(query, mAllowedCorpora);
882         try {
883             assertEquals(message, expectedCount, cursor.getCount());
884         } finally {
885             if (cursor != null) cursor.close();
886         }
887     }
888 
assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora, boolean allowWebSearchShortcuts, SuggestionCursor expected)889     void assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora,
890             boolean allowWebSearchShortcuts, SuggestionCursor expected) {
891         SuggestionCursor cursor = mRepo.getShortcutsForQuery(query, allowedCorpora, allowWebSearchShortcuts, NOW);
892         try {
893             SuggestionCursorUtil.assertSameSuggestions(message, expected, cursor);
894         } finally {
895             if (cursor != null) cursor.close();
896         }
897     }
898 
assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora, SuggestionCursor expected)899     void assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora,
900             SuggestionCursor expected) {
901         assertShortcuts(message, query, allowedCorpora, true, expected);
902     }
903 
getShortcuts(String query, Collection<Corpus> allowedCorpora)904     SuggestionCursor getShortcuts(String query, Collection<Corpus> allowedCorpora) {
905         return mRepo.getShortcutsForQuery(query, allowedCorpora, true, NOW);
906     }
907 
assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora, boolean allowWebSearchShortcuts, SuggestionData... expected)908     void assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora,
909             boolean allowWebSearchShortcuts, SuggestionData... expected) {
910         assertShortcuts(message, query, allowedCorpora, allowWebSearchShortcuts,
911                 new ListSuggestionCursor(query, expected));
912     }
913 
assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora, SuggestionData... expected)914     void assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora,
915             SuggestionData... expected) {
916         assertShortcuts(message, query, allowedCorpora, new ListSuggestionCursor(query, expected));
917     }
918 
assertShortcuts(String message, String query, SuggestionData... expected)919     void assertShortcuts(String message, String query, SuggestionData... expected) {
920         assertShortcuts(message, query, mAllowedCorpora, expected);
921     }
922 
assertHasHistory(boolean expected)923     private void assertHasHistory(boolean expected) {
924         ConsumerTrap<Boolean> trap = new ConsumerTrap<Boolean>();
925         mRepo.hasHistory(trap);
926         mLogExecutor.runNext();
927         assertEquals("hasHistory() returned bad value", expected, (boolean) trap.getValue());
928     }
929 
assertCorpusRanking(String message, Corpus... expected)930     void assertCorpusRanking(String message, Corpus... expected) {
931         String[] expectedNames = new String[expected.length];
932         for (int i = 0; i < expected.length; i++) {
933             expectedNames[i] = expected[i].getName();
934         }
935         Map<String,Integer> scores = getCorpusScores();
936         List<String> observed = sortByValues(scores);
937         // Highest scores should come first
938         Collections.reverse(observed);
939         Log.d(TAG, "scores=" + scores);
940         MoreAsserts.assertContentsInOrder(message, observed, (Object[]) expectedNames);
941     }
942 
getCorpusScores()943     private Map<String,Integer> getCorpusScores() {
944         ConsumerTrap<Map<String,Integer>> trap = new ConsumerTrap<Map<String,Integer>>();
945         mRepo.getCorpusScores(trap);
946         mLogExecutor.runNext();
947         return trap.getValue();
948     }
949 
sortByValues(Map<A,B> map)950     static <A extends Comparable<A>, B extends Comparable<B>> List<A> sortByValues(Map<A,B> map) {
951         Comparator<Map.Entry<A,B>> comp = new Comparator<Map.Entry<A,B>>() {
952             public int compare(Entry<A, B> object1, Entry<A, B> object2) {
953                 int diff = object1.getValue().compareTo(object2.getValue());
954                 if (diff != 0) {
955                     return diff;
956                 } else {
957                     return object1.getKey().compareTo(object2.getKey());
958                 }
959             }
960         };
961         ArrayList<Map.Entry<A,B>> sorted = new ArrayList<Map.Entry<A,B>>(map.size());
962         sorted.addAll(map.entrySet());
963         Collections.sort(sorted, comp);
964         ArrayList<A> out = new ArrayList<A>(sorted.size());
965         for (Map.Entry<A,B> e : sorted) {
966             out.add(e.getKey());
967         }
968         return out;
969     }
970 
assertContentsInOrder(Iterable<?> actual, Object... expected)971     static void assertContentsInOrder(Iterable<?> actual, Object... expected) {
972         MoreAsserts.assertContentsInOrder(null, actual, expected);
973     }
974 
assertExtra(String message, String extraColumn, Object extraValue)975     void assertExtra(String message, String extraColumn, Object extraValue) {
976         SuggestionData s = sourceSuggestion(APP_SOURCE);
977         s.setExtras(new MockSuggestionExtras().put(extraColumn, extraValue));
978         reportClick("a", s);
979         assertShortcutExtra(message, "a", extraColumn, extraValue);
980     }
981 
assertShortcutExtra(String message, String query, String extraColumn, Object extraValue)982     void assertShortcutExtra(String message, String query, String extraColumn, Object extraValue) {
983         SuggestionCursor cursor = getShortcuts(query, mAllowedCorpora);
984         try {
985             SuggestionCursorUtil.assertSuggestionExtras(message, cursor, extraColumn, extraValue);
986         } finally {
987             if (cursor != null) cursor.close();
988         }
989     }
990 
991 }
992