/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.gallery3d.common; import com.android.gallery3d.common.BlobCache; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.LargeTest; import android.util.Log; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.util.Random; public class BlobCacheTest extends AndroidTestCase { private static final String TAG = "BlobCacheTest"; @SmallTest public void testReadIntLong() { byte[] buf = new byte[9]; assertEquals(0, BlobCache.readInt(buf, 0)); assertEquals(0, BlobCache.readLong(buf, 0)); buf[0] = 1; assertEquals(1, BlobCache.readInt(buf, 0)); assertEquals(1, BlobCache.readLong(buf, 0)); buf[3] = 0x7f; assertEquals(0x7f000001, BlobCache.readInt(buf, 0)); assertEquals(0x7f000001, BlobCache.readLong(buf, 0)); assertEquals(0x007f0000, BlobCache.readInt(buf, 1)); assertEquals(0x007f0000, BlobCache.readLong(buf, 1)); buf[3] = (byte) 0x80; buf[7] = (byte) 0xA0; buf[0] = 0; assertEquals(0x80000000, BlobCache.readInt(buf, 0)); assertEquals(0xA000000080000000L, BlobCache.readLong(buf, 0)); for (int i = 0; i < 8; i++) { buf[i] = (byte) (0x11 * (i+8)); } assertEquals(0xbbaa9988, BlobCache.readInt(buf, 0)); assertEquals(0xffeeddccbbaa9988L, BlobCache.readLong(buf, 0)); buf[8] = 0x33; assertEquals(0x33ffeeddccbbaa99L, BlobCache.readLong(buf, 1)); } @SmallTest public void testWriteIntLong() { byte[] buf = new byte[8]; BlobCache.writeInt(buf, 0, 0x12345678); assertEquals(0x78, buf[0]); assertEquals(0x56, buf[1]); assertEquals(0x34, buf[2]); assertEquals(0x12, buf[3]); assertEquals(0x00, buf[4]); BlobCache.writeLong(buf, 0, 0xffeeddccbbaa9988L); for (int i = 0; i < 8; i++) { assertEquals((byte) (0x11 * (i+8)), buf[i]); } } @MediumTest public void testChecksum() throws IOException { BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); byte[] buf = new byte[0]; assertEquals(0x1, bc.checkSum(buf)); buf = new byte[1]; assertEquals(0x10001, bc.checkSum(buf)); buf[0] = 0x47; assertEquals(0x480048, bc.checkSum(buf)); buf = new byte[3]; buf[0] = 0x10; buf[1] = 0x30; buf[2] = 0x01; assertEquals(0x940042, bc.checkSum(buf)); assertEquals(0x310031, bc.checkSum(buf, 1, 1)); assertEquals(0x1, bc.checkSum(buf, 1, 0)); assertEquals(0x630032, bc.checkSum(buf, 1, 2)); buf = new byte[1024]; for (int i = 0; i < buf.length; i++) { buf[i] = (byte)(i*i); } assertEquals(0x3574a610, bc.checkSum(buf)); bc.close(); } private static final int HEADER_SIZE = 32; private static final int DATA_HEADER_SIZE = 4; private static final int BLOB_HEADER_SIZE = 20; private static final String TEST_FILE_NAME = "/sdcard/btest"; private static final int MAX_ENTRIES = 100; private static final int MAX_BYTES = 1000; private static final int INDEX_SIZE = HEADER_SIZE + MAX_ENTRIES * 12 * 2; private static final long KEY_0 = 0x1122334455667788L; private static final long KEY_1 = 0x1122334455667789L; private static final long KEY_2 = 0x112233445566778AL; private static byte[] DATA_0 = new byte[10]; private static byte[] DATA_1 = new byte[10]; @MediumTest public void testBasic() throws IOException { String name = TEST_FILE_NAME; BlobCache bc; File idxFile = new File(name + ".idx"); File data0File = new File(name + ".0"); File data1File = new File(name + ".1"); // Create a brand new cache. bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, true); bc.close(); // Make sure the initial state is correct. assertTrue(idxFile.exists()); assertTrue(data0File.exists()); assertTrue(data1File.exists()); assertEquals(INDEX_SIZE, idxFile.length()); assertEquals(DATA_HEADER_SIZE, data0File.length()); assertEquals(DATA_HEADER_SIZE, data1File.length()); assertEquals(0, bc.getActiveCount()); // Re-open it. bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, false); assertNull(bc.lookup(KEY_0)); // insert one blob genData(DATA_0, 1); bc.insert(KEY_0, DATA_0); assertSameData(DATA_0, bc.lookup(KEY_0)); assertEquals(1, bc.getActiveCount()); bc.close(); // Make sure the file size is right. assertEquals(INDEX_SIZE, idxFile.length()); assertEquals(DATA_HEADER_SIZE + BLOB_HEADER_SIZE + DATA_0.length, data0File.length()); assertEquals(DATA_HEADER_SIZE, data1File.length()); // Re-open it and make sure we can get the old data bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, false); assertSameData(DATA_0, bc.lookup(KEY_0)); // insert with the same key (but using a different blob) genData(DATA_0, 2); bc.insert(KEY_0, DATA_0); assertSameData(DATA_0, bc.lookup(KEY_0)); assertEquals(1, bc.getActiveCount()); bc.close(); // Make sure the file size is right. assertEquals(INDEX_SIZE, idxFile.length()); assertEquals(DATA_HEADER_SIZE + 2 * (BLOB_HEADER_SIZE + DATA_0.length), data0File.length()); assertEquals(DATA_HEADER_SIZE, data1File.length()); // Re-open it and make sure we can get the old data bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, false); assertSameData(DATA_0, bc.lookup(KEY_0)); // insert another key and make sure we can get both key. assertNull(bc.lookup(KEY_1)); genData(DATA_1, 3); bc.insert(KEY_1, DATA_1); assertSameData(DATA_0, bc.lookup(KEY_0)); assertSameData(DATA_1, bc.lookup(KEY_1)); assertEquals(2, bc.getActiveCount()); bc.close(); // Make sure the file size is right. assertEquals(INDEX_SIZE, idxFile.length()); assertEquals(DATA_HEADER_SIZE + 3 * (BLOB_HEADER_SIZE + DATA_0.length), data0File.length()); assertEquals(DATA_HEADER_SIZE, data1File.length()); // Re-open it and make sure we can get the old data bc = new BlobCache(name, 100, 1000, false); assertSameData(DATA_0, bc.lookup(KEY_0)); assertSameData(DATA_1, bc.lookup(KEY_1)); assertEquals(2, bc.getActiveCount()); bc.close(); } @MediumTest public void testNegativeKey() throws IOException { BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); // insert one blob genData(DATA_0, 1); bc.insert(-123, DATA_0); assertSameData(DATA_0, bc.lookup(-123)); bc.close(); } @MediumTest public void testEmptyBlob() throws IOException { BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); byte[] data = new byte[0]; bc.insert(123, data); assertSameData(data, bc.lookup(123)); bc.close(); } @MediumTest public void testLookupRequest() throws IOException { BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); // insert one blob genData(DATA_0, 1); bc.insert(1, DATA_0); assertSameData(DATA_0, bc.lookup(1)); // the same size buffer byte[] buf = new byte[DATA_0.length]; BlobCache.LookupRequest req = new BlobCache.LookupRequest(); req.key = 1; req.buffer = buf; assertTrue(bc.lookup(req)); assertEquals(1, req.key); assertSame(buf, req.buffer); assertEquals(DATA_0.length, req.length); // larger buffer buf = new byte[DATA_0.length + 22]; req = new BlobCache.LookupRequest(); req.key = 1; req.buffer = buf; assertTrue(bc.lookup(req)); assertEquals(1, req.key); assertSame(buf, req.buffer); assertEquals(DATA_0.length, req.length); // smaller buffer buf = new byte[DATA_0.length - 1]; req = new BlobCache.LookupRequest(); req.key = 1; req.buffer = buf; assertTrue(bc.lookup(req)); assertEquals(1, req.key); assertNotSame(buf, req.buffer); assertEquals(DATA_0.length, req.length); assertSameData(DATA_0, req.buffer, DATA_0.length); // null buffer req = new BlobCache.LookupRequest(); req.key = 1; req.buffer = null; assertTrue(bc.lookup(req)); assertEquals(1, req.key); assertNotNull(req.buffer); assertEquals(DATA_0.length, req.length); assertSameData(DATA_0, req.buffer, DATA_0.length); bc.close(); } @MediumTest public void testKeyCollision() throws IOException { BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); for (int i = 0; i < MAX_ENTRIES / 2; i++) { genData(DATA_0, i); long key = KEY_1 + i * MAX_ENTRIES; bc.insert(key, DATA_0); } for (int i = 0; i < MAX_ENTRIES / 2; i++) { genData(DATA_0, i); long key = KEY_1 + i * MAX_ENTRIES; assertSameData(DATA_0, bc.lookup(key)); } bc.close(); } @MediumTest public void testRegionFlip() throws IOException { String name = TEST_FILE_NAME; BlobCache bc; File idxFile = new File(name + ".idx"); File data0File = new File(name + ".0"); File data1File = new File(name + ".1"); // Create a brand new cache. bc = new BlobCache(name, MAX_ENTRIES, MAX_BYTES, true); // This is the number of blobs fits into a region. int maxFit = (MAX_BYTES - DATA_HEADER_SIZE) / (BLOB_HEADER_SIZE + DATA_0.length); for (int k = 0; k < maxFit; k++) { genData(DATA_0, k); bc.insert(k, DATA_0); } assertEquals(maxFit, bc.getActiveCount()); // Make sure the file size is right. assertEquals(INDEX_SIZE, idxFile.length()); assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), data0File.length()); assertEquals(DATA_HEADER_SIZE, data1File.length()); // Now insert another one and let it flip. genData(DATA_0, 777); bc.insert(KEY_1, DATA_0); assertEquals(1, bc.getActiveCount()); assertEquals(INDEX_SIZE, idxFile.length()); assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), data0File.length()); assertEquals(DATA_HEADER_SIZE + 1 * (BLOB_HEADER_SIZE + DATA_0.length), data1File.length()); // Make sure we can find the new data assertSameData(DATA_0, bc.lookup(KEY_1)); // Now find an old blob int old = maxFit / 2; genData(DATA_0, old); assertSameData(DATA_0, bc.lookup(old)); assertEquals(2, bc.getActiveCount()); // Observed data is copied. assertEquals(INDEX_SIZE, idxFile.length()); assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), data0File.length()); assertEquals(DATA_HEADER_SIZE + 2 * (BLOB_HEADER_SIZE + DATA_0.length), data1File.length()); // Now copy everything over (except we should have no space for the last one) assertTrue(old < maxFit - 1); for (int k = 0; k < maxFit; k++) { genData(DATA_0, k); assertSameData(DATA_0, bc.lookup(k)); } assertEquals(maxFit, bc.getActiveCount()); // Now both file should be full. assertEquals(INDEX_SIZE, idxFile.length()); assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), data0File.length()); assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), data1File.length()); // Now insert one to make it flip. genData(DATA_0, 888); bc.insert(KEY_2, DATA_0); assertEquals(1, bc.getActiveCount()); // Check the size after the second flip. assertEquals(INDEX_SIZE, idxFile.length()); assertEquals(DATA_HEADER_SIZE + 1 * (BLOB_HEADER_SIZE + DATA_0.length), data0File.length()); assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), data1File.length()); // Now the last key should be gone. assertNull(bc.lookup(maxFit - 1)); // But others should remain for (int k = 0; k < maxFit - 1; k++) { genData(DATA_0, k); assertSameData(DATA_0, bc.lookup(k)); } assertEquals(maxFit, bc.getActiveCount()); genData(DATA_0, 777); assertSameData(DATA_0, bc.lookup(KEY_1)); genData(DATA_0, 888); assertSameData(DATA_0, bc.lookup(KEY_2)); assertEquals(maxFit, bc.getActiveCount()); // Now two files should be full. assertEquals(INDEX_SIZE, idxFile.length()); assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), data0File.length()); assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), data1File.length()); bc.close(); } @MediumTest public void testEntryLimit() throws IOException { String name = TEST_FILE_NAME; BlobCache bc; File idxFile = new File(name + ".idx"); File data0File = new File(name + ".0"); File data1File = new File(name + ".1"); int maxEntries = 10; int maxFit = maxEntries / 2; int indexSize = HEADER_SIZE + maxEntries * 12 * 2; // Create a brand new cache with a small entry limit. bc = new BlobCache(name, maxEntries, MAX_BYTES, true); // Fill to just before flipping for (int i = 0; i < maxFit; i++) { genData(DATA_0, i); bc.insert(i, DATA_0); } assertEquals(maxFit, bc.getActiveCount()); // Check the file size. assertEquals(indexSize, idxFile.length()); assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), data0File.length()); assertEquals(DATA_HEADER_SIZE, data1File.length()); // Insert one and make it flip genData(DATA_0, 777); bc.insert(777, DATA_0); assertEquals(1, bc.getActiveCount()); // Check the file size. assertEquals(indexSize, idxFile.length()); assertEquals(DATA_HEADER_SIZE + maxFit * (BLOB_HEADER_SIZE + DATA_0.length), data0File.length()); assertEquals(DATA_HEADER_SIZE + 1 * (BLOB_HEADER_SIZE + DATA_0.length), data1File.length()); bc.close(); } @LargeTest public void testDataIntegrity() throws IOException { String name = TEST_FILE_NAME; File idxFile = new File(name + ".idx"); File data0File = new File(name + ".0"); File data1File = new File(name + ".1"); RandomAccessFile f; Log.v(TAG, "It should be readable if the content is not changed."); prepareNewCache(); f = new RandomAccessFile(data0File, "rw"); f.seek(1); byte b = f.readByte(); f.seek(1); f.write(b); f.close(); assertReadable(); Log.v(TAG, "Change the data file magic field"); prepareNewCache(); f = new RandomAccessFile(data0File, "rw"); f.seek(1); f.write(0xFF); f.close(); assertUnreadable(); prepareNewCache(); f = new RandomAccessFile(data1File, "rw"); f.write(0xFF); f.close(); assertUnreadable(); Log.v(TAG, "Change the blob key"); prepareNewCache(); f = new RandomAccessFile(data0File, "rw"); f.seek(4); f.write(0x00); f.close(); assertUnreadable(); Log.v(TAG, "Change the blob checksum"); prepareNewCache(); f = new RandomAccessFile(data0File, "rw"); f.seek(4 + 8); f.write(0x00); f.close(); assertUnreadable(); Log.v(TAG, "Change the blob offset"); prepareNewCache(); f = new RandomAccessFile(data0File, "rw"); f.seek(4 + 12); f.write(0x20); f.close(); assertUnreadable(); Log.v(TAG, "Change the blob length: some other value"); prepareNewCache(); f = new RandomAccessFile(data0File, "rw"); f.seek(4 + 16); f.write(0x20); f.close(); assertUnreadable(); Log.v(TAG, "Change the blob length: -1"); prepareNewCache(); f = new RandomAccessFile(data0File, "rw"); f.seek(4 + 16); f.writeInt(0xFFFFFFFF); f.close(); assertUnreadable(); Log.v(TAG, "Change the blob length: big value"); prepareNewCache(); f = new RandomAccessFile(data0File, "rw"); f.seek(4 + 16); f.writeInt(0xFFFFFF00); f.close(); assertUnreadable(); Log.v(TAG, "Change the blob content"); prepareNewCache(); f = new RandomAccessFile(data0File, "rw"); f.seek(4 + 20); f.write(0x01); f.close(); assertUnreadable(); Log.v(TAG, "Change the index magic"); prepareNewCache(); f = new RandomAccessFile(idxFile, "rw"); f.seek(1); f.write(0x00); f.close(); assertUnreadable(); Log.v(TAG, "Change the active region"); prepareNewCache(); f = new RandomAccessFile(idxFile, "rw"); f.seek(12); f.write(0x01); f.close(); assertUnreadable(); Log.v(TAG, "Change the reserved data"); prepareNewCache(); f = new RandomAccessFile(idxFile, "rw"); f.seek(24); f.write(0x01); f.close(); assertUnreadable(); Log.v(TAG, "Change the checksum"); prepareNewCache(); f = new RandomAccessFile(idxFile, "rw"); f.seek(29); f.write(0x00); f.close(); assertUnreadable(); Log.v(TAG, "Change the key"); prepareNewCache(); f = new RandomAccessFile(idxFile, "rw"); f.seek(32 + 12 * (KEY_1 % MAX_ENTRIES)); f.write(0x00); f.close(); assertUnreadable(); Log.v(TAG, "Change the offset"); prepareNewCache(); f = new RandomAccessFile(idxFile, "rw"); f.seek(32 + 12 * (KEY_1 % MAX_ENTRIES) + 8); f.write(0x05); f.close(); assertUnreadable(); Log.v(TAG, "Change the offset"); prepareNewCache(); f = new RandomAccessFile(idxFile, "rw"); f.seek(32 + 12 * (KEY_1 % MAX_ENTRIES) + 8 + 3); f.write(0xFF); f.close(); assertUnreadable(); Log.v(TAG, "Garbage index"); prepareNewCache(); f = new RandomAccessFile(idxFile, "rw"); int n = (int) idxFile.length(); f.seek(32); byte[] garbage = new byte[1024]; for (int i = 0; i < garbage.length; i++) { garbage[i] = (byte) 0x80; } int i = 32; while (i < n) { int todo = Math.min(garbage.length, n - i); f.write(garbage, 0, todo); i += todo; } f.close(); assertUnreadable(); } // Create a brand new cache and put one entry into it. private void prepareNewCache() throws IOException { BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); genData(DATA_0, 777); bc.insert(KEY_1, DATA_0); bc.close(); } private void assertReadable() throws IOException { BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, false); genData(DATA_0, 777); assertSameData(DATA_0, bc.lookup(KEY_1)); bc.close(); } private void assertUnreadable() throws IOException { BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, false); genData(DATA_0, 777); assertNull(bc.lookup(KEY_1)); bc.close(); } @LargeTest public void testRandomSize() throws IOException { BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, MAX_BYTES, true); // Random size test Random rand = new Random(0); for (int i = 0; i < 100; i++) { byte[] data = new byte[rand.nextInt(MAX_BYTES*12/10)]; try { bc.insert(rand.nextLong(), data); if (data.length > MAX_BYTES - 4 - 20) fail(); } catch (RuntimeException ex) { if (data.length <= MAX_BYTES - 4 - 20) fail(); } } bc.close(); } @LargeTest public void testBandwidth() throws IOException { BlobCache bc = new BlobCache(TEST_FILE_NAME, 1000, 10000000, true); // Write int count = 0; byte[] data = new byte[20000]; long t0 = System.nanoTime(); for (int i = 0; i < 1000; i++) { bc.insert(i, data); count += data.length; } bc.syncAll(); float delta = (System.nanoTime() - t0) * 1e-3f; Log.v(TAG, "write bandwidth = " + (count / delta) + " M/s"); // Copy over BlobCache.LookupRequest req = new BlobCache.LookupRequest(); count = 0; t0 = System.nanoTime(); for (int i = 0; i < 1000; i++) { req.key = i; req.buffer = data; if (bc.lookup(req)) { count += req.length; } } bc.syncAll(); delta = (System.nanoTime() - t0) * 1e-3f; Log.v(TAG, "copy over bandwidth = " + (count / delta) + " M/s"); // Read count = 0; t0 = System.nanoTime(); for (int i = 0; i < 1000; i++) { req.key = i; req.buffer = data; if (bc.lookup(req)) { count += req.length; } } bc.syncAll(); delta = (System.nanoTime() - t0) * 1e-3f; Log.v(TAG, "read bandwidth = " + (count / delta) + " M/s"); bc.close(); } @LargeTest public void testSmallSize() throws IOException { BlobCache bc = new BlobCache(TEST_FILE_NAME, MAX_ENTRIES, 40, true); // Small size test Random rand = new Random(0); for (int i = 0; i < 100; i++) { byte[] data = new byte[rand.nextInt(3)]; bc.insert(rand.nextLong(), data); } bc.close(); } @LargeTest public void testManyEntries() throws IOException { BlobCache bc = new BlobCache(TEST_FILE_NAME, 1, MAX_BYTES, true); // Many entries test Random rand = new Random(0); for (int i = 0; i < 100; i++) { byte[] data = new byte[rand.nextInt(10)]; } bc.close(); } private void genData(byte[] data, int seed) { for(int i = 0; i < data.length; i++) { data[i] = (byte) (seed * i); } } private void assertSameData(byte[] data1, byte[] data2) { if (data1 == null && data2 == null) return; if (data1 == null || data2 == null) fail(); if (data1.length != data2.length) fail(); for (int i = 0; i < data1.length; i++) { if (data1[i] != data2[i]) fail(); } } private void assertSameData(byte[] data1, byte[] data2, int n) { if (data1 == null || data2 == null) fail(); for (int i = 0; i < n; i++) { if (data1[i] != data2[i]) fail(); } } }