blob: 2a911c45b72568fdfbcfaeb88120c687ef323417 [file] [log] [blame]
/*
* 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();
}
}
}