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