Limit tradefed log files to a maximum size.

Refactor the implementation to limit device logcat size to a common
SizeLimitedOutputStream class, and use it for both host log and
device logcat storage. This new implementation also improves the
accuracy in which the 'max data size' attribute is adhered to.

Bug 8095071

Change-Id: I442aa41bb799d2dc3e94385e073333d1e69ae6d9
diff --git a/src/com/android/tradefed/device/BackgroundDeviceAction.java b/src/com/android/tradefed/device/BackgroundDeviceAction.java
index 8e2ca1b..aaeb9e6 100644
--- a/src/com/android/tradefed/device/BackgroundDeviceAction.java
+++ b/src/com/android/tradefed/device/BackgroundDeviceAction.java
@@ -104,8 +104,9 @@
 
         // FIXME: Determine when we should append a message to the receiver.
         if (mReceiver instanceof LargeOutputReceiver) {
-            ((LargeOutputReceiver) mReceiver).appendLogMsg(String.format(
-                    "%s interrupted. May see duplicated content in log.", mDescriptor));
+            byte[] stringData = String.format(
+                    "%s interrupted. May see duplicated content in log.", mDescriptor).getBytes();
+            mReceiver.addOutput(stringData, 0, stringData.length);
         }
 
         // Make sure we haven't been cancelled before we sleep for a long time
diff --git a/src/com/android/tradefed/device/LargeOutputReceiver.java b/src/com/android/tradefed/device/LargeOutputReceiver.java
index 989ecd6..fb8f7ba 100644
--- a/src/com/android/tradefed/device/LargeOutputReceiver.java
+++ b/src/com/android/tradefed/device/LargeOutputReceiver.java
@@ -22,60 +22,38 @@
 import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.SnapshotInputStreamSource;
