From acaaa34d69e26a647f4d9402218f667ab1f70c96 Mon Sep 17 00:00:00 2001 From: Hubertbits <170125456+hubertbits@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:52:26 +0200 Subject: [PATCH] tests: Add/Fix tests for new flag and fix docker wait, make fmt issue Signed-off-by: yxxhero --- pkg/app/app_diff_test.go | 136 ++++++++++ pkg/app/app_test.go | 234 ++++++++++-------- pkg/app/diff_test.go | 149 +++++------ pkg/app/testdata/app_diff_test/include-crds | 11 + .../app_diff_test/include-or-skip-crds-unset | 11 + pkg/app/testdata/app_diff_test/skip-crds | 11 + pkg/state/helmx_test.go | 69 ++++++ pkg/state/state_test.go | 72 +++--- pkg/testcmd/README.md | 65 +++++ pkg/testcmd/helper.go | 111 +++++++++ pkg/testutil/README.md | 91 +++++++ 11 files changed, 743 insertions(+), 217 deletions(-) create mode 100644 pkg/app/testdata/app_diff_test/include-crds create mode 100644 pkg/app/testdata/app_diff_test/include-or-skip-crds-unset create mode 100644 pkg/app/testdata/app_diff_test/skip-crds create mode 100644 pkg/testcmd/README.md create mode 100644 pkg/testcmd/helper.go create mode 100644 pkg/testutil/README.md diff --git a/pkg/app/app_diff_test.go b/pkg/app/app_diff_test.go index 4f34ccb1..4a58f5c3 100644 --- a/pkg/app/app_diff_test.go +++ b/pkg/app/app_diff_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" + "github.com/helmfile/helmfile/pkg/common" "github.com/helmfile/helmfile/pkg/exectest" "github.com/helmfile/helmfile/pkg/filesystem" "github.com/helmfile/helmfile/pkg/helmexec" @@ -416,3 +417,138 @@ releases: }) }) } + +func TestDiffWithIncludeCRDs(t *testing.T) { + type fields struct { + includeCRDs common.BoolFlag + skipCRDs common.BoolFlag + } + + type testcase struct { + fields fields + ns string + error string + selectors []string + diffed []exectest.Release + } + + check := func(t *testing.T, tc testcase) { + t.Helper() + + wantDiffs := tc.diffed + + var helm = &exectest.Helm{ + FailOnUnexpectedList: true, + FailOnUnexpectedDiff: true, + DiffMutex: &sync.Mutex{}, + ChartsMutex: &sync.Mutex{}, + ReleasesMutex: &sync.Mutex{}, + Helm3: true, + } + + bs := runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) { + t.Helper() + + valsRuntime, err := vals.New(vals.Options{CacheSize: 32}) + if err != nil { + t.Errorf("unexpected error creating vals runtime: %v", err) + } + + files := map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: include-crds + chart: incubator/raw + namespace: default +`, + } + + app := appWithFs(&App{ + OverrideHelmBinary: DefaultHelmBinary, + fs: filesystem.DefaultFileSystem(), + OverrideKubeContext: "default", + Env: "default", + Logger: logger, + helms: map[helmKey]helmexec.Interface{ + createHelmKey("helm", "default"): helm, + }, + valsRuntime: valsRuntime, + }, files) + + if tc.ns != "" { + app.Namespace = tc.ns + } + + if tc.selectors != nil { + app.Selectors = tc.selectors + } + + diffConfig := NewApplyConfigWithDefaults(&applyConfig{ + // if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic. + concurrency: 1, + logger: logger, + includeCRDs: tc.fields.includeCRDs, + skipCRDs: tc.fields.skipCRDs, + }) + diffErr := app.Diff(diffConfig) + + var gotErr string + if diffErr != nil { + gotErr = diffErr.Error() + } + + if d := cmp.Diff(tc.error, gotErr); d != "" { + t.Fatalf("unexpected error: want (-), got (+): %s", d) + } + + require.Equal(t, wantDiffs, helm.Diffed) + }) + + testhelper.RequireLog(t, "app_diff_test", bs) + } + + t.Run("include-crds", func(t *testing.T) { + includeCRDs := common.NewBoolFlag(false) + includeCRDs.Set(true) + + check(t, testcase{ + fields: fields{ + skipCRDs: common.NewBoolFlag(false), + includeCRDs: includeCRDs, + }, + diffed: []exectest.Release{ + {Name: "include-crds", Flags: []string{"--kube-context", "default", "--namespace", "default", "--reset-values", "--include-crds"}}, + }, + }) + }) + + t.Run("include-or-skip-crds-unset", func(t *testing.T) { + includeCRDs := common.NewBoolFlag(false) + includeCRDs.Set(true) + + check(t, testcase{ + fields: fields{ + skipCRDs: common.NewBoolFlag(false), + includeCRDs: common.NewBoolFlag(false), + }, + diffed: []exectest.Release{ + {Name: "include-crds", Flags: []string{"--kube-context", "default", "--namespace", "default", "--reset-values"}}, + }, + }) + }) + + t.Run("skip-crds", func(t *testing.T) { + skipCRDs := common.NewBoolFlag(false) + skipCRDs.Set(true) + + check(t, testcase{ + fields: fields{ + skipCRDs: skipCRDs, + includeCRDs: common.NewBoolFlag(false), + }, + diffed: []exectest.Release{ + {Name: "include-crds", Flags: []string{"--kube-context", "default", "--namespace", "default", "--reset-values", "--skip-crds"}}, + }, + }) + }) +} diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index 550625d3..bd5ec6ab 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2172,11 +2172,18 @@ func (c configImpl) IncludeCRDs() bool { return c.includeCRDs.Value() } +// ShouldIncludeCRDs determines if CRDs should be included in the operation. +// It returns true only when: +// - includeCRDs flag is explicitly provided on the command line and set to true +// - AND skipCRDs flag is not provided on the command line +// +// This ensures that CRDs are only included when explicitly requested and not +// contradicted by the skipCRDs flag. func (c configImpl) ShouldIncludeCRDs() bool { includeCRDsExplicit := c.includeCRDs.WasExplicitlySet() && c.includeCRDs.Value() - skipCRDsExplicit := c.skipCRDs.WasExplicitlySet() && !c.skipCRDs.Value() + skipCRDsNotProvided := !c.skipCRDs.WasExplicitlySet() - return includeCRDsExplicit || skipCRDsExplicit + return includeCRDsExplicit && skipCRDsNotProvided } func (c configImpl) SkipRefresh() bool { @@ -2324,203 +2331,210 @@ func NewApplyConfigWithDefaults(existing *applyConfig) *applyConfig { return existing } -func (a applyConfig) Args() string { - return a.args +func (c applyConfig) Args() string { + return c.args } -func (a applyConfig) Cascade() string { - return a.cascade +func (c applyConfig) Cascade() string { + return c.cascade } -func (a applyConfig) Wait() bool { - return a.wait +func (c applyConfig) Wait() bool { + return c.wait } -func (a applyConfig) WaitRetries() int { - return a.waitRetries +func (c applyConfig) WaitRetries() int { + return c.waitRetries } -func (a applyConfig) WaitForJobs() bool { - return a.waitForJobs +func (c applyConfig) WaitForJobs() bool { + return c.waitForJobs } -func (a applyConfig) Values() []string { - return a.values +func (c applyConfig) Values() []string { + return c.values } -func (a applyConfig) Set() []string { - return a.set +func (c applyConfig) Set() []string { + return c.set } -func (a applyConfig) Validate() bool { - return a.validate +func (c applyConfig) Validate() bool { + return c.validate } -func (a applyConfig) SkipCleanup() bool { - return a.skipCleanup +func (c applyConfig) SkipCleanup() bool { + return c.skipCleanup } -func (a applyConfig) SkipCRDs() bool { - return a.skipCRDs.Value() +func (c applyConfig) SkipCRDs() bool { + return c.skipCRDs.Value() } -func (a applyConfig) IncludeCRDs() bool { - return a.includeCRDs.Value() +func (c applyConfig) IncludeCRDs() bool { + return c.includeCRDs.Value() } +// ShouldIncludeCRDs determines if CRDs should be included in the operation. +// It returns true only when: +// - includeCRDs flag is explicitly provided on the command line and set to true +// - AND skipCRDs flag is not provided on the command line +// +// This ensures that CRDs are only included when explicitly requested and not +// contradicted by the skipCRDs flag. func (c applyConfig) ShouldIncludeCRDs() bool { includeCRDsExplicit := c.includeCRDs.WasExplicitlySet() && c.includeCRDs.Value() - skipCRDsExplicit := c.skipCRDs.WasExplicitlySet() && !c.skipCRDs.Value() + skipCRDsNotProvided := !c.skipCRDs.WasExplicitlySet() - return includeCRDsExplicit || skipCRDsExplicit + return includeCRDsExplicit && skipCRDsNotProvided } -func (a applyConfig) SkipDeps() bool { - return a.skipDeps +func (c applyConfig) SkipDeps() bool { + return c.skipDeps } -func (a applyConfig) SkipRefresh() bool { - return a.skipRefresh +func (c applyConfig) SkipRefresh() bool { + return c.skipRefresh } -func (a applyConfig) SkipNeeds() bool { - return a.skipNeeds +func (c applyConfig) SkipNeeds() bool { + return c.skipNeeds } -func (a applyConfig) IncludeNeeds() bool { - return a.includeNeeds || a.IncludeTransitiveNeeds() +func (c applyConfig) IncludeNeeds() bool { + return c.includeNeeds || c.IncludeTransitiveNeeds() } -func (a applyConfig) IncludeTransitiveNeeds() bool { - return a.includeTransitiveNeeds +func (c applyConfig) IncludeTransitiveNeeds() bool { + return c.includeTransitiveNeeds } -func (a applyConfig) IncludeTests() bool { - return a.includeTests +func (c applyConfig) IncludeTests() bool { + return c.includeTests } -func (a applyConfig) Suppress() []string { - return a.suppress +func (c applyConfig) Suppress() []string { + return c.suppress } -func (a applyConfig) SuppressSecrets() bool { - return a.suppressSecrets +func (c applyConfig) SuppressSecrets() bool { + return c.suppressSecrets } -func (a applyConfig) ShowSecrets() bool { - return a.showSecrets +func (c applyConfig) ShowSecrets() bool { + return c.showSecrets } -func (a applyConfig) NoHooks() bool { - return a.noHooks +func (c applyConfig) NoHooks() bool { + return c.noHooks } -func (a applyConfig) SuppressDiff() bool { - return a.suppressDiff +func (c applyConfig) SuppressDiff() bool { + return c.suppressDiff } -func (a applyConfig) Color() bool { - return a.color +func (c applyConfig) Color() bool { + return c.color } -func (a applyConfig) NoColor() bool { - return a.noColor +func (c applyConfig) NoColor() bool { + return c.noColor } -func (a applyConfig) Context() int { - return a.context +func (c applyConfig) Context() int { + return c.context } -func (a applyConfig) DiffOutput() string { - return a.diffOutput +func (c applyConfig) DiffOutput() string { + return c.diffOutput } -func (a applyConfig) Concurrency() int { - return a.concurrency +func (c applyConfig) Concurrency() int { + return c.concurrency } -func (a applyConfig) DetailedExitcode() bool { - return a.detailedExitcode +func (c applyConfig) DetailedExitcode() bool { + return c.detailedExitcode } -func (a applyConfig) StripTrailingCR() bool { - return a.stripTrailingCR +func (c applyConfig) StripTrailingCR() bool { + return c.stripTrailingCR } -func (a applyConfig) Interactive() bool { - return a.interactive +func (c applyConfig) Interactive() bool { + return c.interactive } -func (a applyConfig) Logger() *zap.SugaredLogger { - return a.logger +func (c applyConfig) Logger() *zap.SugaredLogger { + return c.logger } -func (a applyConfig) SkipDiffOnInstall() bool { - return a.skipDiffOnInstall +func (c applyConfig) SkipDiffOnInstall() bool { + return c.skipDiffOnInstall } -func (a applyConfig) SyncArgs() string { - return a.syncArgs +func (c applyConfig) SyncArgs() string { + return c.syncArgs } -func (a applyConfig) DiffArgs() string { - return a.diffArgs +func (c applyConfig) DiffArgs() string { + return c.diffArgs } // helmfile-template-only flags -func (a applyConfig) SkipTests() bool { - return a.skipTests +func (c applyConfig) SkipTests() bool { + return c.skipTests } -func (a applyConfig) OutputDir() string { - return a.outputDir +func (c applyConfig) OutputDir() string { + return c.outputDir } -func (a applyConfig) OutputDirTemplate() string { - return a.outputDirTemplate +func (c applyConfig) OutputDirTemplate() string { + return c.outputDirTemplate } -func (a applyConfig) ReuseValues() bool { - return a.reuseValues +func (c applyConfig) ReuseValues() bool { + return c.reuseValues } -func (a applyConfig) ResetValues() bool { - return !a.reuseValues +func (c applyConfig) ResetValues() bool { + return !c.reuseValues } -func (a applyConfig) PostRenderer() string { - return a.postRenderer +func (c applyConfig) PostRenderer() string { + return c.postRenderer } -func (a applyConfig) PostRendererArgs() []string { - return a.postRendererArgs +func (c applyConfig) PostRendererArgs() []string { + return c.postRendererArgs } -func (a applyConfig) SuppressOutputLineRegex() []string { - return a.suppressOutputLineRegex +func (c applyConfig) SuppressOutputLineRegex() []string { + return c.suppressOutputLineRegex } -func (a applyConfig) KubeVersion() string { - return a.kubeVersion +func (c applyConfig) KubeVersion() string { + return c.kubeVersion } -func (a applyConfig) SkipSchemaValidation() bool { - return a.skipSchemaValidation +func (c applyConfig) SkipSchemaValidation() bool { + return c.skipSchemaValidation } -func (a applyConfig) ShowOnly() []string { - return a.showOnly +func (c applyConfig) ShowOnly() []string { + return c.showOnly } -func (a applyConfig) HideNotes() bool { - return a.hideNotes +func (c applyConfig) HideNotes() bool { + return c.hideNotes } -func (a applyConfig) TakeOwnership() bool { - return a.takeOwnership +func (c applyConfig) TakeOwnership() bool { + return c.takeOwnership } -func (a applyConfig) SyncReleaseLabels() bool { - return a.syncReleaseLabels +func (c applyConfig) SyncReleaseLabels() bool { + return c.syncReleaseLabels } type depsConfig struct { @@ -2528,19 +2542,19 @@ type depsConfig struct { includeTransitiveNeeds bool } -func (d depsConfig) SkipRepos() bool { - return d.skipRepos +func (c depsConfig) SkipRepos() bool { + return c.skipRepos } -func (d depsConfig) IncludeTransitiveNeeds() bool { - return d.includeTransitiveNeeds +func (c depsConfig) IncludeTransitiveNeeds() bool { + return c.includeTransitiveNeeds } -func (d depsConfig) Args() string { +func (c depsConfig) Args() string { return "" } -func (d depsConfig) Concurrency() int { +func (c depsConfig) Concurrency() int { return 2 } @@ -4077,10 +4091,10 @@ releases: assert.NoError(t, err) expected := "NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION\n" + -"myrelease1 testNamespace true false chart:mychart1,common:label,id:myrelease1,name:myrelease1,namespace:testNamespace mychart1\t \n" + -"myrelease2 testNamespace false true chart:mychart1,common:label,name:myrelease2,namespace:testNamespace mychart1\t \n" + -"myrelease3 testNamespace true true chart:mychart1,name:myrelease3,namespace:testNamespace mychart1\t \n" + -"myrelease4 testNamespace true true chart:mychart1,id:myrelease1,name:myrelease4,namespace:testNamespace mychart1\t \n" + "myrelease1 testNamespace true false chart:mychart1,common:label,id:myrelease1,name:myrelease1,namespace:testNamespace mychart1\t \n" + + "myrelease2 testNamespace false true chart:mychart1,common:label,name:myrelease2,namespace:testNamespace mychart1\t \n" + + "myrelease3 testNamespace true true chart:mychart1,name:myrelease3,namespace:testNamespace mychart1\t \n" + + "myrelease4 testNamespace true true chart:mychart1,id:myrelease1,name:myrelease4,namespace:testNamespace mychart1\t \n" assert.Equal(t, expected, out) } diff --git a/pkg/app/diff_test.go b/pkg/app/diff_test.go index dd6f3e54..88579842 100644 --- a/pkg/app/diff_test.go +++ b/pkg/app/diff_test.go @@ -75,151 +75,158 @@ func NewDiffConfigWithDefaults(existing *diffConfig) *diffConfig { return existing } -func (a diffConfig) Args() string { - return a.args +func (c diffConfig) Args() string { + return c.args } -func (a diffConfig) DiffArgs() string { - return a.diffArgs +func (c diffConfig) DiffArgs() string { + return c.diffArgs } -func (a diffConfig) Values() []string { - return a.values +func (c diffConfig) Values() []string { + return c.values } -func (a diffConfig) Set() []string { - return a.set +func (c diffConfig) Set() []string { + return c.set } -func (a diffConfig) Validate() bool { - return a.validate +func (c diffConfig) Validate() bool { + return c.validate } -func (a diffConfig) SkipCRDs() bool { - return a.skipCRDs.Value() +func (c diffConfig) SkipCRDs() bool { + return c.skipCRDs.Value() } -func (a diffConfig) IncludeCRDs() bool { - return a.includeCRDs.Value() +func (c diffConfig) IncludeCRDs() bool { + return c.includeCRDs.Value() } -func (a diffConfig) ShouldIncludeCRDs() bool { - includeCRDsExplicit := a.includeCRDs.WasExplicitlySet() && a.includeCRDs.Value() - skipCRDsExplicit := a.skipCRDs.WasExplicitlySet() && !a.skipCRDs.Value() +// ShouldIncludeCRDs determines if CRDs should be included in the operation. +// It returns true only when: +// - includeCRDs flag is explicitly provided on the command line and set to true +// - AND skipCRDs flag is not provided on the command line +// +// This ensures that CRDs are only included when explicitly requested and not +// contradicted by the skipCRDs flag. +func (c diffConfig) ShouldIncludeCRDs() bool { + includeCRDsExplicit := c.includeCRDs.WasExplicitlySet() && c.includeCRDs.Value() + skipCRDsNotProvided := !c.skipCRDs.WasExplicitlySet() - return includeCRDsExplicit || skipCRDsExplicit + return includeCRDsExplicit && skipCRDsNotProvided } -func (a diffConfig) SkipDeps() bool { - return a.skipDeps +func (c diffConfig) SkipDeps() bool { + return c.skipDeps } -func (a diffConfig) SkipRefresh() bool { - return a.skipRefresh +func (c diffConfig) SkipRefresh() bool { + return c.skipRefresh } -func (a diffConfig) IncludeTests() bool { - return a.includeTests +func (c diffConfig) IncludeTests() bool { + return c.includeTests } -func (a diffConfig) SkipNeeds() bool { - return a.skipNeeds +func (c diffConfig) SkipNeeds() bool { + return c.skipNeeds } -func (a diffConfig) IncludeNeeds() bool { - return a.includeNeeds || a.IncludeTransitiveNeeds() +func (c diffConfig) IncludeNeeds() bool { + return c.includeNeeds || c.IncludeTransitiveNeeds() } -func (a diffConfig) IncludeTransitiveNeeds() bool { - return a.includeTransitiveNeeds +func (c diffConfig) IncludeTransitiveNeeds() bool { + return c.includeTransitiveNeeds } -func (a diffConfig) Suppress() []string { - return a.suppress +func (c diffConfig) Suppress() []string { + return c.suppress } -func (a diffConfig) SuppressSecrets() bool { - return a.suppressSecrets +func (c diffConfig) SuppressSecrets() bool { + return c.suppressSecrets } -func (a diffConfig) ShowSecrets() bool { - return a.showSecrets +func (c diffConfig) ShowSecrets() bool { + return c.showSecrets } -func (a diffConfig) NoHooks() bool { - return a.noHooks +func (c diffConfig) NoHooks() bool { + return c.noHooks } -func (a diffConfig) SuppressDiff() bool { - return a.suppressDiff +func (c diffConfig) SuppressDiff() bool { + return c.suppressDiff } -func (a diffConfig) Color() bool { +func (c diffConfig) Color() bool { return false } -func (a diffConfig) NoColor() bool { - return a.noColor +func (c diffConfig) NoColor() bool { + return c.noColor } -func (a diffConfig) Context() int { - return a.context +func (c diffConfig) Context() int { + return c.context } -func (a diffConfig) DiffOutput() string { - return a.diffOutput +func (c diffConfig) DiffOutput() string { + return c.diffOutput } -func (a diffConfig) Concurrency() int { - return a.concurrency +func (c diffConfig) Concurrency() int { + return c.concurrency } -func (a diffConfig) DetailedExitcode() bool { - return a.detailedExitcode +func (c diffConfig) DetailedExitcode() bool { + return c.detailedExitcode } -func (a diffConfig) StripTrailingCR() bool { - return a.stripTrailingCR +func (c diffConfig) StripTrailingCR() bool { + return c.stripTrailingCR } -func (a diffConfig) Interactive() bool { - return a.interactive +func (c diffConfig) Interactive() bool { + return c.interactive } -func (a diffConfig) SkipDiffOnInstall() bool { - return a.skipDiffOnInstall +func (c diffConfig) SkipDiffOnInstall() bool { + return c.skipDiffOnInstall } -func (a diffConfig) Logger() *zap.SugaredLogger { - return a.logger +func (c diffConfig) Logger() *zap.SugaredLogger { + return c.logger } -func (a diffConfig) RetainValuesFiles() bool { - return a.retainValuesFiles +func (c diffConfig) RetainValuesFiles() bool { + return c.retainValuesFiles } -func (a diffConfig) ReuseValues() bool { - return a.reuseValues +func (c diffConfig) ReuseValues() bool { + return c.reuseValues } -func (a diffConfig) ResetValues() bool { - return !a.reuseValues +func (c diffConfig) ResetValues() bool { + return !c.reuseValues } -func (a diffConfig) PostRenderer() string { +func (c diffConfig) PostRenderer() string { return "" } -func (a diffConfig) PostRendererArgs() []string { +func (c diffConfig) PostRendererArgs() []string { return nil } -func (a diffConfig) SkipSchemaValidation() bool { - return a.skipSchemaValidation +func (c diffConfig) SkipSchemaValidation() bool { + return c.skipSchemaValidation } -func (a diffConfig) SuppressOutputLineRegex() []string { - return a.suppressOutputLineRegex +func (c diffConfig) SuppressOutputLineRegex() []string { + return c.suppressOutputLineRegex } func TestDiff(t *testing.T) { diff --git a/pkg/app/testdata/app_diff_test/include-crds b/pkg/app/testdata/app_diff_test/include-crds new file mode 100644 index 00000000..4623d733 --- /dev/null +++ b/pkg/app/testdata/app_diff_test/include-crds @@ -0,0 +1,11 @@ +processing file "helmfile.yaml" in directory "." +changing working directory to "/path/to" +merged environment: &{default map[] map[]} +1 release(s) found in helmfile.yaml + +processing 1 groups of releases in this order: +GROUP RELEASES +1 default/default/include-crds + +processing releases in group 1/1: default/default/include-crds +changing working directory back to "/path/to" diff --git a/pkg/app/testdata/app_diff_test/include-or-skip-crds-unset b/pkg/app/testdata/app_diff_test/include-or-skip-crds-unset new file mode 100644 index 00000000..4623d733 --- /dev/null +++ b/pkg/app/testdata/app_diff_test/include-or-skip-crds-unset @@ -0,0 +1,11 @@ +processing file "helmfile.yaml" in directory "." +changing working directory to "/path/to" +merged environment: &{default map[] map[]} +1 release(s) found in helmfile.yaml + +processing 1 groups of releases in this order: +GROUP RELEASES +1 default/default/include-crds + +processing releases in group 1/1: default/default/include-crds +changing working directory back to "/path/to" diff --git a/pkg/app/testdata/app_diff_test/skip-crds b/pkg/app/testdata/app_diff_test/skip-crds new file mode 100644 index 00000000..4623d733 --- /dev/null +++ b/pkg/app/testdata/app_diff_test/skip-crds @@ -0,0 +1,11 @@ +processing file "helmfile.yaml" in directory "." +changing working directory to "/path/to" +merged environment: &{default map[] map[]} +1 release(s) found in helmfile.yaml + +processing 1 groups of releases in this order: +GROUP RELEASES +1 default/default/include-crds + +processing releases in group 1/1: default/default/include-crds +changing working directory back to "/path/to" diff --git a/pkg/state/helmx_test.go b/pkg/state/helmx_test.go index 76f4300d..a2682d5f 100644 --- a/pkg/state/helmx_test.go +++ b/pkg/state/helmx_test.go @@ -7,6 +7,7 @@ import ( "github.com/helmfile/helmfile/pkg/helmexec" "github.com/helmfile/helmfile/pkg/testutil" + "github.com/helmfile/helmfile/pkg/version" ) func TestAppendWaitForJobsFlags(t *testing.T) { @@ -432,6 +433,74 @@ func TestAppendTakeOwnershipFlags(t *testing.T) { } } +func TestAppendCRDFlags(t *testing.T) { + type args struct { + flags []string + helm helmexec.Interface + helmSpec HelmSpec + opt *DiffOpts + expected []string + } + tests := []struct { + name string + args args + }{ + { + name: "no include-crds nor skip-crds provided", + args: args{ + flags: []string{}, + helm: testutil.NewVersionHelmExec(version.HelmRequiredVersion), + opt: &DiffOpts{}, + expected: []string{}, + }, + }, + { + name: "include-crds set but no skip-crds", + args: args{ + flags: []string{}, + helm: testutil.NewVersionHelmExec(version.HelmRequiredVersion), + opt: &DiffOpts{ + SkipCRDs: false, + IncludeCRDs: true, + }, + expected: []string{"--include-crds"}, + }, + }, + { + name: "include-crds and skip-crds set", + args: args{ + flags: []string{}, + helm: testutil.NewVersionHelmExec(version.HelmRequiredVersion), + opt: &DiffOpts{ + SkipCRDs: true, + IncludeCRDs: true, + }, + expected: []string{"--skip-crds"}, + }, + }, + { + name: "include-crds not set but skip-crds is", + args: args{ + flags: []string{}, + helm: testutil.NewVersionHelmExec(version.HelmRequiredVersion), + opt: &DiffOpts{ + SkipCRDs: true, + IncludeCRDs: false, + }, + expected: []string{"--skip-crds"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + st := &HelmState{} + st.HelmDefaults = tt.args.helmSpec + got := st.appendCRDFlags(tt.args.flags, tt.args.opt.SkipCRDs, tt.args.opt.IncludeCRDs) + require.Equalf(t, tt.args.expected, got, "appendCRDFlags() = %v, want %v", got, tt.args.expected) + }) + } +} + func TestFormatLabels(t *testing.T) { tests := []struct { name string diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index 15a05da7..7204b216 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -3477,44 +3477,44 @@ func TestCommonDiffFlags(t *testing.T) { } func TestCommonDiffFlags_IncludeCRDs(t *testing.T) { - tests := []struct { - name string - inputOpts *DiffOpts - expected []string - }{ - { - name: "Include CRDs flag is set to true", - inputOpts: &DiffOpts{ - IncludeCRDs: true, - }, - expected: []string{"--reset-values", "--include-crds"}, - }, - { - name: "Include CRDs flag is set to false", - inputOpts: &DiffOpts{ - IncludeCRDs: false, - }, - expected: []string{"--reset-values"}, - }, - { - name: "Include CRDs flag not set in options", - inputOpts: &DiffOpts{}, - expected: []string{"--reset-values"}, - }, - } + tests := []struct { + name string + inputOpts *DiffOpts + expected []string + }{ + { + name: "Include CRDs flag is set to true", + inputOpts: &DiffOpts{ + IncludeCRDs: true, + }, + expected: []string{"--reset-values", "--include-crds"}, + }, + { + name: "Include CRDs flag is set to false", + inputOpts: &DiffOpts{ + IncludeCRDs: false, + }, + expected: []string{"--reset-values"}, + }, + { + name: "Include CRDs flag not set in options", + inputOpts: &DiffOpts{}, + expected: []string{"--reset-values"}, + }, + } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - state := &HelmState{} - result := state.commonDiffFlags(false, false, false, []string{}, false, false, false, test.inputOpts) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + state := &HelmState{} + result := state.commonDiffFlags(false, false, false, []string{}, false, false, false, test.inputOpts) - // Check if the result contains the expected flags - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("commonDiffFlags() with IncludeCRDs=%v = %v, want %v", - test.inputOpts.IncludeCRDs, result, test.expected) - } - }) - } + // Check if the result contains the expected flags + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("commonDiffFlags() with IncludeCRDs=%v = %v, want %v", + test.inputOpts.IncludeCRDs, result, test.expected) + } + }) + } } func TestAppendChartDownloadFlags(t *testing.T) { diff --git a/pkg/testcmd/README.md b/pkg/testcmd/README.md new file mode 100644 index 00000000..0fbbd48e --- /dev/null +++ b/pkg/testcmd/README.md @@ -0,0 +1,65 @@ +# Helmfile Test Command Package + +## Overview + +The `testcommand` package provides utilities for testing Helmfile commands in a controlled environment. This package simplifies the creation and configuration of command objects for testing purposes, allowing developers to verify command behavior without executing the full application. + +## Components + +### CommandTestHelper + +The core structure that encapsulates the components needed for testing commands: + +- `Cmd`: The Cobra command instance +- `Registry`: Flag registry for managing command flags +- `Options`: Command-specific options + +### Available Test Commands + +The package provides helper functions to create test instances of the following Helmfile commands: + +- **TestDiffCmd()**: Creates a test instance of the `diff` command +- **TestApplyCmd()**: Creates a test instance of the `apply` command +- **TestTemplateCmd()**: Creates a test instance of the `template` command +- **TestSyncCmd()**: Creates a test instance of the `sync` command + +## Usage + +```go +import ( + "testing" + "github.com/helmfile/helmfile/pkg/testcommand" +) + +func TestMyDiffCommand(t *testing.T) { + // Create a test diff command + helper := testcommand.TestDiffCmd() + + // Access the command components + cmd := helper.Cmd + options := helper.Options.(*config.DiffOptions) + + // Set up test flags + cmd.Flags().Set("concurrency", "5") + + // Test command behavior + // ... +} +``` + +## Implementation Details + +Each test command function: + +1. Creates an options factory for the specific command +2. Instantiates the command options +3. Gets the flag registry +4. Creates a Cobra command instance +5. Registers the appropriate flags +6. Returns a helper with all components + +For the `diff` command, flag values are automatically transferred to the options object. + +## Notes + +This package is intended for testing purposes only and should not be used in production code. diff --git a/pkg/testcmd/helper.go b/pkg/testcmd/helper.go new file mode 100644 index 00000000..e1a54791 --- /dev/null +++ b/pkg/testcmd/helper.go @@ -0,0 +1,111 @@ +package testcmd + +import ( + "github.com/spf13/cobra" + + "github.com/helmfile/helmfile/pkg/config" + "github.com/helmfile/helmfile/pkg/factory" + "github.com/helmfile/helmfile/pkg/flags" +) + +// CommandTestHelper provides utilities for testing commands +type CommandTestHelper struct { + Cmd *cobra.Command + Registry flags.FlagRegistry + Options config.Options +} + +// TestDiffCmd creates a diff command for testing and returns a helper with its components +func TestDiffCmd() *CommandTestHelper { + // Create command components + optionsFactory := factory.NewDiffOptionsFactory() + options := optionsFactory.CreateOptions().(*config.DiffOptions) + registry := optionsFactory.GetFlagRegistry() + + // Create command manually + cmd := &cobra.Command{ + Use: "diff", + Short: "Diff releases defined in state file", + } + + // Register flags + registry.RegisterFlags(cmd) + + // Transfer flags to options + registry.TransferFlags(cmd, options) + + return &CommandTestHelper{ + Cmd: cmd, + Registry: registry, + Options: options, + } +} + +// TestApplyCmd creates an apply command for testing and returns a helper with its components +func TestApplyCmd() *CommandTestHelper { + // Create command components + optionsFactory := factory.NewApplyOptionsFactory() + options := optionsFactory.CreateOptions().(*config.ApplyOptions) + registry := optionsFactory.GetFlagRegistry() + + // Create command manually + cmd := &cobra.Command{ + Use: "apply", + Short: "Apply all resources from state file only when there are changes", + } + + // Register flags + registry.RegisterFlags(cmd) + + return &CommandTestHelper{ + Cmd: cmd, + Registry: registry, + Options: options, + } +} + +// TestTemplateCmd creates a template command for testing and returns a helper with its components +func TestTemplateCmd() *CommandTestHelper { + // Create command components + optionsFactory := factory.NewTemplateOptionsFactory() + options := optionsFactory.CreateOptions().(*config.TemplateOptions) + registry := optionsFactory.GetFlagRegistry() + + // Create command manually + cmd := &cobra.Command{ + Use: "template", + Short: "Template releases defined in state file", + } + + // Register flags + registry.RegisterFlags(cmd) + + return &CommandTestHelper{ + Cmd: cmd, + Registry: registry, + Options: options, + } +} + +// TestSyncCmd creates a sync command for testing and returns a helper with its components +func TestSyncCmd() *CommandTestHelper { + // Create command components + optionsFactory := factory.NewSyncOptionsFactory() + options := optionsFactory.CreateOptions().(*config.SyncOptions) + registry := optionsFactory.GetFlagRegistry() + + // Create command manually + cmd := &cobra.Command{ + Use: "sync", + Short: "Sync all resources from state file", + } + + // Register flags + registry.RegisterFlags(cmd) + + return &CommandTestHelper{ + Cmd: cmd, + Registry: registry, + Options: options, + } +} diff --git a/pkg/testutil/README.md b/pkg/testutil/README.md new file mode 100644 index 00000000..4023cc9c --- /dev/null +++ b/pkg/testutil/README.md @@ -0,0 +1,91 @@ +# Helmfile Test Utilities + +This package provides testing utilities for the Helmfile project, making it easier to write unit tests for Helm-related functionality. + +## Overview + +The `testutil` package contains: + +1. Mock implementations for Helm execution +2. Utility functions for testing + +## Components + +### Mock Helm Executors + +The package provides mock implementations of Helm executors that can be used in tests: + +- `V3HelmExec`: A mock that can be configured to simulate Helm 3 behavior +- `VersionHelmExec`: A mock that can be configured with a specific Helm version + +```go +// Create a mock for Helm 3 +helmExec := testutil.NewV3HelmExec(true) + +// Create a mock for a specific Helm version +versionExec := testutil.NewVersionHelmExec("3.8.0") +``` + +These mocks implement the Helm executor interface but will panic if any unexpected methods are called, making them useful for strict testing scenarios. + +### Utility Functions + +#### CaptureStdout + +Captures stdout output during the execution of a function: + +```go +output, err := testutil.CaptureStdout(func() { + fmt.Println("Hello, world!") +}) +// output will contain "Hello, world!\n" +``` + +This is useful for testing functions that write to stdout. + +## Usage Examples + +### Testing with V3HelmExec + +```go +func TestMyFunction(t *testing.T) { + // Create a mock Helm executor configured as Helm 3 + helmExec := testutil.NewV3HelmExec(true) + + // Use in your test + result := myFunctionThatChecksHelmVersion(helmExec) + + // Assert that the result is as expected for Helm 3 + assert.True(t, result) +} +``` + +### Testing with VersionHelmExec + +```go +func TestVersionCompatibility(t *testing.T) { + // Create a mock with specific version + helmExec := testutil.NewVersionHelmExec("3.7.1") + + // Test version comparison + assert.True(t, helmExec.IsVersionAtLeast("3.7.0")) + assert.False(t, helmExec.IsVersionAtLeast("3.8.0")) +} +``` + +### Capturing Output + +```go +func TestOutputFunction(t *testing.T) { + output, err := testutil.CaptureStdout(func() { + MyFunctionThatPrintsOutput() + }) + + assert.NoError(t, err) + assert.Contains(t, output, "Expected output") +} +``` + +## Contributing + +When adding new test utilities, please ensure they are well-documented and include appropriate tests.