Skip to content

Commit

Permalink
Verify links and anchors in the documentation (#1911)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vampire authored Mar 20, 2024
1 parent 9f5220c commit 1c7ea7d
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 51 deletions.
12 changes: 12 additions & 0 deletions build-logic/base/base.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ gradlePlugin {
}
}
}

dependencies {
implementation('org.ccil.cowan.tagsoup:tagsoup:1.2.1')
}

testing {
suites {
test {
useSpock('2.3-groovy-3.0')
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package org.spockframework.gradle

import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.xml.XmlSlurper
import org.ccil.cowan.tagsoup.Parser

@CompileStatic
class AsciiDocLinkVerifier {
static verifyAnchorlessCrossDocumentLinks(Iterable<File> sourceFiles) {
sourceFiles
.collectMany { file ->
if ((file.name == 'index.adoc') || (!file.name.endsWith('.adoc'))) {
return []
}

return (file.text =~ /<<([^>#]+\.adoc)#,[^>]+>>/)
.collect { List it -> it[1] }
.unique()
.collect {
"$file.name contains a cross-document link to $it without anchor, this will break in one-page variant"
}
}
.tap {
if (it) {
throw new IllegalArgumentException(it.join('\n'))
}
}
}

@CompileDynamic
static verifyLinksAndAnchors(Iterable<File> outputFiles) {
outputFiles
.collectMany { file ->
if (!file.name.endsWith('.html')) {
return []
}

def xmlSlurper = new XmlSlurper(new Parser())

def subject = xmlSlurper.parse(file)

// collect all relative link targets
def relativeLinkTargets = subject
.'**'
.findAll { it.name() == 'a' }
*.@href
*.text()
.collect { URI.create(it) }
.findAll {
!it.scheme &&
!it.authority &&
!it.userInfo &&
!it.host &&
it.port == -1
}

// verify there are no dead links in the generated docs
def result = relativeLinkTargets
.findAll { it.path }
*.path
.findAll { !new File(file.parentFile, it).file }
.collect { "$file.name contains a dead link to $it" }

// verify there are no dead cross-document anchors in the generated docs
result.addAll(
relativeLinkTargets
.findAll { it.path }
.collect { linkTarget ->
def linkTargetFile = new File(file.parentFile, linkTarget.path)
if (!linkTargetFile.file || !linkTarget.fragment) {
return
}
if (
!xmlSlurper
.parse(linkTargetFile)
.'**'
.find { it.@id == linkTarget.fragment }
) {
return "$file.name contains a dead anchor to $linkTarget"
}
}
.findAll()
)

// verify there are no dead in-document anchors in the generated docs
result.addAll(
relativeLinkTargets
.findAll { !it.path }
*.fragment
.findAll { fragment -> !subject.'**'.find { it.@id == fragment } }
.collect { "$file.name contains a dead anchor to $it" }
.findAll()
)

// verify there are no duplicate anchors in the generated docs
return result + subject
.'**'
*.@id
*.text()
.findAll()
.groupBy()
.findAll { key, value -> value.size() > 1 }
*.key
.collect { "$file.name contains multiple anchors with the name '$it'" }
}
.tap {
if (it) {
throw new IllegalArgumentException(it.join('\n'))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.spockframework.gradle


import spock.lang.Specification
import spock.lang.TempDir
import spock.util.io.FileSystemFixture

import static java.nio.file.Files.list
import static org.spockframework.gradle.AsciiDocLinkVerifier.verifyAnchorlessCrossDocumentLinks
import static org.spockframework.gradle.AsciiDocLinkVerifier.verifyLinksAndAnchors

class AsciiDocLinkVerifierTest extends Specification {
@TempDir
FileSystemFixture tempDir

def "anchorless cross document links are detected"() {
given:
tempDir.create {
file('foo.adoc') << '''
= Foo
<<bar.adoc#,Bar>>
'''.stripIndent(true)

file('bar.adoc') << '''
= Bar
<<foo.adoc#,Foo>>
'''.stripIndent(true)
}

when:
verifyAnchorlessCrossDocumentLinks(list(tempDir.currentPath).map { it.toFile() }.toList())

then:
IllegalArgumentException e = thrown()
e.message == '''
bar.adoc contains a cross-document link to foo.adoc without anchor, this will break in one-page variant
foo.adoc contains a cross-document link to bar.adoc without anchor, this will break in one-page variant
'''.stripIndent(true).trim()
}

def "anchorless cross document links are accepted in index.adoc"() {
given:
tempDir.create {
file('foo.adoc') << '''
= Foo
'''.stripIndent(true)

file('index.adoc') << '''
= Index
<<foo.adoc#,Foo>>
'''.stripIndent(true)
}

when:
verifyAnchorlessCrossDocumentLinks(list(tempDir.currentPath).map { it.toFile() }.toList())

then:
noExceptionThrown()
}

def "problems with links and anchors are detected"() {
given:
tempDir.create {
file('foo.html') << '''
<a href="bar.html"/>
<a href="baz.html#baz"/>
<a href="#foo"/>
<div id="foo1"/>
<div id="foo1"/>
'''.stripIndent(true)

file('baz.html') << '''
'''.stripIndent(true)
}

when:
verifyLinksAndAnchors(list(tempDir.currentPath).map { it.toFile() }.toList())

then:
IllegalArgumentException e = thrown()
e.message == '''
foo.html contains a dead link to bar.html
foo.html contains a dead anchor to baz.html#baz
foo.html contains a dead anchor to foo
foo.html contains multiple anchors with the name 'foo1'
'''.stripIndent(true).trim()
}
}
23 changes: 6 additions & 17 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import static groovy.io.FileType.FILES
import static org.spockframework.gradle.AsciiDocLinkVerifier.verifyAnchorlessCrossDocumentLinks
import static org.spockframework.gradle.AsciiDocLinkVerifier.verifyLinksAndAnchors

plugins {
id "org.spockframework.base" apply false
id "base"
id "org.asciidoctor.jvm.convert"
id "jacoco"
Expand Down Expand Up @@ -372,6 +374,7 @@ dependencies {

asciidoctorj {
version = libs.versions.asciidoctorj
fatalWarnings(missingIncludes())
modules {
diagram.use()
}
Expand All @@ -392,22 +395,8 @@ tasks.named("asciidoctor") {
inputs.dir file("spock-spring/src/test/resources/org/spockframework/spring/docs")
inputs.dir file("spock-spring/boot2-test/src/test/groovy/org/spockframework/boot2")

doFirst {
def errors = []
sourceDir.eachFileRecurse(FILES) { file ->
if (file.name =~ /(?<!^index)\.adoc$/) {
(file.text =~ /<<([^>#]+\.adoc)#,[^>]+>>/)
.collect { it[1] }
.unique()
.each {
errors << "$file.name contains a cross-document link to $it without anchor, this will break in one-page variant"
}
}
}
if (errors) {
throw new IllegalArgumentException(errors.join('\n'))
}
}
doFirst { verifyAnchorlessCrossDocumentLinks(sourceFileTree) }
doLast { verifyLinksAndAnchors(outputs.files.asFileTree) }
}

nexusPublishing {
Expand Down
2 changes: 1 addition & 1 deletion docs/data_driven_testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ string, this value is used as unroll-pattern. This could for example be set to
- `#iterationName` if you make sure that in each data-driven feature you also set
a data variable called `iterationName` that is then used for reporting

[[unroll_tokens]]
[[unroll-tokens]]
=== Special Tokens

This is the complete list of special tokens:
Expand Down
16 changes: 8 additions & 8 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ followed by a predicate and an optional reason:
include::{sourcedir}/extension/IgnoreIfDocSpec.groovy[tag=example-a]
----

[[precondition_context]]
[[precondition-context]]
==== Precondition Context
To make predicates easier to read and write, the following properties are available inside the closure:

Expand Down Expand Up @@ -281,7 +281,7 @@ failure are skipped.
`Stepwise` does not override the behaviour of annotations such as `Ignore`, `IgnoreRest`, and `IgnoreIf`, so care
should be taken when ignoring feature methods in spec classes annotated with `Stepwise`.

NOTE: This will also set the execution mode to `SAME_THREAD`, see <<parallel-execution.adoc#parallel-execution, Parallel Execution>> for more information.
NOTE: This will also set the execution mode to `SAME_THREAD`, see <<parallel_execution.adoc#parallel-execution, Parallel Execution>> for more information.

Since Spock 2.2, `Stepwise` can be applied to data-driven feature methods, having the effect of executing them sequentially (even if concurrent test mode is active) and to skip subsequent iterations if one iteration fails:

Expand Down Expand Up @@ -446,7 +446,7 @@ methods.
To use multiple categories, you can either give multiple categories to the `value` attribute
of the annotation or you can apply the annotation multiple times to the same target.

NOTE: This will also set the execution mode to `SAME_THREAD` if applied on a `Specification`, see <<parallel-execution.adoc#parallel-execution, Parallel Execution>> for more information.
NOTE: This will also set the execution mode to `SAME_THREAD` if applied on a `Specification`, see <<parallel_execution.adoc#parallel-execution, Parallel Execution>> for more information.

=== ConfineMetaClassChanges

Expand All @@ -470,7 +470,7 @@ CAUTION: Temporarily changing the meta classes is only safe when specs are
run in a single thread per JVM. Even though many execution environments do limit themselves to one thread
per JVM, keep in mind that Spock cannot enforce this.

NOTE: This will acquire a `READ_WRITE` lock for `Resources.META_CLASS_REGISTRY`, see <<parallel-execution.adoc#parallel-execution, Parallel Execution>> for more information.
NOTE: This will acquire a `READ_WRITE` lock for `Resources.META_CLASS_REGISTRY`, see <<parallel_execution.adoc#parallel-execution, Parallel Execution>> for more information.

=== RestoreSystemProperties
Saves system properties before the annotated feature method (including any setup and cleanup methods) gets run,
Expand All @@ -494,7 +494,7 @@ CAUTION: Temporarily changing the values of system properties is only safe when
run in a single thread per JVM. Even though many execution environments do limit themselves to one thread
per JVM, keep in mind that Spock cannot enforce this.

NOTE: This will acquire a `READ_WRITE` lock for `Resources.SYSTEM_PROPERTIES`, see <<parallel-execution.adoc#parallel-execution, Parallel Execution>> for more information.
NOTE: This will acquire a `READ_WRITE` lock for `Resources.SYSTEM_PROPERTIES`, see <<parallel_execution.adoc#parallel-execution, Parallel Execution>> for more information.

=== AutoAttach

Expand All @@ -504,7 +504,7 @@ Use this, if there is no direct framework support available.
To create detached mocks, see <<interaction_based_testing.adoc#DetachedMockFactory,Create Mocks Outside Specifications>>

Spring and Guice dependency injection is automatically handled by the
<<module_spring.adoc#_spring_module,Spring Module>> and <<modules.adoc#_guice_module,Guice Module>>, respectively.
<<module_spring.adoc#spring-module,Spring Module>> and <<modules.adoc#_guice_module,Guice Module>>, respectively.

=== AutoCleanup

Expand Down Expand Up @@ -532,7 +532,7 @@ annotated object. To prevent cleanup exceptions from being reported, override th
def ignoreMyExceptions
----

[[_temp_dir]]
[[temp-dir]]
=== TempDir

In order to generate a temporary directory for test and delete it after test, annotate a member field of type
Expand All @@ -549,7 +549,7 @@ Valid methods are `setup()`, `setupSpec()`, or any feature methods.
include::{sourcedir}/extension/TempDirDocSpec.groovy[tag=example]
----

[[_temp_dir_cleanup]]
[[temp-dir-cleanup]]
==== Cleanup

You can configure the cleanup behavior via `@TempDir(cleanup = <mode>)`.
Expand Down
4 changes: 2 additions & 2 deletions docs/interaction_based_testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1064,8 +1064,8 @@ for the mocked `Class` during execution.
[[global-mocks-parallel-execution]]
==== Global mocks and parallel execution

Creating a global `GroovyMock`/`GroovyStub`/`GroovySpy` when <<parallel-execution.adoc#parallel-execution,parallel execution>> is enabled,
requires that the spec is annotated with <<parallel-execution.adoc#isolated-execution, @Isolated>> or `@ResourceLock(org.spockframework.runtime.model.parallel.Resources.META_CLASS_REGISTRY)`.
Creating a global `GroovyMock`/`GroovyStub`/`GroovySpy` when <<parallel_execution.adoc#parallel-execution,parallel execution>> is enabled,
requires that the spec is annotated with <<parallel_execution.adoc#isolated-execution, @Isolated>> or `@ResourceLock(org.spockframework.runtime.model.parallel.Resources.META_CLASS_REGISTRY)`.
If it is only used for a feature, then it suffices that the feature is annotated with `@ResourceLock(org.spockframework.runtime.model.parallel.Resources.META_CLASS_REGISTRY)`.
The rule of thumb to choose between `@ResourceLock` and `@Isolated`, is to look at how widespread the mocked type is used.
If it is widely used, then `@Isolated` is the safe choice, otherwise `@ResourceLock` may be sufficient.
Expand Down
15 changes: 3 additions & 12 deletions docs/migration_guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ include::include.adoc[]

This page explains incompatible changes between successive versions and provides suggestions on how to deal with them.

[[_migration_guide_2_0]]
[[migration-guide-2-0]]
== 2.0

NOTE: This section only touches on the breaking changes, see the <<release_notes.adoc#_release_notes, Release Notes>> for a full list of changes and new features.
NOTE: This section only touches on the breaking changes, see the <<release_notes.adoc#release-notes, Release Notes>> for a full list of changes and new features.

Spock 2.0 aims to be as compatible as possible for existing code bases, while making the necessary changes to stay a modern test framework.

Expand All @@ -19,7 +19,7 @@ See the https://junit.org/junit5/docs/current/user-guide/#running-tests-build[JU

Support for JUnit 4 has been removed from `spock-core`, you can use the new `spock-junit4` module if you still need JUnit 4 features, such as `@Rule`.

You can replace the `TemporaryFolder` rule with the new built-in `@TempDir` <<extensions.adoc#_temp_dir, extension>>.
You can replace the `TemporaryFolder` rule with the new built-in `@TempDir` <<extensions.adoc#temp-dir, extension>>.

Spock 2.0 also removed the `Sputnik` runner, so if you have used `PowerMockRunnerDelegate` or other things that relied on the runner, you'll have to find other solutions.
Take a look at https://github.com/spockframework/spock/wiki/Third-Party-Extensions[Third-Party-Extensions] for solutions.
Expand Down Expand Up @@ -178,12 +178,3 @@ that answer, closeTo(42, 0.001)
----

A future version of Spock will likely remove the former syntax and strengthen the latter one.









1 change: 1 addition & 0 deletions docs/module_spring.adoc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[[spring-module]]
= Spring Module
include::include.adoc[]

Expand Down
Loading

0 comments on commit 1c7ea7d

Please sign in to comment.