diff --git a/pitest-ant/src/main/java/org/pitest/ant/PitestTask.java b/pitest-ant/src/main/java/org/pitest/ant/PitestTask.java index 70cc2b7db..ee767bfc2 100644 --- a/pitest-ant/src/main/java/org/pitest/ant/PitestTask.java +++ b/pitest-ant/src/main/java/org/pitest/ant/PitestTask.java @@ -260,6 +260,10 @@ public void setOutputEncoding(String value) { this.setOption(ConfigOption.OUTPUT_ENCODING, value); } + public void setArgLine(String value) { + this.setOption(ConfigOption.ARG_LINE, value); + } + private void setOption(final ConfigOption option, final String value) { if (!"".equals(value)) { this.options.put(option.getParamName(), value); @@ -271,4 +275,5 @@ public void setUseClasspathJar(String value) { } + } \ No newline at end of file diff --git a/pitest-ant/src/test/java/org/pitest/ant/PitestTaskTest.java b/pitest-ant/src/test/java/org/pitest/ant/PitestTaskTest.java index 038ca4488..3073e3b9a 100644 --- a/pitest-ant/src/test/java/org/pitest/ant/PitestTaskTest.java +++ b/pitest-ant/src/test/java/org/pitest/ant/PitestTaskTest.java @@ -489,6 +489,12 @@ public void passesOutputEncodingToJavaTask() { verify(this.arg).setValue("--outputEncoding=US-ASCII"); } + @Test + public void passesArgLineToJavaTask() { + this.pitestTask.setArgLine("-Dfoo=\"bar\""); + this.pitestTask.execute(this.java); + verify(this.arg).setValue("--argLine=-Dfoo=\"bar\""); + } private static class PathMatcher implements ArgumentMatcher { diff --git a/pitest-command-line/src/main/java/org/pitest/mutationtest/commandline/OptionsParser.java b/pitest-command-line/src/main/java/org/pitest/mutationtest/commandline/OptionsParser.java index afda79cbc..2c0410b66 100644 --- a/pitest-command-line/src/main/java/org/pitest/mutationtest/commandline/OptionsParser.java +++ b/pitest-command-line/src/main/java/org/pitest/mutationtest/commandline/OptionsParser.java @@ -43,6 +43,7 @@ import java.util.function.Predicate; import java.util.logging.Logger; +import static org.pitest.mutationtest.config.ConfigOption.ARG_LINE; 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; @@ -107,6 +108,7 @@ public class OptionsParser { private final OptionSpec historyInputSpec; private final OptionSpec mutators; private final OptionSpec features; + private final OptionSpec argLine; private final OptionSpec jvmArgs; private final CommaAwareArgsProcessor jvmArgsProcessor; private final OptionSpec timeoutFactorSpec; @@ -203,6 +205,9 @@ public OptionsParser(Predicate dependencyFilter) { .ofType(String.class).withValuesSeparatedBy(',') .describedAs("comma separated list of features to enable/disable."); + this.argLine = parserAccepts(ARG_LINE).withRequiredArg() + .describedAs("argline for child JVMs"); + this.jvmArgs = parserAccepts(CHILD_JVM).withRequiredArg() .describedAs("comma separated list of child JVM args"); @@ -416,6 +421,8 @@ private ParseResult parseCommandLine(final ReportOptions data, data.setMutators(this.mutators.values(userArgs)); data.setFeatures(this.features.values(userArgs)); + data.setArgLine(this.argLine.value(userArgs)); + data.addChildJVMArgs(this.jvmArgsProcessor.values(userArgs)); data.setFullMutationMatrix(this.fullMutationMatrixSpec.value(userArgs)); diff --git a/pitest-command-line/src/test/java/org/pitest/mutationtest/commandline/OptionsParserTest.java b/pitest-command-line/src/test/java/org/pitest/mutationtest/commandline/OptionsParserTest.java index 89b073b28..294cb1cd2 100644 --- a/pitest-command-line/src/test/java/org/pitest/mutationtest/commandline/OptionsParserTest.java +++ b/pitest-command-line/src/test/java/org/pitest/mutationtest/commandline/OptionsParserTest.java @@ -96,6 +96,13 @@ public void shouldParseCommaSeparatedListOfSourceDirectories() { assertEquals(Arrays.asList(new File("foo/bar"), new File("bar/far")), actual.getSourceDirs()); } + @Test + public void shouldSetArgLine() { + final ReportOptions actual = parseAddingRequiredArgs("--argLine", "-Dfoo=\"bar\""); + + assertThat(actual.getArgLine()).isEqualTo("-Dfoo=\"bar\""); + } + @Test public void shouldParseCommaSeparatedListOfJVMArgs() { final ReportOptions actual = parseAddingRequiredArgs("--jvmArgs", "foo,bar"); diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/config/ConfigOption.java b/pitest-entry/src/main/java/org/pitest/mutationtest/config/ConfigOption.java index 976750112..3d9868104 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/config/ConfigOption.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/config/ConfigOption.java @@ -54,6 +54,10 @@ public enum ConfigOption { */ CHILD_JVM("jvmArgs"), + /** + * Arguments to launch child processes with expressed as single line + */ + ARG_LINE("argLine"), /** * Do/don't create timestamped folders for reports diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/config/ReportOptions.java b/pitest-entry/src/main/java/org/pitest/mutationtest/config/ReportOptions.java index afab173c9..18d1b5c58 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/config/ReportOptions.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/config/ReportOptions.java @@ -97,6 +97,9 @@ public class ReportOptions { private Collection features; private final List jvmArgs = new ArrayList<>(DEFAULT_CHILD_JVM_ARGS); + + private String argLine; + private int numberOfThreads = 0; private float timeoutFactor = PercentAndConstantTimeoutStrategy.DEFAULT_FACTOR; private long timeoutConstant = PercentAndConstantTimeoutStrategy.DEFAULT_CONSTANT; @@ -219,6 +222,14 @@ public void addChildJVMArgs(final List args) { this.jvmArgs.addAll(args); } + public String getArgLine() { + return argLine; + } + + public void setArgLine(String argLine) { + this.argLine = argLine; + } + public ClassPath getClassPath() { if (this.classPathElements != null) { return createClassPathFromElements(); @@ -628,6 +639,7 @@ public void setOutputEncoding(Charset outputEncoding) { this.outputEncoding = outputEncoding; } + @Override public String toString() { return new StringJoiner(", ", ReportOptions.class.getSimpleName() + "[", "]") @@ -644,6 +656,7 @@ public String toString() { .add("mutators=" + mutators) .add("features=" + features) .add("jvmArgs=" + jvmArgs) + .add("argLine=" + argLine) .add("numberOfThreads=" + numberOfThreads) .add("timeoutFactor=" + timeoutFactor) .add("timeoutConstant=" + timeoutConstant) @@ -676,4 +689,6 @@ public String toString() { .add("outputEncoding=" + outputEncoding) .toString(); } + + } diff --git a/pitest-entry/src/main/java/org/pitest/mutationtest/tooling/EntryPoint.java b/pitest-entry/src/main/java/org/pitest/mutationtest/tooling/EntryPoint.java index 0454f7e60..ee85fc38c 100644 --- a/pitest-entry/src/main/java/org/pitest/mutationtest/tooling/EntryPoint.java +++ b/pitest-entry/src/main/java/org/pitest/mutationtest/tooling/EntryPoint.java @@ -18,6 +18,7 @@ import org.pitest.mutationtest.incremental.WriterFactory; import org.pitest.plugin.Feature; import org.pitest.plugin.FeatureParameter; +import org.pitest.process.ArgLineParser; import org.pitest.process.JavaAgent; import org.pitest.process.LaunchOptions; import org.pitest.util.Log; @@ -28,6 +29,8 @@ import java.io.File; import java.io.IOException; import java.io.Reader; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Consumer; @@ -97,7 +100,7 @@ public AnalysisResult execute(File baseDir, ReportOptions data, final CoverageOptions coverageOptions = settings.createCoverageOptions(); final LaunchOptions launchOptions = new LaunchOptions(ja, - settings.getJavaExecutable(), data.getJvmArgs(), environmentVariables) + settings.getJavaExecutable(), createJvmArgs(data), environmentVariables) .usingClassPathJar(data.useClasspathJar()); final ProjectClassPaths cps = data.getMutationClassPaths(); @@ -132,6 +135,12 @@ public AnalysisResult execute(File baseDir, ReportOptions data, } + private List createJvmArgs(ReportOptions data) { + List args = new ArrayList<>(data.getJvmArgs()); + args.addAll(ArgLineParser.split(data.getArgLine())); + return args; + } + private HistoryStore makeHistoryStore(ReportOptions data, Optional historyWriter) { final Optional reader = data.createHistoryReader(); if (!reader.isPresent() && !historyWriter.isPresent()) { diff --git a/pitest-entry/src/main/java/org/pitest/process/ArgLineParser.java b/pitest-entry/src/main/java/org/pitest/process/ArgLineParser.java new file mode 100644 index 000000000..ce71cfae4 --- /dev/null +++ b/pitest-entry/src/main/java/org/pitest/process/ArgLineParser.java @@ -0,0 +1,107 @@ +package org.pitest.process; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.StringTokenizer; + +import static org.pitest.process.ArgLineParser.State.START; + +/** + * Simple state machine to split arglines into sections. Arglines may + * contain single or double quotes, which might be escaped. + */ +public class ArgLineParser { + + private static final String ESCAPE_CHAR = "\\"; + private static final String SINGLE_QUOTE = "\'"; + public static final String DOUBLE_QUOTE = "\""; + + public static List split(String in) { + return process(stripWhiteSpace(in)); + } + + private static List process(String in) { + if (in.isEmpty()) { + return Collections.emptyList(); + } + + final StringTokenizer tokenizer = new StringTokenizer(in, "\"\' \\", true); + List tokens = new ArrayList<>(); + + Deque state = new ArrayDeque<>(); + state.push(START); + StringBuilder current = new StringBuilder(); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + switch (state.peek()) { + case START: + if (token.equals(SINGLE_QUOTE)) { + state.push(State.IN_QUOTE); + } else if (token.equals(DOUBLE_QUOTE)) { + state.push(State.IN_DOUBLE_QUOTE); + } else if (token.equals(" ")) { + if (current.length() != 0) { + tokens.add(current.toString()); + current = new StringBuilder(); + } + } else { + current.append(token); + if (token.equals(ESCAPE_CHAR)) { + state.push(State.IN_ESCAPE); + } + } + break; + case IN_QUOTE: + if (token.equals(SINGLE_QUOTE)) { + state.pop(); + } else { + current.append(token); + if (token.equals(ESCAPE_CHAR)) { + state.push(State.IN_ESCAPE); + } + } + break; + case IN_DOUBLE_QUOTE: + if (token.equals(DOUBLE_QUOTE)) { + state.pop(); + } else { + current.append(token); + if (token.equals(ESCAPE_CHAR)) { + state.push(State.IN_ESCAPE); + } + } + break; + case IN_ESCAPE: + current.append(token); + if (!token.equals(ESCAPE_CHAR)) { + state.pop(); + } + break; + } + } + + if (current.length() != 0) { + tokens.add(current.toString()); + } + + if (state.size() != 1) { + throw new RuntimeException("Unclosed quote in " + in); + } + + return tokens; + } + + private static String stripWhiteSpace(String in) { + if (in == null) { + return ""; + } + return in.replaceAll("\\s", " ").trim(); + } + + enum State { + START, IN_ESCAPE, IN_QUOTE, IN_DOUBLE_QUOTE + } +} diff --git a/pitest-entry/src/main/java/org/pitest/process/LaunchOptions.java b/pitest-entry/src/main/java/org/pitest/process/LaunchOptions.java index 445774709..170356728 100644 --- a/pitest-entry/src/main/java/org/pitest/process/LaunchOptions.java +++ b/pitest-entry/src/main/java/org/pitest/process/LaunchOptions.java @@ -33,14 +33,17 @@ public LaunchOptions(JavaAgent javaAgentFinder) { } public LaunchOptions(JavaAgent javaAgentFinder, - JavaExecutableLocator javaExecutable, List childJVMArgs, - Map environmentVariables) { + JavaExecutableLocator javaExecutable, + List childJVMArgs, + Map environmentVariables) { this(javaAgentFinder, javaExecutable, childJVMArgs, environmentVariables, false); } public LaunchOptions(JavaAgent javaAgentFinder, - JavaExecutableLocator javaExecutable, List childJVMArgs, - Map environmentVariables, boolean usingClassPathJar) { + JavaExecutableLocator javaExecutable, + List childJVMArgs, + Map environmentVariables, + boolean usingClassPathJar) { this.javaAgentFinder = javaAgentFinder; this.childJVMArgs = childJVMArgs; this.javaExecutable = javaExecutable; diff --git a/pitest-entry/src/main/java/org/pitest/process/WrappingProcess.java b/pitest-entry/src/main/java/org/pitest/process/WrappingProcess.java index f7d935cb2..5c067a99c 100644 --- a/pitest-entry/src/main/java/org/pitest/process/WrappingProcess.java +++ b/pitest-entry/src/main/java/org/pitest/process/WrappingProcess.java @@ -1,7 +1,6 @@ package org.pitest.process; import static org.pitest.functional.prelude.Prelude.or; - import java.io.File; import java.io.IOException; import java.lang.management.ManagementFactory; @@ -34,7 +33,8 @@ public void start() throws IOException { final String[] args = { "" + this.port }; final ProcessBuilder processBuilder = createProcessBuilder( - this.processArgs.getJavaExecutable(), this.processArgs.getJvmArgs(), + this.processArgs.getJavaExecutable(), + this.processArgs.getJvmArgs(), this.minionClass, Arrays.asList(args), this.processArgs.getJavaAgentFinder(), this.processArgs.getLaunchClassPath()); @@ -109,6 +109,7 @@ private List createLaunchArgs(String javaProcess, cmd.addAll(args); addPITJavaAgent(agentJarLocator, cmd); + addLaunchJavaAgents(cmd); cmd.add(mainClass.getName()); diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/ReportTestBase.java b/pitest-entry/src/test/java/org/pitest/mutationtest/ReportTestBase.java index 1dd6c15d1..400811238 100644 --- a/pitest-entry/src/test/java/org/pitest/mutationtest/ReportTestBase.java +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/ReportTestBase.java @@ -106,7 +106,7 @@ protected void createAndRun(SettingsFactory settings) { final CoverageOptions coverageOptions = createCoverageOptions(settings.createCoverageOptions().getPitConfig()); final LaunchOptions launchOptions = new LaunchOptions(agent, new DefaultJavaExecutableLocator(), this.data.getJvmArgs(), - new HashMap()); + new HashMap<>()); final PathFilter pf = new PathFilter(p -> true, p -> true); final ProjectClassPaths cps = new ProjectClassPaths( diff --git a/pitest-entry/src/test/java/org/pitest/mutationtest/TestMutationTesting.java b/pitest-entry/src/test/java/org/pitest/mutationtest/TestMutationTesting.java index 7604c80da..46832cac6 100644 --- a/pitest-entry/src/test/java/org/pitest/mutationtest/TestMutationTesting.java +++ b/pitest-entry/src/test/java/org/pitest/mutationtest/TestMutationTesting.java @@ -232,12 +232,11 @@ private void createEngineAndRun(final ReportOptions data, final JavaAgent agent, final Collection mutators) { - // data.setConfiguration(this.config); final CoverageOptions coverageOptions = createCoverageOptions(data); final LaunchOptions launchOptions = new LaunchOptions(agent, new DefaultJavaExecutableLocator(), data.getJvmArgs(), - new HashMap()); + new HashMap<>()); final PathFilter pf = new PathFilter( Prelude.not(new DefaultDependencyPathPredicate()), diff --git a/pitest-entry/src/test/java/org/pitest/process/ArgLineParserTest.java b/pitest-entry/src/test/java/org/pitest/process/ArgLineParserTest.java new file mode 100644 index 000000000..65c26ddba --- /dev/null +++ b/pitest-entry/src/test/java/org/pitest/process/ArgLineParserTest.java @@ -0,0 +1,139 @@ +package org.pitest.process; + +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; + +public class ArgLineParserTest { + + @Test + public void parsesNullToEmptyList() { + List actual = ArgLineParser.split(null); + assertThat(actual).isEmpty(); + } + + @Test + public void parsesEmptyStringToEmptyList() { + List actual = ArgLineParser.split(""); + assertThat(actual).isEmpty(); + } + + @Test + public void parsesWhiteSpaceToEmptyList() { + List actual = ArgLineParser.split(" "); + assertThat(actual).isEmpty(); + } + + @Test + public void parsesUnquotedItems() { + List actual = ArgLineParser.split("foo bar"); + assertThat(actual).containsExactly("foo", "bar"); + } + + @Test + public void parsesUnquotedItemsWithIrregularWhiteSpace() { + List actual = ArgLineParser.split("foo bar car"); + assertThat(actual).containsExactly("foo", "bar", "car"); + } + + @Test + public void parsesDoubleQuotedItems() { + List actual = ArgLineParser.split("foo \" in double quotes \" bar"); + assertThat(actual).containsExactly("foo", " in double quotes ", "bar"); + } + + @Test + public void parsesSingleQuotedItems() { + List actual = ArgLineParser.split("foo ' in single quotes ' bar"); + assertThat(actual).containsExactly("foo", " in single quotes ", "bar"); + } + + @Test + public void parsesDoubleQuoteWithinSingleQuote() { + List actual = ArgLineParser.split("foo ' \" ' bar"); + assertThat(actual).containsExactly("foo", " \" ", "bar"); + } + + @Test + public void parsesSingleQuoteWithinDoubleQuote() { + List actual = ArgLineParser.split("foo \" ' \" bar"); + assertThat(actual).containsExactly("foo", " ' ", "bar"); + } + + @Test + public void escapedDoubleQuoteRemainsEscaped() { + List actual = ArgLineParser.split("foo \\\" bar"); + assertThat(actual).containsExactly("foo", "\\\"" , "bar"); + } + + @Test + public void escapedSingleQuoteRemainsEscaped() { + List actual = ArgLineParser.split("foo \\' bar"); + assertThat(actual).containsExactly("foo", "\\'" , "bar"); + } + + @Test + public void escapesEscapeChar() { + List actual = ArgLineParser.split("foo \\\\' bar"); + assertThat(actual).containsExactly("foo", "\\\\'" , "bar"); + } + + @Test + public void escapedDoubleQuoteInDoubleQuotesRemainsEscaped() { + List actual = ArgLineParser.split("foo \"bar\\\" car\" la"); + assertThat(actual).containsExactly("foo", "bar\\\" car", "la"); + } + + @Test + public void escapedSingleQuoteInDoubleQuotesRemainsEscaped() { + List actual = ArgLineParser.split("foo \"bar\\' car\" la"); + assertThat(actual).containsExactly("foo", "bar\\' car", "la"); + } + + @Test + public void escapedSingleQuoteInSingleQuotesRemainsEscaped() { + List actual = ArgLineParser.split("foo 'bar\\' car' la"); + assertThat(actual).containsExactly("foo", "bar\\' car", "la"); + } + + @Test + public void escapedDoubleQuoteInSingleQuotesRemainsEscaped() { + List actual = ArgLineParser.split("foo 'bar\\\" car' la"); + assertThat(actual).containsExactly("foo", "bar\\\" car", "la"); + } + + @Test + public void multipleEscapedQuotesRemainEscaped() { + List actual = ArgLineParser.split("foo 'bar\\\" \\\" \\' car' la"); + assertThat(actual).containsExactly("foo", "bar\\\" \\\" \\' car", "la"); + } + + @Test + public void errorsOnUnclosedSingleQuote() { + assertThatCode(() -> ArgLineParser.split("foo '")) + .hasMessageContaining("Unclosed quote"); + } + + @Test + public void errorsOnUnclosedDoubleQuote() { + assertThatCode(() -> ArgLineParser.split("foo \"")) + .hasMessageContaining("Unclosed quote"); + } + + @Test + public void handlesRealExampleArgLine() { + String argLine = "-Dfile.encoding=UTF-8\n" + + " -Dnet.bytebuddy.experimental=true\n" + + " --add-opens=java.base/java.lang=ALL-UNNAMED\n" + + " --add-opens=java.base/java.math=ALL-UNNAMED\n"; + + List actual = ArgLineParser.split(argLine); + assertThat(actual).containsExactly("-Dfile.encoding=UTF-8", + "-Dnet.bytebuddy.experimental=true", + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.math=ALL-UNNAMED"); + } +} \ No newline at end of file diff --git a/pitest-entry/src/test/java/org/pitest/process/WrappingProcessTest.java b/pitest-entry/src/test/java/org/pitest/process/WrappingProcessTest.java index 6d1009974..515889bea 100644 --- a/pitest-entry/src/test/java/org/pitest/process/WrappingProcessTest.java +++ b/pitest-entry/src/test/java/org/pitest/process/WrappingProcessTest.java @@ -33,8 +33,8 @@ public void waitToDieShouldReturnProcessExitCode() throws IOException, InterruptedException { final LaunchOptions launchOptions = new LaunchOptions(NullJavaAgent.instance(), - new DefaultJavaExecutableLocator(), Collections. emptyList(), - new HashMap()); + new DefaultJavaExecutableLocator(), Collections.emptyList(), + new HashMap<>()); final ProcessArgs processArgs = ProcessArgs .withClassPath(new ClassPath().getLocalClassPath()) diff --git a/pitest-maven/src/main/java/org/pitest/maven/AbstractPitMojo.java b/pitest-maven/src/main/java/org/pitest/maven/AbstractPitMojo.java index 283c611d2..26298773e 100644 --- a/pitest-maven/src/main/java/org/pitest/maven/AbstractPitMojo.java +++ b/pitest-maven/src/main/java/org/pitest/maven/AbstractPitMojo.java @@ -165,6 +165,12 @@ public class AbstractPitMojo extends AbstractMojo { @Parameter(property = "jvmArgs") private ArrayList jvmArgs; + /** + * Single line commandline argument + */ + @Parameter(property = "argLine") + private String argLine; + /** * Formats to output during analysis phase */ @@ -591,6 +597,10 @@ public List getJvmArgs() { return withoutNulls(this.jvmArgs); } + public String getArgLine() { + return argLine; + } + public List getOutputFormats() { return withoutNulls(this.outputFormats); } diff --git a/pitest-maven/src/main/java/org/pitest/maven/MojoToReportOptionsConverter.java b/pitest-maven/src/main/java/org/pitest/maven/MojoToReportOptionsConverter.java index 26facf355..43b77a08d 100644 --- a/pitest-maven/src/main/java/org/pitest/maven/MojoToReportOptionsConverter.java +++ b/pitest-maven/src/main/java/org/pitest/maven/MojoToReportOptionsConverter.java @@ -116,6 +116,9 @@ private ReportOptions parseReportOptions(final List classPath) { if (this.mojo.getJvmArgs() != null) { data.addChildJVMArgs(this.mojo.getJvmArgs()); } + if (this.mojo.getArgLine() != null) { + data.setArgLine(this.mojo.getArgLine()); + } data.setMutators(determineMutators()); data.setFeatures(determineFeatures());