All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jvmMain.com.bkahlert.kommons.test.junit.DynamicTestDisplayNameGenerator.kt Maven / Gradle / Ivy

package com.bkahlert.kommons.test.junit

import com.bkahlert.kommons.ansiRemoved
import com.bkahlert.kommons.debug.renderType
import com.bkahlert.kommons.quoted
import com.bkahlert.kommons.rootCause
import com.bkahlert.kommons.test.KommonsTest
import com.bkahlert.kommons.test.LambdaBody
import com.bkahlert.kommons.test.UnicodeFont
import com.bkahlert.kommons.text.LineSeparators
import com.bkahlert.kommons.text.decapitalize
import java.nio.file.Path
import java.text.MessageFormat
import java.util.regex.Pattern
import kotlin.reflect.KCallable
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KProperty
import kotlin.reflect.jvm.reflect
import kotlin.streams.asSequence

/** A generator for dynamic test names. */
public object DynamicTestDisplayNameGenerator {

    internal const val FOR: String = "ꜰᴏʀ"

    @Suppress("MemberVisibilityCanBePrivate", "SpellCheckingInspection")
    internal const val PROPERTY: String = "ᴩʀᴏᴩᴇʀᴛy"

    @Suppress("MemberVisibilityCanBePrivate", "SpellCheckingInspection")
    internal const val FUNCTION: String = "ꜰᴜɴᴄᴛɪᴏɴ"

    @Suppress("MemberVisibilityCanBePrivate", "SpellCheckingInspection")
    internal const val VALUE_OF: String = "ᴠᴀʟᴜᴇ ᴏꜰ"

    @Suppress("MemberVisibilityCanBePrivate", "SpellCheckingInspection")
    internal const val RETURN: String = "ʀᴇᴛᴜʀɴ"

    @Suppress("MemberVisibilityCanBePrivate")
    internal val CLASS: String = UnicodeFont.SansSerifItalic.format("class")

    @Suppress("MemberVisibilityCanBePrivate")
    internal val OBJECT: String = UnicodeFont.SansSerifItalic.format("object")

    @Suppress("MemberVisibilityCanBePrivate")
    internal val NULL: String = UnicodeFont.SansSerifItalic.format("null")

    /**
     * Calculates the display name for a test with the specified [subject],
     * and the optional [testNamePattern] which supports curly placeholders `{}` like [SLF4J] does.
     *
     * If no [testNamePattern] is specified a [displayNameFallback] is calculated heuristically.
     *
     * @see displayNameFallback
     */
    public fun  displayNameFor(subject: T, testNamePattern: String? = null): String {
        val (fallbackPattern: String, args: Array<*>) = displayNameFallback(subject)
        return slf4jFormat(testNamePattern ?: fallbackPattern, *args).ansiRemoved
    }

    /**
     * Attempts to calculate a display name for a test case testing the specified [subject].
     */
    private fun displayNameFallback(subject: Any?): Pair> = when (subject) {
        null -> "{}" to arrayOf(NULL)
        is KProperty<*> -> "$PROPERTY {}" to arrayOf(subject.name)
        is KFunction<*> -> "$FUNCTION {}" to arrayOf(subject.name)
        is Function<*> -> kotlin.runCatching { subject.reflect() }.getOrNull()
            ?.let { displayNameFallback(it) }
            ?: ("{}" to arrayOf(subject.renderType()))

        is KClass<*> -> "{}" to arrayOf(subject.simpleName?.let { "$CLASS $it" } ?: "$OBJECT $subject")
        is Triple<*, *, *> -> "( {}, {}, {} )" to arrayOf(
            displayNameFor(subject.first),
            displayNameFor(subject.second),
            displayNameFor(subject.third),
        )

        is Pair<*, *> -> "( {}, {} )" to arrayOf(
            displayNameFor(subject.first),
            displayNameFor(subject.second),
        )

        is Map.Entry<*, *> -> "{} → {}" to arrayOf(displayNameFor(subject.key), displayNameFor(subject.value))
        is Char -> "{} {}" to arrayOf(subject.quoted, subject.describe())
        is CharSequence -> when (subject.toString().let { it.codePointCount(0, it.length) }) {
            1 -> "{} {}" to arrayOf(subject.quoted, subject.describe())
            else -> "{}" to arrayOf(subject.quoted)
        }

        else -> "{}" to arrayOf(subject.toCompactString())
    }

