feat(libxce-alloc-preason-equality): public alloc_eq/host_alloc_eq/job_alloc_eq with first-difference description (task 2.1)

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

View file

@ -10,7 +10,7 @@ Test-first throughout (red → green → refactor). Every sub-task leaves the bu
- _Requirements: 1.5, 2.3_
- [ ] 2. Core: public equality API in libxce
- [ ] 2.1 Allocation equality for single records, host allocations and job allocations, with an optional first-difference description
- [x] 2.1 Allocation equality for single records, host allocations and job allocations, with an optional first-difference description
- Write the tests first: a match; a mismatch on each compared field (resource name, reserve, value, duration, reservation start time) with the expected description; an allocation-instance-id difference still compares equal; host name, entry order and list length matter; job id, ordered hosts, ordered shared resources and list-length mismatches; NULL handling at the top level, in list entries and in string fields; no description requested; no description returned when equal.
- Add a wrapped description-allocation failure to the existing allocation OOM test binary (bug-1882) and test that a failed description leaves no description and the same result.
- Implement the three comparisons in the allocation module with the field order, exact value comparison, description path format and ownership contract of design.md (Data Models, Components: Allocation equality, Decision 9), enabling the GNU extension the description formatter needs.
@ -87,3 +87,4 @@ Test-first throughout (red → green → refactor). Every sub-task leaves the bu
## Implementation Notes
- `xce-build.sh` resolves a relative build dir under `src/`: `bash src/tools/xce-build.sh lib asan xce.build.alloceq` builds into `src/xce.build.alloceq`, so iterate with `meson test -C src/xce.build.alloceq`.
- `xce-build.sh lib` runs only the `libxce test` binary; run `meson test -C src/xce.build.alloceq "libxce alloc OOM test"` (or plain `meson test`) for the allocation OOM binary, which also wraps `vasprintf`/`__vasprintf_chk` since 2.1.

View file

