From 9a8f8b661b86175d95b574d2c6c61cea96a4691b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Marks?= Date: Mon, 2 Nov 2020 18:50:47 +0100 Subject: [PATCH 1/5] Introduce wrapper over dokka tests --- scala3doc/test/dotty/dokka/ScaladocTest.scala | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 scala3doc/test/dotty/dokka/ScaladocTest.scala diff --git a/scala3doc/test/dotty/dokka/ScaladocTest.scala b/scala3doc/test/dotty/dokka/ScaladocTest.scala new file mode 100644 index 000000000000..de7f576c6e38 --- /dev/null +++ b/scala3doc/test/dotty/dokka/ScaladocTest.scala @@ -0,0 +1,85 @@ +package dotty.dokka + +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.model.{DModule, WithChildren} +import org.jetbrains.dokka.pages.RootPageNode +import org.jetbrains.dokka.testApi.testRunner.{DokkaTestGenerator, TestMethods} +import org.jetbrains.dokka.testApi.logger.TestLogger +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.DokkaConfiguration +import scala.jdk.CollectionConverters.{ListHasAsScala, SeqHasAsJava} +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +abstract class ScaladocTest(val name: String): + def assertions: Seq[Assertion] + + private def getTempDir() : TemporaryFolder = + val folder = new TemporaryFolder() + folder.create() + folder + + private def args = Args( + name = "test", + tastyRoots = Nil , + classpath = System.getProperty("java.class.path"), + None, + output = getTempDir().getRoot, + projectVersion = "1.0", + projectTitle = None, + projectLogo = None, + defaultSyntax = None, + sourceLinks = Nil + ) + + private def tastyFiles = + def collectFiles(dir: File): List[String] = dir.listFiles.toList.flatMap { + case f if f.isDirectory => collectFiles(f) + case f if f.getName endsWith ".tasty" => f.getAbsolutePath :: Nil + case _ => Nil + } + collectFiles(File(s"target/scala-0.27/classes/tests/$name")) + + @Test + def executeTest = + DokkaTestGenerator( + DottyDokkaConfig(DocConfiguration.Standalone(args, tastyFiles)), + TestLogger(DokkaConsoleLogger.INSTANCE), + assertions.asTestMethods, + Nil.asJava + ).generate() + +end ScaladocTest + +/** + * Those assertions map 1-1 to their dokka counterparts. Some of them may be irrelevant in scala3doc. + */ +enum Assertion: + case AfterPluginSetup(fn: DokkaContext => Unit) + case DuringValidation(fn: (() => Unit) => Unit) + case AfterDocumentablesCreation(fn: Seq[DModule] => Unit) + case AfterPreMergeDocumentablesTransformation(fn: Seq[DModule] => Unit) + case AfterDocumentablesMerge(fn: DModule => Unit) + case AfterDocumentablesTransformation(fn: DModule => Unit) + case AfterPagesGeneration(fn: RootPageNode => Unit) + case AfterPagesTransformation(fn: RootPageNode => Unit) + case AfterRendering(fn: (RootPageNode, DokkaContext) => Unit) + +extension (s: Seq[Assertion]): + def asTestMethods: TestMethods = + import Assertion._ + TestMethods( + (context => s.collect { case AfterPluginSetup(fn) => fn(context) }.kUnit), + (validator => s.collect { case DuringValidation(fn) => fn(() => validator) }.kUnit), + (modules => s.collect { case AfterDocumentablesCreation(fn) => fn(modules.asScala.toSeq) }.kUnit), + (modules => s.collect { case AfterPreMergeDocumentablesTransformation(fn) => fn(modules.asScala.toSeq) }.kUnit), + (module => s.collect { case AfterDocumentablesMerge(fn) => fn(module)}.kUnit), + (module => s.collect { case AfterDocumentablesTransformation(fn) => fn(module) }.kUnit), + (root => s.collect { case AfterPagesGeneration(fn) => fn(root) }.kUnit), + (root => s.collect { case AfterPagesTransformation(fn) => fn(root) }.kUnit), + ((root, context) => s.collect { case AfterRendering(fn) => fn(root, context)}.kUnit) + ) + +extension [T] (s: T): + private def kUnit = kotlin.Unit.INSTANCE From a8cff7187119050fe3ca91b27a54cb5a16206444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Marks?= Date: Mon, 2 Nov 2020 18:51:47 +0100 Subject: [PATCH 2/5] Migrate signature tests to the new model --- .../test/dotty/dokka/DottyTestRunner.scala | 126 ------------------ .../test/dotty/dokka/MultipleFileTest.scala | 92 ------------- scala3doc/test/dotty/dokka/ScaladocTest.scala | 19 ++- .../test/dotty/dokka/SignatureTest.scala | 125 +++++++++++++++++ .../test/dotty/dokka/SignatureTestCases.scala | 58 ++++++++ .../test/dotty/dokka/SignatureTests.scala | 57 -------- .../test/dotty/dokka/SingleFileTest.scala | 30 ----- 7 files changed, 195 insertions(+), 312 deletions(-) delete mode 100644 scala3doc/test/dotty/dokka/DottyTestRunner.scala delete mode 100644 scala3doc/test/dotty/dokka/MultipleFileTest.scala create mode 100644 scala3doc/test/dotty/dokka/SignatureTest.scala create mode 100644 scala3doc/test/dotty/dokka/SignatureTestCases.scala delete mode 100644 scala3doc/test/dotty/dokka/SignatureTests.scala delete mode 100644 scala3doc/test/dotty/dokka/SingleFileTest.scala diff --git a/scala3doc/test/dotty/dokka/DottyTestRunner.scala b/scala3doc/test/dotty/dokka/DottyTestRunner.scala deleted file mode 100644 index a9ad7e92e4e6..000000000000 --- a/scala3doc/test/dotty/dokka/DottyTestRunner.scala +++ /dev/null @@ -1,126 +0,0 @@ -package dotty.dokka -import org.jetbrains.dokka.utilities.{DokkaConsoleLogger, DokkaLogger} -import org.jetbrains.dokka.testApi.logger.TestLogger -import org.jetbrains.dokka.testApi.testRunner._ -import org.jetbrains.dokka.plugability.DokkaPlugin -import java.io.File -import dotty.dokka.{DocConfiguration, DottyDokkaConfig} -import collection.JavaConverters._ -import org.junit.rules.TemporaryFolder -import org.junit.{Test, Rule} -import org.junit.Assert._ -import org.junit.rules.ErrorCollector -import org.jetbrains.dokka.testApi.testRunner.AbstractCoreTest$TestBuilder -import scala.io.Source -import org.jetbrains.dokka.pages._ -import org.jetbrains.dokka.pages.ContentNodesKt -import org.jetbrains.dokka._ -import collection.JavaConverters._ -import scala.math.max -import org.jetbrains.dokka.pages.ContentNodesKt -import dotty.dokka.model.api.Link - -abstract class DottyAbstractCoreTest extends AbstractCoreTest: - private def getTempDir() : TemporaryFolder = - val folder = new TemporaryFolder() - folder.create() - folder - - private def args = Args( - name = "test", - tastyRoots = Nil , - classpath = System.getProperty("java.class.path"), - None, - output = getTempDir().getRoot, - projectVersion = "1.0", - projectTitle = None, - projectLogo = None, - defaultSyntax = None, - sourceLinks = List.empty - ) - - def listPages(tastyDir: String): Seq[ContentPage] = - var signatures: Seq[ContentPage] = Nil - val tests = new AbstractCoreTest$TestBuilder() - - - def getAllContentPages(root: PageNode) : Seq[ContentPage] = root match - case c: ContentPage => Seq(c) ++ c.getChildren.asScala.flatMap(getAllContentPages) - case default => default.getChildren.asScala.toSeq.flatMap(getAllContentPages) - - tests.setPagesTransformationStage { root => - val res = root.getChildren.asScala.flatMap(getAllContentPages) - signatures = res.toSeq - kotlin.Unit.INSTANCE - } - - def listTastyFiles(f: File): Seq[File] = - assertTrue(s"Tasty root dir does not exisits: $f", f.isDirectory()) - val (files, dirs) = f.listFiles().partition(_.isFile) - files.toIndexedSeq.filter(_.getName.endsWith(".tasty")) ++ dirs.flatMap(listTastyFiles) - - val tastyFiles = tastyDir.split(File.pathSeparatorChar).toList.flatMap(p => listTastyFiles(new File(p))).map(_.toString) - - val config = new DottyDokkaConfig(DocConfiguration.Standalone(args, tastyFiles, Nil)) - DokkaTestGenerator( - config, - new TestLogger(DokkaConsoleLogger.INSTANCE), - tests.build(), - JList() - ).generate() - - signatures - - def signaturesFromDocumentation(tastyDir: String): Seq[String] = - def flattenToText(node: ContentNode) : Seq[String] = node match - case t: ContentText => Seq(t.getText) - case c: ContentComposite => - c.getChildren.asScala.flatMap(flattenToText).toSeq - case l: DocumentableElement => - (l.annotations ++ Seq(" ") ++ l.modifiers ++ Seq(l.name) ++ l.signature).map { - case s: String => s - case Link(s: String, _) => s - } - case _ => Seq() - - def all(p: ContentNode => Boolean)(n: ContentNode): Seq[ContentNode] = - if p(n) then Seq(n) else n.getChildren.asScala.toSeq.flatMap(all(p)) - - - val pages = listPages(tastyDir) - val nodes = pages.flatMap(p => all(_.isInstanceOf[DocumentableElement])(p.getContent)) - nodes.map(flattenToText(_).mkString.trim) - - def signaturesFromSource(s: Source): SignaturesFromSource = - val ExpectedRegex = ".+//expected: (.+)".r - val UnexpectedRegex = "(.+)//unexpected".r - - // e.g. to remove '(0)' from object IAmACaseObject extends CaseImplementThis/*<-*/(0)/*->*/ - val CommentRegexp = """\/\*<-\*\/[^\/]+\/\*->\*\/""" - - extension (s: String) def doesntStartWithAnyOfThese(c: Char*) = c.forall(char => !s.startsWith(char.toString)) - val lines = s.getLines().map(_.trim).toList - .filter(_.doesntStartWithAnyOfThese('=',':','{','}')) - .filterNot(_.trim.isEmpty) - .filterNot(_.startsWith("//")) - - val expectedSignatures = lines.flatMap { - case UnexpectedRegex(_) => None - case ExpectedRegex(signature) => Some(signature) - case other => - Some(other.replaceAll(CommentRegexp, "").replaceAll(" +", " ")) - } - - val unexpectedSignatures = lines.collect { - case UnexpectedRegex(signature) => signature.trim - } - - SignaturesFromSource(expectedSignatures, unexpectedSignatures) - - val _collector = new ErrorCollector(); - @Rule - def collector = _collector - def reportError(msg: String) = collector.addError(new AssertionError(msg)) - - -case class SignaturesFromSource(expected: Seq[String], unexpected: Seq[String]) diff --git a/scala3doc/test/dotty/dokka/MultipleFileTest.scala b/scala3doc/test/dotty/dokka/MultipleFileTest.scala deleted file mode 100644 index 84c06183e681..000000000000 --- a/scala3doc/test/dotty/dokka/MultipleFileTest.scala +++ /dev/null @@ -1,92 +0,0 @@ -package dotty.dokka - -import org.junit.{Test, Rule} -import org.junit.Assert._ -import org.junit.rules.ErrorCollector -import org.jetbrains.dokka.testApi.testRunner.AbstractCoreTest$TestBuilder -import scala.io.Source -import org.jetbrains.dokka.pages._ -import org.jetbrains.dokka.pages.ContentNodesKt -import org.jetbrains.dokka._ -import scala.jdk.CollectionConverters._ -import scala.math.max - -object MultipleFileTest{ - val classlikeKinds = Seq("class", "object", "trait") // TODO add docs for packages - val members = Seq("type", "def", "val", "var") - val all = classlikeKinds ++ members -} - -abstract class MultipleFileTest(val sourceFiles: List[String], val tastyFolders: List[String], signatureKinds: Seq[String], ignoreUndocumentedSignatures: Boolean = false -) extends DottyAbstractCoreTest: - private val _collector = new ErrorCollector(); - - // This should work correctly except for names in backticks and operator names containing a colon - def extractSymbolName(signature: String) = - val Pattern = s"""(?s).*(?:${signatureKinds.mkString("|")}) ([^\\[(: \\n\\t]+).*""".r - signature match { - case Pattern(name) => name - case x => "NULL" - } - - def matchSignature(s: String, signatureList: List[String]): Seq[String] = - val symbolName = extractSymbolName(s) - val candidates = signatureList.filter(extractSymbolName(_) == symbolName) - - candidates.filter(_ == s) match { - case Nil => - val candidateMsg = - if candidates.isEmpty then s"No candidate found for symbol name $symbolName" - else s"Candidates:\n${candidates.mkString("\n")}\n" - - //reportError(s"No match for:\n$s\n$candidateMsg") All test would fail because of documented inherited methods - //println(s"No match for:\n$s\n$candidateMsg") - Nil - case matching => - matching - } - - @Test - def testSignatures(): Unit = - def cleanup(s: String) = s.replace("\n", " ").replaceAll(" +", " ") - - val allFromSource = sourceFiles.map{ file => - val all = signaturesFromSource(Source.fromFile(s"${BuildInfo.test_testcasesSourceRoot}/tests/$file.scala")) - (all.expected, all.unexpected) - } - - val expectedFromSource = allFromSource.map(_._1).flatten.filter(extractSymbolName(_) != "NULL").map(cleanup) - val unexpectedFromSource = allFromSource.map(_._2).flatten.filter(extractSymbolName(_) != "NULL").map(cleanup) - val unexpectedSignatureSymbolNames = unexpectedFromSource.map(extractSymbolName) - - val allFromDocumentation = tastyFolders.flatMap(folder => signaturesFromDocumentation(s"${BuildInfo.test_testcasesOutputDir}/tests/$folder")) - val fromDocumentation = allFromDocumentation.filter(extractSymbolName(_) != "NULL").map(cleanup) - - val documentedSignatures = fromDocumentation.flatMap(matchSignature(_, expectedFromSource)).toSet - val missingSignatures = expectedFromSource.filterNot(documentedSignatures.contains) - - val unexpectedSignatures = - fromDocumentation.filter(s => unexpectedSignatureSymbolNames.contains(extractSymbolName(s))).toSet - - val reportMissingSignatures = !ignoreUndocumentedSignatures && missingSignatures.nonEmpty - val reportUnexpectedSignatures = unexpectedSignatures.nonEmpty - - if reportMissingSignatures || reportUnexpectedSignatures then - val missingSignaturesMessage = Option.when(reportMissingSignatures) - (s"Not documented signatures:\n${missingSignatures.mkString("\n")}") - - val unexpectedSignaturesMessage = Option.when(reportUnexpectedSignatures) - (s"Unexpectedly documented signatures:\n${unexpectedSignatures.mkString("\n")}") - - val allSignaturesMessage = - s""" - |All documented signatures: - |${documentedSignatures.mkString("\n")} - | - |All expected signatures from source: - |${expectedFromSource.mkString("\n")} - """.stripMargin - - val errorMessages = missingSignaturesMessage ++ unexpectedSignaturesMessage ++ Some(allSignaturesMessage) - - reportError(errorMessages.mkString("\n", "\n\n", "\n")) diff --git a/scala3doc/test/dotty/dokka/ScaladocTest.scala b/scala3doc/test/dotty/dokka/ScaladocTest.scala index de7f576c6e38..1b5b590734e0 100644 --- a/scala3doc/test/dotty/dokka/ScaladocTest.scala +++ b/scala3doc/test/dotty/dokka/ScaladocTest.scala @@ -8,8 +8,8 @@ import org.jetbrains.dokka.testApi.logger.TestLogger import org.jetbrains.dokka.utilities.DokkaConsoleLogger import org.jetbrains.dokka.DokkaConfiguration import scala.jdk.CollectionConverters.{ListHasAsScala, SeqHasAsJava} -import org.junit.Test -import org.junit.rules.TemporaryFolder +import org.junit.{Test, Rule} +import org.junit.rules.{TemporaryFolder, ErrorCollector} import java.io.File abstract class ScaladocTest(val name: String): @@ -33,18 +33,23 @@ abstract class ScaladocTest(val name: String): sourceLinks = Nil ) - private def tastyFiles = + private def tastyFiles = def collectFiles(dir: File): List[String] = dir.listFiles.toList.flatMap { case f if f.isDirectory => collectFiles(f) case f if f.getName endsWith ".tasty" => f.getAbsolutePath :: Nil case _ => Nil } - collectFiles(File(s"target/scala-0.27/classes/tests/$name")) + collectFiles(File(s"${BuildInfo.test_testcasesOutputDir}/tests/$name")) + + @Rule + def collector = _collector + private val _collector = new ErrorCollector(); + def reportError(msg: String) = collector.addError(new AssertionError(msg)) @Test - def executeTest = + def executeTest = DokkaTestGenerator( - DottyDokkaConfig(DocConfiguration.Standalone(args, tastyFiles)), + DottyDokkaConfig(DocConfiguration.Standalone(args, tastyFiles, Nil)), TestLogger(DokkaConsoleLogger.INSTANCE), assertions.asTestMethods, Nil.asJava @@ -71,7 +76,7 @@ extension (s: Seq[Assertion]): import Assertion._ TestMethods( (context => s.collect { case AfterPluginSetup(fn) => fn(context) }.kUnit), - (validator => s.collect { case DuringValidation(fn) => fn(() => validator) }.kUnit), + (validator => s.collect { case DuringValidation(fn) => fn(() => validator.invoke()) }.kUnit), (modules => s.collect { case AfterDocumentablesCreation(fn) => fn(modules.asScala.toSeq) }.kUnit), (modules => s.collect { case AfterPreMergeDocumentablesTransformation(fn) => fn(modules.asScala.toSeq) }.kUnit), (module => s.collect { case AfterDocumentablesMerge(fn) => fn(module)}.kUnit), diff --git a/scala3doc/test/dotty/dokka/SignatureTest.scala b/scala3doc/test/dotty/dokka/SignatureTest.scala new file mode 100644 index 000000000000..1f82115d23ff --- /dev/null +++ b/scala3doc/test/dotty/dokka/SignatureTest.scala @@ -0,0 +1,125 @@ +package dotty.dokka + +import scala.io.Source +import scala.jdk.CollectionConverters._ +import scala.util.matching.Regex + +import org.jetbrains.dokka.pages.{RootPageNode, PageNode, ContentPage, ContentText, ContentNode, ContentComposite} + +import dotty.dokka.model.api.Link + +private enum Signature: + case Expected(name: String, signature: String) + case Unexpected(name: String) +import Signature._ + +abstract class SignatureTest( + testName: String, + signatureKinds: Seq[String], + sourceFiles: List[String] = Nil, + ignoreMissingSignatures: Boolean = false +) extends ScaladocTest(testName): + override def assertions = Assertion.AfterPagesTransformation { root => + val sources = sourceFiles match + case Nil => testName :: Nil + case s => s + + val allSignaturesFromSources = sources + .map { file => Source.fromFile(s"${BuildInfo.test_testcasesSourceRoot}/tests/$file.scala") } + .flatMap(signaturesFromSources(_, signatureKinds)) + .toList + val expectedFromSources: Map[String, List[String]] = allSignaturesFromSources + .collect { case Expected(name, signature) => name -> signature } + .groupMap(_._1)(_._2) + val unexpectedFromSources: Set[String] = allSignaturesFromSources.collect { case Unexpected(name) => name }.toSet + + val actualSignatures: Map[String, Seq[String]] = signaturesFromDocumentation(root).flatMap { signature => + findName(signature, signatureKinds).map(_ -> signature) + }.groupMap(_._1)(_._2) + + val unexpected = unexpectedFromSources.flatMap(actualSignatures.getOrElse(_, Nil)) + val expectedButNotFound = expectedFromSources.flatMap { + case (k, v) => findMissingSingatures(v, actualSignatures.getOrElse(k, Nil)) + } + + val missingReport = Option.when(!ignoreMissingSignatures && !expectedButNotFound.isEmpty) + (s"Not documented signatures:\n${expectedButNotFound.mkString("\n")}") + val unexpectedReport = Option.when(!unexpected.isEmpty) + (s"Unexpectedly documented signatures:\n${unexpected.mkString("\n")}") + val reports = missingReport ++ unexpectedReport + + if !reports.isEmpty then + val allSignaturesMessage = + s""" + |All documented signatures: + |${actualSignatures.flatMap(_._2).mkString("\n")} + | + |All expected signatures from source: + |${expectedFromSources.flatMap(_._2).mkString("\n")} + """.stripMargin + val errorMessage = (reports ++ Some(allSignaturesMessage)).mkString(start = "\n", sep = "\n\n", end = "\n") + reportError(errorMessage) + end if + + } :: Nil + +object SignatureTest { + val classlikeKinds = Seq("class", "object", "trait", "enum") // TODO add docs for packages + val members = Seq("type", "def", "val", "var") + val all = classlikeKinds ++ members +} + +// e.g. to remove '(0)' from object IAmACaseObject extends CaseImplementThis/*<-*/(0)/*->*/ +private val commentRegex = raw"\/\*<-\*\/[^\/]+\/\*->\*\/".r +private val whitespaceRegex = raw"\s+".r +private val expectedRegex = raw".+//expected: (.+)".r +private val unexpectedRegex = raw"(.+)//unexpected".r +private val identifierRegex = raw"^\s*(`.*`|(?:\w+)(?:_[^\[\(\s]+)|\w+|[^\[\(\s]+)".r + +private def findMissingSingatures(expected: Seq[String], actual: Seq[String]): Set[String] = + expected.toSet &~ actual.toSet + +extension (s: String): + private def startWithAnyOfThese(c: String*) = c.exists(s.startsWith) + private def compactWhitespaces = whitespaceRegex.replaceAllIn(s, " ") + +private def findName(signature: String, kinds: Seq[String]): Option[String] = + for + kindMatch <- kinds.flatMap(k => s"\\b$k\\b".r.findFirstMatchIn(signature)).headOption + afterKind <- Option(kindMatch.after(0)) // to filter out nulls + nameMatch <- identifierRegex.findFirstMatchIn(afterKind) + yield nameMatch.group(1) + +private def signaturesFromSources(source: Source, kinds: Seq[String]): Seq[Signature] = + source.getLines.map(_.trim) + .filterNot(_.isEmpty) + .filterNot(_.startWithAnyOfThese("=",":","{","}", "//")) + .toSeq + .flatMap { + case unexpectedRegex(signature) => findName(signature, kinds).map(Unexpected(_)) + case expectedRegex(signature) => findName(signature, kinds).map(Expected(_, signature)) + case signature => + findName(signature, kinds).map(Expected(_, commentRegex.replaceAllIn(signature, "").compactWhitespaces)) + } + +private def signaturesFromDocumentation(root: PageNode): Seq[String] = + def flattenToText(node: ContentNode) : Seq[String] = node match + case t: ContentText => Seq(t.getText) + case c: ContentComposite => + c.getChildren.asScala.flatMap(flattenToText).toSeq + case l: DocumentableElement => + (l.annotations ++ Seq(" ") ++ l.modifiers ++ Seq(l.name) ++ l.signature).map { + case s: String => s + case Link(s: String, _) => s + } + case _ => Seq() + + def all(p: ContentNode => Boolean)(n: ContentNode): Seq[ContentNode] = + if p(n) then Seq(n) else n.getChildren.asScala.toSeq.flatMap(all(p)) + + extension (page: PageNode) def allPages: List[PageNode] = page :: page.getChildren.asScala.toList.flatMap(_.allPages) + + val nodes = root.allPages + .collect { case p: ContentPage => p } + .flatMap(p => all(_.isInstanceOf[DocumentableElement])(p.getContent)) + nodes.map(flattenToText(_).mkString.compactWhitespaces.trim) diff --git a/scala3doc/test/dotty/dokka/SignatureTestCases.scala b/scala3doc/test/dotty/dokka/SignatureTestCases.scala new file mode 100644 index 000000000000..b2b029aabb81 --- /dev/null +++ b/scala3doc/test/dotty/dokka/SignatureTestCases.scala @@ -0,0 +1,58 @@ +package dotty.dokka + +class GenericSignaftures extends SignatureTest("genericSignatures", Seq("class")) + +class ObjectSignatures extends SignatureTest("objectSignatures", Seq("object")) + +class TraitSignatures extends SignatureTest("traitSignatures", Seq("trait")) + + +// We do not support companion objects properly in tests +class ClassSignatureTestSourceTest extends SignatureTest("classSignatureTestSource", + SignatureTest.all diff Seq("val", "var", "object")) + +// TODO we still cannot filter out all constructor-based fields +class SignatureTestSourceTest extends SignatureTest("signatureTestSource", SignatureTest.all) + +class ModifiersSignatureTest extends SignatureTest("modifiersSignatureTestSource", SignatureTest.all) + +class Visibility extends SignatureTest("visibility", SignatureTest.all) + + +class GenericMethodsTest extends SignatureTest("genericMethods", Seq("def")) + +class MethodsAndConstructors extends SignatureTest("methodsAndConstructors", Seq("def")) + +class TypesSignatures extends SignatureTest("typesSignatures", SignatureTest.all) + +class FieldsSignatures extends SignatureTest("fieldsSignatures", SignatureTest.all.filterNot(_ == "object")) + +class NestedSignatures extends SignatureTest("nested", SignatureTest.all) + +class CompanionObjectSignatures extends SignatureTest("companionObjectSignatures", SignatureTest.all) + +class PackageSymbolSignatures extends SignatureTest("packageSymbolSignatures", SignatureTest.all) + +class PackageObjectSymbolSignatures extends SignatureTest("packageObjectSymbolSignatures", SignatureTest.all.filterNot(_ == "object")) + +class MergedPackageSignatures extends SignatureTest("mergedPackage", SignatureTest.all.filterNot(_ == "object"), + sourceFiles = List("mergedPackage1", "mergedPackage2", "mergedPackage3")) + +class ExtensionMethodSignature extends SignatureTest("extensionMethodSignatures", SignatureTest.all) + +class ClassModifiers extends SignatureTest("classModifiers", SignatureTest.classlikeKinds) + +// class EnumSignatures extends SignatureTest("enumSignatures", SignatureTest.all) + +class StructuralTypes extends SignatureTest("structuralTypes", SignatureTest.members) + +class OpaqueTypes extends SignatureTest("opaqueTypes", SignatureTest.all) + +// class GivenSignatures extends SignatureTest("givenSignatures", SignatureTest.all) + +class Annotations extends SignatureTest("annotations", SignatureTest.all) + +class InheritanceLoop extends SignatureTest("inheritanceLoop", SignatureTest.all) + +class InheritedMembers extends SignatureTest("inheritedMembers2", SignatureTest.all.filter(_ != "class"), + sourceFiles = List("inheritedMembers1", "inheritedMembers2")) \ No newline at end of file diff --git a/scala3doc/test/dotty/dokka/SignatureTests.scala b/scala3doc/test/dotty/dokka/SignatureTests.scala deleted file mode 100644 index 29b8bcfa5878..000000000000 --- a/scala3doc/test/dotty/dokka/SignatureTests.scala +++ /dev/null @@ -1,57 +0,0 @@ -package dotty.dokka - -class GenericSignatures extends SingleFileTest("genericSignatures", Seq("class")) - -class ObjectSignatures extends SingleFileTest("objectSignatures", Seq("object")) - -class TraitSignatures extends SingleFileTest("traitSignatures", Seq("trait")) - - -// We do not support companion objects properly in tests -class ClassSignatureTestSourceTest extends SingleFileTest("classSignatureTestSource", - SingleFileTest.all.filterNot(Seq("val", "var", "object").contains)) - -// TODO we still cannot filter out all constructor-based fields -class SignatureTestSourceTest extends SingleFileTest("signatureTestSource", SingleFileTest.all) - -class ModifiersSignatureTest extends SingleFileTest("modifiersSignatureTestSource", SingleFileTest.all) - -class Visibility extends SingleFileTest("visibility", SingleFileTest.all) - - -class GenericMethodsTest extends SingleFileTest("genericMethods", Seq("def")) - -class MethodsAndConstructors extends SingleFileTest("methodsAndConstructors", Seq("def")) - -class TypesSignatures extends SingleFileTest("typesSignatures", SingleFileTest.all) - -class FieldsSignatures extends SingleFileTest("fieldsSignatures", SingleFileTest.all.filter(_ != "object")) - -class NestedSignatures extends SingleFileTest("nested", SingleFileTest.all) - -class CompanionObjectSignatures extends SingleFileTest("companionObjectSignatures", SingleFileTest.all) - -class PackageSymbolSignatures extends SingleFileTest("packageSymbolSignatures", SingleFileTest.all) - -class PackageObjectSymbolSignatures extends SingleFileTest("packageObjectSymbolSignatures", SingleFileTest.all.filter(_ != "object")) - -class MergedPackageSignatures extends MultipleFileTest(List("mergedPackage1", "mergedPackage2", "mergedPackage3"), List("mergedPackage"), SingleFileTest.all.filter(_ != "object")) - -class ExtensionMethodSignature extends SingleFileTest("extensionMethodSignatures", SingleFileTest.all) - -class ClassModifiers extends SingleFileTest("classModifiers", SingleFileTest.classlikeKinds) - -// class EnumSignatures extends SingleFileTest("enumSignatures", SingleFileTest.all) - -class StructuralTypes extends SingleFileTest("structuralTypes", SingleFileTest.members) - -class OpaqueTypes extends SingleFileTest("opaqueTypes", SingleFileTest.all) - -// class GivenSignatures extends SingleFileTest("givenSignatures", SingleFileTest.all) - -class Annotations extends SingleFileTest("annotations", SingleFileTest.all) - -class InheritanceLoop extends SingleFileTest("inheritanceLoop", SingleFileTest.all) - -class InheritedMembers extends MultipleFileTest(List("inheritedMembers1", "inheritedMembers2"), List("inheritedMembers2"), MultipleFileTest.all.filter(_ != "class")) - diff --git a/scala3doc/test/dotty/dokka/SingleFileTest.scala b/scala3doc/test/dotty/dokka/SingleFileTest.scala deleted file mode 100644 index 83864eb06c72..000000000000 --- a/scala3doc/test/dotty/dokka/SingleFileTest.scala +++ /dev/null @@ -1,30 +0,0 @@ -package dotty.dokka - -import org.junit.{Test, Rule} -import org.junit.Assert._ -import org.junit.rules.ErrorCollector -import org.jetbrains.dokka.testApi.testRunner.AbstractCoreTest$TestBuilder -import scala.io.Source -import org.jetbrains.dokka.pages._ -import org.jetbrains.dokka.pages.ContentNodesKt -import org.jetbrains.dokka._ -import scala.jdk.CollectionConverters._ -import scala.math.max - -object SingleFileTest { - val classlikeKinds = Seq("class", "object", "trait", "enum") // TODO add docs for packages - val members = Seq("type", "def", "val", "var") - val all = classlikeKinds ++ members -} - -abstract class SingleFileTest( - val fileName: String, - signatureKinds: Seq[String], - ignoreUndocumentedSignatures: Boolean = false -) extends MultipleFileTest( - List(fileName), - List(fileName), - signatureKinds, - ignoreUndocumentedSignatures -) - From a3e53f691fff71d6a62fa5d1763299c85ee9688b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Marks?= Date: Wed, 4 Nov 2020 13:27:45 +0100 Subject: [PATCH 3/5] Update information about tests in scala3doc readme --- scala3doc/README.md | 46 +++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/scala3doc/README.md b/scala3doc/README.md index c24c67c95fc8..bdc7a6487751 100644 --- a/scala3doc/README.md +++ b/scala3doc/README.md @@ -72,22 +72,36 @@ You can also find the result of building the same sites for latest `master` at: ### Testing -Most tests rely on comparing signatures (of classes, methods, objects etc.) extracted from the generated documentation -to signatures found in source files. Such tests are defined using [MultipleFileTest](src/test/scala/dotty/dokka/MultipleFileTest.scala) class -and its subtypes (such as [SingleFileTest](src/test/scala/dotty/dokka/SingleFileTest.scala)) - -WARNING: As the classes mentioned above are likely to evolve, the description below might easily get out of date. -In case of any discrepancies rely on the source files instead. - -`MultipleFileTest` requires that you specify the names of the files used to extract signatures, -the names of directories containing corresponding TASTY files -and the kinds of signatures from source files (corresponding to keywords used to declare them like `def`, `class`, `object` etc.) -whose presence in the generated documentation will be checked (other signatures, when missing, will be ignored). -The mentioned source files should be located directly inside `src/main/scala/tests` directory -but the file names passed as parameters should contain neither this path prefix nor `.scala` suffix. -The TASTY folders are expected to be located in `target/${dottyVersion}/classes/tests` (after successful compilation of the project) -and similarly only their names relative to this path should be provided as tests' parameters. -For `SingleFileTest` the name of the source file and the TASTY folder are expected to be the same. +To implement integration tests that inspects state of the model on different stages of +documentation generation one can use [ScaladocTest](src/test/scala/dotty/dokka/ScaladocTest.scala#L15). +This class requires providing the name of the test and a the list of assertions. + +Name of the test is used to extract symbols that should be included in given test run from +the TASTY files. All test data is located in [scala3doc-testcases module](../scala3doc-testcases/src). +It is compiled together with the rest of modules. This solves lots of potential problems with +invoking the compiler in a separate process such as mismatching versions. Every test is using +only symbols defined in the package with the same name as the test located inside top level `tests` +package (including subpackages). This restriction may be relaxed in the future. + +Assertions of each test are defined as list of [Assertion enum](src/test/scala/dotty/dokka/ScaladocTest.scala#L63) +instances. Each of them represents a callback that is invoked in corresponding phase of +documentation generation. Those callback can inspect the model, log information and raise errors +using `reportError` method. + +#### Signature tests + +Some of the tests rely on comparing signatures (of classes, methods, objects etc.) extracted from +the generated documentation to signatures found in source files. Such tests are defined using +[SignatureTest](src/test/scala/dotty/dokka/SignatureTest.scala#L16) class, that is a subclass of +`ScaladocTest` and uses exactly tha same mechanism to find input symbols in the TASTY files. + +Signature tests by default assume that sources that were used to generate used TASTY files are +located in the file with the same name as the name of the test. If this is not the case optional +parameter `sourceFiles` can be used to pass list of source file names (without `.scala` extension). + +Those tests also requires specifying kinds of ignatures from source files (corresponding to +keywords used to declare them like `def`, `class`, `object` etc.) whose presence in the generated +documentation will be checked (other signatures, when missing, will be ignored). By default it's expected that all signatures from the source files will be present in the documentation but not vice versa (because the documentation can contain also inherited signatures). From 13550b1f45ae5d24814db15cf2e304ab5d4d1ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Marks?= Date: Wed, 4 Nov 2020 13:45:54 +0100 Subject: [PATCH 4/5] Add testcases for signatures with slightly more complex names --- scala3doc-testcases/src/tests/complexNames.scala | 12 ++++++++++++ scala3doc/test/dotty/dokka/SignatureTestCases.scala | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 scala3doc-testcases/src/tests/complexNames.scala diff --git a/scala3doc-testcases/src/tests/complexNames.scala b/scala3doc-testcases/src/tests/complexNames.scala new file mode 100644 index 000000000000..837e49273c15 --- /dev/null +++ b/scala3doc-testcases/src/tests/complexNames.scala @@ -0,0 +1,12 @@ +package tests + +package complexNames + +abstract class A: + def ++(other: A): A + def +:(other: Int): A + def :+(other: Int): A + // scala3doc has problems with names in backticks + // def `multi word name`: Int + // def `*** name with arbitrary chars ^%`: Int + def complexName_^*(param: String): A diff --git a/scala3doc/test/dotty/dokka/SignatureTestCases.scala b/scala3doc/test/dotty/dokka/SignatureTestCases.scala index b2b029aabb81..eb8f8731d691 100644 --- a/scala3doc/test/dotty/dokka/SignatureTestCases.scala +++ b/scala3doc/test/dotty/dokka/SignatureTestCases.scala @@ -55,4 +55,6 @@ class Annotations extends SignatureTest("annotations", SignatureTest.all) class InheritanceLoop extends SignatureTest("inheritanceLoop", SignatureTest.all) class InheritedMembers extends SignatureTest("inheritedMembers2", SignatureTest.all.filter(_ != "class"), - sourceFiles = List("inheritedMembers1", "inheritedMembers2")) \ No newline at end of file + sourceFiles = List("inheritedMembers1", "inheritedMembers2")) + +class ComplexNames extends SignatureTest("complexNames", Seq("def")) From a5c9ce9763d5849423bbcbaea57ecb21339afbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Marks?= Date: Thu, 5 Nov 2020 17:11:00 +0100 Subject: [PATCH 5/5] Add a few more mischievous test cases for literal identifiers and slightly improved SignatureTests --- scala3doc-testcases/src/tests/complexNames.scala | 14 ++++++++++++++ scala3doc/test/dotty/dokka/ScaladocTest.scala | 9 +++++++-- scala3doc/test/dotty/dokka/SignatureTest.scala | 6 +++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/scala3doc-testcases/src/tests/complexNames.scala b/scala3doc-testcases/src/tests/complexNames.scala index 837e49273c15..df0e515d497d 100644 --- a/scala3doc-testcases/src/tests/complexNames.scala +++ b/scala3doc-testcases/src/tests/complexNames.scala @@ -6,7 +6,21 @@ abstract class A: def ++(other: A): A def +:(other: Int): A def :+(other: Int): A + // scala3doc has problems with names in backticks // def `multi word name`: Int // def `*** name with arbitrary chars ^%`: Int + // def `mischievous(param:Int)`(otherParam: Int): String + // def withMischievousParams(`param: String, param2`: String): String + def complexName_^*(param: String): A + + def `completelyUnnecessaryBackticks`: Int //expected: def completelyUnnecessaryBackticks: Int + def `+++:`(other: Int): A //expected: def +++:(other: Int): A + def `:+++`(other: Int): A //expected: def :+++(other: Int): A + + def `abc_^^_&&`: A //expected: def abc_^^_&&: A + def `abc_def`: A //expected: def abc_def: A + def `abc_def_++`: A //expected: def abc_def_++: A + // def `++_abc`: A + // def `abc_++_--`: A diff --git a/scala3doc/test/dotty/dokka/ScaladocTest.scala b/scala3doc/test/dotty/dokka/ScaladocTest.scala index 1b5b590734e0..a12ea25c4842 100644 --- a/scala3doc/test/dotty/dokka/ScaladocTest.scala +++ b/scala3doc/test/dotty/dokka/ScaladocTest.scala @@ -34,7 +34,10 @@ abstract class ScaladocTest(val name: String): ) private def tastyFiles = - def collectFiles(dir: File): List[String] = dir.listFiles.toList.flatMap { + def listFilesSafe(dir: File) = Option(dir.listFiles).getOrElse { + throw AssertionError(s"$dir not found. The test name is incorrect or scala3doc-testcases were not recompiled.") + } + def collectFiles(dir: File): List[String] = listFilesSafe(dir).toList.flatMap { case f if f.isDirectory => collectFiles(f) case f if f.getName endsWith ".tasty" => f.getAbsolutePath :: Nil case _ => Nil @@ -57,12 +60,14 @@ abstract class ScaladocTest(val name: String): end ScaladocTest +type Validator = () => Unit + /** * Those assertions map 1-1 to their dokka counterparts. Some of them may be irrelevant in scala3doc. */ enum Assertion: case AfterPluginSetup(fn: DokkaContext => Unit) - case DuringValidation(fn: (() => Unit) => Unit) + case DuringValidation(fn: Validator => Unit) case AfterDocumentablesCreation(fn: Seq[DModule] => Unit) case AfterPreMergeDocumentablesTransformation(fn: Seq[DModule] => Unit) case AfterDocumentablesMerge(fn: DModule => Unit) diff --git a/scala3doc/test/dotty/dokka/SignatureTest.scala b/scala3doc/test/dotty/dokka/SignatureTest.scala index 1f82115d23ff..8a59febc7814 100644 --- a/scala3doc/test/dotty/dokka/SignatureTest.scala +++ b/scala3doc/test/dotty/dokka/SignatureTest.scala @@ -29,15 +29,15 @@ abstract class SignatureTest( .flatMap(signaturesFromSources(_, signatureKinds)) .toList val expectedFromSources: Map[String, List[String]] = allSignaturesFromSources - .collect { case Expected(name, signature) => name -> signature } - .groupMap(_._1)(_._2) + .collect { case e: Expected => e } + .groupMap(_.name)(_.signature) val unexpectedFromSources: Set[String] = allSignaturesFromSources.collect { case Unexpected(name) => name }.toSet val actualSignatures: Map[String, Seq[String]] = signaturesFromDocumentation(root).flatMap { signature => findName(signature, signatureKinds).map(_ -> signature) }.groupMap(_._1)(_._2) - val unexpected = unexpectedFromSources.flatMap(actualSignatures.getOrElse(_, Nil)) + val unexpected = unexpectedFromSources.flatMap(actualSignatures.get) val expectedButNotFound = expectedFromSources.flatMap { case (k, v) => findMissingSingatures(v, actualSignatures.getOrElse(k, Nil)) }