helmfile/pkg/state/selector_include_needs_test.go

347 lines
9.7 KiB
Go

package state
import (
"testing"
"github.com/google/go-cmp/cmp"
)
// TestIncludeNeedsVsIncludeTransitiveNeeds demonstrates the difference between
// --include-needs and --include-transitive-needs flags.
//
// Behavior Summary:
// 1. --include-needs: Includes only DIRECT dependencies (immediate needs) of selected releases
// 2. --include-transitive-needs: Includes ALL dependencies including transitive ones (needs of needs)
//
// Example dependency graph:
//
// appA -> appB -> appC
// appA -> appD
//
// When selecting appA with:
// - No flags: Only appA (fails if needs are not satisfied)
// - --include-needs: appA, appB, appD (only direct needs)
// - --include-transitive-needs: appA, appB, appC, appD (all needs including transitive)
func TestIncludeNeedsVsIncludeTransitiveNeeds(t *testing.T) {
type testcase struct {
name string
selector []string
includeNeeds bool
includeTransitiveNeeds bool
want []string
}
// Dependency graph:
// appA needs [appB, appD]
// appB needs [appC]
// appC has no needs
// appD has no needs
// appE is independent (not in dependency chain)
testcases := []testcase{
{
name: "no include flags - only selected release",
selector: []string{"name=appA"},
includeNeeds: false,
includeTransitiveNeeds: false,
want: []string{"appA"},
},
{
name: "include-needs only - direct dependencies (appB, appD)",
selector: []string{"name=appA"},
includeNeeds: true,
includeTransitiveNeeds: false,
want: []string{"appA", "appB", "appD"},
},
{
name: "include-transitive-needs - all dependencies including transitive (appB, appC, appD)",
selector: []string{"name=appA"},
includeNeeds: false, // Note: includeTransitiveNeeds implies includeNeeds
includeTransitiveNeeds: true,
want: []string{"appA", "appB", "appC", "appD"},
},
{
name: "include-needs AND include-transitive-needs - same as include-transitive-needs alone",
selector: []string{"name=appA"},
includeNeeds: true,
includeTransitiveNeeds: true,
want: []string{"appA", "appB", "appC", "appD"},
},
{
name: "include-needs on leaf release (appC) - no dependencies to include",
selector: []string{"name=appC"},
includeNeeds: true,
includeTransitiveNeeds: false,
want: []string{"appC"},
},
{
name: "include-transitive-needs on middle release (appB) - includes appC",
selector: []string{"name=appB"},
includeNeeds: false,
includeTransitiveNeeds: true,
want: []string{"appB", "appC"},
},
{
name: "include-needs on middle release (appB) - includes only appC (direct need)",
selector: []string{"name=appB"},
includeNeeds: true,
includeTransitiveNeeds: false,
want: []string{"appB", "appC"},
},
}
example := []byte(`releases:
- name: appA
namespace: default
chart: stable/testchart
needs:
- appB
- appD
- name: appB
namespace: default
chart: stable/testchart
needs:
- appC
- name: appC
namespace: default
chart: stable/testchart
- name: appD
namespace: default
chart: stable/testchart
- name: appE
namespace: default
chart: stable/testchart
`)
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
state := stateTestEnv{
Files: map[string]string{
"/helmfile.yaml": string(example),
},
WorkDir: "/",
}.MustLoadState(t, "/helmfile.yaml", "default")
var err error
state.Selectors = tc.selector
state.Releases, err = state.GetReleasesWithOverrides()
if err != nil {
t.Fatalf("GetReleasesWithOverrides failed: %v", err)
}
state.Releases = state.GetReleasesWithLabels()
// GetSelectedReleases(includeNeeds, includeTransitiveNeeds)
rs, err := state.GetSelectedReleases(tc.includeNeeds, tc.includeTransitiveNeeds)
if err != nil {
t.Fatalf("GetSelectedReleases failed: %v", err)
}
var got []string
for _, r := range rs {
got = append(got, r.Name)
}
if d := cmp.Diff(tc.want, got); d != "" {
t.Errorf("unexpected releases: want (-), got (+):\n%s", d)
}
})
}
}
// TestIncludeNeedsWithDeepTransitiveChain tests a deeper transitive dependency chain
// to ensure --include-needs only includes direct dependencies.
//
// Dependency graph: app1 -> app2 -> app3 -> app4
//
// With --include-needs on app1: should include app1, app2 (direct only)
// With --include-transitive-needs on app1: should include app1, app2, app3, app4
func TestIncludeNeedsWithDeepTransitiveChain(t *testing.T) {
type testcase struct {
name string
selector []string
includeNeeds bool
includeTransitiveNeeds bool
want []string
}
testcases := []testcase{
{
name: "include-needs on deep chain - direct dependency only",
selector: []string{"name=app1"},
includeNeeds: true,
includeTransitiveNeeds: false,
want: []string{"app1", "app2"},
},
{
name: "include-transitive-needs on deep chain - all dependencies",
selector: []string{"name=app1"},
includeNeeds: false,
includeTransitiveNeeds: true,
want: []string{"app1", "app2", "app3", "app4"},
},
{
name: "include-needs from middle of chain",
selector: []string{"name=app2"},
includeNeeds: true,
includeTransitiveNeeds: false,
want: []string{"app2", "app3"},
},
{
name: "include-transitive-needs from middle of chain",
selector: []string{"name=app2"},
includeNeeds: false,
includeTransitiveNeeds: true,
want: []string{"app2", "app3", "app4"},
},
}
example := []byte(`releases:
- name: app1
namespace: default
chart: stable/testchart
needs:
- app2
- name: app2
namespace: default
chart: stable/testchart
needs:
- app3
- name: app3
namespace: default
chart: stable/testchart
needs:
- app4
- name: app4
namespace: default
chart: stable/testchart
`)
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
state := stateTestEnv{
Files: map[string]string{
"/helmfile.yaml": string(example),
},
WorkDir: "/",
}.MustLoadState(t, "/helmfile.yaml", "default")
var err error
state.Selectors = tc.selector
state.Releases, err = state.GetReleasesWithOverrides()
if err != nil {
t.Fatalf("GetReleasesWithOverrides failed: %v", err)
}
state.Releases = state.GetReleasesWithLabels()
rs, err := state.GetSelectedReleases(tc.includeNeeds, tc.includeTransitiveNeeds)
if err != nil {
t.Fatalf("GetSelectedReleases failed: %v", err)
}
var got []string
for _, r := range rs {
got = append(got, r.Name)
}
if d := cmp.Diff(tc.want, got); d != "" {
t.Errorf("unexpected releases: want (-), got (+):\n%s", d)
}
})
}
}
// TestIncludeNeedsWithMultipleDirectNeeds tests that --include-needs includes
// all direct needs but not transitive needs of those direct needs.
//
// Dependency graph:
//
// frontend -> [backend-api, backend-worker]
// backend-api -> database
// backend-worker -> database
// database -> cache
func TestIncludeNeedsWithMultipleDirectNeeds(t *testing.T) {
type testcase struct {
name string
selector []string
includeNeeds bool
includeTransitiveNeeds bool
want []string
}
testcases := []testcase{
{
name: "include-needs - direct needs only (backend-api, backend-worker)",
selector: []string{"name=frontend"},
includeNeeds: true,
includeTransitiveNeeds: false,
want: []string{"frontend", "backend-api", "backend-worker"},
},
{
name: "include-transitive-needs - all needs (backend-api, backend-worker, database, cache)",
selector: []string{"name=frontend"},
includeNeeds: false,
includeTransitiveNeeds: true,
want: []string{"frontend", "backend-api", "backend-worker", "database", "cache"},
},
}
example := []byte(`releases:
- name: frontend
namespace: default
chart: stable/testchart
needs:
- backend-api
- backend-worker
- name: backend-api
namespace: default
chart: stable/testchart
needs:
- database
- name: backend-worker
namespace: default
chart: stable/testchart
needs:
- database
- name: database
namespace: default
chart: stable/testchart
needs:
- cache
- name: cache
namespace: default
chart: stable/testchart
`)
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
state := stateTestEnv{
Files: map[string]string{
"/helmfile.yaml": string(example),
},
WorkDir: "/",
}.MustLoadState(t, "/helmfile.yaml", "default")
var err error
state.Selectors = tc.selector
state.Releases, err = state.GetReleasesWithOverrides()
if err != nil {
t.Fatalf("GetReleasesWithOverrides failed: %v", err)
}
state.Releases = state.GetReleasesWithLabels()
rs, err := state.GetSelectedReleases(tc.includeNeeds, tc.includeTransitiveNeeds)
if err != nil {
t.Fatalf("GetSelectedReleases failed: %v", err)
}
var got []string
for _, r := range rs {
got = append(got, r.Name)
}
if d := cmp.Diff(tc.want, got); d != "" {
t.Errorf("unexpected releases: want (-), got (+):\n%s", d)
}
})
}
}