/* P0-15e: Cap'n Proto partial merge semantics * * Verifies that the three persistence Cap'n Proto mapped types * (capnp_persist_cfg_t, capnp_snap_hdr_t, capnp_repl_envelope_t) * and the generated struct PersistConfig / SnapshotHeader / ReplEnvelope * round-trip through their C struct representations correctly. * * These are structural/compile-time + value round-trip tests that span * the Cap'n Proto schema generation (P0-C) and the typemap.h definitions. */ #include #include #include #include #include #include #include "config.h" #include "controller.capnp.h" #include "persist.h" /* ------------------------------------------------------------------ */ /* Helpers: persist_config_t → capnp_persist_cfg_t round-trip */ /* ------------------------------------------------------------------ */ /* Manually map persist_config_t fields to capnp_persist_cfg_t, matching the * field mapping in the persist.c checkpoint serialisation path. */ static capnp_persist_cfg_t cfg_to_capnp(const persist_config_t* c) { capnp_persist_cfg_t out; memset(&out, 0, sizeof(out)); out.walSyncMs = c->wal_sync_ms; out.snapIntervalS = c->snap_interval_s; out.snapWalSizeMb = c->snap_wal_size_mb; out.snapKeepN = c->snap_keep_n; out.transientJobTimeoutS = c->transient_job_timeout_s; out.finishedRetainS = c->finished_retain_s; out.workerRetainS = c->worker_retain_s; out.syncTimeoutMs = c->sync_timeout_ms; return out; } /* ------------------------------------------------------------------ */ /* Test 1: persist_config_t → capnp_persist_cfg_t round-trip */ /* ------------------------------------------------------------------ */ static void test_persist_config_capnp_round_trip(void) { persist_config_t src = persist_config_defaults(); /* Override to known values for deterministic checking */ src.wal_sync_ms = 42u; src.snap_interval_s = 120u; src.snap_wal_size_mb = 128u; src.snap_keep_n = 3u; src.transient_job_timeout_s = 600u; src.finished_retain_s = 7200u; src.worker_retain_s = 172800u; src.sync_timeout_ms = 100u; capnp_persist_cfg_t out = cfg_to_capnp(&src); TEST_ASSERT_EQUAL_UINT32(src.wal_sync_ms, out.walSyncMs); TEST_ASSERT_EQUAL_UINT32(src.snap_interval_s, out.snapIntervalS); TEST_ASSERT_EQUAL_UINT32(src.snap_wal_size_mb, out.snapWalSizeMb); TEST_ASSERT_EQUAL_UINT32(src.snap_keep_n, out.snapKeepN); TEST_ASSERT_EQUAL_UINT32(src.transient_job_timeout_s, out.transientJobTimeoutS); TEST_ASSERT_EQUAL_UINT32(src.finished_retain_s, out.finishedRetainS); TEST_ASSERT_EQUAL_UINT32(src.worker_retain_s, out.workerRetainS); TEST_ASSERT_EQUAL_UINT32(src.sync_timeout_ms, out.syncTimeoutMs); printf("test_persist_config_capnp_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 2: SnapshotHeader struct fields and version default */ /* ------------------------------------------------------------------ */ static void test_snapshot_header_capnp_fields(void) { /* Verify struct size constant matches the documented schema */ TEST_ASSERT_EQUAL_INT(48, (int)SnapshotHeader_struct_bytes_count); /* Populate a capnp_snap_hdr_t with known values and check them */ capnp_snap_hdr_t hdr; memset(&hdr, 0, sizeof(hdr)); hdr.lsn = 12345ULL; hdr.term = 2ULL; hdr.timestamp = 999999ULL; hdr.version = 1u; hdr.nJobs = 10u; hdr.nQueues = 3u; hdr.nWorkers = 5u; hdr.checksum = 0xDEADBEEFu; hdr.headerChecksum = 0xCAFEBABEu; TEST_ASSERT_EQUAL_UINT64(12345ULL, hdr.lsn); TEST_ASSERT_EQUAL_UINT64(2ULL, hdr.term); TEST_ASSERT_EQUAL_UINT64(999999ULL, hdr.timestamp); TEST_ASSERT_EQUAL_UINT32(1u, hdr.version); TEST_ASSERT_EQUAL_UINT32(10u, hdr.nJobs); TEST_ASSERT_EQUAL_UINT32(3u, hdr.nQueues); TEST_ASSERT_EQUAL_UINT32(5u, hdr.nWorkers); TEST_ASSERT_EQUAL_UINT32(0xDEADBEEFu, hdr.checksum); TEST_ASSERT_EQUAL_UINT32(0xCAFEBABEu, hdr.headerChecksum); printf("test_snapshot_header_capnp_fields: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 3: ReplEnvelope struct fields accessible */ /* ------------------------------------------------------------------ */ static void test_repl_envelope_capnp_fields(void) { /* Verify struct size constant */ TEST_ASSERT_EQUAL_INT(40, (int)ReplEnvelope_struct_bytes_count); capnp_repl_envelope_t env; memset(&env, 0, sizeof(env)); env.term = 7ULL; env.lsn = 100ULL; env.entryType = (uint32_t)WAL_JOB_SUBMIT; env.payload = NULL; /* optional data pointer */ env.crc = 0x12345678u; env.prevCrc = WAL_CRC_NONE; TEST_ASSERT_EQUAL_UINT64(7ULL, env.term); TEST_ASSERT_EQUAL_UINT64(100ULL, env.lsn); TEST_ASSERT_EQUAL_UINT32(WAL_JOB_SUBMIT, env.entryType); TEST_ASSERT_EQUAL_UINT32(0x12345678u, env.crc); TEST_ASSERT_EQUAL_UINT32(WAL_CRC_NONE, env.prevCrc); printf("test_repl_envelope_capnp_fields: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 4: RequestVoteRequest carries the Raft (lastLogTerm, */ /* lastLogIndex) tuple through a real encode → wire → decode round-trip */ /* (ha-term-safety task 1.2, Req 2.3/2.4/2.6). */ /* ------------------------------------------------------------------ */ static void test_request_vote_last_log_tuple_round_trip(void) { request_vote_req_t src; memset(&src, 0, sizeof(src)); src.term = 5ULL; src.candidate_id = "node-1"; src.config_tag = 9ULL; src.last_log_lsn = 100ULL; src.last_log_term = 4ULL; /* NEW: candidate's last-log term */ src.last_log_index = 100ULL; /* NEW: candidate's last-log index */ /* Encode the typemap struct into a capnp message, serialize to a flat * buffer, then decode it back — exercises BOTH the schema field and the * typemap.h mirror mapping (encode_/decode_ go through the C struct). */ struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; /* encode_*_ptr allocates a struct in the segment and writes the C struct * fields into it (via the $C.mapname mapping). */ RequestVoteRequest_ptr p; encode_RequestVoteRequest_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); RequestVoteRequest_ptr dp; dp.p = capn_getp(capn_root(&d), 0, 1); struct RequestVoteRequest wire; read_RequestVoteRequest(&wire, dp); request_vote_req_t dst; memset(&dst, 0, sizeof(dst)); decode_RequestVoteRequest(&dst, &wire); TEST_ASSERT_EQUAL_UINT64(5ULL, dst.term); TEST_ASSERT_EQUAL_UINT64(9ULL, dst.config_tag); TEST_ASSERT_EQUAL_UINT64(100ULL, dst.last_log_lsn); TEST_ASSERT_EQUAL_UINT64(4ULL, dst.last_log_term); /* survives the wire */ TEST_ASSERT_EQUAL_UINT64(100ULL, dst.last_log_index); /* survives the wire */ capn_free(&d); free(buf); printf("test_request_vote_last_log_tuple_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 5: WalCatchupEntry (replication envelope) round-trips the */ /* AppendEntries term fields (prev_log_index, prev_log_term, term) */ /* (ha-term-safety task 1.1, Req 1.3/1.4). */ /* ------------------------------------------------------------------ */ static void test_wal_catchup_append_term_fields_round_trip(void) { wal_catchup_entry_t src; memset(&src, 0, sizeof(src)); src.lsn = 42ULL; src.entry_type = (uint8_t)WAL_JOB_SUBMIT; src.term = 7ULL; /* NEW: this entry's term stamp */ src.prev_log_index = 41ULL; /* NEW: lsn - 1 */ src.prev_log_term = 6ULL; /* NEW: term at prev_log_index */ struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; WalCatchupEntry_ptr p; encode_WalCatchupEntry_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); WalCatchupEntry_ptr dp; dp.p = capn_getp(capn_root(&d), 0, 1); struct WalCatchupEntry wire; read_WalCatchupEntry(&wire, dp); wal_catchup_entry_t dst; memset(&dst, 0, sizeof(dst)); decode_WalCatchupEntry(&dst, &wire); TEST_ASSERT_EQUAL_UINT64(42ULL, dst.lsn); TEST_ASSERT_EQUAL_UINT64(7ULL, dst.term); /* survives the wire */ TEST_ASSERT_EQUAL_UINT64(41ULL, dst.prev_log_index); /* survives the wire */ TEST_ASSERT_EQUAL_UINT64(6ULL, dst.prev_log_term); /* survives the wire */ capn_free(&d); free(buf); printf("test_wal_catchup_append_term_fields_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 6: Data union's new jobArray variant (job-array-single-wal-record */ /* task 1.2, Req 6.1/6.2) round-trips the compact (base jobid, array size, */ /* one shared spec) push descriptor through the wire -- reusing the client */ /* submit request's existing JobSubmit shape verbatim (no new struct). */ /* This only proves the additive schema variant exists and round-trips; */ /* nothing calls it yet (wiring into sched/apiserver push is task 5.1/5.2). */ /* ------------------------------------------------------------------ */ static void test_data_job_array_variant_round_trip(void) { job_submit_data_t submit; memset(&submit, 0, sizeof(submit)); submit.queue = "batch"; submit.user = "rshen"; submit.mintasks = 1u; submit.maxtasks = 4u; submit.start_info = NULL; submit.job_name = "array-job"; submit.project_name = "proj"; submit.user_group = "grp"; submit.job_description = "job-array-single-wal-record round-trip test"; submit.resreq = "select=1"; xce_job_submit_t src; memset(&src, 0, sizeof(src)); src.jobid = 5000ULL; /* base jobid */ src.array_size = 250ULL; src.submit = &submit; xce_data_t data; memset(&data, 0, sizeof(data)); data.table = XceData_data_jobArray; data.data.job_array = &src; /* Encode -> flat wire buffer -> decode, exercising both the schema * field (jobArray @6) and the typemap.h mirror mapping (xce_data_t's * new job_array union member), the same pattern Test 4/5 use. */ struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; XceData_ptr p; encode_XceData_ptr(cs, &p, &data); capn_setp(capn_root(&c), 0, p.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); XceData_ptr dp; dp.p = capn_getp(capn_root(&d), 0, 1); struct XceData wire; read_XceData(&wire, dp); xce_data_t dst; memset(&dst, 0, sizeof(dst)); decode_XceData(&dst, &wire); TEST_ASSERT_EQUAL_INT(XceData_data_jobArray, dst.table); TEST_ASSERT_NOT_NULL(dst.data.job_array); TEST_ASSERT_EQUAL_UINT64(5000ULL, dst.data.job_array->jobid); TEST_ASSERT_EQUAL_UINT64(250ULL, dst.data.job_array->array_size); TEST_ASSERT_NOT_NULL(dst.data.job_array->submit); TEST_ASSERT_EQUAL_STRING("batch", dst.data.job_array->submit->queue); TEST_ASSERT_EQUAL_STRING("rshen", dst.data.job_array->submit->user); TEST_ASSERT_EQUAL_UINT32(1u, dst.data.job_array->submit->mintasks); TEST_ASSERT_EQUAL_UINT32(4u, dst.data.job_array->submit->maxtasks); TEST_ASSERT_EQUAL_STRING("array-job", dst.data.job_array->submit->job_name); /* Untouched: the existing schedjobs/jobs/etc. wire layout is unaffected -- * spot-check the enum values these variants relied on before this change * did not shift underneath them. */ TEST_ASSERT_EQUAL_INT(0, (int)XceData_data_schedjobs); TEST_ASSERT_EQUAL_INT(2, (int)XceData_data_jobs); TEST_ASSERT_EQUAL_INT(5, (int)XceData_data_resvals); TEST_ASSERT_EQUAL_INT(6, (int)XceData_data_jobArray); free_XceData(&dst); capn_free(&d); free(buf); printf("test_data_job_array_variant_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 7: ReplayHeader + SnapshotEntry (replay stream envelope schema, */ /* scheduler-core task 7.1, Req 5 AC 20) round-trip through the wire. */ /* */ /* Built entirely through the low-level capnp accessor API (new_X / */ /* X_get_* / X_set_*, all generated by $C.fieldgetset) instead of the */ /* encode_X/decode_X "codec" bridge to a mapname'd C struct. The codec */ /* bridge is a documented no-op for Data fields in this codebase's */ /* capnpc-c setup (see decode_UpgradeConfigRequest's empty body above, */ /* and request.c's xce_replconfigpush_req_raw comment) -- encoding */ /* ReplayHeader.configHash (a Data field) through it would silently */ /* write whatever garbage sits in an uninitialized native `struct */ /* ReplayHeader` local. Going through the accessors avoids that pitfall */ /* entirely and, since no "real" application C struct exists yet for */ /* these frame-only replay-envelope types (task 9+ builds the actual */ /* reader/writer), is the correct level to prove the SCHEMA/wire format */ /* itself round-trips -- which is all task 7.1 owns. */ /* ------------------------------------------------------------------ */ static void test_replay_header_snapshot_entry_round_trip(void) { /* Arrange + Act (message 1: ReplayHeader) */ struct capn c1; capn_init_malloc(&c1); struct capn_segment *cs1 = capn_root(&c1).seg; ReplayCluster_ptr cluster = new_ReplayCluster(cs1); capn_text cname = { 7, "xce-lab", NULL }; ReplayCluster_set_name(cluster, cname); capn_ptr ctrl_list = capn_new_ptr_list(cs1, 2); capn_setp(ctrl_list, 0, capn_new_string(cs1, "host1:9991", 10)); capn_setp(ctrl_list, 1, capn_new_string(cs1, "host2:9992", 10)); ReplayCluster_set_controllers(cluster, ctrl_list); capn_text rhost = { 5, "host1", NULL }; ReplayCluster_set_recordingHost(cluster, rhost); uint8_t hash[32]; for (int i = 0; i < 32; i++) hash[i] = (uint8_t)(i * 3 + 1); capn_list8 hash_list = capn_new_list8(cs1, 32); capn_setv8(hash_list, 0, hash, 32); capn_data hash_data; hash_data.p = hash_list.p; ReplayHeader_ptr hdr = new_ReplayHeader(cs1); capn_text rv = { 5, "1.0.0", NULL }; ReplayHeader_set_recorderVersion(hdr, rv); ReplayHeader_set_configHash(hdr, hash_data); ReplayHeader_set_cluster(hdr, cluster); ReplayHeader_set_recordingStartTs(hdr, 123456789ULL); capn_text wall = { 20, "2026-08-04T00:00:00Z", NULL }; ReplayHeader_set_recordingStartWall(hdr, wall); capn_text policy = { 12, "builtin_fcfs", NULL }; ReplayHeader_set_schedPolicy(hdr, policy); capn_setp(capn_root(&c1), 0, hdr.p); int64_t sz1 = capn_size(&c1); TEST_ASSERT_TRUE(sz1 > 0); uint8_t *buf1 = (uint8_t *)malloc((size_t)sz1); TEST_ASSERT_NOT_NULL(buf1); capn_write_mem(&c1, buf1, (size_t)sz1, 0); capn_free(&c1); /* Arrange + Act (message 2: SnapshotEntry -- MUST be the first data * message after ReplayHeader per Req 5 AC 20's stream framing rule; * encoded here as an independent message buffer since the file-level * multi-message concatenation is task 9+ reader/writer logic, not * this schema-only task's concern). */ struct capn c2; capn_init_malloc(&c2); struct capn_segment *cs2 = capn_root(&c2).seg; SnapshotData_ptr sdata = new_SnapshotData(cs2); SnapshotEntry_ptr snap = new_SnapshotEntry(cs2); SnapshotEntry_set_ts(snap, 99999ULL); SnapshotEntry_set_data(snap, sdata); capn_setp(capn_root(&c2), 0, snap.p); int64_t sz2 = capn_size(&c2); TEST_ASSERT_TRUE(sz2 > 0); uint8_t *buf2 = (uint8_t *)malloc((size_t)sz2); TEST_ASSERT_NOT_NULL(buf2); capn_write_mem(&c2, buf2, (size_t)sz2, 0); capn_free(&c2); /* Act: decode both messages back */ struct capn d1; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d1, buf1, (size_t)sz1, 0)); ReplayHeader_ptr rhdr; rhdr.p = capn_getp(capn_root(&d1), 0, 1); struct capn d2; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d2, buf2, (size_t)sz2, 0)); SnapshotEntry_ptr rsnap; rsnap.p = capn_getp(capn_root(&d2), 0, 1); /* Assert: ReplayHeader fields survive field-for-field */ capn_text out_rv = ReplayHeader_get_recorderVersion(rhdr); TEST_ASSERT_EQUAL_INT(5, out_rv.len); TEST_ASSERT_EQUAL_STRING("1.0.0", out_rv.str); capn_data out_hash = ReplayHeader_get_configHash(rhdr); TEST_ASSERT_EQUAL_INT(32, capn_len((capn_list8){ out_hash.p })); uint8_t out_hash_bytes[32]; TEST_ASSERT_EQUAL_INT(32, capn_getv8((capn_list8){ out_hash.p }, 0, out_hash_bytes, 32)); TEST_ASSERT_EQUAL_MEMORY(hash, out_hash_bytes, 32); TEST_ASSERT_EQUAL_UINT64(123456789ULL, ReplayHeader_get_recordingStartTs(rhdr)); capn_text out_wall = ReplayHeader_get_recordingStartWall(rhdr); TEST_ASSERT_EQUAL_STRING("2026-08-04T00:00:00Z", out_wall.str); capn_text out_policy = ReplayHeader_get_schedPolicy(rhdr); TEST_ASSERT_EQUAL_STRING("builtin_fcfs", out_policy.str); ReplayCluster_ptr out_cluster = ReplayHeader_get_cluster(rhdr); /* A nested-struct field getter (capn_getp(..., resolve=0)) can return an * unresolved far pointer; per-field accessors that index raw struct * offsets (capn_read*) do NOT auto-resolve it (unlike capn_get_text/ * capn_get_data, which resolve internally), so any nested struct * pointer must be explicitly resolved before its own field accessors * are used. */ capn_resolve(&out_cluster.p); capn_text out_cname = ReplayCluster_get_name(out_cluster); TEST_ASSERT_EQUAL_STRING("xce-lab", out_cname.str); capn_ptr out_ctrl_list = ReplayCluster_get_controllers(out_cluster); capn_resolve(&out_ctrl_list); TEST_ASSERT_EQUAL_INT(2, out_ctrl_list.len); capn_text text_default = { 0, "", NULL }; capn_text c0 = capn_get_text(out_ctrl_list, 0, text_default); capn_text c1t = capn_get_text(out_ctrl_list, 1, text_default); TEST_ASSERT_EQUAL_STRING("host1:9991", c0.str); TEST_ASSERT_EQUAL_STRING("host2:9992", c1t.str); capn_text out_rhost = ReplayCluster_get_recordingHost(out_cluster); TEST_ASSERT_EQUAL_STRING("host1", out_rhost.str); /* Assert: SnapshotEntry -- the required Observable of task 7.1 * (Req 5 AC 20): a minimal header-plus-snapshot-entry byte stream * round-trips through encode/decode. */ TEST_ASSERT_EQUAL_UINT64(99999ULL, SnapshotEntry_get_ts(rsnap)); SnapshotData_ptr out_sdata = SnapshotEntry_get_data(rsnap); TEST_ASSERT_FALSE(out_sdata.p.type == CAPN_NULL); capn_free(&d1); capn_free(&d2); free(buf1); free(buf2); printf("test_replay_header_snapshot_entry_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 8: SessionSummary (fully-real, all-scalar struct) round-trips. */ /* Bonus coverage beyond task 7.1's required Observable -- proves the */ /* rest of the new schema surface (ReplaySession/ReplayDecision/ */ /* ReplayEvent) actually compiles and links, not just SnapshotEntry. */ /* ------------------------------------------------------------------ */ static void test_session_summary_round_trip(void) { struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; SessionSummary_ptr summary = new_SessionSummary(cs); SessionSummary_set_durationNs(summary, 5000000000ULL); SessionSummary_set_nDecisions(summary, 42u); SessionSummary_set_nDispatched(summary, 40u); SessionSummary_set_nPended(summary, 2u); ReplayDecision_list decisions = new_ReplayDecision_list(cs, 1); set_ReplayDecision(&(struct ReplayDecision){ .ts = 111ULL }, decisions, 0); ReplaySession_ptr sess = new_ReplaySession(cs); ReplaySession_set_beginRound(sess, 7ULL); ReplaySession_set_decisions(sess, decisions); ReplaySession_set_summary(sess, summary); capn_setp(capn_root(&c), 0, sess.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); ReplaySession_ptr rsess; rsess.p = capn_getp(capn_root(&d), 0, 1); TEST_ASSERT_EQUAL_UINT64(7ULL, ReplaySession_get_beginRound(rsess)); SessionSummary_ptr out_summary = ReplaySession_get_summary(rsess); /* See the matching comment in the ReplayHeader test above: a nested * struct pointer returned from a generated X_get_Y() accessor may be an * unresolved far pointer and must be resolved before its own per-field * accessors (capn_read*-based) are used. */ capn_resolve(&out_summary.p); TEST_ASSERT_EQUAL_UINT64(5000000000ULL, SessionSummary_get_durationNs(out_summary)); TEST_ASSERT_EQUAL_UINT32(42u, SessionSummary_get_nDecisions(out_summary)); TEST_ASSERT_EQUAL_UINT32(40u, SessionSummary_get_nDispatched(out_summary)); TEST_ASSERT_EQUAL_UINT32(2u, SessionSummary_get_nPended(out_summary)); ReplayDecision_list out_decisions = ReplaySession_get_decisions(rsess); TEST_ASSERT_EQUAL_INT(1, capn_len(out_decisions)); struct ReplayDecision out_dec; get_ReplayDecision(&out_dec, out_decisions, 0); TEST_ASSERT_EQUAL_UINT64(111ULL, out_dec.ts); capn_free(&d); free(buf); printf("test_session_summary_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 9: JobStateChangeEvent round-trip (scheduler-core task 7.2, */ /* Req 5 AC 20) -- the first of the two genuinely new ReplayEvent.body */ /* payload types. Uses the encode_X/decode_X codec bridge (Test 6's */ /* pattern) since job_state_change_event_t is a real, fully-scalar C */ /* struct with no Data fields -- unlike the frame-only replay-envelope */ /* types in Tests 7-8, which have no live C struct yet. */ /* ------------------------------------------------------------------ */ static void test_job_state_change_event_round_trip(void) { job_state_change_event_t src; memset(&src, 0, sizeof(src)); src.jobid = 42424ULL; src.old_state = 2u; src.new_state = 8u; src.exit_code = -7; struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; JobStateChangeEvent_ptr p; encode_JobStateChangeEvent_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); JobStateChangeEvent_ptr dp; dp.p = capn_getp(capn_root(&d), 0, 1); struct JobStateChangeEvent wire; read_JobStateChangeEvent(&wire, dp); job_state_change_event_t dst; memset(&dst, 0, sizeof(dst)); decode_JobStateChangeEvent(&dst, &wire); TEST_ASSERT_EQUAL_UINT64(42424ULL, dst.jobid); TEST_ASSERT_EQUAL_UINT32(2u, dst.old_state); TEST_ASSERT_EQUAL_UINT32(8u, dst.new_state); TEST_ASSERT_EQUAL_INT32(-7, dst.exit_code); capn_free(&d); free(buf); printf("test_job_state_change_event_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 10: JobUpdateEvent round-trip (scheduler-core task 7.2, */ /* Req 5 AC 20) -- the second genuinely new ReplayEvent.body payload */ /* type. `job` embeds the FULL updated record by reusing the */ /* pre-existing Job/xce_job_t struct; populated non-trivially, including*/ /* nested submit/allocs/pidinfo/pend_reasons (incl. the HostAlloc/ */ /* ResAlloc list-of-pointers two levels down), to prove the whole tree */ /* round-trips through encode_Job_ptr/decode_Job_ptr -- the same nested-*/ /* pointer codec chain already proven for xce_job_submit_t->submit in */ /* Test 6, now exercised for Job's full field set. */ /* ------------------------------------------------------------------ */ static void test_job_update_event_round_trip(void) { /* Innermost: one HostAlloc with one ResAlloc. */ xce_alloc_t res_alloc; memset(&res_alloc, 0, sizeof(res_alloc)); res_alloc.resname = "cpu"; res_alloc.reserve = "resv1"; res_alloc.instid = 42ULL; res_alloc.value = 4.0; res_alloc.from = 500ULL; res_alloc.duration = 3600LL; xce_alloc_t *res_allocs[1] = { &res_alloc }; xce_host_alloc_t host_alloc; memset(&host_alloc, 0, sizeof(host_alloc)); host_alloc.host = "hostA"; host_alloc.nallocs = 1; host_alloc.allocs = res_allocs; xce_host_alloc_t *host_allocs[1] = { &host_alloc }; xce_job_alloc_t job_alloc; memset(&job_alloc, 0, sizeof(job_alloc)); job_alloc.jobid = 8001ULL; job_alloc.nhosts = 1; job_alloc.allocs = host_allocs; job_alloc.nres_shared = 0; job_alloc.allocs_shared = NULL; job_submit_data_t submit; memset(&submit, 0, sizeof(submit)); submit.queue = "normal"; submit.user = "alice"; submit.mintasks = 2u; submit.maxtasks = 8u; submit.start_info = NULL; submit.job_name = "update-test-job"; submit.project_name = "proj-x"; submit.user_group = "grp-x"; submit.job_description = "job update event round-trip test"; submit.resreq = "select=2"; unsigned int pids[2] = { 111u, 222u }; unsigned int pgids[1] = { 333u }; job_pidinfo_t pidinfo; memset(&pidinfo, 0, sizeof(pidinfo)); pidinfo.jobid = 8001ULL; pidinfo.npids = 2; pidinfo.pids = pids; pidinfo.npgids = 1; pidinfo.pgids = pgids; pidinfo.exdata = "pid-exdata"; xce_job_preason_t pend_reasons; memset(&pend_reasons, 0, sizeof(pend_reasons)); pend_reasons.main_reason = 7; pend_reasons.nhost_reasons = 0; pend_reasons.host_based_reasons = NULL; pend_reasons.nres_reasons = 0; pend_reasons.res_based_reasons = NULL; xce_job_t job; memset(&job, 0, sizeof(job)); job.jobid = 8001ULL; job.submit = &submit; job.sub_time = 1000; job.queue_time = 2000; job.start_time = 3000; job.finish_time = 4000; job.state = 5u; job.allocs = &job_alloc; job.run_time = 120; job.pidinfo = &pidinfo; job.pend_reasons = &pend_reasons; job.transient_since = 9999ULL; job_update_event_t src; memset(&src, 0, sizeof(src)); src.jobid = 8001ULL; src.changed_fields = 0x5u; src.job = &job; struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; JobUpdateEvent_ptr p; encode_JobUpdateEvent_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); JobUpdateEvent_ptr dp; dp.p = capn_getp(capn_root(&d), 0, 1); struct JobUpdateEvent wire; read_JobUpdateEvent(&wire, dp); job_update_event_t dst; memset(&dst, 0, sizeof(dst)); decode_JobUpdateEvent(&dst, &wire); TEST_ASSERT_EQUAL_UINT64(8001ULL, dst.jobid); TEST_ASSERT_EQUAL_UINT32(0x5u, dst.changed_fields); TEST_ASSERT_NOT_NULL(dst.job); TEST_ASSERT_EQUAL_UINT64(8001ULL, dst.job->jobid); TEST_ASSERT_EQUAL_UINT64(1000ULL, (uint64_t)dst.job->sub_time); TEST_ASSERT_EQUAL_UINT64(2000ULL, (uint64_t)dst.job->queue_time); TEST_ASSERT_EQUAL_UINT64(3000ULL, (uint64_t)dst.job->start_time); TEST_ASSERT_EQUAL_UINT64(4000ULL, (uint64_t)dst.job->finish_time); TEST_ASSERT_EQUAL_UINT32(5u, dst.job->state); TEST_ASSERT_EQUAL_INT(120, dst.job->run_time); TEST_ASSERT_EQUAL_UINT64(9999ULL, dst.job->transient_since); TEST_ASSERT_NOT_NULL(dst.job->submit); TEST_ASSERT_EQUAL_STRING("normal", dst.job->submit->queue); TEST_ASSERT_EQUAL_STRING("alice", dst.job->submit->user); TEST_ASSERT_EQUAL_UINT32(2u, dst.job->submit->mintasks); TEST_ASSERT_EQUAL_UINT32(8u, dst.job->submit->maxtasks); TEST_ASSERT_EQUAL_STRING("update-test-job", dst.job->submit->job_name); TEST_ASSERT_EQUAL_STRING("proj-x", dst.job->submit->project_name); TEST_ASSERT_EQUAL_STRING("grp-x", dst.job->submit->user_group); TEST_ASSERT_EQUAL_STRING("job update event round-trip test", dst.job->submit->job_description); TEST_ASSERT_EQUAL_STRING("select=2", dst.job->submit->resreq); TEST_ASSERT_NOT_NULL(dst.job->allocs); TEST_ASSERT_EQUAL_UINT64(8001ULL, dst.job->allocs->jobid); TEST_ASSERT_EQUAL_INT(1, dst.job->allocs->nhosts); TEST_ASSERT_NOT_NULL(dst.job->allocs->allocs); TEST_ASSERT_EQUAL_STRING("hostA", dst.job->allocs->allocs[0]->host); TEST_ASSERT_EQUAL_INT(1, dst.job->allocs->allocs[0]->nallocs); TEST_ASSERT_NOT_NULL(dst.job->allocs->allocs[0]->allocs); TEST_ASSERT_EQUAL_STRING("cpu", dst.job->allocs->allocs[0]->allocs[0]->resname); TEST_ASSERT_EQUAL_STRING("resv1", dst.job->allocs->allocs[0]->allocs[0]->reserve); TEST_ASSERT_EQUAL_UINT64(42ULL, dst.job->allocs->allocs[0]->allocs[0]->instid); /* Unity is built without UNITY_INCLUDE_DOUBLE; compare via float cast * (same pattern as scheduler/testsuite/test.c). */ TEST_ASSERT_EQUAL_FLOAT(4.0f, (float)dst.job->allocs->allocs[0]->allocs[0]->value); TEST_ASSERT_EQUAL_UINT64(500ULL, dst.job->allocs->allocs[0]->allocs[0]->from); TEST_ASSERT_EQUAL_INT64(3600LL, dst.job->allocs->allocs[0]->allocs[0]->duration); TEST_ASSERT_EQUAL_INT(0, dst.job->allocs->nres_shared); TEST_ASSERT_NULL(dst.job->allocs->allocs_shared); TEST_ASSERT_NOT_NULL(dst.job->pidinfo); TEST_ASSERT_EQUAL_UINT64(8001ULL, dst.job->pidinfo->jobid); TEST_ASSERT_EQUAL_INT(2, dst.job->pidinfo->npids); TEST_ASSERT_EQUAL_UINT32(111u, dst.job->pidinfo->pids[0]); TEST_ASSERT_EQUAL_UINT32(222u, dst.job->pidinfo->pids[1]); TEST_ASSERT_EQUAL_INT(1, dst.job->pidinfo->npgids); TEST_ASSERT_EQUAL_UINT32(333u, dst.job->pidinfo->pgids[0]); TEST_ASSERT_EQUAL_STRING("pid-exdata", dst.job->pidinfo->exdata); TEST_ASSERT_NOT_NULL(dst.job->pend_reasons); TEST_ASSERT_EQUAL_INT(7, dst.job->pend_reasons->main_reason); TEST_ASSERT_EQUAL_INT(0, dst.job->pend_reasons->nhost_reasons); TEST_ASSERT_NULL(dst.job->pend_reasons->host_based_reasons); TEST_ASSERT_EQUAL_INT(0, dst.job->pend_reasons->nres_reasons); TEST_ASSERT_NULL(dst.job->pend_reasons->res_based_reasons); free_JobUpdateEvent(&dst); capn_free(&d); free(buf); printf("test_job_update_event_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 11: DispatchDecision round-trip (scheduler-core task 7.3, Req 5 */ /* AC 5, AC 20) -- reuses JobAlloc verbatim (jobid lives at */ /* JobAlloc.jobid, no redundant top-level jobid field, per design */ /* review). Populates a real multi-level allocation tree (one host, one */ /* resource) via the JobAlloc/HostAlloc/ResAlloc codec chain already */ /* proven in Test 10. */ /* ------------------------------------------------------------------ */ static void test_dispatch_decision_round_trip(void) { xce_alloc_t res_alloc; memset(&res_alloc, 0, sizeof(res_alloc)); res_alloc.resname = "gpu"; res_alloc.reserve = "resv2"; res_alloc.instid = 7ULL; res_alloc.value = 2.0; res_alloc.from = 100ULL; res_alloc.duration = 1800LL; xce_alloc_t *res_allocs[1] = { &res_alloc }; xce_host_alloc_t host_alloc; memset(&host_alloc, 0, sizeof(host_alloc)); host_alloc.host = "hostB"; host_alloc.nallocs = 1; host_alloc.allocs = res_allocs; xce_host_alloc_t *host_allocs[1] = { &host_alloc }; xce_job_alloc_t job_alloc; memset(&job_alloc, 0, sizeof(job_alloc)); job_alloc.jobid = 9001ULL; job_alloc.nhosts = 1; job_alloc.allocs = host_allocs; job_alloc.nres_shared = 0; job_alloc.allocs_shared = NULL; sched_dispatch_decision_t src; memset(&src, 0, sizeof(src)); src.alloc = &job_alloc; struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; DispatchDecision_ptr p; encode_DispatchDecision_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); DispatchDecision_ptr dp; dp.p = capn_getp(capn_root(&d), 0, 1); struct DispatchDecision wire; read_DispatchDecision(&wire, dp); sched_dispatch_decision_t dst; memset(&dst, 0, sizeof(dst)); decode_DispatchDecision(&dst, &wire); TEST_ASSERT_NOT_NULL(dst.alloc); TEST_ASSERT_EQUAL_UINT64(9001ULL, dst.alloc->jobid); TEST_ASSERT_EQUAL_INT(1, dst.alloc->nhosts); TEST_ASSERT_NOT_NULL(dst.alloc->allocs); TEST_ASSERT_EQUAL_STRING("hostB", dst.alloc->allocs[0]->host); TEST_ASSERT_EQUAL_INT(1, dst.alloc->allocs[0]->nallocs); TEST_ASSERT_NOT_NULL(dst.alloc->allocs[0]->allocs); TEST_ASSERT_EQUAL_STRING("gpu", dst.alloc->allocs[0]->allocs[0]->resname); TEST_ASSERT_EQUAL_STRING("resv2", dst.alloc->allocs[0]->allocs[0]->reserve); TEST_ASSERT_EQUAL_UINT64(7ULL, dst.alloc->allocs[0]->allocs[0]->instid); TEST_ASSERT_EQUAL_FLOAT(2.0f, (float)dst.alloc->allocs[0]->allocs[0]->value); TEST_ASSERT_EQUAL_UINT64(100ULL, dst.alloc->allocs[0]->allocs[0]->from); TEST_ASSERT_EQUAL_INT64(1800LL, dst.alloc->allocs[0]->allocs[0]->duration); TEST_ASSERT_EQUAL_INT(0, dst.alloc->nres_shared); TEST_ASSERT_NULL(dst.alloc->allocs_shared); free_DispatchDecision(&dst); capn_free(&d); free(buf); printf("test_dispatch_decision_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 12: PendDecision round-trip (scheduler-core task 7.3, Req 5 */ /* AC 5, AC 20) -- reuses JobPendReason (xce_job_preason_t) verbatim. */ /* ------------------------------------------------------------------ */ static void test_pend_decision_round_trip(void) { xce_job_preason_t reason; memset(&reason, 0, sizeof(reason)); reason.main_reason = 3; reason.nhost_reasons = 0; reason.host_based_reasons = NULL; reason.nres_reasons = 0; reason.res_based_reasons = NULL; sched_pend_decision_t src; memset(&src, 0, sizeof(src)); src.jobid = 4242ULL; src.reason = &reason; struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; PendDecision_ptr p; encode_PendDecision_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); PendDecision_ptr dp; dp.p = capn_getp(capn_root(&d), 0, 1); struct PendDecision wire; read_PendDecision(&wire, dp); sched_pend_decision_t dst; memset(&dst, 0, sizeof(dst)); decode_PendDecision(&dst, &wire); TEST_ASSERT_EQUAL_UINT64(4242ULL, dst.jobid); TEST_ASSERT_NOT_NULL(dst.reason); TEST_ASSERT_EQUAL_INT(3, dst.reason->main_reason); TEST_ASSERT_EQUAL_INT(0, dst.reason->nhost_reasons); TEST_ASSERT_NULL(dst.reason->host_based_reasons); TEST_ASSERT_EQUAL_INT(0, dst.reason->nres_reasons); TEST_ASSERT_NULL(dst.reason->res_based_reasons); free_PendDecision(&dst); capn_free(&d); free(buf); printf("test_pend_decision_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 13: PreemptDecision round-trip (scheduler-core task 7.3, Req 5 */ /* AC 5, AC 20) -- PreemptVictim is fresh (no pre-existing wire */ /* counterpart) and deliberately distinct from decision.h's */ /* core-internal xce_preempt_victim_t/xce_preempt_t (task 4.4's */ /* in-process live-decision representation). Populates a 2-entry victim */ /* list to exercise List(PreemptVictim) with real data. */ /* ------------------------------------------------------------------ */ static void test_preempt_decision_round_trip(void) { preempt_victim_t v1; memset(&v1, 0, sizeof(v1)); v1.jobid = 111ULL; v1.workerid = "hostX"; preempt_victim_t v2; memset(&v2, 0, sizeof(v2)); v2.jobid = 222ULL; v2.workerid = "hostY"; preempt_victim_t *victims[2] = { &v1, &v2 }; xce_job_preason_t reason; memset(&reason, 0, sizeof(reason)); reason.main_reason = 9; sched_preempt_decision_t src; memset(&src, 0, sizeof(src)); src.jobid = 5555ULL; src.victims = victims; src.n_victims = 2; src.reason = &reason; struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; PreemptDecision_ptr p; encode_PreemptDecision_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); PreemptDecision_ptr dp; dp.p = capn_getp(capn_root(&d), 0, 1); struct PreemptDecision wire; read_PreemptDecision(&wire, dp); sched_preempt_decision_t dst; memset(&dst, 0, sizeof(dst)); decode_PreemptDecision(&dst, &wire); TEST_ASSERT_EQUAL_UINT64(5555ULL, dst.jobid); TEST_ASSERT_EQUAL_INT(2, dst.n_victims); TEST_ASSERT_NOT_NULL(dst.victims); TEST_ASSERT_EQUAL_UINT64(111ULL, dst.victims[0]->jobid); TEST_ASSERT_EQUAL_STRING("hostX", dst.victims[0]->workerid); TEST_ASSERT_EQUAL_UINT64(222ULL, dst.victims[1]->jobid); TEST_ASSERT_EQUAL_STRING("hostY", dst.victims[1]->workerid); TEST_ASSERT_NOT_NULL(dst.reason); TEST_ASSERT_EQUAL_INT(9, dst.reason->main_reason); free_PreemptDecision(&dst); capn_free(&d); free(buf); printf("test_preempt_decision_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 14: ReplayDecision's `body` union round-trips through the */ /* dispatch variant (scheduler-core task 7.3, Req 5 AC 20) -- proves */ /* the purely-additive union bolted onto task 7.1's ts@0-only stub */ /* actually wires up end to end (mirrors the JobControl/JoinData */ /* `:union` group precedent: the group name consumes no ordinal). */ /* ------------------------------------------------------------------ */ static void test_replay_decision_dispatch_union_round_trip(void) { xce_job_alloc_t job_alloc; memset(&job_alloc, 0, sizeof(job_alloc)); job_alloc.jobid = 7777ULL; job_alloc.nhosts = 0; job_alloc.allocs = NULL; job_alloc.nres_shared = 0; job_alloc.allocs_shared = NULL; sched_dispatch_decision_t dispatch; memset(&dispatch, 0, sizeof(dispatch)); dispatch.alloc = &job_alloc; replay_decision_t src; memset(&src, 0, sizeof(src)); src.ts = 55555ULL; src.body_which = ReplayDecision_body_dispatch; src.body.dispatch = &dispatch; struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; ReplayDecision_ptr p; encode_ReplayDecision_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); ReplayDecision_ptr dp; dp.p = capn_getp(capn_root(&d), 0, 1); struct ReplayDecision wire; read_ReplayDecision(&wire, dp); replay_decision_t dst; memset(&dst, 0, sizeof(dst)); decode_ReplayDecision(&dst, &wire); TEST_ASSERT_EQUAL_UINT64(55555ULL, dst.ts); TEST_ASSERT_EQUAL_INT((int)ReplayDecision_body_dispatch, (int)dst.body_which); TEST_ASSERT_NOT_NULL(dst.body.dispatch); TEST_ASSERT_NOT_NULL(dst.body.dispatch->alloc); TEST_ASSERT_EQUAL_UINT64(7777ULL, dst.body.dispatch->alloc->jobid); free_ReplayDecision(&dst); capn_free(&d); free(buf); printf("test_replay_decision_dispatch_union_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 15: SnapshotData round-trip (scheduler-core task 7.3, Req 5 */ /* AC 21, Req 6 AC 5) -- the full snapshot message, replacing task */ /* 7.1's zero-field placeholder wholesale. Populates all 11 fields with */ /* non-trivial data (including >=1 entry in each of the top-level */ /* List(...) fields: queues/users/projects) and round-trips them */ /* field-for-field through encode_SnapshotData_ptr/decode_SnapshotData */ /* -- EXCEPT internalState (a Data field), whose generated codec is a */ /* documented no-op (same limitation as every other Data field in this */ /* schema, e.g. ReplayHeader.configHash in Test 7) -- verified instead */ /* via the low-level SnapshotData_get/set_internalState accessor on the */ /* SAME struct pointer the codec bridge already populated, same hybrid */ /* technique used nowhere else in this file but safe because encode_/ */ /* set_ operate on independent field slots of one already-allocated */ /* capnp struct. */ /* */ /* Also demonstrates this task's internalVersion Observable: "a */ /* snapshot encoded with one internal-state version and decoded */ /* expecting a different version is rejected rather than */ /* misinterpreted." internalVersion is a schema-side format TAG only -- */ /* the real accept/reject decision is production logic for task 8.1+ to */ /* write; here we prove the tag survives the wire faithfully and that a */ /* simple equality check (the only sound way to interpret a version */ /* tag) distinguishes a matching reader from a mismatched one. The */ /* comparison below is TEST-ONLY logic illustrating the intended usage, */ /* not production code. */ /* ------------------------------------------------------------------ */ static void test_snapshot_data_round_trip(void) { /* One queue, reused both as SnapshotData's own top-level `queues` * list and inside `config.queues` (Conf4Sched's own queue list) -- * proves both call sites of the pre-existing QueueConf codec. */ xce_queue_t queue; memset(&queue, 0, sizeof(queue)); queue.queue = "batch"; queue.attrs = 1u; queue.status = 0u; queue.priority = 5; queue.accept_interval = 10; queue.sched_delay = 2; queue.resreq = "select=1"; xce_queue_t *queues[1] = { &queue }; xce_conf4sched_t config; memset(&config, 0, sizeof(config)); config.clustername = "xce-snap-cluster"; config.n_queues = 1; config.queues = queues; xce_job_t sjob; memset(&sjob, 0, sizeof(sjob)); sjob.jobid = 8001ULL; sjob.sub_time = 1000; sjob.queue_time = 2000; sjob.start_time = 3000; sjob.finish_time = 4000; sjob.state = 5u; sjob.run_time = 120; xce_job_t *sjobs[1] = { &sjob }; xce_jobs_data_t jobs_data; memset(&jobs_data, 0, sizeof(jobs_data)); jobs_data.n_total = 1u; jobs_data.pos = 0u; jobs_data.count = 1; jobs_data.jobs = sjobs; xce_worker_t worker; memset(&worker, 0, sizeof(worker)); worker.name = "workerA"; worker.status = XCE_WORKER_UNAVAIL; xce_worker_t *workers_arr[1] = { &worker }; xce_workers_data_t workers_data; memset(&workers_data, 0, sizeof(workers_data)); workers_data.n_total = 1u; workers_data.pos = 0u; workers_data.count = 1; workers_data.workers = workers_arr; xce_resource_t resource; memset(&resource, 0, sizeof(resource)); resource.name = "cpu-res"; resource.desc = "CPU resource"; resource.idx = 0; resource.valtype = 1; resource.ordtype = XCE_ORD_INCR; resource.flags = 0; resource.interval = 60; resource.last_report = 999; xce_resource_t *resources_arr[1] = { &resource }; xce_resource_data_t resources_data; memset(&resources_data, 0, sizeof(resources_data)); resources_data.n_total = 1u; resources_data.pos = 0u; resources_data.count = 1; resources_data.resources = resources_arr; xce_val_t val; memset(&val, 0, sizeof(val)); val.which = 1; /* uvalue32 */ val.u.uvalue32 = 4096u; xce_resval_t resval; memset(&resval, 0, sizeof(resval)); resval.name = "mem"; resval.host = "hostA"; resval.val = &val; xce_resval_t *resvals_arr[1] = { &resval }; xce_resval_list_t resval_list; memset(&resval_list, 0, sizeof(resval_list)); resval_list.count = 1; resval_list.vals = resvals_arr; sched_snapshot_user_t user; memset(&user, 0, sizeof(user)); user.name = "alice"; sched_snapshot_user_t *users_arr[1] = { &user }; sched_snapshot_project_t project; memset(&project, 0, sizeof(project)); project.name = "proj1"; project.priority = 5; sched_snapshot_project_t *projects_arr[1] = { &project }; uint8_t internal_bytes[8] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04 }; sched_snapshot_data_t src; memset(&src, 0, sizeof(src)); src.config = &config; src.jobs = &jobs_data; src.queues = queues; src.n_queues = 1; src.workers = &workers_data; src.resources = &resources_data; src.resvals = &resval_list; src.users = users_arr; src.n_users = 1; src.projects = projects_arr; src.n_projects = 1; src.prng_seed = 123456789ULL; src.internal_state = internal_bytes; src.internal_state_len = sizeof(internal_bytes); src.internal_version = 2u; struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; SnapshotData_ptr p; encode_SnapshotData_ptr(cs, &p, &src); /* internalState (Data field): codec no-op, set via low-level accessor * on the same already-allocated struct pointer (see comment above). */ capn_list8 state_list = capn_new_list8(cs, (int)sizeof(internal_bytes)); capn_setv8(state_list, 0, internal_bytes, sizeof(internal_bytes)); capn_data state_data; state_data.p = state_list.p; SnapshotData_set_internalState(p, state_data); capn_setp(capn_root(&c), 0, p.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); SnapshotData_ptr dp; dp.p = capn_getp(capn_root(&d), 0, 1); struct SnapshotData wire; read_SnapshotData(&wire, dp); sched_snapshot_data_t dst; memset(&dst, 0, sizeof(dst)); decode_SnapshotData(&dst, &wire); /* config (Conf4Sched) */ TEST_ASSERT_NOT_NULL(dst.config); TEST_ASSERT_EQUAL_STRING("xce-snap-cluster", dst.config->clustername); TEST_ASSERT_EQUAL_INT(1, dst.config->n_queues); TEST_ASSERT_NOT_NULL(dst.config->queues); TEST_ASSERT_EQUAL_STRING("batch", dst.config->queues[0]->queue); TEST_ASSERT_EQUAL_UINT32(1u, dst.config->queues[0]->attrs); TEST_ASSERT_EQUAL_INT32(5, dst.config->queues[0]->priority); TEST_ASSERT_EQUAL_STRING("select=1", dst.config->queues[0]->resreq); /* jobs (SchedJobData) */ TEST_ASSERT_NOT_NULL(dst.jobs); TEST_ASSERT_EQUAL_UINT32(1u, dst.jobs->n_total); TEST_ASSERT_EQUAL_INT(1, dst.jobs->count); TEST_ASSERT_NOT_NULL(dst.jobs->jobs); TEST_ASSERT_EQUAL_UINT64(8001ULL, dst.jobs->jobs[0]->jobid); TEST_ASSERT_EQUAL_UINT32(5u, dst.jobs->jobs[0]->state); TEST_ASSERT_EQUAL_INT(120, dst.jobs->jobs[0]->run_time); /* queues (top-level List(QueueConf)) */ TEST_ASSERT_EQUAL_INT(1, dst.n_queues); TEST_ASSERT_NOT_NULL(dst.queues); TEST_ASSERT_EQUAL_STRING("batch", dst.queues[0]->queue); TEST_ASSERT_EQUAL_INT32(5, dst.queues[0]->priority); /* workers (WorkerData) */ TEST_ASSERT_NOT_NULL(dst.workers); TEST_ASSERT_EQUAL_UINT32(1u, dst.workers->n_total); TEST_ASSERT_NOT_NULL(dst.workers->workers); TEST_ASSERT_EQUAL_STRING("workerA", dst.workers->workers[0]->name); TEST_ASSERT_EQUAL_UINT32((uint32_t)XCE_WORKER_UNAVAIL, dst.workers->workers[0]->status); /* resources (ResourceData) */ TEST_ASSERT_NOT_NULL(dst.resources); TEST_ASSERT_EQUAL_UINT32(1u, dst.resources->n_total); TEST_ASSERT_NOT_NULL(dst.resources->resources); TEST_ASSERT_EQUAL_STRING("cpu-res", dst.resources->resources[0]->name); TEST_ASSERT_EQUAL_STRING("CPU resource", dst.resources->resources[0]->desc); /* resvals (ResValList) */ TEST_ASSERT_NOT_NULL(dst.resvals); TEST_ASSERT_EQUAL_INT(1, dst.resvals->count); TEST_ASSERT_NOT_NULL(dst.resvals->vals); TEST_ASSERT_EQUAL_STRING("mem", dst.resvals->vals[0]->name); TEST_ASSERT_EQUAL_STRING("hostA", dst.resvals->vals[0]->host); TEST_ASSERT_NOT_NULL(dst.resvals->vals[0]->val); TEST_ASSERT_EQUAL_UINT32(4096u, dst.resvals->vals[0]->val->u.uvalue32); /* users / projects (top-level List(SchedUser)/List(SchedProject)) */ TEST_ASSERT_EQUAL_INT(1, dst.n_users); TEST_ASSERT_NOT_NULL(dst.users); TEST_ASSERT_EQUAL_STRING("alice", dst.users[0]->name); TEST_ASSERT_EQUAL_INT(1, dst.n_projects); TEST_ASSERT_NOT_NULL(dst.projects); TEST_ASSERT_EQUAL_STRING("proj1", dst.projects[0]->name); TEST_ASSERT_EQUAL_INT32(5, dst.projects[0]->priority); /* prngSeed / internalVersion (plain scalars, codec-bridge tested) */ TEST_ASSERT_EQUAL_UINT64(123456789ULL, dst.prng_seed); TEST_ASSERT_EQUAL_UINT32(2u, dst.internal_version); /* internalState -- low-level accessor read-back (Data field, codec * no-op; see comment above). */ capn_data out_state = SnapshotData_get_internalState(dp); TEST_ASSERT_EQUAL_INT((int)sizeof(internal_bytes), capn_len((capn_list8){ out_state.p })); uint8_t out_state_bytes[sizeof(internal_bytes)]; TEST_ASSERT_EQUAL_INT((int)sizeof(internal_bytes), capn_getv8((capn_list8){ out_state.p }, 0, out_state_bytes, sizeof(internal_bytes))); TEST_ASSERT_EQUAL_MEMORY(internal_bytes, out_state_bytes, sizeof(internal_bytes)); /* Observable: version-mismatch rejection. A reader that expects the * SAME internal_version accepts; a reader that expects a DIFFERENT * version must reject rather than misinterpret the opaque bytes. This * is the only sound reject/accept rule for an opaque versioned blob -- * demonstrated here with test-only comparison logic, not production * code (task 8.1+ writes the real load_snapshot gate). */ const uint32_t matching_reader_expects = 2u; const uint32_t mismatched_reader_expects = 3u; TEST_ASSERT_TRUE(dst.internal_version == matching_reader_expects); TEST_ASSERT_FALSE(dst.internal_version == mismatched_reader_expects); free_SnapshotData(&dst); capn_free(&d); free(buf); printf("test_snapshot_data_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 16: SchedAdminRequest round-trip (scheduler-core task 7.4, */ /* Req 5 AC 10) -- encodes and decodes a SchedAdminRequest for each of */ /* the three SchedAdminAction enum values (recordStart/recordStop/ */ /* recordStatus), proving the plain enum-typed `action` field (mapped */ /* to `int` in sched_admin_req_t, same convention as HostRole/ */ /* JoinData.role) and the `recordFile` Text field round-trip field-for- */ /* field. recordStart uses a non-empty recordFile; recordStop/ */ /* recordStatus use an empty one (auto-generate / pure query), covering */ /* both the populated and empty-string Text cases. Same encode_X_ptr/ */ /* decode_X codec-bridge pattern as Test 9 (job_state_change_event_t) */ /* since sched_admin_req_t is a fully-scalar struct plus one Text */ /* field, no Data fields. */ /* ------------------------------------------------------------------ */ static void sched_admin_request_round_trip_case(int action, const char *record_file) { sched_admin_req_t src; memset(&src, 0, sizeof(src)); src.action = action; src.recordFile = (char *)record_file; struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; SchedAdminRequest_ptr p; encode_SchedAdminRequest_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); SchedAdminRequest_ptr dp; dp.p = capn_getp(capn_root(&d), 0, 1); struct SchedAdminRequest wire; read_SchedAdminRequest(&wire, dp); sched_admin_req_t dst; memset(&dst, 0, sizeof(dst)); decode_SchedAdminRequest(&dst, &wire); TEST_ASSERT_EQUAL_INT(action, (int)dst.action); TEST_ASSERT_EQUAL_STRING(record_file, dst.recordFile); free_SchedAdminRequest(&dst); capn_free(&d); free(buf); } static void test_sched_admin_request_round_trip(void) { /* recordStart with a non-empty recordFile. */ sched_admin_request_round_trip_case(SchedAdminAction_recordStart, "manual-session.replay"); /* recordStop with an empty recordFile (stop takes no filename). */ sched_admin_request_round_trip_case(SchedAdminAction_recordStop, ""); /* recordStatus with an empty recordFile (status is a pure query). */ sched_admin_request_round_trip_case(SchedAdminAction_recordStatus, ""); printf("test_sched_admin_request_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 17: SchedAdminReply round-trip (scheduler-core task 7.4, */ /* Req 5 AC 10) -- encodes and decodes a SchedAdminReply for both an */ /* ok=true success case and an ok=false error case, each populating all */ /* six fields (ok, error, recording, file, eventCount, startedAt) with */ /* non-default values, proving every field -- including the two Bool */ /* fields (ok, recording) packed into the same wire byte -- round-trips */ /* independently regardless of the other's value. `error` is modelled */ /* as a general diagnostic-message field (populated with a non-fatal */ /* advisory even on success, an outright failure reason on error) so */ /* both cases can legitimately carry a non-empty value. Same codec- */ /* bridge pattern as Test 9/16. */ /* ------------------------------------------------------------------ */ static void sched_admin_reply_round_trip_case(int ok, const char *error, int recording, const char *file, uint64_t event_count, uint64_t started_at) { sched_admin_reply_t src; memset(&src, 0, sizeof(src)); src.ok = ok; src.error = (char *)error; src.recording = recording; src.file = (char *)file; src.eventCount = event_count; src.startedAt = started_at; struct capn c; capn_init_malloc(&c); struct capn_segment *cs = capn_root(&c).seg; SchedAdminReply_ptr p; encode_SchedAdminReply_ptr(cs, &p, &src); capn_setp(capn_root(&c), 0, p.p); int64_t sz = capn_size(&c); TEST_ASSERT_TRUE(sz > 0); uint8_t *buf = (uint8_t *)malloc((size_t)sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, (size_t)sz, 0); capn_free(&c); struct capn d; TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0)); SchedAdminReply_ptr dp; dp.p = capn_getp(capn_root(&d), 0, 1); struct SchedAdminReply wire; read_SchedAdminReply(&wire, dp); sched_admin_reply_t dst; memset(&dst, 0, sizeof(dst)); decode_SchedAdminReply(&dst, &wire); TEST_ASSERT_EQUAL_INT(ok ? 1 : 0, dst.ok ? 1 : 0); TEST_ASSERT_EQUAL_STRING(error, dst.error); TEST_ASSERT_EQUAL_INT(recording ? 1 : 0, dst.recording ? 1 : 0); TEST_ASSERT_EQUAL_STRING(file, dst.file); TEST_ASSERT_EQUAL_UINT64(event_count, dst.eventCount); TEST_ASSERT_EQUAL_UINT64(started_at, dst.startedAt); free_SchedAdminReply(&dst); capn_free(&d); free(buf); } static void test_sched_admin_reply_round_trip(void) { /* ok=true success case (e.g. a recordStatus reply): all six fields * non-default. */ sched_admin_reply_round_trip_case(1, "warning: recording directory nearing disk quota", 1, "session-0007.replay", 12345ULL, 1700000000000000000ULL); /* ok=false error case (e.g. a failed recordStart): all six fields * still non-default, proving the wire carries every field regardless * of `ok`. */ sched_admin_reply_round_trip_case(0, "recording directory not writable", 1, "session-0006.replay", 999ULL, 1699999999000000000ULL); printf("test_sched_admin_reply_round_trip: PASS\n"); } /* ------------------------------------------------------------------ */ /* Suite entry point */ /* ------------------------------------------------------------------ */ void test_e2e_capnp(void) { UnitySetTestFile(__FILE__); printf("\n>> P0-15e: Cap'n Proto partial merge semantics\n\n"); RUN_TEST(test_persist_config_capnp_round_trip); RUN_TEST(test_snapshot_header_capnp_fields); RUN_TEST(test_repl_envelope_capnp_fields); RUN_TEST(test_request_vote_last_log_tuple_round_trip); RUN_TEST(test_wal_catchup_append_term_fields_round_trip); RUN_TEST(test_data_job_array_variant_round_trip); RUN_TEST(test_replay_header_snapshot_entry_round_trip); RUN_TEST(test_session_summary_round_trip); RUN_TEST(test_job_state_change_event_round_trip); RUN_TEST(test_job_update_event_round_trip); RUN_TEST(test_dispatch_decision_round_trip); RUN_TEST(test_pend_decision_round_trip); RUN_TEST(test_preempt_decision_round_trip); RUN_TEST(test_replay_decision_dispatch_union_round_trip); RUN_TEST(test_snapshot_data_round_trip); RUN_TEST(test_sched_admin_request_round_trip); RUN_TEST(test_sched_admin_reply_round_trip); }