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