Trac #208: New option classdumpdir for JaCoCo agent
diff --git a/jacoco-maven-plugin.test/it/it-customize-agent/pom.xml b/jacoco-maven-plugin.test/it/it-customize-agent/pom.xml
index 49029cc..f356b7c 100644
--- a/jacoco-maven-plugin.test/it/it-customize-agent/pom.xml
+++ b/jacoco-maven-plugin.test/it/it-customize-agent/pom.xml
@@ -31,6 +31,7 @@
     <jacoco.output>file</jacoco.output>
     <jacoco.address>localhost</jacoco.address>
     <jacoco.port>9999</jacoco.port>
+    <jacoco.classDumpDir>${project.build.directory}/classdumps</jacoco.classDumpDir>
   </properties>
 
   <build>
diff --git a/jacoco-maven-plugin.test/it/it-customize-agent/verify.bsh b/jacoco-maven-plugin.test/it/it-customize-agent/verify.bsh
index 230b2d2..7894b0a 100644
--- a/jacoco-maven-plugin.test/it/it-customize-agent/verify.bsh
+++ b/jacoco-maven-plugin.test/it/it-customize-agent/verify.bsh
@@ -21,10 +21,11 @@
     + ",dumponexit=true"
     + ",output=file"
     + ",address=localhost"
-    + ",port=9999";
+    + ",port=9999"
+    + ",classdumpdir=" + basedir + File.separator + "target" + File.separator + "classdumps";
 String buildLog = FileUtils.fileRead( new File( basedir, "build.log" ) );
 if ( buildLog.indexOf( agentOptions ) < 0 ) {
-    throw new RuntimeException( "Property was not configured" );
+    throw new RuntimeException( "Property was not configured, expected " + agentOptions );
 }
 
 File file = new File( basedir, "target/coverage.exec" );
diff --git a/jacoco-maven-plugin/src/org/jacoco/maven/AgentMojo.java b/jacoco-maven-plugin/src/org/jacoco/maven/AgentMojo.java
index 10ff314..06c8d03 100644
--- a/jacoco-maven-plugin/src/org/jacoco/maven/AgentMojo.java
+++ b/jacoco-maven-plugin/src/org/jacoco/maven/AgentMojo.java
@@ -153,6 +153,16 @@
 	 */
 	private Integer port;
 
