diff --git a/.kiro/specs/scheduler-sim-decision-trace/tasks.md b/.kiro/specs/scheduler-sim-decision-trace/tasks.md index 7657d6a3..80307412 100644 --- a/.kiro/specs/scheduler-sim-decision-trace/tasks.md +++ b/.kiro/specs/scheduler-sim-decision-trace/tasks.md @@ -6,7 +6,7 @@ > - "Under ASAN" means the standard debug + ASAN scheduler build. - [ ] 1. Foundation: baselines, shared formatting, trace model and transport -- [ ] 1.1 Capture the trace-off REPL transcript baseline before any feature change +- [x] 1.1 Capture the trace-off REPL transcript baseline before any feature change - Build a scripted REPL session over a resource-contention replay fixture. The fixture has several jobs competing for a shared license, a cluster-wide limit and a host-scoped limit, plus a parallel job and an alternative-requirement job. - The session exercises step, continue with collapsed ranges, a decision breakpoint, a round-begin breakpoint, info subcommands, set units and set output. - Store the captured console transcript, the `--sim-output` recording and the `--sim-verify` result as committed test fixtures. Add a test that replays the script and compares byte for byte, ignoring only wall-clock metadata fields. @@ -334,3 +334,6 @@ - Correct the stale file sizes in the scheduler component guide and list the new files. - Done when the bounds test passes and a full ASAN build of all components passes with no test failures. - _Requirements: 9.1, 9.3, 9.5, 11.2_ + +## Implementation Notes +- 1.1: build dir is src/xce.build. Baseline test sched_repl_trace_baseline_test runs the built xce_scheduler (depends: scheduler); regenerate fixtures with XCE_REPL_BASELINE_REGEN=1 (script.txt is hand-written). Fixture sends the license value as a RESVALS_FULL event because shared resvals in the snapshot are dropped on load (bug-1923). sim_output.replay has no session decisions (set output switches files before session 1 is finalized); decisions are covered by set_output.replay and sim_verify.txt. diff --git a/src/scheduler/testsuite/fixtures/repl_trace_baseline/contention.replay b/src/scheduler/testsuite/fixtures/repl_trace_baseline/contention.replay new file mode 100644 index 00000000..36a45827 Binary files /dev/null and b/src/scheduler/testsuite/fixtures/repl_trace_baseline/contention.replay differ diff --git a/src/scheduler/testsuite/fixtures/repl_trace_baseline/script.txt b/src/scheduler/testsuite/fixtures/repl_trace_baseline/script.txt new file mode 100644 index 00000000..ca2bdbb6 --- /dev/null +++ b/src/scheduler/testsuite/fixtures/repl_trace_baseline/script.txt @@ -0,0 +1,41 @@ +help +info +info config +step +info session +info summary +info jobs +info job 104 +info worker all +info resource license +info limit all +set units raw +info worker w01 +set units human +info worker w01 +info resource all +set units both +info worker w02 +info queue all +set verify off +info config +set verify on +snapshot snap.replay +set output set_output.replay +break DECISION jobid=104 type=DISPATCH +info break +continue +info session +info job 104 +delete 1 +break BEGIN_ROUND +continue +info session +info summary +delete all +continue +info summary +info jobs +info config +set output off +quit diff --git a/src/scheduler/testsuite/fixtures/repl_trace_baseline/set_output.replay b/src/scheduler/testsuite/fixtures/repl_trace_baseline/set_output.replay new file mode 100644 index 00000000..aeafedde Binary files /dev/null and b/src/scheduler/testsuite/fixtures/repl_trace_baseline/set_output.replay differ diff --git a/src/scheduler/testsuite/fixtures/repl_trace_baseline/sim_output.replay b/src/scheduler/testsuite/fixtures/repl_trace_baseline/sim_output.replay new file mode 100644 index 00000000..b04a6982 Binary files /dev/null and b/src/scheduler/testsuite/fixtures/repl_trace_baseline/sim_output.replay differ diff --git a/src/scheduler/testsuite/fixtures/repl_trace_baseline/sim_verify.txt b/src/scheduler/testsuite/fixtures/repl_trace_baseline/sim_verify.txt new file mode 100644 index 00000000..a546e459 --- /dev/null +++ b/src/scheduler/testsuite/fixtures/repl_trace_baseline/sim_verify.txt @@ -0,0 +1,28 @@ +sim_log_mismatch decision verification MISMATCH at begin_round=2000 decision_index=0 expected_ts=0 -- expected: -- actual: DISPATCH jobid=0-0-0-101 nhosts=2 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=2000 decision_index=1 expected_ts=0 -- expected: -- actual: DISPATCH jobid=0-0-0-102 nhosts=1 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=2000 decision_index=2 expected_ts=0 -- expected: -- actual: DISPATCH jobid=0-0-0-103 nhosts=1 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=2000 decision_index=3 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-104 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=2000 decision_index=4 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-105 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=2000 decision_index=5 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-106 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=2000 decision_index=6 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-107 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=3000 decision_index=0 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-104 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=3000 decision_index=1 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-105 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=3000 decision_index=2 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-106 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=3000 decision_index=3 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-107 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=4000 decision_index=0 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-104 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=4000 decision_index=1 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-105 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=4000 decision_index=2 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-106 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=4000 decision_index=3 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-107 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=5000 decision_index=0 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-104 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=5000 decision_index=1 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-105 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=5000 decision_index=2 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-106 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=5000 decision_index=3 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-107 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=6000 decision_index=0 expected_ts=0 -- expected: -- actual: DISPATCH jobid=0-0-0-104 nhosts=1 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=6000 decision_index=1 expected_ts=0 -- expected: -- actual: DISPATCH jobid=0-0-0-105 nhosts=1 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=6000 decision_index=2 expected_ts=0 -- expected: -- actual: DISPATCH jobid=0-0-0-106 nhosts=2 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=6000 decision_index=3 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-107 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=7000 decision_index=0 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-107 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=8000 decision_index=0 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-107 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=9000 decision_index=0 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-107 main_reason=0 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=9000 decision_index=1 expected_ts=0 -- expected: -- actual: DISPATCH jobid=0-0-0-108 nhosts=1 -- diff: no recorded decision at this index +sim_log_mismatch decision verification MISMATCH at begin_round=10000 decision_index=0 expected_ts=0 -- expected: -- actual: PEND jobid=0-0-0-107 main_reason=0 -- diff: no recorded decision at this index diff --git a/src/scheduler/testsuite/fixtures/repl_trace_baseline/transcript.txt b/src/scheduler/testsuite/fixtures/repl_trace_baseline/transcript.txt new file mode 100644 index 00000000..755c7183 --- /dev/null +++ b/src/scheduler/testsuite/fixtures/repl_trace_baseline/transcript.txt @@ -0,0 +1,181 @@ +[sim] replay file contains 9 sessions +[sim] loaded: 0 jobs, snapshot @ ts=1000 (1970-01-01T00:00:00Z) +[sim] (jobs above reflect this snapshot only -- step/continue to replay later events) +[sim] ready. type 'help' for commands. +[sim] available commands: + +Execution Control: + step [N] Advance N sessions (default 1), pause after. + continue Run until next breakpoint hit or EOF. + quit Exit the scheduler immediately. + help Print this help listing. + +Breakpoints: + break [key=value ...] Set a breakpoint. Valid event types: CONFIG_PUSH, JOB_PUSH, WORKERS_PUSH, RESOURCES_PUSH, RESVALS_FULL, RESVALS_DELTA, JOB_STATE_CHANGE, JOB_UPDATE, QUEUE_UPDATE, JOB_RUSAGE, BEGIN_ROUND, DECISION. + info break List breakpoints. + delete |all Delete breakpoint(s). + +State Inspection: + info session Simulation position. + info job Dump one job. + info jobs List all job IDs. + info queue |all Dump queue(s). + info worker |all Dump worker(s). + info resource |all Dump resource(s). + info summary Quick overview. + info config Show sim parameters. + info limit |all Show limit setting(s) and usage. + +Snapshot & Parameters: + snapshot [FILE] Write a standalone snapshot. + set output |off Start/stop writing decisions. + set verify on|off Toggle decision verification. + set units raw|human|both Show resource values raw, human-scaled, or both (default: both). +[sim] info: usage: info [arg] +[sim] config: output=(active, path unknown) verify=on events_processed=0 current_ts=1000 (1970-01-01T00:00:00Z) sessions=0 +[sim] t=1001 (1970-01-01T00:00:00Z) session=1 +[sim] event: WORKERS_PUSH nworkers=2 names=[w01,w02] +[sim] event: QUEUE_UPDATE queue=normal +[sim] event: RESVALS_FULL n=1 +[sim] event: JOB_PUSH njobs=7 jobids=[0-0-0-101,0-0-0-102,0-0-0-103,0-0-0-104,0-0-0-105,0-0-0-106,0-0-0-107] +[sim] DISPATCH jobid=0-0-0-101 +[sim] DISPATCH jobid=0-0-0-102 +[sim] DISPATCH jobid=0-0-0-103 +[sim] PEND jobid=0-0-0-104 +[sim] PEND jobid=0-0-0-105 +[sim] PEND jobid=0-0-0-106 +[sim] PEND jobid=0-0-0-107 +[sim] session: ts=2000 (1970-01-01T00:00:00Z) event_index=4 sessions=1 +[sim] last session: round=2000 dispatch=3 pend=4 preempt=0 +[sim] summary: 7 jobs (4 pending, 3 running), 2 workers (2 online), 1 queues, 5 resources +[sim] next non-dummy session: 5 -- run 'step 4' to reach it +[sim] jobs: + 0-0-0-101 RUN normal + 0-0-0-102 RUN normal + 0-0-0-103 RUN normal + 0-0-0-104 PEND normal + 0-0-0-105 PEND normal + 0-0-0-106 PEND normal + 0-0-0-107 PEND normal +[sim] info jobs: 7 job(s) +[sim] job 104 (0-0-0-104): state=PEND queue=normal priority=0 queue_time=1104 resreq=Primary: + Tasks: min = 1, max = 1 + Rusages: + res license value 1.000000 per 3 duration -1.000000 decay 0 + allocs=0 +[sim] worker w01: status=online +[sim] -- Consumable/Capacity -- +[sim] resource slots: value=4.00 used=3.00 +[sim] resource mem: value=16384.000000 (16.00 KB) used=2048.000000 (2.00 KB) +[sim] resource limit=host_slots: value=3.00 used=3.00 +[sim] assigned job: jobid=0-0-0-101*2 +[sim] assigned job: jobid=0-0-0-102 +[sim] worker w02: status=online +[sim] -- Consumable/Capacity -- +[sim] resource slots: value=4.00 used=3.00 +[sim] resource mem: value=16384.000000 (16.00 KB) used=2048.000000 (2.00 KB) +[sim] resource limit=host_slots: value=3.00 used=3.00 +[sim] assigned job: jobid=0-0-0-101*2 +[sim] assigned job: jobid=0-0-0-103 +[sim] resource license: desc= valtype=numeric ordtype=incr flags=GLOBAL,SHARED,CONSUMABLE interval=0 +[sim] value=2.00 used=2.00 +[sim] limit: cluster_slots +[sim] USER = all +[sim] QUEUE = all +[sim] PROJECT = all +[sim] HOST = all +[sim] +[sim] slots = 6.000000 +[sim] usage limit=cluster_slots: accounted=6.00 capacity=6.00 +[sim] limit: host_slots +[sim] USER = all +[sim] QUEUE = all +[sim] PROJECT = all +[sim] HOST = all +[sim] +[sim] slots = 75.000000% +[sim] note: this limit's value is a percentage of a resource that is not shared across the cluster, so usage for it is tracked separately per host even though the configuration above does not show an explicit host scope +[sim] usage limit=host_slots host=w01: accounted=3.00 capacity=3.00 +[sim] usage limit=host_slots host=w02: accounted=3.00 capacity=3.00 +[sim] units set to raw +[sim] worker w01: status=online +[sim] -- Consumable/Capacity -- +[sim] resource slots: value=4.00 used=3.00 +[sim] resource mem: value=16384.000000 used=2048.000000 +[sim] resource limit=host_slots: value=3.00 used=3.00 +[sim] assigned job: jobid=0-0-0-101*2 +[sim] assigned job: jobid=0-0-0-102 +[sim] units set to human +[sim] worker w01: status=online +[sim] -- Consumable/Capacity -- +[sim] resource slots: value=4.00 used=3.00 +[sim] resource mem: value=16.00 KB used=2.00 KB +[sim] resource limit=host_slots: value=3.00 used=3.00 +[sim] assigned job: jobid=0-0-0-101*2 +[sim] assigned job: jobid=0-0-0-102 +[sim] -- Consumable/Capacity -- +[sim] resource license: desc= valtype=numeric ordtype=incr flags=GLOBAL,SHARED,CONSUMABLE interval=0 +[sim] value=2.00 used=2.00 +[sim] resource limit=cluster_slots: desc=(none) valtype=numeric ordtype=decr flags=GLOBAL,SHARED,RELEASE,CONSUMABLE,LIMIT interval=0 +[sim] value=6.00 used=6.00 +[sim] units set to both +[sim] worker w02: status=online +[sim] -- Consumable/Capacity -- +[sim] resource slots: value=4.00 used=3.00 +[sim] resource mem: value=16384.000000 (16.00 KB) used=2048.000000 (2.00 KB) +[sim] resource limit=host_slots: value=3.00 used=3.00 +[sim] assigned job: jobid=0-0-0-101*2 +[sim] assigned job: jobid=0-0-0-103 +[sim] queue normal: priority=1 status=open RUNLIMIT=n/a MAX_RUNNING=n/a job_pick_plugin=(inherit) +[sim] queued job: jobid=0-0-0-104 priority=0 +[sim] queued job: jobid=0-0-0-105 priority=0 +[sim] queued job: jobid=0-0-0-106 priority=0 +[sim] queued job: jobid=0-0-0-107 priority=0 +[sim] verify set to off +[sim] config: output=(active, path unknown) verify=off events_processed=4 current_ts=2000 (1970-01-01T00:00:00Z) sessions=1 +[sim] verify set to on +[sim] snapshot written: snap.replay (ts=2000, 1970-01-01T00:00:00Z) +[sim] output started: set_output.replay +[sim] breakpoint 1 set: DECISION jobid=104 type=DISPATCH +[sim] breakpoints: +[sim] 1: DECISION jobid=104 type=DISPATCH +[sim] round 2-4 (3 round(s), t=1970-01-01T00:00:00Z..1970-01-01T00:00:00Z): 0 dispatch, 4 pend, 0 preempt -- jobids=0-0-0-104,0-0-0-105,0-0-0-106,0-0-0-107 +[sim] hit breakpoint 1 @ decision: DISPATCH jobid=0-0-0-104 +[sim] session: ts=6000 (1970-01-01T00:00:00Z) event_index=6 sessions=5 +[sim] last session: round=6000 dispatch=1 pend=0 preempt=0 +[sim] job 104 (0-0-0-104): state=RUN queue=normal priority=0 queue_time=1104 resreq=Primary: + Tasks: min = 1, max = 1 + Rusages: + res license value 1.000000 per 3 duration -1.000000 decay 0 + allocs=1 +[sim] breakpoint 1 deleted +[sim] breakpoint 2 set: BEGIN_ROUND +[sim] round 5-5 (1 round(s), t=1970-01-01T00:00:00Z..1970-01-01T00:00:00Z): 3 dispatch, 1 pend, 0 preempt -- jobids=0-0-0-107 +[sim] hit breakpoint 2 @ BEGIN_ROUND round=7000 +[sim] session: ts=6000 (1970-01-01T00:00:00Z) event_index=6 sessions=6 +[sim] last session: round=7000 dispatch=0 pend=0 preempt=0 +[sim] summary: 7 jobs (1 pending, 4 running), 2 workers (2 online), 1 queues, 5 resources +[sim] next non-dummy session: 8 -- run 'step 2' to reach it +[sim] deleted 1 breakpoint(s) +[sim] round 6-7 (2 round(s), t=1970-01-01T00:00:00Z..1970-01-01T00:00:00Z): 0 dispatch, 1 pend, 0 preempt -- jobids=0-0-0-107 +[sim] t=8500 (1970-01-01T00:00:00Z) session=8 +[sim] event: JOB_PUSH njobs=1 jobids=[0-0-0-108] +[sim] round 8-8 (1 round(s), t=1970-01-01T00:00:00Z..1970-01-01T00:00:00Z): 1 dispatch, 1 pend, 0 preempt -- jobids=0-0-0-107 +[sim] round 9-9 (1 round(s), t=1970-01-01T00:00:00Z..1970-01-01T00:00:00Z): 0 dispatch, 1 pend, 0 preempt -- jobids=0-0-0-107 +[sim] replay complete. 9 session(s) processed. +[sim] output written to: set_output.replay +[sim] simulation finished -- state remains available for inspection (info job/queue/worker/resource/summary). type 'quit' to exit. +[sim] summary: 8 jobs (1 pending, 4 running), 2 workers (2 online), 1 queues, 5 resources +[sim] next non-dummy session: none remaining +[sim] jobs: + 0-0-0-101 DONE normal + 0-0-0-102 EXIT normal + 0-0-0-103 DONE normal + 0-0-0-104 RUN normal + 0-0-0-105 RUN normal + 0-0-0-106 RUN normal + 0-0-0-107 PEND normal + 0-0-0-108 RUN normal +[sim] info jobs: 8 job(s) +[sim] config: output=set_output.replay verify=on events_processed=8 current_ts=10000 (1970-01-01T00:00:00Z) sessions=9 +[sim] output stopped: set_output.replay (4 event(s) written) diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 1247007b..0e760a75 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -2474,3 +2474,38 @@ test('scheduler sim discrete clock test', sched_sim_discrete_clock_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0'], depends: scheduler) + +# scheduler-sim-decision-trace spec, task 1.1 (Req 10.4, 10.5): the +# trace-off REPL transcript baseline. Runs the built xce_scheduler +# (XCE_SCHEDULER_BIN; `depends: scheduler` builds it first) in interactive +# simulation mode on the committed resource-contention replay under +# fixtures/repl_trace_baseline/ with a scripted session, and compares the +# console transcript, the --sim-output and `set output` recordings and the +# --sim-verify result with the committed baseline (wall-clock metadata +# excluded). The replay writer links the real scheduler_srcs, same +# technique as sched_sim_e2e_limit_visibility_test. Set +# XCE_REPL_BASELINE_REGEN=1 to regenerate the fixtures (see the file's +# header comment). +sched_repl_trace_baseline_test_srcs = ['test_repl_trace_baseline.c'] +foreach f : scheduler_srcs + if f != 'src/main.c' + sched_repl_trace_baseline_test_srcs += ['../' + f] + endif +endforeach + +sched_repl_trace_baseline_test = executable( + 'sched_repl_trace_baseline_test', + sched_repl_trace_baseline_test_srcs, + include_directories:[incdir], + dependencies: [scheduler_test_deps], + c_args: ['-DXCE_SCHEDULER_BIN="' + scheduler.full_path() + '"', + '-DXCE_BASELINE_DIR="' + + (meson.current_source_dir() / 'fixtures' + / 'repl_trace_baseline') + + '"'], + install: false) + +test('scheduler repl trace baseline test', + sched_repl_trace_baseline_test, suite: 'scheduler', + env: ['ASAN_OPTIONS=detect_leaks=0'], + depends: scheduler) diff --git a/src/scheduler/testsuite/test_repl_trace_baseline.c b/src/scheduler/testsuite/test_repl_trace_baseline.c new file mode 100644 index 00000000..9bb72f82 --- /dev/null +++ b/src/scheduler/testsuite/test_repl_trace_baseline.c @@ -0,0 +1,917 @@ +/* + * test_repl_trace_baseline.c - scheduler-sim-decision-trace spec, task 1.1 + * (Requirements 10.4, 10.5; design.md "Testing Strategy" -> "Equivalence + * Tests (Requirement 10)" item 2): the trace-off REPL transcript baseline. + * + * A scripted REPL session runs the real, built xce_scheduler binary + * (XCE_SCHEDULER_BIN, from testsuite/meson.build) in interactive simulation + * mode over a committed resource-contention replay. Its console transcript, + * its --sim-output recording, the recording a `set output` command opens + * midway, and its --sim-verify result are compared with committed fixtures + * captured before any decision-trace code existed. Later tasks of the spec + * use this test to prove that, with tracing off, the REPL output is + * unchanged (task 5.2 updates exactly the help and info-usage lines). + * + * Fixtures (testsuite/fixtures/repl_trace_baseline/, XCE_BASELINE_DIR): + * contention.replay input replay (written by write_contention_replay()) + * script.txt the REPL command script fed on stdin + * transcript.txt expected stdout, byte for byte + * sim_output.replay expected --sim-output recording + * set_output.replay expected recording opened by `set output` + * + * sim_output.replay holds only the header, the snapshot and the input + * events, with no session decisions. This is existing behaviour: the + * script's `set output` runs in xce_repl_post_session_pause(), before + * sched_record_session_finalize() writes session 1 (sim_loop.c), so session + * 1's decisions land in set_output.replay, as do all later sessions. + * Decision coverage therefore comes from set_output.replay and + * sim_verify.txt. The file written by the script's `snapshot snap.replay` + * is not compared; only its transcript line is. + * sim_verify.txt expected --sim-verify result: the mismatch lines of + * the log, each cut to start at its message + * ("sim_log_mismatch decision verification MISMATCH") + * so the logger's time/thread-id prefix is dropped + * + * The only metadata the comparison ignores: ReplayHeader. + * recordingStartWall in both recordings, and the log-line prefix (time and + * thread id) of the verify lines. Everything else is compared byte for + * byte. + * + * The contention replay: two workers (4 slots, 16384 mem each), a 2-unit + * cluster license, a cluster-wide limit of 6 slots and a host-scoped limit + * of 75% of each host's slots. Jobs compete for the license, both limits + * and the hosts: license jobs, a parallel job (2..4 tasks), an alternative- + * requirement job whose first alternative cannot fit, and slot-heavy jobs. + * Later rounds finish jobs (JOB_STATE_CHANGE) and push a late job, with + * idle rounds in between so `continue` collapses ranges. Every session's + * recorded decision list is empty, so --sim-verify reports each live + * decision as unexpected and no recorded-decision reconciliation runs. + * + * The child runs in a fresh temporary directory holding copies of the + * replay and script, with relative --sim-output/--logfile/`set output` + * paths, so no temporary path reaches the transcript. + * + * Regenerating (only when a spec task deliberately changes the expected + * output, e.g. task 5.2's help lines): run this binary with + * XCE_REPL_BASELINE_REGEN=1. It rewrites contention.replay and every + * expected-output fixture from the current tree; script.txt is hand + * written and never regenerated. Review the fixture diff before + * committing. + * + * Link style: the real scheduler_srcs list (minus main.c), same as + * test_sim_e2e_limit_visibility.c, which this file's replay writer + * follows (resources, license value and limit config baked into the + * snapshot; workers, queue and jobs as wire events). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "controller.capnp.h" +#include "recording.h" +#include "resobj.h" +#include "scheduler.h" +#include "sim.h" +#include "xcelimit.h" +#include "xstring.h" + +#if !defined(XCE_SCHEDULER_BIN) || !defined(XCE_BASELINE_DIR) +#error "XCE_SCHEDULER_BIN and XCE_BASELINE_DIR must be defined by meson" +#endif + +extern int write_capnp_message(FILE* f, struct capn* c); + +#define REPLAY_NAME "contention.replay" +#define SCRIPT_NAME "script.txt" +#define TRANSCRIPT_NAME "transcript.txt" +#define SIM_OUT_NAME "sim_output.replay" +#define SET_OUT_NAME "set_output.replay" +#define VERIFY_NAME "sim_verify.txt" +#define LOG_NAME "sched.log" +#define STDERR_NAME "stderr.txt" +#define SNAP_NAME "snap.replay" + +#define QUEUE_NAME "normal" +#define WORKER_A "w01" +#define WORKER_B "w02" +#define LICENSE_RES "license" +#define SNAPSHOT_TS 1000ULL +#define GLIMIT_NAME "cluster_slots" +#define HLIMIT_NAME "host_slots" + +#define MISMATCH_TAG "decision verification MISMATCH" +#define VERIFY_MARK "sim_log_mismatch " MISMATCH_TAG + +void setUp(void) { return; } +void tearDown(void) { return; } + +/* ---------------------------------------------------------------------- */ +/* File helpers */ +/* ---------------------------------------------------------------------- */ + +/* Reads a whole file. Returns NULL (and *len = 0) when it cannot be read, + * so a missing fixture fails the caller's assertion, not this helper. */ +static char* slurp(const char* path, long* len) +{ + FILE* f = fopen(path, "rb"); + char* buf; + long n; + + *len = 0; + if (f == NULL) { + return NULL; + } + if ((fseek(f, 0, SEEK_END) != 0) || ((n = ftell(f)) < 0)) { + fclose(f); + return NULL; + } + rewind(f); + buf = (char*)malloc((size_t)n + 1); + if (buf == NULL) { + fclose(f); + return NULL; + } + if ((n > 0) && (fread(buf, 1, (size_t)n, f) != (size_t)n)) { + free(buf); + fclose(f); + return NULL; + } + buf[n] = '\0'; + fclose(f); + *len = n; + return buf; +} + +static void spit(const char* path, const char* buf, long len) +{ + FILE* f = fopen(path, "wb"); + + TEST_ASSERT_NOT_NULL_MESSAGE(f, path); + if (len > 0) { + TEST_ASSERT_EQUAL_UINT((size_t)len, fwrite(buf, 1, (size_t)len, f)); + } + TEST_ASSERT_EQUAL_INT(0, fclose(f)); +} + +static void copy_file(const char* from, const char* to) +{ + long len; + char* buf = slurp(from, &len); + + TEST_ASSERT_NOT_NULL_MESSAGE(buf, from); + spit(to, buf, len); + free(buf); +} + +static void join_path(char* out, size_t outsz, const char* dir, + const char* name) +{ + int n = snprintf(out, outsz, "%s/%s", dir, name); + + TEST_ASSERT_TRUE((n > 0) && ((size_t)n < outsz)); +} + +/* ---------------------------------------------------------------------- */ +/* Contention replay writer */ +/* ---------------------------------------------------------------------- */ + +static xce_resource_t* bake_resource(var resmgr, const char* name, int flags) +{ + xce_resource_t res; + xce_resource_t* r; + + memset(&res, 0, sizeof(res)); + res.name = strdup(name); + res.valtype = XCE_VAL_NUMERIC; + res.flags = flags; + + r = makeResource(resmgr, &res); + TEST_ASSERT_NOT_NULL(r); + return r; +} + +static xce_name_val_t* make_nv(const char* name, float v) +{ + xce_name_val_t* nv = (xce_name_val_t*)calloc(1, sizeof(*nv)); + xce_val_t* val = (xce_val_t*)calloc(1, sizeof(*val)); + + TEST_ASSERT_NOT_NULL(nv); + TEST_ASSERT_NOT_NULL(val); + val->which = Value_u_fvalue; + val->u.fvalue = v; + nv->name = strdup(name); + nv->val = val; + return nv; +} + +static xce_worker_t* make_wire_worker(const char* name, float slots, + float mem) +{ + xce_worker_t* w = (xce_worker_t*)calloc(1, sizeof(*w)); + xce_name_val_t** nv = (xce_name_val_t**)calloc(2, sizeof(*nv)); + + TEST_ASSERT_NOT_NULL(w); + TEST_ASSERT_NOT_NULL(nv); + nv[0] = make_nv("slots", slots); + nv[1] = make_nv("mem", mem); + w->name = strdup(name); + w->status = 0; + w->nresvals_to_sched = 2; + w->resvals_to_sched = nv; + return w; +} + +static xce_job_t* make_push_job(uint64_t jobid, uint64_t sub_time, + const char* resreq, const char* user, + uint32_t mintasks, uint32_t maxtasks) +{ + xce_job_t* j = (xce_job_t*)calloc(1, sizeof(*j)); + + TEST_ASSERT_NOT_NULL(j); + j->jobid = jobid; + j->state = JOB_STAT_PEND; + j->sub_time = sub_time; + j->queue_time = sub_time; + + j->submit = (job_submit_data_t*)calloc(1, sizeof(*(j->submit))); + TEST_ASSERT_NOT_NULL(j->submit); + j->submit->queue = (char*)QUEUE_NAME; + j->submit->user = (char*)user; + j->submit->project_name = (char*)"proj1"; + j->submit->resreq = (char*)resreq; + j->submit->mintasks = mintasks; + j->submit->maxtasks = maxtasks; + return j; +} + +static void write_event(FILE* f, replay_event_t* src) +{ + struct capn c; + struct capn_segment* cs; + ReplayEvent_ptr p; + + capn_init_malloc(&c); + cs = capn_root(&c).seg; + encode_ReplayEvent_ptr(cs, &p, src); + capn_setp(capn_root(&c), 0, p.p); + TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); + capn_free(&c); +} + +static void append_workers(FILE* f, uint64_t ts, xce_worker_t** w, int n) +{ + xce_workers_data_t wrap; + replay_event_t src; + + memset(&wrap, 0, sizeof(wrap)); + wrap.n_total = n; + wrap.count = n; + wrap.workers = w; + + memset(&src, 0, sizeof(src)); + src.ts = ts; + src.body_which = ReplayEvent_body_workersPush; + src.body.workers_push = &wrap; + write_event(f, &src); +} + +static void append_queue(FILE* f, uint64_t ts, const char* name) +{ + xce_queue_t q; + replay_event_t src; + + memset(&q, 0, sizeof(q)); + q.queue = (char*)name; + q.status = QUEUE_STAT_OPEN; + q.priority = 1; + + memset(&src, 0, sizeof(src)); + src.ts = ts; + src.body_which = ReplayEvent_body_queueUpdate; + src.body.queue_update = &q; + write_event(f, &src); +} + +/* The license's cluster-wide value, through the same RESVALS_FULL ingest + * the live driver records. bug-1923: shared resvals baked into the + * snapshot are dropped on load, so the snapshot cannot carry it. */ +static void append_license_value(FILE* f, uint64_t ts, float total) +{ + xce_val_t val; + xce_resval_t rv; + xce_resval_t* rvs[1]; + xce_resval_list_t list; + replay_event_t src; + + memset(&val, 0, sizeof(val)); + val.which = Value_u_fvalue; + val.u.fvalue = total; + + memset(&rv, 0, sizeof(rv)); + rv.name = (char*)LICENSE_RES; + rv.val = &val; + rvs[0] = &rv; + + memset(&list, 0, sizeof(list)); + list.count = 1; + list.vals = rvs; + + memset(&src, 0, sizeof(src)); + src.ts = ts; + src.body_which = ReplayEvent_body_resvalsFull; + src.body.resvals_full = &list; + write_event(f, &src); +} + +static void append_jobs(FILE* f, uint64_t ts, xce_job_t** jobs, int n) +{ + xce_jobs_data_t wrap; + replay_event_t src; + + memset(&wrap, 0, sizeof(wrap)); + wrap.n_total = n; + wrap.count = n; + wrap.jobs = jobs; + + memset(&src, 0, sizeof(src)); + src.ts = ts; + src.body_which = ReplayEvent_body_jobsPush; + src.body.jobs_push = &wrap; + write_event(f, &src); +} + +static void append_state_change(FILE* f, uint64_t ts, uint64_t jobid, + uint32_t old_state, uint32_t new_state) +{ + job_state_change_event_t change; + replay_event_t src; + + memset(&change, 0, sizeof(change)); + change.jobid = jobid; + change.old_state = old_state; + change.new_state = new_state; + + memset(&src, 0, sizeof(src)); + src.ts = ts; + src.body_which = ReplayEvent_body_jobStateChange; + src.body.job_state_change = &change; + write_event(f, &src); +} + +/* An empty decisions list: see this file's header comment. */ +static void append_session(FILE* f, uint64_t begin_round) +{ + struct capn c; + struct capn_segment* cs; + ReplaySession_ptr sp; + + capn_init_malloc(&c); + cs = capn_root(&c).seg; + sp = new_ReplaySession(cs); + ReplaySession_set_beginRound(sp, begin_round); + capn_setp(capn_root(&c), 0, sp.p); + TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c)); + capn_free(&c); +} + +static void append_end_of_stream(FILE* f) +{ + replay_event_t src; + + memset(&src, 0, sizeof(src)); + src.body_which = ReplayEvent_body_endSession; + write_event(f, &src); +} + +/* Header + snapshot via the real sched_record_start(), with the resources + * and both limits baked into the snapshot (the limit plugin is seeded from + * the snapshot before any replay event runs; see + * test_sim_e2e_limit_visibility.c's header comment). Records into a + * scratch directory; the caller moves the file to its final place. */ +static FILE* write_header_and_snapshot(char* out_path, size_t out_len, + const char* record_dir) +{ + xce_scheduler_t sched; + xce_sched_plugin_t* fcfs; + xce_limit_val_t gval = { .resname = (char*)"slots", + .percent = 0, + .val = 6.0 }; + xce_limit_config_t gcfg = { .name = (char*)GLIMIT_NAME, .val = &gval }; + xce_limit_val_t hval = { .resname = (char*)"slots", + .percent = 1, + .val = 0.75 }; + xce_limit_config_t hcfg = { .name = (char*)HLIMIT_NAME, .val = &hval }; + xce_limit_config_t* limits[2] = { &gcfg, &hcfg }; + xce_conf4sched_t config; + + memset(&sched, 0, sizeof(sched)); + mylock_init(&(sched.record_lock)); + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); + + fcfs = builtin_fcfs_new_plugin(); + TEST_ASSERT_NOT_NULL(fcfs); + TEST_ASSERT_TRUE(plugin_register_core(&(sched.core), fcfs) >= 0); + + (void)bake_resource(sched.core.resmgr, "slots", XCE_RESF_CONSUMABLE); + (void)bake_resource(sched.core.resmgr, "mem", XCE_RESF_CONSUMABLE); + (void)bake_resource(sched.core.resmgr, LICENSE_RES, + XCE_RESF_GLOBAL | XCE_RESF_SHARED + | XCE_RESF_CONSUMABLE); + + memset(&config, 0, sizeof(config)); + config.nlimits = 2; + config.limits = limits; + sched.core.config = &config; + + /* A frozen logical clock (speed 0, the simulator's own discrete-clock + * setting) pins the header's recordingStartTs and the snapshot ts to + * SNAPSHOT_TS instead of a CLOCK_MONOTONIC reading. */ + atomic_store(&(sched.core.virtual_clock_base), SNAPSHOT_TS); + atomic_store(&(sched.core.speed_num), 0ULL); + atomic_store(&(sched.core.speed_den), 1ULL); + + sched.core.job_pick_plugin = (char*)"builtin_fcfs"; + sched.record_dir = (char*)record_dir; + + TEST_ASSERT_EQUAL_INT(0, sched_record_start(&sched, REPLAY_NAME)); + TEST_ASSERT_TRUE(strlen(sched.record_file_path) < out_len); + strcpy(out_path, sched.record_file_path); + return sched.record_file; +} + +/* + * write_contention_replay - the input fixture (regeneration only). The + * license's value (2) arrives as a RESVALS_FULL event after the workers. + * Rounds (beginRound = ts) and what the baseline shows for them: + * 2000 jobs 101..107 pending, in FCFS order: 101 is a 2..4-task parallel + * job (dispatches 4 tasks over both hosts), 102/103/104 want one + * license each (104 pends: license exhausted), 105 wants an + * alternative whose first branch (4 tasks x 12288 mem) cannot fit, + * 106/107 want 3 slots each; the cluster-wide limit (6) and the + * host limit (3 per host) are then full, so 105..107 pend + * 3000, 4000, 5000 identical repeats (collapsed by `continue`) + * 6000 after 101 finishes and 102 exits: 104, 105 and 106 dispatch + * 7000, 8000 identical repeats + * 9000 after a late license job 108 is pushed and 103 finishes + * 10000 final round + */ +static void write_contention_replay(const char* dest) +{ + char record_dir[PATH_MAX]; + char path[PATH_MAX]; + FILE* f; + xce_worker_t* workers[2]; + xce_job_t* jobs[7]; + xce_job_t* late[1]; + + snprintf(record_dir, sizeof(record_dir), "/tmp/sched_trbase_gen_XXXXXX"); + TEST_ASSERT_NOT_NULL(mkdtemp(record_dir)); + + f = write_header_and_snapshot(path, sizeof(path), record_dir); + + workers[0] = make_wire_worker(WORKER_A, 4.0f, 16384.0f); + workers[1] = make_wire_worker(WORKER_B, 4.0f, 16384.0f); + append_workers(f, 1001ULL, workers, 2); + append_queue(f, 1002ULL, QUEUE_NAME); + append_license_value(f, 1002ULL, 2.0f); + + jobs[0] = make_push_job(101, 1101, "rusage[mem=1024]", "alice", 2, 4); + jobs[1] = make_push_job(102, 1102, "rusage[license=1]", "bob", 1, 1); + jobs[2] = make_push_job(103, 1103, "rusage[license=1]", "alice", 1, 1); + jobs[3] = make_push_job(104, 1104, "rusage[license=1]", "bob", 1, 1); + jobs[4] = make_push_job(105, 1105, + "{4*{rusage[mem=12288]}} || " + "{1*{rusage[mem=2048]}}", + "alice", 1, 1); + jobs[5] = make_push_job(106, 1106, "rusage[mem=512]", "bob", 3, 3); + jobs[6] = make_push_job(107, 1107, "rusage[mem=512]", "alice", 3, 3); + append_jobs(f, 1003ULL, jobs, 7); + + append_session(f, 2000ULL); + append_session(f, 3000ULL); + append_session(f, 4000ULL); + append_session(f, 5000ULL); + + append_state_change(f, 5500ULL, 101, JOB_STAT_RUN, JOB_STAT_DONE); + append_state_change(f, 5600ULL, 102, JOB_STAT_RUN, JOB_STAT_EXIT); + append_session(f, 6000ULL); + append_session(f, 7000ULL); + append_session(f, 8000ULL); + + late[0] = make_push_job(108, 8500, "rusage[license=1]", "bob", 1, 1); + append_jobs(f, 8500ULL, late, 1); + append_state_change(f, 8600ULL, 103, JOB_STAT_RUN, JOB_STAT_DONE); + append_session(f, 9000ULL); + append_session(f, 10000ULL); + + append_end_of_stream(f); + TEST_ASSERT_EQUAL_INT(0, fclose(f)); + + copy_file(path, dest); + unlink(path); + rmdir(record_dir); +} + +/* ---------------------------------------------------------------------- */ +/* Running the scripted REPL session */ +/* ---------------------------------------------------------------------- */ + +static char g_workdir[PATH_MAX]; +static int g_ran; + +/* Runs the built scheduler in a fresh work directory: stdin = script, + * stdout = transcript, stderr kept apart (sanitizer and startup noise is + * not part of the transcript). Returns the child's exit status. */ +static int run_session(const char* replay_src) +{ + char path[PATH_MAX]; + pid_t pid; + int status; + + snprintf(g_workdir, sizeof(g_workdir), "/tmp/sched_trbase_XXXXXX"); + TEST_ASSERT_NOT_NULL(mkdtemp(g_workdir)); + + join_path(path, sizeof(path), g_workdir, REPLAY_NAME); + copy_file(replay_src, path); + { + char src[PATH_MAX]; + + join_path(src, sizeof(src), XCE_BASELINE_DIR, SCRIPT_NAME); + join_path(path, sizeof(path), g_workdir, SCRIPT_NAME); + copy_file(src, path); + } + + fflush(stdout); + fflush(stderr); + pid = fork(); + TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork must succeed"); + if (pid == 0) { + if ((chdir(g_workdir) != 0) + || (freopen(SCRIPT_NAME, "r", stdin) == NULL) + || (freopen(TRANSCRIPT_NAME, "w", stdout) == NULL) + || (freopen(STDERR_NAME, "w", stderr) == NULL)) { + _exit(120); + } + setenv("TERM", "dumb", 1); + execl(XCE_SCHEDULER_BIN, XCE_SCHEDULER_BIN, "--sim", REPLAY_NAME, + "--interactive", "--sim-verify", "--sim-output", SIM_OUT_NAME, + "--logfile", LOG_NAME, (char*)NULL); + _exit(121); + } + + TEST_ASSERT_EQUAL_INT(pid, waitpid(pid, &status, 0)); + TEST_ASSERT_TRUE_MESSAGE(WIFEXITED(status), + "the scheduler must exit normally"); + return WEXITSTATUS(status); +} + +/* The --sim-verify result: every mismatch line of the log, cut to start at + * the message itself, which begins with the reporting function's name. + * This drops the logger's prefix: the time, the thread id, the level and + * the padded function-name column. */ +static char* extract_verify_result(const char* log_path, long* out_len) +{ + FILE* f = fopen(log_path, "r"); + char* out = NULL; + size_t cap = 0; + size_t len = 0; + char line[8192]; + + TEST_ASSERT_NOT_NULL_MESSAGE(f, "the scheduler log must exist"); + out = (char*)calloc(1, 1); + TEST_ASSERT_NOT_NULL(out); + while (fgets(line, sizeof(line), f) != NULL) { + char* start; + size_t n; + + if (strstr(line, MISMATCH_TAG) == NULL) { + continue; + } + start = strstr(line, VERIFY_MARK); + TEST_ASSERT_NOT_NULL_MESSAGE(start, "mismatch line must name " + VERIFY_MARK); + n = strlen(start); + if (len + n + 1 > cap) { + cap = (len + n + 1) * 2; + out = (char*)realloc(out, cap); + TEST_ASSERT_NOT_NULL(out); + } + memcpy(out + len, start, n + 1); + len += n; + } + fclose(f); + *out_len = (long)len; + return out; +} + +/* Runs the session once per process; in regeneration mode first rewrites + * the input replay, then copies every produced output into the fixtures. */ +static void ensure_session_ran(void) +{ + char replay[PATH_MAX]; + int regen = (getenv("XCE_REPL_BASELINE_REGEN") != NULL); + int rc; + + if (g_ran) { + return; + } + g_ran = 1; + + join_path(replay, sizeof(replay), XCE_BASELINE_DIR, REPLAY_NAME); + if (regen) { + write_contention_replay(replay); + } + TEST_ASSERT_EQUAL_INT_MESSAGE(0, access(replay, R_OK), + "committed contention.replay fixture " + "must exist"); + + rc = run_session(replay); + TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "the scripted session must end " + "with quit's exit(0)"); + + if (regen) { + static const char* const outs[] = { TRANSCRIPT_NAME, SIM_OUT_NAME, + SET_OUT_NAME }; + char from[PATH_MAX]; + char to[PATH_MAX]; + char* verify; + long vlen; + int i; + + for (i = 0; i < 3; i++) { + join_path(from, sizeof(from), g_workdir, outs[i]); + join_path(to, sizeof(to), XCE_BASELINE_DIR, outs[i]); + copy_file(from, to); + } + join_path(from, sizeof(from), g_workdir, LOG_NAME); + verify = extract_verify_result(from, &vlen); + join_path(to, sizeof(to), XCE_BASELINE_DIR, VERIFY_NAME); + spit(to, verify, vlen); + free(verify); + } +} + +/* ---------------------------------------------------------------------- */ +/* Comparisons */ +/* ---------------------------------------------------------------------- */ + +/* Reports the first differing line of two texts, for a readable failure. */ +static void assert_text_equal(const char* what, const char* exp, long elen, + const char* got, long glen) +{ + long i = 0; + long line = 1; + long bol = 0; + char msg[1024]; + + while ((i < elen) && (i < glen) && (exp[i] == got[i])) { + if (exp[i] == '\n') { + line++; + bol = i + 1; + } + i++; + } + if ((i == elen) && (i == glen)) { + return; + } + { + const char* ee = memchr(exp + bol, '\n', (size_t)(elen - bol)); + const char* ge = memchr(got + bol, '\n', (size_t)(glen - bol)); + int el = ee ? (int)(ee - (exp + bol)) : (int)(elen - bol); + int gl = ge ? (int)(ge - (got + bol)) : (int)(glen - bol); + + snprintf(msg, sizeof(msg), + "%s differs from the baseline at line %ld:\n" + " expected: %.*s\n actual: %.*s", + what, line, (el > 400) ? 400 : el, exp + bol, + (gl > 400) ? 400 : gl, got + bol); + } + TEST_FAIL_MESSAGE(msg); +} + +/* A recording's header fields other than recordingStartWall, plus the + * offset just past the header message. */ +static void read_header(const char* path, replay_header_t* hdr, + long* hdr_end) +{ + FILE* f = fopen(path, "rb"); + struct capn c; + ReplayHeader_ptr p; + struct ReplayHeader wire; + + TEST_ASSERT_NOT_NULL_MESSAGE(f, path); + TEST_ASSERT_EQUAL_INT(0, capn_init_fp(&c, f, 0)); + p.p = capn_getp(capn_root(&c), 0, 1); + read_ReplayHeader(&wire, p); + memset(hdr, 0, sizeof(*hdr)); + decode_ReplayHeader(hdr, &wire); + capn_free(&c); + *hdr_end = ftell(f); + fclose(f); +} + +static void assert_recording_equal(const char* name) +{ + char exp_path[PATH_MAX]; + char got_path[PATH_MAX]; + replay_header_t eh; + replay_header_t gh; + long e_end; + long g_end; + long elen; + long glen; + char* eb; + char* gb; + + join_path(exp_path, sizeof(exp_path), XCE_BASELINE_DIR, name); + join_path(got_path, sizeof(got_path), g_workdir, name); + + eb = slurp(exp_path, &elen); + TEST_ASSERT_NOT_NULL_MESSAGE(eb, "baseline recording fixture missing"); + TEST_ASSERT_TRUE_MESSAGE(elen > 0, "baseline recording fixture empty"); + gb = slurp(got_path, &glen); + TEST_ASSERT_NOT_NULL_MESSAGE(gb, "the session must write the recording"); + + read_header(exp_path, &eh, &e_end); + read_header(got_path, &gh, &g_end); + + TEST_ASSERT_EQUAL_STRING(eh.recorder_version, gh.recorder_version); + TEST_ASSERT_EQUAL_UINT64(eh.recording_start_ts, gh.recording_start_ts); + TEST_ASSERT_EQUAL_STRING(eh.sched_policy, gh.sched_policy); + TEST_ASSERT_EQUAL_PTR(NULL, eh.cluster); + TEST_ASSERT_EQUAL_PTR(NULL, gh.cluster); + + TEST_ASSERT_EQUAL_INT64_MESSAGE(elen - e_end, glen - g_end, + "recording length after the header " + "must match the baseline"); + TEST_ASSERT_EQUAL_MEMORY_MESSAGE(eb + e_end, gb + g_end, + (size_t)(elen - e_end), + "recording bytes after the header " + "must match the baseline"); + + free_ReplayHeader(&eh); + free_ReplayHeader(&gh); + free(eb); + free(gb); +} + +/* ---------------------------------------------------------------------- */ +/* Tests */ +/* ---------------------------------------------------------------------- */ + +/* The console transcript is byte-identical to the baseline (10.4, 10.5). */ +static void test_transcript_matches_baseline(void) +{ + char path[PATH_MAX]; + char* exp; + char* got; + long elen; + long glen; + + ensure_session_ran(); + + join_path(path, sizeof(path), XCE_BASELINE_DIR, TRANSCRIPT_NAME); + exp = slurp(path, &elen); + TEST_ASSERT_NOT_NULL_MESSAGE(exp, "baseline transcript fixture missing"); + TEST_ASSERT_TRUE_MESSAGE(elen > 0, "baseline transcript fixture empty"); + + join_path(path, sizeof(path), g_workdir, TRANSCRIPT_NAME); + got = slurp(path, &glen); + TEST_ASSERT_NOT_NULL(got); + + assert_text_equal("transcript", exp, elen, got, glen); + free(exp); + free(got); +} + +/* The baseline really covers what task 1.1 asks the session to exercise + * and the contention it asks the fixture to have, so a regenerated fixture + * cannot silently degrade to a trivial one. */ +static void test_baseline_covers_required_commands_and_contention(void) +{ + static const char* const needles[] = { + "[sim] available commands:", /* help */ + "[sim] info: usage: info <", /* info usage */ + "[sim] summary: ", /* info subcommands */ + "[sim] jobs:", + "[sim] job 104 (0-0-0-104): state=PEND", + "[sim] worker w01: status=online", + "[sim] resource license:", + "[sim] limit: cluster_slots", + "[sim] limit: host_slots", + "[sim] units set to raw", /* set units */ + "[sim] units set to human", + "[sim] units set to both", + "resource mem: value=16.00 KB used=2.00 KB", /* human */ + "resource mem: value=16384.000000 (16.00 KB)", /* both */ + "[sim] queue normal: priority=1", /* info queue all */ + "[sim] verify set to off", + "[sim] verify set to on", + "[sim] snapshot written: snap.replay", + "[sim] output started: set_output.replay", /* set output */ + "[sim] output stopped: set_output.replay", + "last session: round=2000", /* step */ + "[sim] round 2-4 (3 round(s)", /* collapsed range */ + "[sim] hit breakpoint 1 @ decision: DISPATCH jobid=0-0-0-104", + "[sim] hit breakpoint 2 @ BEGIN_ROUND round=7000", + "[sim] replay complete.", + /* contention: parallel job over both hosts, license exhausted, + * cluster-wide and host-scoped limits full */ + "assigned job: jobid=0-0-0-101*2", + "[sim] PEND jobid=0-0-0-104", + " value=2.00 used=2.00", + "usage limit=cluster_slots: accounted=6.00 capacity=6.00", + "usage limit=host_slots host=w01: accounted=3.00 capacity=3.00", + }; + char path[PATH_MAX]; + char* t; + long len; + size_t i; + + ensure_session_ran(); + join_path(path, sizeof(path), XCE_BASELINE_DIR, TRANSCRIPT_NAME); + t = slurp(path, &len); + TEST_ASSERT_NOT_NULL_MESSAGE(t, "baseline transcript fixture missing"); + for (i = 0; i < sizeof(needles) / sizeof(needles[0]); i++) { + TEST_ASSERT_NOT_NULL_MESSAGE(strstr(t, needles[i]), needles[i]); + } + free(t); +} + +/* The --sim-output and `set output` recordings match apart from + * ReplayHeader.recordingStartWall (10.4, 10.5). */ +static void test_recordings_match_baseline(void) +{ + ensure_session_ran(); + assert_recording_equal(SIM_OUT_NAME); + assert_recording_equal(SET_OUT_NAME); +} + +/* The --sim-verify result matches the baseline (10.4, 10.5). */ +static void test_sim_verify_result_matches_baseline(void) +{ + char path[PATH_MAX]; + char* exp; + char* got; + long elen; + long glen; + + ensure_session_ran(); + + join_path(path, sizeof(path), XCE_BASELINE_DIR, VERIFY_NAME); + exp = slurp(path, &elen); + TEST_ASSERT_NOT_NULL_MESSAGE(exp, "baseline sim_verify fixture missing"); + TEST_ASSERT_TRUE_MESSAGE(elen > 0, "baseline sim_verify fixture empty"); + + join_path(path, sizeof(path), g_workdir, LOG_NAME); + got = extract_verify_result(path, &glen); + + assert_text_equal("--sim-verify result", exp, elen, got, glen); + free(exp); + free(got); +} + +/* Removes the work directory and the files the session leaves in it. */ +static void remove_workdir(void) +{ + static const char* const names[] = { REPLAY_NAME, SCRIPT_NAME, + TRANSCRIPT_NAME, SIM_OUT_NAME, + SET_OUT_NAME, LOG_NAME, + STDERR_NAME, SNAP_NAME }; + char path[PATH_MAX]; + size_t i; + + if (g_workdir[0] == '\0') { + return; + } + for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + snprintf(path, sizeof(path), "%s/%s", g_workdir, names[i]); + unlink(path); + } + rmdir(g_workdir); +} + +int main(void) +{ + int rc; + + UNITY_BEGIN(); + RUN_TEST(test_transcript_matches_baseline); + RUN_TEST(test_baseline_covers_required_commands_and_contention); + RUN_TEST(test_recordings_match_baseline); + RUN_TEST(test_sim_verify_result_matches_baseline); + rc = UNITY_END(); + remove_workdir(); + return rc; +}