diff --git a/modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy b/modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy index f916cfd8bf..1269bbad30 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy @@ -189,7 +189,15 @@ abstract class BaseScript extends Script implements ExecutionContext { if( !entryFlow ) { if( meta.getLocalWorkflowNames() ) throw new AbortOperationException("No entry workflow specified") - return result + // Check if we have standalone processes that can be executed automatically + if( meta.hasExecutableProcesses() ) { + // Create a workflow to execute the process (single process or first of multiple) + final handler = new ProcessEntryHandler(this, session, meta) + entryFlow = handler.createAutoProcessWorkflow() + } + else { + return result + } } // invoke the entry workflow diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ProcessEntryHandler.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ProcessEntryHandler.groovy new file mode 100644 index 0000000000..ee73f1dfd5 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/script/ProcessEntryHandler.groovy @@ -0,0 +1,379 @@ +/* + * Copyright 2013-2024, 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 + +import groovy.util.logging.Slf4j +import nextflow.Session +import nextflow.Nextflow + +/** + * Helper class for process entry execution feature. + * + * This feature enables direct execution of Nextflow processes without explicit workflows: + * - Single process scripts run automatically: `nextflow run script.nf --param value` + * - Multi-process scripts run the first process automatically: `nextflow run script.nf --param value` + * - Command-line parameters are mapped directly to process inputs + * - Supports all standard Nextflow input types: val, path, env, tuple, each + * + * @author Paolo Di Tommaso + */ +@Slf4j +class ProcessEntryHandler { + + private final BaseScript script + private final Session session + private final ScriptMeta meta + + ProcessEntryHandler(BaseScript script, Session session, ScriptMeta meta) { + this.script = script + this.session = session + this.meta = meta + } + + /** + * Creates a workflow to execute a standalone process automatically. + * For single process scripts, executes that process. + * For multi-process scripts, executes the first process. + * + * @return WorkflowDef that executes the process with parameter mapping + */ + WorkflowDef createAutoProcessWorkflow() { + def processNames = meta.getLocalProcessNames() + if( processNames.isEmpty() ) { + throw new IllegalStateException("No processes found for auto-execution") + } + + // Always pick the first process (whether single or multiple processes) + final processName = processNames.first() + final processDef = meta.getProcess(processName) + + return createProcessWorkflow(processDef) + } + + /** + * Creates a workflow to execute the specified process with automatic parameter mapping. + */ + private WorkflowDef createProcessWorkflow(ProcessDef processDef) { + final processName = processDef.name + + // Create a simple workflow body that just executes the process + def workflowBodyClosure = { -> + // Create the workflow execution logic + def workflowExecutionClosure = { -> + // Get input parameter values and execute the process + def inputArgs = getProcessInputArguments(processDef) + def processResult = script.invokeMethod(processName, inputArgs as Object[]) + + return processResult + } + + // Create the body definition with execution logic + def sourceCode = " // Auto-generated process workflow\n ${processName}(...)" + return new BodyDef(workflowExecutionClosure, sourceCode, 'workflow') + } + + // Create a simple workflow definition without complex emission logic + return new WorkflowDef(script, workflowBodyClosure) + } + + /** + * Gets the input arguments for a process by parsing input parameter structures + * and mapping them from session.params, supporting dot notation for complex inputs. + * + * @param processDef The ProcessDef object containing the process definition + * @return List of parameter values to pass to the process + */ + private List getProcessInputArguments(ProcessDef processDef) { + try { + log.debug "Getting input arguments for process: ${processDef.name}" + log.debug "Session params: ${session.params}" + + def inputStructures = parseProcessInputStructures(processDef) + log.debug "Parsed input structures: ${inputStructures}" + + if( inputStructures.isEmpty() ) { + log.debug "No input structures found, returning empty list" + return [] + } + + // Parse complex parameters from session.params (handles dot notation) + def complexParams = parseComplexParameters(session.params) + log.debug "Complex parameters: ${complexParams}" + + // Map input structures to actual values + List inputArgs = [] + for( def inputDef : inputStructures ) { + log.debug "Processing input definition: ${inputDef}" + if( inputDef.type == 'tuple' ) { + // Handle tuple inputs - construct list with proper elements + List tupleElements = [] + for( def element : inputDef.elements ) { + log.debug "Getting value for tuple element: ${element}" + def value = getValueForInput(element, complexParams) + tupleElements.add(value) + } + log.debug "Constructed tuple: ${tupleElements}" + inputArgs.add(tupleElements) + } else { + // Handle simple inputs + def value = getValueForInput(inputDef, complexParams) + log.debug "Got simple input value: ${value}" + inputArgs.add(value) + } + } + + log.debug "Final input arguments: ${inputArgs}" + return inputArgs + + } catch (Exception e) { + log.error "Failed to get input arguments for process ${processDef.name}: ${e.message}" + throw e + } + } + + /** + * Parses the process body to extract input parameter structures by intercepting + * Nextflow's internal compiled method calls (_in_val, _in_path, _in_tuple, etc.). + * + * @param processDef The ProcessDef containing the raw process body + * @return List of input structures with type and name information + */ + private List parseProcessInputStructures(ProcessDef processDef) { + def inputStructures = [] + + // Create delegate to capture Nextflow's internal input method calls + def delegate = new Object() { + def _in_val(tokenVar) { + def varName = extractVariableName(tokenVar) + if( varName ) inputStructures.add([type: 'val', name: varName]) + } + def _in_path(tokenVar) { + def varName = extractVariableName(tokenVar) + if( varName ) inputStructures.add([type: 'path', name: varName]) + } + def _in_file(tokenVar) { + def varName = extractVariableName(tokenVar) + if( varName ) inputStructures.add([type: 'file', name: varName]) + } + def _in_env(tokenVar) { + def varName = extractVariableName(tokenVar) + if( varName ) inputStructures.add([type: 'env', name: varName]) + } + def _in_each(tokenVar) { + def varName = extractVariableName(tokenVar) + if( varName ) inputStructures.add([type: 'each', name: varName]) + } + + def extractVariableName(token) { + if( token?.hasProperty('name') ) { + return token.name.toString() + } else { + // Try to extract from string representation + def match = token.toString() =~ /TokenVar\(([^)]+)\)/ + return match ? match[0][1] : null + } + } + + def _in_tuple(Object... items) { + def tupleElements = [] + for( item in items ) { + log.debug "Processing tuple item: ${item} of class ${item?.getClass()?.getSimpleName()}" + + def itemType = 'val' // default + def itemName = null + + // Handle different token call types by checking class name + def className = item.getClass().getSimpleName() + if( className == 'TokenValCall' ) { + itemType = 'val' + itemName = extractVariableNameFromToken(item) + } else if( className == 'TokenPathCall' || className == 'TokenFileCall' ) { + itemType = 'path' + itemName = extractVariableNameFromToken(item) + } else if( className == 'TokenEnvCall' ) { + itemType = 'env' + itemName = extractVariableNameFromToken(item) + } else if( className == 'TokenEachCall' ) { + itemType = 'each' + itemName = extractVariableNameFromToken(item) + } else { + // Fallback: try to extract from string representation + if( item.toString().contains('TokenValCall') ) { + itemType = 'val' + def tokenVar = item.toString().find(/TokenVar\(([^)]+)\)/) { match, varName -> varName } + itemName = tokenVar + } + } + + if( itemName ) { + log.debug "Parsed tuple element: ${itemName} (${itemType})" + tupleElements.add([type: itemType, name: itemName]) + } else { + log.warn "Could not parse tuple element: ${item} of class ${className}" + } + } + log.debug "Parsed tuple with ${tupleElements.size()} elements: ${tupleElements}" + inputStructures.add([type: 'tuple', elements: tupleElements]) + } + + def extractVariableNameFromToken(token) { + // Try to access the variable property directly + try { + if( token.hasProperty('variable') && token.variable?.hasProperty('name') ) { + return token.variable.name.toString() + } + if( token.hasProperty('target') && token.target?.hasProperty('name') ) { + return token.target.name.toString() + } + if( token.hasProperty('name') ) { + return token.name.toString() + } + // Fallback to string parsing + def match = token.toString() =~ /TokenVar\(([^)]+)\)/ + return match ? match[0][1] : null + } catch( Exception e ) { + log.debug "Error extracting variable name from ${token}: ${e.message}" + return null + } + } + + // Handle legacy input block syntax for backward compatibility + def input(Closure inputBody) { + def inputDelegate = new Object() { + def val(name) { + inputStructures.add([type: 'val', name: name.toString()]) + } + def path(name) { + inputStructures.add([type: 'path', name: name.toString()]) + } + def file(name) { + inputStructures.add([type: 'file', name: name.toString()]) + } + def env(name) { + inputStructures.add([type: 'env', name: name.toString()]) + } + def each(name) { + inputStructures.add([type: 'each', name: name.toString()]) + } + def tuple(Object... items) { + def tupleElements = [] + for( item in items ) { + if( item instanceof String || (item instanceof groovy.lang.GString) ) { + tupleElements.add([type: 'val', name: item.toString()]) + } + } + inputStructures.add([type: 'tuple', elements: tupleElements]) + } + def methodMissing(String name, args) { + for( arg in args ) { + if( arg instanceof String || (arg instanceof groovy.lang.GString) ) { + inputStructures.add([type: name, name: arg.toString()]) + } + } + } + } + inputBody.delegate = inputDelegate + inputBody.resolveStrategy = Closure.DELEGATE_FIRST + inputBody.call() + } + + // Ignore all other method calls during parsing + def methodMissing(String name, args) { /* ignore */ } + } + + // Execute the process body with our capturing delegate + def bodyClone = processDef.rawBody.clone() + bodyClone.delegate = delegate + bodyClone.resolveStrategy = Closure.DELEGATE_FIRST + + try { + bodyClone.call() + } catch (Exception e) { + // Ignore exceptions during parsing - we only want to capture input structures + } + + return inputStructures + } + + /** + * Parses complex parameters with dot notation support. + * Converts flat parameters like --meta.id=1 --meta.name=test to nested maps. + * + * @param params Flat parameter map from session.params + * @return Map with nested structures for complex parameters + */ + private Map parseComplexParameters(Map params) { + Map complexParams = [:] + + params.each { key, value -> + def parts = key.toString().split('\\.') + if( parts.length > 1 ) { + // Handle dot notation - build nested map + def current = complexParams + for( int i = 0; i < parts.length - 1; i++ ) { + if( !current.containsKey(parts[i]) ) { + current[parts[i]] = [:] + } + current = current[parts[i]] + } + current[parts[-1]] = value + } else { + // Simple parameter + complexParams[key] = value + } + } + + return complexParams + } + + /** + * Gets the appropriate value for an input definition, handling type conversion. + * + * @param inputDef Input definition with type and name + * @param complexParams Parsed parameter map with nested structures + * @return Properly typed value for the input + */ + private Object getValueForInput(Map inputDef, Map complexParams) { + def paramName = inputDef.name + def paramType = inputDef.type + def paramValue = complexParams.get(paramName) + + if( paramValue == null ) { + throw new IllegalArgumentException("Missing required parameter: --${paramName}") + } + + // Type-specific conversion + switch( paramType ) { + case 'path': + case 'file': + if( paramValue instanceof String ) { + return Nextflow.file(paramValue) + } + return paramValue + + case 'val': + // For val inputs, return as-is (could be Map for complex structures) + return paramValue + + case 'env': + return paramValue?.toString() + + default: + return paramValue + } + } +} \ No newline at end of file diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ScriptMeta.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ScriptMeta.groovy index 47f67f6bd7..77e51dae26 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/ScriptMeta.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/ScriptMeta.groovy @@ -301,6 +301,26 @@ class ScriptMeta { return result } + /** + * Check if this script has standalone processes that can be executed + * automatically without requiring workflows + * + * @return true if the script has one or more processes and no workflows + */ + boolean hasExecutableProcesses() { + // Don't allow execution of true modules (those are meant for inclusion) + if( isModule() ) + return false + + // Must have at least one process + final processNames = getLocalProcessNames() + if( processNames.isEmpty() ) + return false + + // Must not have any workflow definitions (including unnamed workflow) + return getLocalWorkflowNames().isEmpty() + } + void addModule(BaseScript script, String name, String alias) { addModule(get(script), name, alias) } diff --git a/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy new file mode 100644 index 0000000000..d51255baa0 --- /dev/null +++ b/modules/nextflow/src/test/groovy/nextflow/script/ProcessEntryHandlerTest.groovy @@ -0,0 +1,212 @@ +/* + * Copyright 2013-2024, 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 + +import nextflow.Session +import spock.lang.Specification + +/** + * Tests for ProcessEntryHandler parameter mapping functionality + * + * @author Paolo Di Tommaso + */ +class ProcessEntryHandlerTest extends Specification { + + def 'should parse complex parameters with dot notation' () { + given: + def session = Mock(Session) + def script = Mock(BaseScript) + def meta = Mock(ScriptMeta) + def handler = new ProcessEntryHandler(script, session, meta) + + when: + def result = handler.parseComplexParameters([ + 'meta.id': 'SAMPLE_001', + 'meta.name': 'TestSample', + 'meta.other': 'some-value', + 'fasta': '/path/to/file.fa' + ]) + + then: + result.meta instanceof Map + result.meta.id == 'SAMPLE_001' + result.meta.name == 'TestSample' + result.meta.other == 'some-value' + result.fasta == '/path/to/file.fa' + } + + def 'should parse nested dot notation parameters' () { + given: + def session = Mock(Session) + def script = Mock(BaseScript) + def meta = Mock(ScriptMeta) + def handler = new ProcessEntryHandler(script, session, meta) + + when: + def result = handler.parseComplexParameters([ + 'meta.sample.id': '123', + 'meta.sample.name': 'test', + 'meta.config.quality': 'high', + 'output.dir': '/results' + ]) + + then: + result.meta instanceof Map + result.meta.sample instanceof Map + result.meta.sample.id == '123' + result.meta.sample.name == 'test' + result.meta.config instanceof Map + result.meta.config.quality == 'high' + result.output instanceof Map + result.output.dir == '/results' + } + + def 'should handle simple parameters without dots' () { + given: + def session = Mock(Session) + def script = Mock(BaseScript) + def meta = Mock(ScriptMeta) + def handler = new ProcessEntryHandler(script, session, meta) + + when: + def result = handler.parseComplexParameters([ + 'sampleId': 'SAMPLE_001', + 'threads': '4', + 'dataFile': '/path/to/data.txt' + ]) + + then: + result.sampleId == 'SAMPLE_001' + result.threads == '4' + result.dataFile == '/path/to/data.txt' + } + + def 'should get value for val input type' () { + given: + def session = Mock(Session) + def script = Mock(BaseScript) + def meta = Mock(ScriptMeta) + def handler = new ProcessEntryHandler(script, session, meta) + + when: + def complexParams = [ + 'meta': [id: 'SAMPLE_001', name: 'TestSample'], + 'sampleId': 'SIMPLE_001' + ] + def valInput = [type: 'val', name: 'meta'] + def simpleInput = [type: 'val', name: 'sampleId'] + + then: + handler.getValueForInput(valInput, complexParams) == [id: 'SAMPLE_001', name: 'TestSample'] + handler.getValueForInput(simpleInput, complexParams) == 'SIMPLE_001' + } + + def 'should get value for path input type' () { + given: + def session = Mock(Session) + def script = Mock(BaseScript) + def meta = Mock(ScriptMeta) + def handler = new ProcessEntryHandler(script, session, meta) + + when: + def complexParams = [ + 'fasta': '/path/to/file.fa', + 'dataFile': 'data.txt' + ] + def pathInput = [type: 'path', name: 'fasta'] + def fileInput = [type: 'file', name: 'dataFile'] + + then: + def fastaResult = handler.getValueForInput(pathInput, complexParams) + def fileResult = handler.getValueForInput(fileInput, complexParams) + + // Should convert string paths to Path objects (mocked here) + fastaResult.toString().contains('file.fa') + fileResult.toString().contains('data.txt') + } + + def 'should throw exception for missing required parameter' () { + given: + def session = Mock(Session) + def script = Mock(BaseScript) + def meta = Mock(ScriptMeta) + def handler = new ProcessEntryHandler(script, session, meta) + + when: + def complexParams = [ + 'meta': [id: 'SAMPLE_001'] + ] + def missingInput = [type: 'val', name: 'missing'] + handler.getValueForInput(missingInput, complexParams) + + then: + thrown(IllegalArgumentException) + } + + def 'should map tuple input structure correctly' () { + given: + def session = Mock(Session) { + getParams() >> [ + 'meta.id': 'SAMPLE_001', + 'meta.name': 'TestSample', + 'meta.other': 'some-value', + 'fasta': '/path/to/file.fa' + ] + } + def script = Mock(BaseScript) + def meta = Mock(ScriptMeta) + def processDef = Mock(ProcessDef) + def handler = new ProcessEntryHandler(script, session, meta) + + when: + // Mock input structures for tuple val(meta), path(fasta) + def inputStructures = [ + [ + type: 'tuple', + elements: [ + [type: 'val', name: 'meta'], + [type: 'path', name: 'fasta'] + ] + ] + ] + + // Test the parameter mapping logic manually + def complexParams = handler.parseComplexParameters(session.getParams()) + def tupleInput = inputStructures[0] + def tupleElements = [] + + for( def element : tupleInput.elements ) { + def value = handler.getValueForInput(element, complexParams) + tupleElements.add(value) + } + + then: + complexParams.meta instanceof Map + complexParams.meta.id == 'SAMPLE_001' + complexParams.meta.name == 'TestSample' + complexParams.meta.other == 'some-value' + complexParams.fasta == '/path/to/file.fa' + + tupleElements.size() == 2 + tupleElements[0] instanceof Map // meta as map + tupleElements[0].id == 'SAMPLE_001' + tupleElements[0].name == 'TestSample' + tupleElements[0].other == 'some-value' + // tupleElements[1] should be a Path object (mocked) + tupleElements[1].toString().contains('file.fa') + } +} \ No newline at end of file diff --git a/modules/nextflow/src/test/groovy/nextflow/script/ScriptProcessRunTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/ScriptProcessRunTest.groovy new file mode 100644 index 0000000000..856b096e98 --- /dev/null +++ b/modules/nextflow/src/test/groovy/nextflow/script/ScriptProcessRunTest.groovy @@ -0,0 +1,167 @@ +package nextflow.script + +import java.nio.file.Files + +import test.Dsl2Spec +import test.MockScriptRunner + +/** + * Tests for single process execution feature that allows running processes + * directly without explicit workflows. + * + * @author Paolo Di Tommaso + */ +class ScriptProcessRunTest extends Dsl2Spec { + + def 'should execute single process with val input' () { + given: + def SCRIPT = ''' + params.sampleId = 'SAMPLE_001' + + process testProcess { + input: val sampleId + output: val result + exec: + result = "Processed: $sampleId" + } + ''' + + when: + def runner = new MockScriptRunner() + def result = runner.setScript(SCRIPT).execute() + + then: + // For single process execution, the result should contain the process output + result != null + println "Result: $result" + println "Result class: ${result?.getClass()}" + } + + def 'should execute single process with path input' () { + given: + def tempFile = Files.createTempFile('test', '.txt') + def SCRIPT = """ + params.dataFile = '${tempFile}' + + process testProcess { + input: path dataFile + output: val result + exec: + result = "File: \${dataFile.name}" + } + """ + + when: + def runner = new MockScriptRunner() + def result = runner.setScript(SCRIPT).execute() + + then: + result != null + println "Path result: $result" + println "Path result class: ${result?.getClass()}" + + cleanup: + Files.deleteIfExists(tempFile) + } + + def 'should execute single process with tuple input' () { + given: + def SCRIPT = ''' + params.'meta.id' = 'SAMPLE_001' + params.'meta.name' = 'test' + params.threads = 4 + + process testProcess { + input: tuple val(meta), val(threads) + output: val result + exec: + result = "Sample: ${meta.id}, Threads: $threads" + } + ''' + + when: + def runner = new MockScriptRunner() + def result = runner.setScript(SCRIPT).execute() + + then: + result != null + println "Tuple result: $result" + println "Tuple result class: ${result?.getClass()}" + } + + def 'should handle multiple processes by running the first one' () { + given: + def SCRIPT = ''' + params.input = 'test' + + process firstProcess { + input: val input + output: val result + exec: + result = "First: $input" + } + + process secondProcess { + input: val input + output: val result + exec: + result = "Second: $input" + } + ''' + + when: + def runner = new MockScriptRunner() + def result = runner.setScript(SCRIPT).execute() + + then: + result != null + println "Multi-process result: $result" + println "Multi-process result class: ${result?.getClass()}" + } + + def 'should fail when required parameter is missing' () { + given: + def SCRIPT = ''' + process testProcess { + input: val requiredParam + output: val result + exec: + result = "Got: $requiredParam" + } + ''' + + when: + def runner = new MockScriptRunner() + runner.setScript(SCRIPT).execute() + + then: + def e = thrown(Exception) + e.message.contains('Missing required parameter: --requiredParam') + } + + def 'should handle complex parameter mapping' () { + given: + def SCRIPT = ''' + params.'sample.id' = 'S001' + params.'sample.name' = 'TestSample' + params.'config.threads' = 8 + + process complexProcess { + input: val sample + input: val config + output: val result + exec: + result = "Sample: ${sample.id}, Config: ${config.threads}" + } + ''' + + when: + def runner = new MockScriptRunner() + def result = runner.setScript(SCRIPT).execute() + + then: + result != null + println "Complex result: $result" + println "Complex result class: ${result?.getClass()}" + } +} diff --git a/tests/checks/process-entry.nf/.checks b/tests/checks/process-entry.nf/.checks new file mode 100755 index 0000000000..449c125deb --- /dev/null +++ b/tests/checks/process-entry.nf/.checks @@ -0,0 +1,34 @@ +set -e + +# +# Test single process auto-execution with parameter mapping +# +echo '' +echo '=== Testing single process auto-execution ===' +$NXF_RUN --sampleId "SAMPLE_001" --dataFile "../../data/sample_data.txt" --threads "4" | tee stdout + +# Check that the process executed successfully +[[ `grep 'INFO' .nextflow.log | grep -c 'Submitted process > analyzeData'` == 1 ]] || false + +# Check that all parameters were properly mapped and used +[[ `grep -c 'Single process execution test' stdout` == 1 ]] || false +[[ `grep -c 'Sample ID: SAMPLE_001' stdout` == 1 ]] || false +[[ `grep -c 'Data file: sample_data.txt' stdout` == 1 ]] || false +[[ `grep -c 'Threads: 4' stdout` == 1 ]] || false +[[ `grep -c 'Processing file with 4 threads' stdout` == 1 ]] || false +[[ `grep -c 'Analysis completed for sample SAMPLE_001' stdout` == 1 ]] || false + +# Check that file processing worked (5 lines in sample_data.txt) +[[ `grep -c '5 sample_data.txt' stdout` == 1 ]] || false + +# +# Test with relative path +# +echo '' +echo '=== Testing single process with relative path ===' +rm -f .nextflow.log # Clear log for clean test +$NXF_RUN --sampleId "REL_TEST" --dataFile "../../data/sample_data.txt" --threads "2" | tee stdout2 + +[[ `grep 'INFO' .nextflow.log | grep -c 'Submitted process > analyzeData'` == 1 ]] || false +[[ `grep -c 'Sample ID: REL_TEST' stdout2` == 1 ]] || false +[[ `grep -c 'Threads: 2' stdout2` == 1 ]] || false \ No newline at end of file diff --git a/tests/data/results_data.txt b/tests/data/results_data.txt new file mode 100644 index 0000000000..a259ca69a2 --- /dev/null +++ b/tests/data/results_data.txt @@ -0,0 +1,6 @@ +Results header +Data point 1: value A +Data point 2: value B +Data point 3: value C +Summary: 3 data points processed +Final result: SUCCESS \ No newline at end of file diff --git a/tests/data/sample_data.txt b/tests/data/sample_data.txt new file mode 100644 index 0000000000..e35881eb83 --- /dev/null +++ b/tests/data/sample_data.txt @@ -0,0 +1,5 @@ +Line 1: Sample data for process entry testing +Line 2: This file contains test data +Line 3: Used for validating path parameter handling +Line 4: Multiple lines to test file processing +Line 5: End of sample data diff --git a/tests/process-entry.nf b/tests/process-entry.nf new file mode 100644 index 0000000000..d22fdc1bf6 --- /dev/null +++ b/tests/process-entry.nf @@ -0,0 +1,43 @@ +#!/usr/bin/env nextflow +/* + * Copyright 2013-2024, 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. + */ + +// Test single process auto-execution with parameter mapping +process analyzeData { + debug true + + input: + val sampleId + path dataFile + val threads + + script: + """ + echo "Single process execution test" + echo "Sample ID: ${sampleId}" + echo "Data file: ${dataFile}" + echo "Threads: ${threads}" + + if [ -f "${dataFile}" ]; then + echo "Processing file with ${threads} threads..." + wc -l "${dataFile}" + else + echo "Warning: File ${dataFile} not found" + fi + + echo "Analysis completed for sample ${sampleId}" + """ +} \ No newline at end of file