Trac. #78: Sniff file types by contents, support nested archives.
diff --git a/org.jacoco.ant/src/org/jacoco/ant/ReportTask.java b/org.jacoco.ant/src/org/jacoco/ant/ReportTask.java
index c74fe0c..1354556 100644
--- a/org.jacoco.ant/src/org/jacoco/ant/ReportTask.java
+++ b/org.jacoco.ant/src/org/jacoco/ant/ReportTask.java
@@ -481,17 +481,9 @@
final Resource resource = (Resource) i.next();
if (resource.isDirectory() && resource instanceof FileResource) {
analyzer.analyzeAll(((FileResource) resource).getFile());
- continue;
- }
- if (resource.getName().toLowerCase().endsWith(".jar")) {
+ } else {
final InputStream in = resource.getInputStream();
- analyzer.analyzeJAR(in);
- in.close();
- continue;
- }
- if (resource.getName().toLowerCase().endsWith(".class")) {
- final InputStream in = resource.getInputStream();
- analyzer.analyze(in);
+ analyzer.analyzeAll(in);
in.close();
}
}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/instr/AnalyzerTest.java b/org.jacoco.core.test/src/org/jacoco/core/instr/AnalyzerTest.java
index 9a1cccb..885087c 100644
--- a/org.jacoco.core.test/src/org/jacoco/core/instr/AnalyzerTest.java
+++ b/org.jacoco.core.test/src/org/jacoco/core/instr/AnalyzerTest.java
@@ -12,12 +12,23 @@
*******************************************************************************/
package org.jacoco.core.instr;
-import java.io.File;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
import org.jacoco.core.data.IClassStructureVisitor;
import org.jacoco.core.data.IMethodStructureVisitor;
import org.jacoco.core.data.IStructureVisitor;
+import org.jacoco.core.test.TargetLoader;
+import org.junit.Before;
import org.junit.Test;
/**
@@ -28,14 +39,11 @@
*/
public class AnalyzerTest {
- @Test(expected = IOException.class)
- public void testInvalidDirectory() throws IOException {
- Analyzer analyzer = new Analyzer(new EmptyStructureVisitor());
- File invalid = new File("/this/path/should/not/exist/");
- analyzer.analyzeAll(invalid);
- }
+ private Analyzer analyzer;
- private static class EmptyStructureVisitor implements IStructureVisitor,
+ private final Set<String> classes = new HashSet<String>();
+
+ private class EmptyStructureVisitor implements IStructureVisitor,
IClassStructureVisitor, IMethodStructureVisitor {
public IClassStructureVisitor visitClassStructure(long id) {
@@ -44,6 +52,7 @@
public void visit(String name, String signature, String superName,
String[] interfaces) {
+ assertTrue("Class already processed: " + name, classes.add(name));
}
public void visitSourceFile(String name) {
@@ -62,4 +71,71 @@
}
+ @Before
+ public void setup() {
+ analyzer = new Analyzer(new EmptyStructureVisitor());
+ }
+
+ @Test
+ public void testAnalyzeClass1() throws IOException {
+ analyzer.analyzeClass(TargetLoader.getClassData(AnalyzerTest.class));
+ assertEquals(Collections
+ .singleton("org/jacoco/core/instr/AnalyzerTest"), classes);
+ }
+
+ @Test
+ public void testAnalyzeClass2() throws IOException {
+ analyzer.analyzeClass(TargetLoader
+ .getClassDataAsBytes(AnalyzerTest.class));
+ assertEquals(Collections
+ .singleton("org/jacoco/core/instr/AnalyzerTest"), classes);
+ }
+
+ @Test
+ public void testAnalyzeArchive() throws IOException {
+ final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ final ZipOutputStream zip = new ZipOutputStream(buffer);
+ zip.putNextEntry(new ZipEntry(
+ "org/jacoco/core/instr/AnalyzerTest.class"));
+ zip.write(TargetLoader.getClassDataAsBytes(AnalyzerTest.class));
+ zip.finish();
+ final int count = analyzer.analyzeArchive(new ByteArrayInputStream(
+ buffer.toByteArray()));
+ assertEquals(1, count);
+ assertEquals(Collections
+ .singleton("org/jacoco/core/instr/AnalyzerTest"), classes);
+ }
+
+ @Test
+ public void testAnalyzeAll1() throws IOException {
+ final int count = analyzer.analyzeAll(TargetLoader
+ .getClassData(AnalyzerTest.class));
+ assertEquals(1, count);
+ assertEquals(Collections
+ .singleton("org/jacoco/core/instr/AnalyzerTest"), classes);
+ }
+
+ @Test
+ public void testAnalyzeAll2() throws IOException {
+ final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ final ZipOutputStream zip = new ZipOutputStream(buffer);
+ zip.putNextEntry(new ZipEntry(
+ "org/jacoco/core/instr/AnalyzerTest.class"));
+ zip.write(TargetLoader.getClassDataAsBytes(AnalyzerTest.class));
+ zip.finish();
+ final int count = analyzer.analyzeAll(new ByteArrayInputStream(buffer
+ .toByteArray()));
+ assertEquals(1, count);
+ assertEquals(Collections
+ .singleton("org/jacoco/core/instr/AnalyzerTest"), classes);
+ }
+
+ @Test
+ public void testAnalyzeAll3() throws IOException {
+ final int count = analyzer.analyzeAll(new ByteArrayInputStream(
+ new byte[0]));
+ assertEquals(0, count);
+ assertEquals(Collections.emptySet(), classes);
+ }
+
}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/instr/ContentTypeDetectorTest.java b/org.jacoco.core.test/src/org/jacoco/core/instr/ContentTypeDetectorTest.java
new file mode 100644
index 0000000..c968a0e
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/instr/ContentTypeDetectorTest.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2010 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
+ *
+ * $Id: $
+ *******************************************************************************/
+package org.jacoco.core.instr;
+
+import static junit.framework.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.jacoco.core.test.TargetLoader;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link ContentTypeDetector}.
+ *
+ * @author Marc R. Hoffmann
+ * @version $Revision: $
+ */
+public class ContentTypeDetectorTest {
+
+ private byte[] data;
+
+ private ContentTypeDetector detector;
+
+ @Test
+ public void testEmptyStream() throws IOException {
+ initData();
+ assertContent();
+ }
+
+ @Test
+ public void testClassFile() throws IOException {
+ initData(TargetLoader
+ .getClassDataAsBytes(ContentTypeDetectorTest.class));
+ assertEquals(ContentTypeDetector.CLASSFILE, detector.getHeader());
+ assertContent();
+ }
+
+ @Test
+ public void testZipFile() throws IOException {
+ final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ final ZipOutputStream zip = new ZipOutputStream(buffer);
+ zip.putNextEntry(new ZipEntry("hello.txt"));
+ zip.write("Hello Zip!".getBytes());
+ zip.close();
+ initData(buffer.toByteArray());
+ System.out.println(Integer.toHexString(detector.getHeader()));
+ assertEquals(ContentTypeDetector.ZIPFILE, detector.getHeader());
+ assertContent();
+ }
+
+ @Test
+ public void testStreamWithoutMarkSupport() throws IOException {
+ initData(0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 07);
+ detector = new ContentTypeDetector(new ByteArrayInputStream(data) {
+
+ @Override
+ public void mark(int readlimit) {
+ }
+
+ @Override
+ public void reset() {
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ });
+ assertContent();
+ }
+
+ private void initData(byte[] bytes) throws IOException {
+ this.data = bytes;
+ this.detector = new ContentTypeDetector(new ByteArrayInputStream(data));
+ }
+
+ private void initData(final int... bytes) throws IOException {
+ byte[] data = new byte[bytes.length];
+ for (int i = 0; i < bytes.length; i++) {
+ data[i] = (byte) bytes[i];
+ }
+ initData(data);
+ }
+
+ private void assertContent() throws IOException {
+ final InputStream actual = detector.getInputStream();
+ for (int b : data) {
+ assertEquals(b, (byte) actual.read());
+ }
+ assertEquals(-1, actual.read());
+ }
+
+}
diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/validation/ValidationTestBase.java b/org.jacoco.core.test/src/org/jacoco/core/test/validation/ValidationTestBase.java
index a97bea6..27aa8a8 100644
--- a/org.jacoco.core.test/src/org/jacoco/core/test/validation/ValidationTestBase.java
+++ b/org.jacoco.core.test/src/org/jacoco/core/test/validation/ValidationTestBase.java
@@ -89,7 +89,7 @@
final ExecutionDataStore store) {
final CoverageBuilder builder = new CoverageBuilder(store);
final Analyzer analyzer = new Analyzer(builder);
- analyzer.analyze(reader);
+ analyzer.analyzeClass(reader);
final Collection<ClassCoverage> classes = builder.getClasses();
assertEquals(1, classes.size(), 0.0);
classCoverage = classes.iterator().next();
diff --git a/org.jacoco.core/src/org/jacoco/core/instr/Analyzer.java b/org.jacoco.core/src/org/jacoco/core/instr/Analyzer.java
index 17b966b..dd08ef0 100644
--- a/org.jacoco.core/src/org/jacoco/core/instr/Analyzer.java
+++ b/org.jacoco.core/src/org/jacoco/core/instr/Analyzer.java
@@ -12,8 +12,6 @@
*******************************************************************************/
package org.jacoco.core.instr;
-import static java.lang.String.format;
-
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@@ -66,7 +64,7 @@
* @param reader
* reader with class definitions
*/
- public void analyze(final ClassReader reader) {
+ public void analyzeClass(final ClassReader reader) {
final ClassVisitor visitor = createAnalyzingVisitor(CRC64
.checksum(reader.b));
reader.accept(visitor, 0);
@@ -78,8 +76,8 @@
* @param buffer
* class definitions
*/
- public void analyze(final byte[] buffer) {
- analyze(new ClassReader(buffer));
+ public void analyzeClass(final byte[] buffer) {
+ analyzeClass(new ClassReader(buffer));
}
/**
@@ -89,84 +87,82 @@
* stream to read class definition from
* @throws IOException
*/
- public void analyze(final InputStream input) throws IOException {
- analyze(new ClassReader(input));
+ public void analyzeClass(final InputStream input) throws IOException {
+ analyzeClass(new ClassReader(input));
}
/**
- * Analyzes the class definition contained in a given file.
- *
- * @param file
- * class file
- * @throws IOException
- */
- public void analyze(final File file) throws IOException {
- final InputStream in = new FileInputStream(file);
- analyze(new ClassReader(in));
- in.close();
- }
-
- /**
- * Analyzes all class files contained in the given directory and its
- * children.
- *
- * @param directory
- * folder to look for class files
- * @throws IOException
- * thrown if the given file object does not represent a readable
- * directory
- */
- public void analyzeAll(final File directory) throws IOException {
- final File[] files = directory.listFiles();
- if (files == null) {
- throw new IOException(format("Can't read directory %s.", directory));
- }
- for (final File f : files) {
- if (f.isDirectory()) {
- analyzeAll(f);
- continue;
- }
- if (f.getName().endsWith(".class")) {
- analyze(f);
- }
- }
- }
-
- /**
- * Analyzes all class files contained in a JAR file.
+ * Analyzes all classes contained in the ZIP archive (jar, war, ear, etc.)
+ * given as an input stream. Contained archives are read recursively.
*
* @param input
- * stream to read the JAR file from
+ * ZIP archive data
+ * @return number of class files found
* @throws IOException
*/
- public void analyzeJAR(final InputStream input) throws IOException {
+ public int analyzeArchive(final InputStream input) throws IOException {
final ZipInputStream zip = new ZipInputStream(input);
+ int count = 0;
while (true) {
final ZipEntry entry = zip.getNextEntry();
if (entry == null) {
break;
}
- if (entry.getName().endsWith(".class")) {
- analyze(zip);
- }
+ count += analyzeAll(zip);
}
+ return count;
}
/**
- * Analyzes all class files contained in a JAR file.
+ * Analyzes all classes found in the given input stream. The input stream
+ * may either represent a single class file or a ZIP archive that is
+ * searched recursively for class files. All other content types are
+ * ignored.
*
- * @param jarfile
- * JAR file
+ * @param input
+ * input data
+ * @return number of class files found
* @throws IOException
*/
- public void analyzeJAR(final File jarfile) throws IOException {
- final InputStream in = new FileInputStream(jarfile);
- analyzeJAR(in);
- in.close();
+ public int analyzeAll(final InputStream input) throws IOException {
+ final ContentTypeDetector detector = new ContentTypeDetector(input);
+ switch (detector.getHeader()) {
+ case ContentTypeDetector.CLASSFILE:
+ analyzeClass(detector.getInputStream());
+ return 1;
+ case ContentTypeDetector.ZIPFILE:
+ return analyzeArchive(detector.getInputStream());
+ }
+ return 0;
}
/**
- * Analyzes all class from the given class path.
+ * Analyzes all class files contained in the given file or folder. Class
+ * files as well as ZIP files are considered. Folders are searched
+ * recursively.
+ *
+ * @param file
+ * file or folder to look for class files
+ * @return number of class files found
+ * @throws IOException
+ */
+ public int analyzeAll(final File file) throws IOException {
+ int count = 0;
+ if (file.isDirectory()) {
+ for (final File f : file.listFiles()) {
+ count += analyzeAll(f);
+ }
+ } else {
+ final InputStream in = new FileInputStream(file);
+ count += analyzeAll(in);
+ in.close();
+ }
+ return count;
+ }
+
+ /**
+ * Analyzes all classes from the given class path. Directories containing
+ * class files as well as archive files are considered.
*
* @param path
* path definition
@@ -174,22 +170,17 @@
* optional base directory, if <code>null</code> the current
* working directory is used as the base for relative path
* entries
+ * @return number of class files found
* @throws IOException
*/
- public void analyzePath(final String path, final File basedir)
+ public int analyzeAll(final String path, final File basedir)
throws IOException {
- final StringTokenizer tokenizer = new StringTokenizer(path,
- File.pathSeparator);
- while (tokenizer.hasMoreTokens()) {
- final File entry = new File(basedir, tokenizer.nextToken());
- if (entry.isDirectory()) {
- analyzeAll(entry);
- continue;
- }
- if (entry.isFile() && entry.getName().endsWith(".jar")) {
- analyzeJAR(entry);
- }
+ int count = 0;
+ final StringTokenizer st = new StringTokenizer(path, File.pathSeparator);
+ while (st.hasMoreTokens()) {
+ count += analyzeAll(new File(basedir, st.nextToken()));
}
+ return count;
}
}
diff --git a/org.jacoco.core/src/org/jacoco/core/instr/ContentTypeDetector.java b/org.jacoco.core/src/org/jacoco/core/instr/ContentTypeDetector.java
new file mode 100644
index 0000000..57f6e22
--- /dev/null
+++ b/org.jacoco.core/src/org/jacoco/core/instr/ContentTypeDetector.java
@@ -0,0 +1,82 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2010 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
+ *
+ * $Id: $
+ *******************************************************************************/
+package org.jacoco.core.instr;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Detector for content types of binary streams based on a magic headers.
+ *
+ * @author Marc R. Hoffmann
+ * @version $Revision: $
+ */
+class ContentTypeDetector {
+
+ /** Header of Java class files */
+ public static final int CLASSFILE = 0xcafebabe;
+
+ /** Header of ZIP files */
+ public static final int ZIPFILE = 0x504b0304;
+
+ private static final int HEADER_SIZE = 4;
+
+ private final InputStream in;
+
+ private final int header;
+
+ /**
+ * Creates a new detector based on the given input. To process the complete
+ * original input afterwards use the stream returned by
+ * {@link #getInputStream()}.
+ *
+ * @param in
+ * input to read the header from
+ * @throws IOException
+ */
+ ContentTypeDetector(final InputStream in) throws IOException {
+ if (in.markSupported()) {
+ this.in = in;
+ } else {
+ this.in = new BufferedInputStream(in, HEADER_SIZE);
+ }
+ this.in.mark(HEADER_SIZE);
+ this.header = readHeader(this.in);
+ this.in.reset();
+ }
+
+ private static int readHeader(final InputStream in) throws IOException {
+ return in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read();
+ }
+
+ /**
+ * Returns an input stream instance to read the complete content (including
+ * the header) of the underlying stream.
+ *
+ * @return input stream containing the complete content
+ */
+ public InputStream getInputStream() {
+ return in;
+ }
+
+ /**
+ * Returns the file header containing the magic number.
+ *
+ * @return file header
+ */
+ public int getHeader() {
+ return header;
+ }
+
+}
diff --git a/org.jacoco.doc/buildhook.xml b/org.jacoco.doc/buildhook.xml
index 3a05f9a..27ba72f 100644
--- a/org.jacoco.doc/buildhook.xml
+++ b/org.jacoco.doc/buildhook.xml
@@ -46,7 +46,8 @@
<structure name="JaCoCo">
<group name="org.jacoco.agent">
<classfiles>
- <path refid="bundle-org.jacoco.agent"/>
+ <!-- Process class files only, ignore jacocoagent.jar -->
+ <fileset dir="${toString:bundle-org.jacoco.agent}" includes="**/*.class"/>
</classfiles>
<sourcefiles>
<fileset dir="${workspace.dir}/org.jacoco.agent/src"/>
diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index 4403350..0531576 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -17,6 +17,14 @@
<h1>Change History</h1>
+<h2>Next Release</h2>
+
+<h3>New Features</h3>
+<ul>
+ <li>Support for different archives (jar, war, ear etc.) and nested archives
+ (Trac #78).</li>
+</ul>
+
<h2>Release 0.3.2 (2010/04/01)</h2>
<h3>New Features</h3>
diff --git a/org.jacoco.examples/src/org/jacoco/examples/CoreTutorial.java b/org.jacoco.examples/src/org/jacoco/examples/CoreTutorial.java
index b3ca24c..4b892c3 100644
--- a/org.jacoco.examples/src/org/jacoco/examples/CoreTutorial.java
+++ b/org.jacoco.examples/src/org/jacoco/examples/CoreTutorial.java
@@ -151,7 +151,7 @@
final CoverageBuilder coverageBuilder = new CoverageBuilder(
executionData);
final Analyzer analyzer = new Analyzer(coverageBuilder);
- analyzer.analyze(getTargetClass(targetName));
+ analyzer.analyzeClass(getTargetClass(targetName));
// Let's dump some metrics and line coverage information:
for (final ClassCoverage cc : coverageBuilder.getClasses()) {