/* * Copyright 2017-present Facebook, Inc. * * 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.facebook.buck.jvm.java.testutil.compiler; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.facebook.buck.jvm.java.abi.source.FrontendOnlyJavacTask; import com.facebook.buck.jvm.java.plugin.adapter.BuckJavacPlugin; import com.facebook.buck.jvm.java.plugin.adapter.BuckJavacTask; import com.google.common.base.Joiner; import com.google.common.io.ByteStreams; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.util.JavacTask; import com.sun.source.util.TaskListener; import com.sun.source.util.Trees; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import javax.annotation.processing.Processor; import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic; import javax.tools.DiagnosticListener; import javax.tools.JavaCompiler; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; import org.hamcrest.Matchers; import org.junit.rules.ExternalResource; import org.junit.rules.TemporaryFolder; /** * A {@link org.junit.Rule} for working with javac in tests. * * <p>Add it as a public field like this: * * <p> * * <pre> * @Rule * public TestCompiler testCompiler = new TestCompiler(); * </pre> */ public class TestCompiler extends ExternalResource implements AutoCloseable { private final TemporaryFolder inputFolder = new TemporaryFolder(); private final TemporaryFolder outputFolder = new TemporaryFolder(); private final Classes classes = new ClassesImpl(outputFolder); private final JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); private final DiagnosticMessageCollector<JavaFileObject> diagnosticCollector = new DiagnosticMessageCollector<>(); private final StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(diagnosticCollector, null, null); private final List<JavaFileObject> sourceFiles = new ArrayList<>(); private TestCompiler classpathCompiler; private BuckJavacTask javacTask; private boolean useFrontendOnlyJavacTask = false; private boolean allowCompilationErrors = false; private Set<String> classpath = new LinkedHashSet<>(); public void addClasspathFileContents(String fileName, String contents) throws IOException { if (javacTask != null) { throw new AssertionError("Can't add contents after creating the task"); } if (classpathCompiler == null) { classpathCompiler = new TestCompiler(); try { classpathCompiler.before(); } catch (Throwable throwable) { throw new AssertionError(throwable); } } classpathCompiler.addSourceFileContents(fileName, contents); classpath.add(classpathCompiler.getOutputDir()); } public void addClasspath(Collection<Path> paths) { paths.stream().map(Path::toString).forEach(classpath::add); } public void addSourceFileContents(String fileName, String contents) throws IOException { addSourceFileLines(fileName, contents); } public void addSourceFileLines(String fileName, String... lines) throws IOException { if (javacTask != null) { throw new AssertionError("Can't add contents after creating the task"); } Path sourceFilePath = inputFolder.getRoot().toPath().resolve(fileName); sourceFilePath.toFile().getParentFile().mkdirs(); Files.write(sourceFilePath, Arrays.asList(lines), StandardCharsets.UTF_8); fileManager.getJavaFileObjects(sourceFilePath.toFile()).forEach(sourceFiles::add); } public void addSourceFile(Path file) throws IOException { Path outputFile = outputFolder.getRoot().toPath().resolve(file.getFileName()); ByteStreams.copy(Files.newInputStream(file), Files.newOutputStream(outputFile)); fileManager.getJavaFileObjects(outputFile.toFile()).forEach(sourceFiles::add); } public void useFrontendOnlyJavacTask() { if (javacTask != null) { throw new AssertionError("Can't change the task type after creating it"); } this.useFrontendOnlyJavacTask = true; } public JavaFileManager getFileManager() { return fileManager; } public void setTaskListener(TaskListener taskListener) { getJavacTask().setTaskListener(taskListener); } public void addPostEnterCallback(Consumer<Set<TypeElement>> callback) { getJavacTask().addPostEnterCallback(callback); } public void addPlugin(BuckJavacPlugin plugin, String... args) { BuckJavacTask task = getJavacTask(); task.addPlugin(plugin, args); } public void setProcessors(List<Processor> processors) { getJavacTask().setProcessors(processors); } public void setAllowCompilationErrors(boolean allowCompileErrors) { this.allowCompilationErrors = allowCompileErrors; } public Iterable<? extends CompilationUnitTree> parse() throws IOException { return getJavacTask().parse(); } public Iterable<? extends TypeElement> enter() throws IOException { BuckJavacTask javacTask = getJavacTask(); try { @SuppressWarnings("unchecked") Iterable<? extends TypeElement> result = (Iterable<? extends TypeElement>) javacTask.getClass().getMethod("enter").invoke(javacTask); return result; } catch (IllegalAccessException | NoSuchMethodException e) { throw new AssertionError(e); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof IOException) { throw (IOException) cause; } throw new AssertionError(e); } } public void compile() { boolean success = getJavacTask().call(); if (!allowCompilationErrors) { if (!success) { fail( "Compilation failed! Diagnostics:\n" + getDiagnosticMessages().stream().collect(Collectors.joining("\n"))); } assertTrue("Compilation encountered errors", success); } } public List<String> getDiagnosticMessages() { return diagnosticCollector.getDiagnosticMessages(); } public Classes getClasses() { return classes; } public Elements getElements() { return getJavacTask().getElements(); } public Trees getTrees() { BuckJavacTask javacTask = getJavacTask(); return javacTask.getTrees(); } public Types getTypes() { return getJavacTask().getTypes(); } public BuckJavacTask getJavacTask() { if (javacTask == null) { compileClasspath(); List<String> options = new ArrayList<>(); options.add("-d"); options.add(outputFolder.getRoot().toString()); if (!classpath.isEmpty()) { options.add("-cp"); options.add(Joiner.on(File.pathSeparatorChar).join(classpath)); } JavacTask innerTask = (JavacTask) javaCompiler.getTask( null, fileManager, diagnosticCollector, options, null, sourceFiles); if (useFrontendOnlyJavacTask) { javacTask = new FrontendOnlyJavacTask(innerTask); } else { javacTask = new BuckJavacTask(innerTask); } } return javacTask; } private String getOutputDir() { return outputFolder.getRoot().toString(); } private void compileClasspath() { if (classpathCompiler == null) { return; } classpathCompiler.compile(); assertThat(classpathCompiler.getDiagnosticMessages(), Matchers.empty()); } public void init() { try { before(); } catch (Throwable throwable) { throw new AssertionError(throwable); } } @Override protected void before() throws Throwable { inputFolder.create(); outputFolder.create(); } @Override protected void after() { if (classpathCompiler != null) { classpathCompiler.after(); } outputFolder.delete(); inputFolder.delete(); } @Override public void close() { after(); } /** * There's an issue with using the built in {@link javax.tools.DiagnosticCollector}. Grabbing the * {@link Diagnostic}s after compilation can lead to incorrect error messages. The references to * info that make up the Diagnostic's error message string may be GC-ed by the time we request the * message. To work around this, we use our own DiagnosticCollector that grabs the string of the * diagnostic at the time its reported and collect that instead. */ private static class DiagnosticMessageCollector<S> implements DiagnosticListener<S> { private List<String> diagnostics = new ArrayList<>(); @Override public void report(Diagnostic<? extends S> diagnostic) { diagnostics.add(diagnostic.toString()); } private List<String> getDiagnosticMessages() { return diagnostics; } } }