Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More tests for API validation #1056

Merged
merged 1 commit into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.internal.file.FileCollectionFactory
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

@Suppress("unused") // Public API for Gradle plugin users.
abstract class ZiplineApiDumpTask @Inject constructor(
abstract class ZiplineApiValidationTask @Inject constructor(
fileCollectionFactory: FileCollectionFactory,
@Input val mode: Mode,
) : DefaultTask() {

@get:OutputFile
Copy link
Member

@JakeWharton JakeWharton Jun 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this task behave properly with up-to-date checking given the file is declared as an output rather than an input?

Expand Down Expand Up @@ -72,12 +74,44 @@ abstract class ZiplineApiDumpTask @Inject constructor(
}

is ExpectedApiRequiresUpdates -> {
tomlFile.sink().buffer().use { it.writeZiplineApi(decision.updatedApi) }
val tomlFileRelative = tomlFile.relativeTo(project.projectDir)
when (mode) {
Mode.Check -> {
throw Exception(
"""
|Zipline API file is incomplete. Run :ziplineApiDump to update it.
| $tomlFileRelative
""".trimMargin(),
)
}

Mode.Dump -> {
logger.info("Updated $tomlFileRelative because Zipline API has changed")
tomlFile.sink().buffer().use { it.writeZiplineApi(decision.updatedApi) }
}
}
}

ExpectedApiIsUpToDate -> {
// Do nothing.
}
}
}

/**
* This enum decides what happens when the actual API (.kt files) declares more services or
* functions than expected (.toml file):
*
* * ziplineApiCheck fails the build.
* * ziplineApiDump updates the TOML file.
*
* Both modes fail the build if the converse is true; ie. the actual API declares fewer services
* or functions than expected.
*
* Both modes succeed if the actual APIs are equal to the expectations.
*/
enum class Mode {
Check,
Dump,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class ZiplinePlugin : KotlinCompilerPluginSupportPlugin {

target.tasks.withType(KotlinCompile::class.java) { kotlinCompile ->
if (kotlinCompile.name == "compileKotlinJvm") {
registerZiplineApiDumpTask(target, kotlinCompile)
registerZiplineApiTask(target, kotlinCompile, ZiplineApiValidationTask.Mode.Check)
registerZiplineApiTask(target, kotlinCompile, ZiplineApiValidationTask.Mode.Dump)
}
}
}
Expand Down Expand Up @@ -113,22 +114,22 @@ class ZiplinePlugin : KotlinCompilerPluginSupportPlugin {
return ziplineCompileTask
}

private fun registerZiplineApiDumpTask(
private fun registerZiplineApiTask(
project: Project,
kotlinCompileTool: KotlinCompileTool,
): TaskProvider<ZiplineApiDumpTask> {
val result = project.tasks.register(
"ziplineApiDump",
ZiplineApiDumpTask::class.java,
mode: ZiplineApiValidationTask.Mode,
) {
val task = project.tasks.register(
"ziplineApi$mode",
ZiplineApiValidationTask::class.java,
mode,
)

result.configure {
task.configure {
it.ziplineApiFile.set(project.file("api/zipline-api.toml"))
it.sourcepath.setFrom(kotlinCompileTool.sources)
it.classpath.setFrom(kotlinCompileTool.libraries)
}

return result
}

override fun applyToCompilation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,160 @@ class ZiplinePluginTest {
}

@Test
fun ziplineApiDumpCreatesTomlFile() {
fun ziplineApiDumpDoesNothingOnApiMatch() {
ziplineApiTaskDoesNothingOnApiMatch(":lib:ziplineApiDump")
}

@Test
fun ziplineApiCheckDoesNothingOnApiMatch() {
ziplineApiTaskDoesNothingOnApiMatch(":lib:ziplineApiCheck")
}

private fun ziplineApiTaskDoesNothingOnApiMatch(taskName: String) {
val projectDir = File("src/test/projects/basic")
val ziplineApiToml = projectDir.resolve("lib/api/zipline-api.toml")
ziplineApiToml.parentFile.mkdirs()

val ziplineApiTomlContent = """
|# This comment will be clobbered if this file is overwritten
|# by the Gradle task.
|
|[app.cash.zipline.tests.GreetService]
|
|functions = [
| # fun close(): kotlin.Unit
| "moYx+T3e",
|
| # This comment will also be clobbered on an unexpected update.
| "ipvircui",
|]
""".trimMargin()
ziplineApiToml.writeText(ziplineApiTomlContent)

try {
createRunner(projectDir, "clean", taskName).build()
assertThat(ziplineApiToml.readText())
.isEqualTo(ziplineApiTomlContent)
} finally {
ziplineApiToml.delete()
}
}

@Test
fun ziplineApiCheckFailsOnDroppedApi() {
ziplineApiTaskFailsOnDroppedApi(":lib:ziplineApiCheck")
}

@Test
fun ziplineApiDumpFailsOnDroppedApi() {
ziplineApiTaskFailsOnDroppedApi(":lib:ziplineApiDump")
}

private fun ziplineApiTaskFailsOnDroppedApi(taskName: String) {
val projectDir = File("src/test/projects/basic")
val ziplineApiToml = projectDir.resolve("lib/api/zipline-api.toml")
ziplineApiToml.parentFile.mkdirs()

// Expect an API that contains a function not offered.
ziplineApiToml.writeText(
"""
|[app.cash.zipline.tests.GreetService]
|
|functions = [
| # fun close(): kotlin.Unit
| "moYx+T3e",
|
| # fun greet(kotlin.String): kotlin.String
| "ipvircui",
|
| # fun hello(kotlin.String): kotlin.String
| "Cw62Cti7",
|]
|
""".trimMargin(),
)

try {
val result = createRunner(projectDir, "clean", taskName).buildAndFail()
assertThat(result.output).contains(
"""
| Expected function Cw62Cti7 of app.cash.zipline.tests.GreetService not found:
| fun hello(kotlin.String): kotlin.String
""".trimMargin(),
)
} finally {
ziplineApiToml.delete()
}
}

@Test
fun ziplineApiDumpCreatesNewTomlFile() {
val projectDir = File("src/test/projects/basic")
val ziplineApiToml = projectDir.resolve("lib/api/zipline-api.toml")
ziplineApiToml.delete() // In case a previous execution crashed.

try {
val taskName = ":lib:ziplineApiDump"
val result = createRunner(projectDir, "clean", taskName).build()
assertThat(SUCCESS_OUTCOMES)
.contains(result.task(taskName)!!.outcome)
createRunner(projectDir, "clean", taskName).build()
assertThat(ziplineApiToml.readText()).isEqualTo(
"""
|[app.cash.zipline.tests.GreetService]
|
|functions = [
| # fun close(): kotlin.Unit
| "moYx+T3e",
|
| # fun greet(kotlin.String): kotlin.String
| "ipvircui",
|]
|
""".trimMargin(),
)
} finally {
ziplineApiToml.delete()
}
}

@Test
fun ziplineApiCheckFailsOnMissingTomlFile() {
val projectDir = File("src/test/projects/basic")
val ziplineApiToml = projectDir.resolve("lib/api/zipline-api.toml")
ziplineApiToml.delete() // In case a previous execution crashed.

val taskName = ":lib:ziplineApiCheck"
val result = createRunner(projectDir, "clean", taskName).buildAndFail()
assertThat(result.output).contains(
"""
|Zipline API file is incomplete. Run :ziplineApiDump to update it.
| api/zipline-api.toml
""".trimMargin(),
)
}

@Test
fun ziplineApiDumpUpdatesIncompleteFile() {
val projectDir = File("src/test/projects/basic")
val ziplineApiToml = projectDir.resolve("lib/api/zipline-api.toml")
ziplineApiToml.parentFile.mkdirs()

// Expect an API that doesn't declare 'greet'.
ziplineApiToml.writeText(
"""
|[app.cash.zipline.tests.GreetService]
|
|functions = [
| # fun close(): kotlin.Unit
| "moYx+T3e",
|]
|
""".trimMargin(),
)

try {
val taskName = ":lib:ziplineApiDump"
createRunner(projectDir, "clean", taskName).build()

// The task updates the file to include the 'greet' function.
assertThat(ziplineApiToml.readText()).isEqualTo(
"""
|[app.cash.zipline.tests.GreetService]
Expand All @@ -225,6 +369,39 @@ class ZiplinePluginTest {
}
}

@Test
fun ziplineApiCheckFailsOnIncompleteFile() {
val projectDir = File("src/test/projects/basic")
val ziplineApiToml = projectDir.resolve("lib/api/zipline-api.toml")
ziplineApiToml.parentFile.mkdirs()

// Expect an API that doesn't declare 'greet'.
ziplineApiToml.writeText(
"""
|[app.cash.zipline.tests.GreetService]
|
|functions = [
| # fun close(): kotlin.Unit
| "moYx+T3e",
|]
|
""".trimMargin(),
)

try {
val taskName = ":lib:ziplineApiCheck"
val result = createRunner(projectDir, "clean", taskName).buildAndFail()
assertThat(result.output).contains(
"""
|Zipline API file is incomplete. Run :ziplineApiDump to update it.
| api/zipline-api.toml
""".trimMargin(),
)
} finally {
ziplineApiToml.delete()
}
}

private fun createRunner(projectDir: File, vararg taskNames: String): GradleRunner {
val gradleRoot = projectDir.resolve("gradle").also { it.mkdir() }
File("../gradle/wrapper").copyRecursively(gradleRoot.resolve("wrapper"), true)
Expand Down