diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskOutputResolverV1.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskOutputResolverV1.groovy new file mode 100644 index 0000000000..d75e350720 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskOutputResolverV1.groovy @@ -0,0 +1,188 @@ +/* + * Copyright 2013-2026, Seqera Labs + * + * 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 nextflow.processor + +import java.nio.file.Path + +import groovy.transform.CompileStatic +import groovy.transform.Memoized +import groovy.util.logging.Slf4j +import nextflow.exception.IllegalArityException +import nextflow.exception.MissingFileException +import nextflow.exception.MissingValueException +import nextflow.script.ScriptType +import nextflow.script.params.CmdEvalParam +import nextflow.script.params.DefaultOutParam +import nextflow.script.params.EnvOutParam +import nextflow.script.params.FileOutParam +import nextflow.script.params.OutParam +import nextflow.script.params.StdOutParam +import nextflow.script.params.TupleOutParam +import nextflow.script.params.ValueOutParam +/** + * Implements the resolution of task outputs + * for legacy processes. + * + * @author Ben Sherman + */ +@Slf4j +@CompileStatic +class TaskOutputResolverV1 { + + private TaskRun task + + TaskOutputResolverV1(TaskRun task) { + this.task = task + } + + void resolve(OutParam param) { + switch( param ) { + case StdOutParam: + collectStdOut((StdOutParam) param) + break + + case FileOutParam: + collectOutFiles((FileOutParam) param) + break + + case ValueOutParam: + collectOutValue((ValueOutParam) param, task.context) + break + + case EnvOutParam: + collectOutEnv((EnvOutParam) param) + break + + case CmdEvalParam: + collectOutEval((CmdEvalParam) param) + break + + case DefaultOutParam: + task.setOutput(param, DefaultOutParam.Completion.DONE) + break + + default: + throw new IllegalArgumentException("Invalid process output: ${param.class.simpleName}") + } + } + + /** + * Resolve a process `env` output. + * + * @param param + */ + protected void collectOutEnv(EnvOutParam param) { + final value = collectOutEnvMap(task.workDir, null).get(param.name) + if( value == null && !param.optional ) + throw new MissingValueException("Missing environment variable: ${param.name}") + + task.setOutput(param, value) + } + + /** + * Resolve a process `eval` output. + * + * @param param + */ + protected void collectOutEval(CmdEvalParam param) { + final evalCmds = task.getOutputEvals() + final value = collectOutEnvMap(task.workDir, evalCmds).get(param.name) + if( value == null && !param.optional ) + throw new MissingValueException("Missing environment variable: ${param.name}") + + task.setOutput(param, value) + } + + /** + * Parse the `.command.env` file which holds the value for `env` and `eval` + * outputs. + * + * @param workDir + * The task work directory that contains the `.command.env` file + * @param evalCmds + * A {@link Map} instance containing key-value pairs + */ + @Memoized(maxCacheSize = 10_000) + protected Map collectOutEnvMap(Path workDir, Map evalCmds) { + return new TaskEnvCollector(workDir, evalCmds).collect() + } + + /** + * Resolve a process `stdout` output. + * + * @param param + */ + protected void collectStdOut(StdOutParam param) { + final stdout = task.@stdout + + if( stdout == null && task.type == ScriptType.SCRIPTLET ) + throw new IllegalArgumentException("Missing 'stdout' for process > ${task.lazyName()}") + + if( stdout instanceof Path && !stdout.exists() ) + throw new MissingFileException("Missing 'stdout' file: ${stdout.toUriString()} for process > ${task.lazyName()}") + + final result = stdout instanceof Path ? stdout.text : stdout?.toString() + task.setOutput(param, result) + } + + /** + * Resolve a process `file` or `path` output. + * + * @param param + */ + protected void collectOutFiles(FileOutParam param) { + + // `file` outputs can specify multiple file patterns separated by `:` + final filePatterns = param.getFilePatterns(task.context, task.workDir) + final opts = [ + followLinks: param.followLinks, + glob: param.glob, + hidden: param.hidden, + includeInputs: param.includeInputs, + maxDepth: param.maxDepth, + optional: param.optional || param.arity?.min == 0, + type: param.type, + ] + final allFiles = collectOutFiles0(filePatterns, opts) + + if( !param.isValidArity(allFiles.size()) ) + throw new IllegalArityException("Incorrect number of output files for process `${task.lazyName()}` -- expected ${param.arity}, found ${allFiles.size()}") + + final result = allFiles.size() == 1 && param.isSingle() ? allFiles[0] : allFiles + task.setOutput(param, result) + } + + protected List collectOutFiles0(List filePatterns, Map opts) { + return new TaskFileCollector(filePatterns, opts, task).collect() + } + + /** + * Resolve a process `val` output. + * + * @param param + * @param ctx + */ + protected void collectOutValue(ValueOutParam param, Map ctx) { + try { + task.setOutput(param, param.resolve(ctx)) + } + catch( MissingPropertyException e ) { + throw new MissingValueException("Missing value declared as output parameter: ${e.property}") + } + } + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy index de324786ac..a0bd42afa9 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy @@ -75,20 +75,14 @@ import nextflow.script.ProcessConfigV2 import nextflow.script.ScriptMeta import nextflow.script.ScriptType import nextflow.script.bundle.ResourcesBundle -import nextflow.script.params.BaseOutParam -import nextflow.script.params.CmdEvalParam -import nextflow.script.params.DefaultOutParam import nextflow.script.params.EachInParam import nextflow.script.params.EnvInParam -import nextflow.script.params.EnvOutParam import nextflow.script.params.FileInParam import nextflow.script.params.FileOutParam import nextflow.script.params.InParam -import nextflow.script.params.MissingParam import nextflow.script.params.OptionalParam import nextflow.script.params.OutParam import nextflow.script.params.StdInParam -import nextflow.script.params.StdOutParam import nextflow.script.params.TupleInParam import nextflow.script.params.TupleOutParam import nextflow.script.params.ValueInParam @@ -753,34 +747,9 @@ class TaskProcessor { task.config.process = task.processor.name task.config.executor = task.processor.executor.name - if( config instanceof ProcessConfigV1 ) - initializeTaskRunV1(task) - return task } - private void initializeTaskRunV1(TaskRun task) { - /* - * initialize the inputs/outputs for this task instance - */ - configV1().getInputs().each { InParam param -> - if( param instanceof TupleInParam ) - param.inner.each { task.setInput(it) } - else if( param instanceof EachInParam ) - task.setInput(param.inner) - else - task.setInput(param) - } - - configV1().getOutputs().each { OutParam param -> - if( param instanceof TupleOutParam ) { - param.inner.each { task.setOutput(it) } - } - else - task.setOutput(param) - } - } - /** * Try to check if exists a previously executed process result in the a cached folder. If it exists * use the that result and skip the process execution, otherwise the task is sumitted for execution. @@ -874,7 +843,6 @@ class TaskProcessor { return false } - try { // -- expose task exit status to make accessible as output value task.config.exitStatus = TaskConfig.EXIT_ZERO @@ -1334,67 +1302,38 @@ class TaskProcessor { } } + @CompileStatic protected void bindOutputsV1(TaskRun task) { - // -- creates the map of all tuple values to bind - Map tuples = [:] - for( OutParam param : configV1().getOutputs() ) { - tuples.put(param.index, []) - } - - // -- collects the values to bind - for( OutParam param: task.outputs.keySet() ){ - def value = task.outputs.get(param) + for( final output : configV1().getOutputs() ) { + final params = output instanceof TupleOutParam + ? output.inner as List + : List.of((OutParam) output) - switch( param ) { - case StdOutParam: - log.trace "Process $name > normalize stdout param: $param" - value = value instanceof Path ? value.text : value?.toString() + final values = params.collect { param -> task.outputs[param] } - case OptionalParam: - if( !value && param instanceof OptionalParam && param.optional ) { - final holder = [] as MissingParam; holder.missing = param - tuples[param.index] = holder - break + if( output instanceof OptionalParam ) { + boolean missing = false + for( int i = 0; i < params.size(); i++ ) { + if( !values[i] ) { + log.debug "Process $name > Skipping optional output because it is missing: ${params[i]}" + missing = true + } } - - case EnvOutParam: - case ValueOutParam: - case DefaultOutParam: - log.trace "Process $name > collecting out param: ${param} = $value" - tuples[param.index].add(value) - break - - default: - throw new IllegalArgumentException("Illegal output parameter type: $param") + if( missing ) + continue } - } - // -- bind out the collected values - for( OutParam param : configV1().getOutputs() ) { - final outValue = tuples[param.index] - if( outValue == null ) - throw new IllegalStateException() + log.trace "Process $name > Emitting output: ${output} = ${values}" - if( outValue instanceof MissingParam ) { - log.debug "Process $name > Skipping output binding because one or more optional files are missing: $outValue.missing" + final x = values.size() == 1 ? values[0] : values + final ch = output.getOutChannel() + if( ch == null ) continue - } - - log.trace "Process $name > Binding out param: ${param} = ${outValue}" - bindOutParam(param, outValue) - } - } - - protected void bindOutParam( OutParam param, List values ) { - log.trace "<$name> Binding param $param with $values" - final x = values.size() == 1 ? values[0] : values - final ch = param.getOutChannel() - if( ch != null ) { // create a copy of the output list of operation made by a downstream task // can modify the list which is used internally by the task processor // and result in a potential error. See https://github.com/nextflow-io/nextflow/issues/3768 - final copy = x instanceof List && x instanceof Cloneable ? x.clone() : x + final copy = x instanceof List ? new ArrayList<>(x) : x // emit the final value ch.bind(copy) } @@ -1408,9 +1347,9 @@ class TaskProcessor { @CompileStatic protected void collectOutputs( TaskRun task ) { if( config instanceof ProcessConfigV2 ) - collectOutputsV2( task ) + collectOutputsV2(task) else if( config instanceof ProcessConfigV1 ) - collectOutputsV1( task, task.getTargetDir() ) + collectOutputsV1(task) } @CompileStatic @@ -1431,136 +1370,18 @@ class TaskProcessor { task.canBind = true } - final protected void collectOutputsV1( TaskRun task, Path workDir ) { - log.trace "<$name> collecting output: ${task.outputs}" - - for( OutParam param : task.outputs.keySet() ) { - - switch( param ) { - case StdOutParam: - collectStdOut(task, (StdOutParam)param, task.@stdout) - break - - case FileOutParam: - collectOutFiles(task, (FileOutParam)param, workDir) - break - - case ValueOutParam: - collectOutValues(task, (ValueOutParam)param, task.context) - break - - case EnvOutParam: - collectOutEnvParam(task, (EnvOutParam)param, workDir) - break - - case CmdEvalParam: - collectOutEnvParam(task, (CmdEvalParam)param, workDir) - break - - case DefaultOutParam: - task.setOutput(param, DefaultOutParam.Completion.DONE) - break - - default: - throw new IllegalArgumentException("Illegal output parameter: ${param.class.simpleName}") - - } - } - - // mark ready for output binding - task.canBind = true - } - - protected void collectOutEnvParam(TaskRun task, BaseOutParam param, Path workDir) { - - // fetch the output value - final outCmds = param instanceof CmdEvalParam ? task.getOutputEvals() : null - final val = collectOutEnvMap(workDir,outCmds).get(param.name) - if( val == null && !param.optional ) - throw new MissingValueException("Missing environment variable: $param.name") - // set into the output set - task.setOutput(param,val) - // trace the result - log.trace "Collecting param: ${param.name}; value: ${val}" - - } - - /** - * Parse the `.command.env` file which holds the value for `env` and `cmd` - * output types - * - * @param workDir - * The task work directory that contains the `.command.env` file - * @param outEvals - * A {@link Map} instance containing key-value pairs - * @return - */ @CompileStatic - @Memoized(maxCacheSize = 10_000) - protected Map collectOutEnvMap(Path workDir, Map outEvals) { - return new TaskEnvCollector(workDir, outEvals).collect() - } - - /** - * Collects the process 'std output' - * - * @param task The executed process instance - * @param param The declared {@link StdOutParam} object - * @param stdout The object holding the task produced std out object - */ - protected void collectStdOut( TaskRun task, StdOutParam param, def stdout ) { - - if( stdout == null && task.type == ScriptType.SCRIPTLET ) { - throw new IllegalArgumentException("Missing 'stdout' for process > ${safeTaskName(task)}") - } - - if( stdout instanceof Path && !stdout.exists() ) { - throw new MissingFileException("Missing 'stdout' file: ${stdout.toUriString()} for process > ${safeTaskName(task)}") - } + protected void collectOutputsV1(TaskRun task) { + final resolver = new TaskOutputResolverV1(task) - task.setOutput(param, stdout) - } - - protected void collectOutFiles( TaskRun task, FileOutParam param, Path workDir ) { - - // type file parameter can contain a multiple files pattern separating them with a special character - final filePatterns = param.getFilePatterns(task.context, task.workDir) - final opts = [ - followLinks: param.followLinks, - glob: param.glob, - hidden: param.hidden, - includeInputs: param.includeInputs, - maxDepth: param.maxDepth, - optional: param.optional || param.arity?.min == 0, - type: param.type, - ] - final allFiles = collectOutFiles0(task, filePatterns, opts) - - if( !param.isValidArity(allFiles.size()) ) - throw new IllegalArityException("Incorrect number of output files for process `${safeTaskName(task)}` -- expected ${param.arity}, found ${allFiles.size()}") - - task.setOutput( param, allFiles.size()==1 && param.isSingle() ? allFiles[0] : allFiles ) - - } - - protected List collectOutFiles0(TaskRun task, List filePatterns, Map opts) { - return new TaskFileCollector(filePatterns, opts, task).collect() - } - - protected void collectOutValues( TaskRun task, ValueOutParam param, Map ctx ) { - - try { - // fetch the output value - final val = param.resolve(ctx) - // set into the output set - task.setOutput(param,val) - // trace the result - log.trace "Collecting param: ${param.name}; value: ${val}" - } - catch( MissingPropertyException e ) { - throw new MissingValueException("Missing value declared as output parameter: ${e.property}") + for( OutParam param : configV1().getOutputs() ) { + if( param instanceof TupleOutParam ) + param.inner.each { it -> resolver.resolve(it) } + else + resolver.resolve(param) } + task.canBind = true } @Memoized @@ -1666,6 +1487,16 @@ class TaskProcessor { private void resolveTaskInputsV1(TaskRun task, List values, FilePorter.Batch foreignFiles) { + // -- initialize task inputs + configV1().getInputs().each { InParam param -> + if( param instanceof TupleInParam ) + param.inner.each { it -> task.setInput(it) } + else if( param instanceof EachInParam ) + task.setInput(param.inner) + else + task.setInput(param) + } + // -- validate input lengths for( final param : configV1().getInputs().ofType(TupleInParam) ) { final value = values[param.index] diff --git a/modules/nextflow/src/main/groovy/nextflow/script/params/MissingParam.groovy b/modules/nextflow/src/main/groovy/nextflow/script/params/MissingParam.groovy deleted file mode 100644 index 6ff21bc5e6..0000000000 --- a/modules/nextflow/src/main/groovy/nextflow/script/params/MissingParam.groovy +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2013-2026, Seqera Labs - * - * 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 nextflow.script.params - - -/** - * Placeholder trait to mark a missing optional output parameter - * - * @author Paolo Di Tommaso - */ -trait MissingParam { - - OutParam missing - -} diff --git a/modules/nextflow/src/test/groovy/nextflow/processor/TaskOutputResolverV1Test.groovy b/modules/nextflow/src/test/groovy/nextflow/processor/TaskOutputResolverV1Test.groovy new file mode 100644 index 0000000000..b97b616471 --- /dev/null +++ b/modules/nextflow/src/test/groovy/nextflow/processor/TaskOutputResolverV1Test.groovy @@ -0,0 +1,112 @@ +/* + * Copyright 2013-2026, Seqera Labs + * + * 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 nextflow.processor + +import java.nio.file.Path + +import nextflow.exception.IllegalArityException +import nextflow.script.ScriptType +import nextflow.script.params.FileOutParam +import spock.lang.Specification +import spock.lang.Unroll +/** + * + * @author Paolo Di Tommaso + */ +class TaskOutputResolverV1Test extends Specification { + + @Unroll + def 'should collect output files' () { + given: + def context = new TaskContext(holder: new HashMap()) + def task = new TaskRun( + name: 'foo', + type: ScriptType.SCRIPTLET, + context: context, + config: new TaskConfig(), + workDir: Path.of('/work') + ) + and: + def resolver = Spy(new TaskOutputResolverV1(task)) + + when: + def param = new FileOutParam(new Binding(), []) + .setPathQualifier(true) + .optional(OPTIONAL) + .bind(FILE_NAME) as FileOutParam + if( ARITY ) + param.setArity(ARITY) + and: + resolver.resolve(param) + then: + resolver.collectOutFiles0(_,_) >> RESULTS + and: + task.getOutputs().get(param) == EXPECTED + + where: + FILE_NAME | RESULTS | OPTIONAL | ARITY | EXPECTED + 'file.txt' | [Path.of('/work/file.txt')] | false | null | Path.of('/work/file.txt') + '*' | [Path.of('/work/file.txt')] | false | null | Path.of('/work/file.txt') + '*' | [Path.of('/work/A'), Path.of('/work/B')] | false | null | [Path.of('/work/A'), Path.of('/work/B')] + '*' | [] | true | null | [] + and: + 'file.txt' | [Path.of('/work/file.txt')] | false | '1' | Path.of('/work/file.txt') + '*' | [Path.of('/work/file.txt')] | false | '1' | Path.of('/work/file.txt') + '*' | [Path.of('/work/file.txt')] | false | '1..*' | [Path.of('/work/file.txt')] + '*' | [Path.of('/work/A'), Path.of('/work/B')] | false | '2' | [Path.of('/work/A'), Path.of('/work/B')] + '*' | [Path.of('/work/A'), Path.of('/work/B')] | false | '1..*' | [Path.of('/work/A'), Path.of('/work/B')] + '*' | [] | false | '0..*' | [] + } + + @Unroll + def 'should report output file arity error' () { + given: + def context = new TaskContext(holder: new HashMap()) + def task = new TaskRun( + name: 'foo', + type: ScriptType.SCRIPTLET, + context: context, + config: new TaskConfig(), + workDir: Path.of('/work') + ) + and: + def resolver = Spy(new TaskOutputResolverV1(task)) + + when: + def param = new FileOutParam(new Binding(), []) + .setPathQualifier(true) + .optional(OPTIONAL) + .bind(FILE_NAME) as FileOutParam + if( ARITY ) + param.setArity(ARITY) + and: + resolver.resolve(param) + then: + resolver.collectOutFiles0(_,_) >> RESULTS + and: + def e = thrown(EXCEPTION) + e.message == ERROR + + where: + FILE_NAME | RESULTS | OPTIONAL | ARITY | EXCEPTION | ERROR + 'file.txt' | [Path.of('/work/file.txt')] | false | '2' | IllegalArityException | "Incorrect number of output files for process `foo` -- expected 2, found 1" + '*' | [Path.of('/work/file.txt')] | false | '2' | IllegalArityException | "Incorrect number of output files for process `foo` -- expected 2, found 1" + '*' | [Path.of('/work/file.txt')] | false | '2..*' | IllegalArityException | "Incorrect number of output files for process `foo` -- expected 2..*, found 1" + '*' | [] | true | '1..*' | IllegalArityException | "Incorrect number of output files for process `foo` -- expected 1..*, found 0" + } + +} diff --git a/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy b/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy index a4f6c7ef2d..6a62e2ab85 100644 --- a/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy @@ -38,7 +38,6 @@ import nextflow.script.ProcessConfigV1 import nextflow.script.ScriptType import nextflow.script.bundle.ResourcesBundle import nextflow.script.params.FileInParam -import nextflow.script.params.FileOutParam import nextflow.util.CacheHelper import nextflow.util.MemoryUnit import spock.lang.Specification @@ -491,90 +490,6 @@ class TaskProcessorTest extends Specification { 'f*' | ['/a','/b'] | '3' | 'Incorrect number of input files for process `foo` -- expected 3, found 2' } - def 'should collect output files' () { - given: - def executor = Mock(Executor) - def session = Mock(Session) {getFilePorter()>>Mock(FilePorter) } - def processor = Spy(new TaskProcessor(session:session, executor:executor)) - and: - def context = new TaskContext(holder: new HashMap()) - def task = new TaskRun( - name: 'foo', - type: ScriptType.SCRIPTLET, - context: context, - config: new TaskConfig()) - and: - def workDir = Path.of('/work') - - when: - def param = new FileOutParam(new Binding(), []) - .setPathQualifier(true) - .optional(OPTIONAL) - .bind(FILE_NAME) as FileOutParam - if( ARITY ) - param.setArity(ARITY) - and: - processor.collectOutFiles(task, param, workDir) - then: - processor.collectOutFiles0(_,_,_) >> RESULTS - and: - task.getOutputs().get(param) == EXPECTED - - where: - FILE_NAME | RESULTS | OPTIONAL | ARITY | EXPECTED - 'file.txt' | [Path.of('/work/file.txt')] | false | null | Path.of('/work/file.txt') - '*' | [Path.of('/work/file.txt')] | false | null | Path.of('/work/file.txt') - '*' | [Path.of('/work/A'), Path.of('/work/B')] | false | null | [Path.of('/work/A'), Path.of('/work/B')] - '*' | [] | true | null | [] - and: - 'file.txt' | [Path.of('/work/file.txt')] | false | '1' | Path.of('/work/file.txt') - '*' | [Path.of('/work/file.txt')] | false | '1' | Path.of('/work/file.txt') - '*' | [Path.of('/work/file.txt')] | false | '1..*' | [Path.of('/work/file.txt')] - '*' | [Path.of('/work/A'), Path.of('/work/B')] | false | '2' | [Path.of('/work/A'), Path.of('/work/B')] - '*' | [Path.of('/work/A'), Path.of('/work/B')] | false | '1..*' | [Path.of('/work/A'), Path.of('/work/B')] - '*' | [] | false | '0..*' | [] - } - - @Unroll - def 'should report output file arity error' () { - given: - def executor = Mock(Executor) - def session = Mock(Session) - def processor = Spy(new TaskProcessor(session:session, executor:executor)) - and: - def context = new TaskContext(holder: new HashMap()) - def task = new TaskRun( - name: 'foo', - type: ScriptType.SCRIPTLET, - context: context, - config: new TaskConfig()) - and: - def workDir = Path.of('/work') - - when: - def param = new FileOutParam(new Binding(), []) - .setPathQualifier(true) - .optional(OPTIONAL) - .bind(FILE_NAME) as FileOutParam - if( ARITY ) - param.setArity(ARITY) - and: - processor.collectOutFiles(task, param, workDir) - then: - processor.collectOutFiles0(_,_,_) >> RESULTS - and: - def e = thrown(EXCEPTION) - e.message == ERROR - - where: - FILE_NAME | RESULTS | OPTIONAL | ARITY | EXCEPTION | ERROR - 'file.txt' | [Path.of('/work/file.txt')] | false | '2' | IllegalArityException | "Incorrect number of output files for process `foo` -- expected 2, found 1" - '*' | [Path.of('/work/file.txt')] | false | '2' | IllegalArityException | "Incorrect number of output files for process `foo` -- expected 2, found 1" - '*' | [Path.of('/work/file.txt')] | false | '2..*' | IllegalArityException | "Incorrect number of output files for process `foo` -- expected 2..*, found 1" - '*' | [] | true | '1..*' | IllegalArityException | "Incorrect number of output files for process `foo` -- expected 1..*, found 0" - - } - def 'should submit a task' () { given: def exec = Mock(Executor)