diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 03323cf..bc52b29 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -110,6 +110,15 @@ {"id":"REQ-COMP-TEXT001","title":"cfusa comp text output shows 'Total functions: N Exceeding threshold: N' summary; parity with go-FuSa TestRunComp_Text_NoExceedances","standard":"go-FuSa parity","level":"ASIL-A"}, {"id":"REQ-CLI-REQ002","title":"cfusa req import exits 2 for empty CSV or bad header, exits 3 for file not found; parity with go-FuSa TestRunReqImport_CSVEmptyFile/CSVBadHeader/CSVReadError","standard":"go-FuSa parity","level":"ASIL-A"}, {"id":"REQ-CLI-FIX001","title":"cfusa fix accepts --report flag and writes JSON findings report; parity with go-FuSa TestRunFix_WithFindingsAndOutput","standard":"go-FuSa parity","level":"ASIL-A"}, - {"id":"REQ-IEC62443-HEADER001","title":"cfusa iec62443 text header is 'IEC 62443 Gap Report' (contains canonical substring); parity with go-FuSa TestRunIEC62443_TextDefault","standard":"go-FuSa parity","level":"ASIL-A"} + {"id":"REQ-IEC62443-HEADER001","title":"cfusa iec62443 text header is 'IEC 62443 Gap Report' (contains canonical substring); parity with go-FuSa TestRunIEC62443_TextDefault","standard":"go-FuSa parity","level":"ASIL-A"}, + {"id":"REQ-HLR001","title":"cfusa trace --strict-hlr-llr flag exits 1 when any HLR/LLR decomposition violations are found; parent_id field in requirement struct links LLR to HLR","standard":"ISO 26262","level":"HLR"}, + {"id":"REQ-HLR002","title":"cfusa trace validates every LLR references an existing HLR via parentId; orphaned LLRs (empty or invalid parentId) are reported","standard":"ISO 26262","level":"HLR"}, + {"id":"REQ-HLR003","title":"cfusa trace validates every HLR has at least one LLR child; uncovered HLRs are reported","standard":"ISO 26262","level":"HLR"}, + {"id":"REQ-HLR004","title":"cfusa trace JSON output includes hlrllrSummary and parentId field on LLR requirements; text output shows HLR/LLR summary line","standard":"ISO 26262","level":"LLR"}, + {"id":"REQ-QUAL003","title":"cfusa qualify --qualification-method accepts self|independent; --qualifier sets qualifier identity; --record-uri sets qualification record URI","standard":"ISO 26262","level":"HLR"}, + {"id":"REQ-QUAL006","title":"cfusa qualify shows qualification badge: independently-qualified, self-qualified, or unqualified based on --qualification-method","standard":"ISO 26262","level":"HLR"}, + {"id":"REQ-VV001","title":"cfusa qualify --implementation-author, --independent-reviewer, --independent-test-executor, --achievable-asil flags store V&V independence metadata","standard":"ISO 26262","level":"HLR"}, + {"id":"REQ-VV004","title":"cfusa qualify independence status is independent when reviewer differs from author; self-reviewed when same; unqualified when no reviewer","standard":"ISO 26262","level":"LLR"}, + {"id":"REQ-COV015","title":"cfusa coverage --mcdc-file parses LLVM MC/DC JSON; condition covered iff covered_true_count>0 AND covered_false_count>0; gate fails when coverage below --mcdc-threshold","standard":"DO-178C","level":"HLR"} ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index eb77a61..32cd3f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,42 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0. ## [Unreleased] +## v0.5.35 — 2026-07-26 + +### Added +- **Feature 1 — HLR/LLR hierarchical traceability** (`cmd_trace.c`): `parent_id` field in + `req_t` struct; `compute_hlr_llr()` function detecting orphaned LLRs (missing/invalid + `parentId`) and uncovered HLRs (no LLR children). CLI `--strict-hlr-llr` flag gates on + any violation (exit 1). Text renderer shows `HLR/LLR: N HLRs N LLRs` summary line. + JSON renderer adds `hlrllrSummary` object and emits `parentId` field on LLR requirements. + Markdown renderer adds HLR/LLR summary table rows. 8 new tests in `test_hlr_llr.c`. + (REQ-HLR001, REQ-HLR002, REQ-HLR003, REQ-HLR004; closes #48) +- **Feature 2 — Tool qualification display** (`cmd_qualify.c`): `--qualification-method` + (self|independent), `--qualifier`, `--record-uri` CLI flags. `qualification_badge()` + helper returns "independently-qualified", "self-qualified", or "unqualified". Badge and + fields shown in text and JSON output. 9 new tests in `test_qualify_vv.c` covering + Features 2 and 4. (REQ-QUAL003, REQ-QUAL006; closes #49) +- **Feature 3 — MC/DC coverage measurement** (`cmd_coverage.c`): `--mcdc-file` (path to + LLVM coverage JSON export) and `--mcdc-threshold` (0–100, default 100) CLI flags. + `parse_mcdc_json()` parses LLVM `mcdc_records`/`conditions` format. A condition is MC/DC + covered when `covered_true_count > 0 AND covered_false_count > 0`. Gate fails when overall + coverage falls below threshold. MC/DC-file-only mode skips lcov auto-detection. JSON output + adds `mcdcReport` object. 8 new tests in `test_mcdc.c`. (REQ-COV015; closes #50) +- **Feature 4 — V&V independence declaration** (`cmd_qualify.c`): `--implementation-author`, + `--independent-reviewer`, `--independent-test-executor`, `--achievable-asil` CLI flags. + `independence_status()` helper returns "independent" when reviewer differs from author, + "self-reviewed" when they are the same, "unqualified" when no reviewer is set. Fields + included in JSON output. (REQ-VV001, REQ-VV004; closes #51) +- 9 new requirements registered in `.fusa-reqs.json`: + REQ-HLR001–004, REQ-QUAL003, REQ-QUAL006, REQ-VV001, REQ-VV004, REQ-COV015. +- 3 new test files: `tests/test_hlr_llr.c` (8 tests), `tests/test_qualify_vv.c` (9 tests), + `tests/test_mcdc.c` (8 tests). Total tests: 37 (was 34). + +### Fixed +- `cmd_coverage`: Added macOS/BSD `optreset = 1` alongside `optind = 1` to ensure proper + getopt state reset between multiple calls in the same process. +- `cmd_qualify`: Same `optreset` fix. + ## v0.5.34 — 2026-07-25 - Fix CFUSA_SCHEMA_VERSION and CFUSA_SPEC_VERSION from "1.9" to "1.10.4" diff --git a/CMakeLists.txt b/CMakeLists.txt index 5253e60..04ba70b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) project(cfusa - VERSION 0.5.33 + VERSION 0.5.35 DESCRIPTION "C functional safety toolkit" LANGUAGES C ) @@ -152,6 +152,9 @@ cfusa_test(test_req_trace_cov) cfusa_test(test_new_commands) cfusa_test(test_safety_rules) cfusa_test(test_runtime) +cfusa_test(test_hlr_llr) +cfusa_test(test_qualify_vv) +cfusa_test(test_mcdc) # ---- Installation ---- include(GNUInstallDirs) diff --git a/cmd/cfusa/cmd_coverage.c b/cmd/cfusa/cmd_coverage.c index bcf0630..989dde5 100644 --- a/cmd/cfusa/cmd_coverage.c +++ b/cmd/cfusa/cmd_coverage.c @@ -1,3 +1,6 @@ +#if defined(__linux__) || defined(__unix__) +# define _GNU_SOURCE +#endif #include #include #include @@ -8,6 +11,93 @@ /* Parses lcov .info files for line, function, and branch coverage */ +/* ── MC/DC analysis (REQ-COV015) ──────────────────────────────────────────── */ + +typedef struct { + long total_conditions; + long covered_conditions; + double coverage_pct; + int passed; + char note[256]; +} mcdc_report_t; + +/* + * Parse LLVM MC/DC coverage JSON export. + * Format: {"data":[{"functions":[{"name":"...","mcdc_records":[{"conditions":[ + * {"covered_true_count":N,"covered_false_count":M},...]},...]},...]},...]} + * A condition is MC/DC covered iff covered_true_count > 0 AND covered_false_count > 0. + */ +//cfusa:req REQ-COV015 +static void parse_mcdc_json(const char *path, int threshold, mcdc_report_t *rep) +{ + rep->total_conditions = rep->covered_conditions = 0; + rep->coverage_pct = 100.0; + rep->passed = 1; + rep->note[0] = '\0'; + + size_t len; + char *json = cfusa_read_file(path, &len); + if (!json) { + snprintf(rep->note, sizeof(rep->note), + "cannot read MC/DC file: %s", path); + rep->passed = 0; + return; + } + + /* + * Walk all {"covered_true_count":N,"covered_false_count":M} objects. + * We scan for the key "covered_true_count" and then "covered_false_count" + * within the same braced object. + */ + const char *p = json; + while ((p = strstr(p, "\"covered_true_count\"")) != NULL) { + /* Extract the enclosing condition object — scan back to { */ + const char *obj_start = p; + /* Find the next closing brace for this condition object */ + const char *obj_end = strchr(p, '}'); + if (!obj_end) { p++; continue; } + + /* Extract covered_true_count */ + const char *tp = strchr(p, ':'); + long true_count = 0, false_count = 0; + if (tp && tp < obj_end) true_count = atol(tp + 1); + + /* Extract covered_false_count within the same object */ + const char *fp = strstr(p, "\"covered_false_count\""); + if (fp && fp < obj_end) { + fp = strchr(fp, ':'); + if (fp) false_count = atol(fp + 1); + } + + rep->total_conditions++; + if (true_count > 0 && false_count > 0) + rep->covered_conditions++; + + p = obj_end + 1; + (void)obj_start; + } + + free(json); + + if (rep->total_conditions == 0) { + snprintf(rep->note, sizeof(rep->note), + "no MC/DC records found in coverage export"); + rep->passed = 1; + return; + } + + rep->coverage_pct = (double)rep->covered_conditions + * 100.0 / (double)rep->total_conditions; + + int thr = (threshold <= 0) ? 100 : threshold; + rep->passed = (rep->coverage_pct >= (double)thr); + if (!rep->passed) { + snprintf(rep->note, sizeof(rep->note), + "MC/DC gate failed: %.1f%% covered (threshold %d%%)", + rep->coverage_pct, thr); + } +} + typedef struct { long lines_found; long lines_hit; @@ -95,24 +185,35 @@ int cmd_coverage(int argc, char **argv) int mcdc = 0; int mutate = 0; double mutate_score = -1.0; /* <0 = not provided */ + /* Feature 3 — MC/DC LLVM JSON (REQ-COV015) */ + //cfusa:req REQ-COV015 + const char *mcdc_file = NULL; + int mcdc_threshold = 100; static const struct option long_opts[] = { - {"dir", required_argument, NULL, 'd'}, - {"lcov", required_argument, NULL, 'L'}, - {"format", required_argument, NULL, 'f'}, - {"output", required_argument, NULL, 'o'}, - {"dal", required_argument, NULL, 'D'}, - {"threshold", required_argument, NULL, 't'}, - {"mcdc", no_argument, NULL, 'm'}, - {"mutate", no_argument, NULL, 'M'}, - {"mutate-score", required_argument, NULL, 'S'}, - {"help", no_argument, NULL, 'h'}, + {"dir", required_argument, NULL, 'd'}, + {"lcov", required_argument, NULL, 'L'}, + {"format", required_argument, NULL, 'f'}, + {"output", required_argument, NULL, 'o'}, + {"dal", required_argument, NULL, 'D'}, + {"threshold", required_argument, NULL, 't'}, + {"mcdc", no_argument, NULL, 'm'}, + {"mcdc-file", required_argument, NULL, 'C'}, /* REQ-COV015 */ + {"mcdc-threshold", required_argument, NULL, 'T'}, /* REQ-COV015 */ + {"mutate", no_argument, NULL, 'M'}, + {"mutate-score", required_argument, NULL, 'S'}, + {"help", no_argument, NULL, 'h'}, {NULL,0,NULL,0} }; int c; optind = 1; - while ((c = getopt_long(argc, argv, "d:L:f:o:D:t:mMS:h", long_opts, NULL)) != -1) { +#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + { extern int optreset; optreset = 1; } +#elif defined(__linux__) + optind = 0; /* glibc: reset nextchar so stale argv pointer is not followed */ +#endif + while ((c = getopt_long(argc, argv, "d:L:f:o:D:t:mC:T:MS:h", long_opts, NULL)) != -1) { switch (c) { case 'd': dir = optarg; break; case 'L': lcov_in = optarg; break; @@ -122,6 +223,9 @@ int cmd_coverage(int argc, char **argv) dal_explicit = 1; break; case 't': threshold = atof(optarg); break; case 'm': mcdc = 1; break; + case 'C': mcdc_file = optarg; + mcdc = 1; break; /* --mcdc-file implies --mcdc */ + case 'T': mcdc_threshold = atoi(optarg); break; case 'M': mutate = 1; break; case 'S': mutate_score = atof(optarg); mutate = 1; break; @@ -130,6 +234,7 @@ int cmd_coverage(int argc, char **argv) " [--format text|json] [--output ]\n" " [--dal DAL-A|DAL-B|DAL-C|DAL-D]\n" " [--threshold ] [--mcdc]\n" + " [--mcdc-file ] [--mcdc-threshold ]\n" " [--mutate] [--mutate-score ]\n\n" "Parses gcov/lcov output and reports statement, function, and\n" "branch coverage. --dal sets DO-178C level requirements:\n" @@ -138,6 +243,8 @@ int cmd_coverage(int argc, char **argv) " DAL-C: 100%% line (statement)\n" " DAL-D: no coverage threshold\n" "--mcdc flags decision coverage <100%%.\n" + "--mcdc-file parses an LLVM coverage JSON export for MC/DC analysis.\n" + "--mcdc-threshold N sets the minimum %% of conditions covered (default 100).\n" "--mutate reads mutation-report.json (or --mutate-score N) as\n" "MC/DC mutation-testing evidence for DO-178C DAL A/B.\n" "Generate lcov data with: lcov --capture --directory . -o coverage.info\n"); @@ -156,9 +263,11 @@ int cmd_coverage(int argc, char **argv) apply_dal(dal, &threshold, &threshold_branch, &mcdc); } - /* Locate lcov file if not specified */ + /* Locate lcov file if not specified. + * Skip auto-detection in MC/DC-file-only mode (REQ-COV015): when + * --mcdc-file is the sole input, lcov is not required. */ char auto_path[512]; - if (!lcov_in) { + if (!lcov_in && !mcdc_file) { cfusa_path_join(auto_path, sizeof(auto_path), dir, "coverage.info"); if (!cfusa_file_exists(auto_path)) { cfusa_path_join(auto_path, sizeof(auto_path), dir, "lcov.info"); @@ -195,6 +304,8 @@ int cmd_coverage(int argc, char **argv) if (!lcov_in || !cfusa_file_exists(lcov_in)) { if (mutate && mutate_score >= 0.0) { /* mutation-only mode: no lcov required */ + } else if (mcdc_file) { + /* REQ-COV015: MC/DC-file-only mode: no lcov required */ } else { fprintf(stderr, "cfusa coverage: no lcov .info file found.\n" " Generate with: lcov --capture --directory %s -o coverage.info\n" @@ -217,9 +328,18 @@ int cmd_coverage(int argc, char **argv) char ts[32]; cfusa_timestamp_now(ts); + /* Parse LLVM MC/DC JSON if --mcdc-file given (REQ-COV015) */ + mcdc_report_t mcdc_rep = {0, 0, 100.0, 1, ""}; + int have_mcdc_rep = 0; + if (mcdc_file) { + parse_mcdc_json(mcdc_file, mcdc_threshold, &mcdc_rep); + have_mcdc_rep = 1; + } + int line_pass = !lcov_in || line_pct >= threshold; int branch_pass = !lcov_in || threshold_branch <= 0.0 || branch_pct >= threshold_branch; int mcdc_pass = !mcdc || !lcov_in || branch_pct >= 100.0; + if (have_mcdc_rep) mcdc_pass = mcdc_rep.passed; /* LLVM MC/DC overrides branch proxy */ int mut_pass = !mutate || mutate_score < 0.0 || mutate_score >= 100.0; int overall_pass = line_pass && branch_pass && mcdc_pass && mut_pass; @@ -256,6 +376,27 @@ int cmd_coverage(int argc, char **argv) mutate_score, mutate_score >= 100.0 ? "true" : "false"); } + /* REQ-COV015: structured MC/DC report */ + if (have_mcdc_rep) { + fprintf(out_f, + ",\n" + " \"mcdcReport\": {\n" + " \"sourceFile\": \"%s\",\n" + " \"threshold\": %d,\n" + " \"totalConditions\": %ld,\n" + " \"coveredConditions\": %ld,\n" + " \"coveragePct\": %.2f,\n" + " \"passed\": %s", + mcdc_file ? mcdc_file : "", + mcdc_threshold, + mcdc_rep.total_conditions, + mcdc_rep.covered_conditions, + mcdc_rep.coverage_pct, + mcdc_rep.passed ? "true" : "false"); + if (mcdc_rep.note[0]) + fprintf(out_f, ",\n \"note\": \"%s\"", mcdc_rep.note); + fprintf(out_f, "\n }"); + } fprintf(out_f, "\n}\n"); } else { if (lcov_in) @@ -274,12 +415,23 @@ int cmd_coverage(int argc, char **argv) branch_pct, state.branches_hit, state.branches_found, branch_pass ? "PASS" : "FAIL"); } - if (mcdc && lcov_in) { + if (mcdc && lcov_in && !have_mcdc_rep) { fprintf(out_f, "\n MC/DC analysis: branch coverage %.2f%%", branch_pct); if (!mcdc_pass) fprintf(out_f, " [FAIL — DO-178C requires 100%%]"); fprintf(out_f, "\n"); } + /* REQ-COV015: LLVM MC/DC structured report */ + if (have_mcdc_rep) { + fprintf(out_f, "\n MC/DC coverage (LLVM): %.2f%%" + " (%ld/%ld conditions) [%s] (threshold: %d%%)\n", + mcdc_rep.coverage_pct, + mcdc_rep.covered_conditions, mcdc_rep.total_conditions, + mcdc_rep.passed ? "PASS" : "FAIL", + mcdc_threshold); + if (mcdc_rep.note[0]) + fprintf(out_f, " Note: %s\n", mcdc_rep.note); + } if (mutate && mutate_score >= 0.0) { fprintf(out_f, "\n Mutation score: %.2f%%", mutate_score); if (!mut_pass) diff --git a/cmd/cfusa/cmd_qualify.c b/cmd/cfusa/cmd_qualify.c index a26b82c..1a491a1 100644 --- a/cmd/cfusa/cmd_qualify.c +++ b/cmd/cfusa/cmd_qualify.c @@ -400,36 +400,102 @@ static fusa_case_t g_cases[] = { {NULL, NULL, NULL, 0, NULL, 0} }; +/* ── Qualification badge and independence helpers (REQ-QUAL003, REQ-VV001) ── */ + +//cfusa:req REQ-QUAL006 +static const char *qualification_badge(const char *method) +{ + if (!method || !method[0]) return "unqualified"; + if (!strcmp(method, "independent")) return "independently-qualified"; + if (!strcmp(method, "self")) return "self-qualified"; + return "unqualified"; +} + +//cfusa:req REQ-VV004 +static const char *independence_status(const char *author, const char *reviewer) +{ + if (!reviewer || !reviewer[0]) return "unqualified"; + if (author && author[0] && !strcmp(author, reviewer)) return "self-reviewed"; + return "independent"; +} + +/* ── Option constants for new long-only flags ── */ +enum { + OPT_QUAL_METHOD = 256, + OPT_QUALIFIER, + OPT_RECORD_URI, + OPT_IMPL_AUTHOR, + OPT_IND_REVIEWER, + OPT_IND_TEST_EXEC, + OPT_ACHIEVE_ASIL +}; + /* ── Main command ─────────────────────────────────────────────────────── */ int cmd_qualify(int argc, char **argv) { - const char *binary = NULL; - const char *output = NULL; - const char *fmt_s = "text"; - int fmt_explicit = 0; - int verbose = 0; + const char *binary = NULL; + const char *output = NULL; + const char *fmt_s = "text"; + int fmt_explicit = 0; + int verbose = 0; + /* Feature 2 — tool qualification display (REQ-QUAL003) */ + //cfusa:req REQ-QUAL003 + const char *qual_method = NULL; + const char *qualifier = NULL; + const char *record_uri = NULL; + /* Feature 4 — V&V independence (REQ-VV001) */ + //cfusa:req REQ-VV001 + const char *impl_author = NULL; + const char *ind_reviewer = NULL; + const char *ind_test_exec = NULL; + const char *achievable_asil = NULL; static const struct option long_opts[] = { - {"binary", required_argument, NULL, 'b'}, - {"output", required_argument, NULL, 'o'}, - {"format", required_argument, NULL, 'f'}, - {"verbose", no_argument, NULL, 'v'}, - {"help", no_argument, NULL, 'h'}, + {"binary", required_argument, NULL, 'b'}, + {"output", required_argument, NULL, 'o'}, + {"format", required_argument, NULL, 'f'}, + {"verbose", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + /* Feature 2 */ + {"qualification-method", required_argument, NULL, OPT_QUAL_METHOD}, + {"qualifier", required_argument, NULL, OPT_QUALIFIER}, + {"record-uri", required_argument, NULL, OPT_RECORD_URI}, + /* Feature 4 */ + {"implementation-author", required_argument, NULL, OPT_IMPL_AUTHOR}, + {"independent-reviewer", required_argument, NULL, OPT_IND_REVIEWER}, + {"independent-test-executor", required_argument, NULL, OPT_IND_TEST_EXEC}, + {"achievable-asil", required_argument, NULL, OPT_ACHIEVE_ASIL}, {NULL,0,NULL,0} }; int c; optind = 1; +#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + { extern int optreset; optreset = 1; } +#endif while ((c = getopt_long(argc, argv, "b:o:f:vh", long_opts, NULL)) != -1) { switch (c) { - case 'b': binary = optarg; break; - case 'o': output = optarg; break; + case 'b': binary = optarg; break; + case 'o': output = optarg; break; case 'f': fmt_s = optarg; fmt_explicit = 1; break; - case 'v': verbose = 1; break; + case 'v': verbose = 1; break; + case OPT_QUAL_METHOD: qual_method = optarg; break; + case OPT_QUALIFIER: qualifier = optarg; break; + case OPT_RECORD_URI: record_uri = optarg; break; + case OPT_IMPL_AUTHOR: impl_author = optarg; break; + case OPT_IND_REVIEWER: ind_reviewer = optarg; break; + case OPT_IND_TEST_EXEC: ind_test_exec = optarg; break; + case OPT_ACHIEVE_ASIL: achievable_asil = optarg; break; case 'h': printf("Usage: cfusa qualify [--binary ] [--format text|json]\n" - " [--output ] [--verbose]\n\n" + " [--output ] [--verbose]\n" + " [--qualification-method self|independent]\n" + " [--qualifier ] [--record-uri ]\n" + " [--implementation-author ]\n" + " [--independent-reviewer ]\n" + " [--independent-test-executor ]\n" + " [--achievable-asil ]\n\n" "Tool qualification self-test per DO-178C §12 / ISO 26262-8 §11.\n" "Runs SHA-256/HMAC known-answer tests and FUSA rule exercise cases.\n"); return 0; @@ -473,6 +539,9 @@ int cmd_qualify(int argc, char **argv) char ts[32]; cfusa_timestamp_now(ts); + const char *badge = qualification_badge(qual_method); /* REQ-QUAL006 */ + const char *indep = independence_status(impl_author, ind_reviewer); /* REQ-VV004 */ + if (!strcmp(fmt_s, "json")) { fprintf(out, "{\n" @@ -487,10 +556,29 @@ int cmd_qualify(int argc, char **argv) " \"passed\": %d,\n" " \"failed\": %d,\n" " \"qualified\": %s,\n" - " \"results\": [\n", + " \"qualificationBadge\": \"%s\",\n" + " \"independenceStatus\": \"%s\"", CFUSA_VERSION_STRING, ts, bin_hash, total, total_pass, total_fail, - (total_fail == 0) ? "true" : "false"); + (total_fail == 0) ? "true" : "false", + badge, indep); + /* Feature 2 optional fields (REQ-QUAL003) */ + if (qual_method && qual_method[0]) + fprintf(out, ",\n \"qualificationMethod\": \"%s\"", qual_method); + if (qualifier && qualifier[0]) + fprintf(out, ",\n \"qualifierIdentity\": \"%s\"", qualifier); + if (record_uri && record_uri[0]) + fprintf(out, ",\n \"qualificationRecordUri\": \"%s\"", record_uri); + /* Feature 4 optional fields (REQ-VV001) */ + if (impl_author && impl_author[0]) + fprintf(out, ",\n \"implementationAuthor\": \"%s\"", impl_author); + if (ind_reviewer && ind_reviewer[0]) + fprintf(out, ",\n \"independentReviewer\": \"%s\"", ind_reviewer); + if (ind_test_exec && ind_test_exec[0]) + fprintf(out, ",\n \"independentTestExecutor\": \"%s\"", ind_test_exec); + if (achievable_asil && achievable_asil[0]) + fprintf(out, ",\n \"achievableAsil\": \"%s\"", achievable_asil); + fprintf(out, ",\n \"results\": [\n"); for (int i = 0; g_kat[i].name; i++) fprintf(out, " {\"name\": \"%s\", \"kind\": \"kat\", \"result\": \"%s\"},\n", @@ -522,6 +610,24 @@ int cmd_qualify(int argc, char **argv) fprintf(out, "\nResult: %d passed, %d failed — %s\n", total_pass, total_fail, (total_fail == 0) ? "QUALIFIED" : "NOT QUALIFIED"); + /* REQ-QUAL006: qualification badge */ + fprintf(out, "Qualification badge: %s\n", badge); + /* REQ-VV004: independence status */ + fprintf(out, "Independence status: %s\n", indep); + if (qual_method && qual_method[0]) + fprintf(out, "Qualification method: %s\n", qual_method); + if (qualifier && qualifier[0]) + fprintf(out, "Qualifier: %s\n", qualifier); + if (record_uri && record_uri[0]) + fprintf(out, "Record URI: %s\n", record_uri); + if (impl_author && impl_author[0]) + fprintf(out, "Implementation author: %s\n", impl_author); + if (ind_reviewer && ind_reviewer[0]) + fprintf(out, "Independent reviewer: %s\n", ind_reviewer); + if (ind_test_exec && ind_test_exec[0]) + fprintf(out, "Independent test exec: %s\n", ind_test_exec); + if (achievable_asil && achievable_asil[0]) + fprintf(out, "Achievable ASIL: %s\n", achievable_asil); } if (output && out != stdout) fclose(out); diff --git a/cmd/cfusa/cmd_trace.c b/cmd/cfusa/cmd_trace.c index cbc42b2..dabd422 100644 --- a/cmd/cfusa/cmd_trace.c +++ b/cmd/cfusa/cmd_trace.c @@ -31,7 +31,8 @@ #define KIND_SEC_TEST 2 typedef struct { char id[MAX_ID]; char title[MAX_TITLE]; - char standard[64]; char level[32]; } req_t; + char standard[64]; char level[32]; + char parent_id[MAX_ID]; } req_t; /* parent_id: LLR→HLR link (REQ-HLR001) */ typedef struct { char req_id[MAX_ID]; char file[256]; int line; int kind; } tag_t; @@ -39,6 +40,14 @@ static req_t g_reqs[MAX_REQS]; static int g_req_count; static tag_t g_tags[MAX_TAGS]; static int g_tag_count; static char g_dir_abs[512]; /* resolved absolute project root for path relativization */ +/* ---- HLR/LLR decomposition state (REQ-HLR001) ---- */ +#define MAX_HLR_LLR 512 +static char g_hlr_ids[MAX_HLR_LLR][MAX_ID]; static int g_hlr_count; +static char g_llr_ids[MAX_HLR_LLR][MAX_ID]; +static char g_llr_parents[MAX_HLR_LLR][MAX_ID]; static int g_llr_count; +static char g_orphaned[MAX_HLR_LLR][MAX_ID]; static int g_orphaned_count; +static char g_uncovered[MAX_HLR_LLR][MAX_ID]; static int g_uncovered_count; + /* ---- minimal JSON field extractor for { ... } objects ---- */ static void jfield(const char *obj, const char *key, char *out, size_t sz) { @@ -86,10 +95,11 @@ static void load_reqs(const char *dir) size_t ol = (size_t)(be - bs + 1); if (ol > sizeof(obj) - 1) ol = sizeof(obj) - 1; memcpy(obj, bs, ol); - jfield(obj, "id", g_reqs[g_req_count].id, MAX_ID); - jfield(obj, "title", g_reqs[g_req_count].title, MAX_TITLE); - jfield(obj, "standard", g_reqs[g_req_count].standard, 64); - jfield(obj, "level", g_reqs[g_req_count].level, 32); + jfield(obj, "id", g_reqs[g_req_count].id, MAX_ID); + jfield(obj, "title", g_reqs[g_req_count].title, MAX_TITLE); + jfield(obj, "standard", g_reqs[g_req_count].standard, 64); + jfield(obj, "level", g_reqs[g_req_count].level, 32); + jfield(obj, "parentId", g_reqs[g_req_count].parent_id, MAX_ID); /*REQ-HLR001*/ if (g_reqs[g_req_count].id[0]) g_req_count++; p = be + 1; } @@ -277,6 +287,51 @@ static void do_scan_funcs(const char *root) cfusa_walk_sources(root, fexts, 1, funcfile_cb, NULL); } +/* ---- HLR/LLR decomposition analysis (REQ-HLR001, REQ-HLR002, REQ-HLR003) ---- */ +//cfusa:req REQ-HLR001 +static void compute_hlr_llr(void) +{ + g_hlr_count = g_llr_count = g_orphaned_count = g_uncovered_count = 0; + + /* Classify requirements by level field (case-insensitive HLR / LLR) */ + for (int i = 0; i < g_req_count; i++) { + char lvl[33]; + strncpy(lvl, g_reqs[i].level, sizeof(lvl) - 1); + lvl[sizeof(lvl)-1] = '\0'; + for (char *p = lvl; *p; p++) *p = (char)toupper((unsigned char)*p); + + if (!strcmp(lvl, "HLR") && g_hlr_count < MAX_HLR_LLR) { + strncpy(g_hlr_ids[g_hlr_count++], g_reqs[i].id, MAX_ID - 1); + } else if (!strcmp(lvl, "LLR") && g_llr_count < MAX_HLR_LLR) { + strncpy(g_llr_ids[g_llr_count], g_reqs[i].id, MAX_ID - 1); + strncpy(g_llr_parents[g_llr_count], g_reqs[i].parent_id, MAX_ID - 1); + g_llr_count++; + } + } + + /* REQ-HLR002: every LLR must reference an existing HLR */ + for (int i = 0; i < g_llr_count; i++) { + int valid = 0; + if (g_llr_parents[i][0]) { + for (int j = 0; j < g_hlr_count; j++) { + if (!strcmp(g_llr_parents[i], g_hlr_ids[j])) { valid = 1; break; } + } + } + if (!valid && g_orphaned_count < MAX_HLR_LLR) + strncpy(g_orphaned[g_orphaned_count++], g_llr_ids[i], MAX_ID - 1); + } + + /* REQ-HLR003: every HLR must have at least one LLR child */ + for (int j = 0; j < g_hlr_count; j++) { + int covered = 0; + for (int i = 0; i < g_llr_count; i++) { + if (!strcmp(g_llr_parents[i], g_hlr_ids[j])) { covered = 1; break; } + } + if (!covered && g_uncovered_count < MAX_HLR_LLR) + strncpy(g_uncovered[g_uncovered_count++], g_hlr_ids[j], MAX_ID - 1); + } +} + int cmd_trace(int argc, char **argv) { const char *dir = "."; @@ -286,21 +341,28 @@ int cmd_trace(int argc, char **argv) int req_coverage = 0; int sec_tested = 0; int legacy = 1; + int strict_hlr_llr = 0; /* REQ-HLR001 */ static const struct option lo[] = { - {"dir", required_argument, NULL, 'd'}, - {"output", required_argument, NULL, 'o'}, - {"format", required_argument, NULL, 'f'}, - {"gaps", no_argument, NULL, 'g'}, - {"req-coverage", required_argument, NULL, 'r'}, - {"sec-tested", required_argument, NULL, 's'}, - {"no-legacy", no_argument, NULL, 'L'}, - {"help", no_argument, NULL, 'h'}, + {"dir", required_argument, NULL, 'd'}, + {"output", required_argument, NULL, 'o'}, + {"format", required_argument, NULL, 'f'}, + {"gaps", no_argument, NULL, 'g'}, + {"req-coverage", required_argument, NULL, 'r'}, + {"sec-tested", required_argument, NULL, 's'}, + {"no-legacy", no_argument, NULL, 'L'}, + {"strict-hlr-llr", no_argument, NULL, 'H'}, /* REQ-HLR001 */ + {"help", no_argument, NULL, 'h'}, {NULL,0,NULL,0} }; int c; optind = 1; - while ((c = getopt_long(argc, argv, "d:o:f:gr:s:Lh", lo, NULL)) != -1) { +#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + { extern int optreset; optreset = 1; } +#elif defined(__linux__) + optind = 0; /* glibc: reset nextchar so stale argv pointer is not followed */ +#endif + while ((c = getopt_long(argc, argv, "d:o:f:gr:s:LHh", lo, NULL)) != -1) { switch (c) { case 'd': dir = optarg; break; case 'o': out_path = optarg; break; @@ -309,18 +371,21 @@ int cmd_trace(int argc, char **argv) case 'r': req_coverage = atoi(optarg); break; case 's': sec_tested = atoi(optarg); break; case 'L': legacy = 0; break; + case 'H': strict_hlr_llr = 1; break; /* REQ-HLR001 */ case 'h': printf("Usage: cfusa trace [--dir ] [--format text|json|md]\n" " [--output ] [--gaps]\n" - " [--req-coverage ] [--sec-tested ]\n\n" + " [--req-coverage ] [--sec-tested ]\n" + " [--strict-hlr-llr]\n\n" "Builds a requirements traceability matrix from .cfusa-reqs.json\n" "and source annotations:\n" " //cfusa:req REQ-ID — implementation reference\n" " //cfusa:test REQ-ID — test reference\n" " //cfusa:sec-test REQ-ID — security-test reference\n\n" - " --gaps list requirements with no //cfusa:test tag\n" - " --req-coverage N exit 1 if 0 || g_uncovered_count > 0) { + fprintf(stderr, + "cfusa trace: --strict-hlr-llr gate failed: " + "%d orphaned LLR(s), %d uncovered HLR(s)\n", + g_orphaned_count, g_uncovered_count); + return 1; + } + return 0; + } + /* --- req-coverage gate (metric 1 + metric 2) --- */ if (req_coverage > 0) { printf("Requirement Coverage Report\n\n"); @@ -436,21 +529,24 @@ int cmd_trace(int argc, char **argv) " \"generatedAt\": \"%s\",\n" " \"projectRoot\": \"%s\",\n", ts, g_dir_abs); - /* requirements[] */ + /* requirements[] — include parentId for LLR entries (REQ-HLR001) */ fprintf(out, " \"requirements\": [\n"); for (int i = 0; i < g_req_count; i++) { char esc_id[MAX_ID*2], esc_title[MAX_TITLE*2], - esc_std[128], esc_lvl[64]; - cfusa_str_escape_json(g_reqs[i].id, esc_id, sizeof(esc_id)); - cfusa_str_escape_json(g_reqs[i].title, esc_title, sizeof(esc_title)); - cfusa_str_escape_json(g_reqs[i].standard, esc_std, sizeof(esc_std)); - cfusa_str_escape_json(g_reqs[i].level, esc_lvl, sizeof(esc_lvl)); + esc_std[128], esc_lvl[64], esc_pid[MAX_ID*2]; + cfusa_str_escape_json(g_reqs[i].id, esc_id, sizeof(esc_id)); + cfusa_str_escape_json(g_reqs[i].title, esc_title, sizeof(esc_title)); + cfusa_str_escape_json(g_reqs[i].standard, esc_std, sizeof(esc_std)); + cfusa_str_escape_json(g_reqs[i].level, esc_lvl, sizeof(esc_lvl)); + cfusa_str_escape_json(g_reqs[i].parent_id, esc_pid, sizeof(esc_pid)); fprintf(out, " {\"id\": \"%s\", \"title\": \"%s\"", esc_id, esc_title); if (esc_std[0]) fprintf(out, ", \"standard\": \"%s\"", esc_std); if (esc_lvl[0]) fprintf(out, ", \"level\": \"%s\"", esc_lvl); + if (esc_pid[0]) + fprintf(out, ", \"parentId\": \"%s\"", esc_pid); fprintf(out, "}%s\n", (i < g_req_count - 1) ? "," : ""); } fprintf(out, " ],\n"); @@ -479,9 +575,43 @@ int cmd_trace(int argc, char **argv) " \"tracedRequirements\": %d,\n" " \"testedRequirements\": %d,\n" " \"secTestedRequirements\": %d\n" - " }\n" - "}\n", + " }", total, traced, tested, sec_tested_count); + + /* hlrllrSummary{} — only when HLR/LLR levels are in use (REQ-HLR001) */ + if (g_hlr_count > 0 || g_llr_count > 0) { + fprintf(out, + ",\n" + " \"hlrllrSummary\": {\n" + " \"hlrCount\": %d,\n" + " \"llrCount\": %d,\n" + " \"orphanedCount\": %d,\n" + " \"uncoveredCount\": %d", + g_hlr_count, g_llr_count, + g_orphaned_count, g_uncovered_count); + if (g_orphaned_count > 0) { + fprintf(out, ",\n \"orphaned\": ["); + for (int i = 0; i < g_orphaned_count; i++) { + char esc[MAX_ID*2]; + cfusa_str_escape_json(g_orphaned[i], esc, sizeof(esc)); + fprintf(out, "\"%s\"%s", esc, + (i < g_orphaned_count - 1) ? ", " : ""); + } + fprintf(out, "]"); + } + if (g_uncovered_count > 0) { + fprintf(out, ",\n \"uncovered\": ["); + for (int i = 0; i < g_uncovered_count; i++) { + char esc[MAX_ID*2]; + cfusa_str_escape_json(g_uncovered[i], esc, sizeof(esc)); + fprintf(out, "\"%s\"%s", esc, + (i < g_uncovered_count - 1) ? ", " : ""); + } + fprintf(out, "]"); + } + fprintf(out, "\n }"); + } + fprintf(out, "\n}\n"); } else if (fmt == FMT_MD) { fprintf(out, "# Requirements Traceability Matrix\n\n"); if (g_req_count > 0) { @@ -518,6 +648,12 @@ int cmd_trace(int argc, char **argv) } fprintf(out, "\n**Coverage:** %d/%d traced, %d/%d tested\n", traced, total, tested, total); + if (g_hlr_count > 0 || g_llr_count > 0) + fprintf(out, + "\n| HLRs | %d |\n| LLRs | %d |\n" + "| Orphaned LLRs | %d |\n| Uncovered HLRs | %d |\n", + g_hlr_count, g_llr_count, + g_orphaned_count, g_uncovered_count); } else { /* text */ if (show_gaps) { @@ -568,6 +704,9 @@ int cmd_trace(int argc, char **argv) } fprintf(out, "\nCoverage: %d/%d requirements traced, %d/%d tested\n", traced, total, tested, total); + if (g_hlr_count > 0 || g_llr_count > 0) + fprintf(out, "HLR/LLR: %d HLRs %d LLRs %d orphaned %d uncovered\n", + g_hlr_count, g_llr_count, g_orphaned_count, g_uncovered_count); } else if (g_tag_count > 0) { fprintf(out, "\n%-24s %-40s %s\n", "ANNOTATION", "FILE", "LINE"); fprintf(out, "%-24s %-40s %s\n", diff --git a/include/cfusa/version.h b/include/cfusa/version.h index d81427d..fc4d1b4 100644 --- a/include/cfusa/version.h +++ b/include/cfusa/version.h @@ -3,8 +3,8 @@ #define CFUSA_VERSION_MAJOR 0 #define CFUSA_VERSION_MINOR 5 -#define CFUSA_VERSION_PATCH 34 -#define CFUSA_VERSION_STRING "0.5.34" +#define CFUSA_VERSION_PATCH 35 +#define CFUSA_VERSION_STRING "0.5.35" #define CFUSA_SCHEMA_VERSION "1.10.4" #define CFUSA_SPEC_VERSION "1.10.4" diff --git a/tests/test_hlr_llr.c b/tests/test_hlr_llr.c new file mode 100644 index 0000000..35b3827 --- /dev/null +++ b/tests/test_hlr_llr.c @@ -0,0 +1,232 @@ +/* + * Tests for HLR/LLR hierarchical traceability (Feature 1). + * Exercises cmd_trace --strict-hlr-llr and HLR/LLR validation. + */ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include "../vendor/unity/unity.h" + +extern int cmd_trace(int argc, char **argv); + +#define HLR_TEST_DIR "/tmp/cfusa_hlr_llr_testdir" + +void setUp(void) { (void)mkdir(HLR_TEST_DIR, 0700); } +void tearDown(void) {} + +static void write_file(const char *fname, const char *content) +{ + char path[256]; + snprintf(path, sizeof(path), "%s/%s", HLR_TEST_DIR, fname); + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + FILE *f = (fd >= 0) ? fdopen(fd, "w") : NULL; + if (f) { fputs(content, f); fclose(f); } + else if (fd >= 0) { close(fd); } +} + +static void remove_file(const char *fname) +{ + char path[256]; + snprintf(path, sizeof(path), "%s/%s", HLR_TEST_DIR, fname); + (void)remove(path); +} + +/* ── Feature 1 tests ─────────────────────────────────────────────────────── */ + +/* --strict-hlr-llr with no HLR/LLR levels → exit 0 + "no hierarchical" message */ +//cfusa:req REQ-HLR001 +//cfusa:test REQ-HLR001 +void test_strict_hlr_llr_no_levels_returns_zero(void) +{ + write_file(".fusa-reqs.json", + "{\"requirements\":[" + "{\"id\":\"REQ-001\",\"title\":\"Plain requirement\"}" + "]}"); + char *argv[] = {"cfusa", "--dir", HLR_TEST_DIR, + "--strict-hlr-llr", NULL}; + int rc = cmd_trace(4, argv); + TEST_ASSERT_EQUAL(0, rc); + remove_file(".fusa-reqs.json"); +} + +/* --strict-hlr-llr with well-formed HLR/LLR → exit 0 */ +//cfusa:req REQ-HLR002 +//cfusa:test REQ-HLR002 +void test_strict_hlr_llr_well_formed_returns_zero(void) +{ + write_file(".fusa-reqs.json", + "{\"requirements\":[" + "{\"id\":\"HLR-001\",\"title\":\"High-level\",\"level\":\"HLR\"}," + "{\"id\":\"LLR-001\",\"title\":\"Low-level\",\"level\":\"LLR\"," + " \"parentId\":\"HLR-001\"}" + "]}"); + char *argv[] = {"cfusa", "--dir", HLR_TEST_DIR, + "--strict-hlr-llr", NULL}; + int rc = cmd_trace(4, argv); + TEST_ASSERT_EQUAL(0, rc); + remove_file(".fusa-reqs.json"); +} + +/* --strict-hlr-llr with orphaned LLR (empty parentId) → exit 1 */ +//cfusa:req REQ-HLR002 +//cfusa:test REQ-HLR002 +void test_strict_hlr_llr_orphaned_llr_returns_one(void) +{ + write_file(".fusa-reqs.json", + "{\"requirements\":[" + "{\"id\":\"HLR-001\",\"title\":\"High-level\",\"level\":\"HLR\"}," + "{\"id\":\"LLR-001\",\"title\":\"Orphan\",\"level\":\"LLR\"," + " \"parentId\":\"\"}" + "]}"); + char *argv[] = {"cfusa", "--dir", HLR_TEST_DIR, + "--strict-hlr-llr", NULL}; + int rc = cmd_trace(4, argv); + TEST_ASSERT_EQUAL(1, rc); + remove_file(".fusa-reqs.json"); +} + +/* --strict-hlr-llr with LLR referencing nonexistent HLR → exit 1 */ +//cfusa:req REQ-HLR002 +//cfusa:test REQ-HLR002 +void test_strict_hlr_llr_bad_parent_returns_one(void) +{ + write_file(".fusa-reqs.json", + "{\"requirements\":[" + "{\"id\":\"HLR-001\",\"title\":\"High-level\",\"level\":\"HLR\"}," + "{\"id\":\"LLR-001\",\"title\":\"Bad parent\",\"level\":\"LLR\"," + " \"parentId\":\"NONEXISTENT\"}" + "]}"); + char *argv[] = {"cfusa", "--dir", HLR_TEST_DIR, + "--strict-hlr-llr", NULL}; + int rc = cmd_trace(4, argv); + TEST_ASSERT_EQUAL(1, rc); + remove_file(".fusa-reqs.json"); +} + +/* --strict-hlr-llr with uncovered HLR (no LLR children) → exit 1 */ +//cfusa:req REQ-HLR003 +//cfusa:test REQ-HLR003 +void test_strict_hlr_llr_uncovered_hlr_returns_one(void) +{ + write_file(".fusa-reqs.json", + "{\"requirements\":[" + "{\"id\":\"HLR-001\",\"title\":\"Has child\",\"level\":\"HLR\"}," + "{\"id\":\"HLR-002\",\"title\":\"No child\",\"level\":\"HLR\"}," + "{\"id\":\"LLR-001\",\"title\":\"Low\",\"level\":\"LLR\"," + " \"parentId\":\"HLR-001\"}" + "]}"); + char *argv[] = {"cfusa", "--dir", HLR_TEST_DIR, + "--strict-hlr-llr", NULL}; + int rc = cmd_trace(4, argv); + TEST_ASSERT_EQUAL(1, rc); + remove_file(".fusa-reqs.json"); +} + +/* JSON output includes hlrllrSummary when HLR/LLR requirements present */ +//cfusa:req REQ-HLR001 +//cfusa:test REQ-HLR001 +void test_json_output_has_hlrllr_summary(void) +{ + write_file(".fusa-reqs.json", + "{\"requirements\":[" + "{\"id\":\"HLR-001\",\"title\":\"High-level\",\"level\":\"HLR\"}," + "{\"id\":\"LLR-001\",\"title\":\"Low-level\",\"level\":\"LLR\"," + " \"parentId\":\"HLR-001\"}" + "]}"); + char *argv[] = {"cfusa", "--dir", HLR_TEST_DIR, + "--format", "json", + "--output", "/tmp/cfusa_hlr_llr_trace.json", NULL}; + int rc = cmd_trace(7, argv); + TEST_ASSERT_EQUAL(0, rc); + + /* Verify the output file contains hlrllrSummary */ + FILE *f = fopen("/tmp/cfusa_hlr_llr_trace.json", "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[4096] = ""; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + TEST_ASSERT_NOT_NULL(strstr(buf, "hlrllrSummary")); + TEST_ASSERT_NOT_NULL(strstr(buf, "hlrCount")); + (void)remove("/tmp/cfusa_hlr_llr_trace.json"); + } + remove_file(".fusa-reqs.json"); +} + +/* JSON output includes parentId field for LLR requirements */ +//cfusa:req REQ-HLR001 +//cfusa:test REQ-HLR001 +void test_json_output_has_parent_id(void) +{ + write_file(".fusa-reqs.json", + "{\"requirements\":[" + "{\"id\":\"HLR-001\",\"title\":\"High-level\",\"level\":\"HLR\"}," + "{\"id\":\"LLR-001\",\"title\":\"Low-level\",\"level\":\"LLR\"," + " \"parentId\":\"HLR-001\"}" + "]}"); + char *argv[] = {"cfusa", "--dir", HLR_TEST_DIR, + "--format", "json", + "--output", "/tmp/cfusa_hlr_llr_pid.json", NULL}; + int rc = cmd_trace(7, argv); + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen("/tmp/cfusa_hlr_llr_pid.json", "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[4096] = ""; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + TEST_ASSERT_NOT_NULL(strstr(buf, "parentId")); + (void)remove("/tmp/cfusa_hlr_llr_pid.json"); + } + remove_file(".fusa-reqs.json"); +} + +/* Text output includes HLR/LLR summary when hierarchy present */ +//cfusa:req REQ-HLR001 +//cfusa:test REQ-HLR001 +void test_text_output_has_hlrllr_line(void) +{ + write_file(".fusa-reqs.json", + "{\"requirements\":[" + "{\"id\":\"HLR-001\",\"title\":\"High-level\",\"level\":\"HLR\"}," + "{\"id\":\"LLR-001\",\"title\":\"Low-level\",\"level\":\"LLR\"," + " \"parentId\":\"HLR-001\"}" + "]}"); + char *argv[] = {"cfusa", "--dir", HLR_TEST_DIR, + "--format", "text", + "--output", "/tmp/cfusa_hlr_llr_text.txt", NULL}; + int rc = cmd_trace(7, argv); + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen("/tmp/cfusa_hlr_llr_text.txt", "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[4096] = ""; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + TEST_ASSERT_NOT_NULL(strstr(buf, "HLR/LLR")); + (void)remove("/tmp/cfusa_hlr_llr_text.txt"); + } + remove_file(".fusa-reqs.json"); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_strict_hlr_llr_no_levels_returns_zero); + RUN_TEST(test_strict_hlr_llr_well_formed_returns_zero); + RUN_TEST(test_strict_hlr_llr_orphaned_llr_returns_one); + RUN_TEST(test_strict_hlr_llr_bad_parent_returns_one); + RUN_TEST(test_strict_hlr_llr_uncovered_hlr_returns_one); + RUN_TEST(test_json_output_has_hlrllr_summary); + RUN_TEST(test_json_output_has_parent_id); + RUN_TEST(test_text_output_has_hlrllr_line); + return UNITY_END(); +} diff --git a/tests/test_mcdc.c b/tests/test_mcdc.c new file mode 100644 index 0000000..3e0829d --- /dev/null +++ b/tests/test_mcdc.c @@ -0,0 +1,213 @@ +/* + * Tests for MC/DC coverage analysis (Feature 3). + * Exercises cmd_coverage --mcdc-file and --mcdc-threshold. + */ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include "../vendor/unity/unity.h" + +extern int cmd_coverage(int argc, char **argv); + +#define MCDC_TEST_DIR "/tmp/cfusa_mcdc_testdir" + +void setUp(void) { (void)mkdir(MCDC_TEST_DIR, 0700); } +void tearDown(void) {} + +static void write_mcdc_json(const char *fname, const char *content) +{ + char path[256]; + snprintf(path, sizeof(path), "%s/%s", MCDC_TEST_DIR, fname); + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + FILE *f = (fd >= 0) ? fdopen(fd, "w") : NULL; + if (f) { fputs(content, f); fclose(f); } + else if (fd >= 0) { close(fd); } +} + +/* ── Feature 3 tests ─────────────────────────────────────────────────────── */ + +/* --help returns 0 (covers new --mcdc-file/--mcdc-threshold in help) */ +//cfusa:req REQ-COV015 +//cfusa:test REQ-COV015 +void test_mcdc_help_returns_zero(void) +{ + char *argv[] = {"cfusa", "coverage", "--help", NULL}; + int rc = cmd_coverage(3, argv); + TEST_ASSERT_EQUAL(0, rc); +} + +/* All conditions covered → exit 0 */ +//cfusa:req REQ-COV015 +//cfusa:test REQ-COV015 +void test_mcdc_all_covered_passes(void) +{ + /* covered_true_count=3, covered_false_count=2 → covered */ + write_mcdc_json("all_covered.json", + "{\"data\":[{\"functions\":[" + "{\"name\":\"Foo\",\"mcdc_records\":[{\"conditions\":[" + "{\"covered_true_count\":3,\"covered_false_count\":2}" + "]}]}]}]}"); + + char path[256]; + snprintf(path, sizeof(path), "%s/all_covered.json", MCDC_TEST_DIR); + char *argv[] = {"cfusa", "coverage", + "--mcdc-file", path, NULL}; + int rc = cmd_coverage(4, argv); + /* Should pass: 1/1 conditions covered = 100% >= threshold 100 */ + TEST_ASSERT_EQUAL(0, rc); +} + +/* Uncovered condition (false_count=0) → exit 1 */ +//cfusa:req REQ-COV015 +//cfusa:test REQ-COV015 +void test_mcdc_uncovered_condition_fails(void) +{ + /* covered_true_count=5, covered_false_count=0 → NOT MC/DC covered */ + write_mcdc_json("uncovered.json", + "{\"data\":[{\"functions\":[" + "{\"name\":\"Bar\",\"mcdc_records\":[{\"conditions\":[" + "{\"covered_true_count\":5,\"covered_false_count\":0}" + "]}]}]}]}"); + + char path[256]; + snprintf(path, sizeof(path), "%s/uncovered.json", MCDC_TEST_DIR); + char *argv[] = {"cfusa", "coverage", + "--mcdc-file", path, NULL}; + int rc = cmd_coverage(4, argv); + /* Should fail: 0/1 conditions covered = 0% < threshold 100 */ + TEST_ASSERT_EQUAL(1, rc); +} + +/* true_count=0, false_count>0 → NOT MC/DC covered */ +//cfusa:req REQ-COV015 +//cfusa:test REQ-COV015 +void test_mcdc_true_count_zero_fails(void) +{ + write_mcdc_json("tc_zero.json", + "{\"data\":[{\"functions\":[" + "{\"name\":\"Baz\",\"mcdc_records\":[{\"conditions\":[" + "{\"covered_true_count\":0,\"covered_false_count\":4}" + "]}]}]}]}"); + + char path[256]; + snprintf(path, sizeof(path), "%s/tc_zero.json", MCDC_TEST_DIR); + char *argv[] = {"cfusa", "coverage", + "--mcdc-file", path, NULL}; + int rc = cmd_coverage(4, argv); + TEST_ASSERT_EQUAL(1, rc); +} + +/* No mcdc_records in JSON → exit 0 (no records → pass) */ +//cfusa:req REQ-COV015 +//cfusa:test REQ-COV015 +void test_mcdc_no_records_passes(void) +{ + write_mcdc_json("no_records.json", + "{\"data\":[{\"functions\":[" + "{\"name\":\"Qux\",\"mcdc_records\":[]}" + "]}]}"); + + char path[256]; + snprintf(path, sizeof(path), "%s/no_records.json", MCDC_TEST_DIR); + char *argv[] = {"cfusa", "coverage", + "--mcdc-file", path, NULL}; + int rc = cmd_coverage(4, argv); + TEST_ASSERT_EQUAL(0, rc); +} + +/* --mcdc-threshold below actual coverage → pass */ +//cfusa:req REQ-COV015 +//cfusa:test REQ-COV015 +void test_mcdc_threshold_below_coverage_passes(void) +{ + /* 1 covered, 1 uncovered → 50% coverage */ + write_mcdc_json("mixed.json", + "{\"data\":[{\"functions\":[" + "{\"name\":\"Mixed\",\"mcdc_records\":[{\"conditions\":[" + "{\"covered_true_count\":3,\"covered_false_count\":2}," + "{\"covered_true_count\":0,\"covered_false_count\":1}" + "]}]}]}]}"); + + char path[256]; + snprintf(path, sizeof(path), "%s/mixed.json", MCDC_TEST_DIR); + char *argv[] = {"cfusa", "coverage", + "--mcdc-file", path, + "--mcdc-threshold", "50", NULL}; + int rc = cmd_coverage(6, argv); + /* 50% coverage >= 50% threshold → pass */ + TEST_ASSERT_EQUAL(0, rc); +} + +/* --mcdc-threshold above actual coverage → fail */ +//cfusa:req REQ-COV015 +//cfusa:test REQ-COV015 +void test_mcdc_threshold_above_coverage_fails(void) +{ + /* 1 covered, 1 uncovered → 50% coverage */ + write_mcdc_json("mixed2.json", + "{\"data\":[{\"functions\":[" + "{\"name\":\"Mixed\",\"mcdc_records\":[{\"conditions\":[" + "{\"covered_true_count\":3,\"covered_false_count\":2}," + "{\"covered_true_count\":0,\"covered_false_count\":1}" + "]}]}]}]}"); + + char path[256]; + snprintf(path, sizeof(path), "%s/mixed2.json", MCDC_TEST_DIR); + char *argv[] = {"cfusa", "coverage", + "--mcdc-file", path, + "--mcdc-threshold", "80", NULL}; + int rc = cmd_coverage(6, argv); + /* 50% coverage < 80% threshold → fail */ + TEST_ASSERT_EQUAL(1, rc); +} + +/* JSON output includes mcdcReport field */ +//cfusa:req REQ-COV015 +//cfusa:test REQ-COV015 +void test_mcdc_json_output_has_mcdc_report(void) +{ + write_mcdc_json("covered2.json", + "{\"data\":[{\"functions\":[" + "{\"name\":\"Foo\",\"mcdc_records\":[{\"conditions\":[" + "{\"covered_true_count\":2,\"covered_false_count\":3}" + "]}]}]}]}"); + + char path[256]; + snprintf(path, sizeof(path), "%s/covered2.json", MCDC_TEST_DIR); + char *argv[] = {"cfusa", "coverage", + "--mcdc-file", path, + "--format", "json", + "--output", "/tmp/cfusa_mcdc_out.json", NULL}; + int rc = cmd_coverage(8, argv); /* 8 non-NULL elements */ + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen("/tmp/cfusa_mcdc_out.json", "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[4096] = ""; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + TEST_ASSERT_NOT_NULL(strstr(buf, "mcdcReport")); + TEST_ASSERT_NOT_NULL(strstr(buf, "totalConditions")); + TEST_ASSERT_NOT_NULL(strstr(buf, "coveragePct")); + (void)remove("/tmp/cfusa_mcdc_out.json"); + } +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_mcdc_help_returns_zero); + RUN_TEST(test_mcdc_all_covered_passes); + RUN_TEST(test_mcdc_uncovered_condition_fails); + RUN_TEST(test_mcdc_true_count_zero_fails); + RUN_TEST(test_mcdc_no_records_passes); + RUN_TEST(test_mcdc_threshold_below_coverage_passes); + RUN_TEST(test_mcdc_threshold_above_coverage_fails); + RUN_TEST(test_mcdc_json_output_has_mcdc_report); + return UNITY_END(); +} diff --git a/tests/test_qualify_vv.c b/tests/test_qualify_vv.c new file mode 100644 index 0000000..f65ff67 --- /dev/null +++ b/tests/test_qualify_vv.c @@ -0,0 +1,236 @@ +/* + * Tests for tool qualification display (Feature 2) and V&V independence + * (Feature 4) in cmd_qualify. + */ +#include +#include +#include "../vendor/unity/unity.h" + +extern int cmd_qualify(int argc, char **argv); + +void setUp(void) {} +void tearDown(void) {} + +/* ── Feature 2: Tool qualification display ────────────────────────────────── */ + +/* --qualification-method independent → badge "independently-qualified" */ +//cfusa:req REQ-QUAL006 +//cfusa:test REQ-QUAL006 +void test_qualify_badge_independent(void) +{ + char *argv[] = {"cfusa", "qualify", + "--qualification-method", "independent", + "--output", "/tmp/cfusa_qualify_indep.json", NULL}; + int rc = cmd_qualify(6, argv); + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen("/tmp/cfusa_qualify_indep.json", "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[8192] = ""; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + TEST_ASSERT_NOT_NULL(strstr(buf, "independently-qualified")); + TEST_ASSERT_NOT_NULL(strstr(buf, "qualificationBadge")); + (void)remove("/tmp/cfusa_qualify_indep.json"); + } +} + +/* --qualification-method self → badge "self-qualified" */ +//cfusa:req REQ-QUAL006 +//cfusa:test REQ-QUAL006 +void test_qualify_badge_self(void) +{ + char *argv[] = {"cfusa", "qualify", + "--qualification-method", "self", + "--output", "/tmp/cfusa_qualify_self.json", NULL}; + int rc = cmd_qualify(6, argv); + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen("/tmp/cfusa_qualify_self.json", "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[8192] = ""; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + TEST_ASSERT_NOT_NULL(strstr(buf, "self-qualified")); + (void)remove("/tmp/cfusa_qualify_self.json"); + } +} + +/* No --qualification-method → badge "unqualified" */ +//cfusa:req REQ-QUAL006 +//cfusa:test REQ-QUAL006 +void test_qualify_badge_unqualified(void) +{ + char *argv[] = {"cfusa", "qualify", + "--output", "/tmp/cfusa_qualify_unq.json", NULL}; + int rc = cmd_qualify(4, argv); + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen("/tmp/cfusa_qualify_unq.json", "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[8192] = ""; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"qualificationBadge\": \"unqualified\"")); + (void)remove("/tmp/cfusa_qualify_unq.json"); + } +} + +/* --qualifier and --record-uri appear in JSON output */ +//cfusa:req REQ-QUAL003 +//cfusa:test REQ-QUAL003 +void test_qualify_qualifier_and_record_uri_in_json(void) +{ + char *argv[] = {"cfusa", "qualify", + "--qualification-method", "independent", + "--qualifier", "Safety Labs Inc", + "--record-uri", "https://example.com/dossier", + "--output", "/tmp/cfusa_qualify_meta.json", NULL}; + int rc = cmd_qualify(10, argv); + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen("/tmp/cfusa_qualify_meta.json", "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[8192] = ""; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + TEST_ASSERT_NOT_NULL(strstr(buf, "qualifierIdentity")); + TEST_ASSERT_NOT_NULL(strstr(buf, "qualificationRecordUri")); + (void)remove("/tmp/cfusa_qualify_meta.json"); + } +} + +/* ── Feature 4: V&V independence ─────────────────────────────────────────── */ + +/* Different author/reviewer → independence "independent" */ +//cfusa:req REQ-VV004 +//cfusa:test REQ-VV004 +void test_qualify_independence_independent(void) +{ + char *argv[] = {"cfusa", "qualify", + "--implementation-author", "Alice", + "--independent-reviewer", "Bob", + "--output", "/tmp/cfusa_qualify_indep2.json", NULL}; + int rc = cmd_qualify(8, argv); + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen("/tmp/cfusa_qualify_indep2.json", "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[8192] = ""; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"independenceStatus\": \"independent\"")); + (void)remove("/tmp/cfusa_qualify_indep2.json"); + } +} + +/* Same author/reviewer → independence "self-reviewed" */ +//cfusa:req REQ-VV004 +//cfusa:test REQ-VV004 +void test_qualify_independence_self_reviewed(void) +{ + char *argv[] = {"cfusa", "qualify", + "--implementation-author", "Alice", + "--independent-reviewer", "Alice", + "--output", "/tmp/cfusa_qualify_self2.json", NULL}; + int rc = cmd_qualify(8, argv); + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen("/tmp/cfusa_qualify_self2.json", "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[8192] = ""; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"independenceStatus\": \"self-reviewed\"")); + (void)remove("/tmp/cfusa_qualify_self2.json"); + } +} + +/* No reviewer → independence "unqualified" */ +//cfusa:req REQ-VV004 +//cfusa:test REQ-VV004 +void test_qualify_independence_unqualified(void) +{ + char *argv[] = {"cfusa", "qualify", + "--implementation-author", "Alice", + "--output", "/tmp/cfusa_qualify_unq2.json", NULL}; + int rc = cmd_qualify(6, argv); + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen("/tmp/cfusa_qualify_unq2.json", "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[8192] = ""; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + TEST_ASSERT_NOT_NULL(strstr(buf, "\"independenceStatus\": \"unqualified\"")); + (void)remove("/tmp/cfusa_qualify_unq2.json"); + } +} + +/* All V&V independence fields appear in JSON output */ +//cfusa:req REQ-VV001 +//cfusa:test REQ-VV001 +void test_qualify_vv_fields_in_json(void) +{ + char *argv[] = {"cfusa", "qualify", + "--implementation-author", "Alice", + "--independent-reviewer", "Bob", + "--independent-test-executor", "Carol", + "--achievable-asil", "ASIL-D", + "--output", "/tmp/cfusa_qualify_vv.json", NULL}; + int rc = cmd_qualify(12, argv); + TEST_ASSERT_EQUAL(0, rc); + + FILE *f = fopen("/tmp/cfusa_qualify_vv.json", "r"); + TEST_ASSERT_NOT_NULL(f); + if (f) { + char buf[8192] = ""; + size_t n = fread(buf, 1, sizeof(buf) - 1, f); + buf[n] = '\0'; + fclose(f); + TEST_ASSERT_NOT_NULL(strstr(buf, "implementationAuthor")); + TEST_ASSERT_NOT_NULL(strstr(buf, "independentReviewer")); + TEST_ASSERT_NOT_NULL(strstr(buf, "independentTestExecutor")); + TEST_ASSERT_NOT_NULL(strstr(buf, "achievableAsil")); + (void)remove("/tmp/cfusa_qualify_vv.json"); + } +} + +/* --help returns 0 (covers new help text) */ +//cfusa:req REQ-QUAL003 +//cfusa:test REQ-QUAL003 +void test_qualify_vv_help_returns_zero(void) +{ + char *argv[] = {"cfusa", "qualify", "--help", NULL}; + int rc = cmd_qualify(3, argv); + TEST_ASSERT_EQUAL(0, rc); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_qualify_badge_independent); + RUN_TEST(test_qualify_badge_self); + RUN_TEST(test_qualify_badge_unqualified); + RUN_TEST(test_qualify_qualifier_and_record_uri_in_json); + RUN_TEST(test_qualify_independence_independent); + RUN_TEST(test_qualify_independence_self_reviewed); + RUN_TEST(test_qualify_independence_unqualified); + RUN_TEST(test_qualify_vv_fields_in_json); + RUN_TEST(test_qualify_vv_help_returns_zero); + return UNITY_END(); +}