@ -16,4 +16,55 @@ extern xce_job_account_t* make_job_account(xce_job_alloc_t*);
extern xce_job_alloc_t* dup_job_alloc(const xce_job_alloc_t*);
extern int merge_job_alloc(xce_job_alloc_t*, const xce_job_alloc_t*);
/*
* Allocation equality: "do these two records describe the same allocation
* decision".
*
* alloc_eq() compares, in this order: resname, reserve, value, duration,
* from. `value` is compared exactly (`==`, no floating-point tolerance): both
* sides are exact copies of the same double, so any difference is real.
* `from` (the reservation start time) is compared: the scheduler stamps it
* with the deciding session's start, which a replay reproduces. `instid` is
* NOT compared: it comes from a per-process counter (sched_get_id()) whose
* value depends on how many allocations that process built before, so a
* recording and its replay give the same allocation different instids
* (bug-1844).
*
* This is not merge_job_alloc()'s slot matching. merge_job_alloc() asks
* "are these the same reservation slot to add into" (resname, instid,
* reserve; value ignored because merging adds values), where instid IS
* significant. Neither predicate can stand in for the other.
*
* host_alloc_eq() compares host, then nallocs, then allocs[i] in order.
* job_alloc_eq() compares jobid, nhosts, nres_shared, then allocs[i] in
* order, then allocs_shared[i] in order. Lists are order-sensitive: the same
* entries in a different order are not equal.
*
* 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.
* allocs[1](host=h1).allocs[0](resname=mem).value: 2 != 3
* allocs[0](host=h0).host: "h0" != "h2"
* allocs_shared[0]: NULL != non-NULL
* nhosts: 1 != 2
* job_alloc: non-NULL != NULL (a NULL top-level argument)
* Path labels come from `a`, from `b` when a's entry/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 alloc_eq(const xce_alloc_t* a, const xce_alloc_t* b, char** why);
extern int host_alloc_eq(const xce_host_alloc_t* a, const xce_host_alloc_t* b,
char** why);
extern int job_alloc_eq(const xce_job_alloc_t* a, const xce_job_alloc_t* b,
char** why);
#endif

View file

@ -246,12 +246,17 @@ test('libxce test', libxce_test, timeout: 120)
# idiom as bug-1843's sched_record_lifecycle_test `--wrap=realloc`) so the
# tests can make a chosen strdup() inside make_alloc()/make_host_alloc()
# fail. Its own binary, so xcelib_test's other tests never see the wrap.
# libxce-alloc-preason-equality: it also wraps vasprintf (and the
# _FORTIFY_SOURCE __vasprintf_chk variant) to fail the equality functions'
# description allocation.
libxce_alloc_oom_test = executable('xcealloc_oom_test',
['testsuite/test_xcealloc_oom.c'],
include_directories: [incdir, build_inc],
dependencies: libxce_test_deps,
c_args: ['-DXCE_TESTING'],
link_args: ['-Wl,--wrap=strdup'],
link_args: ['-Wl,--wrap=strdup',
'-Wl,--wrap=vasprintf',
'-Wl,--wrap=__vasprintf_chk'],
install: false)
test('libxce alloc OOM test', libxce_alloc_oom_test, timeout: 60)

View file

@ -5,6 +5,7 @@
#include "misc.h"
#include "unity.h"
#include "utils.h"
#include "xce/types.h"
#include "xcealloc.h"
@ -455,6 +456,533 @@ static void test_dup_job_alloc_from_duration_fidelity(void)
free_JobAlloc_ptr(&dup);
}
/* ------------------------------------------------------------------------
* alloc_eq / host_alloc_eq / job_alloc_eq (libxce-alloc-preason-equality
* task 2.1; Req 1.1-1.7, 3.1-3.5, 6.4). Every test that asks for a
* description frees it, so ASAN/LeakSanitizer proves the caller-owns
* contract (Req 3.4).
* ---------------------------------------------------------------------- */
/* One record with every compared field set explicitly (make_alloc() stamps
* from = time(NULL); the tests pin it). */
static xce_alloc_t* eq_alloc(const char* resname, const char* reserve,
uint64_t instid, double value, int64_t duration,
uint64_t from)
{
xce_alloc_t* a = make_alloc((char*)resname, (char*)reserve, instid,
(float)value, 0);
TEST_ASSERT_NOT_NULL(a);
a->value = value;
a->duration = duration;
a->from = from;
return a;
}
/* h0: mem=2, cpu=4; h1: mem=2; shared: lic=1. */
static xce_job_alloc_t* eq_job(void)
{
xce_host_alloc_t** hallocs;
xce_job_alloc_t* j;
hallocs = make_host_allocs(
2,
make_host_alloc((char*)"h0", 2,
make_allocs(2, eq_alloc("mem", "", 1, 2, 10, 1000),
eq_alloc("cpu", "", 2, 4, 10, 1000))),
make_host_alloc((char*)"h1", 1,
make_allocs(1, eq_alloc("mem", "", 3, 2, 10, 1000))));
j = make_job_alloc(4242, 2, hallocs);
TEST_ASSERT_NOT_NULL(j);
j->nres_shared = 1;
j->allocs_shared = make_allocs(1, eq_alloc("lic", "r1", 4, 1, 10, 1000));
return j;
}
/* Replace a string field (the constructors never leave one NULL; a decoded
* or hand-built record can). */
static void eq_set_str(char** field, const char* v)
{
free(*field);
*field = (v != NULL) ? strdup(v) : NULL;
}
/* Assert not-equal with exactly the expected description, then free it. */
#define ASSERT_ALLOC_NE(expect, a, b) \
do { \
char* why_ = NULL; \
TEST_ASSERT_FALSE(alloc_eq((a), (b), &why_)); \
TEST_ASSERT_NOT_NULL(why_); \
TEST_ASSERT_EQUAL_STRING((expect), why_); \
free(why_); \
TEST_ASSERT_FALSE(alloc_eq((a), (b), NULL)); \
} while (0)
#define ASSERT_HOST_NE(expect, a, b) \
do { \
char* why_ = NULL; \
TEST_ASSERT_FALSE(host_alloc_eq((a), (b), &why_)); \
TEST_ASSERT_NOT_NULL(why_); \
TEST_ASSERT_EQUAL_STRING((expect), why_); \
free(why_); \
TEST_ASSERT_FALSE(host_alloc_eq((a), (b), NULL)); \
} while (0)
#define ASSERT_JOB_NE(expect, a, b) \
do { \
char* why_ = NULL; \
TEST_ASSERT_FALSE(job_alloc_eq((a), (b), &why_)); \
TEST_ASSERT_NOT_NULL(why_); \
TEST_ASSERT_EQUAL_STRING((expect), why_); \
free(why_); \
TEST_ASSERT_FALSE(job_alloc_eq((a), (b), NULL)); \
} while (0)
static void test_alloc_eq_match_sets_why_null(void)
{
xce_alloc_t* a = eq_alloc("mem", "r1", 1, 2.5, 10, 1000);
xce_alloc_t* b = eq_alloc("mem", "r1", 1, 2.5, 10, 1000);
char dummy;
char* why = &dummy; /* never read or freed on entry (Req 3.4) */
TEST_ASSERT_TRUE(alloc_eq(a, b, &why));
TEST_ASSERT_NULL(why);
TEST_ASSERT_TRUE(alloc_eq(a, b, NULL));
TEST_ASSERT_TRUE(alloc_eq(a, a, NULL));
free_ResAlloc_ptr(&a);
free_ResAlloc_ptr(&b);
}
static void test_alloc_eq_each_field_mismatch_described(void)
{
xce_alloc_t* a = eq_alloc("mem", "r1", 1, 2, 10, 1000);
xce_alloc_t* b = eq_alloc("mem", "r1", 1, 2, 10, 1000);
eq_set_str(&b->resname, "cpu");
ASSERT_ALLOC_NE("resname: \"mem\" != \"cpu\"", a, b);
eq_set_str(&b->resname, "mem");
eq_set_str(&b->reserve, "r2");
ASSERT_ALLOC_NE("reserve: \"r1\" != \"r2\"", a, b);
eq_set_str(&b->reserve, "r1");
b->value = 3;
ASSERT_ALLOC_NE("value: 2 != 3", a, b);
/* exact compare: a difference far below any tolerance still counts, and
* %.17g makes it visible */
b->value = 2.0000000000000004;
ASSERT_ALLOC_NE("value: 2 != 2.0000000000000004", a, b);
b->value = 2;
b->duration = -1;
ASSERT_ALLOC_NE("duration: 10 != -1", a, b);
b->duration = 10;
b->from = 1001;
ASSERT_ALLOC_NE("from: 1000 != 1001", a, b);
b->from = 1000;
TEST_ASSERT_TRUE(alloc_eq(a, b, NULL));
free_ResAlloc_ptr(&a);
free_ResAlloc_ptr(&b);
}
/* The FIRST difference in comparison order is reported: resname, reserve,
* value, duration, from. */
static void test_alloc_eq_reports_first_difference_in_order(void)
{
xce_alloc_t* a = eq_alloc("mem", "r1", 1, 2, 10, 1000);
xce_alloc_t* b = eq_alloc("mem", "r2", 1, 3, 20, 1001);
ASSERT_ALLOC_NE("reserve: \"r1\" != \"r2\"", a, b);
eq_set_str(&b->reserve, "r1");
ASSERT_ALLOC_NE("value: 2 != 3", a, b);
b->value = 2;
ASSERT_ALLOC_NE("duration: 10 != 20", a, b);
free_ResAlloc_ptr(&a);
free_ResAlloc_ptr(&b);
}
/* instid is a per-process counter (bug-1844): excluded from equality. */
static void test_alloc_eq_ignores_instid(void)
{
xce_alloc_t* a = eq_alloc("mem", "", 826, 2, 10, 1000);
xce_alloc_t* b = eq_alloc("mem", "", 1, 2, 10, 1000);
char* why = NULL;
TEST_ASSERT_TRUE(alloc_eq(a, b, &why));
TEST_ASSERT_NULL(why);
free_ResAlloc_ptr(&a);
free_ResAlloc_ptr(&b);
}
static void test_alloc_eq_null_handling(void)
{
xce_alloc_t* a = eq_alloc("mem", "r1", 1, 2, 10, 1000);
xce_alloc_t* b = eq_alloc("mem", "r1", 1, 2, 10, 1000);
char* why = NULL;
TEST_ASSERT_TRUE(alloc_eq(NULL, NULL, &why));
TEST_ASSERT_NULL(why);
ASSERT_ALLOC_NE("alloc: NULL != non-NULL", NULL, b);
ASSERT_ALLOC_NE("alloc: non-NULL != NULL", a, NULL);
/* NULL string fields */
eq_set_str(&a->resname, NULL);
ASSERT_ALLOC_NE("resname: NULL != \"mem\"", a, b);
eq_set_str(&b->resname, NULL);
TEST_ASSERT_TRUE(alloc_eq(a, b, NULL)); /* NULL == NULL */
eq_set_str(&a->reserve, NULL);
ASSERT_ALLOC_NE("reserve: NULL != \"r1\"", a, b);
ASSERT_ALLOC_NE("reserve: \"r1\" != NULL", b, a);
free_ResAlloc_ptr(&a);
free_ResAlloc_ptr(&b);
}
static void test_host_alloc_eq_match_and_mismatch(void)
{
xce_job_alloc_t* ja = eq_job();
xce_job_alloc_t* jb = eq_job();
xce_host_alloc_t* a = ja->allocs[0];
xce_host_alloc_t* b = jb->allocs[0];
char* why = NULL;
TEST_ASSERT_TRUE(host_alloc_eq(a, b, &why));
TEST_ASSERT_NULL(why);
TEST_ASSERT_TRUE(host_alloc_eq(a, b, NULL));
/* host name matters */
eq_set_str(&b->host, "h2");
ASSERT_HOST_NE("host: \"h0\" != \"h2\"", a, b);
eq_set_str(&b->host, "h0");
/* a nested field difference names the entry path */
b->allocs[1]->value = 5;
ASSERT_HOST_NE("allocs[1](resname=cpu).value: 4 != 5", a, b);
b->allocs[1]->value = 4;
/* instid differences in the entries do not matter */
b->allocs[0]->instid = 99;
TEST_ASSERT_TRUE(host_alloc_eq(a, b, NULL));
free_JobAlloc_ptr(&ja);
free_JobAlloc_ptr(&jb);
}
static void test_host_alloc_eq_order_and_length_matter(void)
{
xce_job_alloc_t* ja = eq_job();
xce_job_alloc_t* jb = eq_job();
xce_host_alloc_t* a = ja->allocs[0];
xce_host_alloc_t* b = jb->allocs[0];
xce_alloc_t* tmp;
/* same entries, different order */
tmp = b->allocs[0];
b->allocs[0] = b->allocs[1];
b->allocs[1] = tmp;
ASSERT_HOST_NE("allocs[0](resname=mem).resname: \"mem\" != \"cpu\"", a, b);
tmp = b->allocs[0];
b->allocs[0] = b->allocs[1];
b->allocs[1] = tmp;
/* list length (the trailing entry is still owned via allocs[]) */
b->nallocs = 1;
ASSERT_HOST_NE("nallocs: 2 != 1", a, b);
b->nallocs = 2;
free_JobAlloc_ptr(&ja);
free_JobAlloc_ptr(&jb);
}
static void test_host_alloc_eq_null_handling(void)
{
xce_job_alloc_t* ja = eq_job();
xce_job_alloc_t* jb = eq_job();
xce_host_alloc_t* a = ja->allocs[0];
xce_host_alloc_t* b = jb->allocs[0];
xce_alloc_t* saved_a;
xce_alloc_t* saved_b;
char* why = NULL;
TEST_ASSERT_TRUE(host_alloc_eq(NULL, NULL, &why));
TEST_ASSERT_NULL(why);
ASSERT_HOST_NE("host_alloc: NULL != non-NULL", NULL, b);
ASSERT_HOST_NE("host_alloc: non-NULL != NULL", a, NULL);
/* NULL list entry */
saved_a = a->allocs[1];
a->allocs[1] = NULL;
ASSERT_HOST_NE("allocs[1]: NULL != non-NULL", a, b);
ASSERT_HOST_NE("allocs[1]: non-NULL != NULL", b, a);
saved_b = b->allocs[1];
b->allocs[1] = NULL;
TEST_ASSERT_TRUE(host_alloc_eq(a, b, NULL)); /* NULL entry == NULL */
a->allocs[1] = saved_a;
b->allocs[1] = saved_b;
/* NULL host string */
eq_set_str(&a->host, NULL);
ASSERT_HOST_NE("host: NULL != \"h0\"", a, b);
/* label falls back to b's string, or '?' when both are NULL */
eq_set_str(&a->allocs[0]->resname, NULL);
eq_set_str(&b->host, NULL);
ASSERT_HOST_NE("allocs[0](resname=mem).resname: NULL != \"mem\"", a, b);
eq_set_str(&b->allocs[0]->resname, NULL);
b->allocs[0]->value = 7;
ASSERT_HOST_NE("allocs[0](resname=?).value: 2 != 7", a, b);
free_JobAlloc_ptr(&ja);
free_JobAlloc_ptr(&jb);
}
static void test_job_alloc_eq_match(void)
{
xce_job_alloc_t* a = eq_job();
xce_job_alloc_t* b = dup_job_alloc(a);
char dummy;
char* why = &dummy;
TEST_ASSERT_NOT_NULL(b);
TEST_ASSERT_TRUE(job_alloc_eq(a, b, &why));
TEST_ASSERT_NULL(why);
TEST_ASSERT_TRUE(job_alloc_eq(a, b, NULL));
TEST_ASSERT_TRUE(job_alloc_eq(a, a, NULL));
/* instid anywhere in the tree does not matter */
b->allocs[1]->allocs[0]->instid = 12345;
b->allocs_shared[0]->instid = 67890;
TEST_ASSERT_TRUE(job_alloc_eq(a, b, NULL));
free_JobAlloc_ptr(&a);
free_JobAlloc_ptr(&b);
}
static void test_job_alloc_eq_jobid_and_lengths(void)
{
xce_job_alloc_t* a = eq_job();
xce_job_alloc_t* b = eq_job();
char ja[128];
char jb[128];
char expect[300];
b->jobid = 4243;
TEST_ASSERT_TRUE(fmt_jobid(ja, sizeof(ja), a->jobid) >= 0);
TEST_ASSERT_TRUE(fmt_jobid(jb, sizeof(jb), b->jobid) >= 0);
snprintf(expect, sizeof(expect), "jobid: %s != %s", ja, jb);
ASSERT_JOB_NE(expect, a, b);
b->jobid = a->jobid;
b->nhosts = 1; /* allocs[1] stays owned; restored before free */
ASSERT_JOB_NE("nhosts: 2 != 1", a, b);
b->nhosts = 2;
b->nres_shared = 0;
ASSERT_JOB_NE("nres_shared: 1 != 0", a, b);
b->nres_shared = 1;
TEST_ASSERT_TRUE(job_alloc_eq(a, b, NULL));
free_JobAlloc_ptr(&a);
free_JobAlloc_ptr(&b);
}
static void test_job_alloc_eq_nested_differences(void)
{
xce_job_alloc_t* a = eq_job();
xce_job_alloc_t* b = eq_job();
/* the design.md Data Models examples */
b->allocs[1]->allocs[0]->value = 3;
ASSERT_JOB_NE("allocs[1](host=h1).allocs[0](resname=mem).value: 2 != 3",
a, b);
b->allocs[1]->allocs[0]->value = 2;
eq_set_str(&b->allocs[0]->host, "h2");
ASSERT_JOB_NE("allocs[0](host=h0).host: \"h0\" != \"h2\"", a, b);
eq_set_str(&b->allocs[0]->host, "h0");
/* from is part of the decision */
b->allocs[0]->allocs[1]->from = 999;
ASSERT_JOB_NE("allocs[0](host=h0).allocs[1](resname=cpu).from: 1000 != 999",
a, b);
b->allocs[0]->allocs[1]->from = 1000;
b->allocs[0]->nallocs = 1;
ASSERT_JOB_NE("allocs[0](host=h0).nallocs: 2 != 1", a, b);
b->allocs[0]->nallocs = 2;
/* shared resources */
b->allocs_shared[0]->duration = 11;
ASSERT_JOB_NE("allocs_shared[0](resname=lic).duration: 10 != 11", a, b);
b->allocs_shared[0]->duration = 10;
eq_set_str(&b->allocs_shared[0]->reserve, "r9");
ASSERT_JOB_NE("allocs_shared[0](resname=lic).reserve: \"r1\" != \"r9\"", a,
b);
eq_set_str(&b->allocs_shared[0]->reserve, "r1");
TEST_ASSERT_TRUE(job_alloc_eq(a, b, NULL));
free_JobAlloc_ptr(&a);
free_JobAlloc_ptr(&b);
}
/* Hosts are compared in order: the same hosts swapped are not equal. Hosts
* are compared before shared resources. */
static void test_job_alloc_eq_order_matters(void)
{
xce_job_alloc_t* a = eq_job();
xce_job_alloc_t* b = eq_job();
xce_host_alloc_t* tmp;
tmp = b->allocs[0];
b->allocs[0] = b->allocs[1];
b->allocs[1] = tmp;
b->allocs_shared[0]->value = 8; /* later difference is not reported */
ASSERT_JOB_NE("allocs[0](host=h0).host: \"h0\" != \"h1\"", a, b);
free_JobAlloc_ptr(&a);
free_JobAlloc_ptr(&b);
}
static void test_job_alloc_eq_shared_order_matters(void)
{
xce_job_alloc_t* a = eq_job();
xce_job_alloc_t* b = eq_job();
int i;
for (i = 0; i < 2; i++) {
xce_job_alloc_t* j = (i == 0) ? a : b;
xce_alloc_t** grown;
grown = (xce_alloc_t**)realloc(j->allocs_shared, 2 * sizeof(*grown));
TEST_ASSERT_NOT_NULL(grown);
j->allocs_shared = grown;
j->allocs_shared[1] = eq_alloc("gpu", "", 5, 1, 10, 1000);
j->nres_shared = 2;
}
TEST_ASSERT_TRUE(job_alloc_eq(a, b, NULL));
{
xce_alloc_t* tmp = b->allocs_shared[0];
b->allocs_shared[0] = b->allocs_shared[1];
b->allocs_shared[1] = tmp;
}
ASSERT_JOB_NE("allocs_shared[0](resname=lic).resname: \"lic\" != \"gpu\"",
a, b);
free_JobAlloc_ptr(&a);
free_JobAlloc_ptr(&b);
}
static void test_job_alloc_eq_null_handling(void)
{
xce_job_alloc_t* a = eq_job();
xce_job_alloc_t* b = eq_job();
xce_host_alloc_t* saved_h;
xce_alloc_t* saved_s;
char* why = NULL;
TEST_ASSERT_TRUE(job_alloc_eq(NULL, NULL, &why));
TEST_ASSERT_NULL(why);
ASSERT_JOB_NE("job_alloc: NULL != non-NULL", NULL, b);
ASSERT_JOB_NE("job_alloc: non-NULL != NULL", a, NULL);
/* NULL host entry */
saved_h = a->allocs[1];
a->allocs[1] = NULL;
ASSERT_JOB_NE("allocs[1]: NULL != non-NULL", a, b);
a->allocs[1] = saved_h;
/* NULL nested resource entry */
saved_s = b->allocs[0]->allocs[0];
b->allocs[0]->allocs[0] = NULL;
ASSERT_JOB_NE("allocs[0](host=h0).allocs[0]: non-NULL != NULL", a, b);
b->allocs[0]->allocs[0] = saved_s;
/* NULL shared entry (the design.md example) */
saved_s = a->allocs_shared[0];
a->allocs_shared[0] = NULL;
ASSERT_JOB_NE("allocs_shared[0]: NULL != non-NULL", a, b);
a->allocs_shared[0] = saved_s;
/* NULL host string: the path label comes from b */
eq_set_str(&a->allocs[1]->host, NULL);
ASSERT_JOB_NE("allocs[1](host=h1).host: NULL != \"h1\"", a, b);
eq_set_str(&b->allocs[1]->host, NULL);
TEST_ASSERT_TRUE(job_alloc_eq(a, b, NULL));
b->allocs[1]->allocs[0]->value = 6;
ASSERT_JOB_NE("allocs[1](host=?).allocs[0](resname=mem).value: 2 != 6", a,
b);
free_JobAlloc_ptr(&a);
free_JobAlloc_ptr(&b);
}
/* The function sizes the description itself: a long path is never
* truncated (Req 3.2). */
static void test_job_alloc_eq_long_description_not_truncated(void)
{
xce_job_alloc_t* a = eq_job();
xce_job_alloc_t* b = eq_job();
char host[1001];
char expect[2200];
char* why = NULL;
memset(host, 'x', sizeof(host) - 1);
host[sizeof(host) - 1] = '\0';
eq_set_str(&a->allocs[0]->host, host);
eq_set_str(&b->allocs[0]->host, host);
b->allocs[0]->allocs[0]->value = 9;
snprintf(expect, sizeof(expect),
"allocs[0](host=%s).allocs[0](resname=mem).value: 2 != 9", host);
TEST_ASSERT_FALSE(job_alloc_eq(a, b, &why));
TEST_ASSERT_NOT_NULL(why);
TEST_ASSERT_EQUAL_STRING(expect, why);
free(why);
free_JobAlloc_ptr(&a);
free_JobAlloc_ptr(&b);
}
/* Read-only (Req 1.7): comparing leaves both sides as they were. */
static void test_job_alloc_eq_does_not_modify_arguments(void)
{
xce_job_alloc_t* a = eq_job();
xce_job_alloc_t* b = eq_job();
xce_job_alloc_t* ca = dup_job_alloc(a);
xce_job_alloc_t* cb;
char* why = NULL;
b->allocs[1]->allocs[0]->value = 3;
cb = dup_job_alloc(b);
TEST_ASSERT_NOT_NULL(ca);
TEST_ASSERT_NOT_NULL(cb);
TEST_ASSERT_FALSE(job_alloc_eq(a, b, &why));
free(why);
TEST_ASSERT_TRUE(job_alloc_eq(a, ca, NULL));
TEST_ASSERT_TRUE(job_alloc_eq(b, cb, NULL));
TEST_ASSERT_EQUAL_UINT64(a->allocs[0]->allocs[0]->instid,
ca->allocs[0]->allocs[0]->instid);
free_JobAlloc_ptr(&a);
free_JobAlloc_ptr(&b);
free_JobAlloc_ptr(&ca);
free_JobAlloc_ptr(&cb);
}
void test_xcealloc(void)
{
printf("\n>> xcealloc test\n\n");
@ -467,4 +995,20 @@ void test_xcealloc(void)
RUN_TEST(test_dup_job_alloc_shared_resource_pool_full_equivalence);
RUN_TEST(test_dup_job_alloc_independence_after_mutate_and_free);
RUN_TEST(test_dup_job_alloc_from_duration_fidelity);
RUN_TEST(test_alloc_eq_match_sets_why_null);
RUN_TEST(test_alloc_eq_each_field_mismatch_described);
RUN_TEST(test_alloc_eq_reports_first_difference_in_order);
RUN_TEST(test_alloc_eq_ignores_instid);
RUN_TEST(test_alloc_eq_null_handling);
RUN_TEST(test_host_alloc_eq_match_and_mismatch);
RUN_TEST(test_host_alloc_eq_order_and_length_matter);
RUN_TEST(test_host_alloc_eq_null_handling);
RUN_TEST(test_job_alloc_eq_match);
RUN_TEST(test_job_alloc_eq_jobid_and_lengths);
RUN_TEST(test_job_alloc_eq_nested_differences);
RUN_TEST(test_job_alloc_eq_order_matters);
RUN_TEST(test_job_alloc_eq_shared_order_matters);
RUN_TEST(test_job_alloc_eq_null_handling);
RUN_TEST(test_job_alloc_eq_long_description_not_truncated);
RUN_TEST(test_job_alloc_eq_does_not_modify_arguments);
}

