Skip to content

Commit a8cff71

Browse files
committed
Migrate signature tests to the new model
1 parent 9a8f8b6 commit a8cff71

File tree

7 files changed

+195
-312
lines changed

7 files changed

+195
-312
lines changed

scala3doc/test/dotty/dokka/DottyTestRunner.scala

Lines changed: 0 additions & 126 deletions
This file was deleted.

scala3doc/test/dotty/dokka/MultipleFileTest.scala

Lines changed: 0 additions & 92 deletions
This file was deleted.

scala3doc/test/dotty/dokka/ScaladocTest.scala

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import org.jetbrains.dokka.testApi.logger.TestLogger
88
import org.jetbrains.dokka.utilities.DokkaConsoleLogger
99
import org.jetbrains.dokka.DokkaConfiguration
1010
import scala.jdk.CollectionConverters.{ListHasAsScala, SeqHasAsJava}
11-
import org.junit.Test
12-
import org.junit.rules.TemporaryFolder
11+
import org.junit.{Test, Rule}
12+
import org.junit.rules.{TemporaryFolder, ErrorCollector}
1313
import java.io.File
1414

1515
abstract class ScaladocTest(val name: String):
@@ -33,18 +33,23 @@ abstract class ScaladocTest(val name: String):
3333
sourceLinks = Nil
3434
)
3535

36-
private def tastyFiles =
36+
private def tastyFiles =
3737
def collectFiles(dir: File): List[String] = dir.listFiles.toList.flatMap {
3838
case f if f.isDirectory => collectFiles(f)
3939
case f if f.getName endsWith ".tasty" => f.getAbsolutePath :: Nil
4040
case _ => Nil
4141
}
42-
collectFiles(File(s"target/scala-0.27/classes/tests/$name"))
42+
collectFiles(File(s"${BuildInfo.test_testcasesOutputDir}/tests/$name"))
43+
44+
@Rule
45+
def collector = _collector
46+
private val _collector = new ErrorCollector();
47+
def reportError(msg: String) = collector.addError(new AssertionError(msg))
4348