    private fun Char.describe(): String = toString().describe()
    private fun CharSequence.describe(): String =
        codePoints().asSequence().map { codePoint ->
            when (val name: String? = Character.getName(codePoint)) {
                null -> "0x${Integer.toHexString(codePoint).uppercase()}"
                else -> name
            }
        }.joinToString(separator = ", ")

    /** Returns an object with its [Any.toString] returning this string in order to protect it from being quoted (again). */
    private val CharSequence.protected: Any
        get() = object {
            override fun toString(): String = [email protected]()
        }

    /**
     * Attempts to calculate a rich display name for a property
     * expressed by the specified [fn].
     */
    public fun  String.property(fn: T.() -> R): String = when (fn) {
        is KProperty<*> -> "$this $VALUE_OF $PROPERTY ${fn.name}"
        is KFunction<*> -> "$this $RETURN $VALUE_OF ${fn.name}"
        is KCallable<*> -> KommonsTest.locateCall().run { "$this $VALUE_OF ${fn.getPropertyName(methodName)}" }
        else -> "$this " + KommonsTest.locateCall().run {
            getLambdaBodyOrNull(this, methodName)?.let { " ❴ $it ❵ " } ?: fn.toCompactString()
        }
    }

    /**
     * Returns the display name for an [subject] asserting test.
     */
    public fun  StackTraceElement.assertingDisplayName(subject: T, assertions: Assertions): String =
        buildString {
            append("❕ ")
            append(displayNameFor(subject))
            append(" ")
            append([email protected](assertions))
        }

    /**
     * Returns the display name for a transforming test.
     */
    public fun  StackTraceElement.expectingDisplayName(transform: (T) -> R): String =
        this.displayName("❔", transform)

    /**
     * Returns the display name for a catching test.
     */
    public fun  StackTraceElement.catchingDisplayName(transform: (T) -> R): String =
        this.displayName("❓", transform)

    /**
     * Returns the display name for an [exceptionType] throwing test.
     */
    public fun throwingDisplayName(exceptionType: KClass): String =
        buildString {
            append("❗")
            append(" ")
            append(exceptionType.simpleName)
        }

    /**
     * Returns the display name for a test applying [transform].
     */
    private fun  StackTraceElement.displayName(symbol: String, transform: (T) -> R): String =
        buildString {
            append(symbol)
            append(" ")
            append([email protected](transform))
            getLambdaBodyOrNull(this@displayName, "that", "it")?.also {
                append(" ")
                append(it)
            }
        }

    /**
     * Returns the display name for a test involving the subject returned by [provideSubject].
     */
    public fun  StackTraceElement.expectingDisplayName(provideSubject: () -> R): String =
        displayName("❔", provideSubject)

    /**
     * Returns the display name for a test involving an eventually thrown exception
     * by [provideSubject].
     */
    public fun  StackTraceElement.catchingDisplayName(provideSubject: () -> R): String =
        displayName("❓", provideSubject)

    /**
     * Returns the display name for a test involving the subject returned by [provide].
     */
    private fun  StackTraceElement.displayName(symbol: String, provide: () -> R): String =
        buildString {
            append(symbol)
            append(" ")
            append(displayNameFor([email protected](provide, null).protected))
            getLambdaBodyOrNull(this@displayName, "that", "it")?.also {
                append(" ")
                append(it)
            }
        }

