From 1055744f5f9f4884e2ac423e375812620ae4502d Mon Sep 17 00:00:00 2001 From: Yogesh Ananda Nikam Date: Sat, 26 Oct 2024 09:40:47 +0530 Subject: [PATCH 1/4] Enhance examples validate command to accept --examples-dir & --spec-dir --- .../kotlin/application/ExamplesCommand.kt | 187 +++++++++++++----- .../server/ExamplesInteractiveServer.kt | 19 +- 2 files changed, 148 insertions(+), 58 deletions(-) diff --git a/application/src/main/kotlin/application/ExamplesCommand.kt b/application/src/main/kotlin/application/ExamplesCommand.kt index 42e5340db..b0c5b389f 100644 --- a/application/src/main/kotlin/application/ExamplesCommand.kt +++ b/application/src/main/kotlin/application/ExamplesCommand.kt @@ -1,12 +1,13 @@ package application +import io.specmatic.core.Feature import io.specmatic.core.Result import io.specmatic.core.Results import io.specmatic.core.SPECMATIC_STUB_DICTIONARY import io.specmatic.core.examples.server.ExamplesInteractiveServer import io.specmatic.core.examples.server.ExamplesInteractiveServer.Companion.validateSingleExample +import io.specmatic.core.examples.server.implicitExternalExampleDirFrom import io.specmatic.core.examples.server.loadExternalExamples -import io.specmatic.core.filters.ScenarioMetadataFilter import io.specmatic.core.log.* import io.specmatic.core.parseContractFileToFeature import io.specmatic.core.pattern.ContractException @@ -16,6 +17,7 @@ import picocli.CommandLine.* import java.io.File import java.lang.Thread.sleep import java.util.concurrent.Callable +import kotlin.system.exitProcess @Command( name = "examples", @@ -174,12 +176,18 @@ For example: ) var filterNot: List = emptyList() - @Option(names = ["--contract-file"], description = ["Contract file path"], required = true) - lateinit var contractFile: File + @Option(names = ["--contract-file", "--spec-file"], description = ["Contract file path"], required = false) + var contractFile: File? = null @Option(names = ["--example-file"], description = ["Example file path"], required = false) val exampleFile: File? = null + @Option(names = ["--spec-dir"], description = ["Specs directory path"], required = false) + val specsDir: File? = null + + @Option(names = ["--example-dir"], description = ["Examples directory path"], required = false) + val examplesDir: File? = null + @Option(names = ["--debug"], description = ["Debug logs"]) var verbose = false @@ -198,6 +206,29 @@ For example: var filterNotName: String = "" override fun call(): Int { + if(contractFile != null && exampleFile != null) return validateExampleFile(contractFile!!, exampleFile) + + if (contractFile != null && examplesDir != null) { + val (exitCode, validationResults) = validateExamplesDir(contractFile!!, examplesDir) + + printValidationResult(validationResults, "Example directory") + if (exitCode == 1) return 1 + if (validationResults.containsFailure()) return 1 + return 0 + } + + if(contractFile != null) return validateImplicitExamplesFrom(contractFile!!) + + if(specsDir != null && examplesDir != null) { + val exitCode = validateAllExamplesAssociatedToEachSpecIn(specsDir, examplesDir) + return exitCode + } + + logger.log("No valid options provided.") + return 1 + } + + private fun validateExampleFile(contractFile: File, exampleFile: File): Int { if (!contractFile.exists()) { logger.log("Could not find file ${contractFile.path}") return 1 @@ -205,74 +236,124 @@ For example: configureLogger(this.verbose) - if (exampleFile != null) { - try { - validateSingleExample(contractFile, exampleFile).throwOnFailure() + try { + validateSingleExample(contractFile, exampleFile).throwOnFailure() - logger.log("The provided example ${exampleFile.name} is valid.") - } catch (e: ContractException) { - logger.log("The provided example ${exampleFile.name} is invalid. Reason:\n") - logger.log(exceptionCauseMessage(e)) - return 1 - } - } else { - val scenarioFilter = ExamplesInteractiveServer.ScenarioFilter(filterName, filterNotName, filter, filterNot) + logger.log("The provided example ${exampleFile.name} is valid.") + return 0 + } catch (e: ContractException) { + logger.log("The provided example ${exampleFile.name} is invalid. Reason:\n") + logger.log(exceptionCauseMessage(e)) + return 1 + } + } + + private fun validateExamplesDir(contractFile: File, examplesDir: File): Pair> { + val feature = parseContractFileToFeature(contractFile) + val (externalExampleDir, externalExamples) = loadExternalExamples( + examplesDir = examplesDir, + specFileName = contractFile.nameWithoutExtension + ) + if (!externalExampleDir.exists()) { + logger.log("$externalExampleDir does not exist, did not find any files to validate") + return 1 to emptyMap() + } + if (externalExamples.none()) { + logger.log("No example files found in $externalExampleDir") + return 1 to emptyMap() + } + return 0 to validateExternalExamples(feature, externalExamples) + } - val (validateInline, validateExternal) = if(!Flags.getBooleanValue("VALIDATE_INLINE_EXAMPLES") && !Flags.getBooleanValue("IGNORE_INLINE_EXAMPLES")) { - true to true - } else { - Flags.getBooleanValue("VALIDATE_INLINE_EXAMPLES") to Flags.getBooleanValue("IGNORE_INLINE_EXAMPLES") + private fun validateAllExamplesAssociatedToEachSpecIn( + specsDir: File, + examplesDir: File + ): Int { + val validationResults = specsDir.walk().flatMap { + validateExamplesDir(it, examplesDir).second.entries.map { entry -> + entry.toPair() } + }.toMap() + printValidationResult(validationResults, "Example directory") + if (validationResults.containsFailure()) return 1 + return 0 + } - val feature = parseContractFileToFeature(contractFile) + private fun validateImplicitExamplesFrom(contractFile: File): Int { + val feature = parseContractFileToFeature(contractFile) - val inlineExampleValidationResults = if(validateInline) { - val inlineExamples = feature.stubsFromExamples.mapValues { - it.value.map { - ScenarioStub(it.first, it.second) - } - } + val (validateInline, validateExternal) = getValidateInlineAndValidateExternalFlags() - ExamplesInteractiveServer.validateMultipleExamples(feature, examples = inlineExamples, inline = true, scenarioFilter = scenarioFilter) - } else emptyMap() + val inlineExampleValidationResults = if (!validateInline) emptyMap() + else validateImplicitInlineExamples(feature) - val externalExampleValidationResults = if(validateExternal) { - val (externalExampleDir, externalExamples) = loadExternalExamples(contractFile) + val externalExampleValidationResults = if (!validateExternal) emptyMap() + else { + val (exitCode, validationResults) + = validateExamplesDir(contractFile, implicitExternalExampleDirFrom(contractFile)) + if(exitCode == 1) exitProcess(1) + validationResults + } - if(!externalExampleDir.exists()) { - logger.log("$externalExampleDir does not exist, did not find any files to validate") - return 1 - } + val hasFailures = + inlineExampleValidationResults.containsFailure() || externalExampleValidationResults.containsFailure() - if(externalExamples.none()) { - logger.log("No example files found in $externalExampleDir") - return 1 - } + printValidationResult(inlineExampleValidationResults, "Inline example") + printValidationResult(externalExampleValidationResults, "Example file") - ExamplesInteractiveServer.validateMultipleExamples(feature, examples = externalExamples, scenarioFilter = scenarioFilter) - } else emptyMap() + if (hasFailures) return 1 + return 0 + } - val hasFailures = inlineExampleValidationResults.any { it.value is Result.Failure } || externalExampleValidationResults.any { it.value is Result.Failure } + private fun validateImplicitInlineExamples(feature: Feature): Map { + return ExamplesInteractiveServer.validateMultipleExamples( + feature, + examples = feature.stubsFromExamples.mapValues { + it.value.map { ScenarioStub(it.first, it.second) } + }, + inline = true, + scenarioFilter = ExamplesInteractiveServer.ScenarioFilter( + filterName, + filterNotName, + filter, + filterNot + ) + ) + } - printValidationResult(inlineExampleValidationResults, "Inline example") - printValidationResult(externalExampleValidationResults, "Example file") + private fun validateExternalExamples( + feature: Feature, + externalExamples: Map> + ): Map { + return ExamplesInteractiveServer.validateMultipleExamples( + feature, + examples = externalExamples, + scenarioFilter = ExamplesInteractiveServer.ScenarioFilter( + filterName, + filterNotName, + filter, + filterNot + ) + ) + } - if(hasFailures) - return 1 - } + private fun getValidateInlineAndValidateExternalFlags(): Pair { + return when { + !Flags.getBooleanValue("VALIDATE_INLINE_EXAMPLES") && !Flags.getBooleanValue( + "IGNORE_INLINE_EXAMPLES" + ) -> true to true - return 0 + else -> Flags.getBooleanValue("VALIDATE_INLINE_EXAMPLES") to Flags.getBooleanValue("IGNORE_INLINE_EXAMPLES") + } } private fun printValidationResult(validationResults: Map, tag: String) { - if(validationResults.isEmpty()) + if (validationResults.isEmpty()) return - val hasFailures = validationResults.any { it.value is Result.Failure } - - val titleTag = tag.split(" ").joinToString(" ") { if(it.isBlank()) it else it.capitalizeFirstChar() } + val titleTag = tag.split(" ").joinToString(" ") { if (it.isBlank()) it else it.capitalizeFirstChar() } - if(hasFailures) { + if (validationResults.containsFailure()) { println() logger.log("=============== $titleTag Validation Results ===============") @@ -290,6 +371,10 @@ For example: logger.log(Results(validationResults.values.toList()).summary()) logger.log("=".repeat(summaryTitle.length)) } + + private fun Map.containsFailure(): Boolean { + return this.any { it.value is Result.Failure } + } } @Command( diff --git a/core/src/main/kotlin/io/specmatic/core/examples/server/ExamplesInteractiveServer.kt b/core/src/main/kotlin/io/specmatic/core/examples/server/ExamplesInteractiveServer.kt index 1240ccb5c..62e99dbaa 100644 --- a/core/src/main/kotlin/io/specmatic/core/examples/server/ExamplesInteractiveServer.kt +++ b/core/src/main/kotlin/io/specmatic/core/examples/server/ExamplesInteractiveServer.kt @@ -717,20 +717,25 @@ data class ExampleTestResponse( } } -fun loadExternalExamples(contractFile: File): Pair>> { - val examplesDir = - contractFile.absoluteFile.parentFile.resolve(contractFile.nameWithoutExtension + "_examples") +fun loadExternalExamples( + examplesDir: File, + specFileName: String +): Pair>> { if (!examplesDir.isDirectory) { logger.log("$examplesDir does not exist, did not find any files to validate") exitProcess(1) } return examplesDir to examplesDir.walk().mapNotNull { - if (it.isFile) - Pair(it.path, it) - else - null + if (it.isFile.not()) return@mapNotNull null + if (it.nameWithoutExtension != "${specFileName}_examples") return@mapNotNull null + + Pair(it.path, it) }.toMap().mapValues { listOf(ScenarioStub.readFromFile(it.value)) } } + +fun implicitExternalExampleDirFrom(contractFile: File): File { + return contractFile.absoluteFile.parentFile.resolve(contractFile.nameWithoutExtension + "_examples") +} From 9ed5a3b77d083f0e34a4c05d444c4811fcf62b93 Mon Sep 17 00:00:00 2001 From: Yogesh Ananda Nikam Date: Sat, 26 Oct 2024 14:32:28 +0530 Subject: [PATCH 2/4] Introduce --examples-dir instead of reusing the same --example-dir for two different purposes --- .../kotlin/application/ExamplesCommand.kt | 50 +++++++++++++------ .../server/ExamplesInteractiveServer.kt | 15 +++--- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/application/src/main/kotlin/application/ExamplesCommand.kt b/application/src/main/kotlin/application/ExamplesCommand.kt index b0c5b389f..90c7dc587 100644 --- a/application/src/main/kotlin/application/ExamplesCommand.kt +++ b/application/src/main/kotlin/application/ExamplesCommand.kt @@ -182,10 +182,17 @@ For example: @Option(names = ["--example-file"], description = ["Example file path"], required = false) val exampleFile: File? = null - @Option(names = ["--spec-dir"], description = ["Specs directory path"], required = false) + @Option(names = ["--example-dir"], description = ["Examples directory path associated to a single spec"], required = false) + val exampleDir: File? = null + + @Option(names = ["--specs-dir"], description = ["Specs directory path"], required = false) val specsDir: File? = null - @Option(names = ["--example-dir"], description = ["Examples directory path"], required = false) + @Option( + names = ["--examples-dir"], + description = ["Examples directory path containing multiple example directories associated to multiple specs"], + required = false + ) val examplesDir: File? = null @Option(names = ["--debug"], description = ["Debug logs"]) @@ -208,8 +215,8 @@ For example: override fun call(): Int { if(contractFile != null && exampleFile != null) return validateExampleFile(contractFile!!, exampleFile) - if (contractFile != null && examplesDir != null) { - val (exitCode, validationResults) = validateExamplesDir(contractFile!!, examplesDir) + if (contractFile != null && exampleDir != null) { + val (exitCode, validationResults) = validateExamplesDir(contractFile!!, exampleDir) printValidationResult(validationResults, "Example directory") if (exitCode == 1) return 1 @@ -248,12 +255,9 @@ For example: } } - private fun validateExamplesDir(contractFile: File, examplesDir: File): Pair> { + private fun validateExamplesDir(contractFile: File, examplesDir: File, enableLogging: Boolean = true): Pair> { val feature = parseContractFileToFeature(contractFile) - val (externalExampleDir, externalExamples) = loadExternalExamples( - examplesDir = examplesDir, - specFileName = contractFile.nameWithoutExtension - ) + val (externalExampleDir, externalExamples) = loadExternalExamples(examplesDir = examplesDir) if (!externalExampleDir.exists()) { logger.log("$externalExampleDir does not exist, did not find any files to validate") return 1 to emptyMap() @@ -262,19 +266,27 @@ For example: logger.log("No example files found in $externalExampleDir") return 1 to emptyMap() } - return 0 to validateExternalExamples(feature, externalExamples) + return 0 to validateExternalExamples(feature, externalExamples, enableLogging) } private fun validateAllExamplesAssociatedToEachSpecIn( specsDir: File, examplesDir: File ): Int { - val validationResults = specsDir.walk().flatMap { - validateExamplesDir(it, examplesDir).second.entries.map { entry -> + val validationResults = specsDir.walk().filter { it.isFile }.flatMapIndexed { index, it -> + val associatedExamplesDir = examplesDir.getAssociatedExampleDirFor(it) ?: return@flatMapIndexed emptyList() + + logger.log("${index.inc()}. Validating examples in ${associatedExamplesDir.name} associated to ${it.name}...${System.lineSeparator()}") + val results = validateExamplesDir(it, associatedExamplesDir, false).second.entries.map { entry -> entry.toPair() } + + printValidationResult(results.toMap(), "The ${associatedExamplesDir.name} Directory") + logger.log(System.lineSeparator()) + results }.toMap() - printValidationResult(validationResults, "Example directory") + logger.log("Summary:") + printValidationResult(validationResults, "Overall") if (validationResults.containsFailure()) return 1 return 0 } @@ -323,7 +335,8 @@ For example: private fun validateExternalExamples( feature: Feature, - externalExamples: Map> + externalExamples: Map>, + enableLogging: Boolean = true ): Map { return ExamplesInteractiveServer.validateMultipleExamples( feature, @@ -333,7 +346,8 @@ For example: filterNotName, filter, filterNot - ) + ), + enableLogging = enableLogging ) } @@ -375,6 +389,12 @@ For example: private fun Map.containsFailure(): Boolean { return this.any { it.value is Result.Failure } } + + private fun File.getAssociatedExampleDirFor(specFile: File): File? { + return this.walk().firstOrNull { exampleDir -> + exampleDir.isFile.not() && exampleDir.nameWithoutExtension == "${specFile.nameWithoutExtension}_examples" + } + } } @Command( diff --git a/core/src/main/kotlin/io/specmatic/core/examples/server/ExamplesInteractiveServer.kt b/core/src/main/kotlin/io/specmatic/core/examples/server/ExamplesInteractiveServer.kt index 62e99dbaa..8cddb1aea 100644 --- a/core/src/main/kotlin/io/specmatic/core/examples/server/ExamplesInteractiveServer.kt +++ b/core/src/main/kotlin/io/specmatic/core/examples/server/ExamplesInteractiveServer.kt @@ -515,11 +515,17 @@ class ExamplesInteractiveServer( return validateMultipleExamples(feature, examples, false, scenarioFilter) } - fun validateMultipleExamples(feature: Feature, examples: Map> = emptyMap(), inline: Boolean = false, scenarioFilter: ScenarioFilter = ScenarioFilter()): Map { + fun validateMultipleExamples( + feature: Feature, + examples: Map> = emptyMap(), + inline: Boolean = false, + scenarioFilter: ScenarioFilter = ScenarioFilter(), + enableLogging: Boolean = true + ): Map { val updatedFeature = scenarioFilter.filter(feature) val results = examples.mapValues { (name, exampleList) -> - logger.log("Validating $name") + if(enableLogging) logger.log("Validating $name") exampleList.mapNotNull { example -> try { @@ -718,8 +724,7 @@ data class ExampleTestResponse( } fun loadExternalExamples( - examplesDir: File, - specFileName: String + examplesDir: File ): Pair>> { if (!examplesDir.isDirectory) { logger.log("$examplesDir does not exist, did not find any files to validate") @@ -728,8 +733,6 @@ fun loadExternalExamples( return examplesDir to examplesDir.walk().mapNotNull { if (it.isFile.not()) return@mapNotNull null - if (it.nameWithoutExtension != "${specFileName}_examples") return@mapNotNull null - Pair(it.path, it) }.toMap().mapValues { listOf(ScenarioStub.readFromFile(it.value)) From 8993d95c100bd9705fbba14fb89486c105054224 Mon Sep 17 00:00:00 2001 From: Yogesh Ananda Nikam Date: Sat, 26 Oct 2024 14:51:21 +0530 Subject: [PATCH 3/4] Add a variant in validate command where the specs-dir contains both the specs and the examples associated with it --- .../kotlin/application/ExamplesCommand.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/application/src/main/kotlin/application/ExamplesCommand.kt b/application/src/main/kotlin/application/ExamplesCommand.kt index 90c7dc587..37c20df67 100644 --- a/application/src/main/kotlin/application/ExamplesCommand.kt +++ b/application/src/main/kotlin/application/ExamplesCommand.kt @@ -182,18 +182,18 @@ For example: @Option(names = ["--example-file"], description = ["Example file path"], required = false) val exampleFile: File? = null - @Option(names = ["--example-dir"], description = ["Examples directory path associated to a single spec"], required = false) - val exampleDir: File? = null + @Option(names = ["--examples-dir"], description = ["Examples directory path associated to a single spec"], required = false) + val examplesDir: File? = null @Option(names = ["--specs-dir"], description = ["Specs directory path"], required = false) val specsDir: File? = null @Option( - names = ["--examples-dir"], + names = ["--examples-base-dir"], description = ["Examples directory path containing multiple example directories associated to multiple specs"], required = false ) - val examplesDir: File? = null + val examplesBaseDir: File? = null @Option(names = ["--debug"], description = ["Debug logs"]) var verbose = false @@ -213,10 +213,10 @@ For example: var filterNotName: String = "" override fun call(): Int { - if(contractFile != null && exampleFile != null) return validateExampleFile(contractFile!!, exampleFile) + if (contractFile != null && exampleFile != null) return validateExampleFile(contractFile!!, exampleFile) - if (contractFile != null && exampleDir != null) { - val (exitCode, validationResults) = validateExamplesDir(contractFile!!, exampleDir) + if (contractFile != null && examplesDir != null) { + val (exitCode, validationResults) = validateExamplesDir(contractFile!!, examplesDir) printValidationResult(validationResults, "Example directory") if (exitCode == 1) return 1 @@ -224,10 +224,14 @@ For example: return 0 } - if(contractFile != null) return validateImplicitExamplesFrom(contractFile!!) + if (contractFile != null) return validateImplicitExamplesFrom(contractFile!!) - if(specsDir != null && examplesDir != null) { - val exitCode = validateAllExamplesAssociatedToEachSpecIn(specsDir, examplesDir) + if (specsDir != null && examplesBaseDir != null) { + val exitCode = validateAllExamplesAssociatedToEachSpecIn(specsDir, examplesBaseDir) + return exitCode + } + if (specsDir != null) { + val exitCode = validateAllExamplesAssociatedToEachSpecIn(specsDir, specsDir) return exitCode } From cf4201af13ef0dc6f81f7968eab6073522142498 Mon Sep 17 00:00:00 2001 From: Yogesh Ananda Nikam Date: Sat, 26 Oct 2024 15:32:26 +0530 Subject: [PATCH 4/4] Update descriptions of certain CLI arguments and refactor some stuff --- .../kotlin/application/ExamplesCommand.kt | 71 ++++++++++--------- .../server/ExamplesInteractiveServer.kt | 10 +-- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/application/src/main/kotlin/application/ExamplesCommand.kt b/application/src/main/kotlin/application/ExamplesCommand.kt index 37c20df67..31dff990a 100644 --- a/application/src/main/kotlin/application/ExamplesCommand.kt +++ b/application/src/main/kotlin/application/ExamplesCommand.kt @@ -6,7 +6,7 @@ import io.specmatic.core.Results import io.specmatic.core.SPECMATIC_STUB_DICTIONARY import io.specmatic.core.examples.server.ExamplesInteractiveServer import io.specmatic.core.examples.server.ExamplesInteractiveServer.Companion.validateSingleExample -import io.specmatic.core.examples.server.implicitExternalExampleDirFrom +import io.specmatic.core.examples.server.defaultExternalExampleDirFrom import io.specmatic.core.examples.server.loadExternalExamples import io.specmatic.core.log.* import io.specmatic.core.parseContractFileToFeature @@ -19,6 +19,9 @@ import java.lang.Thread.sleep import java.util.concurrent.Callable import kotlin.system.exitProcess +private const val SUCCESS_EXIT_CODE = 0 +private const val FAILURE_EXIT_CODE = 1 + @Command( name = "examples", mixinStandardHelpOptions = true, @@ -101,11 +104,11 @@ For example: override fun call(): Int { if (contractFile == null) { println("No contract file provided. Use a subcommand or provide a contract file. Use --help for more details.") - return 1 + return FAILURE_EXIT_CODE } if (!contractFile!!.exists()) { logger.log("Could not find file ${contractFile!!.path}") - return 1 + return FAILURE_EXIT_CODE } configureLogger(this.verbose) @@ -122,10 +125,10 @@ For example: ) } catch (e: Throwable) { logger.log(e) - return 1 + return FAILURE_EXIT_CODE } - return 0 + return SUCCESS_EXIT_CODE } @Command( @@ -182,15 +185,15 @@ For example: @Option(names = ["--example-file"], description = ["Example file path"], required = false) val exampleFile: File? = null - @Option(names = ["--examples-dir"], description = ["Examples directory path associated to a single spec"], required = false) + @Option(names = ["--examples-dir"], description = ["External examples directory path for a single API specification (If you are not following the default naming convention for external examples directory)"], required = false) val examplesDir: File? = null - @Option(names = ["--specs-dir"], description = ["Specs directory path"], required = false) + @Option(names = ["--specs-dir"], description = ["Directory with the API specification files"], required = false) val specsDir: File? = null @Option( names = ["--examples-base-dir"], - description = ["Examples directory path containing multiple example directories associated to multiple specs"], + description = ["Base directory which contains multiple external examples directories each named as per the Specmatic naming convention to associate them with the corresponding API specification"], required = false ) val examplesBaseDir: File? = null @@ -219,9 +222,9 @@ For example: val (exitCode, validationResults) = validateExamplesDir(contractFile!!, examplesDir) printValidationResult(validationResults, "Example directory") - if (exitCode == 1) return 1 - if (validationResults.containsFailure()) return 1 - return 0 + if (exitCode == 1) return FAILURE_EXIT_CODE + if (validationResults.containsFailure()) return FAILURE_EXIT_CODE + return SUCCESS_EXIT_CODE } if (contractFile != null) return validateImplicitExamplesFrom(contractFile!!) @@ -235,14 +238,14 @@ For example: return exitCode } - logger.log("No valid options provided.") - return 1 + logger.log("Invalid combination of CLI options. Please refer to the help section using --help command to understand how to use this command") + return FAILURE_EXIT_CODE } private fun validateExampleFile(contractFile: File, exampleFile: File): Int { if (!contractFile.exists()) { logger.log("Could not find file ${contractFile.path}") - return 1 + return FAILURE_EXIT_CODE } configureLogger(this.verbose) @@ -251,11 +254,11 @@ For example: validateSingleExample(contractFile, exampleFile).throwOnFailure() logger.log("The provided example ${exampleFile.name} is valid.") - return 0 + return SUCCESS_EXIT_CODE } catch (e: ContractException) { logger.log("The provided example ${exampleFile.name} is invalid. Reason:\n") logger.log(exceptionCauseMessage(e)) - return 1 + return FAILURE_EXIT_CODE } } @@ -264,21 +267,21 @@ For example: val (externalExampleDir, externalExamples) = loadExternalExamples(examplesDir = examplesDir) if (!externalExampleDir.exists()) { logger.log("$externalExampleDir does not exist, did not find any files to validate") - return 1 to emptyMap() + return FAILURE_EXIT_CODE to emptyMap() } if (externalExamples.none()) { logger.log("No example files found in $externalExampleDir") - return 1 to emptyMap() + return FAILURE_EXIT_CODE to emptyMap() } - return 0 to validateExternalExamples(feature, externalExamples, enableLogging) + return SUCCESS_EXIT_CODE to validateExternalExamples(feature, externalExamples, enableLogging) } private fun validateAllExamplesAssociatedToEachSpecIn( specsDir: File, - examplesDir: File + examplesBaseDir: File ): Int { val validationResults = specsDir.walk().filter { it.isFile }.flatMapIndexed { index, it -> - val associatedExamplesDir = examplesDir.getAssociatedExampleDirFor(it) ?: return@flatMapIndexed emptyList() + val associatedExamplesDir = examplesBaseDir.associatedExampleDirFor(it) ?: return@flatMapIndexed emptyList() logger.log("${index.inc()}. Validating examples in ${associatedExamplesDir.name} associated to ${it.name}...${System.lineSeparator()}") val results = validateExamplesDir(it, associatedExamplesDir, false).second.entries.map { entry -> @@ -291,8 +294,8 @@ For example: }.toMap() logger.log("Summary:") printValidationResult(validationResults, "Overall") - if (validationResults.containsFailure()) return 1 - return 0 + if (validationResults.containsFailure()) return FAILURE_EXIT_CODE + return SUCCESS_EXIT_CODE } private fun validateImplicitExamplesFrom(contractFile: File): Int { @@ -301,12 +304,12 @@ For example: val (validateInline, validateExternal) = getValidateInlineAndValidateExternalFlags() val inlineExampleValidationResults = if (!validateInline) emptyMap() - else validateImplicitInlineExamples(feature) + else validateInlineExamples(feature) val externalExampleValidationResults = if (!validateExternal) emptyMap() else { val (exitCode, validationResults) - = validateExamplesDir(contractFile, implicitExternalExampleDirFrom(contractFile)) + = validateExamplesDir(contractFile, defaultExternalExampleDirFrom(contractFile)) if(exitCode == 1) exitProcess(1) validationResults } @@ -317,15 +320,17 @@ For example: printValidationResult(inlineExampleValidationResults, "Inline example") printValidationResult(externalExampleValidationResults, "Example file") - if (hasFailures) return 1 - return 0 + if (hasFailures) return FAILURE_EXIT_CODE + return SUCCESS_EXIT_CODE } - private fun validateImplicitInlineExamples(feature: Feature): Map { - return ExamplesInteractiveServer.validateMultipleExamples( + private fun validateInlineExamples(feature: Feature): Map { + return ExamplesInteractiveServer.validateExamples( feature, - examples = feature.stubsFromExamples.mapValues { - it.value.map { ScenarioStub(it.first, it.second) } + examples = feature.stubsFromExamples.mapValues { (_, stub) -> + stub.map { (request, response) -> + ScenarioStub(request, response) + } }, inline = true, scenarioFilter = ExamplesInteractiveServer.ScenarioFilter( @@ -342,7 +347,7 @@ For example: externalExamples: Map>, enableLogging: Boolean = true ): Map { - return ExamplesInteractiveServer.validateMultipleExamples( + return ExamplesInteractiveServer.validateExamples( feature, examples = externalExamples, scenarioFilter = ExamplesInteractiveServer.ScenarioFilter( @@ -394,7 +399,7 @@ For example: return this.any { it.value is Result.Failure } } - private fun File.getAssociatedExampleDirFor(specFile: File): File? { + private fun File.associatedExampleDirFor(specFile: File): File? { return this.walk().firstOrNull { exampleDir -> exampleDir.isFile.not() && exampleDir.nameWithoutExtension == "${specFile.nameWithoutExtension}_examples" } diff --git a/core/src/main/kotlin/io/specmatic/core/examples/server/ExamplesInteractiveServer.kt b/core/src/main/kotlin/io/specmatic/core/examples/server/ExamplesInteractiveServer.kt index 8cddb1aea..d8bdbfb98 100644 --- a/core/src/main/kotlin/io/specmatic/core/examples/server/ExamplesInteractiveServer.kt +++ b/core/src/main/kotlin/io/specmatic/core/examples/server/ExamplesInteractiveServer.kt @@ -151,7 +151,7 @@ class ExamplesInteractiveServer( exampleFilePath to listOf(ScenarioStub.readFromFile(File(exampleFilePath))) } - val results = validateMultipleExamples(contractFile, examples = examples) + val results = validateExamples(contractFile, examples = examples) val validationResults = results.map { (exampleFilePath, result) -> try { @@ -510,12 +510,12 @@ class ExamplesInteractiveServer( } } - fun validateMultipleExamples(contractFile: File, examples: Map> = emptyMap(), scenarioFilter: ScenarioFilter = ScenarioFilter()): Map { + fun validateExamples(contractFile: File, examples: Map> = emptyMap(), scenarioFilter: ScenarioFilter = ScenarioFilter()): Map { val feature = parseContractFileToFeature(contractFile) - return validateMultipleExamples(feature, examples, false, scenarioFilter) + return validateExamples(feature, examples, false, scenarioFilter) } - fun validateMultipleExamples( + fun validateExamples( feature: Feature, examples: Map> = emptyMap(), inline: Boolean = false, @@ -739,6 +739,6 @@ fun loadExternalExamples( } } -fun implicitExternalExampleDirFrom(contractFile: File): File { +fun defaultExternalExampleDirFrom(contractFile: File): File { return contractFile.absoluteFile.parentFile.resolve(contractFile.nameWithoutExtension + "_examples") }