diff options
4 files changed, 514 insertions, 42 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/actions/CommandLinesAndParamFiles.java b/src/main/java/com/google/devtools/build/lib/actions/CommandLinesAndParamFiles.java new file mode 100644 index 0000000000..9eb02d5660 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/actions/CommandLinesAndParamFiles.java @@ -0,0 +1,309 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// 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.google.devtools.build.lib.actions; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander; +import com.google.devtools.build.lib.actions.cache.VirtualActionInput; +import com.google.devtools.build.lib.collect.IterablesChain; +import com.google.devtools.build.lib.util.Fingerprint; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.protobuf.ByteString; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nullable; + +/** + * A class that keeps a list of command lines and optional associated parameter file info. + * + * <p>This class is used by {@link com.google.devtools.build.lib.exec.SpawnRunner} implementations + * to resolve the command lines into a master argument list + any param files needed to be written. + */ +public class CommandLinesAndParamFiles { + + // A (hopefully) conservative estimate of how much long each param file arg would be + // eg. the length of '@path/to/param_file'. + private static final int PARAM_FILE_ARG_LENGTH_ESTIMATE = 512; + private static final UUID PARAM_FILE_UUID = + UUID.fromString("106c1389-88d7-4cc1-8f05-f8a61fd8f7b1"); + + /** A simple tuple of a {@link CommandLine} and a {@link ParamFileInfo}. */ + public static class CommandLineAndParamFileInfo { + private final CommandLine commandLine; + @Nullable private final ParamFileInfo paramFileInfo; + + private CommandLineAndParamFileInfo( + CommandLine commandLine, @Nullable ParamFileInfo paramFileInfo) { + this.commandLine = commandLine; + this.paramFileInfo = paramFileInfo; + } + } + + /** + * Memory optimization: Store as Object instead of <code>List<CommandLineAndParamFileInfo></code>. + * + * <p>We store either a single CommandLine or CommandLineAndParamFileInfo, or list of Objects + * where each item is either a CommandLine or CommandLineAndParamFileInfo. This minimizes unneeded + * wrapper objects. + * + * <p>In the case of actions with a single CommandLine, this saves 48 bytes per action. + */ + private final Object commandLines; + + public CommandLinesAndParamFiles(List<CommandLineAndParamFileInfo> commandLines) { + if (commandLines.size() == 1) { + CommandLineAndParamFileInfo pair = commandLines.get(0); + if (pair.paramFileInfo != null) { + this.commandLines = pair; + } else { + this.commandLines = pair.commandLine; + } + } else { + Object[] result = new Object[commandLines.size()]; + for (int i = 0; i < commandLines.size(); ++i) { + CommandLineAndParamFileInfo pair = commandLines.get(i); + if (pair.paramFileInfo != null) { + result[i] = pair; + } else { + result[i] = pair.commandLine; + } + } + this.commandLines = result; + } + } + + /** + * Resolves this object into a single primary command line and (0-N) param files. The spawn runner + * is expected to write these param files prior to execution of an action. + * + * @param artifactExpander The artifact expander to use. + * @param primaryOutput The primary output of the action. Used to derive param file names. + * @param maxLength The maximum command line length the executing host system can tolerate. + * @return The resolved command line and its param files (if any). + */ + public ResolvedCommandLineAndParamFiles resolve( + ArtifactExpander artifactExpander, Artifact primaryOutput, int maxLength) + throws CommandLineExpansionException { + return resolve( + artifactExpander, primaryOutput.getExecPath(), maxLength, PARAM_FILE_ARG_LENGTH_ESTIMATE); + } + + @VisibleForTesting + ResolvedCommandLineAndParamFiles resolve( + ArtifactExpander artifactExpander, + PathFragment primaryOutputExecPath, + int maxLength, + int paramFileArgLengthEstimate) + throws CommandLineExpansionException { + // Optimize for simple case of single command line + if (commandLines instanceof CommandLine) { + CommandLine commandLine = (CommandLine) commandLines; + return new ResolvedCommandLineAndParamFiles( + commandLine.arguments(artifactExpander), ImmutableList.of()); + } + List<Object> commandLines = + (this.commandLines instanceof CommandLineAndParamFileInfo) + ? ImmutableList.of(this.commandLines) + : Arrays.asList((Object[]) this.commandLines); + IterablesChain.Builder<String> arguments = IterablesChain.builder(); + ArrayList<ParamFileActionInput> paramFiles = new ArrayList<>(commandLines.size()); + int conservativeMaxLength = maxLength - commandLines.size() * paramFileArgLengthEstimate; + int cmdLineLength = 0; + // We name based on the output, starting at <output>-0.params and then incrementing + int paramFileNameSuffix = 0; + for (Object object : commandLines) { + if (object instanceof CommandLine) { + CommandLine commandLine = (CommandLine) object; + Iterable<String> args = commandLine.arguments(artifactExpander); + arguments.add(args); + cmdLineLength += totalArgLen(args); + } else { + CommandLineAndParamFileInfo pair = (CommandLineAndParamFileInfo) object; + CommandLine commandLine = pair.commandLine; + ParamFileInfo paramFileInfo = pair.paramFileInfo; + Preconditions.checkNotNull(paramFileInfo); // If null, we would have just had a CommandLine + Iterable<String> args = commandLine.arguments(artifactExpander); + boolean useParamFile = true; + if (!paramFileInfo.always()) { + int tentativeCmdLineLength = cmdLineLength + totalArgLen(args); + if (tentativeCmdLineLength <= conservativeMaxLength) { + arguments.add(args); + cmdLineLength = tentativeCmdLineLength; + useParamFile = false; + } + } + if (useParamFile) { + PathFragment paramFileExecPath = + ParameterFile.derivePath( + primaryOutputExecPath, Integer.toString(paramFileNameSuffix)); + ++paramFileNameSuffix; + + String paramArg = + paramFileInfo + .getFlagFormatString() + .replaceFirst("%s", paramFileExecPath.getPathString()); + arguments.addElement(paramArg); + cmdLineLength += paramArg.length() + 1; + paramFiles.add(new ParamFileActionInput(paramFileExecPath, args, paramFileInfo)); + } + } + } + return new ResolvedCommandLineAndParamFiles(arguments.build(), paramFiles); + } + + public void addToFingerprint(ActionKeyContext actionKeyContext, Fingerprint fingerprint) + throws CommandLineExpansionException { + // Optimize for simple case of single command line + if (commandLines instanceof CommandLine) { + CommandLine commandLine = (CommandLine) commandLines; + commandLine.addToFingerprint(actionKeyContext, fingerprint); + return; + } + List<Object> commandLines = + (this.commandLines instanceof CommandLineAndParamFileInfo) + ? ImmutableList.of(this.commandLines) + : Arrays.asList((Object[]) this.commandLines); + for (Object object : commandLines) { + if (object instanceof CommandLine) { + CommandLine commandLine = (CommandLine) object; + commandLine.addToFingerprint(actionKeyContext, fingerprint); + } else { + CommandLineAndParamFileInfo pair = (CommandLineAndParamFileInfo) object; + CommandLine commandLine = pair.commandLine; + commandLine.addToFingerprint(actionKeyContext, fingerprint); + ParamFileInfo paramFileInfo = pair.paramFileInfo; + Preconditions.checkNotNull(paramFileInfo); // If null, we would have just had a CommandLine + addParamFileInfoToFingerprint(paramFileInfo, fingerprint); + } + } + } + + /** + * Resolved command lines. + * + * <p>The spawn runner implementation is expected to ensure the param files are available once the + * spawn is executed. + */ + public static class ResolvedCommandLineAndParamFiles { + private final Iterable<String> arguments; + private final List<ParamFileActionInput> paramFiles; + + ResolvedCommandLineAndParamFiles( + Iterable<String> arguments, List<ParamFileActionInput> paramFiles) { + this.arguments = arguments; + this.paramFiles = paramFiles; + } + + /** Returns the primary command line of the command. */ + public Iterable<String> arguments() { + return arguments; + } + + /** Returns the param file action inputs needed to execute the command. */ + public List<ParamFileActionInput> getParamFiles() { + return paramFiles; + } + + /** Convenience function to write all param files locally under the given exec root. */ + public void writeParamFiles(Path execRoot) throws IOException { + for (ParamFileActionInput actionInput : paramFiles) { + Path paramFilePath = execRoot.getRelative(actionInput.paramFileExecPath); + paramFilePath.getParentDirectory().createDirectoryAndParents(); + actionInput.writeTo(paramFilePath.getOutputStream()); + } + } + } + + static final class ParamFileActionInput implements VirtualActionInput { + final PathFragment paramFileExecPath; + final Iterable<String> arguments; + final ParamFileInfo paramFileInfo; + + ParamFileActionInput( + PathFragment paramFileExecPath, Iterable<String> arguments, ParamFileInfo paramFileInfo) { + this.paramFileExecPath = paramFileExecPath; + this.arguments = arguments; + this.paramFileInfo = paramFileInfo; + } + + @Override + public void writeTo(OutputStream out) throws IOException { + ParameterFile.writeParameterFile( + out, arguments, paramFileInfo.getFileType(), paramFileInfo.getCharset()); + } + + @Override + public ByteString getBytes() throws IOException { + ByteString.Output out = ByteString.newOutput(); + writeTo(out); + return out.toByteString(); + } + + @Override + public String getExecPathString() { + return paramFileExecPath.getPathString(); + } + + @Override + public PathFragment getExecPath() { + return paramFileExecPath; + } + } + + private static int totalArgLen(Iterable<String> args) { + int result = 0; + for (String s : args) { + result += s.length() + 1; + } + return result; + } + + private static void addParamFileInfoToFingerprint( + ParamFileInfo paramFileInfo, Fingerprint fingerprint) { + fingerprint.addUUID(PARAM_FILE_UUID); + fingerprint.addString(paramFileInfo.getFlagFormatString()); + fingerprint.addString(paramFileInfo.getFileType().toString()); + fingerprint.addString(paramFileInfo.getCharset().toString()); + } + + public static Builder builder() { + return new Builder(); + } + + /** Builder for {@link CommandLinesAndParamFiles}. */ + public static class Builder { + List<CommandLineAndParamFileInfo> commandLines = new ArrayList<>(); + + public Builder addCommandLine(CommandLine commandLine) { + commandLines.add(new CommandLineAndParamFileInfo(commandLine, null)); + return this; + } + + public Builder addCommandLine(CommandLine commandLine, ParamFileInfo paramFileInfo) { + commandLines.add(new CommandLineAndParamFileInfo(commandLine, paramFileInfo)); + return this; + } + + public CommandLinesAndParamFiles build() { + return new CommandLinesAndParamFiles(commandLines); + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/actions/ParameterFile.java b/src/main/java/com/google/devtools/build/lib/actions/ParameterFile.java index 2671a7e22d..61449f24e1 100644 --- a/src/main/java/com/google/devtools/build/lib/actions/ParameterFile.java +++ b/src/main/java/com/google/devtools/build/lib/actions/ParameterFile.java @@ -15,7 +15,12 @@ package com.google.devtools.build.lib.actions; import com.google.common.annotations.VisibleForTesting; import com.google.devtools.build.lib.util.FileType; +import com.google.devtools.build.lib.util.ShellEscaper; import com.google.devtools.build.lib.vfs.PathFragment; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; /** * Support for parameter file generation (as used by gcc and other tools, e.g. @@ -76,4 +81,41 @@ public class ParameterFile { return original.replaceName(original.getBaseName() + "-" + flavor + ".params"); } + /** Writes an argument list to a parameter file. */ + public static void writeParameterFile( + OutputStream out, Iterable<String> arguments, ParameterFileType type, Charset charset) + throws IOException { + switch (type) { + case SHELL_QUOTED: + writeContentQuoted(out, arguments, charset); + break; + case UNQUOTED: + writeContentUnquoted(out, arguments, charset); + break; + } + } + + /** Writes the arguments from the list into the parameter file. */ + private static void writeContentUnquoted( + OutputStream outputStream, Iterable<String> arguments, Charset charset) throws IOException { + OutputStreamWriter out = new OutputStreamWriter(outputStream, charset); + for (String line : arguments) { + out.write(line); + out.write('\n'); + } + out.flush(); + } + + /** + * Writes the arguments from the list into the parameter file with shell quoting (if required). + */ + private static void writeContentQuoted( + OutputStream outputStream, Iterable<String> arguments, Charset charset) throws IOException { + OutputStreamWriter out = new OutputStreamWriter(outputStream, charset); + for (String line : ShellEscaper.escapeAll(arguments)) { + out.write(line); + out.write('\n'); + } + out.flush(); + } } diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/ParameterFileWriteAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/ParameterFileWriteAction.java index 7af135b67c..8efc38e987 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/actions/ParameterFileWriteAction.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/ParameterFileWriteAction.java @@ -26,16 +26,15 @@ import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander; import com.google.devtools.build.lib.actions.CommandLine; import com.google.devtools.build.lib.actions.CommandLineExpansionException; import com.google.devtools.build.lib.actions.ExecException; +import com.google.devtools.build.lib.actions.ParameterFile; import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType; import com.google.devtools.build.lib.actions.UserExecException; import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization; import com.google.devtools.build.lib.util.Fingerprint; -import com.google.devtools.build.lib.util.ShellEscaper; import java.io.IOException; import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.nio.charset.Charset; /** Action to write a parameter file for a {@link CommandLine}. */ @@ -120,7 +119,7 @@ public final class ParameterFileWriteAction extends AbstractFileWriteAction { } catch (CommandLineExpansionException e) { throw new UserExecException(e); } - return new ParamFileWriter(arguments); + return new ParamFileWriter(arguments, type, charset); } @VisibleForSerialization @@ -128,52 +127,20 @@ public final class ParameterFileWriteAction extends AbstractFileWriteAction { return Iterables.getOnlyElement(outputs); } - private class ParamFileWriter implements DeterministicWriter { + private static class ParamFileWriter implements DeterministicWriter { private final Iterable<String> arguments; + private final ParameterFileType type; + private final Charset charset; - ParamFileWriter(Iterable<String> arguments) { + ParamFileWriter(Iterable<String> arguments, ParameterFileType type, Charset charset) { this.arguments = arguments; + this.type = type; + this.charset = charset; } @Override public void writeOutputFile(OutputStream out) throws IOException { - switch (type) { - case SHELL_QUOTED: - writeContentQuoted(out, arguments); - break; - case UNQUOTED: - writeContentUnquoted(out, arguments); - break; - default: - throw new AssertionError(); - } - } - - /** - * Writes the arguments from the list into the parameter file. - */ - private void writeContentUnquoted(OutputStream outputStream, Iterable<String> arguments) - throws IOException { - OutputStreamWriter out = new OutputStreamWriter(outputStream, charset); - for (String line : arguments) { - out.write(line); - out.write('\n'); - } - out.flush(); - } - - /** - * Writes the arguments from the list into the parameter file with shell - * quoting (if required). - */ - private void writeContentQuoted(OutputStream outputStream, Iterable<String> arguments) - throws IOException { - OutputStreamWriter out = new OutputStreamWriter(outputStream, charset); - for (String line : ShellEscaper.escapeAll(arguments)) { - out.write(line); - out.write('\n'); - } - out.flush(); + ParameterFile.writeParameterFile(out, arguments, type, charset); } } diff --git a/src/test/java/com/google/devtools/build/lib/actions/CommandLinesAndParamFilesTest.java b/src/test/java/com/google/devtools/build/lib/actions/CommandLinesAndParamFilesTest.java new file mode 100644 index 0000000000..bdf5b52e4e --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/actions/CommandLinesAndParamFilesTest.java @@ -0,0 +1,154 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// 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.google.devtools.build.lib.actions; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander; +import com.google.devtools.build.lib.actions.CommandLinesAndParamFiles.ResolvedCommandLineAndParamFiles; +import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType; +import com.google.devtools.build.lib.vfs.FileSystemUtils; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; +import java.nio.charset.StandardCharsets; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link CommandLinesAndParamFiles}. */ +@RunWith(JUnit4.class) +public class CommandLinesAndParamFilesTest { + + private final ArtifactExpander artifactExpander = null; + private final PathFragment execPath = PathFragment.create("output.txt"); + + @Test + public void testSimpleCommandLine() throws Exception { + ResolvedCommandLineAndParamFiles resolved = + CommandLinesAndParamFiles.builder() + .addCommandLine(CommandLine.of(ImmutableList.of("--foo", "--bar"))) + .build() + .resolve(artifactExpander, execPath, 1024, 0); + assertThat(resolved.arguments()).containsExactly("--foo", "--bar"); + assertThat(resolved.getParamFiles()).isEmpty(); + } + + @Test + public void testSimpleParamFileUseAlways() throws Exception { + ResolvedCommandLineAndParamFiles resolved = + CommandLinesAndParamFiles.builder() + .addCommandLine( + CommandLine.of(ImmutableList.of("--foo", "--bar")), + ParamFileInfo.builder(ParameterFileType.UNQUOTED).setUseAlways(true).build()) + .build() + .resolve(artifactExpander, execPath, 1024, 0); + assertThat(resolved.arguments()).containsExactly("@output.txt-0.params"); + assertThat(resolved.getParamFiles()).hasSize(1); + assertThat(resolved.getParamFiles().get(0).arguments).containsExactly("--foo", "--bar"); + } + + @Test + public void testMaybeUseParamsFiles() throws Exception { + CommandLinesAndParamFiles commandLinesAndParamFiles = + CommandLinesAndParamFiles.builder() + .addCommandLine( + CommandLine.of(ImmutableList.of("--foo", "--bar")), + ParamFileInfo.builder(ParameterFileType.UNQUOTED).setUseAlways(false).build()) + .build(); + // Set max length to longer than command line, no param file needed + ResolvedCommandLineAndParamFiles resolved = + commandLinesAndParamFiles.resolve(artifactExpander, execPath, 1024, 0); + assertThat(resolved.arguments()).containsExactly("--foo", "--bar"); + assertThat(resolved.getParamFiles()).isEmpty(); + + // Set max length to 0, spill to param file is forced + resolved = commandLinesAndParamFiles.resolve(artifactExpander, execPath, 0, 0); + assertThat(resolved.arguments()).containsExactly("@output.txt-0.params"); + assertThat(resolved.getParamFiles()).hasSize(1); + assertThat(resolved.getParamFiles().get(0).arguments).containsExactly("--foo", "--bar"); + } + + @Test + public void testMixOfCommandLinesAndParamFiles() throws Exception { + ResolvedCommandLineAndParamFiles resolved = + CommandLinesAndParamFiles.builder() + .addCommandLine(CommandLine.of(ImmutableList.of("a", "b"))) + .addCommandLine( + CommandLine.of(ImmutableList.of("c", "d")), + ParamFileInfo.builder(ParameterFileType.UNQUOTED).setUseAlways(true).build()) + .addCommandLine(CommandLine.of(ImmutableList.of("e", "f"))) + .addCommandLine( + CommandLine.of(ImmutableList.of("g", "h")), + ParamFileInfo.builder(ParameterFileType.UNQUOTED).setUseAlways(true).build()) + .build() + .resolve(artifactExpander, execPath, 1024, 0); + assertThat(resolved.arguments()) + .containsExactly("a", "b", "@output.txt-0.params", "e", "f", "@output.txt-1.params"); + assertThat(resolved.getParamFiles()).hasSize(2); + assertThat(resolved.getParamFiles().get(0).arguments).containsExactly("c", "d"); + assertThat(resolved.getParamFiles().get(0).paramFileExecPath.getPathString()) + .isEqualTo("output.txt-0.params"); + assertThat(resolved.getParamFiles().get(1).arguments).containsExactly("g", "h"); + assertThat(resolved.getParamFiles().get(1).paramFileExecPath.getPathString()) + .isEqualTo("output.txt-1.params"); + } + + @Test + public void testFirstParamFilePassesButSecondFailsLengthTest() throws Exception { + ResolvedCommandLineAndParamFiles resolved = + CommandLinesAndParamFiles.builder() + .addCommandLine( + CommandLine.of(ImmutableList.of("a", "b")), + ParamFileInfo.builder(ParameterFileType.UNQUOTED).setUseAlways(false).build()) + .addCommandLine( + CommandLine.of(ImmutableList.of("c", "d")), + ParamFileInfo.builder(ParameterFileType.UNQUOTED).setUseAlways(false).build()) + .build() + .resolve(artifactExpander, execPath, 4, 0); + assertThat(resolved.arguments()).containsExactly("a", "b", "@output.txt-0.params"); + assertThat(resolved.getParamFiles()).hasSize(1); + assertThat(resolved.getParamFiles().get(0).arguments).containsExactly("c", "d"); + } + + @Test + public void testWriteParamFiles() throws Exception { + CommandLinesAndParamFiles commandLinesAndParamFiles = + CommandLinesAndParamFiles.builder() + .addCommandLine( + CommandLine.of(ImmutableList.of("--foo", "--bar")), + ParamFileInfo.builder(ParameterFileType.UNQUOTED).setUseAlways(true).build()) + .addCommandLine( + CommandLine.of(ImmutableList.of("--baz")), + ParamFileInfo.builder(ParameterFileType.UNQUOTED).setUseAlways(true).build()) + .build(); + InMemoryFileSystem inMemoryFileSystem = new InMemoryFileSystem(); + Path execRoot = inMemoryFileSystem.getPath("/exec"); + execRoot.createDirectoryAndParents(); + ResolvedCommandLineAndParamFiles resolved = + commandLinesAndParamFiles.resolve( + artifactExpander, PathFragment.create("my/param/file/out"), 0, 0); + resolved.writeParamFiles(execRoot); + + assertThat( + FileSystemUtils.readLines( + execRoot.getRelative("my/param/file/out-0.params"), StandardCharsets.ISO_8859_1)) + .containsExactly("--foo", "--bar"); + assertThat( + FileSystemUtils.readLines( + execRoot.getRelative("my/param/file/out-1.params"), StandardCharsets.ISO_8859_1)) + .containsExactly("--baz"); + } +} |