1/*
2 * Copyright (C) 2010 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 com.android.email.provider;
18
19import com.android.email.provider.ContentCache.CacheToken;
20import com.android.email.provider.ContentCache.CachedCursor;
21import com.android.email.provider.ContentCache.TokenList;
22import com.android.emailcommon.provider.Account;
23import com.android.emailcommon.provider.EmailContent;
24import com.android.emailcommon.provider.Mailbox;
25
26import android.content.ContentResolver;
27import android.content.ContentUris;
28import android.content.Context;
29import android.database.Cursor;
30import android.database.CursorWrapper;
31import android.database.MatrixCursor;
32import android.net.Uri;
33import android.test.ProviderTestCase2;
34
35/**
36 * Tests of ContentCache
37 *
38 * You can run this entire test case with:
39 *   runtest -c com.android.email.provider.ContentCacheTests email
40 */
41public class ContentCacheTests extends ProviderTestCase2<EmailProvider> {
42
43    EmailProvider mProvider;
44    Context mMockContext;
45
46    public ContentCacheTests() {
47        super(EmailProvider.class, EmailContent.AUTHORITY);
48    }
49
50    @Override
51    public void setUp() throws Exception {
52        super.setUp();
53        mMockContext = getMockContext();
54    }
55
56    @Override
57    public void tearDown() throws Exception {
58        super.tearDown();
59    }
60
61    public void testCounterMap() {
62        ContentCache.CounterMap<String> map = new ContentCache.CounterMap<String>(4);
63        // Make sure we can find added items
64        map.add("1");
65        assertTrue(map.contains("1"));
66        map.add("2");
67        map.add("2");
68        // Make sure we can remove once for each add
69        map.subtract("2");
70        assertTrue(map.contains("2"));
71        map.subtract("2");
72        // Make sure that over-removing throws an exception
73        try {
74            map.subtract("2");
75            fail("Removing a third time should throw an exception");
76        } catch (IllegalStateException e) {
77        }
78        try {
79            map.subtract("3");
80            fail("Removing object never added should throw an exception");
81        } catch (IllegalStateException e) {
82        }
83        // There should only be one item in the map ("1")
84        assertEquals(1, map.size());
85        assertTrue(map.contains("1"));
86    }
87
88    public void testTokenList() {
89        TokenList list = new TokenList("Name");
90
91        // Add two tokens for "1"
92        CacheToken token1a = list.add("1");
93        assertTrue(token1a.isValid());
94        assertEquals("1", token1a.getId());
95        assertEquals(1, list.size());
96        CacheToken token1b = list.add("1");
97        assertTrue(token1b.isValid());
98        assertEquals("1", token1b.getId());
99        assertTrue(token1a.equals(token1b));
100        assertEquals(2, list.size());
101
102        // Add a token for "2"
103        CacheToken token2 = list.add("2");
104        assertFalse(token1a.equals(token2));
105        assertEquals(3, list.size());
106
107        // Invalidate "1"; there should be two tokens invalidated
108        assertEquals(2, list.invalidateTokens("1"));
109        assertFalse(token1a.isValid());
110        assertFalse(token1b.isValid());
111        // Token2 should still be valid
112        assertTrue(token2.isValid());
113        // Only token2 should be in the list now (invalidation removes tokens)
114        assertEquals(1, list.size());
115        assertEquals(token2, list.get(0));
116
117        // Add 3 tokens for "3"
118        CacheToken token3a = list.add("3");
119        CacheToken token3b = list.add("3");
120        CacheToken token3c = list.add("3");
121        // Remove two of them
122        assertTrue(list.remove(token3a));
123        assertTrue(list.remove(token3b));
124        // Removing tokens doesn't invalidate them
125        assertTrue(token3a.isValid());
126        assertTrue(token3b.isValid());
127        assertTrue(token3c.isValid());
128        // There should be two items left "3" and "2"
129        assertEquals(2, list.size());
130    }
131
132    public void testCachedCursors() {
133        final ContentResolver resolver = mMockContext.getContentResolver();
134        final Context context = mMockContext;
135
136        // Create account and two mailboxes
137        Account acct = ProviderTestUtils.setupAccount("account", true, context);
138        ProviderTestUtils.setupMailbox("box1", acct.mId, true, context);
139        Mailbox box = ProviderTestUtils.setupMailbox("box2", acct.mId, true, context);
140
141        // We need to test with a query that only returns one row (others can't be put in a
142        // CachedCursor)
143        Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, box.mId);
144        Cursor cursor =
145            resolver.query(uri, Mailbox.CONTENT_PROJECTION, null, null, null);
146        // ContentResolver gives us back a wrapper
147        assertTrue(cursor instanceof CursorWrapper);
148        // The wrappedCursor should be a CachedCursor
149        Cursor wrappedCursor = ((CursorWrapper)cursor).getWrappedCursor();
150        assertTrue(wrappedCursor instanceof CachedCursor);
151        CachedCursor cachedCursor = (CachedCursor)wrappedCursor;
152        // The cursor wrapped in cachedCursor is the underlying cursor
153        Cursor activeCursor = cachedCursor.getWrappedCursor();
154
155        // The cursor should be in active cursors
156        int activeCount = ContentCache.sActiveCursors.getCount(activeCursor);
157        assertEquals(1, activeCount);
158
159        // Some basic functionality that shouldn't throw exceptions and should otherwise act as the
160        // underlying cursor would
161        String[] columnNames = cursor.getColumnNames();
162        assertEquals(Mailbox.CONTENT_PROJECTION.length, columnNames.length);
163        for (int i = 0; i < Mailbox.CONTENT_PROJECTION.length; i++) {
164            assertEquals(Mailbox.CONTENT_PROJECTION[i], columnNames[i]);
165        }
166
167        assertEquals(1, cursor.getCount());
168        cursor.moveToNext();
169        assertEquals(0, cursor.getPosition());
170        cursor.moveToPosition(0);
171        assertEquals(0, cursor.getPosition());
172        assertFalse(cursor.moveToPosition(1));
173
174        cursor.close();
175        // We've closed the cached cursor; make sure
176        assertTrue(cachedCursor.isClosed());
177        // The underlying cursor shouldn't be closed because it's in a cache (we'll test
178        // that in testContentCache)
179        assertFalse(activeCursor.isClosed());
180        // Our cursor should no longer be in the active cursors map
181        assertFalse(ContentCache.sActiveCursors.contains(activeCursor));
182
183        // TODO - change the code or the test to enforce the assertion that a cached cursor
184        // should have only zero or one rows.  We cannot test this in the constructor, however,
185        // due to potential for deadlock.
186//        // Make sure that we won't accept cursors with multiple rows
187//        cursor = resolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, null, null, null);
188//        try {
189//            cursor = new CachedCursor(cursor, null, "Foo");
190//            fail("Mustn't accept cursor with more than one row");
191//        } catch (IllegalArgumentException e) {
192//            // Correct
193//        }
194    }
195
196    private static final String[] SIMPLE_PROJECTION = new String[] {"Foo"};
197    private static final Object[] SIMPLE_ROW = new Object[] {"Bar"};
198    private Cursor getOneRowCursor() {
199        MatrixCursor cursor = new MatrixCursor(SIMPLE_PROJECTION, 1);
200        cursor.addRow(SIMPLE_ROW);
201        return cursor;
202    }
203
204    public void testContentCacheRemoveEldestEntry() {
205        // Create a cache of size 2
206        ContentCache cache = new ContentCache("Name", SIMPLE_PROJECTION, 2);
207        // Random cursor; what's in it doesn't matter
208        Cursor cursor1 = getOneRowCursor();
209        // Get a token for arbitrary object named "1"
210        CacheToken token = cache.getCacheToken("1");
211        // Put the cursor in the cache
212        cache.putCursor(cursor1, "1", SIMPLE_PROJECTION, token);
213        assertEquals(1, cache.size());
214
215        // Add another random cursor; what's in it doesn't matter
216        Cursor cursor2 = getOneRowCursor();
217        // Get a token for arbitrary object named "2"
218        token = cache.getCacheToken("2");
219        // Put the cursor in the cache
220        cache.putCursor(cursor1, "2", SIMPLE_PROJECTION, token);
221        assertEquals(2, cache.size());
222
223        // We should be able to find both now in the cache
224        Cursor cachedCursor = cache.getCachedCursor("1", SIMPLE_PROJECTION);
225        assertNotNull(cachedCursor);
226        assertTrue(cachedCursor instanceof CachedCursor);
227        cachedCursor = cache.getCachedCursor("2", SIMPLE_PROJECTION);
228        assertNotNull(cachedCursor);
229        assertTrue(cachedCursor instanceof CachedCursor);
230
231        // Both cursors should be open
232        assertFalse(cursor1.isClosed());
233        assertFalse(cursor2.isClosed());
234
235        // Add another random cursor; what's in it doesn't matter
236        Cursor cursor3 = getOneRowCursor();
237        // Get a token for arbitrary object named "3"
238        token = cache.getCacheToken("3");
239        // Put the cursor in the cache
240        cache.putCursor(cursor1, "3", SIMPLE_PROJECTION, token);
241        // We should never have more than 2 entries in the cache
242        assertEquals(2, cache.size());
243
244        // The first cursor we added should no longer be in the cache (it's the eldest)
245        cachedCursor = cache.getCachedCursor("1", SIMPLE_PROJECTION);
246        assertNull(cachedCursor);
247        // The cursors for 2 and 3 should be cached
248        cachedCursor = cache.getCachedCursor("2", SIMPLE_PROJECTION);
249        assertNotNull(cachedCursor);
250        assertTrue(cachedCursor instanceof CachedCursor);
251        cachedCursor = cache.getCachedCursor("3", SIMPLE_PROJECTION);
252        assertNotNull(cachedCursor);
253        assertTrue(cachedCursor instanceof CachedCursor);
254
255        // Even cursor1 should be open, since all cached cursors are in mActiveCursors until closed
256        assertFalse(cursor1.isClosed());
257        assertFalse(cursor2.isClosed());
258        assertFalse(cursor3.isClosed());
259    }
260
261    public void testCloseCachedCursor() {
262        // Create a cache of size 2
263        ContentCache cache = new ContentCache("Name", SIMPLE_PROJECTION, 2);
264        // Random cursor; what's in it doesn't matter
265        Cursor underlyingCursor = getOneRowCursor();
266        Cursor cachedCursor1 = new CachedCursor(underlyingCursor, cache, "1");
267        Cursor cachedCursor2 = new CachedCursor(underlyingCursor, cache, "1");
268        assertEquals(2, ContentCache.sActiveCursors.getCount(underlyingCursor));
269        cachedCursor1.close();
270        assertTrue(cachedCursor1.isClosed());
271        // Underlying cursor should be open (still one cached cursor open)
272        assertFalse(underlyingCursor.isClosed());
273        cachedCursor2.close();
274        assertTrue(cachedCursor2.isClosed());
275        assertEquals(0, ContentCache.sActiveCursors.getCount(underlyingCursor));
276        // Underlying cursor should be closed (no cached cursors open)
277        assertTrue(underlyingCursor.isClosed());
278
279        underlyingCursor = getOneRowCursor();
280        cachedCursor1 = cache.putCursor(
281                underlyingCursor, "2", SIMPLE_PROJECTION, cache.getCacheToken("2"));
282        cachedCursor2 = new CachedCursor(underlyingCursor, cache, "2");
283        assertEquals(2, ContentCache.sActiveCursors.getCount(underlyingCursor));
284        cachedCursor1.close();
285        cachedCursor2.close();
286        assertEquals(0, ContentCache.sActiveCursors.getCount(underlyingCursor));
287        // Underlying cursor should still be open; it's in the cache
288        assertFalse(underlyingCursor.isClosed());
289        // Cache a new cursor
290        cachedCursor2 = new CachedCursor(underlyingCursor, cache, "2");
291        assertEquals(1, ContentCache.sActiveCursors.getCount(underlyingCursor));
292        // Remove "2" from the cache and close the cursor
293        cache.invalidate();
294        cachedCursor2.close();
295        // The underlying cursor should now be closed (not in the cache and no cached cursors)
296        assertEquals(0, ContentCache.sActiveCursors.getCount(underlyingCursor));
297        assertTrue(underlyingCursor.isClosed());
298    }
299}
300