WUT_Computer_Science/run.sh
Krzysztof kuhy Rudnicki bd24dd57ea Hold the coursework to the same lint bar as the tooling
kuhy overruled the "do not fix coursework to be better than it was submitted"
rule: git history is the archive, so the working tree can be clean. This lands
the config and the mechanical half of the cleanup -- 9,281 ruff findings under
the fleet config down to 3,686, with no per-file-ignores anywhere.

What is in here:

- pyproject.toml, the first one in this repo. select = ["ALL"], mypy strict,
  and the vendored list mirrored into ruff exclude and coverage omit. Five
  rules are off and each says why; CPY001 is the only judgement call, because
  stamping a copyright header onto 137 university assignments would assert a
  licence that was never claimed. No hook is wired up yet -- a gate lands only
  once its subject is clean.

- ruff format + the safe fixes, then a second pass over the docstring rules
  ruff marks unsafe (they only edit docstring text). 108 files.

- TRAK/sightpy is vendored, not coursework: it is rafael-fuente/Python-Raytracer
  (BSD 3-Clause). Verified against a fresh upstream clone with line endings
  normalised -- 30 of 32 files byte-identical, ray.py and scene.py carrying a
  12- and 13-line local patch that counts rays for the report. 35 files that
  should never have been linted, now excluded and restored.

- WDWR, SPD, MOM and ECRYPT are fully clean, by hand and verified by running
  them: WDWR's three plot scripts print the same statistics as before, MOM's
  PuLP model solves to the same cost. WDWR's graph.py had a 342-character CSV
  header embedded as a string literal, which cannot be wrapped -- the data
  moved to data/pareto_front.csv. Two 0-byte .py stubs are deleted; an empty
  file is not a deliverable and git still has it.

Three harness bugs found by actually running things:

- `run` now forces MPLBACKEND=Agg. TRAK finished its ray trace in 37 seconds
  and then sat for 45 minutes: plt.show() had opened an image viewer and was
  waiting for someone to close the window.
- TRAK's main.py only pops that viewer with --show now, and its runner writes
  to a gitignored build/ via the new --entry-args. The default output path is
  outputs/output.png, which is TRACKED -- a plain run overwrote a submitted
  deliverable.
- course_lib.sh was 357 lines, over this repo's own cap. Split into
  course_probe.sh and course_run.sh.

Verified: ECOTE 26 tests pass, ECRYPT_PROJECT 99 pass, TRAK renders
cornell_box in 37s and exits 0, every module compiles, and every import ruff
removed was checked to be genuinely unused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
2026-08-28 19:53:04 +02:00

