Skip to content

Commit

Permalink
Re-implement enum documentation test (#3762)
Browse files Browse the repository at this point in the history
* Re-implement enum documentation test

Re-implement the enum documentation test to download and use the actual Kotlin stdlib enum source code for the synthetic enum functions.

This avoids hardcoding a URL, and helps ensure the tests are properly cacheable (so they don't dynamically download files at runtime).

- Add a utility, `downloadKotlinStdlibSources()`, to download the Kotlin stdlib source code.
- Move StdLibDocumentationIntegrationTest.kt to plugin-base since it's not really an integration test, and otherwise it's hard to access the actual enum template files.
- Update the enum template tests to test all templates.
  • Loading branch information
adam-enko committed Aug 29, 2024
1 parent 421f52c commit 94e979b
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
package dokkabuild.utils

import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFile
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskInputFilePropertyBuilder
Expand All @@ -14,6 +16,7 @@ import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.findByType
import org.gradle.process.CommandLineArgumentProvider
import java.io.File
import javax.inject.Inject


Expand All @@ -34,19 +37,33 @@ val Test.systemProperty: SystemPropertyAdder
abstract class SystemPropertyAdder @Inject internal constructor(
private val task: Test,
) {
private val objects: ObjectFactory = task.project.objects

fun inputDirectory(
key: String,
value: Directory,
value: DirectoryProperty,
): TaskInputFilePropertyBuilder {
task.jvmArgumentProviders.add(
SystemPropertyArgumentProvider(key, value) {
it.asFile.invariantSeparatorsPath
it.get().asFile.invariantSeparatorsPath
}
)
return task.inputs.dir(value)
.withPropertyName("SystemProperty input directory $key")
}

fun inputDirectory(
key: String,
value: Provider<File>,
): TaskInputFilePropertyBuilder =
inputDirectory(key, objects.directoryProperty().fileProvider(value))

fun inputDirectory(
key: String,
value: Directory,
): TaskInputFilePropertyBuilder =
inputDirectory(key, objects.directoryProperty().apply { set(value) })

fun inputFile(
key: String,
file: RegularFile,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package dokkabuild.utils

import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE
import org.gradle.api.attributes.Category.DOCUMENTATION
import org.gradle.api.attributes.DocsType.DOCS_TYPE_ATTRIBUTE
import org.gradle.api.attributes.DocsType.SOURCES
import org.gradle.api.attributes.Usage.JAVA_RUNTIME
import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE
import org.gradle.api.file.ArchiveOperations
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Sync
import org.gradle.kotlin.dsl.*
import org.gradle.kotlin.dsl.support.serviceOf
import java.io.File


/**
* Download and unpack Kotlin stdlib JVM source code.
*
* @returns the directory containing the unpacked sources.
*/
fun downloadKotlinStdlibJvmSources(project: Project): Provider<File> {
val kotlinStdlibJvmSources: Configuration by project.configurations.creating {
description = "kotlin-stdlib JVM source code."
declarable()
withDependencies {
add(project.dependencies.run { create(kotlin("stdlib")) })
}
}

val kotlinStdlibJvmSourcesResolver: Configuration by project.configurations.creating {
description = "Resolver for ${kotlinStdlibJvmSources.name}."
resolvable()
isTransitive = false
extendsFrom(kotlinStdlibJvmSources)
attributes {
attribute(USAGE_ATTRIBUTE, project.objects.named(JAVA_RUNTIME))
attribute(CATEGORY_ATTRIBUTE, project.objects.named(DOCUMENTATION))
attribute(DOCS_TYPE_ATTRIBUTE, project.objects.named(SOURCES))
}
}

val downloadKotlinStdlibSources by project.tasks.registering(Sync::class) {
description = "Download and unpacks kotlin-stdlib JVM source code."
val archives = project.serviceOf<ArchiveOperations>()
val unpackedJvmSources = kotlinStdlibJvmSourcesResolver.incoming.artifacts.resolvedArtifacts.map { artifacts ->
artifacts.map {
archives.zipTree(it.file)
}
}
from(unpackedJvmSources)
into(temporaryDir)
}

return downloadKotlinStdlibSources.map { it.destinationDir }
}

This file was deleted.

12 changes: 12 additions & 0 deletions dokka-subprojects/plugin-base/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
*/

import dokkabuild.overridePublicationArtifactId
import dokkabuild.utils.downloadKotlinStdlibJvmSources
import dokkabuild.utils.systemProperty
import org.gradle.api.tasks.PathSensitivity.RELATIVE

plugins {
id("dokkabuild.kotlin-jvm")
Expand Down Expand Up @@ -79,3 +82,12 @@ sourceSets.main {
tasks.test {
maxHeapSize = "4G"
}

//region Download and unpack kotlin-stdlib, so EnumTemplatesTest can test synthetic enum functions.
val kotlinStdlibSourcesDir = downloadKotlinStdlibJvmSources(project)
tasks.withType<Test>().configureEach {
systemProperty
.inputDirectory("kotlinStdlibSourcesDir", kotlinStdlibSourcesDir)
.withPathSensitivity(RELATIVE)
}
//endregion
119 changes: 119 additions & 0 deletions dokka-subprojects/plugin-base/src/test/kotlin/EnumTemplatesTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
import org.intellij.lang.annotations.Language
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.assertAll
import utils.assertContains
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.test.Test
import kotlin.test.assertTrue

/**
* Documentation for Enum's synthetic `values()` and `valueOf()` functions is only present in source code,
* but not present in the descriptors. However, Dokka needs to generate documentation for these functions,
* so it ships with hardcoded kdoc templates.
*
* This test exists to make sure the kdoc from Kotlin stdlib for the hardcoded synthetic enum functions
* matches (sometimes approximately) Dokka's templates.
*/
class EnumTemplatesTest {

@Test
fun enumValueOf() {
listOf(
"Returns the enum constant of this type with the specified name.",
"(Extraneous whitespace characters are not permitted.)",
).forEach { line ->
assertSubstringIsInFiles(line, enumValueOfTemplate, actualStdlibEnumKt)
}

"The string must match exactly an identifier used to declare an enum constant in this type.".let { line ->
assertSubstringIsInFiles(line, actualStdlibEnumKt)
assertSubstringIsInFiles(
// The Dokka template has a newline, but otherwise the text is the same.
line.replace("identifier used to declare", "identifier used\nto declare"),
enumValueOfTemplate,
)
}

"@throws IllegalArgumentException if this enum type has no constant with the specified name".let { line ->
assertSubstringIsInFiles(line, actualStdlibEnumKt)
assertSubstringIsInFiles(
// The Dokka template uses the FQN of IllegalArgumentException
line.replace("IllegalArgumentException", "kotlin.IllegalArgumentException"),
enumValueOfTemplate,
)
}
}

@Test
fun enumValues() {
listOf(
"Returns an array containing the constants of this enum type, in the order they're declared.",
"This method may be used to iterate over the constants.",
).forEach { line ->
assertSubstringIsInFiles(line, enumValuesTemplate, actualStdlibEnumKt)
}
}

/**
* This test is disabled because the `Enum.entries` does not have accessible documentation.
*
* See https://youtrack.jetbrains.com/issue/KTIJ-23569/Provide-quick-documentation-for-Enum.entries
*/
@Test
@Disabled("Kotlin stdlib does not have kdoc for Enum.entries")
fun enumEntries() {
listOf(
"Returns a representation of an immutable list of all enum entries, in the order they're declared.",
"This method may be used to iterate over the enum entries.",
).forEach { line ->
assertSubstringIsInFiles(line, enumEntriesTemplate, actualStdlibEnumKt)
}
}

companion object {

/**
* Assert that all [files] exist, are files, and contain [substring].
*/
private fun assertSubstringIsInFiles(substring: String, vararg files: Path) {
assertAll(files.map { file ->
{
assertTrue(Files.exists(file), "File does not exist: $file")
assertTrue(Files.isRegularFile(file), "File is not a regular file: $file")
assertContains(file.toFile().readText(), substring)
}
})
}

private fun loadResource(@Language("file-reference") path: String): Path {
val resource = EnumTemplatesTest::class.java.getResource(path)
?.toURI()
?: error("Failed to load resource: $path")
return Paths.get(resource)
}

private val enumEntriesTemplate: Path = loadResource("/dokka/docs/kdoc/EnumEntries.kt.template")
private val enumValueOfTemplate: Path = loadResource("/dokka/docs/kdoc/EnumValueOf.kt.template")
private val enumValuesTemplate: Path = loadResource("/dokka/docs/kdoc/EnumValues.kt.template")

/**
* Base directory for the unpacked Kotlin stdlib source code.
* The system property must be set in the Gradle task.
*/
private val kotlinStdlibSourcesDir: Path by lazy {
val sourcesDir = System.getProperty("kotlinStdlibSourcesDir")
?: error("Missing 'kotlinStdlibSourcesDir' system property")
Paths.get(sourcesDir)
}

/** Get the actual `Enum.kt` source file from Kotlin stdlib. */
private val actualStdlibEnumKt: Path by lazy {
kotlinStdlibSourcesDir.resolve("jvmMain/kotlin/Enum.kt")
}
}
}
25 changes: 20 additions & 5 deletions dokka-subprojects/plugin-base/src/test/kotlin/utils/TestUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ interface AssertDSL {
infix fun Any?.equals(other: Any?) = assertEquals(other, this)
infix fun Collection<Any>?.allEquals(other: Any?) =
this?.onEach { it equals other } ?: run { fail("Collection is empty") }

infix fun <T> Collection<T>?.exists(e: T) {
assertTrue(this.orEmpty().isNotEmpty(), "Collection cannot be null or empty")
assertTrue(this!!.any{it == e}, "Collection doesn't contain $e")
assertTrue(this!!.any { it == e }, "Collection doesn't contain $e")
}

infix fun <T> Collection<T>?.counts(n: Int) = this.orEmpty().assertCount(n)
Expand All @@ -44,16 +45,30 @@ interface AssertDSL {
assertEquals(n, count(), "${prefix}Expected $n, got ${count()}")
}

/*
* TODO replace with kotlin.test.assertContains after migrating to Kotlin 1.5+
*/
internal fun <T> assertContains(iterable: Iterable<T>, element: T, ) {
// TODO replace with kotlin.test.assertContains after migrating to Kotlin 1.5+
internal fun <T> assertContains(iterable: Iterable<T>, element: T) {
asserter.assertTrue(
{ "Expected the collection to contain the element.\nCollection <$iterable>, element <$element>." },
iterable.contains(element)
)
}

private fun messagePrefix(message: String?) = if (message == null) "" else "$message. "

// TODO replace with kotlin.test.assertContains after migrating to Kotlin 1.5+
// https://github.com/JetBrains/kotlin/blob/c072e7c945fed74805d87ecc89c9a650bad23e12/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Assertions.kt#L334-L345
internal fun assertContains(
charSequence: CharSequence,
other: CharSequence,
ignoreCase: Boolean = false,
message: String? = null,
) {
asserter.assertTrue(
{ messagePrefix(message) + "Expected the char sequence to contain the substring.\nCharSequence <$charSequence>, substring <$other>, ignoreCase <$ignoreCase>." },
charSequence.contains(other, ignoreCase)
)
}

inline fun <reified T : Any> Any?.assertIsInstance(name: String): T =
this.let { it as? T } ?: throw AssertionError("$name should not be null")

Expand Down

0 comments on commit 94e979b

Please sign in to comment.