From 6e0e843f77f5c81524c6eca628343c10b5551215 Mon Sep 17 00:00:00 2001 From: Tony Robalik Date: Sun, 14 Jan 2024 11:56:29 -0800 Subject: [PATCH 1/7] feat(testkit-support): new APIs for artifact access from test fixtures. --- .../com/autonomousapps/AdviceStrategy.groovy | 17 +--- .../com/autonomousapps/kit/GradleProject.kt | 78 +++++++++++++++++-- .../com/autonomousapps/kit/Subproject.kt | 8 +- .../autonomousapps/kit/gradle/Dependency.kt | 3 +- .../kit/internal/{Files.kt => files.kt} | 2 +- .../autonomousapps/kit/internal/strings.kt | 7 ++ .../com/autonomousapps/kit/utils/files.kt | 51 ++++++++---- 7 files changed, 124 insertions(+), 42 deletions(-) rename testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/internal/{Files.kt => files.kt} (94%) create mode 100644 testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/internal/strings.kt diff --git a/src/functionalTest/groovy/com/autonomousapps/AdviceStrategy.groovy b/src/functionalTest/groovy/com/autonomousapps/AdviceStrategy.groovy index 9b24d31fb..9f245f0c8 100644 --- a/src/functionalTest/groovy/com/autonomousapps/AdviceStrategy.groovy +++ b/src/functionalTest/groovy/com/autonomousapps/AdviceStrategy.groovy @@ -2,18 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 package com.autonomousapps - import com.autonomousapps.internal.OutputPathsKt import com.autonomousapps.internal.utils.MoshiUtils import com.autonomousapps.kit.GradleProject -import com.autonomousapps.kit.utils.Files -import com.autonomousapps.model.Advice import com.autonomousapps.model.BuildHealth import com.autonomousapps.model.ProjectAdvice import com.squareup.moshi.Types abstract class AdviceStrategy { - abstract List actualAdviceForFirstSubproject(GradleProject gradleProject) abstract def actualBuildHealth(GradleProject gradleProject) @@ -41,7 +37,7 @@ abstract class AdviceStrategy { @Override Map> getDuplicateDependenciesReport(GradleProject gradleProject) { - def json = Files.resolveFromRoot(gradleProject, OutputPathsKt.getDuplicateDependenciesReport()).text.trim() + def json = gradleProject.singleArtifact(':', OutputPathsKt.getDuplicateDependenciesReport()).text.trim() def set = Types.newParameterizedType(Set, String) def map = Types.newParameterizedType(Map, String, set) def adapter = MoshiUtils.MOSHI.>> adapter(map) @@ -50,25 +46,20 @@ abstract class AdviceStrategy { @Override List getResolvedDependenciesReport(GradleProject gradleProject, String projectPath) { - File report = Files.resolveFromName(gradleProject, projectPath, OutputPathsKt.getResolvedDependenciesReport()) + def report = gradleProject.singleArtifact(projectPath, OutputPathsKt.getResolvedDependenciesReport()) return report.text.trim().readLines() } @Override def actualBuildHealth(GradleProject gradleProject) { - File buildHealth = Files.resolveFromRoot(gradleProject, OutputPathsKt.getFinalAdvicePathV2()) + def buildHealth = gradleProject.singleArtifact(':', OutputPathsKt.getFinalAdvicePathV2()) return fromAllProjectAdviceJson(buildHealth.text) } @Override def actualComprehensiveAdviceForProject(GradleProject gradleProject, String projectName) { - File advice = Files.resolveFromName(gradleProject, projectName, OutputPathsKt.getAggregateAdvicePathV2()) + def advice = gradleProject.singleArtifact(projectName, OutputPathsKt.getAggregateAdvicePathV2()) return fromProjectAdvice(advice.text) } - - @Override - List actualAdviceForFirstSubproject(GradleProject gradleProject) { - throw new IllegalStateException("Not yet implemented") - } } } diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/GradleProject.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/GradleProject.kt index ea5ce93da..8ecbc4d8a 100644 --- a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/GradleProject.kt +++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/GradleProject.kt @@ -10,8 +10,13 @@ import com.autonomousapps.kit.android.AndroidSubproject import com.autonomousapps.kit.gradle.BuildScript import com.autonomousapps.kit.gradle.GradleProperties import com.autonomousapps.kit.gradle.SettingsScript +import com.autonomousapps.kit.internal.ensurePrefix +import com.autonomousapps.kit.utils.buildPathForName +import com.google.common.truth.Truth import java.io.File +import java.nio.file.Files import java.nio.file.Path +import kotlin.io.path.exists /** * A Gradle project consists of: @@ -72,14 +77,18 @@ public class GradleProject( return rootDir.toPath().resolve("${project.includedBuild?.let { "$it/" } ?: ""}${project.name.replace(":", "/")}/") } - /** Use ":" for the root project. */ - public fun buildDir(projectName: String): Path { - return buildDir(forName(projectName)) + /** + * Provides access to a build directory for one of the projects in your fixture. Use ":" for the root project. + */ + @JvmOverloads + public fun buildDir(projectName: String, buildDirName: String = "build"): Path { + return buildDir(project = forName(projectName), buildDirName = buildDirName) } /** Use [rootProject] for the root project. */ - public fun buildDir(project: Subproject): Path { - return projectDir(project).resolve("build/") + @JvmOverloads + public fun buildDir(project: Subproject, buildDirName: String = "build"): Path { + return projectDir(project).resolve("${buildDirName}/") } public fun findIncludedBuild(path: String): GradleProject? { @@ -103,13 +112,68 @@ public class GradleProject( } } + /** + * Returns the single artifact at [relativePath] from the build directory of project [projectName], failing if no such + * artifact exists. Uses "build" as the build directory name by default. + */ + @JvmOverloads + public fun singleArtifact( + projectName: String, + relativePath: String, + buildDirName: String = "build", + ): Path { + val artifact = buildPathForName(path = projectName, buildDirName = buildDirName).resolve(relativePath) + Truth.assertWithMessage("No artifact with path '$artifact'").that(artifact.exists()).isTrue() + return artifact + } + + /** + * Returns the single artifact at [relativePath] from the build directory of project [projectName], failing if no such + * artifact exists. An alias for [singleArtifact]. Uses "build" as the build directory name by default. + */ + @JvmOverloads + public fun getArtifact(projectName: String, relativePath: String, buildDirName: String = "build"): Path { + return singleArtifact(projectName = projectName, relativePath = relativePath, buildDirName = buildDirName) + } + + /** + * Returns the single artifact at [relativePath] from the build directory of project [projectName], or `null` if no such + * artifact exists. Uses "build" as the build directory name by default. + */ + @JvmOverloads + public fun findArtifact( + projectName: String, + relativePath: String, + buildDirName: String = "build", + ): Path? { + val artifact = buildPathForName(path = projectName, buildDirName = buildDirName).resolve(relativePath) + return if (artifact.exists()) { + artifact + } else { + null + } + } + + /** + * Returns the directory at [relativePath] from the build directory of project [projectName], failing if no such + * directory exists, or if it is not a directory. Returned [Path] may be empty. Uses "build" as the build directory name + * by default. + */ + @JvmOverloads + public fun artifacts(projectName: String, relativePath: String, buildDirName: String = "build"): Path { + val dir = buildPathForName(path = projectName, buildDirName = buildDirName).resolve(relativePath) + Truth.assertWithMessage("No directory with path '$dir'").that(dir.exists()).isTrue() + Truth.assertWithMessage("Expected directory, was '$dir'").that(Files.isDirectory(dir)).isTrue() + return dir + } + private fun forName(projectName: String): Subproject { if (projectName == ":") { return rootProject } - return subprojects.find { it.name == projectName } - ?: throw IllegalStateException("No subproject with name $projectName") + return subprojects.find { it.name == projectName.ensurePrefix() } + ?: throw IllegalStateException("No subproject with name '$projectName'") } public class Builder @JvmOverloads constructor( diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/Subproject.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/Subproject.kt index eae004162..50c606d96 100644 --- a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/Subproject.kt +++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/Subproject.kt @@ -23,9 +23,7 @@ public open class Subproject( public val variant: String, ) { - /** - * We only care about the subproject's name for equality comparisons and hashing. - */ + /** We only care about the subproject's name for equality comparisons and hashing. */ override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Subproject) return false @@ -34,9 +32,7 @@ public open class Subproject( return true } - /** - * We only care about the subproject's name for equality comparisons and hashing. - */ + /** We only care about the subproject's name for equality comparisons and hashing. */ override fun hashCode(): Int = name.hashCode() public class Builder { diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/gradle/Dependency.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/gradle/Dependency.kt index c83843a65..eb7a7815b 100644 --- a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/gradle/Dependency.kt +++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/gradle/Dependency.kt @@ -3,6 +3,7 @@ package com.autonomousapps.kit.gradle import com.autonomousapps.kit.GradleProject.DslKind +import com.autonomousapps.kit.internal.ensurePrefix import com.autonomousapps.kit.render.Element import com.autonomousapps.kit.render.Scribe @@ -182,8 +183,6 @@ public class Dependency @JvmOverloads constructor( return Dependency(configuration, path.ensurePrefix(), capability = capability) } - private fun String.ensurePrefix(prefix: String = ":"): String = if (startsWith(prefix)) this else "$prefix$this" - @JvmStatic public fun raw(configuration: String, dependency: String): Dependency { check(!dependency.contains(":")) { "Not meant for normal dependencies. Was '$dependency'." } diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/internal/Files.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/internal/files.kt similarity index 94% rename from testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/internal/Files.kt rename to testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/internal/files.kt index 3c5c3b08d..da1586e46 100644 --- a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/internal/Files.kt +++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/internal/files.kt @@ -5,6 +5,6 @@ package com.autonomousapps.kit.internal import java.io.File import java.nio.charset.Charset -internal fun File.writeAny(any: Any, charset: Charset = Charsets.UTF_8): Unit { +internal fun File.writeAny(any: Any, charset: Charset = Charsets.UTF_8) { writeBytes(any.toString().toByteArray(charset)) } diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/internal/strings.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/internal/strings.kt new file mode 100644 index 000000000..b8b71c03e --- /dev/null +++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/internal/strings.kt @@ -0,0 +1,7 @@ +// Copyright (c) 2024. Tony Robalik. +// SPDX-License-Identifier: Apache-2.0 +package com.autonomousapps.kit.internal + +internal fun String.ensurePrefix(prefix: String = ":"): String { + return if (startsWith(prefix)) this else "$prefix$this" +} diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/utils/files.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/utils/files.kt index 904b9d669..55107a3a6 100644 --- a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/utils/files.kt +++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/utils/files.kt @@ -5,19 +5,38 @@ package com.autonomousapps.kit.utils import com.autonomousapps.kit.GradleProject +import com.autonomousapps.kit.internal.ensurePrefix import com.google.common.truth.Truth.assertWithMessage import java.io.File import java.nio.file.Path -/** Returns the path to the build dir of the root project. */ -public fun GradleProject.rootBuildPath(): Path = buildDir(":") +/** + * Returns the path to the build directory of the root project. Uses "build" as the build directory name by default. + */ +@JvmOverloads +public fun GradleProject.rootBuildPath(buildDirName: String = "build"): Path = buildDir( + projectName = ":", + buildDirName = buildDirName, +) -/** Returns the directory of the build dir of the root project. */ -public fun GradleProject.rootBuildDir(): File = rootBuildPath().toFile() +/** + * Returns the directory of the build directory of the root project. Uses "build" as the build directory name by + * default. + */ +@JvmOverloads +public fun GradleProject.rootBuildDir(buildDirName: String = "build"): File = rootBuildPath(buildDirName).toFile() -/** Returns the file specified, relative to the root project. */ -public fun GradleProject.resolveFromRoot(relativePath: String): File { - return rootBuildPath().resolve(relativePath).toFile() +/** + * Returns the file specified, relative to the build directory of the root project. Uses "build" as the build directory + * name by default. + */ +@Deprecated( + "Use singleArtifact", + replaceWith = ReplaceWith("singleArtifact(\":\", relativePath)") +) +@JvmOverloads +public fun GradleProject.resolveFromRoot(relativePath: String, buildDirName: String = "build"): File { + return rootBuildPath(buildDirName).resolve(relativePath).toFile() } /** Returns the path to the build dir of the first subproject, asserting that there is only one. */ @@ -37,17 +56,19 @@ public fun GradleProject.resolveFromSingleSubproject(relativePath: String): File } /** - * Returns the path to the subproject of the given name in the build, asserting that there is only - * one. + * Returns the path to the subproject of the given name in the build, asserting that there is only one. Uses "build" as + * the build directory name by default. */ -public fun GradleProject.buildPathForName(path: String): Path { +public fun GradleProject.buildPathForName(path: String, buildDirName: String = "build"): Path { val project = if (path == ":") { rootProject } else { - subprojects.find { it.name == path } + subprojects + // normalize name/path string so users can pass in `:project` or `project` and it Just Works. + .single { it.name.ensurePrefix() == path.ensurePrefix() } } - assertWithMessage("No project with path $path").that(project as Any?).isNotNull() - return buildDir(project!!) + + return buildDir(project = project, buildDirName = buildDirName) } /** @@ -58,6 +79,10 @@ public fun GradleProject.buildFileForName(path: String): File = buildPathForName /** * Returns the file specified, relative to the subproject specified by [projectName]. */ +@Deprecated( + "Use singleArtifact", + replaceWith = ReplaceWith("singleArtifact(projectName, relativePath)") +) public fun GradleProject.resolveFromName(projectName: String, relativePath: String): File { return buildPathForName(projectName).resolve(relativePath).toFile() } From 0c4a168010185b8bb89ff0c4d6f002e4f327d53f Mon Sep 17 00:00:00 2001 From: Tony Robalik Date: Sun, 14 Jan 2024 22:41:12 -0800 Subject: [PATCH 2/7] feat(testkit-truth): new Subjects for accessing build artifacts. --- .../autonomousapps/kit/truth/TestKitTruth.kt | 18 +-- .../truth/artifact/BuildArtifactsSubject.kt | 129 ++++++++++++++++++ .../kit/truth/artifact/JarSubject.kt | 60 ++++++++ .../kit/truth/artifact/PathSubject.kt | 67 +++++++++ 4 files changed, 265 insertions(+), 9 deletions(-) create mode 100644 testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt create mode 100644 testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/JarSubject.kt create mode 100644 testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/TestKitTruth.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/TestKitTruth.kt index a990ffafc..485109f19 100644 --- a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/TestKitTruth.kt +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/TestKitTruth.kt @@ -5,25 +5,25 @@ package com.autonomousapps.kit.truth import com.autonomousapps.kit.truth.BuildResultSubject.Companion.buildResults import com.autonomousapps.kit.truth.BuildTaskListSubject.Companion.buildTaskList import com.autonomousapps.kit.truth.BuildTaskSubject.Companion.buildTasks +import com.autonomousapps.kit.truth.artifact.BuildArtifactsSubject +import com.autonomousapps.kit.truth.artifact.BuildArtifactsSubject.Companion.buildArtifacts import com.google.common.truth.Truth.assertAbout import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.BuildTask +import java.nio.file.Path public class TestKitTruth { public companion object { @JvmStatic - public fun assertThat(target: BuildResult): BuildResultSubject { - return assertAbout(buildResults()).that(target) - } + public fun assertThat(target: BuildResult): BuildResultSubject = assertAbout(buildResults()).that(target) @JvmStatic - public fun assertThat(target: BuildTask): BuildTaskSubject { - return assertAbout(buildTasks()).that(target) - } + public fun assertThat(target: BuildTask): BuildTaskSubject = assertAbout(buildTasks()).that(target) @JvmStatic - public fun assertThat(target: List): BuildTaskListSubject { - return assertAbout(buildTaskList()).that(target) - } + public fun assertThat(target: List): BuildTaskListSubject = assertAbout(buildTaskList()).that(target) + + @JvmStatic + public fun assertThat(target: Path): BuildArtifactsSubject = assertAbout(buildArtifacts()).that(target) } } diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt new file mode 100644 index 000000000..a19696d79 --- /dev/null +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt @@ -0,0 +1,129 @@ +// Copyright (c) 2024. Tony Robalik. +// SPDX-License-Identifier: Apache-2.0 +package com.autonomousapps.kit.truth.artifact + +import com.google.common.truth.Fact +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject +import com.google.common.truth.Subject.Factory +import com.google.common.truth.Truth +import java.nio.file.LinkOption +import java.nio.file.Path +import kotlin.io.path.* + +// TODO(tsr): what about a new BuildArtifact API? It would wrap Path/File. +public class BuildArtifactsSubject private constructor( + failureMetadata: FailureMetadata, + private val actual: Path?, +) : Subject(failureMetadata, actual) { + + public companion object { + private val BUILD_ARTIFACT_SUBJECT_FACTORY: Factory = + Factory { metadata, actual -> BuildArtifactsSubject(metadata, actual) } + + @JvmStatic + public fun buildArtifacts(): Factory = BUILD_ARTIFACT_SUBJECT_FACTORY + + @JvmStatic + public fun assertThat(actual: Path?): BuildArtifactsSubject { + return Truth.assertAbout(buildArtifacts()).that(actual) + } + } + + private enum class FileType(val humanReadableName: String) { + REGULAR_FILE("regular file"), + DIRECTORY("directory"), + SYMLINK("symbolic link"), + UNKNOWN("unknown"); + + companion object { + fun from(path: Path, vararg options: LinkOption): FileType = when { + path.isRegularFile(*options) -> REGULAR_FILE + path.isDirectory(*options) -> DIRECTORY + path.isSymbolicLink() -> SYMLINK + else -> UNKNOWN + } + } + } + + public fun exists() { + if (actual == null) { + failWithActual(Fact.simpleFact("build artifact was null")) + } + + check(actual!!.exists()) + } + + public fun notExists() { + if (actual == null) { + failWithActual(Fact.simpleFact("build artifact was null")) + } + + check(actual!!.notExists()) + } + + public fun isRegularFile() { + if (actual == null) { + failWithActual(Fact.simpleFact("build artifact was null")) + } + if (actual!!.notExists()) { + failWithActual(Fact.simpleFact("build artifact does not exist")) + } + check(actual.isRegularFile()) { "Expected regular file. Was ${FileType.from(actual).humanReadableName}" } + } + + public fun isDirectory() { + if (actual == null) { + failWithActual(Fact.simpleFact("build artifact was null")) + } + if (actual!!.notExists()) { + failWithActual(Fact.simpleFact("build artifact does not exist")) + } + check(actual.isDirectory()) { "Expected directory. Was ${FileType.from(actual).humanReadableName}" } + } + + public fun isSymbolicLink() { + if (actual == null) { + failWithActual(Fact.simpleFact("build artifact was null")) + } + if (actual!!.notExists()) { + failWithActual(Fact.simpleFact("build artifact does not exist")) + } + check(actual.isSymbolicLink()) { "Expected symbolic link. Was ${FileType.from(actual).humanReadableName}" } + } + + public fun isJar() { + isType("jar") + } + + public fun isType(extension: String) { + isRegularFile() + + if (actual == null) { + failWithActual(Fact.simpleFact("build artifact was null")) + } + if (actual!!.notExists()) { + failWithActual(Fact.simpleFact("build artifact does not exist")) + } + + check(actual.extension == extension) { "Expected extension to be '$extension'. Was '${actual.extension}'" } + } + + public fun jar(): JarSubject { + isJar() + return JarSubject.assertThat(actual) + } + + public fun file(): PathSubject { + isRegularFile() + + if (actual == null) { + failWithActual(Fact.simpleFact("build artifact was null")) + } + if (actual!!.notExists()) { + failWithActual(Fact.simpleFact("build artifact does not exist")) + } + + return PathSubject.assertThat(actual) + } +} diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/JarSubject.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/JarSubject.kt new file mode 100644 index 000000000..faa1d11c1 --- /dev/null +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/JarSubject.kt @@ -0,0 +1,60 @@ +// Copyright (c) 2024. Tony Robalik. +// SPDX-License-Identifier: Apache-2.0 +package com.autonomousapps.kit.truth.artifact + +import com.google.common.truth.Fact +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject +import com.google.common.truth.Subject.Factory +import com.google.common.truth.Truth +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import kotlin.io.path.notExists + +public class JarSubject private constructor( + failureMetadata: FailureMetadata, + private val actual: Path?, +) : Subject(failureMetadata, actual) { + + public companion object { + private val JAR_SUBJECT_FACTORY: Factory = + Factory { metadata, actual -> JarSubject(metadata, actual) } + + @JvmStatic + public fun jars(): Factory = JAR_SUBJECT_FACTORY + + @JvmStatic + public fun assertThat(actual: Path?): JarSubject { + return Truth.assertAbout(jars()).that(actual) + } + } + + public fun containsResource(path: String) { + if (actual == null) { + failWithActual(Fact.simpleFact("jar was null")) + } + + resource(path).exists() + } + + public fun resource(path: String): PathSubject { + if (actual == null) { + failWithActual(Fact.simpleFact("jar was null")) + } + + // Open zip, copy entry to temp dir, close zip. + val tempResource = FileSystems.newFileSystem(actual!!, null).use { fs -> + val resource = fs.getPath(path) + if (resource.notExists()) { + failWithActual(Fact.simpleFact("No resource found at '$path' in '$actual'")) + } + + val tempDir = Files.createTempDirectory(null) + Files.copy(resource, tempDir.resolve(path), StandardCopyOption.REPLACE_EXISTING) + } + + return PathSubject.assertThat(tempResource) + } +} diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt new file mode 100644 index 000000000..c180628d6 --- /dev/null +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt @@ -0,0 +1,67 @@ +// Copyright (c) 2024. Tony Robalik. +// SPDX-License-Identifier: Apache-2.0 +package com.autonomousapps.kit.truth.artifact + +import com.google.common.truth.* +import com.google.common.truth.Subject.Factory +import java.nio.charset.Charset +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.notExists +import kotlin.io.path.readLines +import kotlin.io.path.readText + +public class PathSubject private constructor( + failureMetadata: FailureMetadata, + private val actual: Path?, +) : Subject(failureMetadata, actual) { + + public companion object { + private val PATH_SUBJECT_FACTORY: Factory = + Factory { metadata, actual -> PathSubject(metadata, actual) } + + @JvmStatic + public fun paths(): Factory = PATH_SUBJECT_FACTORY + + @JvmStatic + public fun assertThat(actual: Path?): PathSubject { + return Truth.assertAbout(paths()).that(actual) + } + } + + public fun exists() { + if (actual == null) { + failWithActual(Fact.simpleFact("path was null")) + } + if (actual!!.notExists()) { + failWithActual(Fact.simpleFact("path does not exist")) + } + } + + public fun notExists() { + if (actual == null) { + failWithActual(Fact.simpleFact("path was null")) + } + if (actual!!.exists()) { + failWithActual(Fact.simpleFact("path exists")) + } + } + + @JvmOverloads + public fun text(charset: Charset = Charsets.UTF_8): StringSubject { + if (actual == null) { + failWithActual(Fact.simpleFact("path was null")) + } + + return check("readText()").that(actual!!.readText(charset)) + } + + @JvmOverloads + public fun lines(charset: Charset = Charsets.UTF_8): IterableSubject { + if (actual == null) { + failWithActual(Fact.simpleFact("path was null")) + } + + return check("readLines()").that(actual!!.readLines(charset)) + } +} From 6e00e1607d9d1c236f223b1e86b9e1681f860fab Mon Sep 17 00:00:00 2001 From: Tony Robalik Date: Mon, 15 Jan 2024 10:58:21 -0800 Subject: [PATCH 3/7] chore(testkit-truth): added AbstractSubject to simplify custom Subjects. --- .../kit/truth/AbstractSubject.kt | 28 ++++++++++ .../kit/truth/BuildResultSubject.kt | 2 +- .../kit/truth/BuildTaskSubject.kt | 15 ++---- .../truth/artifact/BuildArtifactsSubject.kt | 52 ++++++------------- .../kit/truth/artifact/JarSubject.kt | 18 ++----- .../kit/truth/artifact/PathSubject.kt | 29 ++++------- 6 files changed, 66 insertions(+), 78 deletions(-) create mode 100644 testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/AbstractSubject.kt diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/AbstractSubject.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/AbstractSubject.kt new file mode 100644 index 000000000..8961ea493 --- /dev/null +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/AbstractSubject.kt @@ -0,0 +1,28 @@ +// Copyright (c) 2024. Tony Robalik. +// SPDX-License-Identifier: Apache-2.0 +package com.autonomousapps.kit.truth + +import com.google.common.truth.Fact +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject + +// We need to extend Subject because failWithActual() is protected in that class. +public abstract class AbstractSubject internal constructor( + failureMetadata: FailureMetadata, + actual: T? +) : Subject(failureMetadata, actual) { + + internal fun assertNonNull(actual: T?, message: () -> String): T { + if (actual == null) { + failWithActual(Fact.simpleFact(message.invoke())) + } + return actual!! + } + + internal fun assertNonNull(actual: T?, key: String, value: Any?): T { + if (actual == null) { + failWithActual(key, value) + } + return actual!! + } +} diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/BuildResultSubject.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/BuildResultSubject.kt index 8cfda51f4..1153955ec 100644 --- a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/BuildResultSubject.kt +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/BuildResultSubject.kt @@ -18,7 +18,7 @@ import org.gradle.testkit.runner.TaskOutcome public class BuildResultSubject private constructor( failureMetadata: FailureMetadata, private val actual: BuildResult? -) : Subject(failureMetadata, actual) { +) : AbstractSubject(failureMetadata, actual) { public companion object { private val BUILD_RESULT_SUBJECT_FACTORY: Factory = diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/BuildTaskSubject.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/BuildTaskSubject.kt index df2f94242..26f8c52ba 100644 --- a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/BuildTaskSubject.kt +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/BuildTaskSubject.kt @@ -4,7 +4,6 @@ package com.autonomousapps.kit.truth import com.google.common.collect.Iterables import com.google.common.truth.FailureMetadata -import com.google.common.truth.Subject import com.google.common.truth.Subject.Factory import com.google.common.truth.Truth.assertAbout import com.google.errorprone.annotations.CanIgnoreReturnValue @@ -14,7 +13,7 @@ import org.gradle.testkit.runner.TaskOutcome public class BuildTaskSubject private constructor( failureMetadata: FailureMetadata, private val actual: BuildTask?, -) : Subject(failureMetadata, actual) { +) : AbstractSubject(failureMetadata, actual) { public companion object { private val BUILD_TASK_SUBJECT_FACTORY: Factory = @@ -49,10 +48,8 @@ public class BuildTaskSubject private constructor( @CanIgnoreReturnValue public fun hasOutcomeIn(outcomes: Iterable): BuildTaskSubject { - if (actual == null) { - failWithActual("expected to have a value", outcomes) - } - if (!Iterables.contains(outcomes, actual!!.outcome)) { + val actual = assertNonNull(actual, "expected to have a value", outcomes) + if (!Iterables.contains(outcomes, actual.outcome)) { failWithActual("expected any of", outcomes) } return this @@ -60,10 +57,8 @@ public class BuildTaskSubject private constructor( @CanIgnoreReturnValue public fun hasOutcomeIn(vararg outcomes: TaskOutcome): BuildTaskSubject { - if (actual == null) { - failWithActual("expected to have a value", outcomes) - } - if (!Iterables.contains(outcomes.toList(), actual!!.outcome)) { + val actual = assertNonNull(actual, "expected to have a value", outcomes) + if (!Iterables.contains(outcomes.toList(), actual.outcome)) { failWithActual("expected any of", outcomes.toList()) } return this diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt index a19696d79..fc91acab8 100644 --- a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 package com.autonomousapps.kit.truth.artifact +import com.autonomousapps.kit.truth.AbstractSubject import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata -import com.google.common.truth.Subject import com.google.common.truth.Subject.Factory import com.google.common.truth.Truth import java.nio.file.LinkOption @@ -15,7 +15,7 @@ import kotlin.io.path.* public class BuildArtifactsSubject private constructor( failureMetadata: FailureMetadata, private val actual: Path?, -) : Subject(failureMetadata, actual) { +) : AbstractSubject(failureMetadata, actual) { public companion object { private val BUILD_ARTIFACT_SUBJECT_FACTORY: Factory = @@ -25,9 +25,7 @@ public class BuildArtifactsSubject private constructor( public fun buildArtifacts(): Factory = BUILD_ARTIFACT_SUBJECT_FACTORY @JvmStatic - public fun assertThat(actual: Path?): BuildArtifactsSubject { - return Truth.assertAbout(buildArtifacts()).that(actual) - } + public fun assertThat(actual: Path?): BuildArtifactsSubject = Truth.assertAbout(buildArtifacts()).that(actual) } private enum class FileType(val humanReadableName: String) { @@ -47,46 +45,34 @@ public class BuildArtifactsSubject private constructor( } public fun exists() { - if (actual == null) { - failWithActual(Fact.simpleFact("build artifact was null")) - } - - check(actual!!.exists()) + val actual = assertNonNull(actual) { "build artifact was null" } + check(actual.exists()) } public fun notExists() { - if (actual == null) { - failWithActual(Fact.simpleFact("build artifact was null")) - } - - check(actual!!.notExists()) + val actual = assertNonNull(actual) { "build artifact was null" } + check(actual.notExists()) } public fun isRegularFile() { - if (actual == null) { - failWithActual(Fact.simpleFact("build artifact was null")) - } - if (actual!!.notExists()) { + val actual = assertNonNull(actual) { "build artifact was null" } + if (actual.notExists()) { failWithActual(Fact.simpleFact("build artifact does not exist")) } check(actual.isRegularFile()) { "Expected regular file. Was ${FileType.from(actual).humanReadableName}" } } public fun isDirectory() { - if (actual == null) { - failWithActual(Fact.simpleFact("build artifact was null")) - } - if (actual!!.notExists()) { + val actual = assertNonNull(actual) { "build artifact was null" } + if (actual.notExists()) { failWithActual(Fact.simpleFact("build artifact does not exist")) } check(actual.isDirectory()) { "Expected directory. Was ${FileType.from(actual).humanReadableName}" } } public fun isSymbolicLink() { - if (actual == null) { - failWithActual(Fact.simpleFact("build artifact was null")) - } - if (actual!!.notExists()) { + val actual = assertNonNull(actual) { "build artifact was null" } + if (actual.notExists()) { failWithActual(Fact.simpleFact("build artifact does not exist")) } check(actual.isSymbolicLink()) { "Expected symbolic link. Was ${FileType.from(actual).humanReadableName}" } @@ -99,10 +85,8 @@ public class BuildArtifactsSubject private constructor( public fun isType(extension: String) { isRegularFile() - if (actual == null) { - failWithActual(Fact.simpleFact("build artifact was null")) - } - if (actual!!.notExists()) { + val actual = assertNonNull(actual) { "build artifact was null" } + if (actual.notExists()) { failWithActual(Fact.simpleFact("build artifact does not exist")) } @@ -117,10 +101,8 @@ public class BuildArtifactsSubject private constructor( public fun file(): PathSubject { isRegularFile() - if (actual == null) { - failWithActual(Fact.simpleFact("build artifact was null")) - } - if (actual!!.notExists()) { + val actual = assertNonNull(actual) { "build artifact was null" } + if (actual.notExists()) { failWithActual(Fact.simpleFact("build artifact does not exist")) } diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/JarSubject.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/JarSubject.kt index faa1d11c1..dc6595ef3 100644 --- a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/JarSubject.kt +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/JarSubject.kt @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 package com.autonomousapps.kit.truth.artifact +import com.autonomousapps.kit.truth.AbstractSubject import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata -import com.google.common.truth.Subject import com.google.common.truth.Subject.Factory import com.google.common.truth.Truth import java.nio.file.FileSystems @@ -16,7 +16,7 @@ import kotlin.io.path.notExists public class JarSubject private constructor( failureMetadata: FailureMetadata, private val actual: Path?, -) : Subject(failureMetadata, actual) { +) : AbstractSubject(failureMetadata, actual) { public companion object { private val JAR_SUBJECT_FACTORY: Factory = @@ -26,26 +26,18 @@ public class JarSubject private constructor( public fun jars(): Factory = JAR_SUBJECT_FACTORY @JvmStatic - public fun assertThat(actual: Path?): JarSubject { - return Truth.assertAbout(jars()).that(actual) - } + public fun assertThat(actual: Path?): JarSubject = Truth.assertAbout(jars()).that(actual) } public fun containsResource(path: String) { - if (actual == null) { - failWithActual(Fact.simpleFact("jar was null")) - } - resource(path).exists() } public fun resource(path: String): PathSubject { - if (actual == null) { - failWithActual(Fact.simpleFact("jar was null")) - } + val actual = assertNonNull(actual) { "jar was null" } // Open zip, copy entry to temp dir, close zip. - val tempResource = FileSystems.newFileSystem(actual!!, null).use { fs -> + val tempResource = FileSystems.newFileSystem(actual, null).use { fs -> val resource = fs.getPath(path) if (resource.notExists()) { failWithActual(Fact.simpleFact("No resource found at '$path' in '$actual'")) diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt index c180628d6..6f83394c1 100644 --- a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.autonomousapps.kit.truth.artifact +import com.autonomousapps.kit.truth.AbstractSubject import com.google.common.truth.* import com.google.common.truth.Subject.Factory import java.nio.charset.Charset @@ -14,7 +15,7 @@ import kotlin.io.path.readText public class PathSubject private constructor( failureMetadata: FailureMetadata, private val actual: Path?, -) : Subject(failureMetadata, actual) { +) : AbstractSubject(failureMetadata, actual) { public companion object { private val PATH_SUBJECT_FACTORY: Factory = @@ -30,38 +31,28 @@ public class PathSubject private constructor( } public fun exists() { - if (actual == null) { - failWithActual(Fact.simpleFact("path was null")) - } - if (actual!!.notExists()) { + val actual = assertNonNull(actual) { "path was null" } + if (actual.notExists()) { failWithActual(Fact.simpleFact("path does not exist")) } } public fun notExists() { - if (actual == null) { - failWithActual(Fact.simpleFact("path was null")) - } - if (actual!!.exists()) { + val actual = assertNonNull(actual) { "path was null" } + if (actual.exists()) { failWithActual(Fact.simpleFact("path exists")) } } @JvmOverloads public fun text(charset: Charset = Charsets.UTF_8): StringSubject { - if (actual == null) { - failWithActual(Fact.simpleFact("path was null")) - } - - return check("readText()").that(actual!!.readText(charset)) + val actual = assertNonNull(actual) { "path was null" } + return check("readText()").that(actual.readText(charset)) } @JvmOverloads public fun lines(charset: Charset = Charsets.UTF_8): IterableSubject { - if (actual == null) { - failWithActual(Fact.simpleFact("path was null")) - } - - return check("readLines()").that(actual!!.readLines(charset)) + val actual = assertNonNull(actual) { "path was null" } + return check("readLines()").that(actual.readLines(charset)) } } From 48700967657eeb367c4cf4a9b49d3d0eaed45bb6 Mon Sep 17 00:00:00 2001 From: Tony Robalik Date: Mon, 15 Jan 2024 11:05:32 -0800 Subject: [PATCH 4/7] fix(testkit-support): remove Truth from runtime classpath of gradle-testkit-support. --- testkit/gradle-testkit-support/build.gradle.kts | 4 ++-- .../main/kotlin/com/autonomousapps/kit/GradleProject.kt | 7 +++---- .../src/main/kotlin/com/autonomousapps/kit/utils/files.kt | 3 +-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/testkit/gradle-testkit-support/build.gradle.kts b/testkit/gradle-testkit-support/build.gradle.kts index 7ead4a9a9..baa14ac85 100644 --- a/testkit/gradle-testkit-support/build.gradle.kts +++ b/testkit/gradle-testkit-support/build.gradle.kts @@ -42,10 +42,10 @@ dependencies { api(platform(libs.kotlin.bom)) api(gradleTestKit()) - implementation(libs.truth) - testImplementation(platform(libs.junit.bom)) testImplementation(libs.junit.api) + testImplementation(libs.truth) + testRuntimeOnly(libs.junit.engine) dokkaHtmlPlugin(libs.kotlin.dokka) diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/GradleProject.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/GradleProject.kt index 8ecbc4d8a..48d1e3b66 100644 --- a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/GradleProject.kt +++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/GradleProject.kt @@ -12,7 +12,6 @@ import com.autonomousapps.kit.gradle.GradleProperties import com.autonomousapps.kit.gradle.SettingsScript import com.autonomousapps.kit.internal.ensurePrefix import com.autonomousapps.kit.utils.buildPathForName -import com.google.common.truth.Truth import java.io.File import java.nio.file.Files import java.nio.file.Path @@ -123,7 +122,7 @@ public class GradleProject( buildDirName: String = "build", ): Path { val artifact = buildPathForName(path = projectName, buildDirName = buildDirName).resolve(relativePath) - Truth.assertWithMessage("No artifact with path '$artifact'").that(artifact.exists()).isTrue() + check(artifact.exists()) { "No artifact with path '$artifact'" } return artifact } @@ -162,8 +161,8 @@ public class GradleProject( @JvmOverloads public fun artifacts(projectName: String, relativePath: String, buildDirName: String = "build"): Path { val dir = buildPathForName(path = projectName, buildDirName = buildDirName).resolve(relativePath) - Truth.assertWithMessage("No directory with path '$dir'").that(dir.exists()).isTrue() - Truth.assertWithMessage("Expected directory, was '$dir'").that(Files.isDirectory(dir)).isTrue() + check(dir.exists()) { "No directory with path '$dir'" } + check(Files.isDirectory(dir)) { "Expected directory, was '$dir'" } return dir } diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/utils/files.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/utils/files.kt index 55107a3a6..ad876f92a 100644 --- a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/utils/files.kt +++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/utils/files.kt @@ -6,7 +6,6 @@ package com.autonomousapps.kit.utils import com.autonomousapps.kit.GradleProject import com.autonomousapps.kit.internal.ensurePrefix -import com.google.common.truth.Truth.assertWithMessage import java.io.File import java.nio.file.Path @@ -41,7 +40,7 @@ public fun GradleProject.resolveFromRoot(relativePath: String, buildDirName: Str /** Returns the path to the build dir of the first subproject, asserting that there is only one. */ public fun GradleProject.singleSubprojectBuildPath(): Path { - assertWithMessage("Expected only a single subproject").that(subprojects.size).isEqualTo(1) + check(subprojects.size == 1) { "Expected only a single subproject" } return buildDir(subprojects.first()) } From 6ffbb90cb9c725a0e64aadc7d2cd8be54ba60794 Mon Sep 17 00:00:00 2001 From: Tony Robalik Date: Mon, 15 Jan 2024 11:25:36 -0800 Subject: [PATCH 5/7] feat(testkit-support): new BuildArtifact API. --- .../com/autonomousapps/AdviceHelper.groovy | 10 ++- .../com/autonomousapps/AdviceStrategy.groovy | 9 ++- .../com/autonomousapps/kit/GradleProject.kt | 22 +++--- .../kit/artifacts/BuildArtifact.kt | 33 ++++++++ .../autonomousapps/kit/artifacts/FileType.kt | 29 +++++++ testkit/gradle-testkit-truth/build.gradle.kts | 3 + .../autonomousapps/kit/truth/TestKitTruth.kt | 4 +- .../truth/artifact/BuildArtifactsSubject.kt | 77 ++++++++++--------- .../kit/truth/artifact/PathSubject.kt | 11 ++- 9 files changed, 137 insertions(+), 61 deletions(-) create mode 100644 testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/artifacts/BuildArtifact.kt create mode 100644 testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/artifacts/FileType.kt diff --git a/src/functionalTest/groovy/com/autonomousapps/AdviceHelper.groovy b/src/functionalTest/groovy/com/autonomousapps/AdviceHelper.groovy index 68515a5de..67218a513 100644 --- a/src/functionalTest/groovy/com/autonomousapps/AdviceHelper.groovy +++ b/src/functionalTest/groovy/com/autonomousapps/AdviceHelper.groovy @@ -51,8 +51,9 @@ final class AdviceHelper { return new ProjectCoordinates(projectPath, defaultGVI(capability), buildPath) } - static Coordinates includedBuildCoordinates(String identifier, ProjectCoordinates resolvedProject, - String capability = null) { + static Coordinates includedBuildCoordinates( + String identifier, ProjectCoordinates resolvedProject, String capability = null + ) { return new IncludedBuildCoordinates(identifier, resolvedProject, defaultGVI(capability)) } @@ -76,8 +77,9 @@ final class AdviceHelper { return projectAdvice(projectPath, advice, pluginAdvice, false) } - static ProjectAdvice projectAdvice(String projectPath, Set advice, Set pluginAdvice, - boolean shouldFail) { + static ProjectAdvice projectAdvice( + String projectPath, Set advice, Set pluginAdvice, boolean shouldFail + ) { return projectAdvice(projectPath, advice, pluginAdvice, [] as Set, shouldFail) } diff --git a/src/functionalTest/groovy/com/autonomousapps/AdviceStrategy.groovy b/src/functionalTest/groovy/com/autonomousapps/AdviceStrategy.groovy index 9f245f0c8..c95cfa6ec 100644 --- a/src/functionalTest/groovy/com/autonomousapps/AdviceStrategy.groovy +++ b/src/functionalTest/groovy/com/autonomousapps/AdviceStrategy.groovy @@ -37,7 +37,8 @@ abstract class AdviceStrategy { @Override Map> getDuplicateDependenciesReport(GradleProject gradleProject) { - def json = gradleProject.singleArtifact(':', OutputPathsKt.getDuplicateDependenciesReport()).text.trim() + def json = gradleProject.singleArtifact(':', OutputPathsKt.getDuplicateDependenciesReport()) + .asPath.text.trim() def set = Types.newParameterizedType(Set, String) def map = Types.newParameterizedType(Map, String, set) def adapter = MoshiUtils.MOSHI.>> adapter(map) @@ -47,19 +48,19 @@ abstract class AdviceStrategy { @Override List getResolvedDependenciesReport(GradleProject gradleProject, String projectPath) { def report = gradleProject.singleArtifact(projectPath, OutputPathsKt.getResolvedDependenciesReport()) - return report.text.trim().readLines() + return report.asPath.text.trim().readLines() } @Override def actualBuildHealth(GradleProject gradleProject) { def buildHealth = gradleProject.singleArtifact(':', OutputPathsKt.getFinalAdvicePathV2()) - return fromAllProjectAdviceJson(buildHealth.text) + return fromAllProjectAdviceJson(buildHealth.asPath.text) } @Override def actualComprehensiveAdviceForProject(GradleProject gradleProject, String projectName) { def advice = gradleProject.singleArtifact(projectName, OutputPathsKt.getAggregateAdvicePathV2()) - return fromProjectAdvice(advice.text) + return fromProjectAdvice(advice.asPath.text) } } } diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/GradleProject.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/GradleProject.kt index 48d1e3b66..075e3bab9 100644 --- a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/GradleProject.kt +++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/GradleProject.kt @@ -7,6 +7,8 @@ import com.autonomousapps.kit.android.AndroidColorRes import com.autonomousapps.kit.android.AndroidManifest import com.autonomousapps.kit.android.AndroidStyleRes import com.autonomousapps.kit.android.AndroidSubproject +import com.autonomousapps.kit.artifacts.BuildArtifact +import com.autonomousapps.kit.artifacts.toBuildArtifact import com.autonomousapps.kit.gradle.BuildScript import com.autonomousapps.kit.gradle.GradleProperties import com.autonomousapps.kit.gradle.SettingsScript @@ -64,9 +66,7 @@ public class GradleProject( } /** Use ":" for the root project. */ - public fun projectDir(projectName: String): Path { - return projectDir(forName(projectName)) - } + public fun projectDir(projectName: String): Path = projectDir(forName(projectName)) /** Use [rootProject] for the root project. */ public fun projectDir(project: Subproject): Path { @@ -120,10 +120,10 @@ public class GradleProject( projectName: String, relativePath: String, buildDirName: String = "build", - ): Path { + ): BuildArtifact { val artifact = buildPathForName(path = projectName, buildDirName = buildDirName).resolve(relativePath) check(artifact.exists()) { "No artifact with path '$artifact'" } - return artifact + return BuildArtifact(artifact) } /** @@ -131,7 +131,7 @@ public class GradleProject( * artifact exists. An alias for [singleArtifact]. Uses "build" as the build directory name by default. */ @JvmOverloads - public fun getArtifact(projectName: String, relativePath: String, buildDirName: String = "build"): Path { + public fun getArtifact(projectName: String, relativePath: String, buildDirName: String = "build"): BuildArtifact { return singleArtifact(projectName = projectName, relativePath = relativePath, buildDirName = buildDirName) } @@ -144,10 +144,10 @@ public class GradleProject( projectName: String, relativePath: String, buildDirName: String = "build", - ): Path? { + ): BuildArtifact? { val artifact = buildPathForName(path = projectName, buildDirName = buildDirName).resolve(relativePath) return if (artifact.exists()) { - artifact + artifact.toBuildArtifact() } else { null } @@ -159,11 +159,11 @@ public class GradleProject( * by default. */ @JvmOverloads - public fun artifacts(projectName: String, relativePath: String, buildDirName: String = "build"): Path { + public fun artifacts(projectName: String, relativePath: String, buildDirName: String = "build"): BuildArtifact { val dir = buildPathForName(path = projectName, buildDirName = buildDirName).resolve(relativePath) check(dir.exists()) { "No directory with path '$dir'" } check(Files.isDirectory(dir)) { "Expected directory, was '$dir'" } - return dir + return dir.toBuildArtifact() } private fun forName(projectName: String): Subproject { @@ -171,7 +171,7 @@ public class GradleProject( return rootProject } - return subprojects.find { it.name == projectName.ensurePrefix() } + return subprojects.find { it.name.ensurePrefix() == projectName.ensurePrefix() } ?: throw IllegalStateException("No subproject with name '$projectName'") } diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/artifacts/BuildArtifact.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/artifacts/BuildArtifact.kt new file mode 100644 index 000000000..4cc3784c8 --- /dev/null +++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/artifacts/BuildArtifact.kt @@ -0,0 +1,33 @@ +// Copyright (c) 2024. Tony Robalik. +// SPDX-License-Identifier: Apache-2.0 +package com.autonomousapps.kit.artifacts + +import java.io.File +import java.nio.file.Path +import kotlin.io.path.* + +/** + * Essentially a wrapper around [path], with the intention to provide an expanded API eventually. + */ +public class BuildArtifact(private val path: Path) { + + /** + * The [Path] represented by this build artifact. + */ + public val asPath: Path get() = path + + /** + * The [File] represented by this build artifact. + */ + public val asFile: File get() = path.toFile() + + public fun exists(): Boolean = path.exists() + public fun notExists(): Boolean = path.notExists() + public fun isRegularFile(): Boolean = path.isRegularFile() + public fun isDirectory(): Boolean = path.isDirectory() + public fun isSymbolicLink(): Boolean = path.isSymbolicLink() + + public val extension: String get() = path.extension +} + +internal fun Path.toBuildArtifact(): BuildArtifact = BuildArtifact(this) diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/artifacts/FileType.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/artifacts/FileType.kt new file mode 100644 index 000000000..fecf0bb7e --- /dev/null +++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/artifacts/FileType.kt @@ -0,0 +1,29 @@ +// Copyright (c) 2024. Tony Robalik. +// SPDX-License-Identifier: Apache-2.0 +package com.autonomousapps.kit.artifacts + +import java.nio.file.LinkOption +import java.nio.file.Path +import kotlin.io.path.isDirectory +import kotlin.io.path.isRegularFile +import kotlin.io.path.isSymbolicLink + +public enum class FileType(public val humanReadableName: String) { + REGULAR_FILE("regular file"), + DIRECTORY("directory"), + SYMLINK("symbolic link"), + UNKNOWN("unknown"); + + public companion object { + public fun from(buildArtifact: BuildArtifact, vararg options: LinkOption): FileType { + return from(buildArtifact.asPath, *options) + } + + public fun from(path: Path, vararg options: LinkOption): FileType = when { + path.isRegularFile(*options) -> REGULAR_FILE + path.isDirectory(*options) -> DIRECTORY + path.isSymbolicLink() -> SYMLINK + else -> UNKNOWN + } + } +} diff --git a/testkit/gradle-testkit-truth/build.gradle.kts b/testkit/gradle-testkit-truth/build.gradle.kts index 2126b9802..7f479a4db 100644 --- a/testkit/gradle-testkit-truth/build.gradle.kts +++ b/testkit/gradle-testkit-truth/build.gradle.kts @@ -39,6 +39,9 @@ tasks.named("javadoc") { } dependencies { + api(project(":gradle-testkit-support")) { + because("Uses BuildArtifact") + } api(kotlin("stdlib")) api(gradleTestKit()) api(libs.truth) diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/TestKitTruth.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/TestKitTruth.kt index 485109f19..dc7f2b356 100644 --- a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/TestKitTruth.kt +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/TestKitTruth.kt @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.autonomousapps.kit.truth +import com.autonomousapps.kit.artifacts.BuildArtifact import com.autonomousapps.kit.truth.BuildResultSubject.Companion.buildResults import com.autonomousapps.kit.truth.BuildTaskListSubject.Companion.buildTaskList import com.autonomousapps.kit.truth.BuildTaskSubject.Companion.buildTasks @@ -10,7 +11,6 @@ import com.autonomousapps.kit.truth.artifact.BuildArtifactsSubject.Companion.bui import com.google.common.truth.Truth.assertAbout import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.BuildTask -import java.nio.file.Path public class TestKitTruth { public companion object { @@ -24,6 +24,6 @@ public class TestKitTruth { public fun assertThat(target: List): BuildTaskListSubject = assertAbout(buildTaskList()).that(target) @JvmStatic - public fun assertThat(target: Path): BuildArtifactsSubject = assertAbout(buildArtifacts()).that(target) + public fun assertThat(target: BuildArtifact): BuildArtifactsSubject = assertAbout(buildArtifacts()).that(target) } } diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt index fc91acab8..6eebc7cc1 100644 --- a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt @@ -2,87 +2,87 @@ // SPDX-License-Identifier: Apache-2.0 package com.autonomousapps.kit.truth.artifact +import com.autonomousapps.kit.artifacts.BuildArtifact +import com.autonomousapps.kit.artifacts.FileType import com.autonomousapps.kit.truth.AbstractSubject import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata import com.google.common.truth.Subject.Factory import com.google.common.truth.Truth -import java.nio.file.LinkOption -import java.nio.file.Path -import kotlin.io.path.* +import com.google.errorprone.annotations.CanIgnoreReturnValue -// TODO(tsr): what about a new BuildArtifact API? It would wrap Path/File. public class BuildArtifactsSubject private constructor( failureMetadata: FailureMetadata, - private val actual: Path?, -) : AbstractSubject(failureMetadata, actual) { + private val actual: BuildArtifact?, +) : AbstractSubject(failureMetadata, actual) { public companion object { - private val BUILD_ARTIFACT_SUBJECT_FACTORY: Factory = + private val BUILD_ARTIFACT_SUBJECT_FACTORY: Factory = Factory { metadata, actual -> BuildArtifactsSubject(metadata, actual) } @JvmStatic - public fun buildArtifacts(): Factory = BUILD_ARTIFACT_SUBJECT_FACTORY + public fun buildArtifacts(): Factory = BUILD_ARTIFACT_SUBJECT_FACTORY @JvmStatic - public fun assertThat(actual: Path?): BuildArtifactsSubject = Truth.assertAbout(buildArtifacts()).that(actual) - } - - private enum class FileType(val humanReadableName: String) { - REGULAR_FILE("regular file"), - DIRECTORY("directory"), - SYMLINK("symbolic link"), - UNKNOWN("unknown"); - - companion object { - fun from(path: Path, vararg options: LinkOption): FileType = when { - path.isRegularFile(*options) -> REGULAR_FILE - path.isDirectory(*options) -> DIRECTORY - path.isSymbolicLink() -> SYMLINK - else -> UNKNOWN - } + public fun assertThat(actual: BuildArtifact?): BuildArtifactsSubject { + return Truth.assertAbout(buildArtifacts()).that(actual) } } - public fun exists() { + @CanIgnoreReturnValue + public fun exists(): BuildArtifact { val actual = assertNonNull(actual) { "build artifact was null" } check(actual.exists()) + + return actual } - public fun notExists() { + @CanIgnoreReturnValue + public fun notExists(): BuildArtifact { val actual = assertNonNull(actual) { "build artifact was null" } check(actual.notExists()) + + return actual } - public fun isRegularFile() { + @CanIgnoreReturnValue + public fun isRegularFile(): BuildArtifact { val actual = assertNonNull(actual) { "build artifact was null" } if (actual.notExists()) { failWithActual(Fact.simpleFact("build artifact does not exist")) } check(actual.isRegularFile()) { "Expected regular file. Was ${FileType.from(actual).humanReadableName}" } + + return actual } - public fun isDirectory() { + @CanIgnoreReturnValue + public fun isDirectory(): BuildArtifact { val actual = assertNonNull(actual) { "build artifact was null" } if (actual.notExists()) { failWithActual(Fact.simpleFact("build artifact does not exist")) } check(actual.isDirectory()) { "Expected directory. Was ${FileType.from(actual).humanReadableName}" } + + return actual } - public fun isSymbolicLink() { + @CanIgnoreReturnValue + public fun isSymbolicLink(): BuildArtifact { val actual = assertNonNull(actual) { "build artifact was null" } if (actual.notExists()) { failWithActual(Fact.simpleFact("build artifact does not exist")) } check(actual.isSymbolicLink()) { "Expected symbolic link. Was ${FileType.from(actual).humanReadableName}" } - } - public fun isJar() { - isType("jar") + return actual } - public fun isType(extension: String) { + @CanIgnoreReturnValue + public fun isJar(): BuildArtifact = isType("jar") + + @CanIgnoreReturnValue + public fun isType(extension: String): BuildArtifact { isRegularFile() val actual = assertNonNull(actual) { "build artifact was null" } @@ -91,21 +91,22 @@ public class BuildArtifactsSubject private constructor( } check(actual.extension == extension) { "Expected extension to be '$extension'. Was '${actual.extension}'" } + + return actual } public fun jar(): JarSubject { - isJar() - return JarSubject.assertThat(actual) + val actual = isJar() + return JarSubject.assertThat(actual.asPath) } public fun file(): PathSubject { - isRegularFile() + val actual = isRegularFile() - val actual = assertNonNull(actual) { "build artifact was null" } if (actual.notExists()) { failWithActual(Fact.simpleFact("build artifact does not exist")) } - return PathSubject.assertThat(actual) + return PathSubject.assertThat(actual.asPath) } } diff --git a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt index 6f83394c1..25d288b35 100644 --- a/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt @@ -5,6 +5,7 @@ package com.autonomousapps.kit.truth.artifact import com.autonomousapps.kit.truth.AbstractSubject import com.google.common.truth.* import com.google.common.truth.Subject.Factory +import com.google.errorprone.annotations.CanIgnoreReturnValue import java.nio.charset.Charset import java.nio.file.Path import kotlin.io.path.exists @@ -30,18 +31,24 @@ public class PathSubject private constructor( } } - public fun exists() { + @CanIgnoreReturnValue + public fun exists(): Path { val actual = assertNonNull(actual) { "path was null" } if (actual.notExists()) { failWithActual(Fact.simpleFact("path does not exist")) } + + return actual } - public fun notExists() { + @CanIgnoreReturnValue + public fun notExists(): Path { val actual = assertNonNull(actual) { "path was null" } if (actual.exists()) { failWithActual(Fact.simpleFact("path exists")) } + + return actual } @JvmOverloads From 0fbbca5d75fa2189bf06d112248f0459db8c5787 Mon Sep 17 00:00:00 2001 From: Tony Robalik Date: Mon, 15 Jan 2024 11:36:38 -0800 Subject: [PATCH 6/7] test(testkit): use latest published testkit plugin to test testkit. --- testkit/settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testkit/settings.gradle.kts b/testkit/settings.gradle.kts index 27ac88435..c40e845ce 100644 --- a/testkit/settings.gradle.kts +++ b/testkit/settings.gradle.kts @@ -23,7 +23,7 @@ pluginManagement { } } plugins { - id("com.autonomousapps.testkit") version "0.4" + id("com.autonomousapps.testkit") version "0.8" id("com.github.johnrengelman.shadow") version "8.1.1" id("com.gradle.enterprise") version "3.15.1" From 98066f12f10ee1642ce1681b1c600c6f25c6b25b Mon Sep 17 00:00:00 2001 From: Tony Robalik Date: Mon, 15 Jan 2024 12:01:23 -0800 Subject: [PATCH 7/7] feat(testkit-support): add BuildScript.Builder.withKotlin() and validation. --- .../autonomousapps/kit/gradle/BuildScript.kt | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/gradle/BuildScript.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/gradle/BuildScript.kt index 5284a5c24..ee829241f 100644 --- a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/gradle/BuildScript.kt +++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/gradle/BuildScript.kt @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.autonomousapps.kit.gradle +import com.autonomousapps.kit.GradleProject import com.autonomousapps.kit.gradle.android.AndroidBlock import com.autonomousapps.kit.render.Scribe import org.intellij.lang.annotations.Language @@ -19,6 +20,8 @@ public class BuildScript( public val java: Java? = null, public val kotlin: Kotlin? = null, public val additions: String = "", + private val usesGroovy: Boolean = false, + private val usesKotlin: Boolean = false, ) { private val groupVersion = GroupVersion(group = group, version = version) @@ -59,6 +62,14 @@ public class BuildScript( kotlin?.let { k -> appendLine(scribe.use { s -> k.render(s) }) } if (additions.isNotBlank()) { + if (usesGroovy && scribe.dslKind != GradleProject.DslKind.GROOVY) { + error("You called withGroovy() but you're using Kotlin DSL") + } + + if (usesKotlin && scribe.dslKind != GradleProject.DslKind.KOTLIN) { + error("You called withKotlin() but you're using Groovy DSL") + } + appendLine(additions) } @@ -80,8 +91,17 @@ public class BuildScript( public var kotlin: Kotlin? = null public var additions: String = "" + private var usesGroovy = false + private var usesKotlin = false + public fun withGroovy(@Language("Groovy") script: String) { additions = script.trimIndent() + usesGroovy = true + } + + public fun withKotlin(@Language("kt") script: String) { + additions = script.trimIndent() + usesKotlin = true } public fun dependencies(vararg dependencies: Dependency) { @@ -120,7 +140,9 @@ public class BuildScript( dependencies = Dependencies(dependencies), java = java, kotlin = kotlin, - additions = additions + additions = additions, + usesGroovy = usesGroovy, + usesKotlin = usesKotlin, ) } }