diff --git a/src/main/java/com/askimed/nf/test/core/AbstractTestSuite.java b/src/main/java/com/askimed/nf/test/core/AbstractTestSuite.java index 5a6b083e..b08fe50d 100644 --- a/src/main/java/com/askimed/nf/test/core/AbstractTestSuite.java +++ b/src/main/java/com/askimed/nf/test/core/AbstractTestSuite.java @@ -5,6 +5,7 @@ import java.util.Vector; import com.askimed.nf.test.config.Config; +import com.askimed.nf.test.lang.extensions.SnapshotFile; public abstract class AbstractTestSuite implements ITestSuite { @@ -24,6 +25,8 @@ public abstract class AbstractTestSuite implements ITestSuite { private String options = ""; + private SnapshotFile snapshotFile; + private List tags = new Vector(); @Override @@ -127,4 +130,26 @@ public ITaggable getParent() { return null; } + @Override + public boolean hasSkippedTests() { + for (ITest test : getTests()) { + if (test.isSkipped()) { + return true; + } + } + return false; + } + + @Override + public SnapshotFile getSnapshot() { + if (snapshotFile == null) { + snapshotFile = SnapshotFile.loadByTestSuite(this); + } + return snapshotFile; + } + + @Override + public boolean hasSnapshotLoaded() { + return (snapshotFile != null); + } } diff --git a/src/main/java/com/askimed/nf/test/core/AnsiTestExecutionListener.java b/src/main/java/com/askimed/nf/test/core/AnsiTestExecutionListener.java index 4a1650b0..b6d14424 100644 --- a/src/main/java/com/askimed/nf/test/core/AnsiTestExecutionListener.java +++ b/src/main/java/com/askimed/nf/test/core/AnsiTestExecutionListener.java @@ -1,6 +1,7 @@ package com.askimed.nf.test.core; +import com.askimed.nf.test.lang.extensions.SnapshotFile; import com.askimed.nf.test.util.AnsiColors; import com.askimed.nf.test.util.AnsiText; @@ -16,6 +17,12 @@ public class AnsiTestExecutionListener implements ITestExecutionListener { private boolean debug = false; + private int updatedSnapshots; + + private int createdSnapshots; + + private int obsoleteSnapshots; + public static final int TEST_PADDING = 2; @Override @@ -33,6 +40,22 @@ public void testPlanExecutionFinished() { double executionTime = ((end - start) / 1000.0); System.out.println(); + + if ((updatedSnapshots + createdSnapshots + obsoleteSnapshots) > 0) { + System.out.println(); + System.out.println("Snapshot Summary:"); + } + if (updatedSnapshots > 0) { + System.out.println(AnsiText.padding(updatedSnapshots + " updated", TEST_PADDING)); + } + if (createdSnapshots > 0) { + System.out.println(AnsiText.padding(createdSnapshots + " created", TEST_PADDING)); + } + + if (obsoleteSnapshots > 0) { + System.out.println(AnsiColors.yellow(AnsiText.padding(obsoleteSnapshots + " obsolete", TEST_PADDING))); + } + System.out.println(); if (failed > 0) { @@ -63,6 +86,46 @@ public void testSuiteExecutionStarted(ITestSuite testSuite) { @Override public void testSuiteExecutionFinished(ITestSuite testSuite) { + if (!testSuite.hasSnapshotLoaded()) { + return; + } + + SnapshotFile snapshot = testSuite.getSnapshot(); + if ((snapshot.getUpdatedSnapshots().size() + snapshot.getCreatedSnapshots().size() + + snapshot.getObsoleteSnapshots().size()) > 0 || testSuite.hasSkippedTests()) { + System.out.println(AnsiText.padding("Snapshots:", TEST_PADDING)); + } + if (snapshot.getUpdatedSnapshots().size() > 0) { + System.out.println(AnsiText.padding( + snapshot.getUpdatedSnapshots().size() + " updated " + snapshot.getUpdatedSnapshots(), + 2 * TEST_PADDING)); + } + if (snapshot.getCreatedSnapshots().size() > 0) { + System.out.println(AnsiText.padding( + snapshot.getCreatedSnapshots().size() + " created " + snapshot.getCreatedSnapshots(), + 2 * TEST_PADDING)); + } + + updatedSnapshots += snapshot.getUpdatedSnapshots().size(); + createdSnapshots += snapshot.getCreatedSnapshots().size(); + + // if we have at least one skipped test, we can not + // determine if a snapshot is obsolete. + // TODO: add check. if at lest one test failed, same situation. + if (testSuite.hasSkippedTests()) { + System.out.println(AnsiText.padding( + "Obsolete snapshots can only be checked if all tests of a file are executed.", 2 * TEST_PADDING)); + return; + } + + if (snapshot.getObsoleteSnapshots().size() > 0) { + System.out.println(AnsiColors.yellow(AnsiText.padding( + snapshot.getObsoleteSnapshots().size() + " obsolete " + snapshot.getObsoleteSnapshots(), + 2 * TEST_PADDING))); + } + + obsoleteSnapshots += snapshot.getObsoleteSnapshots().size(); + } @Override diff --git a/src/main/java/com/askimed/nf/test/core/ITestSuite.java b/src/main/java/com/askimed/nf/test/core/ITestSuite.java index 33fb2f08..1558c414 100644 --- a/src/main/java/com/askimed/nf/test/core/ITestSuite.java +++ b/src/main/java/com/askimed/nf/test/core/ITestSuite.java @@ -4,6 +4,7 @@ import java.util.List; import com.askimed.nf.test.config.Config; +import com.askimed.nf.test.lang.extensions.SnapshotFile; public interface ITestSuite extends ITaggable { @@ -20,5 +21,11 @@ public interface ITestSuite extends ITaggable { public String getFilename(); public void configure(Config config); + + public boolean hasSkippedTests(); + + public SnapshotFile getSnapshot(); + + public boolean hasSnapshotLoaded(); } \ No newline at end of file diff --git a/src/main/java/com/askimed/nf/test/core/TestExecutionEngine.java b/src/main/java/com/askimed/nf/test/core/TestExecutionEngine.java index ca6b12c9..71b8304d 100644 --- a/src/main/java/com/askimed/nf/test/core/TestExecutionEngine.java +++ b/src/main/java/com/askimed/nf/test/core/TestExecutionEngine.java @@ -5,6 +5,7 @@ import java.util.Vector; import com.askimed.nf.test.lang.TestSuiteBuilder; +import com.askimed.nf.test.lang.extensions.SnapshotFile; import com.askimed.nf.test.plugins.PluginManager; import com.askimed.nf.test.util.AnsiColors; import com.askimed.nf.test.util.AnsiText; @@ -202,6 +203,14 @@ public int execute() throws Throwable { } + // Remove obsolete snapshots in update mode and when no test was skipped. + //TODO: removeObsolete snapshots only when no test failed!! + if (updateSnapshot && !testSuite.hasSkippedTests() && testSuite.hasSnapshotLoaded()) { + SnapshotFile snapshot = testSuite.getSnapshot(); + snapshot.removeObsoleteSnapshots(); + snapshot.save(); + } + listener.testSuiteExecutionFinished(testSuite); } 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 0a49d189..a1f14122 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,6 +1,6 @@ package com.askimed.nf.test.lang.extensions; -import java.util.Date; +import java.io.IOException; import com.askimed.nf.test.core.ITest; @@ -14,28 +14,29 @@ public class Snapshot { public Snapshot(Object actual, ITest test) { this.actual = actual; - this.file = SnapshotFile.loadByTestSuite(test.getTestSuite()); + this.file = test.getTestSuite().getSnapshot(); this.test = test; } - public boolean match() { + public boolean match() throws IOException { return match(test.getName()); } - public boolean match(String id) { + public boolean match(String id) throws IOException { SnapshotFileItem expected = file.getSnapshot(id); + //new snapshot --> create snapshot if (expected == null) { - file.updateSnapshot(id, actual); + file.createSnapshot(id, actual); file.save(); return true; } try { - return new SnapshotFileItem(new Date(), actual).equals(expected); + //compare actual snapshot with expected + return new SnapshotFileItem(actual).equals(expected); } catch (Exception e) { - // test failes + // test failes and flag set --> update snapshot if (test.isUpdateSnapshot()) { - // udpate snapshot file.updateSnapshot(id, actual); file.save(); return true; @@ -45,9 +46,9 @@ public boolean match(String id) { } } - + public void view() { - + } } 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 d568735e..ab571bbc 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 @@ -3,18 +3,15 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.nio.file.Path; -import java.security.NoSuchAlgorithmException; -import java.util.Date; import java.util.HashMap; -import java.util.List; +import java.util.HashSet; import java.util.Map; -import java.util.Vector; +import java.util.Set; import com.askimed.nf.test.core.ITestSuite; +import com.askimed.nf.test.lang.extensions.util.PathConverter; import groovy.json.JsonGenerator; -import groovy.json.JsonGenerator.Converter; import groovy.json.JsonOutput; import groovy.json.JsonSlurper; @@ -24,6 +21,14 @@ public class SnapshotFile { private Map snapshots = new HashMap(); + private Set activeSnapshots = new HashSet(); + + private Set createdSnapshots = new HashSet(); + + private Set updatedSnapshots = new HashSet(); + + private boolean removedSnapshots = false; + public static SnapshotFile loadByTestSuite(ITestSuite suite) { String filename = createFilename(suite); return new SnapshotFile(filename); @@ -47,39 +52,78 @@ public SnapshotFile(String filename) { Map> map = (Map>) jsonSlurper.parse(file); for (String id : map.keySet()) { Map object = map.get(id); - SnapshotFileItem item = new SnapshotFileItem(new Date(), object.get("content")); + String timestamp = object.get("timestamp").toString(); + Object content = object.get("content"); + SnapshotFileItem item = new SnapshotFileItem(timestamp, content); snapshots.put(id, item); } } public SnapshotFileItem getSnapshot(String id) { - return snapshots.get(id); + SnapshotFileItem snapshot = snapshots.get(id); + if (snapshot != null) { + activeSnapshots.add(id); + } + return snapshot; + } + + public void createSnapshot(String id, Object object) { + createdSnapshots.add(id); + activeSnapshots.add(id); + snapshots.put(id, new SnapshotFileItem(object)); } public void updateSnapshot(String id, Object object) { - snapshots.put(id, new SnapshotFileItem(new Date(), object)); + updatedSnapshots.add(id); + snapshots.put(id, new SnapshotFileItem(object)); + } + + public Set getCreatedSnapshots() { + return createdSnapshots; + } + + public Set getUpdatedSnapshots() { + return updatedSnapshots; + } + + public Set getActiveSnapshots() { + return activeSnapshots; + } + + public Set getObsoleteSnapshots() { + Set obsolete = new HashSet(snapshots.keySet()); + obsolete.removeAll(activeSnapshots); + return obsolete; + } + + public void removeObsoleteSnapshots() { + removeSnapshots(getObsoleteSnapshots()); + removedSnapshots = true; } - public void save() { + public boolean hasRemovedSnapsshots() { + return removedSnapshots; + } + + private void removeSnapshots(Set obsoleteSnapshots) { + for (String snapshot : obsoleteSnapshots) { + snapshots.remove(snapshot); + } + } + + public void save() throws IOException { JsonGenerator jsonGenerator = createJsonGenerator(); String json = jsonGenerator.toJson(snapshots); String prettyJson = JsonOutput.prettyPrint(json); File file = new File(filename); FileWriter writer; - try { - writer = new FileWriter(file); - writer.append(prettyJson); - writer.close(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - + writer = new FileWriter(file); + writer.append(prettyJson); + writer.close(); } protected static String createFilename(ITestSuite suite) { - // TODO: create subfolder snapshots? read from config? return suite.getFilename() + ".snap"; } @@ -89,58 +133,5 @@ public static JsonGenerator createJsonGenerator() { return jsonGenerator; } - static class PathConverter 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 { - path = new File(value.toString()).toPath(); - - } - - if (path.toFile().exists()) { - try { - if (path.toFile().isFile()) { - return serializeFile(path); - } else { - return serializeDirectory(path); - } - // return path.getFileName() + ":base64," + FileUtil.encodeBase64(path); - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return value; - } - } - return value; - } - - private String serializeFile(Path path) throws NoSuchAlgorithmException, IOException { - return path.getFileName() + ":md5," + PathExtension.getMd5(path); - } - - private List serializeDirectory(Path folder) throws NoSuchAlgorithmException, IOException { - List files = new Vector(); - for (Path subPath : PathExtension.list(folder)) { - if (subPath.toFile().isDirectory()) { - files.add(serializeDirectory(subPath)); - } else { - files.add(serializeFile(subPath)); - } - } - return files; - } - } } 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 f4f12f7a..9957b624 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 @@ -1,6 +1,7 @@ package com.askimed.nf.test.lang.extensions; -import java.util.Date; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import groovy.json.JsonGenerator; import groovy.json.JsonOutput; @@ -9,18 +10,25 @@ public class SnapshotFileItem { private Object content; - private Date timestamp; + private String timestamp; - public SnapshotFileItem(Date timestamp, Object object) { - content = object; + public static DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"); + + public SnapshotFileItem(Object content) { + this.timestamp = createTimestamp(); + this.content = content; + } + + public SnapshotFileItem(String timestamp, Object content) { this.timestamp = timestamp; + this.content = content; } public Object getContent() { return content; } - public Date getTimestamp() { + public String getTimestamp() { return timestamp; } @@ -45,6 +53,10 @@ public boolean equals(Object object) { } + protected String createTimestamp() { + return DateTimeFormatter.ISO_DATE_TIME.format(LocalDateTime.now()); + } + @Override public String toString() { JsonGenerator jsonGenerator = SnapshotFile.createJsonGenerator(); diff --git a/src/main/java/com/askimed/nf/test/lang/extensions/util/PathConverter.java b/src/main/java/com/askimed/nf/test/lang/extensions/util/PathConverter.java new file mode 100644 index 00000000..47d1d56f --- /dev/null +++ b/src/main/java/com/askimed/nf/test/lang/extensions/util/PathConverter.java @@ -0,0 +1,66 @@ +package com.askimed.nf.test.lang.extensions.util; + +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; + +import com.askimed.nf.test.lang.extensions.PathExtension; + +import groovy.json.JsonGenerator.Converter; + +public class PathConverter 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 { + path = new File(value.toString()).toPath(); + + } + + if (path.toFile().exists()) { + try { + if (path.toFile().isFile()) { + return serializeFile(path); + } else { + return serializeDirectory(path); + } + // return path.getFileName() + ":base64," + FileUtil.encodeBase64(path); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return value; + } + } + return value; + } + + private String serializeFile(Path path) throws NoSuchAlgorithmException, IOException { + return path.getFileName() + ":md5," + PathExtension.getMd5(path); + } + + private List serializeDirectory(Path folder) throws NoSuchAlgorithmException, IOException { + List files = new Vector(); + for (Path subPath : PathExtension.list(folder)) { + if (subPath.toFile().isDirectory()) { + files.add(serializeDirectory(subPath)); + } else { + files.add(serializeFile(subPath)); + } + } + return files; + } +} \ No newline at end of file diff --git a/src/test/java/com/askimed/nf/test/lang/FunctionTest.java b/src/test/java/com/askimed/nf/test/lang/FunctionTest.java index 5751e036..06fc609e 100644 --- a/src/test/java/com/askimed/nf/test/lang/FunctionTest.java +++ b/src/test/java/com/askimed/nf/test/lang/FunctionTest.java @@ -37,6 +37,15 @@ public void testScriptSucces() throws Exception { } + @Test + public void testScriptWithSnapshot() throws Exception { + + App app = new App(); + int exitCode = app.run(new String[] { "test", "test-data/function/default/functions.snapshot.nf.test" }); + assertEquals(0, exitCode); + + } + @Test public void testScriptWihtMultipleFunctions() throws Exception { @@ -45,15 +54,14 @@ public void testScriptWihtMultipleFunctions() throws Exception { assertEquals(0, exitCode); } - + @Test public void testGroovyScript() throws Exception { App app = new App(); - int exitCode = app.run(new String[] { "test", "test-data/function/utils/Utils.groovy.test" ,"--debug"}); + int exitCode = app.run(new String[] { "test", "test-data/function/utils/Utils.groovy.test", "--debug" }); assertEquals(0, exitCode); } - } diff --git a/test-data/function/default/functions.snapshot.nf.test b/test-data/function/default/functions.snapshot.nf.test new file mode 100644 index 00000000..8d5f73b5 --- /dev/null +++ b/test-data/function/default/functions.snapshot.nf.test @@ -0,0 +1,28 @@ +nextflow_function { + + name "Test Function Say Hello" + + script "test-data/function/default/functions.nf" + function "say_hello" + + test("Passing case") { + + when { + function { + """ + input[0] = "aaron" + """ + } + } + + then { + snapshot(function).match("forer lukas") + //assert function.success + //assert function.result == "Hello aaron" + //assert function.stdout.contains("Hello aaron") + //assert function.stderr.isEmpty() + } + + } + +} \ No newline at end of file diff --git a/test-data/function/default/functions.snapshot.nf.test.snap b/test-data/function/default/functions.snapshot.nf.test.snap new file mode 100644 index 00000000..c7954473 --- /dev/null +++ b/test-data/function/default/functions.snapshot.nf.test.snap @@ -0,0 +1,27 @@ +{ + "forer lukas": { + "content": [ + { + "stderr": [ + + ], + "errorReport": null, + "exitStatus": 0, + "failed": false, + "result": "Hello aaron", + "stdout": [ + "Hello aaron" + ], + "errorMessage": null, + "trace": { + "tasksFailed": 0, + "tasksCount": 0, + "tasksSucceeded": 0 + }, + "name": "function", + "success": true + } + ], + "timestamp": "2023-08-26T15:20:15.069134" + } +} \ No newline at end of file