diff --git a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/DecisionTableToFixtureMatcher.kt b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/DecisionTableToFixtureMatcher.kt index 5660b808..20b12ae5 100644 --- a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/DecisionTableToFixtureMatcher.kt +++ b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/DecisionTableToFixtureMatcher.kt @@ -2,73 +2,33 @@ package org.livingdoc.engine import org.livingdoc.engine.execution.examples.decisiontables.DecisionTableFixtureModel import org.livingdoc.repositories.model.decisiontable.DecisionTable -import org.livingdoc.repositories.model.decisiontable.Header -import kotlin.reflect.KClass /** * Default matcher to find the right fixture classes for a given list of tables. */ class DecisionTableToFixtureMatcher { - /** - * Finds the fitting fixture for the given decision tables. - */ - fun matchTablesToFixtures(tables: List, fixtures: List>): Map> { - /* - * - Start with a list of Tables and a list of Fixtures - * - Create mapping Model -> Fixture (straightforward) - * - Create mapping Table -> Model (find table that contains all aliases of the model) - * - Create mapping Table -> Fixture (via Table -> Model -> Fixture) - */ - val fixtureModelToFixtureClass = mapModelsToFixtures(fixtures) - val decisionTableToFixtureModel = mapTablesToModels(fixtureModelToFixtureClass.keys, tables) - return mapTablesToFixtures(decisionTableToFixtureModel, fixtureModelToFixtureClass) - } - - private fun mapTablesToFixtures( - decisionTableToFixtureModel: Map, - fixtureModelToFixtureClass: Map?> - ): Map> = - decisionTableToFixtureModel.keys.map { table -> - val model: DecisionTableFixtureModel = decisionTableToFixtureModel[table] - ?: throw NoDecisionTableModelForDecisionTable(table) - val fixture: Class<*> = fixtureModelToFixtureClass[model] - ?: throw NoFixtureForDecisionTableModelException(model) - table to fixture - }.toMap() - - private fun mapModelsToFixtures(fixtureClasses: List>) = - fixtureClasses.map { it.java }.associateByTo(mutableMapOf()) { clazz -> - DecisionTableFixtureModel(clazz) - } + fun findMatchingFixture(decisionTable: DecisionTable, fixtures: List>): Class<*>? { + val headerNames = decisionTable.headers.map { it.name } + val numberOfHeaders = headerNames.size - private fun mapTablesToModels(tableModels: Iterable, tables: Iterable) = - tableModels.associateByTo(mutableMapOf()) { model -> - val table = tables.firstOrNull { (headers, _) -> - headers.map(Header::name).containsAll(model.inputAliases) - } ?: throw NoDecisionTableForDecisionTableModelException(model) - table - } -} + val matchingFixtures = fixtures.filter { fixtureClass -> + val fixtureModel = DecisionTableFixtureModel(fixtureClass) + val aliases = fixtureModel.aliases + val numberOfAliases = aliases.size + val numberOfMatchedHeaders = headerNames.filter { aliases.contains(it) }.size -/** - * This exception is thrown when the [DecisionTableToFixtureMatcher] cannot find a [DecisionTable] that fits - * to a [DecisionTableFixtureModel] of a Fixture class. - */ -class NoDecisionTableForDecisionTableModelException(model: DecisionTableFixtureModel) - : java.lang.IllegalArgumentException("Could not find a Decision Table for Fixture Class ${model.fixtureClass.canonicalName}") + numberOfMatchedHeaders == numberOfHeaders && numberOfMatchedHeaders == numberOfAliases + } -/** - * This exception is thrown when the [DecisionTableToFixtureMatcher] cannot find a [DecisionTableFixtureModel] that fits - * to a [DecisionTable]. - */ -class NoDecisionTableModelForDecisionTable(table: DecisionTable) - : java.lang.IllegalArgumentException("Could not find a Decision Table Model for Decision Table $table") + if (matchingFixtures.size > 1) { + throw MultipleMatchingFixturesException(headerNames, matchingFixtures) + } + return matchingFixtures.firstOrNull() + } -/** - * This exception is thrown when the [DecisionTableToFixtureMatcher] cannot find a Fixture class that fits - * to a [DecisionTableFixtureModel]. - */ -class NoFixtureForDecisionTableModelException(model: DecisionTableFixtureModel) - : java.lang.IllegalArgumentException("Could not find a Decision Table Model for Decision Table ${model.fixtureClass.canonicalName}") + class MultipleMatchingFixturesException(headerNames: List, matchingFixtures: List>) + : RuntimeException("Could not identify a unique fixture matching the Decision Table's headers " + + "${headerNames.map { "'$it'" }}. Matching fixtures found: $matchingFixtures") +} \ No newline at end of file diff --git a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/LivingDoc.kt b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/LivingDoc.kt index d981c755..43b46aba 100644 --- a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/LivingDoc.kt +++ b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/LivingDoc.kt @@ -2,13 +2,21 @@ package org.livingdoc.engine import org.livingdoc.api.documents.ExecutableDocument import org.livingdoc.api.fixtures.decisiontables.DecisionTableFixture +import org.livingdoc.api.fixtures.scenarios.ScenarioFixture import org.livingdoc.engine.execution.DocumentResult import org.livingdoc.engine.execution.ExecutionException +import org.livingdoc.engine.execution.examples.ExampleResult import org.livingdoc.engine.execution.examples.decisiontables.DecisionTableExecutor -import org.livingdoc.engine.execution.examples.decisiontables.DecisionTableFixtureModel +import org.livingdoc.engine.execution.examples.decisiontables.model.DecisionTableResult +import org.livingdoc.engine.execution.examples.scenarios.ScenarioExecutor +import org.livingdoc.engine.execution.examples.scenarios.model.ScenarioResult +import org.livingdoc.repositories.Document import org.livingdoc.repositories.RepositoryManager import org.livingdoc.repositories.config.Configuration +import org.livingdoc.repositories.model.Example import org.livingdoc.repositories.model.decisiontable.DecisionTable +import org.livingdoc.repositories.model.scenario.Scenario +import kotlin.reflect.KClass /** * Executes the given document class and returns the [DocumentResult]. The document's class must be annotated @@ -18,38 +26,128 @@ import org.livingdoc.repositories.model.decisiontable.DecisionTable * @throws ExecutionException in case the execution failed in a way that did not produce a viable result * @since 2.0 */ -class LivingDoc(val tableMatcher: DecisionTableToFixtureMatcher = DecisionTableToFixtureMatcher()) { +class LivingDoc( + val repositoryManager: RepositoryManager = RepositoryManager.from(Configuration.load()), + val decisionTableExecutor: DecisionTableExecutor = DecisionTableExecutor(), + val decisionTableToFixtureMatcher: DecisionTableToFixtureMatcher = DecisionTableToFixtureMatcher(), + val scenarioExecutor: ScenarioExecutor = ScenarioExecutor(), + val scenarioToFixtureMatcher: ScenarioToFixtureMatcher = ScenarioToFixtureMatcher() +) { - private fun validateInputAndMatchFixtures(document: Class<*>): Map> { - if (!document.isAnnotationPresent(ExecutableDocument::class.java)) { - throw IllegalArgumentException("ExecutableDocument annotation is not present on class ${document.canonicalName}.") - } + @Throws(ExecutionException::class) + fun execute(documentClass: Class<*>) = DocumentResult(getExecutableExamples(documentClass).map { it.execute() }) - val annotation = document.getAnnotation(ExecutableDocument::class.java) - val values = annotation.value.split("://") - if (values.size != 2) throw IllegalArgumentException("Illegal annotation value '${annotation.value}'.") + @Throws(ExecutionException::class) + fun getExecutableExamples(documentClass: Class<*>): List> { + val documentClassModel = ExecutableDocumentModel.of(documentClass) + val document = loadDocument(documentClassModel) + return document.elements.mapNotNull { element -> + when (element) { + is DecisionTable -> executableDecisionTable(element, documentClassModel) + is Scenario -> executableScenario(element, documentClassModel) + else -> null + } + } - val (name, id) = values - val repository = RepositoryManager.from(Configuration.load()).getRepository(name) - val (tables, _) = repository.getDocument(id) + } - val fixtureClasses = annotation.fixtureClasses - .filter { it.java.isAnnotationPresent(DecisionTableFixture::class.java) } + private fun loadDocument(documentClassModel: ExecutableDocumentModel): Document { + return with(documentClassModel.documentIdentifier) { + repositoryManager.getRepository(repository).getDocument(id) + } + } - return tableMatcher.matchTablesToFixtures(tables, fixtureClasses) + private fun executableDecisionTable(element: DecisionTable, documentModel: ExecutableDocumentModel): ExecutableDecisionTable? { + val matchingFixture = decisionTableToFixtureMatcher.findMatchingFixture(element, documentModel.decisionTableFixtures) + if (matchingFixture != null) { + return ExecutableDecisionTable(element) { + decisionTableExecutor.execute(it, matchingFixture, documentModel.documentClass) + } + } + return null + } + private fun executableScenario(scenario: Scenario, documentModel: ExecutableDocumentModel): ExecutableScenario? { + val matchingFixture = scenarioToFixtureMatcher.findMatchingFixture(scenario, documentModel.decisionTableFixtures) + if (matchingFixture != null) { + return ExecutableScenario(scenario) { + scenarioExecutor.execute(it, matchingFixture, documentModel.documentClass) + } + } + return null } - @Throws(ExecutionException::class) - fun execute(document: Class<*>): DocumentResult { - val tableToFixture = validateInputAndMatchFixtures(document) +} + +sealed class ExecutableExample( + val example: A, + private val execution: (A) -> B +) { + fun execute(): B = execution(example) +} - val executor = DecisionTableExecutor() - val decisionTableResults = tableToFixture.map { (table, fixture) -> - executor.execute(table, fixture) +class ExecutableDecisionTable( + example: DecisionTable, + execution: (DecisionTable) -> DecisionTableResult +) : ExecutableExample(example, execution) + +class ExecutableScenario( + example: Scenario, + execution: (Scenario) -> ScenarioResult +) : ExecutableExample(example, execution) + +private data class DocumentIdentifier( + val repository: String, + val id: String +) + +private data class ExecutableDocumentModel( + val documentClass: Class<*>, + val documentIdentifier: DocumentIdentifier, + val decisionTableFixtures: List>, + val scenarioFixtures: List> +) { + + companion object { + + fun of(documentClass: Class<*>): ExecutableDocumentModel { + validate(documentClass) + return ExecutableDocumentModel( + documentClass = documentClass, + documentIdentifier = getDocumentIdentifier(documentClass), + decisionTableFixtures = getDecisionTableFixtures(documentClass), + scenarioFixtures = getScenarioFixtures(documentClass) + ) + } + + private fun getDocumentIdentifier(document: Class<*>): DocumentIdentifier { + val annotation = document.executableDocumentAnnotation!! + val values = annotation.value.split("://") + .also { require(it.size == 2) { "Illegal annotation value '${annotation.value}'." } } + return DocumentIdentifier(values[0], values[1]) } - return DocumentResult(decisionTableResults = decisionTableResults) + private fun validate(document: Class<*>) { + if (document.executableDocumentAnnotation == null) { + throw IllegalArgumentException("ExecutableDocument annotation is not present on class ${document.canonicalName}.") + } + } + + private fun getDecisionTableFixtures(document: Class<*>) = getFixtures(document, DecisionTableFixture::class) + private fun getScenarioFixtures(document: Class<*>) = getFixtures(document, ScenarioFixture::class) + + private fun getFixtures(document: Class<*>, annotationClass: KClass): List> { + val declaredInside = document.declaredClasses + .filter { it.isAnnotationPresent(annotationClass.java) } + val fromAnnotation = document.executableDocumentAnnotation!!.fixtureClasses + .map { it.java } + .filter { it.isAnnotationPresent(annotationClass.java) } + return declaredInside + fromAnnotation + } + + private val Class<*>.executableDocumentAnnotation: ExecutableDocument? + get() = getAnnotation(ExecutableDocument::class.java) + } } diff --git a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/ScenarioToFixtureMatcher.kt b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/ScenarioToFixtureMatcher.kt new file mode 100644 index 00000000..dd146af1 --- /dev/null +++ b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/ScenarioToFixtureMatcher.kt @@ -0,0 +1,14 @@ +package org.livingdoc.engine + +import org.livingdoc.repositories.model.scenario.Scenario + +/** + * Default matcher to find the right fixture classes for a given list of tables. + */ +class ScenarioToFixtureMatcher { + + fun findMatchingFixture(scenario: Scenario, fixtures: List>): Class<*>? { + return null + } + +} diff --git a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/DocumentResult.kt b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/DocumentResult.kt index 35d14e43..4c7fe23b 100644 --- a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/DocumentResult.kt +++ b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/DocumentResult.kt @@ -1,9 +1,7 @@ package org.livingdoc.engine.execution -import org.livingdoc.engine.execution.examples.decisiontables.model.DecisionTableResult -import org.livingdoc.engine.execution.examples.scenarios.model.ScenarioResult +import org.livingdoc.engine.execution.examples.ExampleResult data class DocumentResult( - val decisionTableResults: List = emptyList(), - val scenarioResults: List = emptyList() + val results: List = emptyList() ) diff --git a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/ExampleResult.kt b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/ExampleResult.kt new file mode 100644 index 00000000..849cde3d --- /dev/null +++ b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/ExampleResult.kt @@ -0,0 +1,4 @@ +package org.livingdoc.engine.execution.examples + +interface ExampleResult { +} \ No newline at end of file diff --git a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/decisiontables/DecisionTableFixtureModel.kt b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/decisiontables/DecisionTableFixtureModel.kt index adfda851..e16993b6 100644 --- a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/decisiontables/DecisionTableFixtureModel.kt +++ b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/decisiontables/DecisionTableFixtureModel.kt @@ -17,7 +17,7 @@ class DecisionTableFixtureModel( val inputFields: List val inputMethods: List - val inputAliases: MutableSet + private val inputAliases: MutableSet private val inputAliasToField: MutableMap private val inputAliasToMethod: MutableMap @@ -25,6 +25,9 @@ class DecisionTableFixtureModel( private val checkAliases: Set private val checkAliasToMethod: Map + val aliases: Set + get() = inputAliases + checkAliases + init { // method analysis diff --git a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/decisiontables/model/DecisionTableResult.kt b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/decisiontables/model/DecisionTableResult.kt index a49d2a6d..ce23abe8 100644 --- a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/decisiontables/model/DecisionTableResult.kt +++ b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/decisiontables/model/DecisionTableResult.kt @@ -1,14 +1,15 @@ package org.livingdoc.engine.execution.examples.decisiontables.model import org.livingdoc.engine.execution.Result +import org.livingdoc.engine.execution.examples.ExampleResult import org.livingdoc.repositories.model.decisiontable.DecisionTable import org.livingdoc.repositories.model.decisiontable.Header data class DecisionTableResult( - val headers: List
, - val rows: List, - var result: Result = Result.Unknown -) { + val headers: List
, + val rows: List, + var result: Result = Result.Unknown +) : ExampleResult { companion object { fun from(decisionTable: DecisionTable): DecisionTableResult { diff --git a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/scenarios/model/ScenarioResult.kt b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/scenarios/model/ScenarioResult.kt index e3f59083..e22b1e2c 100644 --- a/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/scenarios/model/ScenarioResult.kt +++ b/livingdoc-engine/src/main/kotlin/org/livingdoc/engine/execution/examples/scenarios/model/ScenarioResult.kt @@ -1,12 +1,13 @@ package org.livingdoc.engine.execution.examples.scenarios.model import org.livingdoc.engine.execution.Result +import org.livingdoc.engine.execution.examples.ExampleResult import org.livingdoc.repositories.model.scenario.Scenario data class ScenarioResult( - val steps: List, - var result: Result = Result.Unknown -) { + val steps: List, + var result: Result = Result.Unknown +) : ExampleResult { companion object { fun from(scenario: Scenario): ScenarioResult { diff --git a/livingdoc-junit-engine/src/main/kotlin/org/livingdoc/junit/engine/descriptors/DecisionTableTestDescriptor.kt b/livingdoc-junit-engine/src/main/kotlin/org/livingdoc/junit/engine/descriptors/DecisionTableTestDescriptor.kt index 700461bd..1cba83d8 100644 --- a/livingdoc-junit-engine/src/main/kotlin/org/livingdoc/junit/engine/descriptors/DecisionTableTestDescriptor.kt +++ b/livingdoc-junit-engine/src/main/kotlin/org/livingdoc/junit/engine/descriptors/DecisionTableTestDescriptor.kt @@ -6,8 +6,8 @@ import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor import org.junit.platform.engine.support.hierarchical.Node import org.junit.platform.engine.support.hierarchical.Node.SkipResult.doNotSkip import org.junit.platform.engine.support.hierarchical.Node.SkipResult.skip +import org.livingdoc.engine.ExecutableDecisionTable import org.livingdoc.engine.execution.Result -import org.livingdoc.engine.execution.examples.decisiontables.model.DecisionTableResult import org.livingdoc.engine.execution.examples.decisiontables.model.FieldResult import org.livingdoc.engine.execution.examples.decisiontables.model.RowResult import org.livingdoc.junit.engine.LivingDocContext @@ -16,13 +16,13 @@ import org.livingdoc.repositories.model.decisiontable.Header class DecisionTableTestDescriptor( uniqueId: UniqueId, displayName: String, - private val tableResult: DecisionTableResult + private val decisionTable: ExecutableDecisionTable ) : AbstractTestDescriptor(uniqueId, displayName), Node { override fun getType() = TestDescriptor.Type.CONTAINER override fun execute(context: LivingDocContext, dynamicTestExecutor: Node.DynamicTestExecutor): LivingDocContext { - tableResult.rows.forEachIndexed { index, rowResult -> + decisionTable.execute().rows.forEachIndexed { index, rowResult -> val descriptor = RowTestDescriptor(rowUniqueId(index), rowDisplayName(index), rowResult) .also { it.setParent(this) } dynamicTestExecutor.execute(descriptor) @@ -33,15 +33,6 @@ class DecisionTableTestDescriptor( private fun rowUniqueId(index: Int) = uniqueId.append("row", "$index") private fun rowDisplayName(index: Int) = "Row #${index + 1}" - override fun shouldBeSkipped(context: LivingDocContext): Node.SkipResult { - val result = tableResult.result - return when (result) { - Result.Unknown -> skip("unknown") - Result.Skipped -> skip("skipped") - else -> doNotSkip() - } - } - class RowTestDescriptor( uniqueId: UniqueId, displayName: String, diff --git a/livingdoc-junit-engine/src/main/kotlin/org/livingdoc/junit/engine/descriptors/ExecutableDocumentDescriptor.kt b/livingdoc-junit-engine/src/main/kotlin/org/livingdoc/junit/engine/descriptors/ExecutableDocumentDescriptor.kt index 3d2dbf3b..ddbac17c 100644 --- a/livingdoc-junit-engine/src/main/kotlin/org/livingdoc/junit/engine/descriptors/ExecutableDocumentDescriptor.kt +++ b/livingdoc-junit-engine/src/main/kotlin/org/livingdoc/junit/engine/descriptors/ExecutableDocumentDescriptor.kt @@ -5,18 +5,9 @@ import org.junit.platform.engine.UniqueId import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor import org.junit.platform.engine.support.hierarchical.Node import org.junit.platform.engine.support.hierarchical.Node.DynamicTestExecutor -import org.livingdoc.api.fixtures.decisiontables.DecisionTableFixture -import org.livingdoc.api.fixtures.scenarios.ScenarioFixture -import org.livingdoc.engine.execution.DocumentResult -import org.livingdoc.engine.execution.examples.decisiontables.DecisionTableExecutor -import org.livingdoc.engine.execution.examples.scenarios.ScenarioExecutor +import org.livingdoc.engine.ExecutableDecisionTable +import org.livingdoc.engine.ExecutableScenario import org.livingdoc.junit.engine.LivingDocContext -import org.livingdoc.repositories.model.decisiontable.DecisionTable -import org.livingdoc.repositories.model.decisiontable.Field -import org.livingdoc.repositories.model.decisiontable.Header -import org.livingdoc.repositories.model.decisiontable.Row -import org.livingdoc.repositories.model.scenario.Scenario -import org.livingdoc.repositories.model.scenario.Step class ExecutableDocumentDescriptor( uniqueId: UniqueId, @@ -26,22 +17,27 @@ class ExecutableDocumentDescriptor( override fun getType() = Type.CONTAINER_AND_TEST override fun execute(context: LivingDocContext, dynamicTestExecutor: DynamicTestExecutor): LivingDocContext { - val result = context.livingDoc.execute(documentClass) - // val result = dummyExecution() - - result.decisionTableResults.forEachIndexed { index, tableResult -> - val descriptor = DecisionTableTestDescriptor(tableUniqueId(index), tableDisplayName(index), tableResult) - .also { it.setParent(this) } - dynamicTestExecutor.execute(descriptor) - } + context.livingDoc + .getExecutableExamples(documentClass) + .forEachIndexed { index, example -> + when (example) { + is ExecutableDecisionTable -> execute(index, dynamicTestExecutor, example) + is ExecutableScenario -> execute(index, dynamicTestExecutor, example) + } + } + return context + } - result.scenarioResults.forEachIndexed { index, scenarioResult -> - val descriptor = ScenarioTestDescriptor(scenarioUniqueId(index), scenarioDisplayName(index), scenarioResult) - .also { it.setParent(this) } - dynamicTestExecutor.execute(descriptor) - } + private fun execute(index: Int, dynamicTestExecutor: DynamicTestExecutor, example: ExecutableScenario) { + val descriptor = ScenarioTestDescriptor(scenarioUniqueId(index), scenarioDisplayName(index), example) + .also { it.setParent(this) } + dynamicTestExecutor.execute(descriptor) + } - return context + private fun execute(index: Int, dynamicTestExecutor: DynamicTestExecutor, example: ExecutableDecisionTable) { + val descriptor = DecisionTableTestDescriptor(tableUniqueId(index), tableDisplayName(index), example) + .also { it.setParent(this) } + dynamicTestExecutor.execute(descriptor) } private fun tableUniqueId(index: Int) = uniqueId.append("table", "$index") @@ -50,54 +46,4 @@ class ExecutableDocumentDescriptor( private fun scenarioUniqueId(index: Int) = uniqueId.append("scenario", "$index") private fun scenarioDisplayName(index: Int) = "Scenario #${index + 1}" - // TODO: remove as soon as it is no longer need - private fun dummyExecution(): DocumentResult { - - val headerA = Header("a") - val headerB = Header("b") - val headerAplusB = Header("a + b = ?") - val headerAminusB = Header("a - b = ?") - val headerAtimesB = Header("a * b = ?") - val headerAdividedByB = Header("a / b = ?") - val decisionTable = DecisionTable( - listOf(headerA, headerB, headerAplusB, headerAminusB, headerAtimesB, headerAdividedByB), - listOf( - Row(mapOf( - headerA to Field("10"), - headerB to Field("5"), - headerAplusB to Field("15"), - headerAminusB to Field("5"), - headerAtimesB to Field("50"), - headerAdividedByB to Field("2") - )), - Row(mapOf( - headerA to Field("-3"), - headerB to Field("2"), - headerAplusB to Field("-1"), - headerAminusB to Field("-5"), - headerAtimesB to Field("-6"), - headerAdividedByB to Field("-1.5") - )) - ) - ) - val decisionTableFixture = documentClass.declaredClasses - .single { it.isAnnotationPresent(DecisionTableFixture::class.java) } - val decisionTableResult = DecisionTableExecutor() - .execute(decisionTable, decisionTableFixture, documentClass) - - val scenario = Scenario(listOf( - Step("adding 1 and 2 equals 3"), - Step("subtraction 5 form 15 equals 10"), - Step("multiplying 3 and 6 equals 18"), - Step("dividing 20 by 5 equals 4") - )) - val scenarioFixture = documentClass.declaredClasses - .single { it.isAnnotationPresent(ScenarioFixture::class.java) } - val scenarioResult = ScenarioExecutor() - .execute(scenario, scenarioFixture, documentClass) - - return DocumentResult(listOf(decisionTableResult), listOf(scenarioResult)) - - } - } \ No newline at end of file diff --git a/livingdoc-junit-engine/src/main/kotlin/org/livingdoc/junit/engine/descriptors/ScenarioTestDescriptor.kt b/livingdoc-junit-engine/src/main/kotlin/org/livingdoc/junit/engine/descriptors/ScenarioTestDescriptor.kt index dfaddf99..98e065d5 100644 --- a/livingdoc-junit-engine/src/main/kotlin/org/livingdoc/junit/engine/descriptors/ScenarioTestDescriptor.kt +++ b/livingdoc-junit-engine/src/main/kotlin/org/livingdoc/junit/engine/descriptors/ScenarioTestDescriptor.kt @@ -6,21 +6,21 @@ import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor import org.junit.platform.engine.support.hierarchical.Node import org.junit.platform.engine.support.hierarchical.Node.SkipResult.doNotSkip import org.junit.platform.engine.support.hierarchical.Node.SkipResult.skip +import org.livingdoc.engine.ExecutableScenario import org.livingdoc.engine.execution.Result -import org.livingdoc.engine.execution.examples.scenarios.model.ScenarioResult import org.livingdoc.engine.execution.examples.scenarios.model.StepResult import org.livingdoc.junit.engine.LivingDocContext class ScenarioTestDescriptor( uniqueId: UniqueId, displayName: String, - private val scenarioResult: ScenarioResult + private val scenario: ExecutableScenario ) : AbstractTestDescriptor(uniqueId, displayName), Node { override fun getType() = TestDescriptor.Type.CONTAINER override fun execute(context: LivingDocContext, dynamicTestExecutor: Node.DynamicTestExecutor): LivingDocContext { - scenarioResult.steps.forEachIndexed { index, stepResult -> + scenario.execute().steps.forEachIndexed { index, stepResult -> val descriptor = StepTestDescriptor(stepUniqueId(index), stepDisplayName(stepResult), stepResult) .also { it.setParent(this) } dynamicTestExecutor.execute(descriptor) @@ -31,15 +31,6 @@ class ScenarioTestDescriptor( private fun stepUniqueId(index: Int) = uniqueId.append("step", "$index") private fun stepDisplayName(stepResult: StepResult) = stepResult.value - override fun shouldBeSkipped(context: LivingDocContext): Node.SkipResult { - val result = scenarioResult.result - return when (result) { - Result.Unknown -> skip("unknown") - Result.Skipped -> skip("skipped") - else -> doNotSkip() - } - } - class StepTestDescriptor( uniqueId: UniqueId, displayName: String, diff --git a/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/Document.kt b/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/Document.kt index 9eaa8510..18cf1519 100644 --- a/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/Document.kt +++ b/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/Document.kt @@ -1,10 +1,3 @@ package org.livingdoc.repositories -import org.livingdoc.repositories.model.decisiontable.DecisionTable -import org.livingdoc.repositories.model.scenario.Scenario - -open class Document(val tables: List, val lists: List) { - operator fun component1() = tables - operator fun component2() = lists -} - +open class Document(val elements: List) diff --git a/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/HtmlDocument.kt b/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/HtmlDocument.kt index 5ebc6e82..3cf0a90d 100644 --- a/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/HtmlDocument.kt +++ b/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/HtmlDocument.kt @@ -1,10 +1,6 @@ package org.livingdoc.repositories -import org.livingdoc.repositories.model.decisiontable.DecisionTable -import org.livingdoc.repositories.model.scenario.Scenario - class HtmlDocument( - tables: List, - lists: List, - val jsoupDoc: org.jsoup.nodes.Document -) : Document(tables, lists) + elements: List, + val jsoupDoc: org.jsoup.nodes.Document +) : Document(elements) diff --git a/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/model/Example.kt b/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/model/Example.kt new file mode 100644 index 00000000..ba91714d --- /dev/null +++ b/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/model/Example.kt @@ -0,0 +1,3 @@ +package org.livingdoc.repositories.model + +interface Example \ No newline at end of file diff --git a/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/model/decisiontable/DecisionTable.kt b/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/model/decisiontable/DecisionTable.kt index 1ccae547..d2e8f366 100644 --- a/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/model/decisiontable/DecisionTable.kt +++ b/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/model/decisiontable/DecisionTable.kt @@ -1,6 +1,8 @@ package org.livingdoc.repositories.model.decisiontable +import org.livingdoc.repositories.model.Example + data class DecisionTable( - val headers: List
, - val rows: List -) + val headers: List
, + val rows: List +) : Example diff --git a/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/model/scenario/Scenario.kt b/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/model/scenario/Scenario.kt index 2c16684d..69f67fbb 100644 --- a/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/model/scenario/Scenario.kt +++ b/livingdoc-repositories/src/main/kotlin/org/livingdoc/repositories/model/scenario/Scenario.kt @@ -1,5 +1,7 @@ package org.livingdoc.repositories.model.scenario +import org.livingdoc.repositories.model.Example + data class Scenario( - val steps: List -) + val steps: List +) : Example diff --git a/livingdoc-repositories/src/test/kotlin/org/livingdoc/repositories/RepositoryManagerTest.kt b/livingdoc-repositories/src/test/kotlin/org/livingdoc/repositories/RepositoryManagerTest.kt index 84a47db3..b26a1c15 100644 --- a/livingdoc-repositories/src/test/kotlin/org/livingdoc/repositories/RepositoryManagerTest.kt +++ b/livingdoc-repositories/src/test/kotlin/org/livingdoc/repositories/RepositoryManagerTest.kt @@ -174,7 +174,7 @@ internal class RepositoryManagerTest { } class TestRepository(val config: TestRepositoryConfiguration) : DocumentRepository { - override fun getDocument(documentIdentifier: String): Document = Document(emptyList(), emptyList()) + override fun getDocument(documentIdentifier: String): Document = Document(emptyList()) } data class TestRepositoryConfiguration( diff --git a/livingdoc-repository-file/src/main/kotlin/org/livingdoc/repositories/format/HtmlFormat.kt b/livingdoc-repository-file/src/main/kotlin/org/livingdoc/repositories/format/HtmlFormat.kt index 186c7809..b1dcccb7 100644 --- a/livingdoc-repository-file/src/main/kotlin/org/livingdoc/repositories/format/HtmlFormat.kt +++ b/livingdoc-repository-file/src/main/kotlin/org/livingdoc/repositories/format/HtmlFormat.kt @@ -27,7 +27,9 @@ class HtmlFormat : DocumentFormat { override fun parse(stream: InputStream): HtmlDocument { val streamContent = stream.readBytes().toString(Charset.defaultCharset()) val document = Jsoup.parse(streamContent) - return HtmlDocument(parseTables(document), parseLists(document), document) + val elements = parseTables(document) + parseLists(document) + // TODO: provide elements in order they occur inside the original source document + return HtmlDocument(elements, document) } private fun parseTables(document: Document): List { diff --git a/livingdoc-repository-file/src/main/kotlin/org/livingdoc/repositories/format/MarkdownFormat.kt b/livingdoc-repository-file/src/main/kotlin/org/livingdoc/repositories/format/MarkdownFormat.kt index f1bbb34b..cc47fa97 100644 --- a/livingdoc-repository-file/src/main/kotlin/org/livingdoc/repositories/format/MarkdownFormat.kt +++ b/livingdoc-repository-file/src/main/kotlin/org/livingdoc/repositories/format/MarkdownFormat.kt @@ -7,8 +7,9 @@ import com.vladsch.flexmark.ext.tables.TableRow import com.vladsch.flexmark.ext.tables.TablesExtension import com.vladsch.flexmark.parser.Parser import com.vladsch.flexmark.util.options.MutableDataSet -import org.livingdoc.repositories.* import org.livingdoc.repositories.Document +import org.livingdoc.repositories.DocumentFormat +import org.livingdoc.repositories.ParseException import org.livingdoc.repositories.model.decisiontable.DecisionTable import org.livingdoc.repositories.model.decisiontable.Field import org.livingdoc.repositories.model.decisiontable.Header @@ -30,8 +31,9 @@ class MarkdownFormat : DocumentFormat { val parser = Parser.builder(options).build() val text = stream.reader().use { it.readText() } val (tables, scenarios) = parser.parse(text).mapToNodes() - - return Document(tables, scenarios) + val elements = tables + scenarios + // TODO: provide elements in order they occur inside the original source document + return Document(elements) } private fun com.vladsch.flexmark.ast.Node.mapToNodes(): Pair, List> { diff --git a/livingdoc-repository-file/src/test/kotlin/org/livingdoc/repositories/format/HtmlFormatTest.kt b/livingdoc-repository-file/src/test/kotlin/org/livingdoc/repositories/format/HtmlFormatTest.kt index 635d7ee3..17299045 100644 --- a/livingdoc-repository-file/src/test/kotlin/org/livingdoc/repositories/format/HtmlFormatTest.kt +++ b/livingdoc-repository-file/src/test/kotlin/org/livingdoc/repositories/format/HtmlFormatTest.kt @@ -17,6 +17,7 @@ import org.livingdoc.repositories.format.HtmlFormatTestData.getHtmlWithUnordered import org.livingdoc.repositories.format.HtmlFormatTestData.getHtmlWithUnorderedListContainsOnlyOneItem import org.livingdoc.repositories.format.HtmlFormatTestData.getValidHtml import org.livingdoc.repositories.model.decisiontable.DecisionTable +import org.livingdoc.repositories.model.scenario.Scenario class HtmlFormatTest { @@ -26,7 +27,7 @@ class HtmlFormatTest { @Test fun `tables with one row are ignored`() { val result = cut.parse(getHtmlTableWithOnlyOneRow()) - assertThat(result.tables).hasSize(0) + assertThat(result.elements).hasSize(0) } @Test @@ -51,7 +52,7 @@ class HtmlFormatTest { fun `parse Html into DecisionTable`() { val htmlDocument = cut.parse(getValidHtml()) - val documentNode = htmlDocument.tables[0] + val documentNode = htmlDocument.elements[0] as DecisionTable assertThat(documentNode).isInstanceOf(DecisionTable::class.java) assertThat(documentNode.headers).extracting("name").containsExactly("Firstname", "Lastname", "Age") @@ -61,16 +62,16 @@ class HtmlFormatTest { assertThat(documentNode.rows[1].headerToField).hasSize(3) assertThat(documentNode.rows[0].headerToField.map { it.key.name }).containsExactly( - "Firstname", - "Lastname", - "Age" + "Firstname", + "Lastname", + "Age" ) assertThat(documentNode.rows[0].headerToField.map { it.value.value }).containsExactly("Jill", "Smith", "50") assertThat(documentNode.rows[1].headerToField.map { it.key.name }).containsExactly( - "Firstname", - "Lastname", - "Age" + "Firstname", + "Lastname", + "Age" ) assertThat(documentNode.rows[1].headerToField.map { it.value.value }).containsExactly("Eve", "Jackson", "94") } @@ -79,7 +80,7 @@ class HtmlFormatTest { fun `parse unorderedList into Scenario`() { val htmlDocument = cut.parse(getHtmlWithUnorderedList()) - val scenario = htmlDocument.lists[0] + val scenario = htmlDocument.elements[0] as Scenario assertThat(scenario.steps).isNotNull assertThat(scenario.steps).hasSize(5) @@ -93,7 +94,7 @@ class HtmlFormatTest { @Test fun `parse orderedList into Scenario`() { val htmlDocument = cut.parse(getHtmlWithOrderedList()) - val scenario = htmlDocument.lists[0] + val scenario = htmlDocument.elements[0] as Scenario assertThat(scenario.steps).isNotNull assertThat(scenario.steps).hasSize(5) @@ -108,16 +109,16 @@ class HtmlFormatTest { fun `unordered list with only one item is ignored`() { val htmlDocument = cut.parse(getHtmlWithUnorderedListContainsOnlyOneItem()) - assertThat(htmlDocument.lists).isNotNull - assertThat(htmlDocument.lists).isEmpty() + assertThat(htmlDocument.elements).isNotNull + assertThat(htmlDocument.elements).isEmpty() } @Test fun `ordered list with only one item is ignored`() { val htmlDocument = cut.parse(getHtmlWithOrderedListContainsOnlyOneItem()) - assertThat(htmlDocument.lists).isNotNull - assertThat(htmlDocument.lists).isEmpty() + assertThat(htmlDocument.elements).isNotNull + assertThat(htmlDocument.elements).isEmpty() } @Test diff --git a/livingdoc-repository-file/src/test/kotlin/org/livingdoc/repositories/format/MarkdownFormatTest.kt b/livingdoc-repository-file/src/test/kotlin/org/livingdoc/repositories/format/MarkdownFormatTest.kt index 3158b4d5..7e343256 100644 --- a/livingdoc-repository-file/src/test/kotlin/org/livingdoc/repositories/format/MarkdownFormatTest.kt +++ b/livingdoc-repository-file/src/test/kotlin/org/livingdoc/repositories/format/MarkdownFormatTest.kt @@ -2,7 +2,9 @@ package org.livingdoc.repositories.format import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.livingdoc.repositories.model.decisiontable.DecisionTable import org.livingdoc.repositories.model.decisiontable.Header +import org.livingdoc.repositories.model.scenario.Scenario internal class MarkdownFormatTest { @@ -10,7 +12,7 @@ internal class MarkdownFormatTest { fun `Table is read successfully`() { val mdFormat = MarkdownFormat() val document = mdFormat.parse( - """ + """ # Irrelevant Headline ``` @@ -28,18 +30,18 @@ internal class MarkdownFormatTest { """.trimIndent().byteInputStream() ) - val (tables, scenarios) = document + val elements = document.elements - assertThat(tables).hasSize(1) - assertThat(scenarios).isEmpty() + assertThat(elements).hasSize(1) - assertThat(tables[0].headers.map(Header::name)).containsExactly("Column1", "Column2", "Column3") - tables[0].rows.also { - assertThat(it[0].headerToField.map { it.key.name }).containsExactly("Column1", "Column2", "Column3") - assertThat(it[0].headerToField.map { it.value.value }).containsExactly("Cell11", "Cell12", "Cell13") + val firstElement = elements[0] as DecisionTable + assertThat(firstElement.headers.map(Header::name)).containsExactly("Column1", "Column2", "Column3") + firstElement.rows.also { rows -> + assertThat(rows[0].headerToField.map { it.key.name }).containsExactly("Column1", "Column2", "Column3") + assertThat(rows[0].headerToField.map { it.value.value }).containsExactly("Cell11", "Cell12", "Cell13") - assertThat(it[1].headerToField.map { it.key.name }).containsExactly("Column1", "Column2", "Column3") - assertThat(it[1].headerToField.map { it.value.value }).containsExactly("Cell21", "Cell22", "Cell23") + assertThat(rows[1].headerToField.map { it.key.name }).containsExactly("Column1", "Column2", "Column3") + assertThat(rows[1].headerToField.map { it.value.value }).containsExactly("Cell21", "Cell22", "Cell23") } } @@ -47,7 +49,7 @@ internal class MarkdownFormatTest { fun `Bullet list is read successfully`() { val mdFormat = MarkdownFormat() val document = mdFormat.parse( - """ + """ # Irrelevant Headline ``` @@ -79,10 +81,9 @@ internal class MarkdownFormatTest { """.trimIndent().byteInputStream() ) - val (tables, scenarios) = document + val elements = document.elements - assertThat(tables).isEmpty() - assertThat(scenarios).hasSize(1) + assertThat(elements).hasSize(1) val thirdSentence = """ Listitem3 @@ -96,7 +97,7 @@ internal class MarkdownFormatTest { """.trimIndent() - val scenario = scenarios[0] + val scenario = elements[0] as Scenario scenario.steps.also { assertThat(it).hasSize(4) assertThat(it[0].value).isEqualTo("Listitem1\nSentence in first list item.") @@ -110,7 +111,7 @@ internal class MarkdownFormatTest { fun `Numbered list is read successfully`() { val mdFormat = MarkdownFormat() val document = mdFormat.parse( - """ + """ # Irrelevant Headline ``` @@ -142,10 +143,9 @@ internal class MarkdownFormatTest { """.trimIndent().byteInputStream() ) - val (tables, scenarios) = document + val elements = document.elements - assertThat(tables).isEmpty() - assertThat(scenarios).hasSize(1) + assertThat(elements).hasSize(1) val thirdSentence = """ Listitem3 @@ -159,7 +159,7 @@ internal class MarkdownFormatTest { """.trimIndent() - val scenario = scenarios[0] + val scenario = elements[0] as Scenario assertThat(scenario.steps).hasSize(4) assertThat(scenario.steps[0].value).isEqualTo("Listitem1\nSentence in first list item.") assertThat(scenario.steps[1].value).isEqualTo("Listitem2\nSentence in second list item.") @@ -171,7 +171,7 @@ internal class MarkdownFormatTest { fun `Column table is read as scenario`() { val mdFormat = MarkdownFormat() val document = mdFormat.parse( - """ + """ # Irrelevant Headline ``` @@ -189,12 +189,11 @@ internal class MarkdownFormatTest { """.trimIndent().byteInputStream() ) - val (tables, scenarios) = document + val elements = document.elements - assertThat(tables).isEmpty() - assertThat(scenarios).hasSize(1) + assertThat(elements).hasSize(1) - val scenario = scenarios[0] + val scenario = elements[0] as Scenario scenario.steps.also { assertThat(it).hasSize(3) assertThat(it[0].value).isEqualTo("Column1") diff --git a/livingdoc-sample/src/test/kotlin/org/livingdoc/example/CalculatorDocument.kt b/livingdoc-sample/src/test/kotlin/org/livingdoc/example/CalculatorDocument.kt index d1d17784..c4912d66 100644 --- a/livingdoc-sample/src/test/kotlin/org/livingdoc/example/CalculatorDocument.kt +++ b/livingdoc-sample/src/test/kotlin/org/livingdoc/example/CalculatorDocument.kt @@ -11,13 +11,7 @@ import org.livingdoc.api.fixtures.scenarios.Binding import org.livingdoc.api.fixtures.scenarios.ScenarioFixture import org.livingdoc.api.fixtures.scenarios.Step -@ExecutableDocument( - value = "local://Calculator.md", - fixtureClasses = [ - CalculatorDocument.CalculatorDecisionTableFixture::class, - CalculatorDocument.CalculatorScenarioFixture::class - ] -) +@ExecutableDocument("local://Calculator.md") class CalculatorDocument { @DecisionTableFixture