From 006c5d6a27b7c93abcb068ed920a94279c8445e4 Mon Sep 17 00:00:00 2001 From: Nathanael Silverman Date: Wed, 18 Nov 2020 13:07:45 +0100 Subject: [PATCH] Prepare for release 0.3.0 --- .github/workflows/ci.yml | 23 + .gitignore | 4 + CHANGELOG.md | 24 + CODE_OF_CONDUCT.md | 133 ++++++ CONTRIBUTING.md | 31 ++ LICENSE | 21 + README.md | 225 +++++++++ RELEASING.md | 20 + build.gradle.kts | 144 ++++++ gradle.properties | 4 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++++++ gradlew.bat | 84 ++++ settings.gradle.kts | 1 + src/main/java/dev/specto/belay/Expect.kt | 269 +++++++++++ .../dev/specto/belay/ExpectationException.kt | 46 ++ .../dev/specto/belay/ExpectationHandlers.kt | 193 ++++++++ .../dev/specto/belay/ExpectationReceivers.kt | 219 +++++++++ src/test/java/dev/specto/belay/Assertions.kt | 48 ++ src/test/java/dev/specto/belay/BaseTest.kt | 13 + .../belay/ContinueExpectationHandlerTest.kt | 49 ++ .../belay/ContinueExpectationReceiverTest.kt | 96 ++++ .../belay/ExitExpectationHandlerTest.kt | 46 ++ .../belay/ExitExpectationReceiverTest.kt | 163 +++++++ src/test/java/dev/specto/belay/ExpectTest.kt | 446 ++++++++++++++++++ .../java/dev/specto/belay/ReturnLastTest.kt | 40 ++ .../belay/TestExpectationHandlerProvider.kt | 78 +++ src/test/java/dev/specto/belay/ThrowTest.kt | 39 ++ 29 files changed, 2637 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 RELEASING.md create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts create mode 100644 src/main/java/dev/specto/belay/Expect.kt create mode 100644 src/main/java/dev/specto/belay/ExpectationException.kt create mode 100644 src/main/java/dev/specto/belay/ExpectationHandlers.kt create mode 100644 src/main/java/dev/specto/belay/ExpectationReceivers.kt create mode 100644 src/test/java/dev/specto/belay/Assertions.kt create mode 100644 src/test/java/dev/specto/belay/BaseTest.kt create mode 100644 src/test/java/dev/specto/belay/ContinueExpectationHandlerTest.kt create mode 100644 src/test/java/dev/specto/belay/ContinueExpectationReceiverTest.kt create mode 100644 src/test/java/dev/specto/belay/ExitExpectationHandlerTest.kt create mode 100644 src/test/java/dev/specto/belay/ExitExpectationReceiverTest.kt create mode 100644 src/test/java/dev/specto/belay/ExpectTest.kt create mode 100644 src/test/java/dev/specto/belay/ReturnLastTest.kt create mode 100644 src/test/java/dev/specto/belay/TestExpectationHandlerProvider.kt create mode 100644 src/test/java/dev/specto/belay/ThrowTest.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7deefe4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + check: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Run check + run: ./gradlew check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a60610 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.gradle +/.idea +/build +local.properties diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2430a83 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Belay Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +- Nothing yet! + +## 0.3.0 - 2020-11-20 + +### Added + +- Global expectations. +- Locally-handled expectations. +- Expectation blocks. +- Built-in expectation handlers: + - `Continue` + - `Return` + - `ReturnLast` + - `Throw` +- Custom expectation handlers. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4b29303 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[nathanael@specto.dev](mailto:nathanael@specto.dev). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8b7fb06 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# How to Contribute to Belay + +Thank you for considering making a contribution! :partying_face: + +## Testing + +Belay has an extensive suite of tests. It uses *ktlint* to format code and *detekt* for static code analysis. + +Run all checks: + +``` +./gradlew check +``` + +Please make sure that all checks are passing before proposing changes, and add or update tests whenever possible. + +## Reporting bugs / making requests / asking questions + +* Make sure your topic is not already covered by searching on GitHub under [Issues](https://github.com/specto-dev/belay/issues). + +* If there is no existing issue on this topic, [open a new one](https://github.com/specto-dev/belay/issues/new). Be sure to include a title and clear description with as much relevant information as possible. + +* For bugs, include a code sample or an executable test case demonstrating the expected behavior that is not occurring. + +## Submitting changes + +* Make sure all checks are passing by running `./gradlew check`. + +* If helpful, include tests and code documentation for your changes. + +* Open a GitHub pull request with the patch. Be sure to include a title and clear description with as much relevant information as possible. Include the relevant issue number if applicable. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c793501 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Specto Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..01e1950 --- /dev/null +++ b/README.md @@ -0,0 +1,225 @@ +# Belay — Robust Error-Handling for Kotlin and Android + +[![CI](https://img.shields.io/github/workflow/status/specto-dev/belay/CI/main)](https://github.com/specto-dev/belay/actions?query=workflow%3ACI) [![Maven Central](https://img.shields.io/maven-central/v/dev.specto/belay)](https://search.maven.org/artifact/dev.specto/belay) ![code size](https://img.shields.io/github/languages/code-size/specto-dev/belay) ![Kind Speech](https://api.kindspeech.org/v1/badge?color=4e65d2) + +*Code Complete: A Practical Handbook of Software Construction*, on error-handling techniques: + +> **Consumer applications tend to favor robustness to correctness. Any result whatsoever is usually better than the software shutting down.** The word processor I'm using occasionally displays a fraction of a line of text at the bottom of the screen. If it detects that condition, do I want the word processor to shut down? No. I know that the next time I hit Page Up or Page Down, the screen will refresh and the display will be back to normal. + +Belay is a Kotlin error-handling library which favors robustness. It serves two purposes: + +- Detect errors early during development using assertions. +- Gracefully recover from errors when they occur in production. + +## Installation + +In your project's `build.gradle`: + +```kotlin +dependencies { + implementation("dev.specto:belay:0.3.0") +} +``` + +Declare a top level variable to run expectations: + +```kotlin +val expect = Expect() +``` + +It can be named anything, but for the purposes of this documentation we'll assume it's named "expect". + +Next, set the top level expectation handler as early as possible during your program's initialization: + +```kotlin +fun main() { + expect.onGlobalFail = object : GlobalExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + if (DEBUG) throw exception + else log(exception.stackTraceToString()) + } + } + + // … +} +``` + +The global handler will be invoked when any expectations fail. Of course it's possible, and often desirable, to handle expectations individually or by subgroups, but the global handler is the ideal place to throw exceptions during development—effectively turning expectations into assertions—so that failures can be noticed and addressed immediately. In production it can be used to log all errors, regardless of how they are ultimately handled, so that they can be fixed at a later date. + +:warning: Do not call `expect` from the global handler, it could cause an infinite loop if that expectation fails. + +## Writing Expectations + +The `expect` variable provides a host of utilities to write expectations and specify how they should be handled. They fall in 3 main categories. + +### Global Expectations + +Global expectations only invoke the global handler when they fail. + +```kotlin +expect(condition, optionalErrorMessage) // shorthand for expect.isTrue(…) +expect.fail("error message", optionalCause) +expect.isTrue(condition, optionalErrorMessage) +expect.isFalse(condition, optionalErrorMessage) +expect.isNotNull(value, optionalErrorMessage) +expect.isNull(value, optionalErrorMessage) +expect.isType(value, optionalErrorMessage) +``` + +Unless the global handler interrupts the program, it will proceed even when these expectations fail. Therefore, they are meant to be used when the program can proceed even when the expectations fail. For example: + +```kotlin +fun cleanup() { + expect.isNotNull(configuration) + configuration = null +} +``` + +### Locally-Handled Expectations + +Locally-handled expectations invoke the global handler when they fail, and then a locally-defined error-handling function. + +```kotlin +expect(condition, optionalErrorMessage) { + // Handle the expectation failing. + // Does not need to return or throw an exception. +} + +expect.isTrue(condition, optionalErrorMessage) { + // Handle the expectation failing. + // Must return or throw an exception which enables smart casting. + return +} +expect.isFalse(condition, optionalErrorMessage) { … } +val nonNullValue = expect.isNotNull(value, optionalErrorMessage) { … } +expect.isNull(value, optionalErrorMessage) { … } +val valueCastToType = expect.isType(value, optionalErrorMessage) { … } +``` + +A custom error message can be provided to all these functions. + +This is great for one-off error-handling: + +```kotlin +fun startTime(): Long { + // … + + expect(startTime >= 0, "startTime was negative") { + startTime = 0 + } + + return startTime +} + +fun animate() { + expect.isNotNull(animator) { return } + + // … + animator.animate(…) +} +``` + +### Expectation Blocks + +Often the same error-handling strategy can be used across individual functions or blocks of code. Expectation blocks make this easy. + +```kotlin +expect(blockHandler) { + fail("error message", optionalCause) + isTrue(condition, optionalErrorMessage) + isFalse(condition, optionalErrorMessage) + isNotNull(value, optionalErrorMessage) + isNull(value, optionalErrorMessage) + isType(value, optionalErrorMessage) +} +``` + +A custom error message can be provided to all these functions. + +Several block handlers are offered out of the box. + +`Continue` does nothing when an expectation fails (besides invoking the global handler): + +```kotlin +fun stop() = expect(onFail = Continue) { + isTrue(isActive, "time machine was not active when stop was called") + isNotNull(configuration) + isNotNull(controller) + + isActive = false + configuration = null + controller = null + // … +} +``` + +`Return` immediately returns a default value when an expectation fails: + +```kotlin +fun startTime(): Long = expect(Return(0)) { + // … +} +``` + +`ReturnLast` returns the last value returned or a default if no value has been returned yet: + +```kotlin +fun pixelColor(x: Int, y: Int): Color = expect(ReturnLast(Color.BLACK)) { + // … +} +``` + +`Throw` throws an exception when an expectation fails: + +```kotlin +fun startRadiationTreatment() = expect(Throw) { + // … +} +``` + +All the provided block handlers allow an arbitrary function to be executed when an expectation fails: + +```kotlin +expect(Return { disableController() }) { + // … +} +``` + +Block handlers which interrupt the program, like `Return`, `ReturnLast` and `Throw`, can also treat *exceptions* as failed expectations: + +```kotlin +fun startTime(): Long = expect(Return(0), catchExceptions = true) { + // All exceptions thrown by this function will be automatically caught + // and handled by the expectation handler as a failed expectation. +} +``` + +It's also possible, and easy, to write your own expectation handler. + +## Writing Expectation Handlers + +Writing custom expectation handlers is particularly useful when the same custom logic needs to be reused across a program. There are two types of expectation handlers: those that may interrupt the program when an expectation fails, and those that definitely do. + +Handlers who may interrupt the program when an expectation fails, like `Continue`, must extend `ContinueExpectationHandler`. Handlers that definitely interrupt the program, for example by returning early or throwing an exception, like `Return`, `ReturnLast` or `Throw`, should extend `ExitExpectationHandler`. This distinction serves to enable smart casting for `ExitExpectationHandler` expectations. + +You've actually already implemented a handler which uses the same interface as `ContinueExpectationHandler`, the global handler. The `ExitExpectationHandler` interface is very similar, here's an example implementation: + +```kotlin +class DisableControllerAndReturn( + returnValue: T, + also: ((exception: ExpectationException) -> Unit)? = null +) : ExitExpectationHandler() { + + private val controller: Controller by dependencyGraph + + override fun handleFail(exception: ExpectationException): Nothing { + controller.disable(exception.message) + also?.invoke(exception) + returnFromBlock(returnValue) + } +} +``` + +## Contributing + +We love contributions! Check out our [contributing guidelines](CONTRIBUTING.md) and be sure to follow our [code of conduct](CODE_OF_CONDUCT.md). diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..72d3fdb --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,20 @@ +# Prerequisites + +- The [Gradle signing plugin](https://docs.gradle.org/current/userguide/signing_plugin.html) must be configured. +- The following Gradle properties must be set: + - `specto.sonatype.user` + - `specto.sonatype.password` + +# Releasing a new version + +1. Update the version in `build.gradle` +2. Update the version in `README.md` +3. Update the `CHANGELOG.md` +4. `git commit -am "Prepare for release X.Y.Z"` (where X.Y.Z is the version set in step 1) +5. `git push` +6. Create a new release on GitHub + 1. Tag version `vX.Y.Z` + 2. Release title `vX.Y.Z` + 3. Paste the content from `CHANGELOG.md` as the description +7. `./gradlew publishBelayPublicationToSonatypeStagingRepository` +8. Visit [Sonatype Nexus Repository Manager](https://oss.sonatype.org/) and promote the artifact diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..17f7a36 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,144 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +buildscript { + repositories { + jcenter() + } +} + +plugins { + kotlin("jvm") version "1.3.72" + + // Checks + id("org.jlleitschuh.gradle.ktlint") version "9.4.1" + id("io.gitlab.arturbosch.detekt") version "1.14.1" + + // Code coverage + jacoco + + // Publishing + id("org.jetbrains.dokka") version "1.4.10.2" + id("maven-publish") + id("signing") +} + +group = "dev.specto" +version = "0.3.0" + +repositories { + jcenter() +} + +kotlin { + sourceSets { + all { + languageSettings.apply { + useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts") + } + } + } +} + +tasks.withType().configureEach { + kotlinOptions { + allWarningsAsErrors = true + } +} + +ktlint { + version.set("0.36.0") +} + +detekt { + reports { + html.enabled = true + txt.enabled = true + } +} + +tasks.test { + finalizedBy(tasks.jacocoTestReport) +} +tasks.jacocoTestReport { + dependsOn(tasks.test) +} + +configurations.all { + resolutionStrategy { + failOnVersionConflict() + preferProjectModules() + } +} + +dependencies { + implementation(kotlin("stdlib")) + testImplementation(kotlin("test-junit")) +} + +val dokkaJavadocJar by tasks.registering(Jar::class) { + archiveClassifier.set("javadoc") + from("$buildDir/dokka/javadoc") + dependsOn(tasks.getByName("dokkaJavadoc")) +} + +val sourcesJar by tasks.registering(Jar::class) { + archiveClassifier.set("sources") + from(sourceSets.main.get().allSource) +} + +publishing { + repositories { + maven { + name = "SonatypeSnapshots" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + credentials { + username = findProperty("specto.sonatype.user") as String? + password = findProperty("specto.sonatype.password") as String? + } + } + maven { + name = "SonatypeStaging" + url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2") + credentials { + username = findProperty("specto.sonatype.user") as String? + password = findProperty("specto.sonatype.password") as String? + } + } + } + + publications { + create("belay") { + from(components["java"]) + + artifact(dokkaJavadocJar) + artifact(sourcesJar) + + pom { + name.set("Belay") + description.set("Robust error-handling for Kotlin and Android") + url.set("https://github.com/specto-dev/belay") + licenses { + license { + name.set("MIT") + distribution.set("repo") + url.set("https://opensource.org/licenses/MIT") + } + } + scm { + url.set("https://github.com/specto-dev/belay") + } + developers { + developer { + id.set("nathanael") + name.set("Nathanael Silverman") + email.set("nathanael@specto.dev") + } + } + } + } + } +} + +signing { + sign(publishing.publications) +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..6c3eb40 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx4096m +org.gradle.parallel=true + +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

