Testing the evaluator
Student submissions are graded by a Java program, jars/afct-evaluator.jar, which the
build bakes into the image. Because it is the grading oracle, a regressed,
mis-swapped, or mis-invoked evaluator would silently mis-grade students rather than fail
loudly. To prevent that, every build runs a set of golden tests: known
answer/submission pairs are fed through the real jar and the correct/incorrect verdict is
asserted.
This page explains how the tests work and, most importantly, how to add your own.
How it works
- Fixtures live in
test/evaluator/:cases/holds the.jfffiles (answer keys and submissions).manifest.jsondeclares each case and its expected verdict.
- The runner is
scripts/evaluator-smoke.mjs. It invokes the jar through the same code path production uses (lib/java-runner.js), so the test exercises the exact JVM flags, environment allow-list, timeout, and output cap the submission worker applies. It parses the evaluator's JSON output and checkscorrectagainst the manifest. - CI runs it. The
evaluatorjob in.github/workflows/ci.ymlsets up a JRE 21 (matching the runtime image) plusfontconfigand a font, then runsnpm run test:evaluatoron every push and pull request. The font matters because JFLAP initializes Swing even in headless CLI mode and throwsFontconfig head is nullwithout it. A failing case fails the build.
Running the tests locally
You need a JRE on your PATH, or you can run it inside the worker container, which
already has one:
# With a local JRE:
npm run test:evaluator
# Or against the image, via the worker container:
docker exec afct-dev-worker sh -c 'cd /app && npm run test:evaluator'
Adding a test case
-
Add the fixtures. Put the answer-key
.jffand the submission.jffintotest/evaluator/cases/. The convention is a*-solution.jffanswer key and one or more*-incorrect-answer-N.jffsubmissions, but any names work. -
Declare the case in
test/evaluator/manifest.json, appending tocases:{"name": "Short human-readable description","type": "FA","answer": "my-solution.jff","submission": "my-submission.jff","maxStates": -1,"deterministic": false,"expectCorrect": true}Field Meaning nameShown in the test output. typeFA,RE,CFG,PDA, orTM.answerThe answer-key file, relative to cases/.submissionThe file being graded, relative to cases/.maxStatesFA/PDAonly; state cap,-1for no limit.deterministicFAonly; whether the automaton must be a DFA.expectCorrectThe verdict this jar must return for the case to pass. maxStatesanddeterministicmap to the same per-type arguments thatbuildEvaluatorArgs()sends insrc/lib/submission-worker.ts, so a case runs exactly as production would grade it. -
Confirm and commit. Run
npm run test:evaluatorand check the jar returns the verdict you declared, then commit the fixtures together with the manifest entry.
A good pair of cases for any problem: grade a solution against itself
(expectCorrect: true) and grade a known-wrong submission against the solution
(expectCorrect: false).
What each type exercises
FA(finite automata) andRE(regular expressions) are decidable and need only the jar, so they are the most robust and make good defaults.CFGandPDAadditionally invoke the nativecfganalyzerbinary (bin/cfganalyzer). The runner already pointsCFGANALYZER_BINARYandCFGANALYZER_LIMITat it, so adding a CFG or PDA case is the way to also catch a brokencfganalyzerin the build.TM(Turing machines) are graded by bounded simulation.
Because context-free and Turing equivalence are undecidable in general, the CFG/PDA/TM checks are bounded and heuristic; pick fixtures whose verdict is unambiguous within those bounds.