From 12ce519aafb8193cf7cc77de78e5321f33de6a0f Mon Sep 17 00:00:00 2001 From: Ruben Gees Date: Tue, 25 Jun 2024 19:44:15 +0200 Subject: [PATCH] Modernize project --- .editorconfig | 10 +- .github/workflows/{publish.yml => cd.yml} | 15 +- .github/workflows/ci.yml | 30 +-- README.md | 12 +- RELEASING.md | 2 +- build.gradle | 220 ------------------ build.gradle.kts | 160 +++++++++++++ gradle.properties | 2 - gradle/libs.versions.toml | 76 ++++++ gradle/versions.gradle | 44 ---- gradle/wrapper/gradle-wrapper.jar | Bin 61624 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 34 +-- gradlew.bat | 22 +- .../de/smartsquare/squit/SquitExtension.kt | 70 +++--- .../de/smartsquare/squit/SquitPlugin.kt | 45 ++-- .../squit/config/ConfigExtensions.kt | 31 +-- .../smartsquare/squit/config/TestIndexer.kt | 138 +++++------ .../entity/SquitDatabaseConfiguration.kt | 2 +- .../smartsquare/squit/entity/SquitMetaInfo.kt | 6 +- .../squit/entity/SquitResponseInfo.kt | 10 +- .../smartsquare/squit/entity/SquitResult.kt | 2 +- .../squit/entity/SquitResultTree.kt | 6 +- .../de/smartsquare/squit/entity/SquitTest.kt | 26 +-- .../de/smartsquare/squit/io/FilesUtils.kt | 15 +- .../squit/mediatype/BodyProcessor.kt | 4 +- .../squit/mediatype/MediaTypeConfig.kt | 4 +- .../mediatype/generic/GenericBodyProcessor.kt | 4 +- .../mediatype/generic/GenericCanonicalizer.kt | 4 +- .../squit/mediatype/generic/GenericDiffer.kt | 2 +- .../squit/mediatype/json/JsonBodyProcessor.kt | 16 +- .../squit/mediatype/json/JsonCanonicalizer.kt | 35 ++- .../squit/mediatype/json/JsonDiffer.kt | 18 +- .../squit/mediatype/xml/XmlBodyProcessor.kt | 16 +- .../squit/mediatype/xml/XmlCanonicalizer.kt | 39 ++-- .../squit/report/HtmlReportWriter.kt | 94 ++++---- .../squit/task/SquitPostProcessRunner.kt | 13 +- .../squit/task/SquitPostProcessTask.kt | 82 +++---- .../squit/task/SquitPostTestTask.kt | 4 +- .../squit/task/SquitPreProcessRunner.kt | 32 +-- .../squit/task/SquitPreProcessTask.kt | 81 +++---- .../squit/task/SquitPreTestTask.kt | 4 +- .../squit/task/SquitRequestTask.kt | 126 +++++----- .../smartsquare/squit/task/SquitTestTask.kt | 133 ++++++----- .../smartsquare/squit/util/UtilExtensions.kt | 21 ++ .../squit/GradleCompatibilityTest.kt | 31 ++- .../kotlin/de/smartsquare/squit/TestUtils.kt | 40 +--- .../squit/config/ConfigExtensionsTest.kt | 136 +++++------ .../squit/config/TestIndexerTest.kt | 6 +- .../squit/db/ConnectionCollectionTest.kt | 7 + .../squit/entity/SquitResultTest.kt | 10 +- .../squit/entity/SquitResultTreeTest.kt | 11 +- .../smartsquare/squit/entity/SquitTestTest.kt | 20 +- .../de/smartsquare/squit/io/FilesUtilsTest.kt | 4 +- .../squit/io/JsonParserSupportTest.kt | 2 +- .../squit/io/SAXReaderSupportTest.kt | 2 +- .../mediatype/json/JsonCanonicalizerTest.kt | 12 +- .../mediatype/xml/XmlCanonicalizerTest.kt | 16 +- .../task/SquitPostProcessTaskJsonTest.kt | 7 +- ...uitPostProcessTaskPreviousTaskErrorTest.kt | 4 +- .../squit/task/SquitPostProcessTaskTest.kt | 21 +- ...quitPreProcessTaskDifferentEncodingTest.kt | 12 +- .../squit/task/SquitPreProcessTaskGetTest.kt | 6 +- .../SquitPreProcessTaskInvalidConfTest.kt | 2 +- .../SquitPreProcessTaskInvalidResponseTest.kt | 4 +- .../squit/task/SquitPreProcessTaskJsonTest.kt | 7 +- .../task/SquitPreProcessTaskOptionsTest.kt | 6 +- .../SquitPreProcessTaskPlaceholderTest.kt | 7 +- .../squit/task/SquitPreProcessTaskTest.kt | 33 ++- .../SquitRequestTaskConfigurableTasksTest.kt | 33 ++- .../squit/task/SquitRequestTaskGetTest.kt | 5 +- .../task/SquitRequestTaskInvalidSqlTest.kt | 7 +- .../squit/task/SquitRequestTaskJsonTest.kt | 9 +- .../squit/task/SquitRequestTaskOptionsTest.kt | 5 +- .../SquitRequestTaskPreviousTaskErrorTest.kt | 4 +- .../squit/task/SquitRequestTaskTest.kt | 40 ++-- .../SquitTestTaskDifferentStructureTest.kt | 7 +- .../SquitTestTaskErrorResponseCodeTest.kt | 10 +- .../task/SquitTestTaskIgnoreFailuresTest.kt | 5 +- .../SquitTestTaskJsonDifferentOrderTest.kt | 9 +- .../squit/task/SquitTestTaskJsonTest.kt | 5 +- .../SquitTestTaskNonStrictXmlDiffingTest.kt | 5 +- .../SquitTestTaskPreviousTaskErrorTest.kt | 4 +- .../squit/task/SquitTestTaskTest.kt | 27 ++- .../squit/util/UtilExtensionsTest.kt | 2 +- 85 files changed, 1124 insertions(+), 1172 deletions(-) rename .github/workflows/{publish.yml => cd.yml} (60%) delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 gradle/libs.versions.toml delete mode 100644 gradle/versions.gradle diff --git a/.editorconfig b/.editorconfig index 713e7657..0f3746a8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,11 +7,11 @@ max_line_length = 120 insert_final_newline = true trim_trailing_whitespace = true -[*.kt] -continuation_indent_size = 4 - -[*.{java,gradle}] -continuation_indent_size = 8 +[*.{kt,kts}] +ij_continuation_indent_size = 4 +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ktlint_code_style = intellij_idea [*.{yml,json,conf}] indent_size = 2 diff --git a/.github/workflows/publish.yml b/.github/workflows/cd.yml similarity index 60% rename from .github/workflows/publish.yml rename to .github/workflows/cd.yml index 66e498f7..35050426 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/cd.yml @@ -5,17 +5,20 @@ on: types: [ created ] jobs: - build: + publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 8 - cache: 'gradle' + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - - name: Publish with Gradle + - name: Publish run: ./gradlew publishPlugins -Pgradle.publish.key=${{ secrets.PUBLISH_KEY }} -Pgradle.publish.secret=${{ secrets.PUBLISH_SECRET }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc1ee43d..9a9e13ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,9 @@ name: CI - -on: [push, pull_request] +on: + push: + branches: + - main + pull_request: jobs: build: @@ -10,27 +13,26 @@ jobs: os: - ubuntu-latest - windows-latest - java: [8, 11, 17] + java: [17, 21] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: ${{ matrix.java }} - cache: 'gradle' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - name: Build with Gradle - if: matrix.os == 'ubuntu-latest' run: ./gradlew build --stacktrace - - name: Build with Gradle - if: matrix.os == 'windows-latest' - run: .\gradlew.bat build --stacktrace - name: Store reports - uses: actions/upload-artifact@v3 - if: always() + uses: actions/upload-artifact@v4 + if: failure() with: - name: reports-${{ runner.os }}-${{ matrix.java }} - path: build/reports + name: "Gradle Reports ${{ runner.os }} ${{ matrix.java }}" + path: "**/build/reports" diff --git a/README.md b/README.md index b4dca3a6..83f86be9 100644 --- a/README.md +++ b/README.md @@ -362,21 +362,21 @@ Additionally, these options are evaluated: ### Squit Dsl -Here is a complete example of the `Squit` dsl: +Here is a complete example of the `Squit` dsl (`.kts` syntax): -```groovy +```kotlin squit { // The jdbc drivers to use. Must be on the classpath. - jdbcDrivers = ['oracle.jdbc.driver.OracleDriver'] + jdbcDrivers = listOf("oracle.jdbc.driver.OracleDriver") // The path of your test sources. src/squit is the default. - sourceDir "src/squit" + sourceDir = "src/squit" // The path to save reports in. build/squit/reports is the default. - reportDir "build/squit/reports" + reportDir = "build/squit/reports" // The timeout in seconds for requests before squit fails. The default is 10. - timeout = 60 + requestTimeout = 60 // If Squit should only print if a tests fails. silent = false diff --git a/RELEASING.md b/RELEASING.md index 82daf8c3..e93509cc 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,4 +1,4 @@ -**The following steps are required to do a release of Squit:** +Squit will be automatically released by creating a release on GitHub. To manually release Squit: - Log into the Gradle Plugins Portal with the `login` Gradle task: diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 7040b660..00000000 --- a/build.gradle +++ /dev/null @@ -1,220 +0,0 @@ -import org.gradle.api.internal.file.FileOperations -import org.gradle.internal.jacoco.JacocoAgentJar -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.jlleitschuh.gradle.ktlint.reporter.ReporterType - -buildscript { - apply from: "./gradle/versions.gradle" - - repositories { - gradlePluginPortal() - mavenCentral() - } -} - -plugins { - id "com.gradle.plugin-publish" version "$gradlePublishVersion" - - id 'org.jetbrains.kotlin.jvm' version "$kotlinVersion" - id "org.jetbrains.dokka" version "$dokkaVersion" - - id "io.gitlab.arturbosch.detekt" version "$detektVersion" - id "org.jlleitschuh.gradle.ktlint" version "$ktlintPluginVersion" - id "com.github.ben-manes.versions" version "$gradleVersionsVersion" - - id "java-gradle-plugin" - id "java-library" - id "maven-publish" - id "jacoco" -} - -group = "de.smartsquare" -version = squitVersion - -repositories { - mavenCentral() -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" - implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" - implementation "org.jetbrains.kotlinx:kotlinx-html-jvm:$kotlinHtmlVersion" - - api "org.dom4j:dom4j:$dom4jVersion" - api "com.google.code.gson:gson:$gsonVersion" - api "com.typesafe:config:$typesafeConfigVersion" - - implementation "jaxen:jaxen:$jaxenVersion" - implementation "pull-parser:pull-parser:$pullParserVersion" - implementation "org.apache.santuario:xmlsec:$xmlSecVersion" - implementation "org.xmlunit:xmlunit-core:$xmlUnitVersion" - implementation "net.javacrumbs.json-unit:json-unit:$jsonUnitVersion" - implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" - implementation "se.sawano.java:alphanumeric-comparator:$alphanumericComparatorVersion" - implementation("com.github.wumpz:diffutils:$diffUtilsVersion") { - exclude group: "org.eclipse.jgit", module: "*" - } - - implementation "org.webjars.npm:jquery:$jqueryVersion" - implementation "org.webjars.npm:bootstrap:$bootstrapVersion" - implementation "org.webjars.npm:popper.js:$popperVersion" - implementation "org.webjars:font-awesome:$fontAwesomeVersion" - implementation "org.webjars.npm:marked:$markedVersion" - implementation("org.webjars.npm:diff2html:$diff2htmlVersion") { - exclude group: "org.webjars.npm", module: "*" - } - - testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" - testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" - testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" - testImplementation "org.amshove.kluent:kluent:$kluentVersion" - testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" - testImplementation "com.h2database:h2:$h2Version" - testImplementation "io.mockk:mockk:$mockkVersion" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - testRuntimeOnly "net.bytebuddy:byte-buddy:$byteBuddyVersion" -} - -sourceSets { - test { - resources { - srcDir "$buildDir/testkit" - } - } -} - -jacoco { - toolVersion = jacocoVersion -} - -kotlin { - jvmToolchain { - languageVersion = JavaLanguageVersion.of(project.ext.javaVersion.majorVersion) - } -} - -tasks.withType(KotlinCompile).all { - kotlinOptions { - freeCompilerArgs = ["-Xjsr305=strict"] - allWarningsAsErrors = true - jvmTarget = project.ext.javaVersion - } -} - -// Generates a Gradle properties file with the jacoco javaagent as a jvmarg for Gradle runner tests. -// This allows to generate coverage for code run in the forked JVM. -task generateTestkitFiles { - def jacocoJar = configurations[JacocoPlugin.AGENT_CONFIGURATION_NAME].asPath.toString() - - def jacocoAgentJar = new JacocoAgentJar(project.services.get(FileOperations)).tap { - agentConf = project.files(jacocoJar) - } - - def javaagent = jacocoAgentJar.getJar() - def destfile = new File(buildDir, "jacoco/testkit.exec") - - def jvmArgs = "org.gradle.jvmargs=-javaagent:$javaagent=destfile=$destfile".replace("\\", "\\\\") - def outputDir = file("$buildDir/testkit") - - inputs.files jacocoJar - outputs.dir outputDir - - doLast { - outputDir.mkdirs() - - file("$outputDir/testkit-gradle.properties").text = jvmArgs - } -} - -test { - useJUnitPlatform() - - dependsOn generateTestkitFiles - finalizedBy jacocoTestReport -} - -jacocoTestReport { - getExecutionData().setFrom(fileTree(buildDir).include("/jacoco/*.exec")) -} - -tasks.named("processTestResources") { - dependsOn generateTestkitFiles -} - -ktlint { - disabledRules = ["import-ordering"] - - reporters { - reporter ReporterType.CHECKSTYLE - } -} - -detekt { - config = files("${rootProject.projectDir}/detekt.yml") - input = files("$projectDir/src/main/kotlin") - buildUponDefaultConfig = true -} - -dokkaHtml { - outputDirectory.set(javadoc.destinationDir) - - dokkaSourceSets { - configureEach { - jdkVersion.set(8) - - sourceLink { - localDirectory.set(file("src/main/kotlin")) - remoteUrl.set(new URL("https://github.com/SmartsquareGmbH/squit/blob/master/src/main/kotlin")) - remoteLineSuffix.set("#L") - } - - externalDocumentationLink { - url.set(new URL("https://dom4j.github.io/javadoc/$dom4jVersion/")) - } - - externalDocumentationLink { - url.set(new URL("https://lightbend.github.io/config/latest/api/")) - } - - externalDocumentationLink { - url.set(new URL("https://www.javadoc.io/doc/com.google.code.gson/gson/$gsonVersion/com.google.gson/")) - } - } - } -} - -dependencyUpdates { - rejectVersionIf { - isNonStable(it.candidate.version) && !isNonStable(it.currentVersion) - } -} - -static def isNonStable(String version) { - def stableKeyword = ["RELEASE", "FINAL", "GA"].any { it -> version.toUpperCase().contains(it) } - def regex = /^[0-9,.v-]+(-r)?$/ - - return !stableKeyword && !(version ==~ regex) -} - -tasks.withType(Javadoc).all { - it.dependsOn dokkaHtml -} - -gradlePlugin { - website = "https://github.com/SmartsquareGmbH/squit" - vcsUrl = "https://github.com/SmartsquareGmbH/squit" - - plugins { - squit { - displayName = "Squit" - id = "de.smartsquare.squit" - implementationClass = "de.smartsquare.squit.SquitPlugin" - description = "Gradle plugin for simple testing of JSON/XML/SOAP/etc APIs." - tags.set(["testing", "soap", "xml" ,"automation"]) - } - } -} - -wrapper { - gradleVersion = project.ext.gradleVersion -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..41f86005 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,160 @@ +import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask + +plugins { + kotlin("jvm") version libs.versions.kotlin + alias(libs.plugins.pluginPublish) + alias(libs.plugins.dokkatooHtml) + alias(libs.plugins.dokkatooJavadoc) + alias(libs.plugins.detekt) + alias(libs.plugins.kotlinter) + alias(libs.plugins.jacocoTestkit) + alias(libs.plugins.gradleVersions) + `java-gradle-plugin` + `maven-publish` + jacoco +} + +group = "de.smartsquare" +version = libs.versions.squit.get() + +repositories { + mavenCentral() +} + +dependencies { + api(libs.dom4j) + api(libs.gson) + api(libs.typesafeConfig) + + implementation(libs.jaxen) + implementation(libs.pullParser) + implementation(libs.xmlSec) + implementation(libs.xmlUnit) + implementation(libs.jsonUnit) + implementation(libs.okhttp) + implementation(libs.kotlinxHtml) + implementation(libs.alphanumericComparator) + implementation(libs.diffUtils) { + exclude(group = "org.eclipse.jgit") + } + + implementation(libs.jquery) + implementation(libs.bootstrap) + implementation(libs.popper) + implementation(libs.fontAwesome) + implementation(libs.marked) + implementation(libs.diff2html) { + exclude(group = "org.webjars.npm") + } + + testImplementation(gradleTestKit()) + testImplementation(kotlin("reflect")) + testImplementation(libs.junit) + testImplementation(libs.junitParams) + testImplementation(libs.h2) + testImplementation(libs.kluent) + testImplementation(libs.mockwebserver) + testImplementation(libs.mockk) + + testRuntimeOnly(libs.junitPlatformLauncher) +} + +java { + withJavadocJar() + withSourcesJar() +} + +kotlin { + jvmToolchain { + languageVersion = JavaLanguageVersion.of(libs.versions.java.get()) + } + + compilerOptions { + allWarningsAsErrors = true + } +} + +tasks.named("test") { + useJUnitPlatform() + + finalizedBy(tasks.jacocoTestReport) +} + +jacoco { + toolVersion = libs.versions.jacoco.get() +} + +detekt { + config.from(file("${rootDir}/detekt.yml")) + + buildUponDefaultConfig = true +} + +tasks.named("javadocJar") { + from(tasks.named("dokkatooGeneratePublicationJavadoc")) +} + +dokkatoo { + val dom4jVersion = resolveVersion("org.dom4j:dom4j") + val typesafeConfigVersion = resolveVersion("com.typesafe:config") + val gsonVersion = resolveVersion("com.google.code.gson:gson") + + dokkatooSourceSets.configureEach { + externalDocumentationLinks.create("gradle") { + url("https://docs.gradle.org/${gradle.gradleVersion}/javadoc/") + packageListUrl("https://docs.gradle.org/${gradle.gradleVersion}/javadoc/element-list") + } + + externalDocumentationLinks.create("dom4j") { + url("https://javadoc.io/doc/org.dom4j/dom4j/$dom4jVersion/") + } + + externalDocumentationLinks.create("config") { + url("https://javadoc.io/doc/com.typesafe/config/$typesafeConfigVersion/") + } + + externalDocumentationLinks.create("gson") { + url("https://javadoc.io/doc/com.google.code.gson/gson/$gsonVersion") + packageListUrl("https://javadoc.io/doc/com.google.code.gson/gson/$gsonVersion/element-list") + } + } +} + +fun resolveVersion(dependency: String): String { + return project.configurations.getByName("runtimeClasspath").resolvedConfiguration.resolvedArtifacts + .find { it.moduleVersion.id.module.toString() == dependency } + ?.moduleVersion?.id?.version + ?: "latest" +} + +gradlePlugin { + website = "https://github.com/SmartsquareGmbH/squit" + vcsUrl = "https://github.com/SmartsquareGmbH/squit" + + plugins { + create("squit") { + displayName = "Squit" + id = "de.smartsquare.squit" + implementationClass = "de.smartsquare.squit.SquitPlugin" + description = "Gradle plugin for simple testing of JSON/XML/SOAP/etc APIs." + tags = listOf("testing", "soap", "xml", "automation") + } + } +} + +tasks.withType { + rejectVersionIf { + isNonStable(candidate.version) + } +} + +fun isNonStable(version: String): Boolean { + val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) } + val regex = "^[0-9,.v-]+(-r)?$".toRegex() + val isStable = stableKeyword || regex.matches(version) + return !isStable +} + +tasks.withType { + gradleVersion = libs.versions.gradle.get() +} diff --git a/gradle.properties b/gradle.properties index 5b28bccb..1803c6b3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,3 @@ org.gradle.parallel=true org.gradle.caching=true kotlin.code.style=official -kapt.use.worker.api=false -kapt.include.compile.classpath=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..c12af1fd --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,76 @@ +[versions] +java = "17" +gradle = "8.9" +squit = "6.0.0" + +gradlePublish = "1.2.1" +kotlin = "2.0.0" +dokkatoo = "2.3.1" +detekt = "1.23.6" +kotlinter = "4.4.1" +jacoco = "0.8.12" +jacocoTestkit = "1.0.12" +gradleVersions = "0.51.0" + +jaxen = "2.0.0" +dom4j = "2.1.4" +pullParser = "2.1.10" +gson = "2.11.0" +xmlSec = "4.0.2" +xmlUnit = "2.10.0" +jsonUnit = "3.4.0" +typesafeConfig = "1.4.3" +okhttp = "4.12.0" +kotlinxHtml = "0.11.0" +diffUtils = "3.0" +alphanumericComparator = "1.4.1" + +bootstrap = "4.6.0" +jquery = "3.6.0" +popper = "1.16.1" +fontAwesome = "5.15.4" +diff2html = "3.1.7" +marked = "2.0.6" + +junit = "5.10.3" +kluent = "1.73" +h2 = "2.2.224" +mockk = "1.13.11" + +[plugins] +pluginPublish = { id = "com.gradle.plugin-publish", version.ref = "gradlePublish" } +dokkatooHtml = { id = "dev.adamko.dokkatoo-html", version.ref = "dokkatoo" } +dokkatooJavadoc = { id = "dev.adamko.dokkatoo-javadoc", version.ref = "dokkatoo" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +kotlinter = { id = "org.jmailen.kotlinter", version.ref = "kotlinter" } +jacocoTestkit = { id = "pl.droidsonroids.jacoco.testkit", version.ref = "jacocoTestkit" } +gradleVersions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" } + +[libraries] +kotlinxHtml = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version.ref = "kotlinxHtml" } +dom4j = { module = "org.dom4j:dom4j", version.ref = "dom4j" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +typesafeConfig = { module = "com.typesafe:config", version.ref = "typesafeConfig" } +jaxen = { module = "jaxen:jaxen", version.ref = "jaxen" } +pullParser = { module = "pull-parser:pull-parser", version.ref = "pullParser" } +xmlSec = { module = "org.apache.santuario:xmlsec", version.ref = "xmlSec" } +xmlUnit = { module = "org.xmlunit:xmlunit-core", version.ref = "xmlUnit" } +jsonUnit = { module = "net.javacrumbs.json-unit:json-unit", version.ref = "jsonUnit" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +alphanumericComparator = { module = "se.sawano.java:alphanumeric-comparator", version.ref = "alphanumericComparator" } +diffUtils = { module = "com.github.wumpz:diffutils", version.ref = "diffUtils" } + +jquery = { module = "org.webjars.npm:jquery", version.ref = "jquery" } +bootstrap = { module = "org.webjars.npm:bootstrap", version.ref = "bootstrap" } +popper = { module = "org.webjars.npm:popper.js", version.ref = "popper" } +fontAwesome = { module = "org.webjars:font-awesome", version.ref = "fontAwesome" } +marked = { module = "org.webjars.npm:marked", version.ref = "marked" } +diff2html = { module = "org.webjars.npm:diff2html", version.ref = "diff2html" } + +junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } +junitParams = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } +junitPlatformLauncher = { module = "org.junit.platform:junit-platform-launcher" } +kluent = { module = "org.amshove.kluent:kluent", version.ref = "kluent" } +mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } +h2 = { module = "com.h2database:h2", version.ref = "h2" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } diff --git a/gradle/versions.gradle b/gradle/versions.gradle deleted file mode 100644 index 50bb3aad..00000000 --- a/gradle/versions.gradle +++ /dev/null @@ -1,44 +0,0 @@ -ext { - javaVersion = JavaVersion.VERSION_1_8 - gradleVersion = '7.6.2' - squitVersion = '5.2.0' - - gradlePublishVersion = '1.2.1' - - kotlinVersion = '1.7.21' - dokkaVersion = '1.7.20' - - jacocoVersion = '0.8.8' - gradleVersionsVersion = '0.44.0' - gradleTestIdeaFixVersion = '0.1.0' - - jaxenVersion = '1.2.0' - dom4jVersion = '2.1.3' - pullParserVersion = '2.1.10' - gsonVersion = '2.10' - xmlSecVersion = '3.0.1' - xmlUnitVersion = '2.9.0' - jsonUnitVersion = '2.36.0' - typesafeConfigVersion = '1.4.2' - okhttpVersion = '4.10.0' - kotlinHtmlVersion = '0.8.0' - diffUtilsVersion = '3.0' - alphanumericComparatorVersion = "1.4.1" - - bootstrapVersion = '4.6.0' - jqueryVersion = '3.6.0' - popperVersion = '1.16.1' - fontAwesomeVersion = '5.15.4' - diff2htmlVersion = '3.1.7' - markedVersion = '2.0.6' - - detektVersion = '1.21.0' - ktlintPluginVersion = '11.0.0' - - junitVersion = '5.9.1' - junitPlatformVersion = '1.7.2' - kluentVersion = '1.72' - h2Version = '2.1.214' - mockkVersion = '1.13.2' - byteBuddyVersion = '1.12.19' -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index afba109285af78dbd2a1d187e33ac4f87c76e392..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch literal 43504 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ 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 TZiJi*F`$tkWLn) literal 61624 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�Zy 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4e86b927..09523c0e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68d..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +134,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f..9d21a218 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/src/main/kotlin/de/smartsquare/squit/SquitExtension.kt b/src/main/kotlin/de/smartsquare/squit/SquitExtension.kt index e5bf80ff..f2b1304b 100644 --- a/src/main/kotlin/de/smartsquare/squit/SquitExtension.kt +++ b/src/main/kotlin/de/smartsquare/squit/SquitExtension.kt @@ -3,84 +3,65 @@ package de.smartsquare.squit import org.gradle.api.Action import org.gradle.api.Project import org.gradle.api.file.DirectoryProperty -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.Internal +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property import org.gradle.api.tasks.Nested -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity +import javax.inject.Inject -/** - * Class containing the available extensions for the squit dsl. - */ -open class SquitExtension(private val project: Project) { +open class SquitExtension @Inject constructor(private val project: Project) { /** * Extension for xml configuration. */ - @get:Nested - val xml = XmlExtension() + @Nested + val xml: XmlExtension = project.objects.newInstance(XmlExtension::class.java, project) /** * Extension for json configuration. */ - @get:Nested - val json = JsonExtension() + @Nested + val json: JsonExtension = project.objects.newInstance(JsonExtension::class.java, project) /** * The jdbc driver classes to use. */ - @get:Input - var jdbcDrivers: List = emptyList() + val jdbcDrivers: ListProperty = project.objects.listProperty(String::class.java) /** * The path the sources lie in. Defaults to src/squit. */ - @get:InputDirectory - @get:PathSensitive(PathSensitivity.RELATIVE) val sourceDir: DirectoryProperty = project.objects.directoryProperty() - .convention(project.layout.projectDirectory.dir("src/squit")) /** * The path to save reports and possible failures in. */ - @get:OutputDirectory val reportDir: DirectoryProperty = project.objects.directoryProperty() - .convention(project.layout.buildDirectory.dir("squit/reports")) /** * The timeout in seconds to use for requests. */ - @get:Internal - var timeout = 10L + val requestTimeout: Property = project.objects.property(Long::class.java) /** * If squit should avoid printing anything if all tests pass. */ - @get:Internal - var silent = false + val silent: Property = project.objects.property(Boolean::class.java) /** * If failures should be ignored. * In that case the task passes, even if tests have failed. */ - @get:Input - var ignoreFailures = false + val ignoreFailures: Property = project.objects.property(Boolean::class.java) /** * Helper method to set the sourceDir via String. */ - fun sourceDir(path: String) = sourceDir.apply { - set(project.projectDir.resolve(path)) - } + fun sourceDir(path: String) = sourceDir.set(project.layout.projectDirectory.dir(path)) /** * Helper method to set the reportDir via String. */ - fun reportDir(path: String) = reportDir.apply { - set(project.projectDir.resolve(path)) - } + fun reportDir(path: String) = reportDir.set(project.layout.projectDirectory.dir(path)) /** * Configures the xml dsl. @@ -89,39 +70,42 @@ open class SquitExtension(private val project: Project) { action.execute(xml) } + /** + * Configures the json dsl. + */ + fun json(action: Action) { + action.execute(json) + } + /** * Class containing the available extensions for the xml dsl. */ - open class XmlExtension { + open class XmlExtension @Inject constructor(project: Project) { /** * If the xml diffing should use strict (e.g. identic) comparison. */ - @get:Input - var strict = true + val strict: Property = project.objects.property(Boolean::class.java) /** * If the html report should be canonicalized for xml tests. */ - @get:Input - var canonicalize = true + val canonicalize: Property = project.objects.property(Boolean::class.java) /** * Whether to try to resolve invalid namespaces on canonicalization (e.g. missing http://) */ - @get:Input - var resolveInvalidNamespaces = false + val resolveInvalidNamespaces: Property = project.objects.property(Boolean::class.java) } /** * Class containing the available extensions for the json dsl. */ - open class JsonExtension { + open class JsonExtension @Inject constructor(project: Project) { /** * If the html report should be canonicalized for json tests. */ - @get:Input - var canonicalize = true + val canonicalize: Property = project.objects.property(Boolean::class.java) } } diff --git a/src/main/kotlin/de/smartsquare/squit/SquitPlugin.kt b/src/main/kotlin/de/smartsquare/squit/SquitPlugin.kt index c6c3b30b..73e681e0 100644 --- a/src/main/kotlin/de/smartsquare/squit/SquitPlugin.kt +++ b/src/main/kotlin/de/smartsquare/squit/SquitPlugin.kt @@ -5,7 +5,6 @@ import de.smartsquare.squit.task.SquitPostProcessTask import de.smartsquare.squit.task.SquitPreProcessTask import de.smartsquare.squit.task.SquitRequestTask import de.smartsquare.squit.task.SquitTestTask -import de.smartsquare.squit.util.asPath import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project @@ -18,22 +17,33 @@ import org.gradle.util.GradleVersion class SquitPlugin : Plugin { override fun apply(project: Project) { - if (GradleVersion.current() < GradleVersion.version("6.8")) { + if (GradleVersion.current() < GradleVersion.version("7.3")) { throw GradleException( - "Minimum supported Gradle version is 6.8. Current version is ${GradleVersion.current().version}." + "Minimum supported Gradle version is 7.3. Current version is ${GradleVersion.current().version}.", ) } - val extension = project.extensions.create("squit", SquitExtension::class.java, project) + val extension = project.extensions.create("squit", SquitExtension::class.java, project).apply { + jdbcDrivers.convention(emptyList()) + sourceDir.convention(project.layout.projectDirectory.dir("src/squit")) + reportDir.convention(project.layout.buildDirectory.dir("squit/reports")) + requestTimeout.convention(10) + silent.convention(false) + ignoreFailures.convention(false) + xml.strict.convention(true) + xml.canonicalize.convention(true) + xml.resolveInvalidNamespaces.convention(false) + json.canonicalize.convention(true) + } project.tasks.register("squitPreProcess", SquitPreProcessTask::class.java) { - it.sourceDir = extension.sourceDir.asPath + it.sourceDir.set(extension.sourceDir) } project.tasks.register("squitRunRequests", SquitRequestTask::class.java) { - it.jdbcDrivers = extension.jdbcDrivers - it.timeout = extension.timeout - it.silent = extension.silent + it.jdbcDrivers.set(extension.jdbcDrivers) + it.requestTimeout.set(extension.requestTimeout) + it.silent.set(extension.silent) it.dependsOn("squitPreProcess") it.outputs.upToDateWhen { false } @@ -44,14 +54,17 @@ class SquitPlugin : Plugin { } project.tasks.register("squitTest", SquitTestTask::class.java) { - it.reportDir = extension.reportDir.asPath - it.silent = extension.silent - it.ignoreFailures = extension.ignoreFailures - it.mediaTypeConfig = MediaTypeConfig( - extension.xml.strict, - extension.xml.canonicalize, - extension.xml.resolveInvalidNamespaces, - extension.json.canonicalize + it.reportDir.set(extension.reportDir) + it.silent.set(extension.silent) + it.ignoreFailures.set(extension.ignoreFailures) + + it.mediaTypeConfig.set( + MediaTypeConfig( + xmlStrict = extension.xml.strict.get(), + xmlCanonicalize = extension.xml.canonicalize.get(), + xmlResolveInvalidNamespaces = extension.xml.resolveInvalidNamespaces.get(), + jsonCanonicalize = extension.json.canonicalize.get(), + ), ) it.dependsOn("squitPostProcess") diff --git a/src/main/kotlin/de/smartsquare/squit/config/ConfigExtensions.kt b/src/main/kotlin/de/smartsquare/squit/config/ConfigExtensions.kt index 58ab4358..4e7bdf1f 100644 --- a/src/main/kotlin/de/smartsquare/squit/config/ConfigExtensions.kt +++ b/src/main/kotlin/de/smartsquare/squit/config/ConfigExtensions.kt @@ -118,7 +118,7 @@ val Config.preTestTasks: List else -> listOf( SquitPreTestTask.PRE_RUNNERS, SquitPreTestTask.PRE_RUNNER_SCRIPTS, - SquitPreTestTask.DATABASE_SCRIPTS + SquitPreTestTask.DATABASE_SCRIPTS, ) } @@ -152,7 +152,7 @@ val Config.postTestTasks: List else -> listOf( SquitPostTestTask.DATABASE_SCRIPTS, SquitPostTestTask.POST_RUNNERS, - SquitPostTestTask.POST_RUNNER_SCRIPTS + SquitPostTestTask.POST_RUNNER_SCRIPTS, ) } @@ -170,7 +170,7 @@ val Config.databaseConfigurations it.getString(DATABASE_CONFIGURATION_NAME), it.getString(DATABASE_CONFIGURATION_JDBC_ADDRESS), it.getString(DATABASE_CONFIGURATION_USERNAME), - it.getString(DATABASE_CONFIGURATION_PASSWORD) + it.getString(DATABASE_CONFIGURATION_PASSWORD), ) } @@ -198,7 +198,7 @@ fun Config.mergeTag(tag: String): Config = withValue(TAGS, ConfigValueFactory.fr */ fun Config.withTestDir(testDir: Path): Config = withValue( TEST_DIRECTORY, - ConfigValueFactory.fromAnyRef(testDir.toString()) + ConfigValueFactory.fromAnyRef(testDir.toString()), ) /** @@ -206,7 +206,14 @@ fun Config.withTestDir(testDir: Path): Config = withValue( */ fun Config.validate() = this.apply { // Call getters of properties to check existence and correct declaration. - endpoint; mediaType; shouldExclude; shouldIgnore; headers; testDir; preTestTasks; postTestTasks + endpoint + mediaType + shouldExclude + shouldIgnore + headers + testDir + preTestTasks + postTestTasks preProcessors.forEach { checkClass(it) } preProcessorScripts.forEach { FilesUtils.validateExistence(it) } @@ -236,7 +243,7 @@ fun Config.writeTo( options: ConfigRenderOptions = ConfigRenderOptions.defaults() .setComments(false) .setOriginComments(false) - .setJson(false) + .setJson(false), ): Path = Files.write(path, root().render(options).toByteArray()) private fun Config.getSafeBoolean(path: String, fallback: Boolean = false) = when (hasPath(path)) { @@ -249,13 +256,11 @@ private fun Config.getSafeString(path: String, fallback: String = "") = when (ha false -> fallback } -private fun Config.getSafeStringList( - path: String, - fallback: List = emptyList() -): List = when (hasPath(path)) { - true -> getStringList(path) - false -> fallback -} +private fun Config.getSafeStringList(path: String, fallback: List = emptyList()): List = + when (hasPath(path)) { + true -> getStringList(path) + false -> fallback + } private fun Config.getSafeConfig(path: String, fallback: Config = ConfigFactory.empty()) = when (hasPath(path)) { true -> getConfig(path) diff --git a/src/main/kotlin/de/smartsquare/squit/config/TestIndexer.kt b/src/main/kotlin/de/smartsquare/squit/config/TestIndexer.kt index f3a6f3d2..86540111 100644 --- a/src/main/kotlin/de/smartsquare/squit/config/TestIndexer.kt +++ b/src/main/kotlin/de/smartsquare/squit/config/TestIndexer.kt @@ -34,7 +34,7 @@ class TestIndexer(private val projectConfig: Config) { * individual tests. */ fun index(sourceDir: Path, filter: (Pair) -> Boolean): List { - val leafDirectories = FilesUtils.getLeafDirectories(sourceDir, sort = true).asSequence() + val leafDirectories = FilesUtils.getLeafDirectories(sourceDir, sort = true) val leafDirectoriesWithConfig = indexConfigs(leafDirectories, sourceDir, filter) return indexTests(leafDirectoriesWithConfig, sourceDir) @@ -43,47 +43,42 @@ class TestIndexer(private val projectConfig: Config) { private fun indexConfigs( leafDirectories: Sequence, sourceDir: Path, - filter: (Pair) -> Boolean - ): List> { - return leafDirectories - .onEach { path -> - if (path.cut(sourceDir).toList().size < 2) { - throw GradleException( - "Invalid project structure. Please add a project directory to the src/squit directory." - ) - } + filter: (Pair) -> Boolean, + ): List> = leafDirectories + .onEach { path -> + if (path.cut(sourceDir).toList().size < 2) { + throw GradleException( + "Invalid project structure. Please add a project directory to the src/squit directory.", + ) } - .map { leafDirectory -> leafDirectory to resolveConfigs(leafDirectory, sourceDir) } - .filter(filter) - .map { (leafDirectory, config) -> - try { - leafDirectory to config.resolve().validate() - } catch (error: Exception) { - val innerMessage = when (error) { - is ConfigException -> - configExceptionMessageRegex - .find(error.message ?: "") - ?.groupValues?.getOrNull(1) - else -> error.message - } + } + .map { leafDirectory -> leafDirectory to resolveConfigs(leafDirectory, sourceDir) } + .filter(filter) + .map { (leafDirectory, config) -> + try { + leafDirectory to config.resolve().validate() + } catch (error: Exception) { + val innerMessage = when (error) { + is ConfigException -> + configExceptionMessageRegex + .find(error.message ?: "") + ?.groupValues?.getOrNull(1) + else -> error.message + } - throw GradleException( - """ + throw GradleException( + """ |Invalid test.conf or local.conf file on path of test: | ${leafDirectory.cut(sourceDir)} ($innerMessage) - """.trimMargin().replace("\n", ""), - error - ) - } + """.trimMargin().replace("\n", ""), + error, + ) } - .toList() - } + } + .toList() - private fun indexTests( - leafDirectoriesWithConfig: List>, - sourceDir: Path - ): List { - return leafDirectoriesWithConfig + private fun indexTests(leafDirectoriesWithConfig: List>, sourceDir: Path): List = + leafDirectoriesWithConfig .filterNot { (path, _) -> FilesUtils.isDirectoryEmpty(path) } .mapNotNull { (leafDirectory, config) -> val request = resolveRequest(leafDirectory, config) @@ -121,27 +116,24 @@ class TestIndexer(private val projectConfig: Config) { else -> testParts.reduce { acc, test -> acc.merge(test) } } } - } - private fun resolveConfigs(path: Path, sourceDir: Path): Config { - return FilesUtils.walkUpwards(path, sourceDir) - .map { it to resolveConfig(it) } - .fold(ConfigFactory.empty()) { acc, currentPathToConfig -> - val (currentPath, config) = currentPathToConfig - - // Do not add tag for the last part since it is part of the sourceDir. - if (currentPath.endsWith(sourceDir)) { - acc.withFallback(config) - } else { - acc.withFallback(config).mergeTag(currentPath.fileName.toString()) - } - } - .let { - projectConfig - .withTestDir(path) - .withFallback(it) + private fun resolveConfigs(path: Path, sourceDir: Path): Config = FilesUtils.walkUpwards(path, sourceDir) + .map { it to resolveConfig(it) } + .fold(ConfigFactory.empty()) { acc, currentPathToConfig -> + val (currentPath, config) = currentPathToConfig + + // Do not add tag for the last part since it is part of the sourceDir. + if (currentPath.endsWith(sourceDir)) { + acc.withFallback(config) + } else { + acc.withFallback(config).mergeTag(currentPath.fileName.toString()) } - } + } + .let { + projectConfig + .withTestDir(path) + .withFallback(it) + } private fun resolveConfig(path: Path): Config { val configPath = path.resolve(Constants.CONFIG) @@ -167,34 +159,28 @@ class TestIndexer(private val projectConfig: Config) { } } - private fun resolveResponse(path: Path, config: Config): Path { - return FilesUtils.validateExistence( - path.resolve(MediaTypeFactory.sourceResponse(config.mediaType)) - ) - } + private fun resolveResponse(path: Path, config: Config): Path = FilesUtils.validateExistence( + path.resolve(MediaTypeFactory.sourceResponse(config.mediaType)), + ) private fun resolveSqlScripts( path: Path, config: Config, leafs: List, - leafPath: Path - ): Map { - return config.databaseConfigurations.associate { databaseConfig -> - val pre = FilesUtils.ifExists(path.resolve("${databaseConfig.name}_pre.sql")) - val preOnce = FilesUtils.ifExists(path.resolve("${databaseConfig.name}_pre_once.sql")) - ?.takeIf { leafs.firstOrNull() == leafPath } - - val post = FilesUtils.ifExists(path.resolve("${databaseConfig.name}_post.sql")) - val postOnce = FilesUtils.ifExists(path.resolve("${databaseConfig.name}_post_once.sql")) - ?.takeIf { leafs.lastOrNull() == leafPath } - - databaseConfig.name to SqlScripts(pre, preOnce, post, postOnce) - } - } + leafPath: Path, + ): Map = config.databaseConfigurations.associate { databaseConfig -> + val pre = FilesUtils.ifExists(path.resolve("${databaseConfig.name}_pre.sql")) + val preOnce = FilesUtils.ifExists(path.resolve("${databaseConfig.name}_pre_once.sql")) + ?.takeIf { leafs.firstOrNull() == leafPath } - private fun resolveDescription(path: Path): Path? { - return FilesUtils.ifExists(path.resolve(DESCRIPTION)) + val post = FilesUtils.ifExists(path.resolve("${databaseConfig.name}_post.sql")) + val postOnce = FilesUtils.ifExists(path.resolve("${databaseConfig.name}_post_once.sql")) + ?.takeIf { leafs.lastOrNull() == leafPath } + + databaseConfig.name to SqlScripts(pre, preOnce, post, postOnce) } + private fun resolveDescription(path: Path): Path? = FilesUtils.ifExists(path.resolve(DESCRIPTION)) + private data class SqlScripts(val pre: Path?, val preOnce: Path?, val post: Path?, val postOnce: Path?) } diff --git a/src/main/kotlin/de/smartsquare/squit/entity/SquitDatabaseConfiguration.kt b/src/main/kotlin/de/smartsquare/squit/entity/SquitDatabaseConfiguration.kt index 6bb127c2..98be586c 100644 --- a/src/main/kotlin/de/smartsquare/squit/entity/SquitDatabaseConfiguration.kt +++ b/src/main/kotlin/de/smartsquare/squit/entity/SquitDatabaseConfiguration.kt @@ -12,5 +12,5 @@ data class SquitDatabaseConfiguration( val name: String, val jdbcAddress: String, val username: String, - val password: String + val password: String, ) diff --git a/src/main/kotlin/de/smartsquare/squit/entity/SquitMetaInfo.kt b/src/main/kotlin/de/smartsquare/squit/entity/SquitMetaInfo.kt index 484d975f..1a00d4f1 100644 --- a/src/main/kotlin/de/smartsquare/squit/entity/SquitMetaInfo.kt +++ b/src/main/kotlin/de/smartsquare/squit/entity/SquitMetaInfo.kt @@ -25,7 +25,7 @@ data class SquitMetaInfo(val date: LocalDateTime, val duration: Long) { return SquitMetaInfo( LocalDateTime.parse(config.getString(DATE)), - config.getLong(DURATION) + config.getLong(DURATION), ) } } @@ -36,7 +36,7 @@ data class SquitMetaInfo(val date: LocalDateTime, val duration: Long) { fun toJson(): String = Gson().toJson( mapOf( DATE to date.toString(), - DURATION to duration - ) + DURATION to duration, + ), ) } diff --git a/src/main/kotlin/de/smartsquare/squit/entity/SquitResponseInfo.kt b/src/main/kotlin/de/smartsquare/squit/entity/SquitResponseInfo.kt index 339be919..a95e5c84 100644 --- a/src/main/kotlin/de/smartsquare/squit/entity/SquitResponseInfo.kt +++ b/src/main/kotlin/de/smartsquare/squit/entity/SquitResponseInfo.kt @@ -18,14 +18,12 @@ data class SquitResponseInfo(val responseCode: Int = 0) { /** * Constructs a [SquitResponseInfo] instance from the given [json] String. */ - fun fromJson(json: String): SquitResponseInfo = - Gson().fromJson(json, SquitResponseInfo::class.java) + fun fromJson(json: String): SquitResponseInfo = Gson().fromJson(json, SquitResponseInfo::class.java) /** * Constructs a [SquitResponseInfo] instance from the given [config] Config. */ - fun fromConfig(config: Config): SquitResponseInfo = - SquitResponseInfo(config.expectedResponseCode) + fun fromConfig(config: Config): SquitResponseInfo = SquitResponseInfo(config.expectedResponseCode) } /** @@ -33,8 +31,8 @@ data class SquitResponseInfo(val responseCode: Int = 0) { */ fun toJson(): String = GsonBuilder().setPrettyPrinting().create().toJson( mapOf( - RESPONSE_CODE to responseCode - ) + RESPONSE_CODE to responseCode, + ), ) /** diff --git a/src/main/kotlin/de/smartsquare/squit/entity/SquitResult.kt b/src/main/kotlin/de/smartsquare/squit/entity/SquitResult.kt index 41e063b9..213c0c80 100644 --- a/src/main/kotlin/de/smartsquare/squit/entity/SquitResult.kt +++ b/src/main/kotlin/de/smartsquare/squit/entity/SquitResult.kt @@ -41,7 +41,7 @@ data class SquitResult( private val contextPath: Path, private val suitePath: Path, private val testDirectoryPath: Path, - private val squitBuildDirectoryPath: Path + private val squitBuildDirectoryPath: Path, ) { /** diff --git a/src/main/kotlin/de/smartsquare/squit/entity/SquitResultTree.kt b/src/main/kotlin/de/smartsquare/squit/entity/SquitResultTree.kt index 943da9a1..14ec1036 100644 --- a/src/main/kotlin/de/smartsquare/squit/entity/SquitResultTree.kt +++ b/src/main/kotlin/de/smartsquare/squit/entity/SquitResultTree.kt @@ -19,7 +19,7 @@ data class SquitResultTree( val name: String, val successfulTests: Int, val failedTests: Int, - val ignoredTests: Int + val ignoredTests: Int, ) { companion object { @@ -41,7 +41,7 @@ data class SquitResultTree( group.first().combinedName, successfulTests, failedTests, - ignoredTests + ignoredTests, ) } else { SquitResultTree( @@ -50,7 +50,7 @@ data class SquitResultTree( path.first().fileName.toString(), successfulTests, failedTests, - ignoredTests + ignoredTests, ) } } diff --git a/src/main/kotlin/de/smartsquare/squit/entity/SquitTest.kt b/src/main/kotlin/de/smartsquare/squit/entity/SquitTest.kt index d96776d5..1b541890 100644 --- a/src/main/kotlin/de/smartsquare/squit/entity/SquitTest.kt +++ b/src/main/kotlin/de/smartsquare/squit/entity/SquitTest.kt @@ -23,7 +23,7 @@ data class SquitTest( val response: Path, val preSqlScripts: Map>, val postSqlScripts: Map>, - val descriptions: List + val descriptions: List, ) : Serializable { private companion object { @@ -50,17 +50,15 @@ data class SquitTest( // Paths are not serializable so we have to copy to a special proxy class with Strings instead of paths. // This should be hidden from the user. @Suppress("UnusedPrivateMember") - private fun writeReplace(): Any { - return SquitTestSerializationProxy( - path.toString(), - config, - request?.toString(), - response.toString(), - preSqlScripts.mapValues { (_, scripts) -> scripts.map { it.toString() } }, - postSqlScripts.mapValues { (_, scripts) -> scripts.map { it.toString() } }, - descriptions.map { it.toString() } - ) - } + private fun writeReplace(): Any = SquitTestSerializationProxy( + path.toString(), + config, + request?.toString(), + response.toString(), + preSqlScripts.mapValues { (_, scripts) -> scripts.map { it.toString() } }, + postSqlScripts.mapValues { (_, scripts) -> scripts.map { it.toString() } }, + descriptions.map { it.toString() }, + ) private data class SquitTestSerializationProxy( val path: String, @@ -69,7 +67,7 @@ data class SquitTest( val response: String, val preSqlScripts: Map>, val postSqlScripts: Map>, - val descriptions: List + val descriptions: List, ) : Serializable { private companion object { @@ -84,7 +82,7 @@ data class SquitTest( Paths.get(response), preSqlScripts.mapValues { (_, scripts) -> scripts.map { Paths.get(it) } }, postSqlScripts.mapValues { (_, scripts) -> scripts.map { Paths.get(it) } }, - descriptions.map { Paths.get(it) } + descriptions.map { Paths.get(it) }, ) } } diff --git a/src/main/kotlin/de/smartsquare/squit/io/FilesUtils.kt b/src/main/kotlin/de/smartsquare/squit/io/FilesUtils.kt index 55bce0cd..55793b3f 100644 --- a/src/main/kotlin/de/smartsquare/squit/io/FilesUtils.kt +++ b/src/main/kotlin/de/smartsquare/squit/io/FilesUtils.kt @@ -78,9 +78,8 @@ object FilesUtils { /** * Checks whether the directory at the given [path] is empty. */ - fun isDirectoryEmpty(path: Path): Boolean { - return Files.newDirectoryStream(path).use { dirStream -> !dirStream.iterator().hasNext() } - } + fun isDirectoryEmpty(path: Path): Boolean = + Files.newDirectoryStream(path).use { dirStream -> !dirStream.iterator().hasNext() } /** * Returns the given [path] if it exists or null otherwise. @@ -114,12 +113,10 @@ object FilesUtils { /** * Creates a new [BufferedReader] for the given [path] and executes [block] with improved error handling. */ - fun useBufferedReader(path: Path, block: (BufferedReader) -> T): T { - return try { - Files.newBufferedReader(path).use(block) - } catch (error: MalformedInputException) { - throw IOException("Error reading file $path. Squit expects UTF-8 encoded files only.", error) - } + fun useBufferedReader(path: Path, block: (BufferedReader) -> T): T = try { + Files.newBufferedReader(path).use(block) + } catch (error: MalformedInputException) { + throw IOException("Error reading file $path. Squit expects UTF-8 encoded files only.", error) } /** diff --git a/src/main/kotlin/de/smartsquare/squit/mediatype/BodyProcessor.kt b/src/main/kotlin/de/smartsquare/squit/mediatype/BodyProcessor.kt index 9d2e5cfd..6c8599f6 100644 --- a/src/main/kotlin/de/smartsquare/squit/mediatype/BodyProcessor.kt +++ b/src/main/kotlin/de/smartsquare/squit/mediatype/BodyProcessor.kt @@ -17,7 +17,7 @@ interface BodyProcessor { responsePath: Path, resultRequestPath: Path, resultResponsePath: Path, - config: Config + config: Config, ) /** @@ -28,6 +28,6 @@ interface BodyProcessor { actualResponsePath: Path, expectedResponsePath: Path, resultActualResponseFilePath: Path, - config: Config + config: Config, ) } diff --git a/src/main/kotlin/de/smartsquare/squit/mediatype/MediaTypeConfig.kt b/src/main/kotlin/de/smartsquare/squit/mediatype/MediaTypeConfig.kt index e97efef5..48f06203 100644 --- a/src/main/kotlin/de/smartsquare/squit/mediatype/MediaTypeConfig.kt +++ b/src/main/kotlin/de/smartsquare/squit/mediatype/MediaTypeConfig.kt @@ -12,6 +12,6 @@ import org.gradle.api.tasks.Input data class MediaTypeConfig( @get:Input val xmlStrict: Boolean = true, @get:Input val xmlCanonicalize: Boolean = true, - @get:Input val resolveInvalidNamespaces: Boolean = false, - @get:Input val jsonCanonicalize: Boolean = true + @get:Input val xmlResolveInvalidNamespaces: Boolean = false, + @get:Input val jsonCanonicalize: Boolean = true, ) diff --git a/src/main/kotlin/de/smartsquare/squit/mediatype/generic/GenericBodyProcessor.kt b/src/main/kotlin/de/smartsquare/squit/mediatype/generic/GenericBodyProcessor.kt index 7c9dd73c..705a79a6 100644 --- a/src/main/kotlin/de/smartsquare/squit/mediatype/generic/GenericBodyProcessor.kt +++ b/src/main/kotlin/de/smartsquare/squit/mediatype/generic/GenericBodyProcessor.kt @@ -15,7 +15,7 @@ class GenericBodyProcessor : BodyProcessor { responsePath: Path, resultRequestPath: Path, resultResponsePath: Path, - config: Config + config: Config, ) { if (requestPath != null) Files.copy(requestPath, resultRequestPath) Files.copy(responsePath, resultResponsePath) @@ -25,7 +25,7 @@ class GenericBodyProcessor : BodyProcessor { actualResponsePath: Path, expectedResponsePath: Path, resultActualResponseFilePath: Path, - config: Config + config: Config, ) { Files.copy(actualResponsePath, resultActualResponseFilePath) } diff --git a/src/main/kotlin/de/smartsquare/squit/mediatype/generic/GenericCanonicalizer.kt b/src/main/kotlin/de/smartsquare/squit/mediatype/generic/GenericCanonicalizer.kt index 176bd605..bb13b0c4 100644 --- a/src/main/kotlin/de/smartsquare/squit/mediatype/generic/GenericCanonicalizer.kt +++ b/src/main/kotlin/de/smartsquare/squit/mediatype/generic/GenericCanonicalizer.kt @@ -8,7 +8,5 @@ import de.smartsquare.squit.mediatype.MediaTypeConfig */ class GenericCanonicalizer : Canonicalizer { - override fun canonicalize(input: String, mediaTypeConfig: MediaTypeConfig): String { - return input - } + override fun canonicalize(input: String, mediaTypeConfig: MediaTypeConfig): String = input } diff --git a/src/main/kotlin/de/smartsquare/squit/mediatype/generic/GenericDiffer.kt b/src/main/kotlin/de/smartsquare/squit/mediatype/generic/GenericDiffer.kt index c9c5e260..3cd4f3cc 100644 --- a/src/main/kotlin/de/smartsquare/squit/mediatype/generic/GenericDiffer.kt +++ b/src/main/kotlin/de/smartsquare/squit/mediatype/generic/GenericDiffer.kt @@ -13,7 +13,7 @@ class GenericDiffer : Differ { val diff = DiffUtils.diff( expectedResponse.toString(Charset.defaultCharset()), actualResponse.toString(Charset.defaultCharset()), - null + null, ) return diff.deltas.joinToString("\n") { diff --git a/src/main/kotlin/de/smartsquare/squit/mediatype/json/JsonBodyProcessor.kt b/src/main/kotlin/de/smartsquare/squit/mediatype/json/JsonBodyProcessor.kt index 380a3db5..d23aaec6 100644 --- a/src/main/kotlin/de/smartsquare/squit/mediatype/json/JsonBodyProcessor.kt +++ b/src/main/kotlin/de/smartsquare/squit/mediatype/json/JsonBodyProcessor.kt @@ -26,7 +26,7 @@ class JsonBodyProcessor : BodyProcessor { responsePath: Path, resultRequestPath: Path, resultResponsePath: Path, - config: Config + config: Config, ) { val request = requestPath?.let { JsonParserSupport.read(it) } val response = JsonParserSupport.read(responsePath) @@ -41,7 +41,7 @@ class JsonBodyProcessor : BodyProcessor { actualResponsePath: Path, expectedResponsePath: Path, resultActualResponseFilePath: Path, - config: Config + config: Config, ) { val actualResponse = JsonParserSupport.read(actualResponsePath) val expectedResponse = JsonParserSupport.read(expectedResponsePath) @@ -53,7 +53,7 @@ class JsonBodyProcessor : BodyProcessor { private fun runPreProcessors(config: Config, request: JsonElement?, response: JsonElement) { config.preProcessors.map { Class.forName(it).getConstructor().newInstance() } - .filterIsInstance(SquitJsonPreProcessor::class.java) + .filterIsInstance() .forEach { it.process(request, response, config) } config.preProcessorScripts.forEach { @@ -62,8 +62,8 @@ class JsonBodyProcessor : BodyProcessor { mapOf( "request" to request, "expectedResponse" to response, - "config" to config - ) + "config" to config, + ), ) }.run() } @@ -71,7 +71,7 @@ class JsonBodyProcessor : BodyProcessor { private fun runPostProcessors(config: Config, actualResponse: JsonElement, expectedResponse: JsonElement) { config.postProcessors.map { Class.forName(it).getConstructor().newInstance() } - .filterIsInstance(SquitJsonPostProcessor::class.java) + .filterIsInstance() .forEach { it.process(actualResponse, expectedResponse, config) } config.postProcessorScripts.forEach { @@ -79,8 +79,8 @@ class JsonBodyProcessor : BodyProcessor { binding = Binding( mapOf( "actualResponse" to actualResponse, - "expectedResponse" to expectedResponse - ) + "expectedResponse" to expectedResponse, + ), ) }.run() } diff --git a/src/main/kotlin/de/smartsquare/squit/mediatype/json/JsonCanonicalizer.kt b/src/main/kotlin/de/smartsquare/squit/mediatype/json/JsonCanonicalizer.kt index e009faec..8d779600 100644 --- a/src/main/kotlin/de/smartsquare/squit/mediatype/json/JsonCanonicalizer.kt +++ b/src/main/kotlin/de/smartsquare/squit/mediatype/json/JsonCanonicalizer.kt @@ -14,37 +14,36 @@ class JsonCanonicalizer : Canonicalizer { private val gson = GsonBuilder().setPrettyPrinting().create() - override fun canonicalize(input: String, mediaTypeConfig: MediaTypeConfig): String { - return if (mediaTypeConfig.jsonCanonicalize) { + override fun canonicalize(input: String, mediaTypeConfig: MediaTypeConfig): String = + if (mediaTypeConfig.jsonCanonicalize) { val element = gson.fromJson(input, JsonElement::class.java) gson.toJson(element.canonicalize()) } else { input } - } - private fun JsonElement.canonicalize(): JsonElement { - return when (this) { - is JsonObject -> { - val newEntries = entrySet().map { (key, value) -> key to value.canonicalize() } + private fun JsonElement.canonicalize(): JsonElement = when (this) { + is JsonObject -> { + val newEntries = entrySet().map { (key, value) -> key to value.canonicalize() } - JsonObject().also { newObject -> - newEntries.sortedBy { (key) -> key }.forEach { (key, value) -> - newObject.add(key, value) - } + JsonObject().also { newObject -> + newEntries.sortedBy { (key) -> key }.forEach { (key, value) -> + newObject.add(key, value) } } - is JsonArray -> { - val newEntries = this.map { it.canonicalize() } + } - JsonArray().also { newArray -> - newEntries.forEach { - newArray.add(it) - } + is JsonArray -> { + val newEntries = this.map { it.canonicalize() } + + JsonArray().also { newArray -> + newEntries.forEach { + newArray.add(it) } } - else -> this } + + else -> this } } diff --git a/src/main/kotlin/de/smartsquare/squit/mediatype/json/JsonDiffer.kt b/src/main/kotlin/de/smartsquare/squit/mediatype/json/JsonDiffer.kt index 02ba4426..727c1e46 100644 --- a/src/main/kotlin/de/smartsquare/squit/mediatype/json/JsonDiffer.kt +++ b/src/main/kotlin/de/smartsquare/squit/mediatype/json/JsonDiffer.kt @@ -8,16 +8,14 @@ import net.javacrumbs.jsonunit.JsonAssert */ class JsonDiffer : Differ { - override fun diff(expectedResponse: ByteArray, actualResponse: ByteArray): String { - return try { - JsonAssert.assertJsonEquals( - expectedResponse.toString(Charsets.UTF_8), - actualResponse.toString(Charsets.UTF_8) - ) + override fun diff(expectedResponse: ByteArray, actualResponse: ByteArray): String = try { + JsonAssert.assertJsonEquals( + expectedResponse.toString(Charsets.UTF_8), + actualResponse.toString(Charsets.UTF_8), + ) - "" - } catch (error: AssertionError) { - requireNotNull(error.message) - } + "" + } catch (error: AssertionError) { + requireNotNull(error.message) } } diff --git a/src/main/kotlin/de/smartsquare/squit/mediatype/xml/XmlBodyProcessor.kt b/src/main/kotlin/de/smartsquare/squit/mediatype/xml/XmlBodyProcessor.kt index 127c3066..e754caee 100644 --- a/src/main/kotlin/de/smartsquare/squit/mediatype/xml/XmlBodyProcessor.kt +++ b/src/main/kotlin/de/smartsquare/squit/mediatype/xml/XmlBodyProcessor.kt @@ -26,7 +26,7 @@ class XmlBodyProcessor : BodyProcessor { responsePath: Path, resultRequestPath: Path, resultResponsePath: Path, - config: Config + config: Config, ) { val request = requestPath?.let { SAXReaderSupport.read(requestPath) } val response = SAXReaderSupport.read(responsePath) @@ -41,7 +41,7 @@ class XmlBodyProcessor : BodyProcessor { actualResponsePath: Path, expectedResponsePath: Path, resultActualResponseFilePath: Path, - config: Config + config: Config, ) { val actualResponse = SAXReaderSupport.read(actualResponsePath) val expectedResponse = SAXReaderSupport.read(expectedResponsePath) @@ -53,7 +53,7 @@ class XmlBodyProcessor : BodyProcessor { private fun runPreProcessors(config: Config, request: Document?, response: Document) { config.preProcessors.map { Class.forName(it).getConstructor().newInstance() } - .filterIsInstance(SquitXmlPreProcessor::class.java) + .filterIsInstance() .forEach { it.process(request, response, config) } config.preProcessorScripts.forEach { @@ -62,8 +62,8 @@ class XmlBodyProcessor : BodyProcessor { mapOf( "request" to request, "expectedResponse" to response, - "config" to config - ) + "config" to config, + ), ) }.run() } @@ -71,7 +71,7 @@ class XmlBodyProcessor : BodyProcessor { private fun runPostProcessors(config: Config, actualResponse: Document, expectedResponse: Document) { config.postProcessors.map { Class.forName(it).getConstructor().newInstance() } - .filterIsInstance(SquitXmlPostProcessor::class.java) + .filterIsInstance() .forEach { it.process(actualResponse, expectedResponse, config) } config.postProcessorScripts.forEach { @@ -79,8 +79,8 @@ class XmlBodyProcessor : BodyProcessor { binding = Binding( mapOf( "actualResponse" to actualResponse, - "expectedResponse" to expectedResponse - ) + "expectedResponse" to expectedResponse, + ), ) }.run() } diff --git a/src/main/kotlin/de/smartsquare/squit/mediatype/xml/XmlCanonicalizer.kt b/src/main/kotlin/de/smartsquare/squit/mediatype/xml/XmlCanonicalizer.kt index bf1da6e7..61d63945 100644 --- a/src/main/kotlin/de/smartsquare/squit/mediatype/xml/XmlCanonicalizer.kt +++ b/src/main/kotlin/de/smartsquare/squit/mediatype/xml/XmlCanonicalizer.kt @@ -26,19 +26,21 @@ class XmlCanonicalizer : Canonicalizer { } private companion object { + private const val RESOLVE_NAMESPACE_STRING = "http://" + private val xmlNamespaceRegex = Regex("(xmlns:\\w+=['\"])(.*?)(['\"])") private val urlRegex = Regex("^https?://") - private const val resolveNamespaceString = "http://" } - override fun canonicalize(input: String, mediaTypeConfig: MediaTypeConfig): String { - return if (mediaTypeConfig.xmlCanonicalize) { + override fun canonicalize(input: String, mediaTypeConfig: MediaTypeConfig): String = + if (mediaTypeConfig.xmlCanonicalize) { val outputStream = ByteArrayOutputStream() - val content = if (mediaTypeConfig.resolveInvalidNamespaces) + val content = if (mediaTypeConfig.xmlResolveInvalidNamespaces) { resolveInvalidNamespaces(input) - else + } else { input + } canonicalizer.canonicalize(content.toByteArray(), outputStream, false) @@ -46,25 +48,20 @@ class XmlCanonicalizer : Canonicalizer { } else { input } - } - private fun resolveInvalidNamespaces(content: String): String { - return content.replace(xmlNamespaceRegex) { match -> - val start = match.groupValues[1] - val potentialUrlString = match.groupValues[2] - val end = match.groupValues[3] + private fun resolveInvalidNamespaces(content: String): String = content.replace(xmlNamespaceRegex) { match -> + val start = match.groupValues[1] + val potentialUrlString = match.groupValues[2] + val end = match.groupValues[3] - if (!potentialUrlString.contains(urlRegex)) { - "$start$resolveNamespaceString$potentialUrlString$end" - } else { - "$start$potentialUrlString$end" - } + if (!potentialUrlString.contains(urlRegex)) { + "$start$RESOLVE_NAMESPACE_STRING$potentialUrlString$end" + } else { + "$start$potentialUrlString$end" } } - private fun Document.asString(outputFormat: OutputFormat = SquitOutputFormat): String { - return StringWriter() - .also { XMLWriter(it, outputFormat).write(this) } - .toString() - } + private fun Document.asString(outputFormat: OutputFormat = SquitOutputFormat): String = StringWriter() + .also { XMLWriter(it, outputFormat).write(this) } + .toString() } diff --git a/src/main/kotlin/de/smartsquare/squit/report/HtmlReportWriter.kt b/src/main/kotlin/de/smartsquare/squit/report/HtmlReportWriter.kt index 56aa796a..95005032 100644 --- a/src/main/kotlin/de/smartsquare/squit/report/HtmlReportWriter.kt +++ b/src/main/kotlin/de/smartsquare/squit/report/HtmlReportWriter.kt @@ -25,25 +25,25 @@ class HtmlReportWriter(private val logger: Logger) { private const val DIFF_CONTEXT_SIZE = 1_000_000 private const val HTML_LINE_ENDING = "\\n\\\n" - private const val bootstrapPath = "META-INF/resources/webjars/bootstrap/4.6.0/dist" - private const val fontAwesomePath = "META-INF/resources/webjars/font-awesome/5.15.4" - private const val jqueryPath = "META-INF/resources/webjars/jquery/3.6.0/dist" - private const val popperJsPath = "META-INF/resources/webjars/popper.js/1.16.1/dist/umd" - private const val markedPath = "META-INF/resources/webjars/marked/2.0.6" - private const val diff2htmlPath = "META-INF/resources/webjars/diff2html/3.1.7" + private const val BOOTSTRAP_PATH = "META-INF/resources/webjars/bootstrap/4.6.0/dist" + private const val FONT_AWESOME_PATH = "META-INF/resources/webjars/font-awesome/5.15.4" + private const val JQUERY_PATH = "META-INF/resources/webjars/jquery/3.6.0/dist" + private const val POPPER_JS_PATH = "META-INF/resources/webjars/popper.js/1.16.1/dist/umd" + private const val MARKED_PATH = "META-INF/resources/webjars/marked/2.0.6" + private const val DIFF_2_HTML_PATH = "META-INF/resources/webjars/diff2html/3.1.7" private val resources = arrayOf( - "$bootstrapPath/css/bootstrap.min.css" to "css/bootstrap.css", - "$bootstrapPath/js/bootstrap.min.js" to "js/bootstrap.js", - "$fontAwesomePath/js/all.min.js" to "js/fontawesome.js", - "$jqueryPath/jquery.slim.min.js" to "js/jquery.js", - "$popperJsPath/popper.min.js" to "js/popper.js", - "$markedPath/marked.min.js" to "js/marked.js", - "$diff2htmlPath/bundles/css/diff2html.min.css" to "css/diff2html.css", - "$diff2htmlPath/bundles/js/diff2html.min.js" to "js/diff2html.js", - "$diff2htmlPath/bundles/js/diff2html-ui.min.js" to "js/diff2html-ui.js", + "$BOOTSTRAP_PATH/css/bootstrap.min.css" to "css/bootstrap.css", + "$BOOTSTRAP_PATH/js/bootstrap.min.js" to "js/bootstrap.js", + "$FONT_AWESOME_PATH/js/all.min.js" to "js/fontawesome.js", + "$JQUERY_PATH/jquery.slim.min.js" to "js/jquery.js", + "$POPPER_JS_PATH/popper.min.js" to "js/popper.js", + "$MARKED_PATH/marked.min.js" to "js/marked.js", + "$DIFF_2_HTML_PATH/bundles/css/diff2html.min.css" to "css/diff2html.css", + "$DIFF_2_HTML_PATH/bundles/js/diff2html.min.js" to "js/diff2html.js", + "$DIFF_2_HTML_PATH/bundles/js/diff2html-ui.min.js" to "js/diff2html-ui.js", "squit.js" to "js/squit.js", - "squit.css" to "css/squit.css" + "squit.css" to "css/squit.css", ) private val emptyDiffHeader = listOf("--- $DIFF_FILE_NAME", "+++ $DIFF_FILE_NAME", "@@ -1 +1 @@") @@ -52,11 +52,7 @@ class HtmlReportWriter(private val logger: Logger) { /** * Generates and writes the Squit html report, given the [results] list and [reportDirectoryPath]. */ - fun writeReport( - results: List, - reportDirectoryPath: Path, - mediaTypeConfig: MediaTypeConfig - ) { + fun writeReport(results: List, reportDirectoryPath: Path, mediaTypeConfig: MediaTypeConfig) { val document = StringBuilder("").appendHTML().html { squitHead() squitBody(results) @@ -78,8 +74,9 @@ class HtmlReportWriter(private val logger: Logger) { result.expectedLines, result.mediaType, mediaTypeConfig, - "Could not canonicalize expected response" + "Could not canonicalize expected response", ) + else -> result.expectedLines } @@ -88,8 +85,9 @@ class HtmlReportWriter(private val logger: Logger) { result.actualLines, result.mediaType, mediaTypeConfig, - "Could not canonicalize actual response" + "Could not canonicalize actual response", ) + else -> result.actualLines } @@ -98,8 +96,11 @@ class HtmlReportWriter(private val logger: Logger) { val unifiedDiffForJs = prepareForJs(bodyDiff) val unifiedInfoDiffForJs = prepareInfoForJs(result) - val descriptionForReplacement = if (result.description == null) "null" else "\"${result.description}\"" - .replace("\n", HTML_LINE_ENDING) + val descriptionForReplacement = if (result.description == null) { + "null" + } else { + "\"${result.description}\"".replace("\n", HTML_LINE_ENDING) + } Files.createDirectories(detailPath) Files.write(detailHtmlPath, detailDocument.toString().toByteArray()) @@ -124,8 +125,8 @@ class HtmlReportWriter(private val logger: Logger) { } // Visible for testing. - internal fun prepareInfoForJs(result: SquitResult): String { - return if (!result.expectedResponseInfo.isDefault && !result.isError) { + internal fun prepareInfoForJs(result: SquitResult): String = + if (!result.expectedResponseInfo.isDefault && !result.isError) { val expectedInfoLines = result.expectedResponseInfo.toJson().lines() val actualInfo = result.actualInfoLines.joinToString(separator = "\n") @@ -140,33 +141,28 @@ class HtmlReportWriter(private val logger: Logger) { } else { "" } - } - private fun prepareForJs(bodyDiff: List): String { - return bodyDiff - .map { it.replace(Regex("(?): String = bodyDiff + .map { it.replace(Regex("(?, mediaType: MediaType, mediaTypeConfig: MediaTypeConfig, - errorMessage: String - ): List { - return when { - lines.isEmpty() -> lines - else -> try { - MediaTypeFactory.canonicalizer(mediaType) - .canonicalize(lines.joinToString(""), mediaTypeConfig) - .lines() - } catch (error: Exception) { - logger.warn(errorMessage, error) - - lines - } + errorMessage: String, + ): List = when { + lines.isEmpty() -> lines + else -> try { + MediaTypeFactory.canonicalizer(mediaType) + .canonicalize(lines.joinToString(""), mediaTypeConfig) + .lines() + } catch (error: Exception) { + logger.warn(errorMessage, error) + + lines } } @@ -178,7 +174,7 @@ class HtmlReportWriter(private val logger: Logger) { filename, expectedLines, diff, - DIFF_CONTEXT_SIZE + DIFF_CONTEXT_SIZE, ) return when (unifiedDiff.isEmpty()) { diff --git a/src/main/kotlin/de/smartsquare/squit/task/SquitPostProcessRunner.kt b/src/main/kotlin/de/smartsquare/squit/task/SquitPostProcessRunner.kt index 238e472b..49bdc9b5 100644 --- a/src/main/kotlin/de/smartsquare/squit/task/SquitPostProcessRunner.kt +++ b/src/main/kotlin/de/smartsquare/squit/task/SquitPostProcessRunner.kt @@ -17,14 +17,9 @@ object SquitPostProcessRunner { /** * Runs the post processing. */ - fun run( - processedSourcesPath: Path, - actualResponsesPath: Path, - processedActualResponsesPath: Path, - testPath: Path - ) { + fun run(processedSourcesPath: Path, actualResponsesPath: Path, processedActualResponsesPath: Path, testPath: Path) { val resultActualResponsePath = Files.createDirectories( - processedActualResponsesPath.resolve(testPath.cut(actualResponsesPath)) + processedActualResponsesPath.resolve(testPath.cut(actualResponsesPath)), ) val errorFile = testPath.resolve(Constants.ERROR) @@ -39,13 +34,13 @@ object SquitPostProcessRunner { val config = ConfigFactory.parseFile(configPath.toFile()) val actualResponsePath = FilesUtils.validateExistence( - testPath.resolve(MediaTypeFactory.actualResponse(config.mediaType)) + testPath.resolve(MediaTypeFactory.actualResponse(config.mediaType)), ) val expectedResponsePath = FilesUtils.validateExistence( processedSourcesPath .resolve(testPath.cut(actualResponsesPath)) - .resolve(MediaTypeFactory.expectedResponse(config.mediaType)) + .resolve(MediaTypeFactory.expectedResponse(config.mediaType)), ) val resultActualResponseFilePath = resultActualResponsePath diff --git a/src/main/kotlin/de/smartsquare/squit/task/SquitPostProcessTask.kt b/src/main/kotlin/de/smartsquare/squit/task/SquitPostProcessTask.kt index 0d5d80dd..dd45a365 100644 --- a/src/main/kotlin/de/smartsquare/squit/task/SquitPostProcessTask.kt +++ b/src/main/kotlin/de/smartsquare/squit/task/SquitPostProcessTask.kt @@ -8,9 +8,11 @@ import de.smartsquare.squit.util.Constants.RESPONSES_DIRECTORY import de.smartsquare.squit.util.Constants.SOURCES_DIRECTORY import de.smartsquare.squit.util.Constants.SQUIT_DIRECTORY import de.smartsquare.squit.util.asPath +import de.smartsquare.squit.util.dir import org.gradle.api.DefaultTask -import org.gradle.api.file.ConfigurableFileTree import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileTree +import org.gradle.api.provider.Provider import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.InputFiles @@ -19,42 +21,27 @@ import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction -import org.gradle.util.GradleVersion import org.gradle.workers.WorkAction import org.gradle.workers.WorkParameters import org.gradle.workers.WorkerExecutor import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths import javax.inject.Inject /** * Task for post-processing the responses. */ @CacheableTask -open class SquitPostProcessTask @Inject constructor(private val workerExecutor: WorkerExecutor) : DefaultTask() { +abstract class SquitPostProcessTask @Inject constructor(private val workerExecutor: WorkerExecutor) : DefaultTask() { /** * The directory of the test sources. */ - @InputDirectory - @PathSensitive(PathSensitivity.RELATIVE) - val processedSourcesPath: Path = Paths.get( - project.buildDir.path, - SQUIT_DIRECTORY, - SOURCES_DIRECTORY - ) + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + val processedSourcesDir = project.layout.buildDirectory.dir(SQUIT_DIRECTORY, SOURCES_DIRECTORY) - /** - * The directory of the previously requested responses. - */ - @Internal - val actualResponsesPath: Path = Paths.get( - project.buildDir.path, - SQUIT_DIRECTORY, - RESPONSES_DIRECTORY, - RAW_DIRECTORY - ) + @get:Internal + val actualResponsesDir = project.layout.buildDirectory.dir(SQUIT_DIRECTORY, RESPONSES_DIRECTORY, RAW_DIRECTORY) /** * Collection of actual response files except meta.json files for up-to-date checking. @@ -62,19 +49,15 @@ open class SquitPostProcessTask @Inject constructor(private val workerExecutor: @Suppress("unused") @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) - val actualResponsesFiles: ConfigurableFileTree = project - .fileTree(actualResponsesPath) { it.exclude("**/$META") } + val actualResponseFiles: Provider = actualResponsesDir + .map { dir -> dir.asFileTree.matching { tree -> tree.exclude("**/$META") } } /** * The directory to save the results in. */ - @OutputDirectory - val processedActualResponsesPath: Path = Paths.get( - project.buildDir.path, - SQUIT_DIRECTORY, - RESPONSES_DIRECTORY, - PROCESSED_DIRECTORY - ) + @get:OutputDirectory + val processedActualResponseDir = project.layout.buildDirectory + .dir(SQUIT_DIRECTORY, RESPONSES_DIRECTORY, PROCESSED_DIRECTORY) init { group = "Build" @@ -86,39 +69,28 @@ open class SquitPostProcessTask @Inject constructor(private val workerExecutor: */ @TaskAction fun run() { - FilesUtils.deleteRecursivelyIfExisting(processedActualResponsesPath) - Files.createDirectories(processedActualResponsesPath) + FilesUtils.deleteRecursivelyIfExisting(processedActualResponseDir.asPath) + Files.createDirectories(processedActualResponseDir.asPath) - if (GradleVersion.current() >= GradleVersion.version("5.6")) { - val workerQueue = workerExecutor.noIsolation() + val workerQueue = workerExecutor.noIsolation() - FilesUtils.getLeafDirectories(actualResponsesPath, sort = false).forEach { testPath -> - workerQueue.submit(Worker::class.java) { - it.processedSourcesPath.set(processedSourcesPath.toFile()) - it.actualResponsesPath.set(actualResponsesPath.toFile()) - it.processedActualResponsesPath.set(processedActualResponsesPath.toFile()) - it.testPath.set(testPath.toFile()) - } - } - } else { - FilesUtils.getLeafDirectories(actualResponsesPath, sort = false).forEach { testPath -> - SquitPostProcessRunner.run( - processedSourcesPath, actualResponsesPath, processedActualResponsesPath, testPath - ) + FilesUtils.getLeafDirectories(actualResponsesDir.asPath, sort = false).forEach { testPath -> + workerQueue.submit(Worker::class.java) { + it.processedSourcesPath.set(processedSourcesDir.get().asFile) + it.actualResponsesPath.set(actualResponsesDir.get().asFile) + it.processedActualResponsesPath.set(processedActualResponseDir.get().asFile) + it.testPath.set(testPath.toFile()) } } } internal abstract class Worker : WorkAction { - - private val processedSourcesPath get() = parameters.processedSourcesPath.asPath - private val actualResponsesPath get() = parameters.actualResponsesPath.asPath - private val processedActualResponsesPath get() = parameters.processedActualResponsesPath.asPath - private val testPath get() = parameters.testPath.asPath - override fun execute() { SquitPostProcessRunner.run( - processedSourcesPath, actualResponsesPath, processedActualResponsesPath, testPath + processedSourcesPath = parameters.processedSourcesPath.asPath, + actualResponsesPath = parameters.actualResponsesPath.asPath, + processedActualResponsesPath = parameters.processedActualResponsesPath.asPath, + testPath = parameters.testPath.asPath, ) } } diff --git a/src/main/kotlin/de/smartsquare/squit/task/SquitPostTestTask.kt b/src/main/kotlin/de/smartsquare/squit/task/SquitPostTestTask.kt index 7d278fb7..3407547e 100644 --- a/src/main/kotlin/de/smartsquare/squit/task/SquitPostTestTask.kt +++ b/src/main/kotlin/de/smartsquare/squit/task/SquitPostTestTask.kt @@ -1,5 +1,7 @@ package de.smartsquare.squit.task enum class SquitPostTestTask { - DATABASE_SCRIPTS, POST_RUNNERS, POST_RUNNER_SCRIPTS + DATABASE_SCRIPTS, + POST_RUNNERS, + POST_RUNNER_SCRIPTS, } diff --git a/src/main/kotlin/de/smartsquare/squit/task/SquitPreProcessRunner.kt b/src/main/kotlin/de/smartsquare/squit/task/SquitPreProcessRunner.kt index a3ccf79e..0e9e1e07 100644 --- a/src/main/kotlin/de/smartsquare/squit/task/SquitPreProcessRunner.kt +++ b/src/main/kotlin/de/smartsquare/squit/task/SquitPreProcessRunner.kt @@ -7,8 +7,6 @@ import de.smartsquare.squit.io.FilesUtils import de.smartsquare.squit.mediatype.MediaTypeFactory import de.smartsquare.squit.util.Constants import de.smartsquare.squit.util.cut -import java.io.Reader -import java.io.Writer import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardOpenOption @@ -18,8 +16,6 @@ import java.nio.file.StandardOpenOption */ object SquitPreProcessRunner { - private const val TRANSFER_BUFFER_SIZE = 8192 - /** * Runs the pre processing. */ @@ -56,32 +52,16 @@ object SquitPreProcessRunner { } private fun writeAllTo(inputs: List, output: Path, separator: String = "") { - Files.newBufferedWriter(output, StandardOpenOption.CREATE_NEW, StandardOpenOption.APPEND) - .use { writer -> - inputs.forEachIndexed { index, path -> - FilesUtils.useBufferedReader(path) { reader -> - reader.transferToCompat(writer) + Files.newBufferedWriter(output, StandardOpenOption.CREATE_NEW, StandardOpenOption.APPEND).use { writer -> + inputs.forEachIndexed { index, path -> + FilesUtils.useBufferedReader(path) { reader -> + reader.transferTo(writer) - if (index < inputs.lastIndex && separator.isNotEmpty()) { - writer.write(separator) - } + if (index < inputs.lastIndex && separator.isNotEmpty()) { + writer.write(separator) } } } - } - - // This method is only available since Java 10. - private fun Reader.transferToCompat(out: Writer): Long { - val buffer = CharArray(TRANSFER_BUFFER_SIZE) - - var transferred: Long = 0 - var nRead: Int - - while (read(buffer, 0, TRANSFER_BUFFER_SIZE).also { nRead = it } >= 0) { - out.write(buffer, 0, nRead) - transferred += nRead.toLong() } - - return transferred } } diff --git a/src/main/kotlin/de/smartsquare/squit/task/SquitPreProcessTask.kt b/src/main/kotlin/de/smartsquare/squit/task/SquitPreProcessTask.kt index cfc29468..920982f9 100644 --- a/src/main/kotlin/de/smartsquare/squit/task/SquitPreProcessTask.kt +++ b/src/main/kotlin/de/smartsquare/squit/task/SquitPreProcessTask.kt @@ -11,9 +11,11 @@ import de.smartsquare.squit.util.Constants.SOURCES_DIRECTORY import de.smartsquare.squit.util.Constants.SQUIT_DIRECTORY import de.smartsquare.squit.util.asPath import de.smartsquare.squit.util.cut +import de.smartsquare.squit.util.dir import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory @@ -21,62 +23,62 @@ import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction -import org.gradle.util.GradleVersion import org.gradle.workers.WorkAction import org.gradle.workers.WorkParameters import org.gradle.workers.WorkerExecutor import java.nio.file.Files import java.nio.file.Path -import java.nio.file.Paths import javax.inject.Inject /** * Task for pre-processing the available sources like requests, responses, sql scripts and properties. */ @CacheableTask -open class SquitPreProcessTask @Inject constructor(private val workerExecutor: WorkerExecutor) : DefaultTask() { +abstract class SquitPreProcessTask @Inject constructor(private val workerExecutor: WorkerExecutor) : DefaultTask() { /** * The path the sources lie in. */ @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) - lateinit var sourceDir: Path + abstract val sourceDir: DirectoryProperty /** * The directory to save the results in. */ @get:OutputDirectory - val processedSourcesPath: Path = Paths.get(project.buildDir.path, SQUIT_DIRECTORY, SOURCES_DIRECTORY) + val processedSources = project.layout.buildDirectory.dir(SQUIT_DIRECTORY, SOURCES_DIRECTORY) /** * The tags to filter by (and). */ @get:Input - val tagsAnd = getTags("tags") + getTags("tagsAnd") + val tagsAnd: Provider> = project.provider { + getTags("tags") + getTags("tagsAnd") + } /** * The tags to filter by (or). */ @get:Input - val tagsOr = getTags("tagsOr") + val tagsOr: Provider> = project.provider { getTags("tagsOr") } /** * If all excluded or ignored tests should be run nevertheless. */ @get:Input - val shouldUnexclude by lazy { project.properties.containsKey("unexclude") } + val shouldUnexclude: Provider = project.provider { project.properties.containsKey("unexclude") } /** * The properties of the project parsed into a [Config] object. */ @get:Input - val projectConfig: Config by lazy { + val projectConfig: Provider = project.provider { ConfigValueFactory .fromMap( project.properties .filterKeys { it is String && it.startsWith("squit.") } - .mapKeys { (key, _) -> key.replaceFirst("squit.", "") } + .mapKeys { (key, _) -> key.replaceFirst("squit.", "") }, ) .toConfig() } @@ -91,32 +93,27 @@ open class SquitPreProcessTask @Inject constructor(private val workerExecutor: W */ @TaskAction fun run() { - val index = TestIndexer(projectConfig).index(sourceDir) { filterIndex(it) } + val index = TestIndexer(projectConfig.get()).index(sourceDir.asPath) { filterIndex(it) } - FilesUtils.deleteRecursivelyIfExisting(processedSourcesPath) - Files.createDirectories(processedSourcesPath) + FilesUtils.deleteRecursivelyIfExisting(processedSources.asPath) + Files.createDirectories(processedSources.asPath) - if (GradleVersion.current() >= GradleVersion.version("5.6")) { - val workerQueue = workerExecutor.noIsolation() + val workerQueue = workerExecutor.noIsolation() - index.forEach { test -> - workerQueue.submit(Worker::class.java) { - it.sourceDir.set(sourceDir.toFile()) - it.processedSourcesPath.set(processedSourcesPath.toFile()) - it.test.set(test) - } - } - } else { - index.forEach { test -> - SquitPreProcessRunner.run(sourceDir, processedSourcesPath, test) + index.forEach { test -> + workerQueue.submit(Worker::class.java) { + it.sourceDir.set(sourceDir.get().asFile) + it.processedSourcesPath.set(processedSources.get().asFile) + it.test.set(test) } } } private fun getTags(name: String): Set { - val tagsString = when (project.hasProperty(name)) { - true -> project.property(name) as String? - false -> null + val tagsString = if (project.hasProperty(name)) { + project.property(name)?.toString() + } else { + null } return tagsString @@ -129,35 +126,33 @@ open class SquitPreProcessTask @Inject constructor(private val workerExecutor: W private fun filterIndex(input: Pair) = when { isTestExcluded(input.second) -> { - logger.info("Excluding test ${input.first.cut(sourceDir)}") + logger.info("Excluding test ${input.first.cut(sourceDir.asPath)}") false } + !isTestCoveredByTags(input.second) -> { - logger.info("Ignoring test ${input.first.cut(sourceDir)}") + logger.info("Ignoring test ${input.first.cut(sourceDir.asPath)}") false } + else -> true } - private fun isTestExcluded(config: Config): Boolean { - return config.shouldExclude && !shouldUnexclude - } + private fun isTestExcluded(config: Config): Boolean = config.shouldExclude && !shouldUnexclude.get() - private fun isTestCoveredByTags(config: Config): Boolean { - return (tagsAnd.isEmpty() || tagsAnd.all { it in config.tags }) && - (tagsOr.isEmpty() || tagsOr.any { it in config.tags }) - } + private fun isTestCoveredByTags(config: Config): Boolean = + (tagsAnd.get().isEmpty() || tagsAnd.get().all { it in config.tags }) && + (tagsOr.get().isEmpty() || tagsOr.get().any { it in config.tags }) internal abstract class Worker : WorkAction { - - private val sourceDir get() = parameters.sourceDir.asPath - private val processedSourcesPath get() = parameters.processedSourcesPath.asPath - private val test get() = parameters.test.get() - override fun execute() { - SquitPreProcessRunner.run(sourceDir, processedSourcesPath, test) + SquitPreProcessRunner.run( + sourceDir = parameters.sourceDir.asPath, + processedSourcesPath = parameters.processedSourcesPath.asPath, + test = parameters.test.get(), + ) } } diff --git a/src/main/kotlin/de/smartsquare/squit/task/SquitPreTestTask.kt b/src/main/kotlin/de/smartsquare/squit/task/SquitPreTestTask.kt index 63c7502b..9bbed609 100644 --- a/src/main/kotlin/de/smartsquare/squit/task/SquitPreTestTask.kt +++ b/src/main/kotlin/de/smartsquare/squit/task/SquitPreTestTask.kt @@ -1,5 +1,7 @@ package de.smartsquare.squit.task enum class SquitPreTestTask { - DATABASE_SCRIPTS, PRE_RUNNERS, PRE_RUNNER_SCRIPTS + DATABASE_SCRIPTS, + PRE_RUNNERS, + PRE_RUNNER_SCRIPTS, } diff --git a/src/main/kotlin/de/smartsquare/squit/task/SquitRequestTask.kt b/src/main/kotlin/de/smartsquare/squit/task/SquitRequestTask.kt index 417fdd9e..58ddb3a1 100644 --- a/src/main/kotlin/de/smartsquare/squit/task/SquitRequestTask.kt +++ b/src/main/kotlin/de/smartsquare/squit/task/SquitRequestTask.kt @@ -15,6 +15,7 @@ import de.smartsquare.squit.config.preRunners import de.smartsquare.squit.config.preTestTasks import de.smartsquare.squit.db.ConnectionCollection import de.smartsquare.squit.db.executeScript +import de.smartsquare.squit.entity.SquitDatabaseConfiguration import de.smartsquare.squit.entity.SquitMetaInfo import de.smartsquare.squit.entity.SquitResponseInfo import de.smartsquare.squit.interfaces.SquitPostRunner @@ -29,7 +30,9 @@ import de.smartsquare.squit.util.Constants.RAW_DIRECTORY import de.smartsquare.squit.util.Constants.RESPONSES_DIRECTORY import de.smartsquare.squit.util.Constants.SOURCES_DIRECTORY import de.smartsquare.squit.util.Constants.SQUIT_DIRECTORY +import de.smartsquare.squit.util.asPath import de.smartsquare.squit.util.cut +import de.smartsquare.squit.util.dir import de.smartsquare.squit.util.lifecycleOnSameLine import de.smartsquare.squit.util.newLineIfNeeded import groovy.lang.Binding @@ -41,17 +44,20 @@ import okhttp3.Request import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.internal.http.HttpMethod import org.gradle.api.DefaultTask +import org.gradle.api.file.Directory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.Internal import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import java.io.IOException import java.nio.file.Files import java.nio.file.Path -import java.nio.file.Paths import java.sql.Driver -import java.sql.DriverManager import java.sql.SQLException import java.time.LocalDateTime import java.util.concurrent.TimeUnit @@ -60,67 +66,53 @@ import java.util.concurrent.TimeUnit * Task for running requests against the given api. Also capable of running existing sql scripts before and after the * request. */ -open class SquitRequestTask : DefaultTask() { +abstract class SquitRequestTask : DefaultTask() { /** * The jdbc driver classes to use. */ @get:Input - lateinit var jdbcDrivers: List + abstract val jdbcDrivers: ListProperty /** * The timeout in seconds to use for requests. */ - @get:Internal - var timeout = 10L + @get:Input + abstract val requestTimeout: Property /** * If squit should avoid printing anything if all tests pass. */ - @get:Internal - var silent = false + @get:Input + abstract val silent: Property /** * The class name of the jdbc [Driver] to use. */ @get:Input - val jdbcDriverClassNames by lazy { - jdbcDrivers - .map { it.trim() } - .filter { it.isNotBlank() } - .let { - logger.info("Using $it for jdbc connections.") - - it - } - } + val jdbcDriverClassNames: Provider> + get() = jdbcDrivers.map { driver -> driver.map(String::trim).filter(String::isNotBlank) } /** * The directory of the test sources. */ @get:InputDirectory - val processedSourcesPath: Path = Paths.get( - project.buildDir.path, - SQUIT_DIRECTORY, - SOURCES_DIRECTORY - ) + @get:PathSensitive(PathSensitivity.RELATIVE) + val processedSourcesDir: Provider = project.layout.buildDirectory + .dir(SQUIT_DIRECTORY, SOURCES_DIRECTORY) /** * The directory to save the results in. */ @get:OutputDirectory - val actualResponsesPath: Path = Paths.get( - project.buildDir.path, - SQUIT_DIRECTORY, - RESPONSES_DIRECTORY, - RAW_DIRECTORY - ) + val actualResponsesDir: Provider = project.layout.buildDirectory + .dir(SQUIT_DIRECTORY, RESPONSES_DIRECTORY, RAW_DIRECTORY) private val okHttpClient by lazy { OkHttpClient.Builder() - .connectTimeout(timeout, TimeUnit.SECONDS) - .writeTimeout(timeout, TimeUnit.SECONDS) - .readTimeout(timeout, TimeUnit.SECONDS) + .connectTimeout(requestTimeout.get(), TimeUnit.SECONDS) + .writeTimeout(requestTimeout.get(), TimeUnit.SECONDS) + .readTimeout(requestTimeout.get(), TimeUnit.SECONDS) .build() } @@ -137,27 +129,26 @@ open class SquitRequestTask : DefaultTask() { /** * Runs the task. */ - @Suppress("unused") @TaskAction fun run() { - jdbcDriverClassNames.forEach { - DriverManager.registerDriver(Class.forName(it).getConstructor().newInstance() as Driver) - } + jdbcDriverClassNames.get() + .onEach { logger.info("Using $it for jdbc connections.") } + .forEach { Class.forName(it) } - FilesUtils.deleteRecursivelyIfExisting(actualResponsesPath) - Files.createDirectories(actualResponsesPath) + FilesUtils.deleteRecursivelyIfExisting(actualResponsesDir.asPath) + Files.createDirectories(actualResponsesDir.asPath) dbConnections.use { - FilesUtils.getLeafDirectories(processedSourcesPath).forEachIndexed { index, testDirectoryPath -> - if (!silent) { + FilesUtils.getLeafDirectories(processedSourcesDir.asPath).forEachIndexed { index, testDirectoryPath -> + if (!silent.get()) { logger.lifecycleOnSameLine( "Running test ${index + 1}", - project.gradle.startParameter.consoleOutput + project.gradle.startParameter.consoleOutput, ) } val resultResponsePath = Files.createDirectories( - actualResponsesPath.resolve(testDirectoryPath.cut(processedSourcesPath)) + actualResponsesDir.asPath.resolve(testDirectoryPath.cut(processedSourcesDir.asPath)), ) val errorFile = testDirectoryPath.resolve(ERROR) @@ -204,7 +195,7 @@ open class SquitRequestTask : DefaultTask() { testDirectoryPath: Path, resultResponsePath: Path, requestPath: Path?, - config: Config + config: Config, ) { val resultResponseFilePath = resultResponsePath.resolve(MediaTypeFactory.actualResponse(config.mediaType)) @@ -223,21 +214,21 @@ open class SquitRequestTask : DefaultTask() { Files.write(resultResponseInfoFilePath, responseInfo.toJson().toByteArray()) if (!apiResponse.isSuccessful) { - if (!silent) logger.newLineIfNeeded() + if (!silent.get()) logger.newLineIfNeeded() logger.info( - "Unsuccessful request for test ${testDirectoryPath.cut(processedSourcesPath)} " + - "(status code: ${apiResponse.code})" + "Unsuccessful request for test ${testDirectoryPath.cut(processedSourcesDir.asPath)} " + + "(status code: ${apiResponse.code})", ) } else if ( mediaType?.type != config.mediaType.type || mediaType.subtype != config.mediaType.subtype ) { - if (!silent) logger.newLineIfNeeded() + if (!silent.get()) logger.newLineIfNeeded() logger.info( - "Unexpected Media type $mediaType for test ${testDirectoryPath.cut(processedSourcesPath)}. " + - "Expected ${config.mediaType}" + "Unexpected Media type $mediaType for test " + + "${testDirectoryPath.cut(processedSourcesDir.asPath)}. Expected ${config.mediaType}", ) } } catch (error: IOException) { @@ -259,12 +250,7 @@ open class SquitRequestTask : DefaultTask() { private fun executePreDatabaseScripts(config: Config, testDirectoryPath: Path) { config.databaseConfigurations.forEach { - executeScriptIfExisting( - testDirectoryPath.resolve("${it.name}_pre.sql"), - it.jdbcAddress, - it.username, - it.password - ) + executeScriptIfExisting(it, testDirectoryPath.resolve("${it.name}_pre.sql")) } } @@ -298,12 +284,7 @@ open class SquitRequestTask : DefaultTask() { private fun executePostDatabaseScripts(config: Config, testDirectoryPath: Path) { config.databaseConfigurations.forEach { - executeScriptIfExisting( - testDirectoryPath.resolve("${it.name}_post.sql"), - it.jdbcAddress, - it.username, - it.password - ) + executeScriptIfExisting(it, testDirectoryPath.resolve("${it.name}_post.sql")) } } @@ -333,30 +314,27 @@ open class SquitRequestTask : DefaultTask() { .headers(config.headers.toHeaders()) .method(config.method, requestBody) .url(config.endpoint) - .build() + .build(), ) } - private fun executeScriptIfExisting( - path: Path, - jdbc: String, - username: String, - password: String - ) = if (Files.exists(path)) { - try { - dbConnections.createOrGet(jdbc, username, password).executeScript(path) + private fun executeScriptIfExisting(dbConfiguration: SquitDatabaseConfiguration, path: Path): Boolean { + if (!Files.exists(path)) return true + + return try { + dbConnections + .createOrGet(dbConfiguration.jdbcAddress, dbConfiguration.username, dbConfiguration.password) + .executeScript(path) true } catch (error: SQLException) { logger.newLineIfNeeded() logger.warn( "Could not run database script ${path.fileName} for test " + - "${path.parent.cut(processedSourcesPath)} (${error.toString().trim()})" + "${path.parent.cut(processedSourcesDir.asPath)} (${error.toString().trim()})", ) false } - } else { - true } } diff --git a/src/main/kotlin/de/smartsquare/squit/task/SquitTestTask.kt b/src/main/kotlin/de/smartsquare/squit/task/SquitTestTask.kt index 4e5ed426..9825f4a7 100644 --- a/src/main/kotlin/de/smartsquare/squit/task/SquitTestTask.kt +++ b/src/main/kotlin/de/smartsquare/squit/task/SquitTestTask.kt @@ -23,10 +23,18 @@ import de.smartsquare.squit.util.Constants.RAW_DIRECTORY import de.smartsquare.squit.util.Constants.RESPONSES_DIRECTORY import de.smartsquare.squit.util.Constants.SOURCES_DIRECTORY import de.smartsquare.squit.util.Constants.SQUIT_DIRECTORY +import de.smartsquare.squit.util.asPath import de.smartsquare.squit.util.countTestResults import de.smartsquare.squit.util.cut +import de.smartsquare.squit.util.dir +import de.smartsquare.squit.util.file import org.gradle.api.DefaultTask import org.gradle.api.GradleException +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileTree +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory @@ -41,59 +49,52 @@ import org.gradle.api.tasks.TaskAction import java.nio.charset.Charset import java.nio.file.Files import java.nio.file.Path -import java.nio.file.Paths -import kotlin.streams.toList /** * Task for comparing the actual responses to the expected responses and generating a report. */ @CacheableTask -open class SquitTestTask : DefaultTask() { +abstract class SquitTestTask : DefaultTask() { /** * The path to save reports and possible failures in. */ @get:OutputDirectory - lateinit var reportDir: Path + abstract val reportDir: DirectoryProperty /** * If squit should avoid printing anything if all tests pass. */ @get:Internal - var silent = false + abstract val silent: Property /** * If failures should be ignored. * In that case the task passes, even if tests have failed. */ @get:Input - var ignoreFailures = false + abstract val ignoreFailures: Property /** * Configuration class for various properties of the media types. */ @get:Nested - lateinit var mediaTypeConfig: MediaTypeConfig + abstract val mediaTypeConfig: Property /** * The directory of the test sources. */ @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) - val processedSourcesPath: Path = Paths.get( - project.buildDir.path, - SQUIT_DIRECTORY, SOURCES_DIRECTORY - ) + val processedSourcesDir = project.layout.buildDirectory.dir(SQUIT_DIRECTORY, SOURCES_DIRECTORY) /** * The directory of the previously (processed) requested responses. */ @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) - val processedResponsesPath: Path = Paths.get( - project.buildDir.path, - SQUIT_DIRECTORY, RESPONSES_DIRECTORY, PROCESSED_DIRECTORY - ) + val processedResponsesDir = project.layout.buildDirectory + .dir(SQUIT_DIRECTORY, RESPONSES_DIRECTORY, PROCESSED_DIRECTORY) /** * Collection of meta.json files for up-to-date checking. @@ -102,41 +103,30 @@ open class SquitTestTask : DefaultTask() { @get:Optional @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) - val metaPaths: List by lazy { - val rawDirectoryPath = Paths.get(project.buildDir.path, SQUIT_DIRECTORY, RESPONSES_DIRECTORY, RAW_DIRECTORY) - - if (Files.exists(rawDirectoryPath)) { - Files.walk(rawDirectoryPath).use { stream -> - stream.filter { Files.isRegularFile(it) && it.fileName.toString() == META }.toList() - } - } else { - emptyList() - } - } + val metaFiles: Provider = project.layout.buildDirectory + .dir(SQUIT_DIRECTORY, RESPONSES_DIRECTORY, RAW_DIRECTORY) + .map { dir -> dir.asFileTree.matching { tree -> tree.include("**/$META") } } /** * The directory to generate the xml report file into. */ @get:OutputFile - val xmlReportFilePath: Path by lazy { - reportDir.resolve("xml").resolve("index.xml") - } + val xmlReportFile + get() = reportDir.file("xml", "index.xml") /** * The directory to generate the xml report file into. */ @get:OutputDirectory - val htmlReportDirectoryPath: Path by lazy { - reportDir.resolve("html") - } + val htmlReportDir: Provider + get() = reportDir.dir("html") /** * The directory to copy failed tests into. */ @get:OutputDirectory - val failureResultDirectory: Path by lazy { - reportDir.resolve("failures") - } + val failureReportDir: Provider + get() = reportDir.dir("failures") private var nextResultId = 0L @@ -148,11 +138,10 @@ open class SquitTestTask : DefaultTask() { /** * Runs the task. */ - @Suppress("unused") @TaskAction fun run() { - FilesUtils.deleteRecursivelyIfExisting(reportDir) - Files.createDirectories(processedSourcesPath) + FilesUtils.deleteRecursivelyIfExisting(reportDir.asPath) + Files.createDirectories(processedSourcesDir.asPath) val results = processTests() @@ -162,25 +151,27 @@ open class SquitTestTask : DefaultTask() { val (successfulTests, failedTests, ignoredTests) = results.countTestResults() - if (!silent) { + if (!silent.get()) { val totalText = if (results.size == 1) "One test ran." else "${results.size} tests ran." val ignoredText = if (ignoredTests > 0) " ($ignoredTests ignored)" else "" println("$totalText\n$successfulTests successful and $failedTests failed$ignoredText.") println() - println("XML report: file://$xmlReportFilePath") - println("HTML report: file://${htmlReportDirectoryPath.resolve("index.html")}") + println("XML report: file://${xmlReportFile.asPath}") + println("HTML report: file://${htmlReportDir.asPath.resolve("index.html")}") } - if (failedTests > 0 && !ignoreFailures) throw GradleException("Failing tests.") + if (failedTests > 0 && !ignoreFailures.get()) throw GradleException("Failing tests.") } private fun processTests(): List { val resultList = mutableListOf() - FilesUtils.getLeafDirectories(processedResponsesPath).forEach { actualResponsePath -> + FilesUtils.getLeafDirectories(processedResponsesDir.asPath).forEach { actualResponsePath -> val configPath = FilesUtils.validateExistence( - processedSourcesPath.resolve(actualResponsePath.cut(processedResponsesPath)).resolve(CONFIG) + processedSourcesDir.asPath + .resolve(actualResponsePath.cut(processedResponsesDir.asPath)) + .resolve(CONFIG), ) val config = ConfigFactory.parseFile(configPath.toFile()) @@ -192,7 +183,9 @@ open class SquitTestTask : DefaultTask() { resultList += if (Files.exists(errorFile)) { constructResult( FilesUtils.readAllBytes(errorFile).toString(Charset.defaultCharset()), - expectedResponseInfo, actualResponsePath, config + expectedResponseInfo, + actualResponsePath, + config, ) } else { val bodyDiff = createBodyDifference(actualResponsePath, config) @@ -211,13 +204,13 @@ open class SquitTestTask : DefaultTask() { private fun createResponseInfoDifference( actualResponsePath: Path, - expectedResponseInfo: SquitResponseInfo + expectedResponseInfo: SquitResponseInfo, ): String { if (!expectedResponseInfo.isDefault) { - val contextPath = actualResponsePath.parent.parent.cut(processedResponsesPath) + val contextPath = actualResponsePath.parent.parent.cut(processedResponsesDir.asPath) val suitePath = actualResponsePath.parent.fileName val path: Path = contextPath.resolve(suitePath) - val squitBuildDirectoryPath = Paths.get(project.buildDir.path, SQUIT_DIRECTORY) + val squitBuildDirectoryPath = project.layout.buildDirectory.asPath.resolve(SQUIT_DIRECTORY) val testDirectoryPath = actualResponsePath.fileName val fullPath = path.resolve(testDirectoryPath) val resolvedPath = squitBuildDirectoryPath @@ -226,7 +219,7 @@ open class SquitTestTask : DefaultTask() { .resolve(fullPath) val actualResponseInfoPath = FilesUtils.validateExistence( - resolvedPath.resolve(ACTUAL_RESPONSE_INFO) + resolvedPath.resolve(ACTUAL_RESPONSE_INFO), ) val actualResponse = FilesUtils.readAllBytes(actualResponseInfoPath).toString(Charset.defaultCharset()) @@ -239,43 +232,47 @@ open class SquitTestTask : DefaultTask() { private fun createBodyDifference(actualResponsePath: Path, config: Config): String { val actualResponseFilePath = FilesUtils.validateExistence( - actualResponsePath.resolve(MediaTypeFactory.actualResponse(config.mediaType)) + actualResponsePath.resolve(MediaTypeFactory.actualResponse(config.mediaType)), ) val expectedResponseFilePath = FilesUtils.validateExistence( - processedSourcesPath - .resolve(actualResponsePath.cut(processedResponsesPath)) - .resolve(MediaTypeFactory.expectedResponse(config.mediaType)) + processedSourcesDir.asPath + .resolve(actualResponsePath.cut(processedResponsesDir.asPath)) + .resolve(MediaTypeFactory.expectedResponse(config.mediaType)), ) val expectedResponse = FilesUtils.readAllBytes(expectedResponseFilePath) val actualResponse = FilesUtils.readAllBytes(actualResponseFilePath) - return MediaTypeFactory.differ(config.mediaType, mediaTypeConfig) + return MediaTypeFactory.differ(config.mediaType, mediaTypeConfig.get()) .diff(expectedResponse, actualResponse) } private fun writeXmlReport(result: List) { - Files.createDirectories(xmlReportFilePath.parent) + Files.createDirectories(xmlReportFile.asPath.parent) - XmlReportWriter().writeReport(result, xmlReportFilePath) + XmlReportWriter().writeReport(result, xmlReportFile.asPath) } private fun writeHtmlReport(result: List) { - Files.createDirectories(htmlReportDirectoryPath) + Files.createDirectories(htmlReportDir.asPath) - HtmlReportWriter(logger).writeReport(result, htmlReportDirectoryPath, mediaTypeConfig) + HtmlReportWriter(logger).writeReport(result, htmlReportDir.asPath, mediaTypeConfig.get()) } private fun copyFailures(result: List) { - FilesUtils.deleteRecursivelyIfExisting(failureResultDirectory) - Files.createDirectories(failureResultDirectory) + FilesUtils.deleteRecursivelyIfExisting(failureReportDir.asPath) + Files.createDirectories(failureReportDir.asPath) result.filterNot { it.isSuccess }.forEach { - val resultDirectoryPath = Files.createDirectories(failureResultDirectory.resolve(it.fullPath)) + val resultDirectoryPath = Files.createDirectories(failureReportDir.asPath.resolve(it.fullPath)) + + val testProcessedSourcesPath = FilesUtils + .validateExistence(processedSourcesDir.asPath.resolve(it.fullPath)) + + val testActualResponsesPath = FilesUtils + .validateExistence(processedResponsesDir.asPath.resolve(it.fullPath)) - val testProcessedSourcesPath = FilesUtils.validateExistence(processedSourcesPath.resolve(it.fullPath)) - val testActualResponsesPath = FilesUtils.validateExistence(processedResponsesPath.resolve(it.fullPath)) val testDifferenceFile = Files.createFile(resultDirectoryPath.resolve(DIFF)) FilesUtils.copyFilesFromDirectory(testProcessedSourcesPath, resultDirectoryPath) @@ -289,10 +286,10 @@ open class SquitTestTask : DefaultTask() { responseInfo: SquitResponseInfo, actualResponsePath: Path, config: Config, - isIgnored: Boolean = false + isIgnored: Boolean = false, ): SquitResult { - val squitBuildDirectoryPath = Paths.get(project.buildDir.path, SQUIT_DIRECTORY) - val contextPath = actualResponsePath.parent.parent.cut(processedResponsesPath) + val squitBuildDirectoryPath = project.layout.buildDirectory.dir(SQUIT_DIRECTORY).asPath + val contextPath = actualResponsePath.parent.parent.cut(processedResponsesDir.asPath) val suitePath = actualResponsePath.parent.fileName val testDirectoryPath = actualResponsePath.fileName val id = nextResultId++ @@ -301,13 +298,13 @@ open class SquitTestTask : DefaultTask() { true -> SquitResult( id, differences, responseInfo, isIgnored, config.mediaType, config.title, contextPath, suitePath, - testDirectoryPath, squitBuildDirectoryPath + testDirectoryPath, squitBuildDirectoryPath, ) false -> SquitResult( id, "", responseInfo, isIgnored, config.mediaType, config.title, contextPath, suitePath, - testDirectoryPath, squitBuildDirectoryPath + testDirectoryPath, squitBuildDirectoryPath, ) } } diff --git a/src/main/kotlin/de/smartsquare/squit/util/UtilExtensions.kt b/src/main/kotlin/de/smartsquare/squit/util/UtilExtensions.kt index 981e9861..fcbb8e55 100644 --- a/src/main/kotlin/de/smartsquare/squit/util/UtilExtensions.kt +++ b/src/main/kotlin/de/smartsquare/squit/util/UtilExtensions.kt @@ -7,7 +7,11 @@ import de.smartsquare.squit.entity.SquitOutputFormat import de.smartsquare.squit.entity.SquitResult import org.dom4j.io.OutputFormat import org.dom4j.io.XMLWriter +import org.gradle.api.file.Directory import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileSystemLocation +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Provider import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -72,11 +76,28 @@ fun String.cleanSqlString() = this .replace("\uFEFF", "") // This is a weird unicode blank character, present in some sql files. .trim() +/** + * Resolves one or more directory parts. + */ +fun DirectoryProperty.dir(first: String, vararg more: String): Provider = + dir(Paths.get(first, *more).toString()) + +/** + * Resolves one or more file parts. + */ +fun DirectoryProperty.file(first: String, vararg more: String): Provider = + file(Paths.get(first, *more).toString()) + /** * Returns this as a [Path]. */ val DirectoryProperty.asPath: Path get() = asFile.get().toPath() +/** + * Returns this as a [Path]. + */ +val Provider.asPath: Path get() = get().asFile.toPath() + /** * Iterate the list of [SquitResult]s and returns a [Triple] of successful, failed and ignored tests. */ diff --git a/src/test/kotlin/de/smartsquare/squit/GradleCompatibilityTest.kt b/src/test/kotlin/de/smartsquare/squit/GradleCompatibilityTest.kt index d8367ff3..8c027c07 100644 --- a/src/test/kotlin/de/smartsquare/squit/GradleCompatibilityTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/GradleCompatibilityTest.kt @@ -23,22 +23,16 @@ class GradleCompatibilityTest { @JvmStatic fun provideVersions(): Stream { - val result = mutableListOf( - Arguments.of(GradleVersion.current()) - ) - - // These older Gradle Versions do not work on Java 17+. - if (JavaVersion.current() <= JavaVersion.VERSION_16) { - result += listOf( - Arguments.of(GradleVersion.version("7.0")) - ) + val result = mutableListOf(Arguments.of(GradleVersion.current())) + + // This older Gradle version does not work on Java 20+. + if (JavaVersion.current() < JavaVersion.VERSION_20) { + result += Arguments.of(GradleVersion.version("8.0.2")) } - // These older Gradle Versions do not work on Java 16+. - if (JavaVersion.current() <= JavaVersion.VERSION_15) { - result += listOf( - Arguments.of(GradleVersion.version("6.8")) - ) + // This older Gradle version does not work on Java 21+. + if (JavaVersion.current() < JavaVersion.VERSION_21) { + result += Arguments.of(GradleVersion.version("7.3.2")) } return result.stream() @@ -69,8 +63,9 @@ class GradleCompatibilityTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitTest", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "--stacktrace" + "squitTest", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", ) val result = gradleRunner(project, arguments, gradleVersion).build() @@ -81,8 +76,8 @@ class GradleCompatibilityTest { @Test @DisabledForJreRange(min = JRE.JAVA_16) fun `outdated version`() { - val result = gradleRunner(project, emptyList(), GradleVersion.version("6.7")).buildAndFail() + val result = gradleRunner(project, emptyList(), GradleVersion.version("7.2")).buildAndFail() - result.output shouldContain "Minimum supported Gradle version is 6.8. Current version is 6.7." + result.output shouldContain "Minimum supported Gradle version is 7.3. Current version is 7.2." } } diff --git a/src/test/kotlin/de/smartsquare/squit/TestUtils.kt b/src/test/kotlin/de/smartsquare/squit/TestUtils.kt index e1fa7c24..f0a67ca7 100644 --- a/src/test/kotlin/de/smartsquare/squit/TestUtils.kt +++ b/src/test/kotlin/de/smartsquare/squit/TestUtils.kt @@ -6,23 +6,19 @@ import org.gradle.util.GradleVersion import java.io.File import java.nio.file.Files import java.nio.file.Path -import java.nio.file.StandardCopyOption +import org.h2.Driver as H2Driver /** * Utility class for methods which cannot be a extension function. */ object TestUtils { - private val DB_FILTER = { it: Path -> - Files.isRegularFile(it) && it.fileName.toString().endsWith(".db") - } - /** - * Deletes all database files found in the passed [path] directory. + * Deletes all database files found in the given [path] directory. */ - fun deleteDatabaseFiles(path: Path) = Files.newDirectoryStream(path, DB_FILTER).use { files -> - files.forEach { Files.delete(it) } - } + fun deleteDatabaseFiles(path: Path) = Files + .newDirectoryStream(path) { Files.isRegularFile(it) && it.fileName.toString().endsWith(".db") } + .use { files -> files.forEach { Files.delete(it) } } /** * Returns the [Path] of the resource with the given [name]… @@ -35,41 +31,23 @@ object TestUtils { */ fun GradleRunner.withExtendedPluginClasspath(): GradleRunner { val classpath = PluginUnderTestMetadataReading.readImplementationClasspath() - .plus(File(org.h2.Driver::class.java.protectionDomain.codeSource.location.toURI())) + .plus(File(H2Driver::class.java.protectionDomain.codeSource.location.toURI())) .plus(File(XmlPreProcessor::class.java.protectionDomain.codeSource.location.toURI())) .plus(File(XmlPostProcessor::class.java.protectionDomain.codeSource.location.toURI())) return withPluginClasspath(classpath) } -/** - * Configures the GradleRunner to run with the jacoco agent. - */ -fun GradleRunner.withJacoco(): GradleRunner { - val properties = this.javaClass.classLoader.getResource("testkit-gradle.properties") - - if (properties != null) { - Files.copy( - File(properties.toURI()).toPath(), - projectDir.toPath().resolve("gradle.properties"), - StandardCopyOption.REPLACE_EXISTING - ) - } - - return this -} - /** * Creates a [GradleRunner] with the given [project], [arguments] and an optional [version]. * * This also applies the following default arguments: "clean --stacktrace". */ -fun gradleRunner(project: Path, arguments: List, version: GradleVersion? = null): GradleRunner { - return GradleRunner.create() +fun gradleRunner(project: Path, arguments: List, version: GradleVersion? = null): GradleRunner = + GradleRunner.create() .withArguments(listOf("clean") + arguments + "--stacktrace") .apply { if (version !== null) withGradleVersion(version.version) } .withProjectDir(project.toFile()) .withExtendedPluginClasspath() + .withDebug(true) .forwardOutput() - .withJacoco() -} diff --git a/src/test/kotlin/de/smartsquare/squit/config/ConfigExtensionsTest.kt b/src/test/kotlin/de/smartsquare/squit/config/ConfigExtensionsTest.kt index 7f87a861..a659eb80 100644 --- a/src/test/kotlin/de/smartsquare/squit/config/ConfigExtensionsTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/config/ConfigExtensionsTest.kt @@ -70,8 +70,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "method" to "GET" - ) + "method" to "GET", + ), ) config.method shouldBeEqualTo "GET" @@ -89,8 +89,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "testDir" to Paths.get(".").toString() - ) + "testDir" to Paths.get(".").toString(), + ), ) val call = { config.validate() } @@ -103,8 +103,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "testDir" to Paths.get("does_not_exist").toString() - ) + "testDir" to Paths.get("does_not_exist").toString(), + ), ) val call = { config.validate() } @@ -117,8 +117,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "preProcessors" to listOf("java.lang.String") - ) + "preProcessors" to listOf("java.lang.String"), + ), ) val call = { config.validate() } @@ -131,8 +131,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "preProcessors" to listOf("not.existing") - ) + "preProcessors" to listOf("not.existing"), + ), ) val call = { config.validate() } @@ -145,8 +145,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "preProcessorScripts" to listOf(testProject.resolve("build.gradle").toString()) - ) + "preProcessorScripts" to listOf(testProject.resolve("build.gradle").toString()), + ), ) val call = { config.validate() } @@ -160,8 +160,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "preProcessorScripts" to listOf(notExistingFilePath.toString()) - ) + "preProcessorScripts" to listOf(notExistingFilePath.toString()), + ), ) val call = { config.validate() } @@ -174,8 +174,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "postProcessors" to listOf("java.lang.String") - ) + "postProcessors" to listOf("java.lang.String"), + ), ) val call = { config.validate() } @@ -188,8 +188,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "postProcessors" to listOf("not.existing") - ) + "postProcessors" to listOf("not.existing"), + ), ) val call = { config.validate() } @@ -202,8 +202,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "postProcessorScripts" to listOf(testProject.resolve("build.gradle").toString()) - ) + "postProcessorScripts" to listOf(testProject.resolve("build.gradle").toString()), + ), ) val call = { config.validate() } @@ -217,8 +217,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "postProcessorScripts" to listOf(notExistingFilePath.toString()) - ) + "postProcessorScripts" to listOf(notExistingFilePath.toString()), + ), ) val call = { config.validate() } @@ -231,8 +231,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "preRunners" to listOf("java.lang.String") - ) + "preRunners" to listOf("java.lang.String"), + ), ) val call = { config.validate() } @@ -245,8 +245,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "preRunners" to listOf("not.existing") - ) + "preRunners" to listOf("not.existing"), + ), ) val call = { config.validate() } @@ -259,8 +259,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "preRunnerScripts" to listOf(testProject.resolve("build.gradle").toString()) - ) + "preRunnerScripts" to listOf(testProject.resolve("build.gradle").toString()), + ), ) val call = { config.validate() } @@ -274,8 +274,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "preRunnerScripts" to listOf(notExistingFilePath.toString()) - ) + "preRunnerScripts" to listOf(notExistingFilePath.toString()), + ), ) val call = { config.validate() } @@ -288,8 +288,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "postRunners" to listOf("java.lang.String") - ) + "postRunners" to listOf("java.lang.String"), + ), ) val call = { config.validate() } @@ -302,8 +302,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "postRunners" to listOf("not.existing") - ) + "postRunners" to listOf("not.existing"), + ), ) val call = { config.validate() } @@ -316,8 +316,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "postRunnerScripts" to listOf(testProject.resolve("build.gradle").toString()) - ) + "postRunnerScripts" to listOf(testProject.resolve("build.gradle").toString()), + ), ) val call = { config.validate() } @@ -331,8 +331,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "postRunnerScripts" to listOf(notExistingFilePath.toString()) - ) + "postRunnerScripts" to listOf(notExistingFilePath.toString()), + ), ) val call = { config.validate() } @@ -345,8 +345,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "tags" to listOf("a", "b") - ) + "tags" to listOf("a", "b"), + ), ) val call = { config.validate() } @@ -359,8 +359,8 @@ class ConfigExtensionsTest { val config = ConfigFactory.parseMap( mapOf( "endpoint" to "https://example.com", - "tags" to listOf("a", "") - ) + "tags" to listOf("a", ""), + ), ) val call = { config.validate() } @@ -378,10 +378,10 @@ class ConfigExtensionsTest { "name" to "test1", "jdbc" to "test2", "username" to "test3", - "password" to "test4" - ) - ) - ) + "password" to "test4", + ), + ), + ), ) val call = { config.validate() } @@ -399,10 +399,10 @@ class ConfigExtensionsTest { "name" to "", "jdbc" to "test2", "username" to "test3", - "password" to "test4" - ) - ) - ) + "password" to "test4", + ), + ), + ), ) val call = { config.validate() } @@ -420,10 +420,10 @@ class ConfigExtensionsTest { "name" to "test1", "jdbc" to "", "username" to "test3", - "password" to "test4" - ) - ) - ) + "password" to "test4", + ), + ), + ), ) val call = { config.validate() } @@ -441,10 +441,10 @@ class ConfigExtensionsTest { "name" to "test1", "jdbc" to "test2", "username" to "", - "password" to "test4" - ) - ) - ) + "password" to "test4", + ), + ), + ), ) val call = { config.validate() } @@ -462,10 +462,10 @@ class ConfigExtensionsTest { "name" to "test1", "jdbc" to "test2", "username" to "test3", - "password" to "" - ) - ) - ) + "password" to "", + ), + ), + ), ) val call = { config.validate() } @@ -480,9 +480,9 @@ class ConfigExtensionsTest { "endpoint" to "https://example.com", "headers" to mapOf( "abc" to "def", - "ghi" to "jkl" - ) - ) + "ghi" to "jkl", + ), + ), ) config.headers shouldContain ("abc" to "def") @@ -500,7 +500,7 @@ class ConfigExtensionsTest { "endpoint" to "https://example.com", "preTestTasks" to listOf("DATABASE_SCRIPTS", "PRE_RUNNERS", "PRE_RUNNER_SCRIPTS"), "postTestTasks" to emptyList(), - ) + ), ) val call = { config.validate() } @@ -513,7 +513,7 @@ class ConfigExtensionsTest { mapOf( "endpoint" to "https://example.com", "preTestTasks" to listOf("NotExistingTask"), - ) + ), ) val call = { config.validate() } @@ -528,7 +528,7 @@ class ConfigExtensionsTest { mapOf( "endpoint" to "https://example.com", "postTestTasks" to listOf("NotExistingTask"), - ) + ), ) val call = { config.validate() } diff --git a/src/test/kotlin/de/smartsquare/squit/config/TestIndexerTest.kt b/src/test/kotlin/de/smartsquare/squit/config/TestIndexerTest.kt index bbd0deb5..cce88552 100644 --- a/src/test/kotlin/de/smartsquare/squit/config/TestIndexerTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/config/TestIndexerTest.kt @@ -19,9 +19,9 @@ class TestIndexerTest { ConfigFactory.parseMap( mapOf( "rootDir" to "$testProjectRoot", - "endpointPlaceholder" to "https://example.com" - ) - ) + "endpointPlaceholder" to "https://example.com", + ), + ), ) @Test diff --git a/src/test/kotlin/de/smartsquare/squit/db/ConnectionCollectionTest.kt b/src/test/kotlin/de/smartsquare/squit/db/ConnectionCollectionTest.kt index d5b0104b..87705e6d 100644 --- a/src/test/kotlin/de/smartsquare/squit/db/ConnectionCollectionTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/db/ConnectionCollectionTest.kt @@ -3,7 +3,9 @@ package de.smartsquare.squit.db import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.h2.Driver as H2Driver class ConnectionCollectionTest { @@ -11,6 +13,11 @@ class ConnectionCollectionTest { private val username = "test" private val password = "test" + @BeforeEach + fun setUp() { + H2Driver.load() + } + @Test fun `creating a new db connection`() { val connectionCollection = ConnectionCollection() diff --git a/src/test/kotlin/de/smartsquare/squit/entity/SquitResultTest.kt b/src/test/kotlin/de/smartsquare/squit/entity/SquitResultTest.kt index afa34a0b..1797dc69 100644 --- a/src/test/kotlin/de/smartsquare/squit/entity/SquitResultTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/entity/SquitResultTest.kt @@ -1,9 +1,9 @@ package de.smartsquare.squit.entity -import java.nio.file.Paths import okhttp3.MediaType.Companion.toMediaType import org.amshove.kluent.shouldBeEqualTo import org.junit.jupiter.api.Test +import java.nio.file.Paths class SquitResultTest { @@ -13,7 +13,7 @@ class SquitResultTest { fun `cutting the first path element of result with full path`() { val subject = SquitResult( 0, "", SquitResponseInfo(), false, mediaType, "", - Paths.get("a"), Paths.get("b"), Paths.get("c"), Paths.get("x") + Paths.get("a"), Paths.get("b"), Paths.get("c"), Paths.get("x"), ) val result = subject.cutFirstPathElement() @@ -25,7 +25,7 @@ class SquitResultTest { fun `cutting the first path element of a result without context path`() { val subject = SquitResult( 0, "", SquitResponseInfo(), false, mediaType, "", - Paths.get(""), Paths.get("b"), Paths.get("c"), Paths.get("x") + Paths.get(""), Paths.get("b"), Paths.get("c"), Paths.get("x"), ) val result = subject.cutFirstPathElement() @@ -37,7 +37,7 @@ class SquitResultTest { fun `cutting the first path element of a result with only testDirectoryPath`() { val subject = SquitResult( 0, "", SquitResponseInfo(), false, mediaType, "", - Paths.get(""), Paths.get(""), Paths.get("c"), Paths.get("x") + Paths.get(""), Paths.get(""), Paths.get("c"), Paths.get("x"), ) val result = subject.cutFirstPathElement() @@ -49,7 +49,7 @@ class SquitResultTest { fun `cutting the first path element of a result with empty path`() { val subject = SquitResult( 0, "", SquitResponseInfo(), false, mediaType, "", - Paths.get(""), Paths.get(""), Paths.get(""), Paths.get("x") + Paths.get(""), Paths.get(""), Paths.get(""), Paths.get("x"), ) val result = subject.cutFirstPathElement() diff --git a/src/test/kotlin/de/smartsquare/squit/entity/SquitResultTreeTest.kt b/src/test/kotlin/de/smartsquare/squit/entity/SquitResultTreeTest.kt index 5b531c1c..8caad984 100644 --- a/src/test/kotlin/de/smartsquare/squit/entity/SquitResultTreeTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/entity/SquitResultTreeTest.kt @@ -1,7 +1,5 @@ package de.smartsquare.squit.entity -import java.nio.file.Path -import java.nio.file.Paths import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType import org.amshove.kluent.shouldBe @@ -9,6 +7,8 @@ import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue import org.junit.jupiter.api.Test +import java.nio.file.Path +import java.nio.file.Paths class SquitResultTreeTest { @@ -19,7 +19,7 @@ class SquitResultTreeTest { constructTestSquitResult(Paths.get("a"), Paths.get("b"), Paths.get("c").resolve("c")), constructTestSquitResult(Paths.get("a"), Paths.get("b"), Paths.get("d"), isIgnored = true), constructTestSquitResult(Paths.get("a"), Paths.get("c"), Paths.get(""), result = "xyz"), - constructTestSquitResult(Paths.get("x"), Paths.get("y").resolve("z"), Paths.get("x")) + constructTestSquitResult(Paths.get("x"), Paths.get("y").resolve("z"), Paths.get("x")), ) val resultTrees = SquitResultTree.fromList(resultList) @@ -39,15 +39,16 @@ class SquitResultTreeTest { resultTrees.last().children.first().children.first().children.first().isSuccess.shouldBeTrue() } + @Suppress("LongParameterList") private fun constructTestSquitResult( contextPath: Path, suitePath: Path, testDirectoryPath: Path, mediaType: MediaType = "application/xml".toMediaType(), result: String = "", - isIgnored: Boolean = false + isIgnored: Boolean = false, ) = SquitResult( 0, result, SquitResponseInfo(), isIgnored, mediaType, "", - contextPath, suitePath, testDirectoryPath, Paths.get("") + contextPath, suitePath, testDirectoryPath, Paths.get(""), ) } diff --git a/src/test/kotlin/de/smartsquare/squit/entity/SquitTestTest.kt b/src/test/kotlin/de/smartsquare/squit/entity/SquitTestTest.kt index 6d20718c..9588e54d 100644 --- a/src/test/kotlin/de/smartsquare/squit/entity/SquitTestTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/entity/SquitTestTest.kt @@ -1,14 +1,14 @@ package de.smartsquare.squit.entity import com.typesafe.config.ConfigFactory +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeInstanceOf +import org.junit.jupiter.api.Test import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.nio.file.Paths -import org.amshove.kluent.shouldBeEqualTo -import org.amshove.kluent.shouldBeInstanceOf -import org.junit.jupiter.api.Test class SquitTestTest { @@ -21,7 +21,7 @@ class SquitTestTest { Paths.get("/response"), mapOf("test1_pre.sql" to listOf(Paths.get("/pre/test1_pre.sql"))), mapOf("test1_post.sql" to listOf(Paths.get("/post/test1_post.sql"))), - listOf(Paths.get("/test1/description.md")) + listOf(Paths.get("/test1/description.md")), ) val test2 = SquitTest( @@ -31,10 +31,10 @@ class SquitTestTest { Paths.get("/response"), mapOf( "test2_pre.sql" to listOf(Paths.get("/pre/test2_pre.sql")), - "test1_pre.sql" to listOf(Paths.get("/additional/test1_pre.sql")) + "test1_pre.sql" to listOf(Paths.get("/additional/test1_pre.sql")), ), mapOf("test2_post.sql" to listOf(Paths.get("/post/test2_post.sql"))), - listOf(Paths.get("/test2/description.md")) + listOf(Paths.get("/test2/description.md")), ) val expected = SquitTest( @@ -44,13 +44,13 @@ class SquitTestTest { Paths.get("/response"), mapOf( "test1_pre.sql" to listOf(Paths.get("/additional/test1_pre.sql"), Paths.get("/pre/test1_pre.sql")), - "test2_pre.sql" to listOf(Paths.get("/pre/test2_pre.sql")) + "test2_pre.sql" to listOf(Paths.get("/pre/test2_pre.sql")), ), mapOf( "test1_post.sql" to listOf(Paths.get("/post/test1_post.sql")), - "test2_post.sql" to listOf(Paths.get("/post/test2_post.sql")) + "test2_post.sql" to listOf(Paths.get("/post/test2_post.sql")), ), - listOf(Paths.get("/test2/description.md"), Paths.get("/test1/description.md")) + listOf(Paths.get("/test2/description.md"), Paths.get("/test1/description.md")), ) test1.merge(test2) shouldBeEqualTo expected @@ -65,7 +65,7 @@ class SquitTestTest { Paths.get("/response.xml"), mapOf("test_pre.sql" to listOf(Paths.get("/pre/test_pre.sql"))), mapOf("test_post.sql" to listOf(Paths.get("/post/test_pre.sql"))), - listOf(Paths.get("/description/description.md")) + listOf(Paths.get("/description/description.md")), ) val serialized = ByteArrayOutputStream().let { byteOut -> diff --git a/src/test/kotlin/de/smartsquare/squit/io/FilesUtilsTest.kt b/src/test/kotlin/de/smartsquare/squit/io/FilesUtilsTest.kt index d7d3c626..86cc2ab9 100644 --- a/src/test/kotlin/de/smartsquare/squit/io/FilesUtilsTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/io/FilesUtilsTest.kt @@ -1,10 +1,10 @@ package de.smartsquare.squit.io -import kotlin.io.path.ExperimentalPathApi -import kotlin.io.path.createTempDirectory import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldNotExist import org.junit.jupiter.api.Test +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.createTempDirectory @ExperimentalPathApi class FilesUtilsTest { diff --git a/src/test/kotlin/de/smartsquare/squit/io/JsonParserSupportTest.kt b/src/test/kotlin/de/smartsquare/squit/io/JsonParserSupportTest.kt index a1577570..926eab10 100644 --- a/src/test/kotlin/de/smartsquare/squit/io/JsonParserSupportTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/io/JsonParserSupportTest.kt @@ -1,13 +1,13 @@ package de.smartsquare.squit.io import de.smartsquare.squit.TestUtils -import java.io.IOException import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldThrow import org.amshove.kluent.withCause import org.amshove.kluent.withMessage import org.gradle.api.GradleException import org.junit.jupiter.api.Test +import java.io.IOException class JsonParserSupportTest { diff --git a/src/test/kotlin/de/smartsquare/squit/io/SAXReaderSupportTest.kt b/src/test/kotlin/de/smartsquare/squit/io/SAXReaderSupportTest.kt index 641b5609..68860711 100644 --- a/src/test/kotlin/de/smartsquare/squit/io/SAXReaderSupportTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/io/SAXReaderSupportTest.kt @@ -1,13 +1,13 @@ package de.smartsquare.squit.io import de.smartsquare.squit.TestUtils -import java.io.IOException import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldThrow import org.amshove.kluent.withCause import org.amshove.kluent.withMessage import org.gradle.api.GradleException import org.junit.jupiter.api.Test +import java.io.IOException class SAXReaderSupportTest { diff --git a/src/test/kotlin/de/smartsquare/squit/mediatype/json/JsonCanonicalizerTest.kt b/src/test/kotlin/de/smartsquare/squit/mediatype/json/JsonCanonicalizerTest.kt index 481262ea..c2de6696 100644 --- a/src/test/kotlin/de/smartsquare/squit/mediatype/json/JsonCanonicalizerTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/mediatype/json/JsonCanonicalizerTest.kt @@ -32,8 +32,8 @@ class JsonCanonicalizerTest { MediaTypeConfig( xmlStrict = false, xmlCanonicalize = false, - jsonCanonicalize = true - ) + jsonCanonicalize = true, + ), ) // language=json @@ -86,8 +86,8 @@ class JsonCanonicalizerTest { MediaTypeConfig( xmlStrict = false, xmlCanonicalize = false, - jsonCanonicalize = true - ) + jsonCanonicalize = true, + ), ) // language=json @@ -135,8 +135,8 @@ class JsonCanonicalizerTest { MediaTypeConfig( xmlStrict = false, xmlCanonicalize = false, - jsonCanonicalize = false - ) + jsonCanonicalize = false, + ), ) result shouldBeEqualTo structure diff --git a/src/test/kotlin/de/smartsquare/squit/mediatype/xml/XmlCanonicalizerTest.kt b/src/test/kotlin/de/smartsquare/squit/mediatype/xml/XmlCanonicalizerTest.kt index 290a7ad9..e2bb23ca 100644 --- a/src/test/kotlin/de/smartsquare/squit/mediatype/xml/XmlCanonicalizerTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/mediatype/xml/XmlCanonicalizerTest.kt @@ -22,8 +22,8 @@ class XmlCanonicalizerTest { MediaTypeConfig( xmlStrict = false, xmlCanonicalize = true, - jsonCanonicalize = false - ) + jsonCanonicalize = false, + ), ) // language=xml @@ -54,8 +54,8 @@ class XmlCanonicalizerTest { MediaTypeConfig( xmlStrict = false, xmlCanonicalize = false, - jsonCanonicalize = false - ) + jsonCanonicalize = false, + ), ) result shouldBeEqualTo structure @@ -76,8 +76,8 @@ class XmlCanonicalizerTest { xmlStrict = false, xmlCanonicalize = true, jsonCanonicalize = false, - resolveInvalidNamespaces = true - ) + xmlResolveInvalidNamespaces = true, + ), ) // language=xml @@ -108,8 +108,8 @@ class XmlCanonicalizerTest { xmlStrict = false, xmlCanonicalize = true, jsonCanonicalize = false, - resolveInvalidNamespaces = true - ) + xmlResolveInvalidNamespaces = true, + ), ) // language=xml diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitPostProcessTaskJsonTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitPostProcessTaskJsonTest.kt index 04acd5d8..aea481cd 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitPostProcessTaskJsonTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitPostProcessTaskJsonTest.kt @@ -2,7 +2,6 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.file.Files import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.amshove.kluent.shouldBe @@ -11,6 +10,7 @@ import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.nio.file.Files class SquitPostProcessTaskJsonTest { @@ -38,8 +38,9 @@ class SquitPostProcessTaskJsonTest { server.enqueue(MockResponse().setBody("{\n \"cool\": true\n}")) val arguments = listOf( - "squitPostProcess", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$jsonProject" + "squitPostProcess", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$jsonProject", ) val result = gradleRunner(jsonProject, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitPostProcessTaskPreviousTaskErrorTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitPostProcessTaskPreviousTaskErrorTest.kt index 87ffc901..0f69b165 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitPostProcessTaskPreviousTaskErrorTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitPostProcessTaskPreviousTaskErrorTest.kt @@ -2,12 +2,12 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.charset.Charset -import java.nio.file.Files import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldStartWith import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Test +import java.nio.charset.Charset +import java.nio.file.Files class SquitPostProcessTaskPreviousTaskErrorTest { diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitPostProcessTaskTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitPostProcessTaskTest.kt index 2276303a..4401d4d3 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitPostProcessTaskTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitPostProcessTaskTest.kt @@ -2,7 +2,6 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.file.Files import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.amshove.kluent.shouldBe @@ -13,6 +12,7 @@ import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.nio.file.Files class SquitPostProcessTaskTest { @@ -43,8 +43,10 @@ class SquitPostProcessTaskTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitPostProcess", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-PtagsOr=call1,call2" + "squitPostProcess", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-PtagsOr=call1,call2", ) val result = gradleRunner(project, arguments).build() @@ -63,8 +65,10 @@ class SquitPostProcessTaskTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitPostProcess", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-PtagsOr=call1,call2" + "squitPostProcess", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-PtagsOr=call1,call2", ) val result = gradleRunner(project, arguments).build() @@ -83,8 +87,11 @@ class SquitPostProcessTaskTest { } val arguments = listOf( - "squitPostProcess", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-PtagsOr=call1,call2", "--build-cache" + "squitPostProcess", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-PtagsOr=call1,call2", + "--build-cache", ) val result = gradleRunner(project, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskDifferentEncodingTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskDifferentEncodingTest.kt index e80892d9..6b39b672 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskDifferentEncodingTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskDifferentEncodingTest.kt @@ -2,15 +2,15 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.file.Files -import java.nio.file.StandardOpenOption.CREATE -import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldContain import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.nio.file.Files +import java.nio.file.StandardOpenOption.CREATE +import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING class SquitPreProcessTaskDifferentEncodingTest { @@ -32,8 +32,10 @@ class SquitPreProcessTaskDifferentEncodingTest { @Test fun `normal run`() { val arguments = listOf( - "squitPreProcess", "-Psquit.endpointPlaceholder=https://example.com", - "-Psquit.rootDir=$project", "-PtagsOr=call1,call2,call4" + "squitPreProcess", + "-Psquit.endpointPlaceholder=https://example.com", + "-Psquit.rootDir=$project", + "-PtagsOr=call1,call2,call4", ) val result = gradleRunner(project, arguments).buildAndFail() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskGetTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskGetTest.kt index 1eb09a68..470a0fc1 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskGetTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskGetTest.kt @@ -2,11 +2,11 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.file.Files import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeFalse import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Test +import java.nio.file.Files class SquitPreProcessTaskGetTest { @@ -17,7 +17,9 @@ class SquitPreProcessTaskGetTest { @Test fun `normal run`() { val arguments = listOf( - "squitPreProcess", "-Psquit.endpointPlaceholder=https://example.com", "-Psquit.rootDir=$getProject" + "squitPreProcess", + "-Psquit.endpointPlaceholder=https://example.com", + "-Psquit.rootDir=$getProject", ) val result = gradleRunner(getProject, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskInvalidConfTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskInvalidConfTest.kt index 02382f15..ac1f83e2 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskInvalidConfTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskInvalidConfTest.kt @@ -2,11 +2,11 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.io.File import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldContain import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Test +import java.io.File class SquitPreProcessTaskInvalidConfTest { diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskInvalidResponseTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskInvalidResponseTest.kt index 5af7acc4..672d4c72 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskInvalidResponseTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskInvalidResponseTest.kt @@ -2,12 +2,12 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.charset.Charset -import java.nio.file.Files import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldStartWith import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Test +import java.nio.charset.Charset +import java.nio.file.Files class SquitPreProcessTaskInvalidResponseTest { diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskJsonTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskJsonTest.kt index 41fea65f..4aa07d7c 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskJsonTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskJsonTest.kt @@ -2,11 +2,11 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.file.Files import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeTrue import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Test +import java.nio.file.Files class SquitPreProcessTaskJsonTest { @@ -17,8 +17,9 @@ class SquitPreProcessTaskJsonTest { @Test fun `normal run`() { val arguments = listOf( - "squitPreProcess", "-Psquit.endpointPlaceholder=https://example.com", - "-Psquit.rootDir=$jsonProject" + "squitPreProcess", + "-Psquit.endpointPlaceholder=https://example.com", + "-Psquit.rootDir=$jsonProject", ) val result = gradleRunner(jsonProject, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskOptionsTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskOptionsTest.kt index 2c37e9d2..878df0a6 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskOptionsTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskOptionsTest.kt @@ -2,12 +2,12 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.file.Files import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Test +import java.nio.file.Files class SquitPreProcessTaskOptionsTest { @@ -18,7 +18,9 @@ class SquitPreProcessTaskOptionsTest { @Test fun `normal run`() { val arguments = listOf( - "squitPreProcess", "-Psquit.endpointPlaceholder=https://example.com", "-Psquit.rootDir=$optionsProject" + "squitPreProcess", + "-Psquit.endpointPlaceholder=https://example.com", + "-Psquit.rootDir=$optionsProject", ) val result = gradleRunner(optionsProject, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskPlaceholderTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskPlaceholderTest.kt index 46c753f4..ef4dd8cb 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskPlaceholderTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskPlaceholderTest.kt @@ -13,8 +13,11 @@ class SquitPreProcessTaskPlaceholderTest { @Test fun `normal run`() { val arguments = listOf( - "squitPreProcess", "-Psquit.endpointPlaceholder=https://example.com", "-Ptags=call2", - "-Psquit.rootDir=$projectWithPlaceholders", "-Psquit.placeholder2=test" + "squitPreProcess", + "-Psquit.endpointPlaceholder=https://example.com", + "-Ptags=call2", + "-Psquit.rootDir=$projectWithPlaceholders", + "-Psquit.placeholder2=test", ) val result = gradleRunner(projectWithPlaceholders, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskTest.kt index 15f6c7de..440211a3 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitPreProcessTaskTest.kt @@ -2,7 +2,6 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.file.Files import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeFalse @@ -12,6 +11,7 @@ import org.amshove.kluent.shouldContain import org.amshove.kluent.shouldNotContain import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Test +import java.nio.file.Files class SquitPreProcessTaskTest { @@ -38,8 +38,10 @@ class SquitPreProcessTaskTest { @Test fun `normal run`() { val arguments = listOf( - "squitPreProcess", "-Psquit.endpointPlaceholder=https://example.com", - "-Psquit.rootDir=$project", "-PtagsOr=call1,call2,call4" + "squitPreProcess", + "-Psquit.endpointPlaceholder=https://example.com", + "-Psquit.rootDir=$project", + "-PtagsOr=call1,call2,call4", ) val result = gradleRunner(project, arguments).build() @@ -85,8 +87,10 @@ class SquitPreProcessTaskTest { @Test fun `running with tags`() { val arguments = listOf( - "squitPreProcess", "-Psquit.endpointPlaceholder=https://example.com", - "-Psquit.rootDir=$project", "-PtagsAnd=project,unique" + "squitPreProcess", + "-Psquit.endpointPlaceholder=https://example.com", + "-Psquit.rootDir=$project", + "-PtagsAnd=project,unique", ) val result = gradleRunner(project, arguments).build() @@ -102,8 +106,10 @@ class SquitPreProcessTaskTest { @Test fun `running with the unignore flag`() { val arguments = listOf( - "squitPreProcess", "-Psquit.endpointPlaceholder=https://example.com", - "-Psquit.rootDir=$project", "-Psquit.titlePlaceholder=newTitle" + "squitPreProcess", + "-Psquit.endpointPlaceholder=https://example.com", + "-Psquit.rootDir=$project", + "-Psquit.titlePlaceholder=newTitle", ) val result = gradleRunner(project, arguments).build() @@ -118,8 +124,10 @@ class SquitPreProcessTaskTest { @Test fun `running with overriding config`() { val arguments = listOf( - "squitPreProcess", "-Psquit.endpointPlaceholder=https://example.com", - "-Psquit.rootDir=$project", "-Punignore" + "squitPreProcess", + "-Psquit.endpointPlaceholder=https://example.com", + "-Psquit.rootDir=$project", + "-Punignore", ) val result = gradleRunner(project, arguments).build() @@ -130,8 +138,11 @@ class SquitPreProcessTaskTest { @Test fun `running with build cache twice`() { val arguments = listOf( - "squitPreProcess", "-Psquit.endpointPlaceholder=https://example.com", - "-Psquit.rootDir=$project", "-Punignore", "--build-cache" + "squitPreProcess", + "-Psquit.endpointPlaceholder=https://example.com", + "-Psquit.rootDir=$project", + "-Punignore", + "--build-cache", ) val result = gradleRunner(project, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskConfigurableTasksTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskConfigurableTasksTest.kt index 5b2625c6..67af17f4 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskConfigurableTasksTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskConfigurableTasksTest.kt @@ -18,8 +18,10 @@ import org.junit.jupiter.api.Test import java.sql.DriverManager import java.time.Instant import java.util.concurrent.TimeUnit +import org.h2.Driver as H2Driver class SquitRequestTaskConfigurableTasksTest { + private val project = TestUtils.getResourcePath("test-project-task-config") private val jdbc = "jdbc:h2:$project/testDb;IFEXISTS=TRUE" @@ -32,6 +34,8 @@ class SquitRequestTaskConfigurableTasksTest { @BeforeEach fun setUp() { + H2Driver.load() + server = MockWebServer() } @@ -40,6 +44,7 @@ class SquitRequestTaskConfigurableTasksTest { server.shutdown() TestUtils.deleteDatabaseFiles(project) + preRunFile.delete() postRunFile.delete() } @@ -49,19 +54,23 @@ class SquitRequestTaskConfigurableTasksTest { server.enqueue( MockResponse() .setHeadersDelay(10L, TimeUnit.MILLISECONDS) - .setBody("") + .setBody(""), ) val arguments = listOf( - "squitRunRequests", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-Ptags=default" + "squitRunRequests", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-Ptags=default", ) val result = gradleRunner(project, arguments).build() result.task(":squitRunRequests")?.outcome shouldBe TaskOutcome.SUCCESS + val preScriptExecution = Instant.ofEpochMilli(preRunFile.readText().toLong()) val postScriptExecution = Instant.ofEpochMilli(postRunFile.readText().toLong()) + DriverManager.getConnection(jdbc, username, password).use { connection -> val resultSet = connection.createStatement().executeQuery("SELECT * FROM TIMESTAMPS") resultSet.next() @@ -82,12 +91,14 @@ class SquitRequestTaskConfigurableTasksTest { server.enqueue( MockResponse() .setHeadersDelay(10L, TimeUnit.MILLISECONDS) - .setBody("") + .setBody(""), ) val arguments = listOf( - "squitRunRequests", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-Ptags=configured_order" + "squitRunRequests", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-Ptags=configured_order", ) val result = gradleRunner(project, arguments).build() @@ -96,6 +107,7 @@ class SquitRequestTaskConfigurableTasksTest { val preScriptExecution = Instant.ofEpochMilli(preRunFile.readText().toLong()) val postScriptExecution = Instant.ofEpochMilli(postRunFile.readText().toLong()) + DriverManager.getConnection(jdbc, username, password).use { connection -> val resultSet = connection.createStatement().executeQuery("SELECT * FROM TIMESTAMPS") resultSet.next() @@ -116,12 +128,14 @@ class SquitRequestTaskConfigurableTasksTest { server.enqueue( MockResponse() .setHeadersDelay(10L, TimeUnit.MILLISECONDS) - .setBody("") + .setBody(""), ) val arguments = listOf( - "squitRunRequests", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-Ptags=only_pre_db_script" + "squitRunRequests", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-Ptags=only_pre_db_script", ) val result = gradleRunner(project, arguments).build() @@ -134,6 +148,7 @@ class SquitRequestTaskConfigurableTasksTest { resultSet.getString(3) shouldBeEqualTo "TEST_PRE.SQL" resultSet.next().shouldBeFalse() } + preRunFile.shouldNotExist() postRunFile.shouldExist() } diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskGetTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskGetTest.kt index fe8cf797..30a4cfa5 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskGetTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskGetTest.kt @@ -34,8 +34,9 @@ class SquitRequestTaskGetTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitRunRequests", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$getProject" + "squitRunRequests", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$getProject", ) val result = gradleRunner(getProject, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskInvalidSqlTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskInvalidSqlTest.kt index f65fe322..a99f8397 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskInvalidSqlTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskInvalidSqlTest.kt @@ -2,7 +2,6 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.io.File import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.amshove.kluent.shouldBe @@ -11,6 +10,7 @@ import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.io.File class SquitRequestTaskInvalidSqlTest { @@ -35,8 +35,9 @@ class SquitRequestTaskInvalidSqlTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitRunRequests", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$invalidProject2" + "squitRunRequests", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$invalidProject2", ) val result = gradleRunner(invalidProject2, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskJsonTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskJsonTest.kt index 36ed48b1..c9ecf0cb 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskJsonTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskJsonTest.kt @@ -3,7 +3,6 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.entity.SquitResponseInfo import de.smartsquare.squit.gradleRunner -import java.nio.file.Files import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.amshove.kluent.shouldBe @@ -13,6 +12,7 @@ import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.nio.file.Files class SquitRequestTaskJsonTest { @@ -41,8 +41,9 @@ class SquitRequestTaskJsonTest { server.enqueue(MockResponse().setBody("{\n \"cool\": true\n}")) val arguments = listOf( - "squitRunRequests", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$jsonProject" + "squitRunRequests", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$jsonProject", ) val result = gradleRunner(jsonProject, arguments).build() @@ -56,7 +57,7 @@ class SquitRequestTaskJsonTest { } val (expectedResponseCode) = SquitResponseInfo.fromJson( - Files.readAllBytes(jsonCall1ActualResponseInfo).toString(Charsets.UTF_8) + Files.readAllBytes(jsonCall1ActualResponseInfo).toString(Charsets.UTF_8), ) expectedResponseCode shouldBeInRange 200..599 diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskOptionsTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskOptionsTest.kt index 0ff09787..e345dd75 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskOptionsTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskOptionsTest.kt @@ -35,8 +35,9 @@ class SquitRequestTaskOptionsTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitRunRequests", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$optionsProject" + "squitRunRequests", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$optionsProject", ) val result = gradleRunner(optionsProject, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskPreviousTaskErrorTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskPreviousTaskErrorTest.kt index 6a83b9b1..739f99a0 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskPreviousTaskErrorTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskPreviousTaskErrorTest.kt @@ -2,12 +2,12 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.charset.Charset -import java.nio.file.Files import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldStartWith import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Test +import java.nio.charset.Charset +import java.nio.file.Files class SquitRequestTaskPreviousTaskErrorTest { diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskTest.kt index ac277394..e84842e2 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitRequestTaskTest.kt @@ -3,11 +3,6 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.entity.SquitMetaInfo import de.smartsquare.squit.gradleRunner -import java.io.File -import java.nio.charset.Charset -import java.nio.file.Files -import java.sql.DriverManager -import java.time.LocalDateTime import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.amshove.kluent.shouldBe @@ -23,6 +18,12 @@ import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.io.File +import java.nio.charset.Charset +import java.nio.file.Files +import java.sql.DriverManager +import java.time.LocalDateTime +import org.h2.Driver as H2Driver class SquitRequestTaskTest { @@ -59,8 +60,10 @@ class SquitRequestTaskTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitRunRequests", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-PtagsOr=call1,call2" + "squitRunRequests", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-PtagsOr=call1,call2", ) val result = gradleRunner(project, arguments).build() @@ -73,7 +76,7 @@ class SquitRequestTaskTest { val (date, duration) = SquitMetaInfo.fromJson( Files.readAllBytes(call1Meta) - .toString(Charset.defaultCharset()) + .toString(Charset.defaultCharset()), ) date shouldBeBefore LocalDateTime.now() @@ -92,6 +95,7 @@ class SquitRequestTaskTest { it.headers["some"] shouldBeEqualTo "header" } + H2Driver.load() DriverManager.getConnection(jdbc, username, password).use { connection -> val resultSet = connection.createStatement().executeQuery("SELECT * FROM animals") @@ -113,8 +117,11 @@ class SquitRequestTaskTest { server.enqueue(MockResponse().setBody("error").setResponseCode(500)) val arguments = listOf( - "squitRunRequests", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-PtagsOr=call1", "--info" + "squitRunRequests", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-PtagsOr=call1", + "--info", ) val result = gradleRunner(project, arguments).build() @@ -130,8 +137,10 @@ class SquitRequestTaskTest { // Nothing enqueued to cause timeout. val arguments = listOf( - "squitRunRequests", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-Ptags=call1" + "squitRunRequests", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-Ptags=call1", ) val result = gradleRunner(project, arguments).build() @@ -147,8 +156,11 @@ class SquitRequestTaskTest { server.enqueue(MockResponse().setBody("").setHeader("Content-Type", "text/plain")) val arguments = listOf( - "squitRunRequests", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-Ptags=call1", "--info" + "squitRunRequests", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-Ptags=call1", + "--info", ) val result = gradleRunner(project, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskDifferentStructureTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskDifferentStructureTest.kt index 141ef36a..8d4e559f 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskDifferentStructureTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskDifferentStructureTest.kt @@ -2,7 +2,6 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.file.Files import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.amshove.kluent.shouldBe @@ -11,6 +10,7 @@ import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.nio.file.Files class SquitTestTaskDifferentStructureTest { @@ -37,8 +37,9 @@ class SquitTestTaskDifferentStructureTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitTest", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$differentStructureProject" + "squitTest", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$differentStructureProject", ) val result = gradleRunner(differentStructureProject, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskErrorResponseCodeTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskErrorResponseCodeTest.kt index 465df73e..2672adef 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskErrorResponseCodeTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskErrorResponseCodeTest.kt @@ -33,8 +33,9 @@ class SquitTestTaskErrorResponseCodeTest { server.enqueue(MockResponse().setBody("{\n \"cool\": true\n}").setResponseCode(400)) val arguments = listOf( - "squitTest", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$projectWithResponseCode" + "squitTest", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$projectWithResponseCode", ) val result = gradleRunner(projectWithResponseCode, arguments).build() @@ -47,8 +48,9 @@ class SquitTestTaskErrorResponseCodeTest { server.enqueue(MockResponse().setBody("{\n \"cool\": true\n}").setResponseCode(200)) val arguments = listOf( - "squitTest", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$projectWithResponseCode" + "squitTest", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$projectWithResponseCode", ) val result = gradleRunner(projectWithResponseCode, arguments).buildAndFail() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskIgnoreFailuresTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskIgnoreFailuresTest.kt index e0dfdab1..bae6bdb3 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskIgnoreFailuresTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskIgnoreFailuresTest.kt @@ -33,8 +33,9 @@ class SquitTestTaskIgnoreFailuresTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitTest", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$projectIgnoreFailures" + "squitTest", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$projectIgnoreFailures", ) val result = gradleRunner(projectIgnoreFailures, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskJsonDifferentOrderTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskJsonDifferentOrderTest.kt index ba81eb84..401d4015 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskJsonDifferentOrderTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskJsonDifferentOrderTest.kt @@ -40,13 +40,14 @@ class SquitTestTaskJsonDifferentOrderTest { "olleh": 321, "hello": "123" } - """.trimIndent() - ) + """.trimIndent(), + ), ) val arguments = listOf( - "squitTest", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$jsonProjectWithDifferentOrder" + "squitTest", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$jsonProjectWithDifferentOrder", ) val result = gradleRunner(jsonProjectWithDifferentOrder, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskJsonTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskJsonTest.kt index f6904acd..fbccffb6 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskJsonTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskJsonTest.kt @@ -33,8 +33,9 @@ class SquitTestTaskJsonTest { server.enqueue(MockResponse().setBody("{\n \"cool\": true\n}")) val arguments = listOf( - "squitTest", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$jsonProject" + "squitTest", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$jsonProject", ) val result = gradleRunner(jsonProject, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskNonStrictXmlDiffingTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskNonStrictXmlDiffingTest.kt index 4edfabab..4f74f880 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskNonStrictXmlDiffingTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskNonStrictXmlDiffingTest.kt @@ -33,8 +33,9 @@ class SquitTestTaskNonStrictXmlDiffingTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitTest", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$projectWithNonStrictXml" + "squitTest", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$projectWithNonStrictXml", ) val result = gradleRunner(projectWithNonStrictXml, arguments).build() diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskPreviousTaskErrorTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskPreviousTaskErrorTest.kt index 4cb3a8fc..f23458ab 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskPreviousTaskErrorTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskPreviousTaskErrorTest.kt @@ -2,12 +2,12 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.charset.Charset -import java.nio.file.Files import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldStartWith import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Test +import java.nio.charset.Charset +import java.nio.file.Files class SquitTestTaskPreviousTaskErrorTest { diff --git a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskTest.kt b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskTest.kt index d672b735..16ecedae 100644 --- a/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/task/SquitTestTaskTest.kt @@ -2,8 +2,6 @@ package de.smartsquare.squit.task import de.smartsquare.squit.TestUtils import de.smartsquare.squit.gradleRunner -import java.nio.file.Files -import kotlin.streams.toList import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.amshove.kluent.shouldBe @@ -17,6 +15,8 @@ import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.nio.file.Files +import kotlin.streams.toList class SquitTestTaskTest { @@ -54,8 +54,9 @@ class SquitTestTaskTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitTest", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project" + "squitTest", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", ) val result = gradleRunner(project, arguments).build() @@ -83,8 +84,10 @@ class SquitTestTaskTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitTest", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-PtagsOr=call1,call2" + "squitTest", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-PtagsOr=call1,call2", ) val result = gradleRunner(project, arguments).buildAndFail() @@ -109,8 +112,10 @@ class SquitTestTaskTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitTest", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-Punignore" + "squitTest", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-Punignore", ) val result = gradleRunner(project, arguments).buildAndFail() @@ -129,8 +134,10 @@ class SquitTestTaskTest { server.enqueue(MockResponse().setBody("")) val arguments = listOf( - "squitTest", "-Psquit.endpointPlaceholder=${server.url("/")}", - "-Psquit.rootDir=$project", "-Punexclude" + "squitTest", + "-Psquit.endpointPlaceholder=${server.url("/")}", + "-Psquit.rootDir=$project", + "-Punexclude", ) val result = gradleRunner(project, arguments).buildAndFail() diff --git a/src/test/kotlin/de/smartsquare/squit/util/UtilExtensionsTest.kt b/src/test/kotlin/de/smartsquare/squit/util/UtilExtensionsTest.kt index 3ab78ed2..5120b08e 100644 --- a/src/test/kotlin/de/smartsquare/squit/util/UtilExtensionsTest.kt +++ b/src/test/kotlin/de/smartsquare/squit/util/UtilExtensionsTest.kt @@ -1,8 +1,8 @@ package de.smartsquare.squit.util -import java.nio.file.Paths import org.amshove.kluent.shouldBeEqualTo import org.junit.jupiter.api.Test +import java.nio.file.Paths class UtilExtensionsTest {