WUT_Computer_Science/run.sh
Krzysztof kuhy Rudnicki f7763bb1cd Add a run.sh to every course, and fix the code that would not build
All 44 directories now probe: 38 runnable, 5 no-code, 1 blocked (ENUME needs
MATLAB or Octave, neither of which is on this machine).

The runners are stubs over one shared scripts/course_lib.sh rather than 44
copies of the logic, for the same reason the gates live in ~/utils. A stub
passes its kind as ARGUMENTS -- course_main --kind python --entry ... -- "$@"
-- rather than setting globals, because globals a linter cannot see being read
mean SC2034 on every one of them, and the only cure would be 44 disables.

Fixed along the way:
  - Generating the stubs overwrote Programming/PSD/run.sh, the original
    coursework runner. Restored, and given a --probe early exit instead so it
    is otherwise untouched: it installs packages with sudo and starts Kafka,
    which is exactly what a probe must not trigger.
  - The probe guard only recognised runners containing the literal --probe, so
    the new stubs (which delegate to the library) all read unprobed. It now
    also accepts delegation to course_lib.sh.
  - Blocked probes reported "unknown" because _require_any prints the tool it
    found, so probes redirected its stdout -- swallowing the failure reason
    too. Split into _probe_any, and ENUME now says exactly what it needs.
  - shellcheck runs with -x so it follows the sourced library; without it every
    stub reports SC1091 and the only alternative is a per-file disable. The
    library itself carries `# shellcheck shell=bash` because it has no shebang
    on purpose -- it is sourced, never executed.

Then EADS was actually run, and three of its six C++ files did not compile:
  - lab3.cpp: copyDictionary was declared taking Dictionary* but its body used
    Node members, and the copy constructor called copyTree(), which does not
    exist. The copy also dropped key and balanceFactor. Now compiles AND runs,
    printing its AVL tests.
  - MidTerm/task2.cpp: a const mismatch between declaration and definition, a
    stray semicolon before a function body, findNode calling a Tree method on
    a treeNode*, and a while loop that never advanced. sumNodeSub also had no
    base case -- unreachable before, because the const mismatch meant its body
    was never compiled.
These compiled years ago because older GCC did not diagnose uninstantiated
template bodies. Fragments with no main() are now type-checked with -c rather
than linked, since two of these files are exam answers, not programs.

Binaries go to a gitignored build/ -- an extensionless executable dropped next
to its source is not something .gitignore catches by name.

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

154 lines
5.3 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.
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}"
log "probing course directories (${env_label})"
local tmp
tmp="$(mktemp)"
trap 'rm -f "${tmp:-}"' RETURN
local dir rel line
while IFS= read -r dir; do
rel="${dir#"${SCRIPT_DIR}"/}"
line="$(probe_one "$dir")"
printf '%s\t%s\n' "$rel" "$line" >> "$tmp"
done < <(course_dirs)
write_runnability_doc "$tmp" "$env_label"
log "wrote ${RUNNABILITY_DOC##*/}"
awk -F'\t' '{ n[$2]++ } END { for (k in n) printf " %-10s %d\n", k, n[k] }' "$tmp"
}
write_runnability_doc() {
local data="$1" env_label="$2"
{
cat <<'MD'
# Runnability
Generated by `./run.sh status`. Do not edit by hand.
Status is recorded PER ENVIRONMENT. 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. CI compares only its own column.
MD
printf '| Course | %s | Detail |\n|---|---|---|\n' "$env_label"
awk -F'\t' '{ printf "| `%s` | %s | %s |\n", $1, $2, $3 }' "$data"
} > "$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"
"$runner"
}
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 "$@"