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