Skip to content

Commit df3c373

Browse files
feat: add tests for validation, scanning and ui packages
1 parent fcc3efc commit df3c373

File tree

5 files changed

+287
-113
lines changed

5 files changed

+287
-113
lines changed

internal/completeness/completeness_test.go

Lines changed: 0 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
package completeness
22

33
import (
4-
"os"
5-
"path/filepath"
64
"testing"
75

86
"github.com/idlab-discover/AIBoMGen-cli/internal/fetcher"
9-
bomio "github.com/idlab-discover/AIBoMGen-cli/internal/io"
107
"github.com/idlab-discover/AIBoMGen-cli/internal/metadata"
118
"github.com/idlab-discover/AIBoMGen-cli/internal/scanner"
129

@@ -88,31 +85,6 @@ func buildFullyPopulatedBOMForRegistry(t *testing.T) *cdx.BOM {
8885
return bom
8986
}
9087

91-
func writeBOMFile(t *testing.T, path string, format cdx.BOMFileFormat) {
92-
t.Helper()
93-
94-
bom := cdx.NewBOM()
95-
bom.Metadata = &cdx.Metadata{
96-
Component: &cdx.Component{
97-
Type: cdx.ComponentTypeMachineLearningModel,
98-
Name: "org/model",
99-
},
100-
}
101-
102-
f, err := os.Create(path)
103-
if err != nil {
104-
t.Fatalf("create file: %v", err)
105-
}
106-
t.Cleanup(func() { _ = f.Close() })
107-
108-
enc := cdx.NewBOMEncoder(f, format)
109-
enc.SetPretty(true)
110-
111-
if err := enc.Encode(bom); err != nil {
112-
t.Fatalf("encode bom: %v", err)
113-
}
114-
}
115-
11688
// Test Check function on empty BOM
11789
func TestCheck_EmptyBOM_MissingRequiredMatchesRegistry(t *testing.T) {
11890
r := Check(&cdx.BOM{})
@@ -164,76 +136,3 @@ func TestCheck_FullyPopulatedBOM_ScoreIsOne(t *testing.T) {
164136
t.Fatalf("Score = %v, want ~1.0", r.Score)
165137
}
166138
}
167-
168-
// Test ReadBOM function with various formats and error cases
169-
func TestReadBOM_JSON_ExplicitFormat(t *testing.T) {
170-
dir := t.TempDir()
171-
p := filepath.Join(dir, "bom.json")
172-
writeBOMFile(t, p, cdx.BOMFileFormatJSON)
173-
174-
b, err := bomio.ReadBOM(p, "json")
175-
if err != nil {
176-
t.Fatalf("ReadBOM err = %v", err)
177-
}
178-
if b == nil || b.Metadata == nil || b.Metadata.Component == nil {
179-
t.Fatalf("decoded bom missing metadata/component")
180-
}
181-
}
182-
func TestReadBOM_XML_ExplicitFormat(t *testing.T) {
183-
dir := t.TempDir()
184-
p := filepath.Join(dir, "bom.xml")
185-
writeBOMFile(t, p, cdx.BOMFileFormatXML)
186-
187-
b, err := bomio.ReadBOM(p, "xml")
188-
if err != nil {
189-
t.Fatalf("ReadBOM err = %v", err)
190-
}
191-
if b == nil || b.Metadata == nil || b.Metadata.Component == nil {
192-
t.Fatalf("decoded bom missing metadata/component")
193-
}
194-
}
195-
func TestReadBOM_AutoDetectsByExtension(t *testing.T) {
196-
dir := t.TempDir()
197-
198-
pJSON := filepath.Join(dir, "bom.json")
199-
writeBOMFile(t, pJSON, cdx.BOMFileFormatJSON)
200-
if _, err := bomio.ReadBOM(pJSON, "auto"); err != nil {
201-
t.Fatalf("ReadBOM(json, auto) err = %v", err)
202-
}
203-
204-
pXML := filepath.Join(dir, "bom.xml")
205-
writeBOMFile(t, pXML, cdx.BOMFileFormatXML)
206-
if _, err := bomio.ReadBOM(pXML, ""); err != nil { // empty behaves like auto
207-
t.Fatalf("ReadBOM(xml, empty) err = %v", err)
208-
}
209-
}
210-
211-
// Test ReadBOM function error cases
212-
func TestReadBOM_InvalidPath_ReturnsError(t *testing.T) {
213-
if _, err := bomio.ReadBOM("/definitely/does/not/exist/lol.json", "json"); err == nil {
214-
t.Fatalf("expected error for missing file")
215-
}
216-
}
217-
218-
// Test ReadBOM function error cases
219-
func TestReadBOM_InvalidContent_ReturnsDecodeError(t *testing.T) {
220-
dir := t.TempDir()
221-
p := filepath.Join(dir, "bad.json")
222-
if err := os.WriteFile(p, []byte("{not valid json"), 0o644); err != nil {
223-
t.Fatalf("write file: %v", err)
224-
}
225-
226-
if _, err := bomio.ReadBOM(p, "json"); err == nil {
227-
t.Fatalf("expected decode error")
228-
}
229-
}
230-
231-
func TestReadBOM_FormatMismatch_ReturnsDecodeError(t *testing.T) {
232-
dir := t.TempDir()
233-
p := filepath.Join(dir, "bom.json")
234-
writeBOMFile(t, p, cdx.BOMFileFormatJSON)
235-
236-
if _, err := bomio.ReadBOM(p, "xml"); err == nil {
237-
t.Fatalf("expected decode error when decoding json as xml")
238-
}
239-
}

internal/scanner/scanner_test.go

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,120 @@
11
package scanner
22

33
import (
4+
"bytes"
45
"os"
56
"path/filepath"
7+
"strings"
68
"testing"
9+
10+
"github.com/idlab-discover/AIBoMGen-cli/internal/ui"
711
)
812

9-
func TestScanDetectsModelAndWeight(t *testing.T) {
13+
func TestScanDetectsModelsDedupesAndLogs(t *testing.T) {
14+
ui.Init(true)
1015
dir := t.TempDir()
11-
12-
// Create a python file with a HF model reference
1316
pyPath := filepath.Join(dir, "use_model.py")
14-
pyContent := `from transformers import AutoModel\nAutoModel.from_pretrained("bert-base-uncased")`
17+
pyContent := "from transformers import AutoModel\n" +
18+
"AutoModel.from_pretrained(\"bert-base-uncased\")\n" +
19+
"AutoModel.from_pretrained(\"bert-base-uncased\")\n"
1520
if err := os.WriteFile(pyPath, []byte(pyContent), 0o644); err != nil {
16-
t.Fatalf("failed to create python file: %v", err)
21+
t.Fatalf("write python file: %v", err)
1722
}
1823

19-
// Weight-file detection disabled; only verify model detection.
24+
var buf bytes.Buffer
25+
SetLogger(&buf)
26+
t.Cleanup(func() { SetLogger(nil) })
27+
28+
comps, err := Scan(dir)
29+
if err != nil {
30+
t.Fatalf("Scan failed: %v", err)
31+
}
32+
if len(comps) != 1 {
33+
t.Fatalf("expected 1 component, got %d", len(comps))
34+
}
35+
if !strings.Contains(comps[0].Evidence, "line 2") || !strings.Contains(comps[0].Evidence, "line 3") {
36+
t.Fatalf("expected evidence to include both occurrences, got %q", comps[0].Evidence)
37+
}
38+
39+
logs := buf.String()
40+
if !strings.Contains(logs, "found model 'bert-base-uncased'") {
41+
t.Fatalf("expected detection log, got %q", logs)
42+
}
43+
if !strings.Contains(logs, "detected 1 components (models: 1)") {
44+
t.Fatalf("expected summary log, got %q", logs)
45+
}
46+
}
47+
48+
func TestScanSkipsUnreadableFiles(t *testing.T) {
49+
dir := t.TempDir()
50+
pyPath := filepath.Join(dir, "blocked.py")
51+
if err := os.WriteFile(pyPath, []byte("AutoModel.from_pretrained(\"bert\")"), 0o644); err != nil {
52+
t.Fatalf("write python file: %v", err)
53+
}
54+
if err := os.Chmod(pyPath, 0o000); err != nil {
55+
t.Fatalf("chmod file: %v", err)
56+
}
57+
defer func() { _ = os.Chmod(pyPath, 0o644) }()
2058

2159
comps, err := Scan(dir)
2260
if err != nil {
2361
t.Fatalf("Scan failed: %v", err)
2462
}
25-
var foundModel bool
26-
for _, c := range comps {
27-
if c.Type == "model" && c.Name == "bert-base-uncased" {
28-
foundModel = true
63+
if len(comps) != 0 {
64+
t.Fatalf("expected no components for unreadable files, got %d", len(comps))
65+
}
66+
}
67+
68+
func TestScanInvalidRootReturnsError(t *testing.T) {
69+
missing := filepath.Join(t.TempDir(), "does-not-exist")
70+
if _, err := Scan(missing); err == nil {
71+
t.Fatalf("expected error for missing root")
72+
}
73+
}
74+
75+
func TestShouldScanForModelID(t *testing.T) {
76+
tests := []struct {
77+
ext string
78+
want bool
79+
}{
80+
{ext: ".py", want: true},
81+
{ext: ".ipynb", want: true},
82+
{ext: ".txt", want: true},
83+
{ext: ".md", want: false},
84+
}
85+
for _, tt := range tests {
86+
if got := shouldScanForModelID(tt.ext); got != tt.want {
87+
t.Fatalf("shouldScanForModelID(%q) = %t, want %t", tt.ext, got, tt.want)
2988
}
3089
}
31-
if !foundModel {
32-
t.Errorf("expected model component for bert-base-uncased")
90+
}
91+
92+
func TestDedupeMergesEvidence(t *testing.T) {
93+
components := []Discovery{
94+
{ID: "bert", Type: "model", Evidence: "line 2"},
95+
{ID: "bert", Type: "model", Evidence: "line 3"},
96+
{ID: "bert", Type: "model", Evidence: "line 3"},
97+
{ID: "other", Type: "model", Evidence: "line 5"},
98+
}
99+
100+
deduped := dedupe(components)
101+
if len(deduped) != 2 {
102+
t.Fatalf("expected 2 unique components, got %d", len(deduped))
103+
}
104+
105+
var merged Discovery
106+
for _, c := range deduped {
107+
if c.ID == "bert" {
108+
merged = c
109+
}
110+
}
111+
if merged.ID == "" {
112+
t.Fatalf("expected bert component after dedupe")
113+
}
114+
if !strings.Contains(merged.Evidence, "line 2") {
115+
t.Fatalf("expected merged evidence to include line 2, got %q", merged.Evidence)
116+
}
117+
if strings.Count(merged.Evidence, "line 3") != 1 {
118+
t.Fatalf("expected line 3 evidence once, got %q", merged.Evidence)
33119
}
34120
}

internal/ui/ui_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package ui
2+
3+
import "testing"
4+
5+
func TestInitSetsEnabledFlag(t *testing.T) {
6+
prev := Enabled
7+
t.Cleanup(func() { Enabled = prev })
8+
9+
Enabled = true
10+
Init(true)
11+
if Enabled {
12+
t.Fatalf("Init(true) should disable colors")
13+
}
14+
15+
Init(false)
16+
if !Enabled {
17+
t.Fatalf("Init(false) should enable colors")
18+
}
19+
}
20+
21+
func TestColorRespectsEnabledFlag(t *testing.T) {
22+
prev := Enabled
23+
t.Cleanup(func() { Enabled = prev })
24+
25+
Enabled = true
26+
got := Color("hello", FgGreen)
27+
want := FgGreen + "hello" + Reset
28+
if got != want {
29+
t.Fatalf("Color enabled = %q, want %q", got, want)
30+
}
31+
32+
Enabled = false
33+
if got := Color("world", FgMagenta); got != "world" {
34+
t.Fatalf("Color disabled = %q, want plain string", got)
35+
}
36+
}

internal/validator/report_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package validator
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/idlab-discover/AIBoMGen-cli/internal/metadata"
8+
"github.com/idlab-discover/AIBoMGen-cli/internal/ui"
9+
)
10+
11+
func TestPrintReport_UsesConfiguredLoggerWriter(t *testing.T) {
12+
ui.Init(true)
13+
14+
var buf bytes.Buffer
15+
SetLogger(&buf)
16+
t.Cleanup(func() { SetLogger(nil) })
17+
18+
PrintReport(ValidationResult{
19+
Valid: false,
20+
Errors: []string{"required field missing"},
21+
Warnings: []string{"optional field missing"},
22+
CompletenessScore: 0.5,
23+
MissingRequired: []metadata.Key{metadata.ComponentName},
24+
MissingOptional: []metadata.Key{metadata.ComponentTags, metadata.ComponentLicenses},
25+
})
26+
27+
got := buf.String()
28+
want := "Validation Report: ❌ validation failed\n" +
29+
"Validation Report: errors (1):\n" +
30+
"Validation Report: • required field missing\n" +
31+
"Validation Report: warnings (1):\n" +
32+
"Validation Report: • optional field missing\n" +
33+
"Validation Report: completeness score: 50.0% (1 required, 2 optional missing)\n"
34+
35+
if got != want {
36+
t.Fatalf("output = %q, want %q", got, want)
37+
}
38+
}
39+
40+
func TestPrintReport_NoLoggerWriter_DoesNothing(t *testing.T) {
41+
ui.Init(true)
42+
43+
SetLogger(nil)
44+
PrintReport(ValidationResult{Valid: true})
45+
}
46+
47+
func TestFormatSummary(t *testing.T) {
48+
tests := []struct {
49+
name string
50+
res ValidationResult
51+
want string
52+
}{
53+
{
54+
name: "passed",
55+
res: ValidationResult{
56+
Valid: true,
57+
CompletenessScore: 0.83,
58+
Errors: []string{"ignored"},
59+
Warnings: []string{"one", "two"},
60+
},
61+
want: "Validation: ✅ PASSED | Score: 83.0% | Errors: 1 | Warnings: 2",
62+
},
63+
{
64+
name: "failed",
65+
res: ValidationResult{
66+
Valid: false,
67+
CompletenessScore: 0.42,
68+
Errors: []string{"a", "b"},
69+
Warnings: []string{"c"},
70+
},
71+
want: "Validation: ❌ FAILED | Score: 42.0% | Errors: 2 | Warnings: 1",
72+
},
73+
}
74+
75+
for _, tt := range tests {
76+
t.Run(tt.name, func(t *testing.T) {
77+
if got := FormatSummary(tt.res); got != tt.want {
78+
t.Fatalf("FormatSummary() = %q, want %q", got, tt.want)
79+
}
80+
})
81+
}
82+
}

0 commit comments

Comments
 (0)