From 8ee78b4aacb5d3aba9b7399d583c6ad42c9742c2 Mon Sep 17 00:00:00 2001 From: Lilac Date: Fri, 23 Oct 2026 11:25:43 -0400 Subject: [PATCH] vibe coded a little --- flake.nix | 4 +- init.sh | 2 +- pigeon.sh | 163 ++++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 136 insertions(+), 33 deletions(-) diff --git a/flake.nix b/flake.nix index aefa653..36842a0 100644 --- a/flake.nix +++ b/flake.nix @@ -9,7 +9,7 @@ # rootfs will contain all binaries given by toolchain toolchain = pkgs.buildEnv { name = "pigeon-toolchain"; - paths = with pkgs; [ bash coreutils gcc gnumake util-linux ]; + paths = with pkgs; [ binutils bash coreutils gcc gnumake util-linux ]; }; closure = pkgs.closureInfo { rootPaths = [ toolchain ]; @@ -77,7 +77,7 @@ pigeon = pkgs.writeShellApplication { name = "pigeon"; - runtimeInputs = with pkgs; [ coreutils curl e2fsprogs firecracker git ]; + runtimeInputs = with pkgs; [ coreutils curl e2fsprogs firecracker gcc git gnumake ]; text = builtins.replaceStrings [ "@kernel@" "@rootfs@" ] [ "${kernel}" "${rootfs}"] (builtins.readFile ./pigeon.sh); diff --git a/init.sh b/init.sh index 85c8c3d..12abceb 100755 --- a/init.sh +++ b/init.sh @@ -22,7 +22,6 @@ cd /tmp make >/root/results/build.log 2>&1 echo $? >/root/results/build.status ) -[ "$(cat /root/results/build.status)" != 0 ] && exit 1 for TEST in /root/project/tests/*.sh ; do ( @@ -30,5 +29,6 @@ for TEST in /root/project/tests/*.sh ; do timeout -s 9 2 \ setpriv --reuid=1 --regid=1 --clear-groups --no-new-privs --inh-caps=-all \ "$TEST" >"/root/results/$(basename "$TEST" .sh)" 2>/dev/null + echo $? >"/root/results/$(basename "$TEST" .sh).status" ) done diff --git a/pigeon.sh b/pigeon.sh index 0e2cf6d..850c20c 100755 --- a/pigeon.sh +++ b/pigeon.sh @@ -12,6 +12,7 @@ RELEASE_TIME="$6" RATE_LIMIT=3 PIGEON_DATABASE="$(realpath -m "$PIGEON_STATE/db")" +PIGEON_PROJECTS="$(realpath -m "$PIGEON_PROJECTS")" RELEASE_TIME="$(date -d "$RELEASE_TIME" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || true)" report() { @@ -21,6 +22,7 @@ report() { --data "$(jq -n --arg d "$1" '{"description": $d}')" \ --url "https://gitlab.com/api/v4/projects/$PROJECT_ID/releases/$RELEASE_TAG" } +# shellcheck disable=SC2120,SC2119 db() { sqlite3 -bail -cmd '.timeout 10000' "$PIGEON_DATABASE" "$@" } @@ -35,26 +37,11 @@ CREATE TABLE IF NOT EXISTS results ( project TEXT NOT NULL, hash TEXT NOT NULL, time TEXT NOT NULL DEFAULT (datetime('now')), - score INTEGER CHECK (score >= 0) + score INTEGER CHECK (score >= 0), + token INTEGER NOT NULL ); EOF -ROW="$(db < datetime('now', '-1 day')) < $RATE_LIMIT -RETURNING id; -EOF -)" - -if [ -z "$ROW" ] ; then - report "Rate limit exceeded!" - exit -fi - DIRECTORY="$(mktemp -d /tmp/pigeon.XXXXXXXXXX)" trap 'rm -rf "$DIRECTORY"' EXIT cd "$DIRECTORY" @@ -63,10 +50,23 @@ git -c advice.detachedHead=false clone -q --rev "$RELEASE_COMMIT" --depth 1 \ "https://oauth2:$PIGEON_GITLAB_TOKEN@gitlab.com/$PROJECT_NAMESPACE/$PROJECT_NAME.git" student/ rm -rf student/.git +TESTS="$PIGEON_PROJECTS/$PROJECT_NAME/tests" mkdir -p project/tests project/build -cp -a "$PIGEON_PROJECTS/$PROJECT_NAME"/tests/*.sh project/tests/ +cp -a "$TESTS"/*.sh project/tests/ cp -a "$PIGEON_PROJECTS/$PROJECT_NAME"/build/. project/build/ +# A project may ship an answer-key solution. It is built and run only here, +# on the host, and never copied onto any drive the guest can reach -- tests +# compare the student's reported output against it after the VM has exited. +CANONICAL="" +if [ -d "$PIGEON_PROJECTS/$PROJECT_NAME/canonical" ] ; then + mkdir -p canonical + cp -a "$PIGEON_PROJECTS/$PROJECT_NAME"/build/. canonical/ + cp -a --remove-destination "$PIGEON_PROJECTS/$PROJECT_NAME"/canonical/. canonical/ + ( cd canonical && timeout 30 make >/dev/null 2>&1 ) || true + [ -x canonical/program ] && CANONICAL="$(realpath canonical/program)" +fi + mkfs.ext4 -q -F -d student student.img 16M mkfs.ext4 -q -F -d project project.img 16M mkfs.ext4 -q -F results.img 16M @@ -89,21 +89,124 @@ EOF firecracker --no-api --level Error --config-file vm.json -RESULT="" -SCORE=0 -TESTS="$PIGEON_PROJECTS/$PROJECT_NAME/tests" +PUBLIC_SCORE=0; PUBLIC_TOTAL=0 +RELEASE_SCORE=0; RELEASE_TOTAL=0 +SECRET_SCORE=0; SECRET_TOTAL=0 +NAMES=() +STATUSES=() +ANSWERS=() +QUESTIONS=() +TIERS=() + for TEST in "$TESTS"/*.sh ; do - debugfs -R "cat /$(basename "$TEST" .sh)" results.img >scratch 2>/dev/null - if cmp -s scratch "$TESTS/$(basename "$TEST" .sh).expected" ; then - RESULT="${RESULT}PASS\n" - SCORE=$((SCORE + 1)) + NAME="$(basename "$TEST" .sh)" + TIER="${NAME%%-*}" + QUESTION="$(cat "$TESTS/$NAME.question")" + debugfs -R "cat /$NAME" results.img >scratch 2>/dev/null + EXIT="$(debugfs -R "cat /$NAME.status" results.img 2>/dev/null)" + case "$EXIT" in '' | *[!0-9]*) EXIT=0 ;; esac + + # A test may expect the student's program to exit with a specific status + # (e.g. 134 for a SIGABRT from a correctly-triggered assert()) instead of + # the default 0. + EXPECTED_EXIT=0 + [ -f "$TESTS/$NAME.exit" ] && EXPECTED_EXIT="$(cat "$TESTS/$NAME.exit")" + + if [ -f "$TESTS/$NAME.expected" ] ; then + if cmp -s scratch "$TESTS/$NAME.expected" && [ "$EXIT" -eq "$EXPECTED_EXIT" ] ; then + MATCH=1 + else + MATCH=0 + fi + elif [ -n "$CANONICAL" ] ; then + # No static .expected: the test reported its own "ARGOUTPUT" pairs, + # so check each one against the canonical solution run here on the host. + MATCH=1 + while IFS="$(printf '\t')" read -r ARG OUT ; do + [ -n "$ARG" ] || continue + EXPECTED="$("$CANONICAL" "$ARG" 2>/dev/null)" + [ "$OUT" = "$EXPECTED" ] || MATCH=0 + done &2 +PUBLIC_PASS=0 +[ "$PUBLIC_SCORE" -eq "$PUBLIC_TOTAL" ] && PUBLIC_PASS=1 +SCORE=$((PUBLIC_SCORE + RELEASE_SCORE + SECRET_SCORE)) -report "$RESULT" +TOKEN="$(db < datetime('now', '-1 day') + ) < $RATE_LIMIT THEN 1 ELSE 0 END +RETURNING token; +EOF +)" + +SHOWN_QUESTIONS=() +SHOWN_ANSWERS=() +for i in "${!NAMES[@]}" ; do + case "${TIERS[$i]}" in + public) ;; + release) [ "$TOKEN" = 1 ] || continue ;; + *) continue ;; + esac + SHOWN_QUESTIONS+=("${QUESTIONS[$i]}") + SHOWN_ANSWERS+=("${ANSWERS[$i]}") +done + +MESSAGE="" +for i in "${!SHOWN_QUESTIONS[@]}" ; do + MESSAGE="${MESSAGE}${SHOWN_QUESTIONS[$i]} ${SHOWN_ANSWERS[$i]} "$'\n' +done + +if [ "$TOKEN" != 1 ] && [ "$PUBLIC_PASS" = 1 ] ; then + MESSAGE="${MESSAGE}"$'\n'"All public tests passed, but you have used all $RATE_LIMIT submissions for today. Release test results are withheld until tomorrow." +fi + +report "$MESSAGE"