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 9b24d31fb..c95cfa6ec 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,8 @@ abstract class AdviceStrategy { @Override Map> getDuplicateDependenciesReport(GradleProject gradleProject) { - def json = Files.resolveFromRoot(gradleProject, 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) @@ -50,25 +47,20 @@ abstract class AdviceStrategy { @Override List getResolvedDependenciesReport(GradleProject gradleProject, String projectPath) { - File report = Files.resolveFromName(gradleProject, projectPath, OutputPathsKt.getResolvedDependenciesReport()) - return report.text.trim().readLines() + def report = gradleProject.singleArtifact(projectPath, OutputPathsKt.getResolvedDependenciesReport()) + return report.asPath.text.trim().readLines() } @Override def actualBuildHealth(GradleProject gradleProject) { - File buildHealth = Files.resolveFromRoot(gradleProject, OutputPathsKt.getFinalAdvicePathV2()) - return fromAllProjectAdviceJson(buildHealth.text) + def buildHealth = gradleProject.singleArtifact(':', OutputPathsKt.getFinalAdvicePathV2()) + return fromAllProjectAdviceJson(buildHealth.asPath.text) } @Override def actualComprehensiveAdviceForProject(GradleProject gradleProject, String projectName) { - File advice = Files.resolveFromName(gradleProject, projectName, OutputPathsKt.getAggregateAdvicePathV2()) - return fromProjectAdvice(advice.text) - } - - @Override - List actualAdviceForFirstSubproject(GradleProject gradleProject) { - throw new IllegalStateException("Not yet implemented") + def advice = gradleProject.singleArtifact(projectName, OutputPathsKt.getAggregateAdvicePathV2()) + return fromProjectAdvice(advice.asPath.text) } } } 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 ea5ce93da..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,11 +7,17 @@ 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 +import com.autonomousapps.kit.internal.ensurePrefix +import com.autonomousapps.kit.utils.buildPathForName import java.io.File +import java.nio.file.Files import java.nio.file.Path +import kotlin.io.path.exists /** * A Gradle project consists of: @@ -60,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 { @@ -72,14 +76,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 +111,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", + ): BuildArtifact { + val artifact = buildPathForName(path = projectName, buildDirName = buildDirName).resolve(relativePath) + check(artifact.exists()) { "No artifact with path '$artifact'" } + return BuildArtifact(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"): BuildArtifact { + 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", + ): BuildArtifact? { + val artifact = buildPathForName(path = projectName, buildDirName = buildDirName).resolve(relativePath) + return if (artifact.exists()) { + artifact.toBuildArtifact() + } 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"): 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.toBuildArtifact() + } + 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.ensurePrefix() == 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/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-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, ) } } 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..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 @@ -5,24 +5,42 @@ package com.autonomousapps.kit.utils import com.autonomousapps.kit.GradleProject -import com.google.common.truth.Truth.assertWithMessage +import com.autonomousapps.kit.internal.ensurePrefix 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. */ 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()) } @@ -37,17 +55,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 +78,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() } 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/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/TestKitTruth.kt b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/TestKitTruth.kt index a990ffafc..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,9 +2,12 @@ // 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 +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 @@ -12,18 +15,15 @@ import org.gradle.testkit.runner.BuildTask 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: 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 new file mode 100644 index 000000000..6eebc7cc1 --- /dev/null +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/BuildArtifactsSubject.kt @@ -0,0 +1,112 @@ +// Copyright (c) 2024. Tony Robalik. +// 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 com.google.errorprone.annotations.CanIgnoreReturnValue + +public class BuildArtifactsSubject private constructor( + failureMetadata: FailureMetadata, + private val actual: BuildArtifact?, +) : AbstractSubject(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: BuildArtifact?): BuildArtifactsSubject { + return Truth.assertAbout(buildArtifacts()).that(actual) + } + } + + @CanIgnoreReturnValue + public fun exists(): BuildArtifact { + val actual = assertNonNull(actual) { "build artifact was null" } + check(actual.exists()) + + return actual + } + + @CanIgnoreReturnValue + public fun notExists(): BuildArtifact { + val actual = assertNonNull(actual) { "build artifact was null" } + check(actual.notExists()) + + return actual + } + + @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 + } + + @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 + } + + @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}" } + + return actual + } + + @CanIgnoreReturnValue + public fun isJar(): BuildArtifact = isType("jar") + + @CanIgnoreReturnValue + public fun isType(extension: String): BuildArtifact { + isRegularFile() + + val actual = assertNonNull(actual) { "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}'" } + + return actual + } + + public fun jar(): JarSubject { + val actual = isJar() + return JarSubject.assertThat(actual.asPath) + } + + public fun file(): PathSubject { + val actual = isRegularFile() + + if (actual.notExists()) { + failWithActual(Fact.simpleFact("build artifact does not exist")) + } + + return PathSubject.assertThat(actual.asPath) + } +} 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..dc6595ef3 --- /dev/null +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/JarSubject.kt @@ -0,0 +1,52 @@ +// Copyright (c) 2024. Tony Robalik. +// 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.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?, +) : AbstractSubject(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 = Truth.assertAbout(jars()).that(actual) + } + + public fun containsResource(path: String) { + resource(path).exists() + } + + public fun resource(path: String): PathSubject { + 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 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..25d288b35 --- /dev/null +++ b/testkit/gradle-testkit-truth/src/main/kotlin/com/autonomousapps/kit/truth/artifact/PathSubject.kt @@ -0,0 +1,65 @@ +// Copyright (c) 2024. Tony Robalik. +// 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 com.google.errorprone.annotations.CanIgnoreReturnValue +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?, +) : AbstractSubject(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) + } + } + + @CanIgnoreReturnValue + public fun exists(): Path { + val actual = assertNonNull(actual) { "path was null" } + if (actual.notExists()) { + failWithActual(Fact.simpleFact("path does not exist")) + } + + return actual + } + + @CanIgnoreReturnValue + public fun notExists(): Path { + val actual = assertNonNull(actual) { "path was null" } + if (actual.exists()) { + failWithActual(Fact.simpleFact("path exists")) + } + + return actual + } + + @JvmOverloads + public fun text(charset: Charset = Charsets.UTF_8): StringSubject { + val actual = assertNonNull(actual) { "path was null" } + return check("readText()").that(actual.readText(charset)) + } + + @JvmOverloads + public fun lines(charset: Charset = Charsets.UTF_8): IterableSubject { + val actual = assertNonNull(actual) { "path was null" } + return check("readLines()").that(actual.readLines(charset)) + } +} 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"