759 lines
31 KiB
C
759 lines
31 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_tensix_claim.c
|
|
* @brief Gate for ft_tt_tensix_claim.c — the per-CORE ownership that lets
|
|
* correctness work run in parallel instead of serialising behind the
|
|
* whole-card lock.
|
|
*
|
|
* ---------------------------------------------------------------------------
|
|
* WHAT THIS SUITE IS BUILT TO CATCH, AND HOW IT AVOIDS BEING VACUOUS
|
|
* ---------------------------------------------------------------------------
|
|
* Two rules shape every stage here.
|
|
*
|
|
* **The lock is checked directly, never through the claim layer.** A claim
|
|
* verified by asking `ft_tt_tensix_claim_acquire()` again proves only that the
|
|
* module agrees with itself; a build whose acquire is a no-op passes that. So
|
|
* `lock_file_is_free()` opens `<root>/<card>/tensix/<index>.lock` and takes the
|
|
* `flock` itself. That is the kernel's state, and nothing in the module can make
|
|
* it lie. Same reasoning as test_udrv_claim.c's forged-record stage.
|
|
*
|
|
* **The decisive case is two processes racing for one core**, and it is run in
|
|
* two shapes because they catch different bugs. Stage 4 is DETERMINISTIC — a
|
|
* helper holds the middle core of a three-core request, so the refusal and the
|
|
* roll-back happen at a known point and the "nothing was half-claimed" assertion
|
|
* is not a timing accident. Stage 5 is a REAL RACE — four forked children fire at
|
|
* one core off a shared starting gun, and exactly one may win.
|
|
*
|
|
* @status Off-hardware. Filesystem and fork only; no device is opened, no NoC
|
|
* transaction is possible from anything this links.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
/* flock(2) is BSD, not POSIX, so a bare _POSIX_C_SOURCE hides it on both
|
|
* platforms this code is built on (DEV_NOTES §5, the _DEFAULT_SOURCE note). */
|
|
#if defined(__APPLE__)
|
|
#define _DARWIN_C_SOURCE
|
|
#else
|
|
#define _DEFAULT_SOURCE
|
|
#endif
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/file.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include "ft_tt_tensix_claim.h"
|
|
#include "ft_tt_udrv_claim.h"
|
|
#include "ft_tt_test_report.h"
|
|
#include "ft_tt_udrv.h"
|
|
|
|
#define TEST_CARD "0000:12:00.0"
|
|
#define RACE_CHILD_COUNT 4u
|
|
#define RACE_CORE 9u
|
|
|
|
static char scratch_root[160];
|
|
|
|
static void build_scratch_root(void)
|
|
{
|
|
(void)snprintf(scratch_root, sizeof(scratch_root), "/tmp/ft_tt_tensix_claim_test.%ld",
|
|
(long)getpid());
|
|
}
|
|
|
|
static void remove_scratch_root(void)
|
|
{
|
|
char command[256];
|
|
|
|
(void)snprintf(command, sizeof(command), "rm -rf '%s'", scratch_root);
|
|
if (system(command) != 0)
|
|
(void)fprintf(stderr, "fixture: cleanup of %s failed\n", scratch_root);
|
|
}
|
|
|
|
/**
|
|
* THE INDEPENDENT INSTRUMENT: is the kernel's lock on this core free?
|
|
*
|
|
* Deliberately does not call into ft_tt_tensix_claim or ft_tt_udrv_claim at all.
|
|
* It opens the lock file by path and takes the flock, so a module that forgets to
|
|
* lock, forgets to release, or leaks a descriptor cannot hide from it.
|
|
*/
|
|
static bool lock_file_is_free(uint32_t index)
|
|
{
|
|
char path[256];
|
|
int fd;
|
|
bool free_now;
|
|
|
|
(void)snprintf(path, sizeof(path), "%s/%s/%s/%u.lock", scratch_root, TEST_CARD,
|
|
FT_TT_TENSIX_CLAIM_NAMESPACE, index);
|
|
|
|
fd = open(path, O_RDWR | O_CREAT | O_CLOEXEC, 0666);
|
|
if (fd < 0)
|
|
return true; /* no file at all means nobody ever claimed it */
|
|
|
|
free_now = (flock(fd, LOCK_EX | LOCK_NB) == 0);
|
|
if (free_now)
|
|
(void)flock(fd, LOCK_UN);
|
|
(void)close(fd);
|
|
return free_now;
|
|
}
|
|
|
|
/** Non-NULL with a count of zero: an EXPLICITLY empty reserved set. NULL means
|
|
* "read the environment", and a suite whose result depends on the shell it was
|
|
* launched from is a suite that lies. */
|
|
static const uint32_t k_no_reserved[1] = {0u};
|
|
|
|
/** A request against the scratch root with no reserved set and no retirement. */
|
|
static void build_request(ft_tt_tensix_claim_request *request, const uint32_t *indices,
|
|
size_t count, const char *reason)
|
|
{
|
|
memset(request, 0, sizeof(*request));
|
|
request->root = scratch_root;
|
|
request->card = TEST_CARD;
|
|
request->indices = indices;
|
|
request->count = count;
|
|
request->reason = reason;
|
|
request->reserved = k_no_reserved;
|
|
request->reserved_count = 0u;
|
|
}
|
|
|
|
static void stage_parse_index_lists(void)
|
|
{
|
|
uint32_t out[16];
|
|
size_t count = 0u;
|
|
|
|
ft_tt_test_step("Stage 1 — the core list parser, which decides what a sweep may name");
|
|
|
|
ft_tt_test_check_fmt("a single index parses",
|
|
ft_tt_tensix_claim_parse_indices("71", out, 16u, &count) ==
|
|
FT_TT_UDRV_OK &&
|
|
count == 1u && out[0] == 71u,
|
|
"count=%zu", count);
|
|
|
|
ft_tt_test_check_fmt("a comma list keeps its order",
|
|
ft_tt_tensix_claim_parse_indices("3,1,17", out, 16u, &count) ==
|
|
FT_TT_UDRV_OK &&
|
|
count == 3u && out[0] == 3u && out[1] == 1u && out[2] == 17u,
|
|
"count=%zu", count);
|
|
|
|
ft_tt_test_check_fmt("an inclusive range expands",
|
|
ft_tt_tensix_claim_parse_indices("30-33", out, 16u, &count) ==
|
|
FT_TT_UDRV_OK &&
|
|
count == 4u && out[0] == 30u && out[3] == 33u,
|
|
"count=%zu", count);
|
|
|
|
ft_tt_test_check_fmt("ranges and indices mix",
|
|
ft_tt_tensix_claim_parse_indices("1,4-6,9", out, 16u, &count) ==
|
|
FT_TT_UDRV_OK &&
|
|
count == 5u && out[1] == 4u && out[3] == 6u && out[4] == 9u,
|
|
"count=%zu", count);
|
|
|
|
ft_tt_test_check("an empty string is zero indices, not an error",
|
|
ft_tt_tensix_claim_parse_indices("", out, 16u, &count) == FT_TT_UDRV_OK &&
|
|
count == 0u,
|
|
"unset means no reserved cores");
|
|
|
|
ft_tt_test_check("a NULL string is zero indices",
|
|
ft_tt_tensix_claim_parse_indices(NULL, out, 16u, &count) == FT_TT_UDRV_OK &&
|
|
count == 0u,
|
|
"");
|
|
|
|
ft_tt_test_check("an inverted range is refused",
|
|
ft_tt_tensix_claim_parse_indices("9-3", out, 16u, &count) ==
|
|
FT_TT_UDRV_EINVAL,
|
|
"");
|
|
|
|
ft_tt_test_check("trailing junk is refused",
|
|
ft_tt_tensix_claim_parse_indices("1,2x", out, 16u, &count) ==
|
|
FT_TT_UDRV_EINVAL,
|
|
"a typo must not silently claim fewer cores");
|
|
|
|
ft_tt_test_check("a space is refused",
|
|
ft_tt_tensix_claim_parse_indices("1, 2", out, 16u, &count) ==
|
|
FT_TT_UDRV_EINVAL,
|
|
"more likely a quoting mistake than an intent");
|
|
|
|
ft_tt_test_check("an index at the namespace bound is refused",
|
|
ft_tt_tensix_claim_parse_indices("256", out, 16u, &count) ==
|
|
FT_TT_UDRV_EINVAL,
|
|
"");
|
|
|
|
ft_tt_test_check("overflowing the caller's array is refused, not truncated",
|
|
ft_tt_tensix_claim_parse_indices("0-99", out, 16u, &count) ==
|
|
FT_TT_UDRV_EINVAL,
|
|
"a truncated set is a set that claims the wrong cores");
|
|
}
|
|
|
|
static void stage_set_ownership(void)
|
|
{
|
|
static const uint32_t wanted[] = {40u, 41u, 42u};
|
|
ft_tt_tensix_claim_request request;
|
|
ft_tt_tensix_claim_refusal refusal;
|
|
ft_tt_tensix_claim_set *set = NULL;
|
|
|
|
ft_tt_test_step("Stage 2 — a set is claimed, and the KERNEL agrees it is claimed");
|
|
|
|
ft_tt_test_check("all three cores are free before we start",
|
|
lock_file_is_free(40u) && lock_file_is_free(41u) && lock_file_is_free(42u),
|
|
"checked by flock, not by asking the module");
|
|
|
|
build_request(&request, wanted, 3u, "stage 2 set");
|
|
ft_tt_test_check("the set is granted",
|
|
ft_tt_tensix_claim_acquire(&request, &set, &refusal) == FT_TT_UDRV_OK, "");
|
|
ft_tt_test_check_fmt("it holds three cores", ft_tt_tensix_claim_count(set) == 3u, "%zu",
|
|
ft_tt_tensix_claim_count(set));
|
|
ft_tt_test_check_fmt("...in the order asked for",
|
|
ft_tt_tensix_claim_index_at(set, 0u) == 40u &&
|
|
ft_tt_tensix_claim_index_at(set, 2u) == 42u,
|
|
"%u..%u", ft_tt_tensix_claim_index_at(set, 0u),
|
|
ft_tt_tensix_claim_index_at(set, 2u));
|
|
|
|
ft_tt_test_check("the kernel says all three locks are taken",
|
|
!lock_file_is_free(40u) && !lock_file_is_free(41u) && !lock_file_is_free(42u),
|
|
"flock refused on each");
|
|
ft_tt_test_check("a core NOT in the set is untouched", lock_file_is_free(43u), "");
|
|
|
|
ft_tt_tensix_claim_release(set);
|
|
ft_tt_test_check("releasing gives all three back",
|
|
lock_file_is_free(40u) && lock_file_is_free(41u) && lock_file_is_free(42u),
|
|
"flock granted on each");
|
|
|
|
ft_tt_tensix_claim_release(NULL);
|
|
ft_tt_test_check("releasing NULL is a no-op", true, "returned");
|
|
}
|
|
|
|
static void stage_bad_requests(void)
|
|
{
|
|
static const uint32_t twice[] = {7u, 12u, 7u};
|
|
static const uint32_t out_of_range[] = {FT_TT_TENSIX_CLAIM_INDEX_COUNT};
|
|
ft_tt_tensix_claim_request request;
|
|
ft_tt_tensix_claim_refusal refusal;
|
|
ft_tt_tensix_claim_set *set = NULL;
|
|
|
|
ft_tt_test_step("Stage 3 — malformed requests are refused BEFORE anything is locked");
|
|
|
|
build_request(&request, twice, 3u, "duplicate");
|
|
ft_tt_test_check("a core named twice is refused",
|
|
ft_tt_tensix_claim_acquire(&request, &set, &refusal) == FT_TT_UDRV_EINVAL,
|
|
"it would self-deadlock and report itself as the holder");
|
|
ft_tt_test_check_fmt("...and the refusal names the duplicate",
|
|
refusal.outcome == FT_TT_TENSIX_CLAIM_DUPLICATE && refusal.index == 7u,
|
|
"index %u", refusal.index);
|
|
ft_tt_test_check("...and nothing was locked", lock_file_is_free(7u) && lock_file_is_free(12u),
|
|
"");
|
|
|
|
build_request(&request, out_of_range, 1u, "oob");
|
|
ft_tt_test_check("an index at the namespace bound is refused",
|
|
ft_tt_tensix_claim_acquire(&request, &set, &refusal) == FT_TT_UDRV_EINVAL,
|
|
"");
|
|
|
|
build_request(&request, twice, 0u, "empty");
|
|
ft_tt_test_check("an empty request is refused",
|
|
ft_tt_tensix_claim_acquire(&request, &set, &refusal) == FT_TT_UDRV_EINVAL,
|
|
"claiming nothing is not a claim");
|
|
}
|
|
|
|
/** The other job: claim `index`, say so down the pipe, then wait to be killed
|
|
* WITHOUT releasing anything. Never returns. */
|
|
static void holder_child(uint32_t index, const char *reason, int ready_pipe[2])
|
|
{
|
|
ft_tt_tensix_claim_request request;
|
|
ft_tt_tensix_claim_refusal refusal;
|
|
ft_tt_tensix_claim_set *set = NULL;
|
|
uint32_t held_index = index;
|
|
char signal_byte = 1;
|
|
|
|
(void)close(ready_pipe[0]);
|
|
build_request(&request, &held_index, 1u, reason);
|
|
|
|
if (ft_tt_tensix_claim_acquire(&request, &set, &refusal) != FT_TT_UDRV_OK)
|
|
_exit(3);
|
|
if (write(ready_pipe[1], &signal_byte, 1u) != 1)
|
|
_exit(4);
|
|
for (;;)
|
|
pause();
|
|
}
|
|
|
|
/**
|
|
* Fork the holder above.
|
|
*
|
|
* A pipe handshake rather than a poll loop: a parent that polls by ATTEMPTING the
|
|
* claim can win the race itself, and the stage then goes red for a reason that
|
|
* has nothing to do with the mechanism (test_udrv_claim.c learned this one).
|
|
*
|
|
* @return the child pid, or -1
|
|
*/
|
|
static pid_t fork_holder(uint32_t index, const char *reason, int ready_pipe[2])
|
|
{
|
|
pid_t child;
|
|
|
|
if (pipe(ready_pipe) != 0)
|
|
return -1;
|
|
|
|
child = fork();
|
|
if (child == 0)
|
|
holder_child(index, reason, ready_pipe);
|
|
|
|
(void)close(ready_pipe[1]);
|
|
return child;
|
|
}
|
|
|
|
static void stage_no_half_claim(void)
|
|
{
|
|
static const uint32_t wanted[] = {20u, 21u, 22u};
|
|
ft_tt_tensix_claim_request request;
|
|
ft_tt_tensix_claim_refusal refusal;
|
|
ft_tt_tensix_claim_set *set = NULL;
|
|
int ready_pipe[2];
|
|
char ready = 0;
|
|
pid_t holder;
|
|
int status = 0;
|
|
|
|
ft_tt_test_step("Stage 4 — THE DECISIVE ONE: a refused set leaves NOTHING half-claimed");
|
|
|
|
holder = fork_holder(21u, "the other job", ready_pipe);
|
|
if (holder < 0) {
|
|
ft_tt_test_check("fork succeeded", false, "fork failed");
|
|
return;
|
|
}
|
|
ft_tt_test_check("the other process holds core 21",
|
|
read(ready_pipe[0], &ready, 1u) == 1 && ready == 1, "handshake");
|
|
(void)close(ready_pipe[0]);
|
|
ft_tt_test_check("...and the kernel agrees", !lock_file_is_free(21u), "flock refused");
|
|
|
|
/* 20 is taken first, 21 refuses, 22 is never reached. The roll-back is what
|
|
* must give 20 back — and it is checked by flock, not by re-asking. */
|
|
build_request(&request, wanted, 3u, "the sweep that loses");
|
|
ft_tt_test_check("the whole set is refused",
|
|
ft_tt_tensix_claim_acquire(&request, &set, &refusal) == FT_TT_UDRV_EAGAIN,
|
|
"one core of three was taken");
|
|
ft_tt_test_check_fmt("the refusal names the core it lost on",
|
|
refusal.outcome == FT_TT_TENSIX_CLAIM_HELD && refusal.index == 21u,
|
|
"outcome=%d index=%u", (int)refusal.outcome, refusal.index);
|
|
ft_tt_test_check_fmt("...and names the process holding it", refusal.pid == holder,
|
|
"pid %ld vs holder %ld", (long)refusal.pid, (long)holder);
|
|
ft_tt_test_check_fmt("...and the reason that process registered",
|
|
strcmp(refusal.reason, "the other job") == 0, "%s", refusal.reason);
|
|
|
|
ft_tt_test_check("core 20 — claimed then rolled back — is FREE", lock_file_is_free(20u),
|
|
"the kernel's answer, not the module's");
|
|
ft_tt_test_check("core 22 — never reached — is FREE", lock_file_is_free(22u), "");
|
|
|
|
(void)kill(holder, SIGKILL);
|
|
(void)waitpid(holder, &status, 0);
|
|
|
|
ft_tt_test_check("SIGKILL alone returns core 21", lock_file_is_free(21u),
|
|
"no cleanup path of ours ran");
|
|
ft_tt_test_check("...so the same set now succeeds",
|
|
ft_tt_tensix_claim_acquire(&request, &set, &refusal) == FT_TT_UDRV_OK, "");
|
|
ft_tt_tensix_claim_release(set);
|
|
}
|
|
|
|
/** One racer: wait for the gun, claim RACE_CORE, report the outcome down a pipe. */
|
|
static void race_child(int gate_read, int report_write)
|
|
{
|
|
static uint32_t index = RACE_CORE;
|
|
ft_tt_tensix_claim_request request;
|
|
ft_tt_tensix_claim_refusal refusal;
|
|
ft_tt_tensix_claim_set *set = NULL;
|
|
char gun = 0;
|
|
char line[64];
|
|
int rc;
|
|
int written;
|
|
|
|
if (read(gate_read, &gun, 1u) != 1)
|
|
_exit(9);
|
|
|
|
build_request(&request, &index, 1u, "racer");
|
|
rc = ft_tt_tensix_claim_acquire(&request, &set, &refusal);
|
|
|
|
written = snprintf(line, sizeof(line), "%ld %d %ld\n", (long)getpid(),
|
|
rc == FT_TT_UDRV_OK ? 0 : (int)refusal.outcome, (long)refusal.pid);
|
|
if (written > 0) {
|
|
/* glibc marks write() warn_unused_result, and a (void) cast does NOT silence that
|
|
* under gcc -Werror=unused-result (it does under clang, which is why this built on
|
|
* the Mac and broke Makefile.udrv on the card host). This is a child reporting its verdict
|
|
* down a pipe on a path where there is nothing useful to do about a short write. */
|
|
ssize_t reported = write(report_write, line, (size_t)written);
|
|
(void)reported;
|
|
}
|
|
|
|
/* A winner must hold the claim long enough for every loser to be refused,
|
|
* otherwise the race is decided by scheduling rather than by flock. */
|
|
if (rc == FT_TT_UDRV_OK) {
|
|
sleep(1u);
|
|
ft_tt_tensix_claim_release(set);
|
|
}
|
|
_exit(0);
|
|
}
|
|
|
|
static void stage_real_race(void)
|
|
{
|
|
int gate[2];
|
|
int report[2];
|
|
pid_t children[RACE_CHILD_COUNT];
|
|
char gun[RACE_CHILD_COUNT];
|
|
char buffer[512];
|
|
ssize_t got;
|
|
size_t total = 0u;
|
|
unsigned granted = 0u;
|
|
unsigned held = 0u;
|
|
unsigned other = 0u;
|
|
unsigned named_the_winner = 0u;
|
|
long winner_pid = 0;
|
|
unsigned child;
|
|
char *cursor;
|
|
|
|
ft_tt_test_step("Stage 5 — a REAL race: %u processes, one core, one winner",
|
|
RACE_CHILD_COUNT);
|
|
|
|
if (pipe(gate) != 0 || pipe(report) != 0) {
|
|
ft_tt_test_check("pipes created", false, "pipe failed");
|
|
return;
|
|
}
|
|
|
|
for (child = 0u; child < RACE_CHILD_COUNT; child++) {
|
|
children[child] = fork();
|
|
if (children[child] == 0) {
|
|
(void)close(gate[1]);
|
|
(void)close(report[0]);
|
|
race_child(gate[0], report[1]);
|
|
}
|
|
}
|
|
(void)close(gate[0]);
|
|
(void)close(report[1]);
|
|
|
|
memset(gun, 1, sizeof(gun));
|
|
ft_tt_test_check("the starting gun fires for all of them at once",
|
|
write(gate[1], gun, sizeof(gun)) == (ssize_t)sizeof(gun), "one write");
|
|
(void)close(gate[1]);
|
|
|
|
while (total < sizeof(buffer) - 1u &&
|
|
(got = read(report[0], buffer + total, sizeof(buffer) - 1u - total)) > 0)
|
|
total += (size_t)got;
|
|
buffer[total] = '\0';
|
|
(void)close(report[0]);
|
|
|
|
for (child = 0u; child < RACE_CHILD_COUNT; child++) {
|
|
int status = 0;
|
|
|
|
(void)waitpid(children[child], &status, 0);
|
|
}
|
|
|
|
/* First pass finds the winner, second pass checks every loser named it. */
|
|
for (cursor = buffer; *cursor != '\0';) {
|
|
long reporter = 0;
|
|
int outcome = -1;
|
|
long holder = 0;
|
|
|
|
if (sscanf(cursor, "%ld %d %ld", &reporter, &outcome, &holder) == 3) {
|
|
if (outcome == (int)FT_TT_TENSIX_CLAIM_GRANTED) {
|
|
granted++;
|
|
winner_pid = reporter;
|
|
} else if (outcome == (int)FT_TT_TENSIX_CLAIM_HELD) {
|
|
held++;
|
|
} else {
|
|
other++;
|
|
}
|
|
}
|
|
cursor = strchr(cursor, '\n');
|
|
if (!cursor)
|
|
break;
|
|
cursor++;
|
|
}
|
|
|
|
for (cursor = buffer; *cursor != '\0';) {
|
|
long reporter = 0;
|
|
int outcome = -1;
|
|
long holder = 0;
|
|
|
|
if (sscanf(cursor, "%ld %d %ld", &reporter, &outcome, &holder) == 3 &&
|
|
outcome == (int)FT_TT_TENSIX_CLAIM_HELD && holder == winner_pid)
|
|
named_the_winner++;
|
|
cursor = strchr(cursor, '\n');
|
|
if (!cursor)
|
|
break;
|
|
cursor++;
|
|
}
|
|
|
|
ft_tt_test_check_fmt("exactly one process won", granted == 1u, "%u granted", granted);
|
|
ft_tt_test_check_fmt("every other process was told the core is HELD",
|
|
held == RACE_CHILD_COUNT - 1u && other == 0u, "%u held, %u other", held,
|
|
other);
|
|
ft_tt_test_check_fmt("...and every refusal named the winner by pid",
|
|
named_the_winner == RACE_CHILD_COUNT - 1u, "%u of %u named pid %ld",
|
|
named_the_winner, RACE_CHILD_COUNT - 1u, winner_pid);
|
|
ft_tt_test_check("the core is free once the winner exits", lock_file_is_free(RACE_CORE), "");
|
|
}
|
|
|
|
static void stage_reserved_set(void)
|
|
{
|
|
static const uint32_t wanted[] = {30u, 31u};
|
|
static const uint32_t reserved[] = {31u};
|
|
ft_tt_tensix_claim_request request;
|
|
ft_tt_tensix_claim_refusal refusal;
|
|
ft_tt_tensix_claim_set *set = NULL;
|
|
size_t parsed_count = 0u;
|
|
uint32_t parsed[32];
|
|
|
|
ft_tt_test_step("Stage 6 — reserved cores: another family's, and a live serve's");
|
|
|
|
build_request(&request, wanted, 2u, "sweep into reserved");
|
|
request.reserved = reserved;
|
|
request.reserved_count = 1u;
|
|
|
|
ft_tt_test_check("a request naming a reserved core is refused",
|
|
ft_tt_tensix_claim_acquire(&request, &set, &refusal) == FT_TT_UDRV_EAGAIN,
|
|
"");
|
|
ft_tt_test_check_fmt("...and says RESERVED, not HELD",
|
|
refusal.outcome == FT_TT_TENSIX_CLAIM_RESERVED && refusal.index == 31u,
|
|
"outcome=%d index=%u", (int)refusal.outcome, refusal.index);
|
|
/* The point of checking 30: reserved is evaluated for the WHOLE request
|
|
* before any lock is taken, so even the core that WOULD have been fine is
|
|
* never touched. */
|
|
ft_tt_test_check("...and core 30, which comes first, was never locked",
|
|
lock_file_is_free(30u), "reserved is checked before anything is claimed");
|
|
|
|
request.reserved_count = 0u;
|
|
ft_tt_test_check("with nothing reserved the same request succeeds",
|
|
ft_tt_tensix_claim_acquire(&request, &set, &refusal) == FT_TT_UDRV_OK, "");
|
|
ft_tt_tensix_claim_release(set);
|
|
|
|
(void)setenv(FT_TT_TENSIX_CLAIM_RESERVED_ENVIRONMENT, "0-9,14,15,28", 1);
|
|
ft_tt_test_check_fmt("the environment reserved set parses",
|
|
ft_tt_tensix_claim_reserved_from_environment(parsed, 32u,
|
|
&parsed_count) ==
|
|
FT_TT_UDRV_OK &&
|
|
parsed_count == 13u,
|
|
"%zu cores", parsed_count);
|
|
|
|
{
|
|
static const uint32_t into_environment_reserved[] = {14u};
|
|
ft_tt_tensix_claim_request env_request;
|
|
|
|
build_request(&env_request, into_environment_reserved, 1u, "env reserved");
|
|
env_request.reserved = NULL; /* NULL is what reads the environment */
|
|
ft_tt_test_check("a NULL reserved set reads the environment and refuses",
|
|
ft_tt_tensix_claim_acquire(&env_request, &set, &refusal) ==
|
|
FT_TT_UDRV_EAGAIN,
|
|
"");
|
|
ft_tt_test_check_fmt("...as RESERVED", refusal.outcome == FT_TT_TENSIX_CLAIM_RESERVED,
|
|
"outcome=%d", (int)refusal.outcome);
|
|
}
|
|
(void)unsetenv(FT_TT_TENSIX_CLAIM_RESERVED_ENVIRONMENT);
|
|
}
|
|
|
|
/* Recovery-on-acquire bookkeeping for stage 7. */
|
|
static uint32_t g_retired[8];
|
|
static size_t g_retired_count;
|
|
static uint32_t g_retire_fails_on = UINT32_MAX;
|
|
|
|
static int record_retirement(void *context, uint32_t core_index)
|
|
{
|
|
(void)context;
|
|
if (g_retired_count < 8u)
|
|
g_retired[g_retired_count++] = core_index;
|
|
return (core_index == g_retire_fails_on) ? -7 : 0;
|
|
}
|
|
|
|
static void stage_recovery_on_acquire(void)
|
|
{
|
|
static const uint32_t wanted[] = {50u, 51u, 52u};
|
|
ft_tt_tensix_claim_request request;
|
|
ft_tt_tensix_claim_refusal refusal;
|
|
ft_tt_tensix_claim_set *set = NULL;
|
|
|
|
ft_tt_test_step("Stage 7 — recovery-on-acquire: a released claim is not a retired core");
|
|
|
|
g_retired_count = 0u;
|
|
g_retire_fails_on = UINT32_MAX;
|
|
build_request(&request, wanted, 3u, "retire ok");
|
|
request.retire = record_retirement;
|
|
|
|
ft_tt_test_check("the set is granted",
|
|
ft_tt_tensix_claim_acquire(&request, &set, &refusal) == FT_TT_UDRV_OK, "");
|
|
ft_tt_test_check_fmt("every claimed core was offered for retirement, in order",
|
|
g_retired_count == 3u && g_retired[0] == 50u && g_retired[1] == 51u &&
|
|
g_retired[2] == 52u,
|
|
"%zu retirements", g_retired_count);
|
|
ft_tt_tensix_claim_release(set);
|
|
|
|
/* A retirement that refuses must release the WHOLE set — a job that cannot
|
|
* get one core into a known state must not run on the others. */
|
|
g_retired_count = 0u;
|
|
g_retire_fails_on = 51u;
|
|
ft_tt_test_check("a set whose middle core cannot be retired is refused",
|
|
ft_tt_tensix_claim_acquire(&request, &set, &refusal) == FT_TT_UDRV_EAGAIN,
|
|
"");
|
|
ft_tt_test_check_fmt("...reported as UNRETIRED, naming that core",
|
|
refusal.outcome == FT_TT_TENSIX_CLAIM_UNRETIRED && refusal.index == 51u,
|
|
"outcome=%d index=%u", (int)refusal.outcome, refusal.index);
|
|
ft_tt_test_check("...and ALL THREE locks are back, not just the failing one",
|
|
lock_file_is_free(50u) && lock_file_is_free(51u) && lock_file_is_free(52u),
|
|
"checked by flock");
|
|
ft_tt_test_check_fmt("...and retirement stopped at the refusal", g_retired_count == 2u,
|
|
"%zu retirements", g_retired_count);
|
|
g_retire_fails_on = UINT32_MAX;
|
|
}
|
|
|
|
/**
|
|
* The hazard this project actually has: a harness that `_Exit()`s.
|
|
*
|
|
* `_Exit` runs no atexit handler and no destructor, so nothing of ours releases
|
|
* anything — and the claim comes back anyway, because the kernel closes the
|
|
* descriptor. That is the property flock was chosen for. It is also exactly why
|
|
* `retire` has to exist: the LOCK came back and the CORE did not.
|
|
*/
|
|
static void stage_exit_without_release(void)
|
|
{
|
|
static uint32_t index = 60u;
|
|
ft_tt_tensix_claim_request request;
|
|
ft_tt_tensix_claim_refusal refusal;
|
|
ft_tt_tensix_claim_set *set = NULL;
|
|
pid_t child;
|
|
int status = 0;
|
|
|
|
ft_tt_test_step("Stage 8 — a harness that _Exit()s: the LOCK returns, the CORE does not");
|
|
|
|
child = fork();
|
|
if (child == 0) {
|
|
ft_tt_tensix_claim_request child_request;
|
|
ft_tt_tensix_claim_refusal child_refusal;
|
|
ft_tt_tensix_claim_set *child_set = NULL;
|
|
|
|
build_request(&child_request, &index, 1u, "harness that _Exit()s");
|
|
if (ft_tt_tensix_claim_acquire(&child_request, &child_set, &child_refusal) !=
|
|
FT_TT_UDRV_OK)
|
|
_exit(3);
|
|
_exit(0); /* no release, no cleanup, no unwind */
|
|
}
|
|
if (child < 0) {
|
|
ft_tt_test_check("fork succeeded", false, "fork failed");
|
|
return;
|
|
}
|
|
(void)waitpid(child, &status, 0);
|
|
ft_tt_test_check_fmt("the child claimed and then _Exit()ed",
|
|
WIFEXITED(status) && WEXITSTATUS(status) == 0, "status %d", status);
|
|
|
|
ft_tt_test_check("the lock came back with no cleanup code of ours running",
|
|
lock_file_is_free(60u), "the kernel closed the descriptor");
|
|
|
|
g_retired_count = 0u;
|
|
g_retire_fails_on = UINT32_MAX;
|
|
build_request(&request, &index, 1u, "the next claimant");
|
|
request.retire = record_retirement;
|
|
ft_tt_test_check("the next claimant gets the core",
|
|
ft_tt_tensix_claim_acquire(&request, &set, &refusal) == FT_TT_UDRV_OK, "");
|
|
ft_tt_test_check_fmt("...and is made to retire it before using it — that is the whole point",
|
|
g_retired_count == 1u && g_retired[0] == 60u, "%zu retirements",
|
|
g_retired_count);
|
|
ft_tt_tensix_claim_release(set);
|
|
}
|
|
|
|
/**
|
|
* A whole-card operation must be able to SEE per-core claims before it resets.
|
|
*
|
|
* The card lock only ever saw processes that registered a shared claim, so once
|
|
* ordinary work moved to per-core claims a reset could report "nobody is using
|
|
* the card" while wiping eight live suites. `ft_tt_core` closes that with a
|
|
* census over this namespace — and the census is only true if it resolves the
|
|
* root and card exactly as a claimant does, which is what this stage pins.
|
|
*/
|
|
static void stage_card_wide_census_sees_core_claims(void)
|
|
{
|
|
static const uint32_t wanted[] = {33u};
|
|
ft_tt_tensix_claim_request request;
|
|
ft_tt_tensix_claim_refusal refusal;
|
|
ft_tt_tensix_claim_set *set = NULL;
|
|
ft_tt_udrv_claim_namespace census;
|
|
ft_tt_udrv_claim_info *info;
|
|
uint32_t held = 0u;
|
|
|
|
ft_tt_test_step("Stage 9 — a card-wide operation can see, and name, per-core claims");
|
|
|
|
(void)setenv(FT_TT_TENSIX_CLAIM_ROOT_ENVIRONMENT, scratch_root, 1);
|
|
/* NULL-guarded: an unset root is a LEGAL answer from this resolver (it means
|
|
* "ft_tt_udrv_claim's own default"), so a mutation that stops reading the
|
|
* environment must go red here rather than segfault a stage later. */
|
|
{
|
|
const char *resolved = ft_tt_tensix_claim_resolve_root(NULL);
|
|
|
|
ft_tt_test_check_fmt("the root resolver honours the environment",
|
|
resolved != NULL && strcmp(resolved, scratch_root) == 0, "%s",
|
|
resolved ? resolved : "(null)");
|
|
if (!resolved) {
|
|
(void)unsetenv(FT_TT_TENSIX_CLAIM_ROOT_ENVIRONMENT);
|
|
return; /* the census below would coordinate outside the scratch root */
|
|
}
|
|
}
|
|
ft_tt_test_check("an explicit root still wins over the environment",
|
|
strcmp(ft_tt_tensix_claim_resolve_root("/explicit"), "/explicit") == 0, "");
|
|
ft_tt_test_check("the card resolver falls back to the single-card default",
|
|
strcmp(ft_tt_tensix_claim_resolve_card(NULL),
|
|
FT_TT_TENSIX_CLAIM_CARD_DEFAULT) == 0,
|
|
"");
|
|
|
|
memset(&request, 0, sizeof(request));
|
|
request.root = NULL; /* NULL is what makes the resolver load-bearing here */
|
|
request.card = TEST_CARD;
|
|
request.indices = wanted;
|
|
request.count = 1u;
|
|
request.reason = "a live suite";
|
|
request.reserved = k_no_reserved;
|
|
|
|
ft_tt_test_check("a core is claimed through the resolved root",
|
|
ft_tt_tensix_claim_acquire(&request, &set, &refusal) == FT_TT_UDRV_OK, "");
|
|
|
|
/* The census, built the way ft_tt_core builds it — a SEPARATE namespace
|
|
* object over the lower layer, not a second call into ft_tt_tensix_claim. */
|
|
(void)ft_tt_udrv_claim_namespace_init(&census, ft_tt_tensix_claim_resolve_root(NULL),
|
|
ft_tt_tensix_claim_resolve_card(TEST_CARD),
|
|
FT_TT_TENSIX_CLAIM_NAMESPACE,
|
|
FT_TT_TENSIX_CLAIM_INDEX_COUNT);
|
|
info = calloc((size_t)census.count, sizeof(*info));
|
|
if (!info) {
|
|
ft_tt_test_check("census buffer allocates", false, "out of memory");
|
|
ft_tt_tensix_claim_release(set);
|
|
return;
|
|
}
|
|
(void)ft_tt_udrv_claim_scan(&census, info, (size_t)census.count, &held);
|
|
|
|
ft_tt_test_check_fmt("the census sees exactly the one claimed core", held == 1u, "%u held",
|
|
held);
|
|
ft_tt_test_check_fmt("...at the right index, naming this process",
|
|
info[33].held && info[33].pid == getpid(), "index 33 pid %ld",
|
|
(long)info[33].pid);
|
|
ft_tt_test_check_fmt("...with the reason it registered",
|
|
strcmp(info[33].reason, "a live suite") == 0, "%s", info[33].reason);
|
|
|
|
free(info);
|
|
ft_tt_tensix_claim_release(set);
|
|
(void)unsetenv(FT_TT_TENSIX_CLAIM_ROOT_ENVIRONMENT);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
build_scratch_root();
|
|
/* Nothing here may inherit the operator's reserved set: a suite whose result
|
|
* depends on the shell it was launched from is a suite that lies. */
|
|
(void)unsetenv(FT_TT_TENSIX_CLAIM_RESERVED_ENVIRONMENT);
|
|
|
|
stage_parse_index_lists();
|
|
stage_set_ownership();
|
|
stage_bad_requests();
|
|
stage_no_half_claim();
|
|
stage_real_race();
|
|
stage_reserved_set();
|
|
stage_recovery_on_acquire();
|
|
stage_exit_without_release();
|
|
stage_card_wide_census_sees_core_claims();
|
|
|
|
remove_scratch_root();
|
|
return ft_tt_test_finish();
|
|
}
|