diff options
Diffstat (limited to 'dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures')
13 files changed, 785 insertions, 0 deletions
diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/GradleTestKitUtils.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/GradleTestKitUtils.kt new file mode 100644 index 00000000..2f9e1b41 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/GradleTestKitUtils.kt @@ -0,0 +1,274 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty +import org.gradle.testkit.runner.GradleRunner +import org.intellij.lang.annotations.Language + + +// utils for testing using Gradle TestKit + + +class GradleProjectTest( + override val projectDir: Path, +) : ProjectDirectoryScope { + + constructor( + testProjectName: String, + baseDir: Path = funcTestTempDir, + ) : this(projectDir = baseDir.resolve(testProjectName)) + + val runner: GradleRunner + get() = GradleRunner.create() + .withProjectDir(projectDir.toFile()) + .withJvmArguments( + "-XX:MaxMetaspaceSize=512m", + "-XX:+AlwaysPreTouch", // https://github.com/gradle/gradle/issues/3093#issuecomment-387259298 + ).addArguments( + // disable the logging task so the tests work consistently on local machines and CI/CD + "-P" + "org.jetbrains.dokka.dokkatoo.tasks.logHtmlPublicationLinkEnabled=false" + ) + + val testMavenRepoRelativePath: String = + projectDir.relativize(testMavenRepoDir).toFile().invariantSeparatorsPath + + companion object { + + /** file-based Maven Repo that contains the Dokka dependencies */ + val testMavenRepoDir: Path by systemProperty(Paths::get) + + val projectTestTempDir: Path by systemProperty(Paths::get) + + /** Temporary directory for the functional tests */ + val funcTestTempDir: Path by lazy { + projectTestTempDir.resolve("functional-tests") + } + + /** Dokka Source directory that contains Gradle projects used for integration tests */ + val integrationTestProjectsDir: Path by systemProperty(Paths::get) + /** Dokka Source directory that contains example Gradle projects */ + val exampleProjectsDir: Path by systemProperty(Paths::get) + } +} + + +///** +// * Load a project from the [GradleProjectTest.dokkaSrcIntegrationTestProjectsDir] +// */ +//fun gradleKtsProjectIntegrationTest( +// testProjectName: String, +// build: GradleProjectTest.() -> Unit, +//): GradleProjectTest = +// GradleProjectTest( +// baseDir = GradleProjectTest.dokkaSrcIntegrationTestProjectsDir, +// testProjectName = testProjectName, +// ).apply(build) + + +/** + * Builder for testing a Gradle project that uses Kotlin script DSL and creates default + * `settings.gradle.kts` and `gradle.properties` files. + * + * @param[testProjectName] the path of the project directory, relative to [baseDir + */ +fun gradleKtsProjectTest( + testProjectName: String, + baseDir: Path = GradleProjectTest.funcTestTempDir, + build: GradleProjectTest.() -> Unit, +): GradleProjectTest { + return GradleProjectTest(baseDir = baseDir, testProjectName = testProjectName).apply { + + settingsGradleKts = """ + |rootProject.name = "test" + | + |@Suppress("UnstableApiUsage") + |dependencyResolutionManagement { + | repositories { + | mavenCentral() + | maven(file("$testMavenRepoRelativePath")) { + | mavenContent { + | includeGroup("org.jetbrains.dokka.dokkatoo") + | includeGroup("org.jetbrains.dokka.dokkatoo-html") + | } + | } + | } + |} + | + |pluginManagement { + | repositories { + | mavenCentral() + | gradlePluginPortal() + | maven(file("$testMavenRepoRelativePath")) { + | mavenContent { + | includeGroup("org.jetbrains.dokka.dokkatoo") + | includeGroup("org.jetbrains.dokka.dokkatoo-html") + | } + | } + | } + |} + | + """.trimMargin() + + gradleProperties = """ + |kotlin.mpp.stability.nowarn=true + |org.gradle.cache=true + """.trimMargin() + + build() + } +} + +/** + * Builder for testing a Gradle project that uses Groovy script and creates default, + * `settings.gradle`, and `gradle.properties` files. + * + * @param[testProjectName] the name of the test, which should be distinct across the project + */ +fun gradleGroovyProjectTest( + testProjectName: String, + baseDir: Path = GradleProjectTest.funcTestTempDir, + build: GradleProjectTest.() -> Unit, +): GradleProjectTest { + return GradleProjectTest(baseDir = baseDir, testProjectName = testProjectName).apply { + + settingsGradle = """ + |rootProject.name = "test" + | + |dependencyResolutionManagement { + | repositories { + | mavenCentral() + | maven { url = file("$testMavenRepoRelativePath") } + | } + |} + | + |pluginManagement { + | repositories { + | mavenCentral() + | gradlePluginPortal() + | maven { url = file("$testMavenRepoRelativePath") } + | } + |} + | + """.trimMargin() + + gradleProperties = """ + |kotlin.mpp.stability.nowarn=true + |org.gradle.cache=true + """.trimMargin() + + build() + } +} + + +fun GradleProjectTest.projectFile( + @Language("TEXT") + filePath: String +): PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, String>> = + PropertyDelegateProvider { _, _ -> + TestProjectFileProvidedDelegate(this, filePath) + } + + +/** Delegate for reading and writing a [GradleProjectTest] file. */ +private class TestProjectFileProvidedDelegate( + private val project: GradleProjectTest, + private val filePath: String, +) : ReadWriteProperty<Any?, String> { + override fun getValue(thisRef: Any?, property: KProperty<*>): String = + project.projectDir.resolve(filePath).toFile().readText() + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { + project.createFile(filePath, value) + } +} + +/** Delegate for reading and writing a [GradleProjectTest] file. */ +class TestProjectFileDelegate( + private val filePath: String, +) : ReadWriteProperty<ProjectDirectoryScope, String> { + override fun getValue(thisRef: ProjectDirectoryScope, property: KProperty<*>): String = + thisRef.projectDir.resolve(filePath).toFile().readText() + + override fun setValue(thisRef: ProjectDirectoryScope, property: KProperty<*>, value: String) { + thisRef.createFile(filePath, value) + } +} + + +@DslMarker +annotation class ProjectDirectoryDsl + +@ProjectDirectoryDsl +interface ProjectDirectoryScope { + val projectDir: Path +} + +private data class ProjectDirectoryScopeImpl( + override val projectDir: Path +) : ProjectDirectoryScope + + +fun ProjectDirectoryScope.createFile(filePath: String, contents: String): File = + projectDir.resolve(filePath).toFile().apply { + parentFile.mkdirs() + createNewFile() + writeText(contents) + } + + +@ProjectDirectoryDsl +fun ProjectDirectoryScope.dir( + path: String, + block: ProjectDirectoryScope.() -> Unit = {}, +): ProjectDirectoryScope = + ProjectDirectoryScopeImpl(projectDir.resolve(path)).apply(block) + + +@ProjectDirectoryDsl +fun ProjectDirectoryScope.file( + path: String +): Path = projectDir.resolve(path) + + +fun ProjectDirectoryScope.findFiles(matcher: (File) -> Boolean): Sequence<File> = + projectDir.toFile().walk().filter(matcher) + + +/** Set the content of `settings.gradle.kts` */ +@delegate:Language("kts") +var ProjectDirectoryScope.settingsGradleKts: String by TestProjectFileDelegate("settings.gradle.kts") + + +/** Set the content of `build.gradle.kts` */ +@delegate:Language("kts") +var ProjectDirectoryScope.buildGradleKts: String by TestProjectFileDelegate("build.gradle.kts") + + +/** Set the content of `settings.gradle` */ +@delegate:Language("groovy") +var ProjectDirectoryScope.settingsGradle: String by TestProjectFileDelegate("settings.gradle") + + +/** Set the content of `build.gradle` */ +@delegate:Language("groovy") +var ProjectDirectoryScope.buildGradle: String by TestProjectFileDelegate("build.gradle") + + +/** Set the content of `gradle.properties` */ +@delegate:Language("properties") +var ProjectDirectoryScope.gradleProperties: String by TestProjectFileDelegate( + /* language=text */ "gradle.properties" +) + + +fun ProjectDirectoryScope.createKotlinFile(filePath: String, @Language("kotlin") contents: String) = + createFile(filePath, contents) + + +fun ProjectDirectoryScope.createKtsFile(filePath: String, @Language("kts") contents: String) = + createFile(filePath, contents) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/KotestProjectConfig.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/KotestProjectConfig.kt new file mode 100644 index 00000000..d6eadba0 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/KotestProjectConfig.kt @@ -0,0 +1,10 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import io.kotest.core.config.AbstractProjectConfig + +@Suppress("unused") // this class is automatically picked up by Kotest +object KotestProjectConfig : AbstractProjectConfig() { + init { + displayFullTestPath = true + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/fileTree.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/fileTree.kt new file mode 100644 index 00000000..4ba850d3 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/fileTree.kt @@ -0,0 +1,61 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import java.io.File +import java.nio.file.Path + +// based on https://gist.github.com/mfwgenerics/d1ec89eb80c95da9d542a03b49b5e15b +// context: https://kotlinlang.slack.com/archives/C0B8MA7FA/p1676106647658099 + +fun Path.toTreeString(): String = toFile().toTreeString() + +fun File.toTreeString(): String = when { + isDirectory -> name + "/\n" + buildTreeString(this) + else -> name +} + +private fun buildTreeString( + dir: File, + margin: String = "", +): String { + val entries = dir.listDirectoryEntries() + + return entries.joinToString("\n") { entry -> + val (currentPrefix, nextPrefix) = when (entry) { + entries.last() -> PrefixPair.LAST_ENTRY + else -> PrefixPair.INTERMEDIATE + } + + buildString { + append("$margin${currentPrefix}${entry.name}") + + if (entry.isDirectory) { + append("/") + if (entry.countDirectoryEntries() > 0) { + append("\n") + } + append(buildTreeString(entry, margin + nextPrefix)) + } + } + } +} + +private fun File.listDirectoryEntries(): Sequence<File> = + walkTopDown().maxDepth(1).filter { it != this@listDirectoryEntries } + + +private fun File.countDirectoryEntries(): Int = + listDirectoryEntries().count() + +private data class PrefixPair( + /** The current entry should be prefixed with this */ + val currentPrefix: String, + /** If the next item is a directory, it should be prefixed with this */ + val nextPrefix: String, +) { + companion object { + /** Prefix pair for a non-last directory entry */ + val INTERMEDIATE = PrefixPair("├── ", "│ ") + /** Prefix pair for the last directory entry */ + val LAST_ENTRY = PrefixPair("└── ", " ") + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/files.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/files.kt new file mode 100644 index 00000000..6a423b55 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/files.kt @@ -0,0 +1,6 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import java.io.File + +fun File.copyInto(directory: File, overwrite: Boolean = false) = + copyTo(directory.resolve(name), overwrite = overwrite) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/gradleRunnerUtils.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/gradleRunnerUtils.kt new file mode 100644 index 00000000..912d1df1 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/gradleRunnerUtils.kt @@ -0,0 +1,47 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.BuildTask +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.internal.DefaultGradleRunner + + +/** Edit environment variables in the Gradle Runner */ +@Deprecated("Windows does not support withEnvironment - https://github.com/gradle/gradle/issues/23959") +fun GradleRunner.withEnvironment(build: MutableMap<String, String?>.() -> Unit): GradleRunner { + val env = environment ?: mutableMapOf() + env.build() + return withEnvironment(env) +} + + +inline fun GradleRunner.build( + handleResult: BuildResult.() -> Unit +): Unit = build().let(handleResult) + + +inline fun GradleRunner.buildAndFail( + handleResult: BuildResult.() -> Unit +): Unit = buildAndFail().let(handleResult) + + +fun GradleRunner.withJvmArguments( + vararg jvmArguments: String +): GradleRunner = (this as DefaultGradleRunner).withJvmArguments(*jvmArguments) + + +/** + * Helper function to _append_ [arguments] to any existing + * [GradleRunner arguments][GradleRunner.getArguments]. + */ +fun GradleRunner.addArguments( + vararg arguments: String +): GradleRunner = + withArguments(this@addArguments.arguments + arguments) + + +/** + * Get the name of the task, without the leading [BuildTask.getPath]. + */ +val BuildTask.name: String + get() = path.substringAfterLast(':') diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestCollectionMatchers.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestCollectionMatchers.kt new file mode 100644 index 00000000..8c33e3eb --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestCollectionMatchers.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import io.kotest.matchers.collections.shouldBeSingleton +import io.kotest.matchers.maps.shouldContainAll +import io.kotest.matchers.maps.shouldContainExactly + +/** @see io.kotest.matchers.maps.shouldContainAll */ +fun <K, V> Map<K, V>.shouldContainAll( + vararg expected: Pair<K, V> +): Unit = shouldContainAll(expected.toMap()) + +/** @see io.kotest.matchers.maps.shouldContainExactly */ +fun <K, V> Map<K, V>.shouldContainExactly( + vararg expected: Pair<K, V> +): Unit = shouldContainExactly(expected.toMap()) + +/** Verify the sequence contains a single element, matching [match]. */ +fun <T> Sequence<T>.shouldBeSingleton(match: (T) -> Unit) { + toList().shouldBeSingleton(match) +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestConditions.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestConditions.kt new file mode 100644 index 00000000..7b692afb --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestConditions.kt @@ -0,0 +1,10 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import io.kotest.core.annotation.EnabledCondition +import io.kotest.core.spec.Spec +import kotlin.reflect.KClass + +class NotWindowsCondition : EnabledCondition { + override fun enabled(kclass: KClass<out Spec>): Boolean = + "win" !in System.getProperty("os.name").lowercase() +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestGradleAssertions.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestGradleAssertions.kt new file mode 100644 index 00000000..e1863c8f --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestGradleAssertions.kt @@ -0,0 +1,130 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.* +import org.gradle.api.NamedDomainObjectCollection +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.BuildTask +import org.gradle.testkit.runner.TaskOutcome + +infix fun <T : Any> NamedDomainObjectCollection<out T>?.shouldContainDomainObject( + name: String +): T { + this should containDomainObject(name) + return this?.getByName(name)!! +} + +infix fun <T : Any> NamedDomainObjectCollection<out T>?.shouldNotContainDomainObject( + name: String +): NamedDomainObjectCollection<out T>? { + this shouldNot containDomainObject(name) + return this +} + +private fun <T> containDomainObject(name: String): Matcher<NamedDomainObjectCollection<T>?> = + neverNullMatcher { value -> + MatcherResult( + name in value.names, + { "NamedDomainObjectCollection(${value.names}) should contain DomainObject named '$name'" }, + { "NamedDomainObjectCollection(${value.names}) should not contain DomainObject named '$name'" }) + } + +/** Assert that a task ran. */ +infix fun BuildResult?.shouldHaveRunTask(taskPath: String): BuildTask { + this should haveTask(taskPath) + return this?.task(taskPath)!! +} + +/** Assert that a task ran, with an [expected outcome][expectedOutcome]. */ +fun BuildResult?.shouldHaveRunTask( + taskPath: String, + expectedOutcome: TaskOutcome +): BuildTask { + this should haveTask(taskPath) + val task = this?.task(taskPath)!! + task should haveOutcome(expectedOutcome) + return task +} + +/** + * Assert that a task did not run. + * + * A task might not have run if one of its dependencies failed before it could be run. + */ +infix fun BuildResult?.shouldNotHaveRunTask(taskPath: String) { + this shouldNot haveTask(taskPath) +} + +private fun haveTask(taskPath: String): Matcher<BuildResult?> = + neverNullMatcher { value -> + MatcherResult( + value.task(taskPath) != null, + { "BuildResult should have run task $taskPath. All tasks: ${value.tasks.joinToString { it.path }}" }, + { "BuildResult should not have run task $taskPath. All tasks: ${value.tasks.joinToString { it.path }}" }, + ) + } + + +infix fun BuildTask?.shouldHaveOutcome(outcome: TaskOutcome) { + this should haveOutcome(outcome) +} + + +infix fun BuildTask?.shouldHaveAnyOutcome(outcomes: Collection<TaskOutcome>) { + this should haveAnyOutcome(outcomes) +} + + +infix fun BuildTask?.shouldNotHaveOutcome(outcome: TaskOutcome) { + this shouldNot haveOutcome(outcome) +} + + +private fun haveOutcome(outcome: TaskOutcome): Matcher<BuildTask?> = + haveAnyOutcome(listOf(outcome)) + + +private fun haveAnyOutcome(outcomes: Collection<TaskOutcome>): Matcher<BuildTask?> { + val shouldHaveOutcome = when (outcomes.size) { + 0 -> error("Must provide 1 or more expected task outcome, but received none") + 1 -> "should have outcome ${outcomes.first().name}" + else -> "should have any outcome of ${outcomes.joinToString()}" + } + + return neverNullMatcher { value -> + MatcherResult( + value.outcome in outcomes, + { "Task ${value.path} $shouldHaveOutcome, but was ${value.outcome}" }, + { "Task ${value.path} $shouldHaveOutcome, but was ${value.outcome}" }, + ) + } +} + +fun BuildResult.shouldHaveTaskWithOutcome(taskPath: String, outcome: TaskOutcome) { + this shouldHaveRunTask taskPath shouldHaveOutcome outcome +} + + +fun BuildResult.shouldHaveTaskWithAnyOutcome(taskPath: String, outcomes: Collection<TaskOutcome>) { + this shouldHaveRunTask taskPath shouldHaveAnyOutcome outcomes +} + +fun BuildResult.shouldHaveTasksWithOutcome( + vararg taskPathToExpectedOutcome: Pair<String, TaskOutcome> +) { + assertSoftly { + taskPathToExpectedOutcome.forEach { (taskPath, outcome) -> + shouldHaveTaskWithOutcome(taskPath, outcome) + } + } +} + +fun BuildResult.shouldHaveTasksWithAnyOutcome( + vararg taskPathToExpectedOutcome: Pair<String, Collection<TaskOutcome>> +) { + assertSoftly { + taskPathToExpectedOutcome.forEach { (taskPath, outcomes) -> + shouldHaveTaskWithAnyOutcome(taskPath, outcomes) + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestStringMatchers.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestStringMatchers.kt new file mode 100644 index 00000000..58bbe768 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestStringMatchers.kt @@ -0,0 +1,65 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import io.kotest.assertions.print.print +import io.kotest.matchers.MatcherResult +import io.kotest.matchers.neverNullMatcher +import io.kotest.matchers.should +import io.kotest.matchers.shouldNot + + +infix fun String?.shouldContainAll(substrings: Iterable<String>): String? { + this should containAll(substrings) + return this +} + +infix fun String?.shouldNotContainAll(substrings: Iterable<String>): String? { + this shouldNot containAll(substrings) + return this +} + +fun String?.shouldContainAll(vararg substrings: String): String? { + this should containAll(substrings.asList()) + return this +} + +fun String?.shouldNotContainAll(vararg substrings: String): String? { + this shouldNot containAll(substrings.asList()) + return this +} + +private fun containAll(substrings: Iterable<String>) = + neverNullMatcher<String> { value -> + MatcherResult( + substrings.all { it in value }, + { "${value.print().value} should include substrings ${substrings.print().value}" }, + { "${value.print().value} should not include substrings ${substrings.print().value}" }) + } + + +infix fun String?.shouldContainAnyOf(substrings: Iterable<String>): String? { + this should containAnyOf(substrings) + return this +} + +infix fun String?.shouldNotContainAnyOf(substrings: Iterable<String>): String? { + this shouldNot containAnyOf(substrings) + return this +} + +fun String?.shouldContainAnyOf(vararg substrings: String): String? { + this should containAnyOf(substrings.asList()) + return this +} + +fun String?.shouldNotContainAnyOf(vararg substrings: String): String? { + this shouldNot containAnyOf(substrings.asList()) + return this +} + +private fun containAnyOf(substrings: Iterable<String>) = + neverNullMatcher<String> { value -> + MatcherResult( + substrings.any { it in value }, + { "${value.print().value} should include any of these substrings ${substrings.print().value}" }, + { "${value.print().value} should not include any of these substrings ${substrings.print().value}" }) + } diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/samWithReceiverWorkarounds.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/samWithReceiverWorkarounds.kt new file mode 100644 index 00000000..62cd5860 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/samWithReceiverWorkarounds.kt @@ -0,0 +1,77 @@ +@file:Suppress("FunctionName") + +package org.jetbrains.dokka.dokkatoo.utils + +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaPackageOptionsSpec +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceLinkSpec +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceSetSpec +import org.gradle.api.DomainObjectCollection +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.NamedDomainObjectProvider +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.DependencySet + + +/** + * Workarounds because `SamWithReceiver` not working in test sources + * https://youtrack.jetbrains.com/issue/KTIJ-14684 + * + * The `SamWithReceiver` plugin is automatically applied by the `kotlin-dsl` plugin. + * It converts all [org.gradle.api.Action] so the parameter is the receiver: + * + * ``` + * // with SamWithReceiver ✅ + * tasks.configureEach { + * val task: Task = this + * } + * + * // without SamWithReceiver + * tasks.configureEach { it -> + * val task: Task = it + * } + * ``` + * + * This is nice because it means that the Dokka Gradle Plugin more closely matches `build.gradle.kts` files. + * + * However, [IntelliJ is bugged](https://youtrack.jetbrains.com/issue/KTIJ-14684) and doesn't + * acknowledge that `SamWithReceiver` has been applied in test sources. The code works and compiles, + * but IntelliJ shows red errors. + * + * These functions are workarounds, and should be removed ASAP. + */ +@Suppress("unused") +private object Explain + +fun Project.subprojects_(configure: Project.() -> Unit) = + subprojects(configure) + +@Suppress("SpellCheckingInspection") +fun Project.allprojects_(configure: Project.() -> Unit) = + allprojects(configure) + +fun <T> DomainObjectCollection<T>.configureEach_(configure: T.() -> Unit) = + configureEach(configure) + +fun <T> DomainObjectCollection<T>.all_(configure: T.() -> Unit) = + all(configure) + +fun Configuration.withDependencies_(action: DependencySet.() -> Unit): Configuration = + withDependencies(action) + +fun <T> NamedDomainObjectContainer<T>.create_(name: String, configure: T.() -> Unit = {}): T = + create(name, configure) + +fun <T> NamedDomainObjectContainer<T>.register_( + name: String, + configure: T.() -> Unit +): NamedDomainObjectProvider<T> = + register(name, configure) + +fun DokkaSourceSetSpec.sourceLink_( + action: DokkaSourceLinkSpec.() -> Unit +): Unit = sourceLink(action) + +fun DokkaSourceSetSpec.perPackageOption_( + action: DokkaPackageOptionsSpec.() -> Unit +): Unit = perPackageOption(action) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/stringUtils.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/stringUtils.kt new file mode 100644 index 00000000..eb8777e7 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/stringUtils.kt @@ -0,0 +1,21 @@ +package org.jetbrains.dokka.dokkatoo.utils + + +fun String.splitToPair(delimiter: String): Pair<String, String> = + substringBefore(delimiter) to substringAfter(delimiter) + + +/** Title case the first char of a string */ +fun String.uppercaseFirstChar(): String = mapFirstChar(Character::toTitleCase) + + +private inline fun String.mapFirstChar( + transform: (Char) -> Char +): String = if (isNotEmpty()) transform(this[0]) + substring(1) else this + + +/** Split a string into lines, sort the lines, and re-join them (using [separator]). */ +fun String.sortLines(separator: String = "\n") = + lines() + .sorted() + .joinToString(separator) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/systemVariableProviders.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/systemVariableProviders.kt new file mode 100644 index 00000000..b15b3edb --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/systemVariableProviders.kt @@ -0,0 +1,40 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import kotlin.properties.ReadOnlyProperty + +// Utilities for fetching System Properties and Environment Variables via delegated properties + + +internal fun optionalSystemProperty() = optionalSystemProperty { it } + +internal fun <T : Any> optionalSystemProperty( + convert: (String) -> T? +): ReadOnlyProperty<Any, T?> = + ReadOnlyProperty { _, property -> + val value = System.getProperty(property.name) + if (value != null) convert(value) else null + } + + +internal fun systemProperty() = systemProperty { it } + +internal fun <T> systemProperty( + convert: (String) -> T +): ReadOnlyProperty<Any, T> = + ReadOnlyProperty { _, property -> + val value = requireNotNull(System.getProperty(property.name)) { + "system property ${property.name} is unavailable" + } + convert(value) + } + + +internal fun optionalEnvironmentVariable() = optionalEnvironmentVariable { it } + +internal fun <T : Any> optionalEnvironmentVariable( + convert: (String) -> T? +): ReadOnlyProperty<Any, T?> = + ReadOnlyProperty { _, property -> + val value = System.getenv(property.name) + if (value != null) convert(value) else null + } diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/text.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/text.kt new file mode 100644 index 00000000..ce0ebd9d --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/text.kt @@ -0,0 +1,24 @@ +package org.jetbrains.dokka.dokkatoo.utils + +/** Replace all newlines with `\n`, so the String can be used in assertions cross-platform */ +fun String.invariantNewlines(): String = + lines().joinToString("\n") + +fun Pair<String, String>.sideBySide( + buffer: String = " ", +): String { + val (left, right) = this + + val leftLines = left.lines() + val rightLines = right.lines() + + val maxLeftWidth = leftLines.maxOf { it.length } + + return (0..maxOf(leftLines.size, rightLines.size)).joinToString("\n") { i -> + + val leftLine = (leftLines.getOrNull(i) ?: "").padEnd(maxLeftWidth, ' ') + val rightLine = rightLines.getOrNull(i) ?: "" + + leftLine + buffer + rightLine + } +} |