package common import kotlin.test.FrameworkAdapter import kotlin.collections.* private var sortingContext = SortingContext() private var bodyContext: TestBodyContext? = null fun call(name: String) = bodyContext!!.call(name) fun raise(name: String): Nothing { bodyContext!!.raised(name) throw Exception(name) } // Adapter should be initialized eagerly @Suppress("INVISIBLE_MEMBER") @OptIn(kotlin.ExperimentalStdlibApi::class) @EagerInitialization private val underscore = kotlin.test.setAdapter(object : FrameworkAdapter { override fun suite(name: String, ignored: Boolean, suiteFn: () -> Unit) { sortingContext.suite(name, ignored) { suiteFn() } } override fun test(name: String, ignored: Boolean, testFn: () -> Any?) { sortingContext.test(name, ignored) { returned(testFn()) } } }) interface SuiteContext { fun suite(name: String, ignored: Boolean = false, body: SuiteContext.() -> Unit) fun test(name: String, ignored: Boolean = false, body: TestBodyContext.() -> Unit = {}) } interface TestBodyContext { fun call(name: String) fun raised(msg: String) fun caught(msg: String) fun returned(msg: Any?) } private sealed class Entity(val name: String, val ignored: Boolean) private class Suite(name: String, ignored: Boolean, val body: SuiteContext.() -> Unit): Entity(name, ignored) private class Test(name: String, ignored: Boolean, val body: TestBodyContext.() -> Unit): Entity(name, ignored) private class SortingContext: SuiteContext { val structure = mutableListOf() override fun suite(name: String, ignored: Boolean, body: SuiteContext.() -> Unit) { structure += Suite(name, ignored, body) } override fun test(name: String, ignored: Boolean, body: TestBodyContext.() -> Unit) { structure += Test(name, ignored, body) } fun replayInto(context: T): T { structure.sortedBy { it.name }.forEach { when (it) { is Suite -> context.suite(it.name, it.ignored) { val oldSorter = sortingContext sortingContext = SortingContext() it.body(sortingContext) sortingContext.replayInto(this) sortingContext = oldSorter } is Test -> context.test(it.name, it.ignored) { bodyContext = this it.body(this) bodyContext = null } } } return context } } private class LoggingContext : SuiteContext, TestBodyContext{ val log: String get() = logHead + (lastRecord ?: "") private var indentation = "" override fun suite(name: String, ignored: Boolean, body: SuiteContext.() -> Unit) = indent { record("suite(\"$name\"${optionalIgnore(ignored)}) {") runSafely { this.body() } record("}") } override fun test(name: String, ignored: Boolean, body: TestBodyContext.() -> Unit) = indent { val num = record("test(\"$name\"${optionalIgnore(ignored)}) {") runSafely { this.body() } if (!writtenSince(num)) { record("test(\"$name\"${optionalIgnore(ignored)})", replaceLast = true) } else { record("}") } } override fun call(name: String) = indent { record("call(\"$name\")") } override fun raised(msg: String) = indent { record("raised(\"$msg\")") } override fun caught(msg: String) = indent { record("caught(\"$msg\")") } override fun returned(msg: Any?) = indent { if (msg is String) record("returned(\"$msg\")") } private fun runSafely(body: () -> Unit) { try { body() } catch (t: Throwable) { caught(t.message ?: "") } } private fun indent(body: () -> Unit) { val prevIndentation = indentation indentation += " " body() indentation = prevIndentation } private var logHead: String = "" private var lastRecord: String? = null private var counter = 0 private fun writtenSince(num: Int) = counter > num private fun record(s: String, replaceLast: Boolean = false): Int { if (!replaceLast && lastRecord != null) { logHead += lastRecord } lastRecord = indentation + s + "\n" return ++counter } private fun optionalIgnore(ignored: Boolean) = if (ignored) ", true" else "" } fun checkLog(wrapInEmptySuite: Boolean = true, body: SuiteContext.() -> Unit): String { val expectedContext = SortingContext() if (wrapInEmptySuite) { expectedContext.suite("") { body() } } else { expectedContext.body() } val expectedLog = expectedContext.replayInto(LoggingContext()).log val actualLog = sortingContext.replayInto(LoggingContext()).log if (actualLog != expectedLog) { return "Failed test structure check. Expected: \"${expectedLog}\"; actual: \"${actualLog}\"." } else { return "OK" } }