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