Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ tests/
pre-compiled/.spago
pre-compiled/node_modules
pre-compiled/output
pre-compiled/spago.lock
.appends
.gitignore
.gitattributes
36 changes: 21 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
# Use the latest LTS version of Node.js
FROM node:20-bullseye-slim
# Spago 1.0.3 requires Node.js >= 22.5.0 (uses node:sqlite built-in module)
FROM node:22-bookworm-slim

# Update package lists and install required dependencies
# Install system dependencies:
# - ca-certificates: HTTPS support for downloading packages from the registry
# - git: required by Spago for registry index management
# - jq: used by run.sh to generate results.json output
# - libncurses5: runtime dependency for the PureScript compiler (purs)
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
git \
jq \
libncurses5 \
&& rm -rf /var/lib/apt/lists/*

# Set up working directory
WORKDIR /opt/test-runner/pre-compiled
# Use a single WORKDIR for the test runner root
WORKDIR /opt/test-runner

# Copy and install dependencies
COPY pre-compiled .
RUN npm install && npx spago install && npx spago build --deps-only
# Install PureScript dependencies and pre-compile them.
# The output/ and .spago/ directories are reused at runtime to avoid
# recompiling 280+ modules for every student submission.
COPY pre-compiled pre-compiled/
RUN cd pre-compiled \
&& npm install \
&& npx spago install \
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dockerfile comment says dependencies are “pre-compiled” and runtime reuses pre-compiled/output and pre-compiled/.spago, but the build stage now only runs npx spago install. If spago install doesn’t produce output/, the runtime cp -R -p pre-compiled/output in bin/run.sh will fail and you’ll lose the intended cache/precompile behavior (or potentially fail in stricter shells). Consider adding an explicit dependency build step (e.g., Spago’s deps-only build) or otherwise ensuring output/ is generated during image build.

Suggested change
&& npx spago install \
&& npx spago install \
&& npx spago build --deps-only \

Copilot uses AI. Check for mistakes.
&& npm cache clean --force \
&& rm -rf /root/.npm

# Set up bin directory
WORKDIR /opt/test-runner/bin
COPY bin/run.sh bin/run-tests.sh ./
# Copy runner scripts
COPY bin/run.sh bin/run-tests.sh bin/
RUN chmod +x bin/*.sh

# Ensure scripts have execution permissions
RUN chmod +x /opt/test-runner/bin/*.sh

# Set the entry point
ENTRYPOINT ["/opt/test-runner/bin/run.sh"]
4 changes: 2 additions & 2 deletions bin/run-tests.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash

# Synopsis:
# Test the test runner by running it against a predefined set of solutions
# Test the test runner by running it against a predefined set of solutions
# with an expected output.

# Output:
Expand All @@ -19,7 +19,7 @@ exit_code=0
base_dir=$(builtin cd "${BASH_SOURCE%/*}/.." || exit; pwd)

# Iterate over all test Spago projects
for config in "${base_dir}"/tests/*/spago.dhall; do
for config in "${base_dir}"/tests/*/spago.yaml; do
exercise_dir=$(dirname "${config}")
slug=$(basename "${exercise_dir}")
expected_results_file="${exercise_dir}/expected_results.json"
Expand Down
62 changes: 44 additions & 18 deletions bin/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ results_file="${output_dir}/results.json"
# - We can work with a write-able file-system
# - We avoid copying files between the docker host and client giving a nice speedup.
build_dir=/tmp/build
cache_dir=${build_dir}/cache

if [ ! -d "${input_dir}" ]; then
echo "No such directory: ${input_dir}"
Expand All @@ -55,17 +54,12 @@ fi
mkdir -p ${build_dir}
pushd "${build_dir}" > /dev/null || exit

# Put the basic spago project in place
cp "${input_dir}"/*.dhall .
# Put the spago project in place: copy and rewrite the package name to match
# the pre-compiled lockfile so we can use --pure mode (no registry access).
sed 's/^ name: .*/ name: pre-compiled/' "${input_dir}/spago.yaml" > spago.yaml
ln -s "${input_dir}"/src .
ln -s "${input_dir}"/test .

# Setup cache directory. We require a writable dhall cache because dhall will
# attempt to fetch the upstream package-set definition.
mkdir ${cache_dir}
cp -R "${HOME}"/.cache/dhall ${cache_dir}
cp -R "${HOME}"/.cache/dhall-haskell ${cache_dir}

# Setup our prepared node setup.
ln -s "${base_dir}/pre-compiled/node_modules" .

