diff --git a/pom.xml b/pom.xml index a75421a1..d97f3d9c 100644 --- a/pom.xml +++ b/pom.xml @@ -118,6 +118,12 @@ 1.4.11 + + com.jayway.jsonpath + json-path + 2.6.0 + + diff --git a/src/main/java/com/askimed/nf/test/lang/extensions/GlobalMethods.java b/src/main/java/com/askimed/nf/test/lang/extensions/GlobalMethods.java index 84b14424..1066fa0c 100644 --- a/src/main/java/com/askimed/nf/test/lang/extensions/GlobalMethods.java +++ b/src/main/java/com/askimed/nf/test/lang/extensions/GlobalMethods.java @@ -3,8 +3,12 @@ import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.List; +import java.util.Locale; +import com.askimed.nf.test.util.ObjectUtil; import groovy.lang.Closure; import junit.framework.AssertionFailedError; @@ -59,4 +63,13 @@ public static void assertContainsInAnyOrder(List list, List expe throw new PowerAssertionError(thrown.getMessage()); } } + + public static String format(String format, Number number) { + DecimalFormat df = new DecimalFormat(format, DecimalFormatSymbols.getInstance(Locale.US)); + return df.format(number); + } + + public static String md5(Object object) { + return ObjectUtil.getMd5(object); + } } diff --git a/src/main/java/com/askimed/nf/test/lang/extensions/Snapshot.java b/src/main/java/com/askimed/nf/test/lang/extensions/Snapshot.java index 2dcae783..00673890 100644 --- a/src/main/java/com/askimed/nf/test/lang/extensions/Snapshot.java +++ b/src/main/java/com/askimed/nf/test/lang/extensions/Snapshot.java @@ -1,11 +1,16 @@ package com.askimed.nf.test.lang.extensions; import com.askimed.nf.test.core.ITest; -import com.askimed.nf.test.util.ObjectUtil; +import com.askimed.nf.test.util.*; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; +import groovy.lang.Closure; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; public class Snapshot { @@ -17,8 +22,11 @@ public class Snapshot { private static Logger log = LoggerFactory.getLogger(Snapshot.class); + public static String ROOT_OBJECT = "contents"; + public Snapshot(Object actual, ITest test) { - this.actual = actual; + //this.actual = actual; + this.actual = ObjectUtil.toMap(actual); this.file = test.getTestSuite().getSnapshot(); this.test = test; } @@ -72,8 +80,61 @@ public boolean match(String id) throws IOException { } - public void view() { + private DocumentContext createDocumentContext() { + Map snapshots = new HashMap(); + snapshots.put(ROOT_OBJECT, actual); + return JsonPath.parse(snapshots); + } + + public Snapshot replace(Object value) { + return replace("$..*", value); + } + + public Snapshot replace(String selector, Object value) { + DocumentContext json = createDocumentContext(); + json.set(selector, value); + return this; + } + + public Snapshot map(Closure closure) { + return map("$..*", closure); + } + + public Snapshot map(String selector, Closure closure) { + DocumentContext json = createDocumentContext(); + json.map(selector, (currentValue, configuration) -> { + return closure.call(currentValue); + }); + return this; + } + + public Snapshot traverse(Closure closure) { + + MapTraverser.traverse(ROOT_OBJECT, actual, new MapOperation() { + @Override + public Object map(String path, Object value) { + return closure.call(path, value); + } + }); + + return this; + } + + public Snapshot remove(String selector) { + DocumentContext json = createDocumentContext(); + json.delete(selector); + return this; + } + + public Snapshot view() { + return view(false); + } + public Snapshot view(boolean raw) { + String json = raw ? ObjectUtil.toJsonRaw(actual) : ObjectUtil.toJson(actual); + System.out.println(); + System.out.println(AnsiText.padding(AnsiColors.cyan("Snapshot: \n" + json), 6)); + return this; } } diff --git a/src/main/java/com/askimed/nf/test/lang/extensions/SnapshotFile.java b/src/main/java/com/askimed/nf/test/lang/extensions/SnapshotFile.java index a8d8a8eb..71e94f92 100644 --- a/src/main/java/com/askimed/nf/test/lang/extensions/SnapshotFile.java +++ b/src/main/java/com/askimed/nf/test/lang/extensions/SnapshotFile.java @@ -8,14 +8,12 @@ import java.util.Map; import java.util.Set; +import com.askimed.nf.test.util.ObjectUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.askimed.nf.test.core.ITestSuite; -import com.askimed.nf.test.lang.extensions.util.PathConverter; -import groovy.json.JsonGenerator; -import groovy.json.JsonOutput; import groovy.json.JsonSlurper; public class SnapshotFile { @@ -127,9 +125,7 @@ private void removeSnapshots(Set obsoleteSnapshots) { } public void save() throws IOException { - JsonGenerator jsonGenerator = createJsonGenerator(); - String json = jsonGenerator.toJson(snapshots); - String prettyJson = JsonOutput.prettyPrint(json); + String prettyJson = ObjectUtil.toJson(snapshots); File file = new File(filename); FileWriter writer; writer = new FileWriter(file); @@ -142,10 +138,4 @@ protected static String createFilename(ITestSuite suite) { return suite.getFilename() + EXTENSION; } - public static JsonGenerator createJsonGenerator() { - JsonGenerator jsonGenerator = new JsonGenerator.Options().excludeFieldsByName("mapping") - .addConverter(new PathConverter()).build(); - return jsonGenerator; - } - } diff --git a/src/main/java/com/askimed/nf/test/lang/extensions/SnapshotFileItem.java b/src/main/java/com/askimed/nf/test/lang/extensions/SnapshotFileItem.java index 1d28e1f9..3204389f 100644 --- a/src/main/java/com/askimed/nf/test/lang/extensions/SnapshotFileItem.java +++ b/src/main/java/com/askimed/nf/test/lang/extensions/SnapshotFileItem.java @@ -9,8 +9,7 @@ import com.askimed.nf.test.lang.extensions.util.SnapshotDiffUtil; import com.askimed.nf.test.nextflow.NextflowCommand; -import groovy.json.JsonGenerator; -import groovy.json.JsonOutput; +import com.askimed.nf.test.util.ObjectUtil; public class SnapshotFileItem { @@ -76,11 +75,7 @@ public static String createTimestamp() { @Override public String toString() { - JsonGenerator jsonGenerator = SnapshotFile.createJsonGenerator(); - String json = jsonGenerator.toJson(getContent()); - String prettyJson = JsonOutput.prettyPrint(json); - return prettyJson; + return ObjectUtil.toJson(getContent()); } - } diff --git a/src/main/java/com/askimed/nf/test/lang/extensions/util/PathConverterRaw.java b/src/main/java/com/askimed/nf/test/lang/extensions/util/PathConverterRaw.java new file mode 100644 index 00000000..84e7fbb4 --- /dev/null +++ b/src/main/java/com/askimed/nf/test/lang/extensions/util/PathConverterRaw.java @@ -0,0 +1,44 @@ +package com.askimed.nf.test.lang.extensions.util; + +import com.askimed.nf.test.lang.extensions.PathExtension; +import groovy.json.JsonGenerator.Converter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Vector; + +public class PathConverterRaw implements Converter { + + @Override + public boolean handles(Class type) { + return true; + } + + @Override + public Object convert(Object value, String key) { + Path path = null; + if (value instanceof Path) { + path = (Path) value; + if (!path.toFile().exists()) { + throw new RuntimeException("Path " + path.toString() + " not found."); + } + } else if (value instanceof File) { + path = ((File) value).toPath(); + if (!path.toFile().exists()) { + throw new RuntimeException("Path " + path.toString() + " not found."); + } + } else { + path = new File(value.toString()).toPath(); + + } + + if (path.toFile().exists()) { + return path.toFile().getAbsolutePath(); + } + return value; + } + +} \ No newline at end of file diff --git a/src/main/java/com/askimed/nf/test/lang/extensions/util/SnapshotDiffUtil.java b/src/main/java/com/askimed/nf/test/lang/extensions/util/SnapshotDiffUtil.java index 7f1e09e6..39663184 100644 --- a/src/main/java/com/askimed/nf/test/lang/extensions/util/SnapshotDiffUtil.java +++ b/src/main/java/com/askimed/nf/test/lang/extensions/util/SnapshotDiffUtil.java @@ -5,14 +5,12 @@ import java.io.IOException; import java.util.*; -import com.askimed.nf.test.lang.extensions.SnapshotFile; import com.askimed.nf.test.lang.extensions.SnapshotFileItem; import com.askimed.nf.test.util.BinaryFinder; import com.askimed.nf.test.util.Command; import com.askimed.nf.test.util.FileUtil; -import groovy.json.JsonGenerator; -import groovy.json.JsonOutput; +import com.askimed.nf.test.util.ObjectUtil; public class SnapshotDiffUtil { @@ -91,9 +89,7 @@ public static String getSimpleOutput(SnapshotFileItem expected, SnapshotFileItem } private static void writeSnapshotToFile(File file, SnapshotFileItem snapshot) throws IOException { - JsonGenerator jsonGenerator = SnapshotFile.createJsonGenerator(); - String json = jsonGenerator.toJson(snapshot.getContent()); - String prettyJson = JsonOutput.prettyPrint(json); + String prettyJson = ObjectUtil.toJson(snapshot.getContent()); FileWriter fileWriter = new FileWriter(file); fileWriter.append(prettyJson); fileWriter.close(); diff --git a/src/main/java/com/askimed/nf/test/util/MapOperation.java b/src/main/java/com/askimed/nf/test/util/MapOperation.java new file mode 100644 index 00000000..da9c04cd --- /dev/null +++ b/src/main/java/com/askimed/nf/test/util/MapOperation.java @@ -0,0 +1,5 @@ +package com.askimed.nf.test.util; + +public interface MapOperation { + public Object map(String path, Object value); +} \ No newline at end of file diff --git a/src/main/java/com/askimed/nf/test/util/MapTraverser.java b/src/main/java/com/askimed/nf/test/util/MapTraverser.java new file mode 100644 index 00000000..b325a1f4 --- /dev/null +++ b/src/main/java/com/askimed/nf/test/util/MapTraverser.java @@ -0,0 +1,41 @@ +package com.askimed.nf.test.util; + +import java.util.*; + +public class MapTraverser { + + public static void traverse(String prefix, Object root, MapOperation operation) { + traverseRecursive(root, prefix, operation); + } + + private static void traverseRecursive(Object node, String path, MapOperation operation) { + if (node instanceof Map) { + Map map = (Map) node; + for (Map.Entry entry : new HashSet<>(map.entrySet())) { + String key = entry.getKey(); + Object value = entry.getValue(); + String currentPath = path.isEmpty() ? key : path + "." + key; + traverseRecursive(value, currentPath, operation); + if (!(value instanceof Map) && !(value instanceof List)) { + Object newValue = operation.map(currentPath, value); + if (newValue != null) { + map.put(key, newValue); + } + } + } + } else if (node instanceof List) { + List list = (List) node; + for (int i = 0; i < list.size(); i++) { + Object value = list.get(i); + String currentPath = path + "[" + i + "]"; + traverseRecursive(value, currentPath, operation); + if (!(value instanceof Map) && !(value instanceof List)) { + Object newValue = operation.map(currentPath, value); + if (newValue != null) { + list.set(i, newValue); + } + } + } + } + } +} diff --git a/src/main/java/com/askimed/nf/test/util/ObjectUtil.java b/src/main/java/com/askimed/nf/test/util/ObjectUtil.java index 11e29940..c64bf644 100644 --- a/src/main/java/com/askimed/nf/test/util/ObjectUtil.java +++ b/src/main/java/com/askimed/nf/test/util/ObjectUtil.java @@ -1,8 +1,11 @@ package com.askimed.nf.test.util; import com.askimed.nf.test.lang.extensions.SnapshotFile; +import com.askimed.nf.test.lang.extensions.util.PathConverter; +import com.askimed.nf.test.lang.extensions.util.PathConverterRaw; import groovy.json.JsonGenerator; import groovy.json.JsonOutput; +import groovy.json.JsonSlurper; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -10,7 +13,7 @@ public class ObjectUtil { public static String getMd5(Object object) { - JsonGenerator jsonGenerator = SnapshotFile.createJsonGenerator(); + JsonGenerator jsonGenerator = createJsonGenerator(); String json = jsonGenerator.toJson(object); try { return calculateMD5(JsonOutput.prettyPrint(json)); @@ -38,4 +41,34 @@ public static String calculateMD5(String input) throws NoSuchAlgorithmException return result.toString(); } + + + public static String toJson(Object object) { + JsonGenerator jsonGenerator = createJsonGenerator(); + String json = jsonGenerator.toJson(object); + return JsonOutput.prettyPrint(json); + } + + public static String toJsonRaw(Object object) { + JsonGenerator jsonGenerator = createJsonGeneratorRaw(); + String json = jsonGenerator.toJson(object); + return JsonOutput.prettyPrint(json); + } + + public static JsonGenerator createJsonGenerator() { + return new JsonGenerator.Options().excludeFieldsByName("mapping") + .addConverter(new PathConverter()).build(); + } + + public static JsonGenerator createJsonGeneratorRaw() { + return new JsonGenerator.Options().excludeFieldsByName("mapping") + .addConverter(new PathConverterRaw()).build(); + } + + public static Object toMap(Object object) { + JsonGenerator jsonGenerator = createJsonGeneratorRaw(); + String json = jsonGenerator.toJson(object); + return new JsonSlurper().parseText(json); + } + } diff --git a/src/test/java/com/askimed/nf/test/lang/SnapshotTest.java b/src/test/java/com/askimed/nf/test/lang/SnapshotTest.java new file mode 100644 index 00000000..55726089 --- /dev/null +++ b/src/test/java/com/askimed/nf/test/lang/SnapshotTest.java @@ -0,0 +1,37 @@ +package com.askimed.nf.test.lang; + +import com.askimed.nf.test.App; +import com.askimed.nf.test.util.FileUtil; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SnapshotTest { + + static { + + //AnsiColors.disable(); + //AnsiText.disable(); + + } + + @BeforeAll + public static void setUp() throws IOException { + FileUtil.deleteDirectory(new File(".nf-test")); + new File("nf-test.config").delete(); + } + + @Test + public void testSnapshotTransformer() throws Exception { + + App app = new App(); + int exitCode = app.run(new String[] { "test", "test-data/snapshots/snapshot_transformer.nf.test" }); + assertEquals(0, exitCode); + + } + +} diff --git a/test-data/snapshots/functions.nf b/test-data/snapshots/functions.nf new file mode 100644 index 00000000..2a6fab9f --- /dev/null +++ b/test-data/snapshots/functions.nf @@ -0,0 +1,65 @@ +import java.text.SimpleDateFormat +import java.util.Date + +// Function to generate a log entry with a timestamp +def generateLogEntry(index) { + def dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + def timestamp = dateFormat.format(new Date()) + return "${timestamp} - Log entry number ${index}" +} + +// Function to create a log file with multiple entries +def createLogFile(fileName, entryCount) { + def file = new File(fileName) + file.withWriter { writer -> + (1..entryCount).each { index -> + def logEntry = generateLogEntry(index) + writer.writeLine(logEntry) + } + } +} + +// Function to generate a CSV entry with static values +def generateCsvEntry(index) { + def name = "Item${index}" + def quantity = 10 + index + def price = 1.99 + (index * 0.5) + return "${index},${name},${quantity},${price}" +} + +// Function to create a CSV file with multiple entries +def createCsvFile(fileName, entryCount) { + def file = new File(fileName) + file.withWriter { writer -> + // Write the header + writer.writeLine("ID,Name,Quantity,Price") + // Write the entries + (1..entryCount).each { index -> + def csvEntry = generateCsvEntry(index) + writer.writeLine(csvEntry) + } + } +} + + +def create_object(name) { + + def chunks = [1,2,3,4,5,6,7,8,9] + chunks.shuffle() + + // simulate output file + createCsvFile("output.csv", 20) + + // simulate log file + createLogFile("output.log", 20) + + return [ + id : "1234-5678-9101", + status : "SUCCESS", + start_time : new Date(), + end_time : new Date(), + chunks : chunks, + value : 1.3568956165789456, + files : [file("output.csv"), file("output.log") ] + ] +} \ No newline at end of file diff --git a/test-data/snapshots/snapshot_transformer.nf.test b/test-data/snapshots/snapshot_transformer.nf.test new file mode 100644 index 00000000..4fd17dfe --- /dev/null +++ b/test-data/snapshots/snapshot_transformer.nf.test @@ -0,0 +1,50 @@ +nextflow_function { + + name "Test Function Say Hello" + + script "./functions.nf" + function "create_object" + + test("Passing case") { + + when { + function { + """ + input[0] = "aaron" + """ + } + } + + then { + assert snapshot(function.result). + view(). + view(true). + remove('.start_time'). + replace('.end_time', ""). + map('.chunks', chunks -> chunks.sort()). + + // vs. value is each element. no the list itself. + // map('.chunks[*]', value -> 27). + + map('contents[0].value', value -> format("#0.00", value)). + //remove('contents[0].files[?(@ == "output.log")]'). + //remove('contents[0].files[?(@ =~ /.*\\.log$/)]'). + + //traverse( (key, value) -> { + // value == "output.log" ? "" : value + //}). + //traverse( (key, value) -> { + // value.toString().endsWith(".log") ? "" : value + //}). + traverse( (key, value) -> { + return value.toString().endsWith(".log") ? file(value).name : value + }). + map('contents[0].files[0]', value -> file(value).readLines()). + + view(). + match() + } + + } + +} \ No newline at end of file diff --git a/test-data/snapshots/snapshot_transformer.nf.test.snap b/test-data/snapshots/snapshot_transformer.nf.test.snap new file mode 100644 index 00000000..aec87cc7 --- /dev/null +++ b/test-data/snapshots/snapshot_transformer.nf.test.snap @@ -0,0 +1,54 @@ +{ + "Passing case": { + "content": [ + { + "id": "1234-5678-9101", + "status": "SUCCESS", + "end_time": "", + "chunks": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "value": "1.36", + "files": [ + [ + "ID,Name,Quantity,Price", + "1,Item1,11,2.49", + "2,Item2,12,2.99", + "3,Item3,13,3.49", + "4,Item4,14,3.99", + "5,Item5,15,4.49", + "6,Item6,16,4.99", + "7,Item7,17,5.49", + "8,Item8,18,5.99", + "9,Item9,19,6.49", + "10,Item10,20,6.99", + "11,Item11,21,7.49", + "12,Item12,22,7.99", + "13,Item13,23,8.49", + "14,Item14,24,8.99", + "15,Item15,25,9.49", + "16,Item16,26,9.99", + "17,Item17,27,10.49", + "18,Item18,28,10.99", + "19,Item19,29,11.49", + "20,Item20,30,11.99" + ], + "output.log" + ] + } + ], + "meta": { + "nf-test": "0.9.0-rc2", + "nextflow": "23.04.3" + }, + "timestamp": "2024-06-12T17:08:37.459381" + } +} \ No newline at end of file