408 lines
18 KiB
Racket
408 lines
18 KiB
Racket
#lang scribble/doc
|
|
@(require "common.ss")
|
|
|
|
@(define textoption
|
|
@t{(Effective only when saving a textual version of
|
|
the submission files: when @scheme[:create-text?] is on.)})
|
|
|
|
@title{Checker}
|
|
|
|
@defmodulelang[handin-server/checker]{
|
|
|
|
The @schememodname[handin-server/checker] module provides a
|
|
higher-level of utilities, helpful in implementing `checker' functions
|
|
that are intended for a more automated system. This module is a
|
|
language module---a typical checker that uses it looks like this:
|
|
|
|
@schemeblock[
|
|
(module checker handin-server/checker
|
|
(check: :language '(special intermediate)
|
|
:users pairs-or-singles-with-warning
|
|
:coverage? #t
|
|
(!procedure Fahrenheit->Celsius 1)
|
|
(!test (Fahrenheit->Celsius 32) 0)
|
|
(!test (Fahrenheit->Celsius 212) 100)
|
|
(!test (Fahrenheit->Celsius -4) -20)
|
|
...))]
|
|
|
|
}
|
|
|
|
@defform/subs[(check: keys-n-vals body ...)
|
|
([keys-n-vals code:blank
|
|
(code:line :key val keys-n-vals)])]{
|
|
|
|
Constructs (and provides) an appropriate checker function, using
|
|
keywords to customize features that you want it to have. The body of
|
|
the checker (following the keywords) can contain arbitrary code, using
|
|
utility functions from @schememodname[handin-server/utils], as well as
|
|
additional ones that are defined below. Submission files are arriving
|
|
to the handin server in binary form (in the MrEd format that is used
|
|
to store text and other objects like images), and a number of these
|
|
options involve genrating a textual version of this file. The purpose
|
|
of these options is to have these text files integrate easily into a
|
|
course framework for grading, based on these text files.}
|
|
|
|
Keywords for configuring @scheme[check:]:
|
|
|
|
@itemize[
|
|
|
|
@item{@indexed-scheme[:users]---specification of users that are
|
|
acceptable for submission. Can be either a list of user lists, each
|
|
representing a known team, or procedure which will accept a list of
|
|
users and throw an exception if they are unacceptable. The default
|
|
is to accept only single-user submissions. The
|
|
@scheme[pairs-or-singles-with-warning] procedure is a useful value
|
|
for pair submission where the pairs are unknown.}
|
|
|
|
@item{@indexed-scheme[:eval?]---whether submissions should be
|
|
evaluated. Defaults to @scheme[#t]. Note that if it is specified
|
|
as @scheme[#f], then the checker body will not be able to run any
|
|
tests on the code, unless it contains code that performs some
|
|
evaluation (e.g., using the facilities of
|
|
@schememodname[handin-server/utils]).}
|
|
|
|
@item{@indexed-scheme[:language]---the language that is used for
|
|
evaluating submissions, same as the @scheme[_language] argument for
|
|
@scheme[make-evaluator] (see @schememodname[handin-server/sandbox]).
|
|
There is no default for this, so it must be set or an error is
|
|
raised. (See @scheme[call-with-evaluator/submission] for further
|
|
details.)}
|
|
|
|
@item{@indexed-scheme[:requires]---paths for additional libraries to
|
|
require for evaluating the submission, same as the
|
|
@scheme[_requires] argument for @scheme[make-evaluator] (see
|
|
@schememodname[handin-server/sandbox]). This defaults to null---no
|
|
teachpacks. Note: if a module language is used (See
|
|
@scheme[call-with-evaluator/submission] for further details), it is
|
|
passed as the @scheme[_allow-read] argument.}
|
|
|
|
@item{@indexed-scheme[:teachpacks]---an alternative name for
|
|
@scheme[:requires], kept for legacy checkers.}
|
|
|
|
@item{@indexed-scheme[:create-text?]---if true, then a textual version
|
|
of the submission is saved as @filepath{text.scm} in a
|
|
@filepath{grading} subdirectory (or any suffix that is specified by
|
|
@scheme[:output] below, for example @filepath{hw.java} is converted
|
|
into a textual @filepath{grading/text.java}). This is intended for
|
|
printouts and grading, and is in a subdirectory so students will not
|
|
see it on the status web server. Defaults to @scheme[#t].}
|
|
|
|
@item{@indexed-scheme[:textualize?]---if true, then all submissions
|
|
are converted to text, trying to convert objects like images and
|
|
comment boxes to some form of text. Defaults to @scheme[#f],
|
|
meaning that an exception is raised for submissions that are not all
|
|
text. @textoption
|
|
|
|
This flag is effective only when saving a textual version of the
|
|
submission files --- when @scheme[:create-text?] is on. The
|
|
possible configurations are:
|
|
@itemize[
|
|
@item{@scheme[:create-text?] is on and @scheme[:textualize?] is off
|
|
(the default) --- in this case a text version of submissions is
|
|
created, and submissions must contain only plain text. The text
|
|
file has the same semantics of the submission and can be used to
|
|
run student code.}
|
|
@item{@scheme[:create-text?] is off --- allowing submissions that
|
|
contain non-textual objects, but no text file is created so
|
|
grading and testing must be done using DrScheme (because the saved
|
|
submission is always in binary format).}
|
|
@item{Both flags are on --- allowing submission with non-textual
|
|
objects and generating text files, but these files will not be
|
|
usable as code since objects like images cannot be represented in
|
|
plain text.}]}
|
|
|
|
@item{@indexed-scheme[:untabify?]---if true, then tabs are converted
|
|
to spaces, assuming a standard tab width of 8 places. This is
|
|
needed for a correct computation of line lengths, but note that
|
|
DrScheme does not insert tabs in Scheme mode. Defaults to
|
|
@scheme[#t]. @textoption}
|
|
|
|
@item{@indexed-scheme[:maxwidth]---a number that specifies maximum
|
|
line lengths for submissions (a helpful feature for reading student
|
|
code). Defaults to 79. This feature can be disabled if set to
|
|
@scheme[#f]. @textoption}
|
|
|
|
@item{@indexed-scheme[:output]---the name of the original handin file
|
|
(unrelated to the text-converted files). Defaults to
|
|
@filepath{hw.scm}. (The suffix changes the defaults of
|
|
@scheme[:markup-prefix] and @scheme[:prefix-re].) Can be
|
|
@scheme[#f] for removing the original file after processing. The
|
|
file is always stored in MrEd's binary format.}
|
|
|
|
@item{@indexed-scheme[:multi-file]---by default, this is set to
|
|
@scheme[#f], which means that only DrScheme is used to send
|
|
submissions as usual. See @secref{multi-file} for setting up
|
|
multi-file submissions.}
|
|
|
|
@item{@indexed-scheme[:names-checker]---used for multi-file
|
|
submissions; see @secref{multi-file} for details.}
|
|
|
|
@item{@indexed-scheme[:markup-prefix]---used as the prefix for
|
|
@scheme[:student-lines] and @scheme[:extra-lines] below. The
|
|
default is @scheme[";;> "] or @scheme["//> "], depending on the
|
|
suffix of @scheme[:output] above. Note: if you change this, make
|
|
sure to change @scheme[:prefix-re] too. @textoption}
|
|
|
|
@item{@indexed-scheme[:prefix-re]---used to identify lines with markup
|
|
(@scheme[";>"] or @scheme["//>"] etc), so students cannot fool the
|
|
system by writing marked-up code. The default is @scheme[";>"] or
|
|
@scheme["//>"], depending on the suffix of :output above.
|
|
@textoption}
|
|
|
|
@item{@indexed-scheme[:student-line]---when a submission is converted
|
|
to text, it begins with lines describing the students that have
|
|
submitted it; this is used to specify the format of these lines. It
|
|
is a string with holes that that @scheme[user-substs] fills out.
|
|
The default is @scheme["Student: {username} ({Full Name} <{Email}>)"],
|
|
which requires @scheme["Full Name"] and @scheme["Email"] entries in
|
|
the server's extra-fields configuration. These lines are prefixed
|
|
with @scheme[";;> "] or the prefix specified by
|
|
@scheme[:makup-prefix] above. @textoption}
|
|
|
|
@item{@indexed-scheme[:extra-lines]---a list of lines to add after the
|
|
student lines, all with a @scheme[";;> "] or :markup-prefix too.
|
|
Defaults to a single line:
|
|
@scheme["Maximum points for this assignment: <+100>"]. (Can use
|
|
@scheme["{submission}"] for the submission directory.) See also
|
|
@scheme[add-header-line!]. @textoption}
|
|
|
|
@item{@indexed-scheme[:user-error-message]---a string that is used to
|
|
report an error that occurred during evaluation of the submitted
|
|
code (not during additional tests). It can be a plain string which
|
|
will be used as the error message, or a string with single a
|
|
@scheme["~a"] (or @scheme["~e"], @scheme["~s"], @scheme["~v"]) that
|
|
will be used as a format string with the actual error message. The
|
|
default is @scheme["Error in your code --\n~a"]. Useful examples of
|
|
these messages:
|
|
|
|
@scheme["There is an error in your program, hit \"Run\" to debug"]
|
|
|
|
@scheme["There is an error in your program:\n----\n~a\n----\nHit \"Run\" and debug your code."]
|
|
|
|
Alternatively, the value can be a procedure that will be invoked
|
|
with the error message. The procedure can do anything it wants, and
|
|
if it does not raise an exception, then the checker will proceed as
|
|
usual. For example:
|
|
|
|
@schemeblock[
|
|
(lambda (msg)
|
|
(add-header-line! "Erroneous submission!")
|
|
(add-header-line! (format " --> ~a" msg))
|
|
(message (string-append
|
|
"You have an error in your program -- please hit"
|
|
" \"Run\" and debug your code.\n"
|
|
"Email the course staff if you think your code is"
|
|
" fine.\n"
|
|
"(The submission has been saved but marked as"
|
|
" erroneous.)")
|
|
'(ok))
|
|
(message "Handin saved as erroneous." 'final))]
|
|
|
|
(Note that if you do this, then additional tests should be adjusted
|
|
to not raise an exception too.)}
|
|
|
|
@item{@indexed-scheme[:value-printer]---if specified, this will be
|
|
used for @scheme[current-value-printer].}
|
|
|
|
@item{@indexed-scheme[:coverage?]---collect coverage information when
|
|
evaluating the submission. This will cause an error if some input
|
|
is not covered. This check happens after checker tests are run, but
|
|
the information is collected and stored before, so checker tests do
|
|
not change the result. Also, you can use the @scheme[!all-covered]
|
|
procedure in the checker before other tests, if you want that
|
|
feedback earlier.}]
|
|
|
|
Within the body of @scheme[check:], @scheme[users] and
|
|
@scheme[submission] will be bound to the checker arguments---a
|
|
(sorted) list of usernames and the submission as a byte string. In
|
|
addition to the functionality below, you can use
|
|
@scheme[(!eval _expr)] (or @scheme[((submission-eval) '_expr)]) to
|
|
evaluate expressions in the submitted code context, and you can use
|
|
@scheme[(with-submission-bindings (id ...) body ...)] to evaluate the
|
|
body when @scheme[id]'s are bound to their values from the submission
|
|
code.}
|
|
|
|
@deftogether[(@defform[(pre: body ...)]
|
|
@defform[(post: body ...)])]{
|
|
|
|
These two macros define a pre- and a post-checker. In their bodies,
|
|
@scheme[_users] and @scheme[_submission] are bound as in
|
|
@scheme[check:], but there is nothing else special about these. See
|
|
the description of the @scheme[pre-checker] and
|
|
@scheme[post-checker] values for what can be done with these, and
|
|
note that the check for valid users is always first. An example for
|
|
a sophisticated @scheme[post:] block is below---it will first
|
|
disable timeouts for this session, then it will send a email with a
|
|
submission receipt, with CC to the TA (assuming a single TA), and
|
|
pop-up a message telling the student about it:
|
|
|
|
@schemeblock[
|
|
(require net/sendmail)
|
|
(post:
|
|
(define info
|
|
(format "hw.scm: ~a ~a"
|
|
(file-size "hw.scm")
|
|
(file-or-directory-modify-seconds "hw.scm")))
|
|
(timeout-control 'disable)
|
|
(log-line "Sending a receipt: ~a" info)
|
|
(send-mail-message
|
|
"course-staff@university.edu"
|
|
"Submission Receipt"
|
|
(map (lambda (user) (user-substs user "{Full Name} <{Email}>"))
|
|
users)
|
|
(list (user-substs (car users) "{TA Name} <{TA Email}>"))
|
|
null
|
|
`("Your submission was received" ,info))
|
|
(message (string-append
|
|
"Your submission was successfully saved."
|
|
" You will get an email receipt within 30 minutes;"
|
|
" if not, please contact the course staff.")
|
|
'(ok)))]}
|
|
|
|
@defparam[submission-eval eval (any/c . -> . any)]{
|
|
|
|
Holds an evaluation procedure for evaluating code in the submission
|
|
context.}
|
|
|
|
@; JBC: is this always just a list of strings?
|
|
@defproc[(user-data [user string?]) (listof string?)]{
|
|
|
|
Returns a user information given a username. The returned
|
|
information is a list of strings that corresponds to the configured
|
|
@scheme[extra-fields].}
|
|
|
|
@defproc[(user-substs [user string?] [fmt string?]) string]{
|
|
|
|
Uses the mappings in @scheme[user-data] to substitute user
|
|
information for substrings of the form ``@tt{{some-field-name}}'' in
|
|
@scheme[fmt]. This procedure signals an error if a field name is
|
|
missing in the user data. Also, ``@tt{{username}}'' will always be
|
|
replaced by the username and ``@tt{{submission}}'' by the current
|
|
submission directory.
|
|
|
|
This is used to process the @scheme[:student-line] value in the
|
|
checker, but it is provided for additional uses. See the above
|
|
sample code for @scheme[post:] for using this procedure.}
|
|
|
|
@defproc[(pairs-or-singles-with-warning [users (listof string?)])
|
|
any]{
|
|
|
|
Intended for use as the @scheme[:users] entry in a checker. It will
|
|
do nothing if there are two users, and throw an error if there are
|
|
more. If there is a single user, then the user will be asked to
|
|
verify a single submission. If the student cancels, then an
|
|
exception is raised so the submission directory is retracted. If
|
|
the student approves this, the question is not repeated (this is
|
|
marked by creating a directory with a known name). This is useful
|
|
for cases where you want to allow free pair submissions---students
|
|
will often try to submit their work alone, and later on re-submit
|
|
with a partner.}
|
|
|
|
@defproc[(teams-in-file [team-file path-string?])
|
|
((listof string?) . -> . void?)]{
|
|
|
|
@italic{Returns} a procedure that can be used for the :users entry
|
|
in a checker. The team file (relative from the server's main
|
|
directory) is expected to have user entries---a sequence of
|
|
s-expressions, each one a string or a list of strings. The
|
|
resulting procedure will allow submission only by teams that are
|
|
specified in this file. Furthermore, if this file is modified, the
|
|
new contents will be used immediately, so there is no need to
|
|
restart the server of you want to change student teams. (But
|
|
remember that if you change @scheme[("foo" "bar")] to
|
|
@scheme[("foo" "baz")], and there is already a @filepath{bar+foo}
|
|
submission directory, then the system will not allow ``@tt{foo}'' to
|
|
submit with ``@tt{bar}''.)}
|
|
|
|
@defproc[(add-header-line! [line string?]) void?]{
|
|
During the checker operation, can be used to add header lines to the
|
|
text version of the submitted file (in addition to the
|
|
@scheme[:extra-lines] setting). It will not have an effect if
|
|
@scheme[:create-text?] is false.}
|
|
|
|
@defproc[(procedure/arity? [proc procedure?] [arity number?])
|
|
boolean?]{
|
|
Returns @scheme[#t] if @scheme[proc] is a procedure that accepts
|
|
@scheme[arity] arguments.}
|
|
|
|
@defform[(!defined id ...)]{
|
|
Checks that the given identifiers are defined in the (evaluated)
|
|
submission, and throws an error otherwise. The identifiers can be
|
|
bound as either a plain value or as a syntax.}
|
|
|
|
@defform[(!bound id ...)]{
|
|
Checks that the given identifiers are defined in the (evaluated)
|
|
submission as a plain value. Throws an error if not, or if an
|
|
identifier is bound to a syntax.}
|
|
|
|
@defform[(!syntax id arity)]{
|
|
Checks that @scheme[id] is defined, and is bound as a macro.}
|
|
|
|
@defform[(!procedure id arity)]{
|
|
Checks that @scheme[id] is defined, and is bound to a procedure.}
|
|
|
|
@defform[(!procedure* expr arity)]{
|
|
|
|
Similar to @scheme[!procedure] but omits the defined check, making
|
|
it usable with any expression, which is then evaluated in the
|
|
submission context.}
|
|
|
|
@deftogether[(@defform[(!integer id)]
|
|
@defform[(!integer* expr)])]{
|
|
|
|
Similar to @scheme[!procedure] and @scheme[!procedure*] for
|
|
integers.}
|
|
|
|
@deftogether[(
|
|
@defform*[((!test expr)
|
|
(!test expr result)
|
|
(!test expr result equal?))]
|
|
@defform[(!test/exn expr)]
|
|
)]{
|
|
|
|
The first @scheme[!test] form checks that the given expression
|
|
evaluates to a non-@scheme[#f] value in the submission context,
|
|
throwing an error otherwise. The second form compares the result of
|
|
evaluation, requiring it to be equal to @scheme[result]. The third
|
|
allows specifying an equality procedure. The @scheme[!test/exn] form
|
|
checks that the given expression throws an @scheme[exn:fail?] error,
|
|
throwing an error otherwise.
|
|
|
|
For the latter two @scheme[!test] forms, note that the
|
|
@scheme[result] and @scheme[equal?] forms are @italic{not} evaluated
|
|
in the submission context.}
|
|
|
|
@defform[(!eval expr)]{
|
|
|
|
Evaluate an arbitrary expession in the submission context. This is
|
|
a simple shorthand for @scheme[((submission-eval) `expr)].}
|
|
|
|
@defproc*[([(!all-covered) void?]
|
|
[(!all-covered [proc (string? . -> . any)]) void?])]{
|
|
|
|
When coverage information is enabled (see @scheme[:coverage?]
|
|
above), checks the collected coverage information and throws an
|
|
error with source information if some code is left uncovered. If
|
|
@scheme[proc] is provided, it is applied to a string argument that
|
|
describes the location of the uncovered expression
|
|
(@scheme["<line>:<col>"], @scheme["#<char-pos>"], or
|
|
@scheme["(unknown position)"]) instead of throwing an error. The
|
|
collected information includes only execution coverage by submission
|
|
code, excluding additional checker tests. You do not have to call
|
|
this explicitly---it is called at the end of the process
|
|
automatically when @scheme[:coverage?] is enabled. It is made
|
|
available so you can call it earlier (e.g., before testing) to show
|
|
clients a coverage error first, or if you want to avoid an error.
|
|
For example, you can do this:
|
|
|
|
@schemeblock[
|
|
(!all-covered
|
|
(lambda (where)
|
|
(case (message (string-append
|
|
"Incomplete coverage at "where", do you want"
|
|
" to save this submission with 10% penalty?")
|
|
'(yes-no))
|
|
[(yes) (add-header-line! "No full coverage <*90%>")
|
|
(message "Handin saved with penalty.")]
|
|
[else (error "aborting submission")])))]}
|