/* * Copyright (C) 2012 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.internal.util; import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static android.text.format.DateUtils.SECOND_IN_MILLIS; import static android.text.format.DateUtils.WEEK_IN_MILLIS; import static android.text.format.DateUtils.YEAR_IN_MILLIS; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.Suppress; import android.util.Log; import com.android.internal.util.FileRotator.Reader; import com.android.internal.util.FileRotator.Writer; import com.google.android.collect.Lists; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ProtocolException; import java.util.ArrayList; import java.util.Arrays; import java.util.Random; import junit.framework.Assert; import libcore.io.IoUtils; /** * Tests for {@link FileRotator}. */ public class FileRotatorTest extends AndroidTestCase { private static final String TAG = "FileRotatorTest"; private File mBasePath; private static final String PREFIX = "rotator"; private static final String ANOTHER_PREFIX = "another_rotator"; private static final long TEST_TIME = 1300000000000L; // TODO: test throwing rolls back correctly @Override protected void setUp() throws Exception { super.setUp(); mBasePath = getContext().getFilesDir(); IoUtils.deleteContents(mBasePath); } public void testEmpty() throws Exception { final FileRotator rotate1 = new FileRotator( mBasePath, PREFIX, DAY_IN_MILLIS, WEEK_IN_MILLIS); final FileRotator rotate2 = new FileRotator( mBasePath, ANOTHER_PREFIX, DAY_IN_MILLIS, WEEK_IN_MILLIS); final RecordingReader reader = new RecordingReader(); long currentTime = TEST_TIME; // write single new value rotate1.combineActive(reader, writer("foo"), currentTime); reader.assertRead(); // assert that one rotator doesn't leak into another assertReadAll(rotate1, "foo"); assertReadAll(rotate2); } public void testCombine() throws Exception { final FileRotator rotate = new FileRotator( mBasePath, PREFIX, DAY_IN_MILLIS, WEEK_IN_MILLIS); final RecordingReader reader = new RecordingReader(); long currentTime = TEST_TIME; // first combine should have empty read, but still write data. rotate.combineActive(reader, writer("foo"), currentTime); reader.assertRead(); assertReadAll(rotate, "foo"); // second combine should replace contents; should read existing data, // and write final data to disk. currentTime += SECOND_IN_MILLIS; reader.reset(); rotate.combineActive(reader, writer("bar"), currentTime); reader.assertRead("foo"); assertReadAll(rotate, "bar"); } public void testRotate() throws Exception { final FileRotator rotate = new FileRotator( mBasePath, PREFIX, DAY_IN_MILLIS, WEEK_IN_MILLIS); final RecordingReader reader = new RecordingReader(); long currentTime = TEST_TIME; // combine first record into file rotate.combineActive(reader, writer("foo"), currentTime); reader.assertRead(); assertReadAll(rotate, "foo"); // push time a few minutes forward; shouldn't rotate file reader.reset(); currentTime += MINUTE_IN_MILLIS; rotate.combineActive(reader, writer("bar"), currentTime); reader.assertRead("foo"); assertReadAll(rotate, "bar"); // push time forward enough to rotate file; should still have same data currentTime += DAY_IN_MILLIS + SECOND_IN_MILLIS; rotate.maybeRotate(currentTime); assertReadAll(rotate, "bar"); // combine a second time, should leave rotated value untouched, and // active file should be empty. reader.reset(); rotate.combineActive(reader, writer("baz"), currentTime); reader.assertRead(); assertReadAll(rotate, "bar", "baz"); } public void testDelete() throws Exception { final FileRotator rotate = new FileRotator( mBasePath, PREFIX, MINUTE_IN_MILLIS, DAY_IN_MILLIS); final RecordingReader reader = new RecordingReader(); long currentTime = TEST_TIME; // create first record and trigger rotating it rotate.combineActive(reader, writer("foo"), currentTime); reader.assertRead(); currentTime += MINUTE_IN_MILLIS + SECOND_IN_MILLIS; rotate.maybeRotate(currentTime); // create second record reader.reset(); rotate.combineActive(reader, writer("bar"), currentTime); reader.assertRead(); assertReadAll(rotate, "foo", "bar"); // push time far enough to expire first record currentTime = TEST_TIME + DAY_IN_MILLIS + (2 * MINUTE_IN_MILLIS); rotate.maybeRotate(currentTime); assertReadAll(rotate, "bar"); // push further to delete second record currentTime += WEEK_IN_MILLIS; rotate.maybeRotate(currentTime); assertReadAll(rotate); } public void testThrowRestoresBackup() throws Exception { final FileRotator rotate = new FileRotator( mBasePath, PREFIX, MINUTE_IN_MILLIS, DAY_IN_MILLIS); final RecordingReader reader = new RecordingReader(); long currentTime = TEST_TIME; // first, write some valid data rotate.combineActive(reader, writer("foo"), currentTime); reader.assertRead(); assertReadAll(rotate, "foo"); try { // now, try writing which will throw reader.reset(); rotate.combineActive(reader, new Writer() { public void write(OutputStream out) throws IOException { new DataOutputStream(out).writeUTF("bar"); throw new NullPointerException("yikes"); } }, currentTime); fail("woah, somehow able to write exception"); } catch (IOException e) { // expected from above } // assert that we read original data, and that it's still intact after // the failed write above. reader.assertRead("foo"); assertReadAll(rotate, "foo"); } public void testOtherFilesAndMalformed() throws Exception { final FileRotator rotate = new FileRotator( mBasePath, PREFIX, SECOND_IN_MILLIS, SECOND_IN_MILLIS); // should ignore another prefix touch("another_rotator.1024"); touch("another_rotator.1024-2048"); assertReadAll(rotate); // verify that broken filenames don't crash touch("rotator"); touch("rotator..."); touch("rotator.-"); touch("rotator.---"); touch("rotator.a-b"); touch("rotator_but_not_actually"); assertReadAll(rotate); // and make sure that we can read something from a legit file write("rotator.100-200", "meow"); assertReadAll(rotate, "meow"); } private static final String RED = "red"; private static final String GREEN = "green"; private static final String BLUE = "blue"; private static final String YELLOW = "yellow"; public void testQueryMatch() throws Exception { final FileRotator rotate = new FileRotator( mBasePath, PREFIX, HOUR_IN_MILLIS, YEAR_IN_MILLIS); final RecordingReader reader = new RecordingReader(); long currentTime = TEST_TIME; // rotate a bunch of historical data rotate.maybeRotate(currentTime); rotate.combineActive(reader, writer(RED), currentTime); currentTime += DAY_IN_MILLIS; rotate.maybeRotate(currentTime); rotate.combineActive(reader, writer(GREEN), currentTime); currentTime += DAY_IN_MILLIS; rotate.maybeRotate(currentTime); rotate.combineActive(reader, writer(BLUE), currentTime); currentTime += DAY_IN_MILLIS; rotate.maybeRotate(currentTime); rotate.combineActive(reader, writer(YELLOW), currentTime); final String[] FULL_SET = { RED, GREEN, BLUE, YELLOW }; assertReadAll(rotate, FULL_SET); assertReadMatching(rotate, Long.MIN_VALUE, Long.MAX_VALUE, FULL_SET); assertReadMatching(rotate, Long.MIN_VALUE, currentTime, FULL_SET); assertReadMatching(rotate, TEST_TIME + SECOND_IN_MILLIS, currentTime, FULL_SET); // should omit last value, since it only touches at currentTime assertReadMatching(rotate, TEST_TIME + SECOND_IN_MILLIS, currentTime - SECOND_IN_MILLIS, RED, GREEN, BLUE); // check boundary condition assertReadMatching(rotate, TEST_TIME + DAY_IN_MILLIS, Long.MAX_VALUE, FULL_SET); assertReadMatching(rotate, TEST_TIME + DAY_IN_MILLIS + SECOND_IN_MILLIS, Long.MAX_VALUE, GREEN, BLUE, YELLOW); // test range smaller than file final long blueStart = TEST_TIME + (DAY_IN_MILLIS * 2); final long blueEnd = TEST_TIME + (DAY_IN_MILLIS * 3); assertReadMatching(rotate, blueStart + SECOND_IN_MILLIS, blueEnd - SECOND_IN_MILLIS, BLUE); // outside range should return nothing assertReadMatching(rotate, Long.MIN_VALUE, TEST_TIME - DAY_IN_MILLIS); } public void testClockRollingBackwards() throws Exception { final FileRotator rotate = new FileRotator( mBasePath, PREFIX, DAY_IN_MILLIS, YEAR_IN_MILLIS); final RecordingReader reader = new RecordingReader(); long currentTime = TEST_TIME; // create record at current time // --> foo rotate.combineActive(reader, writer("foo"), currentTime); reader.assertRead(); assertReadAll(rotate, "foo"); // record a day in past; should create a new active file // --> bar currentTime -= DAY_IN_MILLIS; reader.reset(); rotate.combineActive(reader, writer("bar"), currentTime); reader.assertRead(); assertReadAll(rotate, "bar", "foo"); // verify that we rewrite current active file // bar --> baz currentTime += SECOND_IN_MILLIS; reader.reset(); rotate.combineActive(reader, writer("baz"), currentTime); reader.assertRead("bar"); assertReadAll(rotate, "baz", "foo"); // return to present and verify we write oldest active file // baz --> meow currentTime = TEST_TIME + SECOND_IN_MILLIS; reader.reset(); rotate.combineActive(reader, writer("meow"), currentTime); reader.assertRead("baz"); assertReadAll(rotate, "meow", "foo"); // current time should trigger rotate of older active file rotate.maybeRotate(currentTime); // write active file, verify this time we touch original // foo --> yay reader.reset(); rotate.combineActive(reader, writer("yay"), currentTime); reader.assertRead("foo"); assertReadAll(rotate, "meow", "yay"); } @Suppress public void testFuzz() throws Exception { final FileRotator rotate = new FileRotator( mBasePath, PREFIX, HOUR_IN_MILLIS, DAY_IN_MILLIS); final RecordingReader reader = new RecordingReader(); long currentTime = TEST_TIME; // walk forward through time, ensuring that files are cleaned properly final Random random = new Random(); for (int i = 0; i < 1024; i++) { currentTime += Math.abs(random.nextLong()) % DAY_IN_MILLIS; reader.reset(); rotate.combineActive(reader, writer("meow"), currentTime); if (random.nextBoolean()) { rotate.maybeRotate(currentTime); } } rotate.maybeRotate(currentTime); Log.d(TAG, "currentTime=" + currentTime); Log.d(TAG, Arrays.toString(mBasePath.list())); } public void testRecoverAtomic() throws Exception { write("rotator.1024-2048", "foo"); write("rotator.1024-2048.backup", "bar"); write("rotator.2048-4096", "baz"); write("rotator.2048-4096.no_backup", ""); final FileRotator rotate = new FileRotator( mBasePath, PREFIX, SECOND_IN_MILLIS, SECOND_IN_MILLIS); // verify backup value was recovered; no_backup indicates that // corresponding file had no backup and should be discarded. assertReadAll(rotate, "bar"); } public void testFileSystemInaccessible() throws Exception { File inaccessibleDir = null; String dirPath = getContext().getFilesDir() + File.separator + "inaccessible"; inaccessibleDir = new File(dirPath); final FileRotator rotate = new FileRotator(inaccessibleDir, PREFIX, SECOND_IN_MILLIS, SECOND_IN_MILLIS); // rotate should not throw on dir not mkdir-ed (or otherwise inaccessible) rotate.maybeRotate(TEST_TIME); } private void touch(String... names) throws IOException { for (String name : names) { final OutputStream out = new FileOutputStream(new File(mBasePath, name)); out.close(); } } private void write(String name, String value) throws IOException { final DataOutputStream out = new DataOutputStream( new FileOutputStream(new File(mBasePath, name))); out.writeUTF(value); out.close(); } private static Writer writer(final String value) { return new Writer() { public void write(OutputStream out) throws IOException { new DataOutputStream(out).writeUTF(value); } }; } private static void assertReadAll(FileRotator rotate, String... expected) throws IOException { assertReadMatching(rotate, Long.MIN_VALUE, Long.MAX_VALUE, expected); } private static void assertReadMatching( FileRotator rotate, long matchStartMillis, long matchEndMillis, String... expected) throws IOException { final RecordingReader reader = new RecordingReader(); rotate.readMatching(reader, matchStartMillis, matchEndMillis); reader.assertRead(expected); } private static class RecordingReader implements Reader { private ArrayList mActual = Lists.newArrayList(); public void read(InputStream in) throws IOException { mActual.add(new DataInputStream(in).readUTF()); } public void reset() { mActual.clear(); } public void assertRead(String... expected) { assertEquals(expected.length, mActual.size()); final ArrayList actualCopy = new ArrayList(mActual); for (String value : expected) { if (!actualCopy.remove(value)) { final String expectedString = Arrays.toString(expected); final String actualString = Arrays.toString(mActual.toArray()); fail("expected: " + expectedString + " but was: " + actualString); } } } } }