summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jeff Sharkey <jsharkey@android.com> 2012-01-08 16:41:36 -0800
committer Jeff Sharkey <jsharkey@android.com> 2012-01-13 15:27:28 -0800
commita27a3e8ad7d20dea63ef2d5cb8b6ec7e56c20a89 (patch)
treea233bcf4a407daa0652fd6229930aaaa348e2f35
parent6a78cd85867c5f22e4e82259b81fab46088331ad (diff)
Introduce FileRotator.
Utility that rotates files over time, similar to logrotate. There is a single "active" file, which is periodically rotated into historical files, and eventually deleted entirely. Files are stored under a specific directory with a well-known prefix. Bug: 5386531 Change-Id: I29f821a881247e50ce0f6f73b20bbd020db39e43
-rw-r--r--core/java/com/android/internal/util/FileRotator.java330
-rw-r--r--core/tests/coretests/src/com/android/internal/util/FileRotatorTest.java428
2 files changed, 758 insertions, 0 deletions
diff --git a/core/java/com/android/internal/util/FileRotator.java b/core/java/com/android/internal/util/FileRotator.java
new file mode 100644
index 000000000000..3ce95e7d506a
--- /dev/null
+++ b/core/java/com/android/internal/util/FileRotator.java
@@ -0,0 +1,330 @@
+/*
+ * 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 android.os.FileUtils;
+
+import com.android.internal.util.FileRotator.Reader;
+import com.android.internal.util.FileRotator.Writer;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import libcore.io.IoUtils;
+
+/**
+ * Utility that rotates files over time, similar to {@code logrotate}. There is
+ * a single "active" file, which is periodically rotated into historical files,
+ * and eventually deleted entirely. Files are stored under a specific directory
+ * with a well-known prefix.
+ * <p>
+ * Instead of manipulating files directly, users implement interfaces that
+ * perform operations on {@link InputStream} and {@link OutputStream}. This
+ * enables atomic rewriting of file contents in
+ * {@link #combineActive(Reader, Writer, long)}.
+ * <p>
+ * Users must periodically call {@link #maybeRotate(long)} to perform actual
+ * rotation. Not inherently thread safe.
+ */
+public class FileRotator {
+ private final File mBasePath;
+ private final String mPrefix;
+ private final long mRotateAgeMillis;
+ private final long mDeleteAgeMillis;
+
+ private static final String SUFFIX_BACKUP = ".backup";
+ private static final String SUFFIX_NO_BACKUP = ".no_backup";
+
+ // TODO: provide method to append to active file
+
+ /**
+ * External class that reads data from a given {@link InputStream}. May be
+ * called multiple times when reading rotated data.
+ */
+ public interface Reader {
+ public void read(InputStream in) throws IOException;
+ }
+
+ /**
+ * External class that writes data to a given {@link OutputStream}.
+ */
+ public interface Writer {
+ public void write(OutputStream out) throws IOException;
+ }
+
+ /**
+ * Create a file rotator.
+ *
+ * @param basePath Directory under which all files will be placed.
+ * @param prefix Filename prefix used to identify this rotator.
+ * @param rotateAgeMillis Age in milliseconds beyond which an active file
+ * may be rotated into a historical file.
+ * @param deleteAgeMillis Age in milliseconds beyond which a rotated file
+ * may be deleted.
+ */
+ public FileRotator(File basePath, String prefix, long rotateAgeMillis, long deleteAgeMillis) {
+ mBasePath = Preconditions.checkNotNull(basePath);
+ mPrefix = Preconditions.checkNotNull(prefix);
+ mRotateAgeMillis = rotateAgeMillis;
+ mDeleteAgeMillis = deleteAgeMillis;
+
+ // ensure that base path exists
+ mBasePath.mkdirs();
+
+ // recover any backup files
+ for (String name : mBasePath.list()) {
+ if (!name.startsWith(mPrefix)) continue;
+
+ if (name.endsWith(SUFFIX_BACKUP)) {
+ final File backupFile = new File(mBasePath, name);
+ final File file = new File(
+ mBasePath, name.substring(0, name.length() - SUFFIX_BACKUP.length()));
+
+ // write failed with backup; recover last file
+ backupFile.renameTo(file);
+
+ } else if (name.endsWith(SUFFIX_NO_BACKUP)) {
+ final File noBackupFile = new File(mBasePath, name);
+ final File file = new File(
+ mBasePath, name.substring(0, name.length() - SUFFIX_NO_BACKUP.length()));
+
+ // write failed without backup; delete both
+ noBackupFile.delete();
+ file.delete();
+ }
+ }
+ }
+
+ /**
+ * Atomically combine data with existing data in currently active file.
+ * Maintains a backup during write, which is restored if the write fails.
+ */
+ public void combineActive(Reader reader, Writer writer, long currentTimeMillis)
+ throws IOException {
+ final String activeName = getActiveName(currentTimeMillis);
+
+ final File file = new File(mBasePath, activeName);
+ final File backupFile;
+
+ if (file.exists()) {
+ // read existing data
+ readFile(file, reader);
+
+ // backup existing data during write
+ backupFile = new File(mBasePath, activeName + SUFFIX_BACKUP);
+ file.renameTo(backupFile);
+
+ try {
+ writeFile(file, writer);
+
+ // write success, delete backup
+ backupFile.delete();
+ } catch (IOException e) {
+ // write failed, delete file and restore backup
+ file.delete();
+ backupFile.renameTo(file);
+ throw e;
+ }
+
+ } else {
+ // create empty backup during write
+ backupFile = new File(mBasePath, activeName + SUFFIX_NO_BACKUP);
+ backupFile.createNewFile();
+
+ try {
+ writeFile(file, writer);
+
+ // write success, delete empty backup
+ backupFile.delete();
+ } catch (IOException e) {
+ // write failed, delete file and empty backup
+ file.delete();
+ backupFile.delete();
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * Read any rotated data that overlap the requested time range.
+ */
+ public void readMatching(Reader reader, long matchStartMillis, long matchEndMillis)
+ throws IOException {
+ final FileInfo info = new FileInfo(mPrefix);
+ for (String name : mBasePath.list()) {
+ if (!info.parse(name)) continue;
+
+ // read file when it overlaps
+ if (info.startMillis <= matchEndMillis && matchStartMillis <= info.endMillis) {
+ final File file = new File(mBasePath, name);
+ readFile(file, reader);
+ }
+ }
+ }
+
+ /**
+ * Return the currently active file, which may not exist yet.
+ */
+ private String getActiveName(long currentTimeMillis) {
+ String oldestActiveName = null;
+ long oldestActiveStart = Long.MAX_VALUE;
+
+ final FileInfo info = new FileInfo(mPrefix);
+ for (String name : mBasePath.list()) {
+ if (!info.parse(name)) continue;
+
+ // pick the oldest active file which covers current time
+ if (info.isActive() && info.startMillis < currentTimeMillis
+ && info.startMillis < oldestActiveStart) {
+ oldestActiveName = name;
+ oldestActiveStart = info.startMillis;
+ }
+ }
+
+ if (oldestActiveName != null) {
+ return oldestActiveName;
+ } else {
+ // no active file found above; create one starting now
+ info.startMillis = currentTimeMillis;
+ info.endMillis = Long.MAX_VALUE;
+ return info.build();
+ }
+ }
+
+ /**
+ * Examine all files managed by this rotator, renaming or deleting if their
+ * age matches the configured thresholds.
+ */
+ public void maybeRotate(long currentTimeMillis) {
+ final long rotateBefore = currentTimeMillis - mRotateAgeMillis;
+ final long deleteBefore = currentTimeMillis - mDeleteAgeMillis;
+
+ final FileInfo info = new FileInfo(mPrefix);
+ for (String name : mBasePath.list()) {
+ if (!info.parse(name)) continue;
+
+ if (info.isActive()) {
+ // found active file; rotate if old enough
+ if (info.startMillis < rotateBefore) {
+ info.endMillis = currentTimeMillis;
+
+ final File file = new File(mBasePath, name);
+ final File destFile = new File(mBasePath, info.build());
+ file.renameTo(destFile);
+ }
+ } else if (info.endMillis < deleteBefore) {
+ // found rotated file; delete if old enough
+ final File file = new File(mBasePath, name);
+ file.delete();
+ }
+ }
+ }
+
+ private static void readFile(File file, Reader reader) throws IOException {
+ final FileInputStream fis = new FileInputStream(file);
+ final BufferedInputStream bis = new BufferedInputStream(fis);
+ try {
+ reader.read(bis);
+ } finally {
+ IoUtils.closeQuietly(bis);
+ }
+ }
+
+ private static void writeFile(File file, Writer writer) throws IOException {
+ final FileOutputStream fos = new FileOutputStream(file);
+ final BufferedOutputStream bos = new BufferedOutputStream(fos);
+ try {
+ writer.write(bos);
+ bos.flush();
+ } finally {
+ FileUtils.sync(fos);
+ IoUtils.closeQuietly(bos);
+ }
+ }
+
+ /**
+ * Details for a rotated file, either parsed from an existing filename, or
+ * ready to be built into a new filename.
+ */
+ private static class FileInfo {
+ public final String prefix;
+
+ public long startMillis;
+ public long endMillis;
+
+ public FileInfo(String prefix) {
+ this.prefix = Preconditions.checkNotNull(prefix);
+ }
+
+ /**
+ * Attempt parsing the given filename.
+ *
+ * @return Whether parsing was successful.
+ */
+ public boolean parse(String name) {
+ startMillis = endMillis = -1;
+
+ final int dotIndex = name.lastIndexOf('.');
+ final int dashIndex = name.lastIndexOf('-');
+
+ // skip when missing time section
+ if (dotIndex == -1 || dashIndex == -1) return false;
+
+ // skip when prefix doesn't match
+ if (!prefix.equals(name.substring(0, dotIndex))) return false;
+
+ try {
+ startMillis = Long.parseLong(name.substring(dotIndex + 1, dashIndex));
+
+ if (name.length() - dashIndex == 1) {
+ endMillis = Long.MAX_VALUE;
+ } else {
+ endMillis = Long.parseLong(name.substring(dashIndex + 1));
+ }
+
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Build current state into filename.
+ */
+ public String build() {
+ final StringBuilder name = new StringBuilder();
+ name.append(prefix).append('.').append(startMillis).append('-');
+ if (endMillis != Long.MAX_VALUE) {
+ name.append(endMillis);
+ }
+ return name.toString();
+ }
+
+ /**
+ * Test if current file is active (no end timestamp).
+ */
+ public boolean isActive() {
+ return endMillis == Long.MAX_VALUE;
+ }
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/util/FileRotatorTest.java b/core/tests/coretests/src/com/android/internal/util/FileRotatorTest.java
new file mode 100644
index 000000000000..94d1cb6b5b89
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/util/FileRotatorTest.java
@@ -0,0 +1,428 @@
+/*
+ * 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 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 ProtocolException("yikes");
+ }
+ }, currentTime);
+
+ fail("woah, somehow able to write exception");
+ } catch (ProtocolException 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");
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+}