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