feat(libxce-alloc-preason-equality): full-match job_preason_eq with first-difference description (task 2.2)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-28 09:49:57 +00:00
parent a6cbb323b0
commit 62d0b8b686
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
5 changed files with 648 additions and 2 deletions

View file

@ -12,4 +12,45 @@ extern void erase_job_preason(xce_job_preason_t*);
extern int add_job_preason(xce_job_preason_t*, int, char*, char*);
extern xce_pendreason_t* make_pendreason(uint64_t);
/*
* Pending-reason equality: "are these two pending-reason records identical".
*
* job_preason_eq() is a FULL structural match. It compares, in this order:
* main_reason; nhost_reasons, then host_based_reasons[i] in order, each by
* host then reason; nres_reasons, then res_based_reasons[i] in order, each
* by resource, host, then reason. Nothing is excluded. Lists are
* order-sensitive: the same entries in a different order are not equal.
*
* The scheduler simulator (--sim-verify, sim_verify.c) deliberately does NOT
* use this function for its PEND outcome check: it compares main_reason only
* (bug-1845), because a recorded alloc-cache-hit PEND carries only the main
* reason while the simulator (which has no alloc cache) always builds the
* full detail lists. That loosening is a replay property, not object
* identity, so it stays simulator-local.
*
* NULL handling: identical pointers (including two NULLs) are equal; exactly
* one NULL argument, list entry or string field is not equal. Precondition:
* a non-zero count with a NULL entry array is a malformed record and is not
* handled. Neither argument is modified.
*
* `why` is optional. When NULL, nothing is allocated. Otherwise *why is set
* to NULL when the arguments are equal, or to a newly allocated,
* NUL-terminated description of the first difference found, in comparison
* order, e.g.
* main_reason: 5 != 6
* nhost_reasons: 2 != 1
* host_based_reasons[1](host=h2).host: "h2" != "h9"
* res_based_reasons[0](resource=mem).reason: 3 != 8
* res_based_reasons[0]: non-NULL != NULL
* job_preason: NULL != non-NULL (a NULL top-level argument)
* Path labels come from `a`, from `b` when a's string is NULL, or are `?`.
* The caller owns the description and releases it with free(). The value
* *why holds on entry is never read or freed. If allocating the description
* fails, *why is NULL and the result is unchanged. Nothing is logged.
*
* Returns non-zero when equal, 0 when not.
*/
extern int job_preason_eq(const xce_job_preason_t* a,
const xce_job_preason_t* b, char** why);
#endif

View file