<5G2M1WZP5 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 F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c815603 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Aug 19 11:39:33 PDT 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..4c80e1f --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "belay" diff --git a/src/main/java/dev/specto/belay/Expect.kt b/src/main/java/dev/specto/belay/Expect.kt new file mode 100644 index 0000000..92840c0 --- /dev/null +++ b/src/main/java/dev/specto/belay/Expect.kt @@ -0,0 +1,269 @@ +package dev.specto.belay + +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_CONDITION_FALSE_BUT_TRUE +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_NON_NULL_BUT_NULL +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_NULL_BUT_NON_NULL +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_OF_TYPE +import kotlin.contracts.InvocationKind.AT_MOST_ONCE +import kotlin.contracts.contract + +/** + * The entry point for all expectation calls. + * + * The suggested usage is to create a top-level, internal variable, for example named "expect", and + * use it throughout the project: + * + * ``` + * val expect = Expect() + * + * fun initApplication() { + * expect.onGlobalFail = object : GlobalExpectationHandler { + * override fun handleFail(exception: ExpectationException) { + * if (DEBUG) throw exception + * else log(exception) + * } + * } + * } + * + * fun eat(banana: Banana) { + * expect(banana.isRipe) + * expect.isNotNull(banana.plant) + * … + * } + * ``` + * + * @param onGlobalFail the default global expectation handler. + */ +public class Expect( + onGlobalFail: GlobalExpectationHandler = Continue +) : GlobalExpectationReceiver(onGlobalFail) { + + /** + * Invoked whenever a global expectation fails. + * + * This is the ideal place to decide whether expectations should be treated like assertions and + * fail immediately or not, for example: + * + * ``` + * expect.onGlobalFail = object : GlobalExpectationHandler { + * override fun handleFail(exception: ExpectationException) { + * if (DEBUG) throw exception + * else log(exception) + * } + * } + * ``` + * + * Called from the same thread as the expectation check. + */ + public var onGlobalFail: GlobalExpectationHandler = onGlobalFail + set(value) { + handler = value + field = value + } + + /** + * If [condition] is false, sends a [FailedExpectationException] to [onGlobalFail], otherwise + * does nothing. + * + * @param condition the condition to check. + * @param message the error message. + */ + public operator fun invoke( + condition: Boolean, + message: String = MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE + ) { + isTrue(condition, message) + } + + /** + * If [condition] is false, sends a [FailedExpectationException] to [onGlobalFail] and then + * calls [onFail], otherwise does nothing. + * + * @param condition the condition to check. + * @param message the error message. + * @param onFail the function to call in place if condition is false. + */ + public inline operator fun invoke( + condition: Boolean, + message: String = MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE, + onFail: () -> Unit + ) { + if (!condition) { + this.onGlobalFail.handleFail(FailedExpectationException(message)) + onFail() + } + } + + /** + * Runs [block] in place. If any of its expectations fail, sends a [FailedExpectationException] + * to [onGlobalFail] and then to [onFail], otherwise returns the value from [block]. + * + * @param onFail the expectation handler. + * @param block the block of code to call in place. + */ + public inline operator fun invoke( + onFail: ContinueExpectationHandler, + block: ContinueExpectationReceiver.() -> T + ): T { + val handler = object : ContinueExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + this@Expect.onGlobalFail.handleFail(exception) + onFail.handleFail(exception) + } + } + return handler.runInternal(block) + } + + /** + * Runs [block] in place. If any of its expectations fail, sends a [FailedExpectationException] + * to [onGlobalFail] and then to [onFail], otherwise returns the value from [block]. + * + * When an expectation fails it is guaranteed to exit from [block], either through an early + * return or a thrown exception. This enables the expectation calls to smart cast. + * + * @param onFail the expectation handler. + * @param catchExceptions if true, all exceptions thrown by [block] will be caught and treated + * as expectation failures. + * @param block the block of code to call in place. + */ + public inline operator fun invoke( + onFail: ExitExpectationHandler, + catchExceptions: Boolean = false, + block: ExitExpectationReceiver.() -> T + ): T { + val handler = object : ExitExpectationHandler() { + override fun handleFail(exception: ExpectationException): Nothing { + this@Expect.onGlobalFail.handleFail(exception) + onFail.handleFail(exception) + } + } + return handler.runInternal(catchExceptions, block) + } +} + +/** + * If [condition] is false, sends a [FailedExpectationException] to [Expect.onGlobalFail] and then + * calls [onFail], otherwise does nothing. + * + * If [condition] is false this function is guaranteed to exit early, either through an early return + * or a thrown exception. This enables smart casting. + * + * @param condition the condition to check. + * @param message the error message. + * @param onFail the function to call in place if condition is false. + */ +public inline fun Expect.isTrue( + condition: Boolean, + message: String = MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE, + onFail: () -> Nothing +) { + contract { + callsInPlace(onFail, AT_MOST_ONCE) + returns() implies condition + } + + if (!condition) { + this.onGlobalFail.handleFail(FailedExpectationException(message)) + onFail() + } +} + +/** + * If [condition] is true, sends a [FailedExpectationException] to [Expect.onGlobalFail] and then + * calls [onFail], otherwise does nothing. + * + * If [condition] is true this function is guaranteed to exit early, either through an early return + * or a thrown exception. This enables smart casting. + * + * @param condition the condition to check. + * @param message the error message. + * @param onFail the function to call in place if condition is false. + */ +public inline fun Expect.isFalse( + condition: Boolean, + message: String = MESSAGE_EXPECTED_CONDITION_FALSE_BUT_TRUE, + onFail: () -> Nothing +) { + contract { + callsInPlace(onFail, AT_MOST_ONCE) + returns() implies !condition + } + + isTrue(!condition, message, onFail) +} + +/** + * If [value] is null, sends a [FailedExpectationException] to [Expect.onGlobalFail] and then calls + * [onFail], otherwise returns [value] cast to its non-null type. + * + * If [value] is null this function is guaranteed to exit early, either through an early return or + * a thrown exception. As a result, [value] is smart cast to its non-null type. + * + * @param value the value to check. + * @param message the error message. + * @param onFail the function to call in place if value is null. + */ +public inline fun Expect.isNotNull( + value: T?, + message: String = MESSAGE_EXPECTED_VALUE_NON_NULL_BUT_NULL, + onFail: () -> Nothing +): T { + contract { + callsInPlace(onFail, AT_MOST_ONCE) + returns() implies (value != null) + } + + isTrue(value != null, message, onFail) + + return value +} + +/** + * If [value] is not null, sends a [FailedExpectationException] to [Expect.onGlobalFail] and then + * calls [onFail], otherwise does nothing. + * + * If [value] is not null this function is guaranteed to exit early, either through an early return + * or a thrown exception. + * + * @param value the value to check. + * @param message the error message. + * @param onFail the function to call in place if value is not null. + */ +public inline fun Expect.isNull( + value: Any?, + message: String = MESSAGE_EXPECTED_VALUE_NULL_BUT_NON_NULL, + onFail: () -> Nothing +) { + contract { + callsInPlace(onFail, AT_MOST_ONCE) + returns() implies (value == null) + } + + isTrue(value == null, message, onFail) +} + +/** + * If [value] is not of type [T], sends a [FailedExpectationException] to [Expect.onGlobalFail] and + * then calls [onFail], otherwise returns [value] cast to [T]. + * + * If [value] is not of type [T] this function is guaranteed to exit early, either through an early + * return or a thrown exception. + * + * @param value the value to check. + * @param message the error message. + * @param onFail the function to call in place if value is not null. + */ +public inline fun Expect.isType( + value: Any?, + message: String = MESSAGE_EXPECTED_VALUE_OF_TYPE, + onFail: () -> Nothing +): T { + contract { + callsInPlace(onFail, AT_MOST_ONCE) + } + + isTrue(value is T, message, onFail) + + return value +} diff --git a/src/main/java/dev/specto/belay/ExpectationException.kt b/src/main/java/dev/specto/belay/ExpectationException.kt new file mode 100644 index 0000000..bfaf729 --- /dev/null +++ b/src/main/java/dev/specto/belay/ExpectationException.kt @@ -0,0 +1,46 @@ +package dev.specto.belay + +/** + * The base class for expectation exceptions. + */ +public abstract class ExpectationException( + message: String, + cause: Throwable? = null +) : Exception(message, cause) { + + override val message: String = super.message!! +} + +/** + * Indicates that an expectation call (not an automatically caught exception) failed. + */ +public class FailedExpectationException( + message: String, + cause: Throwable? = null +) : ExpectationException(message, cause) { + public companion object { + public const val MESSAGE_EXPECTATION_FAILED: String = "An expectation failed." + public const val MESSAGE_EXPECTED_CONDITION_FALSE_BUT_TRUE: String = + "Expected condition to be false but was true." + public const val MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE: String = + "Expected condition to be true but was false." + public const val MESSAGE_EXPECTED_VALUE_NON_NULL_BUT_NULL: String = + "Expected value to be non-null but was null." + public const val MESSAGE_EXPECTED_VALUE_NULL_BUT_NON_NULL: String = + "Expected value to be null but was non-null." + public const val MESSAGE_EXPECTED_VALUE_OF_TYPE: String = + "Expected value to be of a different type than is was." + } +} + +/** + * Indicates that an exception was caught and treated as an expectation failure. + */ +public class CaughtExpectationException( + message: String, + cause: Throwable? = null +) : ExpectationException(message, cause) { + public companion object { + public const val MESSAGE_EXCEPTION_OCCURRED: String = "An exception occurred." + } +} diff --git a/src/main/java/dev/specto/belay/ExpectationHandlers.kt b/src/main/java/dev/specto/belay/ExpectationHandlers.kt new file mode 100644 index 0000000..6981bc5 --- /dev/null +++ b/src/main/java/dev/specto/belay/ExpectationHandlers.kt @@ -0,0 +1,193 @@ +package dev.specto.belay + +import dev.specto.belay.CaughtExpectationException.Companion.MESSAGE_EXCEPTION_OCCURRED + +public interface ExpectationHandler { + public fun handleFail(exception: ExpectationException): T +} + +/** + * Unlike [ExitExpectationHandler], this base handler class is meant for handlers that do not + * necessarily interrupt execution when an expectation fails. + */ +public abstract class ContinueExpectationHandler : ExpectationHandler { + internal inline fun runInternal(block: ContinueExpectationReceiver.() -> T): T { + return ContinueExpectationReceiver(this).block() + } +} + +public typealias GlobalExpectationHandler = ContinueExpectationHandler + +/** Experimental. */ +public open class BaseContinue( + private val also: ((exception: ExpectationException) -> Unit)? = null +) : ContinueExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + also?.invoke(exception) + } +} + +/** + * Continues execution even when expectations fail. + */ +public object Continue : BaseContinue() { + + /** + * Immediately executes [also] and then continues execution when an expectation fails. + * + * @param also the block of code to execute in place when an expectation fails. + */ + public operator fun invoke( + also: (exception: ExpectationException) -> Unit + ): ContinueExpectationHandler = BaseContinue(also) +} + +/** + * Unlike [ContinueExpectationHandler], this base handler class is meant for handlers that always + * interrupt execution when an expectation fails. This enables the expectation calls to smart cast. + */ +public abstract class ExitExpectationHandler : ExpectationHandler { + + internal class ExpectationReturnExceptionInternal(val value: Any?) : Exception() + + internal open val onRun: ((returnValue: T) -> Unit)? = null + + @Suppress("TooGenericExceptionCaught") + internal inline fun runInternal( + catchExceptions: Boolean, + block: ExitExpectationReceiver.() -> T + ): T { + return catchReturnExceptionInternal { + try { + ExitExpectationReceiver(this).block() + } catch (e: Exception) { + if (e is ExpectationReturnExceptionInternal || !catchExceptions) throw e + val exception = CaughtExpectationException(MESSAGE_EXCEPTION_OCCURRED, e) + handleFail(exception) + } + }.also { returnValue -> + onRun?.invoke(returnValue) + } + } + + internal inline fun catchReturnExceptionInternal(block: () -> T): T { + return try { + block() + } catch (e: ExpectationReturnExceptionInternal) { + @Suppress("UNCHECKED_CAST") + e.value as T + } + } + + /** + * Immediately returns [value] from the block of code executed by [Expect.invoke]. + */ + protected fun returnFromBlock(value: T): Nothing { + throw ExpectationReturnExceptionInternal(value as Any?) + } +} + +/** Experimental. */ +public open class BaseReturn( + private val value: T, + private val also: ((exception: ExpectationException) -> Unit)? = null +) : ExitExpectationHandler() { + override fun handleFail(exception: ExpectationException): Nothing { + also?.invoke(exception) + returnFromBlock(value) + } +} + +/** + * Immediately returns when an expectation fails. + */ +public object Return : BaseReturn(Unit) { + + /** + * Immediately executes [also] and then returns when an expectation fails. + * + * @param also the block of code to execute in place when an expectation fails. + */ + public operator fun invoke( + also: (exception: ExpectationException) -> Unit + ): ExitExpectationHandler = BaseReturn(Unit, also) + + /** + * Immediately returns [value] when an expectation fails. Optionally executes a block of code + * first. + * + * @param value the value to return when an expectation fails. + * @param also the block of code to execute in place when an expectation fails. + */ + public operator fun invoke( + value: T, + also: ((exception: ExpectationException) -> Unit)? = null + ): ExitExpectationHandler = BaseReturn(value, also) +} + +/** + * Immediately returns when an expectation fails. Returns the last value returned through this + * handler, or [defaultValue] if no value has been returned yet. + * + * @param value the value to return when an expectation fails and no other value has been returned + * yet. + * @param also the block of code to execute in place when an expectation fails. + */ +public class ReturnLast( + private val defaultValue: T, + private val also: ((exception: ExpectationException) -> Unit)? = null +) : ExitExpectationHandler() { + + private var last: T? = null + private var hasLast: Boolean = false + + override val onRun: ((returnValue: T) -> Unit)? = { + last = it + hasLast = true + } + + override fun handleFail(exception: ExpectationException): Nothing { + also?.invoke(exception) + @Suppress("UNCHECKED_CAST") + returnFromBlock(if (hasLast) last as T else defaultValue) + } +} + +/** Experimental. */ +public open class BaseThrow( + private val exceptionFactory: ((exception: ExpectationException) -> Exception)? = null, + private val also: ((exception: ExpectationException) -> Unit)? = null +) : ExitExpectationHandler() { + override fun handleFail(exception: ExpectationException): Nothing { + also?.invoke(exception) + throw exceptionFactory?.invoke(exception) ?: exception + } +} + +/** + * Throws an [ExpectationException] when an expectation fails. + */ +public object Throw : BaseThrow() { + + /** + * Immediately executes [also] and then throws an [ExpectationException] when an expectation + * fails. + * + * @param also the block of code to execute in place when an expectation fails. + */ + public operator fun invoke( + also: (exception: ExpectationException) -> Unit + ): ExitExpectationHandler = BaseThrow(also = also) + + /** + * Throws an exception when an expectation fails. Optionally executes a block of code first. + * + * @param exceptionFactory a function to generate the exception to throw when an expectation + * fails. + * @param also the block of code to execute in place when an expectation fails. + */ + public operator fun invoke( + exceptionFactory: ((exception: ExpectationException) -> Exception)? = null, + also: ((exception: ExpectationException) -> Unit)? = null + ): ExitExpectationHandler = BaseThrow(exceptionFactory, also) +} diff --git a/src/main/java/dev/specto/belay/ExpectationReceivers.kt b/src/main/java/dev/specto/belay/ExpectationReceivers.kt new file mode 100644 index 0000000..c66916c --- /dev/null +++ b/src/main/java/dev/specto/belay/ExpectationReceivers.kt @@ -0,0 +1,219 @@ +package dev.specto.belay + +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTATION_FAILED +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_CONDITION_FALSE_BUT_TRUE +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_NON_NULL_BUT_NULL +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_NULL_BUT_NON_NULL +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_OF_TYPE +import kotlin.contracts.contract + +/** + * For handlers that do not necessarily interrupt execution when an expectation fails. + */ +public open class ContinueExpectationReceiver internal constructor( + protected var handler: ContinueExpectationHandler +) { + + /** + * Sends a [FailedExpectationException] to [handler]. + * + * This is particularly useful to funnel caught exceptions through the framework: + * + * ``` + * try { + * // … + * } catch (e: SomeException) { + * expect.fail("some error occurred", e) + * return + * } + * ``` + * + * @param cause the cause of this failure. + * @param message the error message. + */ + public fun fail(message: String, cause: Throwable? = null) { + val exception = FailedExpectationException(message, cause) + handler.handleFail(exception) + } + + /** + * If [condition] is false, sends a [FailedExpectationException] to [handler], otherwise does + * nothing. + * + * @param condition the condition to check. + * @param message the error message. + */ + public fun isTrue( + condition: Boolean, + message: String = MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE + ) { + if (!condition) fail(message = message) + } + + /** + * If [condition] is true, sends a [FailedExpectationException] to [handler], otherwise does + * nothing. + * + * @param condition the condition to check. + * @param message the error message. + */ + public fun isFalse( + condition: Boolean, + message: String = MESSAGE_EXPECTED_CONDITION_FALSE_BUT_TRUE + ) { + if (condition) fail(message = message) + } + + /** + * If [value] is not null, sends a [FailedExpectationException] to [handler], otherwise does + * nothing. + * + * @param value the value to check. + * @param message the error message. + */ + public fun isNotNull( + value: Any?, + message: String = MESSAGE_EXPECTED_VALUE_NON_NULL_BUT_NULL + ) { + if (value == null) fail(message = message) + } + + /** + * If [value] is null, sends a [FailedExpectationException] to [handler], otherwise does + * nothing. + * + * @param value the value to check. + * @param message the error message. + */ + public fun isNull( + value: Any?, + message: String = MESSAGE_EXPECTED_VALUE_NULL_BUT_NON_NULL + ) { + if (value != null) fail(message = message) + } + + /** + * If [value] is not of type [T], sends a [FailedExpectationException] to [handler], otherwise + * does nothing. + * + * @param value the value to check. + * @param message the error message. + */ + public inline fun isType(value: Any?, message: String = MESSAGE_EXPECTED_VALUE_OF_TYPE) { + if (value !is T) fail(message = message) + } +} + +public typealias GlobalExpectationReceiver = ContinueExpectationReceiver + +/** + * For handlers that always interrupt execution when an expectation fails. This enables the + * expectation calls to smart cast. + */ +public class ExitExpectationReceiver(private val handler: ExitExpectationHandler) { + + /** + * Sends a [FailedExpectationException] to [handler]. + * + * @param cause the cause of this failure. + * @param message the error message. + */ + public fun fail( + message: String = MESSAGE_EXPECTATION_FAILED, + cause: Throwable? = null + ): Nothing { + val exception = FailedExpectationException(message, cause) + handler.handleFail(exception) + } + + /** + * If [value] is not of type [T], sends a [FailedExpectationException] to [handler], otherwise + * does nothing. + * + * @param value the value to check. + * @param message the error message. + */ + public inline fun isType( + value: Any?, + message: String = MESSAGE_EXPECTED_VALUE_OF_TYPE + ): T { + if (value !is T) fail(message = message) + + return value + } +} + +/** + * If [condition] is false, sends a [FailedExpectationException] to + * [ExitExpectationReceiver.handler], otherwise does nothing. + * + * @param condition the condition to check. + * @param message the error message. + */ +public fun ExitExpectationReceiver<*>.isTrue( + condition: Boolean, + message: String = MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE +) { + contract { + returns() implies condition + } + + if (!condition) fail(message = message) +} + +/** + * If [condition] is true, sends a [FailedExpectationException] to + * [ExitExpectationReceiver.handler], otherwise does nothing. + * + * @param condition the condition to check. + * @param message the error message. + */ +public fun ExitExpectationReceiver<*>.isFalse( + condition: Boolean, + message: String = MESSAGE_EXPECTED_CONDITION_FALSE_BUT_TRUE +) { + contract { + returns() implies !condition + } + + if (condition) fail(message = message) +} + +/** + * If [value] is not null, sends a [FailedExpectationException] to + * [ExitExpectationReceiver.handler], otherwise does nothing. + * + * @param value the value to check. + * @param message the error message. + */ +public fun ExitExpectationReceiver<*>.isNotNull( + value: T?, + message: String = MESSAGE_EXPECTED_VALUE_NON_NULL_BUT_NULL +): T { + contract { + returns() implies (value != null) + } + + if (value == null) fail(message = message) + + return value +} + +/** + * If [value] is null, sends a [FailedExpectationException] to [ExitExpectationReceiver.handler], + * otherwise does nothing. + * + * @param value the value to check. + * @param message the error message. + */ +public fun ExitExpectationReceiver<*>.isNull( + value: Any?, + message: String = MESSAGE_EXPECTED_VALUE_NULL_BUT_NON_NULL +) { + contract { + returns() implies (value == null) + } + + if (value != null) fail(message = message) +} diff --git a/src/test/java/dev/specto/belay/Assertions.kt b/src/test/java/dev/specto/belay/Assertions.kt new file mode 100644 index 0000000..b129104 --- /dev/null +++ b/src/test/java/dev/specto/belay/Assertions.kt @@ -0,0 +1,48 @@ +package dev.specto.belay + +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Asserts that [block] exits with a non-local return. + */ +inline fun assertReturnsEarly(block: () -> Unit) { + val (exited, threw) = runForExitResult(block) + assertTrue(exited, "The block did not exit.") + assertFalse(threw, "The block threw an exception.") +} + +/** + * Asserts that [block] exits, either with a non-local return or by throwing an exception. + */ +inline fun assertExits(block: () -> Unit) { + val (exited) = runForExitResult(block) + assertTrue(exited, "The block did not exit.") +} + +/** + * Asserts that [block] does not exit with an exception or with a return. + */ +inline fun assertDoesNotExit(block: () -> Unit) { + val (exited) = runForExitResult(block) + assertFalse(exited, "The block exited.") +} + +data class ExitResult(val exited: Boolean, val threw: Boolean) + +/** + * Runs [block] and returns whether it exited prematurely and whether it threw an exception. A + * premature exit without an exception thrown is assumed to be a non-local return. + */ +inline fun runForExitResult(block: () -> Unit): ExitResult { + var exited = true + var threw = false + try { + block() + exited = false + } catch (e: Throwable) { + threw = true + } finally { + return ExitResult(exited, threw) + } +} diff --git a/src/test/java/dev/specto/belay/BaseTest.kt b/src/test/java/dev/specto/belay/BaseTest.kt new file mode 100644 index 0000000..ee20454 --- /dev/null +++ b/src/test/java/dev/specto/belay/BaseTest.kt @@ -0,0 +1,13 @@ +package dev.specto.belay + +import org.junit.Before + +abstract class BaseTest { + + protected lateinit var testExpectationHandlerProvider: TestExpectationHandlerProvider + + @Before + fun baseBefore() { + testExpectationHandlerProvider = TestExpectationHandlerProvider() + } +} diff --git a/src/test/java/dev/specto/belay/ContinueExpectationHandlerTest.kt b/src/test/java/dev/specto/belay/ContinueExpectationHandlerTest.kt new file mode 100644 index 0000000..35d7172 --- /dev/null +++ b/src/test/java/dev/specto/belay/ContinueExpectationHandlerTest.kt @@ -0,0 +1,49 @@ +package dev.specto.belay + +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Test + +class ContinueExpectationHandlerTest : BaseTest() { + + @Test + fun `no failed expectation does not trigger fail`() { + var ran = false + var fail = false + val handler = object : ContinueExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + fail = true + } + } + + val returnValue = handler.runInternal { + ran = true + "hello" + } + + assertTrue(ran) + assertFalse(fail) + assertEquals("hello", returnValue) + } + + @Test + fun `failed expectation triggers fail and continues`() { + var fail = false + val handler = object : ContinueExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + fail = true + } + } + + assertDoesNotExit { + val returnValue = handler.runInternal { + isTrue(false) + "hello" + } + assertEquals("hello", returnValue) + } + + assertTrue(fail) + } +} diff --git a/src/test/java/dev/specto/belay/ContinueExpectationReceiverTest.kt b/src/test/java/dev/specto/belay/ContinueExpectationReceiverTest.kt new file mode 100644 index 0000000..9be1053 --- /dev/null +++ b/src/test/java/dev/specto/belay/ContinueExpectationReceiverTest.kt @@ -0,0 +1,96 @@ +package dev.specto.belay + +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_CONDITION_FALSE_BUT_TRUE +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_NON_NULL_BUT_NULL +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_NULL_BUT_NON_NULL +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_OF_TYPE +import dev.specto.belay.TestExpectationHandlerProvider.TestContinueExpectationHandler +import org.junit.Before +import org.junit.Test + +class ContinueExpectationReceiverTest : BaseTest() { + + private lateinit var testHandler: TestContinueExpectationHandler + + @Before + fun before() { + testHandler = testExpectationHandlerProvider.continueHandler() + } + + @Test + fun `fail triggers fail`() { + ContinueExpectationReceiver(testHandler).fail("error") + testHandler.assertFailedWith("error") + } + + @Test + fun `true condition does not trigger fail for isTrue`() { + ContinueExpectationReceiver(testHandler).isTrue(true) + testHandler.assertNoFailures() + } + + @Test + fun `false condition triggers fail for isTrue`() { + ContinueExpectationReceiver(testHandler).isTrue(false) + testHandler.assertFailedWith( + MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE + ) + } + + @Test + fun `false condition does not trigger fail for isFalse`() { + ContinueExpectationReceiver(testHandler).isFalse(false) + testHandler.assertNoFailures() + } + + @Test + fun `true condition triggers fail for isFalse`() { + ContinueExpectationReceiver(testHandler).isFalse(true) + testHandler.assertFailedWith( + MESSAGE_EXPECTED_CONDITION_FALSE_BUT_TRUE + ) + } + + @Test + fun `non-null value does not trigger fail for isNotNull`() { + ContinueExpectationReceiver(testHandler).isNotNull("not null") + testHandler.assertNoFailures() + } + + @Test + fun `null value triggers fail for isNotNull`() { + ContinueExpectationReceiver(testHandler).isNotNull(null) + testHandler.assertFailedWith( + MESSAGE_EXPECTED_VALUE_NON_NULL_BUT_NULL + ) + } + + @Test + fun `null value does not trigger fail for isNull`() { + ContinueExpectationReceiver(testHandler).isNull(null) + testHandler.assertNoFailures() + } + + @Test + fun `non-null value triggers fail for isNull`() { + ContinueExpectationReceiver(testHandler).isNull("not null") + testHandler.assertFailedWith( + MESSAGE_EXPECTED_VALUE_NULL_BUT_NON_NULL + ) + } + + @Test + fun `correct type does not trigger fail for isType`() { + ContinueExpectationReceiver(testHandler).isType("hello") + testHandler.assertNoFailures() + } + + @Test + fun `wrong type triggers fail for isType`() { + ContinueExpectationReceiver(testHandler).isType(false) + testHandler.assertFailedWith( + MESSAGE_EXPECTED_VALUE_OF_TYPE + ) + } +} diff --git a/src/test/java/dev/specto/belay/ExitExpectationHandlerTest.kt b/src/test/java/dev/specto/belay/ExitExpectationHandlerTest.kt new file mode 100644 index 0000000..e56fd2c --- /dev/null +++ b/src/test/java/dev/specto/belay/ExitExpectationHandlerTest.kt @@ -0,0 +1,46 @@ +package dev.specto.belay + +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Test + +class ExitExpectationHandlerTest : BaseTest() { + + @Test + fun `no failed expectation does not trigger fail`() { + var ran = false + var fail = false + val handler = object : ExitExpectationHandler() { + override fun handleFail(exception: ExpectationException): Nothing { + fail = true + returnFromBlock("error") + } + } + + val returnValue = handler.runInternal(false) { + ran = true + "hello" + } + + assertTrue(ran) + assertFalse(fail) + assertEquals("hello", returnValue) + } + + @Test + fun `failed expectation triggers fail and exits`() { + val handler = object : ExitExpectationHandler() { + override fun handleFail(exception: ExpectationException): Nothing { + returnFromBlock("error") + } + } + + val returnValue = handler.runInternal(false) { + isTrue(false) + "hello" + } + + assertEquals("error", returnValue) + } +} diff --git a/src/test/java/dev/specto/belay/ExitExpectationReceiverTest.kt b/src/test/java/dev/specto/belay/ExitExpectationReceiverTest.kt new file mode 100644 index 0000000..7ca4211 --- /dev/null +++ b/src/test/java/dev/specto/belay/ExitExpectationReceiverTest.kt @@ -0,0 +1,163 @@ +package dev.specto.belay + +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_CONDITION_FALSE_BUT_TRUE +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_NON_NULL_BUT_NULL +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_NULL_BUT_NON_NULL +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_OF_TYPE +import dev.specto.belay.TestExpectationHandlerProvider.TestExitExpectationHandler +import org.junit.Before +import org.junit.Test + +class ExitExpectationReceiverTest : BaseTest() { + + private lateinit var testHandler: TestExitExpectationHandler + + @Before + fun before() { + testHandler = testExpectationHandlerProvider.exitHandler("error") + } + + @Test + fun `fail triggers fail`() { + assertExits { + ExitExpectationReceiver(testHandler).fail("fail") + } + testHandler.assertFailedWith("fail") + } + + @Test + fun `true condition does not trigger fail for isTrue`() { + assertDoesNotExit { + ExitExpectationReceiver(testHandler).isTrue(true) + } + testHandler.assertNoFailures() + } + + @Test + fun `false condition triggers fail for isTrue`() { + assertExits { + ExitExpectationReceiver(testHandler).isTrue(false) + } + testHandler.assertFailedWith( + MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE + ) + } + + @Test + fun `isTrue smart cast`() { + val variable: String? = "hello" + assertDoesNotExit { + ExitExpectationReceiver(testHandler).isTrue(variable != null) + variable.capitalize() + } + } + + @Test + fun `false condition does not trigger fail for isFalse`() { + assertDoesNotExit { + ExitExpectationReceiver(testHandler).isFalse(false) + } + testHandler.assertNoFailures() + } + + @Test + fun `true condition triggers fail for isFalse`() { + assertExits { + ExitExpectationReceiver(testHandler).isFalse(true) + } + testHandler.assertFailedWith( + MESSAGE_EXPECTED_CONDITION_FALSE_BUT_TRUE + ) + } + + @Test + fun `isFalse smart cast`() { + val variable: String? = "hello" + assertDoesNotExit { + ExitExpectationReceiver(testHandler).isFalse(variable == null) + variable.capitalize() + } + } + + @Test + fun `non-null value does not trigger fail for isNotNull`() { + assertDoesNotExit { + ExitExpectationReceiver(testHandler).isNotNull("not null") + } + testHandler.assertNoFailures() + } + + @Test + fun `null value triggers fail for isNotNull`() { + assertExits { + ExitExpectationReceiver(testHandler).isNotNull(null as String?) + } + testHandler.assertFailedWith( + MESSAGE_EXPECTED_VALUE_NON_NULL_BUT_NULL + ) + } + + @Test + fun `isNotNull smart cast`() { + val variable: String? = "hello" + assertDoesNotExit { + ExitExpectationReceiver(testHandler).isNotNull(variable) + variable.capitalize() + } + } + + @Test + fun `isNotNull return value is cast to non-null type`() { + val variable: String? = "hello" + assertDoesNotExit { + val cast = ExitExpectationReceiver(testHandler).isNotNull(variable) + cast.capitalize() + } + } + + @Test + fun `null value does not trigger fail for isNull`() { + assertDoesNotExit { + ExitExpectationReceiver(testHandler).isNull(null) + } + testHandler.assertNoFailures() + } + + @Test + fun `non-null value triggers fail for isNull`() { + assertExits { + ExitExpectationReceiver(testHandler).isNull("not null") + } + testHandler.assertFailedWith( + MESSAGE_EXPECTED_VALUE_NULL_BUT_NON_NULL + ) + } + + @Test + fun `correct type does not trigger fail for isType`() { + assertDoesNotExit { + ExitExpectationReceiver(testHandler).isType("hello") + } + testHandler.assertNoFailures() + } + + @Test + fun `wrong type triggers fail for isType`() { + assertExits { + ExitExpectationReceiver(testHandler).isType(false) + } + testHandler.assertFailedWith( + MESSAGE_EXPECTED_VALUE_OF_TYPE + ) + } + + @Test + fun `isType return value is cast to type`() { + val variable: String? = "hello" + assertDoesNotExit { + val cast = ExitExpectationReceiver(testHandler).isType(variable) + cast.capitalize() + } + } +} diff --git a/src/test/java/dev/specto/belay/ExpectTest.kt b/src/test/java/dev/specto/belay/ExpectTest.kt new file mode 100644 index 0000000..d9d2cd4 --- /dev/null +++ b/src/test/java/dev/specto/belay/ExpectTest.kt @@ -0,0 +1,446 @@ +package dev.specto.belay + +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_CONDITION_FALSE_BUT_TRUE +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_NON_NULL_BUT_NULL +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_NULL_BUT_NON_NULL +import dev.specto.belay.FailedExpectationException.Companion.MESSAGE_EXPECTED_VALUE_OF_TYPE +import dev.specto.belay.TestExpectationHandlerProvider.TestContinueExpectationHandler +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Before +import org.junit.Test + +class ExpectTest : BaseTest() { + + private lateinit var testGlobalHandler: TestContinueExpectationHandler + private lateinit var expect: Expect + + @Before + fun before() { + testGlobalHandler = testExpectationHandlerProvider.continueHandler() + expect = Expect(testGlobalHandler) + } + + @Test + fun `invoke true condition does not trigger fail`() { + expect(true) + testGlobalHandler.assertNoFailures() + } + + @Test + fun `invoke false condition triggers fail`() { + expect(false) + testGlobalHandler.assertFailedWith( + MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE + ) + } + + @Test + fun `invoke false condition triggers fail with custom message`() { + expect(false, "foobar") + testGlobalHandler.assertFailedWith("foobar") + } + + @Test + fun `invoke true condition does not run handlers`() { + var onFailFunctionRan = false + expect(true) { + onFailFunctionRan = true + } + assertFalse(onFailFunctionRan) + testGlobalHandler.assertNoFailures() + } + + @Test + fun `invoke false condition runs handlers`() { + var onFailFunctionRan = false + expect(false) { + onFailFunctionRan = true + } + assertTrue(onFailFunctionRan) + testGlobalHandler.assertFailedWith( + MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE + ) + } + + @Test + fun `invoke false condition runs global expectation handler before onFail function`() { + var globalExpectationHandlerRan = false + var onFailFunctionRan = false + + expect.onGlobalFail = object : GlobalExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + globalExpectationHandlerRan = true + assertFalse(onFailFunctionRan) + } + } + + expect(false) { + assertTrue(globalExpectationHandlerRan) + onFailFunctionRan = true + } + + assertTrue(onFailFunctionRan) + } + + @Test + fun `invoke onFail function can return early`() { + assertReturnsEarly { + expect(false) { + return + } + } + } + + @Test + fun `invoke for continue runs receiver block once`() { + var blockRan = false + val testHandler = testExpectationHandlerProvider.continueHandler() + expect(testHandler) { + assertFalse(blockRan) + blockRan = true + } + assertTrue(blockRan) + } + + @Test + fun `invoke for exit runs receiver block once`() { + var blockRan = false + val testHandler = testExpectationHandlerProvider.exitHandler("error") + expect(testHandler) { + assertFalse(blockRan) + blockRan = true + "success" + } + assertTrue(blockRan) + } + + @Test + fun `invoke for exit returns block value when no expectations fail`() { + val testHandler = testExpectationHandlerProvider.exitHandler("error") + val returnValue = expect(testHandler) { + isTrue(true) + "success" + } + assertEquals("success", returnValue) + } + + @Test + fun `invoke for exit returns handler default value when expectations fail`() { + val testHandler = testExpectationHandlerProvider.exitHandler("error") + val returnValue = expect(testHandler) { + isTrue(false) + "success" + } + assertEquals("error", returnValue) + } + + @Test + fun `invoke for continue runs global expectation handler before local handler`() { + var globalExpectationHandlerRan = false + var localHandlerRan = false + + expect.onGlobalFail = object : GlobalExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + globalExpectationHandlerRan = true + assertFalse(localHandlerRan) + } + } + + val localHandler = object : ContinueExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + assertTrue(globalExpectationHandlerRan) + localHandlerRan = true + } + } + + expect(localHandler) { + isTrue(false) + } + + assertTrue(localHandlerRan) + } + + @Test + fun `invoke for exit runs global expectation handler before local handler`() { + var globalExpectationHandlerRan = false + var localHandlerRan = false + + expect.onGlobalFail = object : GlobalExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + globalExpectationHandlerRan = true + assertFalse(localHandlerRan) + } + } + + val localHandler = object : ExitExpectationHandler() { + override fun handleFail(exception: ExpectationException): Nothing { + assertTrue(globalExpectationHandlerRan) + localHandlerRan = true + returnFromBlock("error") + } + } + + expect(localHandler) { + isTrue(false) + "success" + } + + assertTrue(localHandlerRan) + } + + @Test + fun `isTrue true condition does not run handlers`() { + assertDoesNotExit { + expect.isTrue(true) { + return + } + } + testGlobalHandler.assertNoFailures() + } + + @Test + fun `isTrue false condition runs handlers`() { + assertReturnsEarly { + expect.isTrue(false) { + return + } + } + testGlobalHandler.assertFailedWith( + MESSAGE_EXPECTED_CONDITION_TRUE_BUT_FALSE + ) + } + + @Test + fun `isTrue false condition runs global expectation handler before onFail function`() { + var globalExpectationHandlerRan = false + + expect.onGlobalFail = object : GlobalExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + globalExpectationHandlerRan = true + } + } + + assertReturnsEarly { + expect.isTrue(false) { + assertTrue(globalExpectationHandlerRan) + return + } + } + } + + @Test + fun `isTrue smart cast`() { + val variable: String? = "hello" + assertDoesNotExit { + expect.isTrue(variable != null) { + return + } + variable.capitalize() + } + } + + @Test + fun `isFalse false condition does not run handlers`() { + assertDoesNotExit { + expect.isFalse(false) { + return + } + } + testGlobalHandler.assertNoFailures() + } + + @Test + fun `isFalse true condition runs handlers`() { + assertReturnsEarly { + expect.isFalse(true) { + return + } + } + testGlobalHandler.assertFailedWith( + MESSAGE_EXPECTED_CONDITION_FALSE_BUT_TRUE + ) + } + + @Test + fun `isFalse true condition runs global expectation handler before onFail function`() { + var globalExpectationHandlerRan = false + + expect.onGlobalFail = object : GlobalExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + globalExpectationHandlerRan = true + } + } + + assertReturnsEarly { + expect.isFalse(true) { + assertTrue(globalExpectationHandlerRan) + return + } + } + } + + @Test + fun `isFalse smart cast`() { + val variable: String? = "hello" + assertDoesNotExit { + expect.isFalse(variable == null) { + return + } + variable.capitalize() + } + } + + @Test + fun `isNotNull not null value does not run handlers`() { + assertDoesNotExit { + expect.isNotNull("not null") { + return + } + } + testGlobalHandler.assertNoFailures() + } + + @Test + fun `isNotNull null value runs handlers`() { + assertReturnsEarly { + expect.isNotNull(null as String?) { + return + } + } + testGlobalHandler.assertFailedWith( + MESSAGE_EXPECTED_VALUE_NON_NULL_BUT_NULL + ) + } + + @Test + fun `isNotNull null value runs global expectation handler before onFail function`() { + var globalExpectationHandlerRan = false + + expect.onGlobalFail = object : GlobalExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + globalExpectationHandlerRan = true + } + } + + assertReturnsEarly { + expect.isNotNull(null as String?) { + assertTrue(globalExpectationHandlerRan) + return + } + } + } + + @Test + fun `isNotNull smart cast`() { + val variable: String? = "hello" + assertDoesNotExit { + expect.isNotNull(variable) { + return + } + variable.capitalize() + } + } + + @Test + fun `isNotNull return value is cast to non-null type`() { + assertDoesNotExit { + val cast = expect.isNotNull("hello") { + return + } + cast.capitalize() + } + } + + @Test + fun `isNull not null value does not run handlers`() { + assertDoesNotExit { + expect.isNull(null) { + return + } + } + testGlobalHandler.assertNoFailures() + } + + @Test + fun `isNull not null value runs handlers`() { + assertReturnsEarly { + expect.isNull("not null") { + return + } + } + testGlobalHandler.assertFailedWith( + MESSAGE_EXPECTED_VALUE_NULL_BUT_NON_NULL + ) + } + + @Test + fun `isNull not null value runs global expectation handler before onFail function`() { + var globalExpectationHandlerRan = false + + expect.onGlobalFail = object : GlobalExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + globalExpectationHandlerRan = true + } + } + + assertReturnsEarly { + expect.isNull("not null") { + assertTrue(globalExpectationHandlerRan) + return + } + } + } + + @Test + fun `isType correct type does not run handlers`() { + assertDoesNotExit { + expect.isType("hello") { + return + } + } + testGlobalHandler.assertNoFailures() + } + + @Test + fun `isType wrong type runs handlers`() { + assertReturnsEarly { + expect.isType(false) { + return + } + } + testGlobalHandler.assertFailedWith( + MESSAGE_EXPECTED_VALUE_OF_TYPE + ) + } + + @Test + fun `isType wrong type runs global expectation handler before onFail function`() { + var globalExpectationHandlerRan = false + + expect.onGlobalFail = object : GlobalExpectationHandler() { + override fun handleFail(exception: ExpectationException) { + globalExpectationHandlerRan = true + } + } + + assertReturnsEarly { + expect.isType(false) { + assertTrue(globalExpectationHandlerRan) + return + } + } + } + + @Test + fun `isType return value is cast to correct type`() { + assertDoesNotExit { + val cast = expect.isType("hello" as String?) { + return + } + cast.capitalize() + } + } +} diff --git a/src/test/java/dev/specto/belay/ReturnLastTest.kt b/src/test/java/dev/specto/belay/ReturnLastTest.kt new file mode 100644 index 0000000..a6adc70 --- /dev/null +++ b/src/test/java/dev/specto/belay/ReturnLastTest.kt @@ -0,0 +1,40 @@ +package dev.specto.belay + +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.junit.Test + +class ReturnLastTest : BaseTest() { + + @Test + fun `runInternal returns default value when the first run fails`() { + val handler = ReturnLast("default") + val returnValue = handler.runInternal(false) { + isTrue(false) + "success" + } + assertEquals("default", returnValue) + } + + @Test + fun `runInternal returns last value when an expectation fails`() { + val handler = ReturnLast("default") + handler.runInternal(false) { "success" } + val returnValue = handler.runInternal(false) { + isTrue(false) + "another success" + } + assertEquals("success", returnValue) + } + + @Test + fun `runInternal runs also function when an expectation fails`() { + var ran = false + val handler = ReturnLast("default", { ran = true }) + handler.runInternal(false) { + isTrue(false) + "success" + } + assertTrue(ran) + } +} diff --git a/src/test/java/dev/specto/belay/TestExpectationHandlerProvider.kt b/src/test/java/dev/specto/belay/TestExpectationHandlerProvider.kt new file mode 100644 index 0000000..4ad977b --- /dev/null +++ b/src/test/java/dev/specto/belay/TestExpectationHandlerProvider.kt @@ -0,0 +1,78 @@ +package dev.specto.belay + +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +// TODO Add this to open source project once more fleshed out. +interface TestExpectationHandler { + + val exceptions: List + + fun assertNoFailures() + + fun assertFailed() +} + +inline fun TestExpectationHandler.assertFailedWith(cause: Throwable? = null) { + val exception = exceptions.singleOrNull() + assertNotNull(exception, "One exception expected but got ${exceptions.size}.") + assertTrue( + exception is T, + "Expected exception of type ${T::class.java.canonicalName} but got" + + " ${exception::class.java.canonicalName}" + ) + assertEquals(cause, exception.cause) +} + +inline fun TestExpectationHandler.assertFailedWith( + message: String, + cause: Throwable? = null +) { + assertFailedWith(cause) + assertEquals(message, exceptions.single().message) +} + +class TestExpectationHandlerProvider { + + val exceptions: MutableList = mutableListOf() + + val testExpectationHandler = object : TestExpectationHandler { + + override val exceptions: List = + this@TestExpectationHandlerProvider.exceptions + + override fun assertNoFailures() { + assertTrue( + exceptions.isEmpty(), + "No expectation failures were expected but go ${exceptions.size}" + ) + } + + override fun assertFailed() { + assertNotNull( + exceptions.singleOrNull(), + "One exception expected but got ${exceptions.size}." + ) + } + } + + inner class TestContinueExpectationHandler : ContinueExpectationHandler(), + TestExpectationHandler by testExpectationHandler { + override fun handleFail(exception: ExpectationException) { + this@TestExpectationHandlerProvider.exceptions.add(exception) + } + } + + inner class TestExitExpectationHandler(val returnValue: T) : ExitExpectationHandler(), + TestExpectationHandler by testExpectationHandler { + override fun handleFail(exception: ExpectationException): Nothing { + this@TestExpectationHandlerProvider.exceptions.add(exception) + returnFromBlock(returnValue) + } + } + + fun continueHandler() = TestContinueExpectationHandler() + + fun exitHandler(returnValue: T) = TestExitExpectationHandler(returnValue) +} diff --git a/src/test/java/dev/specto/belay/ThrowTest.kt b/src/test/java/dev/specto/belay/ThrowTest.kt new file mode 100644 index 0000000..85c970d --- /dev/null +++ b/src/test/java/dev/specto/belay/ThrowTest.kt @@ -0,0 +1,39 @@ +package dev.specto.belay + +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue +import org.junit.Test + +class ThrowTest : BaseTest() { + + @Test + fun `runInternal throws an ExpectationException when an expectation fails`() { + assertFailsWith { + Throw.runInternal(false) { + isTrue(false) + } + } + } + + @Test + fun `runInternal runs also function when an expectation fails`() { + var ran = false + assertFailsWith { + Throw { ran = true }.runInternal(false) { + isTrue(false) + } + } + assertTrue(ran) + } + + @Test + fun `runInternal throws exception from factory when an expectation fails`() { + val handler = Throw(exceptionFactory = { IllegalStateException(it.message) }) + assertFailsWith { + handler.runInternal(false) { + isTrue(false) + "success" + } + } + } +}