Skip to content
This repository was archived by the owner on Mar 25, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<DecisionTable>, fixtures: List<KClass<*>>): Map<DecisionTable, Class<*>> {
/*
* - 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<DecisionTable, DecisionTableFixtureModel?>,
fixtureModelToFixtureClass: Map<DecisionTableFixtureModel, Class<*>?>
): Map<DecisionTable, Class<*>> =
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<KClass<*>>) =
fixtureClasses.map { it.java }.associateByTo(mutableMapOf()) { clazz ->
DecisionTableFixtureModel(clazz)
}
fun findMatchingFixture(decisionTable: DecisionTable, fixtures: List<Class<*>>): Class<*>? {
val headerNames = decisionTable.headers.map { it.name }
val numberOfHeaders = headerNames.size

private fun mapTablesToModels(tableModels: Iterable<DecisionTableFixtureModel>, tables: Iterable<DecisionTable>) =
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<String>, matchingFixtures: List<Class<*>>)
: RuntimeException("Could not identify a unique fixture matching the Decision Table's headers " +
"${headerNames.map { "'$it'" }}. Matching fixtures found: $matchingFixtures")

}
142 changes: 120 additions & 22 deletions livingdoc-engine/src/main/kotlin/org/livingdoc/engine/LivingDoc.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<DecisionTable, Class<*>> {
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<ExecutableExample<*, *>> {
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<A : Example, B : ExampleResult>(
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<DecisionTable, DecisionTableResult>(example, execution)

class ExecutableScenario(
example: Scenario,
execution: (Scenario) -> ScenarioResult
) : ExecutableExample<Scenario, ScenarioResult>(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<Class<*>>,
val scenarioFixtures: List<Class<*>>
) {

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<out Annotation>): List<Class<*>> {
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)

}

}
Original file line number Diff line number Diff line change
@@ -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<*>>): Class<*>? {
return null
}

}
Original file line number Diff line number Diff line change
@@ -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<DecisionTableResult> = emptyList(),
val scenarioResults: List<ScenarioResult> = emptyList()
val results: List<ExampleResult> = emptyList()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.livingdoc.engine.execution.examples

interface ExampleResult {
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ class DecisionTableFixtureModel(

val inputFields: List<Field>
val inputMethods: List<Method>
val inputAliases: MutableSet<String>
private val inputAliases: MutableSet<String>
private val inputAliasToField: MutableMap<String, Field>
private val inputAliasToMethod: MutableMap<String, Method>

val checkMethods: List<Method>
private val checkAliases: Set<String>
private val checkAliasToMethod: Map<String, Method>

val aliases: Set<String>
get() = inputAliases + checkAliases

init {

// method analysis
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Header>,
val rows: List<RowResult>,
var result: Result = Result.Unknown
) {
val headers: List<Header>,
val rows: List<RowResult>,
var result: Result = Result.Unknown
) : ExampleResult {

companion object {
fun from(decisionTable: DecisionTable): DecisionTableResult {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<StepResult>,
var result: Result = Result.Unknown
) {
val steps: List<StepResult>,
var result: Result = Result.Unknown
) : ExampleResult {

companion object {
fun from(scenario: Scenario): ScenarioResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<LivingDocContext> {

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)
Expand All @@ -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,
Expand Down
Loading