-
Notifications
You must be signed in to change notification settings - Fork 27
Tester support for Javascript programming language. #698
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
david-yz-liu
merged 64 commits into
MarkUsProject:master
from
Karl-Michaud:js-autotester
Mar 27, 2026
Merged
Changes from 56 commits
Commits
Show all changes
64 commits
Select commit
Hold shift + click to select a range
d67bd04
PR setup
Karl-Michaud 6f7334b
Pushing research to show David
Karl-Michaud be89992
Pushing for David
Karl-Michaud c4877aa
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] e89e819
add JsTest and JsTester skeleton, and Node.js installation
freyazjiner a72d515
schema to define basic tester structure
f51368b
setup.py to handle enviro setup and ensure node installed
b999cdb
Completed js_tester logic
Karl-Michaud 411f915
Add core JsTester and JsTest implementation
Karl-Michaud 6f35703
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] e3394ea
Merge branch 'master' into js-autotester
Karl-Michaud 92aace9
Merge remote-tracking branch 'karl/js-autotester' into freya-js-autot…
freyazjiner d80f840
Add requirements.system to install Node.js for JS tester
Karl-Michaud 7c03e06
Merge pull request #5 from Karl-Michaud/requirements-script
Karl-Michaud d6c57cd
Merge pull request #1 from lizzie-liu/js-tester-skeleton
Karl-Michaud 84b2108
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 06599ca
Merge branch 'js-autotester' of https://github.com/Karl-Michaud/marku…
freyazjiner e49d42f
Move files from javascript/ to js/ for consistent naming
freyazjiner 8a8d7a0
Move files from javascript/ to js/ for consistent naming
freyazjiner ad2d663
Update schema and setup, add tests for the JavaScript tester
freyazjiner 0092b1a
remove redundant code
freyazjiner e0c5ac5
revert change
freyazjiner 2600026
add test file for js tester
freyazjiner bfa886a
Merge pull request #6 from freyazjiner/freya-js-autotester
Karl-Michaud 51275ba
fix docstring for install() in js setup.py
Karl-Michaud bd10eec
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] a3acdd9
use pnpm instead
freyazjiner cc0f7a1
remove per-test Jest timeout from schema and use a global test group …
1be88c2
Merge pull request #7 from freyazjiner/freya-js-autotester
Karl-Michaud 37a9716
define global class var for default timeout
96ee7e7
Merge branch 'js-autotester' into liz-js-autotester
lizzie-liu 605df6f
Merge pull request #8 from lizzie-liu/liz-js-autotester
Karl-Michaud 71bd3ec
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] a4deb80
Added pnpm install in requirements script
Karl-Michaud db2c2e1
Fixed timeout misunderstanding
Karl-Michaud 91a9623
Add docstrings and clean up JS tester files
Karl-Michaud 5dbea9d
Add missing __init__.py for js tester package
Karl-Michaud efa6081
Add and update JS tester tests
Karl-Michaud 18b8aad
Fix flake8 ambiguous variable name in test
Karl-Michaud 881c7da
Removed research doc from branch
Karl-Michaud a812ffd
Use npm to install jest globally in requirements.system
Karl-Michaud 98e7385
bug fixes to pass the autotester in the UI
freyazjiner 859cc73
revert the change
freyazjiner b8c7e41
remove the redundant or in timeout
freyazjiner 8e8a010
Merge pull request #9 from freyazjiner/freya-js-autotester
Karl-Michaud 9838b4e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 8118165
Merge branch 'js-autotester' of https://github.com/Karl-Michaud/marku…
freyazjiner 5edb963
Fix JS autotester environment + Jest execution
freyazjiner f349742
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 7446892
fix js tester jest command logic
freyazjiner cfe4f56
merge the changes
freyazjiner cefa3f3
remove the redundant lines
freyazjiner fee00b3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] bfea50d
fix js tester runtime jest availability in container
freyazjiner 4aed38f
Merge remote-tracking branch 'karl/js-autotester' into freya-js-autot…
freyazjiner 22aed9d
fix corepack cache permissions for pnpm install in js tester
freyazjiner 45abd69
add additional example tests
freyazjiner 7589133
Revert "add additional example tests"
freyazjiner 4456afe
use pnpm exec jest and remove unnecessary js tester package.json
freyazjiner fff2177
update Changelog.md
freyazjiner 7e289d6
Merge branch 'master' into js-autotester
freyazjiner 2dd0264
fix js tester mocks
freyazjiner 93bfb80
Merge remote-tracking branch 'karl/js-autotester' into freya-js-autot…
freyazjiner 1c208e5
Merge branch 'master' into js-autotester
david-yz-liu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import subprocess | ||
| import json | ||
| import os | ||
|
|
||
| from ..tester import Tester, Test, TestError | ||
| from ..specs import TestSpecs | ||
|
|
||
|
|
||
| class JsTest(Test): | ||
| def __init__(self, tester, result): | ||
| self.test_name_ = result.get("fullName", "unknown") | ||
| self.status = result.get("status") | ||
| self.message = "\n".join(result.get("failureMessages", [])) | ||
| super().__init__(tester) | ||
|
|
||
| @property | ||
| def test_name(self): | ||
| """Return the full name of this test.""" | ||
| return self.test_name_ | ||
|
|
||
| @Test.run_decorator | ||
| def run(self): | ||
| """Return the result of this test based on its Jest status.""" | ||
| if self.status == "passed": | ||
| return self.passed() | ||
| elif self.status == "failed": | ||
| return self.failed(self.message) | ||
| else: | ||
| return self.error(message=self.message or f"Unexpected status: {self.status}") | ||
|
|
||
|
|
||
| class JsTester(Tester): | ||
|
|
||
| def __init__( | ||
| self, | ||
| specs: TestSpecs, | ||
| test_class=JsTest, | ||
| resource_settings: list[tuple[int, tuple[int, int]]] | None = None, | ||
| ) -> None: | ||
| """ | ||
| Initialize a JavaScript tester using the specifications in specs. | ||
|
|
||
| This tester will create tests of type test_class. | ||
| """ | ||
| super().__init__(specs, test_class, resource_settings=resource_settings) | ||
|
|
||
| def _run_pnpm_install(self, dir_path): | ||
| """ | ||
| Run pnpm install in dir_path to install dependencies from package.json. | ||
| """ | ||
| corepack_home = os.path.join(dir_path, ".corepack") | ||
| xdg_cache_home = os.path.join(dir_path, ".cache") | ||
| os.makedirs(corepack_home, exist_ok=True) | ||
| os.makedirs(xdg_cache_home, exist_ok=True) | ||
| env = {**os.environ, "COREPACK_HOME": corepack_home, "XDG_CACHE_HOME": xdg_cache_home} | ||
| result = subprocess.run( | ||
| ["pnpm", "install"], | ||
| capture_output=True, | ||
| text=True, | ||
| cwd=dir_path, | ||
| env=env, | ||
| ) | ||
| return result | ||
|
|
||
| def _run_jest(self, dir_path, timeout, test_files=None): | ||
| """ | ||
| Run Jest in dir_path and return its stdout and return code. | ||
|
|
||
| --json: output results as JSON to stdout | ||
| --forceExit: prevents jest from hanging if tests leave open connections | ||
| --runInBand: run all tests serially in the current process | ||
| """ | ||
| local_jest = os.path.join(dir_path, "node_modules", ".bin", "jest") | ||
| jest_cmd = local_jest if os.path.isfile(local_jest) else "jest" | ||
| cmd = [jest_cmd, "--rootDir", dir_path, "--json", "--forceExit", "--runInBand"] | ||
| if test_files: | ||
| cmd.extend(test_files) | ||
| result = subprocess.run( | ||
| cmd, | ||
| capture_output=True, | ||
| text=True, | ||
| cwd=dir_path, | ||
| timeout=timeout, | ||
| ) | ||
| return result.stdout, result.returncode | ||
|
|
||
| def _parse_jest_output(self, raw_json): | ||
| """ | ||
| Parse Jest's JSON output and return a list of individual test results. | ||
|
|
||
| Returns a tuple (results, error) where error is set if JSON parsing fails. | ||
| """ | ||
| try: | ||
| data = json.loads(raw_json) | ||
| except json.JSONDecodeError as e: | ||
| return None, e | ||
|
|
||
| results = [] | ||
| for test_suite in data.get("testResults", []): | ||
| for test in test_suite.get("assertionResults", []): | ||
| results.append(test) | ||
|
|
||
| return results, None | ||
|
|
||
| @Tester.run_decorator | ||
| def run(self): | ||
| """ | ||
| Run pnpm install and then Jest, parsing the results and printing each test outcome. | ||
| """ | ||
| dir_path = os.getcwd() | ||
| test_data = self.specs.get("test_data", default={}) or {} | ||
|
|
||
| timeout = test_data.get("timeout", 30) | ||
| if os.path.isfile(os.path.join(dir_path, "package.json")): | ||
| pnpm_result = self._run_pnpm_install(dir_path) | ||
| if pnpm_result.returncode != 0: | ||
| err = pnpm_result.stderr or pnpm_result.stdout or "(no output)" | ||
| raise TestError(f"pnpm install failed:\n{err}") | ||
|
|
||
| script_files = test_data.get("script_files", []) | ||
| try: | ||
| jest_json_output, jest_rc = self._run_jest(dir_path, timeout, test_files=script_files) | ||
| except subprocess.TimeoutExpired: | ||
| raise TestError("Jest timed out") | ||
|
|
||
| if jest_rc != 0: | ||
| err = jest_json_output | ||
| raise TestError(f"Jest failed (exit code {jest_rc}).\n{err}") | ||
| if not jest_json_output: | ||
| raise TestError("Jest produced no output") | ||
|
|
||
| test_results, err = self._parse_jest_output(jest_json_output) | ||
| if err: | ||
| raise TestError(str(err)) | ||
|
|
||
| for result in test_results: | ||
| if result.get("status") == "pending": | ||
| continue | ||
| test = self.test_class(self, result) | ||
| print(test.run(), flush=True) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file is unnecessary |
||
| "name": "autotest-js", | ||
| "version": "1.0.0", | ||
| "private": true | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| #!/usr/bin/env bash | ||
| set -euxo pipefail | ||
|
|
||
| if ! command -v node &> /dev/null; then | ||
| apt-get -y update | ||
| DEBIAN_FRONTEND=noninteractive apt-get install -y -o 'Dpkg::Options::=--force-confdef' -o 'Dpkg::Options::=--force-confold' curl ca-certificates | ||
| curl -fsSL https://deb.nodesource.com/setup_24.x | bash - | ||
| DEBIAN_FRONTEND=noninteractive apt-get install -y -o 'Dpkg::Options::=--force-confdef' -o 'Dpkg::Options::=--force-confold' nodejs | ||
| fi | ||
|
|
||
| if ! command -v pnpm &> /dev/null; then | ||
| corepack enable | ||
| corepack prepare pnpm@latest --activate | ||
| fi | ||
|
|
||
| if ! command -v jest &> /dev/null; then | ||
| export PNPM_HOME="/usr/local/share/pnpm" | ||
| export PATH="${PNPM_HOME}:${PATH}" | ||
| pnpm config set global-bin-dir /usr/local/bin | ||
| pnpm config set global-dir /usr/local/share/pnpm-global | ||
| pnpm add -g jest | ||
| fi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| from __future__ import annotations | ||
| from typing import Annotated | ||
| from msgspec import Meta | ||
| from markus_autotesting_core.types import BaseTestData, BaseTesterSettings | ||
|
|
||
|
|
||
| class JsTesterSettings(BaseTesterSettings): | ||
| """The settings for the JavaScript tester.""" | ||
|
|
||
| test_data: Annotated[list[JsTestData], Meta(title="Test Groups", min_length=1)] | ||
|
|
||
|
|
||
| class JsTestData(BaseTestData, kw_only=True): | ||
| """The `test_data` specification for the JavaScript tester.""" | ||
|
|
||
| pass |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import os | ||
| import subprocess | ||
| from ..schema import generate_schema | ||
| from .schema import JsTesterSettings | ||
|
|
||
|
|
||
| def create_environment(_settings, _env_dir, default_env_dir): | ||
| return {"PYTHON": os.path.join(default_env_dir, "bin", "python3")} | ||
|
|
||
|
|
||
| def install(): | ||
| """Run the requirements.system shell script to install Node.js, pnpm, and Jest.""" | ||
| path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.system") | ||
| print(f"[AUTOTESTER] Running {path}", flush=True) | ||
| subprocess.run(path, check=True) | ||
|
|
||
|
|
||
| def settings(): | ||
| return generate_schema(JsTesterSettings) |
19 changes: 19 additions & 0 deletions
19
server/autotest_server/tests/fixtures/specs/js/simple.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "tester_type": "js", | ||
| "test_data": [ | ||
| { | ||
| "script_files": [ | ||
| "test.js" | ||
| ], | ||
| "category": [ | ||
| "instructor" | ||
| ], | ||
| "timeout": 30, | ||
| "extra_info": { | ||
| "criterion": "criterion", | ||
| "display_output": "instructors", | ||
| "name": "JS Test Group 1" | ||
| } | ||
| } | ||
| ] | ||
| } |
Empty file.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to use
pnpm exec jesthere instead of manually defining a path.