application.TestCommand.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of specmatic-executable Show documentation
Show all versions of specmatic-executable Show documentation
Command-line standalone executable jar for Specmatic
package application
import application.test.ContractExecutionListener
import `in`.specmatic.core.APPLICATION_NAME_LOWER_CASE
import `in`.specmatic.core.Configuration
import `in`.specmatic.core.DEFAULT_TIMEOUT_IN_SECONDS
import `in`.specmatic.core.Flags
import `in`.specmatic.core.log.Verbose
import `in`.specmatic.core.log.logger
import `in`.specmatic.core.pattern.ContractException
import `in`.specmatic.core.utilities.exitWithMessage
import `in`.specmatic.core.utilities.newXMLBuilder
import `in`.specmatic.core.utilities.xmlToString
import `in`.specmatic.test.SpecmaticJUnitSupport
import `in`.specmatic.test.SpecmaticJUnitSupport.Companion.CONFIG_FILE_NAME
import `in`.specmatic.test.SpecmaticJUnitSupport.Companion.CONTRACT_PATHS
import `in`.specmatic.test.SpecmaticJUnitSupport.Companion.ENV_NAME
import `in`.specmatic.test.SpecmaticJUnitSupport.Companion.FILTER_NAME_PROPERTY
import `in`.specmatic.test.SpecmaticJUnitSupport.Companion.FILTER_NOT_NAME_PROPERTY
import `in`.specmatic.test.SpecmaticJUnitSupport.Companion.HOST
import `in`.specmatic.test.SpecmaticJUnitSupport.Companion.INLINE_SUGGESTIONS
import `in`.specmatic.test.SpecmaticJUnitSupport.Companion.PORT
import `in`.specmatic.test.SpecmaticJUnitSupport.Companion.SUGGESTIONS_PATH
import `in`.specmatic.test.SpecmaticJUnitSupport.Companion.TEST_BASE_URL
import `in`.specmatic.test.SpecmaticJUnitSupport.Companion.TIMEOUT
import `in`.specmatic.test.SpecmaticJUnitSupport.Companion.VARIABLES_FILE_NAME
import org.junit.platform.engine.discovery.DiscoverySelectors.selectClass
import org.junit.platform.launcher.Launcher
import org.junit.platform.launcher.LauncherDiscoveryRequest
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder
import org.springframework.beans.factory.annotation.Autowired
import org.w3c.dom.Document
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import org.xml.sax.InputSource
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import java.io.File
import java.io.PrintWriter
import java.io.StringReader
import java.nio.file.Paths
import java.util.concurrent.Callable
private const val SYSTEM_OUT_TESTCASE_TAG = "system-out"
private const val DISPLAY_NAME_PREFIX_IN_SYSTEM_OUT_TAG_TEXT = "display-name: "
private const val s = "Contract"
@Command(name = "test",
mixinStandardHelpOptions = true,
description = ["Run contract as tests"])
class TestCommand : Callable {
@Autowired
lateinit var junitLauncher: Launcher
@CommandLine.Parameters(arity = "0..*", description = ["Contract file paths"])
var contractPaths: List = emptyList()
@Option(names = ["--host"], description = ["The host to bind to, e.g. localhost or some locally bound IP"], defaultValue = "localhost")
lateinit var host: String
@Option(names = ["--port"], description = ["The port to bind to"])
var port: Int = 0
@Option(names = ["--testBaseURL"], description = ["The base URL, use this instead of host and port"], defaultValue = "")
lateinit var testBaseURL: String
@Option(names = ["--suggestionsPath"], description = ["Location of the suggestions file"], defaultValue = "")
lateinit var suggestionsPath: String
@Option(names = ["--suggestions"], description = ["A json value with scenario name and multiple suggestions"], defaultValue = "")
var suggestions: String = ""
@Option(names = ["--filter-name"], description = ["Run only tests with this value in their name"], defaultValue = "\${env:SPECMATIC_FILTER_NAME}")
var filterName: String = ""
@Option(names = ["--filter-not-name"], description = ["Run only tests which do not have this value in their name"], defaultValue = "\${env:SPECMATIC_FILTER_NOT_NAME}")
var filterNotName: String = ""
@Option(names = ["--env"], description = ["Environment name"])
var envName: String = ""
@Option(names = ["--https"], description = ["Use https instead of the default http"], required = false)
var useHttps: Boolean = false
@Option(names = ["--timeout"], description = ["Specify a timeout in seconds for the test requests. Default value is $DEFAULT_TIMEOUT_IN_SECONDS"], required = false, defaultValue = DEFAULT_TIMEOUT_IN_SECONDS)
var timeout: Int = 60
@Option(names = ["--junitReportDir"], description = ["Create junit xml reports in this directory"])
var junitReportDirName: String? = null
@Option(names = ["--config"], description = ["Configuration file name ($APPLICATION_NAME_LOWER_CASE.json by default)"])
var configFileName: String? = null
@Option(names = ["--variables"], description = ["Variables file name ($APPLICATION_NAME_LOWER_CASE.json by default)"])
var variablesFileName: String? = null
@Option(names = ["--debug"], description = ["Debug logs"])
var verboseMode: Boolean = false
override fun call() = try {
setParallelism()
if(verboseMode) {
logger = Verbose()
}
configFileName?.let {
Configuration.globalConfigFileName = it
System.setProperty(CONFIG_FILE_NAME, it)
}
if(port == 0) {
port = when {
useHttps -> 443
else -> 9000
}
}
val protocol = when {
port == 443 -> "https"
useHttps -> "https"
else -> "http"
}
System.setProperty(HOST, host)
System.setProperty(PORT, port.toString())
System.setProperty(TIMEOUT, timeout.toString())
System.setProperty(SUGGESTIONS_PATH, suggestionsPath)
System.setProperty(INLINE_SUGGESTIONS, suggestions)
System.setProperty(ENV_NAME, envName)
System.setProperty("protocol", protocol)
if(filterName.isNotBlank()) {
System.setProperty(FILTER_NAME_PROPERTY, filterName)
}
if(filterNotName.isNotBlank()) {
System.setProperty(FILTER_NOT_NAME_PROPERTY, filterNotName)
}
variablesFileName?.let {
System.setProperty(VARIABLES_FILE_NAME, it)
}
if(testBaseURL.isNotEmpty())
System.setProperty(TEST_BASE_URL, testBaseURL)
if(contractPaths.isNotEmpty()) System.setProperty(CONTRACT_PATHS, contractPaths.joinToString(","))
val request: LauncherDiscoveryRequest = LauncherDiscoveryRequestBuilder.request()
.selectors(selectClass(SpecmaticJUnitSupport::class.java))
.build()
junitLauncher.discover(request)
val contractExecutionListener = ContractExecutionListener()
junitLauncher.registerTestExecutionListeners(contractExecutionListener)
junitReportDirName?.let { dirName ->
val reportListener = org.junit.platform.reporting.legacy.xml.LegacyXmlReportGeneratingListener(Paths.get(dirName), PrintWriter(System.out, true))
junitLauncher.registerTestExecutionListeners(reportListener)
}
junitLauncher.execute(request)
junitReportDirName?.let {
val reportDirectory = File(it)
val reportFile = reportDirectory.resolve("TEST-junit-jupiter.xml")
if(reportFile.isFile) {
val updatedJUnitXML = updateNamesInJUnitXML(reportFile.readText())
reportFile.writeText(updatedJUnitXML)
} else {
throw ContractException("Was expecting a JUnit report file called TEST-junit-jupiter.xml inside $junitReportDirName but could not find it.")
}
}
contractExecutionListener.exitProcess()
}
catch (e: Throwable) {
logger.log(e)
}
private fun setParallelism() {
Flags.testParallelism()?.let { parallelism ->
validateParallelism(parallelism)
System.setProperty("junit.jupiter.execution.parallel.enabled", "true");
when (parallelism) {
"auto" -> {
logger.log("Running contract tests in parallel (dynamically determined number of threads)")
System.setProperty("junit.jupiter.execution.parallel.config.strategy", "dynamic")
}
else -> {
logger.log("Running contract tests in parallel in $parallelism threads")
System.setProperty("junit.jupiter.execution.parallel.config.strategy", "fixed")
System.setProperty("junit.jupiter.execution.parallel.config.fixed.parallelism", parallelism)
}
}
}
}
private fun validateParallelism(parallelism: String) {
if(parallelism == "auto")
return
try {
parallelism.toInt()
} catch(e: Throwable) {
exitWithMessage("The value of the ${Flags.SPECMATIC_TEST_PARALLELISM} environment variable must be either 'true' or an integer value")
}
}
}
private const val ORIGINAL_JUNIT_TEST_SUITE_NAME = "JUnit Jupiter"
private const val UPDATED_JUNIT_TEST_SUITE_NAME = "Contract Tests"
private const val TEST_NAME_ATTRIBUTE = "name"
internal fun updateNamesInJUnitXML(junitReport: String): String {
val junitReportWithUpdatedTestSuiteTitle = junitReport.replace(
ORIGINAL_JUNIT_TEST_SUITE_NAME,
UPDATED_JUNIT_TEST_SUITE_NAME
)
val builder = newXMLBuilder()
val reportDocument: Document = builder.parse(InputSource(StringReader(junitReportWithUpdatedTestSuiteTitle)))
for (i in 0..reportDocument.documentElement.childNodes.length.minus(1)) {
val testCaseNode = reportDocument.documentElement.childNodes.item(i)
if (testCaseNode.nodeName != "testcase") continue
val systemOutChildNode = findFirstChildNodeByName(testCaseNode.childNodes, SYSTEM_OUT_TESTCASE_TAG) ?: continue
val cdataChildNode = systemOutChildNode.childNodes.item(0) ?: continue
val systemOutTextContent = cdataChildNode.textContent ?: continue
val displayNameLine = systemOutTextContent.lines().find { line ->
line.startsWith(DISPLAY_NAME_PREFIX_IN_SYSTEM_OUT_TAG_TEXT)
} ?: continue
val testName = displayNameLine.removePrefix(DISPLAY_NAME_PREFIX_IN_SYSTEM_OUT_TAG_TEXT).trim()
testCaseNode.attributes.getNamedItem(TEST_NAME_ATTRIBUTE).nodeValue = testName
}
return xmlToString(reportDocument)
}
internal fun findFirstChildNodeByName(nodes: NodeList, nodeName: String): Node? {
for(i in 0..nodes.length.minus(1)) {
val childNode = nodes.item(i)
if(childNode.nodeName == nodeName)
return childNode
}
return null
}