From 7e2327b7dc5b8bc6c5dcc2113b76f29903953ee5 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Wed, 23 Nov 2022 12:39:49 +0100 Subject: [PATCH 01/27] deprecations and co --- README.md | 14 +-- RELEASE_NOTES.md | 63 ++++++------ pom.xml | 2 +- readme/RELEASE_NOTES.md | 10 +- snapshot-tests-bom/pom.xml | 2 +- snapshot-tests-common/pom.xml | 2 +- snapshot-tests-core/pom.xml | 2 +- .../DefaultSnapshotDirectoryStrategy.java | 22 +++++ .../test/snapshots/EnableSnapshotTests.java | 7 ++ .../test/snapshots/SnapshotDirectory.java | 35 +++++++ .../snapshots/SnapshotDirectoryStrategy.java | 9 ++ .../skuzzle/test/snapshots/SnapshotDsl.java | 2 +- .../test/snapshots/SnapshotTestResult.java | 51 ++++++++-- .../impl/DefaultSnapshotConfiguration.java | 17 +--- .../impl/DetermineSnapshotDirectory.java | 72 ++++++++++++++ .../test/snapshots/impl/SnapshotTestImpl.java | 12 ++- .../skuzzle/test/snapshots/SnapshotsTest.java | 2 +- .../impl/DetermineSnapshotDirectoryTest.java | 99 +++++++++++++++++++ snapshot-tests-dependencies/pom.xml | 2 +- snapshot-tests-directory-params/pom.xml | 2 +- snapshot-tests-html/pom.xml | 2 +- .../snapshots/html/HtmlSnapshotTests.java | 2 +- snapshot-tests-jackson/pom.xml | 2 +- snapshot-tests-jaxb/pom.xml | 2 +- .../test/snapshots/xml/SnapshotsTest.java | 4 +- snapshot-tests-normalize/pom.xml | 2 +- snapshot-tests-release-test/pom.xml | 2 +- snapshot-tests-xmlunit/pom.xml | 2 +- 28 files changed, 352 insertions(+), 93 deletions(-) create mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DefaultSnapshotDirectoryStrategy.java create mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java create mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectoryStrategy.java create mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java create mode 100644 snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectoryTest.java diff --git a/README.md b/README.md index 5ba53400..9cc1146c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.6.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-bom/1.6.0/jar) -[![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.6.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-core/1.6.0) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-bom/1.7.0-SNAPSHOT/jar) +[![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-core/1.7.0-SNAPSHOT) [![Coverage Status](https://coveralls.io/repos/github/skuzzle/snapshot-tests/badge.svg?branch=main)](https://coveralls.io/github/skuzzle/snapshot-tests?branch=main) [![Twitter Follow](https://img.shields.io/twitter/follow/skuzzleOSS.svg?style=social)](https://twitter.com/skuzzleOSS) @@ -13,17 +13,17 @@ serialized version of the object during the first test execution and during subs actual object against the stored snapshot. Supported snapshot formats: -- [x] generic plain text via [snapshot-tests-core](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-core/1.6.0/jar) -- [x] Json via [snapshot-tests-jackson](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jackson/1.6.0/jar) -- [x] XML via [snapshot-tests-jaxb](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.6.0/jar) -- [x] HTML via [snapshot-tests-html](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-html/1.6.0/jar) +- [x] generic plain text via [snapshot-tests-core](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-core/1.7.0-SNAPSHOT/jar) +- [x] Json via [snapshot-tests-jackson](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jackson/1.7.0-SNAPSHOT/jar) +- [x] XML via [snapshot-tests-jaxb](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.7.0-SNAPSHOT/jar) +- [x] HTML via [snapshot-tests-html](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-html/1.7.0-SNAPSHOT/jar) Read more about snapshot testing in this accompanying [blog post](https://simon.taddiken.net/the-case-for-snapshot-testing/). ### Latest Maven Central coordinates Please check out the GitHub release page to find Maven & Gradle coordinates for the latest -release [1.6.0](https://github.com/skuzzle/snapshot-tests/releases/tag/v1.6.0) +release [1.7.0-SNAPSHOT](https://github.com/skuzzle/snapshot-tests/releases/tag/v1.7.0-SNAPSHOT) ## Quick start _(assumes using `snapshot-tests-jackson` artifact)_ diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index de441602..628f2bd4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,10 +1,5 @@ -* [#42](https://github.com/skuzzle/snapshot-tests/issues/42): Publish flattened version of our artifact to fix BOM distribution -* [#43](https://github.com/skuzzle/snapshot-tests/issues/43): Allow XML structure comparison for String input -* Add `XmlSnapshot.withEnableXPathDebugging(...)` and `HtmlSnapshot.withEnableXPathDebugging(...)` to print results of applying custom comparison rules -* Fixed bugs and performance issues in XPath comparison engine -* `toString()` of `TestFile` and `TestDirectory` contain full absoulte path to the file instead of just the file name -* Deprecate `FilesFrom.directory` in favor of `FilesFrom.testResourcesDirectory` and `FilesFrom.projectDirectory` -* Deprecate `DirectoriesFrom.directory` in favor of `DirectoriesFrom.testResourcesDirectory` and `DirectoriesFrom.projectDirectory` +* Deprecate `SnapshotTestResult.serializedSnapshot` in favor of `SnapshotTestResult.snapshotFile` +* Add `SnapshotTestResult.serializedActual` Maven Central coordinates for this release: @@ -12,128 +7,128 @@ Maven Central coordinates for this release: ## BOM Artifact Manages the versions of all modules in case you are using multiple in your project -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.6.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-bom/1.6.0/jar) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-bom/1.7.0-SNAPSHOT/jar) ```xml de.skuzzle.test snapshot-tests-bom - 1.6.0 + 1.7.0-SNAPSHOT pom import ``` ``` -testImplementation(platform("de.skuzzle.test:snapshot-tests-bom:1.6.0")) +testImplementation(platform("de.skuzzle.test:snapshot-tests-bom:1.7.0-SNAPSHOT")) ``` ## Artifacts If you only need text based snapshots: -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.6.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-core/1.6.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.6.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-core/1.6.0) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-core/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-core/1.7.0-SNAPSHOT) ```xml de.skuzzle.test snapshot-tests-core - 1.6.0 + 1.7.0-SNAPSHOT test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-core:1.6.0' -testImplementation("de.skuzzle.test:snapshot-tests-core:1.6.0") +testImplementation 'de.skuzzle.test:snapshot-tests-core:1.7.0-SNAPSHOT' +testImplementation("de.skuzzle.test:snapshot-tests-core:1.7.0-SNAPSHOT") ``` If you need json based snapshots (includes `-core`): -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.6.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jackson/1.6.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.6.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-jackson/1.6.0) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jackson/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-jackson/1.7.0-SNAPSHOT) ```xml de.skuzzle.test snapshot-tests-jackson - 1.6.0 + 1.7.0-SNAPSHOT test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-jackson:1.6.0' -testImplementation("de.skuzzle.test:snapshot-tests-jackson:1.6.0") +testImplementation 'de.skuzzle.test:snapshot-tests-jackson:1.7.0-SNAPSHOT' +testImplementation("de.skuzzle.test:snapshot-tests-jackson:1.7.0-SNAPSHOT") ``` If you need xml based snapshots (includes `-core`): -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.6.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.6.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.6.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-jaxb/1.6.0) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-jaxb/1.7.0-SNAPSHOT) ```xml de.skuzzle.test snapshot-tests-jaxb - 1.6.0 + 1.7.0-SNAPSHOT test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-jaxb:1.6.0' -testImplementation("de.skuzzle.test:snapshot-tests-jaxb:1.6.0") +testImplementation 'de.skuzzle.test:snapshot-tests-jaxb:1.7.0-SNAPSHOT' +testImplementation("de.skuzzle.test:snapshot-tests-jaxb:1.7.0-SNAPSHOT") ``` If you need HTML based snapshots (includes `-core`): -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.6.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-html/1.6.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.6.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-html/1.6.0) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-html/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-html/1.7.0-SNAPSHOT) ```xml de.skuzzle.test snapshot-tests-html - 1.6.0 + 1.7.0-SNAPSHOT test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-html:1.6.0' -testImplementation("de.skuzzle.test:snapshot-tests-html:1.6.0") +testImplementation 'de.skuzzle.test:snapshot-tests-html:1.7.0-SNAPSHOT' +testImplementation("de.skuzzle.test:snapshot-tests-html:1.7.0-SNAPSHOT") ``` ## Experimental Directory Params -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.6.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-directory-params/1.6.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.6.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-directory-params/1.6.0) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-directory-params/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-directory-params/1.7.0-SNAPSHOT) ```xml de.skuzzle.test snapshot-tests-directory-params - 1.6.0 + 1.7.0-SNAPSHOT test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-directory-params:1.6.0' -testImplementation("de.skuzzle.test:snapshot-tests-directory-params:1.6.0") +testImplementation 'de.skuzzle.test:snapshot-tests-directory-params:1.7.0-SNAPSHOT' +testImplementation("de.skuzzle.test:snapshot-tests-directory-params:1.7.0-SNAPSHOT") ``` Object normalization -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.6.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-normalize/1.6.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.6.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-normalize/1.6.0) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-normalize/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-normalize/1.7.0-SNAPSHOT) ```xml de.skuzzle.test snapshot-tests-normalize - 1.6.0 + 1.7.0-SNAPSHOT test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-normalize:1.6.0' -testImplementation("de.skuzzle.test:snapshot-tests-normalize:1.6.0") +testImplementation 'de.skuzzle.test:snapshot-tests-normalize:1.7.0-SNAPSHOT' +testImplementation("de.skuzzle.test:snapshot-tests-normalize:1.7.0-SNAPSHOT") ``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index f9010a8a..b7cd46b7 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1-SNAPSHOT + 1.7.0-SNAPSHOT pom Snapshot Tests Parent diff --git a/readme/RELEASE_NOTES.md b/readme/RELEASE_NOTES.md index 479a2813..591f7194 100644 --- a/readme/RELEASE_NOTES.md +++ b/readme/RELEASE_NOTES.md @@ -1,10 +1,6 @@ -* [#42](https://github.com/skuzzle/snapshot-tests/issues/42): Publish flattened version of our artifact to fix BOM distribution -* [#43](https://github.com/skuzzle/snapshot-tests/issues/43): Allow XML structure comparison for String input -* Add `XmlSnapshot.withEnableXPathDebugging(...)` and `HtmlSnapshot.withEnableXPathDebugging(...)` to print results of applying custom comparison rules -* Fixed bugs and performance issues in XPath comparison engine -* `toString()` of `TestFile` and `TestDirectory` contain full absoulte path to the file instead of just the file name -* Deprecate `FilesFrom.directory` in favor of `FilesFrom.testResourcesDirectory` and `FilesFrom.projectDirectory` -* Deprecate `DirectoriesFrom.directory` in favor of `DirectoriesFrom.testResourcesDirectory` and `DirectoriesFrom.projectDirectory` +* Deprecate `EnableSnapshotTests.snapshotDirectory` in favor of new annotation `@SnapshotDirectory` +* Deprecate `SnapshotTestResult.serializedSnapshot` in favor of `SnapshotTestResult.snapshotFile` +* Add `SnapshotTestResult.serializedActual` Maven Central coordinates for this release: diff --git a/snapshot-tests-bom/pom.xml b/snapshot-tests-bom/pom.xml index c811a571..63d447a6 100644 --- a/snapshot-tests-bom/pom.xml +++ b/snapshot-tests-bom/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1-SNAPSHOT + 1.7.0-SNAPSHOT snapshot-tests-bom diff --git a/snapshot-tests-common/pom.xml b/snapshot-tests-common/pom.xml index 107ad853..2681d38e 100644 --- a/snapshot-tests-common/pom.xml +++ b/snapshot-tests-common/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1-SNAPSHOT + 1.7.0-SNAPSHOT snapshot-tests-common diff --git a/snapshot-tests-core/pom.xml b/snapshot-tests-core/pom.xml index cbb78db1..47d9272c 100644 --- a/snapshot-tests-core/pom.xml +++ b/snapshot-tests-core/pom.xml @@ -6,7 +6,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1-SNAPSHOT + 1.7.0-SNAPSHOT snapshot-tests-core diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DefaultSnapshotDirectoryStrategy.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DefaultSnapshotDirectoryStrategy.java new file mode 100644 index 00000000..3a37f381 --- /dev/null +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DefaultSnapshotDirectoryStrategy.java @@ -0,0 +1,22 @@ +package de.skuzzle.test.snapshots; + +import java.nio.file.Path; + +/** + * Just a tagging class file as default value for + * {@link SnapshotDirectory#determinedBy()}. + * + * @author Simon Taddiken + * @since 1.7.0 + */ +final class DefaultSnapshotDirectoryStrategy implements SnapshotDirectoryStrategy { + + // Implementation NOTE: The class's full qualified name is used in the internal impl + // package + + @Override + public Path determineSnapshotDirectory(Class testClass, SnapshotDirectory directory) throws SnapshotException { + throw new UnsupportedOperationException(); + } + +} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java index b23a051a..3d58b791 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java @@ -116,7 +116,10 @@ * package name of the test class. * * @return The relative directory to store the snapshots. + * @deprecated Since 1.7.0 - Use {@link SnapshotDirectory} annotation instead. */ + @Deprecated(since = "1.7.0") + @API(status = Status.DEPRECATED, since = "1.7.0") String snapshotDirectory() default ""; /** @@ -147,6 +150,10 @@ * collected and reported after the test method completed. * * @return Whether to enable soft assertions. Defaults to false. + * @deprecated Since 1.7.0 - Soft assertions will no longer be supported from version + * 2.0 on */ + @Deprecated(since = "1.7.0") + @API(status = Status.DEPRECATED, since = "1.7.0") boolean softAssertions() default false; } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java new file mode 100644 index 00000000..49183812 --- /dev/null +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java @@ -0,0 +1,35 @@ +package de.skuzzle.test.snapshots; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Drop in replacement for {@link EnableSnapshotTests#snapshotDirectory()}. Allows to + * customize the snapshot directory either globally for a test class or locally for a + * single test method. + * + * @author Simon Taddiken + * @since 1.7.0 + */ +@Retention(RUNTIME) +@Target({ TYPE, METHOD }) +@API(status = Status.EXPERIMENTAL, since = "1.7.0") +public @interface SnapshotDirectory { + /** + * Define the snapshot directory relative to src/test/resources. If this + * is not defined, snapshots will be stored in a directory structure according to the + * package name of the test class. + * + * @return The relative directory to store the snapshots. + */ + String value() default ""; + + Class determinedBy() default DefaultSnapshotDirectoryStrategy.class; +} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectoryStrategy.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectoryStrategy.java new file mode 100644 index 00000000..5017dd3c --- /dev/null +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectoryStrategy.java @@ -0,0 +1,9 @@ +package de.skuzzle.test.snapshots; + +import java.nio.file.Path; + +public interface SnapshotDirectoryStrategy { + + Path determineSnapshotDirectory(Class testClass, SnapshotDirectory directory) throws SnapshotException; + +} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDsl.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDsl.java index 3919813b..b6ae40d6 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDsl.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDsl.java @@ -49,7 +49,7 @@ public interface SnapshotDsl { * * * This snippet does not only use two incomplete usages, but also tries to re-use the - * 'choose-actual' stage without having called er terminaly operation. + * 'choose-actual' stage without having called a terminal operation. * * @author Simon Taddiken * @see EnableSnapshotTests diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java index ce3f3a56..9e6dc2bf 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java @@ -22,22 +22,30 @@ public final class SnapshotTestResult { private final SnapshotFile snapshot; private final Path targetFile; private final SnapshotStatus status; + private final String serializedActual; private final Throwable failure; - private SnapshotTestResult(Path targetFile, SnapshotStatus status, SnapshotFile snapshot, Throwable failure) { + private SnapshotTestResult(Path targetFile, SnapshotStatus status, SnapshotFile snapshotFile, + String serializedActual, + Throwable failure) { this.targetFile = Arguments.requireNonNull(targetFile); this.status = Arguments.requireNonNull(status); - this.snapshot = Arguments.requireNonNull(snapshot); + this.snapshot = Arguments.requireNonNull(snapshotFile); + this.serializedActual = serializedActual; this.failure = failure; } - public static SnapshotTestResult forFailedTest(Path targetFile, SnapshotFile snapshot, Throwable failure) { - return new SnapshotTestResult(targetFile, SnapshotStatus.ASSERTED, snapshot, + @API(status = Status.INTERNAL) + public static SnapshotTestResult forFailedTest(Path targetFile, SnapshotFile snapshotFile, String serializedActual, + Throwable failure) { + return new SnapshotTestResult(targetFile, SnapshotStatus.ASSERTED, snapshotFile, serializedActual, Arguments.requireNonNull(failure)); } - public static SnapshotTestResult of(Path targetFile, SnapshotStatus status, SnapshotFile snapshot) { - return new SnapshotTestResult(targetFile, status, snapshot, null); + @API(status = Status.INTERNAL) + public static SnapshotTestResult of(Path targetFile, SnapshotStatus status, SnapshotFile snapshotFile, + String serializedActual) { + return new SnapshotTestResult(targetFile, status, snapshotFile, serializedActual, null); } /** @@ -60,14 +68,43 @@ public SnapshotStatus status() { } /** - * The snapshot. + * The contents of the persisted snapshot file. * * @return The serialized snapshot. + * @deprecated Since 1.7.0 - Use {@link #snapshotFile()} instead. */ + @Deprecated(since = "1.7.0") + @API(status = Status.DEPRECATED, since = "1.7.0") public SnapshotFile serializedSnapshot() { return this.snapshot; } + /** + * The contents of the persisted snapshot file. Note that the file's content and the + * value of {@link #serializedActual()} can be different, even though the snapshot + * test did not fail. For example they could differ in whitespaces if whitespaces were + * ignored during comparison. Or they can differ in certain attributes if you used + * structure compare with custom comparison rules. + * + * @return The snapshot file. + */ + public SnapshotFile snapshotFile() { + return this.snapshot; + } + + /** + * Returns the serialized string value of the actual test input. Note that this value + * can be different from the contents of {@link #snapshotFile()} (see the method's + * documentation for details). + * + * @return The serialized actual value. + * @since 1.7.0 + */ + @API(status = Status.EXPERIMENTAL, since = "1.7.0") + public String serializedActual() { + return this.serializedActual; + } + /** * The exception with which the snapshot assertion failed if any. * diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java index 692c3b9b..dd4198b7 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java @@ -9,7 +9,6 @@ import de.skuzzle.test.snapshots.DeleteOrphanedSnapshots; import de.skuzzle.test.snapshots.EnableSnapshotTests; import de.skuzzle.test.snapshots.ForceUpdateSnapshots; -import de.skuzzle.test.snapshots.io.DirectoryResolver; import de.skuzzle.test.snapshots.validation.Arguments; /** @@ -37,21 +36,7 @@ public static SnapshotConfiguration forTestClass(Class testClass) { @Override public Path determineSnapshotDirectory() { - final Class testClass = testClass(); - final String testDirName = snapshotDirectoryName(testClass); - - final Path testDirectory = DirectoryResolver.resolve(testDirName); - return testDirectory; - } - - private String snapshotDirectoryName(Class testClass) { - final EnableSnapshotTests snapshotAssertions = testClass - .getAnnotation(EnableSnapshotTests.class); - - final String testDirName = snapshotAssertions.snapshotDirectory().isEmpty() - ? testClass().getName().replace('.', '/') + "_snapshots" - : snapshotAssertions.snapshotDirectory(); - return testDirName; + return DetermineSnapshotDirectory.forTestclass(testClass); } @Override diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java new file mode 100644 index 00000000..df7afbf5 --- /dev/null +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java @@ -0,0 +1,72 @@ +package de.skuzzle.test.snapshots.impl; + +import java.nio.file.Path; + +import de.skuzzle.test.snapshots.EnableSnapshotTests; +import de.skuzzle.test.snapshots.SnapshotDirectory; +import de.skuzzle.test.snapshots.SnapshotDirectoryStrategy; +import de.skuzzle.test.snapshots.SnapshotException; +import de.skuzzle.test.snapshots.io.DirectoryResolver; +import de.skuzzle.test.snapshots.validation.State; + +class DetermineSnapshotDirectory { + + static Path forTestclass(Class testClass) { + final Path testDirectoryLegacy = snapshotDirectoryLegacy(testClass); + final SnapshotDirectory annotation = testClass.getAnnotation(SnapshotDirectory.class); + if (testDirectoryLegacy != null) { + State.check(annotation == null, + "Please use either legacy mode of specifying snapshot directory or the new @SnapshotDirectory annotation but not both"); + return testDirectoryLegacy; + } + + if (annotation == null) { + // default behavior if no annotation is present + final String dirName = testClass.getName().replace('.', '/') + "_snapshots"; + return DirectoryResolver.resolve(dirName); + } + + State.check(!isDefaultValue(annotation.determinedBy()) || !annotation.value().isEmpty(), "TBD"); + + if (isDefaultValue(annotation.determinedBy())) { + return DirectoryResolver.resolve(annotation.value()); + } else { + return pathFromStrategy(testClass, annotation); + } + } + + private static boolean isDefaultValue(Class type) { + return "de.skuzzle.test.snapshots.DefaultSnapshotDirectoryStrategy".equals(type.getName()); + } + + private static Path pathFromStrategy(Class testClass, SnapshotDirectory directory) { + try { + return newInstanceOf(directory.determinedBy()).determineSnapshotDirectory(testClass, directory); + } catch (final SnapshotException e) { + throw new IllegalStateException( + "Error determining snapshot directory from strategy " + directory.determinedBy(), e); + } + } + + private static SnapshotDirectoryStrategy newInstanceOf(Class type) { + try { + return type.getConstructor().newInstance(); + } catch (final Exception e) { + throw new IllegalStateException("Error creating an instance of " + type.getName(), e); + } + } + + private static Path snapshotDirectoryLegacy(Class testClass) { + final EnableSnapshotTests snapshotAssertions = testClass + .getAnnotation(EnableSnapshotTests.class); + if (snapshotAssertions.snapshotDirectory().isEmpty()) { + return null; + } + final String testDirName = snapshotAssertions.snapshotDirectory(); + return DirectoryResolver.resolve(testDirName); + } + + private DetermineSnapshotDirectory() { + // hidden + } +} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java index 859aa8c9..4c3254ab 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java @@ -119,7 +119,7 @@ SnapshotTestResult justUpdateSnapshotWith(SnapshotSerializer snapshotSerializer, .writeTo(snapshotFilePath); final SnapshotTestResult result = SnapshotTestResult.of(snapshotFilePath, SnapshotStatus.UPDATED_FORCEFULLY, - snapshotFile); + snapshotFile, serializedActual); recordSnapshotTestResult(result); @@ -145,7 +145,7 @@ SnapshotTestResult disabled(SnapshotSerializer snapshotSerializer, : snapshotSerializer.serialize(actual); final SnapshotFile snapshotFile = SnapshotFile.of(snapshotHeader, serializedActual); final SnapshotTestResult result = SnapshotTestResult.of(snapshotFilePath, SnapshotStatus.DISABLED, - snapshotFile); + snapshotFile, serializedActual); recordSnapshotTestResult(result); return result; @@ -176,7 +176,7 @@ SnapshotTestResult executeAssertionWith(SnapshotSerializer snapshotSerializer, final SnapshotStatus status = snapshotFileAlreadyExists ? SnapshotStatus.UPDATED_FORCEFULLY : SnapshotStatus.CREATED_INITIALLY; - result = SnapshotTestResult.of(snapshotFilePath, status, snapshotFile); + result = SnapshotTestResult.of(snapshotFilePath, status, snapshotFile, serializedActual); } else { final SnapshotFile snapshotFile = readSnapshotFileAndUpdateHeader(snapshotFilePath, snapshotHeader); final String storedSnapshot = snapshotFile.snapshot(); @@ -189,8 +189,10 @@ SnapshotTestResult executeAssertionWith(SnapshotSerializer snapshotSerializer, final String serializedActual = snapshotSerializer.serialize(actual); result = compareTestResults(structuralAssertions, storedSnapshot, serializedActual, snapshotFilePath) - .map(assertionError -> forFailedTest(snapshotFilePath, snapshotFile, assertionError)) - .orElseGet(() -> SnapshotTestResult.of(snapshotFilePath, SnapshotStatus.ASSERTED, snapshotFile)); + .map(assertionError -> forFailedTest(snapshotFilePath, snapshotFile, serializedActual, + assertionError)) + .orElseGet(() -> SnapshotTestResult.of(snapshotFilePath, SnapshotStatus.ASSERTED, snapshotFile, + serializedActual)); } recordSnapshotTestResult(result); diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java index 6e1ff754..cba5cea9 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java @@ -17,7 +17,7 @@ public class SnapshotsTest { @Test void testDisabledWithNullInput(Snapshot snapshot) throws Exception { final SnapshotTestResult testResult = snapshot.assertThat(null).asText().disabled(); - assertThat(testResult.serializedSnapshot().snapshot()).isEqualTo("<>"); + assertThat(testResult.serializedActual()).isEqualTo("<>"); } @Test diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectoryTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectoryTest.java new file mode 100644 index 00000000..54a734e9 --- /dev/null +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectoryTest.java @@ -0,0 +1,99 @@ +package de.skuzzle.test.snapshots.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import de.skuzzle.test.snapshots.EnableSnapshotTests; +import de.skuzzle.test.snapshots.SnapshotDirectory; +import de.skuzzle.test.snapshots.SnapshotDirectoryStrategy; +import de.skuzzle.test.snapshots.SnapshotException; +import de.skuzzle.test.snapshots.io.DirectoryResolver; + +class DetermineSnapshotDirectoryTest { + + @Test + void testDirectoryFromLegacy() throws Exception { + final Path snapshotDirectory = DetermineSnapshotDirectory.forTestclass(Legacy.class); + assertThat(snapshotDirectory).isEqualTo(DirectoryResolver.resolve("test")); + } + + @EnableSnapshotTests(snapshotDirectory = "test") + private static class Legacy { + + } + + @Test + void legacy_and_new_annotation_should_raise_exception() throws Exception { + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> DetermineSnapshotDirectory.forTestclass(LegacyAndNewAnnotation.class)); + } + + @EnableSnapshotTests(snapshotDirectory = "test") + @SnapshotDirectory("test") + private static class LegacyAndNewAnnotation { + + } + + @Test + void empty_snapshot_directory_annotation_should_raise_exception() throws Exception { + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> DetermineSnapshotDirectory.forTestclass(EmptyAnnotation.class)); + } + + @EnableSnapshotTests + @SnapshotDirectory + private static class EmptyAnnotation { + + } + + @Test + void testResolveDefaultDirectory() throws Exception { + final Path snapshotDirectory = DetermineSnapshotDirectory.forTestclass(DefaultSnapshotDirectory.class); + assertThat(snapshotDirectory).isEqualTo( + DirectoryResolver.resolve(DefaultSnapshotDirectory.class.getName().replace('.', '/') + "_snapshots")); + } + + @EnableSnapshotTests + private static class DefaultSnapshotDirectory { + + } + + @Test + void testResolveDirectoryFromAnnotationString() throws Exception { + final Path snapshotDirectory = DetermineSnapshotDirectory.forTestclass(FromAnnotationString.class); + assertThat(snapshotDirectory).isEqualTo(DirectoryResolver.resolve("test")); + } + + @EnableSnapshotTests + @SnapshotDirectory("test") + private static class FromAnnotationString { + + } + + @Test + void testResolveDirectoryFromStrategy() throws Exception { + final Path snapshotDirectory = DetermineSnapshotDirectory.forTestclass(FromStrategy.class); + assertThat(snapshotDirectory).isEqualTo(Path.of("directory")); + } + + @EnableSnapshotTests + @SnapshotDirectory(value = "directory", determinedBy = SimpleTestSnapshotDirectoryStrategy.class) + private static class FromStrategy { + + } + + public static class SimpleTestSnapshotDirectoryStrategy implements SnapshotDirectoryStrategy { + + @Override + public Path determineSnapshotDirectory(Class testClass, SnapshotDirectory directory) + throws SnapshotException { + return Path.of(directory.value()); + } + + } + +} diff --git a/snapshot-tests-dependencies/pom.xml b/snapshot-tests-dependencies/pom.xml index 5c843dcb..a122cbcd 100644 --- a/snapshot-tests-dependencies/pom.xml +++ b/snapshot-tests-dependencies/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1-SNAPSHOT + 1.7.0-SNAPSHOT snapshot-tests-dependencies diff --git a/snapshot-tests-directory-params/pom.xml b/snapshot-tests-directory-params/pom.xml index 02d479b8..8433c997 100644 --- a/snapshot-tests-directory-params/pom.xml +++ b/snapshot-tests-directory-params/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1-SNAPSHOT + 1.7.0-SNAPSHOT snapshot-tests-directory-params diff --git a/snapshot-tests-html/pom.xml b/snapshot-tests-html/pom.xml index 8127229a..c51df07c 100644 --- a/snapshot-tests-html/pom.xml +++ b/snapshot-tests-html/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1-SNAPSHOT + 1.7.0-SNAPSHOT snapshot-tests-html diff --git a/snapshot-tests-html/src/test/java/de/skuzzle/test/snapshots/html/HtmlSnapshotTests.java b/snapshot-tests-html/src/test/java/de/skuzzle/test/snapshots/html/HtmlSnapshotTests.java index 7f6adfe3..55da6dc7 100644 --- a/snapshot-tests-html/src/test/java/de/skuzzle/test/snapshots/html/HtmlSnapshotTests.java +++ b/snapshot-tests-html/src/test/java/de/skuzzle/test/snapshots/html/HtmlSnapshotTests.java @@ -21,7 +21,7 @@ void testCompareHtmlPrettyPrinted(Snapshot snapshot) throws Exception { .as(HtmlSnapshot.html().withPrettyPrintSnapshot(true)) .matchesSnapshotStructure(); - final String snapshotText = result.serializedSnapshot().snapshot(); + final String snapshotText = result.serializedActual(); assertThat(snapshotText).isEqualTo("" + "\n" + " \n" diff --git a/snapshot-tests-jackson/pom.xml b/snapshot-tests-jackson/pom.xml index 40bb1397..a0b3caab 100644 --- a/snapshot-tests-jackson/pom.xml +++ b/snapshot-tests-jackson/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1-SNAPSHOT + 1.7.0-SNAPSHOT snapshot-tests-jackson diff --git a/snapshot-tests-jaxb/pom.xml b/snapshot-tests-jaxb/pom.xml index 374317bb..71a4be28 100644 --- a/snapshot-tests-jaxb/pom.xml +++ b/snapshot-tests-jaxb/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1-SNAPSHOT + 1.7.0-SNAPSHOT snapshot-tests-jaxb diff --git a/snapshot-tests-jaxb/src/test/java/de/skuzzle/test/snapshots/xml/SnapshotsTest.java b/snapshot-tests-jaxb/src/test/java/de/skuzzle/test/snapshots/xml/SnapshotsTest.java index 6ade0693..217eb38a 100644 --- a/snapshot-tests-jaxb/src/test/java/de/skuzzle/test/snapshots/xml/SnapshotsTest.java +++ b/snapshot-tests-jaxb/src/test/java/de/skuzzle/test/snapshots/xml/SnapshotsTest.java @@ -28,7 +28,7 @@ void testXmlAlreadyAStringWithPrettyPrint(Snapshot snapshot) throws Exception { .assertThat("text").as(xml) .matchesSnapshotStructure(); - assertThat(snapshotResult.serializedSnapshot().snapshot()).isEqualTo(String.format("" + assertThat(snapshotResult.serializedActual()).isEqualTo(String.format("" + "%n" + "%n" + " text%n" @@ -43,7 +43,7 @@ void testXmlAlreadyAStringWithoutPrettyPrint(Snapshot snapshot) throws Exception .withPrettyPrintStringXml(false)) .matchesSnapshotStructure(); - assertThat(snapshotResult.serializedSnapshot().snapshot()) + assertThat(snapshotResult.serializedActual()) .isEqualTo("text"); } diff --git a/snapshot-tests-normalize/pom.xml b/snapshot-tests-normalize/pom.xml index c5a2467b..3e63f9bc 100644 --- a/snapshot-tests-normalize/pom.xml +++ b/snapshot-tests-normalize/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1-SNAPSHOT + 1.7.0-SNAPSHOT snapshot-tests-normalize diff --git a/snapshot-tests-release-test/pom.xml b/snapshot-tests-release-test/pom.xml index d7656235..8aa3d395 100644 --- a/snapshot-tests-release-test/pom.xml +++ b/snapshot-tests-release-test/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1-SNAPSHOT + 1.7.0-SNAPSHOT snapshot-tests-release-test diff --git a/snapshot-tests-xmlunit/pom.xml b/snapshot-tests-xmlunit/pom.xml index 74bd153f..f5b6b371 100644 --- a/snapshot-tests-xmlunit/pom.xml +++ b/snapshot-tests-xmlunit/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1-SNAPSHOT + 1.7.0-SNAPSHOT snapshot-tests-xmlunit From fb5803de2f4a09217a920c565b36bfa9c0ade4a5 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 23 Nov 2022 11:57:46 +0000 Subject: [PATCH 02/27] Update for next development version --- pom.xml | 2 +- snapshot-tests-bom/pom.xml | 2 +- snapshot-tests-common/pom.xml | 2 +- snapshot-tests-core/pom.xml | 2 +- snapshot-tests-dependencies/pom.xml | 2 +- snapshot-tests-directory-params/pom.xml | 2 +- snapshot-tests-html/pom.xml | 2 +- snapshot-tests-jackson/pom.xml | 2 +- snapshot-tests-jaxb/pom.xml | 2 +- snapshot-tests-normalize/pom.xml | 2 +- snapshot-tests-release-test/pom.xml | 2 +- snapshot-tests-xmlunit/pom.xml | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index 6e9518f5..788201b2 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1 + 1.6.2-SNAPSHOT pom Snapshot Tests Parent diff --git a/snapshot-tests-bom/pom.xml b/snapshot-tests-bom/pom.xml index 08f5350f..294412e2 100644 --- a/snapshot-tests-bom/pom.xml +++ b/snapshot-tests-bom/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1 + 1.6.2-SNAPSHOT snapshot-tests-bom diff --git a/snapshot-tests-common/pom.xml b/snapshot-tests-common/pom.xml index abaa1a90..77c05a9f 100644 --- a/snapshot-tests-common/pom.xml +++ b/snapshot-tests-common/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1 + 1.6.2-SNAPSHOT snapshot-tests-common diff --git a/snapshot-tests-core/pom.xml b/snapshot-tests-core/pom.xml index 634e62ff..7b5194cf 100644 --- a/snapshot-tests-core/pom.xml +++ b/snapshot-tests-core/pom.xml @@ -6,7 +6,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1 + 1.6.2-SNAPSHOT snapshot-tests-core diff --git a/snapshot-tests-dependencies/pom.xml b/snapshot-tests-dependencies/pom.xml index 1a393352..54c48aab 100644 --- a/snapshot-tests-dependencies/pom.xml +++ b/snapshot-tests-dependencies/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1 + 1.6.2-SNAPSHOT snapshot-tests-dependencies diff --git a/snapshot-tests-directory-params/pom.xml b/snapshot-tests-directory-params/pom.xml index d321c978..3ef897e9 100644 --- a/snapshot-tests-directory-params/pom.xml +++ b/snapshot-tests-directory-params/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1 + 1.6.2-SNAPSHOT snapshot-tests-directory-params diff --git a/snapshot-tests-html/pom.xml b/snapshot-tests-html/pom.xml index 5a38e5eb..f7c3c215 100644 --- a/snapshot-tests-html/pom.xml +++ b/snapshot-tests-html/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1 + 1.6.2-SNAPSHOT snapshot-tests-html diff --git a/snapshot-tests-jackson/pom.xml b/snapshot-tests-jackson/pom.xml index bdc0dfc8..614d3d68 100644 --- a/snapshot-tests-jackson/pom.xml +++ b/snapshot-tests-jackson/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1 + 1.6.2-SNAPSHOT snapshot-tests-jackson diff --git a/snapshot-tests-jaxb/pom.xml b/snapshot-tests-jaxb/pom.xml index 51b9dd77..87913b73 100644 --- a/snapshot-tests-jaxb/pom.xml +++ b/snapshot-tests-jaxb/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1 + 1.6.2-SNAPSHOT snapshot-tests-jaxb diff --git a/snapshot-tests-normalize/pom.xml b/snapshot-tests-normalize/pom.xml index 8a1aef84..3263632b 100644 --- a/snapshot-tests-normalize/pom.xml +++ b/snapshot-tests-normalize/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1 + 1.6.2-SNAPSHOT snapshot-tests-normalize diff --git a/snapshot-tests-release-test/pom.xml b/snapshot-tests-release-test/pom.xml index 5e2d69e1..fab73af9 100644 --- a/snapshot-tests-release-test/pom.xml +++ b/snapshot-tests-release-test/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1 + 1.6.2-SNAPSHOT snapshot-tests-release-test diff --git a/snapshot-tests-xmlunit/pom.xml b/snapshot-tests-xmlunit/pom.xml index b6fd81be..325466c5 100644 --- a/snapshot-tests-xmlunit/pom.xml +++ b/snapshot-tests-xmlunit/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.6.1 + 1.6.2-SNAPSHOT snapshot-tests-xmlunit From a38db653806ee92d93d5206013f1545be275e9c3 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Thu, 24 Nov 2022 17:53:34 +0100 Subject: [PATCH 03/27] Improve documentation --- .../DefaultSnapshotDirectoryStrategy.java | 5 +++ .../test/snapshots/EnableSnapshotTests.java | 8 +++-- .../test/snapshots/SnapshotDirectory.java | 32 +++++++++++++++++-- .../snapshots/SnapshotDirectoryStrategy.java | 20 ++++++++++++ .../skuzzle/test/snapshots/SnapshotDsl.java | 10 ++++-- .../test/snapshots/SnapshotNaming.java | 13 +++++--- .../impl/DetermineSnapshotDirectory.java | 8 +++-- 7 files changed, 82 insertions(+), 14 deletions(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DefaultSnapshotDirectoryStrategy.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DefaultSnapshotDirectoryStrategy.java index 3a37f381..69e3805d 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DefaultSnapshotDirectoryStrategy.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DefaultSnapshotDirectoryStrategy.java @@ -19,4 +19,9 @@ public Path determineSnapshotDirectory(Class testClass, SnapshotDirectory dir throw new UnsupportedOperationException(); } + private DefaultSnapshotDirectoryStrategy() { + // not meant to be instantiated by anyone + throw new UnsupportedOperationException(); + } + } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java index 3d58b791..dfc2b5be 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java @@ -33,8 +33,8 @@ *

* asText() will 'serialize' actual test results using * {@link Object#toString()}. There are additional {@link StructuredDataProvider} - * implementations that allow to serialize snapshots as json or xml. To use them, you need - * to declare their respective maven modules as dependency. + * implementations that allow to serialize snapshots as json, xml or html. To use them, + * you need to declare their respective maven modules as dependency. * *

  *     @Test
@@ -101,6 +101,7 @@
  * @author Simon Taddiken
  * @see Snapshot
  * @see SnapshotNaming
+ * @see SnapshotDirectory
  * @see DeleteOrphanedSnapshots
  * @see ForceUpdateSnapshots
  */
@@ -136,6 +137,7 @@
      * @deprecated Since 1.1.0 - Use the {@link ForceUpdateSnapshots} annotation instead.
      * @return Whether to update the stored snapshots.
      * @see ChooseAssertions#justUpdateSnapshot()
+     * @see ForceUpdateSnapshots
      */
     @API(status = Status.DEPRECATED, since = "1.1.0")
     @Deprecated(since = "1.1.0")
@@ -151,7 +153,7 @@
      *
      * @return Whether to enable soft assertions. Defaults to false.
      * @deprecated Since 1.7.0 - Soft assertions will no longer be supported from version
-     *             2.0 on
+     *             2.0 on. You could use AssertJ's SoftAssertions as a replacement.
      */
     @Deprecated(since = "1.7.0")
     @API(status = Status.DEPRECATED, since = "1.7.0")
diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java
index 49183812..72d6aa07 100644
--- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java
+++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java
@@ -10,26 +10,52 @@
 import org.apiguardian.api.API;
 import org.apiguardian.api.API.Status;
 
+import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot;
+
 /**
  * Drop in replacement for {@link EnableSnapshotTests#snapshotDirectory()}. Allows to
  * customize the snapshot directory either globally for a test class or locally for a
  * single test method.
+ * 

+ * If you don't customize the snapshot directory at all, then snapshots will by default be + * stored in a directory structure according to the package name if the test class. If you + * already specified a custom directory using the DSL (via + * {@link Snapshot#in(java.nio.file.Path)}) then this annotation will have no effect for + * that particular assertion. + *

+ * You can either specify {@link #value()} which will interpret the string as directory + * relative to this project's src/test/resources directory. Or you can specify the type of + * a class that implements {@link SnapshotDirectoryStrategy} using + * {@link #determinedBy()}. With the strategy you are no longer bounded to use a directory + * relative to src/test/resources. * * @author Simon Taddiken * @since 1.7.0 + * @see SnapshotDirectoryStrategy */ @Retention(RUNTIME) @Target({ TYPE, METHOD }) @API(status = Status.EXPERIMENTAL, since = "1.7.0") public @interface SnapshotDirectory { /** - * Define the snapshot directory relative to src/test/resources. If this - * is not defined, snapshots will be stored in a directory structure according to the - * package name of the test class. + * Define the snapshot directory relative to src/test/resources. + *

+ * Note: If you specify a custom {@link SnapshotDirectoryStrategy} using + * {@link #determinedBy()}, the strategy implementation might apply custom semantics + * to this value. Check with the respective strategy's documentation. * * @return The relative directory to store the snapshots. */ String value() default ""; + /** + * Specify the type of a class that implements {@link SnapshotDirectoryStrategy}. The + * strategy will be used to determine the actual snapshot directory. + *

+ * By default, there is no strategy in place, meaning that the default semantics as + * described in the documentation to the {@link #value()} property apply. + * + * @return The strategy to use. + */ Class determinedBy() default DefaultSnapshotDirectoryStrategy.class; } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectoryStrategy.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectoryStrategy.java index 5017dd3c..ab489128 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectoryStrategy.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectoryStrategy.java @@ -2,8 +2,28 @@ import java.nio.file.Path; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Strategy interface which can be used to determine a custom snapshot directory. Use with + * {@link SnapshotDirectory#determinedBy()}. + * + * @author Simon Taddiken + * @since 1.7.0 + */ +@API(status = Status.EXPERIMENTAL, since = "1.7.0") public interface SnapshotDirectoryStrategy { + /** + * Determine the directory into which snapshots will be persisted. + * + * @param testClass The test class for which snapshot tests are enabled. + * @param directory The {@link SnapshotDirectory} annotation instance. + * @return The path at which snapshot files will be persisted. + * @throws SnapshotException Can be thrown by implementors in case determining the + * directory is not possible. + */ Path determineSnapshotDirectory(Class testClass, SnapshotDirectory directory) throws SnapshotException; } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDsl.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDsl.java index b6ae40d6..f9c0b593 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDsl.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDsl.java @@ -79,8 +79,8 @@ public interface ChooseDirectory { /** * Allows to choose the directory into which the snapshot will be persisted. The - * path configured here takes precedence over what is configured in - * {@link EnableSnapshotTests#snapshotDirectory()} + * path configured here takes precedence over what is configured via + * {@link SnapshotDirectory} *

* Warning: Changing the directory has severe impact on orphan detection * and might lead to false positives. Use with caution as long as this API is @@ -102,6 +102,9 @@ public interface ChooseName extends ChooseActual { * of using method name + consecutive number. Note that, when you * specify the same name twice within test cases for the same snapshot directory, * snapshots will be silently overridden and tests may subsequently fail. + *

+ * Note that you can not change the extension of snapshot files which will always + * be .snapshot. * * @param snapshotName The name of the snapshot to create. * @return Fluent API object for choosing the snapshot format. Do NOT assume it is @@ -129,6 +132,9 @@ default ChooseActual named(String snapshotName) { * .matchesSnapshotText(); * } *

+ *

+ * Note that you can not change the extension of snapshot files which will always + * be .snapshot. * * @param namingStrategy The naming strategy to use. * @return Fluent API object for choosing the snapshot format. Do NOT assume it is diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotNaming.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotNaming.java index fa91a385..a78bbfb9 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotNaming.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotNaming.java @@ -12,9 +12,10 @@ import de.skuzzle.test.snapshots.validation.Arguments; /** - * Strategy interface for dynamically determining snapshot names. By default, the snapshot - * name is made up of the test's method name and a serial number representing the count of - * snapshot assertions within this test method. + * Strategy interface for dynamically determining snapshot names. Snapshot name will be + * used as file name of the persisted snapshot file.By default, the snapshot name is made + * up of the test's method name and a serial number representing the count of snapshot + * assertions within this test method. *

* An instance of this strategy can be passed to the DSL to customize the snapshot naming * like this: @@ -25,6 +26,9 @@ *

* Customizing the naming strategy is especially needed for paramterized tests, as * automatic naming would choose the same snapshot name for each execution. + *

+ * Note that you can not change the extension of snapshot files which will always be + * .snapshot. * * @see ChooseName * @since 1.1.0 @@ -87,7 +91,8 @@ static SnapshotNaming withParameters(Object parameter1, Object... furtherParamet * * @param testMethod The test method. * @param counter Serial number of snapshot assertions within that test method (0 - * based). + * based). Will be incremented for every terminal DSL operation within the + * same test method. * @return The name of the snapshot. */ String determineSnapshotName(Method testMethod, int counter); diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java index df7afbf5..0fadd241 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java @@ -9,7 +9,7 @@ import de.skuzzle.test.snapshots.io.DirectoryResolver; import de.skuzzle.test.snapshots.validation.State; -class DetermineSnapshotDirectory { +final class DetermineSnapshotDirectory { static Path forTestclass(Class testClass) { final Path testDirectoryLegacy = snapshotDirectoryLegacy(testClass); @@ -41,7 +41,11 @@ private static boolean isDefaultValue(Class type) { private static Path pathFromStrategy(Class testClass, SnapshotDirectory directory) { try { - return newInstanceOf(directory.determinedBy()).determineSnapshotDirectory(testClass, directory); + final Path snapshotDirectory = newInstanceOf(directory.determinedBy()) + .determineSnapshotDirectory(testClass, directory); + State.check(snapshotDirectory != null, "Custom SnapshotDirectoryStrategy %s returned null for %s", + directory.determinedBy().getName(), directory); + return snapshotDirectory; } catch (final SnapshotException e) { throw new IllegalStateException( "Error determining snapshot directory from strategy " + directory.determinedBy(), e); From 05425df277d7ec9e293a438f1b1b86326718fe53 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Fri, 25 Nov 2022 14:47:13 +0100 Subject: [PATCH 04/27] Improve docs --- .../snapshots/DeleteOrphanedSnapshots.java | 8 ++++- .../snapshots/SnapshotDirectoryStrategy.java | 4 +++ .../skuzzle/test/snapshots/SnapshotFile.java | 6 +++- .../test/snapshots/SnapshotNaming.java | 2 +- .../test/snapshots/SnapshotTestResult.java | 5 ++-- .../skuzzle/test/snapshots/package-info.java | 29 ++++++++++++++++++- 6 files changed, 48 insertions(+), 6 deletions(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DeleteOrphanedSnapshots.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DeleteOrphanedSnapshots.java index 7c038dc8..9bc623c4 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DeleteOrphanedSnapshots.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DeleteOrphanedSnapshots.java @@ -24,7 +24,7 @@ * apply: *

    *
  1. the test class (as stated in the snapshot's header) does not exist anymore
  2. - *
  3. the snapshot file is not located in the test class's snapshot directory
  4. + *
  5. the snapshot file is not located in the test class's snapshot directory (*)
  6. *
  7. the test method (as stated in the header) does not exist in that class anymore
  8. *
  9. the test method is no longer a snapshot test (does not have a {@link Snapshot} * parameter
  10. @@ -32,6 +32,12 @@ *
  11. the test method completed successfully but did not produce the snapshot file
  12. *
*

+ * (*) The directory of a snapshot test or even single assertion can be chosen dynamically + * by either using {@link Snapshot#in(java.nio.file.Path)} or by specifying a + * {@link SnapshotDirectoryStrategy} using {@link SnapshotDirectory}. In these cases, we + * can not statically determine whether a snapshot file still resides in the correct + * directory. Orphan detection might lead to inaccurate results in these cases. + *

* Besides using this annotations, orphans can also be deleted by passing the system * property deleteOrphanedSnapshots (case insensitive) to the JVM. * diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectoryStrategy.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectoryStrategy.java index ab489128..29554259 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectoryStrategy.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectoryStrategy.java @@ -17,6 +17,10 @@ public interface SnapshotDirectoryStrategy { /** * Determine the directory into which snapshots will be persisted. + *

+ * Note that, when returning a path outside src/test/resources, snapshots stored there + * are currently not subjected to static orphan detection (see + * {@link DeleteOrphanedSnapshots} for details). * * @param testClass The test class for which snapshot tests are enabled. * @param directory The {@link SnapshotDirectory} annotation instance. diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotFile.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotFile.java index 8c4f16f8..3d3e856d 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotFile.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotFile.java @@ -25,10 +25,14 @@ * snapshot. The header is a simple key-value format which is separated from the actual * snapshot contents by two line breaks (\n). *

- * This class is immutable. + * You can use this class to manually read the contents of a persisted snapshot file. + *

+ * This class is immutable and only holds the file's contents. It is not a reference to + * the actually stored file. * * @author Simon Taddiken * @since 0.0.5 + * @see SnapshotTestResult */ @API(status = Status.EXPERIMENTAL) public final class SnapshotFile { diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotNaming.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotNaming.java index a78bbfb9..f5ddec24 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotNaming.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotNaming.java @@ -24,7 +24,7 @@ * snapshot.namedAccordingTo(MyCustomNamingScheme.getInstance()).assertThat(...)... * *

- * Customizing the naming strategy is especially needed for paramterized tests, as + * Customizing the naming strategy is especially needed for parameterized tests, as * automatic naming would choose the same snapshot name for each execution. *

* Note that you can not change the extension of snapshot files which will always be diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java index 9e6dc2bf..b6301f22 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java @@ -11,7 +11,8 @@ import de.skuzzle.test.snapshots.validation.Arguments; /** - * Result details of a single snapshot assertion. + * Result details of a single snapshot assertion. An instance of this class is returned by + * every DSL terminal operation. * * @author Simon Taddiken * @since 0.0.2 @@ -152,7 +153,7 @@ public enum SnapshotStatus { */ ASSERTED, /** - * No assertion has been performed and the snapshot file has not been created nor + * No assertion has been performed and the snapshot file has not been created or * updated. * * @since 1.5.0 diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/package-info.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/package-info.java index 54515bf8..dff0aa33 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/package-info.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/package-info.java @@ -1,5 +1,32 @@ /** - * The main public snapshot-testing API. + *

The main public snapshot-testing API.

+ *

+ * Snapshot tests are enabled for a test class via + * {@link de.skuzzle.test.snapshots.EnableSnapshotTests}. This will allow you to inject an + * instance of {@link de.skuzzle.test.snapshots.SnapshotDsl.Snapshot} into any test method + * within said test class. + * + *

+ * @EnableSnapshotTests
+ * class SomethingSomethingTest {
+ *
+ *     @Test
+ *     void test_to_string_representation(Snapshot snapshot) throws Exception {
+ *         Object actualTestResult = subject.codeUnterTest(...);
+ *         snapshot.assertThat(actualTestResult)
+ *             .asText()
+ *             .matchesSnapshotText();
+ *     }
+ *
+ *     @Test
+ *     void test_json_serialization(Snapshot snapshot) throws Exception {
+ *         Object actualTestResult = subject.codeUnterTest(...);
+ *         snapshot.assertThat(actualTestResult)
+ *             .as(XmlSnapshot.xml)
+ *             .matchesSnapshotStructure();
+ *     }
+ * }
+ * 
* * @see de.skuzzle.test.snapshots.EnableSnapshotTests * @see de.skuzzle.test.snapshots.SnapshotDsl.Snapshot From 59f29b5dda656fc07e0fafa552228582b2a41c4f Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Tue, 29 Nov 2022 15:21:23 +0100 Subject: [PATCH 05/27] Deprecate EnabledSnapshotTests Closes #19 --- .gitattributes | 3 +- RELEASE_NOTES.md | 1 + readme/RELEASE_NOTES.md | 5 + .../test/snapshots/EnableSnapshotTests.java | 21 +- .../test/snapshots/SnapshotTestResult.java | 7 + .../impl/DefaultSnapshotConfiguration.java | 12 +- .../impl/DetermineSnapshotDirectory.java | 6 +- .../impl/LegacySnapshotConfiguration.java | 75 ++++ .../snapshots/impl/SnapshotConfiguration.java | 28 +- .../snapshots/impl/SnapshotTestContext.java | 12 +- .../snapshots/junit5/EnableSnapshotTests.java | 119 ++++++ .../junit5/JUnit5SnapshotExtension.java | 7 +- .../Junit5SnapshotTestContextProvider.java | 29 +- .../junit5/LegacyJUnit5SnapshotExtension.java | 18 + .../test/snapshots/junit5/package-info.java | 10 +- .../test/snapshots/SnapshotsLegacyTest.java | 203 ++++++++++ .../skuzzle/test/snapshots/SnapshotsTest.java | 1 + .../DetermineSnapshotDirectoryLegacyTest.java | 99 +++++ .../impl/DetermineSnapshotDirectoryTest.java | 25 +- .../impl/FailingSnapshotTestsNew.java | 348 ++++++++++++++++++ .../impl/OrphanedSnapshotDetectionTest.java | 2 +- .../phil.snapshot | 15 + .../simon.snapshot | 15 + ...tMultipleSnapshotsInOneTestCase_0.snapshot | 15 + ...tMultipleSnapshotsInOneTestCase_1.snapshot | 15 + .../testParameterized_0_string1.snapshot | 7 + .../testParameterized_0_string2.snapshot | 7 + ...chSnapshotHasAlreadyBeenCreated_0.snapshot | 15 + ...chSnapshotHasAlreadyBeenCreated_1.snapshot | 15 + ...ichSnapshotHasNotYetBeenCreated_1.snapshot | 15 + .../testWithSnapshot_0.snapshot | 7 + .../testWithSnapshot_0.snapshot | 7 + .../testWithSnapshot_0.snapshot | 7 + .../testPassNullToSnapshot_0.snapshot | 7 + .../testWithSnapshot_0.snapshot | 8 + .../testWithSnapshot_0.snapshot | 7 + .../testWithSnapshot_0.snapshot | 7 + .../testWithSnapshot_1.snapshot | 7 + .../testWithSnapshot_0.snapshot | 7 + .../testWithSnapshot_0.snapshot | 7 + .../snapshots/html/HtmlSnapshotTests.java | 2 +- .../test/snapshots/json/SnapshotsTest.java | 2 +- .../test/snapshots/xml/SnapshotsTest.java | 2 +- 43 files changed, 1168 insertions(+), 59 deletions(-) create mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/LegacySnapshotConfiguration.java create mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/EnableSnapshotTests.java create mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/LegacyJUnit5SnapshotExtension.java create mode 100644 snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsLegacyTest.java create mode 100644 snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectoryLegacyTest.java create mode 100644 snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew.java create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/phil.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/simon.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testMultipleSnapshotsInOneTestCase_0.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testMultipleSnapshotsInOneTestCase_1.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testParameterized_0_string1.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testParameterized_0_string2.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated_0.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated_1.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreated_1.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseForceUpdateAnnotationOnTestMethod_snapshots/testWithSnapshot_0.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseForceUpdateFromAnnotationOnTestClass_snapshots/testWithSnapshot_0.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseJustUpdate_snapshots/testWithSnapshot_0.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseOfNullInputSnapshotAlreadyExists_snapshots/testPassNullToSnapshot_0.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatchWithWhitespaces_snapshots/testWithSnapshot_0.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatch_snapshots/testWithSnapshot_0.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$MultipleAssertions_snapshots/testWithSnapshot_0.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$MultipleAssertions_snapshots/testWithSnapshot_1.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$WhitespacesDuringStructureTextCompare_snapshots/testWithSnapshot_0.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$WhitespacesDuringTextCompare_snapshots/testWithSnapshot_0.snapshot diff --git a/.gitattributes b/.gitattributes index cf968373..474667b2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ -snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTests$FailBecauseSnapshotMismatchWithWhitespaces_snapshots/testWithSnapshot_0.snapshot eol=crlf \ No newline at end of file +snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTests$FailBecauseSnapshotMismatchWithWhitespaces_snapshots/testWithSnapshot_0.snapshot eol=crlf +snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatchWithWhitespaces_snapshots/testWithSnapshot_0.snapshot eol=crlf \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 628f2bd4..2c9724e8 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,4 @@ +* Deprecate `EnableSnapshotTests.snapshotDirectory` in favor of new annotation `@SnapshotDirectory` * Deprecate `SnapshotTestResult.serializedSnapshot` in favor of `SnapshotTestResult.snapshotFile` * Add `SnapshotTestResult.serializedActual` diff --git a/readme/RELEASE_NOTES.md b/readme/RELEASE_NOTES.md index 591f7194..71161f3c 100644 --- a/readme/RELEASE_NOTES.md +++ b/readme/RELEASE_NOTES.md @@ -1,7 +1,12 @@ +* [#19](https://github.com/skuzzle/snapshot-tests/issues/19): Deprecate the whole `@EnabledSnapshotTests` annotation in favor of annotation with same name within `junit5` package +* [#30](https://github.com/skuzzle/snapshot-tests/issues/30): Deprecate `EnableSnapshotTests.softAssertions`. Soft assertions will no longer be supported in the next major version * Deprecate `EnableSnapshotTests.snapshotDirectory` in favor of new annotation `@SnapshotDirectory` * Deprecate `SnapshotTestResult.serializedSnapshot` in favor of `SnapshotTestResult.snapshotFile` * Add `SnapshotTestResult.serializedActual` +_Note_: This release comes with a few major deprecations that are preparing our transition to the next major version +that is 2.0. That version will likely see all those deprecated methods to be removed. In general, simple drop in +replacements are provided to ensure an easy migration. Maven Central coordinates for this release: diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java index dfc2b5be..d943346e 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java @@ -12,9 +12,11 @@ import de.skuzzle.test.snapshots.SnapshotDsl.ChooseAssertions; import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; -import de.skuzzle.test.snapshots.junit5.JUnit5SnapshotExtension; +import de.skuzzle.test.snapshots.junit5.LegacyJUnit5SnapshotExtension; /** + *

Enabling snapshot tests

+ *

* Enables the snapshot-test capabilities. When you mark a class with this annotation, you * can use snapshot assertions by declaring a parameter of type {@link Snapshot} in your * test case like this: @@ -104,11 +106,18 @@ * @see SnapshotDirectory * @see DeleteOrphanedSnapshots * @see ForceUpdateSnapshots + * @deprecated Since 1.7.0 - This class is deprecated in favor of the + * {@link de.skuzzle.test.snapshots.junit5.EnableSnapshotTests} class within + * the junit5 package. Note that the variant in the + * junit5 package already comes with all the deprecated methods + * removed and replaced with their respective alternatives. For details, + * inspect the deprecation notes on the attributes within this class. */ @Retention(RUNTIME) @Target({ TYPE }) -@ExtendWith(JUnit5SnapshotExtension.class) -@API(status = Status.STABLE) +@ExtendWith(LegacyJUnit5SnapshotExtension.class) +@API(status = Status.DEPRECATED, since = "1.7.0") +@Deprecated(since = "1.7.0", forRemoval = true) public @interface EnableSnapshotTests { /** @@ -119,7 +128,7 @@ * @return The relative directory to store the snapshots. * @deprecated Since 1.7.0 - Use {@link SnapshotDirectory} annotation instead. */ - @Deprecated(since = "1.7.0") + @Deprecated(since = "1.7.0", forRemoval = true) @API(status = Status.DEPRECATED, since = "1.7.0") String snapshotDirectory() default ""; @@ -140,7 +149,7 @@ * @see ForceUpdateSnapshots */ @API(status = Status.DEPRECATED, since = "1.1.0") - @Deprecated(since = "1.1.0") + @Deprecated(since = "1.1.0", forRemoval = true) boolean forceUpdateSnapshots() default false; /** @@ -155,7 +164,7 @@ * @deprecated Since 1.7.0 - Soft assertions will no longer be supported from version * 2.0 on. You could use AssertJ's SoftAssertions as a replacement. */ - @Deprecated(since = "1.7.0") + @Deprecated(since = "1.7.0", forRemoval = true) @API(status = Status.DEPRECATED, since = "1.7.0") boolean softAssertions() default false; } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java index b6301f22..a9ac8772 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java @@ -8,6 +8,8 @@ import org.apiguardian.api.API; import org.apiguardian.api.API.Status; +import de.skuzzle.test.snapshots.SnapshotDsl.ChooseAssertions; +import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; import de.skuzzle.test.snapshots.validation.Arguments; /** @@ -97,6 +99,11 @@ public SnapshotFile snapshotFile() { * Returns the serialized string value of the actual test input. Note that this value * can be different from the contents of {@link #snapshotFile()} (see the method's * documentation for details). + *

+ * Note that this value can be null in case you passed a null value into + * {@link Snapshot#assertThat(Object)}. Unless you called + * {@link ChooseAssertions#disabled()}, passing a null value into a snapshot test will + * fail the test anyway. * * @return The serialized actual value. * @since 1.7.0 diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java index dd4198b7..7d496359 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java @@ -7,7 +7,6 @@ import org.apiguardian.api.API.Status; import de.skuzzle.test.snapshots.DeleteOrphanedSnapshots; -import de.skuzzle.test.snapshots.EnableSnapshotTests; import de.skuzzle.test.snapshots.ForceUpdateSnapshots; import de.skuzzle.test.snapshots.validation.Arguments; @@ -54,12 +53,7 @@ public boolean isDeleteOrphanedSnapshots() { @Override public boolean isForceUpdateSnapshotsGlobal() { - // Annotation on test class - final boolean valueFromLegacyAnnotation = testClass() - .getAnnotation(EnableSnapshotTests.class) - .forceUpdateSnapshots(); - if (valueFromLegacyAnnotation - || testClass().isAnnotationPresent(ForceUpdateSnapshots.class)) { + if (testClass().isAnnotationPresent(ForceUpdateSnapshots.class)) { return true; } @@ -81,8 +75,6 @@ public boolean isForceUpdateSnapshotsLocal(Method testMethod) { @Override public boolean isSoftAssertions() { - return testClass() - .getAnnotation(EnableSnapshotTests.class) - .softAssertions(); + return false; } } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java index 0fadd241..c9b26e8f 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java @@ -61,9 +61,9 @@ private static SnapshotDirectoryStrategy newInstanceOf(Class testClass) { - final EnableSnapshotTests snapshotAssertions = testClass - .getAnnotation(EnableSnapshotTests.class); - if (snapshotAssertions.snapshotDirectory().isEmpty()) { + final EnableSnapshotTests snapshotAssertions = testClass.getAnnotation(EnableSnapshotTests.class); + + if (snapshotAssertions == null || snapshotAssertions.snapshotDirectory().isEmpty()) { return null; } final String testDirName = snapshotAssertions.snapshotDirectory(); diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/LegacySnapshotConfiguration.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/LegacySnapshotConfiguration.java new file mode 100644 index 00000000..022155ba --- /dev/null +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/LegacySnapshotConfiguration.java @@ -0,0 +1,75 @@ +package de.skuzzle.test.snapshots.impl; + +import java.lang.reflect.Method; +import java.nio.file.Path; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +import de.skuzzle.test.snapshots.EnableSnapshotTests; + +/** + * + * @author Simon Taddiken + * @since 1.7.0 + * @deprecated Since 1.7.0 - Only introduced for compatibility reasons. Will be removed + * with 2.0.0 + */ +@Deprecated(since = "1.7.0", forRemoval = true) +@API(status = Status.DEPRECATED, since = "1.7.0") +final class LegacySnapshotConfiguration implements SnapshotConfiguration { + + private static final String FORCE_UPDATE_SYSTEM_PROPERTY = "forceUpdateSnapshots"; + private static final String DELETE_ORPHANS_SYSTEM_PROPERTY = "deleteOrphanedSnapshots"; + + private final Class testClass; + private final SnapshotConfiguration delegate; + + private LegacySnapshotConfiguration(Class testClass) { + this.delegate = DefaultSnapshotConfiguration.forTestClass(testClass); + this.testClass = testClass; + } + + public static SnapshotConfiguration forTestClass(Class testClass) { + return new LegacySnapshotConfiguration(testClass); + } + + @Override + public Path determineSnapshotDirectory() { + return delegate.determineSnapshotDirectory(); + } + + @Override + public Class testClass() { + return delegate.testClass(); + } + + @Override + public boolean isDeleteOrphanedSnapshots() { + return delegate.isDeleteOrphanedSnapshots(); + } + + @Override + public boolean isForceUpdateSnapshotsGlobal() { + // Annotation on test class + final boolean valueFromLegacyAnnotation = testClass() + .getAnnotation(EnableSnapshotTests.class) + .forceUpdateSnapshots(); + if (valueFromLegacyAnnotation) { + return true; + } + return delegate.isForceUpdateSnapshotsGlobal(); + } + + @Override + public boolean isForceUpdateSnapshotsLocal(Method testMethod) { + return delegate.isForceUpdateSnapshotsLocal(testMethod) || isForceUpdateSnapshotsGlobal(); + } + + @Override + public boolean isSoftAssertions() { + return testClass() + .getAnnotation(EnableSnapshotTests.class) + .softAssertions(); + } +} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotConfiguration.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotConfiguration.java index 00b5ade1..37e90c32 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotConfiguration.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotConfiguration.java @@ -14,7 +14,29 @@ * @see DefaultSnapshotConfiguration */ @API(status = Status.INTERNAL, since = "1.1.0") -interface SnapshotConfiguration { +public interface SnapshotConfiguration { + + /** + * Creates a SnapshotConfiguration for the given test class. + * + * @param testClass The test class. + * @return The configuration. + * @since 1.7.0 + */ + @API(status = Status.INTERNAL, since = "1.7.0") + static SnapshotConfiguration defaultConfigurationFor(Class testClass) { + return DefaultSnapshotConfiguration.forTestClass(testClass); + } + + /** + * + * @deprecated Since 1.7.0 - Only introduced for backward compatibility. + */ + @Deprecated(since = "1.7.0", forRemoval = true) + @API(status = Status.DEPRECATED, since = "1.7.0") + static SnapshotConfiguration legacyConfigurationFor(Class testClass) { + return LegacySnapshotConfiguration.forTestClass(testClass); + } /** * Determines the directory into which the snapshot files for the currently executed @@ -61,7 +83,11 @@ interface SnapshotConfiguration { * results are collected and processed at once when the test method finishes. * * @return Whether to use soft assertions. + * @deprecated Since 1.7.0 - Soft assertion will no longer be supported with version + * 2.0 */ + @Deprecated(since = "1.7.0", forRemoval = true) + @API(status = Status.DEPRECATED, since = "1.7.0") boolean isSoftAssertions(); } \ No newline at end of file diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestContext.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestContext.java index ac6a3301..ee9c6646 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestContext.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestContext.java @@ -21,6 +21,15 @@ /** * Context object that pertains to the execution of a whole test class which is annotated * with {@link EnableSnapshotTests}. + *

+ * In order to initiate the run of a test class which uses snapshot assertions, you need + * to meet two requirements: + *

    + *
  1. You need to obtain a {@link SnapshotConfiguration} instance from the resp. test + * class via {@link SnapshotConfiguration#defaultConfigurationFor(Class)}. You can then + * create a {@link SnapshotTestContext} instance from that configuration.
  2. + *
  3. You need to call the lifecycle methods of the context object.
  4. + *
* * @author Simon Taddiken * @since 1.1.0 @@ -40,12 +49,13 @@ private SnapshotTestContext(SnapshotConfiguration snapshotConfiguration) { "snapshotConfiguration must not be null"); } + @Deprecated(since = "1.7.0") public static SnapshotTestContext forTestClass(Class testClass) { final SnapshotConfiguration configuration = DefaultSnapshotConfiguration.forTestClass(testClass); return new SnapshotTestContext(configuration); } - static SnapshotTestContext forConfiguration(SnapshotConfiguration snapshotConfiguration) { + public static SnapshotTestContext forConfiguration(SnapshotConfiguration snapshotConfiguration) { return new SnapshotTestContext(snapshotConfiguration); } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/EnableSnapshotTests.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/EnableSnapshotTests.java new file mode 100644 index 00000000..8b0a9ec3 --- /dev/null +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/EnableSnapshotTests.java @@ -0,0 +1,119 @@ +package de.skuzzle.test.snapshots.junit5; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.junit.jupiter.api.extension.ExtendWith; + +import de.skuzzle.test.snapshots.DeleteOrphanedSnapshots; +import de.skuzzle.test.snapshots.ForceUpdateSnapshots; +import de.skuzzle.test.snapshots.SnapshotDirectory; +import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; +import de.skuzzle.test.snapshots.SnapshotNaming; +import de.skuzzle.test.snapshots.StructuredDataProvider; + +/** + *

Enabling snapshot tests

+ *

+ * Enables the snapshot-test capabilities. When you mark a class with this annotation, you + * can use snapshot assertions by declaring a parameter of type {@link Snapshot} in your + * test case like this: + * + *

+ * @EnableSnapshotTests
+ * class MyTestClass {
+ *
+ *     @Test
+ *     void testSomething(Snapshot snapshot) throws Exception {
+ *         Object actual = ...
+ *         snapshot.assertThat(actual).asText().matchesSnapshotText();
+ *     }
+ * }
+ * 
+ *

+ * asText() will 'serialize' actual test results using + * {@link Object#toString()}. There are additional {@link StructuredDataProvider} + * implementations that allow to serialize snapshots as json, xml or html. To use them, + * you need to declare their respective maven modules as dependency. + * + *

+ *     @Test
+ *     void testSomething(Snapshot snapshot) throws Exception {
+ *         Object actual = ...
+ *         snapshot.assertThat(actual).as(TextSnapshot.text).matchesSnapshotText();
+ *         snapshot.assertThat(actual).as(JsonSnapshot.json).matchesSnapshotText();
+ *         snapshot.assertThat(actual).as(XmlSnapshot.xml).matchesSnapshotText();
+ *     }
+ * 
+ *

+ * When providing a structured data format like json/xml (or in general: an implementation + * of {@link StructuredDataProvider}) you can make use of structural assertions + * to compare snapshots. Depending on the implementation, those might provide better error + * messages than plain text comparison. + * + *

+ *     @Test
+ *     void testSomething(Snapshot snapshot) throws Exception {
+ *         Object actual = ...
+ *         snapshot.assertThat(actual).as(JsonSnapshot.json).matchesSnapshotStructure();
+ *         snapshot.assertThat(actual).as(XmlSnapshot.xml).matchesSnapshotStructure();
+ *     }
+ * 
+ * + *

Parameterized tests

+ *

+ * Snapshot tests can be combined with JUnit5's parameterized tests, but only when you + * provide an explicit name for each snapshot assertion. With the default automatic + * snapshot naming scheme, snapshots would otherwise be overridden for each parameterized + * execution. + * + *

+ *     @ParameterizedTest
+ *     @Values(strings = { "string1", "string2" })
+ *     void testSomething(String parameter, Snapshot snapshot) throws Exception {
+ *         Object actual = ...
+ *
+ *         // BAD: would choose the same snapshot file name 'testSomething_0.snapshot' disregarding the parameter
+ *         // (Note: this could be desired if you expect the same output for all parameters)
+ *         snapshot.assertThat(actual).as...;
+ *
+ *         // GOOD: Append the parameter's value to the snapshot name to have separate snapshots per execution
+ *         // This will create snapshots named 'testSomething_0_string1.snapshot' and 'testSomething_0_string2.snapshot'
+ *         snapshot.namedAccordingTo(SnapshotNaming.withParameters(parameter))
+ *                 .assertThat(actual).as...;
+ * 
+ * + *

Updating snapshots

+ *

+ * Snapshots can become outdated when your code under test changes on purpose. In that + * case you can advice the framework to override existing snapshots with your code under + * test's actual result by placing the annotation {@link ForceUpdateSnapshots} on either + * the whole snapshot test class or on a single test method. + * + *

Orphaned snapshots

+ *

+ * Snapshot files can become orphans if, for example you rename a test class/method or you + * change the snapshot assertions within a test. This framework comes with a sophisticated + * approach for detecting those orphaned files. By default, we will log a warning with the + * found orphan. You can temporarily place the {@link DeleteOrphanedSnapshots} annotation + * on a snapshot test class to have those files deleted automatically. + * + * @author Simon Taddiken + * @see Snapshot + * @see SnapshotNaming + * @see SnapshotDirectory + * @see DeleteOrphanedSnapshots + * @see ForceUpdateSnapshots + */ +@Retention(RUNTIME) +@Target({ TYPE }) +@ExtendWith(JUnit5SnapshotExtension.class) +@API(status = Status.STABLE, since = "1.7.0") +public @interface EnableSnapshotTests { + +} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/JUnit5SnapshotExtension.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/JUnit5SnapshotExtension.java index 782a48e0..1e9693bd 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/JUnit5SnapshotExtension.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/JUnit5SnapshotExtension.java @@ -14,13 +14,8 @@ import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.TestWatcher; -/** - * This class is only public so it can be referenced by the entry point annotation. - * - * @author Simon Taddiken - */ @API(status = Status.INTERNAL) -public final class JUnit5SnapshotExtension implements +class JUnit5SnapshotExtension implements ParameterResolver, BeforeAllCallback, AfterEachCallback, diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/Junit5SnapshotTestContextProvider.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/Junit5SnapshotTestContextProvider.java index 941e1e33..189b2b45 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/Junit5SnapshotTestContextProvider.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/Junit5SnapshotTestContextProvider.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import de.skuzzle.test.snapshots.impl.SnapshotConfiguration; import de.skuzzle.test.snapshots.impl.SnapshotTestContext; /** @@ -54,7 +55,33 @@ public static SnapshotTestContext create(ExtensionContext extensionContext) { }); final var testClass = extensionContext.getRequiredTestClass(); - final var snapshotTestContext = SnapshotTestContext.forTestClass(testClass); + final SnapshotConfiguration snapshotConfiguration = SnapshotConfiguration.defaultConfigurationFor(testClass); + final var snapshotTestContext = SnapshotTestContext.forConfiguration(snapshotConfiguration); + extensionContext.getStore(NAMESPACE).put(KEY_SELF, snapshotTestContext); + return snapshotTestContext; + } + + /** + * Creates a {@link SnapshotTestContext} and attaches it to the given JUnit5 + * {@link ExtensionContext}. The extension context is assumed to be pertaining to the + * test class (as opposed to pertaining to a single test method) + * + * @param extensionContext The extension context to attach to. + * @return The attached {@link SnapshotTestContext}. + * @since 1.7.0 + * @deprecated Since 1.7.0 - Only introduced to handle backward compatibility. + */ + @Deprecated(since = "1.7.0") + public static SnapshotTestContext createLegacy(ExtensionContext extensionContext) { + searchParents(extensionContext) + .ifPresent(existingContext -> { + throw new IllegalStateException( + "There is already a SnapshotTestContext attached to the given ExtensionContext or any of its parents"); + }); + + final var testClass = extensionContext.getRequiredTestClass(); + final SnapshotConfiguration snapshotConfiguration = SnapshotConfiguration.legacyConfigurationFor(testClass); + final var snapshotTestContext = SnapshotTestContext.forConfiguration(snapshotConfiguration); extensionContext.getStore(NAMESPACE).put(KEY_SELF, snapshotTestContext); return snapshotTestContext; } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/LegacyJUnit5SnapshotExtension.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/LegacyJUnit5SnapshotExtension.java new file mode 100644 index 00000000..7b2db8fd --- /dev/null +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/LegacyJUnit5SnapshotExtension.java @@ -0,0 +1,18 @@ +package de.skuzzle.test.snapshots.junit5; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * This class is only public so it can be referenced by the entry point annotation. + * + * @author Simon Taddiken + */ +@API(status = Status.INTERNAL) +public final class LegacyJUnit5SnapshotExtension extends JUnit5SnapshotExtension { + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + Junit5SnapshotTestContextProvider.createLegacy(extensionContext); + } +} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/package-info.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/package-info.java index af2d7596..bc083f5b 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/package-info.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/package-info.java @@ -1,7 +1,13 @@ /** - * JUnit5 extension implementation for snapshot tests. + * JUnit5 extension implementation for snapshot tests. See the documentation for + * {@link de.skuzzle.test.snapshots.junit5.EnableSnapshotTests} for a detailed explanation + * of how to use this extension. + *

+ * Starting with version 1.7.0, this package is no longer considered internal. Instead it + * now represents the officially supported entry point for the snapshot test library when + * using it from junit5. */ -@API(status = Status.INTERNAL) +@API(status = Status.STABLE, since = "1.7.0") package de.skuzzle.test.snapshots.junit5; import org.apiguardian.api.API; diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsLegacyTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsLegacyTest.java new file mode 100644 index 00000000..2cc52b61 --- /dev/null +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsLegacyTest.java @@ -0,0 +1,203 @@ +package de.skuzzle.test.snapshots; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; +import de.skuzzle.test.snapshots.SnapshotTestResult.SnapshotStatus; + +@EnableSnapshotTests +public class SnapshotsLegacyTest { + + @Test + void testDisabledWithNullInput(Snapshot snapshot) throws Exception { + final SnapshotTestResult testResult = snapshot.assertThat(null).asText().disabled(); + assertThat(testResult.serializedActual()).isEqualTo("<>"); + } + + @Test + void testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreated(Snapshot snapshot) throws Exception { + final Person simon = determinePerson(); + final SnapshotTestResult testResultDisabled = snapshot.assertThat(simon).asText().disabled(); + + assertThat(testResultDisabled.status()).isEqualTo(SnapshotStatus.DISABLED); + assertThat(testResultDisabled.targetFile()).doesNotExist(); + + final Person phil = determinePerson().setName("Phil"); + final SnapshotTestResult testResultActive = snapshot.assertThat(phil).asText().matchesSnapshotText(); + + assertThat(testResultActive.status()).isEqualTo(SnapshotStatus.ASSERTED); + } + + @Test + void testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated(Snapshot snapshot) throws Exception { + final Person simon = determinePerson(); + final SnapshotTestResult testResultDisabled = snapshot.assertThat(simon).asText().disabled(); + + assertThat(testResultDisabled.status()).isEqualTo(SnapshotStatus.DISABLED); + assertThat(testResultDisabled.targetFile()).exists(); + + final Person phil = determinePerson().setName("Phil"); + snapshot.assertThat(phil).asText().matchesSnapshotText(); + } + + @Test + void testMultipleSnapshotsInOneTestCase(Snapshot snapshot) throws Throwable { + final Person simon = determinePerson(); + snapshot.assertThat(simon).asText().matchesSnapshotText(); + final Person phil = determinePerson().setName("Phil"); + snapshot.assertThat(phil).asText().matchesSnapshotText(); + } + + @Test + void testWithExplicitSnapshotName(Snapshot snapshot) throws Exception { + final Person simon = determinePerson(); + snapshot.named("simon").assertThat(simon).asText().matchesSnapshotText(); + final Person phil = determinePerson().setName("Phil"); + snapshot.named("phil").assertThat(phil).asText().matchesSnapshotText(); + } + + @ParameterizedTest + @ValueSource(strings = { "string1", "string2" }) + void testParameterized(String param, Snapshot snapshot) { + snapshot.namedAccordingTo(SnapshotNaming.withParameters(param)) + .assertThat(param).asText().matchesSnapshotText(); + } + + private Person determinePerson() { + return new Person() + .setName("Simon") + .setSurname("Taddiken") + .setBirthdate(LocalDate.of(1777, 1, 12)) + .setAddress(new Address() + .setCity("Bielefeld") + .setCountry("Germany") + .setStreet("Gibtsnicht-Straße") + .setNumber("1337") + .setZipCode("4711")); + } + + public static class Address { + private String street; + private String number; + private String zipCode; + private String city; + private String country; + + public String getStreet() { + return this.street; + } + + public Address setStreet(String street) { + this.street = street; + return this; + } + + public String getNumber() { + return this.number; + } + + public Address setNumber(String number) { + this.number = number; + return this; + } + + public String getZipCode() { + return this.zipCode; + } + + public Address setZipCode(String zipCode) { + this.zipCode = zipCode; + return this; + } + + public String getCity() { + return this.city; + } + + public Address setCity(String city) { + this.city = city; + return this; + } + + public String getCountry() { + return this.country; + } + + public Address setCountry(String country) { + this.country = country; + return this; + } + + @Override + public String toString() { + return new StringBuilder() + .append("Street: ").append(street).append("\n") + .append("Number: ").append(number).append("\n") + .append("Zip: ").append(zipCode).append("\n") + .append("City: ").append(city).append("\n") + .append("Country: ").append(country).append("\n") + .toString(); + } + } + + public static class Person { + private String name; + private String surname; + private LocalDate birthdate; + private Address address; + + public String getName() { + return this.name; + } + + public Person setName(String name) { + this.name = name; + return this; + } + + public String getSurname() { + return this.surname; + } + + public Person setSurname(String surname) { + this.surname = surname; + return this; + } + + public LocalDate getBirthdate() { + return this.birthdate; + } + + public Person setBirthdate(LocalDate birthdate) { + this.birthdate = birthdate; + return this; + } + + public Address getAddress() { + return this.address; + } + + public Person setAddress(Address address) { + this.address = address; + return this; + } + + @Override + public String toString() { + return new StringBuilder() + .append("Name: ").append(name).append("\n") + .append("Surname: ").append(surname).append("\n") + .append("Birthdate: ").append(birthdate).append("\n") + .append("Address: ").append(address).append("\n") + .toString(); + } + + } + +} diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java index cba5cea9..dd664520 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java @@ -10,6 +10,7 @@ import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; import de.skuzzle.test.snapshots.SnapshotTestResult.SnapshotStatus; +import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; @EnableSnapshotTests public class SnapshotsTest { diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectoryLegacyTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectoryLegacyTest.java new file mode 100644 index 00000000..6b32d51e --- /dev/null +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectoryLegacyTest.java @@ -0,0 +1,99 @@ +package de.skuzzle.test.snapshots.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import de.skuzzle.test.snapshots.EnableSnapshotTests; +import de.skuzzle.test.snapshots.SnapshotDirectory; +import de.skuzzle.test.snapshots.SnapshotDirectoryStrategy; +import de.skuzzle.test.snapshots.SnapshotException; +import de.skuzzle.test.snapshots.io.DirectoryResolver; + +class DetermineSnapshotDirectoryLegacyTest { + + @Test + void testDirectoryFromLegacy() throws Exception { + final Path snapshotDirectory = DetermineSnapshotDirectory.forTestclass(Legacy.class); + assertThat(snapshotDirectory).isEqualTo(DirectoryResolver.resolve("test")); + } + + @EnableSnapshotTests(snapshotDirectory = "test") + private static class Legacy { + + } + + @Test + void legacy_and_new_annotation_should_raise_exception() throws Exception { + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> DetermineSnapshotDirectory.forTestclass(LegacyAndNewAnnotation.class)); + } + + @EnableSnapshotTests(snapshotDirectory = "test") + @SnapshotDirectory("test") + private static class LegacyAndNewAnnotation { + + } + + @Test + void empty_snapshot_directory_annotation_should_raise_exception() throws Exception { + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> DetermineSnapshotDirectory.forTestclass(EmptyAnnotation.class)); + } + + @EnableSnapshotTests + @SnapshotDirectory + private static class EmptyAnnotation { + + } + + @Test + void testResolveDefaultDirectory() throws Exception { + final Path snapshotDirectory = DetermineSnapshotDirectory.forTestclass(DefaultSnapshotDirectory.class); + assertThat(snapshotDirectory).isEqualTo( + DirectoryResolver.resolve(DefaultSnapshotDirectory.class.getName().replace('.', '/') + "_snapshots")); + } + + @EnableSnapshotTests + private static class DefaultSnapshotDirectory { + + } + + @Test + void testResolveDirectoryFromAnnotationString() throws Exception { + final Path snapshotDirectory = DetermineSnapshotDirectory.forTestclass(FromAnnotationString.class); + assertThat(snapshotDirectory).isEqualTo(DirectoryResolver.resolve("test")); + } + + @EnableSnapshotTests + @SnapshotDirectory("test") + private static class FromAnnotationString { + + } + + @Test + void testResolveDirectoryFromStrategy() throws Exception { + final Path snapshotDirectory = DetermineSnapshotDirectory.forTestclass(FromStrategy.class); + assertThat(snapshotDirectory).isEqualTo(Path.of("directory")); + } + + @EnableSnapshotTests + @SnapshotDirectory(value = "directory", determinedBy = SimpleTestSnapshotDirectoryStrategy.class) + private static class FromStrategy { + + } + + public static class SimpleTestSnapshotDirectoryStrategy implements SnapshotDirectoryStrategy { + + @Override + public Path determineSnapshotDirectory(Class testClass, SnapshotDirectory directory) + throws SnapshotException { + return Path.of(directory.value()); + } + + } + +} diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectoryTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectoryTest.java index 54a734e9..4de7fdd9 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectoryTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectoryTest.java @@ -7,37 +7,14 @@ import org.junit.jupiter.api.Test; -import de.skuzzle.test.snapshots.EnableSnapshotTests; import de.skuzzle.test.snapshots.SnapshotDirectory; import de.skuzzle.test.snapshots.SnapshotDirectoryStrategy; import de.skuzzle.test.snapshots.SnapshotException; import de.skuzzle.test.snapshots.io.DirectoryResolver; +import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; class DetermineSnapshotDirectoryTest { - @Test - void testDirectoryFromLegacy() throws Exception { - final Path snapshotDirectory = DetermineSnapshotDirectory.forTestclass(Legacy.class); - assertThat(snapshotDirectory).isEqualTo(DirectoryResolver.resolve("test")); - } - - @EnableSnapshotTests(snapshotDirectory = "test") - private static class Legacy { - - } - - @Test - void legacy_and_new_annotation_should_raise_exception() throws Exception { - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> DetermineSnapshotDirectory.forTestclass(LegacyAndNewAnnotation.class)); - } - - @EnableSnapshotTests(snapshotDirectory = "test") - @SnapshotDirectory("test") - private static class LegacyAndNewAnnotation { - - } - @Test void empty_snapshot_directory_annotation_should_raise_exception() throws Exception { assertThatExceptionOfType(IllegalStateException.class) diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew.java new file mode 100644 index 00000000..4a15e005 --- /dev/null +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew.java @@ -0,0 +1,348 @@ +package de.skuzzle.test.snapshots.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +import de.skuzzle.test.snapshots.ForceUpdateSnapshots; +import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; +import de.skuzzle.test.snapshots.SnapshotTestResult; +import de.skuzzle.test.snapshots.SnapshotTestResult.SnapshotStatus; +import de.skuzzle.test.snapshots.data.text.TextSnapshot; +import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; + +public class FailingSnapshotTestsNew { + + private final MetaTest frameworkTest = new MetaTest(); + + @Test + void testDetectIncompleteDSLReuse() throws Exception { + frameworkTest.expectTestcase(DetectIncompleteDslReuse.class) + .toAllFailWithExceptionWhich(matches -> matches + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Detected illegal reuse of a DSL stage")); + } + + @EnableSnapshotTests + static class DetectIncompleteDslReuse { + + @Test + void testIllegalReuse(Snapshot snapshot) throws Exception { + MetaTest.assumeMetaTest(); + + snapshot.assertThat(""); + snapshot.assertThat(""); + } + } + + @Test + void testDetectIncompleteDSLUsage() throws Exception { + frameworkTest.expectTestcase(DetectIncompleteDslUsage.class) + .toAllFailWithExceptionWhich(matches -> matches + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Detected incomplete DSL usage")); + } + + @EnableSnapshotTests + static class DetectIncompleteDslUsage { + + @Test + void testOnlyAssert(Snapshot snapshot) throws Exception { + MetaTest.assumeMetaTest(); + + snapshot.assertThat(""); + } + + @Test + void testOnlyDirectory(Snapshot snapshot) throws Exception { + MetaTest.assumeMetaTest(); + + snapshot.in(Paths.get("/")); + } + + @Test + void testOnlyName(Snapshot snapshot) throws Exception { + MetaTest.assumeMetaTest(); + + snapshot.named("whatever"); + } + + @Test + void testNoTerminalOp(Snapshot snapshot) throws Exception { + MetaTest.assumeMetaTest(); + + snapshot.in(Paths.get("/")) + .named("whatever") + .assertThat("") + .as(Object::toString); + } + } + + @Test + void testFailBecauseOfNullInputSnapshotAlreadyExists() throws Throwable { + frameworkTest.expectTestcase(FailBecauseOfNullInputSnapshotAlreadyExists.class) + .toFailWithExceptionWhich() + .isInstanceOf(AssertionError.class) + .hasMessage("Expected actual not to be null but to match stored snapshot:\n\nsnapshot text"); + } + + @EnableSnapshotTests + static class FailBecauseOfNullInputSnapshotAlreadyExists { + + @Test + void testPassNullToSnapshot(Snapshot snapshot) throws Exception { + MetaTest.assumeMetaTest(); + + snapshot.assertThat(null).asText().matchesSnapshotText(); + } + } + + @Test + void testFailBecauseOfNullInputJustUpdateSnapshot() throws Throwable { + frameworkTest.expectTestcase(FailBecauseOfNullInputJustUpdateSnapshot.class) + .toFailWithExceptionWhich() + .isInstanceOf(AssertionError.class) + .hasMessage("Expected actual not to be null in order to take initial snapshot"); + } + + @EnableSnapshotTests + static class FailBecauseOfNullInputJustUpdateSnapshot { + + @Test + void testPassNullToSnapshot(Snapshot snapshot) throws Exception { + MetaTest.assumeMetaTest(); + + snapshot.assertThat(null).asText().justUpdateSnapshot(); + } + } + + @Test + void testFailBecauseOfNullInputInitialSnapshot() throws Throwable { + frameworkTest.expectTestcase(FailBecauseOfNullInputInitialSnapshot.class) + .toFailWithExceptionWhich() + .isInstanceOf(AssertionError.class) + .hasMessage("Expected actual not to be null in order to take initial snapshot"); + } + + @EnableSnapshotTests + static class FailBecauseOfNullInputInitialSnapshot { + + @Test + void testPassNullToSnapshot(Snapshot snapshot) throws Exception { + MetaTest.assumeMetaTest(); + + snapshot.assertThat(null).asText().matchesSnapshotText(); + } + } + + @Test + void testFailBecauseForceUpdateFromAnnotationOnTestClass() throws Throwable { + frameworkTest + .expectTestcase(FailBecauseForceUpdateFromAnnotationOnTestClass.class) + .toFailWithExceptionWhich() + .isInstanceOf(AssertionError.class) + .hasMessage(String.format( + "Snapshots have been updated forcefully.%nRemove 'updateSnapshots = true' attribute from your test class and calls to 'justUpdateSnapshot()' and run the tests again.")); + } + + @EnableSnapshotTests + @ForceUpdateSnapshots + static class FailBecauseForceUpdateFromAnnotationOnTestClass { + + @Test + void testWithSnapshot(Snapshot snapshot) throws Throwable { + MetaTest.assumeMetaTest(); + + final SnapshotTestResult snapshotResult = snapshot.assertThat("test").asText().matchesSnapshotText(); + assertThat(snapshotResult.status()).isEqualTo(SnapshotStatus.UPDATED_FORCEFULLY); + } + } + + @Test + void testFailBecauseJustUpdate() throws Exception { + frameworkTest + .expectTestcase(FailBecauseJustUpdate.class) + .toFailWithExceptionWhich() + .isInstanceOf(AssertionError.class) + .hasMessage(String.format( + "Snapshots have been updated forcefully.%n" + + "Remove 'updateSnapshots = true' attribute from your test class and calls to 'justUpdateSnapshot()' and run the tests again.")); + } + + @EnableSnapshotTests + static class FailBecauseJustUpdate { + + @Test + void testWithSnapshot(Snapshot snapshot) throws Throwable { + MetaTest.assumeMetaTest(); + + final SnapshotTestResult snapshotResult = snapshot.assertThat("test").asText().justUpdateSnapshot(); + assertThat(snapshotResult.status()).isEqualTo(SnapshotStatus.UPDATED_FORCEFULLY); + } + } + + @Test + void testFailBecauseForceUpdateAnnotationOnTestMethod() throws Exception { + frameworkTest.expectTestcase(FailBecauseForceUpdateAnnotationOnTestMethod.class) + .toFailWithExceptionWhich() + .isInstanceOf(AssertionError.class) + .hasMessage(String.format( + "Snapshots have been updated forcefully.%n" + + "Remove 'updateSnapshots = true' attribute from your test class and calls to 'justUpdateSnapshot()' and run the tests again.")); + } + + @EnableSnapshotTests + static class FailBecauseForceUpdateAnnotationOnTestMethod { + + @Test + @ForceUpdateSnapshots + void testWithSnapshot(Snapshot snapshot) throws Throwable { + MetaTest.assumeMetaTest(); + + final SnapshotTestResult snapshotResult = snapshot.assertThat("test").asText().matchesSnapshotText(); + assertThat(snapshotResult.status()).isEqualTo(SnapshotStatus.UPDATED_FORCEFULLY); + } + } + + @Test + void testFailBecauseSnapshotMismatch() throws Throwable { + frameworkTest + .expectTestcase(FailBecauseSnapshotMismatch.class) + .toFailWithExceptionWhich() + .isInstanceOf(AssertionError.class) + .hasMessage(String.format("Stored snapshot doesn't match actual result.%n" + + "%nSnapshot location:%n" + + "\t%s%n" + + "%n" + + "Full unified diff of actual result and stored snapshot:%n" + + "+[NOT ]test", + Path.of("src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatch_snapshots/testWithSnapshot_0.snapshot"))); + } + + @EnableSnapshotTests + static class FailBecauseSnapshotMismatch { + + @Test + void testWithSnapshot(Snapshot snapshot) throws Throwable { + MetaTest.assumeMetaTest(); + + final SnapshotTestResult snapshotResult = snapshot.assertThat("NOT test").asText().matchesSnapshotText(); + assertThat(snapshotResult.status()).isEqualTo(SnapshotStatus.ASSERTED); + } + } + + @Test + void testFailBecauseSnapshotMismatchWithWhitespaces() throws Throwable { + frameworkTest + .expectTestcase(FailBecauseSnapshotMismatchWithWhitespaces.class) + .toFailWithExceptionWhich() + .isInstanceOf(AssertionError.class) + .hasMessage(String.format("Stored snapshot doesn't match actual result.%n" + + "%nSnapshot location:%n" + + "\t%s%n" + + "%n" + + "Full unified diff of actual result and stored snapshot:%n" + + "Strings differ in linebreaks. Expected: 'CRLF(\\r\\n)', Actual encountered: 'LF(\\n)'%n" + + "%n" + + "line-[2]+[4]%n" + + "line-[3]+[5]", + Path.of("src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatchWithWhitespaces_snapshots/testWithSnapshot_0.snapshot"))); + } + + @EnableSnapshotTests + static class FailBecauseSnapshotMismatchWithWhitespaces { + + @Test + void testWithSnapshot(Snapshot snapshot) throws Throwable { + MetaTest.assumeMetaTest(); + + final SnapshotTestResult snapshotResult = snapshot.assertThat("line4\nline5") + .as(TextSnapshot.text().withIgnoreWhitespaces(false).withContextLines(5)) + .matchesSnapshotText(); + assertThat(snapshotResult.status()).isEqualTo(SnapshotStatus.ASSERTED); + } + } + + @Test + void testFailBecauseInitial() throws Throwable { + frameworkTest + .expectTestcase(FailBecauseInitial.class) + .toFailWithExceptionWhich() + .isInstanceOf(AssertionError.class) + .hasMessage(String.format("Snapshots have been created the first time.%n" + + "Run the test again and you should see it succeed.")); + } + + @EnableSnapshotTests + static class FailBecauseInitial { + + @Test + void testWithSnapshot(Snapshot snapshot) throws Throwable { + MetaTest.assumeMetaTest(); + + final SnapshotTestResult snapshotResult = snapshot.assertThat("test").asText().matchesSnapshotText(); + snapshotResult.deleteSnapshot(); + assertThat(snapshotResult.status()).isEqualTo(SnapshotStatus.CREATED_INITIALLY); + } + } + + @Test + void testMultipleAssertions() throws Exception { + frameworkTest + .expectTestcase(MultipleAssertions.class) + .toFailWithExceptionWhich() + .isInstanceOf(AssertionError.class); + } + + @EnableSnapshotTests + static class MultipleAssertions { + @Test + void testWithSnapshot(Snapshot snapshot) throws Throwable { + MetaTest.assumeMetaTest(); + + snapshot.assertThat("test").asText().matchesSnapshotText(); + snapshot.assertThat("test2").asText().matchesSnapshotText(); + } + } + + @Test + void testWhitespacesDuringTextCompare() throws Exception { + frameworkTest + .expectTestcase(WhitespacesDuringTextCompare.class) + .toFailWithExceptionWhich() + .isInstanceOf(AssertionError.class); + } + + @EnableSnapshotTests + static class WhitespacesDuringTextCompare { + @Test + void testWithSnapshot(Snapshot snapshot) throws Throwable { + MetaTest.assumeMetaTest(); + + snapshot.assertThat(" test ").as(TextSnapshot.text().withIgnoreWhitespaces(false)) + .matchesSnapshotText(); + } + } + + @Test + void testWhitespacesDuringStructureTextCompare() throws Exception { + frameworkTest + .expectTestcase(WhitespacesDuringStructureTextCompare.class) + .toFailWithExceptionWhich() + .isInstanceOf(AssertionError.class); + } + + @EnableSnapshotTests + static class WhitespacesDuringStructureTextCompare { + @Test + void testWithSnapshot(Snapshot snapshot) throws Throwable { + MetaTest.assumeMetaTest(); + + snapshot.assertThat(" test ").as(TextSnapshot.text().withIgnoreWhitespaces(false)) + .matchesSnapshotStructure(); + } + } +} diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/OrphanedSnapshotDetectionTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/OrphanedSnapshotDetectionTest.java index 4e056766..920d8161 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/OrphanedSnapshotDetectionTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/OrphanedSnapshotDetectionTest.java @@ -14,11 +14,11 @@ import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; -import de.skuzzle.test.snapshots.EnableSnapshotTests; import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; import de.skuzzle.test.snapshots.SnapshotFile; import de.skuzzle.test.snapshots.SnapshotFile.SnapshotHeader; import de.skuzzle.test.snapshots.io.DirectoryResolver; +import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; @DisplayNameGeneration(ReplaceUnderscores.class) public class OrphanedSnapshotDetectionTest { diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/phil.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/phil.snapshot new file mode 100644 index 00000000..50faf720 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/phil.snapshot @@ -0,0 +1,15 @@ +dynamic-directory: false +snapshot-name: phil +snapshot-number: 1 +test-class: de.skuzzle.test.snapshots.SnapshotsLegacyTest +test-method: testWithExplicitSnapshotName + +Name: Phil +Surname: Taddiken +Birthdate: 1777-01-12 +Address: Street: Gibtsnicht-Straße +Number: 1337 +Zip: 4711 +City: Bielefeld +Country: Germany + diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/simon.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/simon.snapshot new file mode 100644 index 00000000..0f3355d7 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/simon.snapshot @@ -0,0 +1,15 @@ +dynamic-directory: false +snapshot-name: simon +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.SnapshotsLegacyTest +test-method: testWithExplicitSnapshotName + +Name: Simon +Surname: Taddiken +Birthdate: 1777-01-12 +Address: Street: Gibtsnicht-Straße +Number: 1337 +Zip: 4711 +City: Bielefeld +Country: Germany + diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testMultipleSnapshotsInOneTestCase_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testMultipleSnapshotsInOneTestCase_0.snapshot new file mode 100644 index 00000000..57a4a36a --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testMultipleSnapshotsInOneTestCase_0.snapshot @@ -0,0 +1,15 @@ +dynamic-directory: false +snapshot-name: testMultipleSnapshotsInOneTestCase_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.SnapshotsLegacyTest +test-method: testMultipleSnapshotsInOneTestCase + +Name: Simon +Surname: Taddiken +Birthdate: 1777-01-12 +Address: Street: Gibtsnicht-Straße +Number: 1337 +Zip: 4711 +City: Bielefeld +Country: Germany + diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testMultipleSnapshotsInOneTestCase_1.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testMultipleSnapshotsInOneTestCase_1.snapshot new file mode 100644 index 00000000..34b63701 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testMultipleSnapshotsInOneTestCase_1.snapshot @@ -0,0 +1,15 @@ +dynamic-directory: false +snapshot-name: testMultipleSnapshotsInOneTestCase_1 +snapshot-number: 1 +test-class: de.skuzzle.test.snapshots.SnapshotsLegacyTest +test-method: testMultipleSnapshotsInOneTestCase + +Name: Phil +Surname: Taddiken +Birthdate: 1777-01-12 +Address: Street: Gibtsnicht-Straße +Number: 1337 +Zip: 4711 +City: Bielefeld +Country: Germany + diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testParameterized_0_string1.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testParameterized_0_string1.snapshot new file mode 100644 index 00000000..22806bc3 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testParameterized_0_string1.snapshot @@ -0,0 +1,7 @@ +dynamic-directory: false +snapshot-name: testParameterized_0_string1 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.SnapshotsLegacyTest +test-method: testParameterized + +string1 \ No newline at end of file diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testParameterized_0_string2.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testParameterized_0_string2.snapshot new file mode 100644 index 00000000..0e464efe --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testParameterized_0_string2.snapshot @@ -0,0 +1,7 @@ +dynamic-directory: false +snapshot-name: testParameterized_0_string2 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.SnapshotsLegacyTest +test-method: testParameterized + +string2 \ No newline at end of file diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated_0.snapshot new file mode 100644 index 00000000..0590214c --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated_0.snapshot @@ -0,0 +1,15 @@ +dynamic-directory: false +snapshot-name: testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.SnapshotsLegacyTest +test-method: testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated + +Name: Simon +Surname: Taddiken +Birthdate: 1777-01-12 +Address: Street: Gibtsnicht-Straße +Number: 1337 +Zip: 4711 +City: Bielefeld +Country: Germany + diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated_1.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated_1.snapshot new file mode 100644 index 00000000..97714f45 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated_1.snapshot @@ -0,0 +1,15 @@ +dynamic-directory: false +snapshot-name: testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated_1 +snapshot-number: 1 +test-class: de.skuzzle.test.snapshots.SnapshotsLegacyTest +test-method: testWithOneDisabledAssertionForWhichSnapshotHasAlreadyBeenCreated + +Name: Phil +Surname: Taddiken +Birthdate: 1777-01-12 +Address: Street: Gibtsnicht-Straße +Number: 1337 +Zip: 4711 +City: Bielefeld +Country: Germany + diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreated_1.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreated_1.snapshot new file mode 100644 index 00000000..34b1a97b --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsLegacyTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreated_1.snapshot @@ -0,0 +1,15 @@ +dynamic-directory: false +snapshot-name: testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreated_1 +snapshot-number: 1 +test-class: de.skuzzle.test.snapshots.SnapshotsLegacyTest +test-method: testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreated + +Name: Phil +Surname: Taddiken +Birthdate: 1777-01-12 +Address: Street: Gibtsnicht-Straße +Number: 1337 +Zip: 4711 +City: Bielefeld +Country: Germany + diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseForceUpdateAnnotationOnTestMethod_snapshots/testWithSnapshot_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseForceUpdateAnnotationOnTestMethod_snapshots/testWithSnapshot_0.snapshot new file mode 100644 index 00000000..5d3ece79 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseForceUpdateAnnotationOnTestMethod_snapshots/testWithSnapshot_0.snapshot @@ -0,0 +1,7 @@ +dynamic-directory: false +snapshot-name: testWithSnapshot_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.impl.FailingSnapshotTestsNew$FailBecauseForceUpdateAnnotationOnTestMethod +test-method: testWithSnapshot + +test \ No newline at end of file diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseForceUpdateFromAnnotationOnTestClass_snapshots/testWithSnapshot_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseForceUpdateFromAnnotationOnTestClass_snapshots/testWithSnapshot_0.snapshot new file mode 100644 index 00000000..5f4517b8 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseForceUpdateFromAnnotationOnTestClass_snapshots/testWithSnapshot_0.snapshot @@ -0,0 +1,7 @@ +dynamic-directory: false +snapshot-name: testWithSnapshot_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.impl.FailingSnapshotTestsNew$FailBecauseForceUpdateFromAnnotationOnTestClass +test-method: testWithSnapshot + +test \ No newline at end of file diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseJustUpdate_snapshots/testWithSnapshot_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseJustUpdate_snapshots/testWithSnapshot_0.snapshot new file mode 100644 index 00000000..0d359d13 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseJustUpdate_snapshots/testWithSnapshot_0.snapshot @@ -0,0 +1,7 @@ +dynamic-directory: false +snapshot-name: testWithSnapshot_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.impl.FailingSnapshotTestsNew$FailBecauseJustUpdate +test-method: testWithSnapshot + +test \ No newline at end of file diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseOfNullInputSnapshotAlreadyExists_snapshots/testPassNullToSnapshot_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseOfNullInputSnapshotAlreadyExists_snapshots/testPassNullToSnapshot_0.snapshot new file mode 100644 index 00000000..f383383c --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseOfNullInputSnapshotAlreadyExists_snapshots/testPassNullToSnapshot_0.snapshot @@ -0,0 +1,7 @@ +dynamic-directory: false +snapshot-name: testPassNullToSnapshot_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.impl.FailingSnapshotTestsNew$FailBecauseOfNullInputSnapshotAlreadyExists +test-method: testPassNullToSnapshot + +snapshot text \ No newline at end of file diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatchWithWhitespaces_snapshots/testWithSnapshot_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatchWithWhitespaces_snapshots/testWithSnapshot_0.snapshot new file mode 100644 index 00000000..3ddeefc2 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatchWithWhitespaces_snapshots/testWithSnapshot_0.snapshot @@ -0,0 +1,8 @@ +dynamic-directory: false +snapshot-name: testWithSnapshot_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.impl.FailingSnapshotTestsNew$FailBecauseSnapshotMismatchWithWhitespaces +test-method: testWithSnapshot + +line2 +line3 \ No newline at end of file diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatch_snapshots/testWithSnapshot_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatch_snapshots/testWithSnapshot_0.snapshot new file mode 100644 index 00000000..633e98ec --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatch_snapshots/testWithSnapshot_0.snapshot @@ -0,0 +1,7 @@ +dynamic-directory: false +snapshot-name: testWithSnapshot_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.impl.FailingSnapshotTestsNew$FailBecauseSnapshotMismatch +test-method: testWithSnapshot + +test \ No newline at end of file diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$MultipleAssertions_snapshots/testWithSnapshot_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$MultipleAssertions_snapshots/testWithSnapshot_0.snapshot new file mode 100644 index 00000000..b82b3cee --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$MultipleAssertions_snapshots/testWithSnapshot_0.snapshot @@ -0,0 +1,7 @@ +dynamic-directory: false +snapshot-name: testWithSnapshot_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.impl.FailingSnapshotTestsNew$MultipleAssertions +test-method: testWithSnapshot + +test \ No newline at end of file diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$MultipleAssertions_snapshots/testWithSnapshot_1.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$MultipleAssertions_snapshots/testWithSnapshot_1.snapshot new file mode 100644 index 00000000..797fa7d6 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$MultipleAssertions_snapshots/testWithSnapshot_1.snapshot @@ -0,0 +1,7 @@ +dynamic-directory: false +snapshot-name: testWithSnapshot_1 +snapshot-number: 1 +test-class: de.skuzzle.test.snapshots.impl.FailingSnapshotTestsNew$MultipleAssertions +test-method: testWithSnapshot + +test \ No newline at end of file diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$WhitespacesDuringStructureTextCompare_snapshots/testWithSnapshot_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$WhitespacesDuringStructureTextCompare_snapshots/testWithSnapshot_0.snapshot new file mode 100644 index 00000000..ea11e043 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$WhitespacesDuringStructureTextCompare_snapshots/testWithSnapshot_0.snapshot @@ -0,0 +1,7 @@ +dynamic-directory: false +snapshot-name: testWithSnapshot_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.impl.FailingSnapshotTestsNew$WhitespacesDuringStructureTextCompare +test-method: testWithSnapshot + +test \ No newline at end of file diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$WhitespacesDuringTextCompare_snapshots/testWithSnapshot_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$WhitespacesDuringTextCompare_snapshots/testWithSnapshot_0.snapshot new file mode 100644 index 00000000..a52fc31d --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$WhitespacesDuringTextCompare_snapshots/testWithSnapshot_0.snapshot @@ -0,0 +1,7 @@ +dynamic-directory: false +snapshot-name: testWithSnapshot_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.impl.FailingSnapshotTestsNew$WhitespacesDuringTextCompare +test-method: testWithSnapshot + +test \ No newline at end of file diff --git a/snapshot-tests-html/src/test/java/de/skuzzle/test/snapshots/html/HtmlSnapshotTests.java b/snapshot-tests-html/src/test/java/de/skuzzle/test/snapshots/html/HtmlSnapshotTests.java index 55da6dc7..0eb18325 100644 --- a/snapshot-tests-html/src/test/java/de/skuzzle/test/snapshots/html/HtmlSnapshotTests.java +++ b/snapshot-tests-html/src/test/java/de/skuzzle/test/snapshots/html/HtmlSnapshotTests.java @@ -6,10 +6,10 @@ import org.junit.jupiter.api.Test; -import de.skuzzle.test.snapshots.EnableSnapshotTests; import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; import de.skuzzle.test.snapshots.SnapshotTestResult; import de.skuzzle.test.snapshots.data.html.HtmlSnapshot; +import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; @EnableSnapshotTests public class HtmlSnapshotTests { diff --git a/snapshot-tests-jackson/src/test/java/de/skuzzle/test/snapshots/json/SnapshotsTest.java b/snapshot-tests-jackson/src/test/java/de/skuzzle/test/snapshots/json/SnapshotsTest.java index 5b99d4d4..2d7c68ac 100644 --- a/snapshot-tests-jackson/src/test/java/de/skuzzle/test/snapshots/json/SnapshotsTest.java +++ b/snapshot-tests-jackson/src/test/java/de/skuzzle/test/snapshots/json/SnapshotsTest.java @@ -11,11 +11,11 @@ import org.skyscreamer.jsonassert.JSONCompareMode; import org.skyscreamer.jsonassert.comparator.CustomComparator; -import de.skuzzle.test.snapshots.EnableSnapshotTests; import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; import de.skuzzle.test.snapshots.SnapshotTestResult; import de.skuzzle.test.snapshots.SnapshotTestResult.SnapshotStatus; import de.skuzzle.test.snapshots.data.json.JsonSnapshot; +import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; @EnableSnapshotTests public class SnapshotsTest { diff --git a/snapshot-tests-jaxb/src/test/java/de/skuzzle/test/snapshots/xml/SnapshotsTest.java b/snapshot-tests-jaxb/src/test/java/de/skuzzle/test/snapshots/xml/SnapshotsTest.java index 217eb38a..e66eba1c 100644 --- a/snapshot-tests-jaxb/src/test/java/de/skuzzle/test/snapshots/xml/SnapshotsTest.java +++ b/snapshot-tests-jaxb/src/test/java/de/skuzzle/test/snapshots/xml/SnapshotsTest.java @@ -13,11 +13,11 @@ import org.xmlunit.assertj.XmlAssert; import org.xmlunit.diff.DifferenceEvaluators; -import de.skuzzle.test.snapshots.EnableSnapshotTests; import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; import de.skuzzle.test.snapshots.SnapshotTestResult; import de.skuzzle.test.snapshots.SnapshotTestResult.SnapshotStatus; import de.skuzzle.test.snapshots.data.xml.XmlSnapshot; +import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; @EnableSnapshotTests public class SnapshotsTest { From 05fe146de9df5398c85b745242668361cd62971c Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Wed, 30 Nov 2022 09:09:46 +0100 Subject: [PATCH 06/27] Adjust documentation --- RELEASE_NOTES.md | 5 +++++ .../de/skuzzle/test/snapshots/SnapshotTestResult.java | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2c9724e8..79b10763 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,7 +1,12 @@ +* [#19](https://github.com/skuzzle/snapshot-tests/issues/19): Deprecate the whole `@EnabledSnapshotTests` annotation in favor of annotation with same name within `junit5` package +* [#30](https://github.com/skuzzle/snapshot-tests/issues/30): Deprecate `EnableSnapshotTests.softAssertions`. Soft assertions will no longer be supported in the next major version * Deprecate `EnableSnapshotTests.snapshotDirectory` in favor of new annotation `@SnapshotDirectory` * Deprecate `SnapshotTestResult.serializedSnapshot` in favor of `SnapshotTestResult.snapshotFile` * Add `SnapshotTestResult.serializedActual` +_Note_: This release comes with a few major deprecations that are preparing our transition to the next major version +that is 2.0. That version will likely see all those deprecated methods to be removed. In general, simple drop in +replacements are provided to ensure an easy migration. Maven Central coordinates for this release: diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java index a9ac8772..72e7bcd6 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java @@ -34,7 +34,7 @@ private SnapshotTestResult(Path targetFile, SnapshotStatus status, SnapshotFile this.targetFile = Arguments.requireNonNull(targetFile); this.status = Arguments.requireNonNull(status); this.snapshot = Arguments.requireNonNull(snapshotFile); - this.serializedActual = serializedActual; + this.serializedActual = Arguments.requireNonNull(serializedActual); this.failure = failure; } @@ -100,10 +100,10 @@ public SnapshotFile snapshotFile() { * can be different from the contents of {@link #snapshotFile()} (see the method's * documentation for details). *

- * Note that this value can be null in case you passed a null value into + * Note that this value can be a placeholder in case you passed a null value into * {@link Snapshot#assertThat(Object)}. Unless you called - * {@link ChooseAssertions#disabled()}, passing a null value into a snapshot test will - * fail the test anyway. + * {@link ChooseAssertions#disabled()}, passing a null value into a snapshot test + * would fail the test anyway. * * @return The serialized actual value. * @since 1.7.0 From 543eb40247b5a890ba10c87192374e3eeb59285d Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Wed, 30 Nov 2022 09:26:18 +0100 Subject: [PATCH 07/27] clean up --- .../test/snapshots/impl/LegacySnapshotConfiguration.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/LegacySnapshotConfiguration.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/LegacySnapshotConfiguration.java index 022155ba..f3936532 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/LegacySnapshotConfiguration.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/LegacySnapshotConfiguration.java @@ -19,15 +19,10 @@ @API(status = Status.DEPRECATED, since = "1.7.0") final class LegacySnapshotConfiguration implements SnapshotConfiguration { - private static final String FORCE_UPDATE_SYSTEM_PROPERTY = "forceUpdateSnapshots"; - private static final String DELETE_ORPHANS_SYSTEM_PROPERTY = "deleteOrphanedSnapshots"; - - private final Class testClass; private final SnapshotConfiguration delegate; private LegacySnapshotConfiguration(Class testClass) { this.delegate = DefaultSnapshotConfiguration.forTestClass(testClass); - this.testClass = testClass; } public static SnapshotConfiguration forTestClass(Class testClass) { From 5cde6214c3568b97c698d7e6a0e60f98ddb2a6e4 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Wed, 30 Nov 2022 11:19:06 +0100 Subject: [PATCH 08/27] Support jakarta xml namespaces Closes #32 --- README.md | 2 +- RELEASE_NOTES.md | 21 +- pom.xml | 7 +- readme/README.md | 2 +- readme/RELEASE_NOTES.md | 21 +- snapshot-tests-bom/pom.xml | 5 + .../test/snapshots/impl/SnapshotTestImpl.java | 3 +- snapshot-tests-dependencies/pom.xml | 13 +- snapshot-tests-html/pom.xml | 9 +- snapshot-tests-jaxb-jakarta/pom.xml | 109 +++++++ .../data/xml/CachedJAXBContexts.java | 26 ++ .../data/xml/JaxbXmlSnapshotSerializer.java | 76 +++++ .../data/xml/StringXmlPrettyPrint.java | 38 +++ .../test/snapshots/data/xml/XmlSnapshot.java | 207 ++++++++++++ .../test/snapshots/data/xml/package-info.java | 8 + .../test/snapshots/xml/SnapshotsTest.java | 301 ++++++++++++++++++ .../testAsXmlNoRootObject_0.snapshot | 19 ++ ...estAsXmlStructureCompareBuilder_0.snapshot | 19 ++ ...tAsXmlStructureCompareCustomNew_0.snapshot | 19 ++ .../testAsXmlStructureCompare_0.snapshot | 19 ++ ...ctureCustomStructuralAssertions_0.snapshot | 19 ++ .../testAsXmlTextCompare_0.snapshot | 19 ++ ...mlAlreadyAStringWithPrettyPrint_0.snapshot | 10 + ...lreadyAStringWithoutPrettyPrint_0.snapshot | 7 + snapshot-tests-jaxb/pom.xml | 18 +- .../test/FilesAreProperlyProcessedTest.java | 7 +- ...testBomFilePlaceholdersResolved_0.snapshot | 23 +- 27 files changed, 984 insertions(+), 43 deletions(-) create mode 100644 snapshot-tests-jaxb-jakarta/pom.xml create mode 100644 snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/CachedJAXBContexts.java create mode 100644 snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/JaxbXmlSnapshotSerializer.java create mode 100644 snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/StringXmlPrettyPrint.java create mode 100644 snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/XmlSnapshot.java create mode 100644 snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/package-info.java create mode 100644 snapshot-tests-jaxb-jakarta/src/test/java/de/skuzzle/test/snapshots/xml/SnapshotsTest.java create mode 100644 snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlNoRootObject_0.snapshot create mode 100644 snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCompareBuilder_0.snapshot create mode 100644 snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCompareCustomNew_0.snapshot create mode 100644 snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCompare_0.snapshot create mode 100644 snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCustomStructuralAssertions_0.snapshot create mode 100644 snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlTextCompare_0.snapshot create mode 100644 snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testXmlAlreadyAStringWithPrettyPrint_0.snapshot create mode 100644 snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testXmlAlreadyAStringWithoutPrettyPrint_0.snapshot diff --git a/README.md b/README.md index 9cc1146c..e67477df 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ actual object against the stored snapshot. Supported snapshot formats: - [x] generic plain text via [snapshot-tests-core](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-core/1.7.0-SNAPSHOT/jar) - [x] Json via [snapshot-tests-jackson](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jackson/1.7.0-SNAPSHOT/jar) -- [x] XML via [snapshot-tests-jaxb](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.7.0-SNAPSHOT/jar) +- [x] XML via [snapshot-tests-jaxb](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.7.0-SNAPSHOT/jar) and [snapshot-tests-jakarta](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jakarta/1.7.0-SNAPSHOT/jar) - [x] HTML via [snapshot-tests-html](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-html/1.7.0-SNAPSHOT/jar) Read more about snapshot testing in this accompanying [blog post](https://simon.taddiken.net/the-case-for-snapshot-testing/). diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 79b10763..a3556d17 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,6 @@ * [#19](https://github.com/skuzzle/snapshot-tests/issues/19): Deprecate the whole `@EnabledSnapshotTests` annotation in favor of annotation with same name within `junit5` package * [#30](https://github.com/skuzzle/snapshot-tests/issues/30): Deprecate `EnableSnapshotTests.softAssertions`. Soft assertions will no longer be supported in the next major version +* [#32](https://github.com/skuzzle/snapshot-tests/issues/32): Support for jakarta namespaces via new `snapshot-tests-jaxb-jakarta` module * Deprecate `EnableSnapshotTests.snapshotDirectory` in favor of new annotation `@SnapshotDirectory` * Deprecate `SnapshotTestResult.serializedSnapshot` in favor of `SnapshotTestResult.snapshotFile` * Add `SnapshotTestResult.serializedActual` @@ -66,7 +67,7 @@ testImplementation 'de.skuzzle.test:snapshot-tests-jackson:1.7.0-SNAPSHOT' testImplementation("de.skuzzle.test:snapshot-tests-jackson:1.7.0-SNAPSHOT") ``` -If you need xml based snapshots (includes `-core`): +If you need xml based snapshots using `javax.xml` legacy namespaces (includes `-core`): [![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-jaxb/1.7.0-SNAPSHOT) @@ -84,6 +85,24 @@ testImplementation 'de.skuzzle.test:snapshot-tests-jaxb:1.7.0-SNAPSHOT' testImplementation("de.skuzzle.test:snapshot-tests-jaxb:1.7.0-SNAPSHOT") ``` +If you need xml based snapshots using new `jakarta.xml` namespaces (includes `-core`): + +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb-jakarta/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-jaxb-jakarta/1.7.0-SNAPSHOT) + +```xml + + de.skuzzle.test + snapshot-tests-jaxb-jakarta + 1.7.0-SNAPSHOT + test + +``` + +``` +testImplementation 'de.skuzzle.test:snapshot-tests-jaxb-jakarta:1.7.0-SNAPSHOT' +testImplementation("de.skuzzle.test:snapshot-tests-jaxb-jakarta:1.7.0-SNAPSHOT") +``` + If you need HTML based snapshots (includes `-core`): [![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-html/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-html/1.7.0-SNAPSHOT) diff --git a/pom.xml b/pom.xml index b7cd46b7..4b5a2220 100644 --- a/pom.xml +++ b/pom.xml @@ -33,8 +33,10 @@ 3.22.0 1.2.0 - 2.3.1 - 2.3.3 + 2.3.1 + 2.3.3 + 4.0.1 + 2.9.0 1.15.3 @@ -52,6 +54,7 @@ snapshot-tests-normalize snapshot-tests-jackson snapshot-tests-jaxb + snapshot-tests-jaxb-jakarta snapshot-tests-html snapshot-tests-xmlunit snapshot-tests-directory-params diff --git a/readme/README.md b/readme/README.md index b73da466..8fb9d5a2 100644 --- a/readme/README.md +++ b/readme/README.md @@ -15,7 +15,7 @@ actual object against the stored snapshot. Supported snapshot formats: - [x] generic plain text via [snapshot-tests-core](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-core/${project.version}/jar) - [x] Json via [snapshot-tests-jackson](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-jackson/${project.version}/jar) -- [x] XML via [snapshot-tests-jaxb](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-jaxb/${project.version}/jar) +- [x] XML via [snapshot-tests-jaxb](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-jaxb/${project.version}/jar) and [snapshot-tests-jakarta](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-jakarta/${project.version}/jar) - [x] HTML via [snapshot-tests-html](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-html/${project.version}/jar) Read more about snapshot testing in this accompanying [blog post](https://simon.taddiken.net/the-case-for-snapshot-testing/). diff --git a/readme/RELEASE_NOTES.md b/readme/RELEASE_NOTES.md index 71161f3c..c38dfada 100644 --- a/readme/RELEASE_NOTES.md +++ b/readme/RELEASE_NOTES.md @@ -1,5 +1,6 @@ * [#19](https://github.com/skuzzle/snapshot-tests/issues/19): Deprecate the whole `@EnabledSnapshotTests` annotation in favor of annotation with same name within `junit5` package * [#30](https://github.com/skuzzle/snapshot-tests/issues/30): Deprecate `EnableSnapshotTests.softAssertions`. Soft assertions will no longer be supported in the next major version +* [#32](https://github.com/skuzzle/snapshot-tests/issues/32): Support for jakarta namespaces via new `snapshot-tests-jaxb-jakarta` module * Deprecate `EnableSnapshotTests.snapshotDirectory` in favor of new annotation `@SnapshotDirectory` * Deprecate `SnapshotTestResult.serializedSnapshot` in favor of `SnapshotTestResult.snapshotFile` * Add `SnapshotTestResult.serializedActual` @@ -66,7 +67,7 @@ testImplementation '${project.groupId}:snapshot-tests-jackson:${project.version} testImplementation("${project.groupId}:snapshot-tests-jackson:${project.version}") ``` -If you need xml based snapshots (includes `-core`): +If you need xml based snapshots using `javax.xml` legacy namespaces (includes `-core`): [![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=${project.version}&color=blue)](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-jaxb/${project.version}/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=${project.version}&color=orange)](http://www.javadoc.io/doc/${project.groupId}/snapshot-tests-jaxb/${project.version}) @@ -84,6 +85,24 @@ testImplementation '${project.groupId}:snapshot-tests-jaxb:${project.version}' testImplementation("${project.groupId}:snapshot-tests-jaxb:${project.version}") ``` +If you need xml based snapshots using new `jakarta.xml` namespaces (includes `-core`): + +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=${project.version}&color=blue)](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-jaxb-jakarta/${project.version}/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=${project.version}&color=orange)](http://www.javadoc.io/doc/${project.groupId}/snapshot-tests-jaxb-jakarta/${project.version}) + +```xml + + ${project.groupId} + snapshot-tests-jaxb-jakarta + ${project.version} + test + +``` + +``` +testImplementation '${project.groupId}:snapshot-tests-jaxb-jakarta:${project.version}' +testImplementation("${project.groupId}:snapshot-tests-jaxb-jakarta:${project.version}") +``` + If you need HTML based snapshots (includes `-core`): [![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=${project.version}&color=blue)](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-html/${project.version}/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=${project.version}&color=orange)](http://www.javadoc.io/doc/${project.groupId}/snapshot-tests-html/${project.version}) diff --git a/snapshot-tests-bom/pom.xml b/snapshot-tests-bom/pom.xml index 63d447a6..30e4b141 100644 --- a/snapshot-tests-bom/pom.xml +++ b/snapshot-tests-bom/pom.xml @@ -31,6 +31,11 @@ snapshot-tests-jaxb ${project.version} + + ${project.groupId} + snapshot-tests-jaxb-jakarta + ${project.version} + ${project.groupId} snapshot-tests-html diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java index 4c3254ab..2ccf9d18 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java @@ -269,8 +269,7 @@ private AssertionError toDiffableAssertionError(AssertionError original, String private TextDiff determineDiff(AssertionError original, String storedSnapshot, String serializedActual) { if (original instanceof TextDiffAssertionError) { // this is to reuse the diff that has already been created during text - // comparison - // in TextDiffStructuralAssertions + // comparison in TextDiffStructuralAssertions return ((TextDiffAssertionError) original).textDiff(); } else { return TextDiff.diffOf(storedSnapshot, serializedActual, DEFAULT_CONTEXT_LINES); diff --git a/snapshot-tests-dependencies/pom.xml b/snapshot-tests-dependencies/pom.xml index a122cbcd..3a4327e1 100644 --- a/snapshot-tests-dependencies/pom.xml +++ b/snapshot-tests-dependencies/pom.xml @@ -37,17 +37,6 @@ import - - javax.xml.bind - jaxb-api - ${version.jaxb-api} - - - org.glassfish.jaxb - jaxb-runtime - ${version.glassfish-jaxb} - - org.apiguardian apiguardian-api @@ -70,7 +59,7 @@ xmlunit-assertj ${version.xmlunit} - + org.assertj assertj-core diff --git a/snapshot-tests-html/pom.xml b/snapshot-tests-html/pom.xml index c51df07c..1ba9ce69 100644 --- a/snapshot-tests-html/pom.xml +++ b/snapshot-tests-html/pom.xml @@ -38,6 +38,10 @@ ${project.groupId} snapshot-tests-xmlunit + + ${project.groupId} + snapshot-tests-common + org.apiguardian @@ -48,10 +52,7 @@ org.jsoup jsoup - - ${project.groupId} - snapshot-tests-common - + org.xmlunit xmlunit-core diff --git a/snapshot-tests-jaxb-jakarta/pom.xml b/snapshot-tests-jaxb-jakarta/pom.xml new file mode 100644 index 00000000..1f3075ab --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/pom.xml @@ -0,0 +1,109 @@ + + + 4.0.0 + + de.skuzzle.test + snapshot-tests-parent + 1.7.0-SNAPSHOT + + + snapshot-tests-jaxb-jakarta + jar + + Snapshot XML Serialization + XML snapshot serialization using jaxb with new jakarta namespaces + + + de.skuzzle.test.snapshots.xml.jaxbjakarta + + + + + + ${project.groupId} + snapshot-tests-dependencies + ${project.version} + pom + import + + + + org.glassfish.jaxb + jaxb-bom + ${version.glassfish-jaxb-jakarta} + pom + import + + + + + + + ${project.groupId} + snapshot-tests-core + + + ${project.groupId} + snapshot-tests-xmlunit + + + ${project.groupId} + snapshot-tests-common + + + + org.apiguardian + apiguardian-api + + + + jakarta.xml.bind + jakarta.xml.bind-api + + + org.glassfish.jaxb + jaxb-runtime + + + + org.xmlunit + xmlunit-core + compile + + + org.xmlunit + xmlunit-assertj + compile + + + + + org.junit.jupiter + junit-jupiter-api + compile + + + org.junit.platform + junit-platform-engine + test + + + org.assertj + assertj-core + compile + + + + + + + maven-dependency-plugin + + + org.glassfish.jaxb:jaxb-runtime + + + + + + diff --git a/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/CachedJAXBContexts.java b/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/CachedJAXBContexts.java new file mode 100644 index 00000000..bbd8aad9 --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/CachedJAXBContexts.java @@ -0,0 +1,26 @@ +package de.skuzzle.test.snapshots.data.xml; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; + +final class CachedJAXBContexts { + + private static final Map, JAXBContext> cache = new ConcurrentHashMap<>(); + + public static JAXBContext getOrCreateContext(Object actual) { + return cache.computeIfAbsent(actual.getClass(), type -> { + try { + return JAXBContext.newInstance(type); + } catch (final JAXBException e) { + throw new IllegalArgumentException("Could not create JAXBContext for " + type, e); + } + }); + } + + private CachedJAXBContexts() { + // hidden + } +} diff --git a/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/JaxbXmlSnapshotSerializer.java b/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/JaxbXmlSnapshotSerializer.java new file mode 100644 index 00000000..e7d9acb0 --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/JaxbXmlSnapshotSerializer.java @@ -0,0 +1,76 @@ +package de.skuzzle.test.snapshots.data.xml; + +import java.io.StringWriter; + +import javax.xml.namespace.QName; + +import de.skuzzle.test.snapshots.SnapshotException; +import de.skuzzle.test.snapshots.SnapshotSerializer; +import de.skuzzle.test.snapshots.data.xml.XmlSnapshot.MarshallerSupplier; +import de.skuzzle.test.snapshots.validation.Arguments; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBElement; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import jakarta.xml.bind.annotation.XmlRootElement; + +final class JaxbXmlSnapshotSerializer implements SnapshotSerializer { + + private final JAXBContext jaxb; + private final MarshallerSupplier marshallerSupplier; + private final boolean prettyPrintXmlStrings; + + private JaxbXmlSnapshotSerializer(JAXBContext jaxb, MarshallerSupplier marshallerSupplier, + boolean prettyPrintXmlStrings) { + this.jaxb = jaxb; + this.marshallerSupplier = Arguments.requireNonNull(marshallerSupplier); + this.prettyPrintXmlStrings = prettyPrintXmlStrings; + } + + public static SnapshotSerializer withExplicitJaxbContext( + JAXBContext jaxb, + MarshallerSupplier marshallerSupplier, boolean prettyPrintXmlStrings) { + return new JaxbXmlSnapshotSerializer(jaxb, marshallerSupplier, prettyPrintXmlStrings); + } + + private JAXBContext inferJaxbContext(Object object) { + return jaxb == null + ? CachedJAXBContexts.getOrCreateContext(object) + : jaxb; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Object wrapIntoRootObject(Object testResult) { + if (testResult.getClass().isAnnotationPresent(XmlRootElement.class) || testResult instanceof JAXBElement) { + return testResult; + } + + return new JAXBElement<>( + new QName(testResult.getClass().getSimpleName()), + (Class) testResult.getClass(), + testResult); + } + + @Override + public String serialize(Object testResult) throws SnapshotException { + try { + if (testResult instanceof String) { + if (prettyPrintXmlStrings) { + return StringXmlPrettyPrint.prettyPrint(testResult.toString()); + } + return testResult.toString(); + } + + final JAXBContext jaxbContext = inferJaxbContext(testResult); + final StringWriter writer = new StringWriter(); + final Marshaller marshaller = marshallerSupplier.createMarshaller(jaxbContext); + + final Object wrapped = wrapIntoRootObject(testResult); + marshaller.marshal(wrapped, writer); + return writer.toString(); + } catch (final JAXBException e) { + throw new SnapshotException("Error serializing object to XML: " + testResult, e); + } + } + +} diff --git a/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/StringXmlPrettyPrint.java b/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/StringXmlPrettyPrint.java new file mode 100644 index 00000000..18e2ebbc --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/StringXmlPrettyPrint.java @@ -0,0 +1,38 @@ +package de.skuzzle.test.snapshots.data.xml; + +import java.io.StringReader; +import java.io.StringWriter; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +final class StringXmlPrettyPrint { + + private static final TransformerFactory TRANS = TransformerFactory.newInstance(); + + public static String prettyPrint(String doc) { + try { + final Transformer tf = createTransformer(); + // initialize StreamResult with File object to save to file + final StreamResult result = new StreamResult(new StringWriter()); + final StreamSource source = new StreamSource(new StringReader(doc)); + tf.transform(source, result); + return result.getWriter().toString() + .replace("?><", String.format("?>%n<")) // soap + .replace("\" xmlns:", String.format("\"%n\t\t\txmlns:")); + } catch (final TransformerException e) { + throw new IllegalStateException("Error while pretty printing XML", e); + } + } + + private static Transformer createTransformer() throws TransformerException { + final Transformer transformer = TRANS.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + return transformer; + } +} diff --git a/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/XmlSnapshot.java b/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/XmlSnapshot.java new file mode 100644 index 00000000..a69f0cca --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/XmlSnapshot.java @@ -0,0 +1,207 @@ +package de.skuzzle.test.snapshots.data.xml; + +import java.util.function.Consumer; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.xmlunit.assertj.CompareAssert; +import org.xmlunit.diff.DifferenceEvaluator; + +import de.skuzzle.test.snapshots.ComparisonRuleBuilder; +import de.skuzzle.test.snapshots.SnapshotSerializer; +import de.skuzzle.test.snapshots.StructuralAssertions; +import de.skuzzle.test.snapshots.StructuredData; +import de.skuzzle.test.snapshots.StructuredDataProvider; +import de.skuzzle.test.snapshots.data.xmlunit.XmlUnitComparisonRuleBuilder; +import de.skuzzle.test.snapshots.data.xmlunit.XmlUnitStructuralAssertions; +import de.skuzzle.test.snapshots.validation.Arguments; +import de.skuzzle.test.snapshots.validation.State; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; + +/** + * {@link StructuredData} builder for serializing test results to XML, relying on JAXB and + * XML-Unit. This implementation relies on jakarta-xml implementation. + *

+ * You can either use a pre-configured default instance via {@link #xml} or use any of the + * static factory methods to customize the construction. + * + * @author Simon Taddiken + * @since 1.7.0 + */ +@API(status = Status.STABLE, since = "1.7.0") +public final class XmlSnapshot implements StructuredDataProvider { + + /** + * Simple default {@link StructuredData} instance which infers the JAXB context from a + * test's actual result object. + *

+ * If you need control over how the {@link JAXBContext} and the {@link Marshaller} are + * being set up, use the static factory methods in {@link XmlSnapshot} instead of this + * static constant. + * + * @see #xml() + */ + public static final StructuredDataProvider xml = xml().build(); + + // If left null, the JAXBContext will be inferred from the actual test result. + private JAXBContext jaxbContext; + // Creates the Marshaller from the JAXBContext + private MarshallerSupplier marshallerSupplier; + // Defines how snapshots are being asserted on using xml-unit + private Consumer compareAssertConsumer = CompareAssert::areIdentical; + // null unless customized + private DifferenceEvaluator differenceEvaluator; + // used only when actual test result is already a string + private boolean prettyPrintStringXml = true; + + private boolean enableXPathDebugging = false; + + private XmlSnapshot() { + this.marshallerSupplier = ctx -> { + final Marshaller marshaller = ctx.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + return marshaller; + }; + } + + /** + * Creates a new XML {@link StructuredDataProvider} which will try to infer the + * {@link JAXBContext} from the actual test result. + * + * @return A builder for building {@link StructuredData}. + * @since 1.4.0 + */ + public static XmlSnapshot xml() { + return new XmlSnapshot(); + } + + /** + * Uses the given {@link JAXBContext} instead of trying to infer it from the test + * result. + * + * @param jaxbContext The JAXBContext to use. + * @return This builder instance. + */ + public XmlSnapshot withJAXBContext(JAXBContext jaxbContext) { + this.jaxbContext = Arguments.requireNonNull(jaxbContext, "jaxbContext must not be null"); + return this; + } + + /** + * Supplies the {@link Marshaller} which will be used to serialize the snapshot to + * xml. + * + * @param marshallerSupplier The supplier. + * @return This builder instance. + */ + public XmlSnapshot withMarshaller(MarshallerSupplier marshallerSupplier) { + this.marshallerSupplier = Arguments.requireNonNull(marshallerSupplier, "marshallerSupplier must not be null"); + return this; + } + + /** + * Only taken into account if you directly pass a String into the snapshot test which + * is already a XML is does not need to be serialized. In this case, you can advise + * the framework to pretty print the passed in string before persisting it as a + * snapshot. + *

+ * For non-xml input (java classes that need to be serialized), pretty printing can be + * controlled via cusomization of the marshaller using + * {@link #withMarshaller(MarshallerSupplier)}. + *

+ * Defaults to true. + * + * @param prettyPrintStringXml Whether to pretty print XML strings. + * @return This build instance. + */ + @API(status = Status.EXPERIMENTAL) + public XmlSnapshot withPrettyPrintStringXml(boolean prettyPrintStringXml) { + this.prettyPrintStringXml = prettyPrintStringXml; + return this; + } + + /** + * Defines which Xml-Assert assertion method will actually be used. Defaults to + * {@link CompareAssert#areIdentical()}. + *

+ * You can also use this to apply further customizations to the CompareAssert. Consult + * the xml-unit documentation for further information. + *

+ * Note: if you also use {@link #withComparisonRules(Consumer)}, you can not + * use {@link CompareAssert#withDifferenceEvaluator(DifferenceEvaluator)} here, as + * your {@linkplain DifferenceEvaluator} will always be overridden by the one that is + * configured in {@linkplain #withComparisonRules(Consumer)}. + * + * @param xmls Consumes the {@link CompareAssert} which compares the actual and + * expected xml. + * @return This builder instance. + */ + @API(status = Status.EXPERIMENTAL) + public XmlSnapshot compareUsing(Consumer xmls) { + this.compareAssertConsumer = Arguments.requireNonNull(xmls, "CompareAssert consumer must not be null"); + return this; + } + + /** + * Enables a simple debug output to System.out for the xpaths that are used in + * {@link #withComparisonRules(Consumer)}. This will print out all the nodes that are + * matched by the xpaths that are used in custom comparison rules. + *

+ * Note that this method must be called before calling + * {@link #withComparisonRules(Consumer)}. + * + * @param enableXPathDebugging Whether to enable debug output for xpaths used in + * {@link #withComparisonRules(Consumer)}. + * @return This instance. + */ + @API(status = Status.EXPERIMENTAL) + public XmlSnapshot withEnableXPathDebugging(boolean enableXPathDebugging) { + State.check(this.differenceEvaluator == null, + "xpath debugging must be enabled before specifying custom comparison rules"); + this.enableXPathDebugging = enableXPathDebugging; + return this; + } + + /** + * Allows to specify extra comparison rules that are applied to certain paths within + * the xml snapshots. + *

+ * Paths on the {@link ComparisonRuleBuilder} must conform to standard XPath syntax. + * You can enable debug output for xpath expressions using + * {@link #withEnableXPathDebugging(boolean)}. Note that debug output must be enabled + * before calling this method. + *

+ * Note: This will customize the {@link DifferenceEvaluator} that is used. Thus you + * can not use this method in combination with {@link #withComparisonRules(Consumer)} + * if you intend to use an own {@link DifferenceEvaluator}. + * + * @param rules A consumer to which a {@link ComparisonRuleBuilder} will be passed. + * @return This instance. + */ + @API(status = Status.EXPERIMENTAL) + public XmlSnapshot withComparisonRules(Consumer rules) { + Arguments.requireNonNull(rules, "rules consumer must not be null"); + final XmlUnitComparisonRuleBuilder comparatorCustomizerImpl = new XmlUnitComparisonRuleBuilder( + this.enableXPathDebugging); + rules.accept(comparatorCustomizerImpl); + this.differenceEvaluator = comparatorCustomizerImpl.build(); + return this; + } + + @Override + public StructuredData build() { + final SnapshotSerializer snapshotSerializer = JaxbXmlSnapshotSerializer.withExplicitJaxbContext( + jaxbContext, marshallerSupplier, prettyPrintStringXml); + final StructuralAssertions structuralAssertions = new XmlUnitStructuralAssertions(compareAssertConsumer, + differenceEvaluator); + return StructuredData.with(snapshotSerializer, structuralAssertions); + } + + @FunctionalInterface + static interface MarshallerSupplier { + + Marshaller createMarshaller(JAXBContext jaxbContext) throws JAXBException; + } +} diff --git a/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/package-info.java b/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/package-info.java new file mode 100644 index 00000000..d82fb171 --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/main/java/de/skuzzle/test/snapshots/data/xml/package-info.java @@ -0,0 +1,8 @@ +/** + * XML snapshot format support using jaxb and xml-unit. + */ +@API(status = Status.STABLE) +package de.skuzzle.test.snapshots.data.xml; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; diff --git a/snapshot-tests-jaxb-jakarta/src/test/java/de/skuzzle/test/snapshots/xml/SnapshotsTest.java b/snapshot-tests-jaxb-jakarta/src/test/java/de/skuzzle/test/snapshots/xml/SnapshotsTest.java new file mode 100644 index 00000000..501d1c7c --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/test/java/de/skuzzle/test/snapshots/xml/SnapshotsTest.java @@ -0,0 +1,301 @@ +package de.skuzzle.test.snapshots.xml; + +import static de.skuzzle.test.snapshots.data.xml.XmlSnapshot.xml; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; +import org.xmlunit.assertj.CompareAssert; +import org.xmlunit.assertj.XmlAssert; +import org.xmlunit.diff.DifferenceEvaluators; + +import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; +import de.skuzzle.test.snapshots.SnapshotTestResult; +import de.skuzzle.test.snapshots.SnapshotTestResult.SnapshotStatus; +import de.skuzzle.test.snapshots.data.xml.XmlSnapshot; +import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; +import jakarta.xml.bind.annotation.XmlRootElement; + +@EnableSnapshotTests +public class SnapshotsTest { + + @Test + void testXmlAlreadyAStringWithPrettyPrint(Snapshot snapshot) throws Exception { + final SnapshotTestResult snapshotResult = snapshot + .assertThat("text").as(xml) + .matchesSnapshotStructure(); + + assertThat(snapshotResult.serializedActual()).isEqualTo(String.format("" + + "%n" + + "%n" + + " text%n" + + "%n")); + } + + @Test + void testXmlAlreadyAStringWithoutPrettyPrint(Snapshot snapshot) throws Exception { + final SnapshotTestResult snapshotResult = snapshot + .assertThat("text") + .as(XmlSnapshot.xml() + .withPrettyPrintStringXml(false)) + .matchesSnapshotStructure(); + + assertThat(snapshotResult.serializedActual()) + .isEqualTo("text"); + } + + @Test + void testAsXmlNoRootObject(Snapshot snapshot) throws Exception { + final PersonWithoutRootElement myself = determinePersonWithoutRootElement(); + final SnapshotTestResult snapshotResult = snapshot.assertThat(myself).as(xml).matchesSnapshotText(); + assertThat(snapshotResult.status()).isEqualTo(SnapshotStatus.ASSERTED); + } + + @Test + void testAsXmlTextCompare(Snapshot snapshot) throws Exception { + final Person myself = determinePerson(); + final SnapshotTestResult snapshotResult = snapshot.assertThat(myself).as(xml).matchesSnapshotText(); + assertThat(snapshotResult.status()).isEqualTo(SnapshotStatus.ASSERTED); + } + + @Test + void testAsXmlStructureCompare(Snapshot snapshot) throws Exception { + final Person myself = determinePerson(); + final SnapshotTestResult snapshotResult = snapshot.assertThat(myself).as(xml).matchesSnapshotStructure(); + assertThat(snapshotResult.status()).isEqualTo(SnapshotStatus.ASSERTED); + } + + @Test + void testAsXmlStructureCompareBuilder(Snapshot snapshot) throws Exception { + final Person myself = determinePerson(); + final SnapshotTestResult snapshotResult = snapshot.assertThat(myself) + .as(XmlSnapshot + .xml() + .compareUsing(xmls -> xmls.withDifferenceEvaluator(DifferenceEvaluators.Default).areSimilar())) + .matchesSnapshotStructure(); + assertThat(snapshotResult.status()).isEqualTo(SnapshotStatus.ASSERTED); + } + + @Test + void testAsXmlStructureCustomStructuralAssertions(Snapshot snapshot) throws Exception { + final Person myself = determinePerson(); + final SnapshotTestResult snapshotResult = snapshot.assertThat(myself) + .as(XmlSnapshot.xml().compareUsing(CompareAssert::areSimilar)) + .matchesAccordingTo((expected, actual) -> XmlAssert.assertThat(actual).and(expected) + .withDifferenceEvaluator(DifferenceEvaluators.ignorePrologDifferences()).areSimilar()); + assertThat(snapshotResult.status()).isEqualTo(SnapshotStatus.ASSERTED); + } + + @Test + void testAsXmlStructureCompareCustomNew(Snapshot snapshot) throws Exception { + final Person myself = determinePerson().setName("0000-02-02"); + final SnapshotTestResult snapshotResult = snapshot.assertThat(myself) + .as(XmlSnapshot.xml() + .withComparisonRules(rules -> rules + .pathAt("/person/address/city/text()").ignore() + .pathAt("/person/name/text()").mustMatch(Pattern.compile("\\d{4}-\\d{2}-\\d{2}")))) + .matchesSnapshotStructure(); + assertThat(snapshotResult.status()).isEqualTo(SnapshotStatus.ASSERTED); + } + + private Person determinePerson() { + return new Person() + .setName("Simon") + .setSurname("Taddiken") + .setBirthdate(LocalDate.of(1777, 1, 12)) + .setAddress(new Address() + .setCity("Bielefeld") + .setCountry("Germany") + .setStreet("Gibtsnicht-Straße") + .setNumber("1337") + .setZipCode("4711")); + } + + private PersonWithoutRootElement determinePersonWithoutRootElement() { + return new PersonWithoutRootElement() + .setName("Simon") + .setSurname("Taddiken") + .setBirthdate(LocalDate.of(1777, 1, 12)) + .setAddress(new Address() + .setCity("Bielefeld") + .setCountry("Germany") + .setStreet("Gibtsnicht-Straße") + .setNumber("1337") + .setZipCode("4711")); + } + + public static class Address { + private String street; + private String number; + private String zipCode; + private String city; + private String country; + + public String getStreet() { + return this.street; + } + + public Address setStreet(String street) { + this.street = street; + return this; + } + + public String getNumber() { + return this.number; + } + + public Address setNumber(String number) { + this.number = number; + return this; + } + + public String getZipCode() { + return this.zipCode; + } + + public Address setZipCode(String zipCode) { + this.zipCode = zipCode; + return this; + } + + public String getCity() { + return this.city; + } + + public Address setCity(String city) { + this.city = city; + return this; + } + + public String getCountry() { + return this.country; + } + + public Address setCountry(String country) { + this.country = country; + return this; + } + + @Override + public String toString() { + return new StringBuilder() + .append("Street: ").append(street).append("\n") + .append("Number: ").append(number).append("\n") + .append("Zip: ").append(zipCode).append("\n") + .append("City: ").append(city).append("\n") + .append("Country: ").append(country).append("\n") + .toString(); + } + } + + @XmlRootElement + public static class Person { + private String name; + private String surname; + private LocalDate birthdate; + private Address address; + + public String getName() { + return this.name; + } + + public Person setName(String name) { + this.name = name; + return this; + } + + public String getSurname() { + return this.surname; + } + + public Person setSurname(String surname) { + this.surname = surname; + return this; + } + + public LocalDate getBirthdate() { + return this.birthdate; + } + + public Person setBirthdate(LocalDate birthdate) { + this.birthdate = birthdate; + return this; + } + + public Address getAddress() { + return this.address; + } + + public Person setAddress(Address address) { + this.address = address; + return this; + } + + @Override + public String toString() { + return new StringBuilder() + .append("Name: ").append(name).append("\n") + .append("Surname: ").append(surname).append("\n") + .append("Birthdate: ").append(birthdate).append("\n") + .append("Address: ").append(address).append("\n") + .toString(); + } + + } + + public static class PersonWithoutRootElement { + private String name; + private String surname; + private LocalDate birthdate; + private Address address; + + public String getName() { + return this.name; + } + + public PersonWithoutRootElement setName(String name) { + this.name = name; + return this; + } + + public String getSurname() { + return this.surname; + } + + public PersonWithoutRootElement setSurname(String surname) { + this.surname = surname; + return this; + } + + public LocalDate getBirthdate() { + return this.birthdate; + } + + public PersonWithoutRootElement setBirthdate(LocalDate birthdate) { + this.birthdate = birthdate; + return this; + } + + public Address getAddress() { + return this.address; + } + + public PersonWithoutRootElement setAddress(Address address) { + this.address = address; + return this; + } + + @Override + public String toString() { + return new StringBuilder() + .append("Name: ").append(name).append("\n") + .append("Surname: ").append(surname).append("\n") + .append("Birthdate: ").append(birthdate).append("\n") + .append("Address: ").append(address).append("\n") + .toString(); + } + + } +} diff --git a/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlNoRootObject_0.snapshot b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlNoRootObject_0.snapshot new file mode 100644 index 00000000..c9ae73f0 --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlNoRootObject_0.snapshot @@ -0,0 +1,19 @@ +dynamic-directory: false +snapshot-name: testAsXmlNoRootObject_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.xml.SnapshotsTest +test-method: testAsXmlNoRootObject + + + +

+ Bielefeld + Germany + 1337 + Gibtsnicht-Straße + 4711 +
+ + Simon + Taddiken + diff --git a/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCompareBuilder_0.snapshot b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCompareBuilder_0.snapshot new file mode 100644 index 00000000..de7b032c --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCompareBuilder_0.snapshot @@ -0,0 +1,19 @@ +dynamic-directory: false +snapshot-name: testAsXmlStructureCompareBuilder_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.xml.SnapshotsTest +test-method: testAsXmlStructureCompareBuilder + + + +
+ Bielefeld + Germany + 1337 + Gibtsnicht-Straße + 4711 +
+ + Simon + Taddiken +
diff --git a/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCompareCustomNew_0.snapshot b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCompareCustomNew_0.snapshot new file mode 100644 index 00000000..b742f6ee --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCompareCustomNew_0.snapshot @@ -0,0 +1,19 @@ +dynamic-directory: false +snapshot-name: testAsXmlStructureCompareCustomNew_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.xml.SnapshotsTest +test-method: testAsXmlStructureCompareCustomNew + + + +
+ not Bielefeld + Germany + 1337 + Gibtsnicht-Straße + 4711 +
+ + 0000-01-01 + Taddiken +
diff --git a/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCompare_0.snapshot b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCompare_0.snapshot new file mode 100644 index 00000000..8fef0279 --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCompare_0.snapshot @@ -0,0 +1,19 @@ +dynamic-directory: false +snapshot-name: testAsXmlStructureCompare_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.xml.SnapshotsTest +test-method: testAsXmlStructureCompare + + + +
+ Bielefeld + Germany + 1337 + Gibtsnicht-Straße + 4711 +
+ + Simon + Taddiken +
diff --git a/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCustomStructuralAssertions_0.snapshot b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCustomStructuralAssertions_0.snapshot new file mode 100644 index 00000000..f7b12ee9 --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlStructureCustomStructuralAssertions_0.snapshot @@ -0,0 +1,19 @@ +dynamic-directory: false +snapshot-name: testAsXmlStructureCustomStructuralAssertions_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.xml.SnapshotsTest +test-method: testAsXmlStructureCustomStructuralAssertions + + + +
+ Bielefeld + Germany + 1337 + Gibtsnicht-Straße + 4711 +
+ + Simon + Taddiken +
diff --git a/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlTextCompare_0.snapshot b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlTextCompare_0.snapshot new file mode 100644 index 00000000..b453bbe1 --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testAsXmlTextCompare_0.snapshot @@ -0,0 +1,19 @@ +dynamic-directory: false +snapshot-name: testAsXmlTextCompare_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.xml.SnapshotsTest +test-method: testAsXmlTextCompare + + + +
+ Bielefeld + Germany + 1337 + Gibtsnicht-Straße + 4711 +
+ + Simon + Taddiken +
diff --git a/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testXmlAlreadyAStringWithPrettyPrint_0.snapshot b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testXmlAlreadyAStringWithPrettyPrint_0.snapshot new file mode 100644 index 00000000..ef6a725f --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testXmlAlreadyAStringWithPrettyPrint_0.snapshot @@ -0,0 +1,10 @@ +dynamic-directory: false +snapshot-name: testXmlAlreadyAStringWithPrettyPrint_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.xml.SnapshotsTest +test-method: testXmlAlreadyAStringWithPrettyPrint + + + + text + diff --git a/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testXmlAlreadyAStringWithoutPrettyPrint_0.snapshot b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testXmlAlreadyAStringWithoutPrettyPrint_0.snapshot new file mode 100644 index 00000000..2cb89f24 --- /dev/null +++ b/snapshot-tests-jaxb-jakarta/src/test/resources/de/skuzzle/test/snapshots/xml/SnapshotsTest_snapshots/testXmlAlreadyAStringWithoutPrettyPrint_0.snapshot @@ -0,0 +1,7 @@ +dynamic-directory: false +snapshot-name: testXmlAlreadyAStringWithoutPrettyPrint_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.xml.SnapshotsTest +test-method: testXmlAlreadyAStringWithoutPrettyPrint + +text \ No newline at end of file diff --git a/snapshot-tests-jaxb/pom.xml b/snapshot-tests-jaxb/pom.xml index 71a4be28..8f68673f 100644 --- a/snapshot-tests-jaxb/pom.xml +++ b/snapshot-tests-jaxb/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 de.skuzzle.test @@ -13,11 +12,11 @@ Snapshot XML Serialization XML snapshot serialization using jaxb - + de.skuzzle.test.snapshots.xml.jaxb - + @@ -29,7 +28,7 @@ - + ${project.groupId} @@ -43,20 +42,23 @@ ${project.groupId} snapshot-tests-common - + org.apiguardian apiguardian-api - + javax.xml.bind jaxb-api + ${version.jaxb-api-legacy} org.glassfish.jaxb jaxb-runtime + ${version.glassfish-jaxb-legacy} + org.xmlunit xmlunit-core @@ -67,7 +69,7 @@ xmlunit-assertj compile - + org.junit.jupiter diff --git a/snapshot-tests-release-test/src/test/java/de/skuzzle/test/snapshots/release/test/FilesAreProperlyProcessedTest.java b/snapshot-tests-release-test/src/test/java/de/skuzzle/test/snapshots/release/test/FilesAreProperlyProcessedTest.java index 9fcdbc41..9bf600c5 100644 --- a/snapshot-tests-release-test/src/test/java/de/skuzzle/test/snapshots/release/test/FilesAreProperlyProcessedTest.java +++ b/snapshot-tests-release-test/src/test/java/de/skuzzle/test/snapshots/release/test/FilesAreProperlyProcessedTest.java @@ -17,12 +17,12 @@ import org.xmlunit.xpath.JAXPXPathEngine; import org.xmlunit.xpath.XPathEngine; -import de.skuzzle.test.snapshots.EnableSnapshotTests; import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; import de.skuzzle.test.snapshots.data.xml.XmlSnapshot; import de.skuzzle.test.snapshots.directoryparams.FilesFrom; import de.skuzzle.test.snapshots.directoryparams.PathFilter; import de.skuzzle.test.snapshots.directoryparams.TestFile; +import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; @EnableSnapshotTests public class FilesAreProperlyProcessedTest { @@ -97,7 +97,10 @@ void testBomFilePlaceholdersResolved(Snapshot snapshot) throws Exception { .withPrettyPrintStringXml(false) .withComparisonRules(rules -> rules .pathAt(XPATH_GROUP_ID).mustMatch(nodeText(EXPECTED_GROUP_ID)) - .pathAt(XPATH_VERSION).mustMatch(MAVEN_VERSION))) + .pathAt(XPATH_VERSION).mustMatch(MAVEN_VERSION)) + .compareUsing(xmls -> xmls + .normalizeWhitespace() + .areIdentical())) .matchesSnapshotStructure(); } diff --git a/snapshot-tests-release-test/src/test/resources/de/skuzzle/test/snapshots/release/test/FilesAreProperlyProcessedTest_snapshots/testBomFilePlaceholdersResolved_0.snapshot b/snapshot-tests-release-test/src/test/resources/de/skuzzle/test/snapshots/release/test/FilesAreProperlyProcessedTest_snapshots/testBomFilePlaceholdersResolved_0.snapshot index 732a5cfb..6fa947b7 100644 --- a/snapshot-tests-release-test/src/test/resources/de/skuzzle/test/snapshots/release/test/FilesAreProperlyProcessedTest_snapshots/testBomFilePlaceholdersResolved_0.snapshot +++ b/snapshot-tests-release-test/src/test/resources/de/skuzzle/test/snapshots/release/test/FilesAreProperlyProcessedTest_snapshots/testBomFilePlaceholdersResolved_0.snapshot @@ -10,7 +10,7 @@ test-method: testBomFilePlaceholdersResolved 4.0.0 de.skuzzle.test snapshot-tests-bom - 1.6.0-SNAPSHOT + 1.7.0-SNAPSHOT pom Snapshot Tests BOM Manages all child artifact versions in case you need to reference multiple in a client project @@ -46,42 +46,47 @@ test-method: testBomFilePlaceholdersResolved de.skuzzle.test snapshot-tests-core - 1.6.0-SNAPSHOT + 1.7.0-SNAPSHOT de.skuzzle.test snapshot-tests-jackson - 1.6.0-SNAPSHOT + 1.7.0-SNAPSHOT de.skuzzle.test snapshot-tests-jaxb - 1.6.0-SNAPSHOT + 1.7.0-SNAPSHOT + + + de.skuzzle.test + snapshot-tests-jaxb-jakarta + 1.7.0-SNAPSHOT de.skuzzle.test snapshot-tests-html - 1.6.0-SNAPSHOT + 1.7.0-SNAPSHOT de.skuzzle.test snapshot-tests-xmlunit - 1.6.0-SNAPSHOT + 1.7.0-SNAPSHOT de.skuzzle.test snapshot-tests-directory-params - 1.6.0-SNAPSHOT + 1.7.0-SNAPSHOT de.skuzzle.test snapshot-tests-normalize - 1.6.0-SNAPSHOT + 1.7.0-SNAPSHOT de.skuzzle.test snapshot-tests-common - 1.6.0-SNAPSHOT + 1.7.0-SNAPSHOT From 08e470de9a6f9d789cfd14e0776ea60caf61d214 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Thu, 1 Dec 2022 08:29:55 +0100 Subject: [PATCH 09/27] Add required xmlunit dependency for jakarta-jaxb --- snapshot-tests-dependencies/pom.xml | 8 +++++++- snapshot-tests-jaxb-jakarta/pom.xml | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/snapshot-tests-dependencies/pom.xml b/snapshot-tests-dependencies/pom.xml index 3a4327e1..c363bcde 100644 --- a/snapshot-tests-dependencies/pom.xml +++ b/snapshot-tests-dependencies/pom.xml @@ -59,12 +59,17 @@ xmlunit-assertj ${version.xmlunit} - + + org.xmlunit + xmlunit-jakarta-jaxb-impl + ${version.xmlunit} + org.assertj assertj-core ${version.assertj} + org.opentest4j opentest4j @@ -97,3 +102,4 @@ + diff --git a/snapshot-tests-jaxb-jakarta/pom.xml b/snapshot-tests-jaxb-jakarta/pom.xml index 1f3075ab..165b1db8 100644 --- a/snapshot-tests-jaxb-jakarta/pom.xml +++ b/snapshot-tests-jaxb-jakarta/pom.xml @@ -55,7 +55,10 @@ org.apiguardian apiguardian-api - + + org.xmlunit + xmlunit-jakarta-jaxb-impl + jakarta.xml.bind jakarta.xml.bind-api From a8980fa9bd9bcefb23d39d59b8d2e3ee849b9585 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Fri, 2 Dec 2022 08:40:07 +0100 Subject: [PATCH 10/27] Declare used dependency --- README.md | 2 +- readme/README.md | 2 +- snapshot-tests-jaxb-jakarta/pom.xml | 1 + snapshot-tests-release-test/pom.xml | 16 +++++++++++++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e67477df..65053ded 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ actual object against the stored snapshot. Supported snapshot formats: - [x] generic plain text via [snapshot-tests-core](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-core/1.7.0-SNAPSHOT/jar) - [x] Json via [snapshot-tests-jackson](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jackson/1.7.0-SNAPSHOT/jar) -- [x] XML via [snapshot-tests-jaxb](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.7.0-SNAPSHOT/jar) and [snapshot-tests-jakarta](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jakarta/1.7.0-SNAPSHOT/jar) +- [x] XML via [snapshot-tests-jaxb](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.7.0-SNAPSHOT/jar) xor [snapshot-tests-jaxb-jakarta](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb-jakarta/1.7.0-SNAPSHOT/jar) - [x] HTML via [snapshot-tests-html](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-html/1.7.0-SNAPSHOT/jar) Read more about snapshot testing in this accompanying [blog post](https://simon.taddiken.net/the-case-for-snapshot-testing/). diff --git a/readme/README.md b/readme/README.md index 8fb9d5a2..5966dc43 100644 --- a/readme/README.md +++ b/readme/README.md @@ -15,7 +15,7 @@ actual object against the stored snapshot. Supported snapshot formats: - [x] generic plain text via [snapshot-tests-core](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-core/${project.version}/jar) - [x] Json via [snapshot-tests-jackson](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-jackson/${project.version}/jar) -- [x] XML via [snapshot-tests-jaxb](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-jaxb/${project.version}/jar) and [snapshot-tests-jakarta](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-jakarta/${project.version}/jar) +- [x] XML via [snapshot-tests-jaxb](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-jaxb/${project.version}/jar) xor [snapshot-tests-jaxb-jakarta](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-jaxb-jakarta/${project.version}/jar) - [x] HTML via [snapshot-tests-html](https://search.maven.org/artifact/${project.groupId}/snapshot-tests-html/${project.version}/jar) Read more about snapshot testing in this accompanying [blog post](https://simon.taddiken.net/the-case-for-snapshot-testing/). diff --git a/snapshot-tests-jaxb-jakarta/pom.xml b/snapshot-tests-jaxb-jakarta/pom.xml index 165b1db8..f9974008 100644 --- a/snapshot-tests-jaxb-jakarta/pom.xml +++ b/snapshot-tests-jaxb-jakarta/pom.xml @@ -104,6 +104,7 @@ org.glassfish.jaxb:jaxb-runtime + org.xmlunit:xmlunit-jakarta-jaxb-impl diff --git a/snapshot-tests-release-test/pom.xml b/snapshot-tests-release-test/pom.xml index 8aa3d395..1efea91c 100644 --- a/snapshot-tests-release-test/pom.xml +++ b/snapshot-tests-release-test/pom.xml @@ -27,6 +27,11 @@ + + ${project.groupId} + snapshot-tests-core + test + ${project.groupId} snapshot-tests-jaxb @@ -47,7 +52,16 @@ xmlunit-assertj test - + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + From ff1556bf43eed1ac3b7a9b267eecfe53b6cc2b99 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Fri, 2 Dec 2022 08:54:03 +0100 Subject: [PATCH 11/27] Replace some deprecated methods --- .../test/snapshots/directoryparams/FilesFromTest.java | 6 ++++-- .../test/snapshots/normalize/ObjectTraversalTest.java | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/snapshot-tests-directory-params/src/test/java/de/skuzzle/test/snapshots/directoryparams/FilesFromTest.java b/snapshot-tests-directory-params/src/test/java/de/skuzzle/test/snapshots/directoryparams/FilesFromTest.java index fab87abd..44ee8e04 100644 --- a/snapshot-tests-directory-params/src/test/java/de/skuzzle/test/snapshots/directoryparams/FilesFromTest.java +++ b/snapshot-tests-directory-params/src/test/java/de/skuzzle/test/snapshots/directoryparams/FilesFromTest.java @@ -6,10 +6,12 @@ import org.junit.jupiter.params.ParameterizedTest; -import de.skuzzle.test.snapshots.EnableSnapshotTests; +import de.skuzzle.test.snapshots.SnapshotDirectory; import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; +import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; -@EnableSnapshotTests(snapshotDirectory = "test-input") +@EnableSnapshotTests +@SnapshotDirectory("test-input") public class FilesFromTest { @ParameterizedTest diff --git a/snapshot-tests-normalize/src/test/java/de/skuzzle/test/snapshots/normalize/ObjectTraversalTest.java b/snapshot-tests-normalize/src/test/java/de/skuzzle/test/snapshots/normalize/ObjectTraversalTest.java index c34e9907..1a168ae5 100644 --- a/snapshot-tests-normalize/src/test/java/de/skuzzle/test/snapshots/normalize/ObjectTraversalTest.java +++ b/snapshot-tests-normalize/src/test/java/de/skuzzle/test/snapshots/normalize/ObjectTraversalTest.java @@ -16,8 +16,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import de.skuzzle.test.snapshots.EnableSnapshotTests; import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; +import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; @EnableSnapshotTests public class ObjectTraversalTest { From 655bd20453a106958879163a39655ebf483a0f36 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Fri, 9 Dec 2022 12:28:33 +0100 Subject: [PATCH 12/27] Update documentation --- .../de/skuzzle/test/snapshots/DeleteOrphanedSnapshots.java | 4 +--- .../java/de/skuzzle/test/snapshots/SnapshotSerializer.java | 7 +++++++ .../de/skuzzle/test/snapshots/StructuralAssertions.java | 5 +++++ .../de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java | 2 +- .../skuzzle/test/snapshots/junit5/EnableSnapshotTests.java | 4 +++- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DeleteOrphanedSnapshots.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DeleteOrphanedSnapshots.java index 9bc623c4..03bbc9b3 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DeleteOrphanedSnapshots.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/DeleteOrphanedSnapshots.java @@ -50,6 +50,4 @@ @Target({ TYPE }) @API(status = Status.EXPERIMENTAL, since = "1.1.0") @Deprecated -public @interface DeleteOrphanedSnapshots { - -} +public @interface DeleteOrphanedSnapshots {} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotSerializer.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotSerializer.java index 8369b425..52bbaee4 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotSerializer.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotSerializer.java @@ -1,5 +1,7 @@ package de.skuzzle.test.snapshots; +import java.nio.charset.Charset; + import org.apiguardian.api.API; import org.apiguardian.api.API.Status; @@ -25,6 +27,11 @@ public interface SnapshotSerializer { /** * Creates a String representation of the provided object. The passed object is * guaranteed to be non-null. + *

+ * It is strongly advised that serializers should produce system-independent output. + * That is, implementors should NOT use {@link System#lineSeparator()}, + * {@link Charset#defaultCharset()} or similar system dependent information to render + * the snapshot string. * * @param testResult The object to serialize. * @return The serialized object. diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/StructuralAssertions.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/StructuralAssertions.java index a734dd49..ab7a320d 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/StructuralAssertions.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/StructuralAssertions.java @@ -18,6 +18,11 @@ public interface StructuralAssertions { /** * Structurally compares two serialized objects. If comparison fails, this method * should throw an {@link AssertionError} with a helpful error message. + *

+ * Note that, if you don't throw an instance of {@link AssertionFailedError} then the + * framework will take care of rethrowing your assertion failure as an + * {@linkplain AssertionFailedError} to make sure that the diff can be viewed in the + * IDE's diff viewer. * * @param storedSnapshot The persisted snapshot. * @param serializedActual The serialized actual test result. diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java index 2ccf9d18..ac095f27 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java @@ -59,7 +59,7 @@ final class SnapshotTestImpl implements Snapshot { SnapshotTestImpl(SnapshotTestContext context, SnapshotConfiguration configuration, Method testMethod) { this.configuration = Arguments.requireNonNull(configuration, "configuration must not be null"); this.testMethod = Arguments.requireNonNull(testMethod, "testMethod must not be null"); - this.context = context; + this.context = Arguments.requireNonNull(context, "context must not be null"); } @Override diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/EnableSnapshotTests.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/EnableSnapshotTests.java index 8b0a9ec3..027b8de5 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/EnableSnapshotTests.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/EnableSnapshotTests.java @@ -13,6 +13,7 @@ import de.skuzzle.test.snapshots.DeleteOrphanedSnapshots; import de.skuzzle.test.snapshots.ForceUpdateSnapshots; import de.skuzzle.test.snapshots.SnapshotDirectory; +import de.skuzzle.test.snapshots.SnapshotDsl.ChooseAssertions; import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; import de.skuzzle.test.snapshots.SnapshotNaming; import de.skuzzle.test.snapshots.StructuredDataProvider; @@ -93,7 +94,8 @@ * Snapshots can become outdated when your code under test changes on purpose. In that * case you can advice the framework to override existing snapshots with your code under * test's actual result by placing the annotation {@link ForceUpdateSnapshots} on either - * the whole snapshot test class or on a single test method. + * the whole snapshot test class or on a single test method. You can also use the + * {@link ChooseAssertions#justUpdateSnapshot()} terminal operation of the DSL. * *

Orphaned snapshots

*

From dc4e6ce53873c8fb43a12aa98700ffd7839528ab Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Fri, 9 Dec 2022 15:14:45 +0100 Subject: [PATCH 13/27] Add feature to persist additional context files --- .gitignore | 5 +- README.md | 25 +++- RELEASE_NOTES.md | 13 +- readme/README.md | 25 +++- readme/RELEASE_NOTES.md | 13 +- .../test/snapshots/EnableSnapshotTests.java | 1 + .../test/snapshots/SnapshotTestOptions.java | 80 +++++++++++++ .../test/snapshots/SnapshotTestResult.java | 55 ++++++++- .../impl/DefaultSnapshotConfiguration.java | 38 +++++- .../impl/DetermineSnapshotDirectory.java | 4 +- .../impl/InternalSnapshotNaming.java | 8 ++ .../impl/LegacySnapshotConfiguration.java | 28 +++-- .../snapshots/impl/SnapshotConfiguration.java | 16 ++- .../test/snapshots/impl/SnapshotTestImpl.java | 112 ++++++++++++++---- .../skuzzle/test/snapshots/SnapshotsTest.java | 34 ++++++ ...eadyBeenCreatedWithContextFiles_0.snapshot | 15 +++ .../testWriteContextFiles_0.snapshot | 15 +++ 17 files changed, 429 insertions(+), 58 deletions(-) create mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasHasAlreadyBeenCreatedWithContextFiles_0.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsTest_snapshots/testWriteContextFiles_0.snapshot diff --git a/.gitignore b/.gitignore index f50ed4f1..e5eca711 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,7 @@ target # IntelliJ .idea/ *.iml -.flattened-pom.xml +*.flattened-pom.xml + +*.snapshot_raw +*.snapshot_actual \ No newline at end of file diff --git a/README.md b/README.md index 65053ded..1191b027 100644 --- a/README.md +++ b/README.md @@ -212,4 +212,27 @@ will log a warning with the found orphan. You can temporarily place the `@Delete snapshot test class to have those files deleted automatically. **Warning:** Deleting orphans should be handled with care. There might be raw occasions where we falsely detect a -snapshot file as orphan (especially if you are running only parts of your test suite or have disabled tests). \ No newline at end of file +snapshot file as orphan (especially if you are running only parts of your test suite or have disabled tests). + +### Configuring some more details +**New** +Since version `1.7.0` there is a new `@SnapshotTestOptions` annotation that can either be placed on a test method or +test class. It allows to configure some details of the snapshot testing engine. + +#### Generating additional context files +Besides persisting the actual snapshot file, the framework can be advised to generate additional context files via +`@SnapshotTestOptions.alwaysPersistActualResult()` and `@SnapshotTestOptions.alwaysPersistRawResult()`. +Disregarding the outcome of the snapshot assertion, these options will advises the framework to always create a file +containing the latest actual test results. The `..._raw` file will contain the pure serialized actual result without +the snapshot header. + +Note that these context files should _not_ be checked into the SCM. You should add these two lines to your `.gitignore` +file: +``` +*.snapshot_raw +*.snapshot_actual +``` + +#### Showing more context in unified diffs +Using `@SnapshotTestOptions.textDiffContextLines()` you can advise the framework to print more lines surrounding a +detected change in the unified diffs. Per default, we will only print 5 lines around a change. \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a3556d17..5f5d4325 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,13 +1,20 @@ * [#19](https://github.com/skuzzle/snapshot-tests/issues/19): Deprecate the whole `@EnabledSnapshotTests` annotation in favor of annotation with same name within `junit5` package * [#30](https://github.com/skuzzle/snapshot-tests/issues/30): Deprecate `EnableSnapshotTests.softAssertions`. Soft assertions will no longer be supported in the next major version * [#32](https://github.com/skuzzle/snapshot-tests/issues/32): Support for jakarta namespaces via new `snapshot-tests-jaxb-jakarta` module +* [#47](https://github.com/skuzzle/snapshot-tests/issues/47): Add new `SnapshotTestOptions` annotation which allows to configure some detailed aspects of the snapshot engine * Deprecate `EnableSnapshotTests.snapshotDirectory` in favor of new annotation `@SnapshotDirectory` -* Deprecate `SnapshotTestResult.serializedSnapshot` in favor of `SnapshotTestResult.snapshotFile` -* Add `SnapshotTestResult.serializedActual` +* Deprecate `SnapshotTestResult.serializedSnapshot()` in favor of `SnapshotTestResult.snapshotFile()` +* Add `SnapshotTestResult.serializedActual()` +* Add `SnapshotTestResult.actualResultFile()` +* Add `SnapshotTestResult.rawActualResultFile()` +* Add the possibility to configure the number of context lines printed around a change in the default unified diff via `@SnapshotTestOptions.textDiffContextLines()` +* Add the possibility to always persist the latest actual result as a sibling file of the real `.snapshot` file via `@SnapshotTestOptions.alwaysPersistActualResult()` +* Add the possibility to additionally persist the raw actual result in a sibling file of the real `.snapshot` file via `@SnapshotTestOptions.alwaysPersistRawResult()`. The raw result does not contain the snapshot header information. + _Note_: This release comes with a few major deprecations that are preparing our transition to the next major version that is 2.0. That version will likely see all those deprecated methods to be removed. In general, simple drop in -replacements are provided to ensure an easy migration. +replacements are provided and documented to ensure an easy migration. Maven Central coordinates for this release: diff --git a/readme/README.md b/readme/README.md index 5966dc43..7faa04ea 100644 --- a/readme/README.md +++ b/readme/README.md @@ -212,4 +212,27 @@ will log a warning with the found orphan. You can temporarily place the `@Delete snapshot test class to have those files deleted automatically. **Warning:** Deleting orphans should be handled with care. There might be raw occasions where we falsely detect a -snapshot file as orphan (especially if you are running only parts of your test suite or have disabled tests). \ No newline at end of file +snapshot file as orphan (especially if you are running only parts of your test suite or have disabled tests). + +### Configuring some more details +**New** +Since version `1.7.0` there is a new `@SnapshotTestOptions` annotation that can either be placed on a test method or +test class. It allows to configure some details of the snapshot testing engine. + +#### Generating additional context files +Besides persisting the actual snapshot file, the framework can be advised to generate additional context files via +`@SnapshotTestOptions.alwaysPersistActualResult()` and `@SnapshotTestOptions.alwaysPersistRawResult()`. +Disregarding the outcome of the snapshot assertion, these options will advises the framework to always create a file +containing the latest actual test results. The `..._raw` file will contain the pure serialized actual result without +the snapshot header. + +Note that these context files should _not_ be checked into the SCM. You should add these two lines to your `.gitignore` +file: +``` +*.snapshot_raw +*.snapshot_actual +``` + +#### Showing more context in unified diffs +Using `@SnapshotTestOptions.textDiffContextLines()` you can advise the framework to print more lines surrounding a +detected change in the unified diffs. Per default, we will only print 5 lines around a change. \ No newline at end of file diff --git a/readme/RELEASE_NOTES.md b/readme/RELEASE_NOTES.md index c38dfada..b68e7c9e 100644 --- a/readme/RELEASE_NOTES.md +++ b/readme/RELEASE_NOTES.md @@ -1,13 +1,20 @@ * [#19](https://github.com/skuzzle/snapshot-tests/issues/19): Deprecate the whole `@EnabledSnapshotTests` annotation in favor of annotation with same name within `junit5` package * [#30](https://github.com/skuzzle/snapshot-tests/issues/30): Deprecate `EnableSnapshotTests.softAssertions`. Soft assertions will no longer be supported in the next major version * [#32](https://github.com/skuzzle/snapshot-tests/issues/32): Support for jakarta namespaces via new `snapshot-tests-jaxb-jakarta` module +* [#47](https://github.com/skuzzle/snapshot-tests/issues/47): Add new `SnapshotTestOptions` annotation which allows to configure some detailed aspects of the snapshot engine * Deprecate `EnableSnapshotTests.snapshotDirectory` in favor of new annotation `@SnapshotDirectory` -* Deprecate `SnapshotTestResult.serializedSnapshot` in favor of `SnapshotTestResult.snapshotFile` -* Add `SnapshotTestResult.serializedActual` +* Deprecate `SnapshotTestResult.serializedSnapshot()` in favor of `SnapshotTestResult.snapshotFile()` +* Add `SnapshotTestResult.serializedActual()` +* Add `SnapshotTestResult.actualResultFile()` +* Add `SnapshotTestResult.rawActualResultFile()` +* Add the possibility to configure the number of context lines printed around a change in the default unified diff via `@SnapshotTestOptions.textDiffContextLines()` +* Add the possibility to always persist the latest actual result as a sibling file of the real `.snapshot` file via `@SnapshotTestOptions.alwaysPersistActualResult()` +* Add the possibility to additionally persist the raw actual result in a sibling file of the real `.snapshot` file via `@SnapshotTestOptions.alwaysPersistRawResult()`. The raw result does not contain the snapshot header information. + _Note_: This release comes with a few major deprecations that are preparing our transition to the next major version that is 2.0. That version will likely see all those deprecated methods to be removed. In general, simple drop in -replacements are provided to ensure an easy migration. +replacements are provided and documented to ensure an easy migration. Maven Central coordinates for this release: diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java index d943346e..e936b2ac 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/EnableSnapshotTests.java @@ -104,6 +104,7 @@ * @see Snapshot * @see SnapshotNaming * @see SnapshotDirectory + * @see SnapshotTestOptions * @see DeleteOrphanedSnapshots * @see ForceUpdateSnapshots * @deprecated Since 1.7.0 - This class is deprecated in favor of the diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java new file mode 100644 index 00000000..4ad4f3ea --- /dev/null +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java @@ -0,0 +1,80 @@ +package de.skuzzle.test.snapshots; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +import de.skuzzle.test.snapshots.data.text.TextSnapshot; + +/** + * Allows to configure some behavior details of the snapshot testing engine. This + * annotation can either be placed on the test class itself or on single test methods. + * Annotations on test methods take precedence over the one on the test class. If the + * annotation is not specified at all, the the documented defaults apply. + *

+ * If you want to globally modify the snapshot directory, see the + * {@link SnapshotDirectory} annotation. + * + * @author Simon Taddiken + * @since 1.7.0 + */ +@API(status = Status.EXPERIMENTAL, since = "1.7.0") +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface SnapshotTestOptions { + /** + * Defines the number of context lines that are printed around a comparison failure. + * Note that this setting only applies to unified diffs created for structural + * comparisons. If you use text comparison, then you can control the amount of context + * lines using {@link TextSnapshot#withContextLines(int)}. + *

+ * Defaults to 5. + * + * @return The number of context lines to print in unified diffs within our structural + * assertion failures. + */ + int textDiffContextLines() default 5; + + /** + * Whether to always persist the latest actual test result in a parallel file next to + * the .snapshot file. The actual result will be persisted in a file with + * identical name but with .snapshot_actual file extension. + *

+ * Note: If you opt into enabling this option, you should add the + * .snapshot_actual to your .gitignore file. Other than the + * normal .snapshot files, the ..._actual files are not + * intended to be checked into the SCM. + *

+ * If this option is disabled, then all existing ..._actual files will be + * proactively deleted by the framework during test execution. + *

+ * Defaults to false. + * + * @return Whether to always persist the latest actual test result. + */ + boolean alwaysPersistActualResult() default false; + + /** + * When enabled, the framework will persist the raw actual result without snapshot + * header as a sibling file to the real .snapshot file with the extension + * .snapshot_raw. + *

+ * Note: If you opt into enabling this option, you should add the + * .snapshot_actual to your .gitignore file. Other than the + * normal .snapshot files, the ..._raw files are not + * intended to be checked into the SCM. + *

+ * If this option is disabled, then all existing ..._raw files will be + * proactively deleted by the framework during test execution. + *

+ * Defaults to false. + * + * @return Whether to additionally persist the raw snapshot results. + */ + boolean alwaysPersistRawResult() default false; + +} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java index 72e7bcd6..c137cef2 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java @@ -24,14 +24,19 @@ public final class SnapshotTestResult { private final SnapshotFile snapshot; private final Path targetFile; + private final Path actualResultFile; + private final Path rawActualResultFile; private final SnapshotStatus status; private final String serializedActual; private final Throwable failure; - private SnapshotTestResult(Path targetFile, SnapshotStatus status, SnapshotFile snapshotFile, + private SnapshotTestResult(Path targetFile, Path actualResultFile, Path rawActualResultFile, SnapshotStatus status, + SnapshotFile snapshotFile, String serializedActual, Throwable failure) { this.targetFile = Arguments.requireNonNull(targetFile); + this.actualResultFile = Arguments.requireNonNull(actualResultFile); + this.rawActualResultFile = Arguments.requireNonNull(rawActualResultFile); this.status = Arguments.requireNonNull(status); this.snapshot = Arguments.requireNonNull(snapshotFile); this.serializedActual = Arguments.requireNonNull(serializedActual); @@ -39,16 +44,19 @@ private SnapshotTestResult(Path targetFile, SnapshotStatus status, SnapshotFile } @API(status = Status.INTERNAL) - public static SnapshotTestResult forFailedTest(Path targetFile, SnapshotFile snapshotFile, String serializedActual, - Throwable failure) { - return new SnapshotTestResult(targetFile, SnapshotStatus.ASSERTED, snapshotFile, serializedActual, + public static SnapshotTestResult forFailedTest(Path targetFile, Path actualResultFile, Path rawActualResultFile, + SnapshotFile snapshotFile, String serializedActual, Throwable failure) { + return new SnapshotTestResult(targetFile, actualResultFile, rawActualResultFile, SnapshotStatus.ASSERTED, + snapshotFile, serializedActual, Arguments.requireNonNull(failure)); } @API(status = Status.INTERNAL) - public static SnapshotTestResult of(Path targetFile, SnapshotStatus status, SnapshotFile snapshotFile, + public static SnapshotTestResult of(Path targetFile, Path actualResultFile, Path rawActualResultFile, + SnapshotStatus status, SnapshotFile snapshotFile, String serializedActual) { - return new SnapshotTestResult(targetFile, status, snapshotFile, serializedActual, null); + return new SnapshotTestResult(targetFile, actualResultFile, rawActualResultFile, status, snapshotFile, + serializedActual, null); } /** @@ -56,11 +64,46 @@ public static SnapshotTestResult of(Path targetFile, SnapshotStatus status, Snap * that {@link #status()} is {@link SnapshotStatus#DISABLED}. * * @return The snapshot file. + * @see #actualResultFile() + * @see #rawActualResultFile() + * */ public Path targetFile() { return this.targetFile; } + /** + * Path to the file in which the latest actual result will be stored. The file will + * only exist if the recent snapshot assertion was executed with + * {@link SnapshotTestOptions#alwaysPersistActualResult()} being true. + * + * @return The path to the file with the latest actual result file. + * @since 1.7.0 + * @see #targetFile() + * @see #rawActualResultFile() + * @see SnapshotTestOptions#alwaysPersistActualResult() + */ + @API(status = Status.EXPERIMENTAL, since = "1.7.0") + public Path actualResultFile() { + return this.actualResultFile; + } + + /** + * Path to the file in which the latest raw actual result will be stored (without the + * snapshot header). The file will only exist if the recent snapshot assertion was + * executed with {@link SnapshotTestOptions#alwaysPersistRawResult()} being true. + * + * @return The path to the file with the latest raw actual result file. + * @since 1.7.0 + * @see #targetFile() + * @see #actualResultFile() + * @see SnapshotTestOptions#alwaysPersistRawResult() + */ + @API(status = Status.EXPERIMENTAL, since = "1.7.0") + public Path rawActualResultFile() { + return this.rawActualResultFile; + } + /** * Whether snapshot has been created/updated or asserted. * diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java index 7d496359..42bc6ac7 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java @@ -8,6 +8,7 @@ import de.skuzzle.test.snapshots.DeleteOrphanedSnapshots; import de.skuzzle.test.snapshots.ForceUpdateSnapshots; +import de.skuzzle.test.snapshots.SnapshotTestOptions; import de.skuzzle.test.snapshots.validation.Arguments; /** @@ -23,6 +24,10 @@ final class DefaultSnapshotConfiguration implements SnapshotConfiguration { private static final String FORCE_UPDATE_SYSTEM_PROPERTY = "forceUpdateSnapshots"; private static final String DELETE_ORPHANS_SYSTEM_PROPERTY = "deleteOrphanedSnapshots"; + // default number of context lines that will be printed around changes in huge unified + // diffs + private static final int DEFAULT_CONTEXT_LINES = 5; + private final Class testClass; private DefaultSnapshotConfiguration(Class testClass) { @@ -51,8 +56,7 @@ public boolean isDeleteOrphanedSnapshots() { .anyMatch(DELETE_ORPHANS_SYSTEM_PROPERTY::equalsIgnoreCase); } - @Override - public boolean isForceUpdateSnapshotsGlobal() { + private boolean isForceUpdateSnapshotsGlobal() { if (testClass().isAnnotationPresent(ForceUpdateSnapshots.class)) { return true; } @@ -64,7 +68,7 @@ public boolean isForceUpdateSnapshotsGlobal() { } @Override - public boolean isForceUpdateSnapshotsLocal(Method testMethod) { + public boolean isForceUpdateSnapshots(Method testMethod) { // Annotation on test method if (testMethod.isAnnotationPresent(ForceUpdateSnapshots.class)) { return true; @@ -73,6 +77,34 @@ public boolean isForceUpdateSnapshotsLocal(Method testMethod) { return isForceUpdateSnapshotsGlobal(); } + private SnapshotTestOptions determineOptions(Method testMethod) { + final SnapshotTestOptions options = testMethod.getAnnotation(SnapshotTestOptions.class); + if (options != null) { + return options; + } + return testClass.getAnnotation(SnapshotTestOptions.class); + } + + @Override + public boolean alwaysPersistActualResult(Method testMethod) { + final var snapshotTestOptions = determineOptions(testMethod); + return snapshotTestOptions != null && snapshotTestOptions.alwaysPersistActualResult(); + } + + @Override + public boolean alwaysPersistRawResult(Method testMethod) { + final var snapshotTestOptions = determineOptions(testMethod); + return snapshotTestOptions != null && snapshotTestOptions.alwaysPersistRawResult(); + } + + @Override + public int textDiffContextLines(Method testMethod) { + final var snapshotTestOptions = determineOptions(testMethod); + return snapshotTestOptions == null + ? DEFAULT_CONTEXT_LINES + : snapshotTestOptions.textDiffContextLines(); + } + @Override public boolean isSoftAssertions() { return false; diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java index c9b26e8f..3b45b767 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DetermineSnapshotDirectory.java @@ -26,7 +26,9 @@ static Path forTestclass(Class testClass) { return DirectoryResolver.resolve(dirName); } - State.check(!isDefaultValue(annotation.determinedBy()) || !annotation.value().isEmpty(), "TBD"); + State.check(!isDefaultValue(annotation.determinedBy()) || !annotation.value().isEmpty(), + "Either specify the value() attribute or the determinedBy() attribute within the @SnapshotDirectory annotation on %s", + testClass.getName()); if (isDefaultValue(annotation.determinedBy())) { return DirectoryResolver.resolve(annotation.value()); diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/InternalSnapshotNaming.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/InternalSnapshotNaming.java index ad44b07b..c9d697c6 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/InternalSnapshotNaming.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/InternalSnapshotNaming.java @@ -14,6 +14,14 @@ public static String getSnapshotFileName(String snapshotName) { return snapshotName + ".snapshot"; } + public static String getSnapshotFileNameActual(String snapshotName) { + return snapshotName + ".snapshot_actual"; + } + + public static String getSnapshotFileNameRaw(String snapshotName) { + return snapshotName + ".snapshot_raw"; + } + public static boolean isSnapshotFile(Path path) { return path.getFileName().toString().endsWith(".snapshot"); } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/LegacySnapshotConfiguration.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/LegacySnapshotConfiguration.java index f3936532..b358c655 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/LegacySnapshotConfiguration.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/LegacySnapshotConfiguration.java @@ -44,21 +44,18 @@ public boolean isDeleteOrphanedSnapshots() { return delegate.isDeleteOrphanedSnapshots(); } - @Override - public boolean isForceUpdateSnapshotsGlobal() { + private boolean isForceUpdateSnapshotsGlobal() { // Annotation on test class final boolean valueFromLegacyAnnotation = testClass() .getAnnotation(EnableSnapshotTests.class) .forceUpdateSnapshots(); - if (valueFromLegacyAnnotation) { - return true; - } - return delegate.isForceUpdateSnapshotsGlobal(); + + return valueFromLegacyAnnotation; } @Override - public boolean isForceUpdateSnapshotsLocal(Method testMethod) { - return delegate.isForceUpdateSnapshotsLocal(testMethod) || isForceUpdateSnapshotsGlobal(); + public boolean isForceUpdateSnapshots(Method testMethod) { + return delegate.isForceUpdateSnapshots(testMethod) || isForceUpdateSnapshotsGlobal(); } @Override @@ -67,4 +64,19 @@ public boolean isSoftAssertions() { .getAnnotation(EnableSnapshotTests.class) .softAssertions(); } + + @Override + public boolean alwaysPersistActualResult(Method testMethod) { + return delegate.alwaysPersistActualResult(testMethod); + } + + @Override + public boolean alwaysPersistRawResult(Method testMethod) { + return delegate.alwaysPersistRawResult(testMethod); + } + + @Override + public int textDiffContextLines(Method testMethod) { + return delegate.textDiffContextLines(testMethod); + } } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotConfiguration.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotConfiguration.java index 37e90c32..63419d84 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotConfiguration.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotConfiguration.java @@ -53,6 +53,12 @@ static SnapshotConfiguration legacyConfigurationFor(Class testClass) { */ Class testClass(); + boolean alwaysPersistActualResult(Method testMethod); + + boolean alwaysPersistRawResult(Method testMethod); + + int textDiffContextLines(Method testMethod); + /** * Whether to delete orphaned snapshot files during test execution. * @@ -60,14 +66,6 @@ static SnapshotConfiguration legacyConfigurationFor(Class testClass) { */ boolean isDeleteOrphanedSnapshots(); - /** - * Determines whether snapshots are to be forcefully updated during the execution of a - * whole test class. - * - * @return Whether to forcefully update snapshots. - */ - boolean isForceUpdateSnapshotsGlobal(); - /** * Determines whether snapshots are to be forcefully updated during the execution of * the given test method. @@ -75,7 +73,7 @@ static SnapshotConfiguration legacyConfigurationFor(Class testClass) { * @param testMethod The test method. * @return Whether to forcefully update snapshots. */ - boolean isForceUpdateSnapshotsLocal(Method testMethod); + boolean isForceUpdateSnapshots(Method testMethod); /** * Whether soft assertions shall be used. When set to true, a failing snapshot diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java index ac095f27..74aaacb2 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java @@ -1,9 +1,8 @@ package de.skuzzle.test.snapshots.impl; -import static de.skuzzle.test.snapshots.SnapshotTestResult.forFailedTest; - import java.io.IOException; import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; @@ -40,10 +39,6 @@ final class SnapshotTestImpl implements Snapshot { // and null input is given as actual object to snapshot.assertThat(...) private static final String UNAVAILABLE_BECAUSE_ACTUAL_WAS_NULL = "<>"; - // default number of context lines that will be printed around changes in huge unified - // diffs - private static final int DEFAULT_CONTEXT_LINES = 5; - private final Method testMethod; private final SnapshotTestContext context; private final SnapshotConfiguration configuration; @@ -91,11 +86,45 @@ private SnapshotHeader determineNextSnapshotHeader(String snapshotName) { SnapshotHeader.DYNAMIC_DIRECTORY, "" + (this.directoryOverride != null))); } + private SnapshotFilePaths determineSnapshotFilePaths(String snapshotName) throws IOException { + return new SnapshotFilePaths( + determineSnapshotFile(snapshotName), + determineSnapshotFileActual(snapshotName), + determineSnapshotFileRaw(snapshotName)); + } + private Path determineSnapshotFile(String snapshotName) throws IOException { final String snapshotFileName = InternalSnapshotNaming.getSnapshotFileName(snapshotName); return determineSnapshotDirectory().resolve(snapshotFileName); } + private Path determineSnapshotFileActual(String snapshotName) throws IOException { + final String snapshotFileName = InternalSnapshotNaming.getSnapshotFileNameActual(snapshotName); + return determineSnapshotDirectory().resolve(snapshotFileName); + } + + private Path determineSnapshotFileRaw(String snapshotName) throws IOException { + final String snapshotFileName = InternalSnapshotNaming.getSnapshotFileNameRaw(snapshotName); + return determineSnapshotDirectory().resolve(snapshotFileName); + } + + private void writeAdditionalContextFiles(SnapshotFilePaths snapshotFilePaths, SnapshotFile actualSnapshotFile) + throws IOException { + final Path snapshotFileActual = snapshotFilePaths.latestActualSnapshotFile; + if (configuration.alwaysPersistActualResult(testMethod)) { + actualSnapshotFile.writeTo(snapshotFileActual); + } else { + Files.deleteIfExists(snapshotFileActual); + } + + final Path snapshotFileRaw = snapshotFilePaths.rawSnapshotFile; + if (configuration.alwaysPersistRawResult(testMethod)) { + Files.writeString(snapshotFileRaw, actualSnapshotFile.snapshot(), StandardCharsets.UTF_8); + } else { + Files.deleteIfExists(snapshotFileRaw); + } + } + private Path determineSnapshotDirectory() throws IOException { final Path snapshotDirectory = this.directoryOverride != null ? this.directoryOverride @@ -111,15 +140,20 @@ SnapshotTestResult justUpdateSnapshotWith(SnapshotSerializer snapshotSerializer, } final String snapshotName = namingStrategy.determineSnapshotName(testMethod, localResultCollector.size()); - final Path snapshotFilePath = determineSnapshotFile(snapshotName); + final SnapshotFilePaths snapshotFilePaths = determineSnapshotFilePaths(snapshotName); + final String serializedActual = snapshotSerializer.serialize(actual); final SnapshotHeader snapshotHeader = determineNextSnapshotHeader(snapshotName); final SnapshotFile snapshotFile = SnapshotFile.of(snapshotHeader, serializedActual) - .writeTo(snapshotFilePath); + .writeTo(snapshotFilePaths.snapshotFile); + writeAdditionalContextFiles(snapshotFilePaths, snapshotFile); - final SnapshotTestResult result = SnapshotTestResult.of(snapshotFilePath, SnapshotStatus.UPDATED_FORCEFULLY, - snapshotFile, serializedActual); + final SnapshotTestResult result = SnapshotTestResult.of( + snapshotFilePaths.snapshotFile, + snapshotFilePaths.latestActualSnapshotFile, + snapshotFilePaths.rawSnapshotFile, + SnapshotStatus.UPDATED_FORCEFULLY, snapshotFile, serializedActual); recordSnapshotTestResult(result); @@ -136,16 +170,22 @@ SnapshotTestResult disabled(SnapshotSerializer snapshotSerializer, Object actual) throws Exception { state.reset(); final String snapshotName = namingStrategy.determineSnapshotName(testMethod, localResultCollector.size()); - final Path snapshotFilePath = determineSnapshotFile(snapshotName); + final SnapshotFilePaths snapshotFilePaths = determineSnapshotFilePaths(snapshotName); final SnapshotHeader snapshotHeader = determineNextSnapshotHeader(snapshotName); final String serializedActual = actual == null ? UNAVAILABLE_BECAUSE_ACTUAL_WAS_NULL : snapshotSerializer.serialize(actual); + final SnapshotFile snapshotFile = SnapshotFile.of(snapshotHeader, serializedActual); - final SnapshotTestResult result = SnapshotTestResult.of(snapshotFilePath, SnapshotStatus.DISABLED, - snapshotFile, serializedActual); + writeAdditionalContextFiles(snapshotFilePaths, snapshotFile); + + final SnapshotTestResult result = SnapshotTestResult.of( + snapshotFilePaths.snapshotFile, + snapshotFilePaths.latestActualSnapshotFile, + snapshotFilePaths.rawSnapshotFile, + SnapshotStatus.DISABLED, snapshotFile, serializedActual); recordSnapshotTestResult(result); return result; @@ -156,9 +196,10 @@ SnapshotTestResult executeAssertionWith(SnapshotSerializer snapshotSerializer, Object actual) throws Exception { state.reset(); final String snapshotName = namingStrategy.determineSnapshotName(testMethod, localResultCollector.size()); - final Path snapshotFilePath = determineSnapshotFile(snapshotName); + final SnapshotFilePaths snapshotFilePaths = determineSnapshotFilePaths(snapshotName); + final Path snapshotFilePath = snapshotFilePaths.snapshotFile; - final boolean forceUpdateSnapshots = configuration.isForceUpdateSnapshotsLocal(testMethod); + final boolean forceUpdateSnapshots = configuration.isForceUpdateSnapshots(testMethod); final boolean snapshotFileAlreadyExists = Files.exists(snapshotFilePath); final SnapshotHeader snapshotHeader = determineNextSnapshotHeader(snapshotName); @@ -174,9 +215,15 @@ SnapshotTestResult executeAssertionWith(SnapshotSerializer snapshotSerializer, final SnapshotFile snapshotFile = SnapshotFile.of(snapshotHeader, serializedActual) .writeTo(snapshotFilePath); - final SnapshotStatus status = snapshotFileAlreadyExists ? SnapshotStatus.UPDATED_FORCEFULLY + writeAdditionalContextFiles(snapshotFilePaths, snapshotFile); + + final SnapshotStatus status = snapshotFileAlreadyExists + ? SnapshotStatus.UPDATED_FORCEFULLY : SnapshotStatus.CREATED_INITIALLY; - result = SnapshotTestResult.of(snapshotFilePath, status, snapshotFile, serializedActual); + result = SnapshotTestResult.of(snapshotFilePaths.snapshotFile, + snapshotFilePaths.latestActualSnapshotFile, + snapshotFilePaths.rawSnapshotFile, + status, snapshotFile, serializedActual); } else { final SnapshotFile snapshotFile = readSnapshotFileAndUpdateHeader(snapshotFilePath, snapshotHeader); final String storedSnapshot = snapshotFile.snapshot(); @@ -188,11 +235,19 @@ SnapshotTestResult executeAssertionWith(SnapshotSerializer snapshotSerializer, final String serializedActual = snapshotSerializer.serialize(actual); + writeAdditionalContextFiles(snapshotFilePaths, SnapshotFile.of(snapshotHeader, serializedActual)); + result = compareTestResults(structuralAssertions, storedSnapshot, serializedActual, snapshotFilePath) - .map(assertionError -> forFailedTest(snapshotFilePath, snapshotFile, serializedActual, - assertionError)) - .orElseGet(() -> SnapshotTestResult.of(snapshotFilePath, SnapshotStatus.ASSERTED, snapshotFile, - serializedActual)); + .map(assertionError -> SnapshotTestResult.forFailedTest( + snapshotFilePaths.snapshotFile, + snapshotFilePaths.latestActualSnapshotFile, + snapshotFilePaths.rawSnapshotFile, + snapshotFile, serializedActual, assertionError)) + .orElseGet(() -> SnapshotTestResult.of( + snapshotFilePaths.snapshotFile, + snapshotFilePaths.latestActualSnapshotFile, + snapshotFilePaths.rawSnapshotFile, + SnapshotStatus.ASSERTED, snapshotFile, serializedActual)); } recordSnapshotTestResult(result); @@ -272,8 +327,21 @@ private TextDiff determineDiff(AssertionError original, String storedSnapshot, S // comparison in TextDiffStructuralAssertions return ((TextDiffAssertionError) original).textDiff(); } else { - return TextDiff.diffOf(storedSnapshot, serializedActual, DEFAULT_CONTEXT_LINES); + return TextDiff.diffOf(storedSnapshot, serializedActual, configuration.textDiffContextLines(testMethod)); } } + private static class SnapshotFilePaths { + private final Path snapshotFile; + private final Path rawSnapshotFile; + private final Path latestActualSnapshotFile; + + private SnapshotFilePaths(Path snapshotFile, Path latestActualSnapshotFile, Path rawSnapshotFile) { + this.snapshotFile = snapshotFile; + this.latestActualSnapshotFile = latestActualSnapshotFile; + this.rawSnapshotFile = rawSnapshotFile; + } + + } + } diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java index dd664520..7caf7440 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java @@ -15,12 +15,46 @@ @EnableSnapshotTests public class SnapshotsTest { + @Test + @SnapshotTestOptions(alwaysPersistActualResult = true, alwaysPersistRawResult = true) + void testWriteContextFiles(Snapshot snapshot) throws Exception { + final Person simon = determinePerson(); + final SnapshotTestResult testResult = snapshot.assertThat(simon).asText().matchesSnapshotText(); + + assertThat(testResult.rawActualResultFile()).exists(); + assertThat(testResult.actualResultFile()).exists(); + } + @Test void testDisabledWithNullInput(Snapshot snapshot) throws Exception { final SnapshotTestResult testResult = snapshot.assertThat(null).asText().disabled(); assertThat(testResult.serializedActual()).isEqualTo("<>"); } + @Test + @SnapshotTestOptions(alwaysPersistActualResult = true, alwaysPersistRawResult = true) + void testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreatedWithContextFiles(Snapshot snapshot) + throws Exception { + + final Person simon = determinePerson(); + final SnapshotTestResult testResult = snapshot.assertThat(simon).asText().disabled(); + + assertThat(testResult.rawActualResultFile()).exists(); + assertThat(testResult.actualResultFile()).exists(); + } + + @Test + @SnapshotTestOptions(alwaysPersistActualResult = true, alwaysPersistRawResult = true) + void testWithOneDisabledAssertionForWhichSnapshotHasHasAlreadyBeenCreatedWithContextFiles(Snapshot snapshot) + throws Exception { + + final Person simon = determinePerson(); + final SnapshotTestResult testResult = snapshot.assertThat(simon).asText().disabled(); + + assertThat(testResult.rawActualResultFile()).exists(); + assertThat(testResult.actualResultFile()).exists(); + } + @Test void testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreated(Snapshot snapshot) throws Exception { final Person simon = determinePerson(); diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasHasAlreadyBeenCreatedWithContextFiles_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasHasAlreadyBeenCreatedWithContextFiles_0.snapshot new file mode 100644 index 00000000..86acfef2 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsTest_snapshots/testWithOneDisabledAssertionForWhichSnapshotHasHasAlreadyBeenCreatedWithContextFiles_0.snapshot @@ -0,0 +1,15 @@ +dynamic-directory: false +snapshot-name: testWithOneDisabledAssertionForWhichSnapshotHasHasAlreadyBeenCreatedWithContextFiles_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.SnapshotsTest +test-method: testWithOneDisabledAssertionForWhichSnapshotHasHasAlreadyBeenCreatedWithContextFiles + +Name: Simon +Surname: Taddiken +Birthdate: 1777-01-12 +Address: Street: Gibtsnicht-Straße +Number: 1337 +Zip: 4711 +City: Bielefeld +Country: Germany + diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsTest_snapshots/testWriteContextFiles_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsTest_snapshots/testWriteContextFiles_0.snapshot new file mode 100644 index 00000000..a07c17e5 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsTest_snapshots/testWriteContextFiles_0.snapshot @@ -0,0 +1,15 @@ +dynamic-directory: false +snapshot-name: testWriteContextFiles_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.SnapshotsTest +test-method: testWriteContextFiles + +Name: Simon +Surname: Taddiken +Birthdate: 1777-01-12 +Address: Street: Gibtsnicht-Straße +Number: 1337 +Zip: 4711 +City: Bielefeld +Country: Germany + From b71ed1d1c18e35a1f11e35090404d5f5e7045ed9 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Thu, 15 Dec 2022 09:15:22 +0100 Subject: [PATCH 14/27] Improve tests --- .../test/snapshots/SnapshotTestOptions.java | 13 +- .../test/snapshots/SnapshotTestResult.java | 8 +- .../snapshots/data/text/DiffInterpreter.java | 30 +- .../snapshots/data/text/LineSeparator.java | 6 +- .../test/snapshots/data/text/TextDiff.java | 20 +- .../impl/DefaultSnapshotConfiguration.java | 2 +- .../test/snapshots/SnapshotsLegacyTest.java | 34 +++ .../snapshots/data/text/TextDiffTest.java | 266 ++++++++++++------ 8 files changed, 262 insertions(+), 117 deletions(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java index 4ad4f3ea..603cefee 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java @@ -13,8 +13,8 @@ /** * Allows to configure some behavior details of the snapshot testing engine. This * annotation can either be placed on the test class itself or on single test methods. - * Annotations on test methods take precedence over the one on the test class. If the - * annotation is not specified at all, the the documented defaults apply. + * Annotations on test methods take precedence over the one on the test class. If this + * annotation is not specified at all, then the documented defaults apply. *

* If you want to globally modify the snapshot directory, see the * {@link SnapshotDirectory} annotation. @@ -26,6 +26,13 @@ @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface SnapshotTestOptions { + + /** + * Default number of context lines to print in unified diffs of structural comparison + * failures. + */ + public static final int DEFAULT_TEXT_DIFF_CONTEXT_LINES = 5; + /** * Defines the number of context lines that are printed around a comparison failure. * Note that this setting only applies to unified diffs created for structural @@ -37,7 +44,7 @@ * @return The number of context lines to print in unified diffs within our structural * assertion failures. */ - int textDiffContextLines() default 5; + int textDiffContextLines() default DEFAULT_TEXT_DIFF_CONTEXT_LINES; /** * Whether to always persist the latest actual test result in a parallel file next to diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java index c137cef2..69a78a0a 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java @@ -66,7 +66,6 @@ public static SnapshotTestResult of(Path targetFile, Path actualResultFile, Path * @return The snapshot file. * @see #actualResultFile() * @see #rawActualResultFile() - * */ public Path targetFile() { return this.targetFile; @@ -166,12 +165,15 @@ public Optional failure() { } /** - * Deletes the snapshot file. + * Deletes the snapshot file and all the context files ({@link #rawActualResultFile()} + * and {@link #actualResultFile()}) if any. * * @throws IOException if an I/O error occurs */ public void deleteSnapshot() throws IOException { - Files.delete(targetFile); + Files.deleteIfExists(targetFile); + Files.deleteIfExists(actualResultFile); + Files.deleteIfExists(rawActualResultFile); } @Override diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffInterpreter.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffInterpreter.java index e93afde4..c39e8da9 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffInterpreter.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffInterpreter.java @@ -10,7 +10,8 @@ final class DiffInterpreter { - private static final Pattern WHITESPACE_ONLY = Pattern.compile("\\h+"); + private static final Pattern WHITESPACE_ONLY = Pattern.compile("\\s+"); + private static final Pattern HORIZONTAL_WHITESPACE_ONLY = Pattern.compile("\\h+"); private boolean ignoreWhitespaceChanges = true; private int contextLines = Integer.MAX_VALUE; @@ -30,10 +31,21 @@ private boolean isWhitespace(Diff diff) { return WHITESPACE_ONLY.matcher(diff.text).matches(); } + private boolean isHorizontalWhitespace(Diff diff) { + return HORIZONTAL_WHITESPACE_ONLY.matcher(diff.text).matches(); + } + public boolean hasFailures(Collection diffs) { return diffs.stream().anyMatch(this::isFailureDifference); } + public int countLinebreaks(Diff diff) { + final int rawLines = (int) diff.text.lines().count(); + return diff.operation == Operation.DELETE + ? -rawLines + : (int) rawLines; + } + private boolean isFailureDifference(Diff diff) { if (diff.operation == Operation.EQUAL) { return false; @@ -70,22 +82,24 @@ private String einklammern(String text) { // invariant: Diff instances always use System line separators final String lineSeparator = LineSeparator.SYSTEM.toString(); - if (text.endsWith(lineSeparator)) { + if (text.equals(lineSeparator)) { + return text; + } else if (text.endsWith(lineSeparator)) { return "[" + text.substring(0, text.length() - lineSeparator.length()) + "]" + lineSeparator; } else { return "[" + text + "]"; } } - public String renderEqualsDiff(String diffText, EqualDiffPosition position) { - return position.render(diffText, contextLines); + public String renderEqualsDiff(String diffText, EqualDiffPosition position, int lineNumber) { + return position.render(diffText, contextLines, lineNumber); } enum EqualDiffPosition { START { @Override - protected String render(String diffText, int contextLines) { + protected String render(String diffText, int contextLines, int lineNumber) { final int totalLines = (int) diffText.lines().count(); final StringBuilder b = new StringBuilder(); @@ -110,7 +124,7 @@ protected String render(String diffText, int contextLines) { }, MIDDLE { @Override - protected String render(String diffText, int contextLines) { + protected String render(String diffText, int contextLines, int lineNumber) { final int totalLines = (int) diffText.lines().count(); final StringBuilder b = new StringBuilder(); @@ -134,7 +148,7 @@ protected String render(String diffText, int contextLines) { }, END { @Override - protected String render(String diffText, int contextLines) { + protected String render(String diffText, int contextLines, int lineNumber) { final int totalLines = (int) diffText.lines().count(); final StringBuilder b = new StringBuilder(); @@ -157,7 +171,7 @@ protected String render(String diffText, int contextLines) { } }; - protected abstract String render(String diffText, int contextLines); + protected abstract String render(String diffText, int contextLines, int lineNumber); } } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/LineSeparator.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/LineSeparator.java index 593b9a5e..48b49fe0 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/LineSeparator.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/LineSeparator.java @@ -13,8 +13,8 @@ enum LineSeparator { CRLF("\r\n", "\\r\\n"), LF("\n", "\\n"), CR("\r", "\\r"), - DEFAULT("\n", "\\n"), - SYSTEM(System.lineSeparator(), System.lineSeparator().replace("\r", "\\r").replace("\n", "\\n")); + SYSTEM(System.lineSeparator(), System.lineSeparator().replace("\r", "\\r").replace("\n", "\\n")), + NONE("\n", ""); private final String characters; private final String displayName; @@ -30,7 +30,7 @@ public static LineSeparator determineFrom(String s) { return lineSeparator; } } - return DEFAULT; + return NONE; } private boolean existsIn(String s) { diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java index 74fc17d1..37af00de 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java @@ -88,24 +88,28 @@ public String toString() { } final ListIterator cursor = diffs.listIterator(); + int lineNr = 1; while (cursor.hasNext()) { - final boolean hasPrevious = cursor.hasPrevious(); + final boolean isBeginning = cursor.hasPrevious(); final Diff current = cursor.next(); - if (current.operation == Operation.EQUAL && !hasPrevious) { - // equal operation at the beginning - message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.START)); - } else if (current.operation == Operation.EQUAL) { - if (cursor.hasNext()) { + + if (current.operation == Operation.EQUAL) { + if (!isBeginning) { + // equal operation at the beginning of the whole diff + message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.START, lineNr)); + } else if (cursor.hasNext()) { // equal operation between 2 changes - message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.MIDDLE)); + message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.MIDDLE, lineNr)); } else { // equal diff at the end - message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.END)); + message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.END, lineNr)); } } else { message.append(diffInterpreter.renderFailureDiff(current)); } + + lineNr += diffInterpreter.countLinebreaks(current); } return message.toString(); } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java index 42bc6ac7..65d73d9b 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java @@ -26,7 +26,7 @@ final class DefaultSnapshotConfiguration implements SnapshotConfiguration { // default number of context lines that will be printed around changes in huge unified // diffs - private static final int DEFAULT_CONTEXT_LINES = 5; + private static final int DEFAULT_CONTEXT_LINES = SnapshotTestOptions.DEFAULT_TEXT_DIFF_CONTEXT_LINES; private final Class testClass; diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsLegacyTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsLegacyTest.java index 2cc52b61..467fb451 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsLegacyTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsLegacyTest.java @@ -14,12 +14,46 @@ @EnableSnapshotTests public class SnapshotsLegacyTest { + @Test + @SnapshotTestOptions(alwaysPersistActualResult = true, alwaysPersistRawResult = true) + void testWriteContextFiles(Snapshot snapshot) throws Exception { + final Person simon = determinePerson(); + final SnapshotTestResult testResult = snapshot.assertThat(simon).asText().matchesSnapshotText(); + + assertThat(testResult.rawActualResultFile()).exists(); + assertThat(testResult.actualResultFile()).exists(); + } + @Test void testDisabledWithNullInput(Snapshot snapshot) throws Exception { final SnapshotTestResult testResult = snapshot.assertThat(null).asText().disabled(); assertThat(testResult.serializedActual()).isEqualTo("<>"); } + @Test + @SnapshotTestOptions(alwaysPersistActualResult = true, alwaysPersistRawResult = true) + void testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreatedWithContextFiles(Snapshot snapshot) + throws Exception { + + final Person simon = determinePerson(); + final SnapshotTestResult testResult = snapshot.assertThat(simon).asText().disabled(); + + assertThat(testResult.rawActualResultFile()).exists(); + assertThat(testResult.actualResultFile()).exists(); + } + + @Test + @SnapshotTestOptions(alwaysPersistActualResult = true, alwaysPersistRawResult = true) + void testWithOneDisabledAssertionForWhichSnapshotHasHasAlreadyBeenCreatedWithContextFiles(Snapshot snapshot) + throws Exception { + + final Person simon = determinePerson(); + final SnapshotTestResult testResult = snapshot.assertThat(simon).asText().disabled(); + + assertThat(testResult.rawActualResultFile()).exists(); + assertThat(testResult.actualResultFile()).exists(); + } + @Test void testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreated(Snapshot snapshot) throws Exception { final Person simon = determinePerson(); diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java index 1cb44059..37acc3f6 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java @@ -2,114 +2,198 @@ import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; public class TextDiffTest { - @Test - void testHasDifferenceFalseIfIgnoreWhitespacesAndOnlyWhitespaceChanges() throws Exception { - final TextDiff diffOf = TextDiff.diffOf(new DiffInterpreter().withIgnoreWhitespaceChanges(true), " ", " "); - assertThat(diffOf.hasDifference()).isFalse(); - } + private final DiffInterpreter diffInterpreter = new DiffInterpreter(); - @Test - void testHasDifferenceFalseIfIgnoreWhitespacesAndOnlyLineSeparatorChanges() throws Exception { - final TextDiff diffOf = TextDiff.diffOf(new DiffInterpreter().withIgnoreWhitespaceChanges(true), "a\rb", - "a\nb"); - assertThat(diffOf.hasDifference()).isFalse(); - } + @Nested + class IgnoringWhitespaceChanges { + { + diffInterpreter.withIgnoreWhitespaceChanges(true); + } - @Test - void testDiffOfEmptyStrings() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter(), "", ""); - assertThat(diff.toString()).isEmpty(); - assertThat(diff.hasDifference()).isFalse(); - } + @Test + void testHasDifferenceFalseIfOnlyWhitespaceChanges() throws Exception { + final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, " ", " "); + assertThat(diffOf.hasDifference()).isFalse(); + } - @Test - void testDiffWithLinebreaksAndLessThanContextLinesWithTrailingLineBreak() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withIgnoreWhitespaceChanges(false) - .withContextLines(50), - "1\n2\n3\n4\n5\n6\n7\nline1\n", "1\n2\n3\n4\n5\n6\n7\nlineX\n"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "1\n2\n3\n4\n5\n6\n7\nline-[1]+[X]\n")); - assertThat(diff.hasDifference()).isTrue(); - } + @Test + void testHasDifferenceFalseIfOnlyLineSeparatorChanges() throws Exception { + final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, "a\rb", "a\nb"); + assertThat(diffOf.hasDifference()).isFalse(); + } - @Test - void testDiffWithLinebreaksAndLessThanContextLinesWithoutTrailingLineBreak() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withIgnoreWhitespaceChanges(false) - .withContextLines(50), - "1\n2\n3\n4\n5\n6\n7\nline1", "1\n2\n3\n4\n5\n6\n7\nlineX"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "1\n2\n3\n4\n5\n6\n7\nline-[1]+[X]")); - assertThat(diff.hasDifference()).isTrue(); - } + @Test + void testHasDifferenceFalseIfLineSeparatorAdded() throws Exception { + final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, "ab", "a\nb"); + assertThat(diffOf.hasDifference()).isFalse(); + } - @Test - void testDiffOnlyInLinebreaks() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter() - .withContextLines(5) - .withIgnoreWhitespaceChanges(false), - "line1\n1\n2\n3\n4\n5\n6\n7", "line1\r1\r2\r3\r4\r5\r6\r7"); - assertThat(diff.toString()) - .isEqualTo("Strings differ in linebreaks. Expected: 'LF(\\n)', Actual encountered: 'CR(\\r)'"); + @Test + void testHasDifferenceFalseIfLineSeparatorRemoved() throws Exception { + final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, "a\nb", "ab"); + assertThat(diffOf.hasDifference()).isFalse(); + } + + @Test + void testDiffOfEmptyStrings() throws Exception { + final TextDiff diff = TextDiff.diffOf(diffInterpreter, "", ""); + assertThat(diff.toString()).isEmpty(); + assertThat(diff.hasDifference()).isFalse(); + } + + @Nested + class With5ContextLines { + { + diffInterpreter.withContextLines(5); + } + + @Test + void testDiffWithHugeEqualBlockAtTheStart() throws Exception { + final TextDiff diff = TextDiff.diffOf(diffInterpreter, + "1\n2\n3\n4\n5\n6\n7\nline1", + "1\n2\n3\n4\n5\n6\n7\nlineX"); + assertThat(diff.toString()).isEqualTo( + LineSeparator.SYSTEM.convert( + "[...]\n4\n5\n6\n7\nline-[1]+[X]")); + assertThat(diff.hasDifference()).isTrue(); + } + + @Test + void testDiffWithHugeEqualBlockAtTheEnd() throws Exception { + final TextDiff diff = TextDiff.diffOf(diffInterpreter, + "line1\n1\n2\n3\n4\n5\n6\n7", + "lineX\n1\n2\n3\n4\n5\n6\n7"); + assertThat(diff.toString()).isEqualTo( + LineSeparator.SYSTEM.convert( + "line-[1]+[X]\n1\n2\n3\n4\n[...]")); + assertThat(diff.hasDifference()).isTrue(); + } + + @Test + void testDiffWithHugeEqualBlockAtTheStartAndTheEnd() throws Exception { + final TextDiff diff = TextDiff.diffOf(diffInterpreter, + "1\n2\n3\n4\n5\n6\n7\nline1\n8\n9\n10\n11\n12\n13\n14", + "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14"); + assertThat(diff.toString()).isEqualTo( + LineSeparator.SYSTEM.convert( + "[...]\n3\n4\n5\n6\n7\n-[line1]\n8\n9\n10\n11\n12\n[...]")); + assertThat(diff.hasDifference()).isTrue(); + } + + @Test + void testDiffWithHugeEqualBlockInTheMiddle() throws Exception { + final TextDiff diff = TextDiff.diffOf(diffInterpreter, + "line1\n1\n2\n3\n4\n5\n6\n7\nline2\n8\n9\n10\n11\n12\n13\n14\nline3", + "line2\n1\n2\n3\n4\n5\n6\n7\nline2\n8\n9\n10\n11\n12\n13\n14\nline4"); + assertThat(diff.toString()).isEqualTo( + LineSeparator.SYSTEM.convert( + "line-[1]+[2]\n1\n2\n3\n4\n[...]\n11\n12\n13\n14\nline-[3]+[4]")); + assertThat(diff.hasDifference()).isTrue(); + } + } } - @Test - void testDiffInLinebreaks() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter() - .withContextLines(5) - .withIgnoreWhitespaceChanges(false), - "line1\n1\n2\n3\n4\n5\n6\n7", "lineX\r1\r2\r3\r4\r5\r6\r7"); - - assertThat(diff.toString()) - .isEqualTo(LineSeparator.SYSTEM.convert( + @Nested + class ObeyingWhitespaceChanges { + { + diffInterpreter.withIgnoreWhitespaceChanges(false); + } + + @Test + void testHasDifferenceTrueIfOnlyWhitespaceChanges() throws Exception { + final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, " ", " "); + assertThat(diffOf.hasDifference()).isTrue(); + } + + @Test + void testHasDifferenceTrueIfOnlyLineSeparatorChanges() throws Exception { + final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, "a\rb", "a\n\nb"); + assertThat(diffOf.hasDifference()).isTrue(); + assertThat(diffOf.toString()).isEqualTo(LineSeparator.SYSTEM.convert( + "Strings differ in linebreaks. Expected: 'CR(\\r)', Actual encountered: 'LF(\\n)'")); + } + + @Test + void testHasDifferenceTrueIfLineSeparatorAdded() throws Exception { + final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, "ab", "a\nb"); + assertThat(diffOf.hasDifference()).isTrue(); + assertThat(diffOf.toString()).isEqualTo(LineSeparator.SYSTEM.convert( + "Strings differ in linebreaks. Expected: 'NONE()', Actual encountered: 'LF(\\n)'\n\na+\nb")); + } + + @Test + void testHasDifferenceTrueIfLineSeparatorRemoved() throws Exception { + final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, "a\nb", "ab"); + assertThat(diffOf.hasDifference()).isTrue(); + assertThat(diffOf.toString()).isEqualTo(LineSeparator.SYSTEM.convert( + "Strings differ in linebreaks. Expected: 'LF(\\n)', Actual encountered: 'NONE()'\n\na-\nb")); + } + + @Nested + class With5ContextLines { + { + diffInterpreter.withContextLines(5); + } + + @Test + void testDiffOnlyInLinebreaks() throws Exception { + final TextDiff diff = TextDiff.diffOf(diffInterpreter, + "line1\n1\n2\n3\n4\n5\n6\n7", "line1\r1\r2\r3\r4\r5\r6\r7"); + assertThat(diff.toString()) + .isEqualTo("Strings differ in linebreaks. Expected: 'LF(\\n)', Actual encountered: 'CR(\\r)'"); + } + + @Test + void testDiffInLinebreaks() throws Exception { + final TextDiff diff = TextDiff.diffOf(diffInterpreter, + "line1\n1\n2\n3\n4\n5\n6\n7", "lineX\r1\r2\r3\r4\r5\r6\r7"); + + assertThat(diff.toString()).isEqualTo(LineSeparator.SYSTEM.convert( "Strings differ in linebreaks. Expected: 'LF(\\n)', Actual encountered: 'CR(\\r)'\n\nline-[1]+[X]\r1\r2\r3\r4\r[...]")); - assertThat(diff.hasDifference()).isTrue(); - } + assertThat(diff.hasDifference()).isTrue(); + } + } - @Test - void testDiffWithHugeEqualBlockAtTheStart() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withContextLines(5), - "1\n2\n3\n4\n5\n6\n7\nline1", "1\n2\n3\n4\n5\n6\n7\nlineX"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "[...]\n4\n5\n6\n7\nline-[1]+[X]")); - assertThat(diff.hasDifference()).isTrue(); - } + @Nested + class WithMoreContextLinesThanEqualDiffLines { + { + diffInterpreter.withContextLines(50); + } - @Test - void testDiffWithHugeEqualBlockAtTheEnd() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withContextLines(5), - "line1\n1\n2\n3\n4\n5\n6\n7", "lineX\n1\n2\n3\n4\n5\n6\n7"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "line-[1]+[X]\n1\n2\n3\n4\n[...]")); - assertThat(diff.hasDifference()).isTrue(); - } + @Test + void testDiffWithLinebreaksAndTrailingLineBreak() throws Exception { + final TextDiff diff = TextDiff.diffOf(diffInterpreter, + "1\n2\n3\n4\n5\n6\n7\nline1\n", "1\n2\n3\n4\n5\n6\n7\nlineX\n"); + assertThat(diff.toString()).isEqualTo( + LineSeparator.SYSTEM.convert( + "1\n2\n3\n4\n5\n6\n7\nline-[1]+[X]\n")); + assertThat(diff.hasDifference()).isTrue(); + } - @Test - void testDiffWithHugeEqualBlockAtTheStartAndTheEnd() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withContextLines(5), - "1\n2\n3\n4\n5\n6\n7\nline1\n8\n9\n10\n11\n12\n13\n14", - "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "[...]\n3\n4\n5\n6\n7\n-[line1]\n8\n9\n10\n11\n12\n[...]")); - assertThat(diff.hasDifference()).isTrue(); + @Test + void testDiffWithLinebreaksAndWithoutTrailingLineBreak() throws Exception { + final TextDiff diff = TextDiff.diffOf(diffInterpreter, + "1\n2\n3\n4\n5\n6\n7\nline1", "1\n2\n3\n4\n5\n6\n7\nlineX"); + assertThat(diff.toString()).isEqualTo( + LineSeparator.SYSTEM.convert( + "1\n2\n3\n4\n5\n6\n7\nline-[1]+[X]")); + assertThat(diff.hasDifference()).isTrue(); + } + } } @Test - void testDiffWithHugeEqualBlockInTheMiddle() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withContextLines(5), - "line1\n1\n2\n3\n4\n5\n6\n7\nline2\n8\n9\n10\n11\n12\n13\n14\nline3", - "line2\n1\n2\n3\n4\n5\n6\n7\nline2\n8\n9\n10\n11\n12\n13\n14\nline4"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "line-[1]+[2]\n1\n2\n3\n4\n[...]\n11\n12\n13\n14\nline-[3]+[4]")); - assertThat(diff.hasDifference()).isTrue(); + void testSimpleDiffWithNoLinebreaks() throws Exception { + final TextDiff diff = TextDiff.diffOf(new DiffInterpreter(), + "line1line1line1line1line1", + "line1line1line2line1line1"); + + assertThat(diff.toString()).isEqualTo("line1line1line-[1]+[2]line1line1"); } + } From dc2b67328fcae0f743895946df959ea46326465c Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Thu, 15 Dec 2022 09:16:37 +0100 Subject: [PATCH 15/27] Rever prev commit --- .../test/snapshots/SnapshotTestOptions.java | 13 +- .../test/snapshots/SnapshotTestResult.java | 8 +- .../snapshots/data/text/DiffInterpreter.java | 30 +- .../snapshots/data/text/LineSeparator.java | 6 +- .../test/snapshots/data/text/TextDiff.java | 20 +- .../impl/DefaultSnapshotConfiguration.java | 2 +- .../test/snapshots/SnapshotsLegacyTest.java | 34 --- .../snapshots/data/text/TextDiffTest.java | 266 ++++++------------ 8 files changed, 117 insertions(+), 262 deletions(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java index 603cefee..4ad4f3ea 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java @@ -13,8 +13,8 @@ /** * Allows to configure some behavior details of the snapshot testing engine. This * annotation can either be placed on the test class itself or on single test methods. - * Annotations on test methods take precedence over the one on the test class. If this - * annotation is not specified at all, then the documented defaults apply. + * Annotations on test methods take precedence over the one on the test class. If the + * annotation is not specified at all, the the documented defaults apply. *

* If you want to globally modify the snapshot directory, see the * {@link SnapshotDirectory} annotation. @@ -26,13 +26,6 @@ @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface SnapshotTestOptions { - - /** - * Default number of context lines to print in unified diffs of structural comparison - * failures. - */ - public static final int DEFAULT_TEXT_DIFF_CONTEXT_LINES = 5; - /** * Defines the number of context lines that are printed around a comparison failure. * Note that this setting only applies to unified diffs created for structural @@ -44,7 +37,7 @@ * @return The number of context lines to print in unified diffs within our structural * assertion failures. */ - int textDiffContextLines() default DEFAULT_TEXT_DIFF_CONTEXT_LINES; + int textDiffContextLines() default 5; /** * Whether to always persist the latest actual test result in a parallel file next to diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java index 69a78a0a..c137cef2 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestResult.java @@ -66,6 +66,7 @@ public static SnapshotTestResult of(Path targetFile, Path actualResultFile, Path * @return The snapshot file. * @see #actualResultFile() * @see #rawActualResultFile() + * */ public Path targetFile() { return this.targetFile; @@ -165,15 +166,12 @@ public Optional failure() { } /** - * Deletes the snapshot file and all the context files ({@link #rawActualResultFile()} - * and {@link #actualResultFile()}) if any. + * Deletes the snapshot file. * * @throws IOException if an I/O error occurs */ public void deleteSnapshot() throws IOException { - Files.deleteIfExists(targetFile); - Files.deleteIfExists(actualResultFile); - Files.deleteIfExists(rawActualResultFile); + Files.delete(targetFile); } @Override diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffInterpreter.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffInterpreter.java index c39e8da9..e93afde4 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffInterpreter.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffInterpreter.java @@ -10,8 +10,7 @@ final class DiffInterpreter { - private static final Pattern WHITESPACE_ONLY = Pattern.compile("\\s+"); - private static final Pattern HORIZONTAL_WHITESPACE_ONLY = Pattern.compile("\\h+"); + private static final Pattern WHITESPACE_ONLY = Pattern.compile("\\h+"); private boolean ignoreWhitespaceChanges = true; private int contextLines = Integer.MAX_VALUE; @@ -31,21 +30,10 @@ private boolean isWhitespace(Diff diff) { return WHITESPACE_ONLY.matcher(diff.text).matches(); } - private boolean isHorizontalWhitespace(Diff diff) { - return HORIZONTAL_WHITESPACE_ONLY.matcher(diff.text).matches(); - } - public boolean hasFailures(Collection diffs) { return diffs.stream().anyMatch(this::isFailureDifference); } - public int countLinebreaks(Diff diff) { - final int rawLines = (int) diff.text.lines().count(); - return diff.operation == Operation.DELETE - ? -rawLines - : (int) rawLines; - } - private boolean isFailureDifference(Diff diff) { if (diff.operation == Operation.EQUAL) { return false; @@ -82,24 +70,22 @@ private String einklammern(String text) { // invariant: Diff instances always use System line separators final String lineSeparator = LineSeparator.SYSTEM.toString(); - if (text.equals(lineSeparator)) { - return text; - } else if (text.endsWith(lineSeparator)) { + if (text.endsWith(lineSeparator)) { return "[" + text.substring(0, text.length() - lineSeparator.length()) + "]" + lineSeparator; } else { return "[" + text + "]"; } } - public String renderEqualsDiff(String diffText, EqualDiffPosition position, int lineNumber) { - return position.render(diffText, contextLines, lineNumber); + public String renderEqualsDiff(String diffText, EqualDiffPosition position) { + return position.render(diffText, contextLines); } enum EqualDiffPosition { START { @Override - protected String render(String diffText, int contextLines, int lineNumber) { + protected String render(String diffText, int contextLines) { final int totalLines = (int) diffText.lines().count(); final StringBuilder b = new StringBuilder(); @@ -124,7 +110,7 @@ protected String render(String diffText, int contextLines, int lineNumber) { }, MIDDLE { @Override - protected String render(String diffText, int contextLines, int lineNumber) { + protected String render(String diffText, int contextLines) { final int totalLines = (int) diffText.lines().count(); final StringBuilder b = new StringBuilder(); @@ -148,7 +134,7 @@ protected String render(String diffText, int contextLines, int lineNumber) { }, END { @Override - protected String render(String diffText, int contextLines, int lineNumber) { + protected String render(String diffText, int contextLines) { final int totalLines = (int) diffText.lines().count(); final StringBuilder b = new StringBuilder(); @@ -171,7 +157,7 @@ protected String render(String diffText, int contextLines, int lineNumber) { } }; - protected abstract String render(String diffText, int contextLines, int lineNumber); + protected abstract String render(String diffText, int contextLines); } } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/LineSeparator.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/LineSeparator.java index 48b49fe0..593b9a5e 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/LineSeparator.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/LineSeparator.java @@ -13,8 +13,8 @@ enum LineSeparator { CRLF("\r\n", "\\r\\n"), LF("\n", "\\n"), CR("\r", "\\r"), - SYSTEM(System.lineSeparator(), System.lineSeparator().replace("\r", "\\r").replace("\n", "\\n")), - NONE("\n", ""); + DEFAULT("\n", "\\n"), + SYSTEM(System.lineSeparator(), System.lineSeparator().replace("\r", "\\r").replace("\n", "\\n")); private final String characters; private final String displayName; @@ -30,7 +30,7 @@ public static LineSeparator determineFrom(String s) { return lineSeparator; } } - return NONE; + return DEFAULT; } private boolean existsIn(String s) { diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java index 37af00de..74fc17d1 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java @@ -88,28 +88,24 @@ public String toString() { } final ListIterator cursor = diffs.listIterator(); - int lineNr = 1; while (cursor.hasNext()) { - final boolean isBeginning = cursor.hasPrevious(); + final boolean hasPrevious = cursor.hasPrevious(); final Diff current = cursor.next(); - - if (current.operation == Operation.EQUAL) { - if (!isBeginning) { - // equal operation at the beginning of the whole diff - message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.START, lineNr)); - } else if (cursor.hasNext()) { + if (current.operation == Operation.EQUAL && !hasPrevious) { + // equal operation at the beginning + message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.START)); + } else if (current.operation == Operation.EQUAL) { + if (cursor.hasNext()) { // equal operation between 2 changes - message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.MIDDLE, lineNr)); + message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.MIDDLE)); } else { // equal diff at the end - message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.END, lineNr)); + message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.END)); } } else { message.append(diffInterpreter.renderFailureDiff(current)); } - - lineNr += diffInterpreter.countLinebreaks(current); } return message.toString(); } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java index 65d73d9b..42bc6ac7 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java @@ -26,7 +26,7 @@ final class DefaultSnapshotConfiguration implements SnapshotConfiguration { // default number of context lines that will be printed around changes in huge unified // diffs - private static final int DEFAULT_CONTEXT_LINES = SnapshotTestOptions.DEFAULT_TEXT_DIFF_CONTEXT_LINES; + private static final int DEFAULT_CONTEXT_LINES = 5; private final Class testClass; diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsLegacyTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsLegacyTest.java index 467fb451..2cc52b61 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsLegacyTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsLegacyTest.java @@ -14,46 +14,12 @@ @EnableSnapshotTests public class SnapshotsLegacyTest { - @Test - @SnapshotTestOptions(alwaysPersistActualResult = true, alwaysPersistRawResult = true) - void testWriteContextFiles(Snapshot snapshot) throws Exception { - final Person simon = determinePerson(); - final SnapshotTestResult testResult = snapshot.assertThat(simon).asText().matchesSnapshotText(); - - assertThat(testResult.rawActualResultFile()).exists(); - assertThat(testResult.actualResultFile()).exists(); - } - @Test void testDisabledWithNullInput(Snapshot snapshot) throws Exception { final SnapshotTestResult testResult = snapshot.assertThat(null).asText().disabled(); assertThat(testResult.serializedActual()).isEqualTo("<>"); } - @Test - @SnapshotTestOptions(alwaysPersistActualResult = true, alwaysPersistRawResult = true) - void testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreatedWithContextFiles(Snapshot snapshot) - throws Exception { - - final Person simon = determinePerson(); - final SnapshotTestResult testResult = snapshot.assertThat(simon).asText().disabled(); - - assertThat(testResult.rawActualResultFile()).exists(); - assertThat(testResult.actualResultFile()).exists(); - } - - @Test - @SnapshotTestOptions(alwaysPersistActualResult = true, alwaysPersistRawResult = true) - void testWithOneDisabledAssertionForWhichSnapshotHasHasAlreadyBeenCreatedWithContextFiles(Snapshot snapshot) - throws Exception { - - final Person simon = determinePerson(); - final SnapshotTestResult testResult = snapshot.assertThat(simon).asText().disabled(); - - assertThat(testResult.rawActualResultFile()).exists(); - assertThat(testResult.actualResultFile()).exists(); - } - @Test void testWithOneDisabledAssertionForWhichSnapshotHasNotYetBeenCreated(Snapshot snapshot) throws Exception { final Person simon = determinePerson(); diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java index 37acc3f6..1cb44059 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java @@ -2,198 +2,114 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; public class TextDiffTest { - private final DiffInterpreter diffInterpreter = new DiffInterpreter(); - - @Nested - class IgnoringWhitespaceChanges { - { - diffInterpreter.withIgnoreWhitespaceChanges(true); - } - - @Test - void testHasDifferenceFalseIfOnlyWhitespaceChanges() throws Exception { - final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, " ", " "); - assertThat(diffOf.hasDifference()).isFalse(); - } - - @Test - void testHasDifferenceFalseIfOnlyLineSeparatorChanges() throws Exception { - final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, "a\rb", "a\nb"); - assertThat(diffOf.hasDifference()).isFalse(); - } - - @Test - void testHasDifferenceFalseIfLineSeparatorAdded() throws Exception { - final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, "ab", "a\nb"); - assertThat(diffOf.hasDifference()).isFalse(); - } - - @Test - void testHasDifferenceFalseIfLineSeparatorRemoved() throws Exception { - final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, "a\nb", "ab"); - assertThat(diffOf.hasDifference()).isFalse(); - } - - @Test - void testDiffOfEmptyStrings() throws Exception { - final TextDiff diff = TextDiff.diffOf(diffInterpreter, "", ""); - assertThat(diff.toString()).isEmpty(); - assertThat(diff.hasDifference()).isFalse(); - } - - @Nested - class With5ContextLines { - { - diffInterpreter.withContextLines(5); - } - - @Test - void testDiffWithHugeEqualBlockAtTheStart() throws Exception { - final TextDiff diff = TextDiff.diffOf(diffInterpreter, - "1\n2\n3\n4\n5\n6\n7\nline1", - "1\n2\n3\n4\n5\n6\n7\nlineX"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "[...]\n4\n5\n6\n7\nline-[1]+[X]")); - assertThat(diff.hasDifference()).isTrue(); - } - - @Test - void testDiffWithHugeEqualBlockAtTheEnd() throws Exception { - final TextDiff diff = TextDiff.diffOf(diffInterpreter, - "line1\n1\n2\n3\n4\n5\n6\n7", - "lineX\n1\n2\n3\n4\n5\n6\n7"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "line-[1]+[X]\n1\n2\n3\n4\n[...]")); - assertThat(diff.hasDifference()).isTrue(); - } - - @Test - void testDiffWithHugeEqualBlockAtTheStartAndTheEnd() throws Exception { - final TextDiff diff = TextDiff.diffOf(diffInterpreter, - "1\n2\n3\n4\n5\n6\n7\nline1\n8\n9\n10\n11\n12\n13\n14", - "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "[...]\n3\n4\n5\n6\n7\n-[line1]\n8\n9\n10\n11\n12\n[...]")); - assertThat(diff.hasDifference()).isTrue(); - } - - @Test - void testDiffWithHugeEqualBlockInTheMiddle() throws Exception { - final TextDiff diff = TextDiff.diffOf(diffInterpreter, - "line1\n1\n2\n3\n4\n5\n6\n7\nline2\n8\n9\n10\n11\n12\n13\n14\nline3", - "line2\n1\n2\n3\n4\n5\n6\n7\nline2\n8\n9\n10\n11\n12\n13\n14\nline4"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "line-[1]+[2]\n1\n2\n3\n4\n[...]\n11\n12\n13\n14\nline-[3]+[4]")); - assertThat(diff.hasDifference()).isTrue(); - } - } + @Test + void testHasDifferenceFalseIfIgnoreWhitespacesAndOnlyWhitespaceChanges() throws Exception { + final TextDiff diffOf = TextDiff.diffOf(new DiffInterpreter().withIgnoreWhitespaceChanges(true), " ", " "); + assertThat(diffOf.hasDifference()).isFalse(); } - @Nested - class ObeyingWhitespaceChanges { - { - diffInterpreter.withIgnoreWhitespaceChanges(false); - } - - @Test - void testHasDifferenceTrueIfOnlyWhitespaceChanges() throws Exception { - final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, " ", " "); - assertThat(diffOf.hasDifference()).isTrue(); - } - - @Test - void testHasDifferenceTrueIfOnlyLineSeparatorChanges() throws Exception { - final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, "a\rb", "a\n\nb"); - assertThat(diffOf.hasDifference()).isTrue(); - assertThat(diffOf.toString()).isEqualTo(LineSeparator.SYSTEM.convert( - "Strings differ in linebreaks. Expected: 'CR(\\r)', Actual encountered: 'LF(\\n)'")); - } - - @Test - void testHasDifferenceTrueIfLineSeparatorAdded() throws Exception { - final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, "ab", "a\nb"); - assertThat(diffOf.hasDifference()).isTrue(); - assertThat(diffOf.toString()).isEqualTo(LineSeparator.SYSTEM.convert( - "Strings differ in linebreaks. Expected: 'NONE()', Actual encountered: 'LF(\\n)'\n\na+\nb")); - } + @Test + void testHasDifferenceFalseIfIgnoreWhitespacesAndOnlyLineSeparatorChanges() throws Exception { + final TextDiff diffOf = TextDiff.diffOf(new DiffInterpreter().withIgnoreWhitespaceChanges(true), "a\rb", + "a\nb"); + assertThat(diffOf.hasDifference()).isFalse(); + } - @Test - void testHasDifferenceTrueIfLineSeparatorRemoved() throws Exception { - final TextDiff diffOf = TextDiff.diffOf(diffInterpreter, "a\nb", "ab"); - assertThat(diffOf.hasDifference()).isTrue(); - assertThat(diffOf.toString()).isEqualTo(LineSeparator.SYSTEM.convert( - "Strings differ in linebreaks. Expected: 'LF(\\n)', Actual encountered: 'NONE()'\n\na-\nb")); - } + @Test + void testDiffOfEmptyStrings() throws Exception { + final TextDiff diff = TextDiff.diffOf(new DiffInterpreter(), "", ""); + assertThat(diff.toString()).isEmpty(); + assertThat(diff.hasDifference()).isFalse(); + } - @Nested - class With5ContextLines { - { - diffInterpreter.withContextLines(5); - } + @Test + void testDiffWithLinebreaksAndLessThanContextLinesWithTrailingLineBreak() throws Exception { + final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withIgnoreWhitespaceChanges(false) + .withContextLines(50), + "1\n2\n3\n4\n5\n6\n7\nline1\n", "1\n2\n3\n4\n5\n6\n7\nlineX\n"); + assertThat(diff.toString()).isEqualTo( + LineSeparator.SYSTEM.convert( + "1\n2\n3\n4\n5\n6\n7\nline-[1]+[X]\n")); + assertThat(diff.hasDifference()).isTrue(); + } - @Test - void testDiffOnlyInLinebreaks() throws Exception { - final TextDiff diff = TextDiff.diffOf(diffInterpreter, - "line1\n1\n2\n3\n4\n5\n6\n7", "line1\r1\r2\r3\r4\r5\r6\r7"); - assertThat(diff.toString()) - .isEqualTo("Strings differ in linebreaks. Expected: 'LF(\\n)', Actual encountered: 'CR(\\r)'"); - } + @Test + void testDiffWithLinebreaksAndLessThanContextLinesWithoutTrailingLineBreak() throws Exception { + final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withIgnoreWhitespaceChanges(false) + .withContextLines(50), + "1\n2\n3\n4\n5\n6\n7\nline1", "1\n2\n3\n4\n5\n6\n7\nlineX"); + assertThat(diff.toString()).isEqualTo( + LineSeparator.SYSTEM.convert( + "1\n2\n3\n4\n5\n6\n7\nline-[1]+[X]")); + assertThat(diff.hasDifference()).isTrue(); + } - @Test - void testDiffInLinebreaks() throws Exception { - final TextDiff diff = TextDiff.diffOf(diffInterpreter, - "line1\n1\n2\n3\n4\n5\n6\n7", "lineX\r1\r2\r3\r4\r5\r6\r7"); + @Test + void testDiffOnlyInLinebreaks() throws Exception { + final TextDiff diff = TextDiff.diffOf(new DiffInterpreter() + .withContextLines(5) + .withIgnoreWhitespaceChanges(false), + "line1\n1\n2\n3\n4\n5\n6\n7", "line1\r1\r2\r3\r4\r5\r6\r7"); + assertThat(diff.toString()) + .isEqualTo("Strings differ in linebreaks. Expected: 'LF(\\n)', Actual encountered: 'CR(\\r)'"); + } - assertThat(diff.toString()).isEqualTo(LineSeparator.SYSTEM.convert( + @Test + void testDiffInLinebreaks() throws Exception { + final TextDiff diff = TextDiff.diffOf(new DiffInterpreter() + .withContextLines(5) + .withIgnoreWhitespaceChanges(false), + "line1\n1\n2\n3\n4\n5\n6\n7", "lineX\r1\r2\r3\r4\r5\r6\r7"); + + assertThat(diff.toString()) + .isEqualTo(LineSeparator.SYSTEM.convert( "Strings differ in linebreaks. Expected: 'LF(\\n)', Actual encountered: 'CR(\\r)'\n\nline-[1]+[X]\r1\r2\r3\r4\r[...]")); - assertThat(diff.hasDifference()).isTrue(); - } - } - - @Nested - class WithMoreContextLinesThanEqualDiffLines { - { - diffInterpreter.withContextLines(50); - } - - @Test - void testDiffWithLinebreaksAndTrailingLineBreak() throws Exception { - final TextDiff diff = TextDiff.diffOf(diffInterpreter, - "1\n2\n3\n4\n5\n6\n7\nline1\n", "1\n2\n3\n4\n5\n6\n7\nlineX\n"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "1\n2\n3\n4\n5\n6\n7\nline-[1]+[X]\n")); - assertThat(diff.hasDifference()).isTrue(); - } + assertThat(diff.hasDifference()).isTrue(); + } - @Test - void testDiffWithLinebreaksAndWithoutTrailingLineBreak() throws Exception { - final TextDiff diff = TextDiff.diffOf(diffInterpreter, - "1\n2\n3\n4\n5\n6\n7\nline1", "1\n2\n3\n4\n5\n6\n7\nlineX"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "1\n2\n3\n4\n5\n6\n7\nline-[1]+[X]")); - assertThat(diff.hasDifference()).isTrue(); - } - } + @Test + void testDiffWithHugeEqualBlockAtTheStart() throws Exception { + final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withContextLines(5), + "1\n2\n3\n4\n5\n6\n7\nline1", "1\n2\n3\n4\n5\n6\n7\nlineX"); + assertThat(diff.toString()).isEqualTo( + LineSeparator.SYSTEM.convert( + "[...]\n4\n5\n6\n7\nline-[1]+[X]")); + assertThat(diff.hasDifference()).isTrue(); } @Test - void testSimpleDiffWithNoLinebreaks() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter(), - "line1line1line1line1line1", - "line1line1line2line1line1"); + void testDiffWithHugeEqualBlockAtTheEnd() throws Exception { + final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withContextLines(5), + "line1\n1\n2\n3\n4\n5\n6\n7", "lineX\n1\n2\n3\n4\n5\n6\n7"); + assertThat(diff.toString()).isEqualTo( + LineSeparator.SYSTEM.convert( + "line-[1]+[X]\n1\n2\n3\n4\n[...]")); + assertThat(diff.hasDifference()).isTrue(); + } - assertThat(diff.toString()).isEqualTo("line1line1line-[1]+[2]line1line1"); + @Test + void testDiffWithHugeEqualBlockAtTheStartAndTheEnd() throws Exception { + final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withContextLines(5), + "1\n2\n3\n4\n5\n6\n7\nline1\n8\n9\n10\n11\n12\n13\n14", + "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14"); + assertThat(diff.toString()).isEqualTo( + LineSeparator.SYSTEM.convert( + "[...]\n3\n4\n5\n6\n7\n-[line1]\n8\n9\n10\n11\n12\n[...]")); + assertThat(diff.hasDifference()).isTrue(); } + @Test + void testDiffWithHugeEqualBlockInTheMiddle() throws Exception { + final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withContextLines(5), + "line1\n1\n2\n3\n4\n5\n6\n7\nline2\n8\n9\n10\n11\n12\n13\n14\nline3", + "line2\n1\n2\n3\n4\n5\n6\n7\nline2\n8\n9\n10\n11\n12\n13\n14\nline4"); + assertThat(diff.toString()).isEqualTo( + LineSeparator.SYSTEM.convert( + "line-[1]+[2]\n1\n2\n3\n4\n[...]\n11\n12\n13\n14\nline-[3]+[4]")); + assertThat(diff.hasDifference()).isTrue(); + } } From b54f654a2b4b4fa75816ab1b8cd83ba3006cd6c7 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Thu, 15 Dec 2022 18:01:21 +0100 Subject: [PATCH 16/27] Improve diff rendering --- README.md | 4 +- RELEASE_NOTES.md | 2 + pom.xml | 2 + readme/README.md | 4 +- readme/RELEASE_NOTES.md | 2 + snapshot-tests-core/pom.xml | 12 +- .../snapshots/data/text/DiffInterpreter.java | 163 -- .../data/text/DiffListLookahead.java | 29 + .../snapshots/data/text/DiffRenderer.java | 10 + .../data/text/SplitDiffRenderer.java | 95 + .../test/snapshots/data/text/TextDiff.java | 169 +- .../data/text/TextDiffAssertionError.java | 6 +- .../text/TextDiffStructuralAssertions.java | 15 +- .../snapshots/data/text/TextSnapshot.java | 9 +- .../data/text/UnifiedDiffRenderer.java | 104 + .../snapshots/data/text/diff_match_patch.java | 2503 ----------------- .../test/snapshots/impl/SnapshotTestImpl.java | 10 +- .../Junit5SnapshotTestContextProvider.java | 12 - .../snapshots/data/text/TextDiffTest.java | 228 +- .../data/text/diff_match_patch_Test.java | 1125 -------- .../snapshots/impl/FailingSnapshotTests.java | 15 +- .../impl/FailingSnapshotTestsNew.java | 9 +- .../testRenderDiff_0.snapshot | 34 + snapshot-tests-dependencies/pom.xml | 8 +- 24 files changed, 576 insertions(+), 3994 deletions(-) delete mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffInterpreter.java create mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffListLookahead.java create mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffRenderer.java create mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/SplitDiffRenderer.java create mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java delete mode 100644 snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/diff_match_patch.java delete mode 100644 snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/diff_match_patch_Test.java create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/data/text/TextDiffTest_snapshots/testRenderDiff_0.snapshot diff --git a/README.md b/README.md index 1191b027..b4366313 100644 --- a/README.md +++ b/README.md @@ -123,8 +123,8 @@ void testSnapshotToString(Snapshot snapshot) throws Exception { ### Structural assertions Once serialized, the library uses `StructuralAssertions` to compare two serialized objects. By default, we use -`xml-unit` for comparing xmls and `jsonassert` for comparing jsons. Generic text comparison is implemented using the -awesome `diff_match_patch` class from Neil Fraser. +`xml-unit` for comparing xmls and `jsonassert` for comparing jsons. Generic text comparison is implemented using `java-diff-utils`. + When using a custom `SnapshotSerializer` you can also supply a custom `StructuralAssertions` implementation to implement comparisons specific to your serialization format. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5f5d4325..71073987 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -10,6 +10,8 @@ * Add the possibility to configure the number of context lines printed around a change in the default unified diff via `@SnapshotTestOptions.textDiffContextLines()` * Add the possibility to always persist the latest actual result as a sibling file of the real `.snapshot` file via `@SnapshotTestOptions.alwaysPersistActualResult()` * Add the possibility to additionally persist the raw actual result in a sibling file of the real `.snapshot` file via `@SnapshotTestOptions.alwaysPersistRawResult()`. The raw result does not contain the snapshot header information. +* Improve compatibility with JUnit5's `@Nested` tests (general support for `@Nested` is still experimental though) +* Unified diffs within assertion failure messages now come with full line information _Note_: This release comes with a few major deprecations that are preparing our transition to the next major version diff --git a/pom.xml b/pom.xml index 4b5a2220..a14ae057 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,8 @@ 3.22.0 1.2.0 + 4.12 + 2.3.1 2.3.3 4.0.1 diff --git a/readme/README.md b/readme/README.md index 7faa04ea..c57185f3 100644 --- a/readme/README.md +++ b/readme/README.md @@ -123,8 +123,8 @@ void testSnapshotToString(Snapshot snapshot) throws Exception { ### Structural assertions Once serialized, the library uses `StructuralAssertions` to compare two serialized objects. By default, we use -`xml-unit` for comparing xmls and `jsonassert` for comparing jsons. Generic text comparison is implemented using the -awesome `diff_match_patch` class from Neil Fraser. +`xml-unit` for comparing xmls and `jsonassert` for comparing jsons. Generic text comparison is implemented using `java-diff-utils`. + When using a custom `SnapshotSerializer` you can also supply a custom `StructuralAssertions` implementation to implement comparisons specific to your serialization format. diff --git a/readme/RELEASE_NOTES.md b/readme/RELEASE_NOTES.md index b68e7c9e..c38747f8 100644 --- a/readme/RELEASE_NOTES.md +++ b/readme/RELEASE_NOTES.md @@ -10,6 +10,8 @@ * Add the possibility to configure the number of context lines printed around a change in the default unified diff via `@SnapshotTestOptions.textDiffContextLines()` * Add the possibility to always persist the latest actual result as a sibling file of the real `.snapshot` file via `@SnapshotTestOptions.alwaysPersistActualResult()` * Add the possibility to additionally persist the raw actual result in a sibling file of the real `.snapshot` file via `@SnapshotTestOptions.alwaysPersistRawResult()`. The raw result does not contain the snapshot header information. +* Improve compatibility with JUnit5's `@Nested` tests (general support for `@Nested` is still experimental though) +* Unified diffs within assertion failure messages now come with full line information _Note_: This release comes with a few major deprecations that are preparing our transition to the next major version diff --git a/snapshot-tests-core/pom.xml b/snapshot-tests-core/pom.xml index 47d9272c..2759f97a 100644 --- a/snapshot-tests-core/pom.xml +++ b/snapshot-tests-core/pom.xml @@ -1,7 +1,6 @@ - - + + 4.0.0 de.skuzzle.test @@ -36,12 +35,15 @@ ${project.groupId} snapshot-tests-common - + + io.github.java-diff-utils + java-diff-utils + org.apiguardian apiguardian-api - + org.junit.jupiter junit-jupiter-api diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffInterpreter.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffInterpreter.java deleted file mode 100644 index e93afde4..00000000 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffInterpreter.java +++ /dev/null @@ -1,163 +0,0 @@ -package de.skuzzle.test.snapshots.data.text; - -import java.util.Collection; -import java.util.Iterator; -import java.util.regex.Pattern; - -import de.skuzzle.test.snapshots.data.text.diff_match_patch.Diff; -import de.skuzzle.test.snapshots.data.text.diff_match_patch.Operation; -import de.skuzzle.test.snapshots.validation.Arguments; - -final class DiffInterpreter { - - private static final Pattern WHITESPACE_ONLY = Pattern.compile("\\h+"); - - private boolean ignoreWhitespaceChanges = true; - private int contextLines = Integer.MAX_VALUE; - - public DiffInterpreter withContextLines(int contextLines) { - Arguments.check(contextLines >= 0, "contextLines must be > 0 but was %d", contextLines); - this.contextLines = contextLines; - return this; - } - - public DiffInterpreter withIgnoreWhitespaceChanges(boolean ignoreWhitespaceChanges) { - this.ignoreWhitespaceChanges = ignoreWhitespaceChanges; - return this; - } - - private boolean isWhitespace(Diff diff) { - return WHITESPACE_ONLY.matcher(diff.text).matches(); - } - - public boolean hasFailures(Collection diffs) { - return diffs.stream().anyMatch(this::isFailureDifference); - } - - private boolean isFailureDifference(Diff diff) { - if (diff.operation == Operation.EQUAL) { - return false; - } - if (isWhitespace(diff)) { - return !ignoreWhitespaceChanges; - } - return true; - } - - boolean hasLineSeparatorDifference(LineSeparator l1, LineSeparator l2) { - if (ignoreWhitespaceChanges) { - return false; - } - return l1 != l2; - } - - public String renderFailureDiff(Diff diff) { - if (isFailureDifference(diff)) { - switch (diff.operation) { - case DELETE: - return "-" + einklammern(diff.text); - case INSERT: - return "+" + einklammern(diff.text); - default: - throw new IllegalStateException(); - } - } else { - return diff.text; - } - } - - private String einklammern(String text) { - // invariant: Diff instances always use System line separators - final String lineSeparator = LineSeparator.SYSTEM.toString(); - - if (text.endsWith(lineSeparator)) { - return "[" + text.substring(0, text.length() - lineSeparator.length()) + "]" + lineSeparator; - } else { - return "[" + text + "]"; - } - } - - public String renderEqualsDiff(String diffText, EqualDiffPosition position) { - return position.render(diffText, contextLines); - } - - enum EqualDiffPosition { - START { - - @Override - protected String render(String diffText, int contextLines) { - final int totalLines = (int) diffText.lines().count(); - - final StringBuilder b = new StringBuilder(); - boolean appendOnce = true; - final Iterator lineIterator = diffText.lines().iterator(); - for (int lineNr = 0; lineNr < totalLines; ++lineNr) { - final String nextLine = lineIterator.next(); - - if (lineNr >= totalLines - contextLines) { - b.append(nextLine); - if (lineNr < totalLines - 1 || LineSeparator.SYSTEM.endsWith(diffText)) { - b.append(LineSeparator.SYSTEM); - } - } else if (appendOnce) { - b.append("[...]").append(LineSeparator.SYSTEM); - appendOnce = false; - } - } - return b.toString(); - } - - }, - MIDDLE { - @Override - protected String render(String diffText, int contextLines) { - final int totalLines = (int) diffText.lines().count(); - - final StringBuilder b = new StringBuilder(); - boolean appendOnce = true; - final Iterator lineIterator = diffText.lines().iterator(); - for (int lineNr = 0; lineNr < totalLines; ++lineNr) { - final String nextLine = lineIterator.next(); - - if (lineNr < contextLines || lineNr >= totalLines - contextLines) { - b.append(nextLine); - if (lineNr < totalLines - 1) { - b.append(LineSeparator.SYSTEM); - } - } else if (appendOnce) { - b.append("[...]").append(LineSeparator.SYSTEM); - appendOnce = false; - } - } - return b.toString(); - } - }, - END { - @Override - protected String render(String diffText, int contextLines) { - final int totalLines = (int) diffText.lines().count(); - - final StringBuilder b = new StringBuilder(); - boolean appendOnce = true; - final Iterator lineIterator = diffText.lines().iterator(); - for (int lineNr = 0; lineNr < totalLines; ++lineNr) { - final String nextLine = lineIterator.next(); - - if (lineNr < contextLines) { - b.append(nextLine); - if (lineNr < totalLines - 1) { - b.append(LineSeparator.SYSTEM); - } - } else if (appendOnce) { - b.append("[...]"); - appendOnce = false; - } - } - return b.toString(); - } - }; - - protected abstract String render(String diffText, int contextLines); - } - -} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffListLookahead.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffListLookahead.java new file mode 100644 index 00000000..4edebfb4 --- /dev/null +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffListLookahead.java @@ -0,0 +1,29 @@ +package de.skuzzle.test.snapshots.data.text; + +import java.util.List; + +import com.github.difflib.text.DiffRow; +import com.github.difflib.text.DiffRow.Tag; + +class DiffListLookahead { + + public static int indexOfNextNonEqual(List fullDiff, int startIndex) { + for (int i = startIndex; i < fullDiff.size(); ++i) { + if (fullDiff.get(i).getTag() != Tag.EQUAL) { + return i; + } + } + return fullDiff.size(); + } + + public static int lookBehind(List fullDiff, int startIndex) { + int count = 0; + for (int i = startIndex; i >= 0; --i) { + if (fullDiff.get(i).getTag() != Tag.EQUAL) { + return count; + } + count++; + } + return count; + } +} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffRenderer.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffRenderer.java new file mode 100644 index 00000000..f3996379 --- /dev/null +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffRenderer.java @@ -0,0 +1,10 @@ +package de.skuzzle.test.snapshots.data.text; + +import java.util.List; + +import com.github.difflib.text.DiffRow; + +interface DiffRenderer { + + String renderDiff(List rows, int contextLines); +} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/SplitDiffRenderer.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/SplitDiffRenderer.java new file mode 100644 index 00000000..305ca64d --- /dev/null +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/SplitDiffRenderer.java @@ -0,0 +1,95 @@ +package de.skuzzle.test.snapshots.data.text; + +import java.util.Iterator; +import java.util.List; + +import com.github.difflib.text.DiffRow; + +final class SplitDiffRenderer implements DiffRenderer { + + private static final String OPERATION_ADD = "+"; + private static final String OPERATION_DELETE = "-"; + private static final String OPERATION_CHANGE_EQUAL = " "; + + private String padLeft(String text, int targetWidth) { + final int missingSpaces = targetWidth - text.length(); + return " ".repeat(missingSpaces) + text; + } + + private String padRight(String text, int targetWidth) { + final int missingSpaces = targetWidth - text.length(); + return text + " ".repeat(missingSpaces); + } + + private void appendRow(StringBuilder b, int columnWidth, String paddedExpectedLineNumber, String expectedOperation, + String expectedLine, String paddedActualLineNumber, String actualOperation, String actualLine) { + b.append(paddedExpectedLineNumber) + .append(" ") + .append(expectedOperation) + .append(" ") + .append(padRight(expectedLine, columnWidth)) + .append(" |") + .append(paddedActualLineNumber) + .append(" ").append(actualOperation).append(" ").append(actualLine); + + } + + @Override + public String renderDiff(List rows, int contextLines) { + final int columnWidth = rows.stream().map(DiffRow::getOldLine).mapToInt(String::length).max().orElse(80); + final int rowCountEstimate = rows.size(); + final int rowNumberWidth = Math.max(2, (int) Math.log10(rowCountEstimate) + 1); + + int expectedLine = 1; + int actualLine = 1; + final StringBuilder b = new StringBuilder(); + for (final Iterator iterator = rows.iterator(); iterator.hasNext();) { + final DiffRow diffRow = iterator.next(); + + switch (diffRow.getTag()) { + case CHANGE: + case EQUAL: + appendRow(b, columnWidth, + padLeft("" + expectedLine, rowNumberWidth), + OPERATION_CHANGE_EQUAL, + diffRow.getOldLine(), + padLeft("" + actualLine, rowNumberWidth), + OPERATION_CHANGE_EQUAL, diffRow.getNewLine()); + + expectedLine++; + actualLine++; + break; + case DELETE: + appendRow(b, columnWidth, + padLeft("" + expectedLine, rowNumberWidth), + OPERATION_DELETE, + diffRow.getOldLine(), + " ".repeat(rowNumberWidth), + OPERATION_CHANGE_EQUAL, diffRow.getNewLine()); + + expectedLine++; + break; + + case INSERT: + appendRow(b, columnWidth, + " ".repeat(rowNumberWidth), + OPERATION_CHANGE_EQUAL, + diffRow.getOldLine(), + padLeft("" + actualLine, rowNumberWidth), + OPERATION_ADD, diffRow.getNewLine()); + + actualLine++; + break; + default: + throw new IllegalStateException(); + + } + + if (iterator.hasNext()) { + b.append(LineSeparator.SYSTEM); + } + } + + return b.toString(); + } +} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java index 74fc17d1..482e9311 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java @@ -1,112 +1,147 @@ package de.skuzzle.test.snapshots.data.text; -import java.util.LinkedList; +import static java.util.stream.Collectors.toList; + import java.util.List; -import java.util.ListIterator; +import java.util.function.BiFunction; +import java.util.function.Function; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; -import de.skuzzle.test.snapshots.data.text.DiffInterpreter.EqualDiffPosition; -import de.skuzzle.test.snapshots.data.text.diff_match_patch.Diff; -import de.skuzzle.test.snapshots.data.text.diff_match_patch.Operation; +import com.github.difflib.text.DiffRow; +import com.github.difflib.text.DiffRow.Tag; +import com.github.difflib.text.DiffRowGenerator; + import de.skuzzle.test.snapshots.validation.Arguments; /** - * Creates a unified diff of 2 Strings using the popular Neil Fraser diff_match_patch - * implementation under the hood. + * Creates a diff of 2 Strings. For internal use only. Public API is encapsulated in + * {@link TextSnapshot}. * * @author Simon Taddiken */ @API(status = Status.INTERNAL, since = "1.4.0") public final class TextDiff { - private final DiffInterpreter diffInterpreter; - private final List diffs; + private final Settings settings; + private final List diffRows; private final LineSeparator expectedLineSeparator; private final LineSeparator actualLineSeparator; - private TextDiff(DiffInterpreter diffInterpreter, List diffs, - LineSeparator expectedLineSeparator, LineSeparator actualLineSeparator) { - this.diffInterpreter = diffInterpreter; - this.diffs = diffs; + private TextDiff(Settings settings, List diffRows, LineSeparator expectedLineSeparator, + LineSeparator actualLineSeparator) { + this.settings = settings; + this.diffRows = diffRows; this.expectedLineSeparator = expectedLineSeparator; this.actualLineSeparator = actualLineSeparator; } - public static TextDiff diffOf(String expected, String actual, int contextLines) { - return diffOf(new DiffInterpreter() - .withIgnoreWhitespaceChanges(false) - .withContextLines(contextLines), - expected, actual); - } - - static TextDiff diffOf(DiffInterpreter diffInterpreter, String expected, String actual) { + public static TextDiff compare(Settings settings, String expected, String actual) { + Arguments.requireNonNull(settings != null, "settings must not be null"); Arguments.requireNonNull(expected, "expected String must not be null"); Arguments.requireNonNull(actual, "actual String must not be null"); final LineSeparator expectedLineSeparator = LineSeparator.determineFrom(expected); final LineSeparator actualLineSeparator = LineSeparator.determineFrom(actual); - final diff_match_patch diff_match_patch = new diff_match_patch(); - - final LinkedList diffs = diff_match_patch.diff_main( - sanitizeLineSeparators(expected), - sanitizeLineSeparators(actual)); - diff_match_patch.diff_cleanupSemanticLossless(diffs); - - return new TextDiff(diffInterpreter, diffs, expectedLineSeparator, actualLineSeparator); + final List diffRows = settings.buildDiffRowGenerator().generateDiffRows( + expected.lines().collect(toList()), + actual.lines().collect(toList())); + return new TextDiff(settings, diffRows, expectedLineSeparator, actualLineSeparator); } - private static String sanitizeLineSeparators(String s) { - return LineSeparator.SYSTEM.convert(s); - } + public static final class Settings { + private boolean ignoreWhitespaces = false; + private int contextLines = Integer.MAX_VALUE; + private String inlineOpeningChangeMarker = "<<"; + private String inlineClosingChangeMarker = ">>"; + private DiffRenderer diffRenderer = new UnifiedDiffRenderer(); - public boolean hasDifference() { - return diffInterpreter.hasFailures(diffs) - || diffInterpreter.hasLineSeparatorDifference(expectedLineSeparator, actualLineSeparator); - } + private Settings() { + // hidden + } - @Override - public String toString() { - if (diffs.isEmpty()) { - return ""; + public static Settings defaultSettings() { + return new Settings(); } - final StringBuilder message = new StringBuilder(); + public Settings withIgnoreWhitespaces(boolean ignoreWhitespaces) { + this.ignoreWhitespaces = ignoreWhitespaces; + return this; + } - if (diffInterpreter.hasLineSeparatorDifference(expectedLineSeparator, actualLineSeparator)) { - message.append(String.format( - "Strings differ in linebreaks. Expected: '%s', Actual encountered: '%s'", - expectedLineSeparator.displayName(), actualLineSeparator.displayName())); + public Settings withContextLines(int contextLines) { + Arguments.check(contextLines >= 0, "contextLines must be a positive integer"); + this.contextLines = contextLines; + return this; + } - if (diffs.size() == 1 && diffs.get(0).operation == Operation.EQUAL) { - return message.toString(); - } + public Settings withInlineOpeningChangeMarker(String inlineOpeningChangeMarker) { + this.inlineOpeningChangeMarker = Arguments.requireNonNull(inlineOpeningChangeMarker, + "opening marker must not be null"); + ; + return this; + } - message.append(LineSeparator.SYSTEM).append(LineSeparator.SYSTEM); + public Settings withInlineClosingChangeMarker(String inlineClosingChangeMarker) { + this.inlineClosingChangeMarker = Arguments.requireNonNull(inlineClosingChangeMarker, + "closing marker must not be null"); + return this; } - final ListIterator cursor = diffs.listIterator(); - while (cursor.hasNext()) { - final boolean hasPrevious = cursor.hasPrevious(); - - final Diff current = cursor.next(); - if (current.operation == Operation.EQUAL && !hasPrevious) { - // equal operation at the beginning - message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.START)); - } else if (current.operation == Operation.EQUAL) { - if (cursor.hasNext()) { - // equal operation between 2 changes - message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.MIDDLE)); - } else { - // equal diff at the end - message.append(diffInterpreter.renderEqualsDiff(current.text, EqualDiffPosition.END)); + public Settings withDiffRenderer(DiffRenderer renderer) { + this.diffRenderer = Arguments.requireNonNull(renderer, "renderer must not be null"); + return this; + } + + private BiFunction inlineMarker() { + return (tag, isOpening) -> { + if (tag != Tag.CHANGE) { + return ""; } - } else { - message.append(diffInterpreter.renderFailureDiff(current)); + return isOpening ? inlineOpeningChangeMarker : inlineClosingChangeMarker; + }; + } + + private DiffRowGenerator buildDiffRowGenerator() { + return DiffRowGenerator.create() + .showInlineDiffs(true) + .lineNormalizer(Function.identity()) + .inlineDiffByWord(true) + .ignoreWhiteSpaces(ignoreWhitespaces) + .newTag(inlineMarker()) + .oldTag(inlineMarker()) + .build(); + } + } + + private boolean hasLinebreakChange() { + return expectedLineSeparator != actualLineSeparator && !settings.ignoreWhitespaces; + } + + private boolean hasTextDifference() { + return diffRows.stream().map(DiffRow::getTag).anyMatch(tag -> tag != Tag.EQUAL); + } + + public boolean changesDetected() { + return hasLinebreakChange() || hasTextDifference(); + } + + @Override + public String toString() { + final StringBuilder result = new StringBuilder(); + if (hasLinebreakChange()) { + result.append("Strings differ in linebreaks. Expected: '") + .append(expectedLineSeparator.displayName()) + .append("', Actual encountered: '").append(actualLineSeparator.displayName()).append("'"); + + if (hasTextDifference()) { + result.append(LineSeparator.SYSTEM) + .append(LineSeparator.SYSTEM); } } - return message.toString(); + result.append(settings.diffRenderer.renderDiff(diffRows, settings.contextLines)); + return result.toString(); } } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiffAssertionError.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiffAssertionError.java index eac17869..3b811e77 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiffAssertionError.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiffAssertionError.java @@ -6,9 +6,9 @@ import de.skuzzle.test.snapshots.validation.Arguments; /** - * Internal exception that is used to communicate the {@link TextDiff} instance created by the - * {@link TextDiffStructuralAssertions} to the snapshot test. - * + * Internal exception that is used to communicate the {@link TextDiff} instance created by + * the {@link TextDiffStructuralAssertions} to the snapshot test. + * * @author Simon Taddiken * @since 1.5.0 */ diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiffStructuralAssertions.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiffStructuralAssertions.java index 926cae0b..359bd5cd 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiffStructuralAssertions.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiffStructuralAssertions.java @@ -1,20 +1,23 @@ package de.skuzzle.test.snapshots.data.text; +import de.skuzzle.test.snapshots.SnapshotException; import de.skuzzle.test.snapshots.StructuralAssertions; +import de.skuzzle.test.snapshots.data.text.TextDiff.Settings; final class TextDiffStructuralAssertions implements StructuralAssertions { - private final DiffInterpreter diffInterpreter; + private final Settings settings; - TextDiffStructuralAssertions(DiffInterpreter diffInterpreter) { - this.diffInterpreter = diffInterpreter; + public TextDiffStructuralAssertions(Settings settings) { + this.settings = settings; } @Override - public void assertEquals(String storedSnapshot, String serializedActual) { - final TextDiff textDiff = TextDiff.diffOf(diffInterpreter, storedSnapshot, serializedActual); - if (textDiff.hasDifference()) { + public void assertEquals(String storedSnapshot, String serializedActual) throws AssertionError, SnapshotException { + final TextDiff textDiff = TextDiff.compare(settings, storedSnapshot, serializedActual); + if (textDiff.changesDetected()) { throw new TextDiffAssertionError("Stored snapshot doesn't match actual result.", textDiff); } } + } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextSnapshot.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextSnapshot.java index 105c8fb4..69080c32 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextSnapshot.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextSnapshot.java @@ -6,6 +6,7 @@ import de.skuzzle.test.snapshots.StructuralAssertions; import de.skuzzle.test.snapshots.StructuredData; import de.skuzzle.test.snapshots.StructuredDataProvider; +import de.skuzzle.test.snapshots.data.text.TextDiff.Settings; /** * Take snapshots using {@link Object#toString()}. By default, whitespace changes of any @@ -21,7 +22,7 @@ @API(status = Status.STABLE) public final class TextSnapshot implements StructuredDataProvider { - private final DiffInterpreter diffInterpreter = new DiffInterpreter(); + private final Settings settings = Settings.defaultSettings(); /** * Take Snapshots using {@link Object#toString()} and compare the results using a @@ -61,7 +62,7 @@ public static TextSnapshot text() { */ @API(status = Status.STABLE, since = "1.4.0") public TextSnapshot withIgnoreWhitespaces(boolean ignoreWhitespaces) { - this.diffInterpreter.withIgnoreWhitespaceChanges(ignoreWhitespaces); + settings.withIgnoreWhitespaces(ignoreWhitespaces); return this; } @@ -78,13 +79,13 @@ public TextSnapshot withIgnoreWhitespaces(boolean ignoreWhitespaces) { */ @API(status = Status.EXPERIMENTAL, since = "1.5.0") public TextSnapshot withContextLines(int contextLines) { - this.diffInterpreter.withContextLines(contextLines); + settings.withContextLines(contextLines); return this; } @Override public StructuredData build() { - final StructuralAssertions structuralAssertions = new TextDiffStructuralAssertions(diffInterpreter); + final StructuralAssertions structuralAssertions = new TextDiffStructuralAssertions(settings); return StructuredData.with(Object::toString, structuralAssertions); } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java new file mode 100644 index 00000000..6f878b4e --- /dev/null +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java @@ -0,0 +1,104 @@ +package de.skuzzle.test.snapshots.data.text; + +import java.util.List; + +import com.github.difflib.text.DiffRow; +import com.github.difflib.text.DiffRow.Tag; + +final class UnifiedDiffRenderer implements DiffRenderer { + + private String padLeft(String text, int targetWidth) { + final int missingSpaces = targetWidth - text.length(); + return " ".repeat(missingSpaces) + text; + } + + private boolean equalOnly(List rows) { + return rows.stream().map(DiffRow::getTag).allMatch(Tag.EQUAL::equals); + } + + @Override + public String renderDiff(List rows, int contextLines) { + if (equalOnly(rows)) { + return ""; + } + final int rowCountEstimate = rows.size(); + final int rowNumberWidth = Math.max(2, (int) Math.log10(rowCountEstimate) + 1) + 1; + + int expectedLine = 1; + int actualLine = 1; + final StringBuilder b = new StringBuilder(); + + int indexOfLastDifference = -1; + for (int i = 0; i < rows.size(); i++) { + final DiffRow diffRow = rows.get(i); + + switch (diffRow.getTag()) { + case EQUAL: + final int indexOfNextDifference = DiffListLookahead.indexOfNextNonEqual(rows, i); + + final int distanceToNextDifference = indexOfNextDifference == rows.size() + ? Integer.MAX_VALUE + : indexOfNextDifference - i - 1; + final int distanceToPrevDifference = indexOfLastDifference == -1 + ? Integer.MAX_VALUE + : i - indexOfLastDifference - 1; + + if (distanceToNextDifference < contextLines || distanceToPrevDifference < contextLines) { + b + .append(padLeft("" + expectedLine, rowNumberWidth)) + .append(padLeft("" + actualLine, rowNumberWidth)) + .append(" ").append(diffRow.getOldLine()); + + } else if (distanceToNextDifference == contextLines || distanceToPrevDifference == contextLines) { + b.append("[...]"); + } else { + expectedLine++; + actualLine++; + continue; + } + + expectedLine++; + actualLine++; + break; + case CHANGE: + indexOfLastDifference = i; + b + .append(padLeft("" + expectedLine, rowNumberWidth)) + .append(" ".repeat(rowNumberWidth)) + .append(" - ").append(diffRow.getOldLine()).append(LineSeparator.SYSTEM) + .append(" ".repeat(rowNumberWidth)) + .append(padLeft("" + actualLine, rowNumberWidth)) + .append(" + ") + .append(diffRow.getNewLine()); + + expectedLine++; + actualLine++; + break; + + case DELETE: + indexOfLastDifference = i; + b + .append(padLeft("" + expectedLine, rowNumberWidth)) + .append(" ".repeat(rowNumberWidth)) + .append(" - ").append(diffRow.getOldLine()); + break; + case INSERT: + indexOfLastDifference = i; + b + .append(" ".repeat(rowNumberWidth)) + .append(padLeft("" + expectedLine, rowNumberWidth)) + .append(" + ").append(diffRow.getNewLine()); + + break; + default: + throw new IllegalStateException(); + } + + if (i < rows.size() - 1) { + b.append(LineSeparator.SYSTEM); + } + } + + return b.toString(); + } +} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/diff_match_patch.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/diff_match_patch.java deleted file mode 100644 index d27f7d5c..00000000 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/diff_match_patch.java +++ /dev/null @@ -1,2503 +0,0 @@ -/* - * Diff Match and Patch - * Copyright 2018 The diff-match-patch Authors. - * https://github.com/google/diff-match-patch - * - * 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 de.skuzzle.test.snapshots.data.text; - -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Deque; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/* - * Functions for diff, match and patch. - * Computes the difference between two texts to create a patch. - * Applies the patch onto another text, allowing for errors. - * - * @author fraser@google.com (Neil Fraser) - */ - -/** - * Class containing the diff, match and patch methods. Also contains the behaviour - * settings. - */ -class diff_match_patch { - - // Defaults. - // Set these on your diff_match_patch instance to override the defaults. - - /** - * Number of seconds to map a diff before giving up (0 for infinity). - */ - public float Diff_Timeout = 1.0f; - /** - * Cost of an empty edit operation in terms of edit characters. - */ - public short Diff_EditCost = 4; - /** - * At what point is no match declared (0.0 = perfection, 1.0 = very loose). - */ - public float Match_Threshold = 0.5f; - /** - * How far to search for a match (0 = exact location, 1000+ = broad match). A match - * this many characters away from the expected location will add 1.0 to the score (0.0 - * is a perfect match). - */ - public int Match_Distance = 1000; - /** - * When deleting a large block of text (over ~64 characters), how close do the - * contents have to be to match the expected contents. (0.0 = perfection, 1.0 = very - * loose). Note that Match_Threshold controls how closely the end points of a delete - * need to match. - */ - public float Patch_DeleteThreshold = 0.5f; - /** - * Chunk size for context length. - */ - public short Patch_Margin = 4; - - /** - * The number of bits in an int. - */ - private final short Match_MaxBits = 32; - - /** - * Internal class for returning results from diff_linesToChars(). Other less paranoid - * languages just use a three-element array. - */ - protected static class LinesToCharsResult { - protected String chars1; - protected String chars2; - protected List lineArray; - - protected LinesToCharsResult(String chars1, String chars2, - List lineArray) { - this.chars1 = chars1; - this.chars2 = chars2; - this.lineArray = lineArray; - } - } - - // DIFF FUNCTIONS - - /** - * The data structure representing a diff is a Linked list of Diff objects: - * {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"), - * Diff(Operation.EQUAL, " world.")} which means: delete "Hello", add "Goodbye" and - * keep " world." - */ - public enum Operation { - DELETE, - INSERT, - EQUAL - } - - /** - * Find the differences between two texts. Run a faster, slightly less optimal diff. - * This method allows the 'checklines' of diff_main() to be optional. Most of the time - * checklines is wanted, so default to true. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @return Linked List of Diff objects. - */ - public LinkedList diff_main(String text1, String text2) { - return diff_main(text1, text2, true); - } - - /** - * Find the differences between two texts. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @param checklines Speedup flag. If false, then don't run a line-level diff first to - * identify the changed areas. If true, then run a faster slightly less - * optimal diff. - * @return Linked List of Diff objects. - */ - public LinkedList diff_main(String text1, String text2, - boolean checklines) { - // Set a deadline by which time the diff must be complete. - long deadline; - if (Diff_Timeout <= 0) { - deadline = Long.MAX_VALUE; - } else { - deadline = System.currentTimeMillis() + (long) (Diff_Timeout * 1000); - } - return diff_main(text1, text2, checklines, deadline); - } - - /** - * Find the differences between two texts. Simplifies the problem by stripping any - * common prefix or suffix off the texts before diffing. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @param checklines Speedup flag. If false, then don't run a line-level diff first to - * identify the changed areas. If true, then run a faster slightly less - * optimal diff. - * @param deadline Time when the diff should be complete by. Used internally for - * recursive calls. Users should set DiffTimeout instead. - * @return Linked List of Diff objects. - */ - private LinkedList diff_main(String text1, String text2, - boolean checklines, long deadline) { - // Check for null inputs. - if (text1 == null || text2 == null) { - throw new IllegalArgumentException("Null inputs. (diff_main)"); - } - - // Check for equality (speedup). - LinkedList diffs; - if (text1.equals(text2)) { - diffs = new LinkedList(); - if (text1.length() != 0) { - diffs.add(new Diff(Operation.EQUAL, text1)); - } - return diffs; - } - - // Trim off common prefix (speedup). - int commonlength = diff_commonPrefix(text1, text2); - final String commonprefix = text1.substring(0, commonlength); - text1 = text1.substring(commonlength); - text2 = text2.substring(commonlength); - - // Trim off common suffix (speedup). - commonlength = diff_commonSuffix(text1, text2); - final String commonsuffix = text1.substring(text1.length() - commonlength); - text1 = text1.substring(0, text1.length() - commonlength); - text2 = text2.substring(0, text2.length() - commonlength); - - // Compute the diff on the middle block. - diffs = diff_compute(text1, text2, checklines, deadline); - - // Restore the prefix and suffix. - if (commonprefix.length() != 0) { - diffs.addFirst(new Diff(Operation.EQUAL, commonprefix)); - } - if (commonsuffix.length() != 0) { - diffs.addLast(new Diff(Operation.EQUAL, commonsuffix)); - } - - diff_cleanupMerge(diffs); - return diffs; - } - - /** - * Find the differences between two texts. Assumes that the texts do not have any - * common prefix or suffix. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @param checklines Speedup flag. If false, then don't run a line-level diff first to - * identify the changed areas. If true, then run a faster slightly less - * optimal diff. - * @param deadline Time when the diff should be complete by. - * @return Linked List of Diff objects. - */ - private LinkedList diff_compute(String text1, String text2, - boolean checklines, long deadline) { - LinkedList diffs = new LinkedList(); - - if (text1.length() == 0) { - // Just add some text (speedup). - diffs.add(new Diff(Operation.INSERT, text2)); - return diffs; - } - - if (text2.length() == 0) { - // Just delete some text (speedup). - diffs.add(new Diff(Operation.DELETE, text1)); - return diffs; - } - - final String longtext = text1.length() > text2.length() ? text1 : text2; - final String shorttext = text1.length() > text2.length() ? text2 : text1; - final int i = longtext.indexOf(shorttext); - if (i != -1) { - // Shorter text is inside the longer text (speedup). - final Operation op = (text1.length() > text2.length()) ? Operation.DELETE : Operation.INSERT; - diffs.add(new Diff(op, longtext.substring(0, i))); - diffs.add(new Diff(Operation.EQUAL, shorttext)); - diffs.add(new Diff(op, longtext.substring(i + shorttext.length()))); - return diffs; - } - - if (shorttext.length() == 1) { - // Single character string. - // After the previous speedup, the character can't be an equality. - diffs.add(new Diff(Operation.DELETE, text1)); - diffs.add(new Diff(Operation.INSERT, text2)); - return diffs; - } - - // Check to see if the problem can be split in two. - final String[] hm = diff_halfMatch(text1, text2); - if (hm != null) { - // A half-match was found, sort out the return data. - final String text1_a = hm[0]; - final String text1_b = hm[1]; - final String text2_a = hm[2]; - final String text2_b = hm[3]; - final String mid_common = hm[4]; - // Send both pairs off for separate processing. - final LinkedList diffs_a = diff_main(text1_a, text2_a, - checklines, deadline); - final LinkedList diffs_b = diff_main(text1_b, text2_b, - checklines, deadline); - // Merge the results. - diffs = diffs_a; - diffs.add(new Diff(Operation.EQUAL, mid_common)); - diffs.addAll(diffs_b); - return diffs; - } - - if (checklines && text1.length() > 100 && text2.length() > 100) { - return diff_lineMode(text1, text2, deadline); - } - - return diff_bisect(text1, text2, deadline); - } - - /** - * Do a quick line-level diff on both strings, then rediff the parts for greater - * accuracy. This speedup can produce non-minimal diffs. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @param deadline Time when the diff should be complete by. - * @return Linked List of Diff objects. - */ - private LinkedList diff_lineMode(String text1, String text2, - long deadline) { - // Scan the text on a line-by-line basis first. - final LinesToCharsResult a = diff_linesToChars(text1, text2); - text1 = a.chars1; - text2 = a.chars2; - final List linearray = a.lineArray; - - final LinkedList diffs = diff_main(text1, text2, false, deadline); - - // Convert the diff back to original text. - diff_charsToLines(diffs, linearray); - // Eliminate freak matches (e.g. blank lines) - diff_cleanupSemantic(diffs); - - // Rediff any replacement blocks, this time character-by-character. - // Add a dummy entry at the end. - diffs.add(new Diff(Operation.EQUAL, "")); - int count_delete = 0; - int count_insert = 0; - String text_delete = ""; - String text_insert = ""; - final ListIterator pointer = diffs.listIterator(); - Diff thisDiff = pointer.next(); - while (thisDiff != null) { - switch (thisDiff.operation) { - case INSERT: - count_insert++; - text_insert += thisDiff.text; - break; - case DELETE: - count_delete++; - text_delete += thisDiff.text; - break; - case EQUAL: - // Upon reaching an equality, check for prior redundancies. - if (count_delete >= 1 && count_insert >= 1) { - // Delete the offending records and add the merged ones. - pointer.previous(); - for (int j = 0; j < count_delete + count_insert; j++) { - pointer.previous(); - pointer.remove(); - } - for (final Diff subDiff : diff_main(text_delete, text_insert, false, - deadline)) { - pointer.add(subDiff); - } - } - count_insert = 0; - count_delete = 0; - text_delete = ""; - text_insert = ""; - break; - } - thisDiff = pointer.hasNext() ? pointer.next() : null; - } - diffs.removeLast(); // Remove the dummy entry at the end. - - return diffs; - } - - /** - * Find the 'middle snake' of a diff, split the problem in two and return the - * recursively constructed diff. See Myers 1986 paper: An O(ND) Difference Algorithm - * and Its Variations. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @param deadline Time at which to bail if not yet complete. - * @return LinkedList of Diff objects. - */ - protected LinkedList diff_bisect(String text1, String text2, - long deadline) { - // Cache the text lengths to prevent multiple calls. - final int text1_length = text1.length(); - final int text2_length = text2.length(); - final int max_d = (text1_length + text2_length + 1) / 2; - final int v_offset = max_d; - final int v_length = 2 * max_d; - final int[] v1 = new int[v_length]; - final int[] v2 = new int[v_length]; - for (int x = 0; x < v_length; x++) { - v1[x] = -1; - v2[x] = -1; - } - v1[v_offset + 1] = 0; - v2[v_offset + 1] = 0; - final int delta = text1_length - text2_length; - // If the total number of characters is odd, then the front path will - // collide with the reverse path. - final boolean front = (delta % 2 != 0); - // Offsets for start and end of k loop. - // Prevents mapping of space beyond the grid. - int k1start = 0; - int k1end = 0; - int k2start = 0; - int k2end = 0; - for (int d = 0; d < max_d; d++) { - // Bail out if deadline is reached. - if (System.currentTimeMillis() > deadline) { - break; - } - - // Walk the front path one step. - for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { - final int k1_offset = v_offset + k1; - int x1; - if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { - x1 = v1[k1_offset + 1]; - } else { - x1 = v1[k1_offset - 1] + 1; - } - int y1 = x1 - k1; - while (x1 < text1_length && y1 < text2_length - && text1.charAt(x1) == text2.charAt(y1)) { - x1++; - y1++; - } - v1[k1_offset] = x1; - if (x1 > text1_length) { - // Ran off the right of the graph. - k1end += 2; - } else if (y1 > text2_length) { - // Ran off the bottom of the graph. - k1start += 2; - } else if (front) { - final int k2_offset = v_offset + delta - k1; - if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { - // Mirror x2 onto top-left coordinate system. - final int x2 = text1_length - v2[k2_offset]; - if (x1 >= x2) { - // Overlap detected. - return diff_bisectSplit(text1, text2, x1, y1, deadline); - } - } - } - } - - // Walk the reverse path one step. - for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { - final int k2_offset = v_offset + k2; - int x2; - if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { - x2 = v2[k2_offset + 1]; - } else { - x2 = v2[k2_offset - 1] + 1; - } - int y2 = x2 - k2; - while (x2 < text1_length && y2 < text2_length - && text1.charAt(text1_length - x2 - 1) == text2.charAt(text2_length - y2 - 1)) { - x2++; - y2++; - } - v2[k2_offset] = x2; - if (x2 > text1_length) { - // Ran off the left of the graph. - k2end += 2; - } else if (y2 > text2_length) { - // Ran off the top of the graph. - k2start += 2; - } else if (!front) { - final int k1_offset = v_offset + delta - k2; - if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { - final int x1 = v1[k1_offset]; - final int y1 = v_offset + x1 - k1_offset; - // Mirror x2 onto top-left coordinate system. - x2 = text1_length - x2; - if (x1 >= x2) { - // Overlap detected. - return diff_bisectSplit(text1, text2, x1, y1, deadline); - } - } - } - } - } - // Diff took too long and hit the deadline or - // number of diffs equals number of characters, no commonality at all. - final LinkedList diffs = new LinkedList(); - diffs.add(new Diff(Operation.DELETE, text1)); - diffs.add(new Diff(Operation.INSERT, text2)); - return diffs; - } - - /** - * Given the location of the 'middle snake', split the diff in two parts and recurse. - * - * @param text1 Old string to be diffed. - * @param text2 New string to be diffed. - * @param x Index of split point in text1. - * @param y Index of split point in text2. - * @param deadline Time at which to bail if not yet complete. - * @return LinkedList of Diff objects. - */ - private LinkedList diff_bisectSplit(String text1, String text2, - int x, int y, long deadline) { - final String text1a = text1.substring(0, x); - final String text2a = text2.substring(0, y); - final String text1b = text1.substring(x); - final String text2b = text2.substring(y); - - // Compute both diffs serially. - final LinkedList diffs = diff_main(text1a, text2a, false, deadline); - final LinkedList diffsb = diff_main(text1b, text2b, false, deadline); - - diffs.addAll(diffsb); - return diffs; - } - - /** - * Split two texts into a list of strings. Reduce the texts to a string of hashes - * where each Unicode character represents one line. - * - * @param text1 First string. - * @param text2 Second string. - * @return An object containing the encoded text1, the encoded text2 and the List of - * unique strings. The zeroth element of the List of unique strings is - * intentionally blank. - */ - protected LinesToCharsResult diff_linesToChars(String text1, String text2) { - final List lineArray = new ArrayList(); - final Map lineHash = new HashMap(); - // e.g. linearray[4] == "Hello\n" - // e.g. linehash.get("Hello\n") == 4 - - // "\x00" is a valid character, but various debuggers don't like it. - // So we'll insert a junk entry to avoid generating a null character. - lineArray.add(""); - - // Allocate 2/3rds of the space for text1, the rest for text2. - final String chars1 = diff_linesToCharsMunge(text1, lineArray, lineHash, 40000); - final String chars2 = diff_linesToCharsMunge(text2, lineArray, lineHash, 65535); - return new LinesToCharsResult(chars1, chars2, lineArray); - } - - /** - * Split a text into a list of strings. Reduce the texts to a string of hashes where - * each Unicode character represents one line. - * - * @param text String to encode. - * @param lineArray List of unique strings. - * @param lineHash Map of strings to indices. - * @param maxLines Maximum length of lineArray. - * @return Encoded string. - */ - private String diff_linesToCharsMunge(String text, List lineArray, - Map lineHash, int maxLines) { - int lineStart = 0; - int lineEnd = -1; - String line; - final StringBuilder chars = new StringBuilder(); - // Walk the text, pulling out a substring for each line. - // text.split('\n') would would temporarily double our memory footprint. - // Modifying text would create many large strings to garbage collect. - while (lineEnd < text.length() - 1) { - lineEnd = text.indexOf('\n', lineStart); - if (lineEnd == -1) { - lineEnd = text.length() - 1; - } - line = text.substring(lineStart, lineEnd + 1); - - if (lineHash.containsKey(line)) { - chars.append(String.valueOf((char) (int) lineHash.get(line))); - } else { - if (lineArray.size() == maxLines) { - // Bail out at 65535 because - // String.valueOf((char) 65536).equals(String.valueOf(((char) 0))) - line = text.substring(lineStart); - lineEnd = text.length(); - } - lineArray.add(line); - lineHash.put(line, lineArray.size() - 1); - chars.append(String.valueOf((char) (lineArray.size() - 1))); - } - lineStart = lineEnd + 1; - } - return chars.toString(); - } - - /** - * Rehydrate the text in a diff from a string of line hashes to real lines of text. - * - * @param diffs List of Diff objects. - * @param lineArray List of unique strings. - */ - protected void diff_charsToLines(List diffs, - List lineArray) { - StringBuilder text; - for (final Diff diff : diffs) { - text = new StringBuilder(); - for (int j = 0; j < diff.text.length(); j++) { - text.append(lineArray.get(diff.text.charAt(j))); - } - diff.text = text.toString(); - } - } - - /** - * Determine the common prefix of two strings - * - * @param text1 First string. - * @param text2 Second string. - * @return The number of characters common to the start of each string. - */ - public int diff_commonPrefix(String text1, String text2) { - // Performance analysis: https://neil.fraser.name/news/2007/10/09/ - final int n = Math.min(text1.length(), text2.length()); - for (int i = 0; i < n; i++) { - if (text1.charAt(i) != text2.charAt(i)) { - return i; - } - } - return n; - } - - /** - * Determine the common suffix of two strings - * - * @param text1 First string. - * @param text2 Second string. - * @return The number of characters common to the end of each string. - */ - public int diff_commonSuffix(String text1, String text2) { - // Performance analysis: https://neil.fraser.name/news/2007/10/09/ - final int text1_length = text1.length(); - final int text2_length = text2.length(); - final int n = Math.min(text1_length, text2_length); - for (int i = 1; i <= n; i++) { - if (text1.charAt(text1_length - i) != text2.charAt(text2_length - i)) { - return i - 1; - } - } - return n; - } - - /** - * Determine if the suffix of one string is the prefix of another. - * - * @param text1 First string. - * @param text2 Second string. - * @return The number of characters common to the end of the first string and the - * start of the second string. - */ - protected int diff_commonOverlap(String text1, String text2) { - // Cache the text lengths to prevent multiple calls. - final int text1_length = text1.length(); - final int text2_length = text2.length(); - // Eliminate the null case. - if (text1_length == 0 || text2_length == 0) { - return 0; - } - // Truncate the longer string. - if (text1_length > text2_length) { - text1 = text1.substring(text1_length - text2_length); - } else if (text1_length < text2_length) { - text2 = text2.substring(0, text1_length); - } - final int text_length = Math.min(text1_length, text2_length); - // Quick check for the worst case. - if (text1.equals(text2)) { - return text_length; - } - - // Start by looking for a single character match - // and increase length until no match is found. - // Performance analysis: https://neil.fraser.name/news/2010/11/04/ - int best = 0; - int length = 1; - while (true) { - final String pattern = text1.substring(text_length - length); - final int found = text2.indexOf(pattern); - if (found == -1) { - return best; - } - length += found; - if (found == 0 || text1.substring(text_length - length).equals( - text2.substring(0, length))) { - best = length; - length++; - } - } - } - - /** - * Do the two texts share a substring which is at least half the length of the longer - * text? This speedup can produce non-minimal diffs. - * - * @param text1 First string. - * @param text2 Second string. - * @return Five element String array, containing the prefix of text1, the suffix of - * text1, the prefix of text2, the suffix of text2 and the common middle. Or - * null if there was no match. - */ - protected String[] diff_halfMatch(String text1, String text2) { - if (Diff_Timeout <= 0) { - // Don't risk returning a non-optimal diff if we have unlimited time. - return null; - } - final String longtext = text1.length() > text2.length() ? text1 : text2; - final String shorttext = text1.length() > text2.length() ? text2 : text1; - if (longtext.length() < 4 || shorttext.length() * 2 < longtext.length()) { - return null; // Pointless. - } - - // First check if the second quarter is the seed for a half-match. - final String[] hm1 = diff_halfMatchI(longtext, shorttext, - (longtext.length() + 3) / 4); - // Check again based on the third quarter. - final String[] hm2 = diff_halfMatchI(longtext, shorttext, - (longtext.length() + 1) / 2); - String[] hm; - if (hm1 == null && hm2 == null) { - return null; - } else if (hm2 == null) { - hm = hm1; - } else if (hm1 == null) { - hm = hm2; - } else { - // Both matched. Select the longest. - hm = hm1[4].length() > hm2[4].length() ? hm1 : hm2; - } - - // A half-match was found, sort out the return data. - if (text1.length() > text2.length()) { - return hm; - // return new String[]{hm[0], hm[1], hm[2], hm[3], hm[4]}; - } else { - return new String[] { hm[2], hm[3], hm[0], hm[1], hm[4] }; - } - } - - /** - * Does a substring of shorttext exist within longtext such that the substring is at - * least half the length of longtext? - * - * @param longtext Longer string. - * @param shorttext Shorter string. - * @param i Start index of quarter length substring within longtext. - * @return Five element String array, containing the prefix of longtext, the suffix of - * longtext, the prefix of shorttext, the suffix of shorttext and the common - * middle. Or null if there was no match. - */ - private String[] diff_halfMatchI(String longtext, String shorttext, int i) { - // Start with a 1/4 length substring at position i as a seed. - final String seed = longtext.substring(i, i + longtext.length() / 4); - int j = -1; - String best_common = ""; - String best_longtext_a = "", best_longtext_b = ""; - String best_shorttext_a = "", best_shorttext_b = ""; - while ((j = shorttext.indexOf(seed, j + 1)) != -1) { - final int prefixLength = diff_commonPrefix(longtext.substring(i), - shorttext.substring(j)); - final int suffixLength = diff_commonSuffix(longtext.substring(0, i), - shorttext.substring(0, j)); - if (best_common.length() < suffixLength + prefixLength) { - best_common = shorttext.substring(j - suffixLength, j) - + shorttext.substring(j, j + prefixLength); - best_longtext_a = longtext.substring(0, i - suffixLength); - best_longtext_b = longtext.substring(i + prefixLength); - best_shorttext_a = shorttext.substring(0, j - suffixLength); - best_shorttext_b = shorttext.substring(j + prefixLength); - } - } - if (best_common.length() * 2 >= longtext.length()) { - return new String[] { best_longtext_a, best_longtext_b, - best_shorttext_a, best_shorttext_b, best_common }; - } else { - return null; - } - } - - /** - * Reduce the number of edits by eliminating semantically trivial equalities. - * - * @param diffs LinkedList of Diff objects. - */ - public void diff_cleanupSemantic(LinkedList diffs) { - if (diffs.isEmpty()) { - return; - } - boolean changes = false; - final Deque equalities = new ArrayDeque(); // Double-ended queue of - // qualities. - String lastEquality = null; // Always equal to equalities.peek().text - ListIterator pointer = diffs.listIterator(); - // Number of characters that changed prior to the equality. - int length_insertions1 = 0; - int length_deletions1 = 0; - // Number of characters that changed after the equality. - int length_insertions2 = 0; - int length_deletions2 = 0; - Diff thisDiff = pointer.next(); - while (thisDiff != null) { - if (thisDiff.operation == Operation.EQUAL) { - // Equality found. - equalities.push(thisDiff); - length_insertions1 = length_insertions2; - length_deletions1 = length_deletions2; - length_insertions2 = 0; - length_deletions2 = 0; - lastEquality = thisDiff.text; - } else { - // An insertion or deletion. - if (thisDiff.operation == Operation.INSERT) { - length_insertions2 += thisDiff.text.length(); - } else { - length_deletions2 += thisDiff.text.length(); - } - // Eliminate an equality that is smaller or equal to the edits on both - // sides of it. - if (lastEquality != null && (lastEquality.length() <= Math.max(length_insertions1, length_deletions1)) - && (lastEquality.length() <= Math.max(length_insertions2, length_deletions2))) { - // System.out.println("Splitting: '" + lastEquality + "'"); - // Walk back to offending equality. - while (thisDiff != equalities.peek()) { - thisDiff = pointer.previous(); - } - pointer.next(); - - // Replace equality with a delete. - pointer.set(new Diff(Operation.DELETE, lastEquality)); - // Insert a corresponding an insert. - pointer.add(new Diff(Operation.INSERT, lastEquality)); - - equalities.pop(); // Throw away the equality we just deleted. - if (!equalities.isEmpty()) { - // Throw away the previous equality (it needs to be reevaluated). - equalities.pop(); - } - if (equalities.isEmpty()) { - // There are no previous equalities, walk back to the start. - while (pointer.hasPrevious()) { - pointer.previous(); - } - } else { - // There is a safe equality we can fall back to. - thisDiff = equalities.peek(); - while (thisDiff != pointer.previous()) { - // Intentionally empty loop. - } - } - - length_insertions1 = 0; // Reset the counters. - length_insertions2 = 0; - length_deletions1 = 0; - length_deletions2 = 0; - lastEquality = null; - changes = true; - } - } - thisDiff = pointer.hasNext() ? pointer.next() : null; - } - - // Normalize the diff. - if (changes) { - diff_cleanupMerge(diffs); - } - diff_cleanupSemanticLossless(diffs); - - // Find any overlaps between deletions and insertions. - // e.g: abcxxxxxxdef - // -> abcxxxdef - // e.g: xxxabcdefxxx - // -> defxxxabc - // Only extract an overlap if it is as big as the edit ahead or behind it. - pointer = diffs.listIterator(); - Diff prevDiff = null; - thisDiff = null; - if (pointer.hasNext()) { - prevDiff = pointer.next(); - if (pointer.hasNext()) { - thisDiff = pointer.next(); - } - } - while (thisDiff != null) { - if (prevDiff.operation == Operation.DELETE && - thisDiff.operation == Operation.INSERT) { - final String deletion = prevDiff.text; - final String insertion = thisDiff.text; - final int overlap_length1 = this.diff_commonOverlap(deletion, insertion); - final int overlap_length2 = this.diff_commonOverlap(insertion, deletion); - if (overlap_length1 >= overlap_length2) { - if (overlap_length1 >= deletion.length() / 2.0 || - overlap_length1 >= insertion.length() / 2.0) { - // Overlap found. Insert an equality and trim the surrounding - // edits. - pointer.previous(); - pointer.add(new Diff(Operation.EQUAL, - insertion.substring(0, overlap_length1))); - prevDiff.text = deletion.substring(0, deletion.length() - overlap_length1); - thisDiff.text = insertion.substring(overlap_length1); - // pointer.add inserts the element before the cursor, so there is - // no need to step past the new element. - } - } else { - if (overlap_length2 >= deletion.length() / 2.0 || - overlap_length2 >= insertion.length() / 2.0) { - // Reverse overlap found. - // Insert an equality and swap and trim the surrounding edits. - pointer.previous(); - pointer.add(new Diff(Operation.EQUAL, - deletion.substring(0, overlap_length2))); - prevDiff.operation = Operation.INSERT; - prevDiff.text = insertion.substring(0, insertion.length() - overlap_length2); - thisDiff.operation = Operation.DELETE; - thisDiff.text = deletion.substring(overlap_length2); - // pointer.add inserts the element before the cursor, so there is - // no need to step past the new element. - } - } - thisDiff = pointer.hasNext() ? pointer.next() : null; - } - prevDiff = thisDiff; - thisDiff = pointer.hasNext() ? pointer.next() : null; - } - } - - /** - * Look for single edits surrounded on both sides by equalities which can be shifted - * sideways to align the edit to a word boundary. e.g: The cat came. -> The - * cat came. - * - * @param diffs LinkedList of Diff objects. - */ - public void diff_cleanupSemanticLossless(LinkedList diffs) { - String equality1, edit, equality2; - String commonString; - int commonOffset; - int score, bestScore; - String bestEquality1, bestEdit, bestEquality2; - // Create a new iterator at the start. - final ListIterator pointer = diffs.listIterator(); - Diff prevDiff = pointer.hasNext() ? pointer.next() : null; - Diff thisDiff = pointer.hasNext() ? pointer.next() : null; - Diff nextDiff = pointer.hasNext() ? pointer.next() : null; - // Intentionally ignore the first and last element (don't need checking). - while (nextDiff != null) { - if (prevDiff.operation == Operation.EQUAL && - nextDiff.operation == Operation.EQUAL) { - // This is a single edit surrounded by equalities. - equality1 = prevDiff.text; - edit = thisDiff.text; - equality2 = nextDiff.text; - - // First, shift the edit as far left as possible. - commonOffset = diff_commonSuffix(equality1, edit); - if (commonOffset != 0) { - commonString = edit.substring(edit.length() - commonOffset); - equality1 = equality1.substring(0, equality1.length() - commonOffset); - edit = commonString + edit.substring(0, edit.length() - commonOffset); - equality2 = commonString + equality2; - } - - // Second, step character by character right, looking for the best fit. - bestEquality1 = equality1; - bestEdit = edit; - bestEquality2 = equality2; - bestScore = diff_cleanupSemanticScore(equality1, edit) - + diff_cleanupSemanticScore(edit, equality2); - while (edit.length() != 0 && equality2.length() != 0 - && edit.charAt(0) == equality2.charAt(0)) { - equality1 += edit.charAt(0); - edit = edit.substring(1) + equality2.charAt(0); - equality2 = equality2.substring(1); - score = diff_cleanupSemanticScore(equality1, edit) - + diff_cleanupSemanticScore(edit, equality2); - // The >= encourages trailing rather than leading whitespace on edits. - if (score >= bestScore) { - bestScore = score; - bestEquality1 = equality1; - bestEdit = edit; - bestEquality2 = equality2; - } - } - - if (!prevDiff.text.equals(bestEquality1)) { - // We have an improvement, save it back to the diff. - if (bestEquality1.length() != 0) { - prevDiff.text = bestEquality1; - } else { - pointer.previous(); // Walk past nextDiff. - pointer.previous(); // Walk past thisDiff. - pointer.previous(); // Walk past prevDiff. - pointer.remove(); // Delete prevDiff. - pointer.next(); // Walk past thisDiff. - pointer.next(); // Walk past nextDiff. - } - thisDiff.text = bestEdit; - if (bestEquality2.length() != 0) { - nextDiff.text = bestEquality2; - } else { - pointer.remove(); // Delete nextDiff. - nextDiff = thisDiff; - thisDiff = prevDiff; - } - } - } - prevDiff = thisDiff; - thisDiff = nextDiff; - nextDiff = pointer.hasNext() ? pointer.next() : null; - } - } - - /** - * Given two strings, compute a score representing whether the internal boundary falls - * on logical boundaries. Scores range from 6 (best) to 0 (worst). - * - * @param one First string. - * @param two Second string. - * @return The score. - */ - private int diff_cleanupSemanticScore(String one, String two) { - if (one.length() == 0 || two.length() == 0) { - // Edges are the best. - return 6; - } - - // Each port of this function behaves slightly differently due to - // subtle differences in each language's definition of things like - // 'whitespace'. Since this function's purpose is largely cosmetic, - // the choice has been made to use each language's native features - // rather than force total conformity. - final char char1 = one.charAt(one.length() - 1); - final char char2 = two.charAt(0); - final boolean nonAlphaNumeric1 = !Character.isLetterOrDigit(char1); - final boolean nonAlphaNumeric2 = !Character.isLetterOrDigit(char2); - final boolean whitespace1 = nonAlphaNumeric1 && Character.isWhitespace(char1); - final boolean whitespace2 = nonAlphaNumeric2 && Character.isWhitespace(char2); - final boolean lineBreak1 = whitespace1 - && Character.getType(char1) == Character.CONTROL; - final boolean lineBreak2 = whitespace2 - && Character.getType(char2) == Character.CONTROL; - final boolean blankLine1 = lineBreak1 && BLANKLINEEND.matcher(one).find(); - final boolean blankLine2 = lineBreak2 && BLANKLINESTART.matcher(two).find(); - - if (blankLine1 || blankLine2) { - // Five points for blank lines. - return 5; - } else if (lineBreak1 || lineBreak2) { - // Four points for line breaks. - return 4; - } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { - // Three points for end of sentences. - return 3; - } else if (whitespace1 || whitespace2) { - // Two points for whitespace. - return 2; - } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { - // One point for non-alphanumeric. - return 1; - } - return 0; - } - - // Define some regex patterns for matching boundaries. - private final Pattern BLANKLINEEND = Pattern.compile("\\n\\r?\\n\\Z", Pattern.DOTALL); - private final Pattern BLANKLINESTART = Pattern.compile("\\A\\r?\\n\\r?\\n", Pattern.DOTALL); - - /** - * Reduce the number of edits by eliminating operationally trivial equalities. - * - * @param diffs LinkedList of Diff objects. - */ - public void diff_cleanupEfficiency(LinkedList diffs) { - if (diffs.isEmpty()) { - return; - } - boolean changes = false; - final Deque equalities = new ArrayDeque(); // Double-ended queue of - // equalities. - String lastEquality = null; // Always equal to equalities.peek().text - final ListIterator pointer = diffs.listIterator(); - // Is there an insertion operation before the last equality. - boolean pre_ins = false; - // Is there a deletion operation before the last equality. - boolean pre_del = false; - // Is there an insertion operation after the last equality. - boolean post_ins = false; - // Is there a deletion operation after the last equality. - boolean post_del = false; - Diff thisDiff = pointer.next(); - Diff safeDiff = thisDiff; // The last Diff that is known to be unsplittable. - while (thisDiff != null) { - if (thisDiff.operation == Operation.EQUAL) { - // Equality found. - if (thisDiff.text.length() < Diff_EditCost && (post_ins || post_del)) { - // Candidate found. - equalities.push(thisDiff); - pre_ins = post_ins; - pre_del = post_del; - lastEquality = thisDiff.text; - } else { - // Not a candidate, and can never become one. - equalities.clear(); - lastEquality = null; - safeDiff = thisDiff; - } - post_ins = post_del = false; - } else { - // An insertion or deletion. - if (thisDiff.operation == Operation.DELETE) { - post_del = true; - } else { - post_ins = true; - } - /* - * Five types to be split: - * ABXYCD - * AXCD - * ABXC - * AXCD - * ABXC - */ - if (lastEquality != null - && ((pre_ins && pre_del && post_ins && post_del) - || ((lastEquality.length() < Diff_EditCost / 2) - && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) - + (post_ins ? 1 : 0) + (post_del ? 1 : 0)) == 3))) { - // System.out.println("Splitting: '" + lastEquality + "'"); - // Walk back to offending equality. - while (thisDiff != equalities.peek()) { - thisDiff = pointer.previous(); - } - pointer.next(); - - // Replace equality with a delete. - pointer.set(new Diff(Operation.DELETE, lastEquality)); - // Insert a corresponding an insert. - pointer.add(thisDiff = new Diff(Operation.INSERT, lastEquality)); - - equalities.pop(); // Throw away the equality we just deleted. - lastEquality = null; - if (pre_ins && pre_del) { - // No changes made which could affect previous entry, keep going. - post_ins = post_del = true; - equalities.clear(); - safeDiff = thisDiff; - } else { - if (!equalities.isEmpty()) { - // Throw away the previous equality (it needs to be - // reevaluated). - equalities.pop(); - } - if (equalities.isEmpty()) { - // There are no previous questionable equalities, - // walk back to the last known safe diff. - thisDiff = safeDiff; - } else { - // There is an equality we can fall back to. - thisDiff = equalities.peek(); - } - while (thisDiff != pointer.previous()) { - // Intentionally empty loop. - } - post_ins = post_del = false; - } - - changes = true; - } - } - thisDiff = pointer.hasNext() ? pointer.next() : null; - } - - if (changes) { - diff_cleanupMerge(diffs); - } - } - - /** - * Reorder and merge like edit sections. Merge equalities. Any edit section can move - * as long as it doesn't cross an equality. - * - * @param diffs LinkedList of Diff objects. - */ - public void diff_cleanupMerge(LinkedList diffs) { - diffs.add(new Diff(Operation.EQUAL, "")); // Add a dummy entry at the end. - ListIterator pointer = diffs.listIterator(); - int count_delete = 0; - int count_insert = 0; - String text_delete = ""; - String text_insert = ""; - Diff thisDiff = pointer.next(); - Diff prevEqual = null; - int commonlength; - while (thisDiff != null) { - switch (thisDiff.operation) { - case INSERT: - count_insert++; - text_insert += thisDiff.text; - prevEqual = null; - break; - case DELETE: - count_delete++; - text_delete += thisDiff.text; - prevEqual = null; - break; - case EQUAL: - if (count_delete + count_insert > 1) { - final boolean both_types = count_delete != 0 && count_insert != 0; - // Delete the offending records. - pointer.previous(); // Reverse direction. - while (count_delete-- > 0) { - pointer.previous(); - pointer.remove(); - } - while (count_insert-- > 0) { - pointer.previous(); - pointer.remove(); - } - if (both_types) { - // Factor out any common prefixies. - commonlength = diff_commonPrefix(text_insert, text_delete); - if (commonlength != 0) { - if (pointer.hasPrevious()) { - thisDiff = pointer.previous(); - assert thisDiff.operation == Operation.EQUAL : "Previous diff should have been an equality."; - thisDiff.text += text_insert.substring(0, commonlength); - pointer.next(); - } else { - pointer.add(new Diff(Operation.EQUAL, - text_insert.substring(0, commonlength))); - } - text_insert = text_insert.substring(commonlength); - text_delete = text_delete.substring(commonlength); - } - // Factor out any common suffixies. - commonlength = diff_commonSuffix(text_insert, text_delete); - if (commonlength != 0) { - thisDiff = pointer.next(); - thisDiff.text = text_insert.substring(text_insert.length() - - commonlength) + thisDiff.text; - text_insert = text_insert.substring(0, text_insert.length() - - commonlength); - text_delete = text_delete.substring(0, text_delete.length() - - commonlength); - pointer.previous(); - } - } - // Insert the merged records. - if (text_delete.length() != 0) { - pointer.add(new Diff(Operation.DELETE, text_delete)); - } - if (text_insert.length() != 0) { - pointer.add(new Diff(Operation.INSERT, text_insert)); - } - // Step forward to the equality. - thisDiff = pointer.hasNext() ? pointer.next() : null; - } else if (prevEqual != null) { - // Merge this equality with the previous one. - prevEqual.text += thisDiff.text; - pointer.remove(); - thisDiff = pointer.previous(); - pointer.next(); // Forward direction - } - count_insert = 0; - count_delete = 0; - text_delete = ""; - text_insert = ""; - prevEqual = thisDiff; - break; - } - thisDiff = pointer.hasNext() ? pointer.next() : null; - } - if (diffs.getLast().text.length() == 0) { - diffs.removeLast(); // Remove the dummy entry at the end. - } - - /* - * Second pass: look for single edits surrounded on both sides by equalities which - * can be shifted sideways to eliminate an equality. e.g: ABAC -> - * ABAC - */ - boolean changes = false; - // Create a new iterator at the start. - // (As opposed to walking the current one back.) - pointer = diffs.listIterator(); - Diff prevDiff = pointer.hasNext() ? pointer.next() : null; - thisDiff = pointer.hasNext() ? pointer.next() : null; - Diff nextDiff = pointer.hasNext() ? pointer.next() : null; - // Intentionally ignore the first and last element (don't need checking). - while (nextDiff != null) { - if (prevDiff.operation == Operation.EQUAL && - nextDiff.operation == Operation.EQUAL) { - // This is a single edit surrounded by equalities. - if (thisDiff.text.endsWith(prevDiff.text)) { - // Shift the edit over the previous equality. - thisDiff.text = prevDiff.text - + thisDiff.text.substring(0, thisDiff.text.length() - - prevDiff.text.length()); - nextDiff.text = prevDiff.text + nextDiff.text; - pointer.previous(); // Walk past nextDiff. - pointer.previous(); // Walk past thisDiff. - pointer.previous(); // Walk past prevDiff. - pointer.remove(); // Delete prevDiff. - pointer.next(); // Walk past thisDiff. - thisDiff = pointer.next(); // Walk past nextDiff. - nextDiff = pointer.hasNext() ? pointer.next() : null; - changes = true; - } else if (thisDiff.text.startsWith(nextDiff.text)) { - // Shift the edit over the next equality. - prevDiff.text += nextDiff.text; - thisDiff.text = thisDiff.text.substring(nextDiff.text.length()) - + nextDiff.text; - pointer.remove(); // Delete nextDiff. - nextDiff = pointer.hasNext() ? pointer.next() : null; - changes = true; - } - } - prevDiff = thisDiff; - thisDiff = nextDiff; - nextDiff = pointer.hasNext() ? pointer.next() : null; - } - // If shifts were made, the diff needs reordering and another shift sweep. - if (changes) { - diff_cleanupMerge(diffs); - } - } - - /** - * loc is a location in text1, compute and return the equivalent location in text2. - * e.g. "The cat" vs "The big cat", 1->1, 5->8 - * - * @param diffs List of Diff objects. - * @param loc Location within text1. - * @return Location within text2. - */ - public int diff_xIndex(List diffs, int loc) { - int chars1 = 0; - int chars2 = 0; - int last_chars1 = 0; - int last_chars2 = 0; - Diff lastDiff = null; - for (final Diff aDiff : diffs) { - if (aDiff.operation != Operation.INSERT) { - // Equality or deletion. - chars1 += aDiff.text.length(); - } - if (aDiff.operation != Operation.DELETE) { - // Equality or insertion. - chars2 += aDiff.text.length(); - } - if (chars1 > loc) { - // Overshot the location. - lastDiff = aDiff; - break; - } - last_chars1 = chars1; - last_chars2 = chars2; - } - if (lastDiff != null && lastDiff.operation == Operation.DELETE) { - // The location was deleted. - return last_chars2; - } - // Add the remaining character length. - return last_chars2 + (loc - last_chars1); - } - - /** - * Convert a Diff list into a pretty HTML report. - * - * @param diffs List of Diff objects. - * @return HTML representation. - */ - public String diff_prettyHtml(List diffs) { - final StringBuilder html = new StringBuilder(); - for (final Diff aDiff : diffs) { - final String text = aDiff.text.replace("&", "&").replace("<", "<") - .replace(">", ">").replace("\n", "¶
"); - switch (aDiff.operation) { - case INSERT: - html.append("").append(text) - .append(""); - break; - case DELETE: - html.append("").append(text) - .append(""); - break; - case EQUAL: - html.append("").append(text).append(""); - break; - } - } - return html.toString(); - } - - /** - * Compute and return the source text (all equalities and deletions). - * - * @param diffs List of Diff objects. - * @return Source text. - */ - public String diff_text1(List diffs) { - final StringBuilder text = new StringBuilder(); - for (final Diff aDiff : diffs) { - if (aDiff.operation != Operation.INSERT) { - text.append(aDiff.text); - } - } - return text.toString(); - } - - /** - * Compute and return the destination text (all equalities and insertions). - * - * @param diffs List of Diff objects. - * @return Destination text. - */ - public String diff_text2(List diffs) { - final StringBuilder text = new StringBuilder(); - for (final Diff aDiff : diffs) { - if (aDiff.operation != Operation.DELETE) { - text.append(aDiff.text); - } - } - return text.toString(); - } - - /** - * Compute the Levenshtein distance; the number of inserted, deleted or substituted - * characters. - * - * @param diffs List of Diff objects. - * @return Number of changes. - */ - public int diff_levenshtein(List diffs) { - int levenshtein = 0; - int insertions = 0; - int deletions = 0; - for (final Diff aDiff : diffs) { - switch (aDiff.operation) { - case INSERT: - insertions += aDiff.text.length(); - break; - case DELETE: - deletions += aDiff.text.length(); - break; - case EQUAL: - // A deletion and an insertion is one substitution. - levenshtein += Math.max(insertions, deletions); - insertions = 0; - deletions = 0; - break; - } - } - levenshtein += Math.max(insertions, deletions); - return levenshtein; - } - - /** - * Crush the diff into an encoded string which describes the operations required to - * transform text1 into text2. E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, - * insert 'ing'. Operations are tab-separated. Inserted text is escaped using %xx - * notation. - * - * @param diffs List of Diff objects. - * @return Delta text. - */ - public String diff_toDelta(List diffs) { - final StringBuilder text = new StringBuilder(); - for (final Diff aDiff : diffs) { - switch (aDiff.operation) { - case INSERT: - try { - text.append("+").append(URLEncoder.encode(aDiff.text, "UTF-8") - .replace('+', ' ')).append("\t"); - } catch (final UnsupportedEncodingException e) { - // Not likely on modern system. - throw new Error("This system does not support UTF-8.", e); - } - break; - case DELETE: - text.append("-").append(aDiff.text.length()).append("\t"); - break; - case EQUAL: - text.append("=").append(aDiff.text.length()).append("\t"); - break; - } - } - String delta = text.toString(); - if (delta.length() != 0) { - // Strip off trailing tab character. - delta = delta.substring(0, delta.length() - 1); - delta = unescapeForEncodeUriCompatability(delta); - } - return delta; - } - - /** - * Given the original text1, and an encoded string which describes the operations - * required to transform text1 into text2, compute the full diff. - * - * @param text1 Source string for the diff. - * @param delta Delta text. - * @return Array of Diff objects or null if invalid. - * @throws IllegalArgumentException If invalid input. - */ - public LinkedList diff_fromDelta(String text1, String delta) - throws IllegalArgumentException { - final LinkedList diffs = new LinkedList(); - int pointer = 0; // Cursor in text1 - final String[] tokens = delta.split("\t"); - for (final String token : tokens) { - if (token.length() == 0) { - // Blank tokens are ok (from a trailing \t). - continue; - } - // Each token begins with a one character parameter which specifies the - // operation of this token (delete, insert, equality). - String param = token.substring(1); - switch (token.charAt(0)) { - case '+': - // decode would change all "+" to " " - param = param.replace("+", "%2B"); - try { - param = URLDecoder.decode(param, "UTF-8"); - } catch (final UnsupportedEncodingException e) { - // Not likely on modern system. - throw new Error("This system does not support UTF-8.", e); - } catch (final IllegalArgumentException e) { - // Malformed URI sequence. - throw new IllegalArgumentException( - "Illegal escape in diff_fromDelta: " + param, e); - } - diffs.add(new Diff(Operation.INSERT, param)); - break; - case '-': - // Fall through. - case '=': - int n; - try { - n = Integer.parseInt(param); - } catch (final NumberFormatException e) { - throw new IllegalArgumentException( - "Invalid number in diff_fromDelta: " + param, e); - } - if (n < 0) { - throw new IllegalArgumentException( - "Negative number in diff_fromDelta: " + param); - } - String text; - try { - text = text1.substring(pointer, pointer += n); - } catch (final StringIndexOutOfBoundsException e) { - throw new IllegalArgumentException("Delta length (" + pointer - + ") larger than source text length (" + text1.length() - + ").", e); - } - if (token.charAt(0) == '=') { - diffs.add(new Diff(Operation.EQUAL, text)); - } else { - diffs.add(new Diff(Operation.DELETE, text)); - } - break; - default: - // Anything else is an error. - throw new IllegalArgumentException( - "Invalid diff operation in diff_fromDelta: " + token.charAt(0)); - } - } - if (pointer != text1.length()) { - throw new IllegalArgumentException("Delta length (" + pointer - + ") smaller than source text length (" + text1.length() + ")."); - } - return diffs; - } - - // MATCH FUNCTIONS - - /** - * Locate the best instance of 'pattern' in 'text' near 'loc'. Returns -1 if no match - * found. - * - * @param text The text to search. - * @param pattern The pattern to search for. - * @param loc The location to search around. - * @return Best match index or -1. - */ - public int match_main(String text, String pattern, int loc) { - // Check for null inputs. - if (text == null || pattern == null) { - throw new IllegalArgumentException("Null inputs. (match_main)"); - } - - loc = Math.max(0, Math.min(loc, text.length())); - if (text.equals(pattern)) { - // Shortcut (potentially not guaranteed by the algorithm) - return 0; - } else if (text.length() == 0) { - // Nothing to match. - return -1; - } else if (loc + pattern.length() <= text.length() - && text.substring(loc, loc + pattern.length()).equals(pattern)) { - // Perfect match at the perfect spot! (Includes case of null pattern) - return loc; - } else { - // Do a fuzzy compare. - return match_bitap(text, pattern, loc); - } - } - - /** - * Locate the best instance of 'pattern' in 'text' near 'loc' using the Bitap - * algorithm. Returns -1 if no match found. - * - * @param text The text to search. - * @param pattern The pattern to search for. - * @param loc The location to search around. - * @return Best match index or -1. - */ - protected int match_bitap(String text, String pattern, int loc) { - assert (Match_MaxBits == 0 || pattern.length() <= Match_MaxBits) : "Pattern too long for this application."; - - // Initialise the alphabet. - final Map s = match_alphabet(pattern); - - // Highest score beyond which we give up. - double score_threshold = Match_Threshold; - // Is there a nearby exact match? (speedup) - int best_loc = text.indexOf(pattern, loc); - if (best_loc != -1) { - score_threshold = Math.min(match_bitapScore(0, best_loc, loc, pattern), - score_threshold); - // What about in the other direction? (speedup) - best_loc = text.lastIndexOf(pattern, loc + pattern.length()); - if (best_loc != -1) { - score_threshold = Math.min(match_bitapScore(0, best_loc, loc, pattern), - score_threshold); - } - } - - // Initialise the bit arrays. - final int matchmask = 1 << (pattern.length() - 1); - best_loc = -1; - - int bin_min, bin_mid; - int bin_max = pattern.length() + text.length(); - // Empty initialization added to appease Java compiler. - int[] last_rd = new int[0]; - for (int d = 0; d < pattern.length(); d++) { - // Scan for the best match; each iteration allows for one more error. - // Run a binary search to determine how far from 'loc' we can stray at - // this error level. - bin_min = 0; - bin_mid = bin_max; - while (bin_min < bin_mid) { - if (match_bitapScore(d, loc + bin_mid, loc, pattern) <= score_threshold) { - bin_min = bin_mid; - } else { - bin_max = bin_mid; - } - bin_mid = (bin_max - bin_min) / 2 + bin_min; - } - // Use the result from this iteration as the maximum for the next. - bin_max = bin_mid; - int start = Math.max(1, loc - bin_mid + 1); - final int finish = Math.min(loc + bin_mid, text.length()) + pattern.length(); - - final int[] rd = new int[finish + 2]; - rd[finish + 1] = (1 << d) - 1; - for (int j = finish; j >= start; j--) { - int charMatch; - if (text.length() <= j - 1 || !s.containsKey(text.charAt(j - 1))) { - // Out of range. - charMatch = 0; - } else { - charMatch = s.get(text.charAt(j - 1)); - } - if (d == 0) { - // First pass: exact match. - rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; - } else { - // Subsequent passes: fuzzy match. - rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) - | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; - } - if ((rd[j] & matchmask) != 0) { - final double score = match_bitapScore(d, j - 1, loc, pattern); - // This match will almost certainly be better than any existing - // match. But check anyway. - if (score <= score_threshold) { - // Told you so. - score_threshold = score; - best_loc = j - 1; - if (best_loc > loc) { - // When passing loc, don't exceed our current distance from - // loc. - start = Math.max(1, 2 * loc - best_loc); - } else { - // Already passed loc, downhill from here on in. - break; - } - } - } - } - if (match_bitapScore(d + 1, loc, loc, pattern) > score_threshold) { - // No hope for a (better) match at greater error levels. - break; - } - last_rd = rd; - } - return best_loc; - } - - /** - * Compute and return the score for a match with e errors and x location. - * - * @param e Number of errors in match. - * @param x Location of match. - * @param loc Expected location of match. - * @param pattern Pattern being sought. - * @return Overall score for match (0.0 = good, 1.0 = bad). - */ - private double match_bitapScore(int e, int x, int loc, String pattern) { - final float accuracy = (float) e / pattern.length(); - final int proximity = Math.abs(loc - x); - if (Match_Distance == 0) { - // Dodge divide by zero error. - return proximity == 0 ? accuracy : 1.0; - } - return accuracy + (proximity / (float) Match_Distance); - } - - /** - * Initialise the alphabet for the Bitap algorithm. - * - * @param pattern The text to encode. - * @return Hash of character locations. - */ - protected Map match_alphabet(String pattern) { - final Map s = new HashMap(); - final char[] char_pattern = pattern.toCharArray(); - for (final char c : char_pattern) { - s.put(c, 0); - } - int i = 0; - for (final char c : char_pattern) { - s.put(c, s.get(c) | (1 << (pattern.length() - i - 1))); - i++; - } - return s; - } - - // PATCH FUNCTIONS - - /** - * Increase the context until it is unique, but don't let the pattern expand beyond - * Match_MaxBits. - * - * @param patch The patch to grow. - * @param text Source text. - */ - protected void patch_addContext(Patch patch, String text) { - if (text.length() == 0) { - return; - } - String pattern = text.substring(patch.start2, patch.start2 + patch.length1); - int padding = 0; - - // Look for the first and last matches of pattern in text. If two different - // matches are found, increase the pattern length. - while (text.indexOf(pattern) != text.lastIndexOf(pattern) - && pattern.length() < Match_MaxBits - Patch_Margin - Patch_Margin) { - padding += Patch_Margin; - pattern = text.substring(Math.max(0, patch.start2 - padding), - Math.min(text.length(), patch.start2 + patch.length1 + padding)); - } - // Add one chunk for good luck. - padding += Patch_Margin; - - // Add the prefix. - final String prefix = text.substring(Math.max(0, patch.start2 - padding), - patch.start2); - if (prefix.length() != 0) { - patch.diffs.addFirst(new Diff(Operation.EQUAL, prefix)); - } - // Add the suffix. - final String suffix = text.substring(patch.start2 + patch.length1, - Math.min(text.length(), patch.start2 + patch.length1 + padding)); - if (suffix.length() != 0) { - patch.diffs.addLast(new Diff(Operation.EQUAL, suffix)); - } - - // Roll back the start points. - patch.start1 -= prefix.length(); - patch.start2 -= prefix.length(); - // Extend the lengths. - patch.length1 += prefix.length() + suffix.length(); - patch.length2 += prefix.length() + suffix.length(); - } - - /** - * Compute a list of patches to turn text1 into text2. A set of diffs will be - * computed. - * - * @param text1 Old text. - * @param text2 New text. - * @return LinkedList of Patch objects. - */ - public LinkedList patch_make(String text1, String text2) { - if (text1 == null || text2 == null) { - throw new IllegalArgumentException("Null inputs. (patch_make)"); - } - // No diffs provided, compute our own. - final LinkedList diffs = diff_main(text1, text2, true); - if (diffs.size() > 2) { - diff_cleanupSemantic(diffs); - diff_cleanupEfficiency(diffs); - } - return patch_make(text1, diffs); - } - - /** - * Compute a list of patches to turn text1 into text2. text1 will be derived from the - * provided diffs. - * - * @param diffs Array of Diff objects for text1 to text2. - * @return LinkedList of Patch objects. - */ - public LinkedList patch_make(LinkedList diffs) { - if (diffs == null) { - throw new IllegalArgumentException("Null inputs. (patch_make)"); - } - // No origin string provided, compute our own. - final String text1 = diff_text1(diffs); - return patch_make(text1, diffs); - } - - /** - * Compute a list of patches to turn text1 into text2. text2 is ignored, diffs are the - * delta between text1 and text2. - * - * @param text1 Old text - * @param text2 Ignored. - * @param diffs Array of Diff objects for text1 to text2. - * @return LinkedList of Patch objects. - * @deprecated Prefer patch_make(String text1, LinkedList diffs). - */ - @Deprecated - public LinkedList patch_make(String text1, String text2, - LinkedList diffs) { - return patch_make(text1, diffs); - } - - /** - * Compute a list of patches to turn text1 into text2. text2 is not provided, diffs - * are the delta between text1 and text2. - * - * @param text1 Old text. - * @param diffs Array of Diff objects for text1 to text2. - * @return LinkedList of Patch objects. - */ - public LinkedList patch_make(String text1, LinkedList diffs) { - if (text1 == null || diffs == null) { - throw new IllegalArgumentException("Null inputs. (patch_make)"); - } - - final LinkedList patches = new LinkedList(); - if (diffs.isEmpty()) { - return patches; // Get rid of the null case. - } - Patch patch = new Patch(); - int char_count1 = 0; // Number of characters into the text1 string. - int char_count2 = 0; // Number of characters into the text2 string. - // Start with text1 (prepatch_text) and apply the diffs until we arrive at - // text2 (postpatch_text). We recreate the patches one by one to determine - // context info. - String prepatch_text = text1; - String postpatch_text = text1; - for (final Diff aDiff : diffs) { - if (patch.diffs.isEmpty() && aDiff.operation != Operation.EQUAL) { - // A new patch starts here. - patch.start1 = char_count1; - patch.start2 = char_count2; - } - - switch (aDiff.operation) { - case INSERT: - patch.diffs.add(aDiff); - patch.length2 += aDiff.text.length(); - postpatch_text = postpatch_text.substring(0, char_count2) - + aDiff.text + postpatch_text.substring(char_count2); - break; - case DELETE: - patch.length1 += aDiff.text.length(); - patch.diffs.add(aDiff); - postpatch_text = postpatch_text.substring(0, char_count2) - + postpatch_text.substring(char_count2 + aDiff.text.length()); - break; - case EQUAL: - if (aDiff.text.length() <= 2 * Patch_Margin - && !patch.diffs.isEmpty() && aDiff != diffs.getLast()) { - // Small equality inside a patch. - patch.diffs.add(aDiff); - patch.length1 += aDiff.text.length(); - patch.length2 += aDiff.text.length(); - } - - if (aDiff.text.length() >= 2 * Patch_Margin && !patch.diffs.isEmpty()) { - // Time for a new patch. - if (!patch.diffs.isEmpty()) { - patch_addContext(patch, prepatch_text); - patches.add(patch); - patch = new Patch(); - // Unlike Unidiff, our patch lists have a rolling context. - // https://github.com/google/diff-match-patch/wiki/Unidiff - // Update prepatch text & pos to reflect the application of the - // just completed patch. - prepatch_text = postpatch_text; - char_count1 = char_count2; - } - } - break; - } - - // Update the current character count. - if (aDiff.operation != Operation.INSERT) { - char_count1 += aDiff.text.length(); - } - if (aDiff.operation != Operation.DELETE) { - char_count2 += aDiff.text.length(); - } - } - // Pick up the leftover patch if not empty. - if (!patch.diffs.isEmpty()) { - patch_addContext(patch, prepatch_text); - patches.add(patch); - } - - return patches; - } - - /** - * Given an array of patches, return another array that is identical. - * - * @param patches Array of Patch objects. - * @return Array of Patch objects. - */ - public LinkedList patch_deepCopy(LinkedList patches) { - final LinkedList patchesCopy = new LinkedList(); - for (final Patch aPatch : patches) { - final Patch patchCopy = new Patch(); - for (final Diff aDiff : aPatch.diffs) { - final Diff diffCopy = new Diff(aDiff.operation, aDiff.text); - patchCopy.diffs.add(diffCopy); - } - patchCopy.start1 = aPatch.start1; - patchCopy.start2 = aPatch.start2; - patchCopy.length1 = aPatch.length1; - patchCopy.length2 = aPatch.length2; - patchesCopy.add(patchCopy); - } - return patchesCopy; - } - - /** - * Merge a set of patches onto the text. Return a patched text, as well as an array of - * true/false values indicating which patches were applied. - * - * @param patches Array of Patch objects - * @param text Old text. - * @return Two element Object array, containing the new text and an array of boolean - * values. - */ - public Object[] patch_apply(LinkedList patches, String text) { - if (patches.isEmpty()) { - return new Object[] { text, new boolean[0] }; - } - - // Deep copy the patches so that no changes are made to originals. - patches = patch_deepCopy(patches); - - final String nullPadding = patch_addPadding(patches); - text = nullPadding + text + nullPadding; - patch_splitMax(patches); - - int x = 0; - // delta keeps track of the offset between the expected and actual location - // of the previous patch. If there are patches expected at positions 10 and - // 20, but the first patch was found at 12, delta is 2 and the second patch - // has an effective expected position of 22. - int delta = 0; - final boolean[] results = new boolean[patches.size()]; - for (final Patch aPatch : patches) { - final int expected_loc = aPatch.start2 + delta; - final String text1 = diff_text1(aPatch.diffs); - int start_loc; - int end_loc = -1; - if (text1.length() > this.Match_MaxBits) { - // patch_splitMax will only provide an oversized pattern in the case of - // a monster delete. - start_loc = match_main(text, - text1.substring(0, this.Match_MaxBits), expected_loc); - if (start_loc != -1) { - end_loc = match_main(text, - text1.substring(text1.length() - this.Match_MaxBits), - expected_loc + text1.length() - this.Match_MaxBits); - if (end_loc == -1 || start_loc >= end_loc) { - // Can't find valid trailing context. Drop this patch. - start_loc = -1; - } - } - } else { - start_loc = match_main(text, text1, expected_loc); - } - if (start_loc == -1) { - // No match found. :( - results[x] = false; - // Subtract the delta for this failed patch from subsequent patches. - delta -= aPatch.length2 - aPatch.length1; - } else { - // Found a match. :) - results[x] = true; - delta = start_loc - expected_loc; - String text2; - if (end_loc == -1) { - text2 = text.substring(start_loc, - Math.min(start_loc + text1.length(), text.length())); - } else { - text2 = text.substring(start_loc, - Math.min(end_loc + this.Match_MaxBits, text.length())); - } - if (text1.equals(text2)) { - // Perfect match, just shove the replacement text in. - text = text.substring(0, start_loc) + diff_text2(aPatch.diffs) - + text.substring(start_loc + text1.length()); - } else { - // Imperfect match. Run a diff to get a framework of equivalent - // indices. - final LinkedList diffs = diff_main(text1, text2, false); - if (text1.length() > this.Match_MaxBits - && diff_levenshtein(diffs) / (float) text1.length() > this.Patch_DeleteThreshold) { - // The end points match, but the content is unacceptably bad. - results[x] = false; - } else { - diff_cleanupSemanticLossless(diffs); - int index1 = 0; - for (final Diff aDiff : aPatch.diffs) { - if (aDiff.operation != Operation.EQUAL) { - final int index2 = diff_xIndex(diffs, index1); - if (aDiff.operation == Operation.INSERT) { - // Insertion - text = text.substring(0, start_loc + index2) + aDiff.text - + text.substring(start_loc + index2); - } else if (aDiff.operation == Operation.DELETE) { - // Deletion - text = text.substring(0, start_loc + index2) - + text.substring(start_loc + diff_xIndex(diffs, - index1 + aDiff.text.length())); - } - } - if (aDiff.operation != Operation.DELETE) { - index1 += aDiff.text.length(); - } - } - } - } - } - x++; - } - // Strip the padding off. - text = text.substring(nullPadding.length(), text.length() - - nullPadding.length()); - return new Object[] { text, results }; - } - - /** - * Add some padding on text start and end so that edges can match something. Intended - * to be called only from within patch_apply. - * - * @param patches Array of Patch objects. - * @return The padding string added to each side. - */ - public String patch_addPadding(LinkedList patches) { - final short paddingLength = this.Patch_Margin; - String nullPadding = ""; - for (short x = 1; x <= paddingLength; x++) { - nullPadding += String.valueOf((char) x); - } - - // Bump all the patches forward. - for (final Patch aPatch : patches) { - aPatch.start1 += paddingLength; - aPatch.start2 += paddingLength; - } - - // Add some padding on start of first diff. - Patch patch = patches.getFirst(); - LinkedList diffs = patch.diffs; - if (diffs.isEmpty() || diffs.getFirst().operation != Operation.EQUAL) { - // Add nullPadding equality. - diffs.addFirst(new Diff(Operation.EQUAL, nullPadding)); - patch.start1 -= paddingLength; // Should be 0. - patch.start2 -= paddingLength; // Should be 0. - patch.length1 += paddingLength; - patch.length2 += paddingLength; - } else if (paddingLength > diffs.getFirst().text.length()) { - // Grow first equality. - final Diff firstDiff = diffs.getFirst(); - final int extraLength = paddingLength - firstDiff.text.length(); - firstDiff.text = nullPadding.substring(firstDiff.text.length()) - + firstDiff.text; - patch.start1 -= extraLength; - patch.start2 -= extraLength; - patch.length1 += extraLength; - patch.length2 += extraLength; - } - - // Add some padding on end of last diff. - patch = patches.getLast(); - diffs = patch.diffs; - if (diffs.isEmpty() || diffs.getLast().operation != Operation.EQUAL) { - // Add nullPadding equality. - diffs.addLast(new Diff(Operation.EQUAL, nullPadding)); - patch.length1 += paddingLength; - patch.length2 += paddingLength; - } else if (paddingLength > diffs.getLast().text.length()) { - // Grow last equality. - final Diff lastDiff = diffs.getLast(); - final int extraLength = paddingLength - lastDiff.text.length(); - lastDiff.text += nullPadding.substring(0, extraLength); - patch.length1 += extraLength; - patch.length2 += extraLength; - } - - return nullPadding; - } - - /** - * Look through the patches and break up any which are longer than the maximum limit - * of the match algorithm. Intended to be called only from within patch_apply. - * - * @param patches LinkedList of Patch objects. - */ - public void patch_splitMax(LinkedList patches) { - final short patch_size = Match_MaxBits; - String precontext, postcontext; - Patch patch; - int start1, start2; - boolean empty; - Operation diff_type; - String diff_text; - final ListIterator pointer = patches.listIterator(); - Patch bigpatch = pointer.hasNext() ? pointer.next() : null; - while (bigpatch != null) { - if (bigpatch.length1 <= Match_MaxBits) { - bigpatch = pointer.hasNext() ? pointer.next() : null; - continue; - } - // Remove the big old patch. - pointer.remove(); - start1 = bigpatch.start1; - start2 = bigpatch.start2; - precontext = ""; - while (!bigpatch.diffs.isEmpty()) { - // Create one of several smaller patches. - patch = new Patch(); - empty = true; - patch.start1 = start1 - precontext.length(); - patch.start2 = start2 - precontext.length(); - if (precontext.length() != 0) { - patch.length1 = patch.length2 = precontext.length(); - patch.diffs.add(new Diff(Operation.EQUAL, precontext)); - } - while (!bigpatch.diffs.isEmpty() - && patch.length1 < patch_size - Patch_Margin) { - diff_type = bigpatch.diffs.getFirst().operation; - diff_text = bigpatch.diffs.getFirst().text; - if (diff_type == Operation.INSERT) { - // Insertions are harmless. - patch.length2 += diff_text.length(); - start2 += diff_text.length(); - patch.diffs.addLast(bigpatch.diffs.removeFirst()); - empty = false; - } else if (diff_type == Operation.DELETE && patch.diffs.size() == 1 - && patch.diffs.getFirst().operation == Operation.EQUAL - && diff_text.length() > 2 * patch_size) { - // This is a large deletion. Let it pass in one chunk. - patch.length1 += diff_text.length(); - start1 += diff_text.length(); - empty = false; - patch.diffs.add(new Diff(diff_type, diff_text)); - bigpatch.diffs.removeFirst(); - } else { - // Deletion or equality. Only take as much as we can stomach. - diff_text = diff_text.substring(0, Math.min(diff_text.length(), - patch_size - patch.length1 - Patch_Margin)); - patch.length1 += diff_text.length(); - start1 += diff_text.length(); - if (diff_type == Operation.EQUAL) { - patch.length2 += diff_text.length(); - start2 += diff_text.length(); - } else { - empty = false; - } - patch.diffs.add(new Diff(diff_type, diff_text)); - if (diff_text.equals(bigpatch.diffs.getFirst().text)) { - bigpatch.diffs.removeFirst(); - } else { - bigpatch.diffs.getFirst().text = bigpatch.diffs.getFirst().text - .substring(diff_text.length()); - } - } - } - // Compute the head context for the next patch. - precontext = diff_text2(patch.diffs); - precontext = precontext.substring(Math.max(0, precontext.length() - - Patch_Margin)); - // Append the end context for this patch. - if (diff_text1(bigpatch.diffs).length() > Patch_Margin) { - postcontext = diff_text1(bigpatch.diffs).substring(0, Patch_Margin); - } else { - postcontext = diff_text1(bigpatch.diffs); - } - if (postcontext.length() != 0) { - patch.length1 += postcontext.length(); - patch.length2 += postcontext.length(); - if (!patch.diffs.isEmpty() - && patch.diffs.getLast().operation == Operation.EQUAL) { - patch.diffs.getLast().text += postcontext; - } else { - patch.diffs.add(new Diff(Operation.EQUAL, postcontext)); - } - } - if (!empty) { - pointer.add(patch); - } - } - bigpatch = pointer.hasNext() ? pointer.next() : null; - } - } - - /** - * Take a list of patches and return a textual representation. - * - * @param patches List of Patch objects. - * @return Text representation of patches. - */ - public String patch_toText(List patches) { - final StringBuilder text = new StringBuilder(); - for (final Patch aPatch : patches) { - text.append(aPatch); - } - return text.toString(); - } - - /** - * Parse a textual representation of patches and return a List of Patch objects. - * - * @param textline Text representation of patches. - * @return List of Patch objects. - * @throws IllegalArgumentException If invalid input. - */ - public List patch_fromText(String textline) - throws IllegalArgumentException { - final List patches = new LinkedList(); - if (textline.length() == 0) { - return patches; - } - final List textList = Arrays.asList(textline.split("\n")); - final LinkedList text = new LinkedList(textList); - Patch patch; - final Pattern patchHeader = Pattern.compile("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"); - Matcher m; - char sign; - String line; - while (!text.isEmpty()) { - m = patchHeader.matcher(text.getFirst()); - if (!m.matches()) { - throw new IllegalArgumentException( - "Invalid patch string: " + text.getFirst()); - } - patch = new Patch(); - patches.add(patch); - patch.start1 = Integer.parseInt(m.group(1)); - if (m.group(2).length() == 0) { - patch.start1--; - patch.length1 = 1; - } else if (m.group(2).equals("0")) { - patch.length1 = 0; - } else { - patch.start1--; - patch.length1 = Integer.parseInt(m.group(2)); - } - - patch.start2 = Integer.parseInt(m.group(3)); - if (m.group(4).length() == 0) { - patch.start2--; - patch.length2 = 1; - } else if (m.group(4).equals("0")) { - patch.length2 = 0; - } else { - patch.start2--; - patch.length2 = Integer.parseInt(m.group(4)); - } - text.removeFirst(); - - while (!text.isEmpty()) { - try { - sign = text.getFirst().charAt(0); - } catch (final IndexOutOfBoundsException e) { - // Blank line? Whatever. - text.removeFirst(); - continue; - } - line = text.getFirst().substring(1); - line = line.replace("+", "%2B"); // decode would change all "+" to " " - try { - line = URLDecoder.decode(line, "UTF-8"); - } catch (final UnsupportedEncodingException e) { - // Not likely on modern system. - throw new Error("This system does not support UTF-8.", e); - } catch (final IllegalArgumentException e) { - // Malformed URI sequence. - throw new IllegalArgumentException( - "Illegal escape in patch_fromText: " + line, e); - } - if (sign == '-') { - // Deletion. - patch.diffs.add(new Diff(Operation.DELETE, line)); - } else if (sign == '+') { - // Insertion. - patch.diffs.add(new Diff(Operation.INSERT, line)); - } else if (sign == ' ') { - // Minor equality. - patch.diffs.add(new Diff(Operation.EQUAL, line)); - } else if (sign == '@') { - // Start of next patch. - break; - } else { - // WTF? - throw new IllegalArgumentException( - "Invalid patch mode '" + sign + "' in: " + line); - } - text.removeFirst(); - } - } - return patches; - } - - /** - * Class representing one diff operation. - */ - public static class Diff { - /** - * One of: INSERT, DELETE or EQUAL. - */ - public Operation operation; - /** - * The text associated with this diff operation. - */ - public String text; - - /** - * Constructor. Initializes the diff with the provided values. - * - * @param operation One of INSERT, DELETE or EQUAL. - * @param text The text being applied. - */ - public Diff(Operation operation, String text) { - // Construct a diff with the specified operation and text. - this.operation = operation; - this.text = text; - } - - /** - * Display a human-readable version of this Diff. - * - * @return text version. - */ - @Override - public String toString() { - final String prettyText = this.text.replace('\n', '\u00b6'); - return "Diff(" + this.operation + ",\"" + prettyText + "\")"; - } - - /** - * Create a numeric hash value for a Diff. This function is not used by DMP. - * - * @return Hash value. - */ - @Override - public int hashCode() { - final int prime = 31; - int result = (operation == null) ? 0 : operation.hashCode(); - result += prime * ((text == null) ? 0 : text.hashCode()); - return result; - } - - /** - * Is this Diff equivalent to another Diff? - * - * @param obj Another Diff to compare against. - * @return true or false. - */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final Diff other = (Diff) obj; - if (operation != other.operation) { - return false; - } - if (text == null) { - if (other.text != null) { - return false; - } - } else if (!text.equals(other.text)) { - return false; - } - return true; - } - } - - /** - * Class representing one patch operation. - */ - public static class Patch { - public LinkedList diffs; - public int start1; - public int start2; - public int length1; - public int length2; - - /** - * Constructor. Initializes with an empty list of diffs. - */ - public Patch() { - this.diffs = new LinkedList(); - } - - /** - * Emulate GNU diff's format. Header: @@ -382,8 +481,9 @@ Indices are printed as - * 1-based, not 0-based. - * - * @return The GNU diff string. - */ - @Override - public String toString() { - String coords1, coords2; - if (this.length1 == 0) { - coords1 = this.start1 + ",0"; - } else if (this.length1 == 1) { - coords1 = Integer.toString(this.start1 + 1); - } else { - coords1 = (this.start1 + 1) + "," + this.length1; - } - if (this.length2 == 0) { - coords2 = this.start2 + ",0"; - } else if (this.length2 == 1) { - coords2 = Integer.toString(this.start2 + 1); - } else { - coords2 = (this.start2 + 1) + "," + this.length2; - } - final StringBuilder text = new StringBuilder(); - text.append("@@ -").append(coords1).append(" +").append(coords2) - .append(" @@\n"); - // Escape the body of the patch with %xx notation. - for (final Diff aDiff : this.diffs) { - switch (aDiff.operation) { - case INSERT: - text.append('+'); - break; - case DELETE: - text.append('-'); - break; - case EQUAL: - text.append(' '); - break; - } - try { - text.append(URLEncoder.encode(aDiff.text, "UTF-8").replace('+', ' ')) - .append("\n"); - } catch (final UnsupportedEncodingException e) { - // Not likely on modern system. - throw new Error("This system does not support UTF-8.", e); - } - } - return unescapeForEncodeUriCompatability(text.toString()); - } - } - - /** - * Unescape selected chars for compatability with JavaScript's encodeURI. In speed - * critical applications this could be dropped since the receiving application will - * certainly decode these fine. Note that this function is case-sensitive. Thus "%3f" - * would not be unescaped. But this is ok because it is only called with the output of - * URLEncoder.encode which returns uppercase hex. - * - * Example: "%3F" -> "?", "%24" -> "$", etc. - * - * @param str The string to escape. - * @return The escaped string. - */ - private static String unescapeForEncodeUriCompatability(String str) { - return str.replace("%21", "!").replace("%7E", "~") - .replace("%27", "'").replace("%28", "(").replace("%29", ")") - .replace("%3B", ";").replace("%2F", "/").replace("%3F", "?") - .replace("%3A", ":").replace("%40", "@").replace("%26", "&") - .replace("%3D", "=").replace("%2B", "+").replace("%24", "$") - .replace("%2C", ",").replace("%23", "#"); - } -} diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java index 74aaacb2..bb4153c7 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java @@ -23,6 +23,7 @@ import de.skuzzle.test.snapshots.SnapshotTestResult.SnapshotStatus; import de.skuzzle.test.snapshots.StructuralAssertions; import de.skuzzle.test.snapshots.data.text.TextDiff; +import de.skuzzle.test.snapshots.data.text.TextDiff.Settings; import de.skuzzle.test.snapshots.data.text.TextDiffAssertionError; import de.skuzzle.test.snapshots.validation.Arguments; import de.skuzzle.test.snapshots.validation.State; @@ -307,7 +308,7 @@ private AssertionError toDiffableAssertionError(AssertionError original, String .append(System.lineSeparator()); final TextDiff testDiff = determineDiff(original, storedSnapshot, serializedActual); - if (testDiff.hasDifference()) { + if (testDiff.changesDetected()) { assertionMessage .append(System.lineSeparator()) .append("Full unified diff of actual result and stored snapshot:") @@ -327,7 +328,12 @@ private TextDiff determineDiff(AssertionError original, String storedSnapshot, S // comparison in TextDiffStructuralAssertions return ((TextDiffAssertionError) original).textDiff(); } else { - return TextDiff.diffOf(storedSnapshot, serializedActual, configuration.textDiffContextLines(testMethod)); + return TextDiff.compare( + Settings.defaultSettings() + .withInlineOpeningChangeMarker("~~~~") + .withInlineClosingChangeMarker("~~~~") + .withContextLines(configuration.textDiffContextLines(testMethod)), + storedSnapshot, serializedActual); } } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/Junit5SnapshotTestContextProvider.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/Junit5SnapshotTestContextProvider.java index 189b2b45..d7d96f40 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/Junit5SnapshotTestContextProvider.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/junit5/Junit5SnapshotTestContextProvider.java @@ -48,12 +48,6 @@ public static SnapshotTestContext fromExtensionContext(ExtensionContext extensio * @return The attached {@link SnapshotTestContext}. */ public static SnapshotTestContext create(ExtensionContext extensionContext) { - searchParents(extensionContext) - .ifPresent(existingContext -> { - throw new IllegalStateException( - "There is already a SnapshotTestContext attached to the given ExtensionContext or any of its parents"); - }); - final var testClass = extensionContext.getRequiredTestClass(); final SnapshotConfiguration snapshotConfiguration = SnapshotConfiguration.defaultConfigurationFor(testClass); final var snapshotTestContext = SnapshotTestContext.forConfiguration(snapshotConfiguration); @@ -73,12 +67,6 @@ public static SnapshotTestContext create(ExtensionContext extensionContext) { */ @Deprecated(since = "1.7.0") public static SnapshotTestContext createLegacy(ExtensionContext extensionContext) { - searchParents(extensionContext) - .ifPresent(existingContext -> { - throw new IllegalStateException( - "There is already a SnapshotTestContext attached to the given ExtensionContext or any of its parents"); - }); - final var testClass = extensionContext.getRequiredTestClass(); final SnapshotConfiguration snapshotConfiguration = SnapshotConfiguration.legacyConfigurationFor(testClass); final var snapshotTestContext = SnapshotTestContext.forConfiguration(snapshotConfiguration); diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java index 1cb44059..8c32c3a2 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java @@ -2,114 +2,160 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.Arrays; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; +import de.skuzzle.test.snapshots.data.text.TextDiff.Settings; +import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; + +@EnableSnapshotTests public class TextDiffTest { - @Test - void testHasDifferenceFalseIfIgnoreWhitespacesAndOnlyWhitespaceChanges() throws Exception { - final TextDiff diffOf = TextDiff.diffOf(new DiffInterpreter().withIgnoreWhitespaceChanges(true), " ", " "); - assertThat(diffOf.hasDifference()).isFalse(); + private String join(String... lines) { + return Arrays.stream(lines).collect(Collectors.joining("\n")); } - @Test - void testHasDifferenceFalseIfIgnoreWhitespacesAndOnlyLineSeparatorChanges() throws Exception { - final TextDiff diffOf = TextDiff.diffOf(new DiffInterpreter().withIgnoreWhitespaceChanges(true), "a\rb", - "a\nb"); - assertThat(diffOf.hasDifference()).isFalse(); - } + private final Settings settings = Settings.defaultSettings(); - @Test - void testDiffOfEmptyStrings() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter(), "", ""); - assertThat(diff.toString()).isEmpty(); - assertThat(diff.hasDifference()).isFalse(); - } + @Nested + class WithIgnoreWhitespaces { + { + settings.withIgnoreWhitespaces(true); + } - @Test - void testDiffWithLinebreaksAndLessThanContextLinesWithTrailingLineBreak() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withIgnoreWhitespaceChanges(false) - .withContextLines(50), - "1\n2\n3\n4\n5\n6\n7\nline1\n", "1\n2\n3\n4\n5\n6\n7\nlineX\n"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "1\n2\n3\n4\n5\n6\n7\nline-[1]+[X]\n")); - assertThat(diff.hasDifference()).isTrue(); - } + @Test + void testSingleLineRemoved() throws Exception { + final TextDiff textDiff = TextDiff.compare(settings, "Just a single line", ""); + assertThat(textDiff.changesDetected()).isTrue(); + assertThat(textDiff.toString()).isEqualTo(" 1 - Just a single line"); + } - @Test - void testDiffWithLinebreaksAndLessThanContextLinesWithoutTrailingLineBreak() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withIgnoreWhitespaceChanges(false) - .withContextLines(50), - "1\n2\n3\n4\n5\n6\n7\nline1", "1\n2\n3\n4\n5\n6\n7\nlineX"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "1\n2\n3\n4\n5\n6\n7\nline-[1]+[X]")); - assertThat(diff.hasDifference()).isTrue(); - } + @Test + void testSingleLineAdded() throws Exception { + final TextDiff textDiff = TextDiff.compare(settings, "", "Just a single line"); + assertThat(textDiff.changesDetected()).isTrue(); + assertThat(textDiff.toString()).isEqualTo(" 1 + Just a single line"); + } - @Test - void testDiffOnlyInLinebreaks() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter() - .withContextLines(5) - .withIgnoreWhitespaceChanges(false), - "line1\n1\n2\n3\n4\n5\n6\n7", "line1\r1\r2\r3\r4\r5\r6\r7"); - assertThat(diff.toString()) - .isEqualTo("Strings differ in linebreaks. Expected: 'LF(\\n)', Actual encountered: 'CR(\\r)'"); + @Test + void testLinebreakChangeIgnoreWhitespaces() throws Exception { + final TextDiff textDiff = TextDiff.compare(settings, "line1\nline2", "line1\r\nline2"); + assertThat(textDiff.changesDetected()).isFalse(); + assertThat(textDiff.toString()).isEqualTo(""); + } } - @Test - void testDiffInLinebreaks() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter() - .withContextLines(5) - .withIgnoreWhitespaceChanges(false), - "line1\n1\n2\n3\n4\n5\n6\n7", "lineX\r1\r2\r3\r4\r5\r6\r7"); - - assertThat(diff.toString()) - .isEqualTo(LineSeparator.SYSTEM.convert( - "Strings differ in linebreaks. Expected: 'LF(\\n)', Actual encountered: 'CR(\\r)'\n\nline-[1]+[X]\r1\r2\r3\r4\r[...]")); - assertThat(diff.hasDifference()).isTrue(); - } + @Nested + class WithObeyWhitespaces { + { + settings.withIgnoreWhitespaces(false); + } - @Test - void testDiffWithHugeEqualBlockAtTheStart() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withContextLines(5), - "1\n2\n3\n4\n5\n6\n7\nline1", "1\n2\n3\n4\n5\n6\n7\nlineX"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "[...]\n4\n5\n6\n7\nline-[1]+[X]")); - assertThat(diff.hasDifference()).isTrue(); - } + @Test + void testSingleLineRemoved() throws Exception { + final TextDiff textDiff = TextDiff.compare(settings, "Just a single line", ""); + assertThat(textDiff.changesDetected()).isTrue(); + assertThat(textDiff.toString()).isEqualTo(" 1 - Just a single line"); + } - @Test - void testDiffWithHugeEqualBlockAtTheEnd() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withContextLines(5), - "line1\n1\n2\n3\n4\n5\n6\n7", "lineX\n1\n2\n3\n4\n5\n6\n7"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "line-[1]+[X]\n1\n2\n3\n4\n[...]")); - assertThat(diff.hasDifference()).isTrue(); - } + @Test + void testSingleLineAdded() throws Exception { + final TextDiff textDiff = TextDiff.compare(settings, "", "Just a single line"); + assertThat(textDiff.changesDetected()).isTrue(); + assertThat(textDiff.toString()).isEqualTo(" 1 + Just a single line"); + } - @Test - void testDiffWithHugeEqualBlockAtTheStartAndTheEnd() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withContextLines(5), - "1\n2\n3\n4\n5\n6\n7\nline1\n8\n9\n10\n11\n12\n13\n14", - "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "[...]\n3\n4\n5\n6\n7\n-[line1]\n8\n9\n10\n11\n12\n[...]")); - assertThat(diff.hasDifference()).isTrue(); + @Test + void testWhitespaceChangeWithinSingleLine() throws Exception { + final TextDiff textDiff = TextDiff.compare(settings, + "Just a single line", "Just a single line"); + assertThat(textDiff.changesDetected()).isTrue(); + assertThat(textDiff.toString()).isEqualTo(LineSeparator.SYSTEM.convert("" + + " 1 - Just a single<< >>line\n" + + " 1 + Just a single<< >>line")); + } + + @Test + void testLinebreakChangeObeyWhitespaces() throws Exception { + final TextDiff textDiff = TextDiff.compare(settings, + "line1\nline2", "line1\r\nline2"); + assertThat(textDiff.changesDetected()).isTrue(); + assertThat(textDiff.toString()) + .isEqualTo("Strings differ in linebreaks. Expected: 'LF(\\n)', Actual encountered: 'CRLF(\\r\\n)'"); + } } @Test - void testDiffWithHugeEqualBlockInTheMiddle() throws Exception { - final TextDiff diff = TextDiff.diffOf(new DiffInterpreter().withContextLines(5), - "line1\n1\n2\n3\n4\n5\n6\n7\nline2\n8\n9\n10\n11\n12\n13\n14\nline3", - "line2\n1\n2\n3\n4\n5\n6\n7\nline2\n8\n9\n10\n11\n12\n13\n14\nline4"); - assertThat(diff.toString()).isEqualTo( - LineSeparator.SYSTEM.convert( - "line-[1]+[2]\n1\n2\n3\n4\n[...]\n11\n12\n13\n14\nline-[3]+[4]")); - assertThat(diff.hasDifference()).isTrue(); + void testRenderDiff(Snapshot snapshot) throws Exception { + final String expected = join( + "Some unchanged lines1", + "Some unchanged lines2", + "Some unchanged lines3", + "Some unchanged lines4", + "Some unchanged lines5", + "Some unchanged lines6", + "Some unchanged lines7", + "Some unchanged lines8", + "This is a test senctence.", + "This is the second line.", + "Some unchanged lines9", + "Some unchanged lines10", + "Some unchanged lines11", + "Some unchanged lines12", + "And here is the finish with way more than 80 characters and I'm very curious how this is going to be displayed in split view diff.", + "This line is unchanged", + "Some unchanged lines13", + "Some unchanged lines14", + "Some unchanged lines15", + "Some unchanged lines16", + "Some unchanged lines17", + "Some unchanged lines18", + "Some unchanged lines19", + "Some unchanged lines20", + "Another difference", + "Some unchanged lines21", + "Some unchanged lines22", + "Some unchanged lines23", + "Some unchanged lines24"); + + final String actual = join( + "Some unchanged lines1", + "Some unchanged lines2", + "Some unchanged lines3", + "Some unchanged lines4", + "Some unchanged lines5", + "Some unchanged lines6", + "Some unchanged lines7", + "Some unchanged lines8", + "This is a test for diffutils.", + "This is the second line.", + "Some unchanged lines9", + "Some unchanged lines10", + "Some unchanged lines11", + "Some unchanged lines12", + "This line is unchanged", + "This line has been added", + "Some unchanged lines13", + "Some unchanged lines14", + "Some unchanged lines15", + "Some unchanged lines16", + "Some unchanged lines17", + "Some unchanged lines18", + "Some unchanged lines19", + "Some unchanged lines20", + "This has changed", + "Some unchanged lines21", + "Some unchanged lines22", + "Some unchanged lines23", + "Some unchanged lines24"); + + final TextDiff textDiff = TextDiff.compare(Settings.defaultSettings().withContextLines(3), expected, actual); + + System.out.println(textDiff); + snapshot.assertThat(textDiff).asText().matchesSnapshotText(); } } diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/diff_match_patch_Test.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/diff_match_patch_Test.java deleted file mode 100644 index 1d347d89..00000000 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/diff_match_patch_Test.java +++ /dev/null @@ -1,1125 +0,0 @@ -/* - * Diff Match and Patch -- Test harness - * Copyright 2018 The diff-match-patch Authors. - * https://github.com/google/diff-match-patch - * - * 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. - */ - -/** - * Compile from diff-match-patch/java with: - * javac -d classes src/name/fraser/neil/plaintext/diff_match_patch.java tests/name/fraser/neil/plaintext/diff_match_patch_Test.java - * Execute with: - * java -classpath classes name/fraser/neil/plaintext/diff_match_patch_Test - */ - -package de.skuzzle.test.snapshots.data.text; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import de.skuzzle.test.snapshots.data.text.diff_match_patch; -import de.skuzzle.test.snapshots.data.text.diff_match_patch.Diff; -import de.skuzzle.test.snapshots.data.text.diff_match_patch.LinesToCharsResult; -import de.skuzzle.test.snapshots.data.text.diff_match_patch.Patch; - -public class diff_match_patch_Test { - - private final diff_match_patch dmp = new diff_match_patch();; - private final diff_match_patch.Operation DELETE = diff_match_patch.Operation.DELETE; - private final diff_match_patch.Operation EQUAL = diff_match_patch.Operation.EQUAL; - private final diff_match_patch.Operation INSERT = diff_match_patch.Operation.INSERT; - - // DIFF TEST FUNCTIONS - - @Test - void testDiffCommonPrefix() { - // Detect any common prefix. - assertEquals("diff_commonPrefix: Null case.", 0, dmp.diff_commonPrefix("abc", "xyz")); - - assertEquals("diff_commonPrefix: Non-null case.", 4, dmp.diff_commonPrefix("1234abcdef", "1234xyz")); - - assertEquals("diff_commonPrefix: Whole case.", 4, dmp.diff_commonPrefix("1234", "1234xyz")); - } - - @Test - void testDiffCommonSuffix() { - // Detect any common suffix. - assertEquals("diff_commonSuffix: Null case.", 0, dmp.diff_commonSuffix("abc", "xyz")); - - assertEquals("diff_commonSuffix: Non-null case.", 4, dmp.diff_commonSuffix("abcdef1234", "xyz1234")); - - assertEquals("diff_commonSuffix: Whole case.", 4, dmp.diff_commonSuffix("1234", "xyz1234")); - } - - @Test - void testDiffCommonOverlap() { - // Detect any suffix/prefix overlap. - assertEquals("diff_commonOverlap: Null case.", 0, dmp.diff_commonOverlap("", "abcd")); - - assertEquals("diff_commonOverlap: Whole case.", 3, dmp.diff_commonOverlap("abc", "abcd")); - - assertEquals("diff_commonOverlap: No overlap.", 0, dmp.diff_commonOverlap("123456", "abcd")); - - assertEquals("diff_commonOverlap: Overlap.", 3, dmp.diff_commonOverlap("123456xxx", "xxxabcd")); - - // Some overly clever languages (C#) may treat ligatures as equal to their - // component letters. E.g. U+FB01 == 'fi' - assertEquals("diff_commonOverlap: Unicode.", 0, dmp.diff_commonOverlap("fi", "\ufb01i")); - } - - @Test - void testDiffHalfmatch() { - // Detect a halfmatch. - dmp.Diff_Timeout = 1; - assertNull("diff_halfMatch: No match #1.", dmp.diff_halfMatch("1234567890", "abcdef")); - - assertNull("diff_halfMatch: No match #2.", dmp.diff_halfMatch("12345", "23")); - - assertArrayEquals("diff_halfMatch: Single Match #1.", new String[] { "12", "90", "a", "z", "345678" }, - dmp.diff_halfMatch("1234567890", "a345678z")); - - assertArrayEquals("diff_halfMatch: Single Match #2.", new String[] { "a", "z", "12", "90", "345678" }, - dmp.diff_halfMatch("a345678z", "1234567890")); - - assertArrayEquals("diff_halfMatch: Single Match #3.", new String[] { "abc", "z", "1234", "0", "56789" }, - dmp.diff_halfMatch("abc56789z", "1234567890")); - - assertArrayEquals("diff_halfMatch: Single Match #4.", new String[] { "a", "xyz", "1", "7890", "23456" }, - dmp.diff_halfMatch("a23456xyz", "1234567890")); - - assertArrayEquals("diff_halfMatch: Multiple Matches #1.", - new String[] { "12123", "123121", "a", "z", "1234123451234" }, - dmp.diff_halfMatch("121231234123451234123121", "a1234123451234z")); - - assertArrayEquals("diff_halfMatch: Multiple Matches #2.", - new String[] { "", "-=-=-=-=-=", "x", "", "x-=-=-=-=-=-=-=" }, - dmp.diff_halfMatch("x-=-=-=-=-=-=-=-=-=-=-=-=", "xx-=-=-=-=-=-=-=")); - - assertArrayEquals("diff_halfMatch: Multiple Matches #3.", - new String[] { "-=-=-=-=-=", "", "", "y", "-=-=-=-=-=-=-=y" }, - dmp.diff_halfMatch("-=-=-=-=-=-=-=-=-=-=-=-=y", "-=-=-=-=-=-=-=yy")); - - // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not - // -qHillo+x=HelloHe-w+Hulloy - assertArrayEquals("diff_halfMatch: Non-optimal halfmatch.", - new String[] { "qHillo", "w", "x", "Hulloy", "HelloHe" }, - dmp.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")); - - dmp.Diff_Timeout = 0; - assertNull("diff_halfMatch: Optimal no halfmatch.", dmp.diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")); - } - - @Test - void testDiffLinesToChars() { - // Convert lines down to characters. - final ArrayList tmpVector = new ArrayList(); - tmpVector.add(""); - tmpVector.add("alpha\n"); - tmpVector.add("beta\n"); - assertLinesToCharsResultEquals("diff_linesToChars: Shared lines.", - new LinesToCharsResult("\u0001\u0002\u0001", "\u0002\u0001\u0002", tmpVector), - dmp.diff_linesToChars("alpha\nbeta\nalpha\n", "beta\nalpha\nbeta\n")); - - tmpVector.clear(); - tmpVector.add(""); - tmpVector.add("alpha\r\n"); - tmpVector.add("beta\r\n"); - tmpVector.add("\r\n"); - assertLinesToCharsResultEquals("diff_linesToChars: Empty string and blank lines.", - new LinesToCharsResult("", "\u0001\u0002\u0003\u0003", tmpVector), - dmp.diff_linesToChars("", "alpha\r\nbeta\r\n\r\n\r\n")); - - tmpVector.clear(); - tmpVector.add(""); - tmpVector.add("a"); - tmpVector.add("b"); - assertLinesToCharsResultEquals("diff_linesToChars: No linebreaks.", - new LinesToCharsResult("\u0001", "\u0002", tmpVector), dmp.diff_linesToChars("a", "b")); - - // More than 256 to reveal any 8-bit limitations. - final int n = 300; - tmpVector.clear(); - final StringBuilder lineList = new StringBuilder(); - final StringBuilder charList = new StringBuilder(); - for (int i = 1; i < n + 1; i++) { - tmpVector.add(i + "\n"); - lineList.append(i + "\n"); - charList.append(String.valueOf((char) i)); - } - assertEquals("Test initialization fail #1.", n, tmpVector.size()); - final String lines = lineList.toString(); - final String chars = charList.toString(); - assertEquals("Test initialization fail #2.", n, chars.length()); - tmpVector.add(0, ""); - assertLinesToCharsResultEquals("diff_linesToChars: More than 256.", - new LinesToCharsResult(chars, "", tmpVector), dmp.diff_linesToChars(lines, "")); - } - - @Test - void testDiffCharsToLines() { - // First check that Diff equality works. - assertTrue("diff_charsToLines: Equality #1.", new Diff(EQUAL, "a").equals(new Diff(EQUAL, "a"))); - - assertEquals("diff_charsToLines: Equality #2.", new Diff(EQUAL, "a"), new Diff(EQUAL, "a")); - - // Convert chars up to lines. - LinkedList diffs = diffList(new Diff(EQUAL, "\u0001\u0002\u0001"), - new Diff(INSERT, "\u0002\u0001\u0002")); - final ArrayList tmpVector = new ArrayList(); - tmpVector.add(""); - tmpVector.add("alpha\n"); - tmpVector.add("beta\n"); - dmp.diff_charsToLines(diffs, tmpVector); - assertEquals("diff_charsToLines: Shared lines.", - diffList(new Diff(EQUAL, "alpha\nbeta\nalpha\n"), new Diff(INSERT, "beta\nalpha\nbeta\n")), diffs); - - // More than 256 to reveal any 8-bit limitations. - final int n = 300; - tmpVector.clear(); - StringBuilder lineList = new StringBuilder(); - final StringBuilder charList = new StringBuilder(); - for (int i = 1; i < n + 1; i++) { - tmpVector.add(i + "\n"); - lineList.append(i + "\n"); - charList.append(String.valueOf((char) i)); - } - assertEquals("Test initialization fail #3.", n, tmpVector.size()); - final String lines = lineList.toString(); - String chars = charList.toString(); - assertEquals("Test initialization fail #4.", n, chars.length()); - tmpVector.add(0, ""); - diffs = diffList(new Diff(DELETE, chars)); - dmp.diff_charsToLines(diffs, tmpVector); - assertEquals("diff_charsToLines: More than 256.", diffList(new Diff(DELETE, lines)), diffs); - - // More than 65536 to verify any 16-bit limitation. - lineList = new StringBuilder(); - for (int i = 0; i < 66000; i++) { - lineList.append(i + "\n"); - } - chars = lineList.toString(); - final LinesToCharsResult results = dmp.diff_linesToChars(chars, ""); - diffs = diffList(new Diff(INSERT, results.chars1)); - dmp.diff_charsToLines(diffs, results.lineArray); - assertEquals("diff_charsToLines: More than 65536.", chars, diffs.getFirst().text); - } - - @Test - void testDiffCleanupMerge() { - // Cleanup a messy diff. - LinkedList diffs = diffList(); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Null case.", diffList(), diffs); - - diffs = diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "b"), new Diff(INSERT, "c")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: No change case.", - diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "b"), new Diff(INSERT, "c")), diffs); - - diffs = diffList(new Diff(EQUAL, "a"), new Diff(EQUAL, "b"), new Diff(EQUAL, "c")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Merge equalities.", diffList(new Diff(EQUAL, "abc")), diffs); - - diffs = diffList(new Diff(DELETE, "a"), new Diff(DELETE, "b"), new Diff(DELETE, "c")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Merge deletions.", diffList(new Diff(DELETE, "abc")), diffs); - - diffs = diffList(new Diff(INSERT, "a"), new Diff(INSERT, "b"), new Diff(INSERT, "c")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Merge insertions.", diffList(new Diff(INSERT, "abc")), diffs); - - diffs = diffList(new Diff(DELETE, "a"), new Diff(INSERT, "b"), new Diff(DELETE, "c"), new Diff(INSERT, "d"), - new Diff(EQUAL, "e"), new Diff(EQUAL, "f")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Merge interweave.", - diffList(new Diff(DELETE, "ac"), new Diff(INSERT, "bd"), new Diff(EQUAL, "ef")), diffs); - - diffs = diffList(new Diff(DELETE, "a"), new Diff(INSERT, "abc"), new Diff(DELETE, "dc")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Prefix and suffix detection.", - diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "d"), new Diff(INSERT, "b"), new Diff(EQUAL, "c")), - diffs); - - diffs = diffList(new Diff(EQUAL, "x"), new Diff(DELETE, "a"), new Diff(INSERT, "abc"), new Diff(DELETE, "dc"), - new Diff(EQUAL, "y")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Prefix and suffix detection with equalities.", - diffList(new Diff(EQUAL, "xa"), new Diff(DELETE, "d"), new Diff(INSERT, "b"), new Diff(EQUAL, "cy")), - diffs); - - diffs = diffList(new Diff(EQUAL, "a"), new Diff(INSERT, "ba"), new Diff(EQUAL, "c")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Slide edit left.", diffList(new Diff(INSERT, "ab"), new Diff(EQUAL, "ac")), - diffs); - - diffs = diffList(new Diff(EQUAL, "c"), new Diff(INSERT, "ab"), new Diff(EQUAL, "a")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Slide edit right.", diffList(new Diff(EQUAL, "ca"), new Diff(INSERT, "ba")), - diffs); - - diffs = diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "b"), new Diff(EQUAL, "c"), new Diff(DELETE, "ac"), - new Diff(EQUAL, "x")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Slide edit left recursive.", - diffList(new Diff(DELETE, "abc"), new Diff(EQUAL, "acx")), diffs); - - diffs = diffList(new Diff(EQUAL, "x"), new Diff(DELETE, "ca"), new Diff(EQUAL, "c"), new Diff(DELETE, "b"), - new Diff(EQUAL, "a")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Slide edit right recursive.", - diffList(new Diff(EQUAL, "xca"), new Diff(DELETE, "cba")), diffs); - - diffs = diffList(new Diff(DELETE, "b"), new Diff(INSERT, "ab"), new Diff(EQUAL, "c")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Empty merge.", diffList(new Diff(INSERT, "a"), new Diff(EQUAL, "bc")), diffs); - - diffs = diffList(new Diff(EQUAL, ""), new Diff(INSERT, "a"), new Diff(EQUAL, "b")); - dmp.diff_cleanupMerge(diffs); - assertEquals("diff_cleanupMerge: Empty equality.", diffList(new Diff(INSERT, "a"), new Diff(EQUAL, "b")), - diffs); - } - - @Test - void testDiffCleanupSemanticLossless() { - // Slide diffs to match logical boundaries. - LinkedList diffs = diffList(); - dmp.diff_cleanupSemanticLossless(diffs); - assertEquals("diff_cleanupSemanticLossless: Null case.", diffList(), diffs); - - diffs = diffList(new Diff(EQUAL, "AAA\r\n\r\nBBB"), new Diff(INSERT, "\r\nDDD\r\n\r\nBBB"), - new Diff(EQUAL, "\r\nEEE")); - dmp.diff_cleanupSemanticLossless(diffs); - assertEquals("diff_cleanupSemanticLossless: Blank lines.", diffList(new Diff(EQUAL, "AAA\r\n\r\n"), - new Diff(INSERT, "BBB\r\nDDD\r\n\r\n"), new Diff(EQUAL, "BBB\r\nEEE")), diffs); - - diffs = diffList(new Diff(EQUAL, "AAA\r\nBBB"), new Diff(INSERT, " DDD\r\nBBB"), new Diff(EQUAL, " EEE")); - dmp.diff_cleanupSemanticLossless(diffs); - assertEquals("diff_cleanupSemanticLossless: Line boundaries.", - diffList(new Diff(EQUAL, "AAA\r\n"), new Diff(INSERT, "BBB DDD\r\n"), new Diff(EQUAL, "BBB EEE")), - diffs); - - diffs = diffList(new Diff(EQUAL, "The c"), new Diff(INSERT, "ow and the c"), new Diff(EQUAL, "at.")); - dmp.diff_cleanupSemanticLossless(diffs); - assertEquals("diff_cleanupSemanticLossless: Word boundaries.", - diffList(new Diff(EQUAL, "The "), new Diff(INSERT, "cow and the "), new Diff(EQUAL, "cat.")), diffs); - - diffs = diffList(new Diff(EQUAL, "The-c"), new Diff(INSERT, "ow-and-the-c"), new Diff(EQUAL, "at.")); - dmp.diff_cleanupSemanticLossless(diffs); - assertEquals("diff_cleanupSemanticLossless: Alphanumeric boundaries.", - diffList(new Diff(EQUAL, "The-"), new Diff(INSERT, "cow-and-the-"), new Diff(EQUAL, "cat.")), diffs); - - diffs = diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "a"), new Diff(EQUAL, "ax")); - dmp.diff_cleanupSemanticLossless(diffs); - assertEquals("diff_cleanupSemanticLossless: Hitting the start.", - diffList(new Diff(DELETE, "a"), new Diff(EQUAL, "aax")), diffs); - - diffs = diffList(new Diff(EQUAL, "xa"), new Diff(DELETE, "a"), new Diff(EQUAL, "a")); - dmp.diff_cleanupSemanticLossless(diffs); - assertEquals("diff_cleanupSemanticLossless: Hitting the end.", - diffList(new Diff(EQUAL, "xaa"), new Diff(DELETE, "a")), diffs); - - diffs = diffList(new Diff(EQUAL, "The xxx. The "), new Diff(INSERT, "zzz. The "), new Diff(EQUAL, "yyy.")); - dmp.diff_cleanupSemanticLossless(diffs); - assertEquals("diff_cleanupSemanticLossless: Sentence boundaries.", - diffList(new Diff(EQUAL, "The xxx."), new Diff(INSERT, " The zzz."), new Diff(EQUAL, " The yyy.")), - diffs); - } - - @Test - void testDiffCleanupSemantic() { - // Cleanup semantically trivial equalities. - LinkedList diffs = diffList(); - dmp.diff_cleanupSemantic(diffs); - assertEquals("diff_cleanupSemantic: Null case.", diffList(), diffs); - - diffs = diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "cd"), new Diff(EQUAL, "12"), new Diff(DELETE, "e")); - dmp.diff_cleanupSemantic(diffs); - assertEquals("diff_cleanupSemantic: No elimination #1.", - diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "cd"), new Diff(EQUAL, "12"), new Diff(DELETE, "e")), - diffs); - - diffs = diffList(new Diff(DELETE, "abc"), new Diff(INSERT, "ABC"), new Diff(EQUAL, "1234"), - new Diff(DELETE, "wxyz")); - dmp.diff_cleanupSemantic(diffs); - assertEquals("diff_cleanupSemantic: No elimination #2.", diffList(new Diff(DELETE, "abc"), - new Diff(INSERT, "ABC"), new Diff(EQUAL, "1234"), new Diff(DELETE, "wxyz")), diffs); - - diffs = diffList(new Diff(DELETE, "a"), new Diff(EQUAL, "b"), new Diff(DELETE, "c")); - dmp.diff_cleanupSemantic(diffs); - assertEquals("diff_cleanupSemantic: Simple elimination.", - diffList(new Diff(DELETE, "abc"), new Diff(INSERT, "b")), diffs); - - diffs = diffList(new Diff(DELETE, "ab"), new Diff(EQUAL, "cd"), new Diff(DELETE, "e"), new Diff(EQUAL, "f"), - new Diff(INSERT, "g")); - dmp.diff_cleanupSemantic(diffs); - assertEquals("diff_cleanupSemantic: Backpass elimination.", - diffList(new Diff(DELETE, "abcdef"), new Diff(INSERT, "cdfg")), diffs); - - diffs = diffList(new Diff(INSERT, "1"), new Diff(EQUAL, "A"), new Diff(DELETE, "B"), new Diff(INSERT, "2"), - new Diff(EQUAL, "_"), new Diff(INSERT, "1"), new Diff(EQUAL, "A"), new Diff(DELETE, "B"), - new Diff(INSERT, "2")); - dmp.diff_cleanupSemantic(diffs); - assertEquals("diff_cleanupSemantic: Multiple elimination.", - diffList(new Diff(DELETE, "AB_AB"), new Diff(INSERT, "1A2_1A2")), diffs); - - diffs = diffList(new Diff(EQUAL, "The c"), new Diff(DELETE, "ow and the c"), new Diff(EQUAL, "at.")); - dmp.diff_cleanupSemantic(diffs); - assertEquals("diff_cleanupSemantic: Word boundaries.", - diffList(new Diff(EQUAL, "The "), new Diff(DELETE, "cow and the "), new Diff(EQUAL, "cat.")), diffs); - - diffs = diffList(new Diff(DELETE, "abcxx"), new Diff(INSERT, "xxdef")); - dmp.diff_cleanupSemantic(diffs); - assertEquals("diff_cleanupSemantic: No overlap elimination.", - diffList(new Diff(DELETE, "abcxx"), new Diff(INSERT, "xxdef")), diffs); - - diffs = diffList(new Diff(DELETE, "abcxxx"), new Diff(INSERT, "xxxdef")); - dmp.diff_cleanupSemantic(diffs); - assertEquals("diff_cleanupSemantic: Overlap elimination.", - diffList(new Diff(DELETE, "abc"), new Diff(EQUAL, "xxx"), new Diff(INSERT, "def")), diffs); - - diffs = diffList(new Diff(DELETE, "xxxabc"), new Diff(INSERT, "defxxx")); - dmp.diff_cleanupSemantic(diffs); - assertEquals("diff_cleanupSemantic: Reverse overlap elimination.", - diffList(new Diff(INSERT, "def"), new Diff(EQUAL, "xxx"), new Diff(DELETE, "abc")), diffs); - - diffs = diffList(new Diff(DELETE, "abcd1212"), new Diff(INSERT, "1212efghi"), new Diff(EQUAL, "----"), - new Diff(DELETE, "A3"), new Diff(INSERT, "3BC")); - dmp.diff_cleanupSemantic(diffs); - assertEquals("diff_cleanupSemantic: Two overlap eliminations.", - diffList(new Diff(DELETE, "abcd"), new Diff(EQUAL, "1212"), new Diff(INSERT, "efghi"), - new Diff(EQUAL, "----"), new Diff(DELETE, "A"), new Diff(EQUAL, "3"), new Diff(INSERT, "BC")), - diffs); - } - - @Test - void testDiffCleanupEfficiency() { - // Cleanup operationally trivial equalities. - dmp.Diff_EditCost = 4; - LinkedList diffs = diffList(); - dmp.diff_cleanupEfficiency(diffs); - assertEquals("diff_cleanupEfficiency: Null case.", diffList(), diffs); - - diffs = diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "12"), new Diff(EQUAL, "wxyz"), - new Diff(DELETE, "cd"), new Diff(INSERT, "34")); - dmp.diff_cleanupEfficiency(diffs); - assertEquals("diff_cleanupEfficiency: No elimination.", diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "12"), - new Diff(EQUAL, "wxyz"), new Diff(DELETE, "cd"), new Diff(INSERT, "34")), diffs); - - diffs = diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "12"), new Diff(EQUAL, "xyz"), new Diff(DELETE, "cd"), - new Diff(INSERT, "34")); - dmp.diff_cleanupEfficiency(diffs); - assertEquals("diff_cleanupEfficiency: Four-edit elimination.", - diffList(new Diff(DELETE, "abxyzcd"), new Diff(INSERT, "12xyz34")), diffs); - - diffs = diffList(new Diff(INSERT, "12"), new Diff(EQUAL, "x"), new Diff(DELETE, "cd"), new Diff(INSERT, "34")); - dmp.diff_cleanupEfficiency(diffs); - assertEquals("diff_cleanupEfficiency: Three-edit elimination.", - diffList(new Diff(DELETE, "xcd"), new Diff(INSERT, "12x34")), diffs); - - diffs = diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "12"), new Diff(EQUAL, "xy"), new Diff(INSERT, "34"), - new Diff(EQUAL, "z"), new Diff(DELETE, "cd"), new Diff(INSERT, "56")); - dmp.diff_cleanupEfficiency(diffs); - assertEquals("diff_cleanupEfficiency: Backpass elimination.", - diffList(new Diff(DELETE, "abxyzcd"), new Diff(INSERT, "12xy34z56")), diffs); - - dmp.Diff_EditCost = 5; - diffs = diffList(new Diff(DELETE, "ab"), new Diff(INSERT, "12"), new Diff(EQUAL, "wxyz"), - new Diff(DELETE, "cd"), new Diff(INSERT, "34")); - dmp.diff_cleanupEfficiency(diffs); - assertEquals("diff_cleanupEfficiency: High cost elimination.", - diffList(new Diff(DELETE, "abwxyzcd"), new Diff(INSERT, "12wxyz34")), diffs); - dmp.Diff_EditCost = 4; - } - - @Test - void testDiffPrettyHtml() { - // Pretty print. - final LinkedList diffs = diffList(new Diff(EQUAL, "a\n"), new Diff(DELETE, "b"), - new Diff(INSERT, "c&d")); - assertEquals("diff_prettyHtml:", - "
<B>b</B>c&d", - dmp.diff_prettyHtml(diffs)); - } - - @Test - void testDiffText() { - // Compute the source and destination texts. - final LinkedList diffs = diffList(new Diff(EQUAL, "jump"), new Diff(DELETE, "s"), new Diff(INSERT, "ed"), - new Diff(EQUAL, " over "), new Diff(DELETE, "the"), new Diff(INSERT, "a"), new Diff(EQUAL, " lazy")); - assertEquals("diff_text1:", "jumps over the lazy", dmp.diff_text1(diffs)); - assertEquals("diff_text2:", "jumped over a lazy", dmp.diff_text2(diffs)); - } - - @Test - void testDiffDelta() { - // Convert a diff into delta string. - LinkedList diffs = diffList(new Diff(EQUAL, "jump"), new Diff(DELETE, "s"), new Diff(INSERT, "ed"), - new Diff(EQUAL, " over "), new Diff(DELETE, "the"), new Diff(INSERT, "a"), new Diff(EQUAL, " lazy"), - new Diff(INSERT, "old dog")); - String text1 = dmp.diff_text1(diffs); - assertEquals("diff_text1: Base text.", "jumps over the lazy", text1); - - String delta = dmp.diff_toDelta(diffs); - assertEquals("diff_toDelta:", "=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog", delta); - - // Convert delta string into a diff. - assertEquals("diff_fromDelta: Normal.", diffs, dmp.diff_fromDelta(text1, delta)); - - // Generates error (19 < 20). - try { - dmp.diff_fromDelta(text1 + "x", delta); - fail("diff_fromDelta: Too long."); - } catch (final IllegalArgumentException ex) { - // Exception expected. - } - - // Generates error (19 > 18). - try { - dmp.diff_fromDelta(text1.substring(1), delta); - fail("diff_fromDelta: Too short."); - } catch (final IllegalArgumentException ex) { - // Exception expected. - } - - // Generates error (%c3%xy invalid Unicode). - try { - dmp.diff_fromDelta("", "+%c3%xy"); - fail("diff_fromDelta: Invalid character."); - } catch (final IllegalArgumentException ex) { - // Exception expected. - } - - // Test deltas with special characters. - diffs = diffList(new Diff(EQUAL, "\u0680 \000 \t %"), new Diff(DELETE, "\u0681 \001 \n ^"), - new Diff(INSERT, "\u0682 \002 \\ |")); - text1 = dmp.diff_text1(diffs); - assertEquals("diff_text1: Unicode text.", "\u0680 \000 \t %\u0681 \001 \n ^", text1); - - delta = dmp.diff_toDelta(diffs); - assertEquals("diff_toDelta: Unicode.", "=7\t-7\t+%DA%82 %02 %5C %7C", delta); - - assertEquals("diff_fromDelta: Unicode.", diffs, dmp.diff_fromDelta(text1, delta)); - - // Verify pool of unchanged characters. - diffs = diffList(new Diff(INSERT, "A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ")); - final String text2 = dmp.diff_text2(diffs); - assertEquals("diff_text2: Unchanged characters.", "A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", - text2); - - delta = dmp.diff_toDelta(diffs); - assertEquals("diff_toDelta: Unchanged characters.", "+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", - delta); - - // Convert delta string into a diff. - assertEquals("diff_fromDelta: Unchanged characters.", diffs, dmp.diff_fromDelta("", delta)); - - // 160 kb string. - String a = "abcdefghij"; - for (int i = 0; i < 14; i++) { - a += a; - } - diffs = diffList(new Diff(INSERT, a)); - delta = dmp.diff_toDelta(diffs); - assertEquals("diff_toDelta: 160kb string.", "+" + a, delta); - - // Convert delta string into a diff. - assertEquals("diff_fromDelta: 160kb string.", diffs, dmp.diff_fromDelta("", delta)); - } - - @Test - void testDiffXIndex() { - // Translate a location in text1 to text2. - LinkedList diffs = diffList(new Diff(DELETE, "a"), new Diff(INSERT, "1234"), new Diff(EQUAL, "xyz")); - assertEquals("diff_xIndex: Translation on equality.", 5, dmp.diff_xIndex(diffs, 2)); - - diffs = diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "1234"), new Diff(EQUAL, "xyz")); - assertEquals("diff_xIndex: Translation on deletion.", 1, dmp.diff_xIndex(diffs, 3)); - } - - @Test - void testDiffLevenshtein() { - LinkedList diffs = diffList(new Diff(DELETE, "abc"), new Diff(INSERT, "1234"), new Diff(EQUAL, "xyz")); - assertEquals("diff_levenshtein: Levenshtein with trailing equality.", 4, dmp.diff_levenshtein(diffs)); - - diffs = diffList(new Diff(EQUAL, "xyz"), new Diff(DELETE, "abc"), new Diff(INSERT, "1234")); - assertEquals("diff_levenshtein: Levenshtein with leading equality.", 4, dmp.diff_levenshtein(diffs)); - - diffs = diffList(new Diff(DELETE, "abc"), new Diff(EQUAL, "xyz"), new Diff(INSERT, "1234")); - assertEquals("diff_levenshtein: Levenshtein with middle equality.", 7, dmp.diff_levenshtein(diffs)); - } - - @Test - void testDiffBisect() { - // Normal. - final String a = "cat"; - final String b = "map"; - // Since the resulting diff hasn't been normalized, it would be ok if - // the insertion and deletion pairs are swapped. - // If the order changes, tweak this test as required. - LinkedList diffs = diffList(new Diff(DELETE, "c"), new Diff(INSERT, "m"), new Diff(EQUAL, "a"), - new Diff(DELETE, "t"), new Diff(INSERT, "p")); - assertEquals("diff_bisect: Normal.", diffs, dmp.diff_bisect(a, b, Long.MAX_VALUE)); - - // Timeout. - diffs = diffList(new Diff(DELETE, "cat"), new Diff(INSERT, "map")); - assertEquals("diff_bisect: Timeout.", diffs, dmp.diff_bisect(a, b, 0)); - } - - @Test - void testDiffMain() { - // Perform a trivial diff. - LinkedList diffs = diffList(); - assertEquals("diff_main: Null case.", diffs, dmp.diff_main("", "", false)); - - diffs = diffList(new Diff(EQUAL, "abc")); - assertEquals("diff_main: Equality.", diffs, dmp.diff_main("abc", "abc", false)); - - diffs = diffList(new Diff(EQUAL, "ab"), new Diff(INSERT, "123"), new Diff(EQUAL, "c")); - assertEquals("diff_main: Simple insertion.", diffs, dmp.diff_main("abc", "ab123c", false)); - - diffs = diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "123"), new Diff(EQUAL, "bc")); - assertEquals("diff_main: Simple deletion.", diffs, dmp.diff_main("a123bc", "abc", false)); - - diffs = diffList(new Diff(EQUAL, "a"), new Diff(INSERT, "123"), new Diff(EQUAL, "b"), new Diff(INSERT, "456"), - new Diff(EQUAL, "c")); - assertEquals("diff_main: Two insertions.", diffs, dmp.diff_main("abc", "a123b456c", false)); - - diffs = diffList(new Diff(EQUAL, "a"), new Diff(DELETE, "123"), new Diff(EQUAL, "b"), new Diff(DELETE, "456"), - new Diff(EQUAL, "c")); - assertEquals("diff_main: Two deletions.", diffs, dmp.diff_main("a123b456c", "abc", false)); - - // Perform a real diff. - // Switch off the timeout. - dmp.Diff_Timeout = 0; - diffs = diffList(new Diff(DELETE, "a"), new Diff(INSERT, "b")); - assertEquals("diff_main: Simple case #1.", diffs, dmp.diff_main("a", "b", false)); - - diffs = diffList(new Diff(DELETE, "Apple"), new Diff(INSERT, "Banana"), new Diff(EQUAL, "s are a"), - new Diff(INSERT, "lso"), new Diff(EQUAL, " fruit.")); - assertEquals("diff_main: Simple case #2.", diffs, - dmp.diff_main("Apples are a fruit.", "Bananas are also fruit.", false)); - - diffs = diffList(new Diff(DELETE, "a"), new Diff(INSERT, "\u0680"), new Diff(EQUAL, "x"), - new Diff(DELETE, "\t"), new Diff(INSERT, "\000")); - assertEquals("diff_main: Simple case #3.", diffs, dmp.diff_main("ax\t", "\u0680x\000", false)); - - diffs = diffList(new Diff(DELETE, "1"), new Diff(EQUAL, "a"), new Diff(DELETE, "y"), new Diff(EQUAL, "b"), - new Diff(DELETE, "2"), new Diff(INSERT, "xab")); - assertEquals("diff_main: Overlap #1.", diffs, dmp.diff_main("1ayb2", "abxab", false)); - - diffs = diffList(new Diff(INSERT, "xaxcx"), new Diff(EQUAL, "abc"), new Diff(DELETE, "y")); - assertEquals("diff_main: Overlap #2.", diffs, dmp.diff_main("abcy", "xaxcxabc", false)); - - diffs = diffList(new Diff(DELETE, "ABCD"), new Diff(EQUAL, "a"), new Diff(DELETE, "="), new Diff(INSERT, "-"), - new Diff(EQUAL, "bcd"), new Diff(DELETE, "="), new Diff(INSERT, "-"), - new Diff(EQUAL, "efghijklmnopqrs"), new Diff(DELETE, "EFGHIJKLMNOefg")); - assertEquals("diff_main: Overlap #3.", diffs, - dmp.diff_main("ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg", "a-bcd-efghijklmnopqrs", false)); - - diffs = diffList(new Diff(INSERT, " "), new Diff(EQUAL, "a"), new Diff(INSERT, "nd"), - new Diff(EQUAL, " [[Pennsylvania]]"), new Diff(DELETE, " and [[New")); - assertEquals("diff_main: Large equality.", diffs, - dmp.diff_main("a [[Pennsylvania]] and [[New", " and [[Pennsylvania]]", false)); - - dmp.Diff_Timeout = 0.1f; // 100ms - String a = "`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n"; - String b = "I am the very model of a modern major general,\nI've information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n"; - // Increase the text lengths by 1024 times to ensure a timeout. - for (int i = 0; i < 10; i++) { - a += a; - b += b; - } - final long startTime = System.currentTimeMillis(); - dmp.diff_main(a, b); - final long endTime = System.currentTimeMillis(); - // Test that we took at least the timeout period. - assertTrue("diff_main: Timeout min.", dmp.Diff_Timeout * 1000 <= endTime - startTime); - // Test that we didn't take forever (be forgiving). - // Theoretically this test could fail very occasionally if the - // OS task swaps or locks up for a second at the wrong moment. - assertTrue("diff_main: Timeout max.", dmp.Diff_Timeout * 1000 * 2 > endTime - startTime); - dmp.Diff_Timeout = 0; - - // Test the linemode speedup. - // Must be long to pass the 100 char cutoff. - a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; - b = "abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n"; - assertEquals("diff_main: Simple line-mode.", dmp.diff_main(a, b, true), dmp.diff_main(a, b, false)); - - a = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; - b = "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij"; - assertEquals("diff_main: Single line-mode.", dmp.diff_main(a, b, true), dmp.diff_main(a, b, false)); - - a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; - b = "abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n"; - final String[] texts_linemode = diff_rebuildtexts(dmp.diff_main(a, b, true)); - final String[] texts_textmode = diff_rebuildtexts(dmp.diff_main(a, b, false)); - assertArrayEquals("diff_main: Overlap line-mode.", texts_textmode, texts_linemode); - - // Test null inputs. - try { - dmp.diff_main(null, null); - fail("diff_main: Null inputs."); - } catch (final IllegalArgumentException ex) { - // Error expected. - } - } - - // MATCH TEST FUNCTIONS - - @Test - void testMatchAlphabet() { - // Initialise the bitmasks for Bitap. - Map bitmask; - bitmask = new HashMap(); - bitmask.put('a', 4); - bitmask.put('b', 2); - bitmask.put('c', 1); - assertEquals("match_alphabet: Unique.", bitmask, dmp.match_alphabet("abc")); - - bitmask = new HashMap(); - bitmask.put('a', 37); - bitmask.put('b', 18); - bitmask.put('c', 8); - assertEquals("match_alphabet: Duplicates.", bitmask, dmp.match_alphabet("abcaba")); - } - - @Test - void testMatchBitap() { - // Bitap algorithm. - dmp.Match_Distance = 100; - dmp.Match_Threshold = 0.5f; - assertEquals("match_bitap: Exact match #1.", 5, dmp.match_bitap("abcdefghijk", "fgh", 5)); - - assertEquals("match_bitap: Exact match #2.", 5, dmp.match_bitap("abcdefghijk", "fgh", 0)); - - assertEquals("match_bitap: Fuzzy match #1.", 4, dmp.match_bitap("abcdefghijk", "efxhi", 0)); - - assertEquals("match_bitap: Fuzzy match #2.", 2, dmp.match_bitap("abcdefghijk", "cdefxyhijk", 5)); - - assertEquals("match_bitap: Fuzzy match #3.", -1, dmp.match_bitap("abcdefghijk", "bxy", 1)); - - assertEquals("match_bitap: Overflow.", 2, dmp.match_bitap("123456789xx0", "3456789x0", 2)); - - assertEquals("match_bitap: Before start match.", 0, dmp.match_bitap("abcdef", "xxabc", 4)); - - assertEquals("match_bitap: Beyond end match.", 3, dmp.match_bitap("abcdef", "defyy", 4)); - - assertEquals("match_bitap: Oversized pattern.", 0, dmp.match_bitap("abcdef", "xabcdefy", 0)); - - dmp.Match_Threshold = 0.4f; - assertEquals("match_bitap: Threshold #1.", 4, dmp.match_bitap("abcdefghijk", "efxyhi", 1)); - - dmp.Match_Threshold = 0.3f; - assertEquals("match_bitap: Threshold #2.", -1, dmp.match_bitap("abcdefghijk", "efxyhi", 1)); - - dmp.Match_Threshold = 0.0f; - assertEquals("match_bitap: Threshold #3.", 1, dmp.match_bitap("abcdefghijk", "bcdef", 1)); - - dmp.Match_Threshold = 0.5f; - assertEquals("match_bitap: Multiple select #1.", 0, dmp.match_bitap("abcdexyzabcde", "abccde", 3)); - - assertEquals("match_bitap: Multiple select #2.", 8, dmp.match_bitap("abcdexyzabcde", "abccde", 5)); - - dmp.Match_Distance = 10; // Strict location. - assertEquals("match_bitap: Distance test #1.", -1, - dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)); - - assertEquals("match_bitap: Distance test #2.", 0, - dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdxxefg", 1)); - - dmp.Match_Distance = 1000; // Loose location. - assertEquals("match_bitap: Distance test #3.", 0, dmp.match_bitap("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)); - } - - @Test - void testMatchMain() { - // Full match. - assertEquals("match_main: Equality.", 0, dmp.match_main("abcdef", "abcdef", 1000)); - - assertEquals("match_main: Null text.", -1, dmp.match_main("", "abcdef", 1)); - - assertEquals("match_main: Null pattern.", 3, dmp.match_main("abcdef", "", 3)); - - assertEquals("match_main: Exact match.", 3, dmp.match_main("abcdef", "de", 3)); - - assertEquals("match_main: Beyond end match.", 3, dmp.match_main("abcdef", "defy", 4)); - - assertEquals("match_main: Oversized pattern.", 0, dmp.match_main("abcdef", "abcdefy", 0)); - - dmp.Match_Threshold = 0.7f; - assertEquals("match_main: Complex match.", 4, - dmp.match_main("I am the very model of a modern major general.", " that berry ", 5)); - dmp.Match_Threshold = 0.5f; - - // Test null inputs. - try { - dmp.match_main(null, null, 0); - fail("match_main: Null inputs."); - } catch (final IllegalArgumentException ex) { - // Error expected. - } - } - - // PATCH TEST FUNCTIONS - - @Test - void testPatchObj() { - // Patch Object. - final Patch p = new Patch(); - p.start1 = 20; - p.start2 = 21; - p.length1 = 18; - p.length2 = 17; - p.diffs = diffList(new Diff(EQUAL, "jump"), new Diff(DELETE, "s"), new Diff(INSERT, "ed"), - new Diff(EQUAL, " over "), new Diff(DELETE, "the"), new Diff(INSERT, "a"), new Diff(EQUAL, "\nlaz")); - final String strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n"; - assertEquals("Patch: toString.", strp, p.toString()); - } - - @Test - void testPatchFromText() { - assertTrue("patch_fromText: #0.", dmp.patch_fromText("").isEmpty()); - - final String strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n %0Alaz\n"; - assertEquals("patch_fromText: #1.", strp, dmp.patch_fromText(strp).get(0).toString()); - - assertEquals("patch_fromText: #2.", "@@ -1 +1 @@\n-a\n+b\n", - dmp.patch_fromText("@@ -1 +1 @@\n-a\n+b\n").get(0).toString()); - - assertEquals("patch_fromText: #3.", "@@ -1,3 +0,0 @@\n-abc\n", - dmp.patch_fromText("@@ -1,3 +0,0 @@\n-abc\n").get(0).toString()); - - assertEquals("patch_fromText: #4.", "@@ -0,0 +1,3 @@\n+abc\n", - dmp.patch_fromText("@@ -0,0 +1,3 @@\n+abc\n").get(0).toString()); - - // Generates error. - try { - dmp.patch_fromText("Bad\nPatch\n"); - fail("patch_fromText: #5."); - } catch (final IllegalArgumentException ex) { - // Exception expected. - } - } - - @Test - void testPatchToText() { - String strp = "@@ -21,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n"; - List patches; - patches = dmp.patch_fromText(strp); - assertEquals("patch_toText: Single.", strp, dmp.patch_toText(patches)); - - strp = "@@ -1,9 +1,9 @@\n-f\n+F\n oo+fooba\n@@ -7,9 +7,9 @@\n obar\n-,\n+.\n tes\n"; - patches = dmp.patch_fromText(strp); - assertEquals("patch_toText: Dual.", strp, dmp.patch_toText(patches)); - } - - @Test - void testPatchAddContext() { - dmp.Patch_Margin = 4; - Patch p; - p = dmp.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n").get(0); - dmp.patch_addContext(p, "The quick brown fox jumps over the lazy dog."); - assertEquals("patch_addContext: Simple case.", "@@ -17,12 +17,18 @@\n fox \n-jump\n+somersault\n s ov\n", - p.toString()); - - p = dmp.patch_fromText("@@ -21,4 +21,10 @@\n-jump\n+somersault\n").get(0); - dmp.patch_addContext(p, "The quick brown fox jumps."); - assertEquals("patch_addContext: Not enough trailing context.", - "@@ -17,10 +17,16 @@\n fox \n-jump\n+somersault\n s.\n", p.toString()); - - p = dmp.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n").get(0); - dmp.patch_addContext(p, "The quick brown fox jumps."); - assertEquals("patch_addContext: Not enough leading context.", "@@ -1,7 +1,8 @@\n Th\n-e\n+at\n qui\n", - p.toString()); - - p = dmp.patch_fromText("@@ -3 +3,2 @@\n-e\n+at\n").get(0); - dmp.patch_addContext(p, "The quick brown fox jumps. The quick brown fox crashes."); - assertEquals("patch_addContext: Ambiguity.", "@@ -1,27 +1,28 @@\n Th\n-e\n+at\n quick brown fox jumps. \n", - p.toString()); - } - - @SuppressWarnings("deprecation") - @Test - void testPatchMake() { - LinkedList patches; - patches = dmp.patch_make("", ""); - assertEquals("patch_make: Null case.", "", dmp.patch_toText(patches)); - - String text1 = "The quick brown fox jumps over the lazy dog."; - String text2 = "That quick brown fox jumped over a lazy dog."; - String expectedPatch = "@@ -1,8 +1,7 @@\n Th\n-at\n+e\n qui\n@@ -21,17 +21,18 @@\n jump\n-ed\n+s\n over \n-a\n+the\n laz\n"; - // The second patch must be "-21,17 +21,18", not "-22,17 +21,18" due to rolling - // context. - patches = dmp.patch_make(text2, text1); - assertEquals("patch_make: Text2+Text1 inputs.", expectedPatch, dmp.patch_toText(patches)); - - expectedPatch = "@@ -1,11 +1,12 @@\n Th\n-e\n+at\n quick b\n@@ -22,18 +22,17 @@\n jump\n-s\n+ed\n over \n-the\n+a\n laz\n"; - patches = dmp.patch_make(text1, text2); - assertEquals("patch_make: Text1+Text2 inputs.", expectedPatch, dmp.patch_toText(patches)); - - LinkedList diffs = dmp.diff_main(text1, text2, false); - patches = dmp.patch_make(diffs); - assertEquals("patch_make: Diff input.", expectedPatch, dmp.patch_toText(patches)); - - patches = dmp.patch_make(text1, diffs); - assertEquals("patch_make: Text1+Diff inputs.", expectedPatch, dmp.patch_toText(patches)); - - patches = dmp.patch_make(text1, text2, diffs); - assertEquals("patch_make: Text1+Text2+Diff inputs (deprecated).", expectedPatch, dmp.patch_toText(patches)); - - patches = dmp.patch_make("`1234567890-=[]\\;',./", "~!@#$%^&*()_+{}|:\"<>?"); - assertEquals("patch_toText: Character encoding.", - "@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n", - dmp.patch_toText(patches)); - - diffs = diffList(new Diff(DELETE, "`1234567890-=[]\\;',./"), new Diff(INSERT, "~!@#$%^&*()_+{}|:\"<>?")); - assertEquals("patch_fromText: Character decoding.", diffs, - dmp.patch_fromText( - "@@ -1,21 +1,21 @@\n-%601234567890-=%5B%5D%5C;',./\n+~!@#$%25%5E&*()_+%7B%7D%7C:%22%3C%3E?\n") - .get(0).diffs); - - text1 = ""; - for (int x = 0; x < 100; x++) { - text1 += "abcdef"; - } - text2 = text1 + "123"; - expectedPatch = "@@ -573,28 +573,31 @@\n cdefabcdefabcdefabcdefabcdef\n+123\n"; - patches = dmp.patch_make(text1, text2); - assertEquals("patch_make: Long string with repeats.", expectedPatch, dmp.patch_toText(patches)); - - // Test null inputs. - try { - dmp.patch_make(null); - fail("patch_make: Null inputs."); - } catch (final IllegalArgumentException ex) { - // Error expected. - } - } - - @Test - void testPatchSplitMax() { - // Assumes that Match_MaxBits is 32. - LinkedList patches; - patches = dmp.patch_make("abcdefghijklmnopqrstuvwxyz01234567890", - "XabXcdXefXghXijXklXmnXopXqrXstXuvXwxXyzX01X23X45X67X89X0"); - dmp.patch_splitMax(patches); - assertEquals("patch_splitMax: #1.", - "@@ -1,32 +1,46 @@\n+X\n ab\n+X\n cd\n+X\n ef\n+X\n gh\n+X\n ij\n+X\n kl\n+X\n mn\n+X\n op\n+X\n qr\n+X\n st\n+X\n uv\n+X\n wx\n+X\n yz\n+X\n 012345\n@@ -25,13 +39,18 @@\n zX01\n+X\n 23\n+X\n 45\n+X\n 67\n+X\n 89\n+X\n 0\n", - dmp.patch_toText(patches)); - - patches = dmp.patch_make("abcdef1234567890123456789012345678901234567890123456789012345678901234567890uvwxyz", - "abcdefuvwxyz"); - final String oldToText = dmp.patch_toText(patches); - dmp.patch_splitMax(patches); - assertEquals("patch_splitMax: #2.", oldToText, dmp.patch_toText(patches)); - - patches = dmp.patch_make("1234567890123456789012345678901234567890123456789012345678901234567890", "abc"); - dmp.patch_splitMax(patches); - assertEquals("patch_splitMax: #3.", - "@@ -1,32 +1,4 @@\n-1234567890123456789012345678\n 9012\n@@ -29,32 +1,4 @@\n-9012345678901234567890123456\n 7890\n@@ -57,14 +1,3 @@\n-78901234567890\n+abc\n", - dmp.patch_toText(patches)); - - patches = dmp.patch_make("abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1 abcdefghij , h : 0 , t : 1", - "abcdefghij , h : 1 , t : 1 abcdefghij , h : 1 , t : 1 abcdefghij , h : 0 , t : 1"); - dmp.patch_splitMax(patches); - assertEquals("patch_splitMax: #4.", - "@@ -2,32 +2,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n@@ -29,32 +29,32 @@\n bcdefghij , h : \n-0\n+1\n , t : 1 abcdef\n", - dmp.patch_toText(patches)); - } - - @Test - void testPatchAddPadding() { - LinkedList patches; - patches = dmp.patch_make("", "test"); - assertEquals("patch_addPadding: Both edges full.", "@@ -0,0 +1,4 @@\n+test\n", dmp.patch_toText(patches)); - dmp.patch_addPadding(patches); - assertEquals("patch_addPadding: Both edges full.", "@@ -1,8 +1,12 @@\n %01%02%03%04\n+test\n %01%02%03%04\n", - dmp.patch_toText(patches)); - - patches = dmp.patch_make("XY", "XtestY"); - assertEquals("patch_addPadding: Both edges partial.", "@@ -1,2 +1,6 @@\n X\n+test\n Y\n", - dmp.patch_toText(patches)); - dmp.patch_addPadding(patches); - assertEquals("patch_addPadding: Both edges partial.", "@@ -2,8 +2,12 @@\n %02%03%04X\n+test\n Y%01%02%03\n", - dmp.patch_toText(patches)); - - patches = dmp.patch_make("XXXXYYYY", "XXXXtestYYYY"); - assertEquals("patch_addPadding: Both edges none.", "@@ -1,8 +1,12 @@\n XXXX\n+test\n YYYY\n", - dmp.patch_toText(patches)); - dmp.patch_addPadding(patches); - assertEquals("patch_addPadding: Both edges none.", "@@ -5,8 +5,12 @@\n XXXX\n+test\n YYYY\n", - dmp.patch_toText(patches)); - } - - @Test - void testPatchApply() { - dmp.Match_Distance = 1000; - dmp.Match_Threshold = 0.5f; - dmp.Patch_DeleteThreshold = 0.5f; - LinkedList patches; - patches = dmp.patch_make("", ""); - Object[] results = dmp.patch_apply(patches, "Hello world."); - boolean[] boolArray = (boolean[]) results[1]; - String resultStr = results[0] + "\t" + boolArray.length; - assertEquals("patch_apply: Null case.", "Hello world.\t0", resultStr); - - patches = dmp.patch_make("The quick brown fox jumps over the lazy dog.", - "That quick brown fox jumped over a lazy dog."); - results = dmp.patch_apply(patches, "The quick brown fox jumps over the lazy dog."); - boolArray = (boolean[]) results[1]; - resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; - assertEquals("patch_apply: Exact match.", "That quick brown fox jumped over a lazy dog.\ttrue\ttrue", - resultStr); - - results = dmp.patch_apply(patches, "The quick red rabbit jumps over the tired tiger."); - boolArray = (boolean[]) results[1]; - resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; - assertEquals("patch_apply: Partial match.", "That quick red rabbit jumped over a tired tiger.\ttrue\ttrue", - resultStr); - - results = dmp.patch_apply(patches, "I am the very model of a modern major general."); - boolArray = (boolean[]) results[1]; - resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; - assertEquals("patch_apply: Failed match.", "I am the very model of a modern major general.\tfalse\tfalse", - resultStr); - - patches = dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy"); - results = dmp.patch_apply(patches, - "x123456789012345678901234567890-----++++++++++-----123456789012345678901234567890y"); - boolArray = (boolean[]) results[1]; - resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; - assertEquals("patch_apply: Big delete, small change.", "xabcy\ttrue\ttrue", resultStr); - - patches = dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy"); - results = dmp.patch_apply(patches, - "x12345678901234567890---------------++++++++++---------------12345678901234567890y"); - boolArray = (boolean[]) results[1]; - resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; - assertEquals("patch_apply: Big delete, big change 1.", - "xabc12345678901234567890---------------++++++++++---------------12345678901234567890y\tfalse\ttrue", - resultStr); - - dmp.Patch_DeleteThreshold = 0.6f; - patches = dmp.patch_make("x1234567890123456789012345678901234567890123456789012345678901234567890y", "xabcy"); - results = dmp.patch_apply(patches, - "x12345678901234567890---------------++++++++++---------------12345678901234567890y"); - boolArray = (boolean[]) results[1]; - resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; - assertEquals("patch_apply: Big delete, big change 2.", "xabcy\ttrue\ttrue", resultStr); - dmp.Patch_DeleteThreshold = 0.5f; - - // Compensate for failed patch. - dmp.Match_Threshold = 0.0f; - dmp.Match_Distance = 0; - patches = dmp.patch_make("abcdefghijklmnopqrstuvwxyz--------------------1234567890", - "abcXXXXXXXXXXdefghijklmnopqrstuvwxyz--------------------1234567YYYYYYYYYY890"); - results = dmp.patch_apply(patches, "ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567890"); - boolArray = (boolean[]) results[1]; - resultStr = results[0] + "\t" + boolArray[0] + "\t" + boolArray[1]; - assertEquals("patch_apply: Compensate for failed patch.", - "ABCDEFGHIJKLMNOPQRSTUVWXYZ--------------------1234567YYYYYYYYYY890\tfalse\ttrue", resultStr); - dmp.Match_Threshold = 0.5f; - dmp.Match_Distance = 1000; - - patches = dmp.patch_make("", "test"); - String patchStr = dmp.patch_toText(patches); - dmp.patch_apply(patches, ""); - assertEquals("patch_apply: No side effects.", patchStr, dmp.patch_toText(patches)); - - patches = dmp.patch_make("The quick brown fox jumps over the lazy dog.", "Woof"); - patchStr = dmp.patch_toText(patches); - dmp.patch_apply(patches, "The quick brown fox jumps over the lazy dog."); - assertEquals("patch_apply: No side effects with major delete.", patchStr, dmp.patch_toText(patches)); - - patches = dmp.patch_make("", "test"); - results = dmp.patch_apply(patches, ""); - boolArray = (boolean[]) results[1]; - resultStr = results[0] + "\t" + boolArray[0]; - assertEquals("patch_apply: Edge exact match.", "test\ttrue", resultStr); - - patches = dmp.patch_make("XY", "XtestY"); - results = dmp.patch_apply(patches, "XY"); - boolArray = (boolean[]) results[1]; - resultStr = results[0] + "\t" + boolArray[0]; - assertEquals("patch_apply: Near edge exact match.", "XtestY\ttrue", resultStr); - - patches = dmp.patch_make("y", "y123"); - results = dmp.patch_apply(patches, "x"); - boolArray = (boolean[]) results[1]; - resultStr = results[0] + "\t" + boolArray[0]; - assertEquals("patch_apply: Edge partial match.", "x123\ttrue", resultStr); - } - - private static void assertEquals(String error_msg, Object a, Object b) { - if (!a.toString().equals(b.toString())) { - throw new Error("assertEquals fail:\n Expected: " + a + "\n Actual: " + b - + "\n" + error_msg); - } - } - - private static void assertTrue(String error_msg, boolean a) { - if (!a) { - throw new Error("assertTrue fail: " + error_msg); - } - } - - private static void assertNull(String error_msg, Object n) { - if (n != null) { - throw new Error("assertNull fail: " + error_msg); - } - } - - private static void fail(String error_msg) { - throw new Error("Fail: " + error_msg); - } - - private static void assertArrayEquals(String error_msg, Object[] a, Object[] b) { - final List list_a = Arrays.asList(a); - final List list_b = Arrays.asList(b); - assertEquals(error_msg, list_a, list_b); - } - - private static void assertLinesToCharsResultEquals(String error_msg, - LinesToCharsResult a, LinesToCharsResult b) { - assertEquals(error_msg, a.chars1, b.chars1); - assertEquals(error_msg, a.chars2, b.chars2); - assertEquals(error_msg, a.lineArray, b.lineArray); - } - - // Construct the two texts which made up the diff originally. - private static String[] diff_rebuildtexts(LinkedList diffs) { - final String[] text = { "", "" }; - for (final Diff myDiff : diffs) { - if (myDiff.operation != diff_match_patch.Operation.INSERT) { - text[0] += myDiff.text; - } - if (myDiff.operation != diff_match_patch.Operation.DELETE) { - text[1] += myDiff.text; - } - } - return text; - } - - // Private function for quickly building lists of diffs. - private static LinkedList diffList(Diff... diffs) { - return new LinkedList(Arrays.asList(diffs)); - } -} diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/FailingSnapshotTests.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/FailingSnapshotTests.java index 247a9690..03b66d72 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/FailingSnapshotTests.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/FailingSnapshotTests.java @@ -241,7 +241,8 @@ void testFailBecauseSnapshotMismatch() throws Throwable { + "\t%s%n" + "%n" + "Full unified diff of actual result and stored snapshot:%n" - + "+[NOT ]test", + + " 1 - test%n" + + " 1 + NOT test", Path.of("src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTests$FailBecauseSnapshotMismatch_snapshots/testWithSnapshot_0.snapshot"))); } @@ -270,8 +271,10 @@ void testFailBecauseSnapshotMismatchWithWhitespaces() throws Throwable { + "Full unified diff of actual result and stored snapshot:%n" + "Strings differ in linebreaks. Expected: 'CRLF(\\r\\n)', Actual encountered: 'LF(\\n)'%n" + "%n" - + "line-[2]+[4]%n" - + "line-[3]+[5]", + + " 1 - <>%n" + + " 1 + <>%n" + + " 2 - <>%n" + + " 2 + <>", Path.of("src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTests$FailBecauseSnapshotMismatchWithWhitespaces_snapshots/testWithSnapshot_0.snapshot"))); } @@ -342,7 +345,8 @@ void testSoftAssertions() throws Exception { + "\t%s%n" + "%n" + "Full unified diff of actual result and stored snapshot:%n" - + "test+[2]", + + " 1 - <>%n" + + " 1 + <>", Path.of("src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTests$SoftAssertions_snapshots/testWithSnapshot_0.snapshot"))) .hasSuppressedException( new AssertionFailedError(String.format("Stored snapshot doesn't match actual result.%n" @@ -350,7 +354,8 @@ void testSoftAssertions() throws Exception { + "\t%s%n" + "%n" + "Full unified diff of actual result and stored snapshot:%n" - + "test+[3]", + + " 1 - <>%n" + + " 1 + <>", Path.of("src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTests$SoftAssertions_snapshots/testWithSnapshot_1.snapshot")))); } diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew.java index 4a15e005..5d8bff37 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew.java @@ -218,7 +218,8 @@ void testFailBecauseSnapshotMismatch() throws Throwable { + "\t%s%n" + "%n" + "Full unified diff of actual result and stored snapshot:%n" - + "+[NOT ]test", + + " 1 - test%n" + + " 1 + NOT test", Path.of("src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatch_snapshots/testWithSnapshot_0.snapshot"))); } @@ -247,8 +248,10 @@ void testFailBecauseSnapshotMismatchWithWhitespaces() throws Throwable { + "Full unified diff of actual result and stored snapshot:%n" + "Strings differ in linebreaks. Expected: 'CRLF(\\r\\n)', Actual encountered: 'LF(\\n)'%n" + "%n" - + "line-[2]+[4]%n" - + "line-[3]+[5]", + + " 1 - <>%n" + + " 1 + <>%n" + + " 2 - <>%n" + + " 2 + <>", Path.of("src/test/resources/de/skuzzle/test/snapshots/impl/FailingSnapshotTestsNew$FailBecauseSnapshotMismatchWithWhitespaces_snapshots/testWithSnapshot_0.snapshot"))); } diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/data/text/TextDiffTest_snapshots/testRenderDiff_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/data/text/TextDiffTest_snapshots/testRenderDiff_0.snapshot new file mode 100644 index 00000000..e838f86e --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/data/text/TextDiffTest_snapshots/testRenderDiff_0.snapshot @@ -0,0 +1,34 @@ +dynamic-directory: false +snapshot-name: testRenderDiff_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.data.text.TextDiffTest +test-method: testRenderDiff + +[...] + 6 6 Some unchanged lines6 + 7 7 Some unchanged lines7 + 8 8 Some unchanged lines8 + 9 - This is a test <>. + 9 + This is a test <>. + 10 10 This is the second line. + 11 11 Some unchanged lines9 + 12 12 Some unchanged lines10 + 13 13 Some unchanged lines11 + 14 14 Some unchanged lines12 + 15 - And here is the finish with way more than 80 characters and I'm very curious how this is going to be displayed in split view diff. + 15 15 This line is unchanged + 16 + This line has been added + 16 16 Some unchanged lines13 + 17 17 Some unchanged lines14 + 18 18 Some unchanged lines15 +[...] +[...] + 21 21 Some unchanged lines18 + 22 22 Some unchanged lines19 + 23 23 Some unchanged lines20 + 24 - <> <> + 24 + <> <> + 25 25 Some unchanged lines21 + 26 26 Some unchanged lines22 + 27 27 Some unchanged lines23 +[...] \ No newline at end of file diff --git a/snapshot-tests-dependencies/pom.xml b/snapshot-tests-dependencies/pom.xml index c363bcde..0f3ea114 100644 --- a/snapshot-tests-dependencies/pom.xml +++ b/snapshot-tests-dependencies/pom.xml @@ -37,6 +37,12 @@ import + + io.github.java-diff-utils + java-diff-utils + ${version.java-diff-utils} + + org.apiguardian apiguardian-api @@ -69,7 +75,7 @@ assertj-core ${version.assertj} - + org.opentest4j opentest4j From f7acdb1238249a7e74129be5a1952d3bc90b595a Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Thu, 15 Dec 2022 18:04:06 +0100 Subject: [PATCH 17/27] Remove unused code --- .../test/snapshots/data/text/DiffListLookahead.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffListLookahead.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffListLookahead.java index 4edebfb4..84a9cfbe 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffListLookahead.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffListLookahead.java @@ -16,14 +16,4 @@ public static int indexOfNextNonEqual(List fullDiff, int startIndex) { return fullDiff.size(); } - public static int lookBehind(List fullDiff, int startIndex) { - int count = 0; - for (int i = startIndex; i >= 0; --i) { - if (fullDiff.get(i).getTag() != Tag.EQUAL) { - return count; - } - count++; - } - return count; - } } From 78a45d7294ac28b350fd7b1ee546f77e0b850829 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Thu, 15 Dec 2022 18:10:23 +0100 Subject: [PATCH 18/27] Minor improvements --- .../test/snapshots/data/text/TextDiff.java | 18 ++++++++++++------ .../text/TextDiffStructuralAssertions.java | 2 +- .../data/text/UnifiedDiffRenderer.java | 8 -------- .../test/snapshots/impl/SnapshotTestImpl.java | 2 +- .../test/snapshots/data/text/TextDiffTest.java | 14 +++++++------- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java index 482e9311..740ee640 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java @@ -116,7 +116,7 @@ private DiffRowGenerator buildDiffRowGenerator() { } } - private boolean hasLinebreakChange() { + private boolean hasLinebreakDifference() { return expectedLineSeparator != actualLineSeparator && !settings.ignoreWhitespaces; } @@ -124,24 +124,30 @@ private boolean hasTextDifference() { return diffRows.stream().map(DiffRow::getTag).anyMatch(tag -> tag != Tag.EQUAL); } - public boolean changesDetected() { - return hasLinebreakChange() || hasTextDifference(); + public boolean differencesDetected() { + return hasLinebreakDifference() || hasTextDifference(); } @Override public String toString() { final StringBuilder result = new StringBuilder(); - if (hasLinebreakChange()) { + final boolean hasTextDifference = hasTextDifference(); + final boolean hasLinebreakDifference = hasLinebreakDifference(); + + if (hasLinebreakDifference) { result.append("Strings differ in linebreaks. Expected: '") .append(expectedLineSeparator.displayName()) .append("', Actual encountered: '").append(actualLineSeparator.displayName()).append("'"); - if (hasTextDifference()) { + if (hasTextDifference) { result.append(LineSeparator.SYSTEM) .append(LineSeparator.SYSTEM); } } - result.append(settings.diffRenderer.renderDiff(diffRows, settings.contextLines)); + + if (hasTextDifference) { + result.append(settings.diffRenderer.renderDiff(diffRows, settings.contextLines)); + } return result.toString(); } } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiffStructuralAssertions.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiffStructuralAssertions.java index 359bd5cd..6aa9c59d 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiffStructuralAssertions.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiffStructuralAssertions.java @@ -15,7 +15,7 @@ public TextDiffStructuralAssertions(Settings settings) { @Override public void assertEquals(String storedSnapshot, String serializedActual) throws AssertionError, SnapshotException { final TextDiff textDiff = TextDiff.compare(settings, storedSnapshot, serializedActual); - if (textDiff.changesDetected()) { + if (textDiff.differencesDetected()) { throw new TextDiffAssertionError("Stored snapshot doesn't match actual result.", textDiff); } } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java index 6f878b4e..b9a5e587 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java @@ -3,7 +3,6 @@ import java.util.List; import com.github.difflib.text.DiffRow; -import com.github.difflib.text.DiffRow.Tag; final class UnifiedDiffRenderer implements DiffRenderer { @@ -12,15 +11,8 @@ private String padLeft(String text, int targetWidth) { return " ".repeat(missingSpaces) + text; } - private boolean equalOnly(List rows) { - return rows.stream().map(DiffRow::getTag).allMatch(Tag.EQUAL::equals); - } - @Override public String renderDiff(List rows, int contextLines) { - if (equalOnly(rows)) { - return ""; - } final int rowCountEstimate = rows.size(); final int rowNumberWidth = Math.max(2, (int) Math.log10(rowCountEstimate) + 1) + 1; diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java index bb4153c7..63248f2e 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/SnapshotTestImpl.java @@ -308,7 +308,7 @@ private AssertionError toDiffableAssertionError(AssertionError original, String .append(System.lineSeparator()); final TextDiff testDiff = determineDiff(original, storedSnapshot, serializedActual); - if (testDiff.changesDetected()) { + if (testDiff.differencesDetected()) { assertionMessage .append(System.lineSeparator()) .append("Full unified diff of actual result and stored snapshot:") diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java index 8c32c3a2..df957c1e 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java @@ -30,21 +30,21 @@ class WithIgnoreWhitespaces { @Test void testSingleLineRemoved() throws Exception { final TextDiff textDiff = TextDiff.compare(settings, "Just a single line", ""); - assertThat(textDiff.changesDetected()).isTrue(); + assertThat(textDiff.differencesDetected()).isTrue(); assertThat(textDiff.toString()).isEqualTo(" 1 - Just a single line"); } @Test void testSingleLineAdded() throws Exception { final TextDiff textDiff = TextDiff.compare(settings, "", "Just a single line"); - assertThat(textDiff.changesDetected()).isTrue(); + assertThat(textDiff.differencesDetected()).isTrue(); assertThat(textDiff.toString()).isEqualTo(" 1 + Just a single line"); } @Test void testLinebreakChangeIgnoreWhitespaces() throws Exception { final TextDiff textDiff = TextDiff.compare(settings, "line1\nline2", "line1\r\nline2"); - assertThat(textDiff.changesDetected()).isFalse(); + assertThat(textDiff.differencesDetected()).isFalse(); assertThat(textDiff.toString()).isEqualTo(""); } } @@ -58,14 +58,14 @@ class WithObeyWhitespaces { @Test void testSingleLineRemoved() throws Exception { final TextDiff textDiff = TextDiff.compare(settings, "Just a single line", ""); - assertThat(textDiff.changesDetected()).isTrue(); + assertThat(textDiff.differencesDetected()).isTrue(); assertThat(textDiff.toString()).isEqualTo(" 1 - Just a single line"); } @Test void testSingleLineAdded() throws Exception { final TextDiff textDiff = TextDiff.compare(settings, "", "Just a single line"); - assertThat(textDiff.changesDetected()).isTrue(); + assertThat(textDiff.differencesDetected()).isTrue(); assertThat(textDiff.toString()).isEqualTo(" 1 + Just a single line"); } @@ -73,7 +73,7 @@ void testSingleLineAdded() throws Exception { void testWhitespaceChangeWithinSingleLine() throws Exception { final TextDiff textDiff = TextDiff.compare(settings, "Just a single line", "Just a single line"); - assertThat(textDiff.changesDetected()).isTrue(); + assertThat(textDiff.differencesDetected()).isTrue(); assertThat(textDiff.toString()).isEqualTo(LineSeparator.SYSTEM.convert("" + " 1 - Just a single<< >>line\n" + " 1 + Just a single<< >>line")); @@ -83,7 +83,7 @@ void testWhitespaceChangeWithinSingleLine() throws Exception { void testLinebreakChangeObeyWhitespaces() throws Exception { final TextDiff textDiff = TextDiff.compare(settings, "line1\nline2", "line1\r\nline2"); - assertThat(textDiff.changesDetected()).isTrue(); + assertThat(textDiff.differencesDetected()).isTrue(); assertThat(textDiff.toString()) .isEqualTo("Strings differ in linebreaks. Expected: 'LF(\\n)', Actual encountered: 'CRLF(\\r\\n)'"); } From a2a9d6439bcea47c0ede2d9e4015ab1231c92aea Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Thu, 15 Dec 2022 18:33:24 +0100 Subject: [PATCH 19/27] Fix line numbering in unified diffs --- .../data/text/UnifiedDiffRenderer.java | 5 +++- .../snapshots/data/text/TextDiffTest.java | 28 +++++++++++++++++++ .../testRenderDiff_0.snapshot | 24 ++++++++-------- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java index b9a5e587..d8590698 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java @@ -73,13 +73,16 @@ public String renderDiff(List rows, int contextLines) { .append(padLeft("" + expectedLine, rowNumberWidth)) .append(" ".repeat(rowNumberWidth)) .append(" - ").append(diffRow.getOldLine()); + + expectedLine++; break; case INSERT: indexOfLastDifference = i; b .append(" ".repeat(rowNumberWidth)) - .append(padLeft("" + expectedLine, rowNumberWidth)) + .append(padLeft("" + actualLine, rowNumberWidth)) .append(" + ").append(diffRow.getNewLine()); + actualLine++; break; default: diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java index df957c1e..9af52992 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java @@ -27,6 +27,34 @@ class WithIgnoreWhitespaces { settings.withIgnoreWhitespaces(true); } + @Test + void testMultipleLinesAdded() throws Exception { + final TextDiff textDiff = TextDiff.compare(settings, + join("line1", "line5"), + join("line1", "line2", "line3", "line4", "line5")); + assertThat(textDiff.differencesDetected()).isTrue(); + assertThat(textDiff.toString()).isEqualTo(LineSeparator.SYSTEM.convert("" + + " 1 1 line1\n" + + " 2 + line2\n" + + " 3 + line3\n" + + " 4 + line4\n" + + " 2 5 line5")); + } + + @Test + void testMultipleLinesDeleted() throws Exception { + final TextDiff textDiff = TextDiff.compare(settings, + join("line1", "line2", "line3", "line4", "line5"), + join("line1", "line5")); + assertThat(textDiff.differencesDetected()).isTrue(); + assertThat(textDiff.toString()).isEqualTo(LineSeparator.SYSTEM.convert("" + + " 1 1 line1\n" + + " 2 - line2\n" + + " 3 - line3\n" + + " 4 - line4\n" + + " 5 2 line5")); + } + @Test void testSingleLineRemoved() throws Exception { final TextDiff textDiff = TextDiff.compare(settings, "Just a single line", ""); diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/data/text/TextDiffTest_snapshots/testRenderDiff_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/data/text/TextDiffTest_snapshots/testRenderDiff_0.snapshot index e838f86e..79574a0e 100644 --- a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/data/text/TextDiffTest_snapshots/testRenderDiff_0.snapshot +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/data/text/TextDiffTest_snapshots/testRenderDiff_0.snapshot @@ -16,19 +16,19 @@ test-method: testRenderDiff 13 13 Some unchanged lines11 14 14 Some unchanged lines12 15 - And here is the finish with way more than 80 characters and I'm very curious how this is going to be displayed in split view diff. - 15 15 This line is unchanged + 16 15 This line is unchanged 16 + This line has been added - 16 16 Some unchanged lines13 - 17 17 Some unchanged lines14 - 18 18 Some unchanged lines15 + 17 17 Some unchanged lines13 + 18 18 Some unchanged lines14 + 19 19 Some unchanged lines15 [...] [...] - 21 21 Some unchanged lines18 - 22 22 Some unchanged lines19 - 23 23 Some unchanged lines20 - 24 - <> <> - 24 + <> <> - 25 25 Some unchanged lines21 - 26 26 Some unchanged lines22 - 27 27 Some unchanged lines23 + 22 22 Some unchanged lines18 + 23 23 Some unchanged lines19 + 24 24 Some unchanged lines20 + 25 - <> <> + 25 + <> <> + 26 26 Some unchanged lines21 + 27 27 Some unchanged lines22 + 28 28 Some unchanged lines23 [...] \ No newline at end of file From 614648b6674bbcb3d6a8d4ebfd608928cf848dc4 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Fri, 16 Dec 2022 08:24:35 +0100 Subject: [PATCH 20/27] Minor improvements --- .../java/de/skuzzle/test/snapshots/SnapshotDirectory.java | 2 +- .../de/skuzzle/test/snapshots/SnapshotTestOptions.java | 8 +++++++- .../test/snapshots/data/text/DiffListLookahead.java | 5 ++++- .../de/skuzzle/test/snapshots/data/text/TextDiff.java | 6 ++++-- .../test/snapshots/data/text/UnifiedDiffRenderer.java | 1 + .../test/snapshots/impl/DefaultSnapshotConfiguration.java | 2 +- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java index 72d6aa07..e1954413 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java @@ -18,7 +18,7 @@ * single test method. *

* If you don't customize the snapshot directory at all, then snapshots will by default be - * stored in a directory structure according to the package name if the test class. If you + * stored in a directory structure according to the package name of the test class. If you * already specified a custom directory using the DSL (via * {@link Snapshot#in(java.nio.file.Path)}) then this annotation will have no effect for * that particular assertion. diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java index 4ad4f3ea..e8c23d84 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java @@ -26,6 +26,12 @@ @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface SnapshotTestOptions { + + /** + * DEfault number of context lines that are displayed in diffs. + */ + public static final int DEFAULT_CONTEXT_LINES = 5; + /** * Defines the number of context lines that are printed around a comparison failure. * Note that this setting only applies to unified diffs created for structural @@ -37,7 +43,7 @@ * @return The number of context lines to print in unified diffs within our structural * assertion failures. */ - int textDiffContextLines() default 5; + int textDiffContextLines() default DEFAULT_CONTEXT_LINES; /** * Whether to always persist the latest actual test result in a parallel file next to diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffListLookahead.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffListLookahead.java index 84a9cfbe..d2c811ce 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffListLookahead.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/DiffListLookahead.java @@ -5,7 +5,7 @@ import com.github.difflib.text.DiffRow; import com.github.difflib.text.DiffRow.Tag; -class DiffListLookahead { +final class DiffListLookahead { public static int indexOfNextNonEqual(List fullDiff, int startIndex) { for (int i = startIndex; i < fullDiff.size(); ++i) { @@ -16,4 +16,7 @@ public static int indexOfNextNonEqual(List fullDiff, int startIndex) { return fullDiff.size(); } + private DiffListLookahead() { + // hidden + } } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java index 740ee640..088b2653 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextDiff.java @@ -13,10 +13,11 @@ import com.github.difflib.text.DiffRow.Tag; import com.github.difflib.text.DiffRowGenerator; +import de.skuzzle.test.snapshots.SnapshotTestOptions; import de.skuzzle.test.snapshots.validation.Arguments; /** - * Creates a diff of 2 Strings. For internal use only. Public API is encapsulated in + * Creates a diff of 2 Strings. For internal use only. Public API is provided by * {@link TextSnapshot}. * * @author Simon Taddiken @@ -51,9 +52,10 @@ public static TextDiff compare(Settings settings, String expected, String actual return new TextDiff(settings, diffRows, expectedLineSeparator, actualLineSeparator); } + @API(status = Status.INTERNAL, since = "1.7.0") public static final class Settings { private boolean ignoreWhitespaces = false; - private int contextLines = Integer.MAX_VALUE; + private int contextLines = SnapshotTestOptions.DEFAULT_CONTEXT_LINES; private String inlineOpeningChangeMarker = "<<"; private String inlineClosingChangeMarker = ">>"; private DiffRenderer diffRenderer = new UnifiedDiffRenderer(); diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java index d8590698..474d1e90 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/UnifiedDiffRenderer.java @@ -46,6 +46,7 @@ public String renderDiff(List rows, int contextLines) { } else { expectedLine++; actualLine++; + // continue here to not print extra linebreaks for skipped lines continue; } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java index 42bc6ac7..f127e641 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/impl/DefaultSnapshotConfiguration.java @@ -26,7 +26,7 @@ final class DefaultSnapshotConfiguration implements SnapshotConfiguration { // default number of context lines that will be printed around changes in huge unified // diffs - private static final int DEFAULT_CONTEXT_LINES = 5; + private static final int DEFAULT_CONTEXT_LINES = SnapshotTestOptions.DEFAULT_CONTEXT_LINES; private final Class testClass; From abc83c0d8eb85d20d947cc27b08bf722418cce3c Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Fri, 16 Dec 2022 14:23:22 +0100 Subject: [PATCH 21/27] Add public API to change diff renderer --- .../data/text/SplitDiffRenderer.java | 59 ++++++++++++--- .../snapshots/data/text/TextSnapshot.java | 39 ++++++++++ .../skuzzle/test/snapshots/SnapshotsTest.java | 12 ++++ .../snapshots/data/text/TextDiffTest.java | 72 +++++++++++++++++++ .../testCustomizeTextSnapshot_0.snapshot | 15 ++++ .../testRenderDiffSplit_0.snapshot | 32 +++++++++ 6 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsTest_snapshots/testCustomizeTextSnapshot_0.snapshot create mode 100644 snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/data/text/TextDiffTest_snapshots/testRenderDiffSplit_0.snapshot diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/SplitDiffRenderer.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/SplitDiffRenderer.java index 305ca64d..84c68697 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/SplitDiffRenderer.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/SplitDiffRenderer.java @@ -1,15 +1,15 @@ package de.skuzzle.test.snapshots.data.text; -import java.util.Iterator; import java.util.List; import com.github.difflib.text.DiffRow; final class SplitDiffRenderer implements DiffRenderer { + private static final String OPERATION_CHANGE = "!"; private static final String OPERATION_ADD = "+"; private static final String OPERATION_DELETE = "-"; - private static final String OPERATION_CHANGE_EQUAL = " "; + private static final String OPERATION_EQUAL = " "; private String padLeft(String text, int targetWidth) { final int missingSpaces = targetWidth - text.length(); @@ -38,42 +38,79 @@ private void appendRow(StringBuilder b, int columnWidth, String paddedExpectedLi public String renderDiff(List rows, int contextLines) { final int columnWidth = rows.stream().map(DiffRow::getOldLine).mapToInt(String::length).max().orElse(80); final int rowCountEstimate = rows.size(); - final int rowNumberWidth = Math.max(2, (int) Math.log10(rowCountEstimate) + 1); + final int rowNumberWidth = Math.max(2, (int) Math.log10(rowCountEstimate) + 1) + 1; int expectedLine = 1; int actualLine = 1; final StringBuilder b = new StringBuilder(); - for (final Iterator iterator = rows.iterator(); iterator.hasNext();) { - final DiffRow diffRow = iterator.next(); + + int indexOfLastDifference = -1; + + for (int i = 0; i < rows.size(); i++) { + final DiffRow diffRow = rows.get(i); switch (diffRow.getTag()) { case CHANGE: - case EQUAL: + indexOfLastDifference = i; appendRow(b, columnWidth, padLeft("" + expectedLine, rowNumberWidth), - OPERATION_CHANGE_EQUAL, + OPERATION_CHANGE, diffRow.getOldLine(), padLeft("" + actualLine, rowNumberWidth), - OPERATION_CHANGE_EQUAL, diffRow.getNewLine()); + OPERATION_CHANGE, diffRow.getNewLine()); + + expectedLine++; + actualLine++; + break; + case EQUAL: + final int indexOfNextDifference = DiffListLookahead.indexOfNextNonEqual(rows, i); + + final int distanceToNextDifference = indexOfNextDifference == rows.size() + ? Integer.MAX_VALUE + : indexOfNextDifference - i - 1; + final int distanceToPrevDifference = indexOfLastDifference == -1 + ? Integer.MAX_VALUE + : i - indexOfLastDifference - 1; + + if (distanceToNextDifference < contextLines || distanceToPrevDifference < contextLines) { + appendRow(b, columnWidth, + padLeft("" + expectedLine, rowNumberWidth), + OPERATION_EQUAL, + diffRow.getOldLine(), + padLeft("" + actualLine, rowNumberWidth), + OPERATION_EQUAL, diffRow.getNewLine()); + + } else if (distanceToNextDifference == contextLines || distanceToPrevDifference == contextLines) { + b.append("[...]"); + } else { + expectedLine++; + actualLine++; + // continue here to not print extra linebreaks for skipped lines + continue; + } expectedLine++; actualLine++; break; case DELETE: + indexOfLastDifference = i; + appendRow(b, columnWidth, padLeft("" + expectedLine, rowNumberWidth), OPERATION_DELETE, diffRow.getOldLine(), " ".repeat(rowNumberWidth), - OPERATION_CHANGE_EQUAL, diffRow.getNewLine()); + OPERATION_EQUAL, diffRow.getNewLine()); expectedLine++; break; case INSERT: + indexOfLastDifference = i; + appendRow(b, columnWidth, " ".repeat(rowNumberWidth), - OPERATION_CHANGE_EQUAL, + OPERATION_EQUAL, diffRow.getOldLine(), padLeft("" + actualLine, rowNumberWidth), OPERATION_ADD, diffRow.getNewLine()); @@ -85,7 +122,7 @@ public String renderDiff(List rows, int contextLines) { } - if (iterator.hasNext()) { + if (i < rows.size() - 1) { b.append(LineSeparator.SYSTEM); } } diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextSnapshot.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextSnapshot.java index 69080c32..6e02b628 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextSnapshot.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextSnapshot.java @@ -83,6 +83,45 @@ public TextSnapshot withContextLines(int contextLines) { return this; } + /** + * Specify the format of how diffs are rendered within our assertion failure messages. + * + * @param diffFormat The diff format to use. + * @return This instance. + * @since 1.7.0 + */ + @API(status = Status.EXPERIMENTAL, since = "1.7.0") + public TextSnapshot withDiffFormat(DiffFormat diffFormat) { + settings.withDiffRenderer(diffFormat.renderer); + return this; + } + + /** + * Format in which diffs will be rendered within assertion failure messages. + * + * @author Simon Taddiken + * @since 1.7.0 + */ + @API(status = Status.EXPERIMENTAL, since = "1.7.0") + public enum DiffFormat { + /** + * Render diffs in unified format, where changes are displayed line by line and + * lines from expected and actual results are printed interleaved. + */ + UNIFIED(new UnifiedDiffRenderer()), + /** + * Render diff in split view format, where the lines from the expected and actual + * result will be printed side by side. + */ + SPLIT(new SplitDiffRenderer()); + + private final DiffRenderer renderer; + + private DiffFormat(DiffRenderer renderer) { + this.renderer = renderer; + } + } + @Override public StructuredData build() { final StructuralAssertions structuralAssertions = new TextDiffStructuralAssertions(settings); diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java index 7caf7440..6265378a 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/SnapshotsTest.java @@ -10,6 +10,8 @@ import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; import de.skuzzle.test.snapshots.SnapshotTestResult.SnapshotStatus; +import de.skuzzle.test.snapshots.data.text.TextSnapshot; +import de.skuzzle.test.snapshots.data.text.TextSnapshot.DiffFormat; import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; @EnableSnapshotTests @@ -97,6 +99,16 @@ void testWithExplicitSnapshotName(Snapshot snapshot) throws Exception { snapshot.named("phil").assertThat(phil).asText().matchesSnapshotText(); } + @Test + void testCustomizeTextSnapshot(Snapshot snapshot) throws Exception { + final Person simon = determinePerson(); + + snapshot.assertThat(simon).as(TextSnapshot.text() + .withContextLines(10) + .withDiffFormat(DiffFormat.SPLIT) + .withIgnoreWhitespaces(true)).matchesSnapshotText(); + } + @ParameterizedTest @ValueSource(strings = { "string1", "string2" }) void testParameterized(String param, Snapshot snapshot) { diff --git a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java index 9af52992..c78da3f7 100644 --- a/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java +++ b/snapshot-tests-core/src/test/java/de/skuzzle/test/snapshots/data/text/TextDiffTest.java @@ -186,4 +186,76 @@ void testRenderDiff(Snapshot snapshot) throws Exception { System.out.println(textDiff); snapshot.assertThat(textDiff).asText().matchesSnapshotText(); } + + @Test + void testRenderDiffSplit(Snapshot snapshot) throws Exception { + final String expected = join( + "Some unchanged lines1", + "Some unchanged lines2", + "Some unchanged lines3", + "Some unchanged lines4", + "Some unchanged lines5", + "Some unchanged lines6", + "Some unchanged lines7", + "Some unchanged lines8", + "This is a test senctence.", + "This is the second line.", + "Some unchanged lines9", + "Some unchanged lines10", + "Some unchanged lines11", + "Some unchanged lines12", + "And here is the finish with way more than 80 characters and I'm very curious how this is going to be displayed in split view diff.", + "This line is unchanged", + "Some unchanged lines13", + "Some unchanged lines14", + "Some unchanged lines15", + "Some unchanged lines16", + "Some unchanged lines17", + "Some unchanged lines18", + "Some unchanged lines19", + "Some unchanged lines20", + "Another difference", + "Some unchanged lines21", + "Some unchanged lines22", + "Some unchanged lines23", + "Some unchanged lines24"); + + final String actual = join( + "Some unchanged lines1", + "Some unchanged lines2", + "Some unchanged lines3", + "Some unchanged lines4", + "Some unchanged lines5", + "Some unchanged lines6", + "Some unchanged lines7", + "Some unchanged lines8", + "This is a test for diffutils.", + "This is the second line.", + "Some unchanged lines9", + "Some unchanged lines10", + "Some unchanged lines11", + "Some unchanged lines12", + "This line is unchanged", + "This line has been added", + "Some unchanged lines13", + "Some unchanged lines14", + "Some unchanged lines15", + "Some unchanged lines16", + "Some unchanged lines17", + "Some unchanged lines18", + "Some unchanged lines19", + "Some unchanged lines20", + "This has changed", + "Some unchanged lines21", + "Some unchanged lines22", + "Some unchanged lines23", + "Some unchanged lines24"); + + final TextDiff textDiff = TextDiff.compare(Settings.defaultSettings() + .withContextLines(3) + .withDiffRenderer(new SplitDiffRenderer()), expected, actual); + + System.out.println(textDiff); + snapshot.assertThat(textDiff).asText().matchesSnapshotText(); + } } diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsTest_snapshots/testCustomizeTextSnapshot_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsTest_snapshots/testCustomizeTextSnapshot_0.snapshot new file mode 100644 index 00000000..70b2c0ff --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/SnapshotsTest_snapshots/testCustomizeTextSnapshot_0.snapshot @@ -0,0 +1,15 @@ +dynamic-directory: false +snapshot-name: testCustomizeTextSnapshot_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.SnapshotsTest +test-method: testCustomizeTextSnapshot + +Name: Simon +Surname: Taddiken +Birthdate: 1777-01-12 +Address: Street: Gibtsnicht-Straße +Number: 1337 +Zip: 4711 +City: Bielefeld +Country: Germany + diff --git a/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/data/text/TextDiffTest_snapshots/testRenderDiffSplit_0.snapshot b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/data/text/TextDiffTest_snapshots/testRenderDiffSplit_0.snapshot new file mode 100644 index 00000000..fcdcadc9 --- /dev/null +++ b/snapshot-tests-core/src/test/resources/de/skuzzle/test/snapshots/data/text/TextDiffTest_snapshots/testRenderDiffSplit_0.snapshot @@ -0,0 +1,32 @@ +dynamic-directory: false +snapshot-name: testRenderDiffSplit_0 +snapshot-number: 0 +test-class: de.skuzzle.test.snapshots.data.text.TextDiffTest +test-method: testRenderDiffSplit + +[...] + 6 Some unchanged lines6 | 6 Some unchanged lines6 + 7 Some unchanged lines7 | 7 Some unchanged lines7 + 8 Some unchanged lines8 | 8 Some unchanged lines8 + 9 ! This is a test <>. | 9 ! This is a test <>. + 10 This is the second line. | 10 This is the second line. + 11 Some unchanged lines9 | 11 Some unchanged lines9 + 12 Some unchanged lines10 | 12 Some unchanged lines10 + 13 Some unchanged lines11 | 13 Some unchanged lines11 + 14 Some unchanged lines12 | 14 Some unchanged lines12 + 15 - And here is the finish with way more than 80 characters and I'm very curious how this is going to be displayed in split view diff. | + 16 This line is unchanged | 15 This line is unchanged + | 16 + This line has been added + 17 Some unchanged lines13 | 17 Some unchanged lines13 + 18 Some unchanged lines14 | 18 Some unchanged lines14 + 19 Some unchanged lines15 | 19 Some unchanged lines15 +[...] +[...] + 22 Some unchanged lines18 | 22 Some unchanged lines18 + 23 Some unchanged lines19 | 23 Some unchanged lines19 + 24 Some unchanged lines20 | 24 Some unchanged lines20 + 25 ! <> <> | 25 ! <> <> + 26 Some unchanged lines21 | 26 Some unchanged lines21 + 27 Some unchanged lines22 | 27 Some unchanged lines22 + 28 Some unchanged lines23 | 28 Some unchanged lines23 +[...] \ No newline at end of file From bc7dab484585b5a2faced2389365c650a6fbead5 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Fri, 16 Dec 2022 14:43:50 +0100 Subject: [PATCH 22/27] Improve javadoc --- .../test/snapshots/SnapshotTestOptions.java | 6 ++--- .../snapshots/data/text/TextSnapshot.java | 22 ++++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java index e8c23d84..61759380 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotTestOptions.java @@ -28,15 +28,15 @@ public @interface SnapshotTestOptions { /** - * DEfault number of context lines that are displayed in diffs. + * Default number of context lines that are displayed in diffs. */ public static final int DEFAULT_CONTEXT_LINES = 5; /** * Defines the number of context lines that are printed around a comparison failure. * Note that this setting only applies to unified diffs created for structural - * comparisons. If you use text comparison, then you can control the amount of context - * lines using {@link TextSnapshot#withContextLines(int)}. + * comparisons. If you use text comparison, then you must control the amount of + * context lines using {@link TextSnapshot#withContextLines(int)}. *

* Defaults to 5. * diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextSnapshot.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextSnapshot.java index 6e02b628..fc7abc4e 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextSnapshot.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/TextSnapshot.java @@ -3,6 +3,7 @@ import org.apiguardian.api.API; import org.apiguardian.api.API.Status; +import de.skuzzle.test.snapshots.SnapshotTestOptions; import de.skuzzle.test.snapshots.StructuralAssertions; import de.skuzzle.test.snapshots.StructuredData; import de.skuzzle.test.snapshots.StructuredDataProvider; @@ -15,7 +16,12 @@ *

* When rendering unified diffs as String, changes in line separators are not marked at * every occurrence. Instead, detected changes in line separators will result in a single - * informative message at the beginning of the diff. + * informative message at the beginning of the diff, but only if set + * {@link #withIgnoreWhitespaces(boolean)} to false. + *

+ * By default, rendered diffs will display 5 lines of context around a detected change. + * This can be changed using {@link #withContextLines(int)}. Note that + * {@link SnapshotTestOptions#textDiffContextLines()} does not apply to text snapshots. * * @author Simon Taddiken */ @@ -26,8 +32,7 @@ public final class TextSnapshot implements StructuredDataProvider { /** * Take Snapshots using {@link Object#toString()} and compare the results using a - * generic String diff algorithm. Comparison ignores whitespace changes of any kind - * and does not limit the size of the unified diff. + * generic String diff algorithm. Comparison ignores whitespace changes of any kind. *

* You can create a more customized text comparison by using {@link #text()} and the * several customization options. @@ -36,6 +41,7 @@ public final class TextSnapshot implements StructuredDataProvider { */ public static final StructuredData text = text() .withIgnoreWhitespaces(true) + .withContextLines(Integer.MAX_VALUE) .build(); private TextSnapshot() { @@ -56,6 +62,8 @@ public static TextSnapshot text() { /** * Allows to customize the whitespace comparison behavior. + *

+ * Defaults to false. * * @param ignoreWhitespaces Whether to ignore whitespaces during comparison. * @return This instance. @@ -68,9 +76,9 @@ public TextSnapshot withIgnoreWhitespaces(boolean ignoreWhitespaces) { /** * Configures the amount of contextual lines that are printed around a detected - * change. Per default, all lines of the full diff will be printed. If you have huge - * diffs with only little changes, it might be beneficial to reduce this value to a - * low value like 5 or 10. + * change. Per default, all lines of the full diff will be printed. + *

+ * Defaults to 5 * * @param contextLines The amount of lines to print around a detected change in the * unified diffs. @@ -85,6 +93,8 @@ public TextSnapshot withContextLines(int contextLines) { /** * Specify the format of how diffs are rendered within our assertion failure messages. + *

+ * Defaults to {@link DiffFormat#UNIFIED} * * @param diffFormat The diff format to use. * @return This instance. From 9c2eaa31d3feb683f11fe0b1d39401d14e2c192f Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Mon, 19 Dec 2022 08:16:00 +0100 Subject: [PATCH 23/27] Improve javadoc --- .../main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java | 2 ++ .../java/de/skuzzle/test/snapshots/data/text/LineSeparator.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java index e1954413..98550af1 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java @@ -10,6 +10,7 @@ import org.apiguardian.api.API; import org.apiguardian.api.API.Status; +import de.skuzzle.test.snapshots.SnapshotDsl.ChooseDirectory; import de.skuzzle.test.snapshots.SnapshotDsl.Snapshot; /** @@ -32,6 +33,7 @@ * @author Simon Taddiken * @since 1.7.0 * @see SnapshotDirectoryStrategy + * @See {@link ChooseDirectory} */ @Retention(RUNTIME) @Target({ TYPE, METHOD }) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/LineSeparator.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/LineSeparator.java index 593b9a5e..1f0108a0 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/LineSeparator.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/data/text/LineSeparator.java @@ -3,7 +3,7 @@ import java.util.stream.Collectors; /** - * Used to represent a file's line separator. + * Used to represent and detect a file's line separator character(s). * * @since 1.5.0 * @author Simon Taddiken From 25757a17d69605a9405944fc6802bfd67e4db537 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Mon, 19 Dec 2022 10:40:38 +0100 Subject: [PATCH 24/27] Fix javadoc --- .../main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java index 98550af1..8c7c9ff3 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java @@ -33,7 +33,7 @@ * @author Simon Taddiken * @since 1.7.0 * @see SnapshotDirectoryStrategy - * @See {@link ChooseDirectory} + * @see {@link ChooseDirectory} */ @Retention(RUNTIME) @Target({ TYPE, METHOD }) From 9a84df664722ed3854457211dc95c4a8b367f986 Mon Sep 17 00:00:00 2001 From: Simon Taddiken Date: Mon, 19 Dec 2022 10:45:52 +0100 Subject: [PATCH 25/27] Really fix JavaDoc --- .../main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java index 8c7c9ff3..22b2dec7 100644 --- a/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java +++ b/snapshot-tests-core/src/main/java/de/skuzzle/test/snapshots/SnapshotDirectory.java @@ -33,7 +33,7 @@ * @author Simon Taddiken * @since 1.7.0 * @see SnapshotDirectoryStrategy - * @see {@link ChooseDirectory} + * @see ChooseDirectory */ @Retention(RUNTIME) @Target({ TYPE, METHOD }) From 989cf92ee36d5c9293e08aa10dbe5f47b6f8ff66 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Mon, 19 Dec 2022 09:48:40 +0000 Subject: [PATCH 26/27] Update versions for release --- pom.xml | 2 +- snapshot-tests-bom/pom.xml | 2 +- snapshot-tests-common/pom.xml | 2 +- snapshot-tests-core/pom.xml | 2 +- snapshot-tests-dependencies/pom.xml | 2 +- snapshot-tests-directory-params/pom.xml | 2 +- snapshot-tests-html/pom.xml | 2 +- snapshot-tests-jackson/pom.xml | 2 +- snapshot-tests-jaxb-jakarta/pom.xml | 2 +- snapshot-tests-jaxb/pom.xml | 2 +- snapshot-tests-normalize/pom.xml | 2 +- snapshot-tests-release-test/pom.xml | 2 +- snapshot-tests-xmlunit/pom.xml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index a14ae057..fae65f71 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 pom Snapshot Tests Parent diff --git a/snapshot-tests-bom/pom.xml b/snapshot-tests-bom/pom.xml index 30e4b141..0e9236d8 100644 --- a/snapshot-tests-bom/pom.xml +++ b/snapshot-tests-bom/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 snapshot-tests-bom diff --git a/snapshot-tests-common/pom.xml b/snapshot-tests-common/pom.xml index 2681d38e..ba26fe60 100644 --- a/snapshot-tests-common/pom.xml +++ b/snapshot-tests-common/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 snapshot-tests-common diff --git a/snapshot-tests-core/pom.xml b/snapshot-tests-core/pom.xml index 2759f97a..0254f4cf 100644 --- a/snapshot-tests-core/pom.xml +++ b/snapshot-tests-core/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 snapshot-tests-core diff --git a/snapshot-tests-dependencies/pom.xml b/snapshot-tests-dependencies/pom.xml index 0f3ea114..2984b51c 100644 --- a/snapshot-tests-dependencies/pom.xml +++ b/snapshot-tests-dependencies/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 snapshot-tests-dependencies diff --git a/snapshot-tests-directory-params/pom.xml b/snapshot-tests-directory-params/pom.xml index 8433c997..28367cf4 100644 --- a/snapshot-tests-directory-params/pom.xml +++ b/snapshot-tests-directory-params/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 snapshot-tests-directory-params diff --git a/snapshot-tests-html/pom.xml b/snapshot-tests-html/pom.xml index 1ba9ce69..761172a9 100644 --- a/snapshot-tests-html/pom.xml +++ b/snapshot-tests-html/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 snapshot-tests-html diff --git a/snapshot-tests-jackson/pom.xml b/snapshot-tests-jackson/pom.xml index a0b3caab..70e25806 100644 --- a/snapshot-tests-jackson/pom.xml +++ b/snapshot-tests-jackson/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 snapshot-tests-jackson diff --git a/snapshot-tests-jaxb-jakarta/pom.xml b/snapshot-tests-jaxb-jakarta/pom.xml index f9974008..d0bbfafb 100644 --- a/snapshot-tests-jaxb-jakarta/pom.xml +++ b/snapshot-tests-jaxb-jakarta/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 snapshot-tests-jaxb-jakarta diff --git a/snapshot-tests-jaxb/pom.xml b/snapshot-tests-jaxb/pom.xml index 8f68673f..08e8a52d 100644 --- a/snapshot-tests-jaxb/pom.xml +++ b/snapshot-tests-jaxb/pom.xml @@ -4,7 +4,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 snapshot-tests-jaxb diff --git a/snapshot-tests-normalize/pom.xml b/snapshot-tests-normalize/pom.xml index 3e63f9bc..68a3f997 100644 --- a/snapshot-tests-normalize/pom.xml +++ b/snapshot-tests-normalize/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 snapshot-tests-normalize diff --git a/snapshot-tests-release-test/pom.xml b/snapshot-tests-release-test/pom.xml index 1efea91c..7d566852 100644 --- a/snapshot-tests-release-test/pom.xml +++ b/snapshot-tests-release-test/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 snapshot-tests-release-test diff --git a/snapshot-tests-xmlunit/pom.xml b/snapshot-tests-xmlunit/pom.xml index f5b6b371..017bebd5 100644 --- a/snapshot-tests-xmlunit/pom.xml +++ b/snapshot-tests-xmlunit/pom.xml @@ -5,7 +5,7 @@ de.skuzzle.test snapshot-tests-parent - 1.7.0-SNAPSHOT + 1.7.0 snapshot-tests-xmlunit From 8cf8cab0fd013380f1ccdb1c86315d9bee1c9379 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Mon, 19 Dec 2022 09:50:37 +0000 Subject: [PATCH 27/27] Update README and RELEASE_NOTES --- README.md | 14 +++++------ RELEASE_NOTES.md | 62 ++++++++++++++++++++++++------------------------ 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index b4366313..7b2ea818 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-bom/1.7.0-SNAPSHOT/jar) -[![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-core/1.7.0-SNAPSHOT) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-bom/1.7.0/jar) +[![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-core/1.7.0) [![Coverage Status](https://coveralls.io/repos/github/skuzzle/snapshot-tests/badge.svg?branch=main)](https://coveralls.io/github/skuzzle/snapshot-tests?branch=main) [![Twitter Follow](https://img.shields.io/twitter/follow/skuzzleOSS.svg?style=social)](https://twitter.com/skuzzleOSS) @@ -13,17 +13,17 @@ serialized version of the object during the first test execution and during subs actual object against the stored snapshot. Supported snapshot formats: -- [x] generic plain text via [snapshot-tests-core](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-core/1.7.0-SNAPSHOT/jar) -- [x] Json via [snapshot-tests-jackson](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jackson/1.7.0-SNAPSHOT/jar) -- [x] XML via [snapshot-tests-jaxb](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.7.0-SNAPSHOT/jar) xor [snapshot-tests-jaxb-jakarta](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb-jakarta/1.7.0-SNAPSHOT/jar) -- [x] HTML via [snapshot-tests-html](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-html/1.7.0-SNAPSHOT/jar) +- [x] generic plain text via [snapshot-tests-core](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-core/1.7.0/jar) +- [x] Json via [snapshot-tests-jackson](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jackson/1.7.0/jar) +- [x] XML via [snapshot-tests-jaxb](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.7.0/jar) xor [snapshot-tests-jaxb-jakarta](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb-jakarta/1.7.0/jar) +- [x] HTML via [snapshot-tests-html](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-html/1.7.0/jar) Read more about snapshot testing in this accompanying [blog post](https://simon.taddiken.net/the-case-for-snapshot-testing/). ### Latest Maven Central coordinates Please check out the GitHub release page to find Maven & Gradle coordinates for the latest -release [1.7.0-SNAPSHOT](https://github.com/skuzzle/snapshot-tests/releases/tag/v1.7.0-SNAPSHOT) +release [1.7.0](https://github.com/skuzzle/snapshot-tests/releases/tag/v1.7.0) ## Quick start _(assumes using `snapshot-tests-jackson` artifact)_ diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 71073987..367df54b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -23,146 +23,146 @@ Maven Central coordinates for this release: ## BOM Artifact Manages the versions of all modules in case you are using multiple in your project -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-bom/1.7.0-SNAPSHOT/jar) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-bom/1.7.0/jar) ```xml de.skuzzle.test snapshot-tests-bom - 1.7.0-SNAPSHOT + 1.7.0 pom import ``` ``` -testImplementation(platform("de.skuzzle.test:snapshot-tests-bom:1.7.0-SNAPSHOT")) +testImplementation(platform("de.skuzzle.test:snapshot-tests-bom:1.7.0")) ``` ## Artifacts If you only need text based snapshots: -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-core/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-core/1.7.0-SNAPSHOT) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-core/1.7.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-core/1.7.0) ```xml de.skuzzle.test snapshot-tests-core - 1.7.0-SNAPSHOT + 1.7.0 test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-core:1.7.0-SNAPSHOT' -testImplementation("de.skuzzle.test:snapshot-tests-core:1.7.0-SNAPSHOT") +testImplementation 'de.skuzzle.test:snapshot-tests-core:1.7.0' +testImplementation("de.skuzzle.test:snapshot-tests-core:1.7.0") ``` If you need json based snapshots (includes `-core`): -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jackson/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-jackson/1.7.0-SNAPSHOT) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jackson/1.7.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-jackson/1.7.0) ```xml de.skuzzle.test snapshot-tests-jackson - 1.7.0-SNAPSHOT + 1.7.0 test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-jackson:1.7.0-SNAPSHOT' -testImplementation("de.skuzzle.test:snapshot-tests-jackson:1.7.0-SNAPSHOT") +testImplementation 'de.skuzzle.test:snapshot-tests-jackson:1.7.0' +testImplementation("de.skuzzle.test:snapshot-tests-jackson:1.7.0") ``` If you need xml based snapshots using `javax.xml` legacy namespaces (includes `-core`): -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-jaxb/1.7.0-SNAPSHOT) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb/1.7.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-jaxb/1.7.0) ```xml de.skuzzle.test snapshot-tests-jaxb - 1.7.0-SNAPSHOT + 1.7.0 test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-jaxb:1.7.0-SNAPSHOT' -testImplementation("de.skuzzle.test:snapshot-tests-jaxb:1.7.0-SNAPSHOT") +testImplementation 'de.skuzzle.test:snapshot-tests-jaxb:1.7.0' +testImplementation("de.skuzzle.test:snapshot-tests-jaxb:1.7.0") ``` If you need xml based snapshots using new `jakarta.xml` namespaces (includes `-core`): -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb-jakarta/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-jaxb-jakarta/1.7.0-SNAPSHOT) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-jaxb-jakarta/1.7.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-jaxb-jakarta/1.7.0) ```xml de.skuzzle.test snapshot-tests-jaxb-jakarta - 1.7.0-SNAPSHOT + 1.7.0 test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-jaxb-jakarta:1.7.0-SNAPSHOT' -testImplementation("de.skuzzle.test:snapshot-tests-jaxb-jakarta:1.7.0-SNAPSHOT") +testImplementation 'de.skuzzle.test:snapshot-tests-jaxb-jakarta:1.7.0' +testImplementation("de.skuzzle.test:snapshot-tests-jaxb-jakarta:1.7.0") ``` If you need HTML based snapshots (includes `-core`): -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-html/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-html/1.7.0-SNAPSHOT) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-html/1.7.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-html/1.7.0) ```xml de.skuzzle.test snapshot-tests-html - 1.7.0-SNAPSHOT + 1.7.0 test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-html:1.7.0-SNAPSHOT' -testImplementation("de.skuzzle.test:snapshot-tests-html:1.7.0-SNAPSHOT") +testImplementation 'de.skuzzle.test:snapshot-tests-html:1.7.0' +testImplementation("de.skuzzle.test:snapshot-tests-html:1.7.0") ``` ## Experimental Directory Params -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-directory-params/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-directory-params/1.7.0-SNAPSHOT) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-directory-params/1.7.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-directory-params/1.7.0) ```xml de.skuzzle.test snapshot-tests-directory-params - 1.7.0-SNAPSHOT + 1.7.0 test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-directory-params:1.7.0-SNAPSHOT' -testImplementation("de.skuzzle.test:snapshot-tests-directory-params:1.7.0-SNAPSHOT") +testImplementation 'de.skuzzle.test:snapshot-tests-directory-params:1.7.0' +testImplementation("de.skuzzle.test:snapshot-tests-directory-params:1.7.0") ``` Object normalization -[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0-SNAPSHOT&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-normalize/1.7.0-SNAPSHOT/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0-SNAPSHOT&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-normalize/1.7.0-SNAPSHOT) +[![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=1.7.0&color=blue)](https://search.maven.org/artifact/de.skuzzle.test/snapshot-tests-normalize/1.7.0/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=1.7.0&color=orange)](http://www.javadoc.io/doc/de.skuzzle.test/snapshot-tests-normalize/1.7.0) ```xml de.skuzzle.test snapshot-tests-normalize - 1.7.0-SNAPSHOT + 1.7.0 test ``` ``` -testImplementation 'de.skuzzle.test:snapshot-tests-normalize:1.7.0-SNAPSHOT' -testImplementation("de.skuzzle.test:snapshot-tests-normalize:1.7.0-SNAPSHOT") +testImplementation 'de.skuzzle.test:snapshot-tests-normalize:1.7.0' +testImplementation("de.skuzzle.test:snapshot-tests-normalize:1.7.0") ``` \ No newline at end of file