@ -1,5 +1,11 @@
/* vasprintf() (GNU extension) builds job_preason_eq()'s difference
* description. */
#define _GNU_SOURCE
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "preason.h"
#include "utils.h"
@ -175,3 +181,172 @@ xce_pendreason_t *make_pendreason(uint64_t jobid) {
return reason;
}
/* ------------------------------------------------------------------------
* Pending-reason equality (libxce-alloc-preason-equality). Contract in
* preason.h. Same description conventions as xcealloc.c's alloc_eq family;
* the small formatting helpers are duplicated here (static) rather than
* exported, to keep the module boundaries of the design.
* ---------------------------------------------------------------------- */
/* *why = formatted string, or NULL if the allocation fails (vasprintf leaves
* its output undefined then). No-op when why is NULL. */
static void preason_why(char** why, const char* fmt, ...)
__attribute__((format(printf, 2, 3)));
static void preason_why(char** why, const char* fmt, ...)
{
va_list ap;
char* s = NULL;
if (why == NULL) {
return;
}
va_start(ap, fmt);
if (vasprintf(&s, fmt, ap) < 0) {
s = NULL;
}
va_end(ap);
*why = s;
}
static const char* preason_label(const char* a, const char* b)
{
if (a != NULL) {
return a;
}
return (b != NULL) ? b : "?";
}
static const char* preason_nullness(const void* p)
{
return (p != NULL) ? "non-NULL" : "NULL";
}
/* Compare string field `field` of entry `list[i](key=label)`. */
static int preason_str_field(const char* list, int i, const char* key,
const char* label, const char* field,
const char* a, const char* b, char** why)
{
if (str_eq(a, b)) {
return 1;
}
preason_why(why, "%s[%d](%s=%s).%s: %s%s%s != %s%s%s", list, i, key,
label, field, (a != NULL) ? "\"" : "",
(a != NULL) ? a : "NULL", (a != NULL) ? "\"" : "",
(b != NULL) ? "\"" : "", (b != NULL) ? b : "NULL",
(b != NULL) ? "\"" : "");
return 0;
}
static int host_reasons_eq(const xce_job_preason_t* a,
const xce_job_preason_t* b, char** why)
{
static const char* list = "host_based_reasons";
int i;
for (i = 0; i < a->nhost_reasons; i++) {
const xce_host_reason_t* ra = a->host_based_reasons[i];
const xce_host_reason_t* rb = b->host_based_reasons[i];
const char* label;
if (ra == rb) {
continue;
}
if ((ra == NULL) || (rb == NULL)) {
preason_why(why, "%s[%d]: %s != %s", list, i,
preason_nullness(ra), preason_nullness(rb));
return 0;
}
label = preason_label(ra->host, rb->host);
if (!preason_str_field(list, i, "host", label, "host", ra->host,
rb->host, why)) {
return 0;
}
if (ra->reason != rb->reason) {
preason_why(why, "%s[%d](host=%s).reason: %d != %d", list, i,
label, ra->reason, rb->reason);
return 0;
}
}
return 1;
}
static int res_reasons_eq(const xce_job_preason_t* a,
const xce_job_preason_t* b, char** why)
{
static const char* list = "res_based_reasons";
int i;
for (i = 0; i < a->nres_reasons; i++) {
const xce_res_reason_t* ra = a->res_based_reasons[i];
const xce_res_reason_t* rb = b->res_based_reasons[i];
const char* label;
if (ra == rb) {
continue;
}
if ((ra == NULL) || (rb == NULL)) {
preason_why(why, "%s[%d]: %s != %s", list, i,
preason_nullness(ra), preason_nullness(rb));
return 0;
}
label = preason_label(ra->resource, rb->resource);
if (!preason_str_field(list, i, "resource", label, "resource",
ra->resource, rb->resource, why)
|| !preason_str_field(list, i, "resource", label, "host",
ra->host, rb->host, why)) {
return 0;
}
if (ra->reason != rb->reason) {
preason_why(why, "%s[%d](resource=%s).reason: %d != %d", list, i,
label, ra->reason, rb->reason);
return 0;
}
}
return 1;
}
int job_preason_eq(const xce_job_preason_t* a, const xce_job_preason_t* b,
char** why)
{
if (why != NULL) {
*why = NULL;
}
if (a == b) {
return 1;
}
if ((a == NULL) || (b == NULL)) {
preason_why(why, "job_preason: %s != %s", preason_nullness(a),
preason_nullness(b));
return 0;
}
if (a->main_reason != b->main_reason) {
preason_why(why, "main_reason: %d != %d", a->main_reason,
b->main_reason);
return 0;
}
if (a->nhost_reasons != b->nhost_reasons) {
preason_why(why, "nhost_reasons: %d != %d", a->nhost_reasons,
b->nhost_reasons);
return 0;
}
if (!host_reasons_eq(a, b, why)) {
return 0;
}
if (a->nres_reasons != b->nres_reasons) {
preason_why(why, "nres_reasons: %d != %d", a->nres_reasons,
b->nres_reasons);
return 0;
}
return res_reasons_eq(a, b, why);
}

View file

