From fac1331347f862d14a6e9ac7d09fa6174def1b5a Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Sun, 26 Jul 2026 08:23:32 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20v0.33.0=20=E2=80=94=20maximize=20te?= =?UTF-8?q?sts=20and=20requirements=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit-driven improvements addressing all six findings (P0–P2): P0 — REQ-TRACE008: cmd/gofusa/cmd_v032_test.go adds six CLI-level tests for runTraceHLRLLR covering ExitOK with no violations, ExitGateFail for orphaned LLR, ExitGateFail for uncovered HLR, nil-summary path, and two end-to-end --strict-hlr-llr integration tests. P1 — REQ-VERIFY006/007: registered in .fusa-reqs.json (verify.Save and verify.Load); //fusa:test annotations added to TestSaveAndLoad_Roundtrip in verify/verify_test.go. P1 — REQ-QUALIFY005/006: registered in .fusa-reqs.json (qualify.Load and qualify.BuiltinCases); //fusa:test annotations added to TestSaveAndLoad_Roundtrip and TestBuiltinCases_NonEmpty in qualify/qualify_test.go. P2 — REQ-COV015: three cmd-level tests for runCoverage --mcdc: missing --mcdc-file (ExitUsage), below-threshold (ExitGateFail), fully covered (ExitOK). P2 — REQ-QUALIFY007/008: two cmd-level tests for runQualify with new flags; TestRunQualify_NewFlags_PopulateReport reads the saved JSON and asserts all six new fields; TestRunQualify_NoFlags_DefaultsPopulateStdout checks default badge output. P2 — renderMarkdown HLRLLRSummary: TestRender_Markdown_HLRLLRSummary in trace/trace_test.go exercises the v0.32.0 HLR/LLR summary table block. P2 — coverage.req() false branch: TestRenderText_DALD_ReqHelperNoBranch renders with DAL-D (no required metrics) to cover req(false) → "no". Four new requirements added to .fusa-reqs.json: REQ-VERIFY006, REQ-VERIFY007, REQ-QUALIFY005, REQ-QUALIFY006. Version bumped to v0.33.0. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- .fusa-reqs.json | 28 +++ CHANGELOG.md | 18 ++ cmd/gofusa/cmd_v032_test.go | 372 ++++++++++++++++++++++++++++++++++++ coverage/coverage_test.go | 19 ++ fusa.go | 2 +- qualify/qualify_test.go | 2 + trace/trace_test.go | 32 ++++ verify/verify_test.go | 2 + 8 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 cmd/gofusa/cmd_v032_test.go diff --git a/.fusa-reqs.json b/.fusa-reqs.json index f4e641f..9dd2865 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -406,6 +406,20 @@ "standard": "IEC 61508", "level": "SIL-2" }, + { + "id": "REQ-QUALIFY005", + "title": "qualify.Load reads qualification report from disk", + "text": "qualify.Load shall read a qualification report from the given path, returning fusa.ErrNoConfig when the file does not exist and fusa.ErrInvalidConfig when the JSON is malformed.", + "standard": "IEC 61508", + "level": "SIL-2" + }, + { + "id": "REQ-QUALIFY006", + "title": "qualify.BuiltinCases returns non-empty test suite", + "text": "qualify.BuiltinCases shall return a non-empty slice of qualification test cases, with one positive (expects finding) and one negative (expects no finding) case for each registered engine rule.", + "standard": "IEC 61508", + "level": "SIL-2" + }, { "id": "REQ-RELEASE001", "title": "SBOM presence check", @@ -588,6 +602,20 @@ "standard": "ISO 26262", "level": "ASIL-A" }, + { + "id": "REQ-VERIFY006", + "title": "verify.Save persists evidence bundle as indented JSON", + "text": "verify.Save shall marshal the evidence bundle as indented JSON and write it to the specified path with a trailing newline, returning a non-nil error on marshal or write failure.", + "standard": "ISO 26262", + "level": "ASIL-A" + }, + { + "id": "REQ-VERIFY007", + "title": "verify.Load reads evidence bundle from disk", + "text": "verify.Load shall read an evidence bundle from the specified path, returning fusa.ErrNoConfig when the file does not exist and a descriptive error when the JSON is malformed.", + "standard": "ISO 26262", + "level": "ASIL-A" + }, { "id": "REQ-E2E001", "title": "Full pipeline executes successfully on a minimal Go project", diff --git a/CHANGELOG.md b/CHANGELOG.md index a745069..a9220a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ Dates reference the merged commit timestamp. ## [Unreleased] +## v0.33.0 — 2026-07-26 + +### Added +- **Maximize test coverage and requirement annotations** (audit-driven): + - `cmd/gofusa/cmd_v032_test.go`: CLI-level tests for `runTraceHLRLLR` (P0 — REQ-TRACE008), + `runCoverage --mcdc` (P2 — REQ-COV015), and `runQualify` new flags (P2 — REQ-QUALIFY007/008). + - `trace/trace_test.go`: `TestRender_Markdown_HLRLLRSummary` covers the HLR/LLR summary + section of `renderMarkdown` introduced in v0.32.0. + - `coverage/coverage_test.go`: `TestRenderText_DALD_ReqHelperNoBranch` exercises the `false` + branch of the unexported `req()` helper. + - `verify/verify_test.go`: `//fusa:test REQ-VERIFY006` and `//fusa:test REQ-VERIFY007` + annotations added to `TestSaveAndLoad_Roundtrip`. + - `qualify/qualify_test.go`: `//fusa:test REQ-QUALIFY005` on `TestSaveAndLoad_Roundtrip`, + `//fusa:test REQ-QUALIFY006` on `TestBuiltinCases_NonEmpty`. +- **Four new requirements registered** in `.fusa-reqs.json`: + REQ-VERIFY006 (`verify.Save`), REQ-VERIFY007 (`verify.Load`), + REQ-QUALIFY005 (`qualify.Load`), REQ-QUALIFY006 (`qualify.BuiltinCases`). + ## v0.32.0 — 2026-07-26 ### Added diff --git a/cmd/gofusa/cmd_v032_test.go b/cmd/gofusa/cmd_v032_test.go new file mode 100644 index 0000000..19656b4 --- /dev/null +++ b/cmd/gofusa/cmd_v032_test.go @@ -0,0 +1,372 @@ +package main + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + fusa "github.com/SoundMatt/go-FuSa" + "github.com/SoundMatt/go-FuSa/trace" +) + +// ─── runTraceHLRLLR (P0: REQ-TRACE008) ─────────────────────────────────────── + +// buildHLRLLRMatrix constructs a trace.Matrix for a project directory. +func buildHLRLLRMatrix(t *testing.T, dir string) *trace.Matrix { + t.Helper() + m, err := trace.Build(dir) + if err != nil { + t.Fatalf("trace.Build: %v", err) + } + return m +} + +//fusa:test REQ-TRACE008 +func TestRunTraceHLRLLR_NoViolations_ExitOK(t *testing.T) { + // A well-formed HLR/LLR project: one HLR with one valid LLR child. + dir := t.TempDir() + reqs := []trace.Requirement{ + {ID: "REQ-HLR-001", Title: "High-level requirement", Level: "HLR"}, + {ID: "REQ-LLR-001", Title: "Low-level requirement", Level: "LLR", ParentID: "REQ-HLR-001"}, + } + if err := trace.SaveRequirements(dir, reqs); err != nil { + t.Fatalf("SaveRequirements: %v", err) + } + m := buildHLRLLRMatrix(t, dir) + + var out, errOut bytes.Buffer + code := runTraceHLRLLR(m, &out, &errOut) + if code != fusa.ExitOK { + t.Errorf("expected ExitOK (%d), got %d\nstdout: %s\nstderr: %s", + fusa.ExitOK, code, out.String(), errOut.String()) + } + if !strings.Contains(out.String(), "HLR/LLR Decomposition") { + t.Errorf("stdout should contain decomposition header; got: %s", out.String()) + } +} + +//fusa:test REQ-TRACE008 +func TestRunTraceHLRLLR_OrphanedLLR_ExitGateFail(t *testing.T) { + // LLR with a parentId that does not match any HLR → orphaned. + dir := t.TempDir() + reqs := []trace.Requirement{ + {ID: "REQ-HLR-001", Title: "High-level requirement", Level: "HLR"}, + {ID: "REQ-LLR-001", Title: "Low-level requirement", Level: "LLR", ParentID: "REQ-HLR-MISSING"}, + } + if err := trace.SaveRequirements(dir, reqs); err != nil { + t.Fatalf("SaveRequirements: %v", err) + } + m := buildHLRLLRMatrix(t, dir) + + var out, errOut bytes.Buffer + code := runTraceHLRLLR(m, &out, &errOut) + if code != fusa.ExitGateFail { + t.Errorf("expected ExitGateFail (%d), got %d\nstdout: %s\nstderr: %s", + fusa.ExitGateFail, code, out.String(), errOut.String()) + } + if !strings.Contains(out.String(), "ORPHANED LLR") { + t.Errorf("stdout should mention orphaned LLR; got: %s", out.String()) + } +} + +//fusa:test REQ-TRACE008 +func TestRunTraceHLRLLR_UncoveredHLR_ExitGateFail(t *testing.T) { + // HLR with no LLR children → uncovered. + dir := t.TempDir() + reqs := []trace.Requirement{ + {ID: "REQ-HLR-001", Title: "High-level requirement, no children", Level: "HLR"}, + } + if err := trace.SaveRequirements(dir, reqs); err != nil { + t.Fatalf("SaveRequirements: %v", err) + } + m := buildHLRLLRMatrix(t, dir) + + var out, errOut bytes.Buffer + code := runTraceHLRLLR(m, &out, &errOut) + if code != fusa.ExitGateFail { + t.Errorf("expected ExitGateFail (%d), got %d\nstdout: %s\nstderr: %s", + fusa.ExitGateFail, code, out.String(), errOut.String()) + } + if !strings.Contains(out.String(), "UNCOVERED HLR") { + t.Errorf("stdout should mention uncovered HLR; got: %s", out.String()) + } +} + +//fusa:test REQ-TRACE008 +func TestRunTraceHLRLLR_NoHierarchicalReqs_ExitOK(t *testing.T) { + // No HLR/LLR levels → HLRLLRSummary is nil → exit OK. + dir := t.TempDir() + reqs := []trace.Requirement{ + {ID: "REQ-001", Title: "Flat requirement (no Level)"}, + } + if err := trace.SaveRequirements(dir, reqs); err != nil { + t.Fatalf("SaveRequirements: %v", err) + } + m := buildHLRLLRMatrix(t, dir) + + var out, errOut bytes.Buffer + code := runTraceHLRLLR(m, &out, &errOut) + if code != fusa.ExitOK { + t.Errorf("expected ExitOK (%d) for flat reqs, got %d\nstdout: %s\nstderr: %s", + fusa.ExitOK, code, out.String(), errOut.String()) + } + if !strings.Contains(out.String(), "no hierarchical requirements") { + t.Errorf("stdout should note absence of hierarchical requirements; got: %s", out.String()) + } +} + +//fusa:test REQ-TRACE008 +func TestRun_Trace_StrictHLRLLR_GateFail(t *testing.T) { + // CLI integration: --strict-hlr-llr flag with an uncovered HLR exits 1. + dir := t.TempDir() + reqs := []trace.Requirement{ + {ID: "REQ-HLR-001", Title: "Uncovered HLR", Level: "HLR"}, + } + if err := trace.SaveRequirements(dir, reqs); err != nil { + t.Fatalf("SaveRequirements: %v", err) + } + var out, errOut bytes.Buffer + code := run([]string{"trace", "--dir", dir, "--strict-hlr-llr"}, &out, &errOut) + if code != fusa.ExitGateFail { + t.Errorf("trace --strict-hlr-llr with uncovered HLR: expected ExitGateFail (%d), got %d\nstdout: %s\nstderr: %s", + fusa.ExitGateFail, code, out.String(), errOut.String()) + } +} + +//fusa:test REQ-TRACE008 +func TestRun_Trace_StrictHLRLLR_Pass(t *testing.T) { + // CLI integration: --strict-hlr-llr flag with a fully covered HLR/LLR tree exits 0. + dir := t.TempDir() + reqs := []trace.Requirement{ + {ID: "REQ-HLR-001", Title: "Covered HLR", Level: "HLR"}, + {ID: "REQ-LLR-001", Title: "Valid LLR", Level: "LLR", ParentID: "REQ-HLR-001"}, + } + if err := trace.SaveRequirements(dir, reqs); err != nil { + t.Fatalf("SaveRequirements: %v", err) + } + var out, errOut bytes.Buffer + code := run([]string{"trace", "--dir", dir, "--strict-hlr-llr"}, &out, &errOut) + if code != fusa.ExitOK { + t.Errorf("trace --strict-hlr-llr with valid tree: expected ExitOK (%d), got %d\nstdout: %s\nstderr: %s", + fusa.ExitOK, code, out.String(), errOut.String()) + } +} + +// ─── runCoverage --mcdc (P2: REQ-COV015) ───────────────────────────────────── + +//fusa:test REQ-COV015 +func TestRunCoverage_MCDC_MissingFile_ExitUsage(t *testing.T) { + // --mcdc without --mcdc-file must fail with ExitUsage. + dir := t.TempDir() + // Write a minimal coverage profile so the main coverage path succeeds. + profilePath := filepath.Join(dir, "coverage.out") + profileContent := "mode: set\ngithub.com/example/foo/bar.go:1.30,2.2 1 1\n" + if err := os.WriteFile(profilePath, []byte(profileContent), 0o640); err != nil { + t.Fatal(err) + } + var out, errOut bytes.Buffer + code := runCoverage([]string{"--mcdc", profilePath}, &out, &errOut) + if code != fusa.ExitUsage { + t.Errorf("--mcdc without --mcdc-file: expected ExitUsage (%d), got %d\nstderr: %s", + fusa.ExitUsage, code, errOut.String()) + } + if !strings.Contains(errOut.String(), "--mcdc-file") { + t.Errorf("stderr should mention --mcdc-file; got: %s", errOut.String()) + } +} + +//fusa:test REQ-COV015 +func TestRunCoverage_MCDC_BelowThreshold_ExitGateFail(t *testing.T) { + // --mcdc with a valid LLVM JSON file where coverage is below threshold → ExitGateFail. + dir := t.TempDir() + + // Write a minimal Go coverage profile. + profilePath := filepath.Join(dir, "coverage.out") + profileContent := "mode: set\ngithub.com/example/foo/bar.go:1.30,2.2 1 1\n" + if err := os.WriteFile(profilePath, []byte(profileContent), 0o640); err != nil { + t.Fatal(err) + } + + // Write a synthetic LLVM coverage JSON with one condition that has covered_true_count=0 + // (uncovered), so coverage is below the default 100% threshold. + mcdcJSON := map[string]interface{}{ + "data": []map[string]interface{}{ + { + "functions": []map[string]interface{}{ + { + "name": "doThing", + "mcdc_records": []map[string]interface{}{ + { + "conditions": []map[string]interface{}{ + { + "covered_true_count": 0, + "covered_false_count": 1, + }, + }, + }, + }, + }, + }, + }, + }, + } + mcdcData, err := json.Marshal(mcdcJSON) + if err != nil { + t.Fatalf("marshal mcdc json: %v", err) + } + mcdcPath := filepath.Join(dir, "mcdc.json") + if err := os.WriteFile(mcdcPath, mcdcData, 0o640); err != nil { + t.Fatal(err) + } + + var out, errOut bytes.Buffer + code := runCoverage([]string{"--mcdc", "--mcdc-file", mcdcPath, "--mcdc-threshold", "100", profilePath}, &out, &errOut) + if code != fusa.ExitGateFail { + t.Errorf("--mcdc below threshold: expected ExitGateFail (%d), got %d\nstdout: %s\nstderr: %s", + fusa.ExitGateFail, code, out.String(), errOut.String()) + } +} + +//fusa:test REQ-COV015 +func TestRunCoverage_MCDC_FullyCovered_ExitOK(t *testing.T) { + // --mcdc with a valid LLVM JSON file where all conditions are covered → ExitOK. + dir := t.TempDir() + + profilePath := filepath.Join(dir, "coverage.out") + profileContent := "mode: set\ngithub.com/example/foo/bar.go:1.30,2.2 1 1\n" + if err := os.WriteFile(profilePath, []byte(profileContent), 0o640); err != nil { + t.Fatal(err) + } + + // All conditions covered: covered_true_count>0 AND covered_false_count>0. + mcdcJSON := map[string]interface{}{ + "data": []map[string]interface{}{ + { + "functions": []map[string]interface{}{ + { + "name": "doThing", + "mcdc_records": []map[string]interface{}{ + { + "conditions": []map[string]interface{}{ + { + "covered_true_count": 2, + "covered_false_count": 3, + }, + }, + }, + }, + }, + }, + }, + }, + } + mcdcData, err := json.Marshal(mcdcJSON) + if err != nil { + t.Fatalf("marshal mcdc json: %v", err) + } + mcdcPath := filepath.Join(dir, "mcdc.json") + if err := os.WriteFile(mcdcPath, mcdcData, 0o640); err != nil { + t.Fatal(err) + } + + var out, errOut bytes.Buffer + code := runCoverage([]string{"--mcdc", "--mcdc-file", mcdcPath, "--mcdc-threshold", "100", profilePath}, &out, &errOut) + if code != fusa.ExitOK { + t.Errorf("--mcdc fully covered: expected ExitOK (%d), got %d\nstdout: %s\nstderr: %s", + fusa.ExitOK, code, out.String(), errOut.String()) + } +} + +// ─── runQualify new flags (P2: REQ-QUALIFY007, REQ-QUALIFY008) ──────────────── + +//fusa:test REQ-QUALIFY007 +//fusa:test REQ-QUALIFY008 +func TestRunQualify_NewFlags_PopulateReport(t *testing.T) { + // --qualification-method, --qualifier, --record-uri, --independent-reviewer, + // --independent-test-executor, --achievable-asil should all appear in the + // saved qualification report. + outDir := t.TempDir() + reportPath := filepath.Join(outDir, "qualify-report.json") + + var out, errOut bytes.Buffer + code := runQualify([]string{ + "--output", reportPath, + "--qualification-method", "independent", + "--qualifier", "SafetyLabs Inc.", + "--record-uri", "https://example.com/dossier", + "--independent-reviewer", "Eve", + "--independent-test-executor", "Frank", + "--achievable-asil", "ASIL-D", + }, &out, &errOut) + if code != fusa.ExitOK { + t.Fatalf("runQualify with flags: exit %d\nstdout: %s\nstderr: %s", + code, out.String(), errOut.String()) + } + + data, err := os.ReadFile(reportPath) + if err != nil { + t.Fatalf("read report: %v", err) + } + + type minReport struct { + QualificationMethod string `json:"qualificationMethod"` + QualifierIdentity string `json:"qualifierIdentity"` + QualificationRecordUri string `json:"qualificationRecordUri"` + IndependentReviewer string `json:"independentReviewer"` + IndependentTestExecutor string `json:"independentTestExecutor"` + AchievableASIL string `json:"achievableAsil"` + } + var rep minReport + if err := json.Unmarshal(data, &rep); err != nil { + t.Fatalf("unmarshal report: %v", err) + } + if rep.QualificationMethod != "independent" { + t.Errorf("QualificationMethod = %q, want \"independent\"", rep.QualificationMethod) + } + if rep.QualifierIdentity != "SafetyLabs Inc." { + t.Errorf("QualifierIdentity = %q, want \"SafetyLabs Inc.\"", rep.QualifierIdentity) + } + if rep.QualificationRecordUri != "https://example.com/dossier" { + t.Errorf("QualificationRecordUri = %q, want \"https://example.com/dossier\"", rep.QualificationRecordUri) + } + if rep.IndependentReviewer != "Eve" { + t.Errorf("IndependentReviewer = %q, want \"Eve\"", rep.IndependentReviewer) + } + if rep.IndependentTestExecutor != "Frank" { + t.Errorf("IndependentTestExecutor = %q, want \"Frank\"", rep.IndependentTestExecutor) + } + if rep.AchievableASIL != "ASIL-D" { + t.Errorf("AchievableASIL = %q, want \"ASIL-D\"", rep.AchievableASIL) + } + + // Verify badge and independence status appear in stdout. + outStr := out.String() + if !strings.Contains(outStr, "independently-qualified") { + t.Errorf("stdout should contain 'independently-qualified' badge; got: %s", outStr) + } + if !strings.Contains(outStr, "independent") { + t.Errorf("stdout should contain independence status; got: %s", outStr) + } +} + +//fusa:test REQ-QUALIFY007 +//fusa:test REQ-QUALIFY008 +func TestRunQualify_NoFlags_DefaultsPopulateStdout(t *testing.T) { + // Without new flags, badge should be "unqualified" and independence "unqualified". + outDir := t.TempDir() + reportPath := filepath.Join(outDir, "qualify-report.json") + + var out, errOut bytes.Buffer + code := runQualify([]string{"--output", reportPath}, &out, &errOut) + if code != fusa.ExitOK { + t.Fatalf("runQualify no flags: exit %d\nstdout: %s\nstderr: %s", + code, out.String(), errOut.String()) + } + outStr := out.String() + if !strings.Contains(outStr, "unqualified") { + t.Errorf("stdout should show 'unqualified' badge when no flags set; got: %s", outStr) + } +} diff --git a/coverage/coverage_test.go b/coverage/coverage_test.go index ebea741..0cba7ae 100644 --- a/coverage/coverage_test.go +++ b/coverage/coverage_test.go @@ -512,6 +512,25 @@ func TestParseMCDC_ThresholdPartialCoverage(t *testing.T) { } } +// TestRenderText_DAL_D exercises the req() helper's "no" branch: DAL-D has no +// required coverage metrics, so all req(false) calls return "no". +func TestRenderText_DALD_ReqHelperNoBranch(t *testing.T) { + blocks := []coverage.Block{{File: "main.go", StartLine: 1, EndLine: 5, Stmts: 3, Count: 3}} + rep := coverage.Analyse(blocks, coverage.DALD) + var buf bytes.Buffer + if err := coverage.Render(&buf, rep, "text"); err != nil { + t.Fatalf("Render DAL-D: %v", err) + } + out := buf.String() + if !strings.Contains(out, "DAL-D") { + t.Errorf("expected DAL-D in output; got: %s", out) + } + // DAL-D has no required metrics; "no" appears for statement and decision coverage. + if !strings.Contains(out, "no") { + t.Errorf("expected 'no' for non-required metrics at DAL-D; got: %s", out) + } +} + //fusa:test REQ-COV015 func TestRenderText_MCDCReport(t *testing.T) { blocks := []coverage.Block{{File: "foo.go", StartLine: 1, EndLine: 10, Stmts: 5, Count: 5}} diff --git a/fusa.go b/fusa.go index 41cca31..f8c31ba 100644 --- a/fusa.go +++ b/fusa.go @@ -15,7 +15,7 @@ import ( ) // Version is the current release of go-FuSa. -const Version = "0.32.0" +const Version = "0.33.0" // SpecVersion is the x-FuSa spec version this release implements. const SpecVersion = "1.10.4" diff --git a/qualify/qualify_test.go b/qualify/qualify_test.go index b0c829f..647e029 100644 --- a/qualify/qualify_test.go +++ b/qualify/qualify_test.go @@ -21,6 +21,7 @@ import ( // ─── BuiltinCases ───────────────────────────────────────────────────────────── +//fusa:test REQ-QUALIFY006 func TestBuiltinCases_NonEmpty(t *testing.T) { cases := qualify.BuiltinCases() if len(cases) == 0 { @@ -116,6 +117,7 @@ func TestHasFailures(t *testing.T) { // ─── Save / Load ────────────────────────────────────────────────────────────── +//fusa:test REQ-QUALIFY005 func TestSaveAndLoad_Roundtrip(t *testing.T) { cases := qualify.BuiltinCases()[:2] report, err := qualify.Run(context.Background(), engine.Default, cases) diff --git a/trace/trace_test.go b/trace/trace_test.go index 9599d29..d3f9ca5 100644 --- a/trace/trace_test.go +++ b/trace/trace_test.go @@ -328,6 +328,38 @@ func TestRender_Markdown_NoRequirements(t *testing.T) { } } +//fusa:test REQ-TRACE-MD001 +//fusa:test REQ-TRACE008 +func TestRender_Markdown_HLRLLRSummary(t *testing.T) { + // Build a matrix with an HLRLLRSummary populated so the markdown + // renderer exercises the HLR/LLR summary section added in v0.32.0. + m := &trace.Matrix{ + HLRLLRSummary: &trace.HLRLLRSummary{ + HLRCount: 2, + LLRCount: 3, + Orphaned: []string{"REQ-LLR-X"}, + Uncovered: []string{"REQ-HLR-Y"}, + }, + } + var sb strings.Builder + if err := trace.Render(&sb, m, "md"); err != nil { + t.Fatalf("Render md with HLRLLRSummary: %v", err) + } + out := sb.String() + if !strings.Contains(out, "HLRs") { + t.Errorf("markdown should contain HLR count row; got:\n%s", out) + } + if !strings.Contains(out, "LLRs") { + t.Errorf("markdown should contain LLR count row; got:\n%s", out) + } + if !strings.Contains(out, "Orphaned LLRs") { + t.Errorf("markdown should contain Orphaned LLRs row; got:\n%s", out) + } + if !strings.Contains(out, "Uncovered HLRs") { + t.Errorf("markdown should contain Uncovered HLRs row; got:\n%s", out) + } +} + // ─── Engine rules ───────────────────────────────────────────────────────────── //fusa:test REQ-TRACE001 diff --git a/verify/verify_test.go b/verify/verify_test.go index 2118c3e..8ccabe4 100644 --- a/verify/verify_test.go +++ b/verify/verify_test.go @@ -112,6 +112,8 @@ func TestNewBundle(t *testing.T) { } } +//fusa:test REQ-VERIFY006 +//fusa:test REQ-VERIFY007 func TestSaveAndLoad_Roundtrip(t *testing.T) { dir := t.TempDir() results, err := verify.Parse(strings.NewReader(sampleJSON)) From 16f2abefd2894f3d1593045d5bed849b0f293689 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Sun, 26 Jul 2026 09:01:52 -0700 Subject: [PATCH 2/2] docs: update version references to 0.33.0 Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- README.md | 2 +- docs/tool-safety-manual.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4d5eabb..e4c8b9e 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,7 @@ docker build -t go-fusa . docker run --rm -v "$(pwd)":/project go-fusa check ``` -Published tags: `latest`, `0.32`, `0.32.0` (and matching semver for every release). +Published tags: `latest`, `0.33`, `0.33.0` (and matching semver for every release). ## Standards coverage diff --git a/docs/tool-safety-manual.md b/docs/tool-safety-manual.md index 33acf51..0d9904d 100644 --- a/docs/tool-safety-manual.md +++ b/docs/tool-safety-manual.md @@ -1,6 +1,6 @@ # go-FuSa Tool Safety Manual -**Version:** 0.32.0 +**Version:** 0.33.0 **Module:** `github.com/SoundMatt/go-FuSa` **License:** Mozilla Public License 2.0 **Standards addressed:** ISO 26262, IEC 61508, ISO 21434, DO-178C