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