+	/**
+	 * If a directory is specified for this parameter the JaCoCo agent dumps all
+	 * class files it processes to the given location. This can be useful for
+	 * debugging purposes or in case of dynamically created classes for example
+	 * when scripting engines are used.
+	 * 
+	 * @parameter expression="${jacoco.classDumpDir}"
+	 */
+	private File classDumpDir;
+
 	@Override
 	public void executeMojo() {
 		final String vmArgument = StringUtils.quoteAndEscape(
@@ -174,8 +184,7 @@
 
 	private AgentOptions createAgentOptions() {
 		final AgentOptions agentOptions = new AgentOptions();
-		final String destPath = destFile.getAbsolutePath();
-		agentOptions.setDestfile(destPath);
+		agentOptions.setDestfile(destFile.getAbsolutePath());
 		if (append != null) {
 			agentOptions.setAppend(append.booleanValue());
 		}
@@ -207,6 +216,9 @@
 		if (port != null) {
 			agentOptions.setPort(port.intValue());
 		}
+		if (classDumpDir != null) {
+			agentOptions.setClassDumpDir(classDumpDir.getAbsolutePath());
+		}
 		return agentOptions;
 	}
 
diff --git a/org.jacoco.agent.rt.test/src/org/jacoco/agent/rt/ClassFileDumperTest.java b/org.jacoco.agent.rt.test/src/org/jacoco/agent/rt/ClassFileDumperTest.java
new file mode 100644
index 0000000..27d997e
--- /dev/null
+++ b/org.jacoco.agent.rt.test/src/org/jacoco/agent/rt/ClassFileDumperTest.java
@@ -0,0 +1,76 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2012 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *    
+ *******************************************************************************/
+package org.jacoco.agent.rt;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Unit tests for {@link ClassFileDumper}.
+ */
+public class ClassFileDumperTest {
+
+	@Rule
+	public TemporaryFolder folder = new TemporaryFolder();
+
+	private byte[] contents;
+
+	@Before
+	public void setup() throws IOException {
+		contents = "just some bytes".getBytes("UTF-8");
+	}
+
+	@Test
+	public void testDumpClassWithPackage() throws IOException {
+		final File location = new File(folder.getRoot(), "classes");
+		final ClassFileDumper dumper = new ClassFileDumper(location.toString());
+		dumper.dump("org/jacoco/examples/Foo$Inner", contents);
+		assertContents(location, "org/jacoco/examples/Foo$Inner.class");
+	}
+
+	@Test
+	public void testDumpClassInDefaultPackage() throws IOException {
+		final File location = new File(folder.getRoot(), "classes");
+		final ClassFileDumper dumper = new ClassFileDumper(location.toString());
+		dumper.dump("Main", contents);
+		assertContents(location, "Main.class");
+	}
+
+	@Test
+	public void testNoDumps() throws IOException {
+		final ClassFileDumper dumper = new ClassFileDumper(null);
+		dumper.dump("Main", contents);
+	}
+
+	private void assertContents(File location, String filename)
+			throws IOException {
+		InputStream in = new FileInputStream(new File(location, filename));
+		ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+		int b;
+		while ((b = in.read()) != -1) {
+			buffer.write(b);
+		}
+		in.close();
+		assertArrayEquals(contents, buffer.toByteArray());
+	}
+
+}
diff --git a/org.jacoco.agent.rt/src/org/jacoco/agent/rt/ClassFileDumper.java b/org.jacoco.agent.rt/src/org/jacoco/agent/rt/ClassFileDumper.java
new file mode 100644
index 0000000..9749ae1
--- /dev/null
+++ b/org.jacoco.agent.rt/src/org/jacoco/agent/rt/ClassFileDumper.java
@@ -0,0 +1,72 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2012 Mountainminds GmbH & Co. KG and Contributors
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Marc R. Hoffmann - initial API and implementation
+ *    
+ *******************************************************************************/
+package org.jacoco.agent.rt;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Internal dumper for class files.
+ */
+class ClassFileDumper {
+
+	private final File location;
+
+	/**
+	 * Create a new dumper for the given location.
+	 * 
+	 * @param location
+	 *            relative path to dump directory. <code>null</code> if no dumps
+	 *            should be written
+	 */
+	ClassFileDumper(final String location) {
+		if (location == null) {
+			this.location = null;
+		} else {
+			this.location = new File(location);
+		}
+	}
+
+	/**
+	 * Dumps the given binary content under the given name if a non-
+	 * <code>null</code> location has been specified.
+	 * 
+	 * @param name
+	 *            qualified class name in VM notation
+	 * @param contents
+	 *            binary contents
+	 * @throws IOException
+	 *             in case of problems while dumping the file
+	 */
+	void dump(final String name, final byte[] contents) throws IOException {
+		if (location != null) {
+			final File outputdir;
+			final String localname;
+			final int pkgpos = name.lastIndexOf('/');
+			if (pkgpos != -1) {
+				outputdir = new File(location, name.substring(0, pkgpos));
+				localname = name.substring(pkgpos + 1);
+			} else {
+				outputdir = location;
+				localname = name;
+			}
+			outputdir.mkdirs();
+			final File file = new File(outputdir, localname + ".class");
+			final OutputStream out = new FileOutputStream(file);
+			out.write(contents);
+			out.close();
+		}
+	}
+
+}
diff --git a/org.jacoco.agent.rt/src/org/jacoco/agent/rt/CoverageTransformer.java b/org.jacoco.agent.rt/src/org/jacoco/agent/rt/CoverageTransformer.java
index 01cb69f..a0b3290 100644
--- a/org.jacoco.agent.rt/src/org/jacoco/agent/rt/CoverageTransformer.java
+++ b/org.jacoco.agent.rt/src/org/jacoco/agent/rt/CoverageTransformer.java
@@ -46,6 +46,8 @@
 

 	private final WildcardMatcher exclClassloader;

 

+	private final ClassFileDumper classFileDumper;

+

 	/**

 	 * New transformer with the given delegates.

 	 * 

@@ -68,6 +70,7 @@
 				toWildcard(toVMName(options.getExcludes())));

 		exclClassloader = new WildcardMatcher(

 				toWildcard(options.getExclClassloader()));

+		classFileDumper = new ClassFileDumper(options.getClassDumpDir());

 	}

 

 	public byte[] transform(final ClassLoader loader, final String classname,

@@ -80,6 +83,7 @@
 		}

 

 		try {

+			classFileDumper.dump(classname, classfileBuffer);

 			if (classBeingRedefined != null) {

 				// For redefined classes we must clear the execution data

 				// reference as probes might have changed.

diff --git a/org.jacoco.ant.test/src/org/jacoco/ant/AgentTaskTest.xml b/org.jacoco.ant.test/src/org/jacoco/ant/AgentTaskTest.xml
index c6874e3..ecbc9d2 100644
--- a/org.jacoco.ant.test/src/org/jacoco/ant/AgentTaskTest.xml
+++ b/org.jacoco.ant.test/src/org/jacoco/ant/AgentTaskTest.xml
@@ -18,7 +18,9 @@
 	<target name="testCoverageAgent">

 		<jacoco:agent property="jacocoagent" append="false" destfile="test.exec"

 			exclClassLoader="EvilClassLoader" includes="org.example.*"

-		    excludes="*Test" sessionid="testid" dumponexit="false" output="tcpclient" address="remotehost" port="1234"/>

+		    excludes="*Test" sessionid="testid" dumponexit="false"

+			output="tcpclient" address="remotehost" port="1234"

+			classdumpdir="target/dump"/>

 		<au:assertPropertySet name="jacocoagent"/>

 		<au:assertPropertyContains name="jacocoagent" value="-javaagent:"/>

 		<au:assertPropertyContains name="jacocoagent" value="append=false"/>

@@ -32,6 +34,8 @@
 		<au:assertPropertyContains name="jacocoagent" value="output=tcpclient"/>

 		<au:assertPropertyContains name="jacocoagent" value="address=remotehost"/>

 		<au:assertPropertyContains name="jacocoagent" value="port=1234"/>

+		<property name="dump.dir" location="target/dump"/>

+		<au:assertPropertyContains name="jacocoagent" value="classdumpdir=${dump.dir}"/>

 	</target>

 	

 	<target name="testCoverageAgentDisabled">

diff --git a/org.jacoco.ant/src/org/jacoco/ant/AbstractCoverageTask.java b/org.jacoco.ant/src/org/jacoco/ant/AbstractCoverageTask.java
index 4f94eb5..6c8d37c 100644
--- a/org.jacoco.ant/src/org/jacoco/ant/AbstractCoverageTask.java
+++ b/org.jacoco.ant/src/org/jacoco/ant/AbstractCoverageTask.java
@@ -172,6 +172,17 @@
 	}

 

 	/**

+	 * Sets the directory where all class files seen by the agent should be

+	 * dumped to.

+	 * 

+	 * @param dir

+	 *            dump output location

+	 */

+	public void setClassdumpdir(final File dir) {

+		agentOptions.setClassDumpDir(dir.getAbsolutePath());

+	}

+

+	/**

 	 * Creates JVM argument to launch with the specified JaCoCo agent jar and

 	 * the current options

 	 * 

diff --git a/org.jacoco.core.test/src/org/jacoco/core/runtime/AgentOptionsTest.java b/org.jacoco.core.test/src/org/jacoco/core/runtime/AgentOptionsTest.java
index 63ff910..52b77a5 100644
--- a/org.jacoco.core.test/src/org/jacoco/core/runtime/AgentOptionsTest.java
+++ b/org.jacoco.core.test/src/org/jacoco/core/runtime/AgentOptionsTest.java
@@ -45,9 +45,10 @@
 		assertNull(options.getSessionId());

 		assertTrue(options.getDumpOnExit());

 		assertEquals(6300, options.getPort());

-		assertEquals(null, options.getAddress());

+		assertNull(options.getAddress());

 		assertEquals(AgentOptions.OutputMode.file, options.getOutput());

 		assertEquals("", options.toString());

+		assertNull(options.getClassDumpDir());

 	}

 

 	@Test

@@ -273,6 +274,20 @@
 	}

 

 	@Test

+	public void testGetClassDumpDir() {

+		AgentOptions options = new AgentOptions("classdumpdir=target/dump");

+		assertEquals("target/dump", options.getClassDumpDir());

+	}

+

+	@Test

+	public void testSetClassDumpDir() {

+		AgentOptions options = new AgentOptions();

+		options.setClassDumpDir("target/dump");

+		assertEquals("target/dump", options.getClassDumpDir());

+		assertEquals("classdumpdir=target/dump", options.toString());

+	}

+

+	@Test

 	public void testVMArgsWithNoOptions() {

 		AgentOptions options = new AgentOptions();

 		String vmArgument = options.getVMArgument(defaultAgentJarFile);

diff --git a/org.jacoco.core/src/org/jacoco/core/runtime/AgentOptions.java b/org.jacoco.core/src/org/jacoco/core/runtime/AgentOptions.java
index cc828fe..16067a6 100644
--- a/org.jacoco.core/src/org/jacoco/core/runtime/AgentOptions.java
+++ b/org.jacoco.core/src/org/jacoco/core/runtime/AgentOptions.java
@@ -148,9 +148,16 @@
 	 */

 	public static final int DEFAULT_PORT = 6300;

 

+	/**

+	 * Specifies where the agent dumps all class files it encounters. The

+	 * location is specified as a relative path to the working directory.

+	 * Default is <code>null</code> (no dumps).

+	 */

+	public static final String CLASSDUMPDIR = "classdumpdir";

+

 	private static final Collection<String> VALID_OPTIONS = Arrays.asList(

 			DESTFILE, APPEND, INCLUDES, EXCLUDES, EXCLCLASSLOADER, SESSIONID,

-			DUMPONEXIT, OUTPUT, ADDRESS, PORT);

+			DUMPONEXIT, OUTPUT, ADDRESS, PORT, CLASSDUMPDIR);

 

 	private final Map<String, String> options;

 

@@ -415,6 +422,26 @@
 		setOption(OUTPUT, output.name());

 	}

 

