This is an implementation of a polling XQueue client and grader.
There are several components in a working XQueue Watcher service:
- XQueue Watcher: it polls an xqueue service continually for new submissions and grades them.
- Submissions Handler: when the watcher finds any new submission, it will be passed to the handler for grading. It is a generic handler that can be configured to work with different submissions through individual submission graders.
- Individual Submission Grader: each exercise or homework may specify its own "grader". This should map to a file on the server that usually specifies test cases or additional processing for the student submission.
Usually your server will look like this:
root/
├── xqueue-watcher/
│ ├── ... # xqueue-watcher repo, unchanged
│ └── ...
├── config/
│ └── conf.d/
│ │ └── my-course.json
│ └── logging.json
└── my-course/
├── exercise1/
│ ├── grader.py # - per-exercise grader
│ └── answer.py # - if using JailedGrader
├── ...
└── exercise2/
├── grader.py
└── answer.py
Usually you can run XQueue Watcher without making any changes. You should keep course-specific files in another folder like shown above, so that you can update xqueue_watcher anytime.
Install the requirements before running xqueue_watcher
cd xqueue-watcher/
make requirements
Now you're ready to run it.
python -m xqueue_watcher -d [path to the config directory, eg ../config]
The course configuration JSON file in conf.d
should have the following structure:
{
"test-123": {
"SERVER": "http://127.0.0.1:18040",
"CONNECTIONS": 1,
"AUTH": ["uname", "pwd"],
"HANDLERS": [
{
"HANDLER": "xqueue_watcher.grader.Grader",
"KWARGS": {
"grader_root": "/path/to/course/graders/",
}
}
]
}
}
test-123
: the name of the queueSERVER
: XQueue server addressAUTH
: List containing [username, password] of XQueue Django userCONNECTIONS
: how many threads to spawn to watch the queueHANDLERS
: list of callables that will be called for each queue submissionHANDLER
: callable name, see below for Submissions HandlerKWARGS
: optional keyword arguments to apply during instantiationgrader_root
: path to the course directory, eg /path/to/my-course
TODO: document logging.json
When xqueue_watcher
detects any new submission, it will be passed to the submission handler for grading. It will instantiate a new handler based on the name configured above, with submission information retrieved
from XQueue. Base graders are defined in xqueue_watcher
: Grader and JailedGrader (for Python, using CodeJail). If you don't use JailedGrader, you'd have to implement your own Grader by subclassing xqueue_watcher.grader.Grader
The payload received from XQueue will be a JSON object that usually looks like the JSON below. Note that "grader" is a required field in the "grader_payload" and must be configured accordingly in Studio.
{
"student_info": {
"random_seed": 1,
"submission_time": "20210109222647",
"anonymous_student_id": "6d07814a4ece5cdda54af1558a6dfec0"
},
"grader_payload": "\n {\"grader\": \"relative/path/to/grader.py\"}\n ",
"student_response": "print \"hello\"\r\n "
}
To implement a pull grader:
Subclass xqueue_watcher.grader.Grader
and override the grade
method. Then add your grader to the config like "handler": "my_module.MyGrader"
. The arguments for the grade
method are:
grader_path
: absolute path to the grader defined for the current problem.grader_config
: other configuration particular to the problemstudent_response
: student-supplied code
Note that grader_path
is constructed by appending the relative path to the grader from grader_payload
to the grader_root
in the configuration JSON. If the handler cannot find a grader.py
file, it would fail to grade the submission.
xqueue_watcher
provides a few utilities for grading python submissions, including JailedGrader for running python code in a safe environment and grading support utilities.
To sandbox python, use CodeJail. In your handler configuration, add:
"HANDLER": "xqueue_watcher.jailedgrader.JailedGrader",
"CODEJAIL": {
"name": "python",
"python_bin": "/path/to/sandbox/python",
"user": "sandbox_username"
}
Then, codejail_python
will automatically be added to the kwargs for your handler. You can then import codejail.jail_code and run jail_code("python", code...)
. You can define multiple sandboxes and use them as in jail_code("another-python-version", ...)
To use JailedGrader, you also need to provide an answer.py
file on the same folder with the grader.py
file. answer.py
contains the correct/reference implementation of the solution to the problem. The grader will run both student submission and answer.py
and compare the output with each other.
There are several grading support utilities that make writing grader.py
for python code easy. Check out
grader_support/gradelib.py
for the documentation.
grader_support.gradelib.Grader
: a base class for creating a new submission grader. Not to be confused withxqueue-watcher.grader.Grader
. You can add input checks, preprocessors and tests to a Grader object.grader_support.gradelib.Test
: a base class for creating tests for a submission. Usually a submission can be graded with one or a few tests. There are also few useful test functions and classes included, likeInvokeStudentFunctionTest
,exec_wrapped_code
, etc.- Preprocessors: utilities to process the raw submission before grading it.
wrap_in_string
is useful for testing code that is not wrapped in a function. - Input checks: sanity checks before running a submission, eg check
required_string
orprohibited_string
Using the provided grader class, your grader.py
would look something like this:
from grader_support import gradelib
grader = gradelib.Grader()
# invoke student function foo with parameter []
grader.add_test(gradelib.InvokeStudentFunctionTest('foo', []))
Or with a pre-processor:
import gradelib
grader = gradelib.Grader()
# execute a raw student code & capture stdout
grader.add_preprocessor(gradelib.wrap_in_string)
grader.add_test(gradelib.ExecWrappedStudentCodeTest({}, "basic test"))
You can also write your own test class, processor and input checks.