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