+	/**

+	 * Returns the location of the directory where class files should be dumped

+	 * to.

+	 * 

+	 * @return dump location or <code>null</code> (no dumps)

+	 */

+	public String getClassDumpDir() {

+		return getOption(CLASSDUMPDIR, null);

+	}

+

+	/**

+	 * Sets the directory where class files should be dumped to.

+	 * 

+	 * @param location

+	 *            dump location or <code>null</code> (no dumps)

+	 */

+	public void setClassDumpDir(final String location) {

+		setOption(CLASSDUMPDIR, location);

+	}

+

 	private void setOption(final String key, final int value) {

 		setOption(key, Integer.toString(value));

 	}

diff --git a/org.jacoco.doc/docroot/doc/agent.html b/org.jacoco.doc/docroot/doc/agent.html
index 79c5003..f5cf866 100644
--- a/org.jacoco.doc/docroot/doc/agent.html
+++ b/org.jacoco.doc/docroot/doc/agent.html
@@ -167,6 +167,15 @@
       </td>
       <td><code>6300</code></td>
     </tr>
+    <tr>
+      <td><code>classdumpdir</code></td>
+      <td>Location relative to the working directory where all class files seen
+          by the agent are dumped to. This can be useful for debugging purposes
+          or in case of dynamically created classes for example when scripting
+          engines are used.
+      </td>
+      <td><i>no dumps</i></td>
+    </tr>
   </tbody>
 </table>
 
