blob: 8ba3fc5995a479fabe8df6429f9f7fc2238b7c31 [file] [log] [blame]
// Copyright 2016 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.java.turbine.javac;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.java.turbine.javac.JavacTurbine.Result;
import com.google.devtools.build.lib.view.proto.Deps;
import com.google.devtools.build.lib.view.proto.Deps.Dependency;
import com.google.turbine.options.TurbineOptions;
import com.sun.source.tree.LiteralTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskEvent.Kind;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.api.ClientCodeWrapper.Trusted;
import com.sun.tools.javac.api.JavacTool;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.util.Context;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.FileObject;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardLocation;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.util.Textifier;
import org.objectweb.asm.util.TraceClassVisitor;
/** Unit tests for {@link JavacTurbine}. */
@RunWith(JUnit4.class)
public class JavacTurbineTest extends AbstractJavacTurbineCompilationTest {
private static final ImmutableList<String> HOST_CLASSPATH =
ImmutableList.copyOf(
Splitter.on(File.pathSeparatorChar).split(System.getProperty("java.class.path")));
@Test
public void hello() throws Exception {
addSourceLines(
"Hello.java",
"class Hello {",
" public static void main(String[] args) {",
" System.err.println(\"Hello World\");",
" }",
"}");
compile();
Map<String, byte[]> outputs = collectOutputs();
assertThat(filterManifestEntries(outputs.keySet())).containsExactly("Hello.class");
String text = textify(outputs.get("Hello.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x20",
"class Hello {",
"",
"",
" // access flags 0x0",
" <init>()V",
"",
" // access flags 0x9",
" public static main([Ljava/lang/String;)V",
" // parameter args",
"}",
""
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
// verify that FLOW is disabled, as if we had passed -relax
// if it isn't we'd get an error about the missing return in f().
@Test
public void relax() throws Exception {
addSourceLines("Hello.java", "class Hello {", " int f() {}", "}");
compile();
Map<String, byte[]> outputs = collectOutputs();
assertThat(filterManifestEntries(outputs.keySet())).containsExactly("Hello.class");
String text = textify(outputs.get("Hello.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x20",
"class Hello {",
"",
"",
" // access flags 0x0",
" <init>()V",
"",
" // access flags 0x0",
" f()I",
"}",
""
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
public @interface MyAnnotation {}
/**
* A sample annotation processor for testing.
*
* <p>Writes two output files (one source, one data) the very first round it's called. Used to
* verify that annotation processor output is collected into the output jar.
*/
@SupportedAnnotationTypes("MyAnnotation")
public static class MyProcessor extends AbstractProcessor {
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
boolean first = true;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!first) {
// Write the output files exactly once to ensure we don't try to write the same file
// twice or do work on the final round.
return false;
}
if (roundEnv.getRootElements().isEmpty()) {
return false;
}
first = false;
Element element = roundEnv.getRootElements().iterator().next();
try {
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile("Generated", element);
try (OutputStream os = sourceFile.openOutputStream()) {
os.write("public class Generated {}".getBytes(UTF_8));
}
} catch (IOException e) {
throw new IOError(e);
}
try {
FileObject file =
processingEnv
.getFiler()
.createResource(StandardLocation.CLASS_OUTPUT, "com.foo", "hello.txt", element);
try (OutputStream os = file.openOutputStream()) {
os.write("hello".getBytes(UTF_8));
}
} catch (IOException e) {
throw new IOError(e);
}
return false;
}
}
@Test
public void processing() throws Exception {
addSourceLines("MyAnnotation.java", "public @interface MyAnnotation {}");
addSourceLines(
"Hello.java",
"@MyAnnotation",
"class Hello {",
" public static void main(String[] args) {",
" System.err.println(\"Hello World\");",
" }",
"}");
optionsBuilder.addProcessors(ImmutableList.of(MyProcessor.class.getName()));
optionsBuilder.addProcessorPathEntries(HOST_CLASSPATH);
optionsBuilder.addClassPathEntries(HOST_CLASSPATH);
compile();
Map<String, byte[]> outputs = collectOutputs();
assertThat(filterManifestEntries(outputs.keySet()))
.containsExactly(
"Generated.class", "MyAnnotation.class", "Hello.class", "com/foo/hello.txt");
{
String text = textify(outputs.get("Generated.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x21",
"public class Generated {",
"",
"",
" // access flags 0x1",
" public <init>()V",
"}",
""
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
// sanity-check that annotation processing doesn't interfere with stripping
{
String text = textify(outputs.get("Hello.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x20",
"class Hello {",
"",
"",
" @LMyAnnotation;() // invisible",
"",
" // access flags 0x0",
" <init>()V",
"",
" // access flags 0x9",
" public static main([Ljava/lang/String;)V",
" // parameter args",
"}",
""
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
}
/**
* A sample annotation processor for testing.
*
* <p>Writes an output file containing a SJD violation.
*/
@SupportedAnnotationTypes("MyAnnotation")
public static class SjdProcessor extends AbstractProcessor {
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
boolean first = true;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!first) {
return false;
}
if (roundEnv.getRootElements().isEmpty()) {
return false;
}
first = false;
Element element = roundEnv.getRootElements().iterator().next();
try {
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile("Generated", element);
try (OutputStream os = sourceFile.openOutputStream()) {
os.write(
("public class Generated {\n"
+ " public static final int CONST = A.CONST;"
+ " public static B b;"
+ "}")
.getBytes(UTF_8));
}
} catch (IOException e) {
throw new IOError(e);
}
return false;
}
}
@Test
public void processingStrict() throws Exception {
Path libD = temp.newFile("libd.jar").toPath();
compileLib(
libD,
Collections.<Path>emptyList(),
ImmutableList.of(
new StringJavaFileObject("D.java", "public class D { static final int CONST = 42; }")));
Path libC = temp.newFile("libc.jar").toPath();
compileLib(
libC,
Collections.singleton(libD),
ImmutableList.of(new StringJavaFileObject("C.java", "class C extends D {}")));
Path libB = temp.newFile("libb.jar").toPath();
compileLib(
libB,
ImmutableList.of(libC, libD),
ImmutableList.of(new StringJavaFileObject("B.java", "class B extends C {}")));
Path libA = temp.newFile("liba.jar").toPath();
compileLib(
libA,
ImmutableList.of(libB, libC, libD),
ImmutableList.of(new StringJavaFileObject("A.java", "class A extends B {}")));
Path depsA =
writedeps(
"liba.jdeps",
Deps.Dependencies.newBuilder()
.setSuccess(true)
.setRuleLabel("//lib:a")
.addDependency(
Deps.Dependency.newBuilder()
.setPath(libB.toString())
.setKind(Deps.Dependency.Kind.EXPLICIT))
.build());
addSourceLines(
"MyAnnotation.java", //
"public @interface MyAnnotation {}");
addSourceLines(
"Hello.java", //
"@MyAnnotation",
"class Hello {}");
optionsBuilder.addSources(sources.stream().map(p -> p.toString()).collect(toImmutableList()));
optionsBuilder.addProcessors(ImmutableList.of(SjdProcessor.class.getName()));
optionsBuilder.addProcessorPathEntries(HOST_CLASSPATH);
optionsBuilder.addClassPathEntries(
ImmutableList.of(libA.toString(), libB.toString(), libC.toString(), libD.toString()));
optionsBuilder.addAllDepsArtifacts(ImmutableList.of(depsA.toString()));
optionsBuilder.addDirectJars(ImmutableList.of(libA.toString()));
optionsBuilder.setTargetLabel("//my:target");
StringWriter errOutput = new StringWriter();
Result result;
try (JavacTurbine turbine =
new JavacTurbine(new PrintWriter(errOutput, true), optionsBuilder.build())) {
result = turbine.compile();
}
assertThat(errOutput.toString()).isEmpty();
assertThat(result).isEqualTo(Result.OK_WITH_FULL_CLASSPATH);
}
static Map<String, byte[]> collectFiles(Path jar) throws IOException {
Map<String, byte[]> files = new LinkedHashMap<>();
try (JarFile jf = new JarFile(jar.toFile())) {
Enumeration<JarEntry> entries = jf.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
files.put(entry.getName(), ByteStreams.toByteArray(jf.getInputStream(entry)));
}
}
return files;
}
static String textify(byte[] bytes) {
StringWriter sw = new StringWriter();
ClassReader cr = new ClassReader(bytes);
cr.accept(new TraceClassVisitor(null, new Textifier(), new PrintWriter(sw, true)), 0);
return sw.toString();
}
private static final Function<Object, String> TO_STRING =
new Function<Object, String>() {
@Override
public String apply(Object input) {
return String.valueOf(input);
}
};
@Test
public void jdeps() throws Exception {
Path libC = temp.newFile("libc.jar").toPath();
compileLib(
libC,
Collections.<Path>emptyList(),
ImmutableList.of(
new StringJavaFileObject("C.java", "interface C { String getString(); }")));
Path libA = temp.newFile("liba.jar").toPath();
compileLib(
libA,
Collections.singleton(libC),
ImmutableList.of(new StringJavaFileObject("A.java", "interface A { C getC(); }")));
Path depsA =
writedeps(
"liba.jdeps",
Deps.Dependencies.newBuilder()
.setSuccess(true)
.setRuleLabel("//lib:a")
.addDependency(
Deps.Dependency.newBuilder()
.setPath(libC.toString())
.setKind(Deps.Dependency.Kind.EXPLICIT))
.build());
Path libB = temp.newFile("libb.jar").toPath();
compileLib(
libB,
Collections.<Path>emptyList(),
ImmutableList.of(new StringJavaFileObject("B.java", "interface B {}")));
optionsBuilder.addClassPathEntries(
ImmutableList.of(libA.toString(), libB.toString(), libC.toString()));
optionsBuilder.addAllDepsArtifacts(ImmutableList.of(depsA.toString()));
optionsBuilder.addDirectJars(ImmutableList.of(libA.toString(), libB.toString()));
optionsBuilder.setTargetLabel("//my:target");
addSourceLines(
"Hello.java",
"class Hello {",
" public static A a = null;",
" public static String s = a.getC().getString();",
" public static void main(String[] args) {",
" B b = null;",
" }",
"}");
compile();
Deps.Dependencies depsProto = getDeps();
assertThat(depsProto.getSuccess()).isTrue();
assertThat(depsProto.getRuleLabel()).isEqualTo("//my:target");
assertThat(getEntries(depsProto))
.containsExactlyEntriesIn(
ImmutableMap.of(
libA.toString(), Deps.Dependency.Kind.EXPLICIT,
libB.toString(), Deps.Dependency.Kind.INCOMPLETE,
libC.toString(), Deps.Dependency.Kind.INCOMPLETE));
}
private Map<String, Deps.Dependency.Kind> getEntries(Deps.Dependencies deps) {
Map<String, Deps.Dependency.Kind> result = new LinkedHashMap<>();
for (Dependency dep : deps.getDependencyList()) {
result.put(dep.getPath(), dep.getKind());
}
return result;
}
private Deps.Dependencies getDeps() throws IOError {
Deps.Dependencies depsProto;
try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(outputDeps))) {
Deps.Dependencies.Builder builder = Deps.Dependencies.newBuilder();
builder.mergeFrom(in);
depsProto = builder.build();
} catch (IOException e) {
throw new IOError(e);
}
return depsProto;
}
private void compileLib(
Path jar, Collection<Path> classpath, Iterable<? extends JavaFileObject> units)
throws IOException {
final Path outdir = temp.newFolder().toPath();
JavacFileManager fm = new JavacFileManager(new Context(), false, UTF_8);
fm.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Collections.singleton(outdir));
fm.setLocationFromPaths(StandardLocation.CLASS_PATH, classpath);
List<String> options = ImmutableList.of("-d", outdir.toString());
JavacTool tool = JavacTool.create();
JavacTask task =
tool.getTask(
new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
fm,
null,
options,
null,
units);
assertThat(task.call()).isTrue();
try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(jar))) {
Files.walkFileTree(
outdir,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs)
throws IOException {
JarEntry je = new JarEntry(outdir.relativize(path).toString());
jos.putNextEntry(je);
Files.copy(path, jos);
return FileVisitResult.CONTINUE;
}
});
}
}
@Trusted
static class StringJavaFileObject extends SimpleJavaFileObject {
private final String content;
StringJavaFileObject(String name, String... lines) {
super(URI.create(name), JavaFileObject.Kind.SOURCE);
this.content = Joiner.on('\n').join(lines);
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return content;
}
}
@Test
public void reducedClasspath() throws Exception {
Path libD = temp.newFile("libd.jar").toPath();
compileLib(
libD,
Collections.<Path>emptyList(),
ImmutableList.of(new StringJavaFileObject("D.java", "public class D {}")));
Path libC = temp.newFile("libc.jar").toPath();
compileLib(
libC,
Collections.singleton(libD),
ImmutableList.of(new StringJavaFileObject("C.java", "class C { static D d; }")));
Path libB = temp.newFile("libb.jar").toPath();
compileLib(
libB,
ImmutableList.of(libC, libD),
ImmutableList.of(new StringJavaFileObject("B.java", "class B { static C c; }")));
Path libA = temp.newFile("liba.jar").toPath();
compileLib(
libA,
ImmutableList.of(libB, libC, libD),
ImmutableList.of(new StringJavaFileObject("A.java", "class A { static B b; }")));
Path depsA =
writedeps(
"liba.jdeps",
Deps.Dependencies.newBuilder()
.setSuccess(true)
.setRuleLabel("//lib:a")
.addDependency(
Deps.Dependency.newBuilder()
.setPath(libB.toString())
.setKind(Deps.Dependency.Kind.EXPLICIT))
.build());
optionsBuilder.addClassPathEntries(
ImmutableList.of(libA.toString(), libB.toString(), libC.toString(), libD.toString()));
optionsBuilder.addAllDepsArtifacts(ImmutableList.of(depsA.toString()));
optionsBuilder.addDirectJars(ImmutableList.of(libA.toString()));
optionsBuilder.setTargetLabel("//my:target");
addSourceLines(
"Hello.java",
"class Hello {",
" public static A a = new A();",
" public static void main(String[] args) {",
" A a = null;",
" B b = null;",
" C c = null;",
" D d = null;",
" }",
"}");
optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
try (JavacTurbine turbine =
new JavacTurbine(
new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8))),
optionsBuilder.build())) {
assertThat(turbine.compile()).isEqualTo(Result.OK_WITH_REDUCED_CLASSPATH);
Context context = turbine.context;
JavacFileManager fm = (JavacFileManager) context.get(JavaFileManager.class);
assertThat(fm.getLocationAsPaths(StandardLocation.CLASS_PATH)).containsExactly(libA, libB);
Deps.Dependencies depsProto = getDeps();
assertThat(depsProto.getSuccess()).isTrue();
assertThat(depsProto.getRuleLabel()).isEqualTo("//my:target");
assertThat(getEntries(depsProto))
.containsExactlyEntriesIn(
ImmutableMap.of(
libA.toString(),
Deps.Dependency.Kind.EXPLICIT,
libB.toString(),
Deps.Dependency.Kind.INCOMPLETE));
}
}
Path writedeps(String name, Deps.Dependencies deps) throws IOException {
Path path = temp.newFile(name).toPath();
try (OutputStream os = Files.newOutputStream(path)) {
deps.writeTo(os);
}
return path;
}
@Test
public void reducedClasspathFallback() throws Exception {
Path libD = temp.newFile("libd.jar").toPath();
compileLib(
libD,
Collections.<Path>emptyList(),
ImmutableList.of(
new StringJavaFileObject("D.java", "public class D { static final int CONST = 42; }")));
Path libC = temp.newFile("libc.jar").toPath();
compileLib(
libC,
Collections.singleton(libD),
ImmutableList.of(new StringJavaFileObject("C.java", "class C extends D {}")));
Path libB = temp.newFile("libb.jar").toPath();
compileLib(
libB,
ImmutableList.of(libC, libD),
ImmutableList.of(new StringJavaFileObject("B.java", "class B extends C {}")));
Path libA = temp.newFile("liba.jar").toPath();
compileLib(
libA,
ImmutableList.of(libB, libC, libD),
ImmutableList.of(new StringJavaFileObject("A.java", "class A extends B {}")));
Path depsA =
writedeps(
"liba.jdeps",
Deps.Dependencies.newBuilder()
.setSuccess(true)
.setRuleLabel("//lib:a")
.addDependency(
Deps.Dependency.newBuilder()
.setPath(libB.toString())
.setKind(Deps.Dependency.Kind.EXPLICIT))
.build());
optionsBuilder.addClassPathEntries(
ImmutableList.of(libA.toString(), libB.toString(), libC.toString(), libD.toString()));
optionsBuilder.addAllDepsArtifacts(ImmutableList.of(depsA.toString()));
optionsBuilder.addDirectJars(ImmutableList.of(libA.toString()));
optionsBuilder.setTargetLabel("//my:target");
addSourceLines(
"Hello.java",
"class Hello {",
" public static final int CONST = A.CONST;",
" public static void main(String[] args) {}",
"}");
optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
try (JavacTurbine turbine =
new JavacTurbine(
new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8))),
optionsBuilder.build())) {
assertThat(turbine.compile()).isEqualTo(Result.OK_WITH_FULL_CLASSPATH);
Context context = turbine.context;
JavacFileManager fm = (JavacFileManager) context.get(JavaFileManager.class);
assertThat(fm.getLocationAsPaths(StandardLocation.CLASS_PATH))
.containsExactly(libA, libB, libC, libD);
Deps.Dependencies depsProto = getDeps();
assertThat(depsProto.getSuccess()).isTrue();
assertThat(depsProto.getRuleLabel()).isEqualTo("//my:target");
assertThat(getEntries(depsProto))
.containsExactlyEntriesIn(
ImmutableMap.of(
libA.toString(), Deps.Dependency.Kind.EXPLICIT,
libB.toString(), Deps.Dependency.Kind.IMPLICIT,
libC.toString(), Deps.Dependency.Kind.IMPLICIT,
libD.toString(), Deps.Dependency.Kind.IMPLICIT));
}
}
@Test
public void constants() throws Exception {
addSourceLines(
"Const.java",
"class Const {",
" public static final int A = 42;",
" public static final int B = 42 + 42;",
" public static final int C = new Integer(42);",
" public static final int D = 42 + new Integer(42);",
" public static final Integer E = 42;",
" public static final String F = \"42\";",
" public static final java.lang.String G = \"42\";",
"}");
compile();
Map<String, byte[]> outputs = collectOutputs();
assertThat(filterManifestEntries(outputs.keySet())).containsExactly("Const.class");
String text = textify(outputs.get("Const.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x20",
"class Const {",
"",
"",
" // access flags 0x19",
" public final static I A = 42",
"",
" // access flags 0x19",
" public final static I B = 84",
"",
" // access flags 0x19",
" public final static I C",
"",
" // access flags 0x19",
" public final static I D",
"",
" // access flags 0x19",
" public final static Ljava/lang/Integer; E",
"",
" // access flags 0x19",
" public final static Ljava/lang/String; F = \"42\"",
"",
" // access flags 0x19",
" public final static Ljava/lang/String; G = \"42\"",
"",
" // access flags 0x0",
" <init>()V",
"}",
"",
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
@Test
public void constantsEnum() throws Exception {
addSourceLines(
"TheEnum.java", //
"public enum TheEnum {",
" ONE, TWO, THREE;",
"}");
compile();
Map<String, byte[]> outputs = collectOutputs();
// just don't crash; enum constants need to be preserved
assertThat(filterManifestEntries(outputs.keySet())).containsExactly("TheEnum.class");
String text = textify(outputs.get("TheEnum.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x4031",
"// signature Ljava/lang/Enum<LTheEnum;>;",
"// declaration: TheEnum extends java.lang.Enum<TheEnum>",
"public final enum TheEnum extends java/lang/Enum {",
"",
"",
" // access flags 0x4019",
" public final static enum LTheEnum; ONE",
"",
" // access flags 0x4019",
" public final static enum LTheEnum; TWO",
"",
" // access flags 0x4019",
" public final static enum LTheEnum; THREE",
"",
" // access flags 0x9",
" public static values()[LTheEnum;",
"",
" // access flags 0x9",
" public static valueOf(Ljava/lang/String;)LTheEnum;",
" // parameter mandated name",
"}",
""
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
/**
* A sample annotation processor for testing.
*
* <p>Writes an output file that isn't valid UTF-8 to test handling of encoding errors.
*/
@SupportedAnnotationTypes("MyAnnotation")
public static class MyBadEncodingProcessor extends AbstractProcessor {
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
boolean first = true;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!first) {
return false;
}
if (roundEnv.getRootElements().isEmpty()) {
return false;
}
first = false;
Element element = roundEnv.getRootElements().iterator().next();
try {
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile("Generated", element);
try (OutputStream os = sourceFile.openOutputStream()) {
os.write("class Generated { public static String x = \"".getBytes(UTF_8));
os.write(0xc2); // write an unpaired surrogate
os.write("\";}}".getBytes(UTF_8));
}
} catch (IOException e) {
throw new IOError(e);
}
return false;
}
}
@Test
public void badEncoding() throws Exception {
addSourceLines("MyAnnotation.java", "public @interface MyAnnotation {}");
addSourceLines(
"Hello.java",
"@MyAnnotation",
"class Hello {",
" public static void main(String[] args) {",
" System.err.println(\"Hello World\");",
" }",
"}");
optionsBuilder.addProcessors(ImmutableList.of(MyBadEncodingProcessor.class.getName()));
optionsBuilder.addProcessorPathEntries(HOST_CLASSPATH);
optionsBuilder.addClassPathEntries(HOST_CLASSPATH);
optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
try (StringWriter sw = new StringWriter();
JavacTurbine turbine =
new JavacTurbine(new PrintWriter(sw, true), optionsBuilder.build())) {
Result result = turbine.compile();
assertThat(result).isEqualTo(Result.ERROR);
assertThat(sw.toString()).contains("unmappable character");
}
}
@Test
public void requiredConstructor() throws Exception {
addSourceLines(
"Super.java", //
"class Super {",
" public Super(int x) {}",
"}");
addSourceLines(
"Hello.java",
"class Hello extends Super {",
" public Hello() {",
" super(42);",
" }",
"}");
compile();
Map<String, byte[]> outputs = collectOutputs();
assertThat(filterManifestEntries(outputs.keySet()))
.containsExactly("Super.class", "Hello.class");
String text = textify(outputs.get("Hello.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x20",
"class Hello extends Super {",
"",
"",
" // access flags 0x1",
" public <init>()V",
"}",
""
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
@Test
public void annotationDeclaration() throws Exception {
addSourceLines(
"Anno.java",
"import java.lang.annotation.Retention;",
"import java.lang.annotation.RetentionPolicy;",
"@Retention(RetentionPolicy.RUNTIME)",
"@interface Anno {",
" public int value() default CONST;",
" int CONST = 42;",
" int NONCONST = new Integer(42);",
"}");
addSourceLines("Hello.java", "@Anno(value=Anno.CONST)", "class Hello {", "}");
compile();
Map<String, byte[]> outputs = collectOutputs();
assertThat(filterManifestEntries(outputs.keySet()))
.containsExactly("Anno.class", "Hello.class");
String text = textify(outputs.get("Hello.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x20",
"class Hello {",
"",
"",
" @LAnno;(value=42)",
"",
" // access flags 0x0",
" <init>()V",
"}",
""
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
@Test
public void overlappingSourceJars() throws Exception {
Path sourceJar1 = temp.newFile("srcs1.jar").toPath();
try (OutputStream os = Files.newOutputStream(sourceJar1);
JarOutputStream jos = new JarOutputStream(os)) {
jos.putNextEntry(new JarEntry("Hello.java"));
jos.write("public class Hello {}".getBytes(UTF_8));
}
Path sourceJar2 = temp.newFile("srcs2.jar").toPath();
try (OutputStream os = Files.newOutputStream(sourceJar2);
JarOutputStream jos = new JarOutputStream(os)) {
jos.putNextEntry(new JarEntry("Hello.java"));
jos.write("public class Hello {}".getBytes(UTF_8));
}
optionsBuilder.setSourceJars(ImmutableList.of(sourceJar2.toString(), sourceJar1.toString()));
StringWriter errOutput = new StringWriter();
Result result;
try (JavacTurbine turbine =
new JavacTurbine(new PrintWriter(errOutput, true), optionsBuilder.build())) {
result = turbine.compile();
}
assertThat(result).isEqualTo(Result.ERROR);
assertThat(errOutput.toString()).contains("duplicate class: Hello");
}
@Test
public void privateMembers() throws Exception {
addSourceLines("Hello.java", "class Hello {", " private void f() {}", " private int x;", "}");
compile();
Map<String, byte[]> outputs = collectOutputs();
assertThat(filterManifestEntries(outputs.keySet())).containsExactly("Hello.class");
String text = textify(outputs.get("Hello.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x20",
"class Hello {",
"",
"",
" // access flags 0x0",
" <init>()V",
"}",
""
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
@Test
public void invalidJavacopts() throws Exception {
addSourceLines("Hello.java", "class Hello {}");
optionsBuilder.addAllJavacOpts(ImmutableList.of("-NOT_AN_OPTION"));
optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
StringWriter errOutput = new StringWriter();
try (JavacTurbine turbine =
new JavacTurbine(new PrintWriter(errOutput, true), optionsBuilder.build())) {
assertThat(turbine.compile()).isEqualTo(Result.ERROR);
}
assertThat(errOutput.toString()).contains("invalid flag: -NOT_AN_OPTION");
}
/** An annotation processor that reads a file that doesn't exist. */
@SupportedAnnotationTypes("*")
public static class NoSuchFileProcessor extends AbstractProcessor {
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
processingEnv
.getFiler()
.getResource(StandardLocation.CLASS_OUTPUT, "", "NO_SUCH_FILE")
.openInputStream();
} catch (IOException e) {
throw new IOError(e);
}
return false;
}
}
@Test
public void processorReadsNonexistantFile() throws Exception {
addSourceLines("Hello.java", "@Deprecated class Hello {}");
optionsBuilder.addProcessors(ImmutableList.of(NoSuchFileProcessor.class.getName()));
optionsBuilder.addProcessorPathEntries(HOST_CLASSPATH);
optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
StringWriter errOutput = new StringWriter();
try (JavacTurbine turbine =
new JavacTurbine(new PrintWriter(errOutput, true), optionsBuilder.build())) {
assertThat(turbine.compile()).isEqualTo(Result.ERROR);
}
assertThat(errOutput.toString()).contains("classes/NO_SUCH_FILE");
}
@Test
public void emptySources() throws Exception {
// don't set up any source files
compile();
Map<String, byte[]> outputs = collectOutputs();
assertThat(filterManifestEntries(outputs.keySet())).isEmpty();
}
/** An annotation processor that violates the contract. */
@SupportedAnnotationTypes("*")
public static class MisguidedAnnotationProcessor extends AbstractProcessor {
public final class Scanner extends TreeScanner<Void, Void> {
@Override
public Void visitLiteral(LiteralTree tree, Void unused) {
values.add(tree.getValue());
return null;
}
}
public final class Listener implements TaskListener {
public final ProcessingEnvironment processingEnv;
Listener(ProcessingEnvironment processingEnv) {
this.processingEnv = processingEnv;
}
@Override
public void started(TaskEvent e) {}
@Override
public void finished(TaskEvent e) {
if (e.getKind() == Kind.ANALYZE) {
e.getCompilationUnit().accept(new Scanner(), null);
} else if (e.getKind() == Kind.GENERATE) {
try {
FileObject file =
processingEnv
.getFiler()
.createResource(
StandardLocation.CLASS_OUTPUT, "", "output.txt", e.getTypeElement());
try (OutputStream os = file.openOutputStream()) {
os.write(values.toString().getBytes(UTF_8));
}
} catch (IOException exception) {
throw new IOError(exception);
}
}
}
}
public final Set<Object> values = new LinkedHashSet<>();
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
@Override
public synchronized void init(final ProcessingEnvironment processingEnv) {
JavacTask.instance(processingEnv).addTaskListener(new Listener(processingEnv));
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
void setupMisguidedProcessor() throws Exception {
addSourceLines(
"Hello.java",
"@Deprecated class Hello {",
" int x = 42;",
" String s = \"hello\";",
" double y = 42.1;",
"}");
Path processorJar =
createClassJar(
"libprocessor.jar",
MisguidedAnnotationProcessor.class,
MisguidedAnnotationProcessor.Listener.class,
MisguidedAnnotationProcessor.Scanner.class);
optionsBuilder.addProcessors(ImmutableList.of(MisguidedAnnotationProcessor.class.getName()));
optionsBuilder.addProcessorPathEntries(ImmutableList.of(processorJar.toString()));
}
public static class TransitiveDep {}
public static class DirectDep extends TransitiveDep {}
@Test
public void noNativeHeaderOutput() throws Exception {
// deliberately exclude TransitiveDep
Path deps =
createClassJar(
"libdeps.jar",
AbstractJavacTurbineCompilationTest.class,
JavacTurbineTest.class,
DirectDep.class);
// compilation will complete supertypes of DirectDep iff NATIVE_HEADER_OUTPUT is set
addSourceLines(
"Hello.java",
"import " + DirectDep.class.getCanonicalName() + ";",
"class Hello {",
" public native DirectDep foo() /*-{",
" }-*/;",
"}");
optionsBuilder.addClassPathEntries(Collections.singleton(deps.toString()));
optionsBuilder.addDirectJars(ImmutableList.of(deps.toString()));
compile();
Map<String, byte[]> outputs = collectOutputs();
assertThat(filterManifestEntries(outputs.keySet())).containsExactly("Hello.class");
}
public static class Lib {}
@Test
public void ignoreStrictDepsErrors() throws Exception {
Path lib =
createClassJar(
"deps.jar",
AbstractJavacTurbineCompilationTest.class,
JavacTurbineTest.class,
Lib.class);
addSourceLines(
"Hello.java", "import " + Lib.class.getCanonicalName() + ";", "class Hello extends Lib {}");
optionsBuilder.addClassPathEntries(ImmutableList.of(lib.toString()));
optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
StringWriter errOutput = new StringWriter();
Result result;
try (JavacTurbine turbine =
new JavacTurbine(new PrintWriter(errOutput, true), optionsBuilder.build())) {
result = turbine.compile();
}
assertThat(errOutput.toString()).isEmpty();
assertThat(result).isNotEqualTo(Result.OK_WITH_REDUCED_CLASSPATH);
}
@Test
public void ignoreStrictDepsErrorsForFailingCompilations() throws Exception {
Path lib =
createClassJar(
"deps.jar",
AbstractJavacTurbineCompilationTest.class,
JavacTurbineTest.class,
Lib.class);
addSourceLines(
"Hello.java",
"import " + Lib.class.getCanonicalName() + ";",
"import no.such.Class;",
"class Hello extends Lib {",
"}");
optionsBuilder.addClassPathEntries(ImmutableList.of(lib.toString()));
optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
StringWriter errOutput = new StringWriter();
Result result;
try (JavacTurbine turbine =
new JavacTurbine(new PrintWriter(errOutput, true), optionsBuilder.build())) {
result = turbine.compile();
}
assertThat(errOutput.toString()).doesNotContain("[strict]");
assertThat(errOutput.toString()).doesNotContain("** Please add the following dependencies:");
assertThat(result).isEqualTo(Result.ERROR);
}
@Test
public void clinit() throws Exception {
addSourceLines(
"Hello.java",
"class Hello {",
" public static int x;",
" static {",
" x = 42;",
" }",
"}");
compile();
Map<String, byte[]> outputs = collectOutputs();
assertThat(filterManifestEntries(outputs.keySet())).containsExactly("Hello.class");
String text = textify(outputs.get("Hello.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x20",
"class Hello {",
"",
"",
" // access flags 0x9",
" public static I x",
"",
" // access flags 0x0",
" <init>()V",
"}",
""
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
@Test
public void bridge() throws Exception {
addSourceLines(
"Bridge.java",
"import java.util.concurrent.Callable;",
"class Bridge implements Callable<String> {",
" public String call() { return \"\"; }",
"}");
compile();
Map<String, byte[]> outputs = collectOutputs();
assertThat(filterManifestEntries(outputs.keySet())).containsExactly("Bridge.class");
String text = textify(outputs.get("Bridge.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x20",
"// signature Ljava/lang/Object;Ljava/util/concurrent/Callable<Ljava/lang/String;>;",
"// declaration: Bridge implements java.util.concurrent.Callable<java.lang.String>",
"class Bridge implements java/util/concurrent/Callable {",
"",
"",
" // access flags 0x0",
" <init>()V",
"",
" // access flags 0x1",
" public call()Ljava/lang/String;",
"}",
""
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
@Test
public void enumDecl() throws Exception {
addSourceLines(
"P.java",
"import java.util.function.Predicate;",
"enum P implements Predicate<String> {",
" INSTANCE {",
" @Override",
" public boolean test(String s) {",
" return NoSuch.method();",
" }",
" }",
"}");
compile();
Map<String, byte[]> outputs = collectOutputs();
String text = textify(outputs.get("P.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x4420",
"// signature Ljava/lang/Enum<LP;>;Ljava/util/function/Predicate<Ljava/lang/String;>;",
"// declaration: P extends java.lang.Enum<P>"
+ " implements java.util.function.Predicate<java.lang.String>",
"abstract enum P extends java/lang/Enum implements java/util/function/Predicate {",
"",
" // access flags 0x4010",
" final enum INNERCLASS P$1 null null",
"",
" // access flags 0x4019",
" public final static enum LP; INSTANCE",
"",
" // access flags 0x9",
" public static values()[LP;",
"",
" // access flags 0x9",
" public static valueOf(Ljava/lang/String;)LP;",
" // parameter mandated name",
"}",
""
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
@Test
public void lambdaBody() throws Exception {
addSourceLines(
"P.java",
"import java.util.function.Predicate;",
"enum P {",
" INSTANCE(x -> {",
" return false;",
" });",
" P(Predicate<String> p) {}",
"}");
compile();
Map<String, byte[]> outputs = collectOutputs();
String text = textify(outputs.get("P.class"));
String[] expected = {
"// class version 52.0 (52)",
"// access flags 0x4030",
"// signature Ljava/lang/Enum<LP;>;",
"// declaration: P extends java.lang.Enum<P>",
"final enum P extends java/lang/Enum {",
"",
" // access flags 0x19",
" public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup"
+ " java/lang/invoke/MethodHandles Lookup",
"",
" // access flags 0x4019",
" public final static enum LP; INSTANCE",
"",
" // access flags 0x9",
" public static values()[LP;",
"",
" // access flags 0x9",
" public static valueOf(Ljava/lang/String;)LP;",
" // parameter mandated name",
"}",
""
};
assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
}
@SupportedAnnotationTypes("*")
public static class SimpleProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
@Test
public void noWarningDiagnostics() throws Exception {
addSourceLines(
"A.java", //
"@Deprecated public class A {",
"}");
addSourceLines(
"B.java", //
"public class B {",
" public static final A a;",
"}");
optionsBuilder.addProcessors(ImmutableList.of(SimpleProcessor.class.getName()));
optionsBuilder.addProcessorPathEntries(HOST_CLASSPATH);
optionsBuilder.addAllJavacOpts(ImmutableList.of("-Xlint:deprecation"));
optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
StringWriter output = new StringWriter();
Result result;
try (JavacTurbine turbine =
new JavacTurbine(new PrintWriter(output, true), optionsBuilder.build())) {
result = turbine.compile();
}
assertThat(output.toString()).isEmpty();
assertThat(result).isEqualTo(Result.OK_WITH_REDUCED_CLASSPATH);
}
@Test
public void processJavacopts_useSourceByDefault() {
TurbineOptions options = TurbineOptions.builder().setOutput("/out").setTempDir("/tmp").build();
ImmutableList<String> javacopts = JavacTurbine.processJavacopts(options);
assertThat(javacopts).contains("-source");
assertThat(javacopts).doesNotContain("--release");
}
@Test
public void processJavacopts_releaseDefault() {
TurbineOptions options =
TurbineOptions.builder()
.setOutput("/out")
.setTempDir("/tmp")
.addAllJavacOpts(ImmutableList.of("--release", "9"))
.build();
ImmutableList<String> javacopts = JavacTurbine.processJavacopts(options);
assertThat(javacopts).doesNotContain("-source");
}
@Test
public void processJavacopts_normalizeRelease() {
TurbineOptions options =
TurbineOptions.builder()
.setOutput("/out")
.setTempDir("/tmp")
.addAllJavacOpts(ImmutableList.of("-source", "8", "-target", "8", "--release", "9"))
.build();
ImmutableList<String> javacopts = JavacTurbine.processJavacopts(options);
assertThat(javacopts).contains("--release");
assertThat(javacopts).containsNoneOf("-source", "-target");
}
@Test
public void testManifestEntries() throws Exception {
optionsBuilder.setTargetLabel("//foo:foo");
optionsBuilder.setInjectingRuleKind("foo_library");
compile();
try (JarFile jarFile = new JarFile(output.toFile())) {
Manifest manifest = jarFile.getManifest();
Attributes attributes = manifest.getMainAttributes();
assertThat(attributes.getValue("Target-Label")).isEqualTo("//foo:foo");
assertThat(attributes.getValue("Injecting-Rule-Kind")).isEqualTo("foo_library");
assertThat(jarFile.getEntry(JarFile.MANIFEST_NAME).getLastModifiedTime().toInstant())
.isEqualTo(
LocalDateTime.of(2010, 1, 1, 0, 0, 0).atZone(ZoneId.systemDefault()).toInstant());
}
}
private static Set<String> filterManifestEntries(Set<String> entries) {
return entries
.stream()
.filter(
name ->
!(name.equals(JavacTurbine.MANIFEST_DIR)
|| name.equals(JavacTurbine.MANIFEST_NAME)))
.collect(toSet());
}
@Test
public void diagnosticFormattingTest() throws Exception {
addSourceLines(
"A.java", //
"class A {",
"}}");
optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
StringWriter output = new StringWriter();
Result result;
try (JavacTurbine turbine =
new JavacTurbine(new PrintWriter(output, true), optionsBuilder.build())) {
result = turbine.compile();
}
assertThat(result).isEqualTo(Result.ERROR);
assertThat(output.toString()).contains("A.java:2: error: class, interface, or enum expected");
}
}