1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.content; 18 19import android.app.SearchManager; 20import android.database.Cursor; 21import android.net.Uri; 22import android.provider.SearchRecentSuggestions; 23import android.test.ProviderTestCase2; 24import android.test.suitebuilder.annotation.Suppress; 25 26/** 27 * Very simple provider that I can instantiate right here. 28 */ 29class TestProvider extends SearchRecentSuggestionsProvider { 30 final static String AUTHORITY = "android.content.TestProvider"; 31 final static int MODE = DATABASE_MODE_QUERIES + DATABASE_MODE_2LINES; 32 33 public TestProvider() { 34 super(); 35 setupSuggestions(AUTHORITY, MODE); 36 } 37} 38 39/** 40 * ProviderTestCase that performs unit tests of SearchRecentSuggestionsProvider. 41 * 42 * You can run this test in isolation via the commands: 43 * 44 * $ (cd tests/FrameworkTests/ && mm) && adb sync 45 * $ adb shell am instrument -w \ 46 * -e class android.content.SearchRecentSuggestionsProviderTest 47 * com.android.frameworktest.tests/android.test.InstrumentationTestRunner 48 */ 49// Suppress these until bug http://b/issue?id=1416586 is fixed. 50@Suppress 51public class SearchRecentSuggestionsProviderTest extends ProviderTestCase2<TestProvider> { 52 53 // Elements prepared by setUp() 54 SearchRecentSuggestions mSearchHelper; 55 56 public SearchRecentSuggestionsProviderTest() { 57 super(TestProvider.class, TestProvider.AUTHORITY); 58 } 59 60 /** 61 * During setup, grab a helper for DB access 62 */ 63 @Override 64 public void setUp() throws Exception { 65 super.setUp(); 66 67 // Use the recent suggestions helper. As long as we pass in our isolated context, 68 // it should correctly access the provider under test. 69 mSearchHelper = new SearchRecentSuggestions(getMockContext(), 70 TestProvider.AUTHORITY, TestProvider.MODE); 71 72 // test for empty database at setup time 73 checkOpenCursorCount(0); 74 } 75 76 /** 77 * Simple test to see if we can instantiate the whole mess. 78 */ 79 public void testSetup() { 80 assertTrue(true); 81 } 82 83 /** 84 * Simple test to see if we can write and read back a single query 85 */ 86 public void testOneQuery() { 87 final String TEST_LINE1 = "test line 1"; 88 final String TEST_LINE2 = "test line 2"; 89 mSearchHelper.saveRecentQuery(TEST_LINE1, TEST_LINE2); 90 91 // make sure that there are is exactly one entry returned by a non-filtering cursor 92 checkOpenCursorCount(1); 93 94 // test non-filtering cursor for correct entry 95 checkResultCounts(null, 1, 1, TEST_LINE1, TEST_LINE2); 96 97 // test filtering cursor for correct entry 98 checkResultCounts(TEST_LINE1, 1, 1, TEST_LINE1, TEST_LINE2); 99 checkResultCounts(TEST_LINE2, 1, 1, TEST_LINE1, TEST_LINE2); 100 101 // test that a different filter returns zero results 102 checkResultCounts("bad filter", 0, 0, null, null); 103 } 104 105 /** 106 * Simple test to see if we can write and read back a diverse set of queries 107 */ 108 public void testMixedQueries() { 109 // we'll make 10 queries named "query x" and 10 queries named "test x" 110 final String TEST_GROUP_1 = "query "; 111 final String TEST_GROUP_2 = "test "; 112 final String TEST_LINE2 = "line2 "; 113 final int GROUP_COUNT = 10; 114 115 writeEntries(GROUP_COUNT, TEST_GROUP_1, TEST_LINE2); 116 writeEntries(GROUP_COUNT, TEST_GROUP_2, TEST_LINE2); 117 118 // check counts 119 checkOpenCursorCount(2 * GROUP_COUNT); 120 121 // check that each query returns the right result counts 122 checkResultCounts(TEST_GROUP_1, GROUP_COUNT, GROUP_COUNT, null, null); 123 checkResultCounts(TEST_GROUP_2, GROUP_COUNT, GROUP_COUNT, null, null); 124 checkResultCounts(TEST_LINE2, 2 * GROUP_COUNT, 2 * GROUP_COUNT, null, null); 125 } 126 127 /** 128 * Test that the reordering code works properly. The most recently injected queries 129 * should replace existing queries and be sorted to the top of the list. 130 */ 131 public void testReordering() { 132 // first we'll make 10 queries named "group1 x" 133 final int GROUP_1_COUNT = 10; 134 final String GROUP_1_QUERY = "group1 "; 135 final String GROUP_1_LINE2 = "line2 "; 136 writeEntries(GROUP_1_COUNT, GROUP_1_QUERY, GROUP_1_LINE2); 137 138 // check totals 139 checkOpenCursorCount(GROUP_1_COUNT); 140 141 // guarantee that group 1 has older timestamps 142 writeDelay(); 143 144 // next we'll add 10 entries named "group2 x" 145 final int GROUP_2_COUNT = 10; 146 final String GROUP_2_QUERY = "group2 "; 147 final String GROUP_2_LINE2 = "line2 "; 148 writeEntries(GROUP_2_COUNT, GROUP_2_QUERY, GROUP_2_LINE2); 149 150 // check totals 151 checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); 152 153 // guarantee that group 2 has older timestamps 154 writeDelay(); 155 156 // now refresh 5 of the 10 from group 1 157 // change line2 so they can be more easily tracked 158 final int GROUP_3_COUNT = 5; 159 final String GROUP_3_QUERY = GROUP_1_QUERY; 160 final String GROUP_3_LINE2 = "refreshed "; 161 writeEntries(GROUP_3_COUNT, GROUP_3_QUERY, GROUP_3_LINE2); 162 163 // confirm that the total didn't change (those were replacements, not adds) 164 checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); 165 166 // confirm that the are now 5 in group 1, 10 in group 2, and 5 in group 3 167 int newGroup1Count = GROUP_1_COUNT - GROUP_3_COUNT; 168 checkResultCounts(GROUP_1_QUERY, newGroup1Count, newGroup1Count, null, GROUP_1_LINE2); 169 checkResultCounts(GROUP_2_QUERY, GROUP_2_COUNT, GROUP_2_COUNT, null, null); 170 checkResultCounts(GROUP_3_QUERY, GROUP_3_COUNT, GROUP_3_COUNT, null, GROUP_3_LINE2); 171 172 // finally, spot check that the right groups are in the right places 173 // the ordering should be group 3 (newest), group 2, group 1 (oldest) 174 Cursor c = getQueryCursor(null); 175 int colQuery = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_QUERY); 176 int colDisplay1 = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1); 177 int colDisplay2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); 178 179 // Spot check the first and last expected entries of group 3 180 c.moveToPosition(0); 181 assertTrue("group 3 did not properly reorder to head of list", 182 checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_3_QUERY, GROUP_3_LINE2)); 183 c.move(GROUP_3_COUNT - 1); 184 assertTrue("group 3 did not properly reorder to head of list", 185 checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_3_QUERY, GROUP_3_LINE2)); 186 187 // Spot check the first and last expected entries of group 2 188 c.move(1); 189 assertTrue("group 2 not in expected position after reordering", 190 checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_2_QUERY, GROUP_2_LINE2)); 191 c.move(GROUP_2_COUNT - 1); 192 assertTrue("group 2 not in expected position after reordering", 193 checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_2_QUERY, GROUP_2_LINE2)); 194 195 // Spot check the first and last expected entries of group 1 196 c.move(1); 197 assertTrue("group 1 not in expected position after reordering", 198 checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_1_QUERY, GROUP_1_LINE2)); 199 c.move(newGroup1Count - 1); 200 assertTrue("group 1 not in expected position after reordering", 201 checkRow(c, colQuery, colDisplay1, colDisplay2, GROUP_1_QUERY, GROUP_1_LINE2)); 202 203 c.close(); 204 } 205 206 /** 207 * Test that the pruning code works properly, The database should not go beyond 250 entries, 208 * and the oldest entries should always be discarded first. 209 * 210 * TODO: This is a slow test, do we have annotation for that? 211 */ 212 public void testPruning() { 213 // first we'll make 50 queries named "group1 x" 214 final int GROUP_1_COUNT = 50; 215 final String GROUP_1_QUERY = "group1 "; 216 final String GROUP_1_LINE2 = "line2 "; 217 writeEntries(GROUP_1_COUNT, GROUP_1_QUERY, GROUP_1_LINE2); 218 219 // check totals 220 checkOpenCursorCount(GROUP_1_COUNT); 221 222 // guarantee that group 1 has older timestamps (and will be pruned first) 223 writeDelay(); 224 225 // next we'll add 200 entries named "group2 x" 226 final int GROUP_2_COUNT = 200; 227 final String GROUP_2_QUERY = "group2 "; 228 final String GROUP_2_LINE2 = "line2 "; 229 writeEntries(GROUP_2_COUNT, GROUP_2_QUERY, GROUP_2_LINE2); 230 231 // check totals 232 checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); 233 234 // Finally we'll add 10 more entries named "group3 x" 235 // These should push out 10 entries from group 1 236 final int GROUP_3_COUNT = 10; 237 final String GROUP_3_QUERY = "group3 "; 238 final String GROUP_3_LINE2 = "line2 "; 239 writeEntries(GROUP_3_COUNT, GROUP_3_QUERY, GROUP_3_LINE2); 240 241 // total should still be 250 242 checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); 243 244 // there should be 40 group 1, 200 group 2, and 10 group 3 245 int group1NewCount = GROUP_1_COUNT-GROUP_3_COUNT; 246 checkResultCounts(GROUP_1_QUERY, group1NewCount, group1NewCount, null, null); 247 checkResultCounts(GROUP_2_QUERY, GROUP_2_COUNT, GROUP_2_COUNT, null, null); 248 checkResultCounts(GROUP_3_QUERY, GROUP_3_COUNT, GROUP_3_COUNT, null, null); 249 } 250 251 /** 252 * Test that the clear history code works properly. 253 */ 254 public void testClear() { 255 // first we'll make 10 queries named "group1 x" 256 final int GROUP_1_COUNT = 10; 257 final String GROUP_1_QUERY = "group1 "; 258 final String GROUP_1_LINE2 = "line2 "; 259 writeEntries(GROUP_1_COUNT, GROUP_1_QUERY, GROUP_1_LINE2); 260 261 // next we'll add 10 entries named "group2 x" 262 final int GROUP_2_COUNT = 10; 263 final String GROUP_2_QUERY = "group2 "; 264 final String GROUP_2_LINE2 = "line2 "; 265 writeEntries(GROUP_2_COUNT, GROUP_2_QUERY, GROUP_2_LINE2); 266 267 // check totals 268 checkOpenCursorCount(GROUP_1_COUNT + GROUP_2_COUNT); 269 270 // delete all 271 mSearchHelper.clearHistory(); 272 273 // check totals 274 checkOpenCursorCount(0); 275 } 276 277 /** 278 * Write a sequence of queries into the database, with incrementing counters in the strings. 279 */ 280 private void writeEntries(int groupCount, String line1Base, String line2Base) { 281 for (int i = 0; i < groupCount; i++) { 282 final String line1 = line1Base + i; 283 final String line2 = line2Base + i; 284 mSearchHelper.saveRecentQuery(line1, line2); 285 } 286 } 287 288 /** 289 * A very slight delay to ensure that successive groups of queries in the DB cannot 290 * have the same timestamp. 291 */ 292 private void writeDelay() { 293 try { 294 Thread.sleep(10); 295 } catch (InterruptedException e) { 296 fail("Interrupted sleep."); 297 } 298 } 299 300 /** 301 * Access an "open" (no selection) suggestions cursor and confirm that it has the specified 302 * number of entries. 303 * 304 * @param expectCount The expected number of entries returned by the cursor. 305 */ 306 private void checkOpenCursorCount(int expectCount) { 307 Cursor c = getQueryCursor(null); 308 assertEquals(expectCount, c.getCount()); 309 c.close(); 310 } 311 312 /** 313 * Set up a filter cursor and then scan it for specific results. 314 * 315 * @param queryString The query string to apply. 316 * @param minRows The minimum number of matching rows that must be found. 317 * @param maxRows The maximum number of matching rows that must be found. 318 * @param matchDisplay1 If non-null, must match DISPLAY1 column if row counts as match 319 * @param matchDisplay2 If non-null, must match DISPLAY2 column if row counts as match 320 */ 321 private void checkResultCounts(String queryString, int minRows, int maxRows, 322 String matchDisplay1, String matchDisplay2) { 323 324 // get the cursor and apply sanity checks to result 325 Cursor c = getQueryCursor(queryString); 326 assertNotNull(c); 327 assertTrue("Insufficient rows in filtered cursor", c.getCount() >= minRows); 328 329 // look for minimum set of columns (note, display2 is optional) 330 int colQuery = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_QUERY); 331 int colDisplay1 = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1); 332 int colDisplay2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); 333 334 // now loop through rows and look for desired rows 335 int foundRows = 0; 336 c.moveToFirst(); 337 while (!c.isAfterLast()) { 338 if (checkRow(c, colQuery, colDisplay1, colDisplay2, matchDisplay1, matchDisplay2)) { 339 foundRows++; 340 } 341 c.moveToNext(); 342 } 343 344 // now check the results 345 assertTrue(minRows <= foundRows); 346 assertTrue(foundRows <= maxRows); 347 348 c.close(); 349 } 350 351 /** 352 * Check a single row for equality with target strings. 353 * 354 * @param c The cursor, already moved to the row 355 * @param colQuery The column # containing the query. The query must match display1. 356 * @param colDisp1 The column # containing display line 1. 357 * @param colDisp2 The column # containing display line 2, or -1 if no column 358 * @param matchDisplay1 If non-null, this must be the prefix of display1 359 * @param matchDisplay2 If non-null, this must be the prefix of display2 360 * @return Returns true if the row is a "match" 361 */ 362 private boolean checkRow(Cursor c, int colQuery, int colDisp1, int colDisp2, 363 String matchDisplay1, String matchDisplay2) { 364 // Get the data from the row 365 String query = c.getString(colQuery); 366 String display1 = c.getString(colDisp1); 367 String display2 = (colDisp2 >= 0) ? c.getString(colDisp2) : null; 368 369 assertEquals(query, display1); 370 boolean result = true; 371 if (matchDisplay1 != null) { 372 result = result && (display1 != null) && display1.startsWith(matchDisplay1); 373 } 374 if (matchDisplay2 != null) { 375 result = result && (display2 != null) && display2.startsWith(matchDisplay2); 376 } 377 378 return result; 379 } 380 381 /** 382 * Generate a query cursor in a manner like the search dialog would. 383 * 384 * @param queryString The search string, or, null for "all" 385 * @return Returns a cursor, or null if there was some problem. Be sure to close the cursor 386 * when done with it. 387 */ 388 private Cursor getQueryCursor(String queryString) { 389 ContentResolver cr = getMockContext().getContentResolver(); 390 391 String uriStr = "content://" + TestProvider.AUTHORITY + 392 '/' + SearchManager.SUGGEST_URI_PATH_QUERY; 393 Uri contentUri = Uri.parse(uriStr); 394 395 String[] selArgs = new String[] {queryString}; 396 397 Cursor c = cr.query(contentUri, null, null, selArgs, null); 398 399 assertNotNull(c); 400 return c; 401 } 402} 403