-
Notifications
You must be signed in to change notification settings - Fork 0
Testing Results
Validators are ordinary values, so tests assert on their results directly. The Testing module runs a validator and narrows its failures by path, message, code, severity, and state:
module Ex08Testing
import Std.Io as Io
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Number as NumberRule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Testing as Testing
import PuduLangValidator.Validator as Validator
type Customer = { name: Str, age: Int }
fn customerValidator() -> Validator.Validator[Customer] {
let name = Rule.build(Rule.withCode(Rule.withMessage(
TextRule.notEmpty(Rule.ruleFor("name", |customer: Customer| customer.name)),
"Name is required."), "NameRequired"))
let age = Rule.build(Rule.withCode(
NumberRule.greaterThanOrEqualTo(Rule.ruleFor("age", |customer: Customer| customer.age), 18), "AgeCheck"))
Validator.add(Validator.add(Validator.create(), name), age)
}
export fn main() -> Int {
let validator = customerValidator()
let result = Testing.testValidate(&validator, Customer{name: "", age: 3})
let nameFails = Testing.forProperty(&result, "name")
let coded = Testing.withCode(nameFails, "NameRequired")
let line = "any=" + show(Testing.hasAny(&coded))
+ " none=" + show(Testing.hasNone(&Testing.withCode(Testing.forProperty(&result, "name"), "Other")))
+ " onlyAge=" + show(Testing.onlyErrorsFor(&result, ["name", "age"]))
match Io.writeLine(line) {
case Ok(_) => if Testing.hasAny(&coded)
&& Testing.hasErrorFor(&result, "age")
&& Testing.hasCode(&result, "AgeCheck")
&& Testing.onlyErrorsFor(&result, ["name", "age"]) { 0 } else { 2 }
case Err(_) => 1
}
}
Output:
any=true none=true onlyAge=true
Testing.testValidateWith runs with explicit options, and testValidateAsync covers async validators. Each withX filter has a withoutX mirror for asserting absence. hasAny and hasNone ask whether a query matched; only asks whether the query caught every failure; onlyErrorsFor pins down the exact set of failing paths.
The Result module works one level lower: messages joins failure text, forProperty filters by path, toDictionary groups messages by path, and toResult turns a result back into a Result holding the value or its failures.
Result.messages joins failure text, Result.forProperty filters by path, Result.toDictionary
groups messages by path, and Result.toResult turns a result back into a Result holding the
value or its failures. Result.fromFailures rebuilds a result from failures alone, which is how
custom checks and prevalidation answer early. On the Testing side, testValidateWith runs with
explicit options, hasNoErrorFor asserts a clean path, the withoutX mirrors assert absence, and
only asks whether a query caught every failure:
module Ex13Result
import Std.Io as Io
import PuduLangValidator.Result as Validation
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Number as NumberRule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Testing as Testing
import PuduLangValidator.Validator as Validator
type Customer = { name: Str, age: Int }
fn customerValidator() -> Validator.Validator[Customer] {
let name = Rule.build(TextRule.notEmpty(Rule.ruleFor("name", |customer: Customer| customer.name)))
let age = Rule.build(NumberRule.greaterThanOrEqualTo(Rule.ruleFor("age", |customer: Customer| customer.age), 18))
Validator.add(Validator.add(Validator.create(), name), age)
}
export fn main() -> Int {
let validator = customerValidator()
let bad = Customer{name: "", age: 3}
let result = Validator.validate(&validator, bad)
let named = Validation.forProperty(&result, "name")
let catalog = Validation.toDictionary(&result)
let back = Validation.toResult(&result, bad)
let rebuilt = Validation.fromFailures(result.errors)
let pristine = Validation.fromFailures([])
let chosen = Testing.testValidateWith(&validator, bad,
Validator.includeProperties(Validator.defaults(), ["age"]))
let single = Validator.validate(&validator, Customer{name: "Bo", age: 3})
let line = "joined=" + show(Validation.messages(&result, "|").contains("|"))
+ " nameFails=" + show(named.length()) + " paths=" + show(catalog.size())
+ " backFails=" + show(match back { case Ok(_) => 0 case Err(errors) => errors.length() })
+ " rebuilt=" + show(rebuilt.isValid) + " pristine=" + show(pristine.isValid)
+ " chosen=" + show(chosen.errors.length())
+ " cleanAge=" + show(Testing.hasNoErrorFor(&result, "age"))
+ " cleanZed=" + show(Testing.hasNoErrorFor(&result, "zzz"))
+ " narrowed=" + show(Testing.hasNone(&Testing.withoutCode(Testing.forProperty(&result, "name"), "NotEmpty")))
+ " all=" + show(Testing.only(&Testing.forProperty(&result, "name")))
+ " allSingle=" + show(Testing.only(&Testing.forProperty(&single, "age")))
match Io.writeLine(line) {
case Ok(_) => if named.length() == 1 && catalog.size() == 2 && !rebuilt.isValid
&& pristine.isValid && chosen.errors.length() == 1 && !Testing.hasNoErrorFor(&result, "age")
&& Testing.hasNoErrorFor(&result, "zzz")
&& !Testing.only(&Testing.forProperty(&result, "name"))
&& Testing.only(&Testing.forProperty(&single, "age")) { 0 } else { 2 }
case Err(_) => 1
}
}
Output:
joined=true nameFails=1 paths=2 backFails=2 rebuilt=false pristine=true chosen=1 cleanAge=false cleanZed=true narrowed=true all=false allSingle=true
The two failure texts join with a separator; one failure sits at name and two paths share the
dictionary. Turning the result back yields its two failures, rebuilding from them stays invalid,
and rebuilding from nothing stays valid. Selecting age keeps one failure; age is not clean
but zzz is. Removing the NotEmpty code empties the name query, which catches one of two
failures (all=false) while the single-failure query catches everything (allSingle=true).
Every withX filter has a withoutX mirror for asserting absence. Testing.withMessage and
Testing.withoutMessage narrow by failure text, withSeverity and withoutSeverity by
classification, and withState and withoutState by application state:
module Ex20TestFilters
import Std.Io as Io
import PuduLangValidator.Result as Validation
import PuduLangValidator.Rule as Rule
import PuduLangValidator.Rules.Number as NumberRule
import PuduLangValidator.Rules.Text as TextRule
import PuduLangValidator.Testing as Testing
import PuduLangValidator.Validator as Validator
type Customer = { name: Str, age: Int }
fn customerValidator() -> Validator.Validator[Customer] {
let name = Rule.build(Rule.withCode(Rule.withMessage(
TextRule.notEmpty(Rule.ruleFor("name", |customer: Customer| customer.name)),
"Name is required."), "NameRequired"))
let age = Rule.build(Rule.withState(Rule.withSeverity(Rule.withCode(
NumberRule.greaterThanOrEqualTo(Rule.ruleFor("age", |customer: Customer| customer.age), 18),
"AgeCheck"), Validation.Warning), "minor"))
Validator.add(Validator.add(Validator.create(), name), age)
}
export fn main() -> Int {
let validator = customerValidator()
let result = Testing.testValidate(&validator, Customer{name: "", age: 3})
let single = Testing.testValidate(&validator, Customer{name: "Bo", age: 3})
let line = "msgHit=" + show(Testing.hasAny(&Testing.withMessage(Testing.forProperty(&result, "name"), "Name is required.")))
+ " msgGone=" + show(Testing.hasNone(&Testing.withoutMessage(Testing.forProperty(&result, "name"), "Name is required.")))
+ " warnHit=" + show(Testing.hasAny(&Testing.withSeverity(Testing.forProperty(&result, "age"), Validation.Warning)))
+ " warnGone=" + show(Testing.hasNone(&Testing.withoutSeverity(Testing.forProperty(&result, "age"), Validation.Warning)))
+ " stateHit=" + show(Testing.hasAny(&Testing.withState(Testing.forProperty(&result, "age"), "minor")))
+ " stateGone=" + show(Testing.hasNone(&Testing.withoutState(Testing.forProperty(&result, "age"), "minor")))
+ " allSingle=" + show(Testing.only(&Testing.forProperty(&single, "age")))
match Io.writeLine(line) {
case Ok(_) => if Testing.hasAny(&Testing.withMessage(Testing.forProperty(&result, "name"), "Name is required."))
&& Testing.hasNone(&Testing.withoutMessage(Testing.forProperty(&result, "name"), "Name is required."))
&& Testing.hasAny(&Testing.withSeverity(Testing.forProperty(&result, "age"), Validation.Warning))
&& Testing.hasNone(&Testing.withoutSeverity(Testing.forProperty(&result, "age"), Validation.Warning))
&& Testing.hasAny(&Testing.withState(Testing.forProperty(&result, "age"), "minor"))
&& Testing.hasNone(&Testing.withoutState(Testing.forProperty(&result, "age"), "minor"))
&& Testing.only(&Testing.forProperty(&single, "age")) { 0 } else { 2 }
case Err(_) => 1
}
}
Output:
msgHit=true msgGone=true warnHit=true warnGone=true stateHit=true stateGone=true allSingle=true
Each filter finds its failure and each mirror confirms the absence. The single-failure query
catches everything, so only answers true for it.