Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only render subject description on failure #292

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 134 additions & 13 deletions strikt-core/src/main/kotlin/strikt/api/Assertion.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package strikt.api

import filepeek.FileInfo
import filepeek.LambdaBody
import filepeek.SourceFileNotFoundException
import strikt.internal.FilePeek
import java.io.File
import java.util.Locale
import kotlin.jvm.internal.CallableReference
import kotlin.reflect.KFunction
Expand Down Expand Up @@ -233,6 +236,24 @@ interface Assertion {
description: String,
function: T.() -> R,
block: Builder<R>.() -> Unit
): Builder<T> = with({ description }, function, block)

/**
* Runs a group of assertions on the subject returned by [function].
*
* The [description] is only invoked if the test fails.
*
* @param description a lambda that produces a description of the mapped result.
* @param function a lambda whose receiver is the current assertion subject.
* @param block a closure that can perform multiple assertions that will all
* be evaluated regardless of whether preceding ones pass or fail.
* @param R the mapped subject type.
* @return this builder, to facilitate chaining.
*/
fun <R> with(
description: () -> String,
function: T.() -> R,
block: Builder<R>.() -> Unit
): Builder<T>

/**
Expand Down Expand Up @@ -262,6 +283,23 @@ interface Assertion {
fun <R> get(
description: String,
function: T.() -> R
): DescribeableBuilder<R> = get({ description }, function)

/**
* Maps the assertion subject to the result of [function].
* This is useful for chaining to property values or method call results on
* the subject.
*
* The [description] is only invoked if the test fails.
*
* @param description a lambda that produces a description of the mapped result.
* @param function a lambda whose receiver is the current assertion subject.
* @return an assertion builder whose subject is the value returned by
* [function].
*/
fun <R> get(
description: () -> String,
function: T.() -> R
): DescribeableBuilder<R>

