Skip to content

Commit 1282179

Browse files
committed
fix: Adding some more testing for this
1 parent 5f5656b commit 1282179

2 files changed

Lines changed: 233 additions & 0 deletions

File tree

internal/discovery/phase_worktree_integration_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2455,3 +2455,160 @@ locals {
24552455
"catalog units should be excluded by the negated filter, but found: %s", p)
24562456
}
24572457
}
2458+
2459+
// TestWorktreePhase_Integration_GitFilterExclusionPreventsParsingInWorktree verifies that
2460+
// a negated filter prevents excluded units from being parsed inside worktree sub-discoveries.
2461+
// The land-mine unit uses run_cmd("exit 1") which would cause a fatal parse error if evaluated.
2462+
func TestWorktreePhase_Integration_GitFilterExclusionPreventsParsingInWorktree(t *testing.T) {
2463+
t.Parallel()
2464+
2465+
tmpDir, runner := setupGitRepo(t)
2466+
2467+
createUnit(t, tmpDir, "app", `# App unit`)
2468+
createUnit(t, tmpDir, "land-mine", `
2469+
locals {
2470+
boom = run_cmd("--terragrunt-quiet", "bash", "-c", "exit 1")
2471+
}
2472+
`)
2473+
2474+
commitChanges(t, runner, "Initial commit")
2475+
2476+
err := os.WriteFile(filepath.Join(tmpDir, "app", "terragrunt.hcl"), []byte(`# Modified app`), 0o644)
2477+
require.NoError(t, err)
2478+
2479+
err = os.WriteFile(filepath.Join(tmpDir, "land-mine", "terragrunt.hcl"), []byte(`
2480+
locals {
2481+
boom = run_cmd("--terragrunt-quiet", "bash", "-c", "exit 1")
2482+
modified = true
2483+
}
2484+
`), 0o644)
2485+
require.NoError(t, err)
2486+
2487+
commitChanges(t, runner, "Modify both units")
2488+
2489+
l := logger.CreateLogger()
2490+
filters, parseErr := filter.ParseFilterQueries(l, []string{"[HEAD~1...HEAD]", "!./land-mine"})
2491+
require.NoError(t, parseErr)
2492+
2493+
w, err := worktrees.NewWorktrees(t.Context(), l, worktrees.WorktreeOpts{
2494+
WorkingDir: tmpDir,
2495+
GitExpressions: filters.UniqueGitFilters(),
2496+
})
2497+
require.NoError(t, err)
2498+
2499+
t.Cleanup(func() {
2500+
cleanupErr := w.Cleanup(context.WithoutCancel(t.Context()), l)
2501+
require.NoError(t, cleanupErr)
2502+
})
2503+
2504+
opts := options.NewTerragruntOptions()
2505+
opts.WorkingDir = tmpDir
2506+
opts.RootWorkingDir = tmpDir
2507+
2508+
d := discovery.NewDiscovery(tmpDir).
2509+
WithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir, Cmd: "plan"}).
2510+
WithWorktrees(w).
2511+
WithRelationships().
2512+
WithFilters(filters)
2513+
2514+
// If the land-mine unit is parsed, run_cmd("exit 1") causes a fatal error.
2515+
components, err := d.Discover(t.Context(), l, opts)
2516+
require.NoError(t, err)
2517+
2518+
unitPaths := components.Filter(component.UnitKind).Paths()
2519+
2520+
toWorktree := w.WorktreePairs["[HEAD~1...HEAD]"].ToWorktree.Path
2521+
assert.Contains(t, unitPaths, filepath.Join(toWorktree, "app"), "app should be discovered")
2522+
2523+
for _, p := range unitPaths {
2524+
assert.NotContains(t, p, "land-mine", "land-mine should not be discovered: %s", p)
2525+
}
2526+
}
2527+
2528+
// TestWorktreePhase_Integration_StackDiscoveryDoesNotParseUnits verifies that
2529+
// discoverStacks (via GenerateStacks) does not parse non-stack components even when
2530+
// reading filters trigger the parse phase. The land-mine unit uses run_cmd("exit 1")
2531+
// which would cause a fatal error if parsed.
2532+
func TestWorktreePhase_Integration_StackDiscoveryDoesNotParseUnits(t *testing.T) {
2533+
t.Parallel()
2534+
2535+
tmpDir, runner := setupGitRepo(t)
2536+
2537+
// Create a land-mine unit at the repo root
2538+
createUnit(t, tmpDir, "land-mine", `
2539+
locals {
2540+
boom = run_cmd("--terragrunt-quiet", "bash", "-c", "exit 1")
2541+
}
2542+
`)
2543+
2544+
// Create a catalog unit for the stack to source
2545+
catalogDir := filepath.Join(tmpDir, "catalog", "units", "myapp")
2546+
err := os.MkdirAll(catalogDir, 0o755)
2547+
require.NoError(t, err)
2548+
2549+
err = os.WriteFile(filepath.Join(catalogDir, "terragrunt.hcl"), []byte(`# catalog unit`), 0o644)
2550+
require.NoError(t, err)
2551+
2552+
err = os.WriteFile(filepath.Join(catalogDir, "main.tf"), []byte(`output "ok" { value = "ok" }`), 0o644)
2553+
require.NoError(t, err)
2554+
2555+
// Create a stack that reads a config file (triggers reading filter path in worktreeStacksToGenerate)
2556+
stackDir := filepath.Join(tmpDir, "live", "stack")
2557+
err = os.MkdirAll(stackDir, 0o755)
2558+
require.NoError(t, err)
2559+
2560+
err = os.WriteFile(filepath.Join(stackDir, "config.hcl"), []byte(`inputs = { v = "v1" }`), 0o644)
2561+
require.NoError(t, err)
2562+
2563+
err = os.WriteFile(filepath.Join(stackDir, "terragrunt.stack.hcl"), []byte(`
2564+
locals {
2565+
config = read_terragrunt_config("config.hcl")
2566+
}
2567+
2568+
unit "myapp" {
2569+
source = "${get_repo_root()}/catalog/units/myapp"
2570+
path = "myapp"
2571+
values = local.config.inputs
2572+
}
2573+
`), 0o644)
2574+
require.NoError(t, err)
2575+
2576+
commitChanges(t, runner, "Initial commit")
2577+
2578+
// Change only the config file (triggers reading filter, not a direct stack change)
2579+
err = os.WriteFile(filepath.Join(stackDir, "config.hcl"), []byte(`inputs = { v = "v2" }`), 0o644)
2580+
require.NoError(t, err)
2581+
2582+
commitChanges(t, runner, "Update config file")
2583+
2584+
l := logger.CreateLogger()
2585+
gitExpressions := filter.GitExpressions{filter.NewGitExpression("HEAD~1", "HEAD")}
2586+
2587+
w, err := worktrees.NewWorktrees(t.Context(), l, worktrees.WorktreeOpts{
2588+
WorkingDir: tmpDir,
2589+
GitExpressions: gitExpressions,
2590+
})
2591+
require.NoError(t, err)
2592+
2593+
t.Cleanup(func() {
2594+
cleanupErr := w.Cleanup(context.WithoutCancel(t.Context()), l)
2595+
require.NoError(t, cleanupErr)
2596+
})
2597+
2598+
opts := options.NewTerragruntOptions()
2599+
opts.WorkingDir = tmpDir
2600+
opts.RootWorkingDir = tmpDir
2601+
2602+
parsedFilters, parseErr := filter.ParseFilterQueries(l, []string{"[HEAD~1...HEAD]"})
2603+
require.NoError(t, parseErr)
2604+
2605+
opts.Filters = parsedFilters
2606+
opts.Experiments = experiment.NewExperiments()
2607+
err = opts.Experiments.EnableExperiment(experiment.FilterFlag)
2608+
require.NoError(t, err)
2609+
2610+
// GenerateStacks internally calls discoverStacks with reading filters.
2611+
// If the land-mine unit is parsed, run_cmd("exit 1") causes a fatal error.
2612+
err = generate.GenerateStacks(t.Context(), l, opts, w)
2613+
require.NoError(t, err)
2614+
}

