From cf28d8ddd26eabc384883ae3d5372182e9073723 Mon Sep 17 00:00:00 2001 From: David Saff Date: Tue, 3 May 2022 15:13:26 -0400 Subject: [PATCH] Much cleaner output, and default kotlin indentation. --- .gitignore | 2 + build.gradle.kts | 144 +++++++++--------- src/main/java/net/saff/checkmark/Checkmark.kt | 144 ++++++++++++------ .../test/net/saff/checkmark/CheckmarkTest.kt | 28 ++++ 4 files changed, 198 insertions(+), 120 deletions(-) create mode 100644 src/test/java/test/net/saff/checkmark/CheckmarkTest.kt diff --git a/.gitignore b/.gitignore index a1fc39c..57f48b6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ gradle-app.setting # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # gradle/wrapper/gradle-wrapper.properties +local.properties +.idea diff --git a/build.gradle.kts b/build.gradle.kts index 3104d67..9ff8290 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,111 +16,113 @@ limitations under the License. import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { - repositories { - google() - mavenCentral() - } + repositories { + google() + mavenCentral() + } } plugins { - id("java") - id("java-library") - id("org.jetbrains.kotlin.jvm") version "1.6.20" - id("signing") - id("maven-publish") + id("java") + id("java-library") + id("org.jetbrains.kotlin.jvm") version "1.6.20" + id("signing") + id("maven-publish") } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } kotlin {} dependencies { - implementation(kotlin("stdlib-jdk8")) + implementation(kotlin("stdlib-jdk8")) + testImplementation("junit:junit:4.13.1") } repositories { - mavenCentral() + mavenCentral() } val sourcesJar by tasks.creating(Jar::class) { - archiveClassifier.set("sources") - from(sourceSets.main.get().allSource) + archiveClassifier.set("sources") + from(sourceSets.main.get().allSource) } val javadocJar by tasks.creating(Jar::class) { - dependsOn.add(tasks["javadoc"]) - archiveClassifier.set("javadoc") - from(tasks["javadoc"]) + dependsOn.add(tasks["javadoc"]) + archiveClassifier.set("javadoc") + from(tasks["javadoc"]) } val compileKotlin: KotlinCompile by tasks compileKotlin.kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "1.8" } val compileTestKotlin: KotlinCompile by tasks compileTestKotlin.kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "1.8" } publishing { - publications { - create("maven") { - groupId = "net.saff.checkmark" - artifactId = "checkmark" - version = "0.1.1" - - from(components["java"]) - - - artifact(sourcesJar) - artifact(javadocJar) - - pom { - name.set("checkmark") - description.set("Minimum viable kotlin assertion framework") - url.set("https://github.com/dsaff/checkmark") - - licenses { - license { - name.set("Apache License, Version 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") - } - } - - developers { - developer { - id.set("dsaff") - name.set("David Saff") - email.set("david@saff.net") - } - } - - scm { - connection.set("https://github.com/dsaff/checkmark.git") - developerConnection.set("https://github.com/dsaff/checkmark.git") - url.set("https://github.com/dsaff/checkmark") - } - } + publications { + create("maven") { + groupId = "net.saff.checkmark" + artifactId = "checkmark" + version = "0.1.2" + + from(components["java"]) + + + artifact(sourcesJar) + artifact(javadocJar) + + pom { + name.set("checkmark") + description.set("Minimum viable kotlin assertion framework") + url.set("https://github.com/dsaff/checkmark") + + licenses { + license { + name.set("Apache License, Version 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + } } - } - - repositories { - maven { - // For snapshots, url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots") - url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2") - credentials { - // Hopefully these are grabbed from gradle.properties a la - // https://community.sonatype.com/t/how-to-hide-the-nexus-username-and-password-when-publish-artifacts-to-nexus/4439/2 - username = nexusUsername - password = nexusPassword - } + + developers { + developer { + id.set("dsaff") + name.set("David Saff") + email.set("david@saff.net") + } } + + scm { + connection.set("https://github.com/dsaff/checkmark.git") + developerConnection.set("https://github.com/dsaff/checkmark.git") + url.set("https://github.com/dsaff/checkmark") + } + } + } + } + + val nexusUsername: String by project + val nexusPassword: String by project + + repositories { + maven { + // For snapshots, url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots") + url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2") + credentials { + username = nexusUsername + password = nexusPassword + } } + } } signing { - sign(publishing.publications["maven"]) + sign(publishing.publications["maven"]) } \ No newline at end of file diff --git a/src/main/java/net/saff/checkmark/Checkmark.kt b/src/main/java/net/saff/checkmark/Checkmark.kt index 6632e97..1cb2350 100644 --- a/src/main/java/net/saff/checkmark/Checkmark.kt +++ b/src/main/java/net/saff/checkmark/Checkmark.kt @@ -16,64 +16,110 @@ limitations under the License. package net.saff.checkmark class Checkmark { - class Failure(s: String, e: Throwable? = null) : java.lang.RuntimeException(s, e) + class Failure(s: String, e: Throwable? = null) : java.lang.RuntimeException(s, e) - private val marks = mutableListOf<() -> String>() + private val marks = mutableListOf<() -> String>() - fun mark(note: String): String { - marks.add { note } - return note + fun mark(note: T): T { + marks.add { note.toString() } + return note + } + + fun mark(fn: () -> String) { + marks.add(fn) + } + + private fun marks() = marks.joinToString { "\n mark: ${it()}" } + + companion object { + fun fail(s: String, e: Throwable? = null): Nothing = throw Failure(s, e) + + fun T.check(eval: Checkmark.(T) -> Boolean): T { + val cm = Checkmark() + val result = try { + cm.eval(this) + } catch (t: Throwable) { + fail("ERROR: ${allDebugOutput(this, cm, eval)}", t) + } + if (!result) { + fail("Failed assertion: ${allDebugOutput(this, cm, eval)}") + } + return this } - fun mark(fn: () -> String) { - marks.add(fn) + fun checks(fn: () -> Unit) { + try { + fn() + } catch (e: Throwable) { + val data = + extractClosureFields(fn).joinToString("") { it.run { "\n$first: [[$second]]" } } + fail(data, e) + } } - private fun marks() = marks.joinToString { "\n mark: ${it()}" } - - companion object { - fun fail(s: String, e: Throwable? = null): Nothing = throw Failure(s, e) - - fun T.check(eval: Checkmark.(T) -> Boolean): T { - val cm = Checkmark() - val result = try { - cm.eval(this) - } catch (t: Throwable) { - fail("ERROR: ${allDebugOutput(cm)}", t) - } - if (!result) { - fail("Failed assertion: ${allDebugOutput(cm)}") - } - return this + private fun extractClosureFields(closure: Any) = buildList { + closure::class.java.declaredFields.forEach { field -> + // SAFF: do we understand why this is needed? + if (field.name != "INSTANCE") { + field.isAccessible = true + add(field.name.removePrefix("\$") to field.get(closure)) } + } + } - fun checks(fn: () -> Unit) { - try { - fn() - } catch (e: Throwable) { - val data = - extractClosureFields(fn).joinToString("") { it.run { "\n$first: [[$second]]" } } - fail(data, e) - } - } + private fun allDebugOutput( + receiver: T, + cm: Checkmark, + eval: Checkmark.(T) -> Boolean + ): String { + val reports = buildList { + // SAFF: maybe allDebugOutput shouldn't have a receiver? + add("actual" to receiver) + addAll(extractClosureFields(eval)) + addAll(cm.marks.map { "marked" to it() }) + } + return if (reports.size == 1) { + reports[0].second.toString().forCleanDisplay() + } else { + // SAFF: DUP above + reports.joinToString("") { "\n${it.makeDebugLine()}" } + } + } - private fun extractClosureFields(closure: Any) = buildList { - closure::class.java.declaredFields.forEach { field -> - field.isAccessible = true - add(field.name to field.get(closure)) - } - } + // SAFF: inline? + private fun Pair.makeDebugLine(): String { + // SAFF: DUP above? + return "- $first: ${second.toString().forCleanDisplay()}" + } - private fun T.allDebugOutput(cm: Checkmark) = "<<$this>>${cm.marks()}" + private fun String.forCleanDisplay(): String { + return if (!contains("\n")) { + this + } else { + // SAFF: DUP on " |"? + "\n |${replace("\n", "\n |")}" + } + } - fun T.checkCompletes(eval: Checkmark.(T) -> Unit): T { - val cm = Checkmark() - try { - cm.eval(this) - } catch (e: Throwable) { - throw Exception("Failed: <<$this>>${cm.marks()}", e) - } - return this - } + fun T.checkCompletes(eval: Checkmark.(T) -> Unit): T { + val cm = Checkmark() + try { + cm.eval(this) + } catch (e: Throwable) { + throw Exception("Failed: <<$this>>${cm.marks()}", e) + } + return this } -} \ No newline at end of file + } +} + +fun thrown(fn: () -> Any?): Throwable? { + try { + fn() + } catch (t: Throwable) { + return t + } + return null +} + +fun String.showWhitespace() = replace(" ", "_").replace("\n", "\\\n") \ No newline at end of file diff --git a/src/test/java/test/net/saff/checkmark/CheckmarkTest.kt b/src/test/java/test/net/saff/checkmark/CheckmarkTest.kt new file mode 100644 index 0000000..2142603 --- /dev/null +++ b/src/test/java/test/net/saff/checkmark/CheckmarkTest.kt @@ -0,0 +1,28 @@ +package test.net.saff.checkmark + +import net.saff.checkmark.Checkmark.Companion.check +import net.saff.checkmark.showWhitespace +import net.saff.checkmark.thrown +import org.junit.Test + +class CheckmarkTest { + @Test + fun checkIncludesCaptures() { + val n = "Apple sauce" + thrown { "Pear soup".check { it == n } }!!.message.check { it!!.contains("Apple") } + } + + // SAFF: are failures printed twice on purpose? + @Test + fun outputWithMark() { + // SAFF: no "this" if there's only this + val expect = """ + |Failed assertion: + |- actual: A + |- marked: B + """.trimMargin().showWhitespace() + thrown { "A".check { it == mark("B") } }!!.message!!.showWhitespace().check { + it == expect + } + } +} \ No newline at end of file