/* * Copyright 2010 Henry Coles * * 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 org.pitest.mutationtest.commandline; import static org.pitest.mutationtest.config.ConfigOption.AVOID_CALLS; import static org.pitest.mutationtest.config.ConfigOption.CHILD_JVM; import static org.pitest.mutationtest.config.ConfigOption.CLASSPATH; import static org.pitest.mutationtest.config.ConfigOption.CLASSPATH_FILE; import static org.pitest.mutationtest.config.ConfigOption.CODE_PATHS; import static org.pitest.mutationtest.config.ConfigOption.COVERAGE_THRESHOLD; import static org.pitest.mutationtest.config.ConfigOption.DEPENDENCY_DISTANCE; import static org.pitest.mutationtest.config.ConfigOption.EXCLUDED_CLASSES; import static org.pitest.mutationtest.config.ConfigOption.EXCLUDED_GROUPS; import static org.pitest.mutationtest.config.ConfigOption.EXCLUDED_METHOD; import static org.pitest.mutationtest.config.ConfigOption.EXPORT_LINE_COVERAGE; import static org.pitest.mutationtest.config.ConfigOption.FAIL_WHEN_NOT_MUTATIONS; import static org.pitest.mutationtest.config.ConfigOption.HISTORY_INPUT_LOCATION; import static org.pitest.mutationtest.config.ConfigOption.HISTORY_OUTPUT_LOCATION; import static org.pitest.mutationtest.config.ConfigOption.INCLUDED_GROUPS; import static org.pitest.mutationtest.config.ConfigOption.INCLUDE_LAUNCH_CLASSPATH; import static org.pitest.mutationtest.config.ConfigOption.JVM_PATH; import static org.pitest.mutationtest.config.ConfigOption.MAX_MUTATIONS_PER_CLASS; import static org.pitest.mutationtest.config.ConfigOption.MAX_SURVIVING; import static org.pitest.mutationtest.config.ConfigOption.MUTATE_STATIC_INITIALIZERS; import static org.pitest.mutationtest.config.ConfigOption.MUTATIONS; import static org.pitest.mutationtest.config.ConfigOption.MUTATION_ENGINE; import static org.pitest.mutationtest.config.ConfigOption.MUTATION_THRESHOLD; import static org.pitest.mutationtest.config.ConfigOption.MUTATION_UNIT_SIZE; import static org.pitest.mutationtest.config.ConfigOption.OUTPUT_FORMATS; import static org.pitest.mutationtest.config.ConfigOption.PLUGIN_CONFIGURATION; import static org.pitest.mutationtest.config.ConfigOption.REPORT_DIR; import static org.pitest.mutationtest.config.ConfigOption.SOURCE_DIR; import static org.pitest.mutationtest.config.ConfigOption.TARGET_CLASSES; import static org.pitest.mutationtest.config.ConfigOption.TEST_FILTER; import static org.pitest.mutationtest.config.ConfigOption.THREADS; import static org.pitest.mutationtest.config.ConfigOption.TIMEOUT_CONST; import static org.pitest.mutationtest.config.ConfigOption.TIMEOUT_FACTOR; import static org.pitest.mutationtest.config.ConfigOption.TIME_STAMPED_REPORTS; import static org.pitest.mutationtest.config.ConfigOption.USE_INLINED_CODE_DETECTION; import static org.pitest.mutationtest.config.ConfigOption.VERBOSE; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Properties; import java.util.logging.Logger; import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.OptionException; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; import joptsimple.OptionSpecBuilder; import joptsimple.util.KeyValuePair; import org.pitest.classpath.ClassPath; import org.pitest.functional.FCollection; import org.pitest.functional.predicate.Predicate; import org.pitest.mutationtest.config.ConfigOption; import org.pitest.mutationtest.config.ReportOptions; import org.pitest.testapi.TestGroupConfig; import org.pitest.util.Glob; import org.pitest.util.Log; import org.pitest.util.Unchecked; public class OptionsParser { private final Predicate<String> dependencyFilter; private static final Logger LOG = Log.getLogger(); private final OptionParser parser; private final ArgumentAcceptingOptionSpec<String> reportDirSpec; private final OptionSpec<String> targetClassesSpec; private final OptionSpec<String> targetTestsSpec; private final OptionSpec<String> avoidCallsSpec; private final OptionSpec<Integer> depth; private final OptionSpec<Integer> threadsSpec; private final OptionSpec<File> sourceDirSpec; private final OptionSpec<File> historyOutputSpec; private final OptionSpec<File> historyInputSpec; private final OptionSpec<String> mutators; private final OptionSpec<String> jvmArgs; private final ArgumentAcceptingOptionSpec<Boolean> mutateStatics; private final OptionSpec<Float> timeoutFactorSpec; private final OptionSpec<Long> timeoutConstSpec; private final OptionSpec<String> excludedMethodsSpec; private final OptionSpec<Integer> maxMutationsPerClassSpec; private final ArgumentAcceptingOptionSpec<Boolean> verboseSpec; private final OptionSpec<String> excludedClassesSpec; private final OptionSpec<String> outputFormatSpec; private final OptionSpec<String> additionalClassPathSpec; private final OptionSpec<File> classPathFile; private final ArgumentAcceptingOptionSpec<Boolean> failWhenNoMutations; private final ArgumentAcceptingOptionSpec<String> codePaths; private final OptionSpec<String> excludedGroupsSpec; private final OptionSpec<String> includedGroupsSpec; private final OptionSpec<Integer> mutationUnitSizeSpec; private final ArgumentAcceptingOptionSpec<Boolean> timestampedReportsSpec; private final ArgumentAcceptingOptionSpec<Boolean> detectInlinedCode; private final ArgumentAcceptingOptionSpec<Integer> mutationThreshHoldSpec; private final ArgumentAcceptingOptionSpec<Integer> coverageThreshHoldSpec; private final ArgumentAcceptingOptionSpec<Integer> maxSurvivingSpec; private final OptionSpec<String> mutationEngine; private final ArgumentAcceptingOptionSpec<Boolean> exportLineCoverageSpec; private final OptionSpec<String> javaExecutable; private final OptionSpec<KeyValuePair> pluginPropertiesSpec; private final ArgumentAcceptingOptionSpec<Boolean> includeLaunchClasspathSpec; public OptionsParser(Predicate<String> dependencyFilter) { this.dependencyFilter = dependencyFilter; this.parser = new OptionParser(); this.parser.acceptsAll(Arrays.asList("h", "?"), "show help"); this.reportDirSpec = parserAccepts(REPORT_DIR).withRequiredArg() .describedAs("directory to create report folder in").required(); this.targetClassesSpec = parserAccepts(TARGET_CLASSES) .withRequiredArg() .ofType(String.class) .withValuesSeparatedBy(',') .describedAs( "comma separated list of filters to match against classes to test") .required(); this.avoidCallsSpec = parserAccepts(AVOID_CALLS) .withRequiredArg() .ofType(String.class) .withValuesSeparatedBy(',') .describedAs( "comma separated list of packages to consider as untouchable logging calls"); this.targetTestsSpec = parserAccepts(TEST_FILTER) .withRequiredArg() .ofType(String.class) .withValuesSeparatedBy(',') .describedAs( "comma separated list of filters to match against tests to run"); this.depth = parserAccepts(DEPENDENCY_DISTANCE).withRequiredArg() .ofType(Integer.class) .defaultsTo(DEPENDENCY_DISTANCE.getDefault(Integer.class)) .describedAs("maximum distance to look from test for covered classes"); this.threadsSpec = parserAccepts(THREADS).withRequiredArg() .ofType(Integer.class).defaultsTo(THREADS.getDefault(Integer.class)) .describedAs("number of threads to use for testing"); this.maxMutationsPerClassSpec = parserAccepts(MAX_MUTATIONS_PER_CLASS) .withRequiredArg().ofType(Integer.class) .defaultsTo(MAX_MUTATIONS_PER_CLASS.getDefault(Integer.class)) .describedAs("max number of mutations to allow for each class"); this.sourceDirSpec = parserAccepts(SOURCE_DIR).withRequiredArg() .ofType(File.class).withValuesSeparatedBy(',') .describedAs("comma separated list of source directories").required(); this.mutators = parserAccepts(MUTATIONS).withRequiredArg() .ofType(String.class).withValuesSeparatedBy(',') .describedAs("comma separated list of mutation operators"); this.jvmArgs = parserAccepts(CHILD_JVM).withRequiredArg() .withValuesSeparatedBy(',') .describedAs("comma separated list of child JVM args"); this.mutateStatics = parserAccepts(MUTATE_STATIC_INITIALIZERS) .withOptionalArg() .ofType(Boolean.class) .defaultsTo(true) .describedAs( "whether or not to generate mutations in static initializers"); this.detectInlinedCode = parserAccepts(USE_INLINED_CODE_DETECTION) .withOptionalArg() .ofType(Boolean.class) .defaultsTo(true) .describedAs( "whether or not to try and detect code inlined from finally blocks"); this.timestampedReportsSpec = parserAccepts(TIME_STAMPED_REPORTS) .withOptionalArg().ofType(Boolean.class).defaultsTo(true) .describedAs("whether or not to generated timestamped directories"); this.timeoutFactorSpec = parserAccepts(TIMEOUT_FACTOR).withOptionalArg() .ofType(Float.class) .describedAs("factor to apply to calculate maximum test duration") .defaultsTo(TIMEOUT_FACTOR.getDefault(Float.class)); this.timeoutConstSpec = parserAccepts(TIMEOUT_CONST).withOptionalArg() .ofType(Long.class) .describedAs("constant to apply to calculate maximum test duration") .defaultsTo(TIMEOUT_CONST.getDefault(Long.class)); this.excludedMethodsSpec = parserAccepts(EXCLUDED_METHOD) .withRequiredArg() .ofType(String.class) .withValuesSeparatedBy(',') .describedAs( "comma separated list of filters to match against methods to exclude from mutation analysis"); this.excludedClassesSpec = parserAccepts(EXCLUDED_CLASSES) .withRequiredArg() .ofType(String.class) .withValuesSeparatedBy(',') .describedAs( "comma separated list of globs for classes to exclude when looking for both mutation target and tests"); this.verboseSpec = parserAccepts(VERBOSE).withOptionalArg() .ofType(Boolean.class).defaultsTo(true) .describedAs("whether or not to generate verbose output"); this.exportLineCoverageSpec = parserAccepts(EXPORT_LINE_COVERAGE) .withOptionalArg() .ofType(Boolean.class) .defaultsTo(true) .describedAs( "whether or not to dump per test line coverage data to disk"); this.includeLaunchClasspathSpec = parserAccepts(INCLUDE_LAUNCH_CLASSPATH) .withOptionalArg().ofType(Boolean.class).defaultsTo(true) .describedAs("whether or not to analyse launch classpath"); this.outputFormatSpec = parserAccepts(OUTPUT_FORMATS) .withRequiredArg() .ofType(String.class) .withValuesSeparatedBy(',') .describedAs( "comma separated list of listeners to receive mutation results") .defaultsTo("HTML"); this.additionalClassPathSpec = parserAccepts(CLASSPATH).withRequiredArg() .ofType(String.class).withValuesSeparatedBy(',') .describedAs("coma separated list of additional classpath elements"); this.classPathFile = this.parserAccepts(CLASSPATH_FILE).withRequiredArg() .ofType(File.class).describedAs("File with a list of additional classpath elements (one per line)"); this.failWhenNoMutations = parserAccepts(FAIL_WHEN_NOT_MUTATIONS) .withOptionalArg().ofType(Boolean.class).defaultsTo(true) .describedAs("whether to throw error if no mutations found"); this.codePaths = parserAccepts(CODE_PATHS) .withRequiredArg() .ofType(String.class) .withValuesSeparatedBy(',') .describedAs( "Globs identifying classpath roots containing mutable code"); this.includedGroupsSpec = parserAccepts(INCLUDED_GROUPS).withRequiredArg() .ofType(String.class).withValuesSeparatedBy(',') .describedAs("TestNG groups/JUnit categories to include"); this.excludedGroupsSpec = parserAccepts(EXCLUDED_GROUPS).withRequiredArg() .ofType(String.class).withValuesSeparatedBy(',') .describedAs("TestNG groups/JUnit categories to include"); this.mutationUnitSizeSpec = parserAccepts(MUTATION_UNIT_SIZE) .withRequiredArg() .ofType(Integer.class) .describedAs( "Maximum number of mutations to include within a single unit of analysis") .defaultsTo(MUTATION_UNIT_SIZE.getDefault(Integer.class)); this.historyInputSpec = parserAccepts(HISTORY_INPUT_LOCATION) .withRequiredArg().ofType(File.class) .describedAs("File to read history from for incremental analysis"); this.historyOutputSpec = parserAccepts(HISTORY_OUTPUT_LOCATION) .withRequiredArg().ofType(File.class) .describedAs("File to write history to for incremental analysis"); this.mutationThreshHoldSpec = parserAccepts(MUTATION_THRESHOLD) .withRequiredArg().ofType(Integer.class) .describedAs("Mutation score below which to throw an error") .defaultsTo(MUTATION_THRESHOLD.getDefault(Integer.class)); this.maxSurvivingSpec = parserAccepts(MAX_SURVIVING) .withRequiredArg().ofType(Integer.class) .describedAs("Maximum number of surviving mutants to allow without throwing an error") .defaultsTo(MAX_SURVIVING.getDefault(Integer.class)); this.coverageThreshHoldSpec = parserAccepts(COVERAGE_THRESHOLD) .withRequiredArg().ofType(Integer.class) .describedAs("Line coverage below which to throw an error") .defaultsTo(COVERAGE_THRESHOLD.getDefault(Integer.class)); this.mutationEngine = parserAccepts(MUTATION_ENGINE).withRequiredArg() .ofType(String.class).describedAs("mutation engine to use") .defaultsTo(MUTATION_ENGINE.getDefault(String.class)); this.javaExecutable = parserAccepts(JVM_PATH).withRequiredArg() .ofType(String.class).describedAs("path to java executable"); this.pluginPropertiesSpec = parserAccepts(PLUGIN_CONFIGURATION) .withRequiredArg().ofType(KeyValuePair.class) .describedAs("custom plugin properties"); } private OptionSpecBuilder parserAccepts(final ConfigOption option) { return this.parser.accepts(option.getParamName()); } public ParseResult parse(final String[] args) { final ReportOptions data = new ReportOptions(); try { final OptionSet userArgs = this.parser.parse(args); return parseCommandLine(data, userArgs); } catch (final OptionException uoe) { return new ParseResult(data, uoe.getLocalizedMessage()); } } /** * Creates a new ParseResult object using the command line arguments. * * @param data * the ReportOptions to populate. * @param userArgs * the OptionSet which contains the command line arguments. * @return a new ParseResult, correctly configured using the command line * arguments. */ private ParseResult parseCommandLine(final ReportOptions data, final OptionSet userArgs) { data.setReportDir(userArgs.valueOf(this.reportDirSpec)); data.setTargetClasses(FCollection.map( this.targetClassesSpec.values(userArgs), Glob.toGlobPredicate())); data.setTargetTests(FCollection.map(this.targetTestsSpec.values(userArgs), Glob.toGlobPredicate())); data.setSourceDirs(this.sourceDirSpec.values(userArgs)); data.setMutators(this.mutators.values(userArgs)); data.setDependencyAnalysisMaxDistance(this.depth.value(userArgs)); data.addChildJVMArgs(this.jvmArgs.values(userArgs)); data.setMutateStaticInitializers(userArgs.has(this.mutateStatics) && userArgs.valueOf(this.mutateStatics)); data.setDetectInlinedCode(userArgs.has(this.detectInlinedCode) && userArgs.valueOf(this.detectInlinedCode)); data.setIncludeLaunchClasspath(userArgs .valueOf(this.includeLaunchClasspathSpec)); data.setShouldCreateTimestampedReports(userArgs .valueOf(this.timestampedReportsSpec)); data.setNumberOfThreads(this.threadsSpec.value(userArgs)); data.setTimeoutFactor(this.timeoutFactorSpec.value(userArgs)); data.setTimeoutConstant(this.timeoutConstSpec.value(userArgs)); data.setLoggingClasses(this.avoidCallsSpec.values(userArgs)); data.setExcludedMethods(FCollection.map( this.excludedMethodsSpec.values(userArgs), Glob.toGlobPredicate())); data.setExcludedClasses(FCollection.map( this.excludedClassesSpec.values(userArgs), Glob.toGlobPredicate())); data.setMaxMutationsPerClass(this.maxMutationsPerClassSpec.value(userArgs)); data.setVerbose(userArgs.has(this.verboseSpec) && userArgs.valueOf(this.verboseSpec)); data.addOutputFormats(this.outputFormatSpec.values(userArgs)); data.setFailWhenNoMutations(this.failWhenNoMutations.value(userArgs)); data.setCodePaths(this.codePaths.values(userArgs)); data.setMutationUnitSize(this.mutationUnitSizeSpec.value(userArgs)); data.setHistoryInputLocation(this.historyInputSpec.value(userArgs)); data.setHistoryOutputLocation(this.historyOutputSpec.value(userArgs)); data.setMutationThreshold(this.mutationThreshHoldSpec.value(userArgs)); data.setMaximumAllowedSurvivors(this.maxSurvivingSpec.value(userArgs)); data.setCoverageThreshold(this.coverageThreshHoldSpec.value(userArgs)); data.setMutationEngine(this.mutationEngine.value(userArgs)); data.setFreeFormProperties(listToProperties(this.pluginPropertiesSpec .values(userArgs))); data.setExportLineCoverage(userArgs.has(this.exportLineCoverageSpec) && userArgs.valueOf(this.exportLineCoverageSpec)); setClassPath(userArgs, data); setTestGroups(userArgs, data); data.setJavaExecutable(this.javaExecutable.value(userArgs)); if (userArgs.has("?")) { return new ParseResult(data, "See above for supported parameters."); } else { return new ParseResult(data, null); } } private void setClassPath(final OptionSet userArgs, final ReportOptions data) { final List<String> elements = new ArrayList<String>(); if (data.isIncludeLaunchClasspath()) { elements.addAll(ClassPath.getClassPathElementsAsPaths()); } else { elements.addAll(FCollection.filter( ClassPath.getClassPathElementsAsPaths(), this.dependencyFilter)); } if (userArgs.has(this.classPathFile)) { BufferedReader classPathFileBR = null; try { classPathFileBR = new BufferedReader(new FileReader(userArgs.valueOf(this.classPathFile).getAbsoluteFile())); String element; while ((element = classPathFileBR.readLine()) != null) { elements.add(element); } } catch (IOException ioe) { LOG.warning("Unable to read class path file:" + userArgs.valueOf(this.classPathFile).getAbsolutePath() + " - " + ioe.getMessage()); } finally { try { if (classPathFileBR != null) { classPathFileBR.close(); } } catch (IOException ex) { LOG.warning("Error while closing the class path file's buffered reader:" + userArgs.valueOf(this.classPathFile) .getAbsolutePath() + " - " + ex.getMessage()); } } } elements.addAll(userArgs.valuesOf(this.additionalClassPathSpec)); data.setClassPathElements(elements); } private void setTestGroups(final OptionSet userArgs, final ReportOptions data) { final TestGroupConfig conf = new TestGroupConfig( this.excludedGroupsSpec.values(userArgs), this.includedGroupsSpec.values(userArgs)); data.setGroupConfig(conf); } private Properties listToProperties(List<KeyValuePair> kvps) { Properties p = new Properties(); for (KeyValuePair kvp : kvps) { p.put(kvp.key, kvp.value); } return p; } public void printHelp() { try { this.parser.printHelpOn(System.out); } catch (final IOException ex) { throw Unchecked.translateCheckedException(ex); } } }