Common gotchas¶
The full list of things to avoid. None of these are theoretical — every entry is something that's bitten a previous contributor.
Format-string gotchas¶
%d vs %ld (the single biggest bug class)¶
The TI C2000 target has a 16-bit int. The grader host has a
32-bit int. A student writing %d for an int32_t produces
correct-looking output on the grader and broken output on hardware.
| Wrong | Right |
|---|---|
%d for int32_t |
%ld |
%u for uint32_t |
%lu |
%x for uint32_t |
%lx |
%f for any non-double |
%f is fine but pin precision: %.2f |
expect_format parses both the spec and the student's format string
into typed specifier sequences and flags mismatches with a TI-specific
hint. Use the exact spec string verbatim in
expect_format(call, "...spec string...").
Whitespace in the format string is ignored, but the rest isn't¶
expect_format tolerates whitespace differences, but every static
character outside whitespace must match. Time = %.2f sec and
Time=%.2f s are different for the matcher — the trailing static
text differs (sec vs s).
If your spec doesn't pin the static text, use the more permissive
expect_arg_types instead — it only checks the typed specifier
sequence.
Snapshot/restore gotchas¶
Forgetting to restore mid-check¶
Phase 3 / 4 must restore every volatile global it mutated. The canonical pattern (HW1):
const Hw1Phase3Snapshot baseline = take_snapshot();
// ... mutations ...
restore_snapshot(baseline);
return ok ? 1 : 0;
If you skip the restore, the next check in checker() sees polluted
state. The validation step "run twice without rebuilding → identical
pass result" is the regression test for this.
Not snapshotting enough¶
A check that toggles LED bits but forgets to snapshot GpioDataRegs
leaks state. If you mutate it, snapshot it. The HW1
Hw1Phase3Snapshot captures GpioDataRegs, UARTPrint, and
CpuTimer2.InterruptCount — extend it for any new volatile you
touch.
Cooperative driver gotchas¶
Using run_isr_for_us for a print check¶
run_isr_for_us only runs the ISR. If the print is gated by a flag
the main loop body sets (if (UARTPrint) serial_printf(...)),
no print fires because the main body never runs. Use
grader::drive_isr_with_main_pump(...) for any print/cadence
assertion.
Calling start_main_thread() more than once per check¶
Validator::start_main_thread() invokes grader::run_student_init(),
which is idempotent (runs init only on the first call). Calling it
multiple times is harmless but wasteful. Once per check function is
the convention.
If you genuinely need to re-run init (uncommon), call
grader::reset_student_init() first.
Student source has for(;;) or while(true) instead of while(1)¶
tools/patch_student_source.py only rewrites while (1). The fixture
will compile but the main-loop body won't be reachable from
step_main_loop. Symptom: every cadence check reports 0 calls.
The fix is either editing the fixture or extending the patcher's regex; the latter is the right answer if it's a common pattern in your student population.
Stimulus gotchas¶
Skipping the primer¶
When testing "button held → LED toggles", many student implementations
use an edge detector (if prev==1 && cur==0). A naïve test that just
asserts press_button(4) before driving the ISR doesn't produce an
edge because prev starts at 0. Always fire one ISR with the buttons
released first — see
Using stimulus.
Mixing active-high and active-low¶
Buttons on the C2000 LaunchPad are active-low. press_button(n)
writes GPxDAT.bit.GPIO{n} = 0. If your check sees no reaction,
verify the spec's polarity and check what bit press_button actually
sets versus what the student reads.
Cadence tolerance gotchas¶
Loose tolerances mask real bugs¶
Anything ≥ ±25% is a smell. The cooperative driver makes cadence deterministic; if you need >10% slack, the check is probably wrong (wrong ISR period? wrong driver?). Investigate, don't loosen.
Window-based when the spec says "at least"¶
The spec says "fire at least every 100 ms" → use
expect_min_print_calls(port, expected, name). The spec says "fire
exactly every 250 ms" → use expect_print_cadence(port, count, 0.10).
Match the assertion to the wording.
Stub gotchas¶
Adding a new stub without the -include ti_stubs.h knowledge¶
Every TU is force-compiled with -include ti_stubs.h (see
CMakeLists.txt:217).
TI-only keywords (__interrupt, EALLOW, EDIS, DATA_SECTION,
Uint16) are mapped to no-ops or standard types. If your new stub
needs a TI-only macro that isn't in ti_stubs.h, add it there too.
Inventing a shadow array instead of writing to *Regs¶
The existing comparison overloads operate on the *Regs globals (and
gpiosSetup[] for the GPIO pair). If you invent a new shadow array
for your stub's state, you also have to wire it into
generated.cpp / compare_generated.cpp / populate_all_zero().
Prefer writing to the existing *Regs global so the comparison
machinery picks it up for free.
Workflow gotchas¶
Duplicate-id manifest without a slot-aware workflow¶
If you maintain a fork of the reusable workflow, make sure
out/${SLOT}.json (not out/${ID}.json) is the upload path and the
artifact name includes the slot. Single-id manifests still produce
slot == id, so no migration is needed, but two HW1 rows will
overwrite each other's artifact if the path is keyed by id alone.
Forgetting the <slot>.meta.json sidecar¶
render_report.py falls back to filename heuristics when the sidecar
is missing, but the fallback can't recover the commit SHA. Always
write the sidecar — it's three lines of shell and one Python heredoc.
See Manifest & slots for the contract.