scheduled_builder.py - Provide feedback for student answers¶
Imports¶
These are listed in the order prescribed by PEP 8.
Standard library¶
Third-party imports¶
Local imports¶
None.
Celery¶
Provide the Celery configuration.
Use Redis with Celery.
Given that tasks time out in 60 seconds, expire them after that. See result_expires
.
This follows the Redis caveats.
1 hour.
Create and configure the Celery app.
Provide a Celery task to run mdb. This is used when externally compiling code in a book; it’s not used by the webserver.
This function should run the provided code and report the results. It will vary for a given compiler and language.
The name of the builder to use.
An absolute path to the file which contains code to test. The file resides in a temporary directory, which should be used to hold any additional files produced by the test.
An absolute path to the Sphinx root directory.
A relative path to the Sphinx source path from the sphinx_base_path
.
A relative path to the Sphinx output path from the sphinx_base_path
.
A relative path to the source file from the sphinx_source_path
, based on the submitting web page.
Translate the provided builder into a Python function.
Run the builder then return the results.
Utilities¶
Raise this exception if the build fails.
Transform the arguments to subprocess.run
into a string showing what
command will be executed.
Run a subprocess with the provided arguments, returning a string which contains the output. On failure, raise an exception. Returns True if the subprocess completed successfully.
Arguments to invoke the subprocess.
A string describing this step in the build.
A path to the working directory for the subprocess.
A list of output produced thus far in the build; this function will append to it.
True if stderr should be included in the results.
Additional kwargs for the subprocess call.
Add a newline before the next title, unless there’s nothing in the output list yet.
Print the title with a nice underline.
Show the command executed.
Create a failing return code for the logic below.
Record output if available.
Put the timeout message last.
A returncode of 0 indicates success; anything else is an error.
Copy the test file from its Sphinx location to the temporary directory where the student source code is. Return the name of the test file; if the copy failed, assume there’s no test file; instead, return the file name of the student source code.
If not provided, assume the test file’s extension is the same as the student source file.
The test file name takes file_name.old_ext and produces file_name-test.new_ext.
The test file couldn’t be copied; assume the test should run on the student source file instead.
Given an list of arguments to pass as the first parameter of subprocess.run
, wrap this in runguard. Return an updates list of parameters for subprocess.run
.
A list of arguments comprising the first parameter of subprocess.run
.
The directory containing the executable to run in runguard.
Kill COMMAND after TIME seconds (float).
Set maximum CPU time to TIME seconds (float).
Set all (total, stack, etc) memory limits to SIZE kB.
Set maximum created filesize to SIZE kB.
Set maximum no. processes to N.
Disable core dumps when True
Get (hopefully) the prefork pool process index. Use this to select a jobe userid that’s not in use. Since it only applies to prefork pools, use a thread ID as backup.
Give the selected Jobe user access. Inspired by jobe source.
subprocess.run(["setfacl", "-m", f"u:{user}:rwX", cwd], check=True)
return (
[
"sudo",
"/var/www/jobe/runguard/runguard",
f"--user={user}",
"--group=jobe",
f"--time={time_s}",
f"--cputime={cputime_s}",
f"--memsize={memsize_kb}",
f"--filesize={filesize_kb}",
f"--nproc={num_processes}",
]
+ (["--no_core"] if no_core_dumps else [])
+ args
)
Builders¶
Copy the test to the temp directory. Otherwise, running the test file from its book location means it will import the solution, which is in the same directory.
run_file_name = copy_test_file_to_tmp(
file_path, cwd, sphinx_base_path, sphinx_source_path, source_path
)
return report_subprocess(
runguard([sys.executable, run_file_name], cwd), "Run", cwd, []
)
def rust_builder(
file_path, cwd, sphinx_base_path, sphinx_source_path, sphinx_out_path, source_path
):
First, copy the test to the temp directory. Otherwise, running the test file from its book location means it will import the solution, which is in the same directory.
Compile. See rustc tests.
Run.
Assemble or compile the source. We assume that the binaries are already in the path.
o_path = file_path + ".o"
extension = os.path.splitext(file_path)[1]
try:
is_extension_asm = {".s": True, ".c": False}[extension]
except Exception:
raise RuntimeError("Unknown file extension in {}.".format(file_path))
if is_extension_asm:
args = [
"xc16-as",
"-omf=elf",
"-g",
"--processor=33EP128GP502",
file_path,
"-o" + o_path,
]
else:
args = [
"xc16-gcc",
"-mcpu=33EP128GP502",
"-omf=elf",
"-g",
"-O0",
"-msmart-io=1",
"-Wall",
"-Wextra",
"-Wdeclaration-after-statement",
"-I" + os.path.join(sphinx_base_path, sphinx_source_path, "lib/include"),
"-I" + os.path.join(sphinx_base_path, sphinx_source_path, "tests"),
"-I"
+ os.path.join(
sphinx_base_path, sphinx_source_path, "tests/platform/Microchip_PIC24"
),
"-I"
+ os.path.join(
sphinx_base_path, sphinx_source_path, os.path.dirname(source_path)
),
"-DSIM",
file_path,
"-c",
"-o" + o_path,
]
out_list = []
report_subprocess(args, "Compile / Assemble", cwd, out_list)
Build the test code with a random verification code.
verification_code = get_verification_code()
waf_root = os.path.normpath(
os.path.join(
sphinx_base_path, sphinx_out_path, BUILD_SYSTEM_PATH, sphinx_source_path
)
)
test_file_path = os.path.join(
sphinx_base_path,
sphinx_source_path,
os.path.splitext(source_path)[0] + "-test.c",
)
test_object_path = file_path + ".test.o"
args = [
"xc16-gcc",
"-mcpu=33EP128GP502",
"-omf=elf",
"-g",
"-O0",
"-msmart-io=1",
"-Wall",
"-Wextra",
"-Wdeclaration-after-statement",
"-I" + os.path.join(sphinx_base_path, sphinx_source_path, "lib/include"),
"-I" + os.path.join(sphinx_base_path, sphinx_source_path, "tests"),
"-I"
+ os.path.join(
sphinx_base_path, sphinx_source_path, "tests/platform/Microchip_PIC24"
),
"-I"
+ os.path.join(
sphinx_base_path, sphinx_source_path, os.path.dirname(source_path)
),
test_file_path,
"-DSIM",
"-DVERIFICATION_CODE=({}u)".format(verification_code),
"-c",
"-o" + test_object_path,
]
report_subprocess(args, "Compile test code", cwd, out_list)
Link.
elf_path = file_path + ".elf"
args = [
"xc16-gcc",
"-omf=elf",
"-Wl,--heap=100,--stack=16,--check-sections,--data-init,--pack-data,--handles,--isr,--no-gc-sections,--fill-upper=0,--stackguard=16,--no-force-link,--smart-io",
"-Wl,--script="
+ os.path.join(
sphinx_base_path, sphinx_source_path, "lib/lkr/p33EP128GP502_bootldr.gld"
),
test_object_path,
o_path,
"-lpic24_stdlib",
"-L" + os.path.join(waf_root, ".."),
"-o" + elf_path,
]
report_subprocess(args, "Link", cwd, out_list)
Simulate. Create the simulation commands.
Run the simulation. This is a re-coded version of wscript.sim_run
– I couldn’t find a way to re-use that code.
sp_args = dict(
stderr=subprocess.STDOUT,
text=True,
cwd=cwd,
)
out_list.extend(
["\nSimulation\n==========\n", _subprocess_string(args, **sp_args)]
)
try:
cp = subprocess.run(
args, input=ss, stdout=subprocess.PIPE, timeout=10, **sp_args
)
sim_ret = cp.returncode
except subprocess.TimeoutExpired:
sim_ret = 1
timeout_str = "\n\nTimeout."
Read the results of the simulation.
Put the timeout string at the end of all the simulator output.
Assemble or compile the source. We assume that the binaries are already in the path.
Compile and link the source file. The most helpful resource I’ve found on bare-metal ARM with newlib: https://jasonblog.github.io/note/arm_emulation/simplest_bare_metal_program_for_arm.html. However, I prefer this (simpler) approach.
The student source.
Provide picky warnings, etc.
Include paths for the book.
"-I" + os.path.join(sphinx_base_path, sphinx_source_path, "lib/include"),
"-I" + os.path.join(sphinx_base_path, sphinx_source_path, "tests"),
"-I"
+ os.path.join(
sphinx_base_path, sphinx_source_path, "tests/platform/ARMv7-A_ARMv7-R"
),
"-I"
+ os.path.join(
sphinx_base_path, sphinx_source_path, os.path.dirname(source_path)
),
"-c",
"-o" + o_path,
]
out_list = []
report_subprocess(args, "Compile / Assemble", cwd, out_list)
Build the test code with a random verification code.
verification_code = get_verification_code()
waf_root = os.path.normpath(
os.path.join(
sphinx_base_path, sphinx_out_path, BUILD_SYSTEM_PATH, sphinx_source_path
)
)
lib_path = os.path.join(
sphinx_base_path,
sphinx_source_path,
)
test_file_path = os.path.join(
lib_path,
os.path.splitext(source_path)[0] + "-test.c",
)
test_object_path = file_path + ".test.o"
Build the test code with a random verification code.
The test code.
Pass the verification code. TODO: separate compiles, so user code doesn’t have this value.
Provide picky warnings, etc.
Include paths for the book.
"-I" + os.path.join(sphinx_base_path, sphinx_source_path, "lib/include"),
"-I" + os.path.join(sphinx_base_path, sphinx_source_path, "tests"),
"-I"
+ os.path.join(
sphinx_base_path, sphinx_source_path, "tests/platform/ARMv7-A_ARMv7-R"
),
"-I"
+ os.path.join(
sphinx_base_path, sphinx_source_path, os.path.dirname(source_path)
),
"-c",
"-o" + test_object_path,
]
report_subprocess(args, "Compile test code", cwd, out_list)
Link.
Compiled sources.
Output args.
The ARM needs an interrupt vector table defined for this specific processor. It doesn’t work if placed in the library below.
Include the ARM library.
The custom linker file defines a section to correctly place these interrupt vectors.
Use the RDIMON semihosting tools to provide the standard library (write, exit, etc.).
The specific chip we use has a different RAM address than the linker file. Override it here.
Transform to a bin file.
Simulate.
QEMU provides fairly good simulation for ARM. For more of the following options, search the QEMU invocation page.
Pick a specific system, in this case the ARM Versatile Express-A9
Give the processor 32 MB of RAM. Search for -m [size=]
on the QEMU invocation page.
Does this even matter?
Run only from the command line instead of displaying a QEMU GUI plus graphics from the emulated system.
Even with this option, QEMU complains about command-line options. ???
Reserve stdio for I/O from the emulated system, instead of the monitor.
The binary is loaded as if it’s a Linux kernel.
Send all ARM I/O to the console.
Enable semihosting, so that newlib can easily exit the simulator, produce stdio, etc.