@ -4,6 +4,7 @@
#include "preason.h"
#include "unity.h"
#include "utils.h"
static void test_main_reason()
{
@ -175,6 +176,348 @@ static void test_add_more_res_reason(void)
erase_job_preason(&reason);
}
/* ------------------------------------------------------------------------
* job_preason_eq (libxce-alloc-preason-equality task 2.2, Req 2.1-2.3,
* 3.1-3.6, 6.4): full structural match with a first-difference description.
* ---------------------------------------------------------------------- */
#define ASSERT_PREASON_NE_WHY(a, b, expect) \
do { \
char* why_ = NULL; \
TEST_ASSERT_FALSE(job_preason_eq((a), (b), &why_)); \
TEST_ASSERT_NOT_NULL(why_); \
TEST_ASSERT_EQUAL_STRING((expect), why_); \
free(why_); \
TEST_ASSERT_FALSE(job_preason_eq((a), (b), NULL)); \
} while (0)
/* main_reason 5; host reasons h1:1, h2:2; resource reasons (mem,h1):3,
* (cpu,h2):4. add_job_preason() with a main reason erases the lists, so the
* main reason is set directly. */
static void build_full(xce_job_preason_t* r)
{
memset(r, 0x0, sizeof(*r));
TEST_ASSERT_EQUAL_INT(1, add_job_preason(r, 1, "h1", NULL));
TEST_ASSERT_EQUAL_INT(2, add_job_preason(r, 2, "h2", NULL));
TEST_ASSERT_EQUAL_INT(1, add_job_preason(r, 3, "h1", "mem"));
TEST_ASSERT_EQUAL_INT(2, add_job_preason(r, 4, "h2", "cpu"));
r->main_reason = 5;
}
static void test_preason_eq_match_sets_why_null(void)
{
xce_job_preason_t a;
xce_job_preason_t b;
char dummy;
char* why = &dummy; /* never read or freed on entry (Req 3.4) */
build_full(&a);
build_full(&b);
TEST_ASSERT_TRUE(job_preason_eq(&a, &b, &why));
TEST_ASSERT_NULL(why);
TEST_ASSERT_TRUE(job_preason_eq(&a, &b, NULL));
TEST_ASSERT_TRUE(job_preason_eq(&a, &a, &why));
TEST_ASSERT_NULL(why);
erase_job_preason(&a);
erase_job_preason(&b);
}
static void test_preason_eq_empty_lists_match(void)
{
xce_job_preason_t a;
xce_job_preason_t b;
char* why = NULL;
memset(&a, 0x0, sizeof(a));
memset(&b, 0x0, sizeof(b));
add_job_preason(&a, 7, NULL, NULL);
add_job_preason(&b, 7, NULL, NULL);
TEST_ASSERT_TRUE(job_preason_eq(&a, &b, &why));
TEST_ASSERT_NULL(why);
}
static void test_preason_eq_main_reason_mismatch(void)
{
xce_job_preason_t a;
xce_job_preason_t b;
build_full(&a);
build_full(&b);
b.main_reason = 6;
ASSERT_PREASON_NE_WHY(&a, &b, "main_reason: 5 != 6");
erase_job_preason(&a);
erase_job_preason(&b);
}
/* Main reason equal but detail differs: the full match must see it (this is
* what the simulator's main-reason-only check deliberately ignores). */
static void test_preason_eq_detail_matters_when_main_equal(void)
{
xce_job_preason_t a;
xce_job_preason_t b;
build_full(&a);
memset(&b, 0x0, sizeof(b));
b.main_reason = 5;
ASSERT_PREASON_NE_WHY(&a, &b, "nhost_reasons: 2 != 0");
erase_job_preason(&a);
}
static void test_preason_eq_host_reason_field_mismatch(void)
{
xce_job_preason_t a;
xce_job_preason_t b;
/* host name */
build_full(&a);
build_full(&b);
free(b.host_based_reasons[1]->host);
b.host_based_reasons[1]->host = strdup("h9");
ASSERT_PREASON_NE_WHY(&a, &b,
"host_based_reasons[1](host=h2).host: \"h2\" != "
"\"h9\"");
erase_job_preason(&b);
/* reason code */
build_full(&b);
b.host_based_reasons[0]->reason = 9;
ASSERT_PREASON_NE_WHY(&a, &b,
"host_based_reasons[0](host=h1).reason: 1 != 9");
erase_job_preason(&b);
erase_job_preason(&a);
}
static void test_preason_eq_host_reason_order_and_length(void)
{
xce_job_preason_t a;
xce_job_preason_t b;
/* same entries, other order */
build_full(&a);
memset(&b, 0x0, sizeof(b));
add_job_preason(&b, 2, "h2", NULL);
add_job_preason(&b, 1, "h1", NULL);
add_job_preason(&b, 3, "h1", "mem");
add_job_preason(&b, 4, "h2", "cpu");
b.main_reason = 5;
ASSERT_PREASON_NE_WHY(&a, &b,
"host_based_reasons[0](host=h1).host: \"h1\" != "
"\"h2\"");
erase_job_preason(&b);
/* one entry fewer */
memset(&b, 0x0, sizeof(b));
add_job_preason(&b, 1, "h1", NULL);
add_job_preason(&b, 3, "h1", "mem");
add_job_preason(&b, 4, "h2", "cpu");
b.main_reason = 5;
ASSERT_PREASON_NE_WHY(&a, &b, "nhost_reasons: 2 != 1");
erase_job_preason(&b);
erase_job_preason(&a);
}
static void test_preason_eq_res_reason_field_mismatch(void)
{
xce_job_preason_t a;
xce_job_preason_t b;
/* resource name */
build_full(&a);
build_full(&b);
free(b.res_based_reasons[0]->resource);
b.res_based_reasons[0]->resource = strdup("gpu");
ASSERT_PREASON_NE_WHY(&a, &b,
"res_based_reasons[0](resource=mem).resource: "
"\"mem\" != \"gpu\"");
erase_job_preason(&b);
/* host name */
build_full(&b);
free(b.res_based_reasons[1]->host);
b.res_based_reasons[1]->host = strdup("h1");
ASSERT_PREASON_NE_WHY(&a, &b,
"res_based_reasons[1](resource=cpu).host: \"h2\" "
"!= \"h1\"");
erase_job_preason(&b);
/* reason code */
build_full(&b);
b.res_based_reasons[1]->reason = 8;
ASSERT_PREASON_NE_WHY(&a, &b,
"res_based_reasons[1](resource=cpu).reason: 4 != 8");
erase_job_preason(&b);
erase_job_preason(&a);
}
static void test_preason_eq_res_reason_order_and_length(void)
{
xce_job_preason_t a;
xce_job_preason_t b;
/* same entries, other order */
build_full(&a);
memset(&b, 0x0, sizeof(b));
add_job_preason(&b, 1, "h1", NULL);
add_job_preason(&b, 2, "h2", NULL);
add_job_preason(&b, 4, "h2", "cpu");
add_job_preason(&b, 3, "h1", "mem");
b.main_reason = 5;
ASSERT_PREASON_NE_WHY(&a, &b,
"res_based_reasons[0](resource=mem).resource: "
"\"mem\" != \"cpu\"");
erase_job_preason(&b);
/* one entry more */
build_full(&b);
add_job_preason(&b, 5, "h3", "disk");
ASSERT_PREASON_NE_WHY(&a, &b, "nres_reasons: 2 != 3");
erase_job_preason(&b);
erase_job_preason(&a);
}
static void test_preason_eq_null_top_level(void)
{
xce_job_preason_t a;
char* why = NULL;
build_full(&a);
TEST_ASSERT_TRUE(job_preason_eq(NULL, NULL, &why));
TEST_ASSERT_NULL(why);
ASSERT_PREASON_NE_WHY(&a, NULL, "job_preason: non-NULL != NULL");
ASSERT_PREASON_NE_WHY(NULL, &a, "job_preason: NULL != non-NULL");
erase_job_preason(&a);
}
static void test_preason_eq_null_list_entries(void)
{
xce_job_preason_t a;
xce_job_preason_t b;
xce_job_preason_t c;
build_full(&a);
build_full(&b);
build_full(&c);
/* host entry NULL on one side */
free(b.host_based_reasons[1]->host);
free(b.host_based_reasons[1]);
b.host_based_reasons[1] = NULL;
ASSERT_PREASON_NE_WHY(&a, &b, "host_based_reasons[1]: non-NULL != NULL");
ASSERT_PREASON_NE_WHY(&b, &a, "host_based_reasons[1]: NULL != non-NULL");
/* resource entry NULL on one side */
free(c.res_based_reasons[0]->host);
free(c.res_based_reasons[0]->resource);
free(c.res_based_reasons[0]);
c.res_based_reasons[0] = NULL;
ASSERT_PREASON_NE_WHY(&a, &c, "res_based_reasons[0]: non-NULL != NULL");
ASSERT_PREASON_NE_WHY(&c, &a, "res_based_reasons[0]: NULL != non-NULL");
/* NULL entries at the same index on both sides are equal */
{
xce_job_preason_t d;
char* why = NULL;
build_full(&d);
free(d.host_based_reasons[1]->host);
free(d.host_based_reasons[1]);
d.host_based_reasons[1] = NULL;
TEST_ASSERT_TRUE(job_preason_eq(&b, &d, &why));
TEST_ASSERT_NULL(why);
erase_job_preason(&d);
}
erase_job_preason(&a);
erase_job_preason(&b);
erase_job_preason(&c);
}
static void test_preason_eq_null_string_fields(void)
{
xce_job_preason_t a;
xce_job_preason_t b;
char* why = NULL;
build_full(&a);
build_full(&b);
/* host reason host NULL in a: label taken from b */
FREEUP(a.host_based_reasons[0]->host);
ASSERT_PREASON_NE_WHY(&a, &b,
"host_based_reasons[0](host=h1).host: NULL != "
"\"h1\"");
/* both NULL: the field compares equal, so there is no description */
FREEUP(b.host_based_reasons[0]->host);
TEST_ASSERT_TRUE(job_preason_eq(&a, &b, &why));
TEST_ASSERT_NULL(why);
/* both hosts NULL, reason differs: label is `?` */
b.host_based_reasons[0]->reason = 7;
ASSERT_PREASON_NE_WHY(&a, &b,
"host_based_reasons[0](host=?).reason: 1 != 7");
erase_job_preason(&a);
erase_job_preason(&b);
/* resource reason resource NULL in b: label from a */
build_full(&a);
build_full(&b);
FREEUP(b.res_based_reasons[1]->resource);
ASSERT_PREASON_NE_WHY(&a, &b,
"res_based_reasons[1](resource=cpu).resource: "
"\"cpu\" != NULL");
/* resource NULL in a: label from b */
ASSERT_PREASON_NE_WHY(&b, &a,
"res_based_reasons[1](resource=cpu).resource: NULL "
"!= \"cpu\"");
erase_job_preason(&b);
/* resource reason host NULL */
build_full(&b);
FREEUP(b.res_based_reasons[0]->host);
ASSERT_PREASON_NE_WHY(&a, &b,
"res_based_reasons[0](resource=mem).host: \"h1\" != "
"NULL");
erase_job_preason(&b);
erase_job_preason(&a);
}
/* The comparison never modifies its arguments (Req 2.3). */
static void test_preason_eq_does_not_modify_arguments(void)
{
xce_job_preason_t a;
xce_job_preason_t b;
xce_job_preason_t a0;
char* why = NULL;
build_full(&a);
build_full(&b);
b.res_based_reasons[1]->reason = 8;
a0 = a;
TEST_ASSERT_FALSE(job_preason_eq(&a, &b, &why));
free(why);
TEST_ASSERT_EQUAL_MEMORY(&a0, &a, sizeof(a));
TEST_ASSERT_EQUAL_STRING("h1", a.host_based_reasons[0]->host);
TEST_ASSERT_EQUAL_STRING("mem", a.res_based_reasons[0]->resource);
TEST_ASSERT_EQUAL_INT(8, b.res_based_reasons[1]->reason);
erase_job_preason(&a);
erase_job_preason(&b);
}
void test_preason(void)
{
printf("\n>> preason test\n\n");
@ -187,4 +530,16 @@ void test_preason(void)
RUN_TEST(test_add_main_reset_other);
RUN_TEST(test_add_more_host_reason);
RUN_TEST(test_add_more_res_reason);
RUN_TEST(test_preason_eq_match_sets_why_null);
RUN_TEST(test_preason_eq_empty_lists_match);
RUN_TEST(test_preason_eq_main_reason_mismatch);
RUN_TEST(test_preason_eq_detail_matters_when_main_equal);
RUN_TEST(test_preason_eq_host_reason_field_mismatch);
RUN_TEST(test_preason_eq_host_reason_order_and_length);
RUN_TEST(test_preason_eq_res_reason_field_mismatch);
RUN_TEST(test_preason_eq_res_reason_order_and_length);
RUN_TEST(test_preason_eq_null_top_level);
RUN_TEST(test_preason_eq_null_list_entries);
RUN_TEST(test_preason_eq_null_string_fields);
RUN_TEST(test_preason_eq_does_not_modify_arguments);
}

