diff --git a/pmd-dart/pom.xml b/pmd-dart/pom.xml
index 74e75d3dd1..c18d893086 100644
--- a/pmd-dart/pom.xml
+++ b/pmd-dart/pom.xml
@@ -54,5 +54,10 @@
pmd-test
test
+
+ net.sourceforge.pmd
+ pmd-lang-test
+ test
+
diff --git a/pmd-dart/src/test/java/net/sourceforge/pmd/cpd/DartTokenizerTest2.java b/pmd-dart/src/test/java/net/sourceforge/pmd/cpd/DartTokenizerTest2.java
new file mode 100644
index 0000000000..e3db041ad1
--- /dev/null
+++ b/pmd-dart/src/test/java/net/sourceforge/pmd/cpd/DartTokenizerTest2.java
@@ -0,0 +1,52 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.cpd;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import net.sourceforge.pmd.cpd.test.CpdTextComparisonTest;
+
+public class DartTokenizerTest2 extends CpdTextComparisonTest {
+
+ public DartTokenizerTest2() {
+ super(".dart");
+ }
+
+ @NotNull
+ @Override
+ protected String getResourcePrefix() {
+ return "";
+ }
+
+ @Test
+ public void testComment() {
+ doTest("comment");
+ }
+
+
+ @Test
+ public void testMultiline() {
+ doTest("string_multiline");
+ }
+
+ @Test
+ public void testStringWithBackslashes() {
+ doTest("string_with_backslashes");
+ }
+
+ @Test
+ public void testIncrement() {
+ doTest("increment");
+ }
+
+
+ @NotNull
+ @Override
+ public Tokenizer newTokenizer() {
+ return new DartTokenizer();
+ }
+
+}
diff --git a/pmd-dart/src/test/resources/net/sourceforge/pmd/cpd/comment.txt b/pmd-dart/src/test/resources/net/sourceforge/pmd/cpd/comment.txt
new file mode 100644
index 0000000000..6a7b4ca2c1
--- /dev/null
+++ b/pmd-dart/src/test/resources/net/sourceforge/pmd/cpd/comment.txt
@@ -0,0 +1,7 @@
+ [Image] or [Truncated image[ Bcol Ecol
+L3
+ [var] 1 3
+ [x] 5 5
+ [=] 7 7
+ [0] 9 9
+ [EOF] -1 -1
diff --git a/pmd-dart/src/test/resources/net/sourceforge/pmd/cpd/increment.txt b/pmd-dart/src/test/resources/net/sourceforge/pmd/cpd/increment.txt
new file mode 100644
index 0000000000..e2eaf416a6
--- /dev/null
+++ b/pmd-dart/src/test/resources/net/sourceforge/pmd/cpd/increment.txt
@@ -0,0 +1,207 @@
+ [Image] or [Truncated image[ Bcol Ecol
+L3
+ [var] 1 3
+ [x] 5 5
+ [=] 7 7
+ [0] 9 9
+L5
+ [void] 1 4
+ [increment1] 6 15
+ [(] 16 16
+ [)] 17 17
+ [{] 19 19
+ [x] 21 21
+ [+=] 23 24
+ [1] 26 26
+ [}] 29 29
+L6
+ [void] 1 4
+ [increment2] 6 15
+ [(] 16 16
+ [)] 17 17
+ [{] 19 19
+ [x] 21 21
+ [+=] 23 24
+ [1] 26 26
+ [}] 29 29
+L7
+ [void] 1 4
+ [increment3] 6 15
+ [(] 16 16
+ [)] 17 17
+ [{] 19 19
+ [x] 21 21
+ [+=] 23 24
+ [1] 26 26
+ [}] 29 29
+L8
+ [void] 1 4
+ [increment4] 6 15
+ [(] 16 16
+ [)] 17 17
+ [{] 19 19
+ [x] 21 21
+ [+=] 23 24
+ [1] 26 26
+ [}] 29 29
+L9
+ [void] 1 4
+ [increment5] 6 15
+ [(] 16 16
+ [)] 17 17
+ [{] 19 19
+ [x] 21 21
+ [+=] 23 24
+ [1] 26 26
+ [}] 29 29
+L10
+ [void] 1 4
+ [increment6] 6 15
+ [(] 16 16
+ [)] 17 17
+ [{] 19 19
+ [x] 21 21
+ [+=] 23 24
+ [1] 26 26
+ [}] 29 29
+L11
+ [void] 1 4
+ [increment7] 6 15
+ [(] 16 16
+ [)] 17 17
+ [{] 19 19
+ [x] 21 21
+ [+=] 23 24
+ [1] 26 26
+ [}] 29 29
+L12
+ [void] 1 4
+ [increment8] 6 15
+ [(] 16 16
+ [)] 17 17
+ [{] 19 19
+ [x] 21 21
+ [+=] 23 24
+ [1] 26 26
+ [}] 29 29
+L13
+ [void] 1 4
+ [increment9] 6 15
+ [(] 16 16
+ [)] 17 17
+ [{] 19 19
+ [x] 21 21
+ [+=] 23 24
+ [1] 26 26
+ [}] 29 29
+L14
+ [void] 1 4
+ [increment10] 6 16
+ [(] 17 17
+ [)] 18 18
+ [{] 20 20
+ [x] 22 22
+ [+=] 24 25
+ [1] 27 27
+ [}] 30 30
+L15
+ [void] 1 4
+ [increment11] 6 16
+ [(] 17 17
+ [)] 18 18
+ [{] 20 20
+ [x] 22 22
+ [+=] 24 25
+ [1] 27 27
+ [}] 30 30
+L16
+ [void] 1 4
+ [increment12] 6 16
+ [(] 17 17
+ [)] 18 18
+ [{] 20 20
+ [x] 22 22
+ [+=] 24 25
+ [1] 27 27
+ [}] 30 30
+L17
+ [void] 1 4
+ [increment13] 6 16
+ [(] 17 17
+ [)] 18 18
+ [{] 20 20
+ [x] 22 22
+ [+=] 24 25
+ [1] 27 27
+ [}] 30 30
+L18
+ [void] 1 4
+ [increment14] 6 16
+ [(] 17 17
+ [)] 18 18
+ [{] 20 20
+ [x] 22 22
+ [+=] 24 25
+ [1] 27 27
+ [}] 30 30
+L19
+ [void] 1 4
+ [increment15] 6 16
+ [(] 17 17
+ [)] 18 18
+ [{] 20 20
+ [x] 22 22
+ [+=] 24 25
+ [1] 27 27
+ [}] 30 30
+L20
+ [void] 1 4
+ [increment16] 6 16
+ [(] 17 17
+ [)] 18 18
+ [{] 20 20
+ [x] 22 22
+ [+=] 24 25
+ [1] 27 27
+ [}] 30 30
+L21
+ [void] 1 4
+ [increment17] 6 16
+ [(] 17 17
+ [)] 18 18
+ [{] 20 20
+ [x] 22 22
+ [+=] 24 25
+ [1] 27 27
+ [}] 30 30
+L22
+ [void] 1 4
+ [increment18] 6 16
+ [(] 17 17
+ [)] 18 18
+ [{] 20 20
+ [x] 22 22
+ [+=] 24 25
+ [1] 27 27
+ [}] 30 30
+L23
+ [void] 1 4
+ [increment19] 6 16
+ [(] 17 17
+ [)] 18 18
+ [{] 20 20
+ [x] 22 22
+ [+=] 24 25
+ [1] 27 27
+ [}] 30 30
+L24
+ [void] 1 4
+ [increment20] 6 16
+ [(] 17 17
+ [)] 18 18
+ [{] 20 20
+ [x] 22 22
+ [+=] 24 25
+ [1] 27 27
+ [}] 30 30
+ [EOF] -1 -1
diff --git a/pmd-dart/src/test/resources/net/sourceforge/pmd/cpd/string_multiline.txt b/pmd-dart/src/test/resources/net/sourceforge/pmd/cpd/string_multiline.txt
new file mode 100644
index 0000000000..5d6530a0c5
--- /dev/null
+++ b/pmd-dart/src/test/resources/net/sourceforge/pmd/cpd/string_multiline.txt
@@ -0,0 +1,18 @@
+ [Image] or [Truncated image[ Bcol Ecol
+L1
+ [class] 1 5
+ [MyClass] 7 13
+ [{] 15 15
+L2
+ [var] 5 7
+ [s1] 9 10
+ [=] 12 12
+ ['''\nYou can create\nmulti-line st[ 14 69
+L7
+ [var] 5 7
+ [s2] 9 10
+ [=] 12 12
+ ["""This is also a\nmulti-line stri[ 14 52
+L9
+ [}] 1 1
+ [EOF] -1 -1
diff --git a/pmd-dart/src/test/resources/net/sourceforge/pmd/cpd/string_with_backslashes.txt b/pmd-dart/src/test/resources/net/sourceforge/pmd/cpd/string_with_backslashes.txt
new file mode 100644
index 0000000000..fdaa1d6491
--- /dev/null
+++ b/pmd-dart/src/test/resources/net/sourceforge/pmd/cpd/string_with_backslashes.txt
@@ -0,0 +1,13 @@
+ [Image] or [Truncated image[ Bcol Ecol
+L1
+ [class] 1 5
+ [MyClass] 7 13
+ [{] 15 15
+L2
+ [final] 3 7
+ [stringWithBackslashes] 9 29
+ [=] 31 31
+ ["Escaping\\ spaces\\ should work"] 33 63
+L3
+ [}] 1 1
+ [EOF] -1 -1
diff --git a/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/cpd/test/CpdTextComparisonTest.kt b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/cpd/test/CpdTextComparisonTest.kt
new file mode 100644
index 0000000000..cf321aa29c
--- /dev/null
+++ b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/cpd/test/CpdTextComparisonTest.kt
@@ -0,0 +1,112 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.cpd.test
+
+import net.sourceforge.pmd.cpd.SourceCode
+import net.sourceforge.pmd.cpd.TokenEntry
+import net.sourceforge.pmd.cpd.Tokenizer
+import net.sourceforge.pmd.cpd.Tokens
+import net.sourceforge.pmd.test.BaseTextComparisonTest
+import org.apache.commons.lang3.StringUtils
+
+/**
+ * CPD test comparing a dump of a file against a saved baseline.
+ * Each token is printed on a separate line.
+ *
+ * @param extensionIncludingDot File extension for the language.
+ * Baseline files are saved in txt files.
+ */
+abstract class CpdTextComparisonTest(
+ override val extensionIncludingDot: String
+) : BaseTextComparisonTest() {
+
+ abstract fun newTokenizer(): Tokenizer
+
+ override val resourceLoader: Class<*>
+ get() = javaClass
+
+ override val resourcePrefix: String
+ get() = "cpdData"
+
+ override fun transformTextContent(sourceText: String): String {
+ val sourceCode = SourceCode(SourceCode.StringCodeLoader(sourceText))
+ val tokens = Tokens().also { newTokenizer().tokenize(sourceCode, it) }
+
+ return buildString { format(tokens) }
+ }
+
+
+ private fun StringBuilder.format(tokens: Tokens) {
+ appendHeader().appendln()
+
+ var curLine = -1
+
+ for (token in tokens.iterator()) {
+
+ if (curLine != token.beginLine && token !== TokenEntry.EOF) {
+ curLine = token.beginLine
+ append('L').append(curLine).appendln()
+ }
+
+ formatLine(token).appendln()
+ }
+ }
+
+ private fun StringBuilder.appendHeader() =
+ formatLine(
+ escapedImage = "[Image] or [Truncated image[",
+ bcol = "Bcol",
+ ecol = "Ecol"
+ )
+
+
+ private fun StringBuilder.formatLine(token: TokenEntry) =
+ formatLine(
+ escapedImage = escapeImage(token.toString()),
+ bcol = token.beginColumn,
+ ecol = token.endColumn
+ )
+
+
+ private fun StringBuilder.formatLine(escapedImage: String, bcol: Any, ecol: Any): StringBuilder {
+ var colStart = length
+ colStart = append(Indent).append(escapedImage).padCol(colStart, Col0Width)
+ colStart = append(Indent).append(bcol).padCol(colStart, Col1Width)
+ return append(ecol)
+ }
+
+ private fun StringBuilder.padCol(colStart: Int, colWidth: Int): Int {
+ for (i in 1..(colStart + colWidth - this.length))
+ append(' ')
+
+ return length
+ }
+
+
+ private fun escapeImage(str: String): String {
+ val escaped = str
+ .replace("\\", "\\\\") // escape backslashes
+ .replace(Regex("\\R"), "\\\\n") // escape newlines (normalizing)
+ .replace(Regex("[]\\[]"), "\\\\$0") // escape []
+
+ var truncated = StringUtils.truncate(escaped, ImageSize)
+
+ if (truncated.endsWith('\\') && !truncated.endsWith("\\\\"))
+ truncated = truncated.substring(0, truncated.length - 1) // cut inside an escape
+
+ return if (truncated.length < escaped.length)
+ "[$truncated["
+ else
+ "[$truncated]"
+
+ }
+
+ private companion object {
+ const val Indent = " "
+ const val Col0Width = 40
+ const val Col1Width = 10 + Indent.length
+ val ImageSize = Col0Width - Indent.length - 2 // -2 is for the "[]"
+ }
+}
\ No newline at end of file
diff --git a/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseTreeDumpTest.kt b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseTreeDumpTest.kt
index 4fab7ba383..2b4adb99e6 100644
--- a/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseTreeDumpTest.kt
+++ b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseTreeDumpTest.kt
@@ -4,71 +4,31 @@
package net.sourceforge.pmd.lang.ast.test
+import net.sourceforge.pmd.test.BaseTextComparisonTest
import net.sourceforge.pmd.util.treeexport.TreeRenderer
-import java.nio.file.Path
-import java.nio.file.Paths
-import kotlin.test.assertEquals
/**
- * Compare a dump of a file against a saved baseline.
+ * Compare a dump of an AST against a saved baseline.
*
* @param printer The node printer used to dump the trees
- * @param extension Extension that the unparsed source file is supposed to have
+ * @param extensionIncludingDot Extension that the unparsed source file is supposed to have
*/
abstract class BaseTreeDumpTest(
- val printer: TreeRenderer,
- val extension: String
-) {
+ private val printer: TreeRenderer,
+ override val extensionIncludingDot: String
+) : BaseTextComparisonTest() {
abstract val parser: BaseParsingHelper<*, *>
- /**
- * Executes the test. The test files are looked up using the [parser].
- * The reference test file must be named [fileBaseName] + [ExpectedExt].
- * The source file to parse must be named [fileBaseName] + [extension].
- */
- fun doTest(fileBaseName: String) {
- val expectedFile = findTestFile(parser.resourceLoader, "${parser.resourcePrefix}/$fileBaseName$ExpectedExt").toFile()
- val sourceFile = findTestFile(parser.resourceLoader, "${parser.resourcePrefix}/$fileBaseName$extension").toFile()
+ override val resourceLoader: Class<*>
+ get() = parser.resourceLoader
- assert(sourceFile.isFile) {
- "Source file $sourceFile is missing"
- }
-
- val parsed = parser.parse(sourceFile.readText()) // UTF-8
- val actual = StringBuilder().also { printer.renderSubtree(parsed, it) }.toString()
-
- if (!expectedFile.exists()) {
- expectedFile.writeText(actual)
- throw AssertionError("Reference file doesn't exist, created it at $expectedFile")
- }
-
- val expected = expectedFile.readText()
-
- assertEquals(expected.normalize(), actual.normalize(), "Tree dump comparison failed, see the reference: $expectedFile")
- }
-
- // Outputting a path makes for better error messages
- private val srcTestResources = let {
- // this is set from maven surefire - see parent pom.xml configuration for surefire (systemPropertyVariables)
- System.getProperty("mvn.project.src.test.resources")
- ?.let { Paths.get(it).toAbsolutePath() }
- // that's for when the tests are run inside the IDE
- ?: Paths.get(javaClass.protectionDomain.codeSource.location.file)
- // go up from target/test-classes into the project root
- .resolve("../../src/test/resources").normalize()
- }
-
- private fun findTestFile(contextClass: Class<*>, resourcePath: String): Path {
- val path = contextClass.`package`.name.replace('.', '/')
- return srcTestResources.resolve("$path/$resourcePath")
- }
-
- companion object {
- const val ExpectedExt = ".txt"
-
- fun String.normalize() = replace(Regex("\\R"), "\n")
- }
+ override val resourcePrefix: String
+ get() = parser.resourcePrefix
+ override fun transformTextContent(sourceText: String): String =
+ buildString {
+ printer.renderSubtree(parser.parse(sourceText), this)
+ }
}
diff --git a/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/test/BaseTextComparisonTest.kt b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/test/BaseTextComparisonTest.kt
new file mode 100644
index 0000000000..2c08a24590
--- /dev/null
+++ b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/test/BaseTextComparisonTest.kt
@@ -0,0 +1,73 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.test
+
+import java.nio.file.Path
+import java.nio.file.Paths
+import kotlin.test.assertEquals
+
+
+/**
+ * Compare a dump of a file against a saved baseline.
+ * See subclasses CpdTextComparisonTest, BaseTreeDumpTest.
+ */
+abstract class BaseTextComparisonTest {
+
+ protected abstract val resourceLoader: Class<*>
+ protected abstract val resourcePrefix: String
+
+ /** Extension that the unparsed source file is supposed to have. */
+ protected abstract val extensionIncludingDot: String
+ /** Turn the contents of the source file into the "actual" string. */
+ protected abstract fun transformTextContent(sourceText: String): String
+
+ /**
+ * Executes the test. The test files are looked up using the [parser].
+ * The reference test file must be named [fileBaseName] + [ExpectedExt].
+ * The source file to parse must be named [fileBaseName] + [extensionIncludingDot].
+ */
+ fun doTest(fileBaseName: String) {
+ val expectedFile = findTestFile(resourceLoader, "${resourcePrefix}/$fileBaseName$ExpectedExt").toFile()
+ val sourceFile = findTestFile(resourceLoader, "${resourcePrefix}/$fileBaseName$extensionIncludingDot").toFile()
+
+ assert(sourceFile.isFile) {
+ "Source file $sourceFile is missing"
+ }
+
+ val actual = transformTextContent(sourceFile.readText()) // UTF-8
+
+ if (!expectedFile.exists()) {
+ expectedFile.writeText(actual)
+ throw AssertionError("Reference file doesn't exist, created it at $expectedFile")
+ }
+
+ val expected = expectedFile.readText()
+
+ assertEquals(expected.normalize(), actual.normalize(), "Tree dump comparison failed, see the reference: $expectedFile")
+ }
+
+ // Outputting a path makes for better error messages
+ private val srcTestResources = let {
+ // this is set from maven surefire
+ System.getProperty("mvn.project.src.test.resources")
+ ?.let { Paths.get(it).toAbsolutePath() }
+ // that's for when the tests are run inside the IDE
+ ?: Paths.get(javaClass.protectionDomain.codeSource.location.file)
+ // go up from target/test-classes into the project root
+ .resolve("../../src/test/resources").normalize()
+ }
+
+ private fun findTestFile(contextClass: Class<*>, resourcePath: String): Path {
+ val path = contextClass.`package`.name.replace('.', '/')
+ return srcTestResources.resolve("$path/$resourcePath")
+ }
+
+ companion object {
+ const val ExpectedExt = ".txt"
+
+ fun String.normalize() = replace(Regex("\\R"), "\n")
+ }
+
+}