
io.kotest.extensions.allure.AllureWriter.kt Maven / Gradle / Ivy
package io.kotest.extensions.allure
import io.kotest.core.descriptors.Descriptor
import io.kotest.core.descriptors.TestPath
import io.kotest.core.test.TestCase
import io.kotest.core.test.TestCaseSeverityLevel
import io.kotest.core.test.TestResult
import io.qameta.allure.Allure
import io.qameta.allure.AllureLifecycle
import io.qameta.allure.Epic
import io.qameta.allure.Feature
import io.qameta.allure.Issue
import io.qameta.allure.Link
import io.qameta.allure.Links
import io.qameta.allure.Owner
import io.qameta.allure.Severity
import io.qameta.allure.SeverityLevel
import io.qameta.allure.Story
import io.qameta.allure.model.Label
import io.qameta.allure.model.Status
import io.qameta.allure.model.StatusDetails
import io.qameta.allure.model.StepResult
import io.qameta.allure.util.ResultsUtils
import java.lang.reflect.InvocationTargetException
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
class AllureWriter {
companion object {
const val LanguageLabel = "kotlin"
const val FrameworkLabel = "kotest"
}
/**
* Loads the [AllureLifecycle] object which is used to report test lifecycle events.
*/
val allure = try {
Allure.getLifecycle() ?: throw IllegalStateException("Allure lifecycle was null")
} catch (t: Throwable) {
t.printStackTrace()
throw t
}
private val uuids = ConcurrentHashMap()
fun id(testCase: TestCase) = uuids[testCase.descriptor.path()]
fun startTestCase(testCase: TestCase) {
val labels = listOfNotNull(
testCase.epic(),
testCase.feature(),
ResultsUtils.createFrameworkLabel(FrameworkLabel),
ResultsUtils.createHostLabel(),
ResultsUtils.createLanguageLabel(LanguageLabel),
testCase.owner(),
ResultsUtils.createPackageLabel(testCase.spec::class.java.`package`.name),
ResultsUtils.createSuiteLabel(testCase.descriptor.spec().id.value),
testCase.maxSeverity()?.let { ResultsUtils.createSeverityLabel(it) },
testCase.story(),
ResultsUtils.createThreadLabel()
)
val links = mutableListOf()
testCase.issue()?.let {
links.add(testCase.issue())
}
testCase.link()?.let {
links.add(testCase.link())
}
testCase.links()?.forEach {
links.add(ResultsUtils.createLink(it))
}
val uuid = UUID.randomUUID().toString()
uuids[testCase.descriptor.path()] = uuid
val testName = testCase.constructName()
val result = io.qameta.allure.model.TestResult()
.setFullName(testName)
.setName(testName)
.setUuid(uuid)
.setTestCaseId(safeId(testCase.descriptor))
.setHistoryId(safeId(testCase.descriptor))
.setLabels(labels)
.setLinks(links)
.setDescription(testCase.description())
allure.scheduleTestCase(result)
allure.startTestCase(uuid)
}
fun finishTestCase(testCase: TestCase, result: TestResult) {
val status = when (result) {
// what we call an error, allure calls broken
is TestResult.Error -> Status.BROKEN
is TestResult.Failure -> Status.FAILED
is TestResult.Ignored -> Status.SKIPPED
is TestResult.Success -> Status.PASSED
}
val uuid = uuids[testCase.descriptor.path()]
val details = ResultsUtils.getStatusDetails(result.errorOrNull)
allure.updateTestCase(uuid) {
it.status = status
it.statusDetails = details.orElseGet { null }
testCase.descriptor.parents().forEach { d ->
it.steps.add(
StepResult()
.setName(d.id.value)
.setStatus(Status.PASSED)
.setStart(0L)
.setStop(0L)
)
}
}
allure.stopTestCase(uuid)
allure.writeTestCase(uuid)
}
private fun links(kclass: KClass<*>): List {
val links = mutableListOf()
kclass.issue()?.let {
links.add(kclass.issue())
}
kclass.link()?.let {
links.add(kclass.link())
}
kclass.links()?.forEach {
links.add(ResultsUtils.createLink(it))
}
return links.toList()
}
fun allureResultSpecInitFailure(kclass: KClass<*>, t: Throwable) {
val uuid = UUID.randomUUID()
val labels = listOfNotNull(
ResultsUtils.createSuiteLabel(kclass.qualifiedName),
ResultsUtils.createThreadLabel(),
ResultsUtils.createHostLabel(),
ResultsUtils.createLanguageLabel("kotlin"),
ResultsUtils.createFrameworkLabel("kotest"),
ResultsUtils.createPackageLabel(kclass.java.`package`.name),
kclass.severity(),
kclass.owner(),
kclass.epic(),
kclass.feature(),
kclass.story()
)
val links = links(kclass)
val result = io.qameta.allure.model.TestResult()
.setFullName(kclass.qualifiedName)
.setName(kclass.simpleName)
.setUuid(uuid.toString())
.setLabels(labels)
.setLinks(links)
allure.scheduleTestCase(result)
allure.startTestCase(uuid.toString())
val instanceError = (t.cause as InvocationTargetException).targetException
val details = StatusDetails()
details.message = instanceError?.message ?: "Unknown error"
var trace = ""
instanceError.stackTrace?.forEach {
trace += "$it\n"
}
details.trace = trace
allure.updateTestCase(uuid.toString()) {
it.status = Status.FAILED
it.statusDetails = details
}
allure.stopTestCase(uuid.toString())
allure.writeTestCase(uuid.toString())
}
// returns an id that's acceptable in format for allure
private fun safeId(descriptor: Descriptor.TestDescriptor): String = descriptor.path(true).value
}
fun TestCase.epic(): Label? = this.spec::class.findAnnotation()?.let { ResultsUtils.createEpicLabel(it.value) }
fun TestCase.feature(): Label? =
this.spec::class.findAnnotation()?.let { ResultsUtils.createFeatureLabel(it.value) }
fun TestCase.maxSeverity(): SeverityLevel? {
val classSeverity = this.spec::class.findAnnotation()?.value?.toTestCaseSeverity()
val max = if (classSeverity != null) {
maxOf(classSeverity, config.severity, compareBy { it.level })
} else {
config.severity
}
return max.toAllureSeverity()
}
fun TestCase.story(): Label? = this.spec::class.findAnnotation()?.let { ResultsUtils.createStoryLabel(it.value) }
fun TestCase.owner(): Label? = this.spec::class.findAnnotation()?.let { ResultsUtils.createOwnerLabel(it.value) }
fun TestCase.issue() = spec::class.findAnnotation()?.let { ResultsUtils.createIssueLink(it.value) }
fun TestCase.description() = spec::class.findAnnotation()?.value
fun TestCase.link() = spec::class.findAnnotation()?.let { ResultsUtils.createLink(it) }
fun TestCase.links() = spec::class.findAnnotation()?.value
fun TestCase.constructName(): String {
val displayName = descriptor.id.value
val appendPrefix = name.prefix?.let { !displayName.startsWith(it) } ?: false
val result = sequence {
yield(parent?.constructName())
if (appendPrefix) {
yield(name.prefix)
}
yield(displayName)
}.filterNotNull().joinToString(separator = " ") { it.trim() }
return result
}
fun TestCaseSeverityLevel.toAllureSeverity(): SeverityLevel? = when (this) {
TestCaseSeverityLevel.BLOCKER -> SeverityLevel.BLOCKER
TestCaseSeverityLevel.CRITICAL -> SeverityLevel.CRITICAL
TestCaseSeverityLevel.NORMAL -> SeverityLevel.NORMAL
TestCaseSeverityLevel.MINOR -> SeverityLevel.MINOR
TestCaseSeverityLevel.TRIVIAL -> SeverityLevel.TRIVIAL
else -> null
}
fun SeverityLevel.toTestCaseSeverity(): TestCaseSeverityLevel? = when (this) {
SeverityLevel.BLOCKER -> TestCaseSeverityLevel.BLOCKER
SeverityLevel.CRITICAL -> TestCaseSeverityLevel.CRITICAL
SeverityLevel.NORMAL -> TestCaseSeverityLevel.NORMAL
SeverityLevel.MINOR -> TestCaseSeverityLevel.MINOR
SeverityLevel.TRIVIAL -> TestCaseSeverityLevel.TRIVIAL
else -> null
}
fun KClass<*>.epic(): Label? = this.findAnnotation()?.let { ResultsUtils.createEpicLabel(it.value) }
fun KClass<*>.feature(): Label? =
this.findAnnotation()?.let { ResultsUtils.createFeatureLabel(it.value) }
fun KClass<*>.severity(): Label? =
this.findAnnotation()?.let { ResultsUtils.createSeverityLabel(it.value) }
fun KClass<*>.story(): Label? = this.findAnnotation()?.let { ResultsUtils.createStoryLabel(it.value) }
fun KClass<*>.owner(): Label? = this.findAnnotation()?.let { ResultsUtils.createOwnerLabel(it.value) }
fun KClass<*>.issue() = this.findAnnotation()?.let { ResultsUtils.createIssueLink(it.value) }
fun KClass<*>.link() = this.findAnnotation()?.let { ResultsUtils.createLink(it) }
fun KClass<*>.links() = this.findAnnotation()?.value
fun KClass<*>.description() = this.findAnnotation()?.value
© 2015 - 2025 Weber Informatics LLC | Privacy Policy