blob: ffc347628992061c35e20cb47d6ce9d6c19ab393 [file] [log] [blame]
package org.checkerframework.framework.test;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
import org.checkerframework.checker.signature.qual.BinaryName;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.SystemUtil;
import org.plumelib.util.StringsPlume;
/**
* Used to create an instance of TestConfiguration. TestConfigurationBuilder is fluent: it returns
* itself after every call so you can string together configuration methods as follows:
*
* <p>{@code new TestConfigurationBuilder() .addOption("-Awarns") .addSourceFile("src1.java")
* .addDiagnosticFile("src1.out") }
*
* @see TestConfiguration
*/
public class TestConfigurationBuilder {
// Presented first are static helper methods that reduce configuration building to a method call
// However, if you need more complex configuration or custom configuration, use the
// constructors provided below
/**
* This creates a builder for the default configuration used by Checker Framework JUnit tests.
*
* @param testSourcePath the path to the Checker test file sources, usually this is the directory
* of Checker's tests
* @param outputClassDirectory the directory to place classes compiled for testing
* @param classPath the classpath to use for compilation
* @param testSourceFiles the Java files that compose the test
* @param processors the checkers or other annotation processors to run over the testSourceFiles
* @param options the options to the compiler/processors
* @param shouldEmitDebugInfo whether or not debug information should be emitted
* @return the builder that will create an immutable test configuration
*/
public static TestConfigurationBuilder getDefaultConfigurationBuilder(
String testSourcePath,
File outputClassDirectory,
String classPath,
Iterable<File> testSourceFiles,
Iterable<@BinaryName String> processors,
List<String> options,
boolean shouldEmitDebugInfo) {
TestConfigurationBuilder configBuilder =
new TestConfigurationBuilder()
.setShouldEmitDebugInfo(shouldEmitDebugInfo)
.addProcessors(processors)
.addOption("-Xmaxerrs", "9999")
.addOption("-g")
.addOption("-Xlint:unchecked")
.addOption("-XDrawDiagnostics") // use short javac diagnostics
.addSourceFiles(testSourceFiles);
if (outputClassDirectory != null) {
configBuilder.addOption("-d", outputClassDirectory.getAbsolutePath());
}
if (SystemUtil.getJreVersion() == 8) {
configBuilder.addOption("-source", "8").addOption("-target", "8");
}
configBuilder
.addOptionIfValueNonEmpty("-sourcepath", testSourcePath)
.addOption("-implicit:class")
.addOption("-classpath", classPath);
configBuilder.addOptions(options);
return configBuilder;
}
/**
* This is the default configuration used by Checker Framework JUnit tests.
*
* @param testSourcePath the path to the Checker test file sources, usually this is the directory
* of Checker's tests
* @param testFile a single test java file to compile
* @param processor a single checker to include in the processors field
* @param options the options to the compiler/processors
* @param shouldEmitDebugInfo whether or not debug information should be emitted
* @return a TestConfiguration with input parameters added plus the normal default options,
* compiler, and file manager used by Checker Framework tests
*/
@SuppressWarnings(
"signature:argument" // for non-array non-primitive class, getName(): @BinaryName
)
public static TestConfiguration buildDefaultConfiguration(
String testSourcePath,
File testFile,
Class<?> processor,
List<String> options,
boolean shouldEmitDebugInfo) {
return buildDefaultConfiguration(
testSourcePath,
Arrays.asList(testFile),
Collections.emptyList(),
Arrays.asList(processor.getName()),
options,
shouldEmitDebugInfo);
}
/**
* This is the default configuration used by Checker Framework JUnit tests.
*
* @param testSourcePath the path to the Checker test file sources, usually this is the directory
* of Checker's tests
* @param testSourceFiles the Java files that compose the test
* @param processors the checkers or other annotation processors to run over the testSourceFiles
* @param options the options to the compiler/processors
* @param shouldEmitDebugInfo whether or not debug information should be emitted
* @return a TestConfiguration with input parameters added plus the normal default options,
* compiler, and file manager used by Checker Framework tests
*/
public static TestConfiguration buildDefaultConfiguration(
String testSourcePath,
Iterable<File> testSourceFiles,
Iterable<@BinaryName String> processors,
List<String> options,
boolean shouldEmitDebugInfo) {
return buildDefaultConfiguration(
testSourcePath,
testSourceFiles,
Collections.emptyList(),
processors,
options,
shouldEmitDebugInfo);
}
/**
* This is the default configuration used by Checker Framework JUnit tests.
*
* @param testSourcePath the path to the Checker test file sources, usually this is the directory
* of Checker's tests
* @param testSourceFiles the Java files that compose the test
* @param classpathExtra extra entries for the classpath, needed to compile the source files
* @param processors the checkers or other annotation processors to run over the testSourceFiles
* @param options the options to the compiler/processors
* @param shouldEmitDebugInfo whether or not debug information should be emitted
* @return a TestConfiguration with input parameters added plus the normal default options,
* compiler, and file manager used by Checker Framework tests
*/
public static TestConfiguration buildDefaultConfiguration(
String testSourcePath,
Iterable<File> testSourceFiles,
Collection<String> classpathExtra,
Iterable<@BinaryName String> processors,
List<String> options,
boolean shouldEmitDebugInfo) {
String classPath = getDefaultClassPath();
if (!classpathExtra.isEmpty()) {
classPath +=
System.getProperty("path.separator")
+ String.join(System.getProperty("path.separator"), classpathExtra);
}
File outputDir = getOutputDirFromProperty();
TestConfigurationBuilder builder =
getDefaultConfigurationBuilder(
testSourcePath,
outputDir,
classPath,
testSourceFiles,
processors,
options,
shouldEmitDebugInfo);
return builder.validateThenBuild(true);
}
/** The list of files that contain Java diagnostics to compare against. */
private List<File> diagnosticFiles;
/** The set of Java files to test against. */
private List<File> testSourceFiles;
/** The set of Checker Framework processors to test with. */
private Set<@BinaryName String> processors;
/** The set of options to the Javac command line used to run the test. */
private SimpleOptionMap options;
/** Should the Javac options be output before running the test. */
private boolean shouldEmitDebugInfo;
/**
* Note: There are static helper methods named buildConfiguration and buildConfigurationBuilder
* that can be used to create the most common types of configurations
*/
public TestConfigurationBuilder() {
diagnosticFiles = new ArrayList<>();
testSourceFiles = new ArrayList<>();
processors = new LinkedHashSet<>();
options = new SimpleOptionMap();
shouldEmitDebugInfo = false;
}
/**
* Create a builder that has all of the options in initialConfig.
*
* @param initialConfig initial configuration for the newly-created builder
*/
public TestConfigurationBuilder(TestConfiguration initialConfig) {
this.diagnosticFiles = new ArrayList<>(initialConfig.getDiagnosticFiles());
this.testSourceFiles = new ArrayList<>(initialConfig.getTestSourceFiles());
this.processors = new LinkedHashSet<>(initialConfig.getProcessors());
this.options = new SimpleOptionMap();
this.addOptions(initialConfig.getOptions());
this.shouldEmitDebugInfo = initialConfig.shouldEmitDebugInfo();
}
/**
* Ensures that the minimum requirements for running a test are met. These requirements are:
*
* <ul>
* <li>There is at least one source file
* <li>There is at least one processor (if requireProcessors has been set to true)
* <li>There is an output directory specified for class files
* <li>There is no {@code -processor} option in the optionMap (it should be added by
* addProcessor instead)
* </ul>
*
* @param requireProcessors whether or not to require that there is at least one processor
* @return a list of errors found while validating this configuration
*/
public List<String> validate(boolean requireProcessors) {
List<String> errors = new ArrayList<>();
if (testSourceFiles == null || !testSourceFiles.iterator().hasNext()) {
errors.add("No source files specified!");
}
if (requireProcessors && !processors.iterator().hasNext()) {
errors.add("No processors were specified!");
}
final Map<String, @Nullable String> optionMap = options.getOptions();
if (!optionMap.containsKey("-d") || optionMap.get("-d") == null) {
errors.add("No output directory was specified.");
}
if (optionMap.containsKey("-processor")) {
errors.add("Processors should not be added to the options list");
}
return errors;
}
public TestConfigurationBuilder adddToPathOption(String key, String toAppend) {
options.addToPathOption(key, toAppend);
return this;
}
public TestConfigurationBuilder addDiagnosticFile(File diagnostics) {
this.diagnosticFiles.add(diagnostics);
return this;
}
public TestConfigurationBuilder addDiagnosticFiles(Iterable<File> diagnostics) {
this.diagnosticFiles = catListAndIterable(diagnosticFiles, diagnostics);
return this;
}
public TestConfigurationBuilder setDiagnosticFiles(List<File> diagnosticFiles) {
this.diagnosticFiles = new ArrayList<>(diagnosticFiles);
return this;
}
public TestConfigurationBuilder addSourceFile(File sourceFile) {
testSourceFiles.add(sourceFile);
return this;
}
public TestConfigurationBuilder addSourceFiles(Iterable<File> sourceFiles) {
testSourceFiles = catListAndIterable(testSourceFiles, sourceFiles);
return this;
}
public TestConfigurationBuilder setSourceFiles(List<File> sourceFiles) {
this.testSourceFiles = new ArrayList<>(sourceFiles);
return this;
}
public TestConfigurationBuilder setOptions(Map<String, @Nullable String> options) {
this.options.setOptions(options);
return this;
}
public TestConfigurationBuilder addOption(String option) {
this.options.addOption(option);
return this;
}
public TestConfigurationBuilder addOption(String option, String value) {
this.options.addOption(option, value);
return this;
}
public TestConfigurationBuilder addOptionIfValueNonEmpty(String option, String value) {
if (value != null && !value.isEmpty()) {
return addOption(option, value);
}
return this;
}
/**
* Adds the given options to this.
*
* @param options options to add to this
* @return this
*/
@SuppressWarnings("nullness:return") // need @PolyInitialized annotation
@RequiresNonNull("this.options")
public TestConfigurationBuilder addOptions(
@UnknownInitialization(TestConfigurationBuilder.class) TestConfigurationBuilder this,
Map<String, @Nullable String> options) {
this.options.addOptions(options);
return this;
}
public TestConfigurationBuilder addOptions(Iterable<String> newOptions) {
this.options.addOptions(newOptions);
return this;
}
/**
* Set the processors.
*
* @param processors the processors to run
* @return this
*/
public TestConfigurationBuilder setProcessors(Iterable<@BinaryName String> processors) {
this.processors.clear();
for (String proc : processors) {
this.processors.add(proc);
}
return this;
}
/**
* Add a processor.
*
* @param processor a processor to run
* @return this
*/
public TestConfigurationBuilder addProcessor(@BinaryName String processor) {
this.processors.add(processor);
return this;
}
/**
* Add processors.
*
* @param processors processors to run
* @return this
*/
public TestConfigurationBuilder addProcessors(Iterable<@BinaryName String> processors) {
for (String processor : processors) {
this.processors.add(processor);
}
return this;
}
public TestConfigurationBuilder emitDebugInfo() {
this.shouldEmitDebugInfo = true;
return this;
}
public TestConfigurationBuilder dontEmitDebugInfo() {
this.shouldEmitDebugInfo = false;
return this;
}
public TestConfigurationBuilder setShouldEmitDebugInfo(boolean shouldEmitDebugInfo) {
this.shouldEmitDebugInfo = shouldEmitDebugInfo;
return this;
}
/**
* Creates a TestConfiguration using the settings in this builder. The settings are NOT validated
* first.
*
* @return a TestConfiguration using the settings in this builder
*/
public TestConfiguration build() {
return new ImmutableTestConfiguration(
diagnosticFiles,
testSourceFiles,
new ArrayList<>(processors),
options.getOptions(),
shouldEmitDebugInfo);
}
/**
* Creates a TestConfiguration using the settings in this builder. The settings are first
* validated and a runtime exception is thrown if any errors are found
*
* @param requireProcessors whether or not there should be at least 1 processor specified, see
* method validate
* @return a TestConfiguration using the settings in this builder
*/
public TestConfiguration validateThenBuild(boolean requireProcessors) {
List<String> errors = validate(requireProcessors);
if (errors.isEmpty()) {
return build();
}
throw new BugInCF(
"Attempted to build invalid test configuration:%n" + "Errors:%n%s%n%s%n",
String.join("%n", errors), this);
}
/**
* Returns the set of Javac options as a flat list.
*
* @return the set of Javac options as a flat list
*/
public List<String> flatOptions() {
return options.getOptionsAsList();
}
@Override
public String toString() {
return StringsPlume.joinLines(
"TestConfigurationBuilder:",
"testSourceFiles=" + StringsPlume.join(" ", testSourceFiles),
"processors=" + String.join(", ", processors),
"options=" + String.join(", ", options.getOptionsAsList()),
"shouldEmitDebugInfo=" + shouldEmitDebugInfo);
}
/**
* Returns a list that first has the items from parameter list then the items from iterable.
*
* @param <T> the type of the elements in the resulting list
* @param list a list
* @param iterable an iterable
* @return a list that first has the items from parameter list then the items from iterable
*/
private static <T> List<T> catListAndIterable(
final List<? extends T> list, final Iterable<? extends T> iterable) {
final List<T> newList = new ArrayList<>(list);
for (T iterObject : iterable) {
newList.add(iterObject);
}
return newList;
}
public static final String TESTS_OUTPUTDIR = "tests.outputDir";
public static File getOutputDirFromProperty() {
return new File(
System.getProperty(
"tests.outputDir",
"tests" + File.separator + "build" + File.separator + "testclasses"));
}
public static String getDefaultClassPath() {
String classpath = System.getProperty("tests.classpath", "tests" + File.separator + "build");
String globalclasspath = System.getProperty("java.class.path", "");
return classpath + File.pathSeparator + globalclasspath;
}
}