/**
Expand Down Expand Up @@ -322,23 +360,106 @@ interface Assertion {
}
}

private fun <Receiver, Result> (Receiver.() -> Result).describe(): String =
private fun <Receiver, Result> (Receiver.() -> Result).describe(): () -> String =
when (this) {
is KProperty<*> ->
"value of property $name"
is KFunction<*> ->
"return value of $name"
is CallableReference -> "value of $propertyName"
else -> {
try {
val line = FilePeek.filePeek.getCallerFileInfo().line
LambdaBody("get", line).body.trim()
} catch (e: Exception) {
"%s"
}
is KProperty<*> -> {
{ "value of property $name" }
}

is KFunction<*> -> {
{ "return value of $name" }
}

is CallableReference -> {
{ "value of $propertyName" }
}

else -> captureGet(RuntimeException())
}

private fun captureGet(ex: Throwable): () -> String {
return {
try {
val line = FilePeek.filePeek.specialGetCallerInfo(ex).line
LambdaBody("get", line).body.trim()
} catch (e: Exception) {
"%s"
}
}
}

private fun <T> Sequence<T>.takeWhileInclusive(pred: (T) -> Boolean): Sequence<T> {
var shouldContinue = true
return takeWhile {
val result = shouldContinue
shouldContinue = pred(it)
result
}
}

private val FS = File.separator

private val ignoredPackages = listOf(
"strikt.internal",
"strikt.api",
"filepeek"
)

private val sourceRoots: List<String> = listOf("src${FS}test${FS}kotlin", "src${FS}test${FS}java")

private fun filepeek.FilePeek.specialGetCallerInfo(ex: Throwable): FileInfo {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See this PR for filepeek

christophsturm/filepeek#14

val callerStackTraceElement = ex.stackTrace.first { el ->
ignoredPackages
.none { el.className.startsWith(it) }
}
val className = callerStackTraceElement.className.substringBefore('$')
val clazz = javaClass.classLoader.loadClass(className)!!
val classFilePath = File(clazz.protectionDomain.codeSource.location.path)
.absolutePath

val buildDir = when {
classFilePath.contains("${FS}out$FS") -> "out${FS}test${FS}classes" // running inside IDEA
classFilePath.contains("build${FS}classes${FS}java") -> "build${FS}classes${FS}java${FS}test" // gradle 4.x java source
classFilePath.contains("build${FS}classes${FS}kotlin") -> "build${FS}classes${FS}kotlin${FS}test" // gradle 4.x kotlin sources
classFilePath.contains("target${FS}classes") -> "target${FS}classes" // maven
else -> "build${FS}classes${FS}test" // older gradle
}

val sourceFileCandidates = sourceRoots
.map { sourceRoot ->
val sourceFileWithoutExtension =
classFilePath.replace(buildDir, sourceRoot)
.plus(FS + className.replace(".", FS))

File(sourceFileWithoutExtension).parentFile
.resolve(callerStackTraceElement.fileName!!)
}
val sourceFile = sourceFileCandidates.singleOrNull(File::exists) ?: throw SourceFileNotFoundException(
classFilePath,
className,
sourceFileCandidates
)

val callerLine = sourceFile.bufferedReader().useLines { lines ->
var braceDelta = 0
lines.drop(callerStackTraceElement.lineNumber - 1)
.takeWhileInclusive { line ->
val openBraces = line.count { it == '{' }
val closeBraces = line.count { it == '}' }
braceDelta += openBraces - closeBraces
braceDelta != 0
}.map { it.trim() }.joinToString(separator = "")
}

return FileInfo(
callerStackTraceElement.lineNumber,
sourceFileName = sourceFile.absolutePath,
line = callerLine.trim(),
methodName = callerStackTraceElement.methodName

)
}

private val CallableReference.propertyName: String
get() = "^get(.+)$".toRegex().find(name).let { match ->
return when (match) {
Expand Down
12 changes: 6 additions & 6 deletions strikt-core/src/main/kotlin/strikt/internal/AssertionBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ internal class AssertionBuilder<T>(

override fun describedAs(description: String): Builder<T> {
if (context is DescribedNode<*>) {
context.description = description
context.description = { description }
}
return this
}

override fun describedAs(descriptor: T.() -> String): Builder<T> {
if (context is DescribedNode<*>) {
context.description = context.subject.descriptor()
context.description = { context.subject.descriptor() }
}
return this
}
Expand Down Expand Up @@ -98,14 +98,14 @@ internal class AssertionBuilder<T>(
}

override fun <R> get(
description: String,
description: () -> String,
function: (T) -> R
): DescribeableBuilder<R> =
if (context.allowChain) {
runCatching {
function(context.subject)
}
.getOrElse { ex -> throw MappingFailed(description, ex) }
.getOrElse { ex -> throw MappingFailed(description(), ex) }
.let {
AssertionBuilder(
AssertionSubject(context, it, description),
Expand All @@ -121,7 +121,7 @@ internal class AssertionBuilder<T>(
}

override fun <R> with(
description: String,
description: () -> String,
function: T.() -> R,
block: Builder<R>.() -> Unit
): Builder<T> {
Expand All @@ -135,7 +135,7 @@ internal class AssertionBuilder<T>(
strategy.evaluate(nestedContext)
}
}
.onFailure { ex -> throw MappingFailed(description, ex) }
.onFailure { ex -> throw MappingFailed(description(), ex) }
return this
}

Expand Down
8 changes: 4 additions & 4 deletions strikt-core/src/main/kotlin/strikt/internal/AssertionNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ internal interface AssertionNode<S> {
}

internal interface DescribedNode<S> : AssertionNode<S> {
var description: String
var description: () -> String
}

internal interface AssertionGroup<S> : AssertionNode<S> {
Expand All @@ -39,7 +39,7 @@ internal interface AssertionResult<S> : DescribedNode<S> {
internal class AssertionSubject<S>(
override val parent: AssertionGroup<*>?,
override val subject: S,
override var description: String = "%s"
override var description: () -> String = { "%s" }
) : AssertionGroup<S>, DescribedNode<S> {
constructor(value: S) : this(null, value)

Expand Down Expand Up @@ -138,7 +138,7 @@ internal class AssertionChainedGroup<S>(

internal abstract class AtomicAssertionNode<S>(
final override val parent: AssertionGroup<S>,
override var description: String,
override var description: () -> String,
override val expected: Any? = null
) : AssertionResult<S>, AtomicAssertion {

Expand All @@ -156,7 +156,7 @@ internal abstract class AtomicAssertionNode<S>(

internal abstract class CompoundAssertionNode<S>(
final override val parent: AssertionGroup<S>,
override var description: String,
override var description: () -> String,
override val expected: Any? = null
) : AssertionGroup<S>, AssertionResult<S>, CompoundAssertion {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal sealed class AssertionStrategy {
): AtomicAssertionNode<T> =
object : AtomicAssertionNode<T>(
context,
provideDescription(description),
{ provideDescription(description) },
expected
) {

Expand Down Expand Up @@ -62,7 +62,7 @@ internal sealed class AssertionStrategy {
): CompoundAssertionNode<T> =
object : CompoundAssertionNode<T>(
context,
provideDescription(description),
{ provideDescription(description) },
expected
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ internal open class DefaultResultWriter : ResultWriter {
if (isRoot) {
writer.append("Expect that ")
}
val description = this.description()
// if the value spans > 1 line, this is how much to indent following lines
val valueIndent =
(description.indexOf("%")).coerceAtLeast(0) + 14 + (indent * 2)
Expand All @@ -104,6 +105,7 @@ internal open class DefaultResultWriter : ResultWriter {
val failed = status as? Failed
when {
failed?.comparison != null -> {
val description = this.description()
val formattedComparison = failed.comparison.formatValues()
val failedDescription = failed.description ?: "found %s"
val descriptionIndent = description.indexOf("%")
Expand Down Expand Up @@ -143,10 +145,10 @@ internal open class DefaultResultWriter : ResultWriter {
}
failed?.description != null ->
writer
.append(description.format(formatValue(expected)))
.append(description().format(formatValue(expected)))
.append(" : ")
.append(failed.description)
else -> writer.append(description.format(formatValue(expected)))
else -> writer.append(description().format(formatValue(expected)))
}
}

Expand Down