Expand All @@ -75,17 +69,15 @@ ln -s "${base_dir}/pre-compiled/node_modules" .
# flag).
cp -R -p "${base_dir}/pre-compiled/output" .
cp -R "${base_dir}/pre-compiled/.spago" .
cp "${base_dir}/pre-compiled/spago.lock" .

echo "Build and test ${slug} in ${build_dir}..."

# Run the tests for the provided implementation file and redirect stdout and
# stderr to capture it. We do our best to minimize the output to emit and
# compiler errors or unit test output as this scrubbed and presented to the
# student. In addition spago will try to write to ~/cache/.spago and will fail
# on a read-only mount and thus we skip the global cache and request to not
# install packages.
export XDG_CACHE_HOME=${cache_dir}
spago_output=$(npx spago --global-cache skip --no-psa test --no-install 2>&1)
# stderr to capture it.
# --offline --pure: use cached packages and lockfile, no registry/network access
# HOME=/tmp: Spago's SQLite cache needs a writable directory (Docker runs --read-only)
spago_output=$(HOME=/tmp npx spago test --offline --pure 2>&1)
exit_code=$?

popd > /dev/null || exit
Expand All @@ -95,9 +87,43 @@ popd > /dev/null || exit
if [ $exit_code -eq 0 ]; then
jq -n '{version: 1, status: "pass"}' > "${results_file}"
else
# Sanitize output: remove spago/build preamble and noise, keep compiler
# errors and test failure output. Strip JS stack traces from test failures.
sanitized_spago_output=$(echo "${spago_output}" | sed -E \
-e '/^Compiling/d' \
-e '/at.*:[[:digit:]]+:[[:digit:]]+\)?/d')
-e '/^Reading Spago workspace/d' \
-e '/^✓ Selecting package/d' \
-e '/^Checking dependencies/d' \
-e '/^Downloading dependencies/d' \
-e '/^No lockfile found/d' \
-e '/^Lockfile written/d' \
-e '/^Building\.\.\./d' \
-e '/^\[[[:space:]]*[0-9]+ of [0-9]+\] Compiling /d' \
-e '/^✓ Build succeeded/d' \
-e '/^Running tests for package/d' \
-e '/^✘ Tests failed/d' \
-e '/^✘ Failed to build/d' \
-e '/^[[:space:]]+Src[[:space:]]+Lib[[:space:]]+All/d' \
-e '/^Warnings[[:space:]]+[0-9]/d' \
-e '/^Errors[[:space:]]+[0-9]/d' \
-e '/^[[:space:]]+at .*(\.js|\.mjs|node:internal).*:[0-9]/d' \
-e '/^\[WARNING /,/^$/d')

# Remove leading/trailing blank lines and collapse runs of 3+ blank lines
sanitized_spago_output=$(printf '%s\n' "${sanitized_spago_output}" | awk '
{ a[NR] = $0 }
END {
# Find first and last non-empty lines
s = 1; e = NR
while (s <= NR && a[s] == "") s++
while (e >= s && a[e] == "") e--
# Print, collapsing runs of 3+ blank lines to 2
blank = 0
for (i = s; i <= e; i++) {
if (a[i] == "") { blank++; if (blank <= 2) print a[i] }
else { blank = 0; print a[i] }
}
}
')

jq --null-input --arg output "${sanitized_spago_output}" '{version: 1, status: "fail", message: $output}' > "${results_file}"
fi
Expand Down
13 changes: 6 additions & 7 deletions bin/update-tests.sh
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
#!/usr/bin/env bash

# This script will update spago.dhall and package.dhall of all exercises
# using the master files from the project template (pre-compiled/).
# This script will update spago.yaml of all test examples
# using the master files from the pre-compiled project.

set -o pipefail
set -u

base_dir=$(builtin cd "${BASH_SOURCE%/*}/.." || exit; pwd)
project_dir="${base_dir}/pre-compiled"

for config in ./tests/*/spago.dhall; do
for config in "${base_dir}"/tests/*/spago.yaml; do
exercise_dir=$(dirname "${config}")
# slug=$(basename "${exercise_dir}")
slug=$(basename "${exercise_dir}")

echo "Working in ${exercise_dir}..."

# sed -e "s/pre-compiled/${slug}/" < "${project_dir}/spago.dhall" > "${exercise_dir}/spago.dhall"
cp "${project_dir}/packages.dhall" "${exercise_dir}/packages.dhall"
# Test examples use a minimal dependency set, not the full pre-compiled one.
# Only update if the test's spago.yaml workspace section needs to change.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this loop supposed to do anything?

done
Loading
Loading