Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2f57a1f
update changelog with new release v2.9.0 (#686)
donny-wong Dec 18, 2025
6d56d98
Fixed Haskell test results to only include the function name (#687)
david-yz-liu Dec 29, 2025
e91a524
Improved robustness of tester installation scripts and Docker configu…
david-yz-liu Dec 29, 2025
274db10
Resolved Dockerfile warning by switching CMD argument to JSON input
david-yz-liu Dec 29, 2025
eb85245
Fixed Haskell tester installation using ghcup to install stack system…
david-yz-liu Dec 29, 2025
a03e90d
Bump client and server docker image versions
david-yz-liu Dec 30, 2025
f9f58da
[pre-commit.ci] pre-commit autoupdate (#684)
pre-commit-ci[bot] Dec 30, 2025
749e7c3
build(deps): bump rq from 2.6.0 to 2.6.1 in /server (#683)
dependabot[bot] Dec 30, 2025
a9f0693
build(deps): bump rq from 2.6.0 to 2.6.1 in /client (#682)
dependabot[bot] Dec 30, 2025
37c410f
build(deps): bump werkzeug from 3.1.3 to 3.1.4 in /client (#681)
dependabot[bot] Dec 30, 2025
008cd36
Updated tester schema generation to use msgspec data types (#689)
david-yz-liu Jan 5, 2026
5b964b8
Specify frontend version in server Dockerfile (#691)
donny-wong Jan 8, 2026
738f828
build(deps): bump werkzeug from 3.1.4 to 3.1.5 in /client (#692)
dependabot[bot] Jan 14, 2026
80f7292
[pre-commit.ci] pre-commit autoupdate (#694)
pre-commit-ci[bot] Feb 22, 2026
8176631
build(deps): bump msgspec from 0.19.0 to 0.20.0 in /server (#697)
dependabot[bot] Feb 22, 2026
a5523d5
build(deps): bump jsonschema from 4.25.1 to 4.26.0 in /client (#696)
dependabot[bot] Feb 22, 2026
42f2120
build(deps): bump jsonschema from 4.25.1 to 4.26.0 in /server (#695)
dependabot[bot] Feb 22, 2026
907a787
build(deps): bump redis from 7.1.0 to 7.2.1 in /client (#704)
dependabot[bot] Mar 22, 2026
61d2886
build(deps): bump redis from 7.1.0 to 7.2.1 in /server (#701)
dependabot[bot] Mar 22, 2026
c2057c1
[pre-commit.ci] pre-commit autoupdate (#705)
pre-commit-ci[bot] Mar 22, 2026
05b0548
build(deps): bump rq from 2.6.1 to 2.7.0 in /server (#703)
dependabot[bot] Mar 22, 2026
59c3dac
build(deps): bump rq from 2.6.1 to 2.7.0 in /client (#702)
dependabot[bot] Mar 22, 2026
6089f35
build(deps): bump flask from 3.1.2 to 3.1.3 in /client (#699)
dependabot[bot] Mar 22, 2026
6650ec7
build(deps): bump werkzeug from 3.1.5 to 3.1.6 in /client (#700)
dependabot[bot] Mar 22, 2026
d62f5f1
Increased default settings job timeout from 600s to 1200s (#707)
david-yz-liu Mar 22, 2026
ccc148c
Created AI remote URL whitelist configuration (#693)
Naragod Mar 24, 2026
6ecda24
Added tester for Javascript (#698)
Karl-Michaud Mar 27, 2026
30cba0a
Disabled pytest cache in Python tester (#709)
freyazjiner Mar 28, 2026
b496597
build(deps): bump requests from 2.32.5 to 2.33.0 in /server (#708)
dependabot[bot] Mar 28, 2026
a879849
build(deps): bump requests from 2.33.0 to 2.33.1 in /server (#713)
dependabot[bot] Apr 19, 2026
ef87d93
build(deps): bump python-dotenv from 1.2.1 to 1.2.2 in /client (#714)
dependabot[bot] Apr 19, 2026
a787584
build(deps): bump werkzeug from 3.1.6 to 3.1.7 in /client (#715)
dependabot[bot] Apr 19, 2026
a8c5df9
build(deps): bump redis from 7.2.1 to 7.4.0 in /client (#712)
dependabot[bot] Apr 19, 2026
2036d17
build(deps): bump redis from 7.2.1 to 7.4.0 in /server (#711)
dependabot[bot] Apr 19, 2026
dacf807
Fixed marks_earned marker ignored when earned equals total or zero (#…
Naragod Apr 24, 2026
4b176ef
Fixed worker orphan processes and add max test timeout cap (#710)
Naragod Apr 25, 2026
a27b163
Merge branch 'master' into release_2.10.0
May 25, 2026
6680526
Resolved ghcup isolation installation failure in Haskell autotester (…
donny-wong May 26, 2026
cf8b57d
Merge branch 'master' into release_2.10.0
May 26, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ __pycache__
.eggs
venv
venv2
.venv
build
docker-compose.override.yml
/workspace
.vscode
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.11.0
rev: 26.3.1
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
Expand Down
23 changes: 19 additions & 4 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
# CHANGELOG
All notable changes to this project will be documented here.

## [v2.10.0]
- Fixed Haskell test results to only include the function name (#687)
- Improved robustness of tester installation scripts and Docker configuration (#688)
- Moved tidyverse installation steps from server Dockerfile into R tester requirements.system (#688)
- Fixed Haskell tester installation using ghcup to install stack system-wide (#688)
- Updated tester schema generation to use msgspec datatypes (#689)
- Specify dockerfile frontend version (#691)
- Add a new Javascript (Jest) tester to support Javascript autotesting (#698)
- Add remote URL whitelist for AI tester to restrict allowed endpoints (#693)
- Increased default settings job timeout from 600s to 1200s (#707)
- Disable pytest cacheprovider to avoid creating .pytest_cache in isolated runs (#709)
- Fixed Python tester to correctly report marks when `markus_marks_earned` equals total or zero (#716)
- Fix worker orphan processes and add server-side max test timeout cap (#710)
- Resolve ghcup isolation installation failure in Haskell autotester (#725)

## [v2.9.0]
- Install stack with GHCup (#626)
- Fixed AI tester to report error when the specified `submission` file is not found (#663)
Expand Down Expand Up @@ -39,13 +54,13 @@ All notable changes to this project will be documented here.
- Update R tester to allow a renv.lock file (#581)
- Improve display of Python package installation errors when creating environment (#585)
- Update "setting up test environment" message with http response of status code 503 (#589)
- Change rlimit resource settings to apply each worker individually (#587)
- Change rlimit resource settings to apply each worker individually (#587)
- Drop support for Python 3.8 (#590)
- Use Python 3.13 in development (#590)
- Update Docker configuration to install dependencies in a separate service (#590)
- Improve error reporting with handled assertion errors (#591)
- Add custom pytest markers to Python tester to record MarkUs metadata (#592)
- Stop the autotester from running tests if there are errors in test settings (#593)
- Stop the autotester from running tests if there are errors in test settings (#593)
- Implement Redis backoff strategy (#594)

## [v2.6.0]
Expand Down Expand Up @@ -119,7 +134,7 @@ All notable changes to this project will be documented here.
- Add ability to clean up test scripts that haven't been used for X days (#379)

## [v2.1.2]
- Support dependencies on specific package versions and non-CRAN sources for R tester (#323)
- Support dependencies on specific package versions and non-CRAN sources for R tester (#323)

## [v2.1.1]
- Remove the requirement for clients to send unique user name (#318)
Expand All @@ -140,7 +155,7 @@ All notable changes to this project will be documented here.
- Add Jupyter tester (#284)

## [v1.10.3]
- Fix bug where zip archive was unpacked two levels deep instead of just one (#271)
- Fix bug where zip archive was unpacked two levels deep instead of just one (#271)
- Pass PGHOST and PGINFO environment variables to tests (#272)
- Update to new version of markus-api that supports uploading binary files (#273)
- Fix bug where environment variables were not string types (#274)
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ The autotester currently supports testers for the following languages and testin
- [`tasty-quickcheck`](https://hackage.haskell.org/package/tasty-quickcheck)
- `java`
- [JUnit](https://junit.org/junit4/)
- `js` (JavaScript)
- [Jest](https://jestjs.io/)
- `py` (python3)
- [Unittest](https://docs.python.org/3/library/unittest.html)
- [Pytest](https://docs.pytest.org/en/latest/)
Expand Down Expand Up @@ -155,6 +157,8 @@ Installing each tester will also install the following additional packages (syst
- tasty-quickcheck (cabal package)
- `java`
- openjdk-8-jdk
- `js` (JavaScript)
- none
- `py` (python3)
- none
- `pyta`
Expand Down Expand Up @@ -202,6 +206,10 @@ supervisor_url: # url used by the supervisor process. default is: '127.0.0.1:900

worker_log_dir: # an absolute path to a directory containing the worker's stdout and stderr logs.

max_test_timeout: # maximum number of seconds a single test is allowed to run before being killed.
# When set, any per-test timeout exceeding this value is capped to it, and tests
# with no timeout default to this value. default is: 3600

rlimit_settings: # RLIMIT settings (see details below)
nproc: # for example, this setting sets the hard and soft limits for the number of processes available to 300
- 300
Expand Down
2 changes: 1 addition & 1 deletion client/.dockerfiles/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ RUN apt-get update -y && \

WORKDIR /app

CMD /markus_venv/bin/python run.py
CMD ["/markus_venv/bin/python", "run.py"]
2 changes: 1 addition & 1 deletion client/.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ FLASK_HOST=localhost
FLASK_PORT=5000
ACCESS_LOG=
ERROR_LOG=
SETTINGS_JOB_TIMEOUT=600
SETTINGS_JOB_TIMEOUT=1200
2 changes: 1 addition & 1 deletion client/autotest_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

ERROR_LOG = os.environ.get("ERROR_LOG")
ACCESS_LOG = os.environ.get("ACCESS_LOG")
SETTINGS_JOB_TIMEOUT = os.environ.get("SETTINGS_JOB_TIMEOUT", 600)
SETTINGS_JOB_TIMEOUT = os.environ.get("SETTINGS_JOB_TIMEOUT", 1200)
REDIS_URL = os.environ["REDIS_URL"]

REDIS_CONNECTION = redis.Redis.from_url(
Expand Down
1 change: 0 additions & 1 deletion client/autotest_client/form_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,5 @@ def validate_against_schema(test_specs: Dict, schema: Dict, filenames: List[str]
schema["definitions"]["files_list"]["enum"] = filenames
# don't validate based on categories
schema["definitions"]["test_data_categories"].pop("enum")
schema["definitions"]["test_data_categories"].pop("enumNames")
error = _validate_with_defaults(schema, test_specs, best_only=True)
return str(error) if error else None
12 changes: 6 additions & 6 deletions client/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
flask==3.1.2
python-dotenv==1.2.1
rq==2.6.0
redis==7.1.0
jsonschema==4.25.1
Werkzeug==3.1.3
flask==3.1.3
python-dotenv==1.2.2
rq==2.7.0
redis==7.4.0
jsonschema==4.26.0
Werkzeug==3.1.7
4 changes: 2 additions & 2 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
UBUNTU_VERSION: '24.04'
LOGIN_USER: 'docker'
WORKSPACE: '/home/docker/.autotesting'
image: markus-autotest-server-dev:1.4.0
image: markus-autotest-server-dev:1.5.0
volumes:
- ./server:/app:cached
- venv_server:/home/docker/markus_venv
Expand All @@ -29,7 +29,7 @@ services:
dockerfile: ./.dockerfiles/Dockerfile
args:
UBUNTU_VERSION: '24.04'
image: markus-autotest-client-dev:1.4.0
image: markus-autotest-client-dev:1.5.0
container_name: 'autotest-client'
volumes:
- ./client:/app:cached
Expand Down
42 changes: 23 additions & 19 deletions server/.dockerfiles/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1.7-labs
ARG UBUNTU_VERSION=24.04

FROM ubuntu:$UBUNTU_VERSION AS base
Expand All @@ -8,7 +9,9 @@ RUN userdel -r ubuntu
ARG LOGIN_USER
ARG WORKSPACE

RUN apt-get update -y && \
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get -y install software-properties-common && \
DEBIAN_FRONTEND=noninteractive add-apt-repository -y ppa:deadsnakes/ppa && \
DEBIAN_FRONTEND=noninteractive apt-get -y install python3.11 \
Expand All @@ -21,18 +24,7 @@ RUN apt-get update -y && \
postgresql-client \
libpq-dev \
sudo \
git \
libfontconfig1-dev \
libcurl4-openssl-dev \
libfreetype6-dev \
libpng-dev \
libtiff5-dev \
libjpeg-dev \
libharfbuzz-dev \
libfribidi-dev \
libxml2-dev \
libnuma-dev \
r-base
git

RUN useradd -ms /bin/bash $LOGIN_USER && \
usermod -aG sudo $LOGIN_USER && \
Expand All @@ -43,17 +35,29 @@ RUN useradd -ms /bin/bash $LOGIN_USER && \
done && \
chmod a+x /home/${LOGIN_USER}

COPY . /app
RUN mkdir -p ${WORKSPACE} && chown ${LOGIN_USER} ${WORKSPACE} && \
mkdir -p /home/${LOGIN_USER}/markus_venv && chown ${LOGIN_USER} /home/${LOGIN_USER}/markus_venv

RUN find /app/autotest_server/testers -name requirements.system -exec {} \;

RUN echo "TZ=$( cat /etc/timezone )" >> /etc/R/Renviron.site
# Copy requirements.system files for all testers. These are copied separately from other files
# to avoid Docker cache invalidation of the subsequent RUN command if any other files are changed
# in markus-autotesting/server.
COPY --parents \
./autotest_server/testers/java/requirements.system \
./autotest_server/testers/js/requirements.system \
./autotest_server/testers/haskell/requirements.system \
./autotest_server/testers/r/requirements.system \
./autotest_server/testers/racket/requirements.system \
/app
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
--mount=type=cache,target=$WORKSPACE/.stack,sharing=locked \
find /app/autotest_server/testers -name requirements.system -exec {} \;

RUN mkdir -p ${WORKSPACE} && chown ${LOGIN_USER} ${WORKSPACE} && \
mkdir -p /home/${LOGIN_USER}/markus_venv && chown ${LOGIN_USER} /home/${LOGIN_USER}/markus_venv
COPY . /app

WORKDIR /home/${LOGIN_USER}

USER ${LOGIN_USER}

CMD /app/.dockerfiles/cmd-dev.sh
CMD ["/app/.dockerfiles/cmd-dev.sh"]
48 changes: 40 additions & 8 deletions server/autotest_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import shutil
import time
import json
import subprocess
import signal
import subprocess
import socket
import getpass
import requests
Expand Down Expand Up @@ -101,6 +101,31 @@ def _kill_user_processes(test_username: str) -> None:
subprocess.run(kill_cmd, shell=True)


def _kill_pgid_children(proc: subprocess.Popen) -> None:
"""
Kill all processes in our process group except the current (worker) process.
Tests may spawn subprocesses (e.g. CSC209) that share our group;
proc.kill() alone would leave those running.
Falls back to proc.kill() when /proc is unavailable (non-Linux).
"""
worker_pid = os.getpid()
worker_pgid = os.getpgid(worker_pid)
try:
for entry in os.scandir("/proc"):
if not entry.name.isdigit():
continue
pid = int(entry.name)
if pid == worker_pid:
continue
try:
if os.getpgid(pid) == worker_pgid:
os.kill(pid, signal.SIGKILL)
except (ProcessLookupError, PermissionError, OSError):
pass
except FileNotFoundError:
proc.kill()


def _create_test_script_command(tester_type: str) -> str:
"""
Return string representing a command line command to
Expand Down Expand Up @@ -218,13 +243,18 @@ def _run_test_specs(
out, err = "", ""
timeout_expired = None
timeout = test_data.get("timeout")
max_timeout = config.get("max_test_timeout")
if max_timeout is not None:
if timeout is None:
timeout = max_timeout
else:
timeout = min(timeout, max_timeout)
try:
env = settings.get("_env", {})
env_vars = {**os.environ, **_get_env_vars(test_username), **env}
env_vars = _update_env_vars(env_vars, test_env_vars)
proc = subprocess.Popen(
args,
start_new_session=True,
cwd=tests_path,
shell=True,
stdout=subprocess.PIPE,
Expand All @@ -238,14 +268,16 @@ def _run_test_specs(
settings_json = json.dumps({**settings, "test_data": test_data})
out, err = proc.communicate(input=settings_json, timeout=timeout)
except subprocess.TimeoutExpired:
if test_username == getpass.getuser():
pgrp = os.getpgid(proc.pid)
os.killpg(pgrp, signal.SIGKILL)
else:
if test_username != getpass.getuser():
_kill_user_processes(test_username)
else:
_kill_pgid_children(proc)
proc.wait()
out, err = proc.communicate()
if err == "Killed\n": # Default message from shell
test_group_name = test_data.get("extra_info", {}).get("name", "").strip()
test_group_name = test_data.get("extra_info", {}).get("name", "").strip()
if err == "Killed\n" or (not err and proc.returncode is not None and proc.returncode != 0):
# err can be "Killed\n" (shell default) or empty (SIGKILL/OOM silent crash).
# Check proc.returncode to reliably detect both cases.
if test_group_name:
err = f"Tests for {test_group_name} did not complete within time limit ({timeout}s)\n"
else:
Expand Down
3 changes: 1 addition & 2 deletions server/autotest_server/schema_skeleton.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
},
"test_data_categories": {
"type": "string",
"enum": [],
"enumNames": []
"enum": []
},
"extra_group_data": {},
"installed_testers": {
Expand Down
4 changes: 4 additions & 0 deletions server/autotest_server/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ workspace: !ENV ${WORKSPACE}
redis_url: !ENV ${REDIS_URL}
supervisor_url: !ENV ${SUPERVISOR_URL}
worker_log_dir: !ENV ${WORKER_LOG_DIR}
default_remote_url: https://polymouth.teach.cs.toronto.edu:443/chat
remote_url_whitelist:
- https://polymouth.teach.cs.toronto.edu:443/chat
max_test_timeout: 3600
workers:
- user: !ENV ${USER}
queues:
Expand Down
16 changes: 16 additions & 0 deletions server/autotest_server/settings_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,25 @@
"type": "string",
"minLength": 1
},
"default_remote_url": {
"type": "string",
"minLength": 1
},
"remote_url_whitelist": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"uniqueItems": true
},
"rlimit_settings": {
"type": "object"
},
"max_test_timeout": {
"type": "integer",
"minimum": 1
},
"workers": {
"type": "array",
"minItems": 1,
Expand Down
Loading
Loading