From 5bbc9a28aa86dad702361d54578900d7d72acc65 Mon Sep 17 00:00:00 2001 From: Ben Sherman Date: Wed, 20 May 2026 11:47:51 -0500 Subject: [PATCH 1/3] Fix `module run` to allow optional path inputs Signed-off-by: Ben Sherman --- .../script/ProcessEntryHandler.groovy | 10 ++- .../script/ProcessEntryHandlerTest.groovy | 62 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ProcessEntryHandler.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ProcessEntryHandler.groovy index 3f01de6b82..3aebe14b9d 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/ProcessEntryHandler.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/ProcessEntryHandler.groovy @@ -236,7 +236,7 @@ class ProcessEntryHandler { } /** - * Load mapping of input types from the module spec if available. Returns null + * Load mapping of input types from the module spec if available. Returns empty map * if module spec is absent or unreadable. */ private static Map getModuleSpecInputTypes(Path scriptPath) { @@ -292,6 +292,8 @@ class ProcessEntryHandler { final value = namedArgs.get(name) if( value == null ) { + if( param instanceof FileInParam ) + return [] throw new IllegalArgumentException("Missing required parameter: --${name}") } @@ -387,6 +389,8 @@ class ProcessEntryHandler { final value = namedArgs.get(name) if( value == null ) { + if( param.isOptional() ) + return null throw new IllegalArgumentException("Missing required parameter: --${name}") } @@ -423,10 +427,10 @@ class ProcessEntryHandler { * Otherwise returns a single file object. * * @param fileInput String representation of file path(s) - * @return Single file object or List of file objects + * @return Single file or list of files */ protected Object parseFileInput(String fileInput) { - if (fileInput.contains(',')) { + if( fileInput.contains(',') ) { // Split by comma, trim whitespace, and convert each to a file return fileInput.tokenize(',') .collect { it.trim() } diff --git a/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy index da1cf46b5e..3388c8ef34 100644 --- a/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy @@ -353,4 +353,66 @@ class ProcessEntryHandlerTest extends Specification { '/path/to/file1.txt,,/path/to/file2.txt, ,/path/to/file3.txt' | [Path.of('/path/to/file1.txt'), Path.of('/path/to/file2.txt'), Path.of('/path/to/file3.txt')] 'file1.txt,file2.txt' | [Path.of('file1.txt').toAbsolutePath(), Path.of('file2.txt').toAbsolutePath()] } + + def 'should return empty list for missing optional path input (v1)' () { + // Regression test for https://github.com/nextflow-io/nextflow/issues/7161 + given: + def session = Mock(Session) + def script = Mock(BaseScript) + def meta = Mock(ScriptMeta) { + getLocalProcessNames() >> [ 'hello' ] + } + def handler = new ProcessEntryHandler(script, session, meta) + def pathParam = Mock(FileInParam) { getName() >> 'intervals' } + + when: 'the input is not present in params for a path input' + def result = handler.getValueForInputV1(pathParam, [:], [:]) + + then: 'returns an empty list instead of throwing' + result == [] + } + + def 'should return empty list for missing optional input (v2)' () { + // Regression test for https://github.com/nextflow-io/nextflow/issues/7161 + given: + def session = Mock(Session) + def script = Mock(BaseScript) + def meta = Mock(ScriptMeta) { + getLocalProcessNames() >> [ 'hello' ] + } + def handler = new ProcessEntryHandler(script, session, meta) + def param = Mock(ProcessInput) { + getName() >> 'gzi' + getType() >> Path + isOptional() >> true + } + + when: 'a v2 optional path input is not provided' + def result = handler.getValueForInputV2(param, [:]) + + then: 'returns null instead of throwing' + result == null + } + + def 'should throw for missing required input (v2)' () { + given: + def session = Mock(Session) + def script = Mock(BaseScript) + def meta = Mock(ScriptMeta) { + getLocalProcessNames() >> [ 'hello' ] + } + def handler = new ProcessEntryHandler(script, session, meta) + def param = Mock(ProcessInput) { + getName() >> 'reads' + getType() >> Path + isOptional() >> false + } + + when: + handler.getValueForInputV2(param, [:]) + + then: + def e = thrown(IllegalArgumentException) + e.message.contains('Missing required parameter: --reads') + } } From 0adf4bb65492903a9464324ac8372f81066dabfa Mon Sep 17 00:00:00 2001 From: Ben Sherman Date: Thu, 21 May 2026 11:40:36 -0500 Subject: [PATCH 2/3] Apply suggestion from code review [ci skip] Co-authored-by: Jorge Ejarque Signed-off-by: Ben Sherman --- .../test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy index 3388c8ef34..c8ea6f1069 100644 --- a/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy @@ -372,7 +372,7 @@ class ProcessEntryHandlerTest extends Specification { result == [] } - def 'should return empty list for missing optional input (v2)' () { + def 'should return null for missing optional input (v2)' () { // Regression test for https://github.com/nextflow-io/nextflow/issues/7161 given: def session = Mock(Session) From 356cfcfd062c8e40dbe680307b5595a48f7e63a5 Mon Sep 17 00:00:00 2001 From: Ben Sherman Date: Thu, 21 May 2026 12:00:55 -0500 Subject: [PATCH 3/3] Apply suggestions from review Signed-off-by: Ben Sherman --- .../script/ProcessEntryHandler.groovy | 4 +- .../script/ProcessEntryHandlerTest.groovy | 37 ++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ProcessEntryHandler.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ProcessEntryHandler.groovy index 3aebe14b9d..ec37467d4b 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/ProcessEntryHandler.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/ProcessEntryHandler.groovy @@ -292,8 +292,10 @@ class ProcessEntryHandler { final value = namedArgs.get(name) if( value == null ) { - if( param instanceof FileInParam ) + if( param instanceof FileInParam ) { + log.warn "Path input '--${name}' not provided, defaulting to empty list" return [] + } throw new IllegalArgumentException("Missing required parameter: --${name}") } diff --git a/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy index c8ea6f1069..60ffa65ff8 100644 --- a/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy @@ -354,8 +354,7 @@ class ProcessEntryHandlerTest extends Specification { 'file1.txt,file2.txt' | [Path.of('file1.txt').toAbsolutePath(), Path.of('file2.txt').toAbsolutePath()] } - def 'should return empty list for missing optional path input (v1)' () { - // Regression test for https://github.com/nextflow-io/nextflow/issues/7161 + def 'should return empty list for missing path input (v1)' () { given: def session = Mock(Session) def script = Mock(BaseScript) @@ -372,6 +371,24 @@ class ProcessEntryHandlerTest extends Specification { result == [] } + def 'should throw error for missing val input (v1)' () { + given: + def session = Mock(Session) + def script = Mock(BaseScript) + def meta = Mock(ScriptMeta) { + getLocalProcessNames() >> [ 'hello' ] + } + def handler = new ProcessEntryHandler(script, session, meta) + def valParam = Mock(ValueInParam) { getName() >> 'id' } + + when: 'a val input is absent' + handler.getValueForInputV1(valParam, [:], [:]) + + then: + def e = thrown(IllegalArgumentException) + e.message == 'Missing required parameter: --id' + } + def 'should return null for missing optional input (v2)' () { // Regression test for https://github.com/nextflow-io/nextflow/issues/7161 given: @@ -381,11 +398,7 @@ class ProcessEntryHandlerTest extends Specification { getLocalProcessNames() >> [ 'hello' ] } def handler = new ProcessEntryHandler(script, session, meta) - def param = Mock(ProcessInput) { - getName() >> 'gzi' - getType() >> Path - isOptional() >> true - } + def param = new ProcessInput('gzi', Path, true) when: 'a v2 optional path input is not provided' def result = handler.getValueForInputV2(param, [:]) @@ -394,7 +407,7 @@ class ProcessEntryHandlerTest extends Specification { result == null } - def 'should throw for missing required input (v2)' () { + def 'should throw error for missing required input (v2)' () { given: def session = Mock(Session) def script = Mock(BaseScript) @@ -402,17 +415,13 @@ class ProcessEntryHandlerTest extends Specification { getLocalProcessNames() >> [ 'hello' ] } def handler = new ProcessEntryHandler(script, session, meta) - def param = Mock(ProcessInput) { - getName() >> 'reads' - getType() >> Path - isOptional() >> false - } + def param = new ProcessInput('reads', Path, false) when: handler.getValueForInputV2(param, [:]) then: def e = thrown(IllegalArgumentException) - e.message.contains('Missing required parameter: --reads') + e.message == 'Missing required parameter: --reads' } }