// Matcher values
//
// A `Matcher[T]` is a matcher as a value: you can store it, combine it and
// pass it to other matchers. Run one with `expect(value).to(matcher)`.
//
// MoonBit traits cannot take type parameters, so a matcher is a struct of
// closures, as `Iter[T]` is.
///|
/// A check on values of type `T`, with a description for failure messages.
///
/// - `describe` returns what the matcher wants, such as `equal to 3`. It is
/// called only when a failure message is built.
/// - `check` returns `None` when the value matches, or `Some(reason)` when it
/// does not, such as `Some("was 2")`.
///
/// Build your own matcher with a struct literal:
/// `let even : Matcher[Int] = { describe: () => "even", check: x => if x % 2 == 0 { None } else { Some("was odd") } }`.
pub(all) struct Matcher[T] {
describe : () -> String
check : (T) -> String?
}
///|
/// A matcher for values equal to `expected`.
pub fn[T : Eq + @debug.Debug] equal_to(expected : T) -> Matcher[T] {
{
describe: () => "equal to \{show(expected)}",
check: value => {
if value == expected {
None
} else {
Some("was \{show(value)}")
}
},
}
}
///|
/// A matcher for values that satisfy `predicate`.
pub fn[T : @debug.Debug] satisfying(
predicate : (T) -> Bool,
description? : String = "predicate",
) -> Matcher[T] {
{
describe: () => description,
check: value => {
if predicate(value) {
None
} else {
Some("was \{show(value)}")
}
},
}
}
///|
/// The descriptions of `matchers`, in parentheses: `(> 1, < 5)`.
fn[T] describe_all(matchers : Array[Matcher[T]]) -> String {
"(" + matchers.map(m => (m.describe)()).join(", ") + ")"
}
///|
/// The reasons why `value` does not match each of `matchers`, one per line:
/// `< 5: was 7`.
fn[T] mismatches(matchers : Array[Matcher[T]], value : T) -> Array[String] {
matchers.filter_map(m => {
(m.check)(value).map(reason => "\{(m.describe)()}: \{reason}")
})
}
///|
/// A matcher for values that match every one of `matchers`. A mismatch names
/// each part that does not match.
pub fn[T] all_of(matchers : Array[Matcher[T]]) -> Matcher[T] {
{
describe: () => "all of \{describe_all(matchers)}",
check: value => {
let failed = mismatches(matchers, value)
if failed.is_empty() {
None
} else {
Some(failed.join("\n"))
}
},
}
}
///|
/// A matcher for values that match at least one of `matchers`.
pub fn[T] any_of(matchers : Array[Matcher[T]]) -> Matcher[T] {
{
describe: () => "any of \{describe_all(matchers)}",
check: value => {
let failed = mismatches(matchers, value)
if failed.length() < matchers.length() {
None
} else {
Some(failed.join("\n"))
}
},
}
}
///|
/// A matcher for values that do not match `matcher`.
pub fn[T : @debug.Debug] is_not(matcher : Matcher[T]) -> Matcher[T] {
{
describe: () => "not \{(matcher.describe)()}",
check: value => {
match (matcher.check)(value) {
Some(_) => None
None => Some("was \{show(value)}")
}
},
}
}
///|
/// A matcher that applies `f` to the value and checks the result with
/// `matcher`: `field("name", u => u.name, equal_to("Ada"))`.
pub fn[T, U] field(
name : String,
f : (T) -> U,
matcher : Matcher[U],
) -> Matcher[T] {
{
describe: () => "\{name} \{(matcher.describe)()}",
check: value => (matcher.check)(f(value)),
}
}
///|
/// A matcher that runs method matchers on the value:
/// `matching("an adult", it => it.to_be_greater_than_or_equal(18))`.
///
/// Every method matcher works this way, and so do custom matchers. The
/// mismatch comes from the failure of the first method matcher that fails.
pub fn[T] matching(
description : String,
block : (Expectation[T]) -> Unit raise Error,
) -> Matcher[T] {
{
describe: () => description,
check: value => {
block(expect(value)) catch {
error => return Some(mismatch_reason(error))
}
None
},
}
}
///|
/// A short reason from the failure of a method matcher: `was 17 (expected
/// >= 18)`. Other errors give their message.
fn mismatch_reason(error : Error) -> String {
let lines = describe_error(error).split("\n").map(l => l.to_owned()).collect()
let value_of = (name : String) => {
lines
.search_by(line => line.has_prefix("\{name}:"))
.map(i => {
lines[i].view(start_offset=name.length() + 1).trim(chars=" ").to_owned()
})
}
match (value_of("Received"), value_of("Expected")) {
(Some(received), Some(expected)) if !received.is_empty() &&
!expected.is_empty() => "was \{received} (expected \{expected})"
_ => lines.join("\n")
}
}
///|
/// Assert the actual value matches `matcher`.
#callsite(autofill(loc))
pub fn[T : @debug.Debug] Expectation::to(
self : Expectation[T],
matcher : Matcher[T],
loc~ : SourceLoc,
) -> Unit raise Error {
let mismatch = (matcher.check)(self.actual)
self.assert_that(
mismatch is None,
"to",
args="matcher",
expected=() => (matcher.describe)(),
received=() => show(self.actual),
details=() => {
match mismatch {
Some(reason) if !self.negated => [("Mismatch", reason)]
_ => []
}
},
loc~,
)
}
///|
/// Assert at least one element of an Array matches `matcher`. A failure
/// shows why each element does not match.
#callsite(autofill(loc))
pub fn[T : @debug.Debug] Expectation::to_contain_element_matching(
self : Expectation[Array[T]],
matcher : Matcher[T],
loc~ : SourceLoc,
) -> Unit raise Error {
let reasons = self.actual.map(element => (matcher.check)(element))
self.assert_that(
reasons.iter().any(reason => reason is None),
"to_contain_element_matching",
args="matcher",
expected=() => "containing an element: \{(matcher.describe)()}",
received=() => show(self.actual),
details=() => {
if self.negated || reasons.is_empty() {
return []
}
let lines = []
for i, reason in reasons {
if reason is Some(text) {
lines.push("index \{i}: " + text.replace_all(old="\n", new="; "))
}
}
[("Mismatches", lines.join("\n"))]
},
loc~,
)
}