OSDN Git Service

original
[gb-231r1-is01/Gingerbread_2.3.3_r1_IS01.git] / packages / apps / QuickSearchBox / tests / src / com / android / quicksearchbox / ShortcutRepositoryTest.java
1 /*
2  * Copyright (C) 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
17 package com.android.quicksearchbox;
18
19 import com.android.quicksearchbox.util.MockExecutor;
20 import com.android.quicksearchbox.util.Util;
21
22 import android.app.SearchManager;
23 import android.test.AndroidTestCase;
24 import android.test.MoreAsserts;
25 import android.test.suitebuilder.annotation.MediumTest;
26 import android.util.Log;
27
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.Comparator;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Map.Entry;
36
37 import junit.framework.Assert;
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 int MAX_SHORTCUTS = 8;
73
74     protected Config mConfig;
75     protected MockCorpora mCorpora;
76     protected MockExecutor mLogExecutor;
77     protected ShortcutRefresher mRefresher;
78
79     protected List<Corpus> mAllowedCorpora;
80
81     protected ShortcutRepositoryImplLog mRepo;
82
83     protected ListSuggestionCursor mAppSuggestions;
84     protected ListSuggestionCursor mContactSuggestions;
85
86     protected SuggestionData mApp1;
87     protected SuggestionData mApp2;
88     protected SuggestionData mApp3;
89
90     protected SuggestionData mContact1;
91     protected SuggestionData mContact2;
92
93     protected ShortcutRepositoryImplLog createShortcutRepository() {
94         return new ShortcutRepositoryImplLog(getContext(), mConfig, mCorpora,
95                 mRefresher, new MockHandler(), mLogExecutor,
96                 "test-shortcuts-log.db").disableUpdateDelay();
97     }
98
99     @Override
100     protected void setUp() throws Exception {
101         super.setUp();
102
103         mConfig = new Config(getContext());
104         mCorpora = new MockCorpora();
105         mCorpora.addCorpus(APP_CORPUS);
106         mCorpora.addCorpus(CONTACTS_CORPUS);
107         mRefresher = new MockShortcutRefresher();
108         mLogExecutor = new MockExecutor();
109         mRepo = createShortcutRepository();
110
111         mAllowedCorpora = new ArrayList<Corpus>(mCorpora.getAllCorpora());
112
113         mApp1 = makeApp("app1");
114         mApp2 = makeApp("app2");
115         mApp3 = makeApp("app3");
116         mAppSuggestions = new ListSuggestionCursor("foo", mApp1, mApp2, mApp3);
117
118         mContact1 = new SuggestionData(CONTACTS_SOURCE)
119                 .setText1("Joe Blow")
120                 .setIntentAction("view")
121                 .setIntentData("contacts/joeblow")
122                 .setShortcutId("j-blow");
123         mContact2 = new SuggestionData(CONTACTS_SOURCE)
124                 .setText1("Mike Johnston")
125                 .setIntentAction("view")
126                 .setIntentData("contacts/mikeJ")
127                 .setShortcutId("mo-jo");
128
129         mContactSuggestions = new ListSuggestionCursor("foo", mContact1, mContact2);
130     }
131
132     private SuggestionData makeApp(String name) {
133         return new SuggestionData(APP_SOURCE)
134                 .setText1(name)
135                 .setIntentAction("view")
136                 .setIntentData("apps/" + name)
137                 .setShortcutId("shorcut_" + name);
138     }
139
140     private SuggestionData makeContact(String name) {
141         return new SuggestionData(CONTACTS_SOURCE)
142                 .setText1(name)
143                 .setIntentAction("view")
144                 .setIntentData("contacts/" + name)
145                 .setShortcutId("shorcut_" + name);
146     }
147
148     @Override
149     protected void tearDown() throws Exception {
150         super.tearDown();
151         mRepo.deleteRepository();
152     }
153
154     public void testHasHistory() {
155         assertFalse(mRepo.hasHistory());
156         reportClickAtTime(mAppSuggestions, 0, NOW);
157         assertTrue(mRepo.hasHistory());
158         mRepo.clearHistory();
159         assertTrue(mRepo.hasHistory());
160         mLogExecutor.runNext();
161         assertFalse(mRepo.hasHistory());
162     }
163
164     public void testNoMatch() {
165         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
166                 .setText1("bob smith")
167                 .setIntentAction("action")
168                 .setIntentData("data");
169
170         reportClick("bob smith", clicked);
171         assertNoShortcuts("joe");
172     }
173
174     public void testFullPackingUnpacking() {
175         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
176                 .setFormat("<i>%s</i>")
177                 .setText1("title")
178                 .setText2("description")
179                 .setText2Url("description_url")
180                 .setIcon1("android.resource://system/drawable/foo")
181                 .setIcon2("content://test/bar")
182                 .setIntentAction("action")
183                 .setIntentData("data")
184                 .setSuggestionQuery("query")
185                 .setIntentExtraData("extradata")
186                 .setShortcutId("idofshortcut")
187                 .setSuggestionLogType("logtype");
188         reportClick("q", clicked);
189
190         assertShortcuts("q", clicked);
191         assertShortcuts("", clicked);
192     }
193
194     public void testSpinnerWhileRefreshing() {
195         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
196                 .setText1("title")
197                 .setText2("description")
198                 .setIcon2("icon2")
199                 .setSuggestionQuery("query")
200                 .setIntentExtraData("extradata")
201                 .setShortcutId("idofshortcut")
202                 .setSpinnerWhileRefreshing(true);
203
204         reportClick("q", clicked);
205
206         String spinnerUri = Util.getResourceUri(mContext, R.drawable.search_spinner).toString();
207         SuggestionData expected = new SuggestionData(CONTACTS_SOURCE)
208                 .setText1("title")
209                 .setText2("description")
210                 .setIcon2(spinnerUri)
211                 .setSuggestionQuery("query")
212                 .setIntentExtraData("extradata")
213                 .setShortcutId("idofshortcut")
214                 .setSpinnerWhileRefreshing(true);
215
216         assertShortcuts("q", expected);
217     }
218
219     public void testPrefixesMatch() {
220         assertNoShortcuts("bob");
221
222         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
223                 .setText1("bob smith the third")
224                 .setIntentAction("action")
225                 .setIntentData("intentdata");
226
227         reportClick("bob smith", clicked);
228
229         assertShortcuts("bob smith", clicked);
230         assertShortcuts("bob s", clicked);
231         assertShortcuts("b", clicked);
232     }
233
234     public void testMatchesOneAndNotOthers() {
235         SuggestionData bob = new SuggestionData(CONTACTS_SOURCE)
236                 .setText1("bob smith the third")
237                 .setIntentAction("action")
238                 .setIntentData("intentdata/bob");
239
240         reportClick("bob", bob);
241
242         SuggestionData george = new SuggestionData(CONTACTS_SOURCE)
243                 .setText1("george jones")
244                 .setIntentAction("action")
245                 .setIntentData("intentdata/george");
246         reportClick("geor", george);
247
248         assertShortcuts("b for bob", "b", bob);
249         assertShortcuts("g for george", "g", george);
250     }
251
252     public void testDifferentPrefixesMatchSameEntity() {
253         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
254                 .setText1("bob smith the third")
255                 .setIntentAction("action")
256                 .setIntentData("intentdata");
257
258         reportClick("bob", clicked);
259         reportClick("smith", clicked);
260         assertShortcuts("b", clicked);
261         assertShortcuts("s", clicked);
262     }
263
264     public void testMoreClicksWins() {
265         reportClick("app", mApp1);
266         reportClick("app", mApp2);
267         reportClick("app", mApp1);
268
269         assertShortcuts("expected app1 to beat app2 since it has more hits",
270                 "app", mApp1, mApp2);
271
272         reportClick("app", mApp2);
273         reportClick("app", mApp2);
274
275         assertShortcuts("query 'app': expecting app2 to beat app1 since it has more hits",
276                 "app", mApp2, mApp1);
277         assertShortcuts("query 'a': expecting app2 to beat app1 since it has more hits",
278                 "a", mApp2, mApp1);
279     }
280
281     public void testMostRecentClickWins() {
282         // App 1 has 3 clicks
283         reportClick("app", mApp1, NOW - 5);
284         reportClick("app", mApp1, NOW - 5);
285         reportClick("app", mApp1, NOW - 5);
286         // App 2 has 2 clicks
287         reportClick("app", mApp2, NOW - 2);
288         reportClick("app", mApp2, NOW - 2);
289         // App 3 only has 1, but it's most recent
290         reportClick("app", mApp3, NOW - 1);
291
292         assertShortcuts("expected app3 to beat app1 and app2 because it's clicked last",
293                 "app", mApp3, mApp1, mApp2);
294
295         reportClick("app", mApp2, NOW);
296
297         assertShortcuts("query 'app': expecting app2 to beat app1 since it's clicked last",
298                 "app", mApp2, mApp1, mApp3);
299         assertShortcuts("query 'a': expecting app2 to beat app1 since it's clicked last",
300                 "a", mApp2, mApp1, mApp3);
301         assertShortcuts("query '': expecting app2 to beat app1 since it's clicked last",
302                 "", mApp2, mApp1, mApp3);
303     }
304
305     public void testMostRecentClickWinsOnEmptyQuery() {
306         reportClick("app", mApp1, NOW - 3);
307         reportClick("app", mApp1, NOW - 2);
308         reportClick("app", mApp2, NOW - 1);
309
310         assertShortcuts("expected app2 to beat app1 since it's clicked last", "",
311                 mApp2, mApp1);
312     }
313
314     public void testMostRecentClickWinsEvenWithMoreThanLimitShortcuts() {
315         for (int i = 0; i < MAX_SHORTCUTS; i++) {
316             SuggestionData app = makeApp("TestApp" + i);
317             // Each of these shortcuts has two clicks
318             reportClick("app", app, NOW - 2);
319             reportClick("app", app, NOW - 1);
320         }
321
322         // mApp1 has only one click, but is more recent
323         reportClick("app", mApp1, NOW);
324
325         assertShortcutAtPosition(
326             "expecting app1 to beat all others since it's clicked last",
327             "app", 0, mApp1);
328     }
329
330     /**
331      * similar to {@link #testMoreClicksWins()} but clicks are reported with prefixes of the
332      * original query.  we want to make sure a match on query 'a' updates the stats for the
333      * entry it matched against, 'app'.
334      */
335     public void testPrefixMatchUpdatesSameEntry() {
336         reportClick("app", mApp1, NOW);
337         reportClick("app", mApp2, NOW);
338         reportClick("app", mApp1, NOW);
339
340         assertShortcuts("expected app1 to beat app2 since it has more hits",
341                 "app", mApp1, mApp2);
342     }
343
344     private static final long DAY_MILLIS = 86400000L; // just ask the google
345     private static final long HOUR_MILLIS = 3600000L;
346
347     public void testMoreRecentlyClickedWins() {
348         reportClick("app", mApp1, NOW - DAY_MILLIS*2);
349         reportClick("app", mApp2, NOW);
350         reportClick("app", mApp3, NOW - DAY_MILLIS*4);
351
352         assertShortcuts("expecting more recently clicked app to rank higher",
353                 "app", mApp2, mApp1, mApp3);
354     }
355
356     public void testMoreRecentlyClickedWinsSeconds() {
357         reportClick("app", mApp1, NOW - 10000);
358         reportClick("app", mApp2, NOW - 5000);
359         reportClick("app", mApp3, NOW);
360
361         assertShortcuts("expecting more recently clicked app to rank higher",
362                 "app", mApp3, mApp2, mApp1);
363     }
364
365     public void testRecencyOverridesClicks() {
366
367         // 5 clicks, most recent half way through age limit
368         long halfWindow = mConfig.getMaxStatAgeMillis() / 2;
369         reportClick("app", mApp1, NOW - halfWindow);
370         reportClick("app", mApp1, NOW - halfWindow);
371         reportClick("app", mApp1, NOW - halfWindow);
372         reportClick("app", mApp1, NOW - halfWindow);
373         reportClick("app", mApp1, NOW - halfWindow);
374
375         // 3 clicks, the most recent very recent
376         reportClick("app", mApp2, NOW - HOUR_MILLIS);
377         reportClick("app", mApp2, NOW - HOUR_MILLIS);
378         reportClick("app", mApp2, NOW - HOUR_MILLIS);
379
380         assertShortcuts("expecting 3 recent clicks to beat 5 clicks long ago",
381                 "app", mApp2, mApp1);
382     }
383
384     public void testEntryOlderThanAgeLimitFiltered() {
385         reportClick("app", mApp1);
386
387         long pastWindow = mConfig.getMaxStatAgeMillis() + 1000;
388         reportClick("app", mApp2, NOW - pastWindow);
389
390         assertShortcuts("expecting app2 not clicked on recently enough to be filtered",
391                 "app", mApp1);
392     }
393
394     public void testZeroQueryResults_MoreClicksWins() {
395         reportClick("app", mApp1);
396         reportClick("app", mApp1);
397         reportClick("foo", mApp2);
398
399         assertShortcuts("", mApp1, mApp2);
400
401         reportClick("foo", mApp2);
402         reportClick("foo", mApp2);
403
404         assertShortcuts("", mApp2, mApp1);
405     }
406
407     public void testZeroQueryResults_DifferentQueryhitsCreditSameShortcut() {
408         reportClick("app", mApp1);
409         reportClick("foo", mApp2);
410         reportClick("bar", mApp2);
411
412         assertShortcuts("hits for 'foo' and 'bar' on app2 should have combined to rank it " +
413                 "ahead of app1, which only has one hit.",
414                 "", mApp2, mApp1);
415
416         reportClick("z", mApp1);
417         reportClick("2", mApp1);
418
419         assertShortcuts("", mApp1, mApp2);
420     }
421
422     public void testZeroQueryResults_zeroQueryHitCounts() {
423         reportClick("app", mApp1);
424         reportClick("", mApp2);
425         reportClick("", mApp2);
426
427         assertShortcuts("hits for '' on app2 should have combined to rank it " +
428                 "ahead of app1, which only has one hit.",
429                 "", mApp2, mApp1);
430
431         reportClick("", mApp1);
432         reportClick("", mApp1);
433
434         assertShortcuts("zero query hits for app1 should have made it higher than app2.",
435                 "", mApp1, mApp2);
436
437         assertShortcuts("query for 'a' should only match app1.",
438                 "a", mApp1);
439     }
440
441     public void testRefreshShortcut() {
442         final SuggestionData app1 = new SuggestionData(APP_SOURCE)
443                 .setFormat("format")
444                 .setText1("app1")
445                 .setText2("cool app")
446                 .setShortcutId("app1_id");
447
448         reportClick("app", app1);
449
450         final SuggestionData updated = new SuggestionData(APP_SOURCE)
451                 .setFormat("format (updated)")
452                 .setText1("app1 (updated)")
453                 .setText2("cool app")
454                 .setShortcutId("app1_id");
455
456         refreshShortcut(APP_SOURCE, "app1_id", updated);
457
458         assertShortcuts("expected updated properties in match",
459                 "app", updated);
460     }
461
462     public void testRefreshShortcutChangedIntent() {
463
464         final SuggestionData app1 = new SuggestionData(APP_SOURCE)
465                 .setIntentData("data")
466                 .setFormat("format")
467                 .setText1("app1")
468                 .setText2("cool app")
469                 .setShortcutId("app1_id");
470
471         reportClick("app", app1);
472
473         final SuggestionData updated = new SuggestionData(APP_SOURCE)
474                 .setIntentData("data-updated")
475                 .setFormat("format (updated)")
476                 .setText1("app1 (updated)")
477                 .setText2("cool app")
478                 .setShortcutId("app1_id");
479
480         refreshShortcut(APP_SOURCE, "app1_id", updated);
481
482         assertShortcuts("expected updated properties in match",
483                 "app", updated);
484     }
485
486     public void testInvalidateShortcut() {
487         final SuggestionData app1 = new SuggestionData(APP_SOURCE)
488                 .setText1("app1")
489                 .setText2("cool app")
490                 .setShortcutId("app1_id");
491
492         reportClick("app", app1);
493
494         invalidateShortcut(APP_SOURCE, "app1_id");
495
496         assertNoShortcuts("should be no matches since shortcut is invalid.", "app");
497     }
498
499     public void testInvalidateShortcut_sameIdDifferentSources() {
500         final String sameid = "same_id";
501         final SuggestionData app = new SuggestionData(APP_SOURCE)
502                 .setText1("app1")
503                 .setText2("cool app")
504                 .setShortcutId(sameid);
505         reportClick("app", app);
506         assertShortcuts("app should be there", "", app);
507
508         final SuggestionData contact = new SuggestionData(CONTACTS_SOURCE)
509                 .setText1("joe blow")
510                 .setText2("a good pal")
511                 .setShortcutId(sameid);
512         reportClick("joe", contact);
513         reportClick("joe", contact);
514         assertShortcuts("app and contact should be there.", "", contact, app);
515
516         refreshShortcut(APP_SOURCE, sameid, null);
517         assertNoShortcuts("app should not be there.", "app");
518         assertShortcuts("contact with same shortcut id should still be there.",
519                 "joe", contact);
520         assertShortcuts("contact with same shortcut id should still be there.",
521                 "", contact);
522     }
523
524     public void testNeverMakeShortcut() {
525         final SuggestionData contact = new SuggestionData(CONTACTS_SOURCE)
526                 .setText1("unshortcuttable contact")
527                 .setText2("you didn't want to call them again anyway")
528                 .setShortcutId(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
529         reportClick("unshortcuttable", contact);
530         assertNoShortcuts("never-shortcutted suggestion should not be there.", "unshortcuttable");
531     }
532
533     public void testCountResetAfterShortcutDeleted() {
534         reportClick("app", mApp1);
535         reportClick("app", mApp1);
536         reportClick("app", mApp1);
537         reportClick("app", mApp1);
538
539         reportClick("app", mApp2);
540         reportClick("app", mApp2);
541
542         // app1 wins 4 - 2
543         assertShortcuts("app", mApp1, mApp2);
544
545         // reset to 1
546         invalidateShortcut(APP_SOURCE, mApp1.getShortcutId());
547         reportClick("app", mApp1);
548
549         // app2 wins 2 - 1
550         assertShortcuts("expecting app1's click count to reset after being invalidated.",
551                 "app", mApp2, mApp1);
552     }
553
554     public void testShortcutsAllowedCorpora() {
555         reportClick("a", mApp1);
556         reportClick("a", mContact1);
557
558         assertShortcuts("only allowed shortcuts should be returned",
559                 "a", Arrays.asList(APP_CORPUS), mApp1);
560     }
561
562     //
563     // SOURCE RANKING TESTS BELOW
564     //
565
566     public void testSourceRanking_moreClicksWins() {
567         assertCorpusRanking("expected no ranking");
568
569         int minClicks = mConfig.getMinClicksForSourceRanking();
570
571         // click on an app
572         for (int i = 0; i < minClicks + 1; i++) {
573             reportClick("a", mApp1);
574         }
575         // fewer clicks on a contact
576         for (int i = 0; i < minClicks; i++) {
577             reportClick("a", mContact1);
578         }
579
580         assertCorpusRanking("expecting apps to rank ahead of contacts (more clicks)",
581                 APP_CORPUS, CONTACTS_CORPUS);
582
583         // more clicks on a contact
584         reportClick("a", mContact1);
585         reportClick("a", mContact1);
586
587         assertCorpusRanking("expecting contacts to rank ahead of apps (more clicks)",
588                 CONTACTS_CORPUS, APP_CORPUS);
589     }
590
591     public void testOldSourceStatsDontCount() {
592         // apps were popular back in the day
593         final long toOld = mConfig.getMaxStatAgeMillis() + 1;
594         int minClicks = mConfig.getMinClicksForSourceRanking();
595         for (int i = 0; i < minClicks; i++) {
596             reportClick("app", mApp1, NOW - toOld);
597         }
598
599         // and contacts is 1/2
600         for (int i = 0; i < minClicks; i++) {
601             reportClick("bob", mContact1, NOW);
602         }
603
604         assertCorpusRanking("old clicks for apps shouldn't count.",
605                 CONTACTS_CORPUS);
606     }
607
608
609     public void testSourceRanking_filterSourcesWithInsufficientData() {
610         int minClicks = mConfig.getMinClicksForSourceRanking();
611         // not enough
612         for (int i = 0; i < minClicks - 1; i++) {
613             reportClick("app", mApp1);
614         }
615         // just enough
616         for (int i = 0; i < minClicks; i++) {
617             reportClick("bob", mContact1);
618         }
619
620         assertCorpusRanking(
621                 "ordering should only include sources with at least " + minClicks + " clicks.",
622                 CONTACTS_CORPUS);
623     }
624
625     // App upgrade tests
626
627     public void testAppUpgradeClearsShortcuts() {
628         reportClick("a", mApp1);
629         reportClick("add", mApp1);
630         reportClick("a", mContact1);
631
632         assertShortcuts("all shortcuts should be returned",
633                 "a", mAllowedCorpora, mApp1, mContact1);
634
635         // Upgrade an existing corpus
636         MockCorpus upgradedCorpus = new MockCorpus(APP_SOURCE_V2);
637         mCorpora.addCorpus(upgradedCorpus);
638
639         List<Corpus> newAllowedCorpora = new ArrayList<Corpus>(mCorpora.getAllCorpora());
640         assertShortcuts("app shortcuts should be removed when the source was upgraded",
641                 "a", newAllowedCorpora, mContact1);
642     }
643
644     public void testAppUpgradePromotesLowerRanked() {
645
646         ListSuggestionCursor expected = new ListSuggestionCursor("a");
647         for (int i = 0; i < MAX_SHORTCUTS + 1; i++) {
648             reportClick("app", mApp1, NOW);
649         }
650         expected.add(mApp1);
651
652         // Enough contact clicks to make one more shortcut than getMaxShortcutsReturned()
653         for (int i = 0; i < MAX_SHORTCUTS; i++) {
654             SuggestionData contact = makeContact("andy" + i);
655             int numClicks = MAX_SHORTCUTS - i;  // use click count to get shortcuts in order
656             for (int j = 0; j < numClicks; j++) {
657                 reportClick("and", contact, NOW);
658             }
659             expected.add(contact);
660         }
661
662         // Expect the app, and then all but one contact
663         assertShortcuts("app and all but one contact should be returned",
664                 "a", mAllowedCorpora, SuggestionCursorUtil.slice(expected, 0, MAX_SHORTCUTS));
665
666         // Upgrade app corpus
667         MockCorpus upgradedCorpus = new MockCorpus(APP_SOURCE_V2);
668         mCorpora.addCorpus(upgradedCorpus);
669
670         // Expect all contacts
671         List<Corpus> newAllowedCorpora = new ArrayList<Corpus>(mCorpora.getAllCorpora());
672         assertShortcuts("app shortcuts should be removed when the source was upgraded "
673                 + "and a contact should take its place",
674                 "a", newAllowedCorpora, SuggestionCursorUtil.slice(expected, 1, MAX_SHORTCUTS));
675     }
676
677     public void testIrrelevantAppUpgrade() {
678         reportClick("a", mApp1);
679         reportClick("add", mApp1);
680         reportClick("a", mContact1);
681
682         assertShortcuts("all shortcuts should be returned",
683                 "a", mAllowedCorpora, mApp1, mContact1);
684
685         // Fire a corpus set update that affect no shortcuts corpus
686         MockCorpus newCorpus = new MockCorpus(new MockSource("newsource"));
687         mCorpora.addCorpus(newCorpus);
688
689         assertShortcuts("all shortcuts should be returned",
690                 "a", mAllowedCorpora, mApp1, mContact1);
691     }
692
693     // Utilities
694
695     protected ListSuggestionCursor makeCursor(String query, SuggestionData... suggestions) {
696         ListSuggestionCursor cursor = new ListSuggestionCursor(query);
697         for (SuggestionData suggestion : suggestions) {
698             cursor.add(suggestion);
699         }
700         return cursor;
701     }
702
703     protected void reportClick(String query, SuggestionData suggestion) {
704         reportClick(new ListSuggestionCursor(query, suggestion), 0);
705     }
706
707     protected void reportClick(String query, SuggestionData suggestion, long now) {
708         reportClickAtTime(new ListSuggestionCursor(query, suggestion), 0, now);
709     }
710
711     protected void reportClick(SuggestionCursor suggestions, int position) {
712         reportClickAtTime(suggestions, position, NOW);
713     }
714
715     protected void reportClickAtTime(SuggestionCursor suggestions, int position, long now) {
716         mRepo.reportClickAtTime(suggestions, position, now);
717         mLogExecutor.runNext();
718     }
719
720     protected void invalidateShortcut(Source source, String shortcutId) {
721         refreshShortcut(source, shortcutId, null);
722     }
723
724     protected void refreshShortcut(Source source, String shortcutId, SuggestionData suggestion) {
725         SuggestionCursor refreshed =
726                 suggestion == null ? null : new ListSuggestionCursor(null, suggestion);
727         mRepo.refreshShortcut(source, shortcutId, refreshed);
728         mLogExecutor.runNext();
729     }
730
731     protected void sourceImpressions(Source source, int clicks, int impressions) {
732         if (clicks > impressions) throw new IllegalArgumentException("ya moran!");
733
734         for (int i = 0; i < impressions; i++, clicks--) {
735             sourceImpression(source, clicks > 0);
736         }
737     }
738
739     /**
740      * Simulate an impression, and optionally a click, on a source.
741      *
742      * @param source The name of the source.
743      * @param click Whether to register a click in addition to the impression.
744      */
745     protected void sourceImpression(Source source, boolean click) {
746         sourceImpression(source, click, NOW);
747     }
748
749     /**
750      * Simulate an impression, and optionally a click, on a source.
751      *
752      * @param source The name of the source.
753      * @param click Whether to register a click in addition to the impression.
754      */
755     protected void sourceImpression(Source source, boolean click, long now) {
756         SuggestionData suggestionClicked = !click ?
757                 null :
758                 new SuggestionData(source)
759                     .setIntentAction("view")
760                     .setIntentData("data/id")
761                     .setShortcutId("shortcutid");
762
763         reportClick("a", suggestionClicked);
764     }
765
766     void assertNoShortcuts(String query) {
767         assertNoShortcuts("", query);
768     }
769
770     void assertNoShortcuts(String message, String query) {
771         SuggestionCursor cursor = mRepo.getShortcutsForQuery(query, mAllowedCorpora, NOW);
772         try {
773             assertNull(message + ", got shortcuts", cursor);
774         } finally {
775             if (cursor != null) cursor.close();
776         }
777     }
778
779     void assertShortcuts(String query, SuggestionData... expected) {
780         assertShortcuts("", query, expected);
781     }
782
783     void assertShortcutAtPosition(String message, String query,
784             int position, SuggestionData expected) {
785         SuggestionCursor cursor = mRepo.getShortcutsForQuery(query, mAllowedCorpora, NOW);
786         try {
787             SuggestionCursor expectedCursor = new ListSuggestionCursor(query, expected);
788             SuggestionCursorUtil.assertSameSuggestion(message, position, expectedCursor, cursor);
789         } finally {
790             if (cursor != null) cursor.close();
791         }
792     }
793
794     void assertShortcutCount(String message, String query, int expectedCount) {
795         SuggestionCursor cursor = mRepo.getShortcutsForQuery(query, mAllowedCorpora, NOW);
796         try {
797             assertEquals(message, expectedCount, cursor.getCount());
798         } finally {
799             if (cursor != null) cursor.close();
800         }
801     }
802
803     void assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora,
804             SuggestionCursor expected) {
805         SuggestionCursor cursor = mRepo.getShortcutsForQuery(query, allowedCorpora, NOW);
806         try {
807             SuggestionCursorUtil.assertSameSuggestions(message, expected, cursor, true);
808         } finally {
809             if (cursor != null) cursor.close();
810         }
811     }
812
813     void assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora,
814             SuggestionData... expected) {
815         assertShortcuts(message, query, allowedCorpora, new ListSuggestionCursor(query, expected));
816     }
817
818     void assertShortcuts(String message, String query, SuggestionData... expected) {
819         assertShortcuts(message, query, mAllowedCorpora, expected);
820     }
821
822     void assertCorpusRanking(String message, Corpus... expected) {
823         String[] expectedNames = new String[expected.length];
824         for (int i = 0; i < expected.length; i++) {
825             expectedNames[i] = expected[i].getName();
826         }
827         Map<String,Integer> scores = mRepo.getCorpusScores();
828         List<String> observed = sortByValues(scores);
829         // Highest scores should come first
830         Collections.reverse(observed);
831         Log.d(TAG, "scores=" + scores);
832         assertContentsInOrder(message, observed, (Object[]) expectedNames);
833     }
834
835     static <A extends Comparable<A>, B extends Comparable<B>> List<A> sortByValues(Map<A,B> map) {
836         Comparator<Map.Entry<A,B>> comp = new Comparator<Map.Entry<A,B>>() {
837             public int compare(Entry<A, B> object1, Entry<A, B> object2) {
838                 int diff = object1.getValue().compareTo(object2.getValue());
839                 if (diff != 0) {
840                     return diff;
841                 } else {
842                     return object1.getKey().compareTo(object2.getKey());
843                 }
844             }
845         };
846         ArrayList<Map.Entry<A,B>> sorted = new ArrayList<Map.Entry<A,B>>(map.size());
847         sorted.addAll(map.entrySet());
848         Collections.sort(sorted, comp);
849         ArrayList<A> out = new ArrayList<A>(sorted.size());
850         for (Map.Entry<A,B> e : sorted) {
851             out.add(e.getKey());
852         }
853         return out;
854     }
855
856     static void assertContentsInOrder(Iterable<?> actual, Object... expected) {
857         assertContentsInOrder(null, actual, expected);
858     }
859
860     /**
861      * an implementation of {@link MoreAsserts#assertContentsInOrder(String, Iterable, Object[])}
862      * that isn't busted.  a bug has been filed about that, but for now this works.
863      */
864     static void assertContentsInOrder(
865             String message, Iterable<?> actual, Object... expected) {
866         ArrayList actualList = new ArrayList();
867         for (Object o : actual) {
868             actualList.add(o);
869         }
870         Assert.assertEquals(message, Arrays.asList(expected), actualList);
871     }
872 }