| /* |
| * 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<String> 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<String> actualCopy = new ArrayList<String>(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); |
| } |
| } |
| } |
| } |
| } |