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
1 change: 1 addition & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
{"id":"REQ-COUPLING003","title":"COUP003 warns when coupling-report.json absent","standard":"DO-178C","level":"DAL-A"},
{"id":"REQ-DISP001","title":"DISP001 warns on undispositioned ERROR findings","standard":"ISO 26262","level":"ASIL-B"},
{"id":"REQ-COMP001","title":"COMP001 detects cyclomatic complexity violations","standard":"DO-178C","level":"DAL-A"},
{"id":"REQ-COMPTHR001","title":"COMP001's automatic check-engine threshold recognizes an ISO 26262 ASIL declaration in .fusa.json standards[] (not just DO-178C DAL), using the same threshold table as cfusa comp --asil-d/c/b/a; when both a DAL and ASIL are declared the stricter threshold wins","standard":"ISO 26262","level":"ASIL-D"},
{"id":"REQ-DUPREQ001","title":"DUPREQ001 fails check on duplicate requirement ids in .fusa-reqs.json","standard":"ISO 26262","level":"ASIL-B"},
{"id":"REQ-SEV001","title":"cfusa_dal_rank ranks DAL-A..DAL-E from most (4) to least (0) stringent, case-insensitively, returning -1 for unrecognized input","standard":"c-FuSa CLI","level":"ASIL-B"},
{"id":"REQ-SEV002","title":"cfusa_asil_rank ranks QM/ASIL-A..ASIL-D from least (0) to most (4) stringent, case-insensitively, returning -1 for unrecognized input","standard":"c-FuSa CLI","level":"ASIL-B"},
Expand Down
38 changes: 32 additions & 6 deletions cmd/cfusa/cmd_safety_rules.c
Original file line number Diff line number Diff line change
Expand Up @@ -782,19 +782,45 @@ typedef struct {
const cfusa_config_t *cfg;
} comp_ctx_t;

/* Threshold by DAL: A=4, B=10, C=15, D=20, default=10 */
//cfusa:req REQ-COMPTHR001
/*
* Threshold by declared standard: DO-178C DAL A=4,B=10,C=15,D=20 (matches
* cmd_comp.c's --dal-a/b/c/d); ISO 26262 ASIL D=4,C=10,B=15,A=20 (matches
* cmd_comp.c's --asil-d/c/b/a aliases). Default 10 when neither is
* declared.
*
* c-FuSa issue #107: this automatic `check` gate previously only
* recognized DO-178C DAL tags in .fusa.json's standards[], so a project
* declaring only ISO 26262 (e.g. "iso26262:ASIL-D") silently got the
* unscaled default threshold instead of the ASIL-appropriate one, unless
* it separately ran `cfusa comp --asil-d`. When both a DAL and an ASIL
* tag are declared, the stricter (lower/more demanding) threshold wins —
* a project claiming both standards must satisfy whichever is more
* demanding, same combination rule as --dal/--asil in cfusa coverage
* (#106).
*/
static int comp_threshold(const cfusa_config_t *cfg)
{
int dal_t = -1, asil_t = -1;
for (int i = 0; i < cfg->standards_count; i++) {
const char *s = cfg->standards[i];
if (strncmp(s, "do178", 5) == 0 || strncmp(s, "DO-178", 6) == 0) {
if (strstr(s, "dal-a") || strstr(s, "DAL-A")) return 4;
if (strstr(s, "dal-b") || strstr(s, "DAL-B")) return 10;
if (strstr(s, "dal-c") || strstr(s, "DAL-C")) return 15;
if (strstr(s, "dal-d") || strstr(s, "DAL-D")) return 20;
if (strstr(s, "dal-a") || strstr(s, "DAL-A")) dal_t = 4;
else if (strstr(s, "dal-b") || strstr(s, "DAL-B")) dal_t = 10;
else if (strstr(s, "dal-c") || strstr(s, "DAL-C")) dal_t = 15;
else if (strstr(s, "dal-d") || strstr(s, "DAL-D")) dal_t = 20;
}
if (strncmp(s, "iso26262", 8) == 0 || strncmp(s, "ISO 26262", 9) == 0) {
if (strstr(s, "asil-d") || strstr(s, "ASIL-D")) asil_t = 4;
else if (strstr(s, "asil-c") || strstr(s, "ASIL-C")) asil_t = 10;
else if (strstr(s, "asil-b") || strstr(s, "ASIL-B")) asil_t = 15;
else if (strstr(s, "asil-a") || strstr(s, "ASIL-A")) asil_t = 20;
}
}
return 10; /* default */
if (dal_t < 0 && asil_t < 0) return 10; /* default */
if (dal_t < 0) return asil_t;
if (asil_t < 0) return dal_t;
return (dal_t < asil_t) ? dal_t : asil_t; /* stricter wins */
}

/* Count decision points in a single line of C source. */
Expand Down
107 changes: 107 additions & 0 deletions tests/test_safety_rules.c
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,109 @@
rm_file("simple.c");
}

/* ── COMP001/COMP002: ASIL-scaled threshold via .fusa.json (issue #107) ── */

