Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <file> 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"}
]
}
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
)
Expand Down Expand Up @@ -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)
Expand Down
180 changes: 166 additions & 14 deletions cmd/cfusa/cmd_coverage.c
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#if defined(__linux__) || defined(__unix__)
# define _GNU_SOURCE
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Expand All @@ -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)

Check warning

Code scanning / cfusa

function 'parse_mcdc_json' has cyclomatic complexity V(G)=11 (threshold 10) — refactor to reduce branching paths (DO-178C §6.3.4) Warning

function 'parse_mcdc_json' has cyclomatic complexity V(G)=11 (threshold 10) — refactor to reduce branching paths (DO-178C §6.3.4)
{
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 { */

Check notice

Code scanning / CodeQL

Commented-out code Note

This comment appears to contain commented-out code.
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);

Check warning

Code scanning / cfusa

dynamic memory allocation ('free') — MISRA-C 2012 Rule 21.3: heap usage prohibited in safety-critical code Warning

dynamic memory allocation ('free') — MISRA-C 2012 Rule 21.3: heap usage prohibited in safety-critical code

Check notice

Code scanning / cfusa

CWE-416: free() call — ensure pointer is set to NULL immediately after and not referenced again (use-after-free prevention) Note

CWE-416: free() call — ensure pointer is set to NULL immediately after and not referenced again (use-after-free prevention)

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;
Expand Down Expand Up @@ -95,24 +185,35 @@
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; }

Check warning

Code scanning / cfusa

data coupling: exported mutable variable via 'extern' declaration; consider passing state explicitly (DO-178C §6.4.4.3) Warning

data coupling: exported mutable variable via 'extern' declaration; consider passing state explicitly (DO-178C §6.4.4.3)
#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;
Expand All @@ -122,6 +223,9 @@
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 */

Check notice

Code scanning / cfusa

pointer arithmetic detected — verify bounds (MISRA-C:2012 R18.4) Note

pointer arithmetic detected — verify bounds (MISRA-C:2012 R18.4)
case 'T': mcdc_threshold = atoi(optarg); break;
case 'M': mutate = 1; break;
case 'S': mutate_score = atof(optarg);
mutate = 1; break;
Expand All @@ -130,6 +234,7 @@
" [--format text|json] [--output <file>]\n"
" [--dal DAL-A|DAL-B|DAL-C|DAL-D]\n"
" [--threshold <pct>] [--mcdc]\n"
" [--mcdc-file <llvm.json>] [--mcdc-threshold <pct>]\n"
" [--mutate] [--mutate-score <pct>]\n\n"
"Parses gcov/lcov output and reports statement, function, and\n"
"branch coverage. --dal sets DO-178C level requirements:\n"
Expand All @@ -138,6 +243,8 @@
" 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");
Expand All @@ -156,9 +263,11 @@
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");
Expand Down Expand Up @@ -195,6 +304,8 @@
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"
Expand All @@ -217,9 +328,18 @@

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;

Expand Down Expand Up @@ -256,6 +376,27 @@
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)
Expand All @@ -274,12 +415,23 @@
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)
Expand Down
Loading
Loading