myxce/src/scheduler/testsuite/test_trace_equivalence_cmp.c
Rongsong Shen 2e733994c1
test(scheduler-sim-decision-trace): prove tracing never changes scheduling
Task 6.3: the contention and extra replays run with tracing off, all
jobs at summary and full level, and a job subset, in step and continue
mode; decisions, full pend reasons, --sim-output recordings, snapshots,
--sim-verify results and per-step accounting dumps are identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 16:49:26 +00:00

283 lines
8.9 KiB
C

/*
* test_trace_equivalence_cmp.c - the comparisons of the decision-trace
* equivalence test (test_trace_equivalence.c, which documents what is
* compared and why; scheduler-sim-decision-trace task 6.3): texts,
* .replay files apart from the header's wall-clock field, and the decoded
* recorded sessions (decisions, allocations, pend reasons). Each returns
* NULL when equal, else a malloc'd description of the first difference.
* Kept apart from the scenarios for the 800-line file cap.
*/
#include <limits.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "controller.capnp.h"
#include "preason.h"
#include "recording.h"
#include "scheduler.h"
#include "test_repl_trace_e2e.h"
#include "test_trace_equivalence.h"
extern int msg_root_has_shape(struct capn* c, capn_ptr* out,
size_t want_words, size_t want_ptrs);
/* ---------------------------------------------------------------------- */
/* Comparisons: each returns NULL when equal, else a description */
/* ---------------------------------------------------------------------- */
static char* describe(const char* fmt, ...)
__attribute__((format(printf, 1, 2)));
static char* describe(const char* fmt, ...)
{
char* out = (char*)malloc(1024);
va_list ap;
TEST_ASSERT_NOT_NULL(out);
va_start(ap, fmt);
vsnprintf(out, 1024, fmt, ap);
va_end(ap);
return out;
}
/* First differing line of two texts. */
char* text_diff(const char* a, const char* b)
{
long i = 0;
long line = 1;
long bol = 0;
while ((a[i] != '\0') && (a[i] == b[i])) {
if (a[i] == '\n') {
line++;
bol = i + 1;
}
i++;
}
if (a[i] == b[i]) {
return NULL;
}
return describe("line %ld:\n off: %.*s\n traced: %.*s", line,
(int)strcspn(a + bol, "\n"), a + bol,
(int)strcspn(b + bol, "\n"), b + bol);
}
/* A recording (or snapshot) header without recordingStartWall, and the
* offset just past it. */
static void read_header(const char* path, replay_header_t* hdr, long* end)
{
FILE* f = fopen(path, "rb");
struct capn c;
struct ReplayHeader wire;
ReplayHeader_ptr p;
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);
*end = ftell(f);
fclose(f);
}
static bool same_str(const char* a, const char* b)
{
return (a == b) || ((a != NULL) && (b != NULL) && (strcmp(a, b) == 0));
}
/* Byte comparison of two .replay files apart from recordingStartWall. */
char* recording_diff(const char* a_path, const char* b_path)
{
replay_header_t ha;
replay_header_t hb;
long ea;
long eb;
long la;
long lb;
char* a = slurp(a_path, &la);
char* b = slurp(b_path, &lb);
char* why = NULL;
long i;
TEST_ASSERT_NOT_NULL_MESSAGE(a, a_path);
TEST_ASSERT_NOT_NULL_MESSAGE(b, b_path);
read_header(a_path, &ha, &ea);
read_header(b_path, &hb, &eb);
if (!same_str(ha.recorder_version, hb.recorder_version)
|| (ha.recording_start_ts != hb.recording_start_ts)
|| !same_str(ha.sched_policy, hb.sched_policy)
|| (ha.cluster != NULL) || (hb.cluster != NULL)) {
why = describe("header fields differ");
} else if ((la - ea) != (lb - eb)) {
why = describe("length after the header: %ld != %ld", la - ea,
lb - eb);
} else {
for (i = 0; (i < la - ea) && (a[ea + i] == b[eb + i]); i++) {
}
if (i < la - ea) {
why = describe("byte %ld after the header differs", i);
}
}
free_ReplayHeader(&ha);
free_ReplayHeader(&hb);
free(a);
free(b);
return why;
}
/* ---------------------------------------------------------------------- */
/* Decoded decisions */
/* ---------------------------------------------------------------------- */
void sessions_read(const char* path, sessions_t* out)
{
FILE* f = fopen(path, "rb");
struct capn c;
int msg = 0;
memset(out, 0, sizeof(*out));
TEST_ASSERT_NOT_NULL_MESSAGE(f, path);
while (capn_init_fp(&c, f, 0) == 0) {
capn_ptr root;
if ((msg++ > 0)
&& msg_root_has_shape(&c, &root, ReplaySession_word_count,
ReplaySession_pointer_count)) {
ReplaySession_ptr sp;
replay_session_t* s = NULL;
int i;
sp.p = root;
decode_ReplaySession_ptr(&s, sp);
TEST_ASSERT_NOT_NULL(s);
out->s = (replay_session_t**)realloc(
out->s, sizeof(*out->s) * (size_t)(out->n + 1));
TEST_ASSERT_NOT_NULL(out->s);
out->s[out->n++] = s;
for (i = 0; i < s->n_decisions; i++) {
replay_decision_t* d = s->decisions[i];
if (d->body_which == ReplayDecision_body_dispatch) {
out->ndispatch++;
} else if (d->body_which == ReplayDecision_body_pend) {
xce_job_preason_t* pr = d->body.pend->reason;
out->npend++;
if ((pr != NULL)
&& ((pr->nhost_reasons + pr->nres_reasons) > 0)) {
out->npend_detail++;
}
}
}
}
capn_free(&c);
}
fclose(f);
}
void sessions_free(sessions_t* s)
{
int i;
for (i = 0; i < s->n; i++) {
free_ReplaySession_ptr(&(s->s[i]));
}
free(s->s);
memset(s, 0, sizeof(*s));
}
static char* alloc_diff(const xce_job_alloc_t* a, const xce_job_alloc_t* b)
{
int h;
if ((a == NULL) || (b == NULL)) {
return (a == b) ? NULL : describe("alloc NULL on one side");
}
if ((a->jobid != b->jobid) || (a->nhosts != b->nhosts)
|| (a->nres_shared != b->nres_shared)) {
return describe("dispatch %llu: nhosts %d/%d, nres_shared %d/%d",
(unsigned long long)a->jobid, a->nhosts, b->nhosts,
a->nres_shared, b->nres_shared);
}
for (h = 0; h < a->nhosts; h++) {
if (!same_str(a->allocs[h]->host, b->allocs[h]->host)
|| (a->allocs[h]->nallocs != b->allocs[h]->nallocs)) {
return describe("dispatch %llu: host %d differs",
(unsigned long long)a->jobid, h);
}
}
return NULL;
}
static char* decision_diff(const replay_decision_t* a,
const replay_decision_t* b)
{
char* why = NULL;
if ((a->ts != b->ts) || (a->body_which != b->body_which)) {
return describe("ts %llu/%llu, op %d/%d", (unsigned long long)a->ts,
(unsigned long long)b->ts, a->body_which,
b->body_which);
}
if (a->body_which == ReplayDecision_body_dispatch) {
return alloc_diff(a->body.dispatch->alloc, b->body.dispatch->alloc);
}
if (a->body_which == ReplayDecision_body_pend) {
if (a->body.pend->jobid != b->body.pend->jobid) {
return describe("pend jobid %llu/%llu",
(unsigned long long)a->body.pend->jobid,
(unsigned long long)b->body.pend->jobid);
}
if (!job_preason_eq(a->body.pend->reason, b->body.pend->reason,
&why)) {
char* out = describe("pend %llu reason: %s",
(unsigned long long)a->body.pend->jobid,
(why != NULL) ? why : "?");
free(why);
return out;
}
}
return NULL;
}
char* sessions_diff(const sessions_t* a, const sessions_t* b)
{
int i;
int j;
if (a->n != b->n) {
return describe("session count %d != %d", a->n, b->n);
}
for (i = 0; i < a->n; i++) {
if ((a->s[i]->begin_round != b->s[i]->begin_round)
|| (a->s[i]->n_decisions != b->s[i]->n_decisions)) {
return describe("session %d: round %llu/%llu, %d/%d decisions",
i, (unsigned long long)a->s[i]->begin_round,
(unsigned long long)b->s[i]->begin_round,
a->s[i]->n_decisions, b->s[i]->n_decisions);
}
for (j = 0; j < a->s[i]->n_decisions; j++) {
char* why = decision_diff(a->s[i]->decisions[j],
b->s[i]->decisions[j]);
if (why != NULL) {
char* out = describe("session %d decision %d: %s", i, j, why);
free(why);
return out;
}
}
}
return NULL;
}