View file

@ -16,6 +16,8 @@
* wraps vasprintf() (and __vasprintf_chk(), which a _FORTIFY_SOURCE build
* calls instead) so a test can make the equality functions' description
* allocation fail: the result must be unchanged and *why left NULL.
* Task 2.2 uses the same wrap for job_preason_eq() (preason.c, also in the
* static libxce).
*/
#include <errno.h>
#include <stdarg.h>
@ -23,6 +25,7 @@
#include <stdlib.h>
#include <string.h>
#include "preason.h"
#include "unity.h"
#include "xce/types.h"
#include "xcealloc.h"
@ -309,6 +312,77 @@ static void test_job_alloc_eq_description_oom_at_every_level(void)
free_JobAlloc_ptr(&b);
}
/* Req 3.5 for job_preason_eq(): a failed description allocation leaves *why
* NULL and the result unchanged, for a top-level, main-reason, list-length
* and nested-entry difference alike. */
static void test_job_preason_eq_description_oom_leaves_why_null(void)
{
xce_job_preason_t a;
xce_job_preason_t b;
int round;
for (round = 0; round < 4; round++) {
char* why = NULL;
memset(&a, 0x0, sizeof(a));
memset(&b, 0x0, sizeof(b));
TEST_ASSERT_EQUAL_INT(1, add_job_preason(&a, 1, "h1", NULL));
TEST_ASSERT_EQUAL_INT(1, add_job_preason(&a, 3, "h1", "mem"));
TEST_ASSERT_EQUAL_INT(1, add_job_preason(&b, 1, "h1", NULL));
TEST_ASSERT_EQUAL_INT(1, add_job_preason(&b, 3, "h1", "mem"));
a.main_reason = 5;
b.main_reason = 5;
switch (round) {
case 0:
b.main_reason = 6;
break;
case 1:
TEST_ASSERT_EQUAL_INT(2, add_job_preason(&b, 2, "h2", NULL));
break;
case 2:
b.host_based_reasons[0]->reason = 9;
break;
default:
b.res_based_reasons[0]->reason = 9;
break;
}
fail_vasprintf_at(0);
TEST_ASSERT_FALSE(job_preason_eq(&a, &b, &why));
TEST_ASSERT_NULL(why);
TEST_ASSERT_TRUE(g_vasprintf_calls >= 1); /* the wrap was reached */
/* unarmed: the description is produced as usual */
g_vasprintf_fail_at = -1;
TEST_ASSERT_FALSE(job_preason_eq(&a, &b, &why));
TEST_ASSERT_NOT_NULL(why);
free(why);
erase_job_preason(&a);
erase_job_preason(&b);
}
/* top-level NULL */
{
xce_job_preason_t c;
char* why = NULL;
memset(&c, 0x0, sizeof(c));
fail_vasprintf_at(0);
TEST_ASSERT_FALSE(job_preason_eq(&c, NULL, &why));
TEST_ASSERT_NULL(why);
TEST_ASSERT_EQUAL_INT(1, g_vasprintf_calls);
/* equal: no description, so nothing to fail */
fail_vasprintf_at(0);
why = (char*)&g_garbage;
TEST_ASSERT_TRUE(job_preason_eq(&c, &c, &why));
TEST_ASSERT_NULL(why);
TEST_ASSERT_EQUAL_INT(0, g_vasprintf_calls);
}
}
int main(void)
{
UNITY_BEGIN();
@ -325,6 +399,7 @@ int main(void)
RUN_TEST(test_merge_job_alloc_strdup_failure_reports_enomem);
RUN_TEST(test_alloc_eq_description_oom_leaves_why_null);
RUN_TEST(test_job_alloc_eq_description_oom_at_every_level);
RUN_TEST(test_job_preason_eq_description_oom_leaves_why_null);
return UNITY_END();
}