feat(cicontext): rules:changes: path-glob evaluation; 100% test coverage
- Add --changes PATH and --changes-from REF flags to glint check and glint graph
for rules:changes: evaluation. --changes marks files explicitly; --changes-from
runs git diff --name-only <REF> automatically. Both flags can be combined.
- Implement doublestar glob matching (*, ** across path segments) in EvalJob and
EvalWorkflow; extended {paths, compare_to} map form supported.
- Without --changes/--changes-from the condition stays permissive (existing behaviour).
- Context summary line now shows changed-file count when file data is provided.
- Achieve 100% statement coverage: comprehensive tests added across all packages;
removed provably dead code; added testability seams (exit, userHomeDirFn,
execCommandOutput variables) to cover previously unreachable paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,20 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.k3nny.fr/glint/internal/model"
|
||||
)
|
||||
|
||||
// errorYAMLMarshaler implements yaml.Marshaler and always returns an error,
|
||||
// allowing tests to trigger the yaml.Marshal failure path in Resolve.
|
||||
type errorYAMLMarshaler struct{}
|
||||
|
||||
func (e errorYAMLMarshaler) MarshalYAML() (interface{}, error) {
|
||||
return nil, fmt.Errorf("forced marshal error for test")
|
||||
}
|
||||
|
||||
// ── parseExtends ──────────────────────────────────────────────────────────────
|
||||
|
||||
func TestParseExtends(t *testing.T) {
|
||||
@@ -253,4 +262,32 @@ func TestResolve(t *testing.T) {
|
||||
t.Fatal("expected error for invalid extends type")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("yaml.Marshal fails — errorYAMLMarshaler injected into merged map", func(t *testing.T) {
|
||||
// Inject a value whose MarshalYAML() returns an error so yaml.Marshal
|
||||
// fails at extends.go:74-77.
|
||||
p := buildPipeline(map[string]map[string]any{
|
||||
".base": {"script": []any{"echo"}},
|
||||
"child": {"extends": ".base", "bad": errorYAMLMarshaler{}},
|
||||
})
|
||||
_, err := Resolve(p)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when yaml.Marshal fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("yaml.Unmarshal fails — map injected where []string expected", func(t *testing.T) {
|
||||
// Marshal succeeds (maps are valid YAML), but Unmarshal into model.Job
|
||||
// fails because Dependencies []string cannot hold a mapping.
|
||||
p := buildPipeline(map[string]map[string]any{
|
||||
".base": {
|
||||
"dependencies": map[string]any{"invalid": "not-a-list"},
|
||||
},
|
||||
"child": {"extends": ".base", "stage": "build"},
|
||||
})
|
||||
_, err := Resolve(p)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when yaml.Unmarshal fails on incompatible type")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -305,9 +305,6 @@ func substituteInputs(data []byte, inputs map[string]any) []byte {
|
||||
}
|
||||
return inputPlaceholderRe.ReplaceAllFunc(data, func(match []byte) []byte {
|
||||
groups := inputPlaceholderRe.FindSubmatch(match)
|
||||
if len(groups) < 2 {
|
||||
return match
|
||||
}
|
||||
if val, ok := inputs[string(groups[1])]; ok {
|
||||
return []byte(fmt.Sprintf("%v", val))
|
||||
}
|
||||
|
||||
@@ -644,6 +644,35 @@ func TestResolveProjectInclude_InvalidYAML(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveProjectInclude_WithSubIncludes covers includes.go:236-240 —
|
||||
// the fetched project YAML itself contains include: entries.
|
||||
func TestResolveProjectInclude_WithSubIncludes(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Write a local file that the project YAML sub-includes.
|
||||
localContent := "local-from-project:\n script: echo local\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "local.yml"), []byte(localContent), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
// Return YAML that itself has a local sub-include.
|
||||
fmt.Fprintln(w, "include:\n - local: /local.yml\nproj-sub-job:\n script: echo proj")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := &model.Pipeline{Jobs: map[string]model.Job{}, RawJobs: map[string]map[string]any{}}
|
||||
cfg := fetcher.GitLabConfig{BaseURL: srv.URL, Token: "tok"}
|
||||
entry := map[string]any{"project": "g/p", "file": "ci.yml", "ref": "main"}
|
||||
warnings, _ := resolveProjectInclude(p, entry, "g/p", cfg, dir, map[string]bool{}, 0)
|
||||
if len(warnings) != 0 {
|
||||
t.Errorf("sub-includes: unexpected warnings: %v", warnings)
|
||||
}
|
||||
if _, ok := p.Jobs["proj-sub-job"]; !ok {
|
||||
t.Error("proj-sub-job should be merged via project include")
|
||||
}
|
||||
}
|
||||
|
||||
// tlsComp sets up a TLS test server and swaps http.DefaultTransport so the test
|
||||
// client trusts the self-signed cert. Returns the host (without scheme).
|
||||
func tlsComp(t *testing.T, h http.HandlerFunc) (host string) {
|
||||
@@ -691,6 +720,33 @@ func TestResolveComponentInclude_InvalidYAML(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveComponentInclude_WithSubIncludes covers includes.go:288-291 —
|
||||
// the fetched component YAML itself contains include: entries.
|
||||
func TestResolveComponentInclude_WithSubIncludes(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
localContent := "comp-local-job:\n script: echo comp-local\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "sub.yml"), []byte(localContent), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
host := tlsComp(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
// Component YAML contains a local sub-include.
|
||||
fmt.Fprintln(w, "include:\n - local: /sub.yml\ncomp-job:\n script: echo comp")
|
||||
})
|
||||
ref := host + "/g/p/mycomp@v1"
|
||||
|
||||
p := &model.Pipeline{Jobs: map[string]model.Job{}, RawJobs: map[string]map[string]any{}}
|
||||
cfg := fetcher.GitLabConfig{}
|
||||
_, extW, hadErr := resolveComponentInclude(p, ref, nil, cfg, dir, map[string]bool{}, 0)
|
||||
if hadErr {
|
||||
t.Errorf("sub-includes: unexpected hadErr; extWarnings=%v", extW)
|
||||
}
|
||||
if _, ok := p.Jobs["comp-job"]; !ok {
|
||||
t.Error("comp-job should be merged via component include")
|
||||
}
|
||||
}
|
||||
|
||||
// ── resolveRemoteInclude — sub-includes ───────────────────────────────────────
|
||||
|
||||
func TestResolveRemoteInclude_WithSubIncludes(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user