diff --git a/example/data-driven-example.nf.test b/example/data-driven-example.nf.test new file mode 100644 index 00000000..e264a735 --- /dev/null +++ b/example/data-driven-example.nf.test @@ -0,0 +1,111 @@ +nextflow_process { + + name "Test process SAY_HELLO with Spock-style data-driven testing" + script "example/say-hello.nf" + process "SAY_HELLO" + + // Spock-style data table testing + testEach("Test with name=#name and number=#number") { + when { + process { + """ + input[0] = "$name" + input[1] = Channel.of($number) + """ + } + } + + then { + assert process.success + assert process.out.my_tuples.size() == expectedSize + assert process.out.my_tuples[0][0] == number + assert process.out.my_tuples[0][1] == name + } + + where(""" + name | number | expectedSize + "alice" | 1 | 1 + "bob" | 2 | 1 + "charlie" | 3 | 1 + """) + } + + // Spock-style data pipes testing + testEach("Data pipes test #name-#number") { + when { + process { + """ + input[0] = "$name" + input[1] = Channel.of($number) + """ + } + } + + then { + if (shouldFail) { + assert process.failed + } else { + assert process.success + assert process.out.my_tuples[0][1] == name + } + } + + where(""" + name << ["", "valid-name", "special!@#", "normal"] + number << [1, 2, 3, 4] + shouldFail << [true, false, true, false] + """) + } + + // Mixed data pipes and assignments + testEach("Complex test #name with #result") { + when { + process { + """ + input[0] = "$name" + input[1] = Channel.of($number) + """ + } + } + + then { + assert process.success + assert process.out.my_tuples[0][0] == number + assert result.contains(name) + } + + where(""" + name << ["test1", "test2", "test3"] + number << [10, 20, 30] + result = name + "-" + number + """) + } + + // Alternative: Data table with || separator (like Spock) + testEach("Edge cases #input || #expected") { + when { + process { + """ + input[0] = "$input" + input[1] = Channel.of(1) + """ + } + } + + then { + if (expected) { + assert process.success + } else { + assert process.failed + } + } + + where(""" + input || expected + "valid" || true + "" || false + "special!@#" || false + "normal-name" || true + """) + } +} \ No newline at end of file diff --git a/example/data-table-test.nf.test b/example/data-table-test.nf.test new file mode 100644 index 00000000..1aa718d8 --- /dev/null +++ b/example/data-table-test.nf.test @@ -0,0 +1,31 @@ +nextflow_process { + + name "Data table syntax test" + script "example/say-hello.nf" + process "SAY_HELLO" + + // Test the pipe-separated data table syntax + testEach("Table test with #name and #number") { + when { + process { + """ + input[0] = "$name" + input[1] = Channel.of($number) + """ + } + } + + then { + assert process.success + assert process.out.my_tuples[0][1] == name + assert process.out.my_tuples[0][0] == number + } + + where(""" + name | number + "alice" | 1 + "bob" | 2 + "charlie" | 3 + """) + } +} \ No newline at end of file diff --git a/example/double-pipe-test.nf.test b/example/double-pipe-test.nf.test new file mode 100644 index 00000000..a00fc7d7 --- /dev/null +++ b/example/double-pipe-test.nf.test @@ -0,0 +1,35 @@ +nextflow_process { + + name "Double pipe syntax test" + script "example/say-hello.nf" + process "SAY_HELLO" + + // Test the double pipe separator syntax (input || expected) + testEach("Test #name || should be #expected") { + when { + process { + """ + input[0] = "$name" + input[1] = Channel.of(1) + """ + } + } + + then { + if (expected) { + assert process.success + assert process.out.my_tuples[0][1] == name + } else { + // For this test, all should succeed, but this shows the pattern + assert process.success + } + } + + where(""" + name || expected + "alice" || true + "bob" || true + "charlie" || true + """) + } +} \ No newline at end of file diff --git a/example/mixed-syntax-test.nf.test b/example/mixed-syntax-test.nf.test new file mode 100644 index 00000000..fa914a0e --- /dev/null +++ b/example/mixed-syntax-test.nf.test @@ -0,0 +1,31 @@ +nextflow_process { + + name "Mixed syntax test" + script "example/say-hello.nf" + process "SAY_HELLO" + + // Test mixed data pipes and computed assignments + testEach("Test #name with result=#result") { + when { + process { + """ + input[0] = "$name" + input[1] = Channel.of($number) + """ + } + } + + then { + assert process.success + assert process.out.my_tuples[0][1] == name + assert process.out.my_tuples[0][0] == number + assert result == name + "-" + number + } + + where(""" + name << ["test1", "test2", "test3"] + number << [10, 20, 30] + result = name + "-" + number + """) + } +} \ No newline at end of file diff --git a/example/simple-data-driven.nf.test b/example/simple-data-driven.nf.test new file mode 100644 index 00000000..4ff61357 --- /dev/null +++ b/example/simple-data-driven.nf.test @@ -0,0 +1,27 @@ +nextflow_process { + + name "Simple data-driven test" + script "example/say-hello.nf" + process "SAY_HELLO" + + // Basic data table test + testEach("Simple test with #name") { + when { + process { + """ + input[0] = "$name" + input[1] = Channel.of(1) + """ + } + } + + then { + assert process.success + assert process.out.my_tuples[0][1] == name + } + + where(""" + name << ["alice", "bob"] + """) + } +} \ No newline at end of file diff --git a/src/main/java/com/askimed/nf/test/core/AbstractTest.java b/src/main/java/com/askimed/nf/test/core/AbstractTest.java index 39159a4e..b4d3db05 100644 --- a/src/main/java/com/askimed/nf/test/core/AbstractTest.java +++ b/src/main/java/com/askimed/nf/test/core/AbstractTest.java @@ -8,6 +8,11 @@ import com.askimed.nf.test.util.HashUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; import com.askimed.nf.test.config.Config; import com.askimed.nf.test.util.FileUtil; @@ -63,6 +68,9 @@ public abstract class AbstractTest implements ITest { private boolean updateSnapshot = false; private boolean ciMode = false; + private Map parameters = new HashMap(); + + public AbstractTest() { private boolean debug = false; @@ -261,4 +269,12 @@ public String toString() { return getHash().substring(0, 8) + ": " + getName(); } + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + public Map getParameters() { + return parameters; + } + } diff --git a/src/main/java/com/askimed/nf/test/core/ITest.java b/src/main/java/com/askimed/nf/test/core/ITest.java index 0bbeab6e..1e8c624c 100644 --- a/src/main/java/com/askimed/nf/test/core/ITest.java +++ b/src/main/java/com/askimed/nf/test/core/ITest.java @@ -1,6 +1,7 @@ package com.askimed.nf.test.core; import java.io.File; +import java.util.Map; import com.askimed.nf.test.config.Config; @@ -38,4 +39,8 @@ public interface ITest extends ITaggable { public boolean isCIMode(); + public void setParameters(Map parameters); + + public Map getParameters(); + } diff --git a/src/main/java/com/askimed/nf/test/lang/DataDrivenTest.java b/src/main/java/com/askimed/nf/test/lang/DataDrivenTest.java new file mode 100644 index 00000000..bf0db226 --- /dev/null +++ b/src/main/java/com/askimed/nf/test/lang/DataDrivenTest.java @@ -0,0 +1,81 @@ +package com.askimed.nf.test.lang; + +import com.askimed.nf.test.core.ITestSuite; +import com.askimed.nf.test.lang.DataTableParser.DataTable; + +import groovy.lang.Closure; + +public class DataDrivenTest { + + private ITestSuite testSuite; + private DataTable dataTable; + private Closure setupClosure; + private Closure whenClosure; + private Closure thenClosure; + private Closure cleanupClosure; + + public DataDrivenTest(ITestSuite testSuite) { + this.testSuite = testSuite; + } + + public void setup(Closure closure) { + this.setupClosure = closure; + } + + public void when(Closure closure) { + this.whenClosure = closure; + } + + public void then(Closure closure) { + this.thenClosure = closure; + } + + public void cleanup(Closure closure) { + this.cleanupClosure = closure; + } + + /** + * Parse the where block using Spock-style syntax supporting: + * 1. Data tables: a | b || c + * 2. Data pipes: a << [1,2,3] + * 3. Variable assignments: c = a + b + */ + public void where(String whereBlockText) { + this.dataTable = DataTableParser.parseWhereBlock(whereBlockText); + } + + /** + * Alternative where method that accepts a closure for Groovy DSL style + * This would capture the closure content and parse it + */ + public void where(Closure closure) { + // TODO: Implement closure-based where blocks + // This would require more sophisticated AST parsing of the closure + throw new UnsupportedOperationException("Closure-based where blocks not yet implemented. Please use string-based where blocks."); + } + + // Getters + public DataTable getDataTable() { + return dataTable; + } + + public Closure getSetupClosure() { + return setupClosure; + } + + public Closure getWhenClosure() { + return whenClosure; + } + + public Closure getThenClosure() { + return thenClosure; + } + + public Closure getCleanupClosure() { + return cleanupClosure; + } + + public ITestSuite getTestSuite() { + return testSuite; + } +} \ No newline at end of file diff --git a/src/main/java/com/askimed/nf/test/lang/DataTableParser.java b/src/main/java/com/askimed/nf/test/lang/DataTableParser.java new file mode 100644 index 00000000..a1c4bf1c --- /dev/null +++ b/src/main/java/com/askimed/nf/test/lang/DataTableParser.java @@ -0,0 +1,328 @@ +package com.askimed.nf.test.lang; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DataTableParser { + + private static final Pattern PIPE_SEPARATOR = Pattern.compile("\\s*\\|{1,2}\\s*"); + private static final Pattern DATA_PIPE = Pattern.compile("(\\w+)\\s*<<\\s*(.+)"); + private static final Pattern VARIABLE_ASSIGNMENT = Pattern.compile("(\\w+)\\s*=\\s*(.+)"); + + public static class DataTable { + private List parameterNames; + private List> rows; + + public DataTable(List parameterNames, List> rows) { + this.parameterNames = parameterNames; + this.rows = rows; + } + + public List getParameterNames() { + return parameterNames; + } + + public List> getRows() { + return rows; + } + + public int size() { + return rows.size(); + } + } + + /** + * Parse a Spock-style where block that can contain: + * 1. Data tables (a | b || c) + * 2. Data pipes (a << [1,2,3]) + * 3. Variable assignments (c = a + b) + */ + public static DataTable parseWhereBlock(String whereBlockText) { + if (whereBlockText == null || whereBlockText.trim().isEmpty()) { + throw new IllegalArgumentException("Where block text cannot be empty"); + } + + String[] lines = whereBlockText.trim().split("\\n"); + + // Check if this is a data table (contains | characters) + if (containsDataTable(lines)) { + return parseDataTable(lines); + } + + // Otherwise, parse as data pipes and variable assignments + return parseDataPipesAndAssignments(lines); + } + + private static boolean containsDataTable(String[] lines) { + for (String line : lines) { + if (line.trim().contains("|")) { + return true; + } + } + return false; + } + + /** + * Parse traditional data table format: + * name | number | expected + * "alice" | 1 | true + * "bob" | 2 | false + */ + private static DataTable parseDataTable(String[] lines) { + if (lines.length < 2) { + throw new IllegalArgumentException("Data table must have at least a header row and one data row"); + } + + // Parse header row to get parameter names + String headerLine = lines[0].trim(); + String[] headers = PIPE_SEPARATOR.split(headerLine); + List parameterNames = new ArrayList(); + for (String header : headers) { + String trimmed = header.trim(); + if (!trimmed.isEmpty()) { + parameterNames.add(trimmed); + } + } + + // Parse data rows + List> rows = new ArrayList>(); + for (int i = 1; i < lines.length; i++) { + String dataLine = lines[i].trim(); + if (dataLine.isEmpty()) { + continue; // Skip empty lines + } + + String[] values = PIPE_SEPARATOR.split(dataLine); + if (values.length != parameterNames.size()) { + throw new IllegalArgumentException("Data row " + i + " has " + values.length + + " values but header has " + parameterNames.size() + " parameters"); + } + + Map row = new HashMap(); + for (int j = 0; j < parameterNames.size(); j++) { + String rawValue = values[j].trim(); + Object parsedValue = parseValue(rawValue); + row.put(parameterNames.get(j), parsedValue); + } + rows.add(row); + } + + return new DataTable(parameterNames, rows); + } + + /** + * Parse data pipes and variable assignments: + * a << [1, 2, 3] + * b << ["x", "y", "z"] + * c = a + b.length() + */ + private static DataTable parseDataPipesAndAssignments(String[] lines) { + Map> dataPipes = new HashMap>(); + Map assignments = new HashMap(); + List parameterNames = new ArrayList(); + + // First pass: parse data pipes and assignments + for (String line : lines) { + line = line.trim(); + if (line.isEmpty()) continue; + + Matcher dataPipeMatcher = DATA_PIPE.matcher(line); + Matcher assignmentMatcher = VARIABLE_ASSIGNMENT.matcher(line); + + if (dataPipeMatcher.matches()) { + String varName = dataPipeMatcher.group(1).trim(); + String dataExpr = dataPipeMatcher.group(2).trim(); + List values = parseDataPipeExpression(dataExpr); + dataPipes.put(varName, values); + if (!parameterNames.contains(varName)) { + parameterNames.add(varName); + } + } else if (assignmentMatcher.matches()) { + String varName = assignmentMatcher.group(1).trim(); + String expression = assignmentMatcher.group(2).trim(); + assignments.put(varName, expression); + if (!parameterNames.contains(varName)) { + parameterNames.add(varName); + } + } + } + + // Determine number of iterations (max size of data pipes) + int maxIterations = 0; + for (List values : dataPipes.values()) { + maxIterations = Math.max(maxIterations, values.size()); + } + + if (maxIterations == 0 && assignments.isEmpty()) { + throw new IllegalArgumentException("No data pipes or assignments found in where block"); + } + + // Generate rows + List> rows = new ArrayList>(); + for (int i = 0; i < Math.max(maxIterations, 1); i++) { + Map row = new HashMap(); + + // Add data pipe values + for (Map.Entry> entry : dataPipes.entrySet()) { + String varName = entry.getKey(); + List values = entry.getValue(); + Object value = (i < values.size()) ? values.get(i) : values.get(values.size() - 1); + row.put(varName, value); + } + + // Evaluate assignments (simplified - in real implementation would need expression evaluator) + for (Map.Entry entry : assignments.entrySet()) { + String varName = entry.getKey(); + String expression = entry.getValue(); + Object value = evaluateSimpleExpression(expression, row); + row.put(varName, value); + } + + rows.add(row); + } + + return new DataTable(parameterNames, rows); + } + + /** + * Parse data pipe expressions like [1, 2, 3] or ["a", "b", "c"] + */ + private static List parseDataPipeExpression(String expression) { + List values = new ArrayList(); + + // Remove brackets if present + expression = expression.trim(); + if (expression.startsWith("[") && expression.endsWith("]")) { + expression = expression.substring(1, expression.length() - 1); + } + + // Split by comma and parse each value + String[] parts = expression.split(","); + for (String part : parts) { + values.add(parseValue(part.trim())); + } + + return values; + } + + /** + * Simple expression evaluator for basic arithmetic and string operations + */ + private static Object evaluateSimpleExpression(String expression, Map variables) { + // This is a very simplified evaluator + // Handle basic string concatenation with + operator + + // Replace variables in the expression + for (Map.Entry var : variables.entrySet()) { + String varName = var.getKey(); + Object varValue = var.getValue(); + // Use word boundaries to avoid partial replacements + expression = expression.replaceAll("\\b" + varName + "\\b", + varValue instanceof String ? "\"" + varValue + "\"" : String.valueOf(varValue)); + } + + // Handle simple string concatenation: "a" + "b" + "c" + if (expression.contains(" + ")) { + return evaluateStringConcatenation(expression); + } + + // Try to evaluate as a simple value + return parseValue(expression); + } + + /** + * Evaluate simple string concatenation expressions + */ + private static String evaluateStringConcatenation(String expression) { + String[] parts = expression.split("\\s*\\+\\s*"); + StringBuilder result = new StringBuilder(); + + for (String part : parts) { + part = part.trim(); + // Remove quotes if present + if ((part.startsWith("\"") && part.endsWith("\"")) || + (part.startsWith("'") && part.endsWith("'"))) { + result.append(part.substring(1, part.length() - 1)); + } else { + // It's a number or unquoted value + result.append(part); + } + } + + return result.toString(); + } + + /** + * Parse individual values, handling strings, numbers, booleans, etc. + */ + private static Object parseValue(String value) { + if (value == null || value.trim().isEmpty()) { + return ""; + } + + value = value.trim(); + + // Handle quoted strings + if ((value.startsWith("\"") && value.endsWith("\"")) || + (value.startsWith("'") && value.endsWith("'"))) { + return value.substring(1, value.length() - 1); + } + + // Handle booleans + if ("true".equalsIgnoreCase(value)) { + return true; + } + if ("false".equalsIgnoreCase(value)) { + return false; + } + + // Handle null + if ("null".equalsIgnoreCase(value)) { + return null; + } + + // Try to parse as integer + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + // Not an integer, continue + } + + // Try to parse as double + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + // Not a number, return as string + } + + // Return as string + return value; + } + + /** + * Parse variable assignments like "x = 1, y = 2" + */ + public static Map parseVariableAssignments(String assignments) { + Map variables = new HashMap(); + + if (assignments == null || assignments.trim().isEmpty()) { + return variables; + } + + String[] parts = assignments.split(","); + for (String part : parts) { + Matcher matcher = VARIABLE_ASSIGNMENT.matcher(part.trim()); + if (matcher.matches()) { + String varName = matcher.group(1).trim(); + String varValue = matcher.group(2).trim(); + variables.put(varName, parseValue(varValue)); + } + } + + return variables; + } +} \ No newline at end of file diff --git a/src/main/java/com/askimed/nf/test/lang/TestContext.java b/src/main/java/com/askimed/nf/test/lang/TestContext.java index 1853f5a3..8416d141 100644 --- a/src/main/java/com/askimed/nf/test/lang/TestContext.java +++ b/src/main/java/com/askimed/nf/test/lang/TestContext.java @@ -1,6 +1,8 @@ package com.askimed.nf.test.lang; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import org.codehaus.groovy.control.CompilationFailedException; @@ -10,8 +12,9 @@ import com.askimed.nf.test.lang.workflow.Workflow; import groovy.lang.Closure; +import groovy.lang.GroovyObjectSupport; -public class TestContext { +public class TestContext extends GroovyObjectSupport { private ParamsMap params; @@ -35,9 +38,15 @@ public class TestContext { private WorkflowMeta workflow = new WorkflowMeta(); + private Map testParameters = new HashMap(); + public TestContext(ITest test) { params = new ParamsMap(this); this.test = test; + // Initialize test parameters from the test + if (test != null && test.getParameters() != null) { + this.testParameters = new HashMap(test.getParameters()); + } } public void init(AbstractTest test) { @@ -125,4 +134,35 @@ public void setWorkDir(String workDir) { throw new RuntimeException("Variable 'workDir' is read only"); } + public void setTestParameters(Map parameters) { + if (parameters != null) { + this.testParameters = new HashMap(parameters); + } + } + + public Map getTestParameters() { + return testParameters; + } + + @Override + public Object getProperty(String name) { + // First check if it's a test parameter + if (testParameters.containsKey(name)) { + return testParameters.get(name); + } + // Fall back to default Groovy property resolution + return super.getProperty(name); + } + + @Override + public void setProperty(String name, Object value) { + // Allow setting test parameters dynamically + if (testParameters.containsKey(name)) { + testParameters.put(name, value); + } else { + // Fall back to default Groovy property resolution + super.setProperty(name, value); + } + } + } diff --git a/src/main/java/com/askimed/nf/test/lang/function/FunctionTestSuite.java b/src/main/java/com/askimed/nf/test/lang/function/FunctionTestSuite.java index 2e74b089..ecbdb74b 100644 --- a/src/main/java/com/askimed/nf/test/lang/function/FunctionTestSuite.java +++ b/src/main/java/com/askimed/nf/test/lang/function/FunctionTestSuite.java @@ -1,6 +1,10 @@ package com.askimed.nf.test.lang.function; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import com.askimed.nf.test.core.AbstractTestSuite; import com.askimed.nf.test.core.ITest; @@ -35,4 +39,65 @@ protected ITest getNewTestInstance(String name) { return test; } + public void testEach(String nameTemplate, Map> dataTable, + @DelegatesTo(value = FunctionTest.class, strategy = Closure.DELEGATE_ONLY) final Closure closure) { + + // Extract parameter names and values + List paramNames = new ArrayList(dataTable.keySet()); + List> paramValues = new ArrayList>(); + for (String paramName : paramNames) { + paramValues.add(dataTable.get(paramName)); + } + + // Validate all parameter lists have same length + if (paramValues.isEmpty()) { + throw new IllegalArgumentException("Data table cannot be empty"); + } + + int testCount = paramValues.get(0).size(); + for (List values : paramValues) { + if (values.size() != testCount) { + throw new IllegalArgumentException("All parameter lists must have the same length"); + } + } + + // Generate individual tests for each data row + for (int i = 0; i < testCount; i++) { + Map testParams = new HashMap(); + for (int j = 0; j < paramNames.size(); j++) { + testParams.put(paramNames.get(j), paramValues.get(j).get(i)); + } + + // Create test name by replacing placeholders + String testName = interpolateTestName(nameTemplate, testParams, i); + + // Create parameterized test + final FunctionTest test = new FunctionTest(this); + test.name(testName); + test.setParameters(testParams); + + closure.setDelegate(test); + closure.setResolveStrategy(Closure.DELEGATE_ONLY); + closure.call(); + + addTest(test); + } + } + + private String interpolateTestName(String nameTemplate, Map params, int index) { + String result = nameTemplate; + + // Replace parameter placeholders like #paramName with actual values + for (Map.Entry entry : params.entrySet()) { + String placeholder = "#" + entry.getKey(); + String value = entry.getValue() != null ? entry.getValue().toString() : "null"; + result = result.replace(placeholder, value); + } + + // Replace #index with the current row index + result = result.replace("#index", String.valueOf(index)); + + return result; + } + } \ No newline at end of file diff --git a/src/main/java/com/askimed/nf/test/lang/pipeline/PipelineTestSuite.java b/src/main/java/com/askimed/nf/test/lang/pipeline/PipelineTestSuite.java index fee5db10..d3dfbd7f 100644 --- a/src/main/java/com/askimed/nf/test/lang/pipeline/PipelineTestSuite.java +++ b/src/main/java/com/askimed/nf/test/lang/pipeline/PipelineTestSuite.java @@ -1,6 +1,10 @@ package com.askimed.nf.test.lang.pipeline; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import com.askimed.nf.test.core.AbstractTestSuite; import com.askimed.nf.test.core.ITest; @@ -24,4 +28,65 @@ protected ITest getNewTestInstance(String name) { return test; } + public void testEach(String nameTemplate, Map> dataTable, + @DelegatesTo(value = PipelineTest.class, strategy = Closure.DELEGATE_ONLY) final Closure closure) { + + // Extract parameter names and values + List paramNames = new ArrayList(dataTable.keySet()); + List> paramValues = new ArrayList>(); + for (String paramName : paramNames) { + paramValues.add(dataTable.get(paramName)); + } + + // Validate all parameter lists have same length + if (paramValues.isEmpty()) { + throw new IllegalArgumentException("Data table cannot be empty"); + } + + int testCount = paramValues.get(0).size(); + for (List values : paramValues) { + if (values.size() != testCount) { + throw new IllegalArgumentException("All parameter lists must have the same length"); + } + } + + // Generate individual tests for each data row + for (int i = 0; i < testCount; i++) { + Map testParams = new HashMap(); + for (int j = 0; j < paramNames.size(); j++) { + testParams.put(paramNames.get(j), paramValues.get(j).get(i)); + } + + // Create test name by replacing placeholders + String testName = interpolateTestName(nameTemplate, testParams, i); + + // Create parameterized test + final PipelineTest test = new PipelineTest(this); + test.name(testName); + test.setParameters(testParams); + + closure.setDelegate(test); + closure.setResolveStrategy(Closure.DELEGATE_ONLY); + closure.call(); + + addTest(test); + } + } + + private String interpolateTestName(String nameTemplate, Map params, int index) { + String result = nameTemplate; + + // Replace parameter placeholders like #paramName with actual values + for (Map.Entry entry : params.entrySet()) { + String placeholder = "#" + entry.getKey(); + String value = entry.getValue() != null ? entry.getValue().toString() : "null"; + result = result.replace(placeholder, value); + } + + // Replace #index with the current row index + result = result.replace("#index", String.valueOf(index)); + + return result; + } + } \ No newline at end of file diff --git a/src/main/java/com/askimed/nf/test/lang/process/ProcessTest.java b/src/main/java/com/askimed/nf/test/lang/process/ProcessTest.java index 20d4a72b..eb99b5f1 100644 --- a/src/main/java/com/askimed/nf/test/lang/process/ProcessTest.java +++ b/src/main/java/com/askimed/nf/test/lang/process/ProcessTest.java @@ -218,4 +218,43 @@ protected void writeWorkflowMock(File file) throws IOException, CompilationFaile } + @Override + public void setWithTrace(boolean withTrace) { + this.withTrace = withTrace; + } + + // Setter methods for data-driven testing + public void setSetupClosure(Closure closure) { + if (closure != null) { + setup(closure); + } + } + + public void setWhenClosure(Closure closure) { + if (closure != null) { + when(closure); + } + } + + public void setThenClosure(Closure closure) { + if (closure != null) { + then(closure); + } + } + + public void setCleanupClosure(Closure closure) { + if (closure != null) { + cleanup(closure); + } + } + + @Override + public void setParameters(Map parameters) { + super.setParameters(parameters); + // Also update the TestContext with the parameters + if (context != null && parameters != null) { + context.setTestParameters(parameters); + } + } + } \ No newline at end of file diff --git a/src/main/java/com/askimed/nf/test/lang/process/ProcessTestSuite.java b/src/main/java/com/askimed/nf/test/lang/process/ProcessTestSuite.java index 4524d63c..637592d1 100644 --- a/src/main/java/com/askimed/nf/test/lang/process/ProcessTestSuite.java +++ b/src/main/java/com/askimed/nf/test/lang/process/ProcessTestSuite.java @@ -1,9 +1,14 @@ package com.askimed.nf.test.lang.process; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import com.askimed.nf.test.core.AbstractTestSuite; import com.askimed.nf.test.core.ITest; +import com.askimed.nf.test.lang.DataDrivenTest; import com.askimed.nf.test.lang.TestCode; import groovy.lang.Closure; @@ -47,4 +52,50 @@ protected ITest getNewTestInstance(String name) { return test; } + public void testEach(String nameTemplate, + @DelegatesTo(value = DataDrivenTest.class, strategy = Closure.DELEGATE_ONLY) final Closure closure) { + + final DataDrivenTest dataDrivenTest = new DataDrivenTest(this); + closure.setDelegate(dataDrivenTest); + closure.setResolveStrategy(Closure.DELEGATE_ONLY); + closure.call(); + + // Generate individual tests for each data row + for (int i = 0; i < dataDrivenTest.getDataTable().size(); i++) { + Map testParams = dataDrivenTest.getDataTable().getRows().get(i); + + // Create test name by replacing placeholders + String testName = interpolateTestName(nameTemplate, testParams, i); + + // Create parameterized test + final ProcessTest test = new ProcessTest(this); + test.name(testName); + test.setParameters(testParams); + + // Set the test implementation from the data-driven test + test.setWhenClosure(dataDrivenTest.getWhenClosure()); + test.setThenClosure(dataDrivenTest.getThenClosure()); + test.setSetupClosure(dataDrivenTest.getSetupClosure()); + test.setCleanupClosure(dataDrivenTest.getCleanupClosure()); + + addTest(test); + } + } + + private String interpolateTestName(String nameTemplate, Map params, int index) { + String result = nameTemplate; + + // Replace parameter placeholders like #paramName with actual values + for (Map.Entry entry : params.entrySet()) { + String placeholder = "#" + entry.getKey(); + String value = entry.getValue() != null ? entry.getValue().toString() : "null"; + result = result.replace(placeholder, value); + } + + // Replace #index with the current row index + result = result.replace("#index", String.valueOf(index)); + + return result; + } + } \ No newline at end of file diff --git a/src/main/java/com/askimed/nf/test/lang/workflow/WorkflowTestSuite.java b/src/main/java/com/askimed/nf/test/lang/workflow/WorkflowTestSuite.java index 57f898a7..f11610d9 100644 --- a/src/main/java/com/askimed/nf/test/lang/workflow/WorkflowTestSuite.java +++ b/src/main/java/com/askimed/nf/test/lang/workflow/WorkflowTestSuite.java @@ -1,6 +1,10 @@ package com.askimed.nf.test.lang.workflow; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import com.askimed.nf.test.core.AbstractTestSuite; import com.askimed.nf.test.core.ITest; @@ -48,4 +52,65 @@ protected ITest getNewTestInstance(String name) { return test; } + public void testEach(String nameTemplate, Map> dataTable, + @DelegatesTo(value = WorkflowTest.class, strategy = Closure.DELEGATE_ONLY) final Closure closure) { + + // Extract parameter names and values + List paramNames = new ArrayList(dataTable.keySet()); + List> paramValues = new ArrayList>(); + for (String paramName : paramNames) { + paramValues.add(dataTable.get(paramName)); + } + + // Validate all parameter lists have same length + if (paramValues.isEmpty()) { + throw new IllegalArgumentException("Data table cannot be empty"); + } + + int testCount = paramValues.get(0).size(); + for (List values : paramValues) { + if (values.size() != testCount) { + throw new IllegalArgumentException("All parameter lists must have the same length"); + } + } + + // Generate individual tests for each data row + for (int i = 0; i < testCount; i++) { + Map testParams = new HashMap(); + for (int j = 0; j < paramNames.size(); j++) { + testParams.put(paramNames.get(j), paramValues.get(j).get(i)); + } + + // Create test name by replacing placeholders + String testName = interpolateTestName(nameTemplate, testParams, i); + + // Create parameterized test + final WorkflowTest test = new WorkflowTest(this); + test.name(testName); + test.setParameters(testParams); + + closure.setDelegate(test); + closure.setResolveStrategy(Closure.DELEGATE_ONLY); + closure.call(); + + addTest(test); + } + } + + private String interpolateTestName(String nameTemplate, Map params, int index) { + String result = nameTemplate; + + // Replace parameter placeholders like #paramName with actual values + for (Map.Entry entry : params.entrySet()) { + String placeholder = "#" + entry.getKey(); + String value = entry.getValue() != null ? entry.getValue().toString() : "null"; + result = result.replace(placeholder, value); + } + + // Replace #index with the current row index + result = result.replace("#index", String.valueOf(index)); + + return result; + } + } \ No newline at end of file diff --git a/src/test/java/com/askimed/nf/test/lang/DataTableParserTest.java b/src/test/java/com/askimed/nf/test/lang/DataTableParserTest.java new file mode 100644 index 00000000..fa77134d --- /dev/null +++ b/src/test/java/com/askimed/nf/test/lang/DataTableParserTest.java @@ -0,0 +1,70 @@ +package com.askimed.nf.test.lang; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.askimed.nf.test.lang.DataTableParser.DataTable; + +public class DataTableParserTest { + + @Test + public void testDataTableParsing() { + String dataTableText = + "name | number | expected\n" + + "\"alice\" | 1 | true\n" + + "\"bob\" | 2 | false\n" + + "\"charlie\" | 3 | true"; + + DataTable dataTable = DataTableParser.parseWhereBlock(dataTableText); + + assertEquals(3, dataTable.size()); + assertEquals(3, dataTable.getParameterNames().size()); + assertTrue(dataTable.getParameterNames().contains("name")); + assertTrue(dataTable.getParameterNames().contains("number")); + assertTrue(dataTable.getParameterNames().contains("expected")); + + Map firstRow = dataTable.getRows().get(0); + assertEquals("alice", firstRow.get("name")); + assertEquals(1, firstRow.get("number")); + assertEquals(true, firstRow.get("expected")); + } + + @Test + public void testDataPipesParsing() { + String dataPipesText = + "name << [\"alice\", \"bob\", \"charlie\"]\n" + + "number << [1, 2, 3]\n" + + "expected << [true, false, true]"; + + DataTable dataTable = DataTableParser.parseWhereBlock(dataPipesText); + + assertEquals(3, dataTable.size()); + assertEquals(3, dataTable.getParameterNames().size()); + + Map firstRow = dataTable.getRows().get(0); + assertEquals("alice", firstRow.get("name")); + assertEquals(1, firstRow.get("number")); + assertEquals(true, firstRow.get("expected")); + } + + @Test + public void testMixedDataPipesAndAssignments() { + String mixedText = + "name << [\"test1\", \"test2\"]\n" + + "number << [10, 20]\n" + + "result = name + \"-\" + number"; + + DataTable dataTable = DataTableParser.parseWhereBlock(mixedText); + + assertEquals(2, dataTable.size()); + assertEquals(3, dataTable.getParameterNames().size()); + + Map firstRow = dataTable.getRows().get(0); + assertEquals("test1", firstRow.get("name")); + assertEquals(10, firstRow.get("number")); + assertEquals("test1-10", firstRow.get("result")); + } +} \ No newline at end of file