Frameworks/base: Change android.util.Log multiline logging
To avoid long stacktraces being truncated, add code to split up
large chunks along line breaks.
Introduce LineBreakBufferedWriter to chunk up log output. Add a
core test for it.
Change-Id: I34160fbce853c21329f7fa109a9c42506b2066af
diff --git a/core/java/android/util/Log.java b/core/java/android/util/Log.java
index fe41932..544444d 100644
--- a/core/java/android/util/Log.java
+++ b/core/java/android/util/Log.java
@@ -18,9 +18,11 @@
import com.android.internal.os.RuntimeInit;
import com.android.internal.util.FastPrintWriter;
+import com.android.internal.util.LineBreakBufferedWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.io.Writer;
import java.net.UnknownHostException;
/**
@@ -126,7 +128,7 @@
* @param tr An exception to log
*/
public static int v(String tag, String msg, Throwable tr) {
- return println_native(LOG_ID_MAIN, VERBOSE, tag, msg + '\n' + getStackTraceString(tr));
+ return printlns(LOG_ID_MAIN, VERBOSE, tag, msg, tr);
}
/**
@@ -147,7 +149,7 @@
* @param tr An exception to log
*/
public static int d(String tag, String msg, Throwable tr) {
- return println_native(LOG_ID_MAIN, DEBUG, tag, msg + '\n' + getStackTraceString(tr));
+ return printlns(LOG_ID_MAIN, DEBUG, tag, msg, tr);
}
/**
@@ -168,7 +170,7 @@
* @param tr An exception to log
*/
public static int i(String tag, String msg, Throwable tr) {
- return println_native(LOG_ID_MAIN, INFO, tag, msg + '\n' + getStackTraceString(tr));
+ return printlns(LOG_ID_MAIN, INFO, tag, msg, tr);
}
/**
@@ -189,7 +191,7 @@
* @param tr An exception to log
*/
public static int w(String tag, String msg, Throwable tr) {
- return println_native(LOG_ID_MAIN, WARN, tag, msg + '\n' + getStackTraceString(tr));
+ return printlns(LOG_ID_MAIN, WARN, tag, msg, tr);
}
/**
@@ -219,7 +221,7 @@
* @param tr An exception to log
*/
public static int w(String tag, Throwable tr) {
- return println_native(LOG_ID_MAIN, WARN, tag, getStackTraceString(tr));
+ return printlns(LOG_ID_MAIN, WARN, tag, "", tr);
}
/**
@@ -240,7 +242,7 @@
* @param tr An exception to log
*/
public static int e(String tag, String msg, Throwable tr) {
- return println_native(LOG_ID_MAIN, ERROR, tag, msg + '\n' + getStackTraceString(tr));
+ return printlns(LOG_ID_MAIN, ERROR, tag, msg, tr);
}
/**
@@ -292,8 +294,7 @@
// Only mark this as ERROR, do not use ASSERT since that should be
// reserved for cases where the system is guaranteed to abort.
// The onTerribleFailure call does not always cause a crash.
- int bytes = println_native(logId, ERROR, tag, msg + '\n'
- + getStackTraceString(localStack ? what : tr));
+ int bytes = printlns(logId, ERROR, tag, msg, localStack ? what : tr);
sWtfHandler.onTerribleFailure(tag, what, system);
return bytes;
}
@@ -365,4 +366,107 @@
/** @hide */ public static native int println_native(int bufID,
int priority, String tag, String msg);
+
+ /**
+ * Return the maximum payload the log daemon accepts without truncation.
+ * @return LOGGER_ENTRY_MAX_PAYLOAD.
+ */
+ private static native int logger_entry_max_payload_native();
+
+ /**
+ * Helper function for long messages. Uses the LineBreakBufferedWriter to break
+ * up long messages and stacktraces along newlines, but tries to write in large
+ * chunks. This is to avoid truncation.
+ */
+ private static int printlns(int bufID, int priority, String tag, String msg,
+ Throwable tr) {
+ ImmediateLogWriter logWriter = new ImmediateLogWriter(bufID, priority, tag);
+ // Acceptable buffer size. Get the native buffer size, subtract two zero terminators,
+ // and the length of the tag.
+ // Note: we implicitly accept possible truncation for Modified-UTF8 differences. It
+ // is too expensive to compute that ahead of time.
+ int bufferSize = NoPreloadHolder.LOGGER_ENTRY_MAX_PAYLOAD // Base.
+ - 2 // Two terminators.
+ - (tag != null ? tag.length() : 0) // Tag length.
+ - 32; // Some slack.
+ // At least assume you can print *some* characters (tag is not too large).
+ bufferSize = Math.max(bufferSize, 100);
+
+ LineBreakBufferedWriter lbbw = new LineBreakBufferedWriter(logWriter, bufferSize);
+
+ lbbw.println(msg);
+
+ if (tr != null) {
+ // This is to reduce the amount of log spew that apps do in the non-error
+ // condition of the network being unavailable.
+ Throwable t = tr;
+ while (t != null) {
+ if (t instanceof UnknownHostException) {
+ break;
+ }
+ t = t.getCause();
+ }
+ if (t == null) {
+ tr.printStackTrace(lbbw);
+ }
+ }
+
+ lbbw.flush();
+
+ return logWriter.getWritten();
+ }
+
+ /**
+ * NoPreloadHelper class. Caches the LOGGER_ENTRY_MAX_PAYLOAD value to avoid
+ * a JNI call during logging.
+ */
+ static class NoPreloadHolder {
+ public final static int LOGGER_ENTRY_MAX_PAYLOAD =
+ logger_entry_max_payload_native();
+ }
+
+ /**
+ * Helper class to write to the logcat. Different from LogWriter, this writes
+ * the whole given buffer and does not break along newlines.
+ */
+ private static class ImmediateLogWriter extends Writer {
+
+ private int bufID;
+ private int priority;
+ private String tag;
+
+ private int written = 0;
+
+ /**
+ * Create a writer that immediately writes to the log, using the given
+ * parameters.
+ */
+ public ImmediateLogWriter(int bufID, int priority, String tag) {
+ this.bufID = bufID;
+ this.priority = priority;
+ this.tag = tag;
+ }
+
+ public int getWritten() {
+ return written;
+ }
+
+ @Override
+ public void write(char[] cbuf, int off, int len) {
+ // Note: using String here has a bit of overhead as a Java object is created,
+ // but using the char[] directly is not easier, as it needs to be translated
+ // to a C char[] for logging.
+ written += println_native(bufID, priority, tag, new String(cbuf, off, len));
+ }
+
+ @Override
+ public void flush() {
+ // Ignored.
+ }
+
+ @Override
+ public void close() {
+ // Ignored.
+ }
+ }
}
diff --git a/core/java/com/android/internal/util/LineBreakBufferedWriter.java b/core/java/com/android/internal/util/LineBreakBufferedWriter.java
new file mode 100644
index 0000000..f831e7a
--- /dev/null
+++ b/core/java/com/android/internal/util/LineBreakBufferedWriter.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2015 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 java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Arrays;
+
+/**
+ * A writer that breaks up its output into chunks before writing to its out writer,
+ * and which is linebreak aware, i.e., chunks will created along line breaks, if
+ * possible.
+ *
+ * Note: this class is not thread-safe.
+ */
+public class LineBreakBufferedWriter extends PrintWriter {
+
+ /**
+ * A buffer to collect data until the buffer size is reached.
+ *
+ * Note: we manage a char[] ourselves to avoid an allocation when printing to the
+ * out writer. Otherwise a StringBuilder would have been simpler to use.
+ */
+ private char[] buffer;
+
+ /**
+ * The index of the first free element in the buffer.
+ */
+ private int bufferIndex;
+
+ /**
+ * The chunk size (=maximum buffer size) to use for this writer.
+ */
+ private final int bufferSize;
+
+
+ /**
+ * Index of the last newline character discovered in the buffer. The writer will try
+ * to split there.
+ */
+ private int lastNewline = -1;
+
+ /**
+ * The line separator for println().
+ */
+ private final String lineSeparator;
+
+ /**
+ * Create a new linebreak-aware buffered writer with the given output and buffer
+ * size. The initial capacity will be a default value.
+ * @param out The writer to write to.
+ * @param bufferSize The maximum buffer size.
+ */
+ public LineBreakBufferedWriter(Writer out, int bufferSize) {
+ this(out, bufferSize, 16); // 16 is the default size of a StringBuilder buffer.
+ }
+
+ /**
+ * Create a new linebreak-aware buffered writer with the given output, buffer
+ * size and initial capacity.
+ * @param out The writer to write to.
+ * @param bufferSize The maximum buffer size.
+ * @param initialCapacity The initial capacity of the internal buffer.
+ */
+ public LineBreakBufferedWriter(Writer out, int bufferSize, int initialCapacity) {
+ super(out);
+ this.buffer = new char[Math.min(initialCapacity, bufferSize)];
+ this.bufferIndex = 0;
+ this.bufferSize = bufferSize;
+ this.lineSeparator = System.getProperty("line.separator");
+ }
+
+ /**
+ * Flush the current buffer. This will ignore line breaks.
+ */
+ @Override
+ public void flush() {
+ writeBuffer(bufferIndex);
+ bufferIndex = 0;
+ super.flush();
+ }
+
+ @Override
+ public void write(int c) {
+ if (bufferIndex < bufferSize) {
+ buffer[bufferIndex] = (char)c;
+ bufferIndex++;
+ if ((char)c == '\n') {
+ lastNewline = bufferIndex;
+ }
+ } else {
+ // This should be an uncommon case, we mostly expect char[] and String. So
+ // let the chunking be handled by the char[] case.
+ write(new char[] { (char)c }, 0 ,1);
+ }
+ }
+
+ @Override
+ public void println() {
+ write(lineSeparator);
+ }
+
+ @Override
+ public void write(char[] buf, int off, int len) {
+ while (bufferIndex + len > bufferSize) {
+ // Find the next newline in the buffer, see if that's below the limit.
+ // Repeat.
+ int nextNewLine = -1;
+ int maxLength = bufferSize - bufferIndex;
+ for (int i = 0; i < maxLength; i++) {
+ if (buf[off + i] == '\n') {
+ if (bufferIndex + i < bufferSize) {
+ nextNewLine = i;
+ } else {
+ break;
+ }
+ }
+ }
+
+ if (nextNewLine != -1) {
+ // We can add some more data.
+ appendToBuffer(buf, off, nextNewLine);
+ writeBuffer(bufferIndex);
+ bufferIndex = 0;
+ lastNewline = -1;
+ off += nextNewLine + 1;
+ len -= nextNewLine + 1;
+ } else if (lastNewline != -1) {
+ // Use the last newline.
+ writeBuffer(lastNewline);
+ removeFromBuffer(lastNewline + 1);
+ lastNewline = -1;
+ } else {
+ // OK, there was no newline, break at a full buffer.
+ int rest = bufferSize - bufferIndex;
+ appendToBuffer(buf, off, rest);
+ writeBuffer(bufferIndex);
+ bufferIndex = 0;
+ off += rest;
+ len -= rest;
+ }
+ }
+
+ // Add to the buffer, this will fit.
+ if (len > 0) {
+ // Add the chars, find the last newline.
+ appendToBuffer(buf, off, len);
+ for (int i = len - 1; i >= 0; i--) {
+ if (buf[off + i] == '\n') {
+ lastNewline = bufferIndex - len + i;
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void write(String s, int off, int len) {
+ while (bufferIndex + len > bufferSize) {
+ // Find the next newline in the buffer, see if that's below the limit.
+ // Repeat.
+ int nextNewLine = -1;
+ int maxLength = bufferSize - bufferIndex;
+ for (int i = 0; i < maxLength; i++) {
+ if (s.charAt(off + i) == '\n') {
+ if (bufferIndex + i < bufferSize) {
+ nextNewLine = i;
+ } else {
+ break;
+ }
+ }
+ }
+
+ if (nextNewLine != -1) {
+ // We can add some more data.
+ appendToBuffer(s, off, nextNewLine);
+ writeBuffer(bufferIndex);
+ bufferIndex = 0;
+ lastNewline = -1;
+ off += nextNewLine + 1;
+ len -= nextNewLine + 1;
+ } else if (lastNewline != -1) {
+ // Use the last newline.
+ writeBuffer(lastNewline);
+ removeFromBuffer(lastNewline + 1);
+ lastNewline = -1;
+ } else {
+ // OK, there was no newline, break at a full buffer.
+ int rest = bufferSize - bufferIndex;
+ appendToBuffer(s, off, rest);
+ writeBuffer(bufferIndex);
+ bufferIndex = 0;
+ off += rest;
+ len -= rest;
+ }
+ }
+
+ // Add to the buffer, this will fit.
+ if (len > 0) {
+ // Add the chars, find the last newline.
+ appendToBuffer(s, off, len);
+ for (int i = len - 1; i >= 0; i--) {
+ if (s.charAt(off + i) == '\n') {
+ lastNewline = bufferIndex - len + i;
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Append the characters to the buffer. This will potentially resize the buffer,
+ * and move the index along.
+ * @param buf The char[] containing the data.
+ * @param off The start index to copy from.
+ * @param len The number of characters to copy.
+ */
+ private void appendToBuffer(char[] buf, int off, int len) {
+ if (bufferIndex + len > buffer.length) {
+ ensureCapacity(bufferIndex + len);
+ }
+ System.arraycopy(buf, off, buffer, bufferIndex, len);
+ bufferIndex += len;
+ }
+
+ /**
+ * Append the characters from the given string to the buffer. This will potentially
+ * resize the buffer, and move the index along.
+ * @param s The string supplying the characters.
+ * @param off The start index to copy from.
+ * @param len The number of characters to copy.
+ */
+ private void appendToBuffer(String s, int off, int len) {
+ if (bufferIndex + len > buffer.length) {
+ ensureCapacity(bufferIndex + len);
+ }
+ s.getChars(off, off + len, buffer, bufferIndex);
+ bufferIndex += len;
+ }
+
+ /**
+ * Resize the buffer. We use the usual double-the-size plus constant scheme for
+ * amortized O(1) insert. Note: we expect small buffers, so this won't check for
+ * overflow.
+ * @param capacity The size to be ensured.
+ */
+ private void ensureCapacity(int capacity) {
+ int newSize = buffer.length * 2 + 2;
+ if (newSize < capacity) {
+ newSize = capacity;
+ }
+ buffer = Arrays.copyOf(buffer, newSize);
+ }
+
+ /**
+ * Remove the characters up to (and excluding) index i from the buffer. This will
+ * not resize the buffer, but will update bufferIndex.
+ * @param i The number of characters to remove from the front.
+ */
+ private void removeFromBuffer(int i) {
+ int rest = bufferIndex - i;
+ if (rest > 0) {
+ System.arraycopy(buffer, bufferIndex - rest, buffer, 0, rest);
+ bufferIndex = rest;
+ } else {
+ bufferIndex = 0;
+ }
+ }
+
+ /**
+ * Helper method, write the given part of the buffer, [start,length), to the output.
+ * @param length The number of characters to flush.
+ */
+ private void writeBuffer(int length) {
+ if (length > 0) {
+ super.write(buffer, 0, length);
+ }
+ }
+}
diff --git a/core/jni/Android.mk b/core/jni/Android.mk
index a9a198b..3bde6b3 100644
--- a/core/jni/Android.mk
+++ b/core/jni/Android.mk
@@ -184,6 +184,8 @@
$(call include-path-for, libhardware_legacy)/hardware_legacy \
$(TOP)/frameworks/av/include \
$(TOP)/frameworks/base/media/jni \
+ $(TOP)/system/core/base/include \
+ $(TOP)/system/core/include \
$(TOP)/system/media/camera/include \
$(TOP)/system/netd/include \
external/pdfium/core/include/fpdfapi \
diff --git a/core/jni/android_util_Log.cpp b/core/jni/android_util_Log.cpp
index 2d23cda..7719e31 100644
--- a/core/jni/android_util_Log.cpp
+++ b/core/jni/android_util_Log.cpp
@@ -18,8 +18,10 @@
#define LOG_NAMESPACE "log.tag."
#define LOG_TAG "Log_println"
+#include <android-base/macros.h>
#include <assert.h>
#include <cutils/properties.h>
+#include <log/logger.h> // For LOGGER_ENTRY_MAX_PAYLOAD.
#include <utils/Log.h>
#include <utils/String8.h>
@@ -109,12 +111,23 @@
}
/*
+ * In class android.util.Log:
+ * private static native int logger_entry_max_payload_native()
+ */
+static jint android_util_Log_logger_entry_max_payload_native(JNIEnv* env ATTRIBUTE_UNUSED,
+ jobject clazz ATTRIBUTE_UNUSED)
+{
+ return static_cast<jint>(LOGGER_ENTRY_MAX_PAYLOAD);
+}
+
+/*
* JNI registration.
*/
static const JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
{ "isLoggable", "(Ljava/lang/String;I)Z", (void*) android_util_Log_isLoggable },
{ "println_native", "(IILjava/lang/String;Ljava/lang/String;)I", (void*) android_util_Log_println_native },
+ { "logger_entry_max_payload_native", "()I", (void*) android_util_Log_logger_entry_max_payload_native },
};
int register_android_util_Log(JNIEnv* env)
diff --git a/core/tests/coretests/src/com/android/internal/util/LineBreakBufferedWriterTest.java b/core/tests/coretests/src/com/android/internal/util/LineBreakBufferedWriterTest.java
new file mode 100644
index 0000000..49ae104
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/util/LineBreakBufferedWriterTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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.internal.util;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Tests for {@link IndentingPrintWriter}.
+ */
+public class LineBreakBufferedWriterTest extends TestCase {
+
+ private ByteArrayOutputStream mStream;
+ private RecordingWriter mWriter;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mWriter = new RecordingWriter();
+ }
+
+ public void testLessThanBufferSize() {
+ final LineBreakBufferedWriter lw = new LineBreakBufferedWriter(mWriter, 1000);
+
+ lw.println("Hello");
+ lw.println("World");
+ lw.println("Test");
+ lw.flush();
+
+ assertOutput("Hello\nWorld\nTest\n");
+ }
+
+ public void testMoreThanBufferSizeNoLineBreaks() {
+ final LineBreakBufferedWriter lw = new LineBreakBufferedWriter(mWriter, 20);
+
+ String literal = "aaaaaaaaaaaaaaa";
+ lw.print(literal);
+ lw.print(literal);
+ lw.flush();
+
+ // Have to manually inspect output.
+ List<String> result = mWriter.getStrings();
+ // Expect two strings.
+ assertEquals(2, result.size());
+ // Expect the strings to sum up to the original input.
+ assertEquals(2 * literal.length(), result.get(0).length() + result.get(1).length());
+ // Strings should only be a.
+ for (String s : result) {
+ for (int i = 0; i < s.length(); i++) {
+ assertEquals('a', s.charAt(i));
+ }
+ }
+ }
+
+ public void testMoreThanBufferSizeNoLineBreaksSingleString() {
+ final LineBreakBufferedWriter lw = new LineBreakBufferedWriter(mWriter, 20);
+
+ String literal = "aaaaaaaaaaaaaaa";
+ lw.print(literal + literal);
+ lw.flush();
+
+ // Have to manually inspect output.
+ List<String> result = mWriter.getStrings();
+ // Expect two strings.
+ assertEquals(2, result.size());
+ // Expect the strings to sum up to the original input.
+ assertEquals(2 * literal.length(), result.get(0).length() + result.get(1).length());
+ // Strings should only be a.
+ for (String s : result) {
+ for (int i = 0; i < s.length(); i++) {
+ assertEquals('a', s.charAt(i));
+ }
+ }
+ }
+
+ public void testMoreThanBufferSizeLineBreakBefore() {
+ final LineBreakBufferedWriter lw = new LineBreakBufferedWriter(mWriter, 20);
+
+ String literal1 = "aaaaaaaaaa\nbbbb";
+ String literal2 = "cccccccccc";
+ lw.print(literal1);
+ lw.print(literal2);
+ lw.flush();
+
+ assertOutput("aaaaaaaaaa", "bbbbcccccccccc");
+ }
+
+ public void testMoreThanBufferSizeLineBreakBeforeSingleString() {
+ final LineBreakBufferedWriter lw = new LineBreakBufferedWriter(mWriter, 20);
+
+ String literal1 = "aaaaaaaaaa\nbbbb";
+ String literal2 = "cccccccccc";
+ lw.print(literal1 + literal2);
+ lw.flush();
+
+ assertOutput("aaaaaaaaaa", "bbbbcccccccccc");
+ }
+
+ public void testMoreThanBufferSizeLineBreakNew() {
+ final LineBreakBufferedWriter lw = new LineBreakBufferedWriter(mWriter, 20);
+
+ String literal1 = "aaaaaaaaaabbbbb";
+ String literal2 = "c\nd\nddddddddd";
+ lw.print(literal1);
+ lw.print(literal2);
+ lw.flush();
+
+ assertOutput("aaaaaaaaaabbbbbc\nd", "ddddddddd");
+ }
+
+ public void testMoreThanBufferSizeLineBreakBeforeAndNew() {
+ final LineBreakBufferedWriter lw = new LineBreakBufferedWriter(mWriter, 20);
+
+ String literal1 = "aaaaaaaaaa\nbbbbb";
+ String literal2 = "c\nd\nddddddddd";
+ lw.print(literal1);
+ lw.print(literal2);
+ lw.flush();
+
+ assertOutput("aaaaaaaaaa\nbbbbbc\nd", "ddddddddd");
+ }
+
+ public void testMoreThanBufferSizeInt() {
+ final LineBreakBufferedWriter lw = new LineBreakBufferedWriter(mWriter, 15);
+
+ int literal1 = 1234567890;
+ int literal2 = 987654321;
+ lw.print(literal1);
+ lw.print(literal2);
+ lw.flush();
+
+ assertOutput("123456789098765", "4321");
+ }
+
+ public void testMoreThanBufferSizeChar() {
+ final LineBreakBufferedWriter lw = new LineBreakBufferedWriter(mWriter, 15);
+
+ for(int i = 0; i < 10; i++) {
+ lw.print('$');
+ }
+ for(int i = 0; i < 10; i++) {
+ lw.print('%');
+ }
+ lw.flush();
+
+ assertOutput("$$$$$$$$$$%%%%%", "%%%%%");
+ }
+
+ public void testMoreThanBufferSizeLineBreakNewChars() {
+ final LineBreakBufferedWriter lw = new LineBreakBufferedWriter(mWriter, 20);
+
+ String literal1 = "aaaaaaaaaabbbbb";
+ String literal2 = "c\nd\nddddddddd";
+ lw.print(literal1.toCharArray());
+ lw.print(literal2.toCharArray());
+ lw.flush();
+
+ assertOutput("aaaaaaaaaabbbbbc\nd", "ddddddddd");
+ }
+
+ private void assertOutput(String... golden) {
+ List<String> goldList = createTestGolden(golden);
+ assertEquals(goldList, mWriter.getStrings());
+ }
+
+ private static List<String> createTestGolden(String... args) {
+ List<String> ret = new ArrayList<String>();
+ for (String s : args) {
+ ret.add(s);
+ }
+ return ret;
+ }
+
+ // A writer recording calls to write.
+ private final static class RecordingWriter extends Writer {
+
+ private List<String> strings = new ArrayList<String>();
+
+ public RecordingWriter() {
+ }
+
+ public List<String> getStrings() {
+ return strings;
+ }
+
+ @Override
+ public void write(char[] cbuf, int off, int len) {
+ strings.add(new String(cbuf, off, len));
+ }
+
+ @Override
+ public void flush() {
+ // Ignore.
+ }
+
+ @Override
+ public void close() {
+ // Ignore.
+ }
+ }
+}