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