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:
parent
6577e04196
commit
f02bedc52b
3 changed files with 601 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
|
|
|||
576
src/worker/testsuite/test_worker_launch_type_dispatch.c
Normal file
576
src/worker/testsuite/test_worker_launch_type_dispatch.c
Normal 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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue