diff --git a/core/src/main/java/io/kestra/core/models/script/RemoteRunnerInterface.java b/core/src/main/java/io/kestra/core/models/script/RemoteRunnerInterface.java new file mode 100644 index 00000000000..0b8c8da5b91 --- /dev/null +++ b/core/src/main/java/io/kestra/core/models/script/RemoteRunnerInterface.java @@ -0,0 +1,3 @@ +package io.kestra.core.models.script; + +public interface RemoteRunnerInterface {} diff --git a/core/src/main/java/io/kestra/core/models/script/ScriptException.java b/core/src/main/java/io/kestra/core/models/script/ScriptException.java index 72c6f5f3a5e..3c05e9a1627 100644 --- a/core/src/main/java/io/kestra/core/models/script/ScriptException.java +++ b/core/src/main/java/io/kestra/core/models/script/ScriptException.java @@ -3,8 +3,6 @@ import lombok.Builder; import lombok.Getter; -import java.io.Serial; - @Getter @Builder public class ScriptException extends Exception { @@ -13,7 +11,11 @@ public class ScriptException extends Exception { private final int stdErrSize; public ScriptException(int exitCode, int stdOutSize, int stdErrSize) { - super("Command failed with code " + exitCode); + this("Command failed with code " + exitCode, exitCode, stdOutSize, stdErrSize); + } + + public ScriptException(String message, int exitCode, int stdOutSize, int stdErrSize) { + super(message); this.exitCode = exitCode; this.stdOutSize = stdOutSize; this.stdErrSize = stdErrSize; diff --git a/core/src/main/java/io/kestra/core/models/script/ScriptRunner.java b/core/src/main/java/io/kestra/core/models/script/ScriptRunner.java index eaea574fc2a..fb115abfc44 100644 --- a/core/src/main/java/io/kestra/core/models/script/ScriptRunner.java +++ b/core/src/main/java/io/kestra/core/models/script/ScriptRunner.java @@ -1,15 +1,20 @@ package io.kestra.core.models.script; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.kestra.core.runners.RunContext; import io.micronaut.core.annotation.Introspected; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; /** * Base class for all script runners. @@ -24,6 +29,14 @@ public abstract class ScriptRunner { @Pattern(regexp="\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*") protected String type; + @JsonIgnore + @Getter(AccessLevel.NONE) + protected transient Map additionalVars; + + @JsonIgnore + @Getter(AccessLevel.NONE) + protected transient Map env; + /** * This method will be called by the script plugin to run a script on a script runner. * Script runners may be local or remote. @@ -32,4 +45,32 @@ public abstract class ScriptRunner { * and filesToDownload must be used to download output files from the runner. */ public abstract RunnerResult run(RunContext runContext, ScriptCommands scriptCommands, List filesToUpload, List filesToDownload) throws Exception; + + public Map additionalVars(ScriptCommands scriptCommands) { + if (this.additionalVars == null) { + this.additionalVars = scriptCommands.getAdditionalVars(); + } + + return this.additionalVars; + } + + public Map env(ScriptCommands scriptCommands) { + if (this.env == null) { + this.env = Optional.ofNullable(scriptCommands.getEnv()).map(HashMap::new).orElse(new HashMap<>()); + + Map additionalVars = this.additionalVars(scriptCommands); + + if (additionalVars.containsKey(ScriptService.VAR_WORKING_DIR)) { + this.env.put(ScriptService.ENV_WORKING_DIR, additionalVars.get(ScriptService.VAR_WORKING_DIR).toString()); + } + if (additionalVars.containsKey(ScriptService.VAR_OUTPUT_DIR)) { + this.env.put(ScriptService.ENV_OUTPUT_DIR, additionalVars.get(ScriptService.VAR_OUTPUT_DIR).toString()); + } + if (additionalVars.containsKey(ScriptService.VAR_BUCKET_PATH)) { + this.env.put(ScriptService.ENV_BUCKET_PATH, additionalVars.get(ScriptService.VAR_BUCKET_PATH).toString()); + } + } + + return this.env; + } } diff --git a/core/src/main/java/io/kestra/core/models/script/ScriptService.java b/core/src/main/java/io/kestra/core/models/script/ScriptService.java index ebd6f8e5d7c..8787e334e7b 100644 --- a/core/src/main/java/io/kestra/core/models/script/ScriptService.java +++ b/core/src/main/java/io/kestra/core/models/script/ScriptService.java @@ -1,6 +1,7 @@ package io.kestra.core.models.script; import com.google.common.collect.ImmutableMap; +import io.kestra.core.exceptions.IllegalVariableEvaluationException; import io.kestra.core.runners.RunContext; import io.kestra.core.utils.ListUtils; import io.kestra.core.utils.Slugify; @@ -15,10 +16,7 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.BiConsumer; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -46,15 +44,12 @@ public final class ScriptService { private ScriptService() { } - public static String replaceInternalStorage(RunContext runContext, @Nullable String command) throws IOException { - return ScriptService.replaceInternalStorage(runContext, command, (s, s2) -> {}); - } - - public static String replaceInternalStorage(RunContext runContext, @Nullable String command, BiConsumer internalStorageToLocalFileConsumer) throws IOException { - return ScriptService.replaceInternalStorage(runContext, command, internalStorageToLocalFileConsumer, false); - } - - public static String replaceInternalStorage(RunContext runContext, @Nullable String command, BiConsumer internalStorageToLocalFileConsumer, boolean replaceWithRelativePath) throws IOException { + public static String replaceInternalStorage( + RunContext runContext, + @Nullable String command, + BiConsumer internalStorageToLocalFileConsumer, + boolean replaceWithRelativePath + ) throws IOException { if (command == null) { return ""; } @@ -74,22 +69,38 @@ public static String replaceInternalStorage(RunContext runContext, @Nullable Str })); } - public static List uploadInputFiles(RunContext runContext, List commands) throws IOException { - return ScriptService.uploadInputFiles(runContext, commands, (s, s2) -> {}); + public static String replaceInternalStorage( + RunContext runContext, + Map additionalVars, + String command, + BiConsumer internalStorageToLocalFileConsumer, + boolean replaceWithRelativePath + ) throws IOException, IllegalVariableEvaluationException { + return ScriptService.replaceInternalStorage(runContext, additionalVars, List.of(command), internalStorageToLocalFileConsumer, replaceWithRelativePath).get(0); } - public static List uploadInputFiles(RunContext runContext, List commands, BiConsumer internalStorageToLocalFileConsumer) throws IOException { - return uploadInputFiles(runContext, commands, internalStorageToLocalFileConsumer, false); - } - - public static List uploadInputFiles(RunContext runContext, List commands, BiConsumer internalStorageToLocalFileConsumer, boolean replaceWithRelativePath) throws IOException { + public static List replaceInternalStorage( + RunContext runContext, + Map additionalVars, + List commands, + BiConsumer internalStorageToLocalFileConsumer, + boolean replaceWithRelativePath + ) throws IOException, IllegalVariableEvaluationException { return commands .stream() - .map(throwFunction(s -> replaceInternalStorage(runContext, s, internalStorageToLocalFileConsumer, replaceWithRelativePath))) + .map(throwFunction(c -> runContext.render(c, additionalVars))) + .map(throwFunction(c -> ScriptService.replaceInternalStorage(runContext, c, internalStorageToLocalFileConsumer, replaceWithRelativePath))) .collect(Collectors.toList()); } + public static List replaceInternalStorage( + RunContext runContext, + List commands + ) throws IOException, IllegalVariableEvaluationException { + return ScriptService.replaceInternalStorage(runContext, Collections.emptyMap(), commands, (ignored, file) -> {}, false); + } + private static String saveOnLocalStorage(RunContext runContext, String uri) throws IOException { try(InputStream inputStream = runContext.storage().getFile(URI.create(uri))) { Path path = runContext.tempFile(); diff --git a/core/src/main/java/io/kestra/core/models/script/types/ProcessScriptRunner.java b/core/src/main/java/io/kestra/core/models/script/types/ProcessScriptRunner.java index d9a0b38fcf6..783f0a55b63 100644 --- a/core/src/main/java/io/kestra/core/models/script/types/ProcessScriptRunner.java +++ b/core/src/main/java/io/kestra/core/models/script/types/ProcessScriptRunner.java @@ -16,7 +16,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.nio.file.Path; import java.util.List; import java.util.Map; @@ -39,23 +38,13 @@ public RunnerResult run(RunContext runContext, ScriptCommands scriptCommands, Li Logger logger = runContext.logger(); AbstractLogConsumer defaultLogConsumer = scriptCommands.getLogConsumer(); - Map additionalVars = scriptCommands.getAdditionalVars(); - additionalVars.put(ScriptService.VAR_WORKING_DIR, scriptCommands.getWorkingDirectory().toString()); - additionalVars.put(ScriptService.VAR_OUTPUT_DIR, scriptCommands.getOutputDirectory().toString()); - ProcessBuilder processBuilder = new ProcessBuilder(); Map environment = processBuilder.environment(); - if (scriptCommands.getEnv() != null && !scriptCommands.getEnv().isEmpty()) { - environment.putAll(runContext.renderMap(scriptCommands.getEnv(), additionalVars)); - } - environment.put(ScriptService.ENV_WORKING_DIR, scriptCommands.getWorkingDirectory().toString()); - environment.put(ScriptService.ENV_OUTPUT_DIR, scriptCommands.getOutputDirectory().toString()); + environment.putAll(this.env(scriptCommands)); processBuilder.directory(scriptCommands.getWorkingDirectory().toFile()); - - List command = ScriptService.uploadInputFiles(runContext, runContext.render(scriptCommands.getCommands(), additionalVars)); - processBuilder.command(command); + processBuilder.command(scriptCommands.getCommands()); Process process = processBuilder.start(); long pid = process.pid(); @@ -91,6 +80,14 @@ public RunnerResult run(RunContext runContext, ScriptCommands scriptCommands, Li } } + @Override + protected Map runnerAdditionalVars(ScriptCommands scriptCommands) { + return Map.of( + ScriptService.VAR_WORKING_DIR, scriptCommands.getWorkingDirectory().toString(), + ScriptService.VAR_OUTPUT_DIR, scriptCommands.getOutputDirectory().toString() + ); + } + private void killDescendantsOf(ProcessHandle process, Logger logger) { process.descendants().forEach(processHandle -> { if (!processHandle.destroy()) { diff --git a/core/src/main/java/io/kestra/core/runners/FilesService.java b/core/src/main/java/io/kestra/core/runners/FilesService.java index 3433e6cb6a4..d07289f32c9 100644 --- a/core/src/main/java/io/kestra/core/runners/FilesService.java +++ b/core/src/main/java/io/kestra/core/runners/FilesService.java @@ -14,9 +14,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; -import java.util.AbstractMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -25,12 +23,17 @@ public abstract class FilesService { public static Map inputFiles(RunContext runContext, Object inputs) throws Exception { + return FilesService.inputFiles(runContext, Collections.emptyMap(), inputs); + } + + public static Map inputFiles(RunContext runContext, Map additionalVars, Object inputs) throws Exception { Logger logger = runContext.logger(); - Map inputFiles = inputs == null ? Map.of() : PluginUtilsService.transformInputFiles( + Map inputFiles = new HashMap<>(inputs == null ? Map.of() : PluginUtilsService.transformInputFiles( runContext, + additionalVars, inputs - ); + )); inputFiles .forEach(throwBiConsumer((fileName, input) -> { @@ -41,7 +44,7 @@ public static Map inputFiles(RunContext runContext, Object input file.getParentFile().mkdirs(); } - var fileContent = runContext.render(input); + var fileContent = runContext.render(input, additionalVars); if (fileContent.startsWith("kestra://")) { try (var is = runContext.uriToInputStream(URI.create(fileContent)); var out = new FileOutputStream(file)) { diff --git a/core/src/main/java/io/kestra/core/tasks/PluginUtilsService.java b/core/src/main/java/io/kestra/core/tasks/PluginUtilsService.java index 91b0d5e8a91..a3578e44676 100644 --- a/core/src/main/java/io/kestra/core/tasks/PluginUtilsService.java +++ b/core/src/main/java/io/kestra/core/tasks/PluginUtilsService.java @@ -82,15 +82,19 @@ private static void validFilename(String s) { } } - @SuppressWarnings("unchecked") public static Map transformInputFiles(RunContext runContext, @NotNull Object inputFiles) throws IllegalVariableEvaluationException, JsonProcessingException { + return PluginUtilsService.transformInputFiles(runContext, Collections.emptyMap(), inputFiles); + } + + @SuppressWarnings("unchecked") + public static Map transformInputFiles(RunContext runContext, Map additionalVars, @NotNull Object inputFiles) throws IllegalVariableEvaluationException, JsonProcessingException { if (inputFiles instanceof Map) { return (Map) inputFiles; } else if (inputFiles instanceof String) { final TypeReference> reference = new TypeReference<>() {}; return JacksonMapper.ofJson(false).readValue( - runContext.render((String) inputFiles), + runContext.render((String) inputFiles, additionalVars), reference ); } else { diff --git a/core/src/test/java/io/kestra/core/models/script/AbstractScriptRunnerTest.java b/core/src/test/java/io/kestra/core/models/script/AbstractScriptRunnerTest.java index d2700ae4423..1f33df332de 100644 --- a/core/src/test/java/io/kestra/core/models/script/AbstractScriptRunnerTest.java +++ b/core/src/test/java/io/kestra/core/models/script/AbstractScriptRunnerTest.java @@ -5,18 +5,22 @@ import io.kestra.core.models.flows.Flow; import io.kestra.core.models.flows.State; import io.kestra.core.models.tasks.Task; +import io.kestra.core.runners.FilesService; import io.kestra.core.runners.RunContext; import io.kestra.core.runners.RunContextFactory; +import io.kestra.core.storages.StorageInterface; import io.kestra.core.utils.IdUtils; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -26,6 +30,7 @@ @MicronautTest public abstract class AbstractScriptRunnerTest { @Inject private RunContextFactory runContextFactory; + @Inject private StorageInterface storage; @Test protected void run() throws Exception { @@ -51,16 +56,70 @@ protected void fail() { @Test protected void inputAndOutputFiles() throws Exception { - var runContext = runContext(this.runContextFactory); + RunContext runContext = runContextFactory.of(Map.of("internalStorageFile", "kestra://some/internalStorage.txt")); + var commands = initScriptCommands(runContext); - Mockito.when(commands.getCommands()).thenReturn(ScriptService.scriptCommands(List.of("/bin/sh", "-c"), Collections.emptyList(), List.of("cp {{workingDir}}/data.txt {{workingDir}}/out.txt"))); - commands.getWorkingDirectory().resolve("data.txt").toFile().createNewFile(); + // Generate internal storage file + FileUtils.writeStringToFile(Path.of("/tmp/unittest/internalStorage.txt").toFile(), "Hello from internal storage", StandardCharsets.UTF_8); - var scriptRunner = scriptRunner(); - var result = scriptRunner.run(runContext, commands, List.of("data.txt"), List.of("out.txt")); - assertThat(result, notNullValue()); - assertThat(result.getExitCode(), is(0)); + // Generate input files + FileUtils.writeStringToFile(runContext.resolve(Path.of("hello.txt")).toFile(), "Hello World", StandardCharsets.UTF_8); + + DefaultLogConsumer defaultLogConsumer = new DefaultLogConsumer(runContext); + // This is purely to showcase that no logs is sent as STDERR for now as CloudWatch doesn't seem to send such information. + Map logsWithIsStdErr = new HashMap<>(); + + ScriptRunner scriptRunner = scriptRunner(); + + Mockito.when(commands.getLogConsumer()).thenReturn(new AbstractLogConsumer() { + @Override + public void accept(String log, Boolean isStdErr) { + logsWithIsStdErr.put(log, isStdErr); + defaultLogConsumer.accept(log, isStdErr); + } + }); + + List filesToUpload = new ArrayList<>(); + filesToUpload.add("hello.txt"); + + String wdir = this.needsToSpecifyWorkingDirectory() ? "{{ workingDir }}/" : ""; + List renderedCommands = ScriptService.replaceInternalStorage( + runContext, + scriptRunner.additionalVars(commands), + ScriptService.scriptCommands(List.of("/bin/sh", "-c"), null, List.of( + "cat " + wdir + "{{internalStorageFile}} && echo", + "cat " + wdir + "hello.txt && echo", + "cat " + wdir + "hello.txt > " + wdir + "output.txt", + "echo -n 'file from output dir' > {{outputDir}}/file.txt", + "mkdir {{outputDir}}/nested", + "echo -n 'nested file from output dir' > {{outputDir}}/nested/file.txt", + "echo '::{\"outputs\":{\"logOutput\":\"Hello World\"}}::'" + )), + (ignored, file) -> filesToUpload.add(file), + scriptRunner instanceof RemoteRunnerInterface + ); + Mockito.when(commands.getCommands()).thenReturn(renderedCommands); + + List filesToDownload = List.of("output.txt"); + RunnerResult run = scriptRunner.run(runContext, commands, filesToUpload, filesToDownload); + + Map outputFiles = ScriptService.uploadOutputFiles(runContext, commands.getOutputDirectory()); + outputFiles.putAll(FilesService.outputFiles(runContext, filesToDownload)); + + // Exit code for successful job + assertThat(run.getExitCode(), is(0)); + + Set> logEntries = logsWithIsStdErr.entrySet(); + assertThat(logEntries.stream().filter(e -> e.getKey().contains("Hello from internal storage")).findFirst().orElseThrow().getValue(), is(false)); + assertThat(logEntries.stream().filter(e -> e.getKey().contains("Hello World")).findFirst().orElseThrow().getValue(), is(false)); + + // Verify outputFiles + assertThat(IOUtils.toString(storage.get(null, outputFiles.get("output.txt")), StandardCharsets.UTF_8), is("Hello World")); + assertThat(IOUtils.toString(storage.get(null, outputFiles.get("file.txt")), StandardCharsets.UTF_8), is("file from output dir")); + assertThat(IOUtils.toString(storage.get(null, outputFiles.get("nested/file.txt")), StandardCharsets.UTF_8), is("nested file from output dir")); + + assertThat(defaultLogConsumer.getOutputs().get("logOutput"), is("Hello World")); } protected RunContext runContext(RunContextFactory runContextFactory) { @@ -110,7 +169,16 @@ public void accept(String s, Boolean aBoolean) { var outputDirectory = workingDirectory.resolve(IdUtils.create()); outputDirectory.toFile().mkdirs(); Mockito.when(commands.getOutputDirectory()).thenReturn(outputDirectory); + Mockito.when(commands.getAdditionalVars()).thenReturn(new HashMap<>(Map.of( + "workingDir", workingDirectory.toAbsolutePath().toString(), + "outputDir", outputDirectory.toString() + ))); return commands; } + + // If the runner supports working directory override, it's not needed as we can move the current working directory to the proper directory. + protected boolean needsToSpecifyWorkingDirectory() { + return false; + } } \ No newline at end of file diff --git a/core/src/test/java/io/kestra/core/models/script/ScriptServiceTest.java b/core/src/test/java/io/kestra/core/models/script/ScriptServiceTest.java index 633ecdc4f82..645107391f5 100644 --- a/core/src/test/java/io/kestra/core/models/script/ScriptServiceTest.java +++ b/core/src/test/java/io/kestra/core/models/script/ScriptServiceTest.java @@ -1,5 +1,6 @@ package io.kestra.core.models.script; +import io.kestra.core.exceptions.IllegalVariableEvaluationException; import io.kestra.core.models.executions.Execution; import io.kestra.core.models.executions.TaskRun; import io.kestra.core.models.flows.Flow; @@ -15,6 +16,7 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,10 +32,10 @@ class ScriptServiceTest { @Test void replaceInternalStorage() throws IOException { var runContext = runContextFactory.of(); - var command = ScriptService.replaceInternalStorage(runContext, null); + var command = ScriptService.replaceInternalStorage(runContext, null, (ignored, file) -> {}, false); assertThat(command, is("")); - command = ScriptService.replaceInternalStorage(runContext, "my command"); + command = ScriptService.replaceInternalStorage(runContext, "my command", (ignored, file) -> {}, false); assertThat(command, is("my command")); Path path = Path.of("/tmp/unittest/file.txt"); @@ -44,12 +46,12 @@ void replaceInternalStorage() throws IOException { String internalStorageUri = "kestra://some/file.txt"; AtomicReference localFile = new AtomicReference<>(); try { - command = ScriptService.replaceInternalStorage(runContext, "my command with a file: " + internalStorageUri, (ignored, file) -> localFile.set(file)); - assertThat(command, is("my command with a file: " + localFile.get())); + command = ScriptService.replaceInternalStorage(runContext, "my command with an internal storage file: " + internalStorageUri, (ignored, file) -> localFile.set(file), false); + assertThat(command, is("my command with an internal storage file: " + localFile.get())); assertThat(Path.of(localFile.get()).toFile().exists(), is(true)); - command = ScriptService.replaceInternalStorage(runContext, "my command with a file: " + internalStorageUri, (ignored, file) -> localFile.set(file), true); - assertThat(command, is("my command with a file: " + localFile.get().substring(1))); + command = ScriptService.replaceInternalStorage(runContext, "my command with an internal storage file: " + internalStorageUri, (ignored, file) -> localFile.set(file), true); + assertThat(command, is("my command with an internal storage file: " + localFile.get().substring(1))); } finally { Path.of(localFile.get()).toFile().delete(); path.toFile().delete(); @@ -68,13 +70,26 @@ void uploadInputFiles() throws IOException { Map localFileByInternalStorage = new HashMap<>(); String internalStorageUri = "kestra://some/file.txt"; try { - var commands = ScriptService.uploadInputFiles(runContext, List.of("my command with a file: " + internalStorageUri), localFileByInternalStorage::put); + String wdir = "/my/wd"; + var commands = ScriptService.replaceInternalStorage( + runContext, + Map.of("workingDir", wdir), + List.of( + "my command with an internal storage file: " + internalStorageUri, + "my command with some additional var usage: {{ workingDir }}" + ), + localFileByInternalStorage::put, + false + ); assertThat(commands, not(empty())); - assertThat(commands.get(0), is("my command with a file: " + localFileByInternalStorage.get(internalStorageUri))); + assertThat(commands.get(0), is("my command with an internal storage file: " + localFileByInternalStorage.get(internalStorageUri))); assertThat(Path.of(localFileByInternalStorage.get(internalStorageUri)).toFile().exists(), is(true)); + assertThat(commands.get(1), is("my command with some additional var usage: " + wdir)); - commands = ScriptService.uploadInputFiles(runContext, List.of("my command with a file: " + internalStorageUri), localFileByInternalStorage::put, true); - assertThat(commands.get(0), is("my command with a file: " + localFileByInternalStorage.get(internalStorageUri).substring(1))); + commands = ScriptService.replaceInternalStorage(runContext, Collections.emptyMap(), List.of("my command with an internal storage file: " + internalStorageUri), localFileByInternalStorage::put, true); + assertThat(commands.get(0), is("my command with an internal storage file: " + localFileByInternalStorage.get(internalStorageUri).substring(1))); + } catch (IllegalVariableEvaluationException e) { + throw new RuntimeException(e); } finally { localFileByInternalStorage.forEach((k, v) -> Path.of(v).toFile().delete()); path.toFile().delete();