View file

@ -11,8 +11,14 @@
* wrap reaches the calls inside xcealloc.c too. __wrap_strdup() passes
* through to __real_strdup() unless a test arms it; this is a separate
* binary so xcelib_test's other tests are untouched.
*
* libxce-alloc-preason-equality (task 2.1, Req 3.5): the same binary also
* 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.
*/
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -41,6 +47,50 @@ char* __wrap_strdup(const char* s)
return __real_strdup(s);
}
extern int __real_vasprintf(char** strp, const char* fmt, va_list ap);
extern int __real___vasprintf_chk(char** strp, int flag, const char* fmt,
va_list ap);
static int g_vasprintf_fail_at = -1; /* -1 == disabled */
static int g_vasprintf_calls = 0;
static char g_garbage; /* vasprintf leaves *strp undefined on failure */
static int vasprintf_should_fail(char** strp)
{
if (g_vasprintf_fail_at >= 0) {
int this_call = g_vasprintf_calls++;
if (this_call == g_vasprintf_fail_at) {
*strp = &g_garbage;
return 1;
}
}
return 0;
}
int __wrap_vasprintf(char** strp, const char* fmt, va_list ap)
{
if (vasprintf_should_fail(strp)) {
return -1;
}
return __real_vasprintf(strp, fmt, ap);
}
int __wrap___vasprintf_chk(char** strp, int flag, const char* fmt, va_list ap)
{
if (vasprintf_should_fail(strp)) {
return -1;
}
return __real___vasprintf_chk(strp, flag, fmt, ap);
}
/* Make the vasprintf() call with 0-based index `n` (counted from now) fail. */
static void fail_vasprintf_at(int n)
{
g_vasprintf_calls = 0;
g_vasprintf_fail_at = n;
}
/* Make the strdup() call with 0-based index `n` (counted from now) fail. */
static void fail_strdup_at(int n)
{
@ -50,13 +100,16 @@ static void fail_strdup_at(int n)
void setUp(void)
{
g_strdup_fail_at = -1;
g_strdup_calls = 0;
g_strdup_fail_at = -1;
g_strdup_calls = 0;
g_vasprintf_fail_at = -1;
g_vasprintf_calls = 0;
}
void tearDown(void)
{
g_strdup_fail_at = -1;
g_strdup_fail_at = -1;
g_vasprintf_fail_at = -1;
}
static void test_make_alloc_null_resname_returns_null(void)
@ -191,6 +244,71 @@ static void test_merge_job_alloc_strdup_failure_reports_enomem(void)
}
}
/* Req 3.5: a failed description allocation leaves *why NULL and the
* comparison result unchanged. */
static void test_alloc_eq_description_oom_leaves_why_null(void)
{
xce_alloc_t* a = make_alloc((char*)"mem", NULL, 1, 1.0f, 0);
xce_alloc_t* b = make_alloc((char*)"mem", NULL, 2, 2.0f, 0);
char* why = NULL;
TEST_ASSERT_NOT_NULL(a);
TEST_ASSERT_NOT_NULL(b);
b->from = a->from;
fail_vasprintf_at(0);
TEST_ASSERT_FALSE(alloc_eq(a, b, &why));
TEST_ASSERT_NULL(why);
TEST_ASSERT_EQUAL_INT(1, g_vasprintf_calls); /* the wrap was reached */
/* equal: no description, so nothing to fail */
fail_vasprintf_at(0);
b->value = a->value;
why = (char*)&g_garbage;
TEST_ASSERT_TRUE(alloc_eq(a, b, &why));
TEST_ASSERT_NULL(why);
free_ResAlloc_ptr(&a);
free_ResAlloc_ptr(&b);
}
/* A nested difference is described by the innermost call and re-wrapped by
* each parent: failing any one of those formatter calls leaves *why NULL
* (and nothing leaked, per ASAN) with the same result. */
static void test_job_alloc_eq_description_oom_at_every_level(void)
{
xce_job_alloc_t* a = one_host_job("h0");
xce_job_alloc_t* b = one_host_job("h0");
int n;
TEST_ASSERT_NOT_NULL(a);
TEST_ASSERT_NOT_NULL(b);
b->allocs[0]->allocs[0]->from = a->allocs[0]->allocs[0]->from;
b->allocs[0]->allocs[0]->value = 5;
/* alloc (0), host wrap (1), job wrap (2) */
for (n = 0; n < 3; n++) {
char* why = NULL;
fail_vasprintf_at(n);
TEST_ASSERT_FALSE(job_alloc_eq(a, b, &why));
TEST_ASSERT_NULL(why);
}
/* unarmed: the description is produced as usual */
{
char* why = NULL;
g_vasprintf_fail_at = -1;
TEST_ASSERT_FALSE(job_alloc_eq(a, b, &why));
TEST_ASSERT_NOT_NULL(why);
free(why);
}
free_JobAlloc_ptr(&a);
free_JobAlloc_ptr(&b);
}
int main(void)
{
UNITY_BEGIN();
@ -205,6 +323,8 @@ int main(void)
RUN_TEST(test_make_host_alloc_null_host_returns_null);
RUN_TEST(test_make_host_alloc_strdup_failure_leaves_allocs_to_caller);
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);
return UNITY_END();
}

