Skip to content
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
package com.github.jengelman.gradle.plugins.shadow

import com.github.jengelman.gradle.plugins.shadow.snippet.CodeSnippetExtractor
import com.github.jengelman.gradle.plugins.shadow.snippet.DslLang
import com.github.jengelman.gradle.plugins.shadow.snippet.SnippetExecutable
import com.github.jengelman.gradle.plugins.shadow.snippet.extractCodeSnippets
import java.nio.file.Path
import kotlin.io.path.createDirectory
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.TestFactory
import org.junit.jupiter.api.Named.named
import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.Arguments.arguments
import org.junit.jupiter.params.provider.MethodSource

class DocCodeSnippetTest {

@TestFactory
fun provideDynamicTests(@TempDir root: Path): List<DynamicTest> {
val langExecutables = DslLang.entries.map { executor -> CodeSnippetExtractor.extract(executor) }
@ParameterizedTest(name = "{0}")
@MethodSource("snippets")
fun test(executable: SnippetExecutable, @TempDir tempDir: Path) {
executable.execute(tempDir)
}

check(langExecutables.sumOf { it.size } > 0) { "No code snippets found." }
check(langExecutables.size == DslLang.entries.size) {
"We must provide build script snippets for all languages."
}
check(langExecutables.map { it.size }.distinct().size == 1) {
"All languages must have the same number of code snippets."
}
private companion object {
@JvmStatic
fun snippets(): List<Arguments> {
val langExecutables = DslLang.entries.map(DslLang::extractCodeSnippets)

return langExecutables.flatten().map {
val dirName = it.displayName.replace(nonAlphanumeric, "_")
it.tempDir = root.resolve(dirName).createDirectory()
DynamicTest.dynamicTest(it.displayName, it)
check(langExecutables.sumOf { it.size } > 0) { "No code snippets found." }
check(langExecutables.map { it.size }.distinct().size == 1) {
"All languages must have the same number of code snippets."
}

return langExecutables.flatten().map { executable ->
arguments(named(executable.displayName, executable))
}
}
}
}

private val nonAlphanumeric = "[^a-zA-Z0-9]".toRegex()
Original file line number Diff line number Diff line change
@@ -1,52 +1,45 @@
package com.github.jengelman.gradle.plugins.shadow.snippet

import com.github.jengelman.gradle.plugins.shadow.DocumentTestBuildConfig.DOCS_DIR
import java.nio.file.Path
import java.util.regex.Pattern
import kotlin.io.path.Path
import kotlin.io.path.name
import kotlin.io.path.readText
import kotlin.io.path.relativeTo
import kotlin.io.path.walk

object CodeSnippetExtractor {
private val docRoot = Path(DOCS_DIR)

private val markdownPaths =
docRoot.walk().filter { it.name.endsWith(".md", ignoreCase = true) }.toList()

fun extract(lang: DslLang): List<SnippetExecutable> {
return markdownPaths.flatMap { path -> createExecutables(lang, path) }
}

private fun createExecutables(lang: DslLang, markdownPath: Path): List<SnippetExecutable> {
val relativeDocPath = markdownPath.relativeTo(docRoot).toString()
return createSnippets(markdownPath.readText(), lang).map { (lineNumber, snippet) ->
SnippetExecutable.create(lang, snippet, "$relativeDocPath:$lineNumber") { cause ->
RuntimeException(
"The error line in the doc is near ${markdownPath.toUri()}:$lineNumber\n\n${cause.message}",
cause,
)
private val docRoot = Path(DOCS_DIR)

fun DslLang.extractCodeSnippets(): List<SnippetExecutable> {
val lang = this
return docRoot
.walk()
.filter { it.name.endsWith(".md", ignoreCase = true) }
.flatMap { path ->
val source = path.readText()
val matcher = Pattern.compile("(?ims) {4}```${lang}\n(.*?)\n {4}```").matcher(source)

buildList {
while (matcher.find()) {
val lineNumber = source.lineNumberAt(matcher.start())
add(
SnippetExecutable.create(
lang = lang,
snippet = matcher.group(1),
testName = "${path.relativeTo(docRoot)}:$lineNumber",
sourceLocation = "${path.toUri()}:$lineNumber",
)
)
}
}
}
}

private fun createSnippets(source: String, lang: DslLang) = buildMap {
val pattern = Pattern.compile("(?ims) {4}```${lang}\n(.*?)\n {4}```")
val matcher = pattern.matcher(source)

while (matcher.find()) {
val line = source.lineNumberAt(matcher.start())
val code = matcher.group(1)
put(line, code)
}
}
.toList()
}

private fun String.lineNumberAt(index: Int): Int {
var line = 1
for (i in 0 until index.coerceAtMost(length)) {
if (this[i] == '\n') line++
}
return line
private fun String.lineNumberAt(index: Int): Int {
var line = 1
for (i in 0 until index.coerceAtMost(length)) {
if (this[i] == '\n') line++
}
return line
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,10 @@ package com.github.jengelman.gradle.plugins.shadow.snippet
class GroovyBuildExecutable(
override val snippet: String,
override val displayName: String,
override val exceptionTransformer: (Throwable) -> Throwable,
) : SnippetExecutable() {

override val lang: DslLang = DslLang.Groovy

override val sourceLocation: String,
) : SnippetExecutable {
override val buildScriptName: String = "build.gradle"

override val pluginsBlock: String =
"""
|plugins {
| id 'java'
| id 'com.gradleup.shadow'
|}
"""
.trimMargin()

override val assembleDependsOn: String =
"""
|tasks.named('assemble') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,10 @@ package com.github.jengelman.gradle.plugins.shadow.snippet
class KotlinBuildExecutable(
override val snippet: String,
override val displayName: String,
override val exceptionTransformer: (Throwable) -> Throwable,
) : SnippetExecutable() {

override val lang: DslLang = DslLang.Kotlin

override val sourceLocation: String,
) : SnippetExecutable {
override val buildScriptName: String = "build.gradle.kts"

override val pluginsBlock: String =
"""
|plugins {
| java
| id("com.gradleup.shadow")
|}
"""
.trimMargin()

override val assembleDependsOn: String =
"""
|tasks.named("assemble") {
Expand Down
Loading