202 lines
7.8 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# run.sh -- drive this coursework archive.
#
# ./run.sh check run the full local gate (pre-commit, all files)
# ./run.sh install alias for ./install.sh
# ./run.sh status probe every course dir, rewrite DOCS-runnability.md
# ./run.sh run <dir> actually execute one course project
# ./run.sh sync-vendored show the mirrors that gates/vendored.txt feeds
#
# `status` PROBES: it asks each directory whether its toolchain is present and
# never executes the project, so it takes seconds and is safe in CI. Use
# `run <dir>` to actually run something. Those are very different commands and
# conflating them is how a "status check" turns into 44 builds.
# ============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
readonly RUNNABILITY_DOC="${SCRIPT_DIR}/DOCS-runnability.md"
# Exit codes a per-directory run.sh uses to report itself.
# Resource ceiling for `run`. A course build is background work and must never
# be what makes an interactive machine stutter -- PORR's suite solves
# 10000x10000 systems and happily eats every core it is given.
#
# Same mechanism as the pre-push hook in testsAndMisc: a transient systemd
# scope, so the limit is enforced by the kernel rather than by good intentions.
# MemorySwapMax=0 means it gets OOM-killed instead of thrashing swap.
readonly RUN_MEMORY_MAX="${RUN_MEMORY_MAX:-2G}"
readonly RUN_CPU_QUOTA="${RUN_CPU_QUOTA:-200%}"
readonly EXIT_BLOCKED=78
readonly EXIT_NO_CODE=79
log() { printf '==> %s\n' "$*"; }
err() { printf 'error: %s\n' "$*" >&2; }
usage() {
grep -E '^# \./run\.sh' "${BASH_SOURCE[0]}" | sed 's/^# //'
exit "${1:-0}"
}
course_dirs() {
find "${SCRIPT_DIR}/Programming" "${SCRIPT_DIR}/NotProgramming" \
-mindepth 1 -maxdepth 1 -type d | sort
}
cmd_check() {
log "running the full gate"
( cd "$SCRIPT_DIR" && pre-commit run --all-files )
}
cmd_install() { exec "${SCRIPT_DIR}/install.sh" "$@"; }
# A runner opts in either by handling --probe itself, or by delegating to the
# shared library (which does). Anything else predates this harness and is left
# strictly alone.
_supports_probe() {
grep -qF -- '--probe' "$1" || grep -qF 'course_lib.sh' "$1"
}
# Probe one directory. Prints "<status>\t<detail>"; never executes a project.
#
# The guard below is load-bearing. Some course dirs already ship their own
# run.sh from years ago -- Programming/PSD/run.sh installs packages with sudo
# and starts Kafka. Calling one of those with an unknown --probe flag does not
# probe anything, it RUNS the project. So a runner is only invoked if it
# actually declares --probe; anything else is reported unprobed and left alone.
# `timeout` is defence in depth for a script that declares the flag and then
# blocks on input anyway.
probe_one() {
local dir="$1" runner="$1/run.sh"
if [[ -x "$runner" ]] && _supports_probe "$runner"; then
local out status
out="$(timeout 20s "$runner" --probe 2> /dev/null)" && status=0 || status=$?
case "$status" in
0) printf 'runnable\t%s\n' "${out:-ok}" ;;
"$EXIT_BLOCKED") printf 'blocked\t%s\n' "${out:-unknown}" ;;
"$EXIT_NO_CODE") printf 'no-code\t%s\n' "${out:-unknown}" ;;
*) printf 'error\t%s\n' "probe exited $status" ;;
esac
return 0
fi
if [[ -x "$runner" ]]; then
printf 'unprobed\tpre-existing run.sh, no --probe support\n'
else
printf 'unprobed\tno run.sh yet\n'
fi
}
cmd_status() {
local env_label="${CI:+ci}"
env_label="${env_label:-local}"
local store="${SCRIPT_DIR}/gates/runnability-${env_label}.tsv"
log "probing course directories (${env_label})"
local dir rel line
: > "$store"
while IFS= read -r dir; do
rel="${dir#"${SCRIPT_DIR}"/}"
line="$(probe_one "$dir")"
printf '%s\t%s\n' "$rel" "$line" >> "$store"
done < <(course_dirs)
# Each environment writes only its OWN file, then the doc is rendered from
# both. Writing a single shared column would mean a CI run silently
# overwrote the local results with its own -- and the whole point of
# recording per environment is that "runnable here, blocked on a runner"
# is the normal case rather than drift.
write_runnability_doc
log "wrote ${RUNNABILITY_DOC##*/}"
awk -F'\t' '{ n[$2]++ } END { for (k in n) printf " %-10s %d\n", k, n[k] }' "$store"
}
write_runnability_doc() {
local local_tsv="${SCRIPT_DIR}/gates/runnability-local.tsv"
local ci_tsv="${SCRIPT_DIR}/gates/runnability-ci.tsv"
[[ -f "$local_tsv" ]] || : > "$local_tsv"
[[ -f "$ci_tsv" ]] || : > "$ci_tsv"
{
cat <<'MD'
# Runnability
Generated by `./run.sh status`. Do not edit by hand.
Status is recorded PER ENVIRONMENT, from `gates/runnability-local.tsv` and
`gates/runnability-ci.tsv`. A directory that is `runnable` locally and
`blocked` in CI is the normal case, not drift: the runner has no MATLAB, no
Unity and no .NET SDK. Each environment rewrites only its own file, so a CI
run cannot clobber the local column.
`-` means that environment has not probed yet.
MD
printf '| Course | local | ci | Detail (local) |\n|---|---|---|---|\n'
# Compare FILENAME rather than using the NR==FNR idiom: with an EMPTY
# first file awk never reads it, so NR==FNR stays true for every row
# of the second file and the whole table comes out blank.
awk -F'\t' -v cifile="$ci_tsv" '
FILENAME == cifile { ci[$1] = $2; next }
{ printf "| `%s` | %s | %s | %s |\n", $1, $2, ($1 in ci ? ci[$1] : "-"), $3 }
' "$ci_tsv" "$local_tsv"
} > "$RUNNABILITY_DOC"
}
cmd_run() {
local target="${1:-}"
[[ -n "$target" ]] || { err "run needs a directory"; usage 1; }
local runner="${SCRIPT_DIR}/${target}/run.sh"
[[ -x "$runner" ]] || { err "no executable run.sh in $target"; return 1; }
log "running $target (capped at ${RUN_MEMORY_MAX} RAM, ${RUN_CPU_QUOTA} CPU)"
# Agg, so plt.show() draws nothing and returns. TRAK's renderer finished
# its ray trace in a couple of minutes and then sat there for 45 more:
# plt.show() had opened an image viewer and was waiting for a human to
# close the window. A run that needs a person at a keyboard is not a run.
# export, not an assignment prefix: _run_capped is a shell function, and a
# prefixed assignment on one is not put into the child's environment.
export MPLBACKEND="${MPLBACKEND:-Agg}"
_run_capped "$runner"
}
# Run a command under a memory/CPU cap, niced so it yields to anything
# interactive. Falls back to plain nice if systemd-run is unavailable.
_run_capped() {
if command -v systemd-run > /dev/null 2>&1; then
systemd-run --user --scope --quiet \
-p "MemoryMax=${RUN_MEMORY_MAX}" \
-p MemorySwapMax=0 \
-p "CPUQuota=${RUN_CPU_QUOTA}" \
nice -n 19 ionice -c 3 "$@"
else
nice -n 19 ionice -c 3 "$@"
fi
}
cmd_sync_vendored() {
log "gates/vendored.txt declares:"
grep -vE '^\s*#|^\s*$' "${SCRIPT_DIR}/gates/vendored.txt" | sed 's/^/ /'
printf '\nMirrored into:\n'
printf ' ~/utils/file_length/_tables.py VENDORED_SUBPATHS["WUT_Computer_Science"]\n'
printf ' pyproject.toml [tool.ruff] exclude (Milestone 4)\n'
printf ' pyproject.toml [tool.coverage.run] omit (Milestone 4)\n'
}
main() {
case "${1:-}" in
check) shift; cmd_check "$@" ;;
install) shift; cmd_install "$@" ;;
status) shift; cmd_status "$@" ;;
run) shift; cmd_run "$@" ;;
sync-vendored) shift; cmd_sync_vendored "$@" ;;
-h | --help | "") usage ;;
*) err "unknown command: $1"; usage 1 ;;
esac
}
main "$@"