diff --git a/.editorconfig b/.editorconfig
index 1525fadc..46940d65 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -411,6 +411,7 @@ ij_asciidoc_blank_lines_after_header = 1
ij_asciidoc_blank_lines_keep_after_header = 1
ij_asciidoc_formatting_enabled = true
ij_asciidoc_one_sentence_per_line = true
+trim_trailing_whitespace = false
[{*.ant,*.fo,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.qrc,*.rng,*.tld,*.wadl,*.wsdd,*.wsdl,*.xjb,*.xml,*.xsd,*.xsl,*.xslt,*.xul,phpunit.xml.dist}]
ij_xml_align_attributes = true
diff --git a/.run/AsciidocReformater.run.xml b/.run/AsciidocReformater.run.xml
new file mode 100644
index 00000000..70c82a08
--- /dev/null
+++ b/.run/AsciidocReformater.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.run/JsTestCaseSync.run.xml b/.run/JsTestCaseSync.run.xml
new file mode 100644
index 00000000..b42b1ff3
--- /dev/null
+++ b/.run/JsTestCaseSync.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.run/Reformat Tests.run.xml b/.run/Reformat Tests.run.xml
deleted file mode 100644
index ed6f4681..00000000
--- a/.run/Reformat Tests.run.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/AsciiDocParser.kt b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/AsciiDocParser.kt
index 7aba85b5..f4ecac11 100644
--- a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/AsciiDocParser.kt
+++ b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/AsciiDocParser.kt
@@ -1,26 +1,30 @@
package org.neo4j.graphql.asciidoc
-import org.neo4j.graphql.asciidoc.ast.*
import org.apache.commons.csv.CSVFormat
-import java.io.File
+import org.neo4j.graphql.asciidoc.ast.*
import java.net.URI
+import java.nio.file.Path
import java.util.regex.Pattern
import javax.ws.rs.core.UriBuilder
+import kotlin.io.path.readLines
class AsciiDocParser(
- fileName: String
+ private val file: Path
) {
- private val file = File(AsciiDocParser::class.java.getResource("/$fileName")?.toURI()!!)
- private val srcLocation = File("src/test/resources/", fileName).toURI()
-
- private var root = Document(srcLocation)
+ private var root = Document(file.toUri())
private var currentSection: Section = root
private var currentDepth: Int = 0
-
fun parse(): Document {
- val lines = file.readLines()
+ return parseLines(file.readLines())
+ }
+
+ fun parseContent(content: String): Document {
+ return parseLines(content.lines())
+ }
+
+ private fun parseLines(lines: List): Document {
var title: String?
var insideCodeblock = false
@@ -29,7 +33,7 @@ class AsciiDocParser(
val fileContent = StringBuilder()
- root = Document(srcLocation)
+ root = Document(file.toUri())
currentSection = root
currentDepth = 0
var caption: String? = null
@@ -42,11 +46,6 @@ class AsciiDocParser(
loop@ for ((lineNr, line) in lines.withIndex()) {
fileContent.append(line).append('\n')
- if (line.startsWith("#") || line.startsWith("//")) {
- offset += line.length + 1
- continue
- }
-
val headlineMatcher = HEADLINE_PATTERN.matcher(line)
when {
@@ -55,7 +54,7 @@ class AsciiDocParser(
addBlock(content)
val depth = headlineMatcher.group(1).length
title = headlineMatcher.group(2)
- val uri = UriBuilder.fromUri(srcLocation).queryParam("line", lineNr + 1).build()
+ val uri = uriWithLineNr(lineNr)
startSection(title, uri, depth)
}
@@ -65,10 +64,10 @@ class AsciiDocParser(
line.startsWith("[%header,format=csv") -> {
addBlock(content)
- val uri = UriBuilder.fromUri(srcLocation).queryParam("line", lineNr + 1).build()
+ val uri = uriWithLineNr(lineNr)
- val parts = line.substring(19, line.indexOf("]")).trim().split(",")
- val attributes = parts.slice(0.. {
addBlock(content)
- val uri = UriBuilder.fromUri(srcLocation).queryParam("line", lineNr + 1).build()
+ val uri = uriWithLineNr(lineNr)
val parts = line.substring(8, line.indexOf("]")).trim().split(",")
val language = parts[0]
@@ -93,8 +92,6 @@ class AsciiDocParser(
currentCodeBlock = CodeBlock(uri, language, currentSection, attributes).also {
it.caption = caption
- it.markerStart = offset
- it.markerEnd = offset + line.length
currentSection.blocks.add(it)
}
caption = null
@@ -124,10 +121,8 @@ class AsciiDocParser(
line == "----" -> {
insideCodeblock = !insideCodeblock
if (insideCodeblock) {
- currentCodeBlock?.start = offset + line.length + 1
content = StringBuilder()
} else if (currentCodeBlock != null) {
- currentCodeBlock.end = offset
currentCodeBlock.content = content.toString().trim()
currentCodeBlock = null
content = StringBuilder()
@@ -145,10 +140,13 @@ class AsciiDocParser(
return root
}
+ private fun uriWithLineNr(lineNr: Int): URI =
+ UriBuilder.fromUri(file.toUri()).queryParam("line", lineNr + 1).build()
+
private fun addBlock(content: StringBuilder) {
val str = content.toString()
- if (str.trim().isNotEmpty()) {
- currentSection.let { it.blocks.add(Block(it, str)) }
+ if (str.isNotBlank()) {
+ currentSection.let { it.blocks.add(Block(it, str.trimEnd())) }
}
content.clear()
}
diff --git a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Block.kt b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Block.kt
index 1139335b..61706e81 100644
--- a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Block.kt
+++ b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Block.kt
@@ -2,10 +2,14 @@ package org.neo4j.graphql.asciidoc.ast
class Block(
parent: StructuralNode,
- val content: String
+ var content: String
) : StructuralNode(parent) {
override fun toString(): String {
return "Block(content='$content')"
}
+
+ override fun buildContent(contentExtractor: (CodeBlock) -> String): String {
+ return content + "\n"
+ }
}
diff --git a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/CodeBlock.kt b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/CodeBlock.kt
index 44ec0be9..ce3084dc 100644
--- a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/CodeBlock.kt
+++ b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/CodeBlock.kt
@@ -11,11 +11,6 @@ class CodeBlock(
var caption: String? = null
- var markerStart: Int? = null
- var markerEnd: Int? = null
- var start: Int? = null
- var end: Int? = null
-
lateinit var content: String
/**
@@ -49,4 +44,15 @@ class CodeBlock(
fun matches(language: String, filter: Map = emptyMap(), exactly: Boolean = false) =
this.language == language && filter.all { (k, v) -> attributes[k] == v } && (!exactly || attributes.size == filter.size)
+ override fun buildContent(contentExtractor: (CodeBlock) -> String): String {
+ val builder = StringBuilder()
+ caption?.let {
+ builder.append("\n.${it}\n")
+ }
+ builder.append(adjustedMarker)
+ builder.append("\n----\n")
+ builder.append(contentExtractor(this))
+ builder.append("\n----\n")
+ return builder.toString()
+ }
}
diff --git a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Document.kt b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Document.kt
index 5a5252a5..15dc297d 100644
--- a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Document.kt
+++ b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Document.kt
@@ -8,4 +8,12 @@ class Document(
lateinit var content: String
+ override fun buildContent(contentExtractor: (CodeBlock) -> String): String {
+ val builder = StringBuilder()
+ blocks.forEach {
+ builder.append(it.buildContent(contentExtractor))
+ }
+ return builder.toString()
+ }
+
}
diff --git a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Section.kt b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Section.kt
index 42ab2519..c07e15be 100644
--- a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Section.kt
+++ b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Section.kt
@@ -1,5 +1,6 @@
package org.neo4j.graphql.asciidoc.ast
+import org.neo4j.graphql.domain.CodeBlockPredicate
import java.net.URI
open class Section(
@@ -11,4 +12,75 @@ open class Section(
override fun toString(): String {
return "Section(title='$title')"
}
+
+ override fun buildContent(contentExtractor: (CodeBlock) -> String): String {
+
+ val builder = StringBuilder("\n")
+ var current: Section? = this
+ do {
+ builder.append("=")
+ current = current?.parent
+ if (current is Document && current.blocks.filterIsInstance().size > 1) {
+ // we are in a document with multiple sections, so we need to add another level to the title
+ builder.append("=")
+ }
+ } while (current != null && current !is Document)
+ builder.append(" ${title}\n")
+ blocks.forEach {
+ builder.append(it.buildContent(contentExtractor))
+ }
+ return builder.toString()
+ }
+
+ /**
+ * Find all directly nested code blocks of a given section matching the language and filter
+ */
+ fun findCodeBlocks(
+ predicate: CodeBlockPredicate,
+ ): List =
+ blocks
+ .filterIsInstance()
+ .filter { predicate.matches(it) }
+
+ /**
+ * Find a single code block of a given section matching the language and filter
+ */
+ fun findSingleOrNullCodeBlock(
+ predicate: CodeBlockPredicate,
+ ): CodeBlock? =
+ findCodeBlocks(predicate)
+ .also { require(it.size <= 1) { "Found more than one code block matching the predicate" } }
+ .singleOrNull()
+
+ /**
+ * Find all setup blocks for a given section, including the setup blocks of the parent sections
+ */
+ fun findSetupCodeBlocks(
+ predicate: CodeBlockPredicate,
+ ): List {
+ val result = mutableListOf()
+ var currentSection: Section? = this
+ while (currentSection != null) {
+ result.addAll(currentSection.findCodeBlocks(predicate))
+ currentSection.blocks
+ .filterIsInstance()
+ .filter { it.title == "Setup" }
+ .forEach { result.addAll(it.findCodeBlocks(predicate)) }
+ currentSection = currentSection.parent
+ }
+ return result
+ }
+
+ fun addAfter(insertPoint: StructuralNode?, node: StructuralNode) {
+ if (insertPoint == null) {
+ blocks.add(node)
+ } else {
+ val index = blocks.indexOf(insertPoint)
+ if (index == -1) {
+ blocks.add(node)
+ } else {
+ blocks.add(index + 1, node)
+ }
+ }
+ }
}
diff --git a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/StructuralNode.kt b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/StructuralNode.kt
index 26273b2d..930b3fc7 100644
--- a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/StructuralNode.kt
+++ b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/StructuralNode.kt
@@ -4,4 +4,6 @@ sealed class StructuralNode(
open val parent: StructuralNode?
) {
val blocks = mutableListOf()
+
+ abstract fun buildContent(contentExtractor: (CodeBlock) -> String): String
}
diff --git a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Table.kt b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Table.kt
index 51cfb98e..b113086b 100644
--- a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Table.kt
+++ b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/Table.kt
@@ -16,4 +16,22 @@ class Table(
var start: Int? = null
var end: Int? = null
+ override fun buildContent(contentExtractor: (CodeBlock) -> String): String {
+ val builder = StringBuilder()
+ caption?.let {
+ builder.append("\n.${it}\n")
+ }
+ builder.append("[%header,format=csv")
+ attributes.forEach { (k, v) ->
+ builder.append(",${k}=${v}")
+ }
+ builder.append("]\n|===\n")
+ builder.append(records.first().parser.headerNames.joinToString(",")).append("\n")
+ records.forEach { record ->
+ builder.append(record.joinToString(",")).append("\n")
+ }
+ builder.append("|===\n")
+ return builder.toString()
+ }
+
}
diff --git a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/ThematicBreak.kt b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/ThematicBreak.kt
index 33572e33..7a90e350 100644
--- a/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/ThematicBreak.kt
+++ b/core/src/test/kotlin/org/neo4j/graphql/asciidoc/ast/ThematicBreak.kt
@@ -1,3 +1,8 @@
package org.neo4j.graphql.asciidoc.ast
-class ThematicBreak: StructuralNode(null)
+class ThematicBreak: StructuralNode(null) {
+
+ override fun buildContent(contentExtractor: (CodeBlock) -> String): String {
+ return "\n'''\n"
+ }
+}
diff --git a/core/src/test/kotlin/org/neo4j/graphql/domain/CodeBlockPredicate.kt b/core/src/test/kotlin/org/neo4j/graphql/domain/CodeBlockPredicate.kt
new file mode 100644
index 00000000..1bd4d29a
--- /dev/null
+++ b/core/src/test/kotlin/org/neo4j/graphql/domain/CodeBlockPredicate.kt
@@ -0,0 +1,28 @@
+package org.neo4j.graphql.domain
+
+import org.neo4j.graphql.asciidoc.ast.CodeBlock
+
+
+class CodeBlockPredicate private constructor(
+ private val language: String,
+ private val filter: Map = emptyMap(),
+ private val exactly: Boolean = false
+) {
+
+ fun matches(codeBlock: CodeBlock) = codeBlock.matches(language, filter, exactly)
+
+ companion object {
+ val CYPHER = CodeBlockPredicate("cypher", exactly = true)
+ val CYPHER_PARAMS = CodeBlockPredicate("json", exactly = true)
+ val GRAPHQL_SOURCE_SCHEMA = CodeBlockPredicate("graphql", mapOf("schema" to "true"))
+ val GRAPHQL_AUGMENTED_SCHEMA = CodeBlockPredicate("graphql", mapOf("augmented" to "true"))
+ val GRAPHQL_REQUEST = CodeBlockPredicate("graphql", mapOf("request" to "true"))
+ val GRAPHQL_REQUEST_VARIABLES = CodeBlockPredicate("json", mapOf("request" to "true"))
+ val GRAPHQL_RESPONSE = CodeBlockPredicate("json", mapOf("response" to "true"))
+ val QUERY_CONFIG = CodeBlockPredicate("json", mapOf("query-config" to "true"))
+ val SCHEMA_CONFIG = CodeBlockPredicate("json", mapOf("schema-config" to "true"))
+ val TEST_DATA = CodeBlockPredicate("cypher", mapOf("test-data" to "true"))
+ val CUSTOM_RESOLVER = CodeBlockPredicate("kotlin")
+ }
+
+}
diff --git a/core/src/test/kotlin/org/neo4j/graphql/domain/TestCase.kt b/core/src/test/kotlin/org/neo4j/graphql/domain/TestCase.kt
new file mode 100644
index 00000000..a7b0fbeb
--- /dev/null
+++ b/core/src/test/kotlin/org/neo4j/graphql/domain/TestCase.kt
@@ -0,0 +1,67 @@
+package org.neo4j.graphql.domain
+
+import org.neo4j.graphql.asciidoc.ast.CodeBlock
+import org.neo4j.graphql.asciidoc.ast.Section
+import org.neo4j.graphql.asciidoc.ast.Table
+import org.neo4j.graphql.domain.CodeBlockPredicate.Companion.CUSTOM_RESOLVER
+import org.neo4j.graphql.domain.CodeBlockPredicate.Companion.CYPHER
+import org.neo4j.graphql.domain.CodeBlockPredicate.Companion.CYPHER_PARAMS
+import org.neo4j.graphql.domain.CodeBlockPredicate.Companion.GRAPHQL_AUGMENTED_SCHEMA
+import org.neo4j.graphql.domain.CodeBlockPredicate.Companion.GRAPHQL_REQUEST
+import org.neo4j.graphql.domain.CodeBlockPredicate.Companion.GRAPHQL_REQUEST_VARIABLES
+import org.neo4j.graphql.domain.CodeBlockPredicate.Companion.GRAPHQL_RESPONSE
+import org.neo4j.graphql.domain.CodeBlockPredicate.Companion.GRAPHQL_SOURCE_SCHEMA
+import org.neo4j.graphql.domain.CodeBlockPredicate.Companion.QUERY_CONFIG
+import org.neo4j.graphql.domain.CodeBlockPredicate.Companion.SCHEMA_CONFIG
+import org.neo4j.graphql.domain.CodeBlockPredicate.Companion.TEST_DATA
+import kotlin.reflect.KMutableProperty1
+
+data class TestCase(
+ val setup: Setup,
+ var cypher: CodeBlock? = null,
+ var cypherParams: CodeBlock? = null,
+ var graphqlRequest: CodeBlock? = null,
+ var graphqlRequestVariables: CodeBlock? = null,
+ var graphqlResponse: CodeBlock? = null,
+ var graphqlResponseAssertions: Table? = null,
+ var queryConfig: CodeBlock? = null,
+ var augmentedSchema: CodeBlock? = null,
+) {
+
+ data class Setup(
+ val schema: CodeBlock?,
+ val schemaConfig: CodeBlock?,
+ val testData: List,
+ val customResolver: CodeBlock?,
+ ) {
+ constructor(section: Section) : this(
+ section.findSetupCodeBlocks(GRAPHQL_SOURCE_SCHEMA).firstOrNull(),
+ section.findSetupCodeBlocks(SCHEMA_CONFIG).firstOrNull(),
+ section.findSetupCodeBlocks(TEST_DATA),
+ section.findSetupCodeBlocks(CUSTOM_RESOLVER).firstOrNull()
+ )
+ }
+
+ fun parseCodeBlock(codeBlock: CodeBlock) {
+ when {
+ CYPHER.matches(codeBlock) -> init(codeBlock, TestCase::cypher)
+ CYPHER_PARAMS.matches(codeBlock) -> init(codeBlock, TestCase::cypherParams)
+ GRAPHQL_REQUEST.matches(codeBlock) -> init(codeBlock, TestCase::graphqlRequest)
+ GRAPHQL_REQUEST_VARIABLES.matches(codeBlock) -> init(codeBlock, TestCase::graphqlRequestVariables)
+ GRAPHQL_RESPONSE.matches(codeBlock) -> init(codeBlock, TestCase::graphqlResponse)
+ QUERY_CONFIG.matches(codeBlock) -> init(codeBlock, TestCase::queryConfig)
+ GRAPHQL_AUGMENTED_SCHEMA.matches(codeBlock) -> init(codeBlock, TestCase::augmentedSchema)
+ }
+ }
+
+ fun parseTable(table: Table) {
+ if (table.attributes.containsKey("response")) {
+ graphqlResponseAssertions = table
+ }
+ }
+
+ private fun init(current: V, prop: KMutableProperty1) {
+ check(prop.get(this) == null) { "Only one ${prop.name} block is allowed per test case" }
+ prop.set(this, current)
+ }
+}
diff --git a/core/src/test/kotlin/org/neo4j/graphql/factories/AsciiDocTestFactory.kt b/core/src/test/kotlin/org/neo4j/graphql/factories/AsciiDocTestFactory.kt
index b2f42ce6..190e9e5e 100644
--- a/core/src/test/kotlin/org/neo4j/graphql/factories/AsciiDocTestFactory.kt
+++ b/core/src/test/kotlin/org/neo4j/graphql/factories/AsciiDocTestFactory.kt
@@ -1,52 +1,28 @@
package org.neo4j.graphql.factories
-import com.fasterxml.jackson.core.JsonGenerator
-import com.fasterxml.jackson.databind.JsonSerializer
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.databind.SerializerProvider
-import com.fasterxml.jackson.databind.module.SimpleModule
-import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
-import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import com.intellij.rt.execution.junit.FileComparisonFailure
import org.junit.jupiter.api.DynamicContainer
import org.junit.jupiter.api.DynamicNode
import org.junit.jupiter.api.DynamicTest
import org.neo4j.graphql.asciidoc.AsciiDocParser
import org.neo4j.graphql.asciidoc.ast.*
-import java.io.File
-import java.io.FileWriter
-import java.time.temporal.TemporalAmount
+import org.neo4j.graphql.domain.TestCase
+import org.neo4j.graphql.domain.TestCase.Setup
+import org.neo4j.graphql.utils.TestUtils
+import java.nio.file.Path
import java.util.*
import java.util.stream.Stream
-import kotlin.reflect.KMutableProperty1
+import kotlin.io.path.writeText
/**
- * @param fileName the name of the test file
- * @param relevantBlocks a list of pairs of filter functions and properties to set the found code blocks
+ * @param file the test file
*/
-abstract class AsciiDocTestFactory(
- protected val fileName: String,
- private val relevantBlocks: List>,
+abstract class AsciiDocTestFactory(
+ protected val file: Path,
private val createMissingBlocks: Boolean = true
) {
- abstract class CodeBlockMatcher(
- val language: String,
- val filter: Map = emptyMap(),
- val exactly: Boolean = false
- ) {
- abstract fun set(testData: T, codeBlock: CodeBlock)
- }
-
-
- protected val srcLocation = File("src/test/resources/", fileName).toURI()
-
- private val document = AsciiDocParser(fileName).parse()
-
- /**
- * all parsed code blocks of the test file
- */
- private val knownBlocks = collectBlocks(document).toMutableList()
+ private val document = AsciiDocParser(file).parse()
fun generateTests(): Stream {
val tests = createTestsOfSection(document).toMutableList()
@@ -55,54 +31,37 @@ abstract class AsciiDocTestFactory(
// this test prints out the adjusted test file
tests += DynamicTest.dynamicTest(
"Write updated Testfile",
- srcLocation,
+ file.toUri(),
this@AsciiDocTestFactory::writeAdjustedTestFile
)
- } else if (REFORMAT_TEST_FILE) {
- tests += DynamicTest.dynamicTest(
- "Reformat Testfile",
- srcLocation,
- this@AsciiDocTestFactory::reformatTestFile
- )
} else if (GENERATE_TEST_FILE_DIFF) {
// this test prints out the adjusted test file
tests += DynamicTest.dynamicTest(
"Adjusted Tests",
- srcLocation,
+ file.toUri(),
this@AsciiDocTestFactory::printAdjustedTestFile
)
}
- addAdditionalTests(tests)
-
- return if (FLATTEN_TESTS) flatten(tests.stream(), "$fileName:") else tests.stream()
- }
-
- open fun addAdditionalTests(tests: MutableList) {
+ return if (FLATTEN_TESTS) {
+ val relativeFile = Path.of(TestUtils.TEST_RESOURCES).relativize(file)
+ flatten(tests.stream(), "$relativeFile:")
+ } else {
+ tests.stream()
+ }
}
private fun createTestsOfSection(section: Section, parentIgnoreReason: String? = null): List {
val tests = mutableListOf()
- var testCase = createTestCase(section)
+ val setup = Setup(section)
+ var testCase = TestCase(setup)
var ignoreReason: String? = null
for (node in section.blocks) {
when (node) {
- is CodeBlock -> {
- for (matcher in relevantBlocks) {
- if (testCase != null && node.matches(matcher.language, matcher.filter, matcher.exactly)) {
- matcher.set(testCase, node)
- }
- }
+ is CodeBlock -> testCase.parseCodeBlock(node)
-
- }
-
- is Table -> {
- if (testCase != null) {
- setTableData(testCase, node)
- }
- }
+ is Table -> testCase.parseTable(node)
is Block -> {
val blockContent = node.content.trim()
@@ -112,11 +71,9 @@ abstract class AsciiDocTestFactory(
}
is ThematicBreak -> {
- if (testCase != null) {
- tests += createTests(testCase, section, ignoreReason ?: parentIgnoreReason)
- }
+ tests += createTests(testCase, section, ignoreReason ?: parentIgnoreReason)
ignoreReason = null
- testCase = createTestCase(section) ?: continue
+ testCase = TestCase(setup)
}
is Section -> {
@@ -127,17 +84,12 @@ abstract class AsciiDocTestFactory(
}
}
}
- if (testCase != null) {
- tests += createTests(testCase, section, ignoreReason ?: parentIgnoreReason)
- }
+ tests += createTests(testCase, section, ignoreReason ?: parentIgnoreReason)
return tests
}
- abstract fun createTestCase(section: Section): T?
-
- abstract fun createTests(testCase: T, section: Section, ignoreReason: String?): List
- open fun setTableData(testCase: T, table: Table) {}
+ abstract fun createTests(testCase: TestCase, section: Section, ignoreReason: String?): List
private fun flatten(stream: Stream, name: String): Stream {
return stream.flatMap {
@@ -149,28 +101,16 @@ abstract class AsciiDocTestFactory(
}
}
- private fun collectBlocks(node: StructuralNode): List {
- return when (node) {
- is CodeBlock -> listOf(node)
- else -> node.blocks.flatMap { collectBlocks(it) }
- }
- }
-
private fun writeAdjustedTestFile() {
- val content = generateAdjustedFileContent(
- { it.generatedContent },
- { !UPDATE_SEMANTIC_EQUALLY_BLOCKS || (it.semanticEqual && (it.tandemUpdate?.semanticEqual ?: true)) }
- )
- FileWriter(File("src/test/resources/", fileName)).use {
- it.write(content)
+ val content = generateAdjustedFileContent {
+ if (UPDATE_SEMANTIC_EQUALLY_BLOCKS && it.semanticEqual && (it.tandemUpdate?.semanticEqual != false))
+ it.generatedContent
+ else
+ it.content
+ it.generatedContent ?: it.content
}
- }
- private fun reformatTestFile() {
- val content = generateAdjustedFileContent({ it.reformattedContent })
- FileWriter(File("src/test/resources/", fileName)).use {
- it.write(content)
- }
+ file.writeText(content)
}
private fun printAdjustedTestFile() {
@@ -179,34 +119,14 @@ abstract class AsciiDocTestFactory(
// This special exception will be handled by intellij so that you can diff directly with the file
throw FileComparisonFailure(
null, document.content, rebuildTest,
- File("src/test/resources/", fileName).absolutePath, null
+ file.toFile().absolutePath, null
)
}
}
protected fun generateAdjustedFileContent(
- extractor: (CodeBlock) -> String? = { it.generatedContent },
- matcher: (CodeBlock) -> Boolean = { extractor(it) != null }
- ): String {
- knownBlocks.sortWith(compareByDescending { it.start })
- val rebuildTest = StringBuffer(document.content)
- knownBlocks.filter { matcher(it) }
- .forEach { block ->
- val start = block.start ?: error("unknown start position")
- if (block.end == null) {
- rebuildTest.insert(
- start,
- ".${block.caption}\n${block.adjustedMarker}\n----\n${extractor(block)}\n----\n\n"
- )
- } else {
- rebuildTest.replace(start, block.end!!, extractor(block) + "\n")
- if (block.markerStart != null) {
- rebuildTest.replace(block.markerStart!!, block.markerEnd!!, block.adjustedMarker)
- }
- }
- }
- return rebuildTest.toString()
- }
+ extractor: (CodeBlock) -> String = { it.generatedContent ?: it.content },
+ ): String = document.buildContent(extractor)
fun createCodeBlock(
insertPoint: CodeBlock,
@@ -222,8 +142,7 @@ abstract class AsciiDocTestFactory(
caption = headline
content = ""
}
- codeBlock.start = (insertPoint.end ?: error("no start for block defined")) + 6
- knownBlocks += codeBlock
+ insertPoint.parent.addAfter(insertPoint, codeBlock)
return codeBlock
}
@@ -234,89 +153,9 @@ abstract class AsciiDocTestFactory(
val FLATTEN_TESTS = System.getProperty("neo4j-graphql-java.flatten-tests", "false") == "true"
val GENERATE_TEST_FILE_DIFF =
System.getProperty("neo4j-graphql-java.generate-test-file-diff", "false") == "true"
- val REFORMAT_TEST_FILE = System.getProperty("neo4j-graphql-java.reformat", "false") == "true"
val UPDATE_TEST_FILE = System.getProperty("neo4j-graphql-java.update-test-file", "false") == "true"
val UPDATE_SEMANTIC_EQUALLY_BLOCKS =
System.getProperty("neo4j-graphql-java.update-semantic-equally-blocks", "false") == "true"
- val MAPPER = ObjectMapper()
- .registerKotlinModule()
- .registerModules(JavaTimeModule())
- .registerModule(
- SimpleModule().addSerializer(
- TemporalAmount::class.java,
- object : JsonSerializer() {
- override fun serialize(
- value: TemporalAmount?,
- gen: JsonGenerator?,
- serializers: SerializerProvider?
- ) {
- gen?.writeString(value.toString())
- }
- })
- )
- .disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
- .disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
-
-
- fun String.parseJsonMap(): Map = this.let {
- @Suppress("UNCHECKED_CAST")
- MAPPER.readValue(this, Map::class.java) as Map
- }
-
-
- /**
- * Find all directly nested code blocks of a given section matching the language and filter
- */
- private fun findCodeBlocks(
- section: Section,
- language: String,
- filter: Map = emptyMap()
- ): List =
- section.blocks
- .filterIsInstance()
- .filter { it.matches(language, filter) }
-
- /**
- * Find all setup blocks for a given section, including the setup blocks of the parent sections
- */
- fun findSetupCodeBlocks(
- section: Section,
- language: String,
- fiter: Map = emptyMap()
- ): List {
- val result = mutableListOf()
- var currentSection: Section? = section
- while (currentSection != null) {
- result.addAll(findCodeBlocks(currentSection, language, fiter))
- currentSection.blocks
- .filterIsInstance()
- .filter { it.title == "Setup" }
- .forEach { result.addAll(findCodeBlocks(it, language, fiter)) }
- currentSection = currentSection.parent
- }
- return result
- }
-
- fun matcher(
- language: String,
- filter: Map = emptyMap(),
- exactly: Boolean = false,
- setter: KMutableProperty1
- ): CodeBlockMatcher =
- matcher(language, filter, exactly) { testData, codeBlock -> setter.set(testData, codeBlock) }
-
- fun matcher(
- language: String,
- filter: Map = emptyMap(),
- exactly: Boolean = false,
- setter: (T, CodeBlock) -> Unit
- ): CodeBlockMatcher =
- object : CodeBlockMatcher(language, filter, exactly) {
- override fun set(testData: T, codeBlock: CodeBlock) {
- setter(testData, codeBlock)
- }
- }
-
}
}
diff --git a/core/src/test/kotlin/org/neo4j/graphql/factories/CypherTestFactory.kt b/core/src/test/kotlin/org/neo4j/graphql/factories/CypherTestFactory.kt
index 036fafe0..1ca023e1 100644
--- a/core/src/test/kotlin/org/neo4j/graphql/factories/CypherTestFactory.kt
+++ b/core/src/test/kotlin/org/neo4j/graphql/factories/CypherTestFactory.kt
@@ -1,7 +1,5 @@
package org.neo4j.graphql.factories
-import com.fasterxml.jackson.module.kotlin.readValue
-import com.jayway.jsonpath.JsonPath
import graphql.ExceptionWhileDataFetching
import graphql.ExecutionInput
import graphql.GraphQL
@@ -12,98 +10,45 @@ import graphql.schema.FieldCoordinates
import graphql.schema.GraphQLCodeRegistry
import graphql.schema.GraphQLSchema
import org.assertj.core.api.Assertions
-import org.assertj.core.api.InstanceOfAssertFactories
import org.junit.jupiter.api.Assumptions
import org.junit.jupiter.api.DynamicNode
import org.junit.jupiter.api.DynamicTest
-import org.neo4j.cypherdsl.core.renderer.Configuration
-import org.neo4j.cypherdsl.core.renderer.Dialect
-import org.neo4j.cypherdsl.core.renderer.Renderer
-import org.neo4j.cypherdsl.parser.CypherParser
-import org.neo4j.cypherdsl.parser.Options
import org.neo4j.driver.Driver
import org.neo4j.graphql.QueryContext
import org.neo4j.graphql.SchemaBuilder
import org.neo4j.graphql.SchemaConfig
import org.neo4j.graphql.asciidoc.ast.CodeBlock
import org.neo4j.graphql.asciidoc.ast.Section
-import org.neo4j.graphql.asciidoc.ast.Table
import org.neo4j.graphql.custom_resolver.TestDataFetcher
+import org.neo4j.graphql.domain.TestCase
import org.neo4j.graphql.driver.adapter.Neo4jAdapter
import org.neo4j.graphql.driver.adapter.Neo4jDriverAdapter
-import org.neo4j.graphql.scalars.TemporalScalar
-import org.neo4j.graphql.utils.InvalidQueryException
+import org.neo4j.graphql.utils.Assertions.assertCypherParams
+import org.neo4j.graphql.utils.Assertions.assertEqualIgnoreOrder
+import org.neo4j.graphql.utils.Assertions.assertWithJsonPath
+import org.neo4j.graphql.utils.CypherUtils
+import org.neo4j.graphql.utils.JsonUtils
+import org.neo4j.graphql.utils.TestUtils.TEST_RESOURCES
import org.opentest4j.AssertionFailedError
-import org.threeten.extra.PeriodDuration
import java.io.File
-import java.io.FileWriter
-import java.math.BigInteger
-import java.time.*
-import java.time.format.DateTimeFormatter
-import java.time.format.DateTimeParseException
-import java.time.temporal.TemporalAccessor
+import java.nio.file.Path
import java.util.*
import java.util.concurrent.FutureTask
-import java.util.function.Consumer
import kotlin.reflect.full.findAnnotation
-class CypherTestFactory(fileName: String, val driver: Driver? = null, createMissingBlocks: Boolean = true) :
- AsciiDocTestFactory(
- fileName,
- listOf(
- matcher("cypher", exactly = true) { t, c -> t.cypher.add(c) },
- matcher("json", exactly = true) { t, c -> t.cypherParams.add(c) },
- matcher("graphql", exactly = true, setter = TestCase::graphqlRequest),
- matcher("json", mapOf("request" to "true"), setter = TestCase::graphqlRequestVariables),
- matcher("json", mapOf("response" to "true"), setter = TestCase::graphqlResponse),
- matcher("json", mapOf("query-config" to "true"), setter = TestCase::queryConfig),
- ),
- createMissingBlocks,
- ) {
+class CypherTestFactory(file: Path, private val driver: Driver? = null, createMissingBlocks: Boolean = true) :
+ AsciiDocTestFactory(file, createMissingBlocks) {
- data class TestCase(
- var schema: CodeBlock,
- var schemaConfig: CodeBlock?,
- var testData: List,
- var customResolver: CodeBlock?,
- var cypher: MutableList = mutableListOf(),
- var cypherParams: MutableList = mutableListOf(),
- var graphqlRequest: CodeBlock? = null,
- var graphqlRequestVariables: CodeBlock? = null,
- var graphqlResponse: CodeBlock? = null,
- var graphqlResponseAssertions: Table? = null,
- var queryConfig: CodeBlock? = null,
- )
+ constructor(file: String, driver: Driver?) : this(Path.of(TEST_RESOURCES, file), driver)
data class CypherResult(val query: String, val params: Map)
- override fun createTestCase(section: Section): TestCase? {
- val schema = findSetupCodeBlocks(section, "graphql", mapOf("schema" to "true")).firstOrNull() ?: return null
- val schemaConfig = findSetupCodeBlocks(section, "json", mapOf("schema-config" to "true")).firstOrNull()
- val testData = findSetupCodeBlocks(section, "cypher", mapOf("test-data" to "true"))
- val customResolver = findSetupCodeBlocks(section, "kotlin").firstOrNull()
-
- return TestCase(schema, schemaConfig, testData, customResolver)
- }
-
- override fun setTableData(testCase: TestCase, table: Table) {
- if (table.attributes.containsKey("response")) {
- testCase.graphqlResponseAssertions = table
- }
- }
-
- override fun addAdditionalTests(tests: MutableList) {
- if (ADD_IGNORE_ORDER_TO_INTEGRATION_TESTS) {
- tests += DynamicTest.dynamicTest("Create ignore-order", srcLocation, this::reformatMarker)
- }
- }
-
override fun createTests(testCase: TestCase, section: Section, ignoreReason: String?): List {
- if (testCase.graphqlRequest == null) {
+ if (testCase.graphqlRequest == null || testCase.setup.schema == null) {
return emptyList()
}
if (ignoreReason != null) {
- return listOf(DynamicTest.dynamicTest("Test Cypher", testCase.cypher.firstOrNull()?.uri) {
+ return listOf(DynamicTest.dynamicTest("Test Cypher", testCase.cypher?.uri) {
Assumptions.assumeFalse(true) { ignoreReason }
})
}
@@ -112,23 +57,12 @@ class CypherTestFactory(fileName: String, val driver: Driver? = null, createMiss
val tests = mutableListOf()
- if (ADD_IGNORE_ORDER_TO_INTEGRATION_TESTS) {
- val hasOrder = testCase.cypher.any { it.content.contains("ORDER BY") }
- val graphqlResponse = testCase.graphqlResponse
- if (!hasOrder && graphqlResponse != null
- && hasArrayWithMoreThenOneItems(MAPPER.readValue(graphqlResponse.content))
- ) {
- graphqlResponse.adjustedAttributes = graphqlResponse.attributes.toMutableMap()
- .also { it["ignore-order"] = null }
- }
- }
-
if (DEBUG) {
tests.add(printGeneratedQuery(result))
tests.add(printReplacedParameter(result))
}
if (driver != null) {
- val testData = testCase.testData.firstOrNull()
+ val testData = testCase.setup.testData.firstOrNull()
val responseAssertions = testCase.graphqlResponseAssertions
var response = testCase.graphqlResponse
if (responseAssertions == null && response == null) {
@@ -140,67 +74,23 @@ class CypherTestFactory(fileName: String, val driver: Driver? = null, createMiss
tests.add(integrationTest(section.title, testCase))
}
}
- if (REFORMAT_TEST_FILE) {
- testCase.cypher.forEach { cypher ->
- val statement = CypherParser.parse(cypher.content, Options.defaultOptions())
- val query = Renderer.getRenderer(
- Configuration
- .newConfig()
- .withDialect(Dialect.NEO4J_5)
- .withIndentStyle(Configuration.IndentStyle.TAB)
- .withPrettyPrint(true)
- .build()
- ).render(statement)
- cypher.reformattedContent = query
- }
-
- (testCase.cypherParams.takeIf { it.isNotEmpty() }
- ?: createCodeBlock(testCase.cypher.first(), "json", "Cypher Params")?.let { listOf(it) }
- ?: emptyList())
- .filter { it.content.isNotBlank() }
- .forEach { params ->
- val cypherParams = params.content.parseJsonMap()
- params.reformattedContent = MAPPER
- .writerWithDefaultPrettyPrinter()
- .writeValueAsString(cypherParams.toSortedMap())
- }
- }
-
- tests.addAll(testCypher(section.title, testCase.cypher, result))
- tests.addAll(testCypherParams(testCase.cypher, testCase.cypherParams, result))
+ testCypher(testCase.cypher, result)?.let { tests.add(it) }
+ testCypherParams(testCase.cypher, testCase.cypherParams, result)?.let { tests.add(it) }
return tests
}
- private fun hasArrayWithMoreThenOneItems(value: Any): Boolean {
- when (value) {
- is Map<*, *> -> {
- return value.any {
- val mapValue = it.value
- mapValue != null && hasArrayWithMoreThenOneItems(mapValue)
- }
- }
-
- is Collection<*> -> {
- return value.size > 1 || value.filterNotNull().any { hasArrayWithMoreThenOneItems(it) }
- }
- }
- return false
- }
-
private fun createSchema(
- schemaBlock: CodeBlock,
- schemaConfigBlock: CodeBlock?,
- customResolver: CodeBlock?,
+ setup: TestCase.Setup,
neo4jAdapter: Neo4jAdapter = Neo4jAdapter.NO_OP
): GraphQLSchema {
- val schemaString = schemaBlock.content
- val schemaConfig = schemaConfigBlock?.content
- ?.let { return@let MAPPER.readValue(it, SchemaConfig::class.java) }
+ val schemaString = setup.schema?.content ?: error("missing schema")
+ val schemaConfig = setup.schemaConfig?.content
+ ?.let { return@let JsonUtils.parseJson(it) }
?: SchemaConfig()
val codeRegistryBuilder = GraphQLCodeRegistry.newCodeRegistry()
- customResolver?.let { registerTestResolver(it, codeRegistryBuilder) }
+ setup.customResolver?.let { registerTestResolver(it, codeRegistryBuilder) }
return SchemaBuilder.fromSchema(schemaString, schemaConfig)
.withNeo4jAdapter(neo4jAdapter)
@@ -216,9 +106,8 @@ class CypherTestFactory(fileName: String, val driver: Driver? = null, createMiss
if (path == customResolver.content) {
error("Custom resolver must be an include statement")
}
- val resolvedFile = srcLocation.resolve(path)
- val className = File(resolvedFile)
- .toRelativeString(File("src/test/kotlin").absoluteFile)
+ val resolvedFile = file.toUri().resolve(path)
+ val className = File(resolvedFile).toRelativeString(File("src/test/kotlin").absoluteFile)
.replace(".kt", "")
.replace("/", ".")
@@ -231,40 +120,33 @@ class CypherTestFactory(fileName: String, val driver: Driver? = null, createMiss
}
}
- private fun createTransformationTask(testCase: TestCase): () -> List {
+ private fun createTransformationTask(testCase: TestCase): () -> CypherResult {
val transformationTask = FutureTask {
val cypherResults = mutableListOf()
- val schema =
- createSchema(
- testCase.schema,
- testCase.schemaConfig,
- testCase.customResolver,
- object : Neo4jAdapter {
+ val schema = createSchema(testCase.setup, object : Neo4jAdapter {
- override fun getDialect(): Neo4jAdapter.Dialect = Neo4jAdapter.Dialect.NEO4J_5
+ override fun getDialect(): Neo4jAdapter.Dialect = Neo4jAdapter.Dialect.NEO4J_5
- override fun executeQuery(
- cypher: String,
- params: Map
- ): List