diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 19ea5421..387fb751 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,14 +20,16 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - java_version: [ '8' ] os: [ ubuntu-latest, windows-latest, macOS-latest ] steps: - uses: actions/checkout@v2 - - name: Set up JDK ${{ matrix.java_version }} - uses: actions/setup-java@v1 + - name: Set up JDKs + uses: actions/setup-java@v5 with: - java-version: ${{ matrix.java_version }} + distribution: 'temurin' + java-version: | + 11 + 17 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle @@ -38,10 +40,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK 8 - uses: actions/setup-java@v1 + - name: Set up JDKs + uses: actions/setup-java@v5 with: - java-version: 8 + distribution: 'temurin' + java-version: | + 11 + 17 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Jar @@ -60,5 +65,4 @@ jobs: ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_KEY_PASSPHRASE }} MAVEN_CENTRAL_TOKEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_TOKEN_USERNAME }} MAVEN_CENTRAL_TOKEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_TOKEN_PASSWORD }} - run: ./gradlew signArchives uploadArchives -Pversion=$SNAPSHOT_VERSION -PossrhUsername=${MAVEN_CENTRAL_TOKEN_USERNAME} -PossrhPassword=${MAVEN_CENTRAL_TOKEN_PASSWORD} -Psign=true - + run: ./gradlew publish -Pversion=$SNAPSHOT_VERSION -PossrhUsername=${MAVEN_CENTRAL_TOKEN_USERNAME} -PossrhPassword=${MAVEN_CENTRAL_TOKEN_PASSWORD} -Psign=true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0c397997..55981603 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,6 +40,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 + - name: Set up JDKs + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: | + 11 + 17 + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91958ef8..761a96f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,10 +16,13 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} WITH_V: false DRY_RUN: true - - name: Set up JDK 8 - uses: actions/setup-java@v1 + - name: Set up JDKs + uses: actions/setup-java@v5 with: - java-version: 8 + distribution: 'temurin' + java-version: | + 11 + 17 - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Jar @@ -33,7 +36,7 @@ jobs: MAVEN_CENTRAL_TOKEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_TOKEN_USERNAME }} MAVEN_CENTRAL_TOKEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_TOKEN_PASSWORD }} RELEASE_VERSION: ${{ steps.tag_version_dry_run.outputs.tag }} - run: ./gradlew -Pversion=$RELEASE_VERSION signArchives uploadArchives -PossrhUsername=${MAVEN_CENTRAL_TOKEN_USERNAME} -PossrhPassword=${MAVEN_CENTRAL_TOKEN_PASSWORD} -Psign=true + run: ./gradlew -Pversion=$RELEASE_VERSION publish -PossrhUsername=${MAVEN_CENTRAL_TOKEN_USERNAME} -PossrhPassword=${MAVEN_CENTRAL_TOKEN_PASSWORD} -Psign=true - name: Close & Release Staging Repository env: MAVEN_CENTRAL_TOKEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_TOKEN_USERNAME }} @@ -55,4 +58,4 @@ jobs: uses: softprops/action-gh-release@v1 with: body: ${{steps.github_release.outputs.changelog}} - tag_name: ${{ steps.tag_version.outputs.tag }} \ No newline at end of file + tag_name: ${{ steps.tag_version.outputs.tag }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 020d4d12..f22fb610 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,9 +47,7 @@ Gradle release plugin is not currently working so this is a manual process at th ``` gpg --import private.key -cd ~/.gnupg gpg -k -gpg --export-secret-key YOUR_KEY_ID > ~/.gnupg/secring.gpg ``` ## Preparing @@ -62,13 +60,12 @@ gpg --export-secret-key YOUR_KEY_ID > ~/.gnupg/secring.gpg ``` export SONAR_USERNAME=? export SONAR_PASSWORD=? -export GPG_KEY_ID=? -export GPG_KEY_PASSPHRASE=? -export PATH_TO_SECRING_GPG=~/.gnupg/secring.gpg +export ORG_GRADLE_PROJECT_signingKey="$(cat private.key)" +export ORG_GRADLE_PROJECT_signingPassword=? # I found shadowed classes are not included if you don't separate the gradle operations ./gradlew clean shadowJar -./gradlew signArchives uploadArchives -PossrhUsername=${SONAR_USERNAME} -PossrhPassword=${SONAR_PASSWORD} -Psigning.keyId=${GPG_KEY_ID} -Psigning.password=${GPG_KEY_PASSPHRASE} -Psigning.secretKeyRingFile=${PATH_TO_SECRING_GPG} +./gradlew publish -PossrhUsername=${SONAR_USERNAME} -PossrhPassword=${SONAR_PASSWORD} -Psign=true ``` ## Releasing [Full Tutorial](https://central.sonatype.org/pages/ossrh-guide.html) @@ -86,4 +83,4 @@ export PATH_TO_SECRING_GPG=~/.gnupg/secring.gpg 1. Checkout master branch 1. Increment version number in `gradle.properties` -1. Create pull request for merge \ No newline at end of file +1. Create pull request for merge diff --git a/README.md b/README.md index 04c057f0..064f0c13 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,11 @@ testImplementation 'io.github.origin-energy:java-snapshot-testing-junit5:4.+' testImplementation("org.slf4j:slf4j-simple:2.0.0-alpha0") // Optional: Many will want to serialize into JSON. In this case you should also add the Jackson plugin +testImplementation 'io.github.origin-energy:java-snapshot-testing-plugin-jackson3:4.+' +testImplementation 'tools.jackson.core:jackson-core:3.1.0' +testImplementation 'tools.jackson.core:jackson-databind:3.1.0' + +// For Jackson 2 use the dedicated plugin and serializer classes instead testImplementation 'io.github.origin-energy:java-snapshot-testing-plugin-jackson:4.+' testImplementation 'com.fasterxml.jackson.core:jackson-core:2.11.3' testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.11.3' @@ -49,8 +54,12 @@ testRuntimeOnly 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.3' ```text serializer=au.com.origin.snapshots.serializers.v1.ToStringSnapshotSerializer serializer.base64=au.com.origin.snapshots.serializers.v1.Base64SnapshotSerializer -serializer.json=au.com.origin.snapshots.jackson.serializers.v1.JacksonSnapshotSerializer -serializer.orderedJson=au.com.origin.snapshots.jackson.serializers.v1.DeterministicJacksonSnapshotSerializer +serializer.json=au.com.origin.snapshots.jackson3.serializers.v1.Jackson3SnapshotSerializer +serializer.orderedJson=au.com.origin.snapshots.jackson3.serializers.v1.DeterministicJackson3SnapshotSerializer + +# Jackson 2 alternative +# serializer.json=au.com.origin.snapshots.jackson.serializers.v1.JacksonSnapshotSerializer +# serializer.orderedJson=au.com.origin.snapshots.jackson.serializers.v1.DeterministicJacksonSnapshotSerializer comparator=au.com.origin.snapshots.comparators.v1.PlainTextEqualsComparator reporters=au.com.origin.snapshots.reporters.v1.PlainTextSnapshotReporter snapshot-dir=__snapshots__ @@ -151,14 +160,18 @@ We currently support: Plugins - [Jackson for JSON serialization](https://search.maven.org/search?q=a:java-snapshot-testing-plugin-jackson) +- [Jackson 3 for JSON serialization](https://search.maven.org/search?q=a:java-snapshot-testing-plugin-jackson3) - You need jackson on your classpath (Gradle example) ```groovy - // Required java-snapshot-testing peer dependencies + // Jackson 2 plugin + testImplementation 'io.github.origin-energy:java-snapshot-testing-plugin-jackson:4.+' testImplementation 'com.fasterxml.jackson.core:jackson-core:2.11.3' testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.11.3' - // Optional java-snapshot-testing peer dependencies - testRuntimeOnly 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.11.3' - testRuntimeOnly 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.3' + + // Jackson 3 plugin + testImplementation 'io.github.origin-energy:java-snapshot-testing-plugin-jackson3:4.+' + testImplementation 'tools.jackson.core:jackson-core:3.1.0' + testImplementation 'tools.jackson.core:jackson-databind:3.1.0' ``` ## How does it work? diff --git a/build.gradle b/build.gradle index 20ec5f82..67f16856 100644 --- a/build.gradle +++ b/build.gradle @@ -1,41 +1,79 @@ +import org.gradle.api.tasks.compile.GroovyCompile +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.api.tasks.javadoc.Javadoc +import org.gradle.api.tasks.testing.Test + plugins { id 'net.researchgate.release' version '2.6.0' apply false - id 'com.github.johnrengelman.shadow' version '5.2.0' apply false + id 'com.gradleup.shadow' version '9.3.2' apply false id 'io.codearte.nexus-staging' version '0.22.0' - id "com.diffplug.spotless" version "6.11.0" apply false + id 'com.diffplug.spotless' version '8.4.0' apply false +} + +ext { + junit5Version = '5.10.2' + lombokVersion = '1.18.20' + slf4jVersion = '2.0.0-alpha0' + assertjVersion = '3.11.1' } subprojects { subproject -> - // FIXME this plugin is currently not working for multi-module projects even when defined at the top level only - // Will need to release and TAG manually for now subproject.apply plugin: 'net.researchgate.release' subproject.apply plugin: 'java-library' - subproject.apply plugin: 'com.github.johnrengelman.shadow' + subproject.apply plugin: 'com.gradleup.shadow' subproject.apply plugin: 'maven-publish' - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } repositories { mavenCentral() } + tasks.withType(JavaCompile).configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(11) + } + } + + tasks.withType(Javadoc).configureEach { + javadocTool = javaToolchains.javadocToolFor { + languageVersion = JavaLanguageVersion.of(11) + } + } + + tasks.withType(Test).configureEach { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(11) + } + } + + tasks.withType(GroovyCompile).configureEach { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(11) + } + } + shadowJar { - classifier = '' + archiveClassifier = '' relocate 'org.assertj', 'shadow.org.assertj' relocate 'org.opentest4j', 'shadow.org.opentest4j' - exclude "module-info.class" + exclude 'module-info.class' + } + + jar { + archiveClassifier = 'thin' } dependencies { - // Lombok - compileOnly 'org.projectlombok:lombok:1.18.20' - annotationProcessor 'org.projectlombok:lombok:1.18.20' - testCompileOnly 'org.projectlombok:lombok:1.18.20' - testAnnotationProcessor 'org.projectlombok:lombok:1.18.20' + compileOnly "org.projectlombok:lombok:${project.lombokVersion}" + annotationProcessor "org.projectlombok:lombok:${project.lombokVersion}" + testCompileOnly "org.projectlombok:lombok:${project.lombokVersion}" + testAnnotationProcessor "org.projectlombok:lombok:${project.lombokVersion}" - // Logging implementation - compileOnly 'org.slf4j:slf4j-api:2.0.0-alpha0' + compileOnly "org.slf4j:slf4j-api:${project.slf4jVersion}" } // Add verbose logging for Github Actions @@ -45,4 +83,4 @@ subprojects { subproject -> exceptionFormat "full" } } -} +} \ No newline at end of file diff --git a/gradle/junit5.gradle b/gradle/junit5.gradle new file mode 100644 index 00000000..6c1f8517 --- /dev/null +++ b/gradle/junit5.gradle @@ -0,0 +1,9 @@ +dependencies { + testImplementation platform("org.junit:junit-bom:${project.junit5Version}") + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gradle/publishing.gradle b/gradle/publishing.gradle index 5fd7c68b..f8548750 100644 --- a/gradle/publishing.gradle +++ b/gradle/publishing.gradle @@ -8,81 +8,82 @@ task sourcesJar(type: Jar) { from sourceSets.main.allSource } -artifacts { - archives javadocJar, sourcesJar -} - apply plugin: 'net.researchgate.release' -// Sign for maven central deployment +publishing { + publications.withType(MavenPublication).configureEach { + artifact javadocJar + artifact sourcesJar + + pom { + name = 'java-snapshot-testing' + description = 'Snapshot Testing for Java' + url = 'https://github.com/origin-energy/java-snapshot-testing' + + scm { + connection = 'scm:git:https://github.com/origin-energy/java-snapshot-testing' + url = 'https://github.com/origin-energy/java-snapshot-testing' + } + + licenses { + license { + name = 'MIT License' + url = 'http://www.opensource.org/licenses/mit-license.php' + } + } + + developers { + developer { + id = 'jack.matthews' + name = 'Jack Matthews' + email = 'jack.matthews@origin.com.au' + } + } + + withXml { + def pomNode = asNode() + pomNode.children().removeAll { child -> child instanceof Node && child.name() == 'dependencies' } + } + } + } +} + if (project.hasProperty("sign")) { apply plugin: 'signing' signing { def signingKey = findProperty("signingKey") def signingPassword = findProperty("signingPassword") useInMemoryPgpKeys(signingKey, signingPassword) - sign configurations.archives } +} - // previously the plugin was using the `build` version which did not include the shadowed dependencies - signArchives.dependsOn 'shadowJar' +afterEvaluate { + if (project.hasProperty("sign")) { + signing { + sign publishing.publications + } + } } -// Maven Central Publishing if (project.hasProperty("ossrhUsername") && project.hasProperty("ossrhPassword")) { - apply plugin: 'maven' + publishing { + repositories { + maven { + name = 'sonatype' + url = uri(project.version.toString().endsWith('-SNAPSHOT') + ? 'https://oss.sonatype.org/content/repositories/snapshots/' + : 'https://oss.sonatype.org/service/local/staging/deploy/maven2/') + credentials { + username = project.property("ossrhUsername") + password = project.property("ossrhPassword") + } + } + } + } nexusStaging { username project.property("ossrhUsername") password project.property("ossrhPassword") packageGroup 'io.github.origin-energy' } - - uploadArchives { - repositories { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: ossrhUsername, password: ossrhPassword) - } - - snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { - authentication(userName: ossrhUsername, password: ossrhPassword) - } - - pom.project { - name 'java-snapsho-testing' - packaging 'jar' - description 'Snapshot Testing for Java' - url 'https://github.com/origin-energy/java-snapshot-testing' - scm { - connection 'scm:git:https://github.com/origin-energy/java-snapshot-testing' - url 'https://github.com/origin-energy/java-snapshot-testing' - } - - licenses { - license { - name 'MIT License' - url 'http://www.opensource.org/licenses/mit-license.php' - } - } - - developers { - developer { - id 'jack.matthews' - name 'Jack Matthews' - email 'jack.matthews@origin.com.au' - } - } - } - - // We clear out all the dependencies because we have shadowed them inside the jar - pom.whenConfigured { - p -> p.dependencies = [] - } - } - } - } } - diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d355f4c4..3e50ed2f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Fri Apr 17 09:29:12 CEST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/java-snapshot-testing-core/build.gradle b/java-snapshot-testing-core/build.gradle index 7b4e6c37..56ed5855 100644 --- a/java-snapshot-testing-core/build.gradle +++ b/java-snapshot-testing-core/build.gradle @@ -1,5 +1,6 @@ apply from: "../gradle/publishing.gradle" apply from: "../gradle/spotless.gradle" +apply from: "../gradle/junit5.gradle" dependencies { @@ -12,14 +13,9 @@ dependencies { testImplementation 'org.slf4j:slf4j-simple:2.0.0-alpha0' testImplementation 'org.mockito:mockito-junit-jupiter:2.23.0' testImplementation 'org.mockito:mockito-core:2.23.4' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.2' - testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.3.2' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.3.2' testImplementation group: 'commons-io', name: 'commons-io', version: '2.6' } -test { useJUnitPlatform() } - publishing { publications { myPublication(MavenPublication) { diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/SnapshotFile.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/SnapshotFile.java index 079476ff..62132efe 100644 --- a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/SnapshotFile.java +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/SnapshotFile.java @@ -8,6 +8,7 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; @@ -155,13 +156,17 @@ public void cleanup() { @SneakyThrows private boolean snapshotsAreTheSame() { - Path path = Paths.get(this.getDebugFilename()); - if (Files.exists(path)) { + Path debugPath = Paths.get(this.getDebugFilename()); + if (Files.exists(debugPath)) { List snapshotFileContent = Files.readAllLines(Paths.get(this.fileName), StandardCharsets.UTF_8); - List debugSnapshotFileContent = - Files.readAllLines(Paths.get(this.getDebugFilename()), StandardCharsets.UTF_8); - return Objects.equals(snapshotFileContent, debugSnapshotFileContent); + try { + List debugSnapshotFileContent = + Files.readAllLines(debugPath, StandardCharsets.UTF_8); + return Objects.equals(snapshotFileContent, debugSnapshotFileContent); + } catch (NoSuchFileException e) { + return false; + } } return false; diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/SnapshotCaptor.java b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/SnapshotCaptor.java index 25872f0c..e790bfeb 100644 --- a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/SnapshotCaptor.java +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/SnapshotCaptor.java @@ -73,8 +73,10 @@ private Object shallowCopy(Object value) { } private Object constructCopy(Class argumentClass) - throws InstantiationException, IllegalAccessException, - java.lang.reflect.InvocationTargetException, NoSuchMethodException { + throws InstantiationException, + IllegalAccessException, + java.lang.reflect.InvocationTargetException, + NoSuchMethodException { try { return argumentClass.getDeclaredConstructor().newInstance(); diff --git a/java-snapshot-testing-junit4/build.gradle b/java-snapshot-testing-junit4/build.gradle index 54745cb1..27fbbd60 100644 --- a/java-snapshot-testing-junit4/build.gradle +++ b/java-snapshot-testing-junit4/build.gradle @@ -4,17 +4,14 @@ apply from: "../gradle/spotless.gradle" dependencies { implementation project(':java-snapshot-testing-core') - // User supplied JUnit4 Version compileOnly 'org.junit.platform:junit-platform-runner:1.2.0' compileOnly 'org.junit.vintage:junit-vintage-engine:5.2.0' - // Testing - testImplementation 'org.slf4j:slf4j-simple:2.0.0-alpha0' + testImplementation "org.slf4j:slf4j-simple:${project.slf4jVersion}" testImplementation 'org.junit.platform:junit-platform-runner:1.2.0' testImplementation 'org.junit.vintage:junit-vintage-engine:5.2.0' - testImplementation 'org.assertj:assertj-core:3.11.1' + testImplementation "org.assertj:assertj-core:${project.assertjVersion}" - // Required java-snapshot-testing peer dependencies testImplementation 'com.fasterxml.jackson.core:jackson-core:2.11.3' testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.11.3' } diff --git a/java-snapshot-testing-junit5/build.gradle b/java-snapshot-testing-junit5/build.gradle index 0b09446c..fa3d81fa 100644 --- a/java-snapshot-testing-junit5/build.gradle +++ b/java-snapshot-testing-junit5/build.gradle @@ -1,32 +1,21 @@ apply from: "../gradle/publishing.gradle" apply from: "../gradle/spotless.gradle" - -ext { - junitVersion = '5.7.2' -} +apply from: "../gradle/junit5.gradle" dependencies { implementation project(':java-snapshot-testing-core') - // User supplied Junit5 version - compileOnly "org.junit.jupiter:junit-jupiter-api:${project.junitVersion}" - compileOnly "org.junit.jupiter:junit-jupiter-engine:${project.junitVersion}" + compileOnly "org.junit.jupiter:junit-jupiter-api:${project.junit5Version}" + compileOnly "org.junit.jupiter:junit-jupiter-engine:${project.junit5Version}" - // Testing - testImplementation 'org.slf4j:slf4j-simple:2.0.0-alpha0' - testImplementation "org.junit.jupiter:junit-jupiter-params:${project.junitVersion}" - testImplementation "org.junit.jupiter:junit-jupiter-api:${project.junitVersion}" - testImplementation "org.junit.jupiter:junit-jupiter-engine:${project.junitVersion}" - testImplementation 'org.assertj:assertj-core:3.11.1' + testImplementation "org.slf4j:slf4j-simple:${project.slf4jVersion}" + testImplementation "org.assertj:assertj-core:${project.assertjVersion}" - // Required java-snapshot-testing peer dependencies testImplementation project(':java-snapshot-testing-plugin-jackson') testImplementation 'com.fasterxml.jackson.core:jackson-core:2.11.3' testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.11.3' } -test { useJUnitPlatform() } - publishing { publications { myPublication(MavenPublication) { diff --git a/java-snapshot-testing-plugin-jackson/build.gradle b/java-snapshot-testing-plugin-jackson/build.gradle index c2660d38..a4eff439 100644 --- a/java-snapshot-testing-plugin-jackson/build.gradle +++ b/java-snapshot-testing-plugin-jackson/build.gradle @@ -1,25 +1,17 @@ apply from: "../gradle/publishing.gradle" apply from: "../gradle/spotless.gradle" +apply from: "../gradle/junit5.gradle" dependencies { compileOnly project(':java-snapshot-testing-core') - // Client needs to supply their own versions compileOnly 'com.fasterxml.jackson.core:jackson-core:2.11.3' compileOnly 'com.fasterxml.jackson.core:jackson-databind:2.11.3' - // User supplied Junit5 version - compileOnly 'org.junit.jupiter:junit-jupiter-api:5.3.2' - compileOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.2' - - // Testing testImplementation project(':java-snapshot-testing-core') - testImplementation 'org.slf4j:slf4j-simple:2.0.0-alpha0' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.3.2' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.2' - testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.3.2' - testImplementation 'org.assertj:assertj-core:3.11.1' - testImplementation 'org.skyscreamer:jsonassert:1.5.0' // For docs/ reporter example + testImplementation "org.slf4j:slf4j-simple:${project.slf4jVersion}" + testImplementation "org.assertj:assertj-core:${project.assertjVersion}" + testImplementation 'org.skyscreamer:jsonassert:1.5.0' testImplementation 'com.fasterxml.jackson.core:jackson-core:2.16.0' testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.16.0' @@ -27,8 +19,6 @@ dependencies { testRuntimeOnly 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.0' } -test { useJUnitPlatform() } - publishing { publications { myPublication(MavenPublication) { diff --git a/java-snapshot-testing-plugin-jackson3/build.gradle b/java-snapshot-testing-plugin-jackson3/build.gradle new file mode 100644 index 00000000..c035bd90 --- /dev/null +++ b/java-snapshot-testing-plugin-jackson3/build.gradle @@ -0,0 +1,54 @@ +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.api.tasks.javadoc.Javadoc +import org.gradle.api.tasks.testing.Test + +apply from: "../gradle/publishing.gradle" +apply from: "../gradle/spotless.gradle" +apply from: "../gradle/junit5.gradle" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType(JavaCompile).configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(17) + } +} + +tasks.withType(Javadoc).configureEach { + javadocTool = javaToolchains.javadocToolFor { + languageVersion = JavaLanguageVersion.of(17) + } +} + +tasks.withType(Test).configureEach { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(17) + } +} + +dependencies { + compileOnly project(':java-snapshot-testing-core') + + compileOnly 'tools.jackson.core:jackson-core:3.1.0' + compileOnly 'tools.jackson.core:jackson-databind:3.1.0' + + testImplementation project(':java-snapshot-testing-core') + testImplementation "org.slf4j:slf4j-simple:${project.slf4jVersion}" + testImplementation "org.assertj:assertj-core:${project.assertjVersion}" + + testImplementation 'tools.jackson.core:jackson-core:3.1.0' + testImplementation 'tools.jackson.core:jackson-databind:3.1.0' +} + +publishing { + publications { + myPublication(MavenPublication) { + artifact shadowJar + groupId 'io.github.origin-energy' + artifactId 'java-snapshot-testing-plugin-jackson3' + } + } +} \ No newline at end of file diff --git a/java-snapshot-testing-plugin-jackson3/src/main/java/au/com/origin/snapshots/jackson3/serializers/DeterministicCollectionModule.java b/java-snapshot-testing-plugin-jackson3/src/main/java/au/com/origin/snapshots/jackson3/serializers/DeterministicCollectionModule.java new file mode 100644 index 00000000..977f560d --- /dev/null +++ b/java-snapshot-testing-plugin-jackson3/src/main/java/au/com/origin/snapshots/jackson3/serializers/DeterministicCollectionModule.java @@ -0,0 +1,54 @@ +package au.com.origin.snapshots.jackson3.serializers; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.module.SimpleModule; + +/** + * Inspired by: + * https://www.stubbornjava.com/posts/creating-a-somewhat-deterministic-jackson-objectmapper + */ +public class DeterministicCollectionModule extends SimpleModule { + + private static final Logger LOGGER = LoggerFactory.getLogger(DeterministicCollectionModule.class); + + public DeterministicCollectionModule() { + addSerializer(Collection.class, new CollectionSerializer()); + } + + private static class CollectionSerializer extends ValueSerializer { + + @Override + public void serialize(Collection value, JsonGenerator gen, SerializationContext context) { + Object[] sorted = convert(value); + context.writeValue(gen, sorted); + } + + private Object[] convert(Collection value) { + if (value == null || value.isEmpty()) { + return Collections.emptyList().toArray(); + } + + try { + return value.stream() + .filter(Objects::nonNull) + .sorted() + .collect(Collectors.toList()) + .toArray(); + } catch (ClassCastException ex) { + LOGGER.warn( + "Unable to sort() collection - this may result in a non deterministic snapshot.\n" + + "Consider adding a custom serializer for this type via the Jackson3SnapshotSerializer#configure() method.\n" + + ex.getMessage()); + return value.toArray(); + } + } + } +} diff --git a/java-snapshot-testing-plugin-jackson3/src/main/java/au/com/origin/snapshots/jackson3/serializers/v1/DeterministicJackson3SnapshotSerializer.java b/java-snapshot-testing-plugin-jackson3/src/main/java/au/com/origin/snapshots/jackson3/serializers/v1/DeterministicJackson3SnapshotSerializer.java new file mode 100644 index 00000000..5bc7e4c4 --- /dev/null +++ b/java-snapshot-testing-plugin-jackson3/src/main/java/au/com/origin/snapshots/jackson3/serializers/v1/DeterministicJackson3SnapshotSerializer.java @@ -0,0 +1,24 @@ +package au.com.origin.snapshots.jackson3.serializers.v1; + +import au.com.origin.snapshots.jackson3.serializers.DeterministicCollectionModule; +import tools.jackson.databind.MapperFeature; +import tools.jackson.databind.json.JsonMapper; + +/** + * Attempts to deterministically render a snapshot. + * + *

This can help in situations where collections are rendering in a different order on subsequent + * runs. + * + *

Note that collections will be ordered which mar or may not be desirable given your use case. + */ +public class DeterministicJackson3SnapshotSerializer extends Jackson3SnapshotSerializer { + + @Override + protected JsonMapper.Builder configure(JsonMapper.Builder builder) { + return super.configure(builder) + .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) + .disable(MapperFeature.SORT_CREATOR_PROPERTIES_FIRST) + .addModule(new DeterministicCollectionModule()); + } +} diff --git a/java-snapshot-testing-plugin-jackson3/src/main/java/au/com/origin/snapshots/jackson3/serializers/v1/Jackson3SnapshotSerializer.java b/java-snapshot-testing-plugin-jackson3/src/main/java/au/com/origin/snapshots/jackson3/serializers/v1/Jackson3SnapshotSerializer.java new file mode 100644 index 00000000..9a01c205 --- /dev/null +++ b/java-snapshot-testing-plugin-jackson3/src/main/java/au/com/origin/snapshots/jackson3/serializers/v1/Jackson3SnapshotSerializer.java @@ -0,0 +1,72 @@ +package au.com.origin.snapshots.jackson3.serializers.v1; + +import au.com.origin.snapshots.Snapshot; +import au.com.origin.snapshots.SnapshotSerializerContext; +import au.com.origin.snapshots.exceptions.SnapshotExtensionException; +import au.com.origin.snapshots.serializers.SerializerType; +import au.com.origin.snapshots.serializers.SnapshotSerializer; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.Arrays; +import java.util.List; +import tools.jackson.core.PrettyPrinter; +import tools.jackson.databind.MapperFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.json.JsonMapper; + +public class Jackson3SnapshotSerializer implements SnapshotSerializer { + + private final PrettyPrinter pp = new SnapshotPrettyPrinter(); + private final ObjectMapper objectMapper = buildObjectMapper(); + + protected JsonMapper.Builder configure(JsonMapper.Builder builder) { + return builder; + } + + protected boolean shouldFindAndRegisterModules() { + return true; + } + + private ObjectMapper buildObjectMapper() { + JsonMapper.Builder builder = + JsonMapper.builder() + .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) + .enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID) + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) + .changeDefaultPropertyInclusion( + inclusion -> inclusion.withValueInclusion(JsonInclude.Include.NON_NULL)) + .changeDefaultVisibility( + visibility -> + visibility + .withFieldVisibility(JsonAutoDetect.Visibility.ANY) + .withGetterVisibility(JsonAutoDetect.Visibility.NONE) + .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE) + .withSetterVisibility(JsonAutoDetect.Visibility.NONE) + .withCreatorVisibility(JsonAutoDetect.Visibility.NONE)); + + if (shouldFindAndRegisterModules()) { + builder.findAndAddModules(); + } + return configure(builder).build(); + } + + @Override + public Snapshot apply(Object object, SnapshotSerializerContext gen) { + try { + List objects = Arrays.asList(object); + String body = objectMapper.writer().with(pp).writeValueAsString(objects); + return gen.toSnapshot(body); + } catch (Exception e) { + throw new SnapshotExtensionException("Jackson Serialization failed", e); + } + } + + @Override + public String getOutputFormat() { + return SerializerType.JSON.name(); + } +} diff --git a/java-snapshot-testing-plugin-jackson3/src/main/java/au/com/origin/snapshots/jackson3/serializers/v1/SnapshotPrettyPrinter.java b/java-snapshot-testing-plugin-jackson3/src/main/java/au/com/origin/snapshots/jackson3/serializers/v1/SnapshotPrettyPrinter.java new file mode 100644 index 00000000..6afd96ab --- /dev/null +++ b/java-snapshot-testing-plugin-jackson3/src/main/java/au/com/origin/snapshots/jackson3/serializers/v1/SnapshotPrettyPrinter.java @@ -0,0 +1,44 @@ +package au.com.origin.snapshots.jackson3.serializers.v1; + +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.util.DefaultIndenter; +import tools.jackson.core.util.DefaultPrettyPrinter; +import tools.jackson.core.util.Separators; + +class SnapshotPrettyPrinter extends DefaultPrettyPrinter { + + private static final Separators SNAPSHOT_SEPARATORS = + Separators.createDefaultInstance().withObjectNameValueSpacing(Separators.Spacing.AFTER); + + public SnapshotPrettyPrinter() { + this(SNAPSHOT_SEPARATORS); + } + + private SnapshotPrettyPrinter(Separators separators) { + super(separators); + Indenter lfOnlyIndenter = new DefaultIndenter(" ", "\n"); + this.indentArraysWith(lfOnlyIndenter); + this.indentObjectsWith(lfOnlyIndenter); + } + + private SnapshotPrettyPrinter(SnapshotPrettyPrinter base) { + super(base); + } + + private SnapshotPrettyPrinter(SnapshotPrettyPrinter base, Separators separators) { + super(base, separators); + } + + @Override + public void writeRootValueSeparator(JsonGenerator gen) {} + + @Override + public SnapshotPrettyPrinter withSeparators(Separators separators) { + return new SnapshotPrettyPrinter(this, separators); + } + + @Override + public DefaultPrettyPrinter createInstance() { + return new SnapshotPrettyPrinter(this); + } +} diff --git a/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/NoNameChangeTest.java b/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/NoNameChangeTest.java new file mode 100644 index 00000000..197919c3 --- /dev/null +++ b/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/NoNameChangeTest.java @@ -0,0 +1,19 @@ +package au.com.origin.snapshots.jackson3; + +import static org.assertj.core.api.Assertions.assertThat; + +import au.com.origin.snapshots.jackson3.serializers.v1.DeterministicJackson3SnapshotSerializer; +import au.com.origin.snapshots.jackson3.serializers.v1.Jackson3SnapshotSerializer; +import org.junit.jupiter.api.Test; + +public class NoNameChangeTest { + + @Test + public void serializersApiShouldNotChange() { + assertThat(Jackson3SnapshotSerializer.class.getName()) + .isEqualTo("au.com.origin.snapshots.jackson3.serializers.v1.Jackson3SnapshotSerializer"); + assertThat(DeterministicJackson3SnapshotSerializer.class.getName()) + .isEqualTo( + "au.com.origin.snapshots.jackson3.serializers.v1.DeterministicJackson3SnapshotSerializer"); + } +} diff --git a/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/serializers/DeterministicJackson3SnapshotSerializerTest.java b/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/serializers/DeterministicJackson3SnapshotSerializerTest.java new file mode 100644 index 00000000..599765a8 --- /dev/null +++ b/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/serializers/DeterministicJackson3SnapshotSerializerTest.java @@ -0,0 +1,147 @@ +package au.com.origin.snapshots.jackson3.serializers; + +import au.com.origin.snapshots.Expect; +import au.com.origin.snapshots.SnapshotVerifier; +import au.com.origin.snapshots.config.PropertyResolvingSnapshotConfig; +import au.com.origin.snapshots.jackson3.serializers.v1.DeterministicJackson3SnapshotSerializer; +import au.com.origin.snapshots.serializers.SerializerType; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.TreeMap; +import java.util.TreeSet; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +public class DeterministicJackson3SnapshotSerializerTest { + + @Test + public void shouldSerializeDifferentTypes(TestInfo testInfo) { + SnapshotVerifier snapshotVerifier = + new SnapshotVerifier( + new PropertyResolvingSnapshotConfig(), testInfo.getTestClass().get(), false); + Expect expect = Expect.of(snapshotVerifier, testInfo.getTestMethod().get()); + expect.serializer("orderedJson").toMatchSnapshot(new TypeDummy()); + snapshotVerifier.validateSnapshots(); + } + + @Test + void shouldSupportJsonFormat() { + Assertions.assertThat(new DeterministicJackson3SnapshotSerializer().getOutputFormat()) + .isEqualTo(SerializerType.JSON.name()); + } + + private Map nonDeterministicMap(Map target) { + final List items = + new ArrayList() { + { + add("f"); + add("a"); + add("d"); + add("e"); + add("g"); + add("b"); + add("c"); + } + }; + + int size = items.size(); + for (int i = 0; i < size; i++) { + String random = pluckRandom(items); + target.put(random, (int) random.charAt(0)); + } + return target; + } + + private Collection nonDeterministicCollection(Collection target) { + final List items = + new ArrayList() { + { + add("f"); + add("a"); + add("d"); + add("e"); + add("g"); + add("b"); + add("c"); + } + }; + + int size = items.size(); + for (int i = 0; i < size; i++) { + target.add(pluckRandom(items)); + } + + return target; + } + + private String pluckRandom(List array) { + int rnd = new Random().nextInt(array.size()); + return array.remove(rnd); + } + + private enum AnEnum { + F, + A, + D, + E, + G, + B, + C + } + + private final class TypeDummy { + private final Void aNull = null; + private final Object anObject = new Object(); + private final byte aByte = "A".getBytes()[0]; + private final short aShort = 32767; + private final int anInt = 2147483647; + private final long aLong = 9223372036854775807L; + private final float aFloat = 0.1234567F; + private final double aDouble = 1.123456789123456D; + private final boolean aBoolean = true; + private final char aChar = 'A'; + private final String string = "Hello World"; + private final Date date = Date.from(Instant.parse("2020-10-19T22:21:07.103Z")); + private final LocalDate localDate = LocalDate.parse("2020-10-19"); + private final LocalDateTime localDateTime = LocalDateTime.parse("2020-10-19T22:21:07.103"); + private final ZonedDateTime zonedDateTime = + ZonedDateTime.parse("2020-04-19T22:21:07.103+10:00[Australia/Melbourne]"); + private final AnEnum anEnum = AnEnum.A; + private final Optional presentOptional = Optional.of("Hello World"); + private final Optional emptyOptional = Optional.empty(); + private final String[] stringArray = {"f", "a", "d", "e", "g", "b", "c"}; + private final Object[] anEnumArray = Arrays.stream(AnEnum.values()).toArray(); + private final Map hashMap = nonDeterministicMap(new java.util.HashMap<>()); + private final Map treeMap = nonDeterministicMap(new TreeMap<>()); + private final Map linkedHashMap = nonDeterministicMap(new LinkedHashMap<>()); + private final Collection linkedHashSet = + nonDeterministicCollection(new LinkedHashSet<>()); + private final Collection hashSet = + nonDeterministicCollection(new java.util.HashSet<>()); + private final Collection treeSet = nonDeterministicCollection(new TreeSet<>()); + private final Collection arrayList = nonDeterministicCollection(new ArrayList<>()); + private final Collection linkedList = nonDeterministicCollection(new LinkedList<>()); + private final Collection listOfCollections = + new ArrayList() { + { + add(nonDeterministicMap(new LinkedHashMap<>())); + add(nonDeterministicCollection(new LinkedHashSet<>())); + add(nonDeterministicCollection(new LinkedList<>())); + } + }; + } +} diff --git a/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/serializers/Jackson3SnapshotSerializerTest.java b/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/serializers/Jackson3SnapshotSerializerTest.java new file mode 100644 index 00000000..50e43fe3 --- /dev/null +++ b/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/serializers/Jackson3SnapshotSerializerTest.java @@ -0,0 +1,164 @@ +package au.com.origin.snapshots.jackson3.serializers; + +import au.com.origin.snapshots.Expect; +import au.com.origin.snapshots.Snapshot; +import au.com.origin.snapshots.SnapshotHeader; +import au.com.origin.snapshots.SnapshotSerializerContext; +import au.com.origin.snapshots.SnapshotVerifier; +import au.com.origin.snapshots.config.PropertyResolvingSnapshotConfig; +import au.com.origin.snapshots.config.SnapshotConfig; +import au.com.origin.snapshots.jackson3.serializers.v1.Jackson3SnapshotSerializer; +import au.com.origin.snapshots.serializers.SerializerType; +import au.com.origin.snapshots.serializers.SnapshotSerializer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import java.util.TreeSet; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +public class Jackson3SnapshotSerializerTest { + + private static final SnapshotConfig DEFAULT_CONFIG = + new PropertyResolvingSnapshotConfig() { + @Override + public SnapshotSerializer getSerializer() { + return new Jackson3SnapshotSerializer(); + } + }; + + private final SnapshotSerializerContext gen = + new SnapshotSerializerContext( + "test", null, new SnapshotHeader(), Jackson3SnapshotSerializerTest.class, null); + + @Test + public void shouldSerializeMap() { + Map map = new HashMap<>(); + map.put("name", "John Doe"); + map.put("age", 40); + + SnapshotSerializer serializer = new Jackson3SnapshotSerializer(); + Snapshot result = serializer.apply(map, gen); + Assertions.assertThat(result.getBody()) + .isEqualTo( + "[\n" + + " {\n" + + " \"age\": 40,\n" + + " \"name\": \"John Doe\"\n" + + " }\n" + + "]"); + } + + @Test + public void shouldSerializeDifferentTypes(TestInfo testInfo) { + SnapshotVerifier snapshotVerifier = + new SnapshotVerifier(DEFAULT_CONFIG, testInfo.getTestClass().get()); + Expect expect = Expect.of(snapshotVerifier, testInfo.getTestMethod().get()); + expect.toMatchSnapshot(new TypeDummy()); + snapshotVerifier.validateSnapshots(); + } + + @Test + void shouldSupportJsonFormat() { + Assertions.assertThat(new Jackson3SnapshotSerializer().getOutputFormat()) + .isEqualTo(SerializerType.JSON.name()); + } + + private Map deterministicMap(Map target) { + final List items = + new ArrayList() { + { + add("f"); + add("a"); + add("d"); + add("e"); + add("g"); + add("b"); + add("c"); + } + }; + items.forEach(it -> target.put(it, (int) it.charAt(0))); + return target; + } + + private Collection deterministicCollection(Collection target) { + final List items = + new ArrayList() { + { + add("f"); + add("a"); + add("d"); + add("e"); + add("g"); + add("b"); + add("c"); + } + }; + target.addAll(items); + return target; + } + + private enum AnEnum { + F, + A, + D, + E, + G, + B, + C + } + + private final class TypeDummy { + private final Void aNull = null; + private final Object anObject = new Object(); + private final byte aByte = "A".getBytes()[0]; + private final short aShort = 32767; + private final int anInt = 2147483647; + private final long aLong = 9223372036854775807L; + private final float aFloat = 0.1234567F; + private final double aDouble = 1.123456789123456D; + private final boolean aBoolean = true; + private final char aChar = 'A'; + private final String string = "Hello World"; + private final Date date = Date.from(Instant.parse("2020-10-19T22:21:07.103Z")); + private final LocalDate localDate = LocalDate.parse("2020-10-19"); + private final LocalDateTime localDateTime = LocalDateTime.parse("2020-10-19T22:21:07.103"); + private final ZonedDateTime zonedDateTime = + ZonedDateTime.parse("2020-04-19T22:21:07.103+10:00[Australia/Melbourne]"); + private final AnEnum anEnum = AnEnum.A; + private final Optional presentOptional = Optional.of("Hello World"); + private final Optional emptyOptional = Optional.empty(); + private final String[] stringArray = {"f", "a", "d", "e", "g", "b", "c"}; + private final Object[] anEnumArray = Arrays.stream(AnEnum.values()).toArray(); + private final Map hashMap = deterministicMap(new HashMap<>()); + private final Map treeMap = deterministicMap(new TreeMap<>()); + private final Map linkedHashMap = deterministicMap(new LinkedHashMap<>()); + private final Collection linkedHashSet = deterministicCollection(new LinkedHashSet<>()); + private final Collection hashSet = deterministicCollection(new java.util.HashSet<>()); + private final Collection treeSet = deterministicCollection(new TreeSet<>()); + private final Collection arrayList = deterministicCollection(new ArrayList<>()); + private final Collection linkedList = deterministicCollection(new LinkedList<>()); + private final Collection listOfCollections = + new ArrayList() { + { + add(deterministicMap(new LinkedHashMap<>())); + add(deterministicCollection(new LinkedHashSet<>())); + add(deterministicCollection(new LinkedList<>())); + } + }; + } +} diff --git a/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/serializers/__snapshots__/DeterministicJackson3SnapshotSerializerTest.snap b/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/serializers/__snapshots__/DeterministicJackson3SnapshotSerializerTest.snap new file mode 100644 index 00000000..27dbed91 --- /dev/null +++ b/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/serializers/__snapshots__/DeterministicJackson3SnapshotSerializerTest.snap @@ -0,0 +1,140 @@ +au.com.origin.snapshots.jackson3.serializers.DeterministicJackson3SnapshotSerializerTest.shouldSerializeDifferentTypes=[ + { + "aBoolean": true, + "aByte": 65, + "aChar": "A", + "aDouble": 1.123456789123456, + "aFloat": 0.1234567, + "aLong": 9223372036854775807, + "aShort": 32767, + "anEnum": "A", + "anEnumArray": [ + "F", + "A", + "D", + "E", + "G", + "B", + "C" + ], + "anInt": 2147483647, + "anObject": { }, + "arrayList": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "date": "2020-10-19T22:21:07.103Z", + "emptyOptional": null, + "hashMap": { + "a": 97, + "b": 98, + "c": 99, + "d": 100, + "e": 101, + "f": 102, + "g": 103 + }, + "hashSet": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "linkedHashMap": { + "a": 97, + "b": 98, + "c": 99, + "d": 100, + "e": 101, + "f": 102, + "g": 103 + }, + "linkedHashSet": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "linkedList": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "listOfCollections": [ + { + "a": 97, + "b": 98, + "c": 99, + "d": 100, + "e": 101, + "f": 102, + "g": 103 + }, + [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ] + ], + "localDate": "2020-10-19", + "localDateTime": "2020-10-19T22:21:07.103", + "presentOptional": "Hello World", + "string": "Hello World", + "stringArray": [ + "f", + "a", + "d", + "e", + "g", + "b", + "c" + ], + "treeMap": { + "a": 97, + "b": 98, + "c": 99, + "d": 100, + "e": 101, + "f": 102, + "g": 103 + }, + "treeSet": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "zonedDateTime": "2020-04-19T22:21:07.103+10:00[Australia/Melbourne]" + } +] \ No newline at end of file diff --git a/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/serializers/__snapshots__/Jackson3SnapshotSerializerTest.snap b/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/serializers/__snapshots__/Jackson3SnapshotSerializerTest.snap new file mode 100644 index 00000000..82f19830 --- /dev/null +++ b/java-snapshot-testing-plugin-jackson3/src/test/java/au/com/origin/snapshots/jackson3/serializers/__snapshots__/Jackson3SnapshotSerializerTest.snap @@ -0,0 +1,140 @@ +au.com.origin.snapshots.jackson3.serializers.Jackson3SnapshotSerializerTest.shouldSerializeDifferentTypes=[ + { + "anObject": { }, + "aByte": 65, + "aShort": 32767, + "anInt": 2147483647, + "aLong": 9223372036854775807, + "aFloat": 0.1234567, + "aDouble": 1.123456789123456, + "aBoolean": true, + "aChar": "A", + "string": "Hello World", + "date": "2020-10-19T22:21:07.103Z", + "localDate": "2020-10-19", + "localDateTime": "2020-10-19T22:21:07.103", + "zonedDateTime": "2020-04-19T22:21:07.103+10:00[Australia/Melbourne]", + "anEnum": "A", + "presentOptional": "Hello World", + "emptyOptional": null, + "stringArray": [ + "f", + "a", + "d", + "e", + "g", + "b", + "c" + ], + "anEnumArray": [ + "F", + "A", + "D", + "E", + "G", + "B", + "C" + ], + "hashMap": { + "a": 97, + "b": 98, + "c": 99, + "d": 100, + "e": 101, + "f": 102, + "g": 103 + }, + "treeMap": { + "a": 97, + "b": 98, + "c": 99, + "d": 100, + "e": 101, + "f": 102, + "g": 103 + }, + "linkedHashMap": { + "a": 97, + "b": 98, + "c": 99, + "d": 100, + "e": 101, + "f": 102, + "g": 103 + }, + "linkedHashSet": [ + "f", + "a", + "d", + "e", + "g", + "b", + "c" + ], + "hashSet": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "treeSet": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "arrayList": [ + "f", + "a", + "d", + "e", + "g", + "b", + "c" + ], + "linkedList": [ + "f", + "a", + "d", + "e", + "g", + "b", + "c" + ], + "listOfCollections": [ + { + "a": 97, + "b": 98, + "c": 99, + "d": 100, + "e": 101, + "f": 102, + "g": 103 + }, + [ + "f", + "a", + "d", + "e", + "g", + "b", + "c" + ], + [ + "f", + "a", + "d", + "e", + "g", + "b", + "c" + ] + ] + } +] diff --git a/java-snapshot-testing-plugin-jackson3/src/test/resources/snapshot.properties b/java-snapshot-testing-plugin-jackson3/src/test/resources/snapshot.properties new file mode 100644 index 00000000..1fde1745 --- /dev/null +++ b/java-snapshot-testing-plugin-jackson3/src/test/resources/snapshot.properties @@ -0,0 +1,8 @@ +serializer=au.com.origin.snapshots.jackson3.serializers.v1.Jackson3SnapshotSerializer +serializer.orderedJson=au.com.origin.snapshots.jackson3.serializers.v1.DeterministicJackson3SnapshotSerializer +comparator=au.com.origin.snapshots.comparators.PlainTextEqualsComparator +reporters=au.com.origin.snapshots.reporters.PlainTextSnapshotReporter +snapshot-dir=__snapshots__ +output-dir=src/test/java +ci-env-var=CI +update-snapshot=none diff --git a/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/EnableSnapshots.groovy b/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/EnableSnapshots.groovy index 9379c8c6..8fdf1f5d 100644 --- a/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/EnableSnapshots.groovy +++ b/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/EnableSnapshots.groovy @@ -1,11 +1,10 @@ package au.com.origin.snapshots.spock -import org.spockframework.runtime.extension.ExtensionAnnotation - import java.lang.annotation.ElementType import java.lang.annotation.Retention import java.lang.annotation.RetentionPolicy import java.lang.annotation.Target +import org.spockframework.runtime.extension.ExtensionAnnotation @Retention(RetentionPolicy.RUNTIME) @Target([ElementType.TYPE, ElementType.METHOD]) diff --git a/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/SnapshotExtension.groovy b/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/SnapshotExtension.groovy index 1ec0ae43..eba93058 100644 --- a/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/SnapshotExtension.groovy +++ b/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/SnapshotExtension.groovy @@ -9,20 +9,20 @@ import org.spockframework.runtime.model.SpecInfo class SnapshotExtension extends AbstractAnnotationDrivenExtension implements SnapshotConfigInjector { - SnapshotVerifier snapshotVerifier; + SnapshotVerifier snapshotVerifier; - void visitSpecAnnotation(EnableSnapshots annotation, SpecInfo spec) { - this.snapshotVerifier = new SnapshotVerifier(getSnapshotConfig(), spec.reflection, false) - } + void visitSpecAnnotation(EnableSnapshots annotation, SpecInfo spec) { + this.snapshotVerifier = new SnapshotVerifier(getSnapshotConfig(), spec.reflection, false) + } - void visitSpec(SpecInfo spec) { - def snapshotMethodInterceptor = new SnapshotMethodInterceptor(snapshotVerifier) - spec.allFeatures.featureMethod*.addInterceptor(snapshotMethodInterceptor) - spec.addCleanupSpecInterceptor(snapshotMethodInterceptor) - } + void visitSpec(SpecInfo spec) { + def snapshotMethodInterceptor = new SnapshotMethodInterceptor(snapshotVerifier) + spec.allFeatures.featureMethod*.addInterceptor(snapshotMethodInterceptor) + spec.addCleanupSpecInterceptor(snapshotMethodInterceptor) + } - @Override - SnapshotConfig getSnapshotConfig() { - return new PropertyResolvingSnapshotConfig() - } + @Override + SnapshotConfig getSnapshotConfig() { + return new PropertyResolvingSnapshotConfig() + } } \ No newline at end of file diff --git a/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/SnapshotMethodInterceptor.groovy b/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/SnapshotMethodInterceptor.groovy index ac94958c..0f6b7898 100644 --- a/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/SnapshotMethodInterceptor.groovy +++ b/java-snapshot-testing-spock/src/main/groovy/au/com/origin/snapshots/spock/SnapshotMethodInterceptor.groovy @@ -1,58 +1,57 @@ package au.com.origin.snapshots.spock import au.com.origin.snapshots.Expect -import au.com.origin.snapshots.utils.ReflectionUtils import au.com.origin.snapshots.SnapshotVerifier import au.com.origin.snapshots.logging.LoggingHelper +import au.com.origin.snapshots.utils.ReflectionUtils +import java.lang.reflect.Method import org.slf4j.LoggerFactory import org.spockframework.runtime.extension.AbstractMethodInterceptor import org.spockframework.runtime.extension.IMethodInvocation -import java.lang.reflect.Method - // Based on this issue: https://github.com/spockframework/spock/issues/652 class SnapshotMethodInterceptor extends AbstractMethodInterceptor { - private log = LoggerFactory.getLogger( SnapshotMethodInterceptor.class ) - private final SnapshotVerifier snapshotVerifier; - - SnapshotMethodInterceptor(SnapshotVerifier snapshotVerifier) { - this.snapshotVerifier = snapshotVerifier - } - - @Override - void interceptFeatureMethod(IMethodInvocation invocation) throws Throwable { - updateInstanceVariable(invocation.instance, invocation.feature.featureMethod.reflection) - - def parameterCount = invocation.method.reflection.parameterCount - if (parameterCount > invocation.arguments.length) { - def newArguments = new Object[parameterCount] - System.arraycopy invocation.arguments, 0, newArguments, 0, invocation.arguments.length - invocation.arguments = newArguments - } - invocation.method.reflection.parameterTypes.eachWithIndex { type, i -> - if (Expect.class == type) { - LoggingHelper.deprecatedV5(log, "Injecting 'Expect' via method a argument is no longer recommended. Consider using instance variable injection instead.") - invocation.arguments[i] = new Expect(snapshotVerifier, invocation.feature.featureMethod.reflection) - } - } - invocation.proceed() - } - - private void updateInstanceVariable(Object testInstance, Method testMethod) { - ReflectionUtils.findFieldByPredicate(testInstance.class, { field -> field.getType() == Expect.class }) - .ifPresent({ field -> - Expect expect = Expect.of(snapshotVerifier, testMethod); - ReflectionUtils.makeAccessible(field); - try { - field.set(testInstance, expect); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - }); - } - - @Override - void interceptCleanupSpecMethod(IMethodInvocation invocation) throws Throwable { - this.snapshotVerifier.validateSnapshots(); - } + private log = LoggerFactory.getLogger( SnapshotMethodInterceptor.class ) + private final SnapshotVerifier snapshotVerifier; + + SnapshotMethodInterceptor(SnapshotVerifier snapshotVerifier) { + this.snapshotVerifier = snapshotVerifier + } + + @Override + void interceptFeatureMethod(IMethodInvocation invocation) throws Throwable { + updateInstanceVariable(invocation.instance, invocation.feature.featureMethod.reflection) + + def parameterCount = invocation.method.reflection.parameterCount + if (parameterCount > invocation.arguments.length) { + def newArguments = new Object[parameterCount] + System.arraycopy invocation.arguments, 0, newArguments, 0, invocation.arguments.length + invocation.arguments = newArguments + } + invocation.method.reflection.parameterTypes.eachWithIndex { type, i -> + if (Expect.class == type) { + LoggingHelper.deprecatedV5(log, "Injecting 'Expect' via method a argument is no longer recommended. Consider using instance variable injection instead.") + invocation.arguments[i] = new Expect(snapshotVerifier, invocation.feature.featureMethod.reflection) + } + } + invocation.proceed() + } + + private void updateInstanceVariable(Object testInstance, Method testMethod) { + ReflectionUtils.findFieldByPredicate(testInstance.class, { field -> field.getType() == Expect.class }) + .ifPresent({ field -> + Expect expect = Expect.of(snapshotVerifier, testMethod); + ReflectionUtils.makeAccessible(field); + try { + field.set(testInstance, expect); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + void interceptCleanupSpecMethod(IMethodInvocation invocation) throws Throwable { + this.snapshotVerifier.validateSnapshots(); + } } diff --git a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/SpecificationBase.groovy b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/SpecificationBase.groovy index c5f73890..5b47cd91 100644 --- a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/SpecificationBase.groovy +++ b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/SpecificationBase.groovy @@ -6,5 +6,5 @@ import spock.lang.Specification @RunWith(Sputnik.class) class SpecificationBase extends Specification { - Expect expect; + Expect expect; } diff --git a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/SpockExtensionUsedSpec.groovy b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/SpockExtensionUsedSpec.groovy index e9bd9eac..3a7b26d0 100644 --- a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/SpockExtensionUsedSpec.groovy +++ b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/SpockExtensionUsedSpec.groovy @@ -8,72 +8,71 @@ import spock.lang.Unroll @EnableSnapshots class SpockExtensionUsedSpec extends Specification { - Expect expect - - @SnapshotName("Should use extension") - def "Should use extension"(Expect expect) { - when: - expect.toMatchSnapshot("Hello World") - - then: - true - } - - @SnapshotName("Should use extension again") - def "Should use extension again"(Expect expect) { - when: - expect.toMatchSnapshot("Hello World") - - then: - true - } - - @SnapshotName("Should use extension via instance variable") - def "Should use extension via instance variable"() { - when: - expect.toMatchSnapshot("Hello World") - - then: - true - } - - @SnapshotName("DataTable example 1") - @Unroll - def 'DataTable example 1: #letter'(def letter) { - given: 'I use an @Unroll function' - String result = letter.toUpperCase() - - when: 'I snapshot the letter' - expect.scenario("letter $letter").toMatchSnapshot(result) - - then: - true - - where: - [letter] << [['A'],['B'],['C']] - } - - - @SnapshotName("DataTable example 2") - def 'DataTable example 2: #scenario to uppercase'() { - when: 'I convert to uppercase' - String result = value.toUpperCase(); - then: 'Should convert letters to uppercase' - // Check you snapshot against your output using a unique scenario - expect.scenario(scenario).toMatchSnapshot(result) - where: - scenario | value - 'letter' | 'a' - 'number' | '1' - } - - @SnapshotName("Can run a non snapshot test") - def "Can run a non snapshot test"() { - when: - def isTrue = true - - then: - isTrue - } - + Expect expect + + @SnapshotName("Should use extension") + def "Should use extension"(Expect expect) { + when: + expect.toMatchSnapshot("Hello World") + + then: + true + } + + @SnapshotName("Should use extension again") + def "Should use extension again"(Expect expect) { + when: + expect.toMatchSnapshot("Hello World") + + then: + true + } + + @SnapshotName("Should use extension via instance variable") + def "Should use extension via instance variable"() { + when: + expect.toMatchSnapshot("Hello World") + + then: + true + } + + @SnapshotName("DataTable example 1") + @Unroll + def 'DataTable example 1: #letter'(def letter) { + given: 'I use an @Unroll function' + String result = letter.toUpperCase() + + when: 'I snapshot the letter' + expect.scenario("letter $letter").toMatchSnapshot(result) + + then: + true + + where: + [letter] << [['A'], ['B'], ['C']] + } + + + @SnapshotName("DataTable example 2") + def 'DataTable example 2: #scenario to uppercase'() { + when: 'I convert to uppercase' + String result = value.toUpperCase(); + then: 'Should convert letters to uppercase' + // Check you snapshot against your output using a unique scenario + expect.scenario(scenario).toMatchSnapshot(result) + where: + scenario | value + 'letter' | 'a' + 'number' | '1' + } + + @SnapshotName("Can run a non snapshot test") + def "Can run a non snapshot test"() { + when: + def isTrue = true + + then: + isTrue + } } diff --git a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/TestBaseSpec.groovy b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/TestBaseSpec.groovy index 9383a837..eea73cce 100644 --- a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/TestBaseSpec.groovy +++ b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/TestBaseSpec.groovy @@ -6,13 +6,12 @@ import au.com.origin.snapshots.spock.EnableSnapshots @EnableSnapshots class TestBaseSpec extends SpecificationBase { - @SnapshotName("Should use extension") - def "Should use extension"() { - when: - expect.toMatchSnapshot("Hello World") - - then: - true - } + @SnapshotName("Should use extension") + def "Should use extension"() { + when: + expect.toMatchSnapshot("Hello World") + then: + true + } } diff --git a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/docs/SpockExample.groovy b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/docs/SpockExample.groovy index 2eff02c4..22ced015 100644 --- a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/docs/SpockExample.groovy +++ b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/docs/SpockExample.groovy @@ -9,26 +9,26 @@ import spock.lang.Specification @EnableSnapshots class SpockExample extends Specification { - // Option 1: inject Expect as an instance variable - private Expect expect + // Option 1: inject Expect as an instance variable + private Expect expect - // With spock tests you should always use @SnapshotName - otherwise they become coupled to test order - @SnapshotName("should_use_extension") - def "Should use extension"() { - when: - expect.toMatchSnapshot("Hello World") + // With spock tests you should always use @SnapshotName - otherwise they become coupled to test order + @SnapshotName("should_use_extension") + def "Should use extension"() { + when: + expect.toMatchSnapshot("Hello World") - then: - true - } + then: + true + } - @SnapshotName("should_use_extension_as_method_argument") - // Option 2: inject Expect into the method signature - def "Should use extension as method argument"(Expect expect) { - when: - expect.toMatchSnapshot("Hello World") + @SnapshotName("should_use_extension_as_method_argument") + // Option 2: inject Expect into the method signature + def "Should use extension as method argument"(Expect expect) { + when: + expect.toMatchSnapshot("Hello World") - then: - true - } + then: + true + } } diff --git a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/docs/SpockWithParametersExample.groovy b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/docs/SpockWithParametersExample.groovy index 855bbcd9..81e343ce 100644 --- a/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/docs/SpockWithParametersExample.groovy +++ b/java-snapshot-testing-spock/src/test/groovy/au/com/origin/snapshots/docs/SpockWithParametersExample.groovy @@ -8,18 +8,18 @@ import spock.lang.Specification @EnableSnapshots class SpockWithParametersExample extends Specification { - private Expect expect + private Expect expect - @SnapshotName("convert_to_uppercase") - def 'Convert #scenario to uppercase'() { - when: 'I convert to uppercase' - String result = value.toUpperCase(); - then: 'Should convert letters to uppercase' - // Check you snapshot against your output using a unique scenario - expect.scenario(scenario).toMatchSnapshot(result) - where: - scenario | value - 'letter' | 'a' - 'number' | '1' - } + @SnapshotName("convert_to_uppercase") + def 'Convert #scenario to uppercase'() { + when: 'I convert to uppercase' + String result = value.toUpperCase(); + then: 'Should convert letters to uppercase' + // Check you snapshot against your output using a unique scenario + expect.scenario(scenario).toMatchSnapshot(result) + where: + scenario | value + 'letter' | 'a' + 'number' | '1' + } } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 05e3e32f..5a463474 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,12 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' +} + rootProject.name = 'java-snapshot-testing' -include 'java-snapshot-testing-core', 'java-snapshot-testing-junit4', 'java-snapshot-testing-junit5', 'java-snapshot-testing-spock', 'java-snapshot-testing-plugin-jackson' \ No newline at end of file +include 'java-snapshot-testing-core', + 'java-snapshot-testing-junit4', + 'java-snapshot-testing-junit5', + 'java-snapshot-testing-spock', + 'java-snapshot-testing-plugin-jackson', + 'java-snapshot-testing-plugin-jackson3'