diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 8dbf32d..3a68149 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -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"}, diff --git a/cmd/cfusa/cmd_safety_rules.c b/cmd/cfusa/cmd_safety_rules.c index 50d59d6..8fa92c6 100644 --- a/cmd/cfusa/cmd_safety_rules.c +++ b/cmd/cfusa/cmd_safety_rules.c @@ -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. */ diff --git a/tests/test_safety_rules.c b/tests/test_safety_rules.c index 46e13bd..3a48591 100644 --- a/tests/test_safety_rules.c +++ b/tests/test_safety_rules.c @@ -468,6 +468,109 @@ void test_comp001_passes_simple_function(void) 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) @@ -502,6 +605,10 @@ int main(void) /* 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();