- Added TestSummary (TestResult was better but is conflicting with JUnit) which sole purpose is to provide a helpful summary of the sequence to listeners. git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@271124 13f79535-47bb-0310-9956-ffa450edef68master
@@ -116,19 +116,24 @@ public class Server { | |||
} | |||
/** start a server to the specified port */ | |||
public void start() { | |||
try { | |||
start(false); | |||
} catch (InterruptedException e){ | |||
} | |||
} | |||
/** start a server to the specified port and wait for end */ | |||
public void start(boolean flag) throws InterruptedException { | |||
Worker worker = new Worker(); | |||
worker.start(); | |||
if (flag){ | |||
worker.join(); | |||
public void start(boolean loop) throws IOException { | |||
server = new ServerSocket(port); | |||
while (server != null) { | |||
client = server.accept(); | |||
messenger = new Messenger(client.getInputStream(), client.getOutputStream()); | |||
TestRunEvent evt = null; | |||
try { | |||
while ( (evt = messenger.read()) != null ) { | |||
dispatcher.dispatchEvent(evt); | |||
} | |||
} catch (Exception e){ | |||
e.printStackTrace(); | |||
//@fixme this stacktrace might be normal when closing | |||
// the socket. So decompose the above in distinct steps | |||
} | |||
if (!loop){ | |||
break; | |||
} | |||
} | |||
} | |||
@@ -169,26 +174,4 @@ public class Server { | |||
} catch (IOException e) { | |||
} | |||
} | |||
//----- | |||
private class Worker extends Thread { | |||
public void run() { | |||
try { | |||
server = new ServerSocket(port); | |||
client = server.accept(); | |||
messenger = new Messenger(client.getInputStream(), client.getOutputStream()); | |||
TestRunEvent evt = null; | |||
while ( (evt = messenger.read()) != null ) { | |||
dispatcher.dispatchEvent(evt); | |||
} | |||
} catch (Exception e) { | |||
//@fixme this stacktrace might be normal when closing | |||
// the socket. So decompose the above in distinct steps | |||
} finally { | |||
cancel(); | |||
shutdown(); | |||
} | |||
} | |||
} | |||
} |
@@ -1,95 +0,0 @@ | |||
/* | |||
* The Apache Software License, Version 1.1 | |||
* | |||
* Copyright (c) 2002 The Apache Software Foundation. All rights | |||
* reserved. | |||
* | |||
* Redistribution and use in source and binary forms, with or without | |||
* modification, are permitted provided that the following conditions | |||
* are met: | |||
* | |||
* 1. Redistributions of source code must retain the above copyright | |||
* notice, this list of conditions and the following disclaimer. | |||
* | |||
* 2. Redistributions in binary form must reproduce the above copyright | |||
* notice, this list of conditions and the following disclaimer in | |||
* the documentation and/or other materials provided with the | |||
* distribution. | |||
* | |||
* 3. The end-user documentation included with the redistribution, if | |||
* any, must include the following acknowlegement: | |||
* "This product includes software developed by the | |||
* Apache Software Foundation (http://www.apache.org/)." | |||
* Alternately, this acknowlegement may appear in the software itself, | |||
* if and wherever such third-party acknowlegements normally appear. | |||
* | |||
* 4. The names "The Jakarta Project", "Ant", and "Apache Software | |||
* Foundation" must not be used to endorse or promote products derived | |||
* from this software without prior written permission. For written | |||
* permission, please contact apache@apache.org. | |||
* | |||
* 5. Products derived from this software may not be called "Apache" | |||
* nor may "Apache" appear in their names without prior written | |||
* permission of the Apache Group. | |||
* | |||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED | |||
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | |||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |||
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR | |||
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF | |||
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | |||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT | |||
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | |||
* SUCH DAMAGE. | |||
* ==================================================================== | |||
* | |||
* This software consists of voluntary contributions made by many | |||
* individuals on behalf of the Apache Software Foundation. For more | |||
* information on the Apache Software Foundation, please see | |||
* <http://www.apache.org/>. | |||
*/ | |||
package org.apache.tools.ant.taskdefs.optional.rjunit.remote; | |||
import java.io.ByteArrayInputStream; | |||
import java.io.ByteArrayOutputStream; | |||
import java.io.ObjectInputStream; | |||
import java.io.ObjectOutputStream; | |||
/** | |||
* A set of helper methods related to sockets. | |||
* | |||
* @author <a href="mailto:sbailliez@apache.org">Stephane Bailliez</a> | |||
*/ | |||
public class SocketUtil { | |||
/** | |||
* Helper method to deserialize an object | |||
* @param bytes the binary data representing the serialized object. | |||
* @return the deserialized object. | |||
* @throws Exception a generic exception if an error occurs when | |||
* deserializing the object. | |||
*/ | |||
public static Object deserialize(byte[] bytes) throws Exception { | |||
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); | |||
return ois.readObject(); | |||
} | |||
/** | |||
* Helper method to serialize an object | |||
* @param o the object to serialize. | |||
* @return the binary data representing the serialized object. | |||
* @throws Exception a generic exception if an error occurs when | |||
* serializing the object. | |||
*/ | |||
public static byte[] serialize(Object o) throws Exception { | |||
ByteArrayOutputStream out = new ByteArrayOutputStream(); | |||
ObjectOutputStream oos = new ObjectOutputStream(out); | |||
oos.writeObject(o); | |||
oos.close(); | |||
return out.toByteArray(); | |||
} | |||
} |
@@ -59,6 +59,9 @@ import java.util.Properties; | |||
import org.apache.tools.ant.util.StringUtils; | |||
/** | |||
* Provide the basic events to be used during the tests. | |||
* This is not very extensible but since the events should be somewhat | |||
* limited, for now this is better to do it like this. | |||
* | |||
* @author <a href="mailto:sbailliez@apache.org">Stephane Bailliez</a> | |||
*/ | |||
@@ -81,7 +84,7 @@ public class TestRunEvent extends EventObject { | |||
/** the type of event */ | |||
private int type = -1; | |||
/** timestamp for all tests */ | |||
/** timestamp for all events */ | |||
private long timestamp = System.currentTimeMillis(); | |||
/** name of testcase(method name) or testsuite (classname) */ | |||
@@ -93,19 +96,28 @@ public class TestRunEvent extends EventObject { | |||
/** properties for end of testrun */ | |||
private Properties props; | |||
/** handy result for each end of sequence */ | |||
private TestSummary result; | |||
public TestRunEvent(Integer id, int type){ | |||
super(id); | |||
this.type = type; | |||
} | |||
public TestRunEvent(Integer id, int type, String name, TestSummary result){ | |||
this(id, type, name); | |||
this.result = result; | |||
} | |||
public TestRunEvent(Integer id, int type, String name){ | |||
this(id, type); | |||
this.name = name; | |||
} | |||
public TestRunEvent(Integer id, int type, Properties props){ | |||
public TestRunEvent(Integer id, int type, Properties props, TestSummary result){ | |||
this(id, type); | |||
this.props = props; | |||
this.result = result; | |||
} | |||
public TestRunEvent(Integer id, int type, String name, Throwable t){ | |||
@@ -145,6 +157,10 @@ public class TestRunEvent extends EventObject { | |||
return name; | |||
} | |||
public TestSummary getSummary(){ | |||
return result; | |||
} | |||
public String getStackTrace(){ | |||
return stacktrace; | |||
} | |||
@@ -160,7 +176,8 @@ public class TestRunEvent extends EventObject { | |||
(timestamp == other.timestamp) && | |||
( name == null ? other.name == null : name.equals(other.name) ) && | |||
( stacktrace == null ? other.stacktrace == null : stacktrace.equals(other.stacktrace) ) && | |||
( props == null ? other.props == null : props.equals(other.props) ) ) ; | |||
( props == null ? other.props == null : props.equals(other.props) ) && | |||
( result == null ? other.result == null : result.equals(other.result) ) ); | |||
} | |||
return false; | |||
} | |||
@@ -62,6 +62,11 @@ import java.util.Properties; | |||
import java.util.StringTokenizer; | |||
import java.util.Vector; | |||
import java.util.Random; | |||
import java.util.Map; | |||
import java.util.HashMap; | |||
import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.Iterator; | |||
import junit.framework.AssertionFailedError; | |||
import junit.framework.Test; | |||
@@ -71,6 +76,8 @@ import junit.framework.TestResult; | |||
import junit.framework.TestSuite; | |||
import org.apache.tools.ant.taskdefs.optional.rjunit.JUnitHelper; | |||
import org.apache.tools.ant.taskdefs.optional.rjunit.formatter.Formatter; | |||
import org.apache.tools.ant.taskdefs.optional.rjunit.formatter.PlainFormatter; | |||
import org.apache.tools.ant.util.StringUtils; | |||
/** | |||
@@ -95,10 +102,11 @@ public class TestRunner implements TestListener { | |||
/** port to connect to */ | |||
private int port = -1; | |||
/** handy debug flag */ | |||
private boolean debug = false; | |||
/** the list of test class names to run */ | |||
private Vector testClassNames = new Vector(); | |||
private final ArrayList testClassNames = new ArrayList(); | |||
/** result of the current test */ | |||
private TestResult testResult; | |||
@@ -109,8 +117,14 @@ public class TestRunner implements TestListener { | |||
/** writer to send message to the server */ | |||
private Messenger messenger; | |||
/** helpful formatter to debug events directly here */ | |||
private final Formatter debugFormatter = new PlainFormatter(); | |||
/** bean constructor */ | |||
public TestRunner() { | |||
Properties props = new Properties(); | |||
props.setProperty("file", "rjunit-client-debug.log"); | |||
debugFormatter.init(props); | |||
} | |||
/** | |||
@@ -142,7 +156,7 @@ public class TestRunner implements TestListener { | |||
* @param classname the class name of the test to run. | |||
*/ | |||
public void addTestClassName(String classname) { | |||
testClassNames.addElement(classname); | |||
testClassNames.add(classname); | |||
} | |||
/** | |||
@@ -265,16 +279,16 @@ public class TestRunner implements TestListener { | |||
* @throws Exception a generic exception that can be thrown while | |||
* instantiating a test case. | |||
*/ | |||
protected Test[] getSuites() throws Exception { | |||
protected Map getSuites() throws Exception { | |||
final int count = testClassNames.size(); | |||
log("Extracting testcases from " + count + " classnames..."); | |||
final Vector suites = new Vector(count); | |||
final Map suites = new HashMap(); | |||
for (int i = 0; i < count; i++) { | |||
String classname = (String) testClassNames.elementAt(i); | |||
String classname = (String) testClassNames.get(i); | |||
try { | |||
Test test = JUnitHelper.getTest(null, classname); | |||
if (test != null) { | |||
suites.addElement(test); | |||
suites.put(classname, test); | |||
} | |||
} catch (Exception e) { | |||
// notify log error instead ? | |||
@@ -283,9 +297,7 @@ public class TestRunner implements TestListener { | |||
} | |||
} | |||
log("Extracted " + suites.size() + " testcases."); | |||
Test[] array = new Test[suites.size()]; | |||
suites.copyInto(array); | |||
return array; | |||
return suites; | |||
} | |||
/** | |||
@@ -293,41 +305,75 @@ public class TestRunner implements TestListener { | |||
*/ | |||
private void runTests() throws Exception { | |||
Test[] suites = getSuites(); | |||
Map suites = getSuites(); | |||
// count all testMethods and inform TestRunListeners | |||
int count = countTests(suites); | |||
int count = countTests(suites.values()); | |||
log("Total tests to run: " + count); | |||
fireEvent(new TestRunEvent(id, TestRunEvent.RUN_STARTED)); | |||
long startTime = System.currentTimeMillis(); | |||
for (int i = 0; i < suites.length; i++) { | |||
String name = suites[i].getClass().getName(); | |||
if (suites[i] instanceof TestCase) { | |||
suites[i] = new TestSuite(name); | |||
TestRunEvent evt = new TestRunEvent(id, TestRunEvent.RUN_STARTED); | |||
if (debug){ | |||
debugFormatter.onRunStarted(evt); | |||
} | |||
fireEvent(evt); | |||
TestSummary runSummary = new TestSummary(); | |||
runSummary.start(testResult); | |||
for (Iterator it = suites.entrySet().iterator(); it.hasNext(); ) { | |||
Map.Entry entry = (Map.Entry)it.next(); | |||
String name = (String)entry.getKey(); | |||
Test test = (Test)entry.getValue(); | |||
if (test instanceof TestCase) { | |||
test = new TestSuite(name); | |||
} | |||
log("running suite: " + suites[i]); | |||
fireEvent(new TestRunEvent(id, TestRunEvent.SUITE_STARTED, name)); | |||
suites[i].run(testResult); | |||
fireEvent(new TestRunEvent(id, TestRunEvent.SUITE_ENDED, name)); | |||
runTest(test, name); | |||
} | |||
runSummary.stop(testResult); | |||
// inform TestRunListeners of test end | |||
long elapsedTime = System.currentTimeMillis() - startTime; | |||
if (testResult == null || testResult.shouldStop()) { | |||
fireEvent(new TestRunEvent(id, TestRunEvent.RUN_STOPPED, System.getProperties())); | |||
} else { | |||
fireEvent(new TestRunEvent(id, TestRunEvent.RUN_ENDED, System.getProperties())); | |||
int type = (testResult == null || testResult.shouldStop()) ? | |||
TestRunEvent.RUN_STOPPED : TestRunEvent.RUN_ENDED; | |||
evt = new TestRunEvent(id, type, System.getProperties(), runSummary); | |||
if (debug){ | |||
debugFormatter.onRunEnded(evt); | |||
} | |||
log("Finished after " + elapsedTime + "ms"); | |||
fireEvent(evt); | |||
log("Finished after " + runSummary.elapsedTime() + "ms"); | |||
shutDown(); | |||
} | |||
/** count the number of test methods in all tests */ | |||
private final int countTests(Test[] tests) { | |||
/** | |||
* run a single suite and dispatch its results. | |||
* @param test the instance of the testsuite to run. | |||
* @param name the name of the testsuite (classname) | |||
*/ | |||
private void runTest(Test test, String name){ | |||
TestRunEvent evt = new TestRunEvent(id, TestRunEvent.SUITE_STARTED, name); | |||
if (debug){ | |||
debugFormatter.onSuiteStarted(evt); | |||
} | |||
fireEvent(evt); | |||
TestSummary suiteSummary = new TestSummary(); | |||
suiteSummary.start(testResult); | |||
try { | |||
test.run(testResult); | |||
} finally { | |||
suiteSummary.stop(testResult); | |||
evt = new TestRunEvent(id, TestRunEvent.SUITE_ENDED, name, suiteSummary); | |||
if (debug){ | |||
debugFormatter.onSuiteEnded(evt); | |||
} | |||
fireEvent(evt); | |||
} | |||
} | |||
/** | |||
* count the number of test methods in all tests | |||
*/ | |||
private final int countTests(Collection tests) { | |||
int count = 0; | |||
for (int i = 0; i < tests.length; i++) { | |||
count = count + tests[i].countTestCases(); | |||
for (Iterator it = tests.iterator(); it.hasNext(); ) { | |||
Test test = (Test)it.next(); | |||
count = count + test.countTestCases(); | |||
} | |||
return count; | |||
} | |||
@@ -383,14 +429,20 @@ public class TestRunner implements TestListener { | |||
public void startTest(Test test) { | |||
String testName = test.toString(); | |||
log("starting test: " + test); | |||
fireEvent(new TestRunEvent(id, TestRunEvent.TEST_STARTED, testName)); | |||
TestRunEvent evt = new TestRunEvent(id, TestRunEvent.TEST_STARTED, testName); | |||
if (debug){ | |||
debugFormatter.onTestStarted(evt); | |||
} | |||
fireEvent(evt); | |||
} | |||
public void addError(Test test, Throwable t) { | |||
log("Adding error for test: " + test); | |||
String testName = test.toString(); | |||
fireEvent(new TestRunEvent(id, TestRunEvent.TEST_ERROR, testName, t)); | |||
TestRunEvent evt = new TestRunEvent(id, TestRunEvent.TEST_ERROR, testName, t); | |||
if (debug){ | |||
debugFormatter.onTestError(evt); | |||
} | |||
fireEvent(evt); | |||
} | |||
/** | |||
@@ -406,15 +458,21 @@ public class TestRunner implements TestListener { | |||
* @see addFailure(Test, AssertionFailedError) | |||
*/ | |||
public void addFailure(Test test, Throwable t) { | |||
log("Adding failure for test: " + test); | |||
String testName = test.toString(); | |||
fireEvent(new TestRunEvent(id, TestRunEvent.TEST_FAILURE, testName, t)); | |||
TestRunEvent evt = new TestRunEvent(id, TestRunEvent.TEST_FAILURE, testName, t); | |||
if (debug){ | |||
debugFormatter.onTestFailure(evt); | |||
} | |||
fireEvent(evt); | |||
} | |||
public void endTest(Test test) { | |||
log("Ending test: " + test); | |||
String testName = test.toString(); | |||
fireEvent(new TestRunEvent(id, TestRunEvent.TEST_ENDED, testName)); | |||
TestRunEvent evt = new TestRunEvent(id, TestRunEvent.TEST_ENDED, testName); | |||
if (debug){ | |||
debugFormatter.onTestEnded(evt); | |||
} | |||
fireEvent(evt); | |||
} | |||
public void log(String msg) { | |||
@@ -0,0 +1,177 @@ | |||
/* | |||
* The Apache Software License, Version 1.1 | |||
* | |||
* Copyright (c) 2002 The Apache Software Foundation. All rights | |||
* reserved. | |||
* | |||
* Redistribution and use in source and binary forms, with or without | |||
* modification, are permitted provided that the following conditions | |||
* are met: | |||
* | |||
* 1. Redistributions of source code must retain the above copyright | |||
* notice, this list of conditions and the following disclaimer. | |||
* | |||
* 2. Redistributions in binary form must reproduce the above copyright | |||
* notice, this list of conditions and the following disclaimer in | |||
* the documentation and/or other materials provided with the | |||
* distribution. | |||
* | |||
* 3. The end-user documentation included with the redistribution, if | |||
* any, must include the following acknowlegement: | |||
* "This product includes software developed by the | |||
* Apache Software Foundation (http://www.apache.org/)." | |||
* Alternately, this acknowlegement may appear in the software itself, | |||
* if and wherever such third-party acknowlegements normally appear. | |||
* | |||
* 4. The names "The Jakarta Project", "Ant", and "Apache Software | |||
* Foundation" must not be used to endorse or promote products derived | |||
* from this software without prior written permission. For written | |||
* permission, please contact apache@apache.org. | |||
* | |||
* 5. Products derived from this software may not be called "Apache" | |||
* nor may "Apache" appear in their names without prior written | |||
* permission of the Apache Group. | |||
* | |||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED | |||
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | |||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |||
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR | |||
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF | |||
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | |||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT | |||
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | |||
* SUCH DAMAGE. | |||
* ==================================================================== | |||
* | |||
* This software consists of voluntary contributions made by many | |||
* individuals on behalf of the Apache Software Foundation. For more | |||
* information on the Apache Software Foundation, please see | |||
* <http://www.apache.org/>. | |||
*/ | |||
package org.apache.tools.ant.taskdefs.optional.rjunit.remote; | |||
import java.io.Serializable; | |||
import junit.framework.AssertionFailedError; | |||
import junit.framework.Test; | |||
import junit.framework.TestListener; | |||
import junit.framework.TestResult; | |||
/** | |||
* A helpful test summary that is somewhat similar to <tt>TestResult</tt>. | |||
* Here the difference is that this test summary should register to | |||
* the test result the time you wan to collect information. | |||
* | |||
* @author <a href="mailto:sbailliez@apache.org">Stephane Bailliez</a> | |||
*/ | |||
public final class TestSummary implements Serializable, TestListener { | |||
/** time elapsed during tests run in ms */ | |||
private long elapsedTime; | |||
/** number of errors */ | |||
private int errorCount; | |||
/** number of successes */ | |||
private int successCount; | |||
/** number of failures */ | |||
private int failureCount; | |||
/** number of runs */ | |||
private int runCount; | |||
private transient String toString; | |||
/** bean constructor */ | |||
public TestSummary() { | |||
} | |||
/** | |||
* @return the number of errors that occurred in this test. | |||
*/ | |||
public int errorCount() { | |||
return errorCount; | |||
} | |||
/** | |||
* @return the number of successes that occurred in this test. | |||
*/ | |||
public int successCount() { | |||
return successCount; | |||
} | |||
/** | |||
* @return the number of failures that occurred in this test. | |||
*/ | |||
public int failureCount() { | |||
return failureCount; | |||
} | |||
/** | |||
* @return the number of runs that occurred in this test. | |||
* a run is the sum of failures + errors + successes. | |||
*/ | |||
public int runCount() { | |||
return runCount; | |||
} | |||
/** | |||
* @return the elapsed time in ms | |||
*/ | |||
public long elapsedTime() { | |||
return elapsedTime; | |||
} | |||
// | |||
/** | |||
* register to the <tt>TestResult</tt> and starts the time counter. | |||
* @param result the instance to register to. | |||
* @see #stop() | |||
*/ | |||
public void start(TestResult result){ | |||
elapsedTime = System.currentTimeMillis(); | |||
result.addListener(this); | |||
} | |||
/** | |||
* unregister from the <tt>TestResult</tt> and stops the time counter. | |||
* @param result the instance to unregister from. | |||
* @see #start() | |||
*/ | |||
public void stop(TestResult result){ | |||
elapsedTime = System.currentTimeMillis() - elapsedTime; | |||
result.removeListener(this); | |||
} | |||
// test listener implementation | |||
public void addError(Test test, Throwable throwable) { | |||
errorCount++; | |||
} | |||
public void addFailure(Test test, AssertionFailedError error) { | |||
failureCount++; | |||
} | |||
public void endTest(Test test) { | |||
successCount++; | |||
} | |||
public void startTest(Test test) { | |||
runCount++; | |||
} | |||
public String toString(){ | |||
StringBuffer buf = new StringBuffer(); | |||
buf.append("run: ").append(runCount); | |||
buf.append(" success: ").append(successCount); | |||
buf.append(" failures: ").append(failureCount); | |||
buf.append(" errors: ").append(errorCount); | |||
buf.append(" elapsed: ").append(elapsedTime); | |||
return buf.toString(); | |||
} | |||
} |