/* V(G)=5 (1 base + 4 "if" decisions): above ASIL-D's threshold (4), at or
* below the unscaled default (10) and DAL-D's threshold (20) — a probe
* that only fires when the ASIL-D threshold is actually the one in
* effect. */
static void make_vg5_function(void)
{
make_file("vg5.c",
"int mid(int a, int b, int c, int d) {\n"
" if (a > 0) { return 1; }\n"
" if (b > 0) { return 2; }\n"
" if (c > 0) { return 3; }\n"
" if (d > 0) { return 4; }\n"
" return 0;\n"
"}\n");
}

static int run_comp001_warning_count(void)
{
cfusa_engine_reset();
cfusa_safety_register_rules();

cfusa_config_t cfg; cfusa_config_load(SR_DIR, &cfg);
cfusa_report_t rpt; cfusa_report_init(&rpt);

int count = cfusa_engine_rule_count();
for (int i = 0; i < count; i++) {
const cfusa_rule_t *r = cfusa_engine_get_rule(i);
if (strcmp(r->id, "COMP001") == 0) r->run(SR_DIR, &cfg, &rpt);
}
int n = rpt.warning_count;
cfusa_report_free(&rpt);
return n;
}

//cfusa:req REQ-COMPTHR001
//cfusa:test REQ-COMPTHR001
void test_comp_threshold_default_passes_vg5(void)
{
/* No .fusa.json -> unscaled default threshold (10) -> V(G)=5 is clean. */
make_vg5_function();
TEST_ASSERT_EQUAL_INT(0, run_comp001_warning_count());
rm_file("vg5.c");
}

//cfusa:req REQ-COMPTHR001
//cfusa:test REQ-COMPTHR001
void test_comp_threshold_iso26262_asil_d_fails_vg5(void)
{
/* standards[] declares ISO 26262 ASIL-D (threshold 4, same as
* cfusa comp --asil-d) -> V(G)=5 exceeds it -> warns. Previously this
* gate only recognized DO-178C DAL tags and would have silently used
* the unscaled default (10) instead. */
make_file(".fusa.json",
"{\"configVersion\":\"1.0\",\"standards\":[\"iso26262:ASIL-D\"]}\n");
make_vg5_function();
TEST_ASSERT_TRUE(run_comp001_warning_count() > 0);
rm_file("vg5.c");
rm_file(".fusa.json");
}

//cfusa:req REQ-COMPTHR001
//cfusa:test REQ-COMPTHR001
void test_comp_threshold_dal_and_asil_combine_to_stricter(void)
{
/* Both DO-178C DAL-D (threshold 20, would alone pass V(G)=5) and ISO
* 26262 ASIL-D (threshold 4) declared together -> the stricter of the
* two (ASIL-D) must win, not DAL-D silently suppressing it. */
make_file(".fusa.json",
"{\"configVersion\":\"1.0\","
"\"standards\":[\"do178:DAL-D\",\"iso26262:ASIL-D\"]}\n");
make_vg5_function();
TEST_ASSERT_TRUE(run_comp001_warning_count() > 0);
rm_file("vg5.c");
rm_file(".fusa.json");
}

//cfusa:req REQ-COMPTHR001
//cfusa:test REQ-COMPTHR001
void test_comp_threshold_asil_d_matches_comp_command_asil_d(void)
{
/* cfusa comp --asil-d already uses threshold 4 (aliased to
* THRESHOLD_DAL_A) -- this proves check's automatic gate now derives
* the identical threshold from an ISO 26262 ASIL-D declaration
* without needing a separate `cfusa comp --asil-d` invocation. */
make_file(".fusa.json",
"{\"configVersion\":\"1.0\",\"standards\":[\"iso26262:ASIL-D\"]}\n");
/* V(G)=4 exactly at the ASIL-D/threshold=4 boundary: COMP001 only
* warns when complexity strictly exceeds the threshold, so this one
* function must NOT warn while the vg5 (V(G)=5) case above does. */
make_file("vg4.c",
"int lo(int a, int b, int c) {\n"
" if (a > 0) { return 1; }\n"
" if (b > 0) { return 2; }\n"
" if (c > 0) { return 3; }\n"
" return 0;\n"
"}\n");
TEST_ASSERT_EQUAL_INT(0, run_comp001_warning_count());
rm_file("vg4.c");
rm_file(".fusa.json");
}

/* ── Rule count sanity ──────────────────────────────────────────────── */

void test_safety_rules_register_count(void)
Expand Down Expand Up @@ -502,6 +605,10 @@
/* Complexity rule */
RUN_TEST(test_comp001_detects_complex_function);
RUN_TEST(test_comp001_passes_simple_function);
RUN_TEST(test_comp_threshold_default_passes_vg5);
RUN_TEST(test_comp_threshold_iso26262_asil_d_fails_vg5);
RUN_TEST(test_comp_threshold_dal_and_asil_combine_to_stricter);
RUN_TEST(test_comp_threshold_asil_d_matches_comp_command_asil_d);
/* Sanity */
RUN_TEST(test_safety_rules_register_count);
return UNITY_END();
Expand Down
Loading