From f8c47c4d6049a5ad87bdf5e387d0b8f9d2c4ecd3 Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Sun, 10 Nov 2024 18:41:48 +0100 Subject: [PATCH 1/3] Improve testing framework - split up parsing asciidoc and test case generation - allow hierarchical lookup for setup code --- .../neo4j/graphql/TranslatorExceptionTests.kt | 43 +- .../neo4j/graphql/utils/AsciiDocTestSuite.kt | 412 +++++++----------- .../neo4j/graphql/utils/CypherTestSuite.kt | 260 ++++++----- .../graphql/utils/GraphQLSchemaTestSuite.kt | 54 ++- .../graphql/utils/asciidoc/AsciiDocParser.kt | 134 ++++++ .../neo4j/graphql/utils/asciidoc/ast/Block.kt | 11 + .../graphql/utils/asciidoc/ast/CodeBlock.kt | 59 +++ .../graphql/utils/asciidoc/ast/Document.kt | 11 + .../graphql/utils/asciidoc/ast/Section.kt | 14 + .../utils/asciidoc/ast/StructuralNode.kt | 7 + .../utils/asciidoc/ast/ThematicBreak.kt | 3 + core/src/test/resources/custom-fields.adoc | 6 +- core/src/test/resources/issues/gh-210.adoc | 5 +- .../gh-295-wrong-target-node-alias.adoc | 5 +- .../cypher/advanced-filtering.adoc | 7 +- .../tck-test-files/cypher/types/datetime.adoc | 5 +- 16 files changed, 591 insertions(+), 445 deletions(-) create mode 100644 core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/AsciiDocParser.kt create mode 100644 core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/Block.kt create mode 100644 core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/CodeBlock.kt create mode 100644 core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/Document.kt create mode 100644 core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/Section.kt create mode 100644 core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/StructuralNode.kt create mode 100644 core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/ThematicBreak.kt diff --git a/core/src/test/kotlin/org/neo4j/graphql/TranslatorExceptionTests.kt b/core/src/test/kotlin/org/neo4j/graphql/TranslatorExceptionTests.kt index d2fbee80..551b618a 100644 --- a/core/src/test/kotlin/org/neo4j/graphql/TranslatorExceptionTests.kt +++ b/core/src/test/kotlin/org/neo4j/graphql/TranslatorExceptionTests.kt @@ -1,5 +1,7 @@ package org.neo4j.graphql +import demo.org.neo4j.graphql.utils.asciidoc.ast.CodeBlock +import demo.org.neo4j.graphql.utils.asciidoc.ast.Section import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DynamicNode import org.junit.jupiter.api.DynamicTest @@ -7,41 +9,44 @@ import org.junit.jupiter.api.TestFactory import org.neo4j.graphql.utils.AsciiDocTestSuite import java.util.stream.Stream -class TranslatorExceptionTests : AsciiDocTestSuite("translator-tests1.adoc") { +class TranslatorExceptionTests : AsciiDocTestSuite("translator-tests1.adoc", emptyList()) { - @TestFactory - fun createTests(): Stream { - return generateTests() + override fun createTestCase(section: Section): CodeBlock? { + return findSetupCodeBlocks(section, "graphql", mapOf("schema" to "true")).firstOrNull() ?: return null } - override fun schemaTestFactory(schema: String): List { - val translator = Translator(SchemaBuilder.buildSchema(schema)) + override fun createTests(testCase: CodeBlock, section: Section, ignoreReason: String?): List { + if (section.title != "Tests") { + return emptyList() + } return listOf( DynamicTest.dynamicTest("unknownType") { Assertions.assertThrows(InvalidQueryException::class.java) { - translator.translate( - """ - { - company { - name - } - } + Translator(SchemaBuilder.buildSchema(testCase.content)).translate( """ + { + company { + name + } + } + """ ) } }, DynamicTest.dynamicTest("mutation") { Assertions.assertThrows(InvalidQueryException::class.java) { - translator.translate( + Translator(SchemaBuilder.buildSchema(testCase.content)).translate( """ - { - createPerson() - } - """.trimIndent() + { + createPerson() + } + """.trimIndent() ) } } - ) } + + @TestFactory + fun createTests(): Stream = generateTests() } diff --git a/core/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt b/core/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt index 58219385..1a96bd6f 100644 --- a/core/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt +++ b/core/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt @@ -3,206 +3,133 @@ package org.neo4j.graphql.utils import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.registerKotlinModule import com.intellij.rt.execution.junit.FileComparisonFailure +import demo.org.neo4j.graphql.utils.asciidoc.AsciiDocParser +import demo.org.neo4j.graphql.utils.asciidoc.ast.* import org.junit.jupiter.api.DynamicContainer import org.junit.jupiter.api.DynamicNode import org.junit.jupiter.api.DynamicTest import java.io.File import java.io.FileWriter -import java.net.URI import java.util.* -import java.util.regex.Pattern import java.util.stream.Stream -import javax.ws.rs.core.UriBuilder +import kotlin.reflect.KMutableProperty1 /** * @param fileName the name of the test file - * @param testCaseMarkers the markers for the test case - * @param globalMarkers the markers for global blocks + * @param relevantBlocks a list of pairs of filter functions and properties to set the found code blocks */ -open class AsciiDocTestSuite( +abstract class AsciiDocTestSuite( private val fileName: String, - private val testCaseMarkers: List = emptyList(), - private val globalMarkers: List = listOf(SCHEMA_MARKER) + private val relevantBlocks: List>, ) { + abstract class CodeBlockMatcher( + val language: String, + val filter: Map = emptyMap(), + val exactly: Boolean = false + ) { + abstract fun set(testData: T, codeBlock: CodeBlock) + } + + private val srcLocation = File("src/test/resources/", fileName).toURI() - private val fileContent: StringBuilder = StringBuilder() + private val document = AsciiDocParser(fileName).parse() /** - * all parsed blocks of the test file + * all parsed code blocks of the test file */ - private val knownBlocks: MutableList = mutableListOf() + private val knownBlocks = collectBlocks(document).toMutableList() fun generateTests(): Stream { - val stream = FileParser().parse() - return if (FLATTEN_TESTS) flatten(stream, "$fileName:") else stream - } - - private fun flatten(stream: Stream, name: String): Stream { - return stream.flatMap { - when (it) { - is DynamicContainer -> flatten(it.children, "$name[${it.displayName}]") - is DynamicTest -> Stream.of(DynamicTest.dynamicTest("$name[${it.displayName}]", it.executable)) - else -> throw IllegalArgumentException("unknown type ${it.javaClass.name}") - } + val tests = createTestsOfSection(document).toMutableList() + + if (UPDATE_TEST_FILE) { + // this test prints out the adjusted test file + tests += DynamicTest.dynamicTest( + "Write updated Testfile", + srcLocation, + this@AsciiDocTestSuite::writeAdjustedTestFile + ) + } else if (REFORMAT_TEST_FILE) { + tests += DynamicTest.dynamicTest("Reformat Testfile", srcLocation, this@AsciiDocTestSuite::reformatTestFile) + } else if (GENERATE_TEST_FILE_DIFF) { + // this test prints out the adjusted test file + tests += DynamicTest.dynamicTest( + "Adjusted Tests", + srcLocation, + this@AsciiDocTestSuite::printAdjustedTestFile + ) } - } - class ParsedBlock( - val marker: String, - val uri: URI, - var headline: String? = null - ) { - var start: Int? = null - var end: Int? = null - var adjustedCode: String? = null - var reformattedCode: String? = null - val code: StringBuilder = StringBuilder() - - fun code() = code.trim().toString() + return if (FLATTEN_TESTS) flatten(tests.stream(), "$fileName:") else tests.stream() } - private inner class FileParser { - - private var root: DocumentLevel? = null - private var currentDocumentLevel: DocumentLevel? = null - private var currentDepth = 0 - - private val globalCodeBlocks = mutableMapOf>() - private var codeBlocksOfTest = mutableMapOf>() - - fun parse(): Stream { - val file = File(AsciiDocTestSuite::class.java.getResource("/$fileName")?.toURI()!!) - val lines = file.readLines() - - var title: String? = null - var currentBlock: ParsedBlock? = null - var globalDone = false + private fun createTestsOfSection(section: Section, parentIgnoreReason: String? = null): List { + + val tests = mutableListOf() + var testCase = createTestCase(section) + 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) + } + } - var ignore = false - var inside = false - var offset = 0 - 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 { - !globalDone && globalMarkers.contains(line) -> currentBlock = - startBlock(line, lineNr, globalCodeBlocks) - - testCaseMarkers.contains(line) -> { - globalDone = true - currentBlock = startBlock(line, lineNr, codeBlocksOfTest) - } - line == "'''" -> { - createTests(title, lineNr, ignore) - currentBlock = null - ignore = false + is Block -> { + val blockContent = node.content.trim() + if (blockContent.startsWith("CAUTION:")) { + ignoreReason = blockContent.substring("CAUTION:".length).trim() } + } - line == "----" -> { - inside = !inside - if (inside) { - - currentBlock?.start = offset + line.length + 1 - - } else if (currentBlock != null) { - - currentBlock.end = offset - when (currentBlock.marker) { - - SCHEMA_MARKER -> { - val schemaTests = schemaTestFactory(currentBlock.code()) - currentDocumentLevel?.tests?.add(schemaTests) - if (testCaseMarkers.isEmpty()) { - break@loop - } - } - } - - } + is ThematicBreak -> { + if (testCase != null) { + tests += createTests(testCase, section, ignoreReason ?: parentIgnoreReason) } + ignoreReason = null + testCase = createTestCase(section) ?: continue + } - headlineMatcher.matches() -> { - val depth = headlineMatcher.group(1).length - title = headlineMatcher.group(2) - val uri = UriBuilder.fromUri(srcLocation).queryParam("line", lineNr + 1).build() - handleHeadline(title, uri, depth) + is Section -> { + val nestedTests = createTestsOfSection(node, ignoreReason ?: parentIgnoreReason) + if (nestedTests.isNotEmpty()) { + tests += DynamicContainer.dynamicContainer(node.title, node.uri, nestedTests.stream()) } - - line.startsWith("CAUTION:") -> ignore = true - - inside -> currentBlock?.code?.append(line)?.append("\n") - } - offset += line.length + 1 // +1 b/c of newline - } - - if (UPDATE_TEST_FILE) { - // this test prints out the adjusted test file - root?.afterTests?.add( - DynamicTest.dynamicTest( - "Write updated Testfile", - srcLocation, - this@AsciiDocTestSuite::writeAdjustedTestFile - ) - ) - } else if (REFORMAT_TEST_FILE) { - root?.afterTests?.add( - DynamicTest.dynamicTest("Reformat Testfile", srcLocation, this@AsciiDocTestSuite::reformatTestFile) - ) - } else if (GENERATE_TEST_FILE_DIFF) { - // this test prints out the adjusted test file - root?.afterTests?.add( - DynamicTest.dynamicTest( - "Adjusted Tests", - srcLocation, - this@AsciiDocTestSuite::printAdjustedTestFile - ) - ) } - return root?.generateTests() ?: Stream.empty() } - - private fun createTests(title: String?, lineNr: Int, ignore: Boolean) { - if (codeBlocksOfTest.isEmpty()) { - throw IllegalStateException("no code blocks for tests (line $lineNr)") - } - val tests = testFactory( - title ?: throw IllegalStateException("Title should be defined (line $lineNr)"), - globalCodeBlocks, - codeBlocksOfTest, - ignore - ) - currentDocumentLevel?.tests?.add(tests) - codeBlocksOfTest = mutableMapOf() + if (testCase != null) { + tests += createTests(testCase, section, ignoreReason ?: parentIgnoreReason) } + return tests + } - private fun handleHeadline(title: String, uri: URI, depth: Int) { - if (root == null) { - root = DocumentLevel(null, title, uri) - currentDocumentLevel = root - } else { - val parent = when { - depth > currentDepth -> currentDocumentLevel - depth == currentDepth -> currentDocumentLevel?.parent - ?: throw IllegalStateException("cannot create sub-level on null") - - else -> currentDocumentLevel?.parent?.parent - ?: throw IllegalStateException("cannot create sub-level on null") - } - currentDocumentLevel = DocumentLevel(parent, title, uri) + abstract fun createTestCase(section: Section): T? + + abstract fun createTests(testCase: T, section: Section, ignoreReason: String?): List + + private fun flatten(stream: Stream, name: String): Stream { + return stream.flatMap { + when (it) { + is DynamicContainer -> flatten(it.children, "$name[${it.displayName}]") + is DynamicTest -> Stream.of(DynamicTest.dynamicTest("$name[${it.displayName}]", it.executable)) + else -> throw IllegalArgumentException("unknown type ${it.javaClass.name}") } - currentDepth = depth } + } + private fun collectBlocks(node: StructuralNode): List { + return when (node) { + is CodeBlock -> listOf(node) + else -> node.blocks.flatMap { collectBlocks(it) } + } } private fun writeAdjustedTestFile() { @@ -213,7 +140,7 @@ open class AsciiDocTestSuite( } private fun reformatTestFile() { - val content = generateAdjustedFileContent { it.reformattedCode } + val content = generateAdjustedFileContent { it.reformattedContent } FileWriter(File("src/test/resources/", fileName)).use { it.write(content) } @@ -221,26 +148,25 @@ open class AsciiDocTestSuite( private fun printAdjustedTestFile() { val rebuildTest = generateAdjustedFileContent() - if (!Objects.equals(rebuildTest, fileContent.toString())) { + if (!Objects.equals(rebuildTest, document.content)) { // This special exception will be handled by intellij so that you can diff directly with the file throw FileComparisonFailure( - null, fileContent.toString(), rebuildTest, + null, document.content, rebuildTest, File("src/test/resources/", fileName).absolutePath, null ) } } - private fun generateAdjustedFileContent(extractor: (ParsedBlock) -> String? = { it.adjustedCode }): String { - knownBlocks.sortWith(compareByDescending { it.start } - .thenByDescending { testCaseMarkers.indexOf(it.marker) }) - val rebuildTest = StringBuffer(fileContent) + private fun generateAdjustedFileContent(extractor: (CodeBlock) -> String? = { it.generatedContent }): String { + knownBlocks.sortWith(compareByDescending { it.start }) + val rebuildTest = StringBuffer(document.content) knownBlocks.filter { extractor(it) != null } .forEach { block -> - val start = block.start ?: throw IllegalArgumentException("unknown start position") + val start = block.start ?: error("unknown start position") if (block.end == null) { rebuildTest.insert( start, - ".${block.headline}\n${block.marker}\n----\n${extractor(block)}\n----\n\n" + ".${block.caption}\n${block.marker}\n----\n${extractor(block)}\n----\n\n" ) } else { rebuildTest.replace(start, block.end!!, extractor(block) + "\n") @@ -249,43 +175,14 @@ open class AsciiDocTestSuite( return rebuildTest.toString() } - fun startBlock(marker: String, lineIndex: Int, blocks: MutableMap>): ParsedBlock { - val uri = UriBuilder.fromUri(srcLocation).queryParam("line", lineIndex + 1).build() - val block = ParsedBlock(marker, uri) - knownBlocks += block - blocks.computeIfAbsent(marker) { mutableListOf() }.add(block) - return block - } - - protected open fun testFactory( - title: String, - globalBlocks: Map>, - codeBlocks: Map>, - ignore: Boolean - ): List { - return emptyList() - } - - protected open fun schemaTestFactory(schema: String): List { - return emptyList() - } - - protected fun getOrCreateBlocks( - codeBlocks: Map>, - marker: String, - headline: String - ): List { - val blocks = codeBlocks[marker]?.toMutableList() ?: mutableListOf() - if (blocks.isEmpty() && (GENERATE_TEST_FILE_DIFF || UPDATE_TEST_FILE)) { - val insertPoints = testCaseMarkers.indexOf(marker).let { testCaseMarkers.subList(0, it).asReversed() } - val insertPoint = insertPoints.mapNotNull { codeBlocks[it]?.firstOrNull() }.firstOrNull() - ?: throw IllegalArgumentException("none of the insert points $insertPoints found in $fileName") - val block = ParsedBlock(marker, insertPoint.uri, headline) - block.start = (insertPoint.end ?: throw IllegalStateException("no start for block defined")) + 6 - knownBlocks += blocks - blocks += block + fun createCodeBlock(insertPoint: CodeBlock, marker: String, headline: String): CodeBlock? { + if (!GENERATE_TEST_FILE_DIFF && !UPDATE_TEST_FILE) { + return null } - return blocks + val codeBlock = CodeBlock.parseMeta(insertPoint.parent, insertPoint.uri, marker).also { it.caption = headline } + codeBlock.start = (insertPoint.end ?: error("no start for block defined")) + 6 + knownBlocks += codeBlock + return codeBlock } companion object { @@ -297,67 +194,66 @@ open class AsciiDocTestSuite( 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 MAPPER = ObjectMapper().registerKotlinModule() - val HEADLINE_PATTERN: Pattern = Pattern.compile("^(=+) (.*)$") - - const val SCHEMA_MARKER = "[source,graphql,schema=true]" - const val SCHEMA_CONFIG_MARKER = "[source,json,schema-config=true]" - - class DocumentLevel( - val parent: DocumentLevel?, - val name: String, - private val testSourceUri: URI - ) { - private val children = mutableListOf() - val tests = mutableListOf>() - val afterTests = mutableListOf() - - init { - parent?.children?.add(this) - } - - fun generateTests(): Stream { - val streamBuilder = Stream.builder() - if (tests.size > 1) { - if (children.isNotEmpty()) { - streamBuilder.add( - DynamicContainer.dynamicContainer( - name, - testSourceUri, - children.stream().flatMap { it.generateTests() }) - ) - } - for ((index, test) in tests.withIndex()) { - streamBuilder.add( - DynamicContainer.dynamicContainer( - name + " " + (index + 1), - testSourceUri, - test.stream() - ) - ) - } - } else { - val nodes = Stream.concat( - tests.stream().flatMap { it.stream() }, - children.stream().flatMap { it.generateTests() } - ) - streamBuilder.add(DynamicContainer.dynamicContainer(name, testSourceUri, nodes)) - } - afterTests.forEach { streamBuilder.add(it) } - return streamBuilder.build() - } - } fun String.parseJsonMap(): Map = this.let { @Suppress("UNCHECKED_CAST") MAPPER.readValue(this, Map::class.java) as Map } - fun String.normalize(): String = this - .replace(Regex("\\s+"), " ") - .replace(Regex(",(\\S)"), ", $1") - .replace(Regex("\\{(\\S)"), "{ $1") - .replace(Regex("(\\S)}"), "$1 }") - .replace(Regex(":(\\S)"), ": $1") + + /** + * 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" || it.title == "Schema" } + .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/utils/CypherTestSuite.kt b/core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt index f43fdd3f..775ca6d7 100644 --- a/core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt +++ b/core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt @@ -1,5 +1,7 @@ package org.neo4j.graphql.utils +import demo.org.neo4j.graphql.utils.asciidoc.ast.CodeBlock +import demo.org.neo4j.graphql.utils.asciidoc.ast.Section import graphql.ExecutionInput import graphql.GraphQL import graphql.schema.DataFetcher @@ -32,27 +34,49 @@ import java.util.function.Consumer import java.util.regex.Matcher import java.util.regex.Pattern -class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTestSuite( +class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTestSuite( fileName, - TEST_CASE_MARKERS, - GLOBAL_MARKERS + listOf( + matcher("cypher") { 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), + ) ) { - override fun testFactory( - title: String, - globalBlocks: Map>, - codeBlocks: Map>, - ignore: Boolean - ): List { - val cypherBlocks = getOrCreateBlocks(codeBlocks, CYPHER_MARKER, "Cypher") - - if (ignore) { - return Collections.singletonList(DynamicTest.dynamicTest("Test Cypher", cypherBlocks.firstOrNull()?.uri) { - Assumptions.assumeFalse(true) + data class TestCase( + var schema: CodeBlock, + var schemaConfig: CodeBlock?, + var testData: List, + var cypher: MutableList = mutableListOf(), + var cypherParams: MutableList = mutableListOf(), + var graphqlRequest: CodeBlock? = null, + var graphqlRequestVariables: CodeBlock? = null, + var graphqlResponse: CodeBlock? = null, + var queryConfig: CodeBlock? = null, + ) + + 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, "json", mapOf("test-data" to "true")) + + return TestCase(schema, schemaConfig, testData) + } + + override fun createTests(testCase: TestCase, section: Section, ignoreReason: String?): List { + if (testCase.graphqlRequest == null) { + return emptyList() + } + if (ignoreReason != null) { + return listOf(DynamicTest.dynamicTest("Test Cypher", testCase.cypher.firstOrNull()?.uri) { + Assumptions.assumeFalse(true) { ignoreReason } }) } - val result = createTransformationTask(title, globalBlocks, codeBlocks) + val result = createTransformationTask(testCase) val tests = mutableListOf() if (DEBUG) { @@ -60,21 +84,19 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest tests.add(printReplacedParameter(result)) } if (neo4j != null) { - val testData = globalBlocks[TEST_DATA_MARKER]?.firstOrNull() - var response = codeBlocks[GRAPHQL_RESPONSE_IGNORE_ORDER_MARKER]?.firstOrNull() - var ignoreOrder = false - if (response != null) { - ignoreOrder = true - } else { - response = getOrCreateBlocks(codeBlocks, GRAPHQL_RESPONSE_MARKER, "GraphQL-Response").firstOrNull() + val testData = testCase.testData.firstOrNull() + var response = testCase.graphqlResponse + if (response == null) { + response = createCodeBlock(testCase.graphqlRequest!!, GRAPHQL_RESPONSE_MARKER, "GraphQL-Response") + testCase.graphqlResponse = response } if (testData != null && response != null) { - tests.add(integrationTest(title, globalBlocks, codeBlocks, testData, response, ignoreOrder)) + tests.add(integrationTest(section.title, testCase)) } } if (REFORMAT_TEST_FILE) { - cypherBlocks.forEach { - val statement = CypherParser.parse(it.code(), Options.defaultOptions()) + testCase.cypher.forEach { cypher -> + val statement = CypherParser.parse(cypher.content, Options.defaultOptions()) val query = Renderer.getRenderer( Configuration .newConfig() @@ -82,53 +104,49 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest .withPrettyPrint(true) .build() ).render(statement) - it.reformattedCode = query - } - getOrCreateBlocks(codeBlocks, CYPHER_PARAMS_MARKER, "Cypher Params").forEach { - val cypherParams = it.code().parseJsonMap() - it.reformattedCode = MAPPER - .writerWithDefaultPrettyPrinter() - .writeValueAsString(cypherParams.toSortedMap()) + cypher.reformattedContent = query } + (testCase.cypherParams.takeIf { it.isNotEmpty() } + ?: createCodeBlock(testCase.cypher.first(), CYPHER_PARAMS_MARKER, "Cypher Params")?.let { listOf(it) } + ?: emptyList()) + .forEach { params -> + val cypherParams = params.content.parseJsonMap() + params.reformattedContent = MAPPER + .writerWithDefaultPrettyPrinter() + .writeValueAsString(cypherParams.toSortedMap()) + } } - tests.addAll(testCypher(title, cypherBlocks, result)) - tests.addAll(testCypherParams(codeBlocks, result)) + tests.addAll(testCypher(section.title, testCase.cypher, result)) + tests.addAll(testCypherParams(testCase.cypher, testCase.cypherParams, result)) return tests } private fun createSchema( - globalBlocks: Map>, - codeBlocks: Map>, + schemaBlock: CodeBlock, + schemaConfigBlock: CodeBlock?, dataFetchingInterceptor: DataFetchingInterceptor? = null ): GraphQLSchema { - val schemaString = globalBlocks[SCHEMA_MARKER]?.firstOrNull()?.code() - ?: throw IllegalStateException("Schema should be defined") - val schemaConfig = (codeBlocks[SCHEMA_CONFIG_MARKER]?.firstOrNull() - ?: globalBlocks[SCHEMA_CONFIG_MARKER]?.firstOrNull())?.code() + val schemaString = schemaBlock.content + val schemaConfig = schemaConfigBlock?.content ?.let { return@let MAPPER.readValue(it, SchemaConfig::class.java) } ?: SchemaConfig() return SchemaBuilder.buildSchema(schemaString, schemaConfig, dataFetchingInterceptor) } - private fun createTransformationTask( - title: String, - globalBlocks: Map>, - codeBlocks: Map> - ): () -> List { + private fun createTransformationTask(testCase: TestCase): () -> List { val transformationTask = FutureTask { - val schema = createSchema(globalBlocks, codeBlocks) + val schema = createSchema(testCase.schema, testCase.schemaConfig) - val request = codeBlocks[GRAPHQL_MARKER]?.firstOrNull()?.code() - ?: throw IllegalStateException("missing graphql for $title") + val request = testCase.graphqlRequest!!.content - val requestParams = codeBlocks[GRAPHQL_VARIABLES_MARKER]?.firstOrNull()?.code()?.parseJsonMap() + val requestParams = testCase.graphqlRequestVariables?.content?.parseJsonMap() ?: emptyMap() - val queryContext = codeBlocks[QUERY_CONFIG_MARKER]?.firstOrNull()?.code() + val queryContext = testCase.queryConfig?.content ?.let { config -> return@let MAPPER.readValue(config, QueryContext::class.java) } ?: QueryContext() @@ -163,7 +181,7 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest private fun testCypher( title: String, - cypherBlocks: List, + cypherBlocks: List, result: () -> List ): List = cypherBlocks.mapIndexed { index, cypherBlock -> var name = "Test Cypher" @@ -177,22 +195,22 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest .build() val renderer = Renderer.getRenderer(cfg) - val cypher = cypherBlock.code() + val cypher = cypherBlock.content val expectedNormalized = renderer.render(CypherParser.parse(cypher, PARSE_OPTIONS)) val actual = (result().getOrNull(index)?.query ?: throw IllegalStateException("missing cypher query for $title ($index)")) val actualNormalized = renderer.render(CypherParser.parse(actual, PARSE_OPTIONS)) if (!Objects.equals(expectedNormalized, actual)) { - cypherBlock.adjustedCode = actual + cypherBlock.generatedContent = actual } if (actualNormalized != expectedNormalized) { - val SPLITTER = + val splitter = "\n\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n~ source query\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n" throw AssertionFailedError( "Cypher does not match", - expectedNormalized + SPLITTER + cypher, - actualNormalized + SPLITTER + actual + expectedNormalized + splitter + cypher, + actualNormalized + splitter + actual ) // TODO // throw AssertionFailedError("Cypher does not match", cypher, actual) @@ -201,11 +219,10 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest } private fun testCypherParams( - codeBlocks: Map>, + cypherBlocks: List, + cypherParamsBlocks: List, result: () -> List ): List { - val cypherParamsBlocks = getOrCreateBlocks(codeBlocks, CYPHER_PARAMS_MARKER, "Cypher Params") - return cypherParamsBlocks.mapIndexed { index, cypherParamsBlock -> var name = "Test Cypher Params" if (cypherParamsBlocks.size > 1) { @@ -216,21 +233,21 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest ?: throw IllegalStateException("Expected a cypher query with index $index") val actualParamsJson = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(params) - if (cypherParamsBlock.code().isBlank()) { + if (cypherParamsBlock.content.isBlank()) { if (params.isNotEmpty()) { - cypherParamsBlock.adjustedCode = actualParamsJson + cypherParamsBlock.generatedContent = actualParamsJson Assertions.fail("No params defined") } return@dynamicTest } - val expectedCypherParams = cypherParamsBlock.code().parseJsonMap() + val expectedCypherParams = cypherParamsBlock.content.parseJsonMap() val expected = fixNumbers(expectedCypherParams) val actual = fixNumbers(actualParamsJson.parseJsonMap()) if (!Objects.equals(expected, actual)) { - cypherParamsBlock.adjustedCode = actualParamsJson + cypherParamsBlock.generatedContent = actualParamsJson } - val expectedRenamedParameters = codeBlocks[CYPHER_MARKER]?.get(index)?.code() + val expectedRenamedParameters = cypherBlocks.getOrNull(index)?.content ?.let { CypherParser.parse(it, PARSE_OPTIONS).catalog.renamedParameters } if (expectedRenamedParameters != null) { @@ -249,17 +266,15 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest } } - private fun setupDataFetchingInterceptor(testData: ParsedBlock): DataFetchingInterceptor { + private fun setupDataFetchingInterceptor(testData: CodeBlock?): DataFetchingInterceptor { return object : DataFetchingInterceptor { override fun fetchData(env: DataFetchingEnvironment, delegate: DataFetcher): Any? = neo4j ?.defaultDatabaseService()?.let { db -> db.executeTransactionally("MATCH (n) DETACH DELETE n") - if (testData.code().isNotBlank()) { - testData.code() - .split(";") - .filter { it.isNotBlank() } - .forEach { db.executeTransactionally(it) } - } + testData?.content + ?.split(";") + ?.filter { it.isNotBlank() } + ?.forEach { db.executeTransactionally(it) } val (cypher, params, type, variable) = delegate.get(env) return db.executeTransactionally(cypher, params) { result -> result.stream().map { it[variable] }.let { @@ -276,51 +291,51 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest private fun integrationTest( title: String, - globalBlocks: Map>, - codeBlocks: Map>, - testData: ParsedBlock, - response: ParsedBlock, - ignoreOrder: Boolean - ): DynamicNode = DynamicTest.dynamicTest("Integration Test", response.uri) { - val dataFetchingInterceptor = setupDataFetchingInterceptor(testData) - val request = codeBlocks[GRAPHQL_MARKER]?.firstOrNull()?.code() - ?: throw IllegalStateException("missing graphql for $title") - - - val requestParams = codeBlocks[GRAPHQL_VARIABLES_MARKER]?.firstOrNull()?.code()?.parseJsonMap() ?: emptyMap() - - val queryContext = codeBlocks[QUERY_CONFIG_MARKER]?.firstOrNull()?.code() - ?.let { config -> return@let MAPPER.readValue(config, QueryContext::class.java) } - ?: QueryContext() - - - val schema = createSchema(globalBlocks, codeBlocks, dataFetchingInterceptor) - val graphql = GraphQL.newGraphQL(schema).build() - val result = graphql.execute( - ExecutionInput.newExecutionInput() - .query(request) - .variables(requestParams) - .graphQLContext(mapOf(QueryContext.KEY to queryContext)) - .build() - ) - Assertions.assertThat(result.errors).isEmpty() - - val values = result?.getData() - - if (response.code.isEmpty()) { - val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values) - response.adjustedCode = actualCode - } else { - val expected = fixNumbers(response.code().parseJsonMap()) - val actual = fixNumber(values) - if (!Objects.equals(expected, actual)) { + testCase: TestCase, + ): DynamicNode { + val graphqlResponse = testCase.graphqlResponse + ?: error("missing graphql response for $title") + + return DynamicTest.dynamicTest("Integration Test", graphqlResponse.uri) { + val dataFetchingInterceptor = setupDataFetchingInterceptor(testCase.testData.firstOrNull()) + val request = testCase.graphqlRequest?.content + ?: error("missing graphql for $title") + + val requestParams = testCase.graphqlRequestVariables?.content?.parseJsonMap() ?: emptyMap() + + val queryContext = testCase.queryConfig?.content + ?.let { config -> return@let MAPPER.readValue(config, QueryContext::class.java) } + ?: QueryContext() + + + val schema = createSchema(testCase.schema, testCase.schemaConfig, dataFetchingInterceptor) + val graphql = GraphQL.newGraphQL(schema).build() + val result = graphql.execute( + ExecutionInput.newExecutionInput() + .query(request) + .variables(requestParams) + .graphQLContext(mapOf(QueryContext.KEY to queryContext)) + .build() + ) + Assertions.assertThat(result.errors).isEmpty() + + val values = result?.getData() + + if (graphqlResponse.content.isEmpty()) { val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values) - response.adjustedCode = actualCode - } - if (ignoreOrder) { - assertEqualIgnoreOrder(expected, actual) + graphqlResponse.generatedContent = actualCode } else { - Assertions.assertThat(actual).isEqualTo(expected) + val expected = fixNumbers(graphqlResponse.content.parseJsonMap()) + val actual = fixNumber(values) + if (!Objects.equals(expected, actual)) { + val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values) + graphqlResponse.generatedContent = actualCode + } + if (graphqlResponse.attributes.containsKey("ignore-order")) { + assertEqualIgnoreOrder(expected, actual) + } else { + Assertions.assertThat(actual).isEqualTo(expected) + } } } } @@ -348,26 +363,9 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest private val DEBUG = System.getProperty("neo4j-graphql-java.debug", "false") == "true" private val CONVERT_NUMBER = System.getProperty("neo4j-graphql-java.convert-number", "true") == "true" - private const val TEST_DATA_MARKER = "[source,cypher,test-data=true]" - private const val CYPHER_MARKER = "[source,cypher]" - private const val GRAPHQL_MARKER = "[source,graphql]" - private const val GRAPHQL_VARIABLES_MARKER = "[source,json,request=true]" private const val GRAPHQL_RESPONSE_MARKER = "[source,json,response=true]" - private const val GRAPHQL_RESPONSE_IGNORE_ORDER_MARKER = "[source,json,response=true,ignore-order]" - private const val QUERY_CONFIG_MARKER = "[source,json,query-config=true]" private const val CYPHER_PARAMS_MARKER = "[source,json]" - private val TEST_CASE_MARKERS: List = listOf( - SCHEMA_CONFIG_MARKER, - GRAPHQL_MARKER, - GRAPHQL_VARIABLES_MARKER, - GRAPHQL_RESPONSE_MARKER, - GRAPHQL_RESPONSE_IGNORE_ORDER_MARKER, - QUERY_CONFIG_MARKER, - CYPHER_PARAMS_MARKER, - CYPHER_MARKER - ) - private val GLOBAL_MARKERS: List = listOf(SCHEMA_MARKER, SCHEMA_CONFIG_MARKER, TEST_DATA_MARKER) private val DURATION_PATTERN: Pattern = Pattern.compile("^P(.*?)(?:T(.*))?$") diff --git a/core/src/test/kotlin/org/neo4j/graphql/utils/GraphQLSchemaTestSuite.kt b/core/src/test/kotlin/org/neo4j/graphql/utils/GraphQLSchemaTestSuite.kt index 694fc2a8..83182ff1 100644 --- a/core/src/test/kotlin/org/neo4j/graphql/utils/GraphQLSchemaTestSuite.kt +++ b/core/src/test/kotlin/org/neo4j/graphql/utils/GraphQLSchemaTestSuite.kt @@ -1,5 +1,7 @@ package org.neo4j.graphql.utils +import demo.org.neo4j.graphql.utils.asciidoc.ast.CodeBlock +import demo.org.neo4j.graphql.utils.asciidoc.ast.Section import graphql.language.InterfaceTypeDefinition import graphql.language.UnionTypeDefinition import graphql.schema.GraphQLScalarType @@ -9,6 +11,7 @@ import graphql.schema.diff.SchemaDiffSet import graphql.schema.diff.reporting.CapturingReporter import graphql.schema.idl.* import org.assertj.core.api.Assertions.assertThat +import org.bouncycastle.asn1.x500.style.RFC4519Style.title import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assumptions import org.junit.jupiter.api.DynamicNode @@ -19,48 +22,58 @@ import org.neo4j.graphql.SchemaConfig import org.opentest4j.AssertionFailedError import java.util.* -class GraphQLSchemaTestSuite(fileName: String) : AsciiDocTestSuite(fileName, TEST_CASE_MARKERS) { +class GraphQLSchemaTestSuite(fileName: String) : AsciiDocTestSuite( + fileName, + listOf( + matcher("graphql", exactly = true, setter = TestCase::augmentedSchema), + ) +) { - override fun testFactory( - title: String, - globalBlocks: Map>, - codeBlocks: Map>, - ignore: Boolean - ): List { - val targetSchemaBlock = codeBlocks[GRAPHQL_MARKER]?.first() + data class TestCase( + var schema: CodeBlock, + var schemaConfig: CodeBlock?, + var augmentedSchema: CodeBlock? = null, + ) + + 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() + return TestCase(schema, schemaConfig) + } + + override fun createTests(testCase: TestCase, section: Section, ignoreReason: String?): List { + val targetSchemaBlock = testCase.augmentedSchema targetSchemaBlock?.let { try { - it.reformattedCode = SCHEMA_PRINTER.print(createMockSchema(it.code())) + it.reformattedContent = SCHEMA_PRINTER.print(createMockSchema(it.content)) } catch (ignore: Exception) { } } val compareSchemaTest = DynamicTest.dynamicTest("compare schema", targetSchemaBlock?.uri) { - val configBlock = codeBlocks[SCHEMA_CONFIG_MARKER]?.first() - val config = configBlock?.code()?.let { MAPPER.readValue(it, SchemaConfig::class.java) } ?: SchemaConfig() + val configBlock = testCase.schemaConfig + val config = configBlock?.content?.let { MAPPER.readValue(it, SchemaConfig::class.java) } ?: SchemaConfig() - val targetSchema = targetSchemaBlock?.code() - ?: throw IllegalStateException("missing graphql for $title") + val targetSchema = targetSchemaBlock?.content ?: error("missing graphql for ${section.title}") var augmentedSchema: GraphQLSchema? = null var expectedSchema: GraphQLSchema? = null try { - val schema = globalBlocks[SCHEMA_MARKER]?.first()?.code() - ?: throw IllegalStateException("Schema should be defined") + val schema = testCase.schema.content augmentedSchema = SchemaBuilder.buildSchema(schema, config) expectedSchema = createMockSchema(targetSchema) diff(expectedSchema, augmentedSchema) diff(augmentedSchema, expectedSchema) - targetSchemaBlock.adjustedCode = SCHEMA_PRINTER.print(augmentedSchema) + targetSchemaBlock.generatedContent = SCHEMA_PRINTER.print(augmentedSchema) } catch (e: Throwable) { - if (ignore) { - Assumptions.assumeFalse(true, e.message) + if (ignoreReason != null) { + Assumptions.assumeFalse(true) { "$ignoreReason ${e.message}" } } else { if (augmentedSchema == null) { Assertions.fail(e) } val actualSchema = SCHEMA_PRINTER.print(augmentedSchema) - targetSchemaBlock.adjustedCode = actualSchema + targetSchemaBlock.generatedContent = actualSchema throw AssertionFailedError("augmented schema differs for '$title'", expectedSchema?.let { SCHEMA_PRINTER.print(it) } ?: targetSchema, actualSchema, @@ -72,6 +85,7 @@ class GraphQLSchemaTestSuite(fileName: String) : AsciiDocTestSuite(fileName, TES return Collections.singletonList(compareSchemaTest) } + private fun createMockSchema(targetSchema: String): GraphQLSchema { val schemaParser = SchemaParser() @@ -97,8 +111,6 @@ class GraphQLSchemaTestSuite(fileName: String) : AsciiDocTestSuite(fileName, TES } companion object { - private const val GRAPHQL_MARKER = "[source,graphql]" - private val TEST_CASE_MARKERS: List = listOf(SCHEMA_CONFIG_MARKER, GRAPHQL_MARKER) private val SCHEMA_PRINTER = SchemaPrinter( SchemaPrinter.Options.defaultOptions() diff --git a/core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/AsciiDocParser.kt b/core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/AsciiDocParser.kt new file mode 100644 index 00000000..75428e1f --- /dev/null +++ b/core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/AsciiDocParser.kt @@ -0,0 +1,134 @@ +package demo.org.neo4j.graphql.utils.asciidoc + +import demo.org.neo4j.graphql.utils.asciidoc.ast.* +import java.io.File +import java.net.URI +import java.util.regex.Pattern +import javax.ws.rs.core.UriBuilder + +class AsciiDocParser( + fileName: String +) { + + 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 currentSection: Section = root + private var currentDepth: Int = 0 + + + fun parse(): Document { + val lines = file.readLines() + var title: String? + + var insideCodeblock = false + var offset = 0 + + val fileContent = StringBuilder() + + root = Document(srcLocation) + currentSection = root + currentDepth = 0 + var caption: String? = null + + var currentCodeBlock: CodeBlock? = null + var content = StringBuilder() + + + 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 { + + headlineMatcher.matches() -> { + addBlock(content) + val depth = headlineMatcher.group(1).length + title = headlineMatcher.group(2) + val uri = UriBuilder.fromUri(srcLocation).queryParam("line", lineNr + 1).build() + startSection(title, uri, depth) + } + + line.startsWith(".") && !insideCodeblock -> { + caption = line.substring(1).trim() + } + + line.startsWith("[source,") -> { + addBlock(content) + val uri = UriBuilder.fromUri(srcLocation).queryParam("line", lineNr + 1).build() + currentCodeBlock = CodeBlock.parseMeta(currentSection, uri, line).also { + it.caption = caption + currentSection.blocks.add(it) + } + caption = null + } + + line == "'''" -> { + addBlock(content) + currentSection.blocks.add(ThematicBreak()) + } + + 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() + } + } + + else -> { + content.append(line).append("\n") + } + } + offset += line.length + 1 // +1 b/c of newline + } + addBlock(content) + root.content = fileContent.toString() + return root + } + + private fun addBlock(content: StringBuilder) { + val str = content.toString() + if (str.trim().isNotEmpty()) { + currentSection.let { it.blocks.add(Block(it, str)) } + } + content.clear() + } + + private fun startSection( + title: String, uri: URI, depth: Int + ) { + + val parent = when { + depth > currentDepth -> currentSection + depth == currentDepth -> currentSection.parent + else -> { + var parent = currentSection.parent + for (i in 0 until currentDepth - depth) { + parent = parent?.parent + } + parent + } + } ?: error("cannot create sub-level on null") + currentSection = Section(title, uri, parent) + .also { parent.blocks.add(it) } + + currentDepth = depth + } + + companion object { + private val HEADLINE_PATTERN: Pattern = Pattern.compile("^(=+) (.*)$") + } +} diff --git a/core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/Block.kt b/core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/Block.kt new file mode 100644 index 00000000..ed7067d1 --- /dev/null +++ b/core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/Block.kt @@ -0,0 +1,11 @@ +package demo.org.neo4j.graphql.utils.asciidoc.ast + +class Block( + parent: StructuralNode, + val content: String +) : StructuralNode(parent) { + + override fun toString(): String { + return "Block(content='$content')" + } +} diff --git a/core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/CodeBlock.kt b/core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/CodeBlock.kt new file mode 100644 index 00000000..0f3e0113 --- /dev/null +++ b/core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/CodeBlock.kt @@ -0,0 +1,59 @@ +package demo.org.neo4j.graphql.utils.asciidoc.ast + +import java.net.URI + +class CodeBlock( + val uri: URI, + val language: String, + override val parent: Section, + val attributes: Map +) : StructuralNode(parent) { + + var caption: String? = null + + var start: Int? = null + var end: Int? = null + + lateinit var content: String + + /** + * The content that was generated but diffs to the current content + */ + var generatedContent: String? = null + + /** + * The original content reformatted + */ + var reformattedContent: String? = null + + val marker: String + get() = "[source,$language${attributes.map { ",${it.key}${it.value?.let { "=${it}" } ?: ""}" }.joinToString()}]" + + override fun toString(): String { + return "CodeBlock(language='$language', attributes=$attributes)" + } + + 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) + + + companion object { + + fun parseMeta(parent: Section, uri: URI, meta: String): CodeBlock { + if (!meta.startsWith("[source,")) { + error("Invalid code block meta: $meta") + } + + val parts = meta.substring(8, meta.indexOf("]")).trim().split(",") + val language = parts[0] + val attributes = parts.slice(1..() +} diff --git a/core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/ThematicBreak.kt b/core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/ThematicBreak.kt new file mode 100644 index 00000000..7199db15 --- /dev/null +++ b/core/src/test/kotlin/org/neo4j/graphql/utils/asciidoc/ast/ThematicBreak.kt @@ -0,0 +1,3 @@ +package demo.org.neo4j.graphql.utils.asciidoc.ast + +class ThematicBreak: StructuralNode(null) diff --git a/core/src/test/resources/custom-fields.adoc b/core/src/test/resources/custom-fields.adoc index 232e6f4b..97f32e4b 100644 --- a/core/src/test/resources/custom-fields.adoc +++ b/core/src/test/resources/custom-fields.adoc @@ -2,8 +2,9 @@ = Custom queries and mutations -== Schema +== Setup +.Schema [source,graphql,schema=true] ---- type Person { @@ -41,8 +42,7 @@ type MutationType { } ---- -== Schema Configuration - +.Schema Configuration [source,json,schema-config=true] ---- { diff --git a/core/src/test/resources/issues/gh-210.adoc b/core/src/test/resources/issues/gh-210.adoc index f389c270..11415d23 100644 --- a/core/src/test/resources/issues/gh-210.adoc +++ b/core/src/test/resources/issues/gh-210.adoc @@ -2,8 +2,9 @@ = GitHub Issue #210: Nested filters are not working -== Schema +== Setup +.Schema [source,graphql,schema=true] ---- interface Period { @@ -19,8 +20,6 @@ type Week implements Period { } ---- -== Configuration - .Configuration [source,json,schema-config=true] ---- diff --git a/core/src/test/resources/issues/gh-295-wrong-target-node-alias.adoc b/core/src/test/resources/issues/gh-295-wrong-target-node-alias.adoc index 39a3976a..8aea45f7 100644 --- a/core/src/test/resources/issues/gh-295-wrong-target-node-alias.adoc +++ b/core/src/test/resources/issues/gh-295-wrong-target-node-alias.adoc @@ -2,8 +2,9 @@ = GitHub Issue #295: Wrong node name used for end node in rich relationship -== Schema +== Setup +.Schema [source,graphql,schema=true] ---- type Person{ @@ -24,8 +25,6 @@ type Person_HAS_Target_Book @relation(name: "HAS", from: "source", to: "target", } ---- -== Configuration - .Configuration [source,json,schema-config=true] ---- diff --git a/core/src/test/resources/tck-test-files/cypher/advanced-filtering.adoc b/core/src/test/resources/tck-test-files/cypher/advanced-filtering.adoc index a966411a..1be3f8eb 100644 --- a/core/src/test/resources/tck-test-files/cypher/advanced-filtering.adoc +++ b/core/src/test/resources/tck-test-files/cypher/advanced-filtering.adoc @@ -4,8 +4,9 @@ Tests advanced filtering. -== Inputs +== Setup +.Schema [source,graphql,schema=true] ---- type Movie { @@ -24,9 +25,7 @@ type Genre { } ---- -== Configuration - -.Configuration +.Schema Configuration [source,json,schema-config=true] ---- { diff --git a/core/src/test/resources/tck-test-files/cypher/types/datetime.adoc b/core/src/test/resources/tck-test-files/cypher/types/datetime.adoc index ef9f8155..24e0614b 100644 --- a/core/src/test/resources/tck-test-files/cypher/types/datetime.adoc +++ b/core/src/test/resources/tck-test-files/cypher/types/datetime.adoc @@ -4,8 +4,9 @@ Tests DateTime operations. -== Schema +== Setup +.Schema [source,graphql,schema=true] ---- type Movie { @@ -14,8 +15,6 @@ type Movie { } ---- -== Configuration - .Configuration [source,json,schema-config=true] ---- From e6d225b24bf89047ee2c89b5933265b1fb4b172c Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Sun, 10 Nov 2024 19:19:07 +0100 Subject: [PATCH 2/3] small fixes --- .run/Reformat Tests.run.xml | 13 +++++++++++++ .../org/neo4j/graphql/utils/AsciiDocTestSuite.kt | 8 ++++++-- .../org/neo4j/graphql/utils/CypherTestSuite.kt | 7 ++++--- .../neo4j/graphql/utils/GraphQLSchemaTestSuite.kt | 5 ++++- core/src/test/resources/augmentation-tests.adoc | 3 ++- core/src/test/resources/custom-fields.adoc | 6 ++---- core/src/test/resources/cypher-directive-tests.adoc | 3 ++- core/src/test/resources/dynamic-property-tests.adoc | 3 ++- core/src/test/resources/filter-tests.adoc | 6 +++--- core/src/test/resources/issues/gh-112.adoc | 6 +++--- core/src/test/resources/issues/gh-147.adoc | 6 +++--- core/src/test/resources/issues/gh-149.adoc | 3 ++- core/src/test/resources/issues/gh-160.adoc | 3 ++- core/src/test/resources/issues/gh-163.adoc | 3 ++- core/src/test/resources/issues/gh-169.adoc | 3 ++- core/src/test/resources/issues/gh-170.adoc | 4 +++- .../gh-190-cypher-directive-with-passThrough.adoc | 4 +++- .../gh-245-cypher-directive-on-relationship.adoc | 3 ++- .../gh-265-querying-multiple-root-fields.adoc | 5 ++--- ...s-not-work-if-using-datafetchinginterceptor.adoc | 5 ++--- core/src/test/resources/issues/gh-27.adoc | 3 ++- ...n-translation-for-IDs-with-property-aliases.adoc | 3 ++- core/src/test/resources/issues/gh-45.adoc | 3 ++- core/src/test/resources/issues/gh-47.adoc | 3 ++- core/src/test/resources/issues/gh-65.adoc | 6 +++--- core/src/test/resources/issues/gh-85.adoc | 6 +++--- core/src/test/resources/movie-tests.adoc | 3 ++- .../test/resources/optimized-query-for-filter.adoc | 3 ++- core/src/test/resources/property-tests.adoc | 3 ++- core/src/test/resources/relationship-tests.adoc | 3 ++- .../src/test/resources/schema-operations-tests.adoc | 3 ++- .../tck-test-files/cypher/directives/ignore.adoc | 5 ++--- .../resources/tck-test-files/cypher/pagination.adoc | 5 ++--- .../test/resources/tck-test-files/cypher/sort.adoc | 5 ++--- .../tck-test-files/cypher/types/datetime.adoc | 3 +-- .../test/resources/tck-test-files/cypher/where.adoc | 5 ++--- .../tck-test-files/schema/directives/ignore.adoc | 5 ++--- .../tck-test-files/schema/relationship.adoc | 5 ++--- .../resources/tck-test-files/schema/simple.adoc | 5 ++--- .../tck-test-files/schema/types/arrays.adoc | 5 ++--- .../tck-test-files/schema/types/datetime.adoc | 5 ++--- .../resources/translator-tests-custom-scalars.adoc | 3 ++- core/src/test/resources/translator-tests1.adoc | 3 ++- core/src/test/resources/translator-tests2.adoc | 3 ++- core/src/test/resources/translator-tests3.adoc | 6 +++--- 45 files changed, 116 insertions(+), 85 deletions(-) create mode 100644 .run/Reformat Tests.run.xml diff --git a/.run/Reformat Tests.run.xml b/.run/Reformat Tests.run.xml new file mode 100644 index 00000000..ed6f4681 --- /dev/null +++ b/.run/Reformat Tests.run.xml @@ -0,0 +1,13 @@ + + + +