├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── src
├── androidMain
│ └── AndroidManifest.xml
├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── varabyte
│ │ └── truthish
│ │ ├── subjects
│ │ ├── BooleanSubject.kt
│ │ ├── ComparableSubject.kt
│ │ ├── NumberSubjects.kt
│ │ ├── AnySubject.kt
│ │ ├── MapSubject.kt
│ │ ├── StringSubject.kt
│ │ ├── IterableSubject.kt
│ │ └── ArraySubjects.kt
│ │ ├── failure
│ │ ├── ValueStringifier.kt
│ │ ├── Summaries.kt
│ │ ├── Reportable.kt
│ │ ├── FailureStrategy.kt
│ │ └── Report.kt
│ │ └── Truth.kt
└── commonTest
│ └── kotlin
│ └── com
│ └── varabyte
│ └── truthish
│ ├── BooleanAsserts.kt
│ ├── failure
│ └── FailureExtensions.kt
│ ├── AmbiguityTest.kt
│ ├── MapAsserts.kt
│ ├── MessageAsserts.kt
│ ├── StringAsserts.kt
│ ├── NumberAsserts.kt
│ ├── BasicAsserts.kt
│ ├── AssertAllTest.kt
│ ├── IterableAsserts.kt
│ └── ArrayAsserts.kt
├── settings.gradle.kts
├── .gitignore
├── gradle.properties
├── .github
└── workflows
│ ├── gradle-test-all.yml
│ ├── coverage-badge.yml
│ └── publish.yml
├── kotlin-js-store
└── .gitignore
├── CONTRIBUTING.md
├── gradlew.bat
├── README.md
├── gradlew
└── LICENSE
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/varabyte/truthish/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | }
6 | }
7 |
8 | rootProject.name = "truthish"
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Gradle
2 |
3 | # Ignore Gradle project-specific cache directory
4 | .gradle
5 |
6 | # Ignore Gradle build output directory
7 | build
8 |
9 | ## Kotlin
10 | .kotlin
11 |
12 | ## IntelliJ
13 |
14 | # Gradle is the source of truth for this project; ignore IDEA settings
15 | .idea
16 | *.iml
17 |
18 | ## Mac
19 |
20 | # Mac noise...
21 | .DS_store
22 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 | kotlin.native.ignoreDisabledTargets=true
3 | # Native builds keep failing; bump up Gradle memory sizes
4 | org.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=768m
5 | kotlin.mpp.stability.nowarn=true
6 | # The Android Gradle plugin asked me to do this
7 | kotlin.mpp.androidSourceSetLayoutVersion=2
8 |
9 | kotlin.native.cacheKind.macosArm64=none
10 | kotlin.native.cacheKind.iosSimulatorArm64=none
11 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/subjects/BooleanSubject.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.subjects
2 |
3 | import com.varabyte.truthish.failure.Report
4 | import com.varabyte.truthish.failure.Summaries
5 |
6 | class BooleanSubject(private val actual: Boolean) : ComparableSubject(actual) {
7 | fun isTrue() {
8 | if (!actual) {
9 | report(Report(Summaries.EXPECTED_TRUE))
10 | }
11 | }
12 |
13 | fun isFalse() {
14 | if (actual) {
15 | report(Report(Summaries.EXPECTED_FALSE))
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/.github/workflows/gradle-test-all.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 | # Note: name shows up in a badge. Be careful about renaming.
3 |
4 | on:
5 | workflow_dispatch:
6 | push:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ${{ matrix.os }}
12 |
13 | strategy:
14 | matrix:
15 | os: [ubuntu-latest, macos-latest]
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | # Java 17 needed for resolving the Android Gradle Plugin
21 | - uses: actions/setup-java@v4
22 | with:
23 | distribution: temurin
24 | java-version: 17
25 |
26 | - name: Setup Gradle
27 | uses: gradle/actions/setup-gradle@v3
28 |
29 | - name: Cache Kotlin Native compiler
30 | uses: actions/cache@v4
31 | with:
32 | path: ~/.konan
33 | key: kotlin-native-compiler-${{ runner.OS }}
34 |
35 | - name: Test with Gradle
36 | run: ./gradlew allTests
37 |
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com/varabyte/truthish/BooleanAsserts.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish
2 |
3 | import com.varabyte.truthish.failure.ReportError
4 | import com.varabyte.truthish.failure.Summaries
5 | import com.varabyte.truthish.failure.assertSubstrings
6 | import com.varabyte.truthish.failure.withStrategy
7 | import kotlin.test.Test
8 |
9 | class BooleanAsserts {
10 | @Test
11 | fun boolChecks() {
12 | run {
13 | // Test true statements
14 | assertThat(true).isEqualTo(true)
15 | assertThat(true).isNotEqualTo(false)
16 |
17 | assertThat(true).isTrue()
18 | assertThat(false).isFalse()
19 | }
20 |
21 | run {
22 | // Test false statements
23 | assertThrows {
24 | assertThat(true).isFalse()
25 | }.assertSubstrings(Summaries.EXPECTED_FALSE)
26 |
27 | assertThrows {
28 | assertThat(false).isTrue()
29 | }.assertSubstrings(Summaries.EXPECTED_TRUE)
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com/varabyte/truthish/failure/FailureExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.failure
2 |
3 | /**
4 | * Verify that all of the passed in substrings are present in the final report.
5 | *
6 | * It can be difficult to test the exact output of a failure report, and it would be fragile to check the content of the
7 | * whole string, since text can change over time. However, checking for the presence of certain substrings can be a
8 | * reasonable compromise.
9 | */
10 | fun Report.assertSubstrings(vararg substrings: String) {
11 | val reportStr = this.toString()
12 | for (substring in substrings) {
13 | if (!reportStr.contains(substring)) {
14 | throw AssertionError(
15 | """
16 | The failure did not contain "$substring"
17 |
18 | Original report:
19 |
20 | $reportStr
21 | """
22 | )
23 | }
24 | }
25 | }
26 |
27 | /**
28 | * @see [Report.assertSubstrings]
29 | */
30 | internal fun ReportError.assertSubstrings(vararg substrings: String) {
31 | this.report.assertSubstrings(*substrings)
32 | }
33 |
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com/varabyte/truthish/AmbiguityTest.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish
2 |
3 | import com.varabyte.truthish.failure.ReportError
4 | import com.varabyte.truthish.failure.assertSubstrings
5 | import com.varabyte.truthish.failure.withStrategy
6 | import kotlin.test.Test
7 |
8 | class AmbiguityTest {
9 | class ClassA(private val value: String) {
10 | override fun toString() = value
11 | }
12 |
13 | class ClassB(private val value: String) {
14 | override fun toString() = value
15 | }
16 |
17 | @Test
18 | fun extraInformationShownIfToStringIsAmbiguous() {
19 | assertThrows {
20 | assertThat(ClassA("x")).isEqualTo(ClassB("x"))
21 | }.assertSubstrings("x (Type: `ClassB`)", "x (Type: `ClassA`)")
22 |
23 | assertThrows {
24 | assertThat(ClassA("x")).isEqualTo("x")
25 | }.assertSubstrings("\"x\" (Type: `String`)", "x (Type: `ClassA`)")
26 |
27 | assertThrows {
28 | assertThat("x").isEqualTo(ClassB("x"))
29 | }.assertSubstrings("x (Type: `ClassB`)", "\"x\" (Type: `String`)")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/subjects/ComparableSubject.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.subjects
2 |
3 | import com.varabyte.truthish.failure.DetailsFor
4 | import com.varabyte.truthish.failure.Report
5 | import com.varabyte.truthish.failure.Summaries
6 |
7 | /**
8 | * A subject that supports comparing one value to another
9 | */
10 | open class ComparableSubject>(private val actual: T): NotNullSubject(actual) {
11 | fun isGreaterThan(target: T) {
12 | if (actual <= target) {
13 | report(Report(Summaries.EXPECTED_COMPARISON, DetailsFor.expectedActual("greater than", target, actual)))
14 | }
15 | }
16 |
17 | fun isGreaterThanEqual(target: T) {
18 | if (actual < target) {
19 | report(Report(Summaries.EXPECTED_COMPARISON, DetailsFor.expectedActual("greater than or equal to", target, actual)))
20 | }
21 | }
22 |
23 | fun isLessThanEqual(target: T) {
24 | if (actual > target) {
25 | report(Report(Summaries.EXPECTED_COMPARISON, DetailsFor.expectedActual("less than or equal to", target, actual)))
26 | }
27 | }
28 |
29 | fun isLessThan(target: T) {
30 | if (actual >= target) {
31 | report(Report(Summaries.EXPECTED_COMPARISON, DetailsFor.expectedActual("less than", target, actual)))
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/.github/workflows/coverage-badge.yml:
--------------------------------------------------------------------------------
1 | # coverage-badge.yml
2 |
3 | name: Create coverage badge
4 |
5 | on:
6 | push:
7 | branches: [ main ]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | # Java 17 needed for resolving the Android Gradle Plugin
18 | - uses: actions/setup-java@v4
19 | with:
20 | distribution: temurin
21 | java-version: 17
22 |
23 | - name: Setup Gradle
24 | uses: gradle/actions/setup-gradle@v3
25 |
26 | - name: Cache Kotlin Native compiler
27 | uses: actions/cache@v4
28 | with:
29 | path: ~/.konan
30 | key: kotlin-native-compiler-${{ runner.OS }}
31 |
32 | - name: Generate coverage output
33 | # NOTE: the Android plugin is chatty with its output and spams stuff like "Install Android SDK ..."
34 | # This will mess up the next command (gradlew -q printLineCoverage) so we filter out anything that doesn't match
35 | # a very simple numeric value.
36 | run: |
37 | COVERAGE=$(${{github.workspace}}/gradlew -q printLineCoverage | grep -E '^[0-9]+(\.[0-9]+)?$')
38 | echo "COVERAGE=$COVERAGE" >> $GITHUB_ENV
39 |
40 | - name: Update dynamic badge gist
41 | uses: schneegans/dynamic-badges-action@v1.7.0
42 | with:
43 | auth: ${{secrets.COVERAGE_GIST_SECRET}}
44 | gistID: 01b6bfe88483946d9f5438f5616d9b9f
45 | filename: truthish-coverage-badge.json
46 | label: coverage
47 | message: ${{env.COVERAGE}}%
48 | valColorRange: ${{env.COVERAGE}}
49 | minColorRange: 0
50 | maxColorRange: 100
51 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/failure/ValueStringifier.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.failure
2 |
3 | internal fun stringifierFor(value: Any?): ValueStringifier {
4 | return when (value) {
5 | is ValueStringifier -> value // In case caller already wrapped `value` in a stringifier themselves
6 | is Char -> CharStringifier(value)
7 | is CharSequence -> StringStringifier(value)
8 | is Map<*, *> -> MapStringifier(value)
9 | is Set<*> -> SetStringifier(value)
10 | is Iterable<*> -> IterableStringifier(value)
11 | else -> AnyStringifier(value)
12 | }
13 | }
14 |
15 | /**
16 | * A base class which forces subclasses to override their [toString] method.
17 | */
18 | sealed class ValueStringifier {
19 | abstract override fun toString(): String
20 | }
21 |
22 | /**
23 | * A default Stringifier that can handle any case.
24 | */
25 | class AnyStringifier(private val value: Any?) : ValueStringifier() {
26 | override fun toString() = value?.let { "$it" } ?: "(null)"
27 | }
28 |
29 | class CharStringifier(private val value: Char) : ValueStringifier() {
30 | override fun toString() = "'$value'"
31 | }
32 |
33 | class StringStringifier(private val value: CharSequence) : ValueStringifier() {
34 | override fun toString() = "\"$value\""
35 | }
36 |
37 | class IterableStringifier(private val value: Iterable<*>) : ValueStringifier() {
38 | override fun toString() = "[ ${value.joinToString(", ") { stringifierFor(it).toString() }} ]"
39 | }
40 |
41 | class MapStringifier(private val value: Map<*, *>) : ValueStringifier() {
42 | override fun toString() = "{ ${value.entries.joinToString(", ") { stringifierFor(it).toString() }} }"
43 | }
44 |
45 | // This should take precedence over IterableStringifier!
46 | class SetStringifier(private val value: Set<*>) : ValueStringifier() {
47 | override fun toString() = "{ ${value.joinToString(", ") { stringifierFor(it).toString() }} }"
48 | }
--------------------------------------------------------------------------------
/kotlin-js-store/.gitignore:
--------------------------------------------------------------------------------
1 | # Normally, it is good practice to check in your yarn.lock file.
2 | # https://classic.yarnpkg.com/blog/2016/11/24/lockfiles-for-all
3 | # to keep dependencies consistent across developers.
4 | # However, we're writing Kotlin, not JS, so this is less of a
5 | # concern for us, as the dependencies they use are tied to the
6 | # Kotlin plugin version, which *is* checked into source control.
7 | #
8 | # Furthermore, Truthish is a test assertion library, which means
9 | # it only gets run on developer machnes, not end users nested into
10 | # public sites, so I'm not terribly worried about security issues.
11 | # As long as the Kotlin/JS logic is sound, `jsBrowserTest` and
12 | # `jsNodeTest` will pass all tests on the CI.
13 | #
14 | # Meanwhile, keeping it around is annoying -- it requires updating
15 | # every time we upgrade Kotlin (which, above, I'd argue is unecessary
16 | # for our project), and this happens a fair bit because we use a
17 | # newer version of Kotlin for testing than we do publishing (to work
18 | # around test issues fixed in newer versions of the Kotlin plugin).
19 | #
20 | # Our yarn.lock file also keeps causing GitHub to complain about
21 | # dependabot issues even though I can't do anything about them since
22 | # I'm tied to an older version of the Kotlin plugin in order to
23 | # ensure that my library has maximum reach.
24 | #
25 | # So, let's just remove it and trust our passing tests to verify
26 | # that all generated Kotlin/JS code is good to go.
27 | #
28 | # NOTE: If you ran tests before and then updated the Kotlin version
29 | # since then, you will likely need to run
30 | # `./gradlew :kobwebUpgradeYarnLock`. Or just delete the yarn.lock
31 | # file manually! (I would have added logic to the build script to
32 | # disable this warning if I could have, but since I'm using an
33 | # older Kotlin plugin version I can't simply follow the instructions
34 | # at https://kotlinlang.org/docs/js-project-setup.html#reporting-that-yarn-lock-has-been-updated
35 |
36 | yarn.lock
37 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Are you interested in contributing code to Truthish? First of all, let me say -- thanks! I really mean it. I appreciate
4 | your time and that you're giving some of it to this project.
5 |
6 | As it's still very early in the life of the project, the requirements set out for now will be pretty minimal, but I
7 | expect to tighten them over time as the project stabilizes.
8 |
9 | First of all, **be aware of our code of conduct**
10 |
11 | * For now, we're borrowing almost wholesale from the
12 | [Rust Code of conduct](https://www.rust-lang.org/policies/code-of-conduct)
13 | * TL;DR - debate is welcome to generate the best ideas, but respect each other. We'll never tolerate behavior where
14 | someone is affecting the community negatively or attacking people and not ideas.
15 |
16 | Other than that, just a few rules:
17 |
18 | * **Code consistency and readability are the highest priorities right now**
19 | * Consider using IntelliJ IDEA (Community Edition is fine). This way, we can set common project settings in a file
20 | and lean on the tools to resolve disagreements, so we can focus on more interesting work.
21 | * Changes should be tested.
22 | * **Pull requests should be associated with a bug that you have assigned to yourself**
23 | * This ensures that we won't have multiple people working on the same issue at the same time.
24 | * **New features should be discussed with me first**
25 | * This ensures that you won't waste your time on something I might have reasons to reject.
26 | * Of course, pull requests can be an effective way of proposing a feature, so go for it as long as you're OK that
27 | there are no guarantees we'll take the change.
28 | * Ways to connect:
29 | * [Join my Discord!](https://discord.gg/5NZ2GKV5Cs)
30 | * Follow me on Twitter: [@bitspittle](https://twitter.com/bitspittle)
31 | * You can send direct queries to [my email](mailto:bitspittle@gmail.com).
32 |
33 | These rules are NOT dogma. I am always open to exceptions, but be ready to explain why your situation is special!
34 |
35 | Thank you, again, for choosing to help out on Truthish.
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to Maven
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | repo-gcloud:
7 | type: boolean
8 | description: "Repository: GCloud"
9 | default: true
10 | repo-mavencentral:
11 | type: boolean
12 | description: "Repository: Maven Central"
13 | default: true
14 |
15 | jobs:
16 | build:
17 | runs-on: ${{ matrix.os }}
18 |
19 | env:
20 | # See: https://vanniktech.github.io/gradle-maven-publish-plugin/central/#secrets
21 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.VARABYTE_SIGNING_KEY }}
22 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.VARABYTE_SIGNING_KEY_ID }}
23 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.VARABYTE_SIGNING_PASSWORD }}
24 |
25 | strategy:
26 | matrix:
27 | # Note: We use macos since it can build mac, linux, and windows targets. ubuntu is faster but cannot build mac
28 | # artifacts.
29 | os: [macos-latest]
30 |
31 | steps:
32 | - uses: actions/checkout@v4
33 |
34 | # Java 17 needed for resolving the Android Gradle Plugin
35 | - uses: actions/setup-java@v4
36 | with:
37 | distribution: temurin
38 | java-version: 17
39 |
40 | - name: Setup Gradle
41 | uses: gradle/actions/setup-gradle@v3
42 |
43 | - name: Add secret Gradle properties
44 | env:
45 | GRADLE_PROPERTIES: ${{ secrets.VARABYTE_GRADLE_PROPERTIES }}
46 | shell: bash
47 | run: |
48 | mkdir -p ~/.gradle/
49 | echo "GRADLE_USER_HOME=${HOME}/.gradle" >> $GITHUB_ENV
50 | echo "${GRADLE_PROPERTIES}" > ~/.gradle/gradle.properties
51 |
52 | - name: Cache Kotlin Native compiler
53 | uses: actions/cache@v4
54 | with:
55 | path: ~/.konan
56 | key: kotlin-native-compiler-${{ runner.OS }}
57 |
58 | - name: Publish GCloud
59 | if: inputs.repo-gcloud
60 | run: |
61 | ./gradlew publishAllPublicationsToGCloudMavenRepository
62 |
63 | - name: Publish Maven Central
64 | if: inputs.repo-mavencentral
65 | run: |
66 | ./gradlew publishAllPublicationsToMavenCentralRepository
67 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/failure/Summaries.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.failure
2 |
3 | /**
4 | * Re-usable summaries for all subjects
5 | */
6 | object Summaries {
7 | const val EXPECTED_NULL = "An object was not null"
8 | const val EXPECTED_NOT_NULL = "An object was null"
9 | const val EXPECTED_EQUAL = "Two objects were not equal"
10 | const val EXPECTED_NOT_EQUAL = "Two objects were equal"
11 | const val EXPECTED_SAME = "Two objects were not the same"
12 | const val EXPECTED_NOT_SAME = "Two objects were the same"
13 | const val EXPECTED_INSTANCE = "An object was not an instance of a class"
14 | const val EXPECTED_NOT_INSTANCE = "An object was an instance of a class"
15 |
16 | const val EXPECTED_TRUE = "A value was false"
17 | const val EXPECTED_FALSE = "A value was true"
18 |
19 | const val EXPECTED_EXCEPTION = "An exception was not thrown"
20 |
21 | const val EXPECTED_COMPARISON = "Two values did not compare with each other as expected"
22 |
23 | const val EXPECTED_EMPTY = "A value was not empty"
24 | const val EXPECTED_BLANK = "A value was not blank"
25 | const val EXPECTED_NOT_EMPTY = "A value was empty"
26 | const val EXPECTED_NOT_BLANK = "A value was blank"
27 |
28 | const val EXPECTED_STARTS_WITH = "A value did not start with another"
29 | const val EXPECTED_NOT_STARTS_WITH = "A value started with another"
30 | const val EXPECTED_ENDS_WITH = "A value did not end with another"
31 | const val EXPECTED_NOT_ENDS_WITH = "A value ended with another"
32 | const val EXPECTED_CONTAINS = "A value did not contain another"
33 | const val EXPECTED_NOT_CONTAINS = "A value contained another"
34 | const val EXPECTED_MATCH = "A value did not match another"
35 | const val EXPECTED_NOT_MATCH = "A value matched another"
36 |
37 | const val EXPECTED_COLLECTION_EMPTY = "A collection was not empty"
38 | const val EXPECTED_COLLECTION_NOT_EMPTY = "A collection was empty"
39 | const val EXPECTED_COLLECTION_CONTAINS = "A collection did not contain element(s)"
40 | const val EXPECTED_COLLECTION_NOT_CONTAINS = "A collection contained element(s)"
41 | const val EXPECTED_COLLECTION_NO_DUPLICATES = "A collection had duplicates"
42 | const val EXPECTED_COLLECTION_ORDERED = "A collection did not contain element(s) in an expected order"
43 | }
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/failure/Reportable.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.failure
2 |
3 | import com.varabyte.truthish.assertWithMessage
4 |
5 | /**
6 | * Base class for any object that may want to send a failure report.
7 | */
8 | abstract class Reportable {
9 | var strategy: FailureStrategy = AssertionStrategy()
10 | var message: String? = null
11 | var name: String? = null
12 |
13 | /**
14 | * Handle a [Report] using this current reportable instance's strategy and other values.
15 | */
16 | fun report(report: Report) {
17 | // Note: The following lines are in reverse order, since each add to the 0th element. In
18 | // practice, it's rare that both will even be set at the same time.
19 | name?.let { report.details.add(0, "Name" to AnyStringifier(it)) }
20 | message?.let { report.details.add(0, "Message" to AnyStringifier(it)) }
21 |
22 | strategy.handle(report)
23 | }
24 | }
25 |
26 | /**
27 | * Adds a name to a value, which can be useful for providing more context to a failure.
28 | *
29 | * For example:
30 | *
31 | * ```
32 | * val voters = personDb.queryVoters()
33 | * for (voter in voters) {
34 | * assertThat(voter.age).named("Age of ${voter.name}").isGreaterThanOrEqualTo(18)
35 | * }
36 | * ```
37 | *
38 | * This is very similar to [withMessage], but there is a subtle difference. An assertion's message should describe the
39 | * overall check itself, while the name should describe the value being checked. It can even make sense to set both
40 | * values.
41 | *
42 | * @see withMessage
43 | */
44 | inline fun R.named(name: String): R {
45 | this.name = name
46 | return this
47 | }
48 |
49 | /**
50 | * Adds a message that describes the assertion, which can be useful for providing more context to a failure.
51 | *
52 | * @see assertWithMessage
53 | * @see named
54 | */
55 | inline fun R.withMessage(message: String): R {
56 | this.message = message
57 | return this
58 | }
59 |
60 | /**
61 | * Overrides the [FailureStrategy] used by the assertion.
62 | *
63 | * Most users will never need to use this, but if you need custom [Report] handling that doesn't simply throw an
64 | * [AssertionError] on failure, this is how you can override the default behavior.
65 | */
66 | inline fun R.withStrategy(strategy: FailureStrategy): R {
67 | this.strategy = strategy
68 | return this
69 | }
70 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/subjects/NumberSubjects.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.subjects
2 |
3 | import com.varabyte.truthish.failure.DetailsFor
4 | import com.varabyte.truthish.failure.Report
5 | import com.varabyte.truthish.failure.Summaries
6 | import kotlin.math.absoluteValue
7 |
8 | class ByteSubject(actual: Byte) : ComparableSubject(actual)
9 | class ShortSubject(actual: Short) : ComparableSubject(actual)
10 | class IntSubject(actual: Int) : ComparableSubject(actual)
11 | class LongSubject(actual: Long) : ComparableSubject(actual)
12 |
13 | class FloatSubject(internal val actual: Float) : ComparableSubject(actual) {
14 | fun isWithin(epsilon: Float) = FloatEpsilonAsserter(this, epsilon)
15 | fun isNotWithin(epsilon: Float) = FloatEpsilonAsserter(this, epsilon, within = false)
16 | }
17 |
18 | class DoubleSubject(internal val actual: Double) : ComparableSubject(actual) {
19 | fun isWithin(epsilon: Double) = DoubleEpsilonAsserter(this, epsilon)
20 | fun isNotWithin(epsilon: Double) = DoubleEpsilonAsserter(this, epsilon, within = false)
21 | }
22 |
23 | class FloatEpsilonAsserter(
24 | private val parent: FloatSubject,
25 | private val epsilon: Float,
26 | private val within: Boolean = true
27 | ) {
28 | fun of(target: Float) {
29 | if (within) {
30 | if ((target - parent.actual).absoluteValue > epsilon) {
31 | parent.report(
32 | Report(
33 | Summaries.EXPECTED_COMPARISON,
34 | DetailsFor.expectedActual("within $epsilon of", parent.actual, target)
35 | )
36 | )
37 | }
38 | }
39 | else {
40 | if ((target - parent.actual).absoluteValue <= epsilon) {
41 | parent.report(
42 | Report(
43 | Summaries.EXPECTED_COMPARISON,
44 | DetailsFor.expectedActual("not within $epsilon of", parent.actual, target)
45 | )
46 | )
47 | }
48 | }
49 | }
50 | }
51 |
52 | class DoubleEpsilonAsserter(
53 | private val parent: DoubleSubject,
54 | private val epsilon: Double,
55 | private val within: Boolean = true
56 | ) {
57 | fun of(target: Double) {
58 | if (within) {
59 | if ((target - parent.actual).absoluteValue > epsilon) {
60 | parent.report(
61 | Report(
62 | Summaries.EXPECTED_COMPARISON,
63 | DetailsFor.expectedActual("within $epsilon of", parent.actual, target))
64 | )
65 | }
66 | }
67 | else {
68 | if ((target - parent.actual).absoluteValue <= epsilon) {
69 | parent.report(
70 | Report(
71 | Summaries.EXPECTED_COMPARISON,
72 | DetailsFor.expectedActual("not within $epsilon of", parent.actual, target))
73 | )
74 | }
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/subjects/AnySubject.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.subjects
2 |
3 | import com.varabyte.truthish.failure.DetailsFor
4 | import com.varabyte.truthish.failure.Report
5 | import com.varabyte.truthish.failure.Reportable
6 | import com.varabyte.truthish.failure.Summaries
7 | import kotlin.reflect.KClass
8 |
9 | /**
10 | * Base-class for a subject with an [Any] value that one would want to test against another.
11 | *
12 | * Code cannot instantiate this base-class directly. Instead, it must instantiate a
13 | * [NullableSubject] or [NotNullSubject].
14 | */
15 | abstract class AnySubject(private val actual: T?) : Reportable() {
16 | fun isEqualTo(expected: Any?) {
17 | if (actual != expected) {
18 | report(Report(Summaries.EXPECTED_EQUAL, DetailsFor.expectedActual(expected, actual)))
19 | }
20 | }
21 |
22 | fun isNotEqualTo(expected: Any?) {
23 | if (actual == expected) {
24 | report(Report(Summaries.EXPECTED_NOT_EQUAL, DetailsFor.actual(actual)))
25 | }
26 | }
27 |
28 | fun isSameAs(expected: Any?) {
29 | if (actual !== expected) {
30 | report(Report(Summaries.EXPECTED_SAME, DetailsFor.expectedActual(expected, actual)))
31 | }
32 | }
33 |
34 | fun isNotSameAs(expected: Any?) {
35 | if (actual === expected) {
36 | report(Report(Summaries.EXPECTED_NOT_SAME, DetailsFor.actual(actual)))
37 | }
38 | }
39 |
40 | fun isInstanceOf(expected: KClass<*>) {
41 | if (!expected.isInstance(actual)) {
42 | val kClass = actual?.let { it::class }
43 | report(Report(Summaries.EXPECTED_INSTANCE, DetailsFor.expectedActual(expected, kClass)))
44 |
45 | }
46 | }
47 |
48 | fun isNotInstanceOf(expected: KClass<*>) {
49 | if (expected.isInstance(actual)) {
50 | val kClass = actual!!::class
51 | report(Report(Summaries.EXPECTED_NOT_INSTANCE, DetailsFor.actual(kClass)))
52 |
53 | }
54 | }
55 |
56 | inline fun isInstanceOf() = isInstanceOf(T::class)
57 | inline fun isNotInstanceOf() = isNotInstanceOf(T::class)
58 | }
59 |
60 | /**
61 | * A subject which enforces a non-null value.
62 | *
63 | * This allows excluding assertion tests that have to do with nullity checks.
64 | */
65 | open class NotNullSubject(private val actual: T) : AnySubject(actual)
66 |
67 | /**
68 | * A subject whose value can be null or non-null.
69 | */
70 | open class NullableSubject(private val actual: T?) : AnySubject(actual) {
71 | /**
72 | * Verify that this subject's value is actually null.
73 | */
74 | fun isNull() {
75 | if (actual != null) {
76 | report(Report(Summaries.EXPECTED_NULL, DetailsFor.actual(actual)))
77 | }
78 | }
79 |
80 | /**
81 | * Verify that this subject's value is not null.
82 | *
83 | * Note that the more idiomatic way to do this is by using Kotlin's !! operator, but this
84 | * method is still provided for completion / readability.
85 | */
86 | fun isNotNull() {
87 | if (actual == null) {
88 | report(Report(Summaries.EXPECTED_NOT_NULL))
89 | }
90 | }
91 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/subjects/MapSubject.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.subjects
2 |
3 | import com.varabyte.truthish.failure.DetailsFor
4 | import com.varabyte.truthish.failure.Report
5 | import com.varabyte.truthish.failure.Summaries
6 |
7 | private fun Map.toIterable(): Iterable> = entries.map { it.toPair() }
8 |
9 | /**
10 | * A subject useful for testing the state of a [Map]'s entries.
11 | *
12 | * Note that if you want to make assertions about a map's keys or values, you can assert against
13 | * them directly:
14 | *
15 | * ```
16 | * val asciiMap: Map = ... // mapping of 'a-z' to their ascii codes
17 | *
18 | * assertThat(map).contains('e' to 101)
19 | * assertThat(map).containsKey('e')
20 | * assertThat(map).containsValue(101)
21 | * assertThat(map.keys).contains('e')
22 | * assertThat(map.values).contains(101)
23 | *
24 | * assertThat(map).containsAllIn('a' to 97, 'z' to 122)
25 | * assertThat(map.keys).containsAllIn('a', 'z')
26 | * assertThat(map.values).containsAllIn(97, 122)
27 | * ```
28 | */
29 | class MapSubject(private val actual: Map) : IterableSubject>(actual.toIterable()) {
30 | fun isEqualTo(expected: Map) {
31 | if (actual != expected) {
32 | report(Report(Summaries.EXPECTED_EQUAL, DetailsFor.expectedActual(expected, actual)))
33 | }
34 | }
35 |
36 | fun isNotEqualTo(expected: Map) {
37 | if (actual == expected) {
38 | report(Report(Summaries.EXPECTED_NOT_EQUAL, DetailsFor.expected(actual)))
39 | }
40 | }
41 |
42 | fun containsKey(key: K) {
43 | if (!actual.containsKey(key)) {
44 | report(
45 | Report(
46 | Summaries.EXPECTED_COLLECTION_CONTAINS,
47 | DetailsFor.expectedActual("to contain key", key, actual.keys)
48 | )
49 | )
50 | }
51 | }
52 |
53 | fun containsValue(value: V) {
54 | if (!actual.containsValue(value)) {
55 | report(
56 | Report(
57 | Summaries.EXPECTED_COLLECTION_CONTAINS,
58 | DetailsFor.expectedActual("to contain value", value, actual.values)
59 | )
60 | )
61 | }
62 | }
63 |
64 | fun doesNotContainKey(key: K) {
65 | if (actual.containsKey(key)) {
66 | report(
67 | Report(
68 | Summaries.EXPECTED_COLLECTION_NOT_CONTAINS,
69 | DetailsFor.expectedActual("not to contain key", key, actual.keys)
70 | )
71 | )
72 | }
73 | }
74 |
75 | fun doesNotContainValue(value: V) {
76 | if (actual.containsValue(value)) {
77 | report(
78 | Report(
79 | Summaries.EXPECTED_COLLECTION_NOT_CONTAINS,
80 | DetailsFor.expectedActual("not to contain value", value, actual.values)
81 | )
82 | )
83 | }
84 | }
85 |
86 | fun containsAnyIn(other: Map) = containsAnyIn(other.toIterable())
87 | fun containsAllIn(other: Map) = containsAllIn(other.toIterable())
88 | fun containsNoneIn(other: Map) = containsNoneIn(other.toIterable())
89 | fun containsExactly(other: Map) = containsExactly(other.toIterable())
90 | }
91 |
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com/varabyte/truthish/MapAsserts.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish
2 |
3 | import com.varabyte.truthish.failure.ReportError
4 | import com.varabyte.truthish.failure.withStrategy
5 | import kotlin.test.Test
6 |
7 | class MapAsserts {
8 | @Test
9 | fun mapChecks() {
10 | val asciiMap = ('a'..'z').associateWith { it.code }
11 |
12 | run {
13 | assertThat(asciiMap).isEqualTo(asciiMap.toMap())
14 | assertThat(asciiMap).isNotEqualTo(emptyMap())
15 |
16 | assertThat(asciiMap).containsKey('g')
17 | assertThat(asciiMap.keys).contains('g')
18 | assertThat(asciiMap).doesNotContainKey('G')
19 | assertThat(asciiMap.keys).doesNotContain('G')
20 | assertThat(asciiMap).containsValue(107)
21 | assertThat(asciiMap.values).contains(107)
22 | assertThat(asciiMap).doesNotContainValue(9999)
23 | assertThat(asciiMap.values).doesNotContain(9999)
24 | assertThat(asciiMap).contains('e' to 101)
25 | assertThat(asciiMap).doesNotContain('e' to 102)
26 |
27 | assertThat(asciiMap).containsAnyIn('a' to 97, 'a' to 98)
28 | assertThat(asciiMap).containsAllIn('a' to 97, 'z' to 122).inOrder()
29 | assertThat(asciiMap).containsNoneIn('a' to 122, 'z' to 97)
30 | assertThat(asciiMap).containsAnyIn(mapOf('a' to 97, '?' to 9999))
31 | assertThat(asciiMap).containsAllIn(mapOf('a' to 97, 'z' to 122)).inOrder()
32 | assertThat(asciiMap).containsNoneIn(mapOf('a' to 122, 'z' to 97))
33 |
34 | assertThat(asciiMap).containsExactly(asciiMap.toMap())
35 | }
36 |
37 | run {
38 | // Test false statements
39 | assertThrows {
40 | assertThat(asciiMap).isEqualTo(emptyMap())
41 | }
42 |
43 | assertThrows {
44 | assertThat(asciiMap).isNotEqualTo(asciiMap)
45 | }
46 |
47 | assertThrows {
48 | assertThat(asciiMap).doesNotContainKey('g')
49 | }
50 |
51 | assertThrows {
52 | assertThat(asciiMap.keys).doesNotContain('g')
53 | }
54 |
55 | assertThrows {
56 | assertThat(asciiMap).containsKey('G')
57 | }
58 |
59 | assertThrows {
60 | assertThat(asciiMap.keys).contains('G')
61 | }
62 |
63 | assertThrows {
64 | assertThat(asciiMap).doesNotContainValue(107)
65 | }
66 |
67 | assertThrows {
68 | assertThat(asciiMap.values).doesNotContain(107)
69 | }
70 |
71 | assertThrows {
72 | assertThat(asciiMap).containsValue(9999)
73 | }
74 |
75 | assertThrows {
76 | assertThat(asciiMap.values).contains(9999)
77 | }
78 |
79 | assertThrows {
80 | assertThat(asciiMap).doesNotContain('e' to 101)
81 | }
82 |
83 | assertThrows {
84 | assertThat(asciiMap).contains('e' to 102)
85 | }
86 |
87 | assertThrows {
88 | assertThat(asciiMap).containsAllIn('a' to 122, 'z' to 122)
89 | }
90 |
91 | assertThrows {
92 | assertThat(asciiMap).containsAllIn('z' to 122, 'a' to 97).inOrder()
93 | }
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/failure/FailureStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.failure
2 |
3 | /**
4 | * A strategy for how an assertion failure should be handled.
5 | *
6 | * The most common strategy is to throw an exception, but a custom strategy can be registered with
7 | * a [Reportable] if needed.
8 | */
9 | interface FailureStrategy {
10 | fun handle(report: Report)
11 | }
12 |
13 | internal class ReportError(val report: Report) : AssertionError(report.toString())
14 |
15 | internal class MultipleReportsError(val reports: List, val summary: String? = null) : AssertionError(reports.buildErrorMessage(summary)) {
16 | companion object {
17 | private fun List.buildErrorMessage(summary: String?): String {
18 | val self = this
19 | return buildString {
20 | append("Grouped assertions had ${self.size} failure(s)\n")
21 | if (summary != null) {
22 | appendLine("Summary: $summary")
23 | }
24 | appendLine()
25 | self.forEachIndexed { i, report ->
26 | append("Failure ${i + 1}:\n")
27 | append(report.toString())
28 | appendLine()
29 | }
30 | }
31 | }
32 | }
33 |
34 | init {
35 | check(reports.isNotEmpty())
36 | }
37 | }
38 |
39 | /**
40 | * A strategy that will cause the test to fail immediately.
41 | */
42 | class AssertionStrategy : FailureStrategy {
43 | override fun handle(report: Report) {
44 | throw ReportError(report)
45 | }
46 | }
47 |
48 | /**
49 | * A strategy that collects reports and defers them from asserting until we request it.
50 | *
51 | * This is useful for cases where multiple assertions are being made in a group, and you want to collect all the
52 | * failures at the same time, instead of dying on the first one you run into.
53 | */
54 | class DeferredStrategy(private val summary: String? = null) : FailureStrategy {
55 | private val reports = mutableListOf()
56 |
57 | // e.g. JVM: at com.varabyte.truthish.AssertAllTest.assertAllCallstacksAreCorrect(AssertAllTest.kt:133)
58 | // exclude slashes so as not to clash with js node
59 | private val jvmCallstackRegex = Regex("\\s+at ([^ /]+\\.kt:\\d+[^ ]+)")
60 |
61 | // e.g. Node: at /Users/d9n/Code/1p/truthish/src/commonTest/kotlin/com/varabyte/truthish/AssertAllTest.kt:18:25
62 | // NOTE: There are also callstack lines like "at AssertAllTest.protoOf.assertAllCollectsMultipleErrors_tljnxt_k$ (/Users/d9n/Code/1p/truthish/src/commonTest/kotlin/com/varabyte/truthish/AssertAllTest.kt:14:13)"
63 | // but I think we can skip over them and still get the user callstack line that we want, as user code will
64 | // always(?) look like this.
65 | private val jsNodeCallstackRegex = Regex("\\s+at (/[^)]+)\\)?")
66 |
67 | // e.g. at 1 test.kexe 0x104adf41f kfun:com.varabyte.truthish.AssertAllTest#assertAllCallstacksAreCorrect(){} + 1847 (/Users/d9n/Code/1p/truthish/src/commonTest/kotlin/com/varabyte/truthish/AssertAllTest.kt:133:21)
68 | private val knCallstackRegex = Regex("\\s+at.+kfun:(.+)")
69 |
70 | // e.g. at protoOf.assertAllCallstacksAreCorrect_nyk2hi(/var/folders/5x/f_r3s2p53rx2l_lffc7m9nmw0000gn/T/_karma_webpack_413015/commons.js:29616)
71 | private val jsBrowserCallstackRegex = Regex("\\.js\\?")
72 |
73 | override fun handle(report: Report) {
74 | reports.add(report)
75 |
76 | val callstackLine =
77 | Throwable()
78 | .stackTraceToString()
79 | // Reject JS browser callstacks because they're mangled
80 | .takeUnless { jsBrowserCallstackRegex.containsMatchIn(it) }
81 | ?.split("\n")
82 | ?.drop(1) // Drop "java.lang.Throwable" line
83 | ?.asSequence()
84 | ?.mapNotNull { stackTraceLine ->
85 | jvmCallstackRegex.matchEntire(stackTraceLine)
86 | ?: knCallstackRegex.matchEntire(stackTraceLine)
87 | ?: jsNodeCallstackRegex.matchEntire(stackTraceLine)
88 | }
89 | ?.map { match -> match.groupValues[1] }
90 | ?.filterNot {
91 | it.startsWith("com.varabyte.truthish.failure.")
92 | || it.startsWith("com.varabyte.truthish.subjects.")
93 | || it.startsWith("kotlin.") // Kotlin/Native
94 | }
95 | ?.firstOrNull()
96 |
97 | if (callstackLine != null) {
98 | report.details.add(DetailsFor.AT to AnyStringifier(callstackLine))
99 | }
100 | }
101 |
102 | fun handleNow() {
103 | if (reports.isNotEmpty()) {
104 | throw MultipleReportsError(reports, summary)
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/subjects/StringSubject.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.subjects
2 |
3 | import com.varabyte.truthish.failure.DetailsFor
4 | import com.varabyte.truthish.failure.Report
5 | import com.varabyte.truthish.failure.Summaries
6 |
7 | class StringSubject(private val actual: String) : ComparableSubject(actual) {
8 |
9 | fun isEmpty() {
10 | if (!actual.isEmpty()) {
11 | report(Report(Summaries.EXPECTED_EMPTY, DetailsFor.actual(actual)))
12 | }
13 | }
14 |
15 | fun isNotEmpty() {
16 | if (actual.isEmpty()) {
17 | report(Report(Summaries.EXPECTED_NOT_EMPTY))
18 | }
19 | }
20 |
21 | fun isBlank() {
22 | if (!actual.isBlank()) {
23 | report(Report(Summaries.EXPECTED_BLANK, DetailsFor.actual(actual)))
24 | }
25 | }
26 |
27 | fun isNotBlank() {
28 | if (actual.isBlank()) {
29 | report(Report(Summaries.EXPECTED_NOT_BLANK))
30 | }
31 | }
32 |
33 | fun hasLength(expectedLength: Int) {
34 | if (actual.length != expectedLength) {
35 | report(
36 | Report(
37 | Summaries.EXPECTED_COMPARISON,
38 | DetailsFor.expectedActual("length", expectedLength, actual.length)
39 | )
40 | )
41 | }
42 | }
43 |
44 | fun startsWith(expected: CharSequence) {
45 | if (!actual.startsWith(expected)) {
46 | report(
47 | Report(Summaries.EXPECTED_STARTS_WITH, DetailsFor.expectedActual("starts with", expected, actual))
48 | )
49 | }
50 | }
51 |
52 | fun doesNotStartWith(expected: CharSequence) {
53 | if (actual.startsWith(expected)) {
54 | report(
55 | Report(
56 | Summaries.EXPECTED_NOT_STARTS_WITH,
57 | DetailsFor.expectedActual("not to start with", expected, actual)
58 | )
59 | )
60 | }
61 | }
62 |
63 | fun endsWith(expected: CharSequence) {
64 | if (!actual.endsWith(expected)) {
65 | report(
66 | Report(Summaries.EXPECTED_ENDS_WITH, DetailsFor.expectedActual("ends with", expected, actual))
67 | )
68 | }
69 | }
70 |
71 | fun doesNotEndWith(expected: CharSequence) {
72 | if (actual.endsWith(expected)) {
73 | report(
74 | Report(
75 | Summaries.EXPECTED_NOT_ENDS_WITH,
76 | DetailsFor.expectedActual("not to end with", expected, actual)
77 | )
78 | )
79 | }
80 | }
81 |
82 | fun matches(regex: Regex) {
83 | if (!regex.matches(actual)) {
84 | report(
85 | Report(Summaries.EXPECTED_MATCH, DetailsFor.expectedActual("to match", regex.pattern, actual))
86 | )
87 | }
88 | }
89 |
90 | fun matches(regex: String) {
91 | matches(Regex(regex))
92 | }
93 |
94 | fun doesNotMatch(regex: Regex) {
95 | if (regex.matches(actual)) {
96 | report(
97 | Report(Summaries.EXPECTED_NOT_MATCH, DetailsFor.expectedActual("not to match", regex.pattern, actual))
98 | )
99 | }
100 | }
101 |
102 | fun doesNotMatch(regex: String) {
103 | doesNotMatch(Regex(regex))
104 | }
105 |
106 | fun contains(expected: CharSequence) {
107 | if (!actual.contains(expected)) {
108 | report(
109 | Report(Summaries.EXPECTED_CONTAINS, DetailsFor.expectedActual("to contain", expected, actual))
110 | )
111 | }
112 | }
113 |
114 | fun containsMatch(regex: Regex) {
115 | if (!actual.contains(regex)) {
116 | report(
117 | Report(
118 | Summaries.EXPECTED_CONTAINS,
119 | DetailsFor.expectedActual("to contain match", regex.pattern, actual)
120 | )
121 | )
122 | }
123 | }
124 |
125 | fun containsMatch(regex: String) {
126 | containsMatch(Regex(regex))
127 | }
128 |
129 | fun doesNotContain(expected: CharSequence) {
130 | if (actual.contains(expected)) {
131 | report(
132 | Report(Summaries.EXPECTED_NOT_CONTAINS, DetailsFor.expectedActual("to not contain", expected, actual))
133 | )
134 | }
135 | }
136 |
137 | fun doesNotContainMatch(regex: Regex) {
138 | if (actual.contains(regex)) {
139 | report(
140 | Report(
141 | Summaries.EXPECTED_NOT_CONTAINS,
142 | DetailsFor.expectedActual("to not contain match", regex.pattern, actual)
143 | )
144 | )
145 | }
146 | }
147 |
148 | fun doesNotContainMatch(regex: String) {
149 | doesNotContainMatch(Regex(regex))
150 | }
151 | }
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com/varabyte/truthish/MessageAsserts.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish
2 |
3 | import com.varabyte.truthish.failure.ReportError
4 | import com.varabyte.truthish.failure.assertSubstrings
5 | import com.varabyte.truthish.failure.withMessage
6 | import com.varabyte.truthish.subjects.*
7 | import kotlin.test.Test
8 |
9 | const val TEST_MESSAGE = "Your message here"
10 |
11 | class MessageAsserts {
12 | class Stub
13 |
14 | class IntValue(val value: Int) : Comparable {
15 | override fun compareTo(other: IntValue): Int {
16 | return value.compareTo(other.value)
17 | }
18 | }
19 |
20 | @Test
21 | fun messageChecks() {
22 | val stub: Stub? = Stub()
23 |
24 | assertThrows {
25 | assertWithMessage(TEST_MESSAGE).that(stub).isNull()
26 | }.assertSubstrings(TEST_MESSAGE)
27 |
28 | assertThrows {
29 | assertWithMessage(TEST_MESSAGE).that(stub!!).isNotInstanceOf()
30 | }.assertSubstrings(TEST_MESSAGE)
31 |
32 | assertThrows {
33 | assertWithMessage(TEST_MESSAGE).that(IntValue(3)).isGreaterThan(IntValue(4))
34 | }.assertSubstrings(TEST_MESSAGE)
35 |
36 | assertThrows {
37 | assertWithMessage(TEST_MESSAGE).that(false).isTrue()
38 | }.assertSubstrings(TEST_MESSAGE)
39 |
40 | assertThrows {
41 | assertWithMessage(TEST_MESSAGE).that(10).isGreaterThanEqual(100)
42 | }.assertSubstrings(TEST_MESSAGE)
43 |
44 | assertThrows {
45 | assertWithMessage(TEST_MESSAGE).that(10.toByte()).isGreaterThanEqual(100.toByte())
46 | }.assertSubstrings(TEST_MESSAGE)
47 |
48 | assertThrows {
49 | assertWithMessage(TEST_MESSAGE).that(10.toShort()).isGreaterThanEqual(100.toShort())
50 | }.assertSubstrings(TEST_MESSAGE)
51 |
52 | assertThrows {
53 | assertWithMessage(TEST_MESSAGE).that(10.toLong()).isGreaterThanEqual(100.toLong())
54 | }.assertSubstrings(TEST_MESSAGE)
55 |
56 | assertThrows {
57 | assertWithMessage(TEST_MESSAGE).that(10f).isGreaterThanEqual(100f)
58 | }.assertSubstrings(TEST_MESSAGE)
59 |
60 | assertThrows {
61 | assertWithMessage(TEST_MESSAGE).that(10.0).isGreaterThanEqual(100.0)
62 | }.assertSubstrings(TEST_MESSAGE)
63 |
64 | assertThrows {
65 | assertWithMessage(TEST_MESSAGE).that("XYZ").isEmpty()
66 | }.assertSubstrings(TEST_MESSAGE)
67 |
68 | assertThrows {
69 | assertWithMessage(TEST_MESSAGE).that(listOf(1, 2, 3)).containsAnyIn(listOf(4, 5, 6))
70 | }.assertSubstrings(TEST_MESSAGE)
71 |
72 | assertThrows {
73 | assertWithMessage(TEST_MESSAGE).that(mapOf(1 to 1, 2 to 4, 3 to 9)).contains(4 to 16)
74 | }.assertSubstrings(TEST_MESSAGE)
75 |
76 | assertThrows {
77 | assertWithMessage(TEST_MESSAGE).that(sequenceOf(1, 2, 3)).isEmpty()
78 | }.assertSubstrings(TEST_MESSAGE)
79 |
80 | assertThrows {
81 | assertWithMessage(TEST_MESSAGE).that(arrayOf(1, 2, 3)).isEqualTo(arrayOf("a", "b", "c"))
82 | }.assertSubstrings(TEST_MESSAGE)
83 |
84 | assertThrows {
85 | assertWithMessage(TEST_MESSAGE).that(booleanArrayOf(true, false)).isEqualTo(booleanArrayOf(false, true))
86 | }.assertSubstrings(TEST_MESSAGE)
87 |
88 | assertThrows {
89 | assertWithMessage(TEST_MESSAGE).that(byteArrayOf(1, 2, 3)).isEqualTo(byteArrayOf(1, 2, 4))
90 | }.assertSubstrings(TEST_MESSAGE)
91 |
92 | assertThrows {
93 | assertWithMessage(TEST_MESSAGE).that(charArrayOf('a', 'b', 'c')).isEqualTo(charArrayOf('a', 'b', 'd'))
94 | }.assertSubstrings(TEST_MESSAGE)
95 |
96 | assertThrows {
97 | assertWithMessage(TEST_MESSAGE).that(shortArrayOf(1, 2, 3)).isEqualTo(shortArrayOf(1, 2, 4))
98 | }.assertSubstrings(TEST_MESSAGE)
99 |
100 | assertThrows {
101 | assertWithMessage(TEST_MESSAGE).that(intArrayOf(1, 2, 3)).isEqualTo(intArrayOf(1, 2, 4))
102 | }.assertSubstrings(TEST_MESSAGE)
103 |
104 | assertThrows {
105 | assertWithMessage(TEST_MESSAGE).that(longArrayOf(1, 2, 3)).isEqualTo(longArrayOf(1, 2, 4))
106 | }.assertSubstrings(TEST_MESSAGE)
107 |
108 | assertThrows {
109 | assertWithMessage(TEST_MESSAGE).that(floatArrayOf(1f, 2f, 3f)).isEqualTo(floatArrayOf(1f, 2f, 4f))
110 | }.assertSubstrings(TEST_MESSAGE)
111 |
112 | assertThrows {
113 | assertWithMessage(TEST_MESSAGE).that(doubleArrayOf(1.0, 2.0, 3.0)).isEqualTo(doubleArrayOf(1.0, 2.0, 4.0))
114 | }.assertSubstrings(TEST_MESSAGE)
115 | }
116 | }
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com/varabyte/truthish/StringAsserts.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish
2 |
3 | import com.varabyte.truthish.failure.ReportError
4 | import com.varabyte.truthish.failure.Summaries
5 | import com.varabyte.truthish.failure.assertSubstrings
6 | import kotlin.test.Test
7 |
8 | class StringAsserts {
9 | @Test
10 | fun stringChecks() {
11 | run {
12 | // Test true statements
13 | assertThat("").isEmpty()
14 | assertThat(" ").isBlank()
15 | assertThat(" ").isNotEmpty()
16 | assertThat("Hello World").isNotEmpty()
17 | assertThat("Hello World").isNotBlank()
18 |
19 | assertThat("").hasLength(0)
20 | assertThat(" ").hasLength(2)
21 | assertThat("Hello World").hasLength(11)
22 |
23 | assertThat("Hello World").startsWith("Hell")
24 | assertThat("Hello World").endsWith("orld")
25 | assertThat("Hello World").doesNotStartWith("orld")
26 | assertThat("Hello World").doesNotEndWith("Hell")
27 |
28 | assertThat("Hello World").matches("He.+ ...ld")
29 | assertThat("Hello World").matches(Regex(".+"))
30 |
31 | assertThat("Hello World").doesNotMatch("Goodbye")
32 | assertThat("Hello World").doesNotMatch(Regex("A"))
33 |
34 | assertThat("Hello World").contains("ello")
35 | assertThat("Hello World").containsMatch("elll?o")
36 | assertThat("Hello World").containsMatch(Regex("l.+ W"))
37 |
38 | assertThat("Hello World").doesNotContain("Jello")
39 | assertThat("Hello World").doesNotContainMatch("0+")
40 | assertThat("Hello World").doesNotContainMatch(Regex("[0-9]"))
41 | }
42 |
43 | run {
44 | // Test false statements
45 | assertThrows {
46 | assertThat("ASDF").isEmpty()
47 | }.assertSubstrings(Summaries.EXPECTED_EMPTY)
48 | assertThrows {
49 | assertThat("???").isBlank()
50 | }.assertSubstrings(Summaries.EXPECTED_BLANK)
51 | assertThrows {
52 | assertThat("").isNotEmpty()
53 | }.assertSubstrings(Summaries.EXPECTED_NOT_EMPTY)
54 | assertThrows {
55 | assertThat(" ").isNotBlank()
56 | }.assertSubstrings(Summaries.EXPECTED_NOT_BLANK)
57 |
58 | assertThrows {
59 | assertThat("Hello World").hasLength(3)
60 | }.assertSubstrings(Summaries.EXPECTED_COMPARISON)
61 |
62 | assertThrows {
63 | assertThat("Hello World").startsWith("llo")
64 | }.assertSubstrings(Summaries.EXPECTED_STARTS_WITH)
65 | assertThrows {
66 | assertThat("Hello World").endsWith("llo")
67 | }.assertSubstrings(Summaries.EXPECTED_ENDS_WITH)
68 | assertThrows {
69 | assertThat("Hello World").doesNotStartWith("He")
70 | }.assertSubstrings(Summaries.EXPECTED_NOT_STARTS_WITH)
71 | assertThrows {
72 | assertThat("Hello World").doesNotEndWith("ld")
73 | }.assertSubstrings(Summaries.EXPECTED_NOT_ENDS_WITH)
74 |
75 | assertThrows {
76 | assertThat("Hello World").matches("ASDF")
77 | }.assertSubstrings(Summaries.EXPECTED_MATCH)
78 | assertThrows {
79 | assertThat("Hello World").matches(Regex("[0-9]+"))
80 | }.assertSubstrings(Summaries.EXPECTED_MATCH)
81 |
82 | assertThrows {
83 | assertThat("Hello World").doesNotMatch("..... .....")
84 | }.assertSubstrings(Summaries.EXPECTED_NOT_MATCH)
85 | assertThrows {
86 | assertThat("Hello World").doesNotMatch(Regex(".+"))
87 | }.assertSubstrings(Summaries.EXPECTED_NOT_MATCH)
88 |
89 | assertThrows {
90 | assertThat("Hello World").contains("Wello")
91 | }.assertSubstrings(Summaries.EXPECTED_CONTAINS)
92 | assertThrows {
93 | assertThat("Hello World").containsMatch("AAA?A")
94 | }.assertSubstrings(Summaries.EXPECTED_CONTAINS)
95 | assertThrows {
96 | assertThat("Hello World").containsMatch(Regex("12(34)"))
97 | }.assertSubstrings(Summaries.EXPECTED_CONTAINS)
98 |
99 | assertThrows {
100 | assertThat("Hello World").doesNotContain("o Wo")
101 | }.assertSubstrings(Summaries.EXPECTED_NOT_CONTAINS)
102 | assertThrows {
103 | assertThat("Hello World").doesNotContainMatch("l+")
104 | }.assertSubstrings(Summaries.EXPECTED_NOT_CONTAINS)
105 | assertThrows {
106 | assertThat("Hello World").doesNotContainMatch(Regex("or."))
107 | }.assertSubstrings(Summaries.EXPECTED_NOT_CONTAINS)
108 | }
109 | }
110 | }
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com/varabyte/truthish/NumberAsserts.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish
2 |
3 | import com.varabyte.truthish.failure.ReportError
4 | import com.varabyte.truthish.failure.Summaries
5 | import com.varabyte.truthish.failure.assertSubstrings
6 | import kotlin.test.Test
7 |
8 | class NumberAssets {
9 | @Test
10 | fun intChecks() {
11 | run {
12 | // Test true statements
13 | assertThat(5).isEqualTo(5)
14 | assertThat(5).isNotEqualTo(6)
15 | assertThat(5).isGreaterThanEqual(5)
16 | assertThat(5).isGreaterThanEqual(4)
17 | assertThat(5).isGreaterThan(4)
18 | assertThat(5).isLessThanEqual(5)
19 | assertThat(5).isLessThanEqual(6)
20 | assertThat(5).isLessThan(6)
21 | }
22 |
23 | run {
24 | // Test false statements
25 | assertThrows {
26 | assertThat(5).isGreaterThanEqual(6)
27 | }.assertSubstrings(Summaries.EXPECTED_COMPARISON, "5", "6")
28 | assertThrows {
29 | assertThat(5).isGreaterThan(5)
30 | }.assertSubstrings(Summaries.EXPECTED_COMPARISON, "5")
31 | assertThrows {
32 | assertThat(5).isLessThanEqual(4)
33 | }.assertSubstrings(Summaries.EXPECTED_COMPARISON, "5", "4")
34 | assertThrows {
35 | assertThat(5).isLessThan(3)
36 | }.assertSubstrings(Summaries.EXPECTED_COMPARISON, "5", "3")
37 | }
38 | }
39 |
40 | @Test
41 | fun byteChecks() {
42 | assertThat(5.toByte()).isEqualTo(5.toByte())
43 | assertThat(5.toByte()).isNotEqualTo(6.toByte())
44 | assertThat(5.toByte()).isGreaterThanEqual(5.toByte())
45 | assertThat(5.toByte()).isGreaterThanEqual(4.toByte())
46 | assertThat(5.toByte()).isGreaterThan(4.toByte())
47 | assertThat(5.toByte()).isLessThanEqual(5.toByte())
48 | assertThat(5.toByte()).isLessThanEqual(6.toByte())
49 | assertThat(5.toByte()).isLessThan(6.toByte())
50 | }
51 |
52 | @Test
53 | fun shortChecks() {
54 | assertThat(5.toShort()).isEqualTo(5.toShort())
55 | assertThat(5.toShort()).isNotEqualTo(6.toShort())
56 | assertThat(5.toShort()).isGreaterThanEqual(5.toShort())
57 | assertThat(5.toShort()).isGreaterThanEqual(4.toShort())
58 | assertThat(5.toShort()).isGreaterThan(4.toShort())
59 | assertThat(5.toShort()).isLessThanEqual(5.toShort())
60 | assertThat(5.toShort()).isLessThanEqual(6.toShort())
61 | assertThat(5.toShort()).isLessThan(6.toShort())
62 | }
63 |
64 |
65 | @Test
66 | fun longChecks() {
67 | assertThat(5.toLong()).isEqualTo(5.toLong())
68 | assertThat(5.toLong()).isNotEqualTo(6.toLong())
69 | assertThat(5.toLong()).isGreaterThanEqual(5.toLong())
70 | assertThat(5.toLong()).isGreaterThanEqual(4.toLong())
71 | assertThat(5.toLong()).isGreaterThan(4.toLong())
72 | assertThat(5.toLong()).isLessThanEqual(5.toLong())
73 | assertThat(5.toLong()).isLessThanEqual(6.toLong())
74 | assertThat(5.toLong()).isLessThan(6.toLong())
75 | }
76 |
77 | @Test
78 | fun floatChecks() {
79 | run {
80 | // Test true statements
81 | assertThat(5f).isEqualTo(5f)
82 | assertThat(5f).isNotEqualTo(6f)
83 | assertThat(5f).isGreaterThanEqual(5f)
84 | assertThat(5f).isGreaterThanEqual(4f)
85 | assertThat(5f).isGreaterThan(4f)
86 | assertThat(5f).isLessThanEqual(5f)
87 | assertThat(5f).isLessThanEqual(6f)
88 | assertThat(5f).isLessThan(6f)
89 |
90 | assertThat(5.3f).isWithin(0.5f).of(5.5f)
91 | assertThat(5.3f).isNotWithin(0.1f).of(5.5f)
92 | }
93 |
94 | run {
95 | // Test false statements
96 | assertThrows {
97 | assertThat(5.3f).isNotWithin(0.5f).of(5.5f)
98 | }.assertSubstrings(Summaries.EXPECTED_COMPARISON, "5.3", "0.5", "5.5")
99 | assertThrows {
100 | assertThat(5.3f).isWithin(0.1f).of(5.5f)
101 | }.assertSubstrings(Summaries.EXPECTED_COMPARISON, "5.3", "0.1", "5.5")
102 | }
103 | }
104 |
105 | @Test
106 | fun doubleChecks() {
107 | run {
108 | // Test true statements
109 | assertThat(5.0).isEqualTo(5.0)
110 | assertThat(5.0).isNotEqualTo(6.0)
111 | assertThat(5.0).isGreaterThanEqual(5.0)
112 | assertThat(5.0).isGreaterThanEqual(4.0)
113 | assertThat(5.0).isGreaterThan(4.0)
114 | assertThat(5.0).isLessThanEqual(5.0)
115 | assertThat(5.0).isLessThanEqual(6.0)
116 | assertThat(5.0).isLessThan(6.0)
117 |
118 | assertThat(5.3).isWithin(0.5).of(5.5)
119 | assertThat(5.3).isNotWithin(0.1).of(5.5)
120 | }
121 |
122 | run {
123 | // Test false statements
124 | assertThrows {
125 | assertThat(5.3).isNotWithin(0.5).of(5.5)
126 | }
127 | assertThrows {
128 | assertThat(5.3).isWithin(0.1).of(5.5)
129 | }
130 | }
131 | }
132 |
133 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Truthish
2 |
3 | 
4 | 
5 | 
6 |
7 | 
8 | 
9 |
10 |
11 |
12 |
13 | [](https://bsky.app/profile/bitspittle.bsky.social)
14 |
15 | A testing API inspired by [Google Truth](https://github.com/google/truth) but
16 | rewritten in Kotlin from the ground up, so it can be used in Kotlin
17 | multiplatform projects.
18 |
19 | For example, you can write `assertThat` checks in tests like this:
20 |
21 | ```kotlin
22 | import com.varabyte.truthish.*
23 |
24 | fun isEven(num: Int) = (num % 2) == 0
25 | fun square(num: Int) = (num * num)
26 |
27 | @Test
28 | fun testEvenOdd() {
29 | assertThat(isEven(1234)).isTrue()
30 | assertThat(isEven(1235)).isFalse()
31 | }
32 |
33 | @Test
34 | fun testSum() {
35 | val nums = listOf(1, 2, 3, 4, 5)
36 | assertThat(nums.sum()).isEqualTo(15)
37 | }
38 |
39 | @Test
40 | fun testMap() {
41 | assertThat(listOf(1, 2, 3, 4, 5).map { square(it) })
42 | .containsExactly(1, 4, 9, 16, 25)
43 | .inOrder()
44 | }
45 |
46 | @Test
47 | fun customMessage() {
48 | assertWithMessage("Unexpected list size")
49 | .that(listOf(1, 2, 3, 4, 5)).hasSize(5)
50 | }
51 |
52 | @Test
53 | fun testDivideByZeroException() {
54 | val ex = assertThrows {
55 | 10 / 0
56 | }
57 | assertThat(ex.message).isEqualTo("/ by zero")
58 | }
59 | ```
60 |
61 | If you would like to check multiple assertions at the same time (meaning a failure won't be reported until all checks
62 | have run), you can use the `assertAll` function:
63 |
64 | ```kotlin
65 | val person = Person("Alice", 30)
66 | assertAll {
67 | that(person.name).isEqualTo("Bob")
68 | that(person.age).isEqualTo(45)
69 | }
70 | // The above will assert once, reporting both equality check failures
71 | ```
72 |
73 | You can read the [Google Truth documentation](https://truth.dev/) for why they
74 | believe their fluent approach to assertions is both more readable and produces
75 | cleaner error messages, but let's break one of the tests above to see a
76 | specific example error message:
77 |
78 | ```kotlin
79 | @Test
80 | fun testMapButIntentionallyBroken() {
81 | assertThat(listOf(1, 2, 3, 4, 5).map { square(it) })
82 | .containsExactly(1, 4, 9, 15, 26) // <-- Ooops, messed up 16 and 25 here
83 | .inOrder()
84 | }
85 | ```
86 |
87 | Output:
88 |
89 | ```text
90 | A collection did not contain element(s)
91 |
92 | Expected exactly all elements from: [ 1, 4, 9, 15, 26 ]
93 | But was : [ 1, 4, 9, 16, 25 ]
94 | Missing : [ 15, 26 ]
95 | Extraneous : [ 16, 25 ]
96 | ```
97 |
98 | # Using Truthish in Your Project
99 |
100 | ## Multiplatform
101 |
102 | To use *Truthish* in your multiplatform application, declare any of the following dependencies relevant to your project:
103 |
104 | ```kotlin
105 | // build.gradle.kts
106 | // Multiplatform
107 |
108 | repositories {
109 | mavenCentral()
110 | }
111 |
112 | kotlin {
113 | jvm()
114 | js {
115 | browser()
116 | nodeJs()
117 | }
118 | wasmJs {
119 | browser()
120 | nodeJs()
121 | d8()
122 | }
123 |
124 | linuxArm64()
125 | linuxX64()
126 | macosArm64() // Mac M1+
127 | macosX64() // Mac Intel
128 | mingwX64() // Windows
129 | iosArm64() // iOS M1+
130 | iosX64() // iOS Intel
131 | iosSimulatorArm64()
132 | androidTarget()
133 |
134 | sourceSets {
135 | commonTest.dependencies {
136 | implementation("com.varabyte.truthish:truthish:1.0.3")
137 | implementation(kotlin("test"))
138 | }
139 | }
140 | }
141 | ```
142 |
143 | ## Single platform
144 |
145 | You can also use *Truthish* in non-multiplatform projects as well:
146 |
147 | ### JVM
148 |
149 | ```kotlin
150 | // build.gradle.kts
151 |
152 | repositories {
153 | mavenCentral()
154 | }
155 |
156 | dependencies {
157 | // ...
158 |
159 | testImplementation(kotlin("test"))
160 | testImplementation("com.varabyte.truthish:truthish:1.0.3")
161 | }
162 | ```
163 |
164 | ### Android
165 |
166 | ```kotlin
167 | // build.gradle.kts
168 |
169 | repositories {
170 | mavenCentral()
171 | }
172 |
173 | android { /* ... */ }
174 |
175 | dependencies {
176 | // ...
177 |
178 | // If used in tests that are run on the host (i.e. your dev machine)
179 | testImplementation("com.varabyte.truthish:truthish:1.0.3")
180 |
181 | // If used in tests that are run on the device
182 | androidTestImplementation("com.varabyte.truthish:truthish:1.0.3")
183 | }
184 | ```
185 |
186 | ### Testing snapshots
187 |
188 | Most users won't ever need to run a Truthish snapshot, so feel free to skip this section! However, occasionally, bug
189 | fixes and new features will be available for testing for a short period before they are released.
190 |
191 | If you ever file a bug with Truthish and are asked to test a fix using a snapshot, you must add an entry for the sonatype
192 | snapshots repository to your `repositories` block in order to allow Gradle to find it:
193 |
194 | ```diff
195 | // build.gradle.kts
196 |
197 | repositories {
198 | mavenCentral()
199 | + maven("https://central.sonatype.com/repository/maven-snapshots/") {
200 | + mavenContent {
201 | + includeGroup("com.varabyte.truthish")
202 | + snapshotsOnly()
203 | + }
204 | + }
205 | }
206 | ```
207 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/failure/Report.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.failure
2 |
3 | /**
4 | * Create a report which consists of a single summary line and an optional list of details.
5 | *
6 | * The report's [toString] is overloaded so it can be used directly in [println] calls.
7 | *
8 | * Use a [FailureStrategy] to handle a report.
9 | *
10 | * A sample report's output might look something like
11 | *
12 | * ```
13 | Two values did not equal each other
14 | *
15 | * Expected: 25
16 | * But was : 24
17 | * ```
18 | *
19 | * or
20 | *
21 | * ```
22 | * A collection was missing at least one item
23 | *
24 | * Missing : [ GREEN ]
25 | * Expected: [ RED, GREEN, BLUE ]
26 | * But was : [ RED, BLUE ]
27 | * ```
28 | *
29 | * @param summary A one-line summary of this report. This should be a generic, re-usable message,
30 | * and any specific details for this particular report should be specified in the [details] parameter.
31 | *
32 | * @see FailureStrategy
33 | */
34 | class Report(private val summary: String, details: Details? = null) {
35 | internal val details = details ?: Details()
36 | override fun toString(): String {
37 | val builder = StringBuilder(summary)
38 | val detailItems = details.items
39 | if (detailItems.isNotEmpty()) {
40 | builder.append("\n\n")
41 | val longestKey = detailItems.maxOf { it.first.length }
42 |
43 | detailItems.forEachIndexed { index, pair ->
44 | if (index > 0) {
45 | builder.append("\n")
46 | }
47 | builder.append(pair.first)
48 | builder.append(" ".repeat(longestKey - pair.first.length)).append(": ")
49 | builder.append(stringifierFor(pair.second))
50 | details.extras[pair.first]?.let { extra -> builder.append(" ($extra)") }
51 | }
52 | }
53 | builder.append("\n")
54 | return builder.toString()
55 | }
56 | }
57 |
58 | /**
59 | * A collection of key/value pairs which should be included in a table-like report.
60 | *
61 | * @property extras Optional additional information which, if specified, should be appended after the value
62 | * somehow. The key for the extra information should be the same as the key for the details item. For example,
63 | * the detail ("Expected" to "xyz") can be decorated with extra information by adding the extra entry
64 | * ("Expected" to "Type is `ClassXyz`").
65 | */
66 | class Details(
67 | items: List> = emptyList(),
68 | internal val extras: Map = emptyMap()
69 | ) {
70 | private val _items = items.toMutableList()
71 | val items: List> = _items
72 |
73 | fun add(index: Int, element: Pair) {
74 | _items.add(index, element)
75 | }
76 |
77 | fun add(element: Pair) {
78 | _items.add(element)
79 | }
80 | }
81 |
82 | fun Details.find(key: String): Any? {
83 | return items.find { it.first == key }?.second
84 | }
85 |
86 | /**
87 | * Helpful utility methods providing detail lists for common scenarios.
88 | */
89 | object DetailsFor {
90 | const val VALUE = "Value"
91 | const val EXPECTED = "Expected"
92 | const val BUT_WAS = "But was"
93 | const val AT = "At"
94 |
95 | /**
96 | * A detail list useful when asserting about the state of a single value, e.g. it was
97 | * expected to be `null` but instead it's *(some value)*.
98 | */
99 | fun actual(actual: Any?): Details {
100 | return Details(listOf(VALUE to actual))
101 | }
102 |
103 | /**
104 | * A detail list useful when asserting about something expected not happening, e.g.
105 | * it was expected that an IoException would be thrown but one wasn't.
106 | */
107 | fun expected(expected: Any?): Details {
108 | return Details(listOf(EXPECTED to expected))
109 | }
110 |
111 | private fun createExpectedActualExtrasFor(
112 | expected: Pair,
113 | actual: Pair
114 | ): Map {
115 | return buildMap {
116 | val expectedValue = expected.second
117 | val actualValue = actual.second
118 | if (expectedValue != null && actualValue != null) {
119 | val expectedStringifier = stringifierFor(expectedValue)
120 | val actualStringifier = stringifierFor(actualValue)
121 |
122 | var isOutputAmbiguous = expectedStringifier.toString() == actualStringifier.toString()
123 |
124 | // String output adds surrounding quotes, but that can still result in confusing error like
125 | // Expected: "x" But was: x
126 | // so handle those cases too.
127 | if (!isOutputAmbiguous && expectedValue is String && actualValue !is String && expectedValue == actualStringifier.toString()) {
128 | isOutputAmbiguous = true
129 | }
130 | if (!isOutputAmbiguous && expectedValue !is String && actualValue is String && expectedStringifier.toString() == actualValue) {
131 | isOutputAmbiguous = true
132 | }
133 |
134 | if (isOutputAmbiguous) {
135 | put(EXPECTED, "Type: `${expectedValue::class.simpleName}`")
136 | put(BUT_WAS, "Type: `${actualValue::class.simpleName}`")
137 | }
138 | }
139 | }
140 | }
141 |
142 | /**
143 | * A detail list useful when asserting about an expected value vs. an actual one.
144 | */
145 | fun expectedActual(expected: Any?, actual: Any?): Details {
146 | val items = listOf(
147 | EXPECTED to expected,
148 | BUT_WAS to actual
149 | )
150 | return Details(items, createExpectedActualExtrasFor(items[0], items[1]))
151 | }
152 |
153 | /**
154 | * Like the other [expectedActual] method, except you can add more details to the "Expected"
155 | * label. For example, "Expected greater than:" vs just "Expected"
156 | *
157 | * For consistency / readability, [expectedSuffix] should be lower-case. A space will automatically be inserted
158 | * between the "expected" label and the extra information.
159 | */
160 | fun expectedActual(expectedSuffix: String, expected: Any?, actual: Any?): Details {
161 | val items = listOf(
162 | "$EXPECTED $expectedSuffix" to expected,
163 | BUT_WAS to actual
164 | )
165 | return Details(items, createExpectedActualExtrasFor(items[0], items[1]))
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com/varabyte/truthish/BasicAsserts.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish
2 |
3 | import com.varabyte.truthish.failure.*
4 | import com.varabyte.truthish.failure.ReportError
5 | import kotlin.test.Test
6 |
7 | class BasicAsserts {
8 | data class IntValue(val value: Int) : Comparable {
9 | override fun compareTo(other: IntValue) = value.compareTo(other.value)
10 | }
11 |
12 | class Stub
13 |
14 | @Test
15 | fun assertEquality() {
16 | run {
17 | // Test true statements
18 | assertThat("str").isEqualTo("str")
19 | assertThat("str1").isNotEqualTo("str2")
20 |
21 | assertThat(IntValue(10)).isEqualTo(IntValue(10))
22 | assertThat(IntValue(10)).isNotEqualTo(IntValue(11))
23 | }
24 |
25 | run {
26 | // Test false statements
27 | assertThrows {
28 | assertThat("str").isNotEqualTo("str")
29 | }.assertSubstrings(Summaries.EXPECTED_NOT_EQUAL, "str")
30 | assertThrows {
31 | assertThat("str1").isEqualTo("str2")
32 | }.assertSubstrings(Summaries.EXPECTED_EQUAL, "str1", "str2")
33 | }
34 | }
35 |
36 | @Test
37 | fun assertNullity() {
38 | run {
39 | // Test true statements
40 | var stub: Stub? = Stub()
41 | assertThat(stub).isNotNull()
42 |
43 | stub = null
44 | assertThat(stub).isNull()
45 | }
46 |
47 | run {
48 | // Test false statements
49 | var stub: Stub? = Stub()
50 | assertThrows {
51 | assertThat(stub).isNull()
52 | }.assertSubstrings(Summaries.EXPECTED_NULL)
53 |
54 | stub = null
55 | assertThrows {
56 | assertThat(stub).isNotNull()
57 | }.assertSubstrings(Summaries.EXPECTED_NOT_NULL)
58 | }
59 | }
60 |
61 | @Test
62 | fun assertInstance() {
63 | run {
64 | // Test true statements
65 | assertThat(IntValue(234)).isInstanceOf()
66 | assertThat(IntValue(456)).isNotInstanceOf()
67 | assertThat(IntValue(789)).isInstanceOf()
68 | }
69 |
70 | run {
71 | // Test false statements
72 | assertThrows {
73 | assertThat(IntValue(234)).isInstanceOf()
74 | }.assertSubstrings(Summaries.EXPECTED_INSTANCE, "IntValue", "Int")
75 |
76 | assertThrows {
77 | assertThat(IntValue(234)).isNotInstanceOf()
78 | }.assertSubstrings(Summaries.EXPECTED_NOT_INSTANCE, "IntValue")
79 | }
80 | }
81 |
82 | @Test
83 | fun assertSame() {
84 | run {
85 | // Test true statements
86 | val stubValue1 = Stub()
87 | val stubValue2 = Stub()
88 | val stubValue3 = stubValue1
89 |
90 | assertThat(stubValue1).isSameAs(stubValue1)
91 | assertThat(stubValue1).isNotSameAs(stubValue2)
92 | assertThat(stubValue1).isSameAs(stubValue3)
93 |
94 | }
95 |
96 | run {
97 | // Test false statements
98 | val stubValue1 = Stub()
99 | val stubValue2 = Stub()
100 | val stubValue3 = stubValue1
101 |
102 | assertThrows {
103 | assertThat(stubValue1).isSameAs(stubValue2)
104 | }.assertSubstrings(Summaries.EXPECTED_SAME)
105 |
106 | assertThrows {
107 | assertThat(stubValue1).isNotSameAs(stubValue3)
108 | }.assertSubstrings(Summaries.EXPECTED_NOT_SAME)
109 | }
110 | }
111 |
112 | @Test
113 | fun assertNamed() {
114 | val stub: Stub? = Stub()
115 | assertThrows {
116 | assertThat(stub).named("Stubby McStubberson").isNull()
117 | }.assertSubstrings("Stubby McStubberson")
118 | }
119 |
120 | @Test
121 | fun assertThrows() {
122 | run { // Verify the correct path
123 | val e = assertThrows {
124 | throw IllegalArgumentException("xyz")
125 | }
126 | assertThat(e.message).isEqualTo("xyz")
127 | }
128 |
129 | run { // assertThrows doesn't accept no exceptions.
130 | val e = assertThrows {
131 | // This is the real assert test. The outer one captures it in order to verify the failure text.
132 | assertThrows {
133 | }
134 | }
135 | assertThat(e.message!!).contains(Summaries.EXPECTED_EXCEPTION)
136 | assertThat(e.message!!).contains("IllegalArgumentException")
137 | }
138 |
139 | run { // assertThrows doesn't accept invalid exceptions.
140 | val e = assertThrows {
141 | // This is the real assert test. The outer one captures it in order to verify the failure text.
142 | assertThrows {
143 | throw IllegalStateException()
144 | }
145 | }
146 | assertThat(e.message!!).contains(Summaries.EXPECTED_EXCEPTION)
147 | assertThat(e.message!!).contains("IllegalArgumentException")
148 | assertThat(e.message!!).contains("IllegalStateException")
149 | }
150 | }
151 |
152 | @Test
153 | fun assertThrowsWithMessage() {
154 | run { // Verify the correct path
155 | val e = assertThrows(message = "not used") {
156 | throw IllegalArgumentException("xyz")
157 | }
158 | assertThat(e.message).isEqualTo("xyz")
159 | }
160 |
161 | run { // assertThrows doesn't accept no exceptions.
162 | val e = assertThrows(message = "outer assert") {
163 | // This is the real assert test. The outer one captures it in order to verify the failure text.
164 | assertThrows(message = "inner assert") {
165 | }
166 | }
167 | assertThat(e.message!!).contains(Summaries.EXPECTED_EXCEPTION)
168 | assertThat(e.message!!).contains("IllegalArgumentException")
169 | assertThat(e.message!!).contains("inner assert")
170 | assertThat(e.message!!).doesNotContain("outer assert")
171 | }
172 |
173 | run { // assertThrows doesn't accept invalid exceptions.
174 | val e = assertThrows(message = "outer assert") {
175 | // This is the real assert test. The outer one captures it in order to verify the failure text.
176 | assertThrows(message = "inner assert") {
177 | throw IllegalStateException()
178 | }
179 | }
180 | assertThat(e.message!!).contains(Summaries.EXPECTED_EXCEPTION)
181 | assertThat(e.message!!).contains("IllegalArgumentException")
182 | assertThat(e.message!!).contains("IllegalStateException")
183 | assertThat(e.message!!).contains("inner assert")
184 | assertThat(e.message!!).doesNotContain("outer assert")
185 | }
186 | }
187 | }
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com/varabyte/truthish/AssertAllTest.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish
2 |
3 | import com.varabyte.truthish.failure.*
4 | import kotlin.test.Test
5 |
6 | class AssertAllTest {
7 | @Test
8 | fun assertAllCollectsMultipleErrors() {
9 | val a = "A"
10 | val b = "B"
11 | val c = "C"
12 |
13 | assertThrows {
14 | assertAll("Just an assertAll test") {
15 | that(a).isEqualTo(a) // ✅
16 | that(a).isEqualTo(b) // ❌
17 | that(b).isEqualTo(b) // ✅
18 | that(b).isEqualTo(c) // ❌
19 | that(c).isEqualTo(a) // ❌
20 | that(c).isEqualTo(c) // ✅
21 | that(a).isEqualTo(c) // ❌
22 | }
23 | }.let { e ->
24 | assertAll {
25 | that(e.summary).isEqualTo("Just an assertAll test")
26 | that(e.reports.size).isEqualTo(4)
27 | // Failure 1: "that(a).isEqualTo(b)"
28 | with(e.reports[0].details) {
29 | that(this.find(DetailsFor.EXPECTED)!!).isEqualTo(b)
30 | that(this.find(DetailsFor.BUT_WAS)!!).isEqualTo(a)
31 | }
32 | // Failure 4: "that(a).isEqualTo(c)"
33 | with(e.reports[3].details) {
34 | that(this.find(DetailsFor.EXPECTED)!!).isEqualTo(c)
35 | that(this.find(DetailsFor.BUT_WAS)!!).isEqualTo(a)
36 | }
37 | }
38 | }
39 | }
40 |
41 | @Test
42 | fun assertAllWithMessageWorks() {
43 | assertThrows {
44 | val a = "A"
45 | val b = "B"
46 | val c = "C"
47 |
48 | assertAll {
49 | withMessage("A is equal to B").that(a).isEqualTo(b)
50 | withMessage("B is equal to C").that(b).isEqualTo(c)
51 | }
52 | }.let { e ->
53 | assertAll {
54 | that(e.summary).isNull()
55 | that(e.reports.size).isEqualTo(2)
56 | // that(a).isEqualTo(b)
57 | assertThat(e.reports[0].assertSubstrings("A is equal to B"))
58 | assertThat(e.reports[1].assertSubstrings("B is equal to C"))
59 | }
60 | }
61 | }
62 |
63 | class DummyComparable(private val value: Int) : Comparable {
64 | override fun compareTo(other: DummyComparable) = value.compareTo(other.value)
65 | }
66 |
67 | @Test
68 | fun assertAllTestForCoverage() {
69 | assertAll {
70 | that(null).isNull()
71 | that(Any()).isNotSameAs(Any())
72 | that(DummyComparable(10)).isGreaterThan(DummyComparable(9))
73 | that(true).isTrue()
74 | that(100.toByte()).isEqualTo(100.toByte())
75 | that(100.toShort()).isEqualTo(100.toShort())
76 | that(100).isEqualTo(100)
77 | that(100L).isEqualTo(100L)
78 | that(100.0f).isEqualTo(100.0f)
79 | that(100.0).isEqualTo(100.0)
80 | that("Hello").isEqualTo("Hello")
81 | that(listOf(1, 2, 3)).isEqualTo(listOf(1, 2, 3))
82 | that(mapOf(1 to 2, 3 to 4)).isEqualTo(mapOf(1 to 2, 3 to 4))
83 | that(sequenceOf(1, 2, 3)).containsAllIn(1, 2, 3)
84 | that(arrayOf(1, 2, 3)).isEqualTo(arrayOf(1, 2, 3))
85 | that(booleanArrayOf(true, false)).isEqualTo(booleanArrayOf(true, false))
86 | that(byteArrayOf(1, 2, 3)).isEqualTo(byteArrayOf(1, 2, 3))
87 | that(charArrayOf('a', 'b', 'c')).isEqualTo(charArrayOf('a', 'b', 'c'))
88 | that(shortArrayOf(1, 2, 3)).isEqualTo(shortArrayOf(1, 2, 3))
89 | that(intArrayOf(1, 2, 3)).isEqualTo(intArrayOf(1, 2, 3))
90 | that(longArrayOf(1, 2, 3)).isEqualTo(longArrayOf(1, 2, 3))
91 | that(floatArrayOf(1.0f, 2.0f, 3.0f)).isEqualTo(floatArrayOf(1.0f, 2.0f, 3.0f))
92 | that(doubleArrayOf(1.0, 2.0, 3.0)).isEqualTo(doubleArrayOf(1.0, 2.0, 3.0))
93 | }
94 |
95 | assertThrows {
96 | assertAll {
97 | // Dummy "inOrder" asserters are provided from a different code branch if the first part of the
98 | // assert fails early.
99 | that(arrayOf(1, 2, 3)).containsAllIn(4, 5, 6).inOrder()
100 | that(arrayOf(1, 2, 3)).containsExactly(1, 2, 3, 4).inOrder()
101 | }
102 | }
103 | }
104 |
105 | @Test
106 | fun assertAllCallstacksAreCorrect() {
107 | // This test can be useful if we ever refactor code and don't realize we broke our logic for determining how
108 | // callstacks are collected.
109 |
110 | val a = "A"
111 | val b = "B"
112 | val c = "C"
113 |
114 | assertThrows {
115 | assertAll {
116 | that(a).isEqualTo(a) // ✅
117 | that(a).isEqualTo(b) // ❌ // First failure, line0
118 | that(b).isEqualTo(b) // ✅
119 | that(b).isEqualTo(c) // ❌ // Second failure, line0 + 2
120 | that(c).isEqualTo(a) // ❌ // Third failure, line0 + 3
121 | that(c).isEqualTo(c) // ✅
122 | that(a).isEqualTo(c) // ❌ // Fourth failure, line0 + 5
123 | }
124 | }.let { e ->
125 | // JS callstacks are mangled in release mode, so "At" details are skipped in that case
126 | val atValue = e.reports[0].details.find(DetailsFor.AT)?.toString() ?: return
127 |
128 | // Use a regex to extract callstack values, so that this test will still pass even if the line numbers change
129 | // Example AT entry for...
130 | // jvm: com.varabyte.truthish.AssertAllTest$assertAllCallstacksAreCorrect$2$1.invoke(AssertAllTest.kt:123)
131 | // k/n: com.varabyte.truthish.AssertAllTest#assertAllCallstacksAreCorrect(){} + 1847 (/Users/d9n/Code/1p/truthish/src/commonTest/kotlin/com/varabyte/truthish/AssertAllTest.kt:133:21)
132 | val lineRegex = Regex(".+${AssertAllTest::class.simpleName}\\.kt:(\\d+).+")
133 |
134 | // Windows and Linux don't include line numbers in their callstacks?!
135 | // At least verify the specified callstack contains this class in it and isn't in some random part of the
136 | // Truthish codebase.
137 | val match = lineRegex.matchEntire(atValue) ?: run {
138 | assertThat(atValue).contains(AssertAllTest::class.simpleName.toString())
139 | return
140 | }
141 |
142 | val lineNumber = match.groupValues[1].toInt()
143 |
144 | assertAll("Comparing callstacks against \"$atValue\"") {
145 | // "Error report" to "delta distance from the first report"
146 | val reportsToCheck = listOf(
147 | e.reports[1] to 2,
148 | e.reports[2] to 3,
149 | e.reports[3] to 5
150 | )
151 |
152 | reportsToCheck.forEachIndexed { i, (report, lineDelta) ->
153 | with(lineRegex.matchEntire(report.details.find(DetailsFor.AT).toString())!!) {
154 | that(groupValues[1].toInt()).named("Line delta for report ${i + 1}").isEqualTo(lineNumber + lineDelta)
155 | }
156 | }
157 | }
158 | }
159 | }
160 |
161 | }
162 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/subjects/IterableSubject.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.subjects
2 |
3 | import com.varabyte.truthish.failure.DetailsFor
4 | import com.varabyte.truthish.failure.Report
5 | import com.varabyte.truthish.failure.Reportable
6 | import com.varabyte.truthish.failure.Summaries
7 |
8 | @Suppress("NAME_SHADOWING")
9 | open class IterableSubject(actual: Iterable) : NotNullSubject>(actual) {
10 | private val actual = actual.toList()
11 |
12 | fun isEmpty() {
13 | if (actual.isNotEmpty()) {
14 | report(Report(Summaries.EXPECTED_COLLECTION_EMPTY, DetailsFor.actual(actual)))
15 | }
16 | }
17 |
18 | fun isNotEmpty() {
19 | if (actual.isEmpty()) {
20 | report(Report(Summaries.EXPECTED_COLLECTION_NOT_EMPTY))
21 | }
22 | }
23 |
24 | fun hasSize(size: Int) {
25 | val actualSize = actual.count()
26 | if (actualSize != size) {
27 | report(Report(Summaries.EXPECTED_COMPARISON, DetailsFor.expectedActual("size", size, actualSize)))
28 | }
29 | }
30 |
31 | fun contains(element: T) {
32 | if (!actual.contains(element)) {
33 | report(
34 | Report(
35 | Summaries.EXPECTED_COLLECTION_CONTAINS,
36 | DetailsFor.expectedActual("to contain", element, actual)
37 | )
38 | )
39 | }
40 | }
41 |
42 | fun doesNotContain(element: T) {
43 | if (actual.contains(element)) {
44 | report(
45 | Report(
46 | Summaries.EXPECTED_COLLECTION_NOT_CONTAINS,
47 | DetailsFor.expectedActual("not to contain", element, actual)
48 | )
49 | )
50 | }
51 | }
52 |
53 | fun hasNoDuplicates() {
54 | val duplicates = actual.groupingBy { it }.eachCount().filter { it.value > 1 }
55 | if (duplicates.isNotEmpty()) {
56 | report(
57 | Report(
58 | Summaries.EXPECTED_COLLECTION_NO_DUPLICATES,
59 | DetailsFor.actual(actual).apply {
60 | add("Duplicates" to duplicates.map { it.key }.toList())
61 | }
62 | )
63 | )
64 | }
65 | }
66 |
67 | fun containsAnyIn(other: Iterable) {
68 | val other = other.toList()
69 | if (other.isEmpty()) {
70 | return
71 | }
72 |
73 | val intersection = actual.intersect(other)
74 | if (intersection.isEmpty()) {
75 | report(
76 | Report(
77 | Summaries.EXPECTED_COLLECTION_CONTAINS,
78 | DetailsFor.expectedActual("at least one element from", other, actual)
79 | )
80 | )
81 | }
82 | }
83 |
84 | fun containsAnyIn(vararg elements: T) = containsAnyIn(elements.asIterable())
85 |
86 | fun containsAllIn(other: Iterable): OrderedAsserter {
87 | val other = other.toList()
88 |
89 | if (!actual.containsAll(other)) {
90 | report(
91 | Report(
92 | Summaries.EXPECTED_COLLECTION_CONTAINS,
93 | DetailsFor.expectedActual("all elements from", other, actual).apply {
94 | add("Missing" to (other - actual))
95 | }
96 | )
97 | )
98 | return skipInOrderCheck(this)
99 | }
100 | return OrderedAsserter(this, actual, other)
101 | }
102 |
103 | fun containsAllIn(vararg elements: T) = containsAllIn(elements.asIterable())
104 |
105 | fun containsNoneIn(other: Iterable) {
106 | val other = other.toList()
107 |
108 | val commonElements = actual.intersect(other)
109 | if (commonElements.isNotEmpty()) {
110 | report(
111 | Report(
112 | Summaries.EXPECTED_COLLECTION_NOT_CONTAINS,
113 | DetailsFor.expectedActual("no elements from", other, actual).apply {
114 | add("Containing" to commonElements)
115 | }
116 | )
117 | )
118 | }
119 | }
120 |
121 | fun containsNoneIn(vararg elements: T) = containsNoneIn(elements.asIterable())
122 |
123 | fun containsExactly(other: Iterable): OrderedAsserter {
124 | val other = other.toList()
125 |
126 | val remainingActual = actual.toMutableList()
127 | val remainingOther = other.toMutableList()
128 | other.forEach { remainingActual.remove(it) }
129 | actual.forEach { remainingOther.remove(it) }
130 |
131 | if (remainingActual.isNotEmpty() || remainingOther.isNotEmpty()) {
132 | report(
133 | Report(
134 | Summaries.EXPECTED_COLLECTION_CONTAINS,
135 | DetailsFor.expectedActual("exactly all elements from", other, actual).apply {
136 | if (remainingOther.isNotEmpty()) {
137 | add("Missing" to remainingOther)
138 | }
139 | if (remainingActual.isNotEmpty()) {
140 | add("Extraneous" to remainingActual)
141 | }
142 | }
143 | )
144 | )
145 | return skipInOrderCheck(this)
146 | }
147 |
148 | return OrderedAsserter(this, actual, other)
149 | }
150 |
151 | fun containsExactly(vararg elements: T) = containsExactly(elements.asIterable())
152 | }
153 |
154 | fun IterableSubject.containsAnyIn(other: Array) = containsAnyIn(*other)
155 | fun IterableSubject.containsAllIn(other: Array) = containsAllIn(*other)
156 | fun IterableSubject.containsNoneIn(other: Array) = containsNoneIn(*other)
157 | fun IterableSubject.containsExactly(other: Array) = containsExactly(*other)
158 |
159 |
160 | /**
161 | * We don't want to test inorder if a check already failed, so provide this [OrderedAsserter]\
162 | * instead, which is guaranteed to pass.
163 | */
164 | internal fun skipInOrderCheck(parent: Reportable) = OrderedAsserter(parent, emptyList(), emptyList())
165 |
166 | class OrderedAsserter(
167 | private val parent: Reportable,
168 | actual: Iterable,
169 | other: Iterable
170 | ) {
171 | private val actual = actual.toList()
172 | private val other = other.toList()
173 |
174 | /**
175 | * It's possible that our lists might have duplicate values - make sure we don't ever recount the
176 | * same element in order!
177 | */
178 | private val usedIndices = mutableSetOf()
179 |
180 | fun inOrder() {
181 | var actualIndex = 0
182 | var otherIndex = 0
183 |
184 | while (otherIndex < other.size) {
185 | val lookingFor = other[otherIndex]
186 | while (actualIndex < actual.size) {
187 | if (!usedIndices.contains(actualIndex) && actual[actualIndex] == lookingFor) {
188 | usedIndices.add(actualIndex)
189 | break
190 | }
191 | ++actualIndex
192 | }
193 | if (actualIndex == actual.size) {
194 | // If we got here, we couldn't find the next element from [other]
195 | parent.report(
196 | Report(
197 | Summaries.EXPECTED_COLLECTION_ORDERED,
198 | DetailsFor.expectedActual("in order", other, actual)
199 | )
200 | )
201 | break
202 | }
203 |
204 | ++otherIndex
205 | }
206 |
207 | // If we got here -- congrats! All elements of [other] were found in [actual] in order.
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com/varabyte/truthish/IterableAsserts.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish
2 |
3 | import com.varabyte.truthish.failure.ReportError
4 | import com.varabyte.truthish.failure.Summaries
5 | import com.varabyte.truthish.failure.assertSubstrings
6 | import kotlin.test.Test
7 |
8 | class IterableAsserts {
9 | private val emptyInts
10 | get() = listOf()
11 | private val emptyStrs
12 | get() = listOf()
13 |
14 | @Test
15 | fun listChecks() {
16 | run {
17 | // Test true statements
18 | assertThat(listOf("a", "b", "c")).isEqualTo(listOf("a", "b", "c"))
19 | assertThat(listOf("a", "b", "c")).isNotEqualTo(listOf("a", "b", "d"))
20 |
21 | assertThat(emptyInts).isEmpty()
22 | assertThat(listOf(1, 2, 3)).isNotEmpty()
23 | assertThat(listOf(1, 2, 3)).hasSize(3)
24 |
25 | assertThat(listOf("a", "b", "c")).contains("b")
26 | assertThat(listOf("a", "b", "c")).doesNotContain("d")
27 | assertThat(emptyStrs).doesNotContain("d")
28 |
29 | assertThat(listOf("a", "b", "c")).hasNoDuplicates()
30 | assertThat(emptyStrs).hasNoDuplicates()
31 |
32 | assertThat(listOf("a", "b", "c")).containsAnyIn("c", "d", "e")
33 | assertThat(listOf("a", "b", "c")).containsAnyIn(emptyStrs)
34 | assertThat(emptyStrs).containsAnyIn(emptyStrs)
35 |
36 | assertThat(listOf("a", "b", "c")).containsAllIn("c", "b")
37 | assertThat(listOf("a", "b", "c")).containsAllIn("b", "c").inOrder()
38 | assertThat(listOf("a", "b", "c")).containsAllIn(emptyStrs)
39 |
40 | assertThat(listOf("a", "b", "c")).containsNoneIn("d", "e", "f")
41 | assertThat(emptyStrs).containsNoneIn("d", "e", "f")
42 |
43 | assertThat(listOf("a", "b", "c")).containsExactly("b", "c", "a")
44 | assertThat(listOf("a", "b", "c")).containsExactly("a", "b", "c").inOrder()
45 | assertThat(emptyStrs).containsExactly(emptyStrs)
46 | assertThat(emptyStrs).containsExactly(emptyStrs).inOrder()
47 | }
48 |
49 | run {
50 | // Test false statements
51 | assertThrows {
52 | assertThat(listOf("a", "b", "c")).isNotEqualTo(listOf("a", "b", "c"))
53 | }.assertSubstrings(Summaries.EXPECTED_NOT_EQUAL, "[ \"a\", \"b\", \"c\" ]")
54 |
55 | assertThrows {
56 | assertThat(listOf(1, 2, 3)).isEmpty()
57 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_EMPTY)
58 | assertThrows {
59 | assertThat(emptyInts).isNotEmpty()
60 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_NOT_EMPTY)
61 | assertThrows {
62 | assertThat(listOf(1, 2, 3)).hasSize(2)
63 | }.assertSubstrings(Summaries.EXPECTED_COMPARISON)
64 |
65 | assertThrows {
66 | assertThat(listOf("a", "b", "c")).contains("d")
67 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_CONTAINS)
68 | assertThrows {
69 | assertThat(listOf("a", "b", "c")).doesNotContain("b")
70 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_NOT_CONTAINS)
71 |
72 | assertThrows {
73 | assertThat(listOf("a", "b", "a")).hasNoDuplicates()
74 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_NO_DUPLICATES)
75 |
76 | assertThrows {
77 | assertThat(listOf("a", "b", "c")).containsAnyIn("d", "e", "f")
78 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_CONTAINS)
79 |
80 | assertThrows {
81 | assertThat(listOf("a", "b", "c")).containsAllIn("c", "b", "d")
82 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_CONTAINS)
83 | assertThrows {
84 | assertThat(listOf("a", "b", "c")).containsAllIn("c", "b", "a").inOrder()
85 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_ORDERED)
86 |
87 | assertThrows {
88 | assertThat(listOf("a", "b", "c")).containsNoneIn("c", "d", "e")
89 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_NOT_CONTAINS)
90 |
91 | assertThrows {
92 | assertThat(listOf("a", "b", "c")).containsExactly("b", "b", "c")
93 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_CONTAINS)
94 | assertThrows {
95 | assertThat(listOf("a", "b", "c")).containsExactly("c", "b", "a").inOrder()
96 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_ORDERED)
97 | assertThrows {
98 | assertThat(listOf("a", "a", "b", "c")).containsExactly("a", "b", "c")
99 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_CONTAINS)
100 | assertThrows {
101 | assertThat(listOf("a", "a", "b", "c")).containsExactly("a", "b", "b", "c")
102 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_CONTAINS)
103 |
104 | // Verify inOrder doesn't get tripped up by duplicate elements in the list
105 | assertThrows {
106 | assertThat(listOf("30", "20", "10", "20")).containsExactly("30", "20", "20", "10").inOrder()
107 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_ORDERED)
108 |
109 | // Verify inOrder doesn't get triggered if the original assert already failed
110 | assertThrows {
111 | assertThat(listOf("a", "b", "c")).containsAllIn("x", "y", "z").inOrder()
112 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_CONTAINS)
113 | assertThrows {
114 | assertThat(listOf("a", "b", "c")).containsExactly("c").inOrder()
115 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_CONTAINS)
116 | }
117 | }
118 |
119 | @Test
120 | fun setChecks() {
121 | // Sets are basically just iterables, but we include tests just to make sure using them feels right
122 | val evensThru10 = (2 .. 10 step 2).toSet()
123 |
124 | run {
125 | // Test true statements
126 | assertThat(evensThru10).isNotEmpty()
127 | assertThat(setOf()).isEmpty()
128 |
129 | assertThat(evensThru10).contains(8)
130 | assertThat(evensThru10).doesNotContain(9)
131 |
132 | assertThat(evensThru10).containsAnyIn(1, 2, 3)
133 | assertThat(evensThru10).containsAllIn(10, 4, 8)
134 | assertThat(evensThru10).containsAllIn(4, 8, 10).inOrder()
135 | assertThat(evensThru10).containsNoneIn(1, 11)
136 | assertThat(evensThru10).containsExactly(2, 4, 6, 8, 10).inOrder()
137 |
138 | assertThat(setOf()).containsExactly(emptyInts)
139 | }
140 |
141 | run {
142 | // Test false statements
143 | assertThrows {
144 | assertThat(evensThru10).isEmpty()
145 | }
146 | assertThrows {
147 | assertThat(setOf()).isNotEmpty()
148 | }
149 |
150 | assertThrows {
151 | assertThat(evensThru10).contains(9)
152 | }
153 | assertThrows {
154 | assertThat(evensThru10).doesNotContain(8)
155 | }
156 |
157 | assertThrows {
158 | assertThat(evensThru10).containsAnyIn(1, 3, 5)
159 | }
160 | assertThrows {
161 | assertThat(evensThru10).containsAllIn(1, 4, 8)
162 | }
163 | assertThrows {
164 | assertThat(evensThru10).containsAllIn(8, 10, 4).inOrder()
165 | }
166 | assertThrows {
167 | assertThat(evensThru10).containsNoneIn(2, 5, 7, 8)
168 | }
169 | assertThrows {
170 | assertThat(evensThru10).containsExactly(0, 2, 4, 6, 8, 10)
171 | }
172 | }
173 | }
174 |
175 | @Test
176 | fun sequenceChecks() {
177 | // Sequences are treated as iterables, so just do a few quick sanity checks
178 | assertThat(sequenceOf("a", "b", "c")).containsExactly("a", "b", "c")
179 | assertThat(sequenceOf()).isEmpty()
180 | assertThat(sequenceOf(1, 2, 3)).isNotEmpty()
181 | assertThat(('a' .. 'z').asSequence()).hasSize(26)
182 | }
183 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/Truth.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish
2 |
3 | import com.varabyte.truthish.failure.*
4 | import com.varabyte.truthish.subjects.*
5 |
6 | fun assertThat(actual: Any?) = NullableSubject(actual)
7 | fun assertThat(actual: Any) = NotNullSubject(actual)
8 | fun > assertThat(actual: T) = ComparableSubject(actual)
9 | fun assertThat(actual: Boolean) = BooleanSubject(actual)
10 | fun assertThat(actual: Byte) = ByteSubject(actual)
11 | fun assertThat(actual: Short) = ShortSubject(actual)
12 | fun assertThat(actual: Int) = IntSubject(actual)
13 | fun assertThat(actual: Long) = LongSubject(actual)
14 | fun assertThat(actual: Float) = FloatSubject(actual)
15 | fun assertThat(actual: Double) = DoubleSubject(actual)
16 | fun assertThat(actual: String) = StringSubject(actual)
17 | fun > assertThat(actual: I) = IterableSubject(actual)
18 | fun > assertThat(actual: T) = MapSubject(actual)
19 | fun > assertThat(actual: S) = IterableSubject(actual.asIterable())
20 | fun assertThat(actual: Array) = ArraySubject(actual)
21 | fun assertThat(actual: BooleanArray) = BooleanArraySubject(actual)
22 | fun assertThat(actual: ByteArray) = ByteArraySubject(actual)
23 | fun assertThat(actual: CharArray) = CharArraySubject(actual)
24 | fun assertThat(actual: ShortArray) = ShortArraySubject(actual)
25 | fun assertThat(actual: IntArray) = IntArraySubject(actual)
26 | fun assertThat(actual: LongArray) = LongArraySubject(actual)
27 | fun assertThat(actual: FloatArray) = FloatArraySubject(actual)
28 | fun assertThat(actual: DoubleArray) = DoubleArraySubject(actual)
29 | // Adding a new [assertThat] here? Also add it to SummarizedSubjectBuilder and AssetAllScope
30 |
31 | /**
32 | * Create a block of grouped assertions.
33 | *
34 | * Within an `assertAll` block, all assertions are run, and if any fail, all failures are deferred to the end of the
35 | * block and reported all at once.
36 | *
37 | * For example, a test like:
38 | * ```
39 | * val person = personDatabase.query(id = 123)
40 | * assertAll {
41 | * that(person.name).isEqualTo("Alice")
42 | * that(person.age).isEqualTo(30)
43 | * that(person.id).isEqualTo(123)
44 | * }
45 | * ```
46 | *
47 | * could output something like:
48 | *
49 | * ```
50 | * Grouped assertions had 2 failure(s)
51 | *
52 | * Failure 1:
53 | * Two objects were not equal
54 | *
55 | * Expected: "Bob"
56 | * But was : "Alice"
57 | * At : "org.example.DatabaseTest$queryPerson$1.invoke(DatabaseTest.kt:47)"
58 | *
59 | * Failure 2:
60 | * Two objects were not equal
61 | *
62 | * Expected: 45
63 | * But was : 30
64 | * At : "org.example.DatabaseTest$queryPerson$1.invoke(DatabaseTest.kt:48)"
65 | * ```
66 | */
67 | fun assertAll(summary: String? = null, block: AssertAllScope.() -> Unit) {
68 | val assertAllScope = AssertAllScope(summary)
69 | assertAllScope.block()
70 | assertAllScope.deferredStrategy.handleNow()
71 | }
72 | class AssertAllScope(summary: String?) {
73 | internal val deferredStrategy = DeferredStrategy(summary)
74 |
75 | fun that(actual: Any?) = NullableSubject(actual).withStrategy(deferredStrategy)
76 | fun that(actual: Any) = NotNullSubject(actual).withStrategy(deferredStrategy)
77 | fun > that(actual: T) = ComparableSubject(actual).withStrategy(deferredStrategy)
78 | fun that(actual: Boolean) = BooleanSubject(actual).withStrategy(deferredStrategy)
79 | fun that(actual: Byte) = ByteSubject(actual).withStrategy(deferredStrategy)
80 | fun that(actual: Short) = ShortSubject(actual).withStrategy(deferredStrategy)
81 | fun that(actual: Int) = IntSubject(actual).withStrategy(deferredStrategy)
82 | fun that(actual: Long) = LongSubject(actual).withStrategy(deferredStrategy)
83 | fun that(actual: Float) = FloatSubject(actual).withStrategy(deferredStrategy)
84 | fun that(actual: Double) = DoubleSubject(actual).withStrategy(deferredStrategy)
85 | fun that(actual: String) = StringSubject(actual).withStrategy(deferredStrategy)
86 | fun > that(actual: I) = IterableSubject(actual).withStrategy(deferredStrategy)
87 | fun > that(actual: T) = MapSubject(actual).withStrategy(deferredStrategy)
88 | fun > that(actual: S) = IterableSubject(actual.asIterable()).withStrategy(deferredStrategy)
89 | fun that(actual: Array) = ArraySubject(actual).withStrategy(deferredStrategy)
90 | fun that(actual: BooleanArray) = BooleanArraySubject(actual).withStrategy(deferredStrategy)
91 | fun that(actual: ByteArray) = ByteArraySubject(actual).withStrategy(deferredStrategy)
92 | fun that(actual: CharArray) = CharArraySubject(actual).withStrategy(deferredStrategy)
93 | fun that(actual: ShortArray) = ShortArraySubject(actual).withStrategy(deferredStrategy)
94 | fun that(actual: IntArray) = IntArraySubject(actual).withStrategy(deferredStrategy)
95 | fun that(actual: LongArray) = LongArraySubject(actual).withStrategy(deferredStrategy)
96 | fun that(actual: FloatArray) = FloatArraySubject(actual).withStrategy(deferredStrategy)
97 | fun that(actual: DoubleArray) = DoubleArraySubject(actual).withStrategy(deferredStrategy)
98 |
99 | fun withMessage(message: String) = SummarizedSubjectBuilder(message, deferredStrategy)
100 | }
101 |
102 | /**
103 | * Create an assertion with a message that describes it, which can be useful for providing more context to a failure.
104 | *
105 | * For example:
106 | *
107 | * ```
108 | * assertWithMessage("IDs should match").that(item1.id).isEqualTo(item2.id)
109 | * ```
110 | *
111 | * could generate output like:
112 | *
113 | * ```
114 | * Two objects were not equal
115 | *
116 | * Message : IDs should match
117 | * Expected: 672528
118 | * But was : 187974
119 | * ```
120 | *
121 | * Without that message, you might just see a test failure that showed two random numbers not being the same, which
122 | * would require the developer to dig into the test to understand what was being compared.
123 | *
124 | * Note that the example above is functionally identical to:
125 | *
126 | * ```
127 | * assertThat(item1.id).withMessage("IDs should match").isEqualTo(item2.id)
128 | * ```
129 | *
130 | * but the `assertWithMessage` approach is provided as it reads a lot more naturally and doesn't obscure the check by
131 | * jamming a long message in the middle of the assertion. In fact, despite being syntax sugar, you should prefer
132 | * using `assertWithMessage` over `withMessage` for this reason.
133 | */
134 | fun assertWithMessage(message: String) = SummarizedSubjectBuilder(message)
135 | class SummarizedSubjectBuilder(private val message: String, private val strategyOverride: FailureStrategy? = null) {
136 | private inline fun R.withStrategyOverride() = if (strategyOverride != null) withStrategy(strategyOverride) else this
137 |
138 | fun that(actual: Any?) = NullableSubject(actual).withMessage(message).withStrategyOverride()
139 | fun that(actual: Any) = NotNullSubject(actual).withMessage(message).withStrategyOverride()
140 | fun > that(actual: T) = ComparableSubject(actual).withMessage(message).withStrategyOverride()
141 | fun that(actual: Boolean) = BooleanSubject(actual).withMessage(message).withStrategyOverride()
142 | fun that(actual: Byte) = ByteSubject(actual).withMessage(message).withStrategyOverride()
143 | fun that(actual: Short) = ShortSubject(actual).withMessage(message).withStrategyOverride()
144 | fun that(actual: Int) = IntSubject(actual).withMessage(message).withStrategyOverride()
145 | fun that(actual: Long) = LongSubject(actual).withMessage(message).withStrategyOverride()
146 | fun that(actual: Float) = FloatSubject(actual).withMessage(message).withStrategyOverride()
147 | fun that(actual: Double) = DoubleSubject(actual).withMessage(message).withStrategyOverride()
148 | fun that(actual: String) = StringSubject(actual).withMessage(message).withStrategyOverride()
149 | fun > that(actual: I) = IterableSubject(actual).withMessage(message).withStrategyOverride()
150 | fun > that(actual: T) = MapSubject(actual).withMessage(message).withStrategyOverride()
151 | fun > that(actual: S) = IterableSubject(actual.asIterable()).withMessage(message).withStrategyOverride()
152 | fun that(actual: Array) = ArraySubject(actual).withMessage(message).withStrategyOverride()
153 | fun that(actual: BooleanArray) = BooleanArraySubject(actual).withMessage(message).withStrategyOverride()
154 | fun that(actual: ByteArray) = ByteArraySubject(actual).withMessage(message).withStrategyOverride()
155 | fun that(actual: CharArray) = CharArraySubject(actual).withMessage(message).withStrategyOverride()
156 | fun that(actual: ShortArray) = ShortArraySubject(actual).withMessage(message).withStrategyOverride()
157 | fun that(actual: IntArray) = IntArraySubject(actual).withMessage(message).withStrategyOverride()
158 | fun that(actual: LongArray) = LongArraySubject(actual).withMessage(message).withStrategyOverride()
159 | fun that(actual: FloatArray) = FloatArraySubject(actual).withMessage(message).withStrategyOverride()
160 | fun that(actual: DoubleArray) = DoubleArraySubject(actual).withMessage(message).withStrategyOverride()
161 | }
162 |
163 | /**
164 | * Helpful utility function for verifying that a block throws an expected exception type.
165 | * This method also returns the exception, so further asserts can be made against it if desired.
166 | *
167 | * Unfortunately, there is no way to override the failure strategy for this assert, since
168 | * for usability we need to guarantee that we'll either return a valid exception or abort via a
169 | * different exception, so we throw [AssertionError] directly.
170 | *
171 | * @param message If set, include a custom message in the final assertion.
172 | */
173 | inline fun assertThrows(message: String? = null, block: () -> Unit): T {
174 | val report = try {
175 | block()
176 | Report(Summaries.EXPECTED_EXCEPTION, DetailsFor.expected(T::class).apply { if (message != null) add("Message" to message) })
177 | }
178 | catch (t: Throwable) {
179 | if (t !is T) {
180 | Report(Summaries.EXPECTED_EXCEPTION, DetailsFor.expectedActual(T::class, t::class).apply { if (message != null) add("Message" to message) })
181 | }
182 | else {
183 | return t
184 | }
185 | }
186 |
187 | throw AssertionError(report)
188 | }
189 |
--------------------------------------------------------------------------------
/src/commonMain/kotlin/com/varabyte/truthish/subjects/ArraySubjects.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish.subjects
2 |
3 | import com.varabyte.truthish.failure.DetailsFor
4 | import com.varabyte.truthish.failure.Report
5 | import com.varabyte.truthish.failure.Summaries
6 |
7 | open class ArraySubject(private val actualArray: Array) : IterableSubject(actualArray.toList()) {
8 | fun isEqualTo(expected: Array) {
9 | if (!actualArray.contentEquals(expected)) {
10 | report(
11 | Report(
12 | Summaries.EXPECTED_EQUAL,
13 | DetailsFor.expectedActual(expected.contentToString(), actualArray.contentToString())
14 | )
15 | )
16 | }
17 | }
18 |
19 | fun isNotEqualTo(expected: Array) {
20 | if (actualArray.contentEquals(expected)) {
21 | report(Report(Summaries.EXPECTED_NOT_EQUAL, DetailsFor.expected(actualArray.contentToString())))
22 | }
23 | }
24 | }
25 |
26 | open class BooleanArraySubject(private val actualArray: BooleanArray) :
27 | ArraySubject(actualArray.toTypedArray()) {
28 | private fun BooleanArray.toTypedArray(): Array = this.map { it }.toTypedArray()
29 |
30 | fun isEqualTo(expected: BooleanArray) {
31 | if (!actualArray.contentEquals(expected)) {
32 | report(
33 | Report(
34 | Summaries.EXPECTED_EQUAL,
35 | DetailsFor.expectedActual(expected.contentToString(), actualArray.contentToString())
36 | )
37 | )
38 | }
39 | }
40 |
41 | fun isNotEqualTo(expected: BooleanArray) {
42 | if (actualArray.contentEquals(expected)) {
43 | report(Report(Summaries.EXPECTED_NOT_EQUAL, DetailsFor.expected(actualArray.contentToString())))
44 | }
45 | }
46 |
47 | fun containsAnyIn(other: BooleanArray) = containsAnyIn(other.toTypedArray())
48 | fun containsAllIn(other: BooleanArray) = containsAllIn(other.toTypedArray())
49 | fun containsNoneIn(other: BooleanArray) = containsNoneIn(other.toTypedArray())
50 | fun containsExactly(other: BooleanArray) = containsExactly(other.toTypedArray())
51 | }
52 |
53 | open class ByteArraySubject(private val actualArray: ByteArray) : ArraySubject(actualArray.toTypedArray()) {
54 | private fun ByteArray.toTypedArray(): Array = this.map { it }.toTypedArray()
55 |
56 | fun isEqualTo(expected: ByteArray) {
57 | if (!actualArray.contentEquals(expected)) {
58 | report(
59 | Report(
60 | Summaries.EXPECTED_EQUAL,
61 | DetailsFor.expectedActual(expected.contentToString(), actualArray.contentToString())
62 | )
63 | )
64 | }
65 | }
66 |
67 | fun isNotEqualTo(expected: ByteArray) {
68 | if (actualArray.contentEquals(expected)) {
69 | report(Report(Summaries.EXPECTED_NOT_EQUAL, DetailsFor.expected(actualArray.contentToString())))
70 | }
71 | }
72 |
73 | fun containsAnyIn(other: ByteArray) = containsAnyIn(other.toTypedArray())
74 | fun containsAllIn(other: ByteArray) = containsAllIn(other.toTypedArray())
75 | fun containsNoneIn(other: ByteArray) = containsNoneIn(other.toTypedArray())
76 | fun containsExactly(other: ByteArray) = containsExactly(other.toTypedArray())
77 | }
78 |
79 | open class CharArraySubject(private val actualArray: CharArray) : ArraySubject(actualArray.toTypedArray()) {
80 | private fun CharArray.toTypedArray(): Array = this.map { it }.toTypedArray()
81 |
82 | fun isEqualTo(expected: CharArray) {
83 | if (!actualArray.contentEquals(expected)) {
84 | report(
85 | Report(
86 | Summaries.EXPECTED_EQUAL,
87 | DetailsFor.expectedActual(expected.contentToString(), actualArray.contentToString())
88 | )
89 | )
90 | }
91 | }
92 |
93 | fun isNotEqualTo(expected: CharArray) {
94 | if (actualArray.contentEquals(expected)) {
95 | report(Report(Summaries.EXPECTED_NOT_EQUAL, DetailsFor.expected(actualArray.contentToString())))
96 | }
97 | }
98 |
99 | fun containsAnyIn(other: CharArray) = containsAnyIn(other.toTypedArray())
100 | fun containsAllIn(other: CharArray) = containsAllIn(other.toTypedArray())
101 | fun containsNoneIn(other: CharArray) = containsNoneIn(other.toTypedArray())
102 | fun containsExactly(other: CharArray) = containsExactly(other.toTypedArray())
103 | }
104 |
105 | open class ShortArraySubject(private val actualArray: ShortArray) : ArraySubject(actualArray.toTypedArray()) {
106 | private fun ShortArray.toTypedArray(): Array = this.map { it }.toTypedArray()
107 |
108 | fun isEqualTo(expected: ShortArray) {
109 | if (!actualArray.contentEquals(expected)) {
110 | report(
111 | Report(
112 | Summaries.EXPECTED_EQUAL,
113 | DetailsFor.expectedActual(expected.contentToString(), actualArray.contentToString())
114 | )
115 | )
116 | }
117 | }
118 |
119 | fun isNotEqualTo(expected: ShortArray) {
120 | if (actualArray.contentEquals(expected)) {
121 | report(Report(Summaries.EXPECTED_NOT_EQUAL, DetailsFor.expected(actualArray.contentToString())))
122 | }
123 | }
124 |
125 | fun containsAnyIn(other: ShortArray) = containsAnyIn(other.toTypedArray())
126 | fun containsAllIn(other: ShortArray) = containsAllIn(other.toTypedArray())
127 | fun containsNoneIn(other: ShortArray) = containsNoneIn(other.toTypedArray())
128 | fun containsExactly(other: ShortArray) = containsExactly(other.toTypedArray())
129 | }
130 |
131 | open class IntArraySubject(private val actualArray: IntArray) : ArraySubject(actualArray.toTypedArray()) {
132 | private fun IntArray.toTypedArray(): Array = this.map { it }.toTypedArray()
133 |
134 | fun isEqualTo(expected: IntArray) {
135 | if (!actualArray.contentEquals(expected)) {
136 | report(
137 | Report(
138 | Summaries.EXPECTED_EQUAL,
139 | DetailsFor.expectedActual(expected.contentToString(), actualArray.contentToString())
140 | )
141 | )
142 | }
143 | }
144 |
145 | fun isNotEqualTo(expected: IntArray) {
146 | if (actualArray.contentEquals(expected)) {
147 | report(Report(Summaries.EXPECTED_NOT_EQUAL, DetailsFor.expected(actualArray.contentToString())))
148 | }
149 | }
150 |
151 | fun containsAnyIn(other: IntArray) = containsAnyIn(other.toTypedArray())
152 | fun containsAllIn(other: IntArray) = containsAllIn(other.toTypedArray())
153 | fun containsNoneIn(other: IntArray) = containsNoneIn(other.toTypedArray())
154 | fun containsExactly(other: IntArray) = containsExactly(other.toTypedArray())
155 | }
156 |
157 | open class LongArraySubject(private val actualArray: LongArray) : ArraySubject(actualArray.toTypedArray()) {
158 | private fun LongArray.toTypedArray(): Array = this.map { it }.toTypedArray()
159 |
160 | fun isEqualTo(expected: LongArray) {
161 | if (!actualArray.contentEquals(expected)) {
162 | report(
163 | Report(
164 | Summaries.EXPECTED_EQUAL,
165 | DetailsFor.expectedActual(expected.contentToString(), actualArray.contentToString())
166 | )
167 | )
168 | }
169 | }
170 |
171 | fun isNotEqualTo(expected: LongArray) {
172 | if (actualArray.contentEquals(expected)) {
173 | report(Report(Summaries.EXPECTED_NOT_EQUAL, DetailsFor.expected(actualArray.contentToString())))
174 | }
175 | }
176 |
177 | fun containsAnyIn(other: LongArray) = containsAnyIn(other.toTypedArray())
178 | fun containsAllIn(other: LongArray) = containsAllIn(other.toTypedArray())
179 | fun containsNoneIn(other: LongArray) = containsNoneIn(other.toTypedArray())
180 | fun containsExactly(other: LongArray) = containsExactly(other.toTypedArray())
181 | }
182 |
183 | open class FloatArraySubject(private val actualArray: FloatArray) : ArraySubject(actualArray.toTypedArray()) {
184 | private fun FloatArray.toTypedArray(): Array = this.map { it }.toTypedArray()
185 |
186 | fun isEqualTo(expected: FloatArray) {
187 | if (!actualArray.contentEquals(expected)) {
188 | report(
189 | Report(
190 | Summaries.EXPECTED_EQUAL,
191 | DetailsFor.expectedActual(expected.contentToString(), actualArray.contentToString())
192 | )
193 | )
194 | }
195 | }
196 |
197 | fun isNotEqualTo(expected: FloatArray) {
198 | if (actualArray.contentEquals(expected)) {
199 | report(Report(Summaries.EXPECTED_NOT_EQUAL, DetailsFor.expected(actualArray.contentToString())))
200 | }
201 | }
202 |
203 | fun containsAnyIn(other: FloatArray) = containsAnyIn(other.toTypedArray())
204 | fun containsAllIn(other: FloatArray) = containsAllIn(other.toTypedArray())
205 | fun containsNoneIn(other: FloatArray) = containsNoneIn(other.toTypedArray())
206 | fun containsExactly(other: FloatArray) = containsExactly(other.toTypedArray())
207 | }
208 |
209 | open class DoubleArraySubject(private val actualArray: DoubleArray) : ArraySubject(actualArray.toTypedArray()) {
210 | private fun DoubleArray.toTypedArray(): Array = this.map { it }.toTypedArray()
211 |
212 | fun isEqualTo(expected: DoubleArray) {
213 | if (!actualArray.contentEquals(expected)) {
214 | report(
215 | Report(
216 | Summaries.EXPECTED_EQUAL,
217 | DetailsFor.expectedActual(expected.contentToString(), actualArray.contentToString())
218 | )
219 | )
220 | }
221 | }
222 |
223 | fun isNotEqualTo(expected: DoubleArray) {
224 | if (actualArray.contentEquals(expected)) {
225 | report(Report(Summaries.EXPECTED_NOT_EQUAL, DetailsFor.expected(actualArray.contentToString())))
226 | }
227 | }
228 |
229 | fun containsAnyIn(other: DoubleArray) = containsAnyIn(other.toTypedArray())
230 | fun containsAllIn(other: DoubleArray) = containsAllIn(other.toTypedArray())
231 | fun containsNoneIn(other: DoubleArray) = containsNoneIn(other.toTypedArray())
232 | fun containsExactly(other: DoubleArray) = containsExactly(other.toTypedArray())
233 | }
234 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2021 Varabyte
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/commonTest/kotlin/com/varabyte/truthish/ArrayAsserts.kt:
--------------------------------------------------------------------------------
1 | package com.varabyte.truthish
2 |
3 | import com.varabyte.truthish.failure.ReportError
4 | import com.varabyte.truthish.failure.Summaries
5 | import com.varabyte.truthish.failure.assertSubstrings
6 | import com.varabyte.truthish.subjects.containsAllIn
7 | import com.varabyte.truthish.subjects.containsAnyIn
8 | import com.varabyte.truthish.subjects.containsExactly
9 | import kotlin.test.Test
10 |
11 | class ArrayAsserts {
12 | private val emptyBools
13 | get() = BooleanArray(0)
14 | private val emptyBytes
15 | get() = ByteArray(0)
16 | private val emptyChars
17 | get() = CharArray(0)
18 | private val emptyShorts
19 | get() = ShortArray(0)
20 | private val emptyInts
21 | get() = IntArray(0)
22 | private val emptyLongs
23 | get() = LongArray(0)
24 | private val emptyFloats
25 | get() = FloatArray(0)
26 | private val emptyDoubles
27 | get() = DoubleArray(0)
28 |
29 | private val emptyStrs
30 | get() = arrayOf()
31 |
32 | @Test
33 | fun generalArrayChecks() {
34 | run {
35 | // Test true statements
36 | assertThat(arrayOf("a", "b", "c")).isEqualTo(arrayOf("a", "b", "c"))
37 | assertThat(arrayOf("a", "b", "c")).isNotEqualTo(arrayOf("a", "b", "d"))
38 |
39 | assertThat(emptyInts).isEmpty()
40 | assertThat(arrayOf(1, 2, 3)).isNotEmpty()
41 | assertThat(arrayOf(1, 2, 3)).hasSize(3)
42 |
43 | assertThat(arrayOf("a", "b", "c")).contains("b")
44 | assertThat(arrayOf("a", "b", "c")).doesNotContain("d")
45 | assertThat(emptyStrs).doesNotContain("d")
46 |
47 | assertThat(arrayOf("a", "b", "c")).hasNoDuplicates()
48 | assertThat(emptyStrs).hasNoDuplicates()
49 |
50 | assertThat(arrayOf("a", "b", "c")).containsAnyIn("c", "d", "e")
51 | assertThat(arrayOf("a", "b", "c")).containsAnyIn(emptyStrs)
52 | assertThat(emptyStrs).containsAnyIn(emptyStrs)
53 |
54 | assertThat(arrayOf("a", "b", "c")).containsAllIn("c", "b")
55 | assertThat(arrayOf("a", "b", "c")).containsAllIn("b", "c").inOrder()
56 | assertThat(arrayOf("a", "b", "c")).containsAllIn(emptyStrs)
57 |
58 | assertThat(arrayOf("a", "b", "c")).containsNoneIn("d", "e", "f")
59 | assertThat(emptyStrs).containsNoneIn("d", "e", "f")
60 |
61 | assertThat(arrayOf("a", "b", "c")).containsExactly("b", "c", "a")
62 | assertThat(arrayOf("a", "b", "c")).containsExactly("a", "b", "c").inOrder()
63 | assertThat(emptyStrs).containsExactly(emptyStrs)
64 | assertThat(emptyStrs).containsExactly(emptyStrs).inOrder()
65 | }
66 |
67 | run {
68 | // Test false statements
69 |
70 | assertThrows {
71 | assertThat(arrayOf("a", "b", "c")).isEqualTo(arrayOf("a", "b", "d"))
72 | }.assertSubstrings(Summaries.EXPECTED_EQUAL, "[a, b, d]")
73 | assertThrows {
74 | assertThat(arrayOf("a", "b", "c")).isNotEqualTo(arrayOf("a", "b", "c"))
75 | }.assertSubstrings(Summaries.EXPECTED_NOT_EQUAL, "[a, b, c]")
76 |
77 |
78 | assertThrows {
79 | assertThat(arrayOf(1, 2, 3)).isEmpty()
80 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_EMPTY)
81 | assertThrows {
82 | assertThat(emptyInts).isNotEmpty()
83 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_NOT_EMPTY)
84 | assertThrows {
85 | assertThat(arrayOf(1, 2, 3)).hasSize(2)
86 | }.assertSubstrings(Summaries.EXPECTED_COMPARISON)
87 |
88 | assertThrows {
89 | assertThat(arrayOf("a", "b", "c")).contains("d")
90 | }.assertSubstrings(Summaries.EXPECTED_COLLECTION_CONTAINS)
91 | assertThrows