View file

@ -1,4 +1,8 @@
/* vasprintf() (GNU extension) builds the equality functions' difference
* descriptions. */
#define _GNU_SOURCE
#include <errno.h>
#include <inttypes.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
@ -454,3 +458,225 @@ int merge_job_alloc(xce_job_alloc_t* dst, const xce_job_alloc_t* src)
return merge_allocs(&(dst->nres_shared), &(dst->allocs_shared),
src->nres_shared, src->allocs_shared);
}
/* ------------------------------------------------------------------------
* Allocation equality (libxce-alloc-preason-equality). Contract in
* xcealloc.h. The description, when requested, is built bottom-up: the
* innermost comparison formats the differing field, and each enclosing list
* prefixes its own path segment and frees the inner string.
* ---------------------------------------------------------------------- */
/* *why = formatted string, or NULL if the allocation fails (vasprintf leaves
* its output undefined then). No-op when why is NULL. */
static void eq_why(char** why, const char* fmt, ...)
__attribute__((format(printf, 2, 3)));
static void eq_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;
}
/* Prefix an inner description with the list entry's path segment,
* `list[i](key=label).`; a failed inner description stays NULL. */
static void eq_why_wrap(char** why, const char* list, int i, const char* key,
const char* label)
{
char* inner;
if ((why == NULL) || (*why == NULL)) {
return;
}
inner = *why;
eq_why(why, "%s[%d](%s=%s).%s", list, i, key, label, inner);
free(inner);
}
static const char* eq_label(const char* a, const char* b)
{
if (a != NULL) {
return a;
}
return (b != NULL) ? b : "?";
}
static const char* eq_nullness(const void* p)
{
return (p != NULL) ? "non-NULL" : "NULL";
}
static int eq_str_field(const char* name, const char* a, const char* b,
char** why)
{
if (str_eq(a, b)) {
return 1;
}
eq_why(why, "%s: %s%s%s != %s%s%s", name, (a != NULL) ? "\"" : "",
(a != NULL) ? a : "NULL", (a != NULL) ? "\"" : "",
(b != NULL) ? "\"" : "", (b != NULL) ? b : "NULL",
(b != NULL) ? "\"" : "");
return 0;
}
/* In-order comparison of two `n`-entry xce_alloc_t lists named `list`. */
static int eq_alloc_list(const char* list, xce_alloc_t* const* a,
xce_alloc_t* const* b, int n, char** why)
{
int i;
for (i = 0; i < n; i++) {
if (a[i] == b[i]) {
continue;
}
if ((a[i] == NULL) || (b[i] == NULL)) {
eq_why(why, "%s[%d]: %s != %s", list, i, eq_nullness(a[i]),
eq_nullness(b[i]));
return 0;
}
if (!alloc_eq(a[i], b[i], why)) {
eq_why_wrap(why, list, i, "resname",
eq_label(a[i]->resname, b[i]->resname));
return 0;
}
}
return 1;
}
int alloc_eq(const xce_alloc_t* a, const xce_alloc_t* b, char** why)
{
if (why != NULL) {
*why = NULL;
}
if (a == b) {
return 1;
}
if ((a == NULL) || (b == NULL)) {
eq_why(why, "alloc: %s != %s", eq_nullness(a), eq_nullness(b));
return 0;
}
if (!eq_str_field("resname", a->resname, b->resname, why)
|| !eq_str_field("reserve", a->reserve, b->reserve, why)) {
return 0;
}
if (a->value != b->value) {
eq_why(why, "value: %.17g != %.17g", a->value, b->value);
return 0;
}
if (a->duration != b->duration) {
eq_why(why, "duration: %" PRId64 " != %" PRId64, a->duration,
b->duration);
return 0;
}
if (a->from != b->from) {
eq_why(why, "from: %" PRIu64 " != %" PRIu64, a->from, b->from);
return 0;
}
return 1;
}
int host_alloc_eq(const xce_host_alloc_t* a, const xce_host_alloc_t* b,
char** why)
{
if (why != NULL) {
*why = NULL;
}
if (a == b) {
return 1;
}
if ((a == NULL) || (b == NULL)) {
eq_why(why, "host_alloc: %s != %s", eq_nullness(a), eq_nullness(b));
return 0;
}
if (!eq_str_field("host", a->host, b->host, why)) {
return 0;
}
if (a->nallocs != b->nallocs) {
eq_why(why, "nallocs: %d != %d", a->nallocs, b->nallocs);
return 0;
}
return eq_alloc_list("allocs", a->allocs, b->allocs, a->nallocs, why);
}
static void eq_fmt_jobid(char* buf, size_t sz, uint64_t jobid)
{
if (fmt_jobid(buf, sz, jobid) < 0) {
snprintf(buf, sz, "%" PRIu64, jobid);
}
}
int job_alloc_eq(const xce_job_alloc_t* a, const xce_job_alloc_t* b,
char** why)
{
int i;
if (why != NULL) {
*why = NULL;
}
if (a == b) {
return 1;
}
if ((a == NULL) || (b == NULL)) {
eq_why(why, "job_alloc: %s != %s", eq_nullness(a), eq_nullness(b));
return 0;
}
if (a->jobid != b->jobid) {
if (why != NULL) {
char ja[128];
char jb[128];
eq_fmt_jobid(ja, sizeof(ja), a->jobid);
eq_fmt_jobid(jb, sizeof(jb), b->jobid);
eq_why(why, "jobid: %s != %s", ja, jb);
}
return 0;
}
if (a->nhosts != b->nhosts) {
eq_why(why, "nhosts: %d != %d", a->nhosts, b->nhosts);
return 0;
}
if (a->nres_shared != b->nres_shared) {
eq_why(why, "nres_shared: %d != %d", a->nres_shared, b->nres_shared);
return 0;
}
for (i = 0; i < a->nhosts; i++) {
const xce_host_alloc_t* ha = a->allocs[i];
const xce_host_alloc_t* hb = b->allocs[i];
if (ha == hb) {
continue;
}
if ((ha == NULL) || (hb == NULL)) {
eq_why(why, "allocs[%d]: %s != %s", i, eq_nullness(ha),
eq_nullness(hb));
return 0;
}
if (!host_alloc_eq(ha, hb, why)) {
eq_why_wrap(why, "allocs", i, "host", eq_label(ha->host, hb->host));
return 0;
}
}
return eq_alloc_list("allocs_shared", a->allocs_shared, b->allocs_shared,
a->nres_shared, why);
}