racket/collects/handin-server/checker.rkt
2010-11-09 20:13:07 -05:00

772 lines
34 KiB
Racket

#lang racket/base
(require (for-syntax racket/base) "utils.rkt"
racket/file racket/class racket/gui/base)
(provide (except-out (all-from-out racket/base) #%module-begin)
(all-from-out "utils.rkt"))
(provide (rename-out [module-begin~ #%module-begin]))
(define-syntax (module-begin~ stx)
(let ([e (if (syntax? stx) (syntax-e stx) stx)])
(if (pair? e)
(with-syntax ([user-pre (datum->syntax stx 'user-pre stx)]
[user-post (datum->syntax stx 'user-post stx)])
(datum->syntax
(quote-syntax here)
(list* (quote-syntax #%plain-module-begin)
#'(define user-pre #f)
#'(define user-post #f)
(cdr e))
stx))
(raise-syntax-error #f "bad syntax" stx))))
(define (error* fmt . args)
(error (apply format fmt args)))
(define fields (map car (get-conf 'extra-fields)))
(provide submission-dir)
(define submission-dir-re
(regexp (string-append "[/\\]([^/\\]+)[/\\](?:[^/\\]+)[/\\]"
"(?:SUCCESS-[0-9]+|ATTEMPT)[/\\]?$")))
(define (submission-dir)
(let ([m (regexp-match submission-dir-re
(path->string (current-directory)))])
(if m
(cadr m)
(error* "internal error: unexpected directory name: \"~a\""
(current-directory)))))
(provide user-data)
(define (user-data user)
;; the student is always assumed to exist
(cdr (get-preference (if (string? user) (string->symbol user) user)
(lambda () #f) 'timestamp
(build-path server-dir "users.rktd"))))
(provide user-substs)
(define (user-substs user str)
(subst str `(("username" . ,user) ("submission" . ,submission-dir)
,@(map cons fields (user-data user)))))
(define (subst str substs)
(if (list? str)
(map (lambda (x) (subst x substs)) str)
(let* ([m (regexp-match-positions #rx"{([^{}]+)}" str)]
[s (and m (substring str (caadr m) (cdadr m)))])
(if m
(subst (string-append
(substring str 0 (caar m))
(cond [(assoc s substs)
=> (lambda (x)
(let ([s (cdr x)])
(if (procedure? s) (s) s)))]
[else (error 'subst "unknown substitution: ~s" s)])
(substring str (cdar m)))
substs)
str))))
(define (verify-line-length line n len)
(define (string-width str)
(let loop ([l (string->list str)] [w 0])
(if (null? l)
w
(loop (cdr l)
(if (char=? #\tab (car l)) (+ 8 (- w (modulo w 8))) (add1 w))))))
(unless (<= (bytes-length line) len)
(let ([line (bytes->string/utf-8 line)])
(unless (or (< (string-length line) len)
(< (string-width line) len))
(error* "~a \"~a\" in ~a is longer than ~a characters"
(if n (format "Line #~a" n) "The line")
(regexp-replace #rx"^[ \t]*(.*?)[ \t]*$" line "\\1")
(currently-processed-file-name)
len)))))
;; ============================================================================
;; Text conversion
;; Code that turns binary stuff into text is split into three places:
;; * input-port->text-input-port implements a simple generic textualization
;; filter
;; * snip->text is used earlier in the process, where comment-box text is still
;; available
(require framework ; for drracket snips, used below
mrlib/matrix-snip) ; avoid errors from files with matrix snips
;; input-port->text-input-port : input-port (any -> any) -> input-port
;; the `filter' function is applied to special values; the filter result is
;; `display'ed into the stream in place of the special
(define (input-port->text-input-port src . filter)
;; note that snip->text below already takes care of some snips
(define (item->text x)
(cond [(is-a? x snip%)
(format "~a" (or (send x get-text 0 (send x get-count) #t) x))]
[(special-comment? x)
(format "#| ~a |#" (special-comment-value x))]
[(syntax? x) (syntax->datum x)]
[else x]))
(let-values ([(filter) (if (pair? filter) (car filter) item->text)]
[(in out) (make-pipe 4096)])
(thread
(lambda ()
(let ([s (make-bytes 4096)])
(let loop ()
(let ([c (read-bytes-avail! s src)])
(cond [(number? c) (write-bytes s out 0 c) (loop)]
[(procedure? c)
(let ([v (let-values ([(l col p) (port-next-location src)])
(c (object-name src) l col p))])
(display (filter v) out))
(loop)]
[else (close-output-port out)])))))) ; Must be EOF
in))
(define (snip->text x)
(let ([name (and (is-a? x snip%)
(send (send x get-snipclass) get-classname))])
(cond [(equal? name "wximage") "{{IMAGE}}"]
[(regexp-match? #rx"(lib \"comment-snip.(?:rkt|ss)\" \"framework\")"
name)
;; comments will have ";" prefix on every line, and "\n" suffix
(format ";{{COMMENT:\n~a;}}\n"
(send x get-text 0 (send x get-count)))]
[else x])))
(define (untabify str)
(let loop ([idx 0] [pos 0] [strs '()])
(let ([tab (regexp-match-positions #rx"\t" str idx)])
(if tab
(let* ([pos (+ pos (- (caar tab) idx))]
[newpos (* (add1 (quotient pos 8)) 8)])
(loop (cdar tab) newpos
(list* (make-bytes (- newpos pos) 32)
(subbytes str idx (caar tab))
strs)))
(apply bytes-append (reverse (cons (subbytes str idx) strs)))))))
(define current-processed-file ; set when processing multi-file submissions
(make-parameter #f))
(define (currently-processed-file-name)
(let ([c (current-processed-file)])
(if c (format "\"~a\"" c) "your code")))
(define (input->process->output maxwidth textualize? untabify? bad-re)
(let loop ([n 1])
(let ([line (if textualize?
(read-bytes-line (current-input-port) 'any)
(with-handlers ([void
(lambda (e)
(error* "The submission must not ~a"
"have non-textual items"))])
(read-bytes-line (current-input-port) 'any)))])
(unless (eof-object? line)
(let* ([line (regexp-replace #rx#"[ \t]+$" line #"")]
[line (if (and untabify? (regexp-match? #rx"\t" line))
(untabify line) line)])
(when (and bad-re (regexp-match? bad-re line))
(error* "You cannot use \"~a\" in ~a!~a"
(if (regexp? bad-re) (object-name bad-re) bad-re)
(currently-processed-file-name)
(if textualize? "" (format " (line ~a)" n))))
(when maxwidth
(verify-line-length line (and (not textualize?) n) maxwidth))
(display line) (newline) (loop (add1 n)))))))
(define (submission->bytes submission maxwidth textualize? untabify?
markup-prefix bad-re)
(define magic #rx#"^(?:#reader[(]lib\"read.(?:rkt|ss)\"\"wxme\"[)])?WXME")
(unless (regexp-match? magic submission)
(error* "bad submission format, expecting a single DrRacket submission"))
(let-values ([(defs inters) (unpack-submission submission)])
(parameterize ([current-input-port
(if textualize?
(input-port->text-input-port
(open-input-text-editor defs 0 'end snip->text))
(open-input-text-editor defs))]
[current-output-port (open-output-bytes)])
(input->process->output maxwidth textualize? untabify? bad-re)
(get-output-bytes (current-output-port)))))
;; ------------------------------------------------
;; This code will hack textualization of text boxes
(define (insert-to-editor editor . xs)
(for ([x (in-list xs)])
(send editor insert (if (string? x) x (make-object editor-snip% x)))))
;; support for "text-box%"
(define text-box-sc
(new (class snip-class%
(define/override (read f)
(let ([text (new text-box%)]) (send text read-from-file f) text))
(super-new))))
(define text-box%
(class editor-snip%
(inherit set-snipclass get-editor)
(define text (new text%))
(define/public (read-from-file f)
(unless (eq? 1 (send text-box-sc reading-version f)) (error "BOOM"))
(send text read-from-file f))
(super-new)
(set-snipclass text-box-sc)
(insert-to-editor (get-editor) "{{TEXT: " text "}}")))
(send text-box-sc set-classname "text-box%")
(send text-box-sc set-version 2)
(send (get-the-snip-class-list) add text-box-sc)
;; ============================================================================
;; Dealing with multi-file submissions
(define (read-multifile . port)
(define magic #"<<<MULTI-SUBMISSION-FILE>>>")
(define (assert-format b)
(unless b
(error* "bad submission format, expecting a multi-file submission -- ~a"
"use the multi-file submission tool")))
(define (read-it)
(assert-format (equal? magic (read-bytes (bytes-length magic))))
(let loop ([files '()])
(let ([f (with-handlers ([void void]) (read))])
(if (eof-object? f)
(sort files (lambda (x y) (string<? (car x) (car y))))
(loop (cons f files))))))
(let ([files (if (pair? port)
(parameterize ([current-input-port (car port)]) (read-it))
(read-it))])
(assert-format (and (list? files)
(andmap (lambda (x)
(and (list? x) (= 2 (length x))
(string? (car x)) (bytes? (cadr x))))
files)))
files))
(define ((unpack-multifile-submission names-checker raw-file-name)
submission maxwidth textualize? untabify?
markup-prefix prefix-re)
(let* ([files (read-multifile (open-input-bytes submission))]
[names (map car files)])
(cond [(ormap (lambda (f)
(and (regexp-match? #rx"^[.]|[/\\ ]" (car f)) (car f)))
files)
=> (lambda (file) (error* "bad filename: ~e" file))])
(cond [(procedure? names-checker) (names-checker names)]
[(or (regexp? names-checker)
(string? names-checker) (bytes? names-checker))
(cond [(ormap (lambda (n)
(and (not (regexp-match? names-checker n)) n))
names)
=> (lambda (file) (error* "bad filename: ~e" file))])]
[(and (list? names-checker) (andmap string? names-checker))
(let ([missing (remove* names names-checker)])
(when (pair? missing) (error* "missing files: ~e" missing)))
(let ([extra (remove* names-checker names)])
(when (pair? extra) (error* "unexpected files: ~e" extra)))]
[names-checker (error* "bad names-checker specification: ~e"
names-checker)])
;; problem: students might think that submitting files one-by-one will keep
;; them all; solution: if there is already a submission, then warn against
;; files that disappear.
(let* ([raw (build-path 'up raw-file-name)]
[old (and (file-exists? raw)
(with-handlers ([void (lambda _ #f)])
(with-input-from-file raw read-multifile)))]
[removed (and old (remove* names (map car old)))])
(when (and (pair? removed)
(not (eq? 'ok (message
(apply string-append
"The following file"
(if (pair? (cdr removed)) "s" "")
" will be lost:"
(map (lambda (n) (string-append " " n))
removed))
'(ok-cancel caution)))))
(error* "Aborting...")))
;; This will create copies of the original files
;; (for ([file (in-list files)])
;; (with-output-to-file (car file)
;; (lambda () (display (cadr file)) (flush-output))))
(let* ([pfx-len (string-length markup-prefix)]
[line-len (- maxwidth pfx-len)]
[=s (lambda (n) (if (<= 0 n) (make-string n #\=) ""))]
[=== (format "~a~a\n" markup-prefix (=s line-len))])
(define (sep name)
(newline)
(display ===)
(let ([n (/ (- line-len 4 (string-length name)) 2)])
(printf "~a~a< ~a >~a\n"
markup-prefix (=s (floor n)) name (=s (ceiling n))))
(display ===)
(newline))
(parameterize ([current-output-port (open-output-bytes)])
(for ([file (in-list files)])
(sep (car file))
(parameterize ([current-input-port (open-input-bytes (cadr file))]
[current-processed-file (car file)])
(input->process->output
maxwidth textualize? untabify? prefix-re)))
(get-output-bytes (current-output-port))))))
;; ============================================================================
;; Checker function
(provide submission-eval)
(define submission-eval (make-parameter #f))
;; without this the primitive eval is not available
(provide (rename-out [eval prim-eval]))
;; for adding lines in the checker
(define added-lines (make-thread-cell #f))
(provide add-header-line!)
(define (add-header-line! line)
(let ([new (list line)] [cur (thread-cell-ref added-lines)])
(if cur
(set-box! cur (append (unbox cur) new))
(thread-cell-set! added-lines (box new)))))
(define ((wrap-evaluator eval) expr)
(define unknown "unknown")
(define (reraise exn)
(raise
(let-values ([(struct-type skipped?) (struct-info exn)])
(if (and struct-type (not skipped?))
(let ([vals (cdr (vector->list (struct->vector exn unknown)))])
(if (memq unknown vals)
exn
(apply (struct-type-make-constructor struct-type)
(format "while evaluating ~s:\n ~a" expr (car vals))
(cdr vals))))
exn))))
(with-handlers ([exn? reraise]) (eval expr)))
(provide check:)
(define-syntax (check: stx)
(define (id s) (datum->syntax stx s stx))
(let loop ([stx (syntax-case stx () [(_ x ...) #'(x ...)])]
[keyvals '()]
[got null])
(define (get key . default)
(cond [(assq key keyvals)
=> (lambda (x) (set! got (cons x got)) (caddr x))]
[(pair? default) (car default)]
[else #f]))
(syntax-case stx ()
[(key val x ...)
(and (identifier? #'key)
(regexp-match? #rx"^:" (symbol->string (syntax-e #'key))))
(loop #'(x ...)
(cons (list (syntax-e #'key) #'key #'val) keyvals)
(cons (syntax-e #'key) got))]
[(body ...)
(with-syntax
([users* (get ':users #'#f)]
[eval?* (get ':eval? #'#t)]
[language* (get ':language #'#f)]
[requires* (get ':requires #''())]
[teachpacks* (get ':teachpacks #''())]
[create-text?* (get ':create-text? #'#t)]
[untabify?* (get ':untabify? #'#t)]
[textualize?* (get ':textualize? #'#f)]
[maxwidth* (get ':maxwidth #'79)]
[markup-prefix* (get ':markup-prefix #'#f)]
[prefix-re* (get ':prefix-re #'#f)]
[student-line*
(get ':student-line
#'"Student: {username} ({Full Name} <{Email}>)")]
[extra-lines*
(get ':extra-lines
#''("Maximum points for this assignment: <+100>"))]
[value-printer* (get ':value-printer #'#f)]
[coverage?* (get ':coverage? #'#f)]
[output* (get ':output #'"hw.rkt")]
[multi-file* (get ':multi-file #'#f)]
[names-checker* (get ':names-checker #'#f)]
[user-error-message*
(get ':user-error-message #'"Error in your code --\n~a")]
[checker (id 'checker)]
[users (id 'users)]
[submission (id 'submission)]
[eval (id 'eval)]
[with-submission-bindings (id 'with-submission-bindings)]
[user-pre (id 'user-pre)]
[user-post (id 'user-post)]
[(body ...) (syntax-case #'(body ...) ()
[() #'(void)] [_ #'(body ...)])])
(for ([x (in-list keyvals)])
(unless (memq (car x) got)
(raise-syntax-error #f "unknown keyword" stx (cadr x))))
#'(begin
(provide checker)
(define checker
(let ([allowed (let ([us users*])
(if (list? us)
(map (lambda (x)
(if (list? x)
(sort x string<?)
(list x)))
us)
us))]
[eval? eval?*]
[language language*]
[requires requires*]
[teachpacks teachpacks*]
[create-text? create-text?*]
[untabify? untabify?*]
[textualize? textualize?*]
[maxwidth maxwidth*]
[markup-prefix markup-prefix*]
[prefix-re prefix-re*]
[student-line student-line*]
[extra-lines extra-lines*]
[value-printer value-printer*]
[coverage? coverage?*]
[output-file output*]
[multi-file multi-file*]
[names-checker names-checker*]
[uem user-error-message*])
;; ========================================
;; set defaults that depend on file name
(define suffix
(let ([sfx (string->symbol
(string-downcase
(if multi-file
(format "~a" multi-file)
(and output-file
(regexp-replace
#rx"^.*[.]" output-file "")))))])
(case sfx
[(java c cc c++)
(unless markup-prefix (set! markup-prefix "//> "))
(unless prefix-re (set! prefix-re #rx"//>"))]
[else
(unless markup-prefix (set! markup-prefix ";;> "))
(unless prefix-re (set! prefix-re #rx";>"))])
sfx))
;; ========================================
;; verify submitting users
(define (pre users submission)
(set-run-status "checking submission username(s)")
(cond [(list? allowed)
(unless (member users allowed)
(error*
"You are not registered for ~a submission"
(case (length users)
[(1) "individual"]
[(2) "pair"]
[else "group"])))]
[(procedure? allowed) (allowed users)]
[(not allowed) ; default is single-user submission
(unless (= 1 (length users))
(error*
"This homework is for individual submissions"))]
[else (error* "bad user specifications")])
(when user-pre (user-pre users submission)))
;; ========================================
;; convert to text, evaluate, check
(define (check users submission)
(define text-file (format "grading/text.~a" suffix))
(define (prefix-line str)
(printf "~a~a\n" markup-prefix str))
(define generic-substs `(("submission" . ,submission-dir)))
(define (prefix-line/substs str)
(prefix-line (subst str generic-substs)))
(define (write-text)
(set-run-status "creating text file")
(with-output-to-file text-file #:exists 'truncate
(lambda ()
(for ([user (in-list users)])
(prefix-line (user-substs user student-line)))
(for-each prefix-line/substs extra-lines)
(for-each prefix-line/substs
(cond [(thread-cell-ref added-lines)
=> unbox]
[else '()]))
(display submission-text))))
(define submission-text
(and create-text?
(begin (set-run-status "reading submission")
((if multi-file
(unpack-multifile-submission
names-checker output-file)
submission->bytes)
submission maxwidth textualize? untabify?
markup-prefix prefix-re))))
(define (uem-handler e)
(let ([m (if (exn? e) (exn-message e) (format "~a" e))])
(cond
[(procedure? uem) (uem m)]
[(not (string? uem))
(error* "badly configured user-error-message")]
[(regexp-match? #rx"~[aesvAESV]" uem) (error* uem m)]
[else (error* "~a" uem)])))
(when create-text? (make-directory "grading") (write-text))
(when value-printer (current-value-printer value-printer))
(when coverage? (sandbox-coverage-enabled #t))
(set-run-status "checking submission")
(cond
[(not eval?) (let () body ...)]
[language
(let ([eval (with-handlers ([void uem-handler])
(call-with-evaluator/submission
language (append requires teachpacks)
submission values))])
(set-run-status "running tests")
(parameterize ([submission-eval (wrap-evaluator eval)])
(let-syntax ([with-submission-bindings
(syntax-rules ()
[(_ bindings body*1 body* (... ...))
(with-bindings eval bindings
body*1 body* (... ...))])])
(let () body ...))
;; will do nothing when called a second time
(when coverage? (!all-covered))
(when (thread-cell-ref added-lines) (write-text))))]
[else (error* "no language configured for submissions")])
output-file)
;; ========================================
;; indirection for user-post (may be set after `check:')
(define (post users submission)
(when user-post (user-post users submission)))
;; ========================================
;; configuration sanity checks
(let ([bad (cond [(and eval? (not language))
"`eval?' without `language'"]
[(and (not create-text?) textualize?)
"`textualize?' without `create-text?'"]
[(and maxwidth (not untabify?))
"`untabify?' without `maxwidth'"]
[(and (not eval?) coverage?)
"`coverage?' without `eval?'"]
[(and (pair? requires) (pair? teachpacks))
"`requires' and `teachpacks'"]
;; [(and textualize? coverage?)
;; "`textualize?' and `coverage?'"]
[else #f])])
(when bad
(error* "bad checker specifications: ~e" bad)))
;; ========================================
(list pre check post)))))])))
(define-syntax (with-bindings stx)
(syntax-case stx ()
[(_ get (var ...) body ...)
(with-syntax ([(>var ...)
(map (lambda (v)
(datum->syntax
v
(string->symbol
(string-append
"~" (symbol->string (syntax-e v))))
v))
(syntax->list #'(var ...)))])
#'(let ([>var (get 'var)] ...) body ...))]))
;; Similar utilities for pre- and post-checkers
(provide pre: post:)
(define-syntaxes (pre: post:)
(let ([make-pre/post:
(lambda (what)
(lambda (stx)
(define (id s) (datum->syntax stx s stx))
(syntax-case stx ()
[(_ body ...)
(with-syntax ([users (id 'users)]
[submission (id 'submission)]
[what (id what)])
#'(set! what (lambda (users submission) body ...)))])))])
(values (make-pre/post: 'user-pre) (make-pre/post: 'user-post))))
;; ============================================================================
;; User-checker utilities
;; To verify single submissions, we use a "wants-single-submission" directory
;; in the user's main submission directory to mark if already verified. If the
;; users regrets the submission, the *whole* directory is removed. This
;; assumes that there is nothing there, so this check should not be added after
;; submission has already began since students may lose work.
(define (warn-single user)
;; we're in ATTEMPT, climb up to the main directory
(parameterize ([current-directory ".."])
(unless (directory-exists? "wants-single-submission")
(if (eq? 'yes
(message
(string-append
"You have chosen to submit your work individually;"
" if you continue, it will be impossible for you to"
" later submit with a partner. Are you sure you want"
" to continue?")
'(yes-no)))
(make-directory "wants-single-submission")
(error* "Aborting single submission!")))))
(provide pairs-or-singles-with-warning)
(define (pairs-or-singles-with-warning users)
(case (length users)
[(2) #t]
[(1) (warn-single (car users))]
[else (error* "too many users in the team: ~a" users)]))
(provide teams-in-file)
(define (teams-in-file file)
(define last-time 0)
(define teams '())
(define file* (build-path server-dir file))
(define (read-teams!)
(let ([cur-time (file-or-directory-modify-seconds file*)])
(unless (equal? last-time cur-time)
(set! last-time cur-time)
(set! teams
(with-input-from-file file*
(lambda ()
(let loop ([r '()])
(let ([x (read)])
(cond [(eof-object? x) (reverse r)]
[(null? x) (loop r)]
[(list? x) (loop (cons (sort x string<?) r))]
[else (loop (cons (list x) r))])))))))))
(lambda (users)
(read-teams!)
(unless (member users teams)
(error* "You are not registered ~a for this submission"
(case (length users)
[(1) "for individual submission"]
[(2) "as a pair"]
[else "as a group"])))))
;; ============================================================================
;; Checker utilities
(define (->disp x)
(cond [(pair? x)
(if (and (eq? 'unquote (car x)) (= 2 (length x)))
(->disp (cadr x))
(cons (->disp (car x)) (->disp (cdr x))))]
[(not (symbol? x)) x]
[else (regexp-replace "^~" (symbol->string x) "") x]))
(provide procedure/arity?)
(define (procedure/arity? proc arity)
(and (procedure? proc) (procedure-arity-includes? proc arity)))
(define (get-namespace evaluator)
(call-in-sandbox-context evaluator current-namespace))
;; checks that ids are defined, either as variables or syntaxes
(provide !defined)
(define-syntax-rule (!defined id ...)
;; expected to be used only with identifiers
(begin (with-handlers ([exn:fail:contract:variable?
(lambda (_)
(error* "missing binding: ~.s" (->disp 'id)))]
[exn:fail:syntax? void])
(parameterize ([current-namespace (get-namespace (submission-eval))])
(namespace-variable-value `id)))
...))
;; checks that ids are defined as variables, not syntaxes
(provide !bound)
(define-syntax-rule (!bound id ...)
;; expected to be used only with identifiers
(begin (with-handlers ([exn:fail:contract:variable?
(lambda (_)
(error* "missing binding: ~.s" (->disp 'id)))]
[exn:fail:syntax?
(lambda (_)
(error* "bound to a syntax, expecting a value: ~.s"
(->disp 'id)))])
(parameterize ([current-namespace (get-namespace (submission-eval))])
(namespace-variable-value `id)))
...))
;; checks that ids are defined as syntaxes, not variables
(provide !syntax)
(define-syntax-rule (!syntax id ...)
;; expected to be used only with identifiers
(begin (with-handlers ([exn:fail:syntax? void]
[exn:fail:contract:variable?
(lambda (_)
(error* "missing binding: ~.s" (->disp 'id)))])
(parameterize ([current-namespace (get-namespace (submission-eval))])
(namespace-variable-value `id))
(error* "bound to a value, expecting a syntax: ~.s" (->disp 'id)))
...))
(provide !procedure* !procedure)
(define-syntax !procedure*
(syntax-rules ()
[(_ expr)
(unless (procedure? ((submission-eval) `expr))
(error* "~.s is expected to be bound to a procedure" (->disp 'expr)))]
[(_ expr arity)
(let ([ar arity]
[val ((submission-eval) `expr)])
(unless (procedure? val)
(error* "~.s is expected to be bound to a procedure" (->disp 'expr)))
(unless (procedure-arity-includes? val ar)
(error* "~.s is expected to be bound to a procedure of ~s arguments"
(->disp 'expr) ar)))]))
(define-syntax !procedure
(syntax-rules ()
[(_ id) (begin (!defined id) (!procedure* id))]
[(_ id arity) (begin (!defined id) (!procedure* id arity))]))
(provide !integer* !integer)
(define-syntax-rule (!integer* expr)
(unless (integer? ((submission-eval) `expr))
(error* "~.s is expected to be bound to an integer" (->disp 'expr))))
(define-syntax-rule (!integer id)
(begin (!defined id) (!integer* id)))
(provide !eval)
(define-syntax-rule (!eval expr) ((submission-eval) `expr))
(provide !test)
(define-syntax !test
(syntax-rules ()
[(_ expr)
(unless ((submission-eval) `expr)
(error* "your code failed a test: ~.s is false" (->disp 'expr)))]
[(_ expr result) (!test expr result equal?)]
[(_ expr result equal?)
(let ([val ((submission-eval) `expr)])
(unless (equal? result val)
(error* "your code failed a test: ~.s evaluated to ~e, expecting ~e"
(->disp 'expr) (->disp val) (->disp result))))]))
(provide !test/exn)
(define-syntax (!test/exn stx)
(syntax-case stx ()
[(_ test-exp)
#`(unless (with-handlers ([exn:fail? (lambda (exn) #t)])
((submission-eval) `test-exp)
#f)
(error* "expected exception on test expression: ~v"
(->disp 'test-exp)))]))
(provide !all-covered)
(define coverage-checked (make-thread-cell #f))
(define (!all-covered . proc)
(define uncovered (get-uncovered-expressions (submission-eval)))
(define (handler loc)
(error* "your code is not completely covered by tests: ~a ~a ~s"
"uncovered expression at" loc proc))
(cond
[(thread-cell-ref coverage-checked) #f]
[(pair? uncovered)
(let* ([stx (car uncovered)]
[loc
(cond [(not stx) #f]
[(and (syntax-line stx) (syntax-column stx))
(format "~a:~a" (syntax-line stx) (syntax-column stx))]
[(syntax-position stx) => (lambda (p) (format "#~a" p))]
[else "(unknown location)"])])
(when loc
(thread-cell-set! coverage-checked #t)
((if (pair? proc) (car proc) handler) loc)))]
[(null? uncovered) #f]
[else (error* "bad checker: no coverage information for !all-covered")]))