diff --git a/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java b/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java index 9d6442bb..5e229fbf 100644 --- a/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java +++ b/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java @@ -32,7 +32,10 @@ import java.io.Writer; import java.net.URI; import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; import java.util.Map.Entry; +import java.util.Set; import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; import javax.tools.JavaFileManager; @@ -69,6 +72,14 @@ private static URI uriForFileObject(Location location, String packageName, Strin return URI.create(uri.toString()); } + @Override + public String inferBinaryName(Location location, JavaFileObject file) { + if (location == StandardLocation.CLASS_PATH && file instanceof SimpleJavaFileObject) { + return toBinaryName(file); + } + return super.inferBinaryName(location, file); + } + private static URI uriForJavaFileObject(Location location, String className, Kind kind) { return URI.create( "mem:///" + location.getName() + '/' + className.replace('.', '/') + kind.extension); @@ -133,7 +144,49 @@ ImmutableList getOutputFiles() { return ImmutableList.copyOf(inMemoryFileObjects.asMap().values()); } - private static final class InMemoryJavaFileObject extends SimpleJavaFileObject + public void addToSourcePath(JavaFileObject... files) { + for (JavaFileObject file : files) { + String className = toBinaryName(file); + URI sourceUri = uriForJavaFileObject(StandardLocation.SOURCE_PATH, className, Kind.SOURCE); + inMemoryFileObjects.put(sourceUri, file); + } + } + + public boolean deleteJavaFileObject(Location location, String className, Kind kind) { + URI key = uriForJavaFileObject(location, className, kind); + JavaFileObject fileObject = inMemoryFileObjects.asMap().remove(key); + return fileObject.delete(); + } + + //this won't work for inner classes + public static String toBinaryName(JavaFileObject file) { + String fileName = file.getName(); + return fileName.substring(0, fileName.lastIndexOf('.')).replace('/', '.'); + } + + @Override + public boolean hasLocation(Location location) { + return location == StandardLocation.CLASS_PATH || location == StandardLocation.SOURCE_PATH || location == StandardLocation.PLATFORM_CLASS_PATH; // we don't care about source and other location types - not needed for compilation + } + + @Override + public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException { + List result = new ArrayList(); + if (location == StandardLocation.CLASS_PATH) { + String packageAsPath = packageName.replace('.', '/'); + for (Entry entry : inMemoryFileObjects.asMap().entrySet()) { + if (entry.getKey().toString().contains("mem:///" + StandardLocation.CLASS_OUTPUT.getName() + '/' + packageAsPath)) { + result.add(entry.getValue()); + } + } + } + for (JavaFileObject javaFileObject : super.list(location, packageName, kinds, recurse)) { + result.add(javaFileObject); + } + return result; + } + + static final class InMemoryJavaFileObject extends SimpleJavaFileObject implements JavaFileObject { private long lastModified = 0L; private Optional data = Optional.absent(); diff --git a/src/main/java/com/google/testing/compile/IncrementalCompileTester.java b/src/main/java/com/google/testing/compile/IncrementalCompileTester.java new file mode 100644 index 00000000..6531999e --- /dev/null +++ b/src/main/java/com/google/testing/compile/IncrementalCompileTester.java @@ -0,0 +1,103 @@ +package com.google.testing.compile; + +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import javax.annotation.processing.Processor; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.ToolProvider; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static javax.tools.JavaFileObject.Kind.CLASS; +import static javax.tools.JavaFileObject.Kind.SOURCE; +import static javax.tools.StandardLocation.CLASS_OUTPUT; +import static javax.tools.StandardLocation.SOURCE_OUTPUT; + +//many ideas come from http://atamur.blogspot.de/2009/10/using-built-in-javacompiler-with-custom.html +public class IncrementalCompileTester { + + InMemoryJavaFileManager fileManager; + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + private DiagnosticCollector diagnosticCollector; + private Set fullBuildSources; + + public IncrementalCompileTester() { + reset(); + } + + public void reset() { + this.fileManager = initFileManager(); + } + + public final Compilation compile(Iterable files, Iterable processors, + ImmutableList options) { + JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnosticCollector, options, null, files); + task.setProcessors(processors); + boolean succeeded = task.call(); + Compiler internalCompiler = Compiler.compiler(compiler).withProcessors(processors); + return new Compilation(internalCompiler, files, succeeded, diagnosticCollector.getDiagnostics(), getGeneratedFiles()); + } + + private ImmutableList getGeneratedFiles() { + return fileManager.getGeneratedSources(); + } + + public final Compilation fullBuild(Iterable files, Iterable processors, + ImmutableList options) { + this.fullBuildSources = new HashSet<>(); + for (JavaFileObject file : files) { + fullBuildSources.add(file); + } + + Compilation compilation = compile(files, processors, options); + return compilation; + } + + public final Compilation incrementalBuild(Iterable files, Iterable processors, + ImmutableList options) { + return compile(files, processors, options); + } + + public InMemoryJavaFileManager.InMemoryJavaFileObject getGeneratedJavaFile(String FQNclassName) { + try { + int lastDotIndex = FQNclassName.lastIndexOf('.'); + String packageName = lastDotIndex == -1 ? "" : FQNclassName.substring(0, lastDotIndex); + String className = FQNclassName.substring(lastDotIndex + 1); + return (InMemoryJavaFileManager.InMemoryJavaFileObject) fileManager.getFileForOutput(SOURCE_OUTPUT, packageName, className + ".java", null); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + public InMemoryJavaFileManager.InMemoryJavaFileObject getClassFile(String FQNclassName) { + try { + int lastDotIndex = FQNclassName.lastIndexOf('.'); + String packageName = lastDotIndex == -1 ? "" : FQNclassName.substring(0, lastDotIndex); + String className = FQNclassName.substring(lastDotIndex + 1); + return (InMemoryJavaFileManager.InMemoryJavaFileObject) fileManager.getFileForOutput(CLASS_OUTPUT, packageName, className + ".class", null); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + private InMemoryJavaFileManager initFileManager() { + diagnosticCollector = new DiagnosticCollector<>(); + return new InMemoryJavaFileManager(compiler.getStandardFileManager(diagnosticCollector, Locale.getDefault(), UTF_8)); + } + + public boolean deleteGeneratedFiles(JavaFileObject... generatedJavaFiles) { + boolean allDeleted = true; + for (JavaFileObject generatedJavaFile : generatedJavaFiles) { + String name = generatedJavaFile.getName().substring(0, generatedJavaFile.getName().lastIndexOf('.')); + allDeleted &= fileManager.deleteJavaFileObject(SOURCE_OUTPUT, name, SOURCE); + allDeleted &= fileManager.deleteJavaFileObject(CLASS_OUTPUT, name, CLASS); + } + return allDeleted; + } +} diff --git a/src/test/java/com/google/testing/compile/IncrementalCompileTesterTest.java b/src/test/java/com/google/testing/compile/IncrementalCompileTesterTest.java new file mode 100644 index 00000000..e868c1d7 --- /dev/null +++ b/src/test/java/com/google/testing/compile/IncrementalCompileTesterTest.java @@ -0,0 +1,41 @@ +package com.google.testing.compile; + +import java.io.IOException; +import javax.tools.JavaFileObject; +import org.junit.Test; + +import static com.google.common.collect.ImmutableList.of; +import static com.google.testing.compile.Compilation.Status.SUCCESS; +import static com.google.testing.compile.JavaFileObjects.forSourceString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +public class IncrementalCompileTesterTest { + + @Test + public void processor_creates_GeneratedFiles_incrementally() throws IOException { + //GIVEN + JavaFileObject source0 = forSourceString("test.Test0", "" // + + "package test;\n" // + + "public class Test0 {\n" // + + "}"); + JavaFileObject source1 = forSourceString("test.Test1", "" // + + "package test;\n" // + + "public class Test1 extends Test0 {\n" // + + "}"); + + //WHEN + + //full build + IncrementalCompileTester compileTester = new IncrementalCompileTester(); + Compilation compilation = compileTester.fullBuild(of(source0, source1), of(), of()); + + //assert it works + assertThat(compilation.status(), is(SUCCESS)); + + //incremental rebuild of source1. Should fail if not incremental + //as source1 uses source0 + compilation = compileTester.incrementalBuild(of(source1), of(), of()); + assertThat(compilation.status(), is(SUCCESS)); + } +}