    /**
     * Attempts to calculate a rich display name for a property
     * expressed by the specified [fn].
     */
    private fun  StackTraceElement.displayName(fn: T.() -> R, fnName: String? = null): String {
        return when (fn) {
            is KProperty<*> -> fn.name
            is KFunction<*> -> fn.name
            is KCallable<*> -> run { fn.getPropertyName(methodName) }
            else -> fnName?.let { getLambdaBodyOrNull(this, it) } ?: getLambdaBodyOrNull(this) ?: fn.toCompactString()
        }
    }

    /**
     * Attempts to calculate a rich display name for a property
     * expressed by the specified [fn].
     */
    private fun  StackTraceElement.displayName(fn: () -> R, fnName: String? = null): String {
        return when (fn) {
            is KProperty<*> -> fn.name
            is KFunction<*> -> fn.name
            is KCallable<*> -> run { fn.getPropertyName(methodName) }
            else -> fnName?.let { getLambdaBodyOrNull(this, it) } ?: getLambdaBodyOrNull(this) ?: fn.toCompactString()
        }
    }

    private fun KCallable<*>.getPropertyName(callerMethodName: String): String =
        "^$callerMethodName(?.+)$".toRegex().find(name)?.destructured?.let { (arg) -> arg.decapitalize() } ?: name

    private fun getLambdaBodyOrNull(
        stackTraceElement: StackTraceElement,
        vararg methodNameHints: String,
    ) = LambdaBody.parseOrNull(stackTraceElement, *methodNameHints)?.removePrefix("it.")?.toString()
}


internal fun Any?.toCompactString(): String {
    return when (this) {
        is Path -> toUri().toString()
        is ByteArray -> "0x" + joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
        is Array<*> -> toList().toCompactString()
        is Iterable<*> -> joinToString(prefix = "[", postfix = "]") { it.toCompactString() }
        is Process -> also { waitFor() }.exitValue().toString()
        is CharSequence -> split(*LineSeparators.Unicode).joinToString(separator = "⏎").removeSuffix("⏎").quoted
        else -> when (this) {
            null -> "null"
            Unit -> ""
            else -> {
                val string = toCustomStringOrNull() ?: renderType(simplified = true)
                string.split(*LineSeparators.Unicode).joinToString(separator = "⏎").removeSuffix("⏎")
            }
        }
    }
}

private fun Any.toDefaultString() =
    javaClass.name + "@" + Integer.toHexString(hashCode())

private fun Any?.toCustomStringOrNull(): String? =
    if (this == null) null else toString().takeUnless { it == toDefaultString() }


internal fun Throwable?.toCompactString(): String {
    if (this == null) return ""
    val messagePart = message?.let { ": " + it.lines()[0] } ?: ""
    return rootCause.run {
        this::class.simpleName + messagePart + stackTrace.firstOrNull()
            ?.let { element -> " at.(${element.fileName}:${element.lineNumber})" }
    }
}

internal fun Result<*>?.toCompactString(): String {
    if (this == null) return ""
    return if (isSuccess) getOrNull().toCompactString()
    else exceptionOrNull().toCompactString()
}


private const val SLF4J_ANCHOR = "{}"
private val SLF4J_PATTERN = Pattern.compile(Pattern.quote(SLF4J_ANCHOR))
private const val MESSAGE_FORMAT_REPLACEMENT = "{%d}"

/**
 * Formats the specified [message] by replacing `{}` placeholders with the
 * specified [args].
 */
private fun slf4jFormat(message: String, vararg args: Any?): String {
    var messageFormatPattern = message
    var index = 0

    var matcher = SLF4J_PATTERN.matcher(messageFormatPattern)
    while (matcher.find()) {
        messageFormatPattern = matcher.replaceFirst(String.format(MESSAGE_FORMAT_REPLACEMENT, index))
        matcher = SLF4J_PATTERN.matcher(messageFormatPattern)
        index++
    }
    val messageFormat = MessageFormat(messageFormatPattern)
    return messageFormat.format(args, StringBuffer(message.length shl 1), null).toString()
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy