CSTester¶
A Computer Science programming submission testing tool
CSTester is a Python curses package
to automate Computer Science programming assignment evaluation.
Requirements¶
This project requires PyYAML and Python >= 3.9.
This is an ncurses application which provides a TUI,
so a terminal with ncurses is also required.
Installation¶
This package is available on PyPi,
and can be installed with the command pip install cstester,
alternatively, you may clone the repo and pip install -e .
for a “live” version.
Note
This project is currently under active refinement
Documentation and minor changes should be expected
Refer to the current documentation
Commands¶
- Following a
pip install, you are provided with 3 commands: cstester– the mainCSTesterapplicationdiffwin– a standalone driver for theDiffWindowclasstestOutput– a standalone driver to test an individual program
Terminology¶
This utility was made to test many programming project/assignment submissions.
While individual submissions do not need to be “group” submissions, this is how a submission is referred to, as a group’s submission.
Groups are differentiated from each other based on their number.
Note
- Group numbers
Are non-negative,
0|[1-9][0-9]*Do not have to be consecutive
Must not have leading zeroes
number==str(int(number))
- Numeric groups eases sorting and building include/exclude number/range lists
Commands performed need not apply to all groups
The “include” number/range list specifies groups to only include
The “exclude” number/range list specified groups to not include
- A number/range list is a string with comma-separated values, each either
a single number
a range from start-stop, where start is not greater than stop
- Some valid example number/range lists:
4,6,1,5,7-9is equivalent to1,4-923a single number0-10a single range for the beginning groups, including10
Regular expressions¶
Several options make use of regular expressions.
REs are compiled with Python’s re module, so refer to their excellent documentation for additonal information.
- The search strings method
reads files line-by-line
uses
re.searchwill report whether the pattern is found anywhere in a line.
- All other regexes use
re.match they begin with an implicit caret
^
The group regex string, to extract the group number from their zip filename,
must have at least 1 capture group
must have the group number in the first capture group and
must be something which captures a valid group number
- These are all valid group regex strings:
project(\d+)_.*.(?:project|assignment)(\d+)_.*(\d+)_.*
The optional keyfile, used to aid extraction, is an alternative to using a group regex, but is not as efficient.
Input validation¶
Where possible, input for options is validated.
- There are 3 validations performed:
regex – regexes are passed to
re.compileto determine validitynumrange – a valid number/range list is converted to its condensed form
command – we check whether argv[0] is a shell built-in or executable file
Of these, we can’t reliably verify a command
if it is not found in PATH or an executable file.
We don’t want to attempt to execute the command to determine whether the command works, as attempting to run the command may have side-effects.
Attention
Command verification may be on the chopping-block
- For the time being, if a command is given and fails verification
substitute with a known good command, e.g.,
echo "hallo"save your configuration
replace the command in the yaml file
load the configuration
CSTester usage¶
The main application is invoked with the cstester command,
optionally providing a saved configuration .yaml as an argument, e.g.,
cstester assgn1.yaml.
- In most screens
“cancel keys” are { <escape>, Q, q }
“return keys” are { <enter> }
“help keys” are { ?, H, h }
Some screens return immediately on a keypress, e.g.,
a prompt asking Y/n, y/N, or to press the any key.
When a “yes or no” prompt is given, the default response is capitalized. Unless you give the lowercased response, case-insensitive, the default response is chosen.
Phase directory setup¶
One of the first things which must be done is setting up the phase directory. This directory can be created in the extraction process.
Caution
extraction to an existing directory removes its contents
The phase directory contains each group’s submission in a subdirectory
in the form group_n, where n is a valid group number.
A phase zip can be chosen to extract to the selected phase directory.
The extraction keyfile and group regex are used to associate group submission filenames with their group number.
zip include is a dictionary option which specifies filenames that we
explicitly want in a specific location in the group directory,
e.g., "README":"" means to put README in the group’s root directory,
and "driver.c":"src/" means to put driver.c in group_n/src/.
zip excludeis a list of files and directories to not extract.".*\.o"– regex excludes object files"\..*"– regex excludes files beginning with a period
Both zip include and exclude are patterns compiled and used with re.match,
files and directory names are compared case-insensitive during extraction.
Group options¶
This menu allows to enter number/range list strings for both
groups to only include as well as groups to exclude,
there are also menu entries to allow selection,
which uses a curses window to select entries.
Also (currently) in the group options menu,
are the options to freeze and clean files.
Freezing files¶
Files to freeze are those which match an entered regex pattern.
- The “freeze” operation performs 2 important functions
matching files are archived in a zip in the root phase directory
the files’ hashes are stored in a text file in the root phase directory
Note
Performing a freeze will overwrite any previous freeze files
The purpose of freezing files is to later make a patch.
If a submission requires modification to compile, or some other reason, a patch can be made to provide the group so that they can either
Use the
patchcommand to apply the patch to their fileReview the changes made to their submission
Making patches¶
- Unlike with the “freeze” operation, this doesn’t require a regex:
the “frozen” text file is read
files which no longer exist are ignored
the hashes for files listed are compared with their stored hash
when hashes differ a patch is created and placed next to the original
a listing of patches made is shown (or that none were created)
Clean files¶
This operation is meant to simulate a make clean command.
Files matching a pattern withing the clean files regexes will be removed.
Caution
Search for files begins at the root of each group’s directory,
so no files outside of their directory will be removed,
but a bad pattern, e.g., a blank string "" which matches anything,
could clear the group directories.
Evaluation¶
The evaluation menu provides additional simulated make commands.
The test command is the command that will run from the group’s directory.
If an input test case is to be given as a command-line argument,
use @case@ which will later be substituted with the case’s file path.
If @case@ is not present in the command-line arguments,
then the input casefile is read and passed to the program as its stdin.
Preparation commands are a list of commands which will run in each group’s directory.
Commands will stop running after the first failure, and a list of failures by-group will be displayed after completion.
Preparation commands are not validated commands, for obvious reasons.
Preparation commands can be used to simulate make clean && make,
or for some other purpose.
There is also a clean option in this menu,
the duplicate path to clean
should be removed from either the group menu or this menu.
Search strings allows to search files matching a pattern for strings.
This can be useful for multiple reasons, but probably the most useful is to search for potentially malicious code.
Read readmes, if each submission should have a text file, will display the readme of each submission (or a note that it is missing).
Configuration options¶
See the API documentation for the CSTesterConfig dataclass
for a listing of the configuration options saved/loaded from yaml,
and a description of their use.
Extraction¶
The extractor was made to extract a single zip, e.g., submissions.zip,
that contains a zip for each group’s submission.
Group numbers are obtained through a group regex, or, optionally, using a keyfile.
The group regex extracts a number from a filename,
using re.match, e.g.,
^group_(0|[1-9][0-9]*).*\.zip$
The key-value pairs taken from the keyfile are of the form
^(0|[1-9][0-9]*)(?:,.*):([^:]+)$
That is, lines start with a valid group number, optionally followed by a comma and some series of text before a trailing colon, finally, the key to search for in the filename is after the final colon.
For example, with either
42,Group leader: A name, Members: Another name:groupname
42:othername
if either groupname or othername exists within the filename then the group number will be inferred to be 42.
All regex matches performed during extraction are all case-insensitive. Regexes in a keyfile must be unique, group numbers need not be.
Group directories are of the form group_n, where n is the group number
Within the phase directory, an extraction log is created: phasedir/x.log
- The extraction log records general information about extraction
If a keyfile was used and how many keys were obtained from the keyfile
If files were skipped/ignored
If a group number was inferred from keyfile and why
If a group number was manually entered (decisions in subsequent extractions of the same zipfile can be repeated)
Which file was chosen among multiple submissions
How many group->file pairs were found
If duplicate files were ignored
If a submission zip had symlink files
If extraction of a submission zip had an exception
If a submission wasn’t extracted due to a suspicious compression ratio
If duplicate files were ignored
If extracted files needed to be renamed
- Within group directories, e.g.,
phasedir/group_1/ group1.zip–contains the original zipfilesub.nfo–contains details of the original zipfile The original zip filename The compressed size, extracted size, and compression ratio Whether symlink files existed (and were skipped) List of extracted files List of files excluded
Extracting individual zip files
The submissions zip is extracted to a temporary directory.
For each group’s zip, we examine the zip memberlist against exclude patterns and do not extract files which match (case-insensitive) an exclude pattern.
For files we want at a specific location, these can be specified in the include pattern map:
The
keyis a regex to match the fileThe
valis a relative location from the group directory
Hint
include={‘Makefile’,’’,’driver.c’,’src’} Will put Makefile in the group’s root directory and driver.c in src/
We exclude (and print a warning) any symlinks within the archive. We extract the contents of the zip to a new temporary directory. We ensure all files are chmod 600 (owner rw), and all directories are chmod 700 (owner rwx).
We move all files to the final extraction directory,
we remove any special characters from file/directory names,
and convert spaces to underscores.
We put the save the original zip in the group’s directory.
A text file, ./sub.nfo,
will contain the zip filename,
original content list with sizes (compressed/decompressed),
and compression ratio of extracted files.
Note
This project is under active development