4449
@Test
45-
def executeTest =
50+
def executeTest =
4651
DokkaTestGenerator(
47-
DottyDokkaConfig(DocConfiguration.Standalone(args, tastyFiles)),
52+
DottyDokkaConfig(DocConfiguration.Standalone(args, tastyFiles, Nil)),
4853
TestLogger(DokkaConsoleLogger.INSTANCE),
4954
assertions.asTestMethods,
5055
Nil.asJava
@@ -71,7 +76,7 @@ extension (s: Seq[Assertion]):
7176
import Assertion._
7277
TestMethods(
7378
(context => s.collect { case AfterPluginSetup(fn) => fn(context) }.kUnit),
74-
(validator => s.collect { case DuringValidation(fn) => fn(() => validator) }.kUnit),
79+
(validator => s.collect { case DuringValidation(fn) => fn(() => validator.invoke()) }.kUnit),
7580
(modules => s.collect { case AfterDocumentablesCreation(fn) => fn(modules.asScala.toSeq) }.kUnit),
7681
(modules => s.collect { case AfterPreMergeDocumentablesTransformation(fn) => fn(modules.asScala.toSeq) }.kUnit),
7782
(module => s.collect { case AfterDocumentablesMerge(fn) => fn(module)}.kUnit),
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package dotty.dokka
2+
3+
import scala.io.Source
4+
import scala.jdk.CollectionConverters._
5+
import scala.util.matching.Regex
6+
7+
import org.jetbrains.dokka.pages.{RootPageNode, PageNode, ContentPage, ContentText, ContentNode, ContentComposite}
8+
9+
import dotty.dokka.model.api.Link
10+
11+
private enum Signature:
12+
case Expected(name: String, signature: String)
13+
case Unexpected(name: String)
14+
import Signature._
15+
16+
abstract class SignatureTest(
17+
testName: String,
18+
signatureKinds: Seq[String],
19+
sourceFiles: List[String] = Nil,
20+
ignoreMissingSignatures: Boolean = false
21+
) extends ScaladocTest(testName):
22+
override def assertions = Assertion.AfterPagesTransformation { root =>
23+
val sources = sourceFiles match
24+
case Nil => testName :: Nil
25+
case s => s
26+
27+
val allSignaturesFromSources = sources
28+
.map { file => Source.fromFile(s"${BuildInfo.test_testcasesSourceRoot}/tests/$file.scala") }
29+
.flatMap(signaturesFromSources(_, signatureKinds))
30+
.toList
31+
val expectedFromSources: Map[String, List[String]] = allSignaturesFromSources
32+
.collect { case Expected(name, signature) => name -> signature }
33+
.groupMap(_._1)(_._2)
34+
val unexpectedFromSources: Set[String] = allSignaturesFromSources.collect { case Unexpected(name) => name }.toSet
35+
36+
val actualSignatures: Map[String, Seq[String]] = signaturesFromDocumentation(root).flatMap { signature =>
37+
findName(signature, signatureKinds).map(_ -> signature)
38+
}.groupMap(_._1)(_._2)
39+
40+
val unexpected = unexpectedFromSources.flatMap(actualSignatures.getOrElse(_, Nil))
41+
val expectedButNotFound = expectedFromSources.flatMap {
42+
case (k, v) => findMissingSingatures(v, actualSignatures.getOrElse(k, Nil))
43+
}
44+
45+
val missingReport = Option.when(!ignoreMissingSignatures && !expectedButNotFound.isEmpty)
46+
(s"Not documented signatures:\n${expectedButNotFound.mkString("\n")}")
47+
val unexpectedReport = Option.when(!unexpected.isEmpty)
48+
(s"Unexpectedly documented signatures:\n${unexpected.mkString("\n")}")
49+
val reports = missingReport ++ unexpectedReport
50+
51+
if !reports.isEmpty then
52+
val allSignaturesMessage =
53+
s"""
54+
|All documented signatures:
55+
|${actualSignatures.flatMap(_._2).mkString("\n")}
56+
|
57+
|All expected signatures from source:
58+
|${expectedFromSources.flatMap(_._2).mkString("\n")}
59+
""".stripMargin
60+
val errorMessage = (reports ++ Some(allSignaturesMessage)).mkString(start = "\n", sep = "\n\n", end = "\n")
61+
reportError(errorMessage)
62+
end if
63+
64+
} :: Nil
65+
66+
object SignatureTest {
67+
val classlikeKinds = Seq("class", "object", "trait", "enum") // TODO add docs for packages
68+
val members = Seq("type", "def", "val", "var")
69+
val all = classlikeKinds ++ members
70+
}
71+
72+
// e.g. to remove '(0)' from object IAmACaseObject extends CaseImplementThis/*<-*/(0)/*->*/
73+
private val commentRegex = raw"\/\*<-\*\/[^\/]+\/\*->\*\/".r
74+
private val whitespaceRegex = raw"\s+".r
75+
private val expectedRegex = raw".+//expected: (.+)".r
76+
private val unexpectedRegex = raw"(.+)//unexpected".r
77+
private val identifierRegex = raw"^\s*(`.*`|(?:\w+)(?:_[^\[\(\s]+)|\w+|[^\[\(\s]+)".r
78+
79+
private def findMissingSingatures(expected: Seq[String], actual: Seq[String]): Set[String] =
80+
expected.toSet &~ actual.toSet
81+
82+
extension (s: String):
83+
private def startWithAnyOfThese(c: String*) = c.exists(s.startsWith)
84+
private def compactWhitespaces = whitespaceRegex.replaceAllIn(s, " ")
85+
86+
private def findName(signature: String, kinds: Seq[String]): Option[String] =
87+
for
88+
kindMatch <- kinds.flatMap(k => s"\\b$k\\b".r.findFirstMatchIn(signature)).headOption
89+
afterKind <- Option(kindMatch.after(0)) // to filter out nulls
90+
nameMatch <- identifierRegex.findFirstMatchIn(afterKind)
91+
yield nameMatch.group(1)
92+
93+
private def signaturesFromSources(source: Source, kinds: Seq[String]): Seq[Signature] =
94+
source.getLines.map(_.trim)
95+
.filterNot(_.isEmpty)
96+
.filterNot(_.startWithAnyOfThese("=",":","{","}", "//"))
97+
.toSeq
98+
.flatMap {
99+
case unexpectedRegex(signature) => findName(signature, kinds).map(Unexpected(_))
100+
case expectedRegex(signature) => findName(signature, kinds).map(Expected(_, signature))
101+
case signature =>
102+
findName(signature, kinds).map(Expected(_, commentRegex.replaceAllIn(signature, "").compactWhitespaces))
103+
}
104+
105+
private def signaturesFromDocumentation(root: PageNode): Seq[String] =
106+
def flattenToText(node: ContentNode) : Seq[String] = node match
107+
case t: ContentText => Seq(t.getText)
108+
case c: ContentComposite =>
109+
c.getChildren.asScala.flatMap(flattenToText).toSeq
110+
case l: DocumentableElement =>
111+
(l.annotations ++ Seq(" ") ++ l.modifiers ++ Seq(l.name) ++ l.signature).map {
112+
case s: String => s
113+
case Link(s: String, _) => s
114+
}
115+
case _ => Seq()
116+
117+
def all(p: ContentNode => Boolean)(n: ContentNode): Seq[ContentNode] =
118+
if p(n) then Seq(n) else n.getChildren.asScala.toSeq.flatMap(all(p))
119+
120+
extension (page: PageNode) def allPages: List[PageNode] = page :: page.getChildren.asScala.toList.flatMap(_.allPages)
121+
122+
val nodes = root.allPages
123+
.collect { case p: ContentPage => p }
124+
.flatMap(p => all(_.isInstanceOf[DocumentableElement])(p.getContent))
125+
nodes.map(flattenToText(_).mkString.compactWhitespaces.trim)

0 commit comments

Comments
 (0)