Skip to content

Add DackkaPlugin tests #4508

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
29 changes: 21 additions & 8 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,30 @@ gradlePlugin {
}
}

tasks.register("testPlugins") {
project.ext["enablePluginTests"] = "true"
dependsOn("test")
}

tasks.register<Test>("dackkaPluginTests") {
systemProperty("rebuildDackkaOutput", "true")
include("com/google/firebase/gradle/plugins/DackkaPluginTests.class")
}

tasks.register("updateDackkaTestsOutput") {
project.ext["enablePluginTests"] = "true"
dependsOn("dackkaPluginTests")
}

tasks.test.configure {
onlyIf {
project.hasProperty("enablePluginTests")
}
}

tasks.withType<Test> {
testLogging {
// Make sure output from standard out or error is shown in Gradle output.
showStandardStreams = true
}
val enablePluginTests: String? by rootProject
enabled = enablePluginTests == "true"
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "11"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,135 @@

package com.google.firebase.gradle.plugins

import java.io.BufferedReader
import java.io.File

/** Replaces all matching substrings with an empty string (nothing) */
fun String.remove(regex: Regex) = replace(regex, "")

/** Replaces all matching substrings with an empty string (nothing) */
fun String.remove(str: String) = replace(str, "")

/** The value of this string or an empty string if null. */
fun String?.orEmpty() = this ?: ""

/**
* Represents a Diff in the context of two inputs.
*
* Every subclass overrides toString() to provide output similar to that of the UNIX diff command.
*
* @see FileChanged
* @see ContentChanged
* @see LineChanged
*/
interface DiffEntry

/** When a file is either added or removed. */
data class FileChanged(val file: File, val added: Boolean) : DiffEntry {
override fun toString() = "${if (added) "+++" else "---"} ${file.name}"
}

/** When the contents of a file are changed. */
data class ContentChanged(val file: File, val lines: List<LineChanged>) : DiffEntry {
override fun toString() =
"""
== [ ${file.path} ] ==
${lines.joinToString("\n")}

"""
.trimIndent()
}

/**
* Represents an individual line change, providing the original and new strings.
*
* This exists to provide a type-safe way of organizing data, while also overriding toString() to
* match the output of the UNIX diff command.
*
* @see ContentChanged
*/
data class LineChanged(val from: String, val to: String) {
override fun toString() =
"""
--- ${from.trim()}
+++ ${to.trim()}
"""
.trimEnd()
}

/**
* Recursively compares two directories and returns their diff.
*
* You should call [File.diff] instead for individual files and non recursive needs.
*
* @throws RuntimeException when called from or on a non directory file.
*/
fun File.recursiveDiff(newDirectory: File): List<DiffEntry> {
if (!isDirectory) throw RuntimeException("Called on a non directory file: $path")
if (!newDirectory.isDirectory)
throw RuntimeException("Called for a non directory file: ${newDirectory.path}")

val changedFiles =
walkTopDown().mapNotNull {
val relativePath = it.toRelativeString(this)
val newFile = File("${newDirectory.path}/$relativePath")

if (!newFile.exists()) {
FileChanged(it, false)
} else {
it.diff(newFile)
}
}

val addedFiles =
newDirectory.walkTopDown().mapNotNull {
val relativePath = it.toRelativeString(newDirectory)
val oldFile = File("$path/$relativePath")

FileChanged(it, true).takeUnless { oldFile.exists() }
}

return (changedFiles + addedFiles).toList()
}

/**
* Compares two files and returns their diff.
*
* While this can handle comparing directories, it will NOT recursively compare them. If that is the
* behavior you are looking for, you should use [File.recursiveDiff] instead.
*
* @see [DiffEntry]
*/
fun File.diff(otherFile: File): DiffEntry? {
if (isDirectory || otherFile.isDirectory) {
return FileChanged(this, false).takeUnless { isDirectory && otherFile.isDirectory }
}

val otherFileReader = otherFile.bufferedReader()

val changedLines =
bufferedReader().useLines {
it
.mapNotNull {
LineChanged(it, otherFileReader.safeReadLine().orEmpty()).takeIf { it.from != it.to }
}
.toList()
}

val addedLines = otherFileReader.useLines { it.map { LineChanged("", it) }.toList() }

val diff = changedLines + addedLines

return ContentChanged(otherFile, diff).takeUnless { diff.isEmpty() }
}

/**
* A safe variant of [BufferedReader.readLine] that will catch [NullPointerException] and return
* null instead.
*/
fun BufferedReader.safeReadLine(): String? =
try {
readLine()
} catch (_: NullPointerException) {
null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.firebase.gradle.plugins

import com.google.common.truth.Truth.assertThat
import java.io.File
import org.gradle.testkit.runner.GradleRunner
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.TemporaryFolder

/**
* Ensures the current state of Dackka outputs what we expect from it.
*
* We do this by running the [DackkaPlugin] against a small project fixture and comparing the output
* to a pre-compiled output that represents what we expect Dackka to generate.
*
* ## Resources
*
* The resources for the tests are stored under `src/test/resources/dackka-plugin-tests` with two
* sub directories that will be explained below.
*
* ### project
*
* Directory containing a small gradle project, with various sub-projects. These exist to test edge
* case scenarios in our doc generation- to ensure any changes do not break previous fixes.
*
* ### output
*
* Directory containing the **expected** output from running the [DackkaPlugin] against the
* predefined project fixture.
*
* ## Updating Output
*
* Should the time come where Dackka behavior completely changes, the format changes, or maybe we
* find a new edge-case; the output directory that gets compared during testing time should be
* updated.
*
* Since the tests run on a project fixture, the easiest way to update the output is to add a clause
* in the tests itself to overwrite the previous output. This behavior is not preferred, and should
* be evaluated at a later date. You can see this in [DackkaPluginTests.updateDocs].
*
* You can trigger this function to run one of two ways; passing the `rebuildDackkaOutput` property
* to gradle during the build, or calling the `updateDackkaTestsPlugin` task.
*
* Example:
* ```
* ./gradlew -b buildSrc/build.gradle.kts updateDackkaTestsPlugin
* ```
*/
class DackkaPluginTests {

companion object {
@ClassRule @JvmField val testProjectDir = TemporaryFolder()

private val resourcesDirectory = File("src/test/resources/dackka-plugin-tests/")
private val outputDirectory = File("$resourcesDirectory/output/firebase-kotlindoc")

@BeforeClass
@JvmStatic
fun setup() {
copyFixtureToTempDirectory()
buildDocs()
if (System.getProperty("rebuildDackkaOutput") == "true") {
updateDocs()
}
}

/**
* Updates the current docs and output to match an updated source.
*
* Unfortunately, we need GradleRunner to be able to do this automatically, and this was the
* cleanest way I could accomplish such. I'm sure this can be fixed down the line should we
* expand buildSrc into its own composite build.
*/
private fun updateDocs() {
removeOldOutputFiles()

val docDirectory = File("${testProjectDir.root}/build/firebase-kotlindoc")

docDirectory.copyRecursively(outputDirectory, true)
}

private fun removeOldOutputFiles() {
outputDirectory.deleteRecursively()
}

private fun copyFixtureToTempDirectory() {
val project = File("$resourcesDirectory/project")
project.copyRecursively(testProjectDir.root)
}

private fun buildDocs() {
GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withPluginClasspath()
.withArguments("kotlindoc")
.build()
}
}

@Test
fun `Transforms correctly`() {
val buildDirectory = File("${testProjectDir.root}/build")
val docDirectory = File("$buildDirectory/firebase-kotlindoc")

val diff = docDirectory.recursiveDiff(outputDirectory)
val diffAsString = diff.joinToString("\n")

println(diffAsString)
assertThat(diffAsString).isEmpty()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ class LicenseResolverPluginTests {
}

thirdPartyLicenses {
add 'customLib1', "${File("src/test/fixtures/license.txt").absolutePath}"
add 'customLib1', "${File("src/test/resources/license.txt").absolutePath}"
}
"""
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
toc:
- title: "com.example"
path: "/reference/com/example/package-summary.html"

section:
- title: "Interfaces"

section:
- title: "Phrase"
path: "/reference/com/example/Phrase.html"

- title: "Classes"

section:
- title: "CheckTheReleaseSpreadsheet"
path: "/reference/com/example/CheckTheReleaseSpreadsheet.html"
- title: "Goodbye"
path: "/reference/com/example/Goodbye.html"
- title: "Hello"
path: "/reference/com/example/Hello.html"


Loading