diff --git a/diagnostic/build-137421ad.json b/diagnostic/build-137421ad.json new file mode 100644 index 000000000..ec4cd71f5 --- /dev/null +++ b/diagnostic/build-137421ad.json @@ -0,0 +1,39 @@ +{ + "generated_at": "2026-06-20T21:31:15.481556+00:00", + "commit": "137421ad", + "change_commit": "137421ad578e7d080d714f1aeca6aa1331341e8e", + "base_commit": "d5241a4f6e76cb0bda32639d1f254aa06f967cf7", + "diagnostic_logd": "diagnostic/build-137421ad.logd", + "diagnostic_logd_error": null, + "chunked": false, + "chunk_size_bytes": null, + "password": "f7a896ad71390928e915", + "decrypt_command": "encryptly unpack diagnostic/build-137421ad.logd --password f7a896ad71390928e915", + "total_modules": 3, + "passed": 3, + "failed": 0, + "modules": [ + { + "name": "frailbox-selftest-json-fixtures", + "status": "PASS", + "elapsed_seconds": 0, + "artifact": "frailbox/build/frailbox_selftest", + "output": "make -C frailbox selftest-json-fixtures" + }, + { + "name": "frailbox-selftest-json-success", + "status": "PASS", + "elapsed_seconds": 0, + "artifact": null, + "output": "make -s -C frailbox selftest-json produced parseable JSON" + }, + { + "name": "frailbox-selftest-json-failure", + "status": "PASS", + "elapsed_seconds": 0, + "artifact": null, + "output": "failure fixture returns exit 1 and includes failure_reason" + } + ], + "pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-137421ad.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-137421ad.logd b/diagnostic/build-137421ad.logd new file mode 100644 index 000000000..677eadce8 Binary files /dev/null and b/diagnostic/build-137421ad.logd differ diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 58642e7b9..3e869df53 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -286,6 +286,12 @@ Audit logs are retained for 365 days and include: 3. Check database connectivity: `kubectl exec -n tent-production deploy/backend-api -- nc -zv postgresql 5432` 4. Check resource limits: `kubectl describe pod -n tent-production -l app=backend-api` +**Frailbox self-test JSON summary** +1. Run the human-readable self-test fixture: `make -C frailbox selftest`. +2. Emit a machine-readable summary for CI or operators: `make -s -C frailbox selftest-json`. +3. Validate success and failure fixture coverage: `make -s -C frailbox selftest-json-fixtures`. +4. JSON output includes each test name, `pass`/`fail` status, duration in milliseconds, and a failure reason when available. The self-test is deterministic and does not perform network access. + **High latency** 1. Check database query performance: `SELECT * FROM pg_stat_activity WHERE state = 'active'` 2. Check connection pool utilization diff --git a/frailbox/Makefile b/frailbox/Makefile index d4383d853..da5f97ec1 100644 --- a/frailbox/Makefile +++ b/frailbox/Makefile @@ -14,6 +14,7 @@ OBJS = $(patsubst %.c, $(BUILDDIR)/%.o, $(SRCS)) DEPS = $(OBJS:.o=.d) TARGET = frailbox +SELFTEST_TARGET = $(BUILDDIR)/frailbox_selftest .PHONY: all clean @@ -26,6 +27,10 @@ $(BUILDDIR)/%.o: %.c @mkdir -p $(dir $@) $(CC) $(CFLAGS) -I$(INCDIR) -MMD -MP -c $< -o $@ +$(SELFTEST_TARGET): tests/selftest_summary.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -I$(INCDIR) $< -o $@ $(LDFLAGS) + -include $(DEPS) clean: @@ -37,7 +42,19 @@ distclean: clean test: $(TARGET) ./$(TARGET) --sandbox-type seccomp --memory-limit 64 --verbose +selftest: $(SELFTEST_TARGET) + @./$(SELFTEST_TARGET) --fixture tests/fixtures/selftest_success.fixture + +selftest-json: $(SELFTEST_TARGET) + @./$(SELFTEST_TARGET) --json --fixture tests/fixtures/selftest_success.fixture + +selftest-json-fixtures: $(SELFTEST_TARGET) + @./$(SELFTEST_TARGET) --json --fixture tests/fixtures/selftest_success.fixture >/tmp/frailbox-selftest-success.json + @set +e; ./$(SELFTEST_TARGET) --json --fixture tests/fixtures/selftest_failure.fixture >/tmp/frailbox-selftest-failure.json; status=$$?; if [ "$$status" -ne 1 ]; then exit 1; fi + @grep -q '"status": "pass"' /tmp/frailbox-selftest-success.json + @grep -q '"status": "fail"' /tmp/frailbox-selftest-failure.json + valgrind: $(TARGET) valgrind --leak-check=full --show-leak-kinds=all ./$(TARGET) -.PHONY: all clean distclean test valgrind +.PHONY: all clean distclean test selftest selftest-json selftest-json-fixtures valgrind diff --git a/frailbox/tests/fixtures/selftest_failure.fixture b/frailbox/tests/fixtures/selftest_failure.fixture new file mode 100644 index 000000000..971d0c1a6 --- /dev/null +++ b/frailbox/tests/fixtures/selftest_failure.fixture @@ -0,0 +1,3 @@ +name=fixture_failure +expect=fail +reason=intentional failure fixture diff --git a/frailbox/tests/fixtures/selftest_success.fixture b/frailbox/tests/fixtures/selftest_success.fixture new file mode 100644 index 000000000..a6fe6bf12 --- /dev/null +++ b/frailbox/tests/fixtures/selftest_success.fixture @@ -0,0 +1,2 @@ +name=fixture_success +expect=pass diff --git a/frailbox/tests/selftest_summary.c b/frailbox/tests/selftest_summary.c new file mode 100644 index 000000000..fae79479d --- /dev/null +++ b/frailbox/tests/selftest_summary.c @@ -0,0 +1,242 @@ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include + +#define MAX_TESTS 16 +#define NAME_SIZE 96 +#define REASON_SIZE 256 +#define LINE_SIZE 512 + +typedef struct selftest_result { + char name[NAME_SIZE]; + int passed; + double duration_ms; + char failure_reason[REASON_SIZE]; +} selftest_result_t; + +static double now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1000000.0; +} + +static void copy_string(char *dst, size_t dst_size, const char *src) { + if (dst_size == 0) { + return; + } + snprintf(dst, dst_size, "%s", src ? src : ""); +} + +static char *trim(char *value) { + while (*value && isspace((unsigned char)*value)) { + value++; + } + + char *end = value + strlen(value); + while (end > value && isspace((unsigned char)*(end - 1))) { + end--; + } + *end = '\0'; + return value; +} + +static void json_string(FILE *out, const char *value) { + fputc('"', out); + for (const unsigned char *p = (const unsigned char *)(value ? value : ""); *p; p++) { + switch (*p) { + case '\\': + fputs("\\\\", out); + break; + case '"': + fputs("\\\"", out); + break; + case '\n': + fputs("\\n", out); + break; + case '\r': + fputs("\\r", out); + break; + case '\t': + fputs("\\t", out); + break; + default: + if (*p < 0x20) { + fprintf(out, "\\u%04x", *p); + } else { + fputc(*p, out); + } + break; + } + } + fputc('"', out); +} + +static selftest_result_t run_memory_smoke(void) { + selftest_result_t result = {0}; + double start = now_ms(); + copy_string(result.name, sizeof(result.name), "memory_smoke"); + result.passed = 1; + + unsigned char *buffer = calloc(32, 1); + if (!buffer) { + result.passed = 0; + copy_string(result.failure_reason, sizeof(result.failure_reason), "calloc returned NULL"); + } else { + for (size_t i = 0; i < 32; i++) { + buffer[i] = (unsigned char)i; + } + for (size_t i = 0; i < 32; i++) { + if (buffer[i] != (unsigned char)i) { + result.passed = 0; + copy_string(result.failure_reason, sizeof(result.failure_reason), "memory roundtrip mismatch"); + break; + } + } + free(buffer); + } + + result.duration_ms = now_ms() - start; + return result; +} + +static selftest_result_t run_fixture(const char *path) { + selftest_result_t result = {0}; + double start = now_ms(); + copy_string(result.name, sizeof(result.name), "fixture"); + result.passed = 0; + + FILE *file = fopen(path, "r"); + if (!file) { + snprintf(result.failure_reason, sizeof(result.failure_reason), + "could not open fixture: %s", strerror(errno)); + result.duration_ms = now_ms() - start; + return result; + } + + char expected[32] = ""; + char line[LINE_SIZE]; + while (fgets(line, sizeof(line), file)) { + char *clean = trim(line); + if (*clean == '\0' || *clean == '#') { + continue; + } + char *equals = strchr(clean, '='); + if (!equals) { + continue; + } + *equals = '\0'; + char *key = trim(clean); + char *value = trim(equals + 1); + if (strcmp(key, "name") == 0) { + copy_string(result.name, sizeof(result.name), value); + } else if (strcmp(key, "expect") == 0) { + copy_string(expected, sizeof(expected), value); + } else if (strcmp(key, "reason") == 0) { + copy_string(result.failure_reason, sizeof(result.failure_reason), value); + } + } + fclose(file); + + if (strcmp(expected, "pass") == 0) { + result.passed = 1; + result.failure_reason[0] = '\0'; + } else if (strcmp(expected, "fail") == 0) { + result.passed = 0; + if (result.failure_reason[0] == '\0') { + copy_string(result.failure_reason, sizeof(result.failure_reason), "fixture requested failure"); + } + } else { + result.passed = 0; + copy_string(result.failure_reason, sizeof(result.failure_reason), "fixture missing expect=pass or expect=fail"); + } + + result.duration_ms = now_ms() - start; + return result; +} + +static void print_text_summary(const selftest_result_t *results, size_t count) { + size_t passed = 0; + for (size_t i = 0; i < count; i++) { + passed += results[i].passed ? 1U : 0U; + printf("%s: %s (%.3fms)", results[i].name, + results[i].passed ? "PASS" : "FAIL", + results[i].duration_ms); + if (!results[i].passed && results[i].failure_reason[0] != '\0') { + printf(" - %s", results[i].failure_reason); + } + putchar('\n'); + } + printf("RESULTS: %zu passed, %zu failed out of %zu\n", passed, count - passed, count); +} + +static void print_json_summary(const selftest_result_t *results, size_t count) { + size_t passed = 0; + for (size_t i = 0; i < count; i++) { + passed += results[i].passed ? 1U : 0U; + } + + printf("{\n"); + printf(" \"total\": %zu,\n", count); + printf(" \"passed\": %zu,\n", passed); + printf(" \"failed\": %zu,\n", count - passed); + printf(" \"tests\": [\n"); + for (size_t i = 0; i < count; i++) { + printf(" {\n"); + printf(" \"name\": "); + json_string(stdout, results[i].name); + printf(",\n"); + printf(" \"status\": \"%s\",\n", results[i].passed ? "pass" : "fail"); + printf(" \"duration_ms\": %.3f,\n", results[i].duration_ms); + printf(" \"failure_reason\": "); + if (results[i].passed) { + printf("null\n"); + } else { + json_string(stdout, results[i].failure_reason); + putchar('\n'); + } + printf(" }%s\n", i + 1 == count ? "" : ","); + } + printf(" ]\n"); + printf("}\n"); +} + +int main(int argc, char **argv) { + int json = 0; + const char *fixture = NULL; + selftest_result_t results[MAX_TESTS]; + size_t count = 0; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--json") == 0) { + json = 1; + } else if (strcmp(argv[i], "--fixture") == 0 && i + 1 < argc) { + fixture = argv[++i]; + } else { + fprintf(stderr, "usage: %s [--json] [--fixture PATH]\n", argv[0]); + return 2; + } + } + + results[count++] = run_memory_smoke(); + if (fixture) { + results[count++] = run_fixture(fixture); + } + + if (json) { + print_json_summary(results, count); + } else { + print_text_summary(results, count); + } + + for (size_t i = 0; i < count; i++) { + if (!results[i].passed) { + return 1; + } + } + return 0; +}