internal/filter/filters_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,82 @@ func TestFilters_RestrictToStacks(t *testing.T) {
629629
})
630630
}
631631

632+
func TestFilters_ExcludingGitFilters(t *testing.T) {
633+
t.Parallel()
634+
635+
t.Run("empty filters", func(t *testing.T) {
636+
t.Parallel()
637+
638+
filters := filter.Filters{}
639+
result := filters.ExcludingGitFilters()
640+
assert.Empty(t, result)
641+
})
642+
643+
t.Run("only git expression", func(t *testing.T) {
644+
t.Parallel()
645+
646+
filters, err := filter.ParseFilterQueries(testLogger(), []string{"[HEAD~1...HEAD]"})
647+
require.NoError(t, err)
648+
649+
result := filters.ExcludingGitFilters()
650+
assert.Empty(t, result)
651+
})
652+
653+
t.Run("no git expressions", func(t *testing.T) {
654+
t.Parallel()
655+
656+
filters, err := filter.ParseFilterQueries(testLogger(), []string{"./live/**", "!./iac/**"})
657+
require.NoError(t, err)
658+
require.Len(t, filters, 2)
659+
660+
result := filters.ExcludingGitFilters()
661+
assert.Len(t, result, 2)
662+
})
663+
664+
t.Run("mixed git and non-git", func(t *testing.T) {
665+
t.Parallel()
666+
667+
filters, err := filter.ParseFilterQueries(testLogger(), []string{"[HEAD~1...HEAD]", "!./iac/**", "type=unit"})
668+
require.NoError(t, err)
669+
require.Len(t, filters, 3)
670+
671+
result := filters.ExcludingGitFilters()
672+
assert.Len(t, result, 2)
673+
674+
// Verify the remaining filters are the non-git ones
675+
resultStrs := make([]string, 0, len(result))
676+
for _, f := range result {
677+
resultStrs = append(resultStrs, f.String())
678+
}
679+
680+
assert.Contains(t, resultStrs, "!./iac/**")
681+
assert.Contains(t, resultStrs, "type=unit")
682+
})
683+
684+
t.Run("infix containing git expression is excluded", func(t *testing.T) {
685+
t.Parallel()
686+
687+
filters, err := filter.ParseFilterQueries(testLogger(), []string{"[HEAD~1...HEAD] | !./iac/**"})
688+
require.NoError(t, err)
689+
require.Len(t, filters, 1)
690+
691+
result := filters.ExcludingGitFilters()
692+
assert.Empty(t, result, "infix filter containing a git expression should be excluded")
693+
})
694+
695+
t.Run("does not modify original", func(t *testing.T) {
696+
t.Parallel()
697+
698+
filters, err := filter.ParseFilterQueries(testLogger(), []string{"[HEAD~1...HEAD]", "./live/**"})
699+
require.NoError(t, err)
700+
require.Len(t, filters, 2)
701+
702+
result := filters.ExcludingGitFilters()
703+
assert.Len(t, result, 1)
704+
assert.Len(t, filters, 2, "original should not be modified")
705+
})
706+
}
707+
632708
func TestFilters_RequiresGitReferences(t *testing.T) {
633709
t.Parallel()
634710

0 commit comments

Comments
 (0)