-import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.SizeLimitedOutputStream;
 
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.SequenceInputStream;
 
 /**
  * A class designed to help run long running commands collect output.
  * <p>
  * The maximum size of the tmp file is limited to approximately {@code maxFileSize}.
- * To prevent data loss when the limit has been reached, this file keeps two tmp host
+ * To prevent data loss when the limit has been reached, this file keeps set of tmp host
  * files.
  * </p>
  */
 public class LargeOutputReceiver implements IShellOutputReceiver {
-    /** The max number of bytes to store in the buffer */
-    public static final int BUFF_SIZE = 32 * 1024;
-
     private String mSerialNumber;
     private String mDescriptor;
-    private long mMaxFileSize;
 
     private boolean mIsCancelled = false;
-    private OutputStream mOutStream;
-    /** the archived previous temp file */
-    private File mPreviousTmpFile = null;
-    /** the current temp file which data will be streamed into */
-    private File mTmpFile = null;
-    private long mTmpBytesStored = 0;
+    private SizeLimitedOutputStream mOutStream;
+    private long mMaxDataSize;
 
     /**
      * Creates a {@link LargeOutputReceiver}.
      *
      * @param descriptor the descriptor of the command to run. For logging only.
      * @param serialNumber the serial number of the device. For logging only.
-     * @param maxFileSize the max file size of the tmp backing file in bytes.  Since the receiver
-     * keeps two tmp host files, the size of the output can be up to twice {@code maxFileSize}.
+     * @param maxDataSize the approximate max amount of data to keep.
      */
-    public LargeOutputReceiver(String descriptor, String serialNumber, long maxFileSize) {
+    public LargeOutputReceiver(String descriptor, String serialNumber, long maxDataSize) {
         mDescriptor = descriptor;
         mSerialNumber = serialNumber;
-        mMaxFileSize = maxFileSize;
-
-        try {
-            createTmpFile();
-        }  catch (IOException e) {
-            CLog.w("failed to create %s file for %s.", mDescriptor, mSerialNumber);
-        }
+        mMaxDataSize = maxDataSize;
+        mOutStream = createOutputStream();
     }
 
     /**
@@ -88,12 +66,6 @@
         }
         try {
             mOutStream.write(data, offset, length);
-            mTmpBytesStored += length;
-            if (mTmpBytesStored > mMaxFileSize) {
-                CLog.i("Max tmp %s file size reached for %s, swapping", mDescriptor, mSerialNumber);
-                createTmpFile();
-                mTmpBytesStored = 0;
-            }
         } catch (IOException e) {
             CLog.w("failed to write %s data for %s.", mDescriptor, mSerialNumber);
         }
@@ -105,20 +77,9 @@
      * @return The collected output from the command.
      */
     public synchronized InputStreamSource getData() {
-        if (mTmpFile != null) {
-            flush();
+        if (mOutStream != null) {
             try {
-                FileInputStream fileStream = new FileInputStream(mTmpFile);
-                if (mPreviousTmpFile != null) {
-                    // return an input stream that first reads from mPreviousTmpFile, then reads
-                    // from mTmpFile
-                    InputStream stream = new SequenceInputStream(
-                            new FileInputStream(mPreviousTmpFile), fileStream);
-                    return new SnapshotInputStreamSource(stream);
-                } else {
-                    // no previous file, just return a wrapper around mTmpFile's stream
-                    return new SnapshotInputStreamSource(fileStream);
-                }
+                return new SnapshotInputStreamSource(mOutStream.getData());
             } catch (IOException e) {
                 CLog.e("failed to get %s data for %s.", mDescriptor, mSerialNumber);
                 CLog.e(e);
@@ -137,11 +98,7 @@
         if (mOutStream == null) {
             return;
         }
-        try {
-            mOutStream.flush();
-        } catch (IOException e) {
-            CLog.w("failed to flush %s data for %s.", mDescriptor, mSerialNumber);
-        }
+        mOutStream.flush();
     }
 
     /**
@@ -149,12 +106,12 @@
      */
     public synchronized void clear() {
         delete();
+        mOutStream = createOutputStream();
+    }
 
-        try {
-            createTmpFile();
-        }  catch (IOException e) {
-            CLog.w("failed to create %s file for %s.", mDescriptor, mSerialNumber);
-        }
+    private SizeLimitedOutputStream createOutputStream() {
+        return new SizeLimitedOutputStream(mMaxDataSize, String.format("%s_%s",
+                getDescriptor(), mSerialNumber), ".txt");
     }
 
     /**
@@ -168,30 +125,8 @@
      * Delete all accumulated data.
      */
     public void delete() {
-        flush();
-        closeLogStream();
-
-        FileUtil.deleteFile(mTmpFile);
-        mTmpFile = null;
-        FileUtil.deleteFile(mPreviousTmpFile);
-        mPreviousTmpFile = null;
-        mTmpBytesStored = 0;
-    }
-
-    /**
-     * Closes the stream to tmp log file
-     */
-    private void closeLogStream() {
-        try {
-            if (mOutStream != null) {
-                mOutStream.flush();
-                mOutStream.close();
-                mOutStream = null;
-            }
-
-        } catch (IOException e) {
-            CLog.w("failed to close %s stream for %s.", mDescriptor, mSerialNumber);
-        }
+        mOutStream.delete();
+        mOutStream = null;
     }
 
     /**
@@ -203,60 +138,6 @@
     }
 
     /**
-     * Creates a new tmp file, closing the old one as necessary
-     * <p>
-     * Exposed for unit testing.
-     * </p>
-     * @throws IOException
-     * @throws FileNotFoundException
-     */
-    synchronized void createTmpFile() throws IOException, FileNotFoundException {
-        if (mIsCancelled) {
-            CLog.w("Attempted to createTmpFile() after cancel() for device %s, ignoring.",
-                    mSerialNumber);
-            return;
-        }
-
-        closeLogStream();
-        if (mPreviousTmpFile != null) {
-            mPreviousTmpFile.delete();
-        }
-        mPreviousTmpFile = mTmpFile;
-        mTmpFile = FileUtil.createTempFile(String.format("%s_%s_", mDescriptor, mSerialNumber),
-                ".txt");
-        CLog.i("Created tmp %s file %s", mDescriptor, mTmpFile.getAbsolutePath());
-        mOutStream = new BufferedOutputStream(new FileOutputStream(mTmpFile),
-                BUFF_SIZE);
-        // add an initial message to log, to give info to viewer
-        if (mPreviousTmpFile == null) {
-            // first log!
-            appendLogMsg(String.format("%s for device %s", mDescriptor, mSerialNumber));
-        } else {
-            appendLogMsg(String.format("Continuing %s capture for device %s. Previous content may "
-                    + "have been truncated.", mDescriptor, mSerialNumber));
-        }
-    }
-
-    /**
-     * Adds a message to the captured device log.
-     *
-     * @param msg
-     */
-    protected synchronized void appendLogMsg(String msg) {
-        if (mOutStream == null || msg == null) {
-            return;
-        }
-        // add the msg to log, so readers will know the command was interrupted
-        try {
-            mOutStream.write("\n*******************\n".getBytes());
-            mOutStream.write(msg.getBytes());
-            mOutStream.write("\n*******************\n".getBytes());
-        } catch (IOException e) {
-            CLog.w("failed to write %s data for %s.", mDescriptor, mSerialNumber);
-        }
-    }
-
-    /**
      * Get the descriptor.
      * <p>
      * Exposed for unit testing.
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index c882744..ca2d3e4 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -215,7 +215,7 @@
      * @param size max byte size of tmp file
      */
     void setTmpLogcatSize(long size) {
-        mOptions.setMaxLogcatFileSize(size);
+        mOptions.setMaxLogcatDataSize(size);
     }
 
     /**
@@ -1499,7 +1499,7 @@
      * Exposed for unit testing.
      */
     LogcatReceiver createLogcatReceiver() {
-        return new LogcatReceiver(this, mOptions.getMaxLogcatFileSize(), mLogStartDelay);
+        return new LogcatReceiver(this, mOptions.getMaxLogcatDataSize(), mLogStartDelay);
     }
 
     /**
diff --git a/src/com/android/tradefed/device/TestDeviceOptions.java b/src/com/android/tradefed/device/TestDeviceOptions.java
index be23cea..c94eeb8 100644
--- a/src/com/android/tradefed/device/TestDeviceOptions.java
+++ b/src/com/android/tradefed/device/TestDeviceOptions.java
@@ -32,8 +32,8 @@
     private String mDisableKeyguardCmd = "input keyevent 82";
 
     @Option(name = "max-tmp-logcat-file", description =
-        "The maximum size of a tmp logcat file, in bytes.")
-    private long mMaxLogcatFileSize = 10 * 1024 * 1024;
+        "The maximum size of tmp logcat data to retain, in bytes.")
+    private long mMaxLogcatDataSize = 20 * 1024 * 1024;
 
     @Option(name = "fastboot-timeout", description =
             "time in ms to wait for a device to boot into fastboot.")
@@ -106,22 +106,17 @@
     }
 
     /**
-     * Get the maximum size of a tmp logcat file, in bytes.
-     * <p/>
-     * The actual size of the log info stored will be up to twice this number, as two logcat files
-     * are stored.
-     *
-     * TODO: make this represent a strictly enforced total max size
+     * Get the approximate maximum size of a tmp logcat data to retain, in bytes.
      */
-    public long getMaxLogcatFileSize() {
-        return mMaxLogcatFileSize;
+    public long getMaxLogcatDataSize() {
+        return mMaxLogcatDataSize;
     }
 
     /**
-     * @param maxLogcatFileSize the max logcat file size to set
+     * @param maxLogcatDataSize the max logcat tmp size to set
      */
-    public void setMaxLogcatFileSize(long maxLogcatFileSize) {
-        mMaxLogcatFileSize = maxLogcatFileSize;
+    public void setMaxLogcatDataSize(long maxLogcatDataSize) {
+        mMaxLogcatDataSize = maxLogcatDataSize;
     }
 
     /**
diff --git a/src/com/android/tradefed/log/FileLogger.java b/src/com/android/tradefed/log/FileLogger.java
index 5877920..4a2f77e 100644
--- a/src/com/android/tradefed/log/FileLogger.java
+++ b/src/com/android/tradefed/log/FileLogger.java
@@ -22,15 +22,13 @@
 import com.android.tradefed.result.ByteArrayInputStreamSource;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.SnapshotInputStreamSource;
-import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.SizeLimitedOutputStream;
 import com.android.tradefed.util.StreamUtil;
 
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.Writer;
 import java.util.Collection;
 import java.util.HashSet;
 
@@ -42,9 +40,6 @@
     private static final String TEMP_FILE_PREFIX = "tradefed_log_";
     private static final String TEMP_FILE_SUFFIX = ".txt";
 
-    private File mTempLogFile = null;
-    private BufferedWriter mLogWriter = null;
-
     @Option(name = "log-level", description = "the minimum log level to log.")
     private LogLevel mLogLevel = LogLevel.DEBUG;
 
@@ -56,8 +51,11 @@
     @Option(name = "log-tag-display", description = "Always display given tags logs on stdout")
     private Collection<String> mLogTagsDisplay = new HashSet<String>();
 
-    // temp: track where this log was closed
-    private StackTraceElement[] mCloseStackFrames = null;
+    @Option(name = "max-log-size", description = "maximum allowable size of tmp log data in mB.")
+    private int mMaxLogSizeMbytes = 20;
+
+    private Writer mLogWriter = null;
+    private SizeLimitedOutputStream mLogStream;
 
     /**
      * Adds tags to the log-tag-display list
@@ -76,16 +74,9 @@
      */
     @Override
     public void init() throws IOException {
-        try {
-            mTempLogFile = FileUtil.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
-            mLogWriter = new BufferedWriter(new FileWriter(mTempLogFile));
-        }
-        catch (IOException e) {
-            if (mTempLogFile != null) {
-                mTempLogFile.delete();
-            }
-            throw e;
-        }
+        mLogStream = new SizeLimitedOutputStream(mMaxLogSizeMbytes * 1024 * 1024, TEMP_FILE_PREFIX,
+                TEMP_FILE_SUFFIX);
+        mLogWriter = new PrintWriter(mLogStream);
     }
 
     /**
@@ -187,36 +178,15 @@
     }
 
     /**
-     * Returns the path representation of the file being logged to by this file logger
-     */
-    String getFilename() throws SecurityException {
-        if (mTempLogFile == null) {
-            throw new IllegalStateException(
-                    "logger has already been closed or has not been initialized");
-        }
-        return mTempLogFile.getAbsolutePath();
-    }
-
-    /**
      * {@inheritDoc}
      */
     @Override
     public InputStreamSource getLog() {
-        if (mLogWriter == null) {
-            // TODO: change this back to throw new IllegalStateException(
-            System.err.println(String.format(
-                    "logger has already been closed or has not been initialized, Thread %s",
-                    Thread.currentThread().getName()));
-            System.err.println("Current stack:");
-            printStackTrace(Thread.currentThread().getStackTrace());
-            System.err.println("\nLog closed at:");
-            printStackTrace(mCloseStackFrames);
-        } else {
+        if (mLogWriter != null && mLogStream != null) {
             try {
                 // create a InputStream from log file
                 mLogWriter.flush();
-                return new SnapshotInputStreamSource(new FileInputStream(mTempLogFile));
-
+                return new SnapshotInputStreamSource(mLogStream.getData());
             } catch (IOException e) {
                 System.err.println("Failed to get log");
                 e.printStackTrace();
@@ -225,16 +195,6 @@
         return new ByteArrayInputStreamSource(new byte[0]);
     }
 
-    private void printStackTrace(StackTraceElement[] trace) {
-        if (trace == null) {
-            System.err.println("no stack");
-            return;
-        }
-        for (StackTraceElement element : trace) {
-            System.err.println("\tat " + element);
-        }
-    }
-
     /**
      * {@inheritDoc}
      */
@@ -257,20 +217,16 @@
     void doCloseLog() throws IOException {
         try {
             if (mLogWriter != null) {
-                // TODO: temp: track where this log was closed
-                mCloseStackFrames = Thread.currentThread().getStackTrace();
-                // set mLogWriter to null first before closing, to prevent "write" calls after
-                // "close"
-                BufferedWriter writer = mLogWriter;
+                Writer writer = mLogWriter;
                 mLogWriter = null;
 
                 writer.flush();
                 writer.close();
             }
         } finally {
-            if (mTempLogFile != null) {
-                mTempLogFile.delete();
-                mTempLogFile = null;
+            if (mLogStream != null) {
+                mLogStream.delete();
+                mLogStream = null;
             }
         }
     }
diff --git a/src/com/android/tradefed/util/SizeLimitedOutputStream.java b/src/com/android/tradefed/util/SizeLimitedOutputStream.java
new file mode 100644
index 0000000..268963d
--- /dev/null
+++ b/src/com/android/tradefed/util/SizeLimitedOutputStream.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2013 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.tradefed.util;
+
+import com.android.tradefed.log.LogUtil.CLog;
+import com.google.common.io.CountingOutputStream;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.SequenceInputStream;
+
+/**
+ * A thread safe file backed {@link OutputStream} that limits the maximum amount of data that can be
+ * written.
+ * <p/>
+ * This is implemented by keeping a circular list of Files of fixed size. Once a File has reached a
+ * certain size, the class jumps to use the next File in the list. If the next File is non empty, it
+ * is deleted, and a new file created.
+ */
+public class SizeLimitedOutputStream extends OutputStream {
+
+    private static final int DEFAULT_NUM_TMP_FILES = 5;
+
+    /** The max number of bytes to store in the buffer */
+    private static final int BUFF_SIZE = 32 * 1024;
+
+    // circular array of backing files
+    private final File[] mFiles;
+    private final long mMaxFileSize;
+    private CountingOutputStream mCurrentOutputStream;
+    private int mCurrentFilePos = 0;
+    private final String mTempFilePrefix;
+    private final String mTempFileSuffix;
+
+    /**
+     * Creates a {@link SizeLimitedOutputStream}.
+     *
+     * @param maxDataSize the approximate max size in bytes to keep in the output stream
+     * @param numFiles the max number of backing files to use to store data. Higher values will mean
+     *            max data kept will be close to maxDataSize, but with a possible performance
+     *            penalty.
+     * @param tempFilePrefix prefix to use for temporary files
+     * @param tempFileSuffix suffix to use for temporary files
+     */
+    public SizeLimitedOutputStream(long maxDataSize, int numFiles, String tempFilePrefix,
+            String tempFileSuffix) {
+        mMaxFileSize = maxDataSize / numFiles;
+        mFiles = new File[numFiles];
+        mCurrentFilePos = numFiles;
+        mTempFilePrefix = tempFilePrefix;
+        mTempFileSuffix = tempFileSuffix;
+    }
+
+    /**
+     * Creates a {@link SizeLimitedOutputStream} with default number of backing files.
+     *
+     * @param maxDataSize the approximate max size to keep in the output stream
+     * @param tempFilePrefix prefix to use for temporary files
+     * @param tempFileSuffix suffix to use for temporary files
+     */
+    public SizeLimitedOutputStream(long maxDataSize, String tempFilePrefix, String tempFileSuffix) {
+        this(maxDataSize, DEFAULT_NUM_TMP_FILES, tempFilePrefix, tempFileSuffix);
+    }
+
+    /**
+     * Gets the collected output as a {@link InputStream}.
+     * <p/>
+     * It is recommended to  buffer returned stream before using.
+     *
+     * @return The collected output as a {@link InputStream}.
+     */
+    public synchronized InputStream getData() throws IOException {
+        flush();
+        InputStream combinedStream = null;
+        for (int i = 0; i < mFiles.length; i++) {
+            // oldest/starting file is always the next one up from current
+            int currentPos = (mCurrentFilePos + i + 1) % mFiles.length;
+            if (mFiles[currentPos] != null) {
+                @SuppressWarnings("resource")
+                FileInputStream fStream = new FileInputStream(mFiles[currentPos]);
+                if (combinedStream == null) {
+                    combinedStream = fStream;
+                } else {
+                    combinedStream = new SequenceInputStream(combinedStream, fStream);
+                }
+            }
+        }
+        if (combinedStream == null) {
+            combinedStream = new ByteArrayInputStream(new byte[0]);
+        }
+        return combinedStream;
+
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public synchronized void flush() {
+        if (mCurrentOutputStream == null) {
+            return;
+        }
+        try {
+            mCurrentOutputStream.flush();
+        } catch (IOException e) {
+            CLog.w("failed to flush data: %s", e);
+        }
+    }
+
+    /**
+     * Delete all accumulated data.
+     */
+    public void delete() {
+        close();
+        for (int i = 0; i < mFiles.length; i++) {
+            FileUtil.deleteFile(mFiles[i]);
+            mFiles[i] = null;
+        }
+    }
+
+    /**
+     * Closes the write stream
+     */
+    @Override
+    public void close() {
+        try {
+            if (mCurrentOutputStream != null) {
+                mCurrentOutputStream.flush();
+                mCurrentOutputStream.close();
+                mCurrentOutputStream = null;
+            }
+
+        } catch (IOException e) {
+            CLog.w("failed to close %s stream", e);
+        }
+    }
+
+    /**
+     * Creates a new tmp file, closing the old one as necessary
+     * <p>
+     * Exposed for unit testing.
+     * </p>
+     *
+     * @throws IOException
+     * @throws FileNotFoundException
+     */
+    synchronized void generateNextFile() throws IOException, FileNotFoundException {
+        // close current stream
+        close();
+        mCurrentFilePos = getNextIndex(mCurrentFilePos);
+        if (mFiles[mCurrentFilePos] != null) {
+            mFiles[mCurrentFilePos].delete();
+        }
+        mFiles[mCurrentFilePos] = FileUtil.createTempFile(mTempFilePrefix, mTempFileSuffix);
+        CLog.d("Created tmp file %s", mFiles[mCurrentFilePos].getAbsolutePath());
+        mCurrentOutputStream = new CountingOutputStream(new BufferedOutputStream(
+                new FileOutputStream(mFiles[mCurrentFilePos]), BUFF_SIZE));
+    }
+
+    /**
+     * Gets the next index to use for <var>mFiles</var>, treating it as a circular list.
+     *
+     * @return
+     */
+    private int getNextIndex(int i) {
+        return (i + 1) % mFiles.length;
+    }
+
+    @Override
+    public synchronized void write(int data) throws IOException {
+        if (mCurrentOutputStream == null) {
+            generateNextFile();
+        }
+        mCurrentOutputStream.write(data);
+        if (mCurrentOutputStream.getCount() >= mMaxFileSize) {
+            generateNextFile();
+        }
+    }
+}
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 1bf700f..ad755d9 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -40,7 +40,6 @@
 import com.android.tradefed.device.DeviceMonitorAsyncProxyTest;
 import com.android.tradefed.device.DeviceSelectionOptionsTest;
 import com.android.tradefed.device.DeviceStateMonitorTest;
-import com.android.tradefed.device.LargeOutputReceiverTest;
 import com.android.tradefed.device.ReconnectingRecoveryTest;
 import com.android.tradefed.device.TestDeviceTest;
 import com.android.tradefed.device.WaitDeviceRecoveryTest;
@@ -91,6 +90,7 @@
 import com.android.tradefed.util.QuotationAwareTokenizerTest;
 import com.android.tradefed.util.RegexTrieTest;
 import com.android.tradefed.util.RunUtilTest;
+import com.android.tradefed.util.SizeLimitedOutputStreamTest;
 import com.android.tradefed.util.brillopad.BrillopadTests;
 import com.android.tradefed.util.net.HttpMultipartPostTest;
 import com.android.tradefed.util.xml.AndroidManifestWriterTest;
@@ -138,7 +138,6 @@
         addTestSuite(DeviceMonitorAsyncProxyTest.class);
         addTestSuite(DeviceSelectionOptionsTest.class);
         addTestSuite(DeviceStateMonitorTest.class);
-        addTestSuite(LargeOutputReceiverTest.class);
         addTestSuite(ReconnectingRecoveryTest.class);
         addTestSuite(TestDeviceTest.class);
         addTestSuite(WaitDeviceRecoveryTest.class);
@@ -197,12 +196,13 @@
         addTestSuite(ConditionPriorityBlockingQueueTest.class);
         addTestSuite(EmailTest.class);
         addTestSuite(FileUtilTest.class);
+        addTestSuite(HttpMultipartPostTest.class);
         addTestSuite(MultiMapTest.class);
         addTestSuite(NullUtilTest.class);
         addTestSuite(QuotationAwareTokenizerTest.class);
         addTestSuite(RegexTrieTest.class);
         addTestSuite(RunUtilTest.class);
-        addTestSuite(HttpMultipartPostTest.class);
+        addTestSuite(SizeLimitedOutputStreamTest.class);
 
         // util subdirs
         addTestSuite(AndroidManifestWriterTest.class);
diff --git a/tests/src/com/android/tradefed/device/LargeOutputReceiverTest.java b/tests/src/com/android/tradefed/device/LargeOutputReceiverTest.java
deleted file mode 100644
index d56d9d6..0000000
--- a/tests/src/com/android/tradefed/device/LargeOutputReceiverTest.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2011 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.tradefed.device;
-
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.util.StreamUtil;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-
-import java.io.IOException;
-
-/**
- * Unit tests for {@link LargeOutputReceiver}
- */
-public class LargeOutputReceiverTest extends TestCase {
-    private ITestDevice mTestDevice;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mTestDevice = EasyMock.createMock(ITestDevice.class);
-        EasyMock.expect(mTestDevice.getSerialNumber()).andReturn("serial").anyTimes();
-        EasyMock.expect(mTestDevice.getBuildId()).andReturn("id").anyTimes();
-    }
-
-    /**
-     * Test the log file size limiting.
-     */
-    public void testMaxFileSizeHelper() throws IOException {
-        final String input1 = "this is the output of greater than 10 bytes.";
-        final String input2 = "this is the second output of greater than 10 bytes.";
-        final String input3 = "<10bytes";
-        EasyMock.replay(mTestDevice);
-
-        LargeOutputReceiver helper = new LargeOutputReceiver("command", "serial", 10);
-
-        try {
-            helper.createTmpFile();
-            byte[] inputData1 = input1.getBytes();
-            // add log data > maximum. This will trigger a log swap, where inputData1 will be moved
-            // to the backup log file
-            helper.addOutput(inputData1, 0, inputData1.length);
-            // inject the second input data > maximum. This will trigger another log swap, that will
-            // discard inputData. the backup log file will have inputData2, and the current log file
-            // will be empty
-            byte[] inputData2 = input2.getBytes();
-            helper.addOutput(inputData2, 0, inputData2.length);
-            // inject log data smaller than max log data - that will not trigger a log swap. The
-            // backup log file should contain inputData2, and the current should contain inputData3
-            byte[] inputData3 = input3.getBytes();
-            helper.addOutput(inputData3, 0, inputData3.length);
-
-            InputStreamSource iss = helper.getData();
-            String actualString;
-            try {
-                actualString = StreamUtil.getStringFromStream(iss.createInputStream());
-            } finally {
-                iss.cancel();
-            }
-            // verify that data from both the backup log file (input2) and current log file
-            // (input3) is retrieved
-            assertFalse(actualString.contains(input1));
-            assertTrue(actualString.contains(input2));
-            assertTrue(actualString.contains(input3));
-        } finally {
-            helper.cancel();
-            helper.delete();
-        }
-    }
-}
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index bd9229a..6e20e31 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -25,7 +25,6 @@
 import com.android.tradefed.device.ITestDevice.MountPointInfo;
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
 import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.util.ArrayUtil;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
@@ -332,81 +331,6 @@
         mTestDevice.clearErrorDialogs();
     }
 
-    /**
-     * Test the log file size limiting.
-     */
-    public void testLogCatReceiver() throws IOException, InterruptedException, TimeoutException,
-            AdbCommandRejectedException, ShellCommandUnresponsiveException {
-        mTestDevice.setTmpLogcatSize(10);
-        final String input = "this is the output of greater than 10 bytes.";
-        final String input2 = "this is the second output of greater than 10 bytes.";
-        final String input3 = "<10bytes";
-        final Object notifier = new Object();
-        LogcatReceiver receiver = null;
-
-        // mock the call to get system build id
-        EasyMock.expect(mMockIDevice.getProperty((String)EasyMock.anyObject())).andStubReturn("1");
-
-        try {
-            IAnswer<Object> shellAnswer = new IAnswer<Object>() {
-                @Override
-                public Object answer() throws Throwable {
-                    IShellOutputReceiver receiver =
-                        (IShellOutputReceiver)EasyMock.getCurrentArguments()[1];
-                    byte[] inputData = input.getBytes();
-                    // add log data > maximum. This will trigger a log swap, where inputData
-                    // will be moved to the backup log file
-                    receiver.addOutput(inputData, 0, inputData.length);
-                    // inject the second input data > maximum. This will trigger another log
-                    // swap, that will discard inputData. the backup log file will have
-                    // inputData2, and the current log file will be empty
-                    byte[] inputData2 = input2.getBytes();
-                    receiver.addOutput(inputData2, 0, inputData2.length);
-                    // inject log data smaller than max log data - that will not trigger a
-                    // log swap. The backup log file should contain inputData2, and the
-                    // current should contain inputData3
-                    byte[] inputData3 = input3.getBytes();
-                    receiver.addOutput(inputData3, 0, inputData3.length);
-                    synchronized (notifier) {
-                        notifier.notify();
-                        try {
-                          // block until interrupted
-                          notifier.wait();
-                        } catch (InterruptedException e) {
-                        }
-                    }
-                    return null;
-                }
-            };
-            // expect shell command to be called, with any receiver
-            mMockIDevice.executeShellCommand((String)EasyMock.anyObject(), (IShellOutputReceiver)
-                    EasyMock.anyObject(), EasyMock.eq(0));
-            EasyMock.expectLastCall().andAnswer(shellAnswer);
-            EasyMock.replay(mMockIDevice);
-            receiver = mTestDevice.createLogcatReceiver();
-            receiver.start();
-            synchronized (notifier) {
-                notifier.wait();
-            }
-            InputStreamSource iss = receiver.getLogcatData();
-            String actualString = "";
-            try {
-                actualString = StreamUtil.getStringFromStream(iss.createInputStream());
-            } finally {
-                iss.cancel();
-            }
-            // verify that data from both the backup log file (input2) and current log file
-            // (input3) is retrieved
-            assertFalse(actualString.contains(input));
-            assertTrue(actualString.contains(input2));
-            assertTrue(actualString.contains(input3));
-        } finally {
-            if (receiver != null) {
-                receiver.stop();
-            }
-        }
-    }
-
     public void testGetBugreport_deviceUnavail() throws Exception {
         final String testCommand = "bugreport";
         final String expectedOutput = "this is the output\r\n in two lines\r\n";
diff --git a/tests/src/com/android/tradefed/log/FileLoggerTest.java b/tests/src/com/android/tradefed/log/FileLoggerTest.java
index 33b8b22..90c788b 100644
--- a/tests/src/com/android/tradefed/log/FileLoggerTest.java
+++ b/tests/src/com/android/tradefed/log/FileLoggerTest.java
@@ -22,7 +22,6 @@
 import junit.framework.TestCase;
 
 import java.io.BufferedReader;
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStreamReader;
 
@@ -34,22 +33,6 @@
     private static String LOG_TAG = "FileLoggerTest";
 
     /**
-     * Test that we are able to create a logger.
-     *
-     * @throws ConfigurationException if unable to create log file
-     * @throws SecurityException if unable to delete the log file on cleanup
-     */
-    public void testCreateLogger() throws IOException, SecurityException {
-        FileLogger logger = new FileLogger();
-        logger.init();
-        String tempFile = logger.getFilename();
-        File logFile = new File(tempFile);
-        assertTrue(logFile.exists());
-        logger.closeLog();
-        assertFalse(logFile.exists());
-    }
-
-    /**
      * Test logging to a logger.
      *
      * @throws ConfigurationException if unable to create log file
diff --git a/tests/src/com/android/tradefed/util/SizeLimitedOutputStreamTest.java b/tests/src/com/android/tradefed/util/SizeLimitedOutputStreamTest.java
new file mode 100644
index 0000000..31adf97
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/SizeLimitedOutputStreamTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2011 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.tradefed.util;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Unit tests for {@link SizeLimitedOutputStreamTest}
+ */
+public class SizeLimitedOutputStreamTest extends TestCase {
+
+    /**
+     * Test the file size limiting.
+     */
+    public void testMaxFileSizeHelper() throws IOException {
+        final byte[] data = new byte[29];
+
+        // fill data with values
+        for (byte i = 0; i < data.length; i++) {
+            data[i] = i;
+        }
+
+        // use a max size of 20 - expect first 10 bytes to get dropped
+        SizeLimitedOutputStream outStream = new SizeLimitedOutputStream(20, 4, "foo", "bar");
+        try {
+            outStream.write(data);
+            outStream.close();
+            InputStream readStream = outStream.getData();
+            byte[] readData = new byte[64];
+            int readDataPos = 0;
+            int read;
+            while ((read = readStream.read()) != -1) {
+                readData[readDataPos] = (byte)read;
+                readDataPos++;
+            }
+            int bytesRead = readDataPos;
+            assertEquals(19, bytesRead);
+            assertEquals(10, readData[0]);
+            assertEquals(28, readData[18]);
+        } finally {
+            outStream.delete();
+        }
+    }
+}