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