From 290170f450fdb09a5344ded6435b8176cfaa8d8e Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Fri, 27 Apr 2018 23:53:31 +0100 Subject: [PATCH] Simple test harness --- syndicate/bag.rkt | 6 +- syndicate/dataspace.rkt | 11 +- syndicate/syntax.rkt | 553 +++++++++++++++++++++++----------------- syndicate/test.rkt | 196 ++++++++++++++ 4 files changed, 531 insertions(+), 235 deletions(-) create mode 100644 syndicate/test.rkt diff --git a/syndicate/bag.rkt b/syndicate/bag.rkt index 9dc230e..6149082 100644 --- a/syndicate/bag.rkt +++ b/syndicate/bag.rkt @@ -10,7 +10,8 @@ bag-member? in-bag in-bag/count - set->bag) + set->bag + bag->set) (require racket/set) @@ -55,3 +56,6 @@ (define (set->bag s [count 1]) (for/hash [(e (in-set s))] (values e count))) + +(define (bag->set b) + (list->set (hash-keys b))) diff --git a/syndicate/dataspace.rkt b/syndicate/dataspace.rkt index 4ddbed0..fd0169c 100644 --- a/syndicate/dataspace.rkt +++ b/syndicate/dataspace.rkt @@ -8,6 +8,7 @@ (struct-out observe) dataspace? + dataspace-assertions ;; TODO: shouldn't be provided - needed by test.rkt generate-id! ;; TODO: shouldn't be provided - inline syntax.rkt?? actor? @@ -25,6 +26,8 @@ field-handle-owner field-handle-value + current-actor-crash-logger + current-actor current-facet in-script? ;; TODO: shouldn't be provided - inline syntax.rkt?? @@ -169,6 +172,12 @@ (field-handle-owner f) (current-actor))) +;; Parameterof (Actor Exn -> Void) +(define current-actor-crash-logger + (make-parameter + (lambda (a e) + (log-error "Actor ~a died with exception:\n~a" a (exn->string e))))) + ;; Parameterof Actor (define current-actor (make-parameter #f)) @@ -259,7 +268,7 @@ (in-script? script?)) (with-handlers ([(lambda (e) (not (exn:break? e))) (lambda (e) - (log-error "Actor ~a died with exception:\n~a" a (exn->string e)) + ((current-actor-crash-logger) a e) (abandon-queued-work! a) (terminate-actor! a))]) ;; TODO: tracing (call-with-syndicate-prompt diff --git a/syndicate/syntax.rkt b/syndicate/syntax.rkt index d917504..b6fbed5 100644 --- a/syndicate/syntax.rkt +++ b/syndicate/syntax.rkt @@ -581,267 +581,354 @@ ;;--------------------------------------------------------------------------- (module+ test - (message-struct set-box (new-value)) - (assertion-struct box-state (value)) + (require "test.rkt") - (define ds - (make-dataspace - (lambda () - (schedule-script! - (current-actor) + (test-case + ;; See .../syndicate/examples/actor/example-during-criterion-shapshotting.rkt + [(struct foo (x y) #:prefab) + (spawn (field [x 123]) + (assert (foo (x) 999)) + (during (foo (x) $v) + (printf "x=~a v=~a\n" (x) v) + (when (= (x) 123) (x 124)) + (on-stop (printf "finally for x=~a v=~a\n" (x) v))))] + no-crashes + (expected-output "x=123 v=999" + "x=124 v=999" + "finally for x=124 v=999")) - #;(lambda () - ;; See .../syndicate/examples/actor/example-during-criterion-shapshotting.rkt + (test-case + ;; Goal: no matter the circumstances (e.g. exception in a + ;; stop script), we will never retract an assertion more or + ;; fewer than the correct number of times. + [(spawn #:name 'supply + #:assertions ['marker] + (assert 'marker) ;; NB this is the change wrt the test case immediately below + (error 'test-case "Deliberate error")) - (struct foo (x y) #:prefab) + (spawn (on (asserted 'marker) (printf "marker appeared\n")) + (on (retracted 'marker) (printf "marker disappeared\n")))] + (it "should crash deliberately" (actor-died? 'supply "Deliberate error")) + (expected-output "marker appeared" + "marker disappeared")) - (spawn (field [x 123]) - (assert (foo (x) 999)) - (during (foo (x) $v) - (log-info "x=~a v=~a" (x) v) - (when (= (x) 123) (x 124)) - (on-stop - (log-info "finally for x=~a v=~a" (x) v)))) - ) + (test-case + ;; Goal: no matter the circumstances (e.g. exception in a + ;; stop script), we will never retract an assertion more or + ;; fewer than the correct number of times. + [(spawn #:name 'supply + #:assertions ['marker] + (error 'test-case "Deliberate error")) - #;(lambda () - ;; Goal: no matter the circumstances (e.g. exception in a - ;; stop script), we will never retract an assertion more or - ;; fewer than the correct number of times. + (spawn (on (asserted 'marker) (printf "marker appeared\n")) + (on (retracted 'marker) (printf "marker disappeared\n")))] + (it "should crash deliberately" (actor-died? 'supply "Deliberate error")) + (expected-output "marker appeared" + "marker disappeared")) - ;; Expected output: - ;; - ;; marker appeared - ;; marker disappeared - ;; - ;; (plus the exception report for "Deliberate error") + (test-case + ;; Goal: no matter the circumstances (e.g. exception in a + ;; stop script), we will never retract an assertion more or + ;; fewer than the correct number of times. + ;; + ;; Test cleanup after exception in stop script + [(assertion-struct layer (name)) + (spawn #:name 'crasher + (define root-facet (current-facet)) + (assert (layer 'outer)) + (on-start (react (assert (layer 'middle)) + (on-start (flush!) (flush!) (stop-facet root-facet)) + (on-stop (/ 1 0)) + (on-start (react (assert (layer 'inner))))))) + (spawn (on (retracted (layer $x)) (printf "~a gone\n" x)))] + (it "should crash with division by zero" (actor-died? 'crasher "division by zero")) + ;; a permutation of these lines is acceptable: + (expected-output "middle gone" + "inner gone" + "outer gone")) - (spawn #:assertions ['marker] - (assert 'marker) ;; NB this is the change wrt the test case immediately below - (error 'test-case "Deliberate error") - (void)) + (begin ;; Suite of four related tests. + ;; See .../syndicate/examples/actor/example-responsibility-transfer-2.rkt - (spawn (on (asserted 'marker) (printf "marker appeared\n")) - (on (retracted 'marker) (printf "marker disappeared\n"))) - ) + (test-case + [(spawn #:name 'factory-1 + (on (asserted (list 'X 1)) + (spawn #:name 'service-1 + #:assertions [(observe (list 'X 1))] ;; (A) + (stop-when (retracted (list 'X 1))) ;; (B) + (on (message 'dummy))) + (stop-current-facet))) + (spawn (on (asserted (observe (list 'X $supplier))) + (printf "Supply ~v asserted.\n" supplier) + (assert! (list 'X supplier))) + (on (retracted (observe (list 'X $supplier))) + (printf "Supply ~v retracted.\n" supplier)))] + no-crashes + (expected-output "Supply 1 asserted.")) - #;(lambda () - ;; Goal: no matter the circumstances (e.g. exception in a - ;; stop script), we will never retract an assertion more or - ;; fewer than the correct number of times. + (test-case + [(spawn #:name 'factory-1 + (on (asserted (list 'X 1)) + (spawn #:name 'service-1 + ;; #:assertions [(observe (list 'X 1))] ;; (A) + (stop-when (retracted (list 'X 1))) ;; (B) + (on (message 'dummy))) + (stop-current-facet))) + (spawn (on (asserted (observe (list 'X $supplier))) + (printf "Supply ~v asserted.\n" supplier) + (assert! (list 'X supplier))) + (on (retracted (observe (list 'X $supplier))) + (printf "Supply ~v retracted.\n" supplier)))] + no-crashes + (expected-output "Supply 1 asserted." + "Supply 1 retracted." + "Supply 1 asserted.")) - ;; Expected output: - ;; - ;; marker appeared - ;; marker disappeared - ;; - ;; (plus the exception report for "Deliberate error") + (test-case + [(spawn #:name 'factory-1 + (on (asserted (list 'X 1)) + (spawn #:name 'service-1 + #:assertions [(observe (list 'X 1))] ;; (A) + ;; (stop-when (retracted (list 'X 1))) ;; (B) + (on (message 'dummy))) + (stop-current-facet))) + (spawn (on (asserted (observe (list 'X $supplier))) + (printf "Supply ~v asserted.\n" supplier) + (assert! (list 'X supplier))) + (on (retracted (observe (list 'X $supplier))) + (printf "Supply ~v retracted.\n" supplier)))] + no-crashes + (expected-output "Supply 1 asserted." + "Supply 1 retracted.")) - (spawn #:assertions ['marker] - (error 'test-case "Deliberate error") - (void)) + (test-case + [(spawn #:name 'factory-1 + (on (asserted (list 'X 1)) + (spawn #:name 'service-1 + ;; #:assertions [(observe (list 'X 1))] ;; (A) + ;; (stop-when (retracted (list 'X 1))) ;; (B) + (on (message 'dummy))) + (stop-current-facet))) + (spawn (on (asserted (observe (list 'X $supplier))) + (printf "Supply ~v asserted.\n" supplier) + (assert! (list 'X supplier))) + (on (retracted (observe (list 'X $supplier))) + (printf "Supply ~v retracted.\n" supplier)))] + no-crashes + (expected-output "Supply 1 asserted." + "Supply 1 retracted.")) - (spawn (on (asserted 'marker) (printf "marker appeared\n")) - (on (retracted 'marker) (printf "marker disappeared\n"))) - ) + ) - #;(lambda () - ;; Goal: no matter the circumstances (e.g. exception in a - ;; stop script), we will never retract an assertion more or - ;; fewer than the correct number of times. + (test-case + ;; See .../syndicate/examples/actor/example-responsibility-transfer-1.rkt + [(spawn #:name 'demand-watcher + (during/spawn 'demand + #:name (gensym 'intermediate-demand-asserter) + (assert 'intermediate-demand))) - ;; Test cleanup after exception in stop script - ;; - ;; Correct output: - ;; - ;; inner gone - ;; outer gone - ;; middle gone - ;; - ;; (or some permutation of these) plus an exception report for the division by zero. + (spawn #:name 'intermediate-demand-watcher + (during/spawn 'intermediate-demand + #:name (gensym 'supply-asserter) + (assert 'supply))) - (assertion-struct layer (name)) + (spawn* #:name 'driver + (react (on (asserted 'supply) (displayln "Supply asserted.")) + (on (retracted 'supply) (displayln "Supply retracted."))) + (until (asserted (observe 'demand))) + (displayln "Asserting demand.") + (assert! 'demand) + (until (asserted 'supply)) + (displayln "Glitching demand.") + (retract! 'demand) + (flush!) + (assert! 'demand) + (displayln "Demand now steady."))] + no-crashes + (expected-output "Asserting demand." + "Supply asserted." + "Glitching demand." + "Demand now steady." + "Supply retracted." + "Supply asserted.")) - (spawn (define root-facet (current-facet)) - (assert (layer 'outer)) - (on-start (react (assert (layer 'middle)) - (on-start (flush!) (flush!) (stop-facet root-facet)) - (on-stop (/ 1 0)) - (on-start (react (assert (layer 'inner))))))) + (test-case + [(assertion-struct one-plus (n m)) + (spawn #:name 'add1-server + (during/spawn (observe (one-plus $n _)) + #:name (list 'solving 'one-plus n) + (assert (one-plus n (+ n 1))))) + (spawn #:name 'client-process + (stop-when (asserted (one-plus 3 $value)) + (printf "1 + 3 = ~a\n" value)))] + no-crashes + (expected-output "1 + 3 = 4")) - (spawn (on (retracted (layer $x)) (printf "~a gone\n" x)))) + (test-case + ;; .../racket/syndicate/examples/actor/example-partial-retraction.rkt + [(struct ready (what) #:prefab) + (struct entry (key val) #:prefab) - #;(lambda () - ;; See .../syndicate/examples/actor/example-responsibility-transfer-2.rkt + (spawn (assert (ready 'listener)) + (on (asserted (entry $key _)) + (printf "key ~v asserted\n" key) + (until (retracted (entry key _)) + (on (asserted (entry key $value)) + (printf "add binding: ~v -> ~v\n" key value)) + (on (retracted (entry key $value)) + (printf "del binding: ~v -> ~v\n" key value))) + (printf "key ~v retracted\n" key))) - (spawn #:name 'factory-1 - (on (asserted (list 'X 1)) - (spawn #:name 'service-1 - #:assertions [(observe (list 'X 1))] ;; (A) - (stop-when (retracted (list 'X 1))) ;; (B) - (on (message 'dummy))) ;; exists just to keep the service alive if - ;; there are no other endpoints - ;; spawn executes *before* teardown of this on-asserted endpoint, and thus - ;; before the patch withdrawing (observe (list 'X 1)). - (stop-current-facet))) + (spawn (assert (ready 'other-listener)) + (during (entry $key _) + (printf "(other-listener) key ~v asserted\n" key) + (on-stop (printf "(other-listener) key ~v retracted\n" key)) + (during (entry key $value) + (printf "(other-listener) ~v ---> ~v\n" key value) + (on-stop (printf "(other-listener) ~v -/-> ~v\n" key value))))) - (spawn (on (asserted (observe (list 'X $supplier))) - (printf "Supply ~v asserted.\n" supplier) - (assert! (list 'X supplier))) - (on (retracted (observe (list 'X $supplier))) - (printf "Supply ~v retracted.\n" supplier))) - ) + (define (pause) + (displayln "pause") + (define token (gensym 'pause)) ;; FIXME:: If we use the same token every time, need epochs! + (until (asserted (ready token)) + (assert (ready token)))) - #;(lambda () - ;; See .../syndicate/examples/actor/example-responsibility-transfer-1.rkt + (spawn* (until (asserted (ready 'listener))) + (until (asserted (ready 'other-listener))) + (assert! (entry 'a 1)) + (assert! (entry 'a 2)) + (assert! (entry 'b 3)) + (assert! (entry 'c 33)) + (assert! (entry 'a 4)) + (assert! (entry 'a 5)) + (pause) + (retract! (entry 'a 2)) + (retract! (entry 'c 33)) + (assert! (entry 'a 9)) + (pause) + (local-require "bag.rkt") + (for [(a (in-bag (current-adhoc-assertions)))] + (local-require racket/match) + (match a + [(entry 'a _) (retract! a)] + [_ (void)])) + ;; ^ (retract! (entry 'a ?)) + (pause))] + no-crashes + ;; Within the following, some permutations are acceptable: + #;(expected-output + "pause" + "pause" + "(other-listener) key 'a asserted" + "(other-listener) key 'c asserted" + "(other-listener) key 'b asserted" + "(other-listener) 'a ---> 4" + "(other-listener) 'a ---> 1" + "(other-listener) 'a ---> 2" + "(other-listener) 'a ---> 5" + "(other-listener) 'c ---> 33" + "(other-listener) 'b ---> 3" + "key 'a asserted" + "key 'c asserted" + "key 'b asserted" + "add binding: 'a -> 4" + "add binding: 'a -> 1" + "add binding: 'a -> 2" + "add binding: 'a -> 5" + "add binding: 'c -> 33" + "add binding: 'b -> 3" + "pause" + "del binding: 'a -> 2" + "del binding: 'c -> 33" + "add binding: 'a -> 9" + "key 'c retracted" + "(other-listener) 'a ---> 9" + "(other-listener) 'a -/-> 2" + "(other-listener) 'c -/-> 33" + "(other-listener) key 'c retracted" + "del binding: 'a -> 1" + "del binding: 'a -> 9" + "del binding: 'a -> 5" + "del binding: 'a -> 4" + "key 'a retracted" + "(other-listener) 'a -/-> 1" + "(other-listener) 'a -/-> 9" + "(other-listener) 'a -/-> 5" + "(other-listener) 'a -/-> 4" + "(other-listener) key 'a retracted" + "del binding: 'b -> 3" + "key 'b retracted" + "(other-listener) 'b -/-> 3" + "(other-listener) key 'b retracted" + )) - (spawn #:name 'demand-watcher - (during/spawn 'demand - #:name (gensym 'intermediate-demand-asserter) - (assert 'intermediate-demand))) + (let ((N 100000)) + (test-case + ;; Rough message send speed test + [(spawn (on (message $v) + (if (= v N) + (stop-current-facet) + (send! (+ v 1)))) + (on-start (send! 0)))] + no-crashes + (it "should be fairly quick" + (log-info "Rough message send speed: ~a msgs in ~a ms = ~a Hz" + N + (test-run-time) + (/ N (/ (test-run-time) 1000.0)))))) - (spawn #:name 'intermediate-demand-watcher - (during/spawn 'intermediate-demand - #:name (gensym 'supply-asserter) - (assert 'supply))) + (test-case + ;; Tests that pending actions are abandoned during a turn in which there is an exception + [(message-struct stage (n)) - (spawn* #:name 'driver - (react (on (asserted 'supply) (log-info "Supply asserted.")) - (on (retracted 'supply) (log-info "Supply retracted."))) - (until (asserted (observe 'demand))) - (log-info "Asserting demand.") - (assert! 'demand) - (until (asserted 'supply)) - (log-info "Glitching demand.") - (retract! 'demand) - (flush!) - (assert! 'demand) - (log-info "Demand now steady.")) - ) + (spawn #:name 'actor0 + (on (message (stage 0)) + (send! (stage 1))) - #;(lambda () - ;; Trivial example program to demonstrate tracing + (on (message (stage 2)) + (send! (stage 3)) + (/ 1 0) + (send! (stage 3)))) - (assertion-struct one-plus (n m)) + (spawn #:name 'main + (on (message (stage $v)) + (printf "Got message ~v\n" v)) + (on-start + (until (asserted (observe (stage 0)))) + (send! (stage 0)) + (until (message (stage 1))) + (send! (stage 2))))] + (it "should involve one crash" (actor-died? 'actor0 "division by zero")) + (expected-output "Got message 0" + "Got message 1" + "Got message 2")) - (spawn #:name 'add1-server - (during/spawn (observe (one-plus $n _)) - #:name (list 'solving 'one-plus n) - (assert (one-plus n (+ n 1))))) + (test-case + ;; Box-and-client + [(message-struct set-box (new-value)) + (assertion-struct box-state (value)) - (spawn #:name 'client-process - (stop-when (asserted (one-plus 3 $value)) - (printf "1 + 3 = ~a\n" value))) - ) + (spawn (field [current-value 0]) + (assert (box-state (current-value))) + (stop-when-true (= (current-value) 3) + (displayln "box: terminating")) + (on (message (set-box $new-value)) + (printf "box: taking on new-value ~v\n" new-value) + (current-value new-value))) - #;(lambda () - ;; .../racket/syndicate/examples/actor/example-partial-retraction.rkt - ;; - (struct ready (what) #:prefab) - (struct entry (key val) #:prefab) + (spawn (stop-when (retracted (observe (set-box _))) + (displayln "client: box has gone")) + (on (asserted (box-state $v)) + (printf "client: learned that box's value is now ~v\n" v) + (send! (set-box (+ v 1)))))] + no-crashes + (expected-output "client: learned that box's value is now 0" + "box: taking on new-value 1" + "client: learned that box's value is now 1" + "box: taking on new-value 2" + "client: learned that box's value is now 2" + "box: taking on new-value 3" + "box: terminating" + "client: box has gone")) - (spawn (assert (ready 'listener)) - (on (asserted (entry $key _)) - (log-info "key ~v asserted" key) - (until (retracted (entry key _)) - (on (asserted (entry key $value)) - (log-info "add binding: ~v -> ~v" key value)) - (on (retracted (entry key $value)) - (log-info "del binding: ~v -> ~v" key value))) - (log-info "key ~v retracted" key))) - - (spawn (assert (ready 'other-listener)) - (during (entry $key _) - (log-info "(other-listener) key ~v asserted" key) - (on-stop (log-info "(other-listener) key ~v retracted" key)) - (during (entry key $value) - (log-info "(other-listener) ~v ---> ~v" key value) - (on-stop (log-info "(other-listener) ~v -/-> ~v" key value))))) - - (define (pause) - (log-info "pause") - (define token (gensym 'pause)) ;; FIXME:: If we use the same token every time, need epochs! - (until (asserted (ready token)) - (assert (ready token)))) - - (spawn* (until (asserted (ready 'listener))) - (until (asserted (ready 'other-listener))) - (assert! (entry 'a 1)) - (assert! (entry 'a 2)) - (assert! (entry 'b 3)) - (assert! (entry 'c 33)) - (assert! (entry 'a 4)) - (assert! (entry 'a 5)) - (pause) - (retract! (entry 'a 2)) - (retract! (entry 'c 33)) - (assert! (entry 'a 9)) - (pause) - (local-require "bag.rkt") - (for [(a (in-bag (current-adhoc-assertions)))] - (local-require racket/match) - (match a - [(entry 'a _) (retract! a)] - [_ (void)])) - ;; ^ (retract! (entry 'a ?)) - (pause)) - ) - - #;(lambda () - (spawn (on (message $v) - (if (= v 10000000) - (stop-current-facet) - (send! (+ v 1)))) - (on-start (send! 0))) - ) - - #;(lambda () - - (message-struct stage (n)) - - (spawn #:name 'actor0 - (on (message (stage 0)) - (send! (stage 1))) - - (on (message (stage 2)) - (send! (stage 3)) - (/ 1 0) - (send! (stage 3)))) - - (spawn #:name 'main - (on (message (stage $v)) - (printf "Got message ~v\n" v)) - (on-start - (until (asserted (observe (stage 0)))) - (send! (stage 0)) - (until (message (stage 1))) - (send! (stage 2)))) - ) - - (lambda () - (spawn (field [current-value 0]) - (assert (box-state (current-value))) - (stop-when-true (= (current-value) 10) - (log-info "box: terminating")) - (on (message (set-box $new-value)) - (log-info "box: taking on new-value ~v" new-value) - (current-value new-value))) - - (spawn (stop-when (retracted (observe (set-box _))) - (log-info "client: box has gone")) - (on (asserted (box-state $v)) - (log-info "client: learned that box's value is now ~v" v) - (send! (set-box (+ v 1))))) - ) - )))) - - (require racket/pretty) - ;; (pretty-print ds) - (#;time values - (let loop ((i 0)) - ;; (printf "--- i = ~v\n" i) - (when (run-scripts! ds) - ;; (pretty-print ds) - (loop (+ i 1))))) - ;; (pretty-print ds) ) diff --git a/syndicate/test.rkt b/syndicate/test.rkt new file mode 100644 index 0000000..a14a46e --- /dev/null +++ b/syndicate/test.rkt @@ -0,0 +1,196 @@ +#lang racket/base +;; Test drivers for Syndicate implementation. + +(provide collected-events + collected-output + collected-lines + final-dataspace + final-assertions + test-run-time + test-gc-time + + asserted? + emitted? + actor-died? + emit! + test-case + it + + no-crashes + expected-output + + run-syndicate-test! + log-test-result!) + +(require racket/exn) +(require racket/match) +(require (only-in racket/string string-split string-join string-contains?)) + +(require "bag.rkt") +(require "dataspace.rkt") + +(require (for-syntax racket/base)) +(require (for-syntax syntax/srcloc)) + +(define-logger syndicate/test) + +(define event-accumulator (make-parameter #f)) +(define exn-accumulator (make-parameter #f)) +(define failure-detected? (make-parameter #f)) + +(define collected-events (make-parameter '())) +(define collected-exns (make-parameter '())) +(define collected-output (make-parameter "")) +(define collected-lines (make-parameter '())) +(define final-dataspace (make-parameter #f)) +(define test-run-time (make-parameter 0)) +(define test-gc-time (make-parameter 0)) + +(define (asserted? v) + (bag-member? (dataspace-assertions (final-dataspace)) v)) + +(define (final-assertions) + (bag->set (dataspace-assertions (final-dataspace)))) + +(define (emitted? v) + (member v (collected-events))) + +(define (actor-died? name [substr-or-regex ""]) + (define entry (findf (lambda (e) (equal? name (actor-name (car e)))) (collected-exns))) + (and entry + (let ((message (exn-message (cadr entry)))) + (match substr-or-regex + [(? string? substr) (string-contains? message substr)] + [(? regexp? re) (regexp-match? re message)])))) + +(define (emit! event) + (define b (event-accumulator)) + (set-box! b (cons event (unbox b)))) + +(define-syntax (test-case stx) + (syntax-case stx () + [(_ [body ...] checks ...) + (quasisyntax/loc stx + (run-syndicate-test! + #,(source-location->string stx) + (lambda () body ...) + (list checks ...)))])) + +(define (not-break? e) + (not (exn:break? e))) + +(struct check (location description thunk) #:prefab) + +(define (run-syndicate-test! location-str body-thunk list-of-checks) + (parameterize ((failure-detected? #f)) + (define events-box (box '())) + (define exns-box (box '())) + (define (get-items b) (reverse (unbox b))) + (define op (open-output-string)) + (with-handlers ([not-break? (lambda (e) (log-test-result! location-str #f e))]) + (log-syndicate/test-info "~a--- Running test at ~a~a" + (color YELLOW) + location-str + (color NORMAL)) + (define ds (make-dataspace + (lambda () + (schedule-script! (current-actor) body-thunk)))) + (define-values (_results cpu-ms _wall-ms gc-ms) + (parameterize ((current-output-port op) + (current-actor-crash-logger + (lambda (a e) + (set-box! exns-box (cons (list a e) (unbox exns-box))))) + (event-accumulator events-box)) + (time-apply (lambda () (let loop () (when (run-scripts! ds) (loop)))) + '()))) + (define op-string (get-output-string op)) + (parameterize ((collected-events (get-items events-box)) + (collected-exns (get-items exns-box)) + (collected-output op-string) + (collected-lines (string-split op-string "\n")) + (final-dataspace ds) + (test-run-time cpu-ms) + (test-gc-time gc-ms)) + (for [(check (in-list list-of-checks))] + (with-handlers ([not-break? (lambda (e) (log-test-result! location-str check e))]) + (match check + [(? procedure?) + (log-test-result! location-str check (check))] + [(? check?) + (log-test-result! location-str check ((check-thunk check)))]))))) + (when (failure-detected?) + (log-syndicate/test-debug "Collected events:") + (for [(e (get-items events-box))] + (log-syndicate/test-debug " ~v" e)) + (log-syndicate/test-debug "Collected output:") + (for [(l (string-split (get-output-string op) "\n"))] + (log-syndicate/test-debug " ~a" l)) + (log-syndicate/test-debug "Crashed actors:") + (for [(entry (get-items exns-box))] + (match-define (list a e) entry) + (log-syndicate/test-debug " ~a\n ~a" + a + (string-join (string-split (exn->string e) "\n") + " \n")))))) + +(define-syntax (it stx) + (syntax-case stx () + [(_ description body ...) + (quasisyntax/loc stx + (check #,(source-location->string stx) + description + (lambda () + (and body ...))))])) + +(define no-crashes (it "shouldn't involve any crashing actors" + (null? (collected-exns)))) + +(define-syntax (expected-output stx) + (syntax-case stx () + [(_ line ...) + (quasisyntax/loc stx + (it "should produce correct output" + (equal? (collected-lines) + (list line ...))))])) + +(define RED ";31") +(define BRIGHT-RED ";1;31") +(define GREEN ";32") +(define BRIGHT-GREEN ";1;32") +(define YELLOW ";33") +(define NORMAL "") + +(define (color c) (format "\e[0~am" c)) + +(define (log-test-result! test-loc maybe-check result) + (if (not maybe-check) + (begin + (failure-detected? #t) + (log-syndicate/test-error "~a ✗ Exception running program under test:\n~a~a" + (color BRIGHT-RED) + (exn->string result) + (color NORMAL))) + (let ((description + (match maybe-check + [(? procedure?) (format "~a" check)] + [(check #f description _thunk) (format "~a" description)] + [(check check-loc description _thunk) (format "~a (~a)" description check-loc)]))) + (match result + [(? exn?) + (failure-detected? #t) + (log-syndicate/test-error "~a ✗ ... ~a:\n~a~a" + (color RED) + description + (exn->string result) + (color NORMAL))] + [#f + (failure-detected? #t) + (log-syndicate/test-error "~a ✗ ... ~a~a" + (color RED) + description + (color NORMAL))] + [_ + (log-syndicate/test-info "~a ✓ ... ~a~a" + (color GREEN) + description + (color NORMAL))]))))