Skip to content

Commit 6d5a0bd

Browse files
pditommasoclaudechristopher-hakkaartrobsymebentsherman
authored
Implement Process Execution with Command-Line Parameter Mapping (#6381) [experimental]
Signed-off-by: Paolo Di Tommaso <[email protected]> Signed-off-by: Rob Syme <[email protected]> Signed-off-by: Ben Sherman <[email protected]> Co-authored-by: Claude <[email protected]> Co-authored-by: Chris Hakkaart <[email protected]> Co-authored-by: Robert Syme <[email protected]> Co-authored-by: Ben Sherman <[email protected]>
1 parent a5c19b8 commit 6d5a0bd

File tree

9 files changed

+875
-1
lines changed

9 files changed

+875
-1
lines changed

modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,15 @@ abstract class BaseScript extends Script implements ExecutionContext {
189189
if( !entryFlow ) {
190190
if( meta.getLocalWorkflowNames() )
191191
throw new AbortOperationException("No entry workflow specified")
192-
return result
192+
// Check if we have standalone processes that can be executed automatically
193+
if( meta.hasExecutableProcesses() ) {
194+
// Create a workflow to execute the process (single process or first of multiple)
195+
final handler = new ProcessEntryHandler(this, session, meta)
196+
entryFlow = handler.createAutoProcessWorkflow()
197+
}
198+
else {
199+
return result
200+
}
193201
}
194202

195203
// invoke the entry workflow

modules/nextflow/src/main/groovy/nextflow/script/ProcessEntryHandler.groovy

Lines changed: 379 additions & 0 deletions
Large diffs are not rendered by default.

modules/nextflow/src/main/groovy/nextflow/script/ScriptMeta.groovy

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,26 @@ class ScriptMeta {
301301
return result
302302
}
303303

304+
/**
305+
* Check if this script has standalone processes that can be executed
306+
* automatically without requiring workflows
307+
*
308+
* @return true if the script has one or more processes and no workflows
309+
*/
310+
boolean hasExecutableProcesses() {
311+
// Don't allow execution of true modules (those are meant for inclusion)
312+
if( isModule() )
313+
return false
314+
315+
// Must have at least one process
316+
final processNames = getLocalProcessNames()
317+
if( processNames.isEmpty() )
318+
return false
319+
320+
// Must not have any workflow definitions (including unnamed workflow)
321+
return getLocalWorkflowNames().isEmpty()
322+
}
323+
304324
void addModule(BaseScript script, String name, String alias) {
305325
addModule(get(script), name, alias)
306326
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Copyright 2013-2024, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package nextflow.script
18+
19+
import nextflow.Session
20+
import spock.lang.Specification
21+
22+
/**
23+
* Tests for ProcessEntryHandler parameter mapping functionality
24+
*
25+
* @author Paolo Di Tommaso <[email protected]>
26+
*/
27+
class ProcessEntryHandlerTest extends Specification {
28+
29+
def 'should parse complex parameters with dot notation' () {
30+
given:
31+
def session = Mock(Session)
32+
def script = Mock(BaseScript)
33+
def meta = Mock(ScriptMeta)
34+
def handler = new ProcessEntryHandler(script, session, meta)
35+
36+
when:
37+
def result = handler.parseComplexParameters([
38+
'meta.id': 'SAMPLE_001',
39+
'meta.name': 'TestSample',
40+
'meta.other': 'some-value',
41+
'fasta': '/path/to/file.fa'
42+
])
43+
44+
then:
45+
result.meta instanceof Map
46+
result.meta.id == 'SAMPLE_001'
47+
result.meta.name == 'TestSample'
48+
result.meta.other == 'some-value'
49+
result.fasta == '/path/to/file.fa'
50+
}
51+
52+
def 'should parse nested dot notation parameters' () {
53+
given:
54+
def session = Mock(Session)
55+
def script = Mock(BaseScript)
56+
def meta = Mock(ScriptMeta)
57+
def handler = new ProcessEntryHandler(script, session, meta)
58+
59+
when:
60+
def result = handler.parseComplexParameters([
61+
'meta.sample.id': '123',
62+
'meta.sample.name': 'test',
63+
'meta.config.quality': 'high',
64+
'output.dir': '/results'
65+
])
66+
67+
then:
68+
result.meta instanceof Map
69+
result.meta.sample instanceof Map
70+
result.meta.sample.id == '123'
71+
result.meta.sample.name == 'test'
72+
result.meta.config instanceof Map
73+
result.meta.config.quality == 'high'
74+
result.output instanceof Map
75+
result.output.dir == '/results'
76+
}
77+
78+
def 'should handle simple parameters without dots' () {
79+
given:
80+
def session = Mock(Session)
81+
def script = Mock(BaseScript)
82+
def meta = Mock(ScriptMeta)
83+
def handler = new ProcessEntryHandler(script, session, meta)
84+
85+
when:
86+
def result = handler.parseComplexParameters([
87+
'sampleId': 'SAMPLE_001',
88+
'threads': '4',
89+
'dataFile': '/path/to/data.txt'
90+
])
91+
92+
then:
93+
result.sampleId == 'SAMPLE_001'
94+
result.threads == '4'
95+
result.dataFile == '/path/to/data.txt'
96+
}
97+
98+
def 'should get value for val input type' () {
99+
given:
100+
def session = Mock(Session)
101+
def script = Mock(BaseScript)
102+
def meta = Mock(ScriptMeta)
103+
def handler = new ProcessEntryHandler(script, session, meta)
104+
105+
when:
106+
def complexParams = [
107+
'meta': [id: 'SAMPLE_001', name: 'TestSample'],
108+
'sampleId': 'SIMPLE_001'
109+
]
110+
def valInput = [type: 'val', name: 'meta']
111+
def simpleInput = [type: 'val', name: 'sampleId']
112+
113+
then:
114+
handler.getValueForInput(valInput, complexParams) == [id: 'SAMPLE_001', name: 'TestSample']
115+
handler.getValueForInput(simpleInput, complexParams) == 'SIMPLE_001'
116+
}
117+
118+
def 'should get value for path input type' () {
119+
given:
120+
def session = Mock(Session)
121+
def script = Mock(BaseScript)
122+
def meta = Mock(ScriptMeta)
123+
def handler = new ProcessEntryHandler(script, session, meta)
124+
125+
when:
126+
def complexParams = [
127+
'fasta': '/path/to/file.fa',
128+
'dataFile': 'data.txt'
129+
]
130+
def pathInput = [type: 'path', name: 'fasta']
131+
def fileInput = [type: 'file', name: 'dataFile']
132+
133+
then:
134+
def fastaResult = handler.getValueForInput(pathInput, complexParams)
135+
def fileResult = handler.getValueForInput(fileInput, complexParams)
136+
137+
// Should convert string paths to Path objects (mocked here)
138+
fastaResult.toString().contains('file.fa')
139+
fileResult.toString().contains('data.txt')
140+
}
141+
142+
def 'should throw exception for missing required parameter' () {
143+
given:
144+
def session = Mock(Session)
145+
def script = Mock(BaseScript)
146+
def meta = Mock(ScriptMeta)
147+
def handler = new ProcessEntryHandler(script, session, meta)
148+
149+
when:
150+
def complexParams = [
151+
'meta': [id: 'SAMPLE_001']
152+
]
153+
def missingInput = [type: 'val', name: 'missing']
154+
handler.getValueForInput(missingInput, complexParams)
155+
156+
then:
157+
thrown(IllegalArgumentException)
158+
}
159+
160+
def 'should map tuple input structure correctly' () {
161+
given:
162+
def session = Mock(Session) {
163+
getParams() >> [
164+
'meta.id': 'SAMPLE_001',
165+
'meta.name': 'TestSample',
166+
'meta.other': 'some-value',
167+
'fasta': '/path/to/file.fa'
168+
]
169+
}
170+
def script = Mock(BaseScript)
171+
def meta = Mock(ScriptMeta)
172+
def processDef = Mock(ProcessDef)
173+
def handler = new ProcessEntryHandler(script, session, meta)
174+
175+
when:
176+
// Mock input structures for tuple val(meta), path(fasta)
177+
def inputStructures = [
178+
[
179+
type: 'tuple',
180+
elements: [
181+
[type: 'val', name: 'meta'],
182+
[type: 'path', name: 'fasta']
183+
]
184+
]
185+
]
186+
187+
// Test the parameter mapping logic manually
188+
def complexParams = handler.parseComplexParameters(session.getParams())
189+
def tupleInput = inputStructures[0]
190+
def tupleElements = []
191+
192+
for( def element : tupleInput.elements ) {
193+
def value = handler.getValueForInput(element, complexParams)
194+
tupleElements.add(value)
195+
}
196+
197+
then:
198+
complexParams.meta instanceof Map
199+
complexParams.meta.id == 'SAMPLE_001'
200+
complexParams.meta.name == 'TestSample'
201+
complexParams.meta.other == 'some-value'
202+
complexParams.fasta == '/path/to/file.fa'
203+
204+
tupleElements.size() == 2
205+
tupleElements[0] instanceof Map // meta as map
206+
tupleElements[0].id == 'SAMPLE_001'
207+
tupleElements[0].name == 'TestSample'
208+
tupleElements[0].other == 'some-value'
209+
// tupleElements[1] should be a Path object (mocked)
210+
tupleElements[1].toString().contains('file.fa')
211+
}
212+
}

0 commit comments

Comments
 (0)