|
Initial comments on SRFI 269
Wolfgang Corcoran-Mathe
(14 Jun 2026 01:43 UTC)
|
|
Re: Initial comments on SRFI 269
Andrew Tropin
(19 Jun 2026 11:29 UTC)
|
|
Re: Initial comments on SRFI 269
Wolfgang Corcoran-Mathe
(19 Jun 2026 18:16 UTC)
|
|
Re: Initial comments on SRFI 269
Wolfgang Corcoran-Mathe
(19 Jun 2026 18:29 UTC)
|
|
Re: Initial comments on SRFI 269 Andrew Tropin (23 Jun 2026 05:58 UTC)
|
On 2026-06-19 14:16, Wolfgang Corcoran-Mathe wrote:
> Andrew,
>
> Thanks for your reply.
>
> On 2026-06-19 18:29 +0700, Andrew Tropin wrote:
>> > 1. Why is the parameter called 'test-runner*' and not 'test-runner'?
>>
>> No particular reason, it's just a naming convention in guile-ares-rs
>> project (where reference implementation of the suitbl testing library is
>> developed) to make dynamically scoped variables stand out. I derived it
>> from clojure's style of `*dynamic-var*`, but without prefix asterisk. I
>> know it conflicts with let* and alike. Also, somebody already mentioned
>> and suggest current-test-runner.
>
> Yes, I heartily endorse 'current-test-runner'. The "current-" prefix
> is standard for several parameters. SRFI 64 also uses
> 'current-test-runner' for the equivalent feature.
>
Oki, wrote down a task for the next revision.
>> > 2. There is currently no assertion support for multiple values.
>>
>> [snip]
>>
>> I gave it a brief thought and it still seems general enough (I will
>> address issue with predicate form replying to the #3). The design of
>> SRFI-269 is heavily relies on shifting as much as possible to test
>> runner and focusing only on definition semantics, not execusion. So,
>> it's the responsibility of test runner to decide what asserting means.
>>
>> As for test runner in suitbl, we just inherit semantics of `if`. For
>> Guile it's following (I just checked it a few minutes ago): If
>> there are 0 values, it's exception, if more than zero the first value is
>> checked for truethness.
>
> (This is intertwined with the predicate-argument form issue.)
>
> I think this is a bit too subtle, especially when
> the default test runner may silently discard extra values!
The same way as if, as well as all other predicates:
(boolean? (values #f 2 3)) => #t
> There should be a way to test multiple values explicitly,
There is. Very explicit. Will demonstrate below.
> rather than leaving it to the runner to decide what a "pass" is in the
> multiple-values case (all values truthy?).
>
> Here's a rough sketch of a reworked assertion form which supports
> multi-valued test expressions *and* some degree of rich-mode error
> reporting. It is based vaguely on SRFI 61's generalized 'cond'
> clause.
>
> == Begin spec ==
>
> (is <generator> <consumer>)
>
> <generator> and <consumer> are expressions, and <consumer> must
> evaluate to a procedure. Asserts that
>
> (call-with-values (lambda () <generator>)
> <consumer>)
>
> evaluates to a single true value. The entity constructed from this
> form has (at least) the following keys:
>
> assertion/generator-thunk
>
> Equivalent to (lambda () <generator>).
>
> assertion/generator-source
>
> A datum representation of <generator>.
>
> assertion/consumer-proc
>
> The value of <consumer>.
>
> assertion/consumer-source
>
> A datum representation of <consumer>.
>
> assertion/location
>
> An assertion source code location
>
>
> (is <generator> <consumer> <description>)
>
> As above, with a human-readable description. (Keys as above, plus
> 'assertion/description'.)
>
>
> (is <expression>)
>
> Special case. Equivalent to
>
> (is <expression> values).
>
> == End spec ==
>
>
> This subsumes the current assertion model. The special-case
> 'assertion/args-thunk' can be eliminated, since this model decouples
> test-expression evaluation and consumer application. <generator>, its
> values, and <consumer> can all be reported separately, without the need
> for subterm analysis. (Instead of writing (is (integer? (+ 2 2))) to
> get rich-mode reporting, you would write (is (+ 2 2) integer?).)
>
> A minor drawback is losing (is <expression> <description>) due to
> syntactic ambiguity. That needs some work. One solution is to "tag"
> the <description> argument, e.g.
>
> (is (integer? 4) (desc "4 is an integer")).
>
> While not exactly beautiful, this trick has the advantage of allowing
> descriptions to appear anywhere, since they can be unambiguously
> identified:
>
> (is (desc "4 is an integer") 4 integer?)
>
> I'm not sure the additional syntax is worth it, but it's an idea. SRFI
> 64, chibi-test, and CHICKEN test users are probably used to writing
> their descriptions first, which becomes possible with a tagged
> <description> argument.
The another solution of ambigouty could be
(is <generator> [=> <consumer>] [<description])
where [] is for optional macro arguments.
> Let me know what you think.
The overall idea looks cool, I even started to wonder about some elegant
examples. Those ones look quite readable:
(is (+ 2 2) => integer?)
(is (values expected actual) => equal?)
Not much difference with original form, arguably "flatter" (less
nested), but slightly more involved and verbose.
(is (integer? (+ 2 2)))
(is (equal? expected actual))
However, I still can't find a practical application and semantical
meaning for multiple-values assertion macro. Also, it seems we mix
multiple-value destructuring with asserting here.
Let's take a look at the example, where builtin multiple-value
destructuring could be useful.
(is (floor/ 13 4)
=> (lambda (q r) (and (= q 3) (= r 1)))
"floor/ returns proper quotient and remainder")
This makes consumer "opaque". In case of failure we won't know what the
actual problem is. Is it wrong quotient or remainder or something else?
Even if we (test runner or IDE) bring debbuger, when we encounter the
failure, we won't have access to the stack inside consumer and won't be
able to explore q/r values.
On the other hand, when we decouple destructuring from assertion the
test body will look like this:
(let-values (((q r) (floor/ 13 4)))
(is (= q 3) "quotient is 3")
(is (= r 1) "remainder is 1"))
The failure in this case will be very explicit and if that's not enough
the debugger would be able to access lexical scope of failure.
Also, `(assert #falsy) => failure` semantics seems very intuitive.
It is wide-spread and thus expected.
In summary, I'm in for removing "predicate form", special case handling
and optional assertion/args-thunk. I don't think we need to mix in
values destructuring and handling into assertion. If you have any
counter-examples or related thoughts, please share. Thanks again for
very valuable ideas and feedback!
--
Best regards,
Andrew Tropin