diff --git a/org.jacoco.doc/docroot/doc/ant.html b/org.jacoco.doc/docroot/doc/ant.html
index 38d8bdd..4da2fff 100644
--- a/org.jacoco.doc/docroot/doc/ant.html
+++ b/org.jacoco.doc/docroot/doc/ant.html
@@ -254,6 +254,15 @@
       </td>
       <td><code>6300</code></td>
     </tr>
+    <tr>
+      <td><code>classdumpdir</code></td>
+      <td>Location relative to the working directory where all class files seen
+          by the agent are dumped to. This can be useful for debugging purposes
+          or in case of dynamically created classes for example when scripting
+          engines are used.
+      </td>
+      <td><i>no dumps</i></td>
+    </tr>
   </tbody>
 </table>
 
diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index 1c3f073..683cb6e 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -20,11 +20,18 @@
 
 <h2>Trunk Build @qualified.bundle.version@ (@build.date@)</h2>
 
+<h3>New Features</h3>
+<ul>
+  <li>Support for parallel Maven builds (Trac #191).</li>
+  <li>New agent option <code>classdumpdir</code> to dump all class files seen
+      by the JaCoCo agent to disk. This option is also available for Ant and
+      Maven (Trac #208).</li>
+</ul>
+
 <h3>Non-functional Changes</h3>
 <ul>
   <li>Documentation now includes Maven example and Maven goal documentation
       (Trac #201, #202).</li>
-  <li>Support for parallel Maven builds (Trac #191).</li>
   <li>Reworked instrumentation strategy to avoid verifier error "Uninitialized
       object exists on backward branch" with certain Java 7 class files
       (Trac #154).</li>