test(resreq-maxtasks-wiring): close worker launch-type dispatch coverage gap

Adds white-box tests for job_start_remote_task()'s launch-type gate
(MPI types defer to their own remote-launch mechanism; default type
fans out via xcecli_runtask() across every allocated host, verified
via a #define-before-#include mock capturing taskrun/taskhost
contents) and create_mpi_cmdline()'s host-list/command construction
for openmpi and mpich. Zero production code changes -- this closes a
previously zero-coverage gap in existing, unmodified worker code
(Requirement 4.4), independent of this feature's resreq wiring work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
This commit is contained in:
Rongsong Shen 2026-09-06 05:26:32 +00:00
parent 6577e04196
commit f02bedc52b
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
3 changed files with 601 additions and 1 deletions

View file

@ -22,7 +22,7 @@
- _Requirements: 1.3_
- _Boundary: snapshot_load.c_
- [ ] 2.3 (P) Close the worker's non-default launch-type coverage gap
- [x] 2.3 (P) Close the worker's non-default launch-type coverage gap
- Write a white-box test proving the worker's per-host launch dispatch correctly defers to an MPI launch type's own remote-launch mechanism instead of fanning the job out itself
- Write a white-box test proving the same dispatch correctly fans a default-launch-type job out across every allocated host itself
- Write a white-box test proving the local-launch path's MPI command-line construction builds the correct host list and command, without requiring a real MPI runtime in the test environment

View file

@ -123,3 +123,27 @@ test('worker xcecd_resync test', xcecd_resync_test,
suite: 'worker',
timeout: 60,
env: ['ASAN_OPTIONS=detect_leaks=0'])
# resreq-maxtasks-wiring task 2.3 (Requirement 4.4): closes a pre-existing,
# zero-coverage gap in the worker's launch-type dispatch decision --
# job_start_remote_task()'s MPI-vs-default launch-type gate and
# create_mpi_cmdline()'s command-line construction -- against unmodified
# production code (pure test addition, no production change). Same
# white-box #include of controller_connector.c and -DDBUS_MANAGER flag as
# controller_connector_test above; see test_worker_launch_type_dispatch.c's
# own header comment for why xcecli_runtask()/xcecli_unblock_tasks() cannot
# be stubbed here the way the other job-handling externs above are.
launch_type_dispatch_test = executable('launch_type_dispatch_test',
'test_worker_launch_type_dispatch.c',
c_args: ['-DDBUS_MANAGER',
'-DXCE_CONTROLLER_CONNECTOR_C_PATH="'
+ (meson.current_source_dir() / '..' / 'src' / 'controller_connector.c')
+ '"'],
include_directories: [worker_test_incdir],
dependencies: worker_test_deps,
install: false)
test('worker launch_type_dispatch test', launch_type_dispatch_test,
suite: 'worker',
timeout: 60,
env: ['ASAN_OPTIONS=detect_leaks=0'])

View file

@ -0,0 +1,576 @@
/*
* test_worker_launch_type_dispatch.c — white-box tests closing a pre-
* existing, zero-coverage gap in the worker's launch-type dispatch decision
* (resreq-maxtasks-wiring, task 2.3, boundary: WorkerLaunchTypeCoverage,
* Requirement 4.4).
*
* This gap is adjacent to, but not caused by, the rest of the resreq-
* maxtasks-wiring feature: Requirement 4.4 says "if a job's submission
* specifies a non-default launch type (for example, an MPI launch type),
* the Worker shall launch the job using that launch type's existing
* mechanism across all allocated hosts" -- and before this file, NOTHING
* exercised the two functions that decision actually depends on:
*
* - job_start_remote_task() (controller_connector.c): the launch-type
* GATE itself. For "openmpi"/"mpich" it logs and returns 0 immediately
* -- deferring entirely to MPI's own remote-launch mechanism (the
* command line create_mpi_cmdline() builds, run once via
* job_start_local_task(), covers every host itself). For any other
* launch type it builds an xce_taskhost_t[] covering
* launchinfo->hosts[1..nhosts-1] (host index 0 is the local host,
* started via job_start_local_task() instead) and calls
* xcecli_runtask() to fan the job out via XCE's own per-host dispatch.
* - create_mpi_cmdline() (controller_connector.c): the command-line
* construction job_start_local_task()'s MPI branch calls into --
* builds the "-host"/"-hosts" argument from every allocated host's
* name:rank pair and wraps the original mpirun/mpiexec invocation with
* xce's own xrun launcher.
*
* This is a PURE TEST ADDITION against unmodified, existing production
* code (design.md's WorkerLaunchTypeCoverage component: "Does not modify
* controller_connector.c"). It follows test_controller_connector.c's
* established white-box pattern exactly: #include controller_connector.c
* directly (so these tests exercise the real static functions, not a
* reimplementation), link-satisfy the same set of externs that file's own
* job-handling code paths reference but this file's fixtures don't
* exercise, and use the same -DDBUS_MANAGER build flag as
* controller_connector_test so the #ifdef shape compiles identically to
* that already-established target.
*
* Verification technique for job_start_remote_task() (Req 4.4's real
* decision point): xcecli_runtask() and xcecli_unblock_tasks() are real
* functions from libxce's client.c, which this test binary genuinely
* links (controller_connector.c itself calls many other client.c
* functions -- xcecli_ctx_init, xcecli_join, xcecli_hello, etc. -- that
* test_controller_connector.c already exercises for real, so client.c.o is
* necessarily pulled whole out of libxce.a; a same-named stub definition
* in this file would collide with it at link time). A REAL, connected
* xcecli_ctx_t would route xcecli_runtask() through XCECLI_FAILOVER_CALL's
* full retry/backoff budget (up to ~100s on failure, per
* perform_pending_rejoin()'s own doc comment in controller_connector.c) --
* impractical for a fast unit test, and design.md explicitly says none of
* this requires a real MPI/network runtime. Instead, these tests leave
* xcecli_ctx == NULL (its default): both xcecli_unblock_tasks() and
* xcecli_runtask() check for a NULL ctx and return immediately (-EINVAL
* for xcecli_runtask(), a plain no-op for xcecli_unblock_tasks()) BEFORE
* touching any lock or retry state -- fast, deterministic, and safe. Since
* -EINVAL is the ONLY value job_start_remote_task() can return via that
* path (its other returns are -1 for invalid parameters, -ENOMEM on a
* failed calloc, or 0 for the MPI no-op), seeing rc == -EINVAL back from
* job_start_remote_task() is unambiguous proof that xcecli_runtask() was
* actually reached -- and rc == 0 for an MPI launch type is unambiguous
* proof that it was not.
*
* The "excludes host 0" part of Req 4.4 (job_start_remote_task() fans the
* job out across every host the CONTROLLER allocated it to except index 0,
* which is separately started by job_start_local_task()) is proven with a
* deliberate landmine: launchinfo->hosts[0] is left NULL in the default-
* launch-type fixture below. job_start_remote_task()'s fan-out loop runs
* `for (i = 1; i < launchinfo->nhosts; i++)`; if a regression ever changed
* that lower bound to 0, the very first iteration would dereference
* launchinfo->hosts[0]->host on a NULL pointer and crash immediately --
* there is no assertion that could paper over that, unlike a merely-wrong
* return code.
*
* create_mpi_cmdline() is fully self-contained (no xcecli_* / client.c
* dependency at all) and is called directly, so its returned command
* string is asserted verbatim. Per design.md ("without requiring a real
* MPI runtime"), the one external dependency it has -- an executable
* "xrun" launcher at $XCE_SERVER_DIR/xrun -- is satisfied with a trivial
* shell-script stub created once in main(), below; the tests never invoke
* it, they only need access(..., X_OK) to succeed against it.
*/
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <unity/unity.h>
/* ── Fan-out content-verification mock (remediation) ─────────────────────
*
* job_start_remote_task()'s default-launch-type branch hands the built
* xce_taskrun_t to two src/lib/client.c entry points: xcecli_unblock_tasks()
* and xcecli_runtask(). Leaving xcecli_ctx == NULL (as the file header
* comment above explains) makes the REAL xcecli_runtask() short-circuit on
* its own NULL-ctx guard before ever reading the taskrun argument -- proving
* the call was reached, but nothing about what was actually built.
*
* A `#define`-before-`#include` macro substitution closes that gap: it
* rewrites the CALL SITE inside controller_connector.c to name the mocks
* below instead. This is a different technique from a same-named stub
* definition (which would collide at link time with the real
* xcecli_runtask()/xcecli_unblock_tasks() symbols this binary still pulls
* whole out of client.c.o for the other xcecli_* entry points
* controller_connector.c calls for real): a macro substitution never
* defines a duplicate global symbol, so there is no collision. It applies
* file-wide to this TU, so it also renames the unrelated xcecli_runtask()
* call inside the (unexercised by any test here) remote-job-start-by-
* hostname function further down in controller_connector.c -- harmless,
* since nothing calls that function in this file.
*
* client.h is included here, BEFORE the #define below, purely to bring
* xcecli_ctx_t/xce_taskrun_t/xce_taskhost_t into scope so the mocks can be
* declared with the exact real signatures, and so the mocks -- declared
* `static` -- are this TU's ONLY declaration of their own names (avoiding
* an extern-then-static linkage conflict). This first #include also
* establishes client.h's header guard, so controller_connector.c's own
* later `#include "client.h"` is a no-op that neither sees nor needs to see
* the #define below -- only the call sites inside controller_connector.c's
* function bodies (plain token occurrences of `xcecli_runtask`/
* `xcecli_unblock_tasks`, substituted regardless of header guards) are
* what actually get redirected to the mocks. */
#include "client.h"
#define MOCK_RUNTASK_MAX_HOSTS 8
typedef struct {
int called;
uint64_t jobid;
int nhosts;
char host[MOCK_RUNTASK_MAX_HOSTS][128];
int ntasks[MOCK_RUNTASK_MAX_HOSTS];
} mock_runtask_capture_t;
static mock_runtask_capture_t mock_runtask_capture;
static int mock_unblock_tasks_call_count;
/* Signature mirrors the real xcecli_runtask() (src/include/client.h)
* exactly. Captures every field the strengthened fan-out test below
* asserts on (nhosts, jobid, and each taskhosts[i]'s host name and rank
* count), then returns success so job_start_remote_task() reports the
* fan-out succeeded. */
static int mock_xcecli_runtask(xcecli_ctx_t ctx, xce_taskrun_t* taskrun)
{
int i;
(void)ctx;
mock_runtask_capture.called = 1;
mock_runtask_capture.jobid = taskrun->jobid;
mock_runtask_capture.nhosts = taskrun->nhosts;
for (i = 0; i < taskrun->nhosts && i < MOCK_RUNTASK_MAX_HOSTS; i++) {
xce_taskhost_t* th = taskrun->taskhosts[i];
if (th != NULL && th->host != NULL) {
strncpy(mock_runtask_capture.host[i], th->host,
sizeof(mock_runtask_capture.host[i]) - 1);
mock_runtask_capture.host[i][sizeof(mock_runtask_capture.host[i])
- 1] = '\0';
} else {
mock_runtask_capture.host[i][0] = '\0';
}
mock_runtask_capture.ntasks[i] = (th != NULL) ? th->ntasks : -1;
}
return 0;
}
/* Signature mirrors the real xcecli_unblock_tasks() exactly; true no-op
* (matching the real NULL-ctx short-circuit's own observable behavior), so
* it cannot influence any assertion -- it only counts calls, for tests 1,
* 3, 4, 5 below to confirm (implicitly, by continuing to pass unmodified)
* that this mock does not change their behavior. */
static void mock_xcecli_unblock_tasks(xcecli_ctx_t ctx)
{
(void)ctx;
mock_unblock_tasks_call_count++;
}
/* From here on, every plain-token occurrence of xcecli_runtask(...) /
* xcecli_unblock_tasks(...) -- including the ones inside
* controller_connector.c's own function bodies, included right below --
* is redirected to the real-signature mocks just defined above. */
#define xcecli_runtask(...) mock_xcecli_runtask(__VA_ARGS__)
#define xcecli_unblock_tasks(...) mock_xcecli_unblock_tasks(__VA_ARGS__)
/* Pull the real launch-dispatch logic under test into this TU. */
#include "../src/controller_connector.c"
#ifndef XCE_CONTROLLER_CONNECTOR_C_PATH
#error \
"XCE_CONTROLLER_CONNECTOR_C_PATH must be defined by meson.build to controller_connector.c's absolute path"
#endif
/* controller_connector.c declares `extern hostattrs_t host_attrs;` in its
* non-XRUN branch (this build); provide the definition normally supplied by
* xcecd.c, which this test does not link -- same as
* test_controller_connector.c. */
hostattrs_t host_attrs = { 0 };
/* ── Link-satisfying stubs for externs controller_connector.c references
* but this file's fixtures never exercise (thread pool / worker-list /
* limits-parsing / IPC-signal paths) -- identical set and rationale to
* test_controller_connector.c's own stub block; reused verbatim rather
* than reinvented, per this task's design constraint. ── */
int submit_fork_task(thread_pool_t* pool, fork_task_t* task)
{
(void)pool;
(void)task;
return 0;
}
xcecd_worker_t* find_worker_by_jobid(xcecd_worker_t* worker_list,
uint64_t jobid)
{
(void)worker_list;
(void)jobid;
return NULL;
}
bool parse_limits(const char* exdata, res_limits_t* limits)
{
(void)exdata;
(void)limits;
return true;
}
bool ipc_master_send_job_signal(uint64_t job_id, int signal, bool force)
{
(void)job_id;
(void)signal;
(void)force;
return true;
}
bool ipc_master_send_job_ckpt(uint64_t job_id, int kill, const char* method)
{
(void)job_id;
(void)kill;
(void)method;
return true;
}
void xcecd_process_command(void* arg) { (void)arg; }
void xcecd_finished_job_retention_set_window_ms(uint64_t window_ms)
{
(void)window_ms;
}
bool xcecd_resync_is_pending(void) { return false; }
void xcecd_resync_reset(void) { }
int xcecd_resync_handle_query_page(xcecd_worker_t* worker_list, endpoint_t* ep,
xce_peerinfo_t* from,
const xce_job_state_query_t* page,
uint64_t now_ms, int pktlimit)
{
(void)worker_list;
(void)ep;
(void)from;
(void)page;
(void)now_ms;
(void)pktlimit;
return -EINVAL;
}
/* ════════════════════════════════════════════════════════════════════════
* Fixtures
* ════════════════════════════════════════════════════════════════════════ */
/* Directory/file created once in main() below (before UNITY_BEGIN()) so
* create_mpi_cmdline()'s access(launcher, X_OK) check succeeds without a
* real MPI/xrun install anywhere in the test environment. */
#define FAKE_XRUN_DIR "/tmp/xce_test_worker_launch_type_dispatch_xrun"
static void setup_fake_xrun_launcher(void)
{
char path[512];
mkdir(FAKE_XRUN_DIR, 0755); /* ignore EEXIST from a prior run */
snprintf(path, sizeof(path), "%s/xrun", FAKE_XRUN_DIR);
FILE* f = fopen(path, "w");
if (f) {
fputs("#!/bin/sh\nexit 0\n", f);
fclose(f);
}
chmod(path, 0755);
setenv("XCE_SERVER_DIR", FAKE_XRUN_DIR, 1);
}
void setUp(void)
{
cc_ctx = NULL;
xcecli_ctx = NULL;
memset(&mock_runtask_capture, 0, sizeof(mock_runtask_capture));
mock_unblock_tasks_call_count = 0;
}
void tearDown(void) { return; }
/* ════════════════════════════════════════════════════════════════════════
* Req 4.4 (primary target) — job_start_remote_task()'s launch-type gate:
* an MPI launch type must no-op, deferring entirely to MPI's own
* remote-launch mechanism (create_mpi_cmdline() + job_start_local_task(),
* exercised separately below) rather than fanning the job out itself.
* ════════════════════════════════════════════════════════════════════════ */
static void
test_job_start_remote_task_mpi_launch_types_defer_to_mpi_mechanism(void)
{
static const char* mpi_types[] = { "openmpi", "mpich" };
for (size_t t = 0; t < sizeof(mpi_types) / sizeof(mpi_types[0]); t++) {
job_startinfo_t startinfo = { 0 };
startinfo.launch_type = (char*)mpi_types[t];
startinfo.exechome = (char*)"/home/user";
startinfo.execcwd = (char*)"/home/user";
startinfo.cmdline = (char*)"mpirun myapp";
startinfo.execuser = (char*)"user1";
/* Landmine (see file header comment): if this no-op guard were
* ever removed or narrowed, execution would fall into the fan-out
* loop and immediately dereference launchinfo->hosts[1], which is
* deliberately NULL here -- an unmaskable crash, not just a wrong
* return value. */
xce_host_launch_t host0 = { .host = (char*)"host0", .nranks = 2 };
xce_host_launch_t* hosts[2] = { &host0, NULL };
xce_launch_t launchinfo = { .nhosts = 2, .hosts = hosts };
xcecli_ctx = NULL;
int rc = job_start_remote_task(1000 + (uint64_t)t, &startinfo,
&launchinfo);
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, rc,
"an MPI launch type (\"openmpi\"/\"mpich\") must be a pure "
"no-op returning 0 -- deferring to MPI's own remote-launch "
"mechanism instead of fanning the job out via XCE's own "
"per-host dispatch");
}
printf(
"test_job_start_remote_task_mpi_launch_types_defer_to_mpi_"
"mechanism: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* Req 4.4 (primary target) — job_start_remote_task()'s default-type branch:
* a non-MPI launch type must fan the job out via XCE's own per-host
* dispatch (xcecli_runtask()), covering every allocated host except index
* 0 (the local host, started separately by job_start_local_task()).
* ════════════════════════════════════════════════════════════════════════ */
static void
test_job_start_remote_task_default_launch_type_fans_out_excluding_host_zero(
void)
{
job_startinfo_t startinfo = { 0 };
startinfo.launch_type = (char*)"default";
startinfo.exechome = (char*)"/home/user";
startinfo.execcwd = (char*)"/home/user";
startinfo.cmdline = (char*)"myapp arg1";
startinfo.execuser = (char*)"user1";
xce_host_launch_t host1 = { .host = (char*)"remoteA", .nranks = 4 };
xce_host_launch_t host2 = { .host = (char*)"remoteB", .nranks = 3 };
/* hosts[0] is deliberately NULL: host index 0 is the local host,
* launched via job_start_local_task() instead, and must never be
* touched by this fan-out loop. If a regression ever changed the loop
* to start at i=0 instead of i=1, this crashes immediately on
* launchinfo->hosts[0]->host -- see file header comment. */
xce_host_launch_t* hosts[3] = { NULL, &host1, &host2 };
xce_launch_t launchinfo = { .nhosts = 3, .hosts = hosts };
/* xcecli_ctx's value is irrelevant now: xcecli_runtask()/
* xcecli_unblock_tasks() are macro-substituted (see the mock block
* above the controller_connector.c #include) to test-local mocks that
* capture the real xce_taskrun_t contents instead of the real,
* NULL-ctx-short-circuiting client.c functions. Left NULL for
* consistency with the other tests in this file. */
xcecli_ctx = NULL;
int rc = job_start_remote_task(4242, &startinfo, &launchinfo);
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, rc,
"with xcecli_runtask() mocked to succeed, job_start_remote_task() "
"must report success");
TEST_ASSERT_TRUE_MESSAGE(
mock_runtask_capture.called,
"a non-MPI (default) launch type must fan the job out by actually "
"calling xcecli_runtask() -- not skip it the way the MPI launch "
"types above do");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
4242, mock_runtask_capture.jobid,
"the taskrun handed to xcecli_runtask() must carry the job's own "
"jobid");
TEST_ASSERT_EQUAL_INT_MESSAGE(
2, mock_runtask_capture.nhosts,
"the taskrun must report exactly nhosts-1 = 2 hosts -- every "
"allocated host except index 0 (the local host), neither more nor "
"fewer; this is the count a one-host-short fan-out regression "
"(e.g. stopping the host-filling loop at nhosts-1 instead of "
"nhosts) would get wrong");
TEST_ASSERT_EQUAL_STRING_MESSAGE(
"remoteA", mock_runtask_capture.host[0],
"taskhosts[0] must be host index 1 from launchinfo (\"remoteA\") -- "
"the first host after the excluded local host 0");
TEST_ASSERT_EQUAL_INT_MESSAGE(
4, mock_runtask_capture.ntasks[0],
"taskhosts[0]'s rank count must match remoteA's allocated nranks");
TEST_ASSERT_EQUAL_STRING_MESSAGE(
"remoteB", mock_runtask_capture.host[1],
"taskhosts[1] must be host index 2 from launchinfo (\"remoteB\") -- "
"proving the fan-out reaches every remaining allocated host, not "
"just the first one after the exclusion");
TEST_ASSERT_EQUAL_INT_MESSAGE(
3, mock_runtask_capture.ntasks[1],
"taskhosts[1]'s rank count must match remoteB's allocated nranks");
printf(
"test_job_start_remote_task_default_launch_type_fans_out_excluding_"
"host_zero: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* Req 4.4 (secondary target) — create_mpi_cmdline(): the local-launch
* path's (job_start_local_task()) MPI command-line construction must build
* the correct host list and command, for each supported MPI type.
* ════════════════════════════════════════════════════════════════════════ */
static void
test_create_mpi_cmdline_openmpi_builds_correct_hostlist_and_command(void)
{
xce_host_launch_t hostA = { .host = (char*)"hostA", .nranks = 4 };
xce_host_launch_t hostB = { .host = (char*)"hostB", .nranks = 2 };
xce_host_launch_t* hosts[2] = { &hostA, &hostB };
xce_launch_t launchinfo = { .nhosts = 2, .hosts = hosts };
char cmd[] = "mpirun myapp arg1 arg2";
char type[] = "openmpi";
char* result = create_mpi_cmdline(cmd, type, &launchinfo);
TEST_ASSERT_NOT_NULL_MESSAGE(
result, "create_mpi_cmdline must succeed given a valid mpirun "
"command, a supported MPI type, and a populated host "
"list");
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, strncmp(result, "mpirun ", strlen("mpirun ")),
"the final command must start with the original mpi executable");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(result, "-host hostA:4,hostB:2"),
"the -host option must list every allocated host with its rank "
"count, comma-separated, in order");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(result, "--mca plm_rsh_agent"),
"openmpi must use its own launcher-agent flag");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(result, "/xrun"),
"the xce xrun launcher path must be embedded in the final "
"command");
size_t applen = strlen("myapp arg1 arg2");
TEST_ASSERT_TRUE_MESSAGE(
strlen(result) >= applen
&& strcmp(result + strlen(result) - applen, "myapp arg1 arg2")
== 0,
"the original application command must be preserved verbatim at "
"the end of the final command line");
printf(
"test_create_mpi_cmdline_openmpi_builds_correct_hostlist_and_"
"command: PASS (\"%s\")\n",
result);
FREEUP(result);
}
static void
test_create_mpi_cmdline_mpich_builds_correct_hostlist_and_command(void)
{
xce_host_launch_t hostA = { .host = (char*)"hostA", .nranks = 1 };
xce_host_launch_t hostB = { .host = (char*)"hostB", .nranks = 1 };
xce_host_launch_t hostC = { .host = (char*)"hostC", .nranks = 2 };
xce_host_launch_t* hosts[3] = { &hostA, &hostB, &hostC };
xce_launch_t launchinfo = { .nhosts = 3, .hosts = hosts };
char cmd[] = "mpiexec myjob --flag";
char type[] = "mpich";
char* result = create_mpi_cmdline(cmd, type, &launchinfo);
TEST_ASSERT_NOT_NULL_MESSAGE(
result, "create_mpi_cmdline must succeed given a valid mpiexec "
"command, a supported MPI type, and a populated host "
"list");
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, strncmp(result, "mpiexec ", strlen("mpiexec ")),
"the final command must start with the original mpi executable");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(result, "-hosts hostA:1,hostB:1,hostC:2"),
"mpich's -hosts option must list every allocated host with its "
"rank count, comma-separated, in order (all three, not just two)");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(result, "-launcher-exec"),
"mpich must use its own launcher-exec flag, distinct from "
"openmpi's --mca plm_rsh_agent");
size_t applen = strlen("myjob --flag");
TEST_ASSERT_TRUE_MESSAGE(
strlen(result) >= applen
&& strcmp(result + strlen(result) - applen, "myjob --flag") == 0,
"the original application command must be preserved verbatim at "
"the end of the final command line");
printf(
"test_create_mpi_cmdline_mpich_builds_correct_hostlist_and_command: "
"PASS (\"%s\")\n",
result);
FREEUP(result);
}
/* Bonus robustness coverage, same target function: an MPI type outside the
* two supported values must be rejected, not silently accepted -- proves
* the type check itself is reached (the launcher-access check above it
* already passed, via the fake xrun stub, so a NULL here can only come
* from the unsupported-type branch). */
static void test_create_mpi_cmdline_rejects_unsupported_mpi_type(void)
{
xce_host_launch_t hostA = { .host = (char*)"hostA", .nranks = 1 };
xce_host_launch_t* hosts[1] = { &hostA };
xce_launch_t launchinfo = { .nhosts = 1, .hosts = hosts };
char cmd[] = "mpirun myapp";
char type[] = "some-other-mpi";
char* result = create_mpi_cmdline(cmd, type, &launchinfo);
TEST_ASSERT_NULL_MESSAGE(
result,
"an MPI type other than openmpi/mpich must be rejected (NULL), "
"not silently accepted");
printf("test_create_mpi_cmdline_rejects_unsupported_mpi_type: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* main
* ════════════════════════════════════════════════════════════════════════ */
int main(void)
{
xce_init_xcecd_log("/tmp/test_worker_launch_type_dispatch.log",
1024 * 1024, 2);
setup_fake_xrun_launcher();
UNITY_BEGIN();
RUN_TEST(
test_job_start_remote_task_mpi_launch_types_defer_to_mpi_mechanism);
RUN_TEST(
test_job_start_remote_task_default_launch_type_fans_out_excluding_host_zero);
RUN_TEST(test_create_mpi_cmdline_openmpi_builds_correct_hostlist_and_command);
RUN_TEST(test_create_mpi_cmdline_mpich_builds_correct_hostlist_and_command);
RUN_TEST(test_create_mpi_cmdline_rejects_unsupported_mpi_type);
return UNITY_END();
}