package org.checkerframework.framework.test;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.checkerframework.framework.util.PluginUtil;
/**
* Used to create an instance of TestConfiguration, TestConfigurationBuilder follows the standard
* builder pattern. That is, 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<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
.addOption("-AprintErrorStack")
.addSourceFiles(testSourceFiles);
if (outputClassDirectory != null) {
configBuilder.addOption("-d", outputClassDirectory.getAbsolutePath());
}
// Use the annotated jdk for the compile bootclasspath
// This is set by build.xml
String jdkJarPath = getJdkJarPathFromProperty();
if (notNullOrEmpty(jdkJarPath)) {
configBuilder.addOption("-Xbootclasspath/p:" + jdkJarPath);
}
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 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<String> processors,
List<String> options,
boolean shouldEmitDebugInfo) {
String classPath = getDefaultClassPath();
File outputDir = getOutputDirFromProperty();
TestConfigurationBuilder builder =
getDefaultConfigurationBuilder(
testSourcePath,
outputDir,
classPath,
testSourceFiles,
processors,
options,
shouldEmitDebugInfo);
return builder.validateThenBuild(true);
}
private static boolean notNullOrEmpty(String str) {
return str != null && !str.isEmpty();
}
/**
* 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 checkerName 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
*/
public static TestConfiguration buildDefaultConfiguration(
String testSourcePath,
File testFile,
String checkerName,
List<String> options,
boolean shouldEmitDebugInfo) {
List<File> javaFiles = Arrays.asList(testFile);
List<String> processors = Arrays.asList(checkerName);
return buildDefaultConfiguration(
testSourcePath, javaFiles, processors, options, shouldEmitDebugInfo);
}
/** 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<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 optoins in initialConfig */
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, 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, 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;
}
public TestConfigurationBuilder addOptions(Map<String, String> options) {
this.options.addOptions(options);
return this;
}
public TestConfigurationBuilder addOptions(Iterable<String> newOptions) {
this.options.addOptions(newOptions);
return this;
}
public TestConfigurationBuilder setProcessors(Iterable<String> processors) {
this.processors.clear();
for (String proc : processors) {
this.processors.add(proc);
}
return this;
}
public TestConfigurationBuilder addProcessor(String processor) {
this.processors.add(processor);
return this;
}
public TestConfigurationBuilder addProcessors(Iterable<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
*/
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
*/
public TestConfiguration validateThenBuild(boolean requireProcessors) {
List<String> errors = validate(requireProcessors);
if (errors.isEmpty()) {
return build();
}
throw new RuntimeException(
"Attempted to build invalid test configuration:\n"
+ "Errors:\n"
+ PluginUtil.join("\n", errors)
+ "\n"
+ this.toString()
+ "\n");
}
/** @return the set of Javac options as a flat list */
public List<String> flatOptions() {
return options.getOptionsAsList();
}
@Override
public String toString() {
return "TestConfigurationBuilder:\n"
+ "testSourceFiles="
+ (testSourceFiles == null ? "null" : PluginUtil.join(" ", testSourceFiles))
+ "\n"
+ "processors="
+ (processors == null ? "null" : PluginUtil.join(", ", processors))
+ "\n"
+ "options="
+ (options == null ? "null" : PluginUtil.join(", ", options.getOptionsAsList()))
+ "\n"
+ "shouldEmitDebugInfo="
+ shouldEmitDebugInfo;
}
/** @return a list that first has the items from parameter list then the items from iterable */
private static <T> List<T> catListAndIterable(
final List<T> list, final Iterable<? extends T> iterable) {
final List<T> newList = new ArrayList<T>();
for (T listObject : list) {
newList.add(listObject);
}
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 "build"
+ File.pathSeparator
+ "junit-4.12.jar"
+ File.pathSeparator
+ "hamcrest-core-1.3.jar"
+ File.pathSeparator
+ classpath
+ File.pathSeparator
+ globalclasspath;
}
/** Uses the system property "JDK_JAR" to find the annotated JDK */
public static String getJdkJarPathFromProperty() {
return System.getProperty("JDK_JAR");
}
}