Files
glint/internal/linter/inherit_completeness_test.go
k3nny 04f17f8616
ci / vet, staticcheck, test, build (push) Successful in 2m25s
test(coverage): add unit tests across all packages; remove dead code
- Added comprehensive table-driven test suites for all packages:
  cmd/glint, cicontext, fetcher, graph, linter, model, resolver.
  Coverage reaches 98%+ statement coverage across the codebase.
- Replaced os.Exit calls in cmd/glint with an `exit` variable so tests
  can capture exit codes without terminating the test process.
- Removed unreachable code found during coverage analysis:
  dead guard in cicontext.parseRegexLiteral; dead len(jobs)==0 branch
  in graph.Pipeline; skipWin struct field and dead continue in
  graph.convertToPNG; pipelineSVG return type simplified to string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 22:03:46 +02:00

166 lines
5.4 KiB
Go

package linter
import (
"testing"
"git.k3nny.fr/glint/internal/model"
)
func TestCheckInheritCompleteness(t *testing.T) {
cases := []struct {
name string
default_ *model.DefaultConfig
inherit any // job.Inherit value
wantHit bool
}{
{
name: "no inherit — no finding",
inherit: nil,
wantHit: false,
},
{
name: "inherit: default: false, no default block — GL043",
inherit: map[string]any{"default": false},
wantHit: true,
},
{
name: "inherit: default: true, no default block — GL043",
inherit: map[string]any{"default": true},
wantHit: true,
},
{
name: "inherit: default: [image], no default block — GL043",
inherit: map[string]any{"default": []any{"image"}},
wantHit: true,
},
{
name: "inherit: default: false, default block exists — no finding",
default_: &model.DefaultConfig{Image: "node:20"},
inherit: map[string]any{"default": false},
wantHit: false,
},
{
name: "inherit: default: [image], image in default — no finding",
default_: &model.DefaultConfig{Image: "node:20"},
inherit: map[string]any{"default": []any{"image"}},
wantHit: false,
},
{
name: "inherit: default: [before_script], before_script NOT in default — GL043",
default_: &model.DefaultConfig{Image: "node:20"},
inherit: map[string]any{"default": []any{"before_script"}},
wantHit: true,
},
{
name: "inherit: default: [image, before_script], only image in default — GL043 for before_script",
default_: &model.DefaultConfig{Image: "node:20"},
inherit: map[string]any{"default": []any{"image", "before_script"}},
wantHit: true,
},
{
name: "inherit: variables only — no default check",
default_: nil,
inherit: map[string]any{"variables": false},
wantHit: false,
},
{
name: "inherit: unknown field in list — conservative, no finding",
default_: &model.DefaultConfig{Image: "node:20"},
inherit: map[string]any{"default": []any{"nonexistent_field"}},
wantHit: false, // unknown fields return true from defaultBlockHasField
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
job := model.Job{Inherit: tc.inherit}
p := &model.Pipeline{
Default: tc.default_,
Jobs: map[string]model.Job{"test-job": job},
}
findings := checkInheritCompleteness(p)
hit := false
for _, f := range findings {
if f.Rule == RuleInheritNoDefault {
hit = true
}
}
if hit != tc.wantHit {
t.Errorf("GL043 hit=%v, want=%v; findings: %v", hit, tc.wantHit, findings)
}
})
}
}
// TestCheckInheritCompleteness_BoolInherit verifies that a scalar (non-map)
// inherit value (e.g. inherit: true) is silently ignored.
func TestCheckInheritCompleteness_BoolInherit(t *testing.T) {
job := model.Job{Inherit: true}
p := &model.Pipeline{
Default: nil,
Jobs: map[string]model.Job{"job": job},
}
if findings := checkInheritCompleteness(p); len(findings) != 0 {
t.Errorf("bool inherit should produce no findings; got %v", findings)
}
}
// TestCheckInheritCompleteness_ListItemNotString verifies that non-string items
// in the inherit: default: list are skipped.
func TestCheckInheritCompleteness_ListItemNotString(t *testing.T) {
// The list contains 42 (int) and "image" (string). Only "image" should be checked.
job := model.Job{Inherit: map[string]any{"default": []any{42, "image"}}}
p := &model.Pipeline{
Default: &model.DefaultConfig{Image: "node:20"},
Jobs: map[string]model.Job{"job": job},
}
// "image" IS set in default → no findings.
if findings := checkInheritCompleteness(p); len(findings) != 0 {
t.Errorf("non-string item should be skipped; got %v", findings)
}
}
// TestDefaultBlockHasField_AllFields exercises every field branch in
// defaultBlockHasField, including the ones not covered by the main table test.
func TestDefaultBlockHasField_AllFields(t *testing.T) {
cases := []struct {
name string
d *model.DefaultConfig
field string
want bool
}{
{"after_script set", &model.DefaultConfig{AfterScript: []any{"echo"}}, "after_script", true},
{"after_script nil", &model.DefaultConfig{}, "after_script", false},
{"cache set", &model.DefaultConfig{Cache: map[string]any{"key": "main"}}, "cache", true},
{"cache nil", &model.DefaultConfig{}, "cache", false},
{"artifacts set", &model.DefaultConfig{Artifacts: map[string]any{"paths": []any{"dist/"}}}, "artifacts", true},
{"artifacts nil", &model.DefaultConfig{}, "artifacts", false},
{"retry set", &model.DefaultConfig{Retry: 2}, "retry", true},
{"retry nil", &model.DefaultConfig{}, "retry", false},
{"timeout set", &model.DefaultConfig{Timeout: "30m"}, "timeout", true},
{"timeout empty", &model.DefaultConfig{}, "timeout", false},
{"tags set", &model.DefaultConfig{Tags: []string{"docker"}}, "tags", true},
{"tags empty", &model.DefaultConfig{}, "tags", false},
{"unknown field", &model.DefaultConfig{}, "unknown_field", true}, // conservative
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := defaultBlockHasField(tc.d, tc.field)
if got != tc.want {
t.Errorf("defaultBlockHasField(%q) = %v, want %v", tc.field, got, tc.want)
}
})
}
}
// TestPluralIs covers both branches of pluralIs.
func TestPluralIs(t *testing.T) {
if pluralIs(1) != "this field is" {
t.Errorf("pluralIs(1) = %q, want 'this field is'", pluralIs(1))
}
if pluralIs(2) != "these fields are" {
t.Errorf("pluralIs(2) = %q, want 'these fields are'", pluralIs(2))
}
}