diff --git a/cli/commands/dag/cli.go b/cli/commands/dag/cli.go index e7eb72cd6e..9634a1835b 100644 --- a/cli/commands/dag/cli.go +++ b/cli/commands/dag/cli.go @@ -4,7 +4,6 @@ package dag import ( "github.com/gruntwork-io/terragrunt/cli/commands/dag/graph" - "github.com/gruntwork-io/terragrunt/cli/flags" "github.com/gruntwork-io/terragrunt/internal/cli" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/log" @@ -15,13 +14,11 @@ const ( ) func NewCommand(l log.Logger, opts *options.TerragruntOptions) *cli.Command { - prefix := flags.Prefix{CommandName} - return &cli.Command{ Name: CommandName, Usage: "Interact with the Directed Acyclic Graph (DAG).", Subcommands: cli.Commands{ - graph.NewCommand(l, opts, prefix), + graph.NewCommand(l, opts), }, Action: cli.ShowCommandHelp, } diff --git a/cli/commands/dag/graph/cli.go b/cli/commands/dag/graph/cli.go index bbd7d10ed5..92fe749b5b 100644 --- a/cli/commands/dag/graph/cli.go +++ b/cli/commands/dag/graph/cli.go @@ -1,14 +1,14 @@ // Package graph implements the terragrunt dag graph command which generates a visual // representation of the Terragrunt dependency graph in DOT language format. +// +// Alias for 'list --format=dot --dag --dependencies --external'. package graph import ( - "github.com/gruntwork-io/terragrunt/cli/commands/common/graph" - "github.com/gruntwork-io/terragrunt/cli/commands/common/runall" - "github.com/gruntwork-io/terragrunt/cli/commands/run" - "github.com/gruntwork-io/terragrunt/cli/flags" + "github.com/gruntwork-io/terragrunt/cli/commands/list" + runCmd "github.com/gruntwork-io/terragrunt/cli/commands/run" + "github.com/gruntwork-io/terragrunt/cli/flags/shared" "github.com/gruntwork-io/terragrunt/internal/cli" - "github.com/gruntwork-io/terragrunt/internal/runner" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/log" ) @@ -17,32 +17,39 @@ const ( CommandName = "graph" ) -func NewCommand(l log.Logger, opts *options.TerragruntOptions, _ flags.Prefix) *cli.Command { - cmd := &cli.Command{ +func NewCommand(l log.Logger, opts *options.TerragruntOptions) *cli.Command { + // Build flags: queue flags + backend/feature flags + filter flag + cmdFlags := shared.NewQueueFlags(opts, nil) + cmdFlags = append(cmdFlags, runCmd.NewBackendFlags(l, opts, nil)...) + cmdFlags = append(cmdFlags, runCmd.NewFeatureFlags(l, opts, nil)...) + cmdFlags = append(cmdFlags, shared.NewFilterFlag(opts)) + + return &cli.Command{ Name: CommandName, - Usage: "Graph the Directed Acyclic Graph (DAG) in DOT language.", + Usage: "Graph the Directed Acyclic Graph (DAG) in DOT language. Alias for 'list --format=dot --dag --dependencies --external'.", UsageText: "terragrunt dag graph", + Flags: cmdFlags, Action: func(ctx *cli.Context) error { return Run(ctx, l, opts) }, - Flags: run.NewFlags(l, opts, nil), } - - cmd = runall.WrapCommand(l, opts, cmd, run.Run, true) - cmd = graph.WrapCommand(l, opts, cmd, run.Run, true) - - return cmd } func Run(ctx *cli.Context, l log.Logger, opts *options.TerragruntOptions) error { - stack, err := runner.FindStackInSubfolders(ctx, l, opts) - if err != nil { - return err - } - - if err := stack.GetStack().Units.WriteDot(l, opts.Writer, opts); err != nil { - l.Warnf("Failed to graph dot: %v", err) + listOpts := list.NewOptions(opts) + listOpts.Format = list.FormatDot + listOpts.Mode = list.ModeDAG + listOpts.Dependencies = true + listOpts.Hidden = true + + // By default, graph includes external dependencies. + // Respect queue flags to override this behavior. + if opts.IgnoreExternalDependencies { + listOpts.External = false + } else { + // Default to true, or explicitly set if --queue-include-external is used + listOpts.External = true } - return nil + return list.Run(ctx, l, listOpts) } diff --git a/cli/commands/list/cli.go b/cli/commands/list/cli.go index 2dbfa749f2..e9baa396dc 100644 --- a/cli/commands/list/cli.go +++ b/cli/commands/list/cli.go @@ -41,7 +41,7 @@ func NewFlags(opts *Options, prefix flags.Prefix) cli.Flags { Name: FormatFlagName, EnvVars: tgPrefix.EnvVars(FormatFlagName), Destination: &opts.Format, - Usage: "Output format for list results. Valid values: text, tree, long.", + Usage: "Output format for list results. Valid values: text, tree, long, dot.", DefaultText: FormatText, }), flags.NewFlag(&cli.BoolFlag{ diff --git a/cli/commands/list/list.go b/cli/commands/list/list.go index 1d55e4e355..787299d869 100644 --- a/cli/commands/list/list.go +++ b/cli/commands/list/list.go @@ -2,6 +2,7 @@ package list import ( "context" + "fmt" "os" "path/filepath" "sort" @@ -104,6 +105,8 @@ func Run(ctx context.Context, l log.Logger, opts *Options) error { return outputTree(l, opts, listedComponents, opts.Mode) case FormatLong: return outputLong(l, opts, listedComponents) + case FormatDot: + return outputDot(l, opts, listedComponents) default: // This should never happen, because of validation in the command. // If it happens, we want to throw so we can fix the validation. @@ -131,10 +134,10 @@ func shouldDiscoverDependencies(opts *Options) bool { type ListedComponents []*ListedComponent type ListedComponent struct { - Type component.Kind - Path string - + Type component.Kind + Path string Dependencies []*ListedComponent + Excluded bool } // Contains checks to see if the given path is in the listed components. @@ -168,11 +171,17 @@ func discoveredToListed(components component.Components, opts *Options) (ListedC continue } + excluded := false + if opts.QueueConstructAs != "" { if unit, ok := c.(*component.Unit); ok { if cfg := unit.Config(); cfg != nil && cfg.Exclude != nil { if cfg.Exclude.IsActionListed(opts.QueueConstructAs) { - continue + if opts.Format != FormatDot { + continue + } + + excluded = true } } } @@ -186,8 +195,9 @@ func discoveredToListed(components component.Components, opts *Options) (ListedC } listedCfg := &ListedComponent{ - Type: c.Kind(), - Path: relPath, + Type: c.Kind(), + Path: relPath, + Excluded: excluded, } if len(c.Dependencies()) == 0 { @@ -206,9 +216,22 @@ func discoveredToListed(components component.Components, opts *Options) (ListedC continue } + depExcluded := false + + if opts.QueueConstructAs != "" { + if depUnit, ok := dep.(*component.Unit); ok { + if depCfg := depUnit.Config(); depCfg != nil && depCfg.Exclude != nil { + if depCfg.Exclude.IsActionListed(opts.QueueConstructAs) { + depExcluded = true + } + } + } + } + listedCfg.Dependencies[i] = &ListedComponent{ - Type: dep.Kind(), - Path: relDepPath, + Type: dep.Kind(), + Path: relDepPath, + Excluded: depExcluded, } } @@ -442,6 +465,11 @@ func outputTree(l log.Logger, opts *Options, components ListedComponents, sort s return renderTree(opts, components, s, sort) } +// outputDot outputs the discovered components in GraphViz DOT format. +func outputDot(_ log.Logger, opts *Options, components ListedComponents) error { + return renderDot(opts, components) +} + type TreeStyler struct { entryStyle lipgloss.Style rootStyle lipgloss.Style @@ -674,3 +702,61 @@ func getLongestPathLen(components ListedComponents) int { return longest } + +// renderDot renders the components in GraphViz DOT format. +func renderDot(opts *Options, components ListedComponents) error { + if _, err := opts.Writer.Write([]byte("digraph {\n")); err != nil { + return errors.New(err) + } + + sortedComponents := make(ListedComponents, len(components)) + copy(sortedComponents, components) + sort.Slice(sortedComponents, func(i, j int) bool { + return sortedComponents[i].Path < sortedComponents[j].Path + }) + + for _, component := range sortedComponents { + if len(component.Dependencies) > 1 { + sort.Slice(component.Dependencies, func(i, j int) bool { + return component.Dependencies[i].Path < component.Dependencies[j].Path + }) + } + } + + for _, component := range sortedComponents { + style := "" + if component.Excluded { + style = "[color=red]" + } + + if _, writeErr := opts.Writer.Write( + fmt.Appendf( + nil, + "\t\"%s\" %s;\n", + component.Path, + style, + ), + ); writeErr != nil { + return errors.New(writeErr) + } + + for _, dep := range component.Dependencies { + if _, writeErr := opts.Writer.Write( + fmt.Appendf( + nil, + "\t\"%s\" -> \"%s\";\n", + component.Path, + dep.Path, + ), + ); writeErr != nil { + return errors.New(writeErr) + } + } + } + + if _, err := opts.Writer.Write([]byte("}\n")); err != nil { + return errors.New(err) + } + + return nil +} diff --git a/cli/commands/list/list_test.go b/cli/commands/list/list_test.go index 0283425e15..7280b93027 100644 --- a/cli/commands/list/list_test.go +++ b/cli/commands/list/list_test.go @@ -587,3 +587,519 @@ func TestColorizer(t *testing.T) { }) } } + +func TestDotFormat(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + + testDirs := []string{ + "unit1", + "unit2", + } + + for _, dir := range testDirs { + err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755) + require.NoError(t, err) + } + + testFiles := map[string]string{ + "unit1/terragrunt.hcl": "", + "unit2/terragrunt.hcl": ` +dependency "unit1" { + config_path = "../unit1" +} +`, + } + + for path, content := range testFiles { + err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644) + require.NoError(t, err) + } + + l := logger.CreateLogger() + tgOptions, err := options.NewTerragruntOptionsForTest(tmpDir) + require.NoError(t, err) + + opts := list.NewOptions(tgOptions) + opts.Format = list.FormatDot + opts.Mode = list.ModeDAG + opts.Dependencies = true + opts.External = false + + r, w, err := os.Pipe() + require.NoError(t, err) + + opts.Writer = w + + err = list.Run(t.Context(), l, opts) + require.NoError(t, err) + + w.Close() + + output, err := io.ReadAll(r) + require.NoError(t, err) + + outputStr := string(output) + + assert.Equal( + t, + `digraph { + "001/unit1" ; + "001/unit2" ; + "001/unit2" -> "001/unit1"; +} +`, + outputStr, + ) +} + +func TestDotFormatWithoutDependencies(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + + testDirs := []string{ + "unit1", + "unit2", + "unit3", + } + + for _, dir := range testDirs { + err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755) + require.NoError(t, err) + } + + testFiles := map[string]string{ + "unit1/terragrunt.hcl": "", + "unit2/terragrunt.hcl": "", + "unit3/terragrunt.hcl": "", + } + + for path, content := range testFiles { + err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644) + require.NoError(t, err) + } + + l := logger.CreateLogger() + tgOptions, err := options.NewTerragruntOptionsForTest(tmpDir) + require.NoError(t, err) + + opts := list.NewOptions(tgOptions) + opts.Format = list.FormatDot + opts.Dependencies = false + + r, w, err := os.Pipe() + require.NoError(t, err) + + opts.Writer = w + + err = list.Run(t.Context(), l, opts) + require.NoError(t, err) + + w.Close() + + output, err := io.ReadAll(r) + require.NoError(t, err) + + outputStr := string(output) + + assert.Equal( + t, + `digraph { + "001/unit1" ; + "001/unit2" ; + "001/unit3" ; +} +`, + outputStr, + ) +} + +func TestDotFormatWithComplexDependencies(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + + testDirs := []string{ + "unit1", + "unit2", + "unit3", + } + + for _, dir := range testDirs { + err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755) + require.NoError(t, err) + } + + testFiles := map[string]string{ + "unit1/terragrunt.hcl": "", + "unit2/terragrunt.hcl": ` +dependency "unit1" { + config_path = "../unit1" +} +`, + "unit3/terragrunt.hcl": ` +dependency "unit1" { + config_path = "../unit1" +} + +dependency "unit2" { + config_path = "../unit2" +} +`, + } + + for path, content := range testFiles { + err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644) + require.NoError(t, err) + } + + l := logger.CreateLogger() + tgOptions, err := options.NewTerragruntOptionsForTest(tmpDir) + require.NoError(t, err) + + opts := list.NewOptions(tgOptions) + opts.Format = list.FormatDot + opts.Mode = list.ModeDAG + opts.Dependencies = true + + r, w, err := os.Pipe() + require.NoError(t, err) + + opts.Writer = w + + err = list.Run(t.Context(), l, opts) + require.NoError(t, err) + + w.Close() + + output, err := io.ReadAll(r) + require.NoError(t, err) + + outputStr := string(output) + + assert.Equal( + t, + `digraph { + "001/unit1" ; + "001/unit2" ; + "001/unit2" -> "001/unit1"; + "001/unit3" ; + "001/unit3" -> "001/unit1"; + "001/unit3" -> "001/unit2"; +} +`, + outputStr, + ) +} + +func TestDotFormatWithExcludedComponents(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + + testDirs := []string{ + "unit1", + "unit2", + "unit3", + } + + for _, dir := range testDirs { + err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755) + require.NoError(t, err) + } + + testFiles := map[string]string{ + "unit1/terragrunt.hcl": "", + "unit2/terragrunt.hcl": ` +dependency "unit1" { + config_path = "../unit1" +} + +exclude { + if = true + actions = ["apply"] +} +`, + "unit3/terragrunt.hcl": ` +dependency "unit2" { + config_path = "../unit2" +} +`, + } + + for path, content := range testFiles { + err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644) + require.NoError(t, err) + } + + l := logger.CreateLogger() + tgOptions, err := options.NewTerragruntOptionsForTest(tmpDir) + require.NoError(t, err) + + opts := list.NewOptions(tgOptions) + opts.Format = list.FormatDot + opts.Mode = list.ModeDAG + opts.Dependencies = true + opts.QueueConstructAs = "apply" + + r, w, err := os.Pipe() + require.NoError(t, err) + + opts.Writer = w + + err = list.Run(t.Context(), l, opts) + require.NoError(t, err) + + w.Close() + + output, err := io.ReadAll(r) + require.NoError(t, err) + + outputStr := string(output) + + assert.Equal( + t, + `digraph { + "001/unit1" ; + "001/unit2" [color=red]; + "001/unit2" -> "001/unit1"; + "001/unit3" ; + "001/unit3" -> "001/unit2"; +} +`, + outputStr, + ) +} + +func TestDotFormatWithExcludedDependency(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + + testDirs := []string{ + "unit1", + "unit2", + } + + for _, dir := range testDirs { + err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755) + require.NoError(t, err) + } + + testFiles := map[string]string{ + "unit1/terragrunt.hcl": ` +exclude { + if = true + actions = ["plan"] +} +`, + "unit2/terragrunt.hcl": ` +dependency "unit1" { + config_path = "../unit1" +} +`, + } + + for path, content := range testFiles { + err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644) + require.NoError(t, err) + } + + l := logger.CreateLogger() + tgOptions, err := options.NewTerragruntOptionsForTest(tmpDir) + require.NoError(t, err) + + opts := list.NewOptions(tgOptions) + opts.Format = list.FormatDot + opts.Mode = list.ModeDAG + opts.Dependencies = true + opts.QueueConstructAs = "plan" + + r, w, err := os.Pipe() + require.NoError(t, err) + + opts.Writer = w + + err = list.Run(t.Context(), l, opts) + require.NoError(t, err) + + w.Close() + + output, err := io.ReadAll(r) + require.NoError(t, err) + + outputStr := string(output) + + assert.Equal( + t, + `digraph { + "001/unit1" [color=red]; + "001/unit2" ; + "001/unit2" -> "001/unit1"; +} +`, + outputStr, + ) +} + +func TestTextFormatExcludesExcludedComponents(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + + testDirs := []string{ + "unit1", + "unit2", + "unit3", + } + + for _, dir := range testDirs { + err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755) + require.NoError(t, err) + } + + testFiles := map[string]string{ + "unit1/terragrunt.hcl": "", + "unit2/terragrunt.hcl": ` +exclude { + if = true + actions = ["destroy"] +} +`, + "unit3/terragrunt.hcl": "", + } + + for path, content := range testFiles { + err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644) + require.NoError(t, err) + } + + l := logger.CreateLogger() + l.Formatter().SetDisabledColors(true) + + tgOptions, err := options.NewTerragruntOptionsForTest(tmpDir) + require.NoError(t, err) + + opts := list.NewOptions(tgOptions) + opts.Format = list.FormatText + opts.QueueConstructAs = "destroy" + + r, w, err := os.Pipe() + require.NoError(t, err) + + opts.Writer = w + + err = list.Run(t.Context(), l, opts) + require.NoError(t, err) + + w.Close() + + output, err := io.ReadAll(r) + require.NoError(t, err) + + outputStr := string(output) + + expectedPaths := []string{"001/unit1", "001/unit3"} + + fields := strings.Fields(outputStr) + for i, field := range fields { + fields[i] = filepath.ToSlash(field) + } + + assert.Len(t, fields, len(expectedPaths)) + assert.ElementsMatch(t, expectedPaths, fields) +} + +func TestDotFormatWithMultipleExcludedComponents(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + + testDirs := []string{ + "unit1", + "unit2", + "unit3", + "unit4", + } + + for _, dir := range testDirs { + err := os.MkdirAll(filepath.Join(tmpDir, dir), 0755) + require.NoError(t, err) + } + + testFiles := map[string]string{ + "unit1/terragrunt.hcl": ` +exclude { + if = true + actions = ["all"] +} +`, + "unit2/terragrunt.hcl": ` +dependency "unit1" { + config_path = "../unit1" +} +`, + "unit3/terragrunt.hcl": ` +dependency "unit2" { + config_path = "../unit2" +} + +exclude { + if = true + actions = ["all"] +} +`, + "unit4/terragrunt.hcl": ` +dependency "unit3" { + config_path = "../unit3" +} +`, + } + + for path, content := range testFiles { + err := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644) + require.NoError(t, err) + } + + l := logger.CreateLogger() + tgOptions, err := options.NewTerragruntOptionsForTest(tmpDir) + require.NoError(t, err) + + opts := list.NewOptions(tgOptions) + opts.Format = list.FormatDot + opts.Mode = list.ModeDAG + opts.Dependencies = true + opts.QueueConstructAs = "apply" + + r, w, err := os.Pipe() + require.NoError(t, err) + + opts.Writer = w + + err = list.Run(t.Context(), l, opts) + require.NoError(t, err) + + w.Close() + + output, err := io.ReadAll(r) + require.NoError(t, err) + + outputStr := string(output) + + assert.Equal( + t, + `digraph { + "001/unit1" [color=red]; + "001/unit2" ; + "001/unit2" -> "001/unit1"; + "001/unit3" [color=red]; + "001/unit3" -> "001/unit2"; + "001/unit4" ; + "001/unit4" -> "001/unit3"; +} +`, + outputStr, + ) +} diff --git a/cli/commands/list/options.go b/cli/commands/list/options.go index 3533bb05a6..32c81d40a3 100644 --- a/cli/commands/list/options.go +++ b/cli/commands/list/options.go @@ -15,6 +15,9 @@ const ( // FormatLong outputs the discovered configurations in long format. FormatLong = "long" + // FormatDot outputs the discovered configurations in GraphViz DOT format. + FormatDot = "dot" + // SortAlpha sorts the discovered configurations in alphabetical order. SortAlpha = "alpha" @@ -94,6 +97,8 @@ func (o *Options) validateFormat() error { return nil case FormatLong: return nil + case FormatDot: + return nil default: return errors.New("invalid format: " + o.Format) } diff --git a/internal/runner/common/unit.go b/internal/runner/common/unit.go index 8b7f161d00..c9f5264792 100644 --- a/internal/runner/common/unit.go +++ b/internal/runner/common/unit.go @@ -2,7 +2,6 @@ package common import ( "fmt" - "io" "maps" "path/filepath" "slices" @@ -11,7 +10,6 @@ import ( "sync" "github.com/gruntwork-io/go-commons/files" - "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/util" "github.com/gruntwork-io/terragrunt/config" @@ -259,55 +257,6 @@ func (unitsMap UnitsMap) CrossLinkDependencies(canonicalTerragruntConfigPaths [] return units, nil } -// WriteDot is used to emit a GraphViz compatible definition -// for a directed graph. It can be used to dump a .dot file. -// This is a similar implementation to terraform's digraph https://github.com/hashicorp/terraform/blob/v1.5.7/internal/dag/dag.go -// adding some styling to units that are excluded from the execution in *-all commands -func (units Units) WriteDot(l log.Logger, w io.Writer, opts *options.TerragruntOptions) error { - if _, err := w.Write([]byte("digraph {\n")); err != nil { - return errors.New(err) - } - defer func(w io.Writer, p []byte) { - _, err := w.Write(p) - if err != nil { - l.Warnf("Failed to close graphviz output: %v", err) - } - }(w, []byte("}\n")) - - // all paths are relative to the TerragruntConfigPath - prefix := filepath.Dir(opts.TerragruntConfigPath) + "/" - - for _, source := range units { - // apply a different coloring for excluded nodes - style := "" - if source.FlagExcluded { - style = "[color=red]" - } - - nodeLine := fmt.Sprintf("\t\"%s\" %s;\n", - strings.TrimPrefix(source.Path, prefix), style) - - _, err := w.Write([]byte(nodeLine)) - if err != nil { - return errors.New(err) - } - - for _, target := range source.Dependencies { - line := fmt.Sprintf("\t\"%s\" -> \"%s\";\n", - strings.TrimPrefix(source.Path, prefix), - strings.TrimPrefix(target.Path, prefix), - ) - - _, err := w.Write([]byte(line)) - if err != nil { - return errors.New(err) - } - } - } - - return nil -} - // CheckForCycles checks for dependency cycles in the given list of units and return an error if one is found. func (units Units) CheckForCycles() error { var visitedPaths []string diff --git a/internal/runner/common/unit_test.go b/internal/runner/common/unit_test.go index e814ac45a7..dec590bf1b 100644 --- a/internal/runner/common/unit_test.go +++ b/internal/runner/common/unit_test.go @@ -135,28 +135,6 @@ func TestUnitsMap_CrossLinkDependencies(t *testing.T) { assert.Equal(t, aPath, units[1].Dependencies[0].Path) } -func TestUnits_WriteDot(t *testing.T) { - t.Parallel() - - units := common.Units{ - &common.Unit{Path: "a"}, - &common.Unit{Path: "b", Dependencies: common.Units{&common.Unit{Path: "a"}}, FlagExcluded: true}, - } - - var buf bytes.Buffer - - opts := &options.TerragruntOptions{TerragruntConfigPath: "/foo/terragrunt.hcl"} - l := logger.CreateLogger() - err := units.WriteDot(l, &buf, opts) - require.NoError(t, err) - - out := buf.String() - assert.Contains(t, out, "digraph {") - assert.Contains(t, out, "a") - assert.Contains(t, out, "b") - assert.Contains(t, out, "[color=red]") -} - func TestUnits_CheckForCycles(t *testing.T) { t.Parallel() diff --git a/test/integration_dag_test.go b/test/integration_dag_test.go index bce752e223..c7fbfa9495 100644 --- a/test/integration_dag_test.go +++ b/test/integration_dag_test.go @@ -30,12 +30,13 @@ func TestIncludeExternalInDagGraphCmd(t *testing.T) { t.Parallel() helpers.CleanupTerraformFolder(t, testFixtureGraphDAG) - workDir := filepath.Join(testFixtureGraphDAG, "region-1", "unit-a") + workDir := filepath.Join(testFixtureGraphDAG, "region-1") + workDir, err := filepath.EvalSymlinks(workDir) + require.NoError(t, err) cmd := "terragrunt dag graph --queue-include-external --working-dir " + workDir stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, cmd) require.NoError(t, err) - assert.Contains(t, stdout, "unit-a\" ->") }