Skip to content

Commit 75f70b6

Browse files
Sanitize snapshots (#1002)
* sanitize snapshots * [autofix.ci] apply automated fixes * check for empty repo root * normalize slashes * normalize slashes * try to fix windows snapshots with windows paths * handle multiple slashes * handle multiple slashes * changed strategy for removing double slashes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent b8fd5dc commit 75f70b6

4 files changed

+125
-28
lines changed

tests/cli_test.go

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"github.com/charmbracelet/lipgloss"
1818
"github.com/creack/pty"
19+
"github.com/go-git/go-git/v5"
1920
"github.com/hexops/gotextdiff"
2021
"github.com/hexops/gotextdiff/myers"
2122
"github.com/hexops/gotextdiff/span"
@@ -26,9 +27,11 @@ import (
2627
)
2728

2829
// Command-line flag for regenerating snapshots
29-
var regenerateSnapshots = flag.Bool("regenerate-snapshots", false, "Regenerate all golden snapshots")
30-
var startingDir string
31-
var snapshotBaseDir string
30+
var (
31+
regenerateSnapshots = flag.Bool("regenerate-snapshots", false, "Regenerate all golden snapshots")
32+
startingDir string
33+
snapshotBaseDir string
34+
)
3235

3336
// Define styles using lipgloss
3437
var (
@@ -172,7 +175,78 @@ func (pm *PathManager) Apply() error {
172175

173176
// Determine if running in a CI environment
174177
func isCIEnvironment() bool {
175-
return os.Getenv("CI") != ""
178+
// Check for common CI environment variables
179+
// Note, that the CI variable has many possible truthy values, so we check for any non-empty value that is not "false".
180+
return (os.Getenv("CI") != "" && os.Getenv("CI") != "false") || os.Getenv("GITHUB_ACTIONS") == "true"
181+
}
182+
183+
// collapseExtraSlashes replaces multiple consecutive slashes with a single slash.
184+
func collapseExtraSlashes(s string) string {
185+
return regexp.MustCompile("/+").ReplaceAllString(s, "/")
186+
}
187+
188+
// sanitizeOutput replaces occurrences of the repository's absolute path in the output
189+
// with the placeholder "/absolute/path/to/repo". It first normalizes both the repository root
190+
// and the output to use forward slashes, ensuring that the replacement works reliably.
191+
// An error is returned if the repository root cannot be determined.
192+
// Convert something like:
193+
//
194+
// D:\\a\atmos\atmos\examples\demo-stacks\stacks\deploy\**\*
195+
// --> /absolute/path/to/repo/examples/demo-stacks/stacks/deploy/**/*
196+
// /home/runner/work/atmos/atmos/examples/demo-stacks/stacks/deploy/**/*
197+
// --> /absolute/path/to/repo/examples/demo-stacks/stacks/deploy/**/*
198+
func sanitizeOutput(output string) (string, error) {
199+
// 1. Get the repository root.
200+
repoRoot, err := findGitRepoRoot(startingDir)
201+
if err != nil {
202+
return "", err
203+
}
204+
205+
if repoRoot == "" {
206+
return "", errors.New("failed to determine repository root")
207+
}
208+
209+
// 2. Normalize the repository root:
210+
// - Clean the path (which may not collapse all extra slashes after the drive letter, etc.)
211+
// - Convert to forward slashes,
212+
// - And explicitly collapse extra slashes.
213+
normalizedRepoRoot := collapseExtraSlashes(filepath.ToSlash(filepath.Clean(repoRoot)))
214+
// Also normalize the output to use forward slashes.
215+
normalizedOutput := filepath.ToSlash(output)
216+
217+
// 3. Build a regex that matches the repository root even if extra slashes appear.
218+
// First, escape any regex metacharacters in the normalized repository root.
219+
quoted := regexp.QuoteMeta(normalizedRepoRoot)
220+
// Replace each literal "/" with the regex token "/+" so that e.g. "a/b/c" becomes "a/+b/+c".
221+
patternBody := strings.ReplaceAll(quoted, "/", "/+")
222+
// Allow for extra trailing slashes.
223+
pattern := patternBody + "/*"
224+
repoRootRegex, err := regexp.Compile(pattern)
225+
if err != nil {
226+
return "", err
227+
}
228+
229+
// 4. Replace any occurrence of the repository root (with extra slashes) with a fixed placeholder.
230+
// The placeholder will end with exactly one slash.
231+
placeholder := "/absolute/path/to/repo/"
232+
replaced := repoRootRegex.ReplaceAllString(normalizedOutput, placeholder)
233+
234+
// 5. Now collapse extra slashes in the remainder of file paths that start with the placeholder.
235+
// We use a regex to find segments that start with the placeholder followed by some path characters.
236+
// (We assume that file paths appear in quotes or other delimited contexts, and that URLs won't match.)
237+
fixRegex := regexp.MustCompile(`(/absolute/path/to/repo)([^",]+)`)
238+
result := fixRegex.ReplaceAllStringFunc(replaced, func(match string) string {
239+
// The regex has two groups: group 1 is the placeholder, group 2 is the remainder.
240+
groups := fixRegex.FindStringSubmatch(match)
241+
if len(groups) < 3 {
242+
return match
243+
}
244+
// Collapse extra slashes in the remainder.
245+
fixedRemainder := collapseExtraSlashes(groups[2])
246+
return groups[1] + fixedRemainder
247+
})
248+
249+
return result, nil
176250
}
177251

178252
// sanitizeTestName converts t.Name() into a valid filename.
@@ -561,11 +635,11 @@ func verifyFileContains(t *testing.T, filePatterns map[string][]MatchPattern) bo
561635
}
562636

563637
func updateSnapshot(fullPath, output string) {
564-
err := os.MkdirAll(filepath.Dir(fullPath), 0755) // Ensure parent directories exist
638+
err := os.MkdirAll(filepath.Dir(fullPath), 0o755) // Ensure parent directories exist
565639
if err != nil {
566640
panic(fmt.Sprintf("Failed to create snapshot directory: %v", err))
567641
}
568-
err = os.WriteFile(fullPath, []byte(output), 0644) // Write snapshot
642+
err = os.WriteFile(fullPath, []byte(output), 0o644) // Write snapshot
569643
if err != nil {
570644
panic(fmt.Sprintf("Failed to write snapshot file: %v", err))
571645
}
@@ -645,6 +719,17 @@ func verifySnapshot(t *testing.T, tc TestCase, stdoutOutput, stderrOutput string
645719
return true
646720
}
647721

722+
// Sanitize outputs and fail the test if sanitization fails.
723+
var err error
724+
stdoutOutput, err = sanitizeOutput(stdoutOutput)
725+
if err != nil {
726+
t.Fatalf("failed to sanitize stdout output: %v", err)
727+
}
728+
stderrOutput, err = sanitizeOutput(stderrOutput)
729+
if err != nil {
730+
t.Fatalf("failed to sanitize stderr output: %v", err)
731+
}
732+
648733
testName := sanitizeTestName(t.Name())
649734
stdoutFileName := fmt.Sprintf("%s.stdout.golden", testName)
650735
stderrFileName := fmt.Sprintf("%s.stderr.golden", testName)
@@ -675,7 +760,6 @@ $ go test -run=%q -regenerate-snapshots`, stdoutPath, t.Name())
675760
if isCIEnvironment() || !term.IsTerminal(int(os.Stdout.Fd())) {
676761
// Generate a colorized diff for better readability
677762
diff = generateUnifiedDiff(filteredStdoutActual, filteredStdoutExpected)
678-
679763
} else {
680764
diff = colorizeDiffWithThreshold(filteredStdoutActual, filteredStdoutExpected, 10)
681765
}
@@ -706,6 +790,29 @@ $ go test -run=%q -regenerate-snapshots`, stderrPath, t.Name())
706790
return true
707791
}
708792

793+
// findGitRepo finds the Git repository root
794+
func findGitRepoRoot(path string) (string, error) {
795+
// Open the Git repository starting from the given path
796+
repo, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{DetectDotGit: true})
797+
if err != nil {
798+
return "", fmt.Errorf("failed to find git repository: %w", err)
799+
}
800+
801+
// Get the repository's working tree
802+
worktree, err := repo.Worktree()
803+
if err != nil {
804+
return "", fmt.Errorf("failed to get worktree: %w", err)
805+
}
806+
807+
// Return the absolute path to the root of the working tree
808+
root, err := filepath.Abs(worktree.Filesystem.Root())
809+
if err != nil {
810+
return "", fmt.Errorf("failed to get absolute path of repository root: %w", err)
811+
}
812+
813+
return root, nil
814+
}
815+
709816
func TestUnmarshalMatchPattern(t *testing.T) {
710817
yamlData := `
711818
expect:

tests/snapshots/TestCLICommands_atmos_describe_config.stdout.golden

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/snapshots/TestCLICommands_atmos_describe_config_-f_yaml.stdout.golden

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/test-cases/demo-stacks.yaml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,7 @@ tests:
9696
- "-f"
9797
- "yaml"
9898
expect:
99-
diff:
100-
- "stacksBaseAbsolutePath"
101-
- "terraformDirAbsolutePath"
102-
- "helmfileDirAbsolutePath"
103-
- 'examples[/\\]+demo-stacks[/\\]+stacks[/\\]+\*\*[/\\]+_defaults.yaml'
104-
- 'examples[/\\]+demo-stacks[/\\]+stacks[/\\]deploy[/\\]+\*\*[/\\]+\*'
99+
diff: []
105100
stdout:
106101
- 'append_user_agent: Atmos/(\d+\.\d+\.\d+|test) \(Cloud Posse; \+https:\/\/atmos\.tools\)'
107102
stderr:
@@ -120,11 +115,6 @@ tests:
120115
expect:
121116
diff:
122117
- '"append_user_agent": "Atmos/(\d+\.\d+\.\d+|test) \(Cloud Posse; \+https:\/\/atmos\.tools\)"'
123-
- "stacksBaseAbsolutePath"
124-
- "terraformDirAbsolutePath"
125-
- "helmfileDirAbsolutePath"
126-
- 'examples[/\\]+demo-stacks[/\\]+stacks[/\\]+\*\*[/\\]+_defaults.yaml'
127-
- 'examples[/\\]+demo-stacks[/\\]+stacks[/\\]+deploy[/\\]+\*\*[/\\]+\*'
128118
stdout:
129119
- '"append_user_agent": "Atmos/(\d+\.\d+\.\d+|test) \(Cloud Posse; \+https:\/\/atmos\.tools\)"'
130120
stderr:

0 commit comments

Comments
 (0)