371 lines
11 KiB
C
371 lines
11 KiB
C
// Dynamic test runner for every executable in the runner directory.
|
|
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
#include <ntlite/syscall.h>
|
|
#include <ntlite/path.h>
|
|
#include <ntlite/process_loader.h>
|
|
|
|
#define ALL_TESTS_INITIAL_CAPACITY 32u
|
|
|
|
typedef struct TEST_ENTRY {
|
|
char Name[SYS_DIR_NAME_MAX];
|
|
} TEST_ENTRY;
|
|
|
|
static void print(const char* s) {
|
|
SYS_WRITE_CONSOLE_REQUEST req = { sizeof(SYS_WRITE_CONSOLE_REQUEST), SysSvFromCstr(s) };
|
|
SysWriteConsole(&req);
|
|
}
|
|
|
|
static void print_u64(uint64_t v) {
|
|
char buf[32];
|
|
int i = 0;
|
|
if (v == 0) {
|
|
print("0");
|
|
return;
|
|
}
|
|
while (v && i < (int)sizeof(buf)) {
|
|
buf[i++] = (char)('0' + (v % 10));
|
|
v /= 10;
|
|
}
|
|
while (i--) {
|
|
char c[2] = { buf[i], 0 };
|
|
print(c);
|
|
}
|
|
}
|
|
|
|
static void print_i32(int32_t v) {
|
|
if (v < 0) {
|
|
print("-");
|
|
print_u64((uint32_t)(-(v + 1)) + 1u);
|
|
return;
|
|
}
|
|
print_u64((uint32_t)v);
|
|
}
|
|
|
|
static void print_hex32(uint32_t v) {
|
|
static const char hex[] = "0123456789abcdef";
|
|
for (int i = 7; i >= 0; i--) {
|
|
char c[2] = { hex[(v >> (i * 4)) & 0xfu], 0 };
|
|
print(c);
|
|
}
|
|
}
|
|
|
|
// Exits the process without returning to the runtime.
|
|
static void exit_now(int code) {
|
|
while (true) SysExitProcess(code);
|
|
}
|
|
|
|
// Converts ASCII to lowercase for stable case-insensitive comparisons.
|
|
static char ascii_lower(char c) {
|
|
if (c >= 'A' && c <= 'Z') return (char)(c - 'A' + 'a');
|
|
return c;
|
|
}
|
|
|
|
// Compares two names case-insensitively, with a case-sensitive tie-breaker.
|
|
static int compare_names(const char* a, const char* b) {
|
|
size_t i = 0;
|
|
while (true) {
|
|
char ca = ascii_lower(a[i]);
|
|
char cb = ascii_lower(b[i]);
|
|
if (ca != cb) return (unsigned char)ca < (unsigned char)cb ? -1 : 1;
|
|
if (ca == '\0') break;
|
|
i++;
|
|
}
|
|
return strcmp(a, b);
|
|
}
|
|
|
|
// Returns whether one character is a path separator.
|
|
static bool is_separator(char c) {
|
|
return c == '/' || c == '\\';
|
|
}
|
|
|
|
// Copies a SYS_STRING_VIEW into a NUL-terminated fixed buffer.
|
|
static bool copy_view(char* out, size_t outSize, SYS_STRING_VIEW view) {
|
|
if (!out || outSize == 0 || !view.Data || view.Length + 1 > outSize) return false;
|
|
memcpy(out, view.Data, view.Length);
|
|
out[view.Length] = '\0';
|
|
return true;
|
|
}
|
|
|
|
// Splits an executable path into its directory and basename components.
|
|
static bool split_self_path(const char* path, char* dir, size_t dirSize, char* name, size_t nameSize) {
|
|
if (!path || !dir || !name || dirSize == 0 || nameSize == 0) return false;
|
|
|
|
size_t len = strlen(path);
|
|
size_t last = (size_t)-1;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (is_separator(path[i])) last = i;
|
|
}
|
|
|
|
const char* base = path;
|
|
size_t dirLen = 1;
|
|
if (last == (size_t)-1) {
|
|
if (dirSize < 2) return false;
|
|
dir[0] = '.';
|
|
dir[1] = '\0';
|
|
} else {
|
|
dirLen = last;
|
|
if (last == 0) dirLen = 1;
|
|
if (last >= 2 && path[1] == ':' && is_separator(path[2])) dirLen = last + 1;
|
|
if (dirLen + 1 > dirSize) return false;
|
|
memcpy(dir, path, dirLen);
|
|
dir[dirLen] = '\0';
|
|
base = path + last + 1;
|
|
}
|
|
|
|
size_t baseLen = strlen(base);
|
|
if (baseLen + 1 > nameSize) return false;
|
|
memcpy(name, base, baseLen + 1);
|
|
return true;
|
|
}
|
|
|
|
// Returns whether one filename ends in .exe, case-insensitively.
|
|
static bool has_exe_suffix(const char* name) {
|
|
size_t len = strlen(name);
|
|
if (len < 4) return false;
|
|
const char* ext = name + len - 4;
|
|
return ascii_lower(ext[0]) == '.' &&
|
|
ascii_lower(ext[1]) == 'e' &&
|
|
ascii_lower(ext[2]) == 'x' &&
|
|
ascii_lower(ext[3]) == 'e';
|
|
}
|
|
|
|
// Allocates a test-entry array from user VM.
|
|
static uint32_t alloc_entries(uint32_t capacity, TEST_ENTRY** entriesOut) {
|
|
void* base = 0;
|
|
size_t bytes = (size_t)capacity * sizeof(TEST_ENTRY);
|
|
size_t pages = (bytes + 4095u) / 4096u;
|
|
if (pages == 0) pages = 1;
|
|
|
|
SYS_ALLOC_VM_REQUEST req;
|
|
memset(&req, 0, sizeof(req));
|
|
req.Size = sizeof(req);
|
|
req.Protect = SYS_VM_READWRITE;
|
|
req.Flags = SYS_VM_FLAG_NONE;
|
|
req.PageCount = pages;
|
|
req.BaseOut = &base;
|
|
|
|
KSTATUS status = SysAllocVm(&req);
|
|
if (status != NTLSTATUS_SUCCESS || !base) {
|
|
*entriesOut = 0;
|
|
return status ? status : NTLSTATUS_NO_MEMORY;
|
|
}
|
|
|
|
*entriesOut = (TEST_ENTRY*)base;
|
|
return NTLSTATUS_SUCCESS;
|
|
}
|
|
|
|
// Grows the test-entry array.
|
|
static uint32_t grow_entries(TEST_ENTRY** entries, uint32_t* capacity, uint32_t count) {
|
|
uint32_t oldCapacity = *capacity;
|
|
uint32_t newCapacity = oldCapacity ? oldCapacity * 2u : ALL_TESTS_INITIAL_CAPACITY;
|
|
TEST_ENTRY* next = 0;
|
|
KSTATUS status = alloc_entries(newCapacity, &next);
|
|
if (status != NTLSTATUS_SUCCESS) return status;
|
|
|
|
if (*entries && count) memcpy(next, *entries, (size_t)count * sizeof(TEST_ENTRY));
|
|
if (*entries) SysFreeVm(*entries);
|
|
*entries = next;
|
|
*capacity = newCapacity;
|
|
return NTLSTATUS_SUCCESS;
|
|
}
|
|
|
|
// Sorts test entries in deterministic execution order.
|
|
static void sort_entries(TEST_ENTRY* entries, uint32_t count) {
|
|
for (uint32_t i = 1; i < count; i++) {
|
|
TEST_ENTRY current = entries[i];
|
|
uint32_t j = i;
|
|
while (j > 0 && compare_names(current.Name, entries[j - 1].Name) < 0) {
|
|
entries[j] = entries[j - 1];
|
|
j--;
|
|
}
|
|
entries[j] = current;
|
|
}
|
|
}
|
|
|
|
// Builds one child path from the runner directory and test filename.
|
|
static bool build_child_path(const char* dir, const char* name, char* out, size_t outSize) {
|
|
if (!dir || !name || !out || outSize == 0) return false;
|
|
size_t dirLen = strlen(dir);
|
|
size_t nameLen = strlen(name);
|
|
bool needSep = dirLen > 0 && !is_separator(dir[dirLen - 1]);
|
|
size_t total = dirLen + (needSep ? 1u : 0u) + nameLen;
|
|
if (total + 1 > outSize) return false;
|
|
|
|
memcpy(out, dir, dirLen);
|
|
size_t pos = dirLen;
|
|
if (needSep) out[pos++] = '/';
|
|
memcpy(out + pos, name, nameLen);
|
|
out[total] = '\0';
|
|
return true;
|
|
}
|
|
|
|
// Collects executable tests from the runner directory.
|
|
static uint32_t collect_tests(const char* dir, const char* selfName, TEST_ENTRY** entriesOut, uint32_t* countOut) {
|
|
*entriesOut = 0;
|
|
*countOut = 0;
|
|
|
|
uint32_t hDir = 0;
|
|
KSTATUS status = NtlOpenDir(SysSvFromCstr(dir),
|
|
SYS_DIR_ACCESS_ENUMERATE | SYS_DIR_ACCESS_OPEN,
|
|
SYS_DIR_FLAG_NONE,
|
|
&hDir);
|
|
if (status != NTLSTATUS_SUCCESS) return status;
|
|
|
|
TEST_ENTRY* entries = 0;
|
|
uint32_t count = 0;
|
|
uint32_t capacity = 0;
|
|
|
|
while (true) {
|
|
SYS_DIR_ENTRY entry;
|
|
status = SysReadDir(hDir, &entry);
|
|
if (status != NTLSTATUS_SUCCESS) break;
|
|
if (!entry.Name[0]) continue;
|
|
if ((entry.Attr & 0x10u) != 0) continue;
|
|
if (!has_exe_suffix(entry.Name)) continue;
|
|
if (compare_names(entry.Name, selfName) == 0) continue;
|
|
|
|
if (count == capacity) {
|
|
status = grow_entries(&entries, &capacity, count);
|
|
if (status != NTLSTATUS_SUCCESS) {
|
|
SysCloseHandle(hDir);
|
|
if (entries) SysFreeVm(entries);
|
|
return status;
|
|
}
|
|
}
|
|
|
|
strncpy(entries[count].Name, entry.Name, sizeof(entries[count].Name) - 1);
|
|
entries[count].Name[sizeof(entries[count].Name) - 1] = '\0';
|
|
count++;
|
|
}
|
|
|
|
SysCloseHandle(hDir);
|
|
sort_entries(entries, count);
|
|
*entriesOut = entries;
|
|
*countOut = count;
|
|
return NTLSTATUS_SUCCESS;
|
|
}
|
|
|
|
// Runs one test process and returns the child's exit code through exitCodeOut.
|
|
static uint32_t run_test(const char* path, int32_t* exitCodeOut) {
|
|
uint32_t imageFile = SYS_HANDLE_INVALID;
|
|
KSTATUS status = NtlOpenFile(SysSvFromCstr(path),
|
|
SYS_FILE_ACCESS_READ | SYS_FILE_ACCESS_QUERY,
|
|
SYS_FILE_OPEN_EXISTING,
|
|
SYS_FILE_FLAG_NONE,
|
|
&imageFile);
|
|
if (status != NTLSTATUS_SUCCESS) return status;
|
|
|
|
uint32_t process = 0;
|
|
NTL_CREATE_SYSTEM_PROCESS_REQUEST req;
|
|
memset(&req, 0, sizeof(req));
|
|
req.Size = sizeof(req);
|
|
req.Flags = NTL_PROCESS_LOAD_FLAG_NONE;
|
|
req.ImageFile = imageFile;
|
|
req.Path = SysSvFromCstr(path);
|
|
req.CommandLine = SysSvFromCstr(path);
|
|
req.SecurityContext = SYS_SECURITY_CONTEXT_CURRENT;
|
|
req.ResolveModuleName = NtlResolveSystemModuleName;
|
|
req.ProcessHandleOut = &process;
|
|
|
|
status = NtlCreateSystemProcess(&req);
|
|
(void)SysCloseHandle(imageFile);
|
|
if (status != NTLSTATUS_SUCCESS) return status;
|
|
|
|
status = SysWait(process, SYS_WAIT_FLAG_NONE);
|
|
if (status != NTLSTATUS_SUCCESS) {
|
|
SysCloseHandle(process);
|
|
return status;
|
|
}
|
|
|
|
int32_t exitCode = 1;
|
|
status = SysGetProcessExitCode(process, &exitCode);
|
|
SysCloseHandle(process);
|
|
if (status != NTLSTATUS_SUCCESS) return status;
|
|
|
|
*exitCodeOut = exitCode;
|
|
return NTLSTATUS_SUCCESS;
|
|
}
|
|
|
|
int main(void) {
|
|
print("all_tests: start\n");
|
|
|
|
char selfPath[SYS_PATH_MAX];
|
|
char selfDir[SYS_PATH_MAX];
|
|
char selfName[SYS_DIR_NAME_MAX];
|
|
SYS_PEB* peb = SysGetPeb();
|
|
if (!peb || peb->Magic != SYS_PEB_MAGIC || peb->Version != SYS_ENV_VERSION ||
|
|
!copy_view(selfPath, sizeof(selfPath), peb->ImagePath) ||
|
|
!split_self_path(selfPath, selfDir, sizeof(selfDir), selfName, sizeof(selfName))) {
|
|
print("all_tests: failed to resolve runner path\n");
|
|
exit_now(1);
|
|
}
|
|
|
|
TEST_ENTRY* entries = 0;
|
|
uint32_t count = 0;
|
|
KSTATUS status = collect_tests(selfDir, selfName, &entries, &count);
|
|
if (status != NTLSTATUS_SUCCESS) {
|
|
print("all_tests: failed to enumerate tests status=0x");
|
|
print_hex32(status);
|
|
print("\n");
|
|
exit_now(1);
|
|
}
|
|
|
|
if (count == 0) {
|
|
print("all_tests: no test executables found\n");
|
|
if (entries) SysFreeVm(entries);
|
|
exit_now(1);
|
|
}
|
|
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
char childPath[SYS_PATH_MAX];
|
|
if (!build_child_path(selfDir, entries[i].Name, childPath, sizeof(childPath))) {
|
|
print("all_tests: child path too long: ");
|
|
print(entries[i].Name);
|
|
print("\n");
|
|
SysFreeVm(entries);
|
|
exit_now(1);
|
|
}
|
|
|
|
print("all_tests: RUN ");
|
|
print(childPath);
|
|
print("\n");
|
|
|
|
int32_t exitCode = 1;
|
|
status = run_test(childPath, &exitCode);
|
|
if (status != NTLSTATUS_SUCCESS) {
|
|
print("all_tests: FAIL ");
|
|
print(childPath);
|
|
print(" launch/wait status=0x");
|
|
print_hex32(status);
|
|
print("\n");
|
|
SysFreeVm(entries);
|
|
exit_now(1);
|
|
}
|
|
|
|
if (exitCode != 0) {
|
|
print("all_tests: FAIL ");
|
|
print(childPath);
|
|
print(" exit=");
|
|
print_i32(exitCode);
|
|
print("\n");
|
|
SysFreeVm(entries);
|
|
exit_now(exitCode ? exitCode : 1);
|
|
}
|
|
|
|
print("all_tests: PASS ");
|
|
print(childPath);
|
|
print("\n");
|
|
}
|
|
|
|
print("all_tests: PASS count=");
|
|
print_u64(count);
|
|
print("\n");
|
|
SysFreeVm(entries);
|
|
exit_now(0);
|
|
return 0;
|
|
}
|