Skip to content

Commit

Permalink
More tests for API validation (#1056)
Browse files Browse the repository at this point in the history
Also add a ziplineApiCheck task.
  • Loading branch information
swankjesse committed Jun 23, 2023
1 parent 5f71614 commit 0afde76
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 15 deletions.
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
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

0 comments on commit 0afde76

Please sign in to comment.