Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sanitize snapshots #1002

Merged
merged 13 commits into from
Feb 4, 2025
121 changes: 114 additions & 7 deletions tests/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/charmbracelet/lipgloss"
"github.com/creack/pty"
"github.com/go-git/go-git/v5"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
Expand All @@ -26,9 +27,11 @@ import (
)

// Command-line flag for regenerating snapshots
var regenerateSnapshots = flag.Bool("regenerate-snapshots", false, "Regenerate all golden snapshots")
var startingDir string
var snapshotBaseDir string
var (
regenerateSnapshots = flag.Bool("regenerate-snapshots", false, "Regenerate all golden snapshots")
startingDir string
snapshotBaseDir string
)

// Define styles using lipgloss
var (
Expand Down Expand Up @@ -172,7 +175,78 @@ func (pm *PathManager) Apply() error {

// Determine if running in a CI environment
func isCIEnvironment() bool {
return os.Getenv("CI") != ""
// Check for common CI environment variables
// Note, that the CI variable has many possible truthy values, so we check for any non-empty value that is not "false".
return (os.Getenv("CI") != "" && os.Getenv("CI") != "false") || os.Getenv("GITHUB_ACTIONS") == "true"
}

// collapseExtraSlashes replaces multiple consecutive slashes with a single slash.
func collapseExtraSlashes(s string) string {
return regexp.MustCompile("/+").ReplaceAllString(s, "/")
}

// sanitizeOutput replaces occurrences of the repository's absolute path in the output
// with the placeholder "/absolute/path/to/repo". It first normalizes both the repository root
// and the output to use forward slashes, ensuring that the replacement works reliably.
// An error is returned if the repository root cannot be determined.
// Convert something like:
//
// D:\\a\atmos\atmos\examples\demo-stacks\stacks\deploy\**\*
// --> /absolute/path/to/repo/examples/demo-stacks/stacks/deploy/**/*
// /home/runner/work/atmos/atmos/examples/demo-stacks/stacks/deploy/**/*
// --> /absolute/path/to/repo/examples/demo-stacks/stacks/deploy/**/*
func sanitizeOutput(output string) (string, error) {
// 1. Get the repository root.
repoRoot, err := findGitRepoRoot(startingDir)
if err != nil {
return "", err
}

if repoRoot == "" {
return "", errors.New("failed to determine repository root")
}

// 2. Normalize the repository root:
// - Clean the path (which may not collapse all extra slashes after the drive letter, etc.)
// - Convert to forward slashes,
// - And explicitly collapse extra slashes.
normalizedRepoRoot := collapseExtraSlashes(filepath.ToSlash(filepath.Clean(repoRoot)))
// Also normalize the output to use forward slashes.
normalizedOutput := filepath.ToSlash(output)

// 3. Build a regex that matches the repository root even if extra slashes appear.
// First, escape any regex metacharacters in the normalized repository root.
quoted := regexp.QuoteMeta(normalizedRepoRoot)
// Replace each literal "/" with the regex token "/+" so that e.g. "a/b/c" becomes "a/+b/+c".
patternBody := strings.ReplaceAll(quoted, "/", "/+")
// Allow for extra trailing slashes.
pattern := patternBody + "/*"
repoRootRegex, err := regexp.Compile(pattern)
if err != nil {
return "", err
}

// 4. Replace any occurrence of the repository root (with extra slashes) with a fixed placeholder.
// The placeholder will end with exactly one slash.
placeholder := "/absolute/path/to/repo/"
replaced := repoRootRegex.ReplaceAllString(normalizedOutput, placeholder)

// 5. Now collapse extra slashes in the remainder of file paths that start with the placeholder.
// We use a regex to find segments that start with the placeholder followed by some path characters.
// (We assume that file paths appear in quotes or other delimited contexts, and that URLs won't match.)
fixRegex := regexp.MustCompile(`(/absolute/path/to/repo)([^",]+)`)
result := fixRegex.ReplaceAllStringFunc(replaced, func(match string) string {
// The regex has two groups: group 1 is the placeholder, group 2 is the remainder.
groups := fixRegex.FindStringSubmatch(match)
if len(groups) < 3 {
return match
}
// Collapse extra slashes in the remainder.
fixedRemainder := collapseExtraSlashes(groups[2])
return groups[1] + fixedRemainder
})

return result, nil
}

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

func updateSnapshot(fullPath, output string) {
err := os.MkdirAll(filepath.Dir(fullPath), 0755) // Ensure parent directories exist
err := os.MkdirAll(filepath.Dir(fullPath), 0o755) // Ensure parent directories exist
if err != nil {
panic(fmt.Sprintf("Failed to create snapshot directory: %v", err))
}
err = os.WriteFile(fullPath, []byte(output), 0644) // Write snapshot
err = os.WriteFile(fullPath, []byte(output), 0o644) // Write snapshot
if err != nil {
panic(fmt.Sprintf("Failed to write snapshot file: %v", err))
}
Expand Down Expand Up @@ -645,6 +719,17 @@ func verifySnapshot(t *testing.T, tc TestCase, stdoutOutput, stderrOutput string
return true
}

// Sanitize outputs and fail the test if sanitization fails.
var err error
stdoutOutput, err = sanitizeOutput(stdoutOutput)
if err != nil {
t.Fatalf("failed to sanitize stdout output: %v", err)
}
stderrOutput, err = sanitizeOutput(stderrOutput)
if err != nil {
t.Fatalf("failed to sanitize stderr output: %v", err)
}

testName := sanitizeTestName(t.Name())
stdoutFileName := fmt.Sprintf("%s.stdout.golden", testName)
stderrFileName := fmt.Sprintf("%s.stderr.golden", testName)
Expand Down Expand Up @@ -675,7 +760,6 @@ $ go test -run=%q -regenerate-snapshots`, stdoutPath, t.Name())
if isCIEnvironment() || !term.IsTerminal(int(os.Stdout.Fd())) {
// Generate a colorized diff for better readability
diff = generateUnifiedDiff(filteredStdoutActual, filteredStdoutExpected)

} else {
diff = colorizeDiffWithThreshold(filteredStdoutActual, filteredStdoutExpected, 10)
}
Expand Down Expand Up @@ -706,6 +790,29 @@ $ go test -run=%q -regenerate-snapshots`, stderrPath, t.Name())
return true
}

// findGitRepo finds the Git repository root
func findGitRepoRoot(path string) (string, error) {
// Open the Git repository starting from the given path
repo, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
return "", fmt.Errorf("failed to find git repository: %w", err)
}

// Get the repository's working tree
worktree, err := repo.Worktree()
if err != nil {
return "", fmt.Errorf("failed to get worktree: %w", err)
}

// Return the absolute path to the root of the working tree
root, err := filepath.Abs(worktree.Filesystem.Root())
if err != nil {
return "", fmt.Errorf("failed to get absolute path of repository root: %w", err)
}

return root, nil
}

func TestUnmarshalMatchPattern(t *testing.T) {
yamlData := `
expect:
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 1 addition & 11 deletions tests/test-cases/demo-stacks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,7 @@ tests:
- "-f"
- "yaml"
expect:
diff:
- "stacksBaseAbsolutePath"
- "terraformDirAbsolutePath"
- "helmfileDirAbsolutePath"
- 'examples[/\\]+demo-stacks[/\\]+stacks[/\\]+\*\*[/\\]+_defaults.yaml'
- 'examples[/\\]+demo-stacks[/\\]+stacks[/\\]deploy[/\\]+\*\*[/\\]+\*'
diff: []
stdout:
- 'append_user_agent: Atmos/(\d+\.\d+\.\d+|test) \(Cloud Posse; \+https:\/\/atmos\.tools\)'
stderr:
Expand All @@ -120,11 +115,6 @@ tests:
expect:
diff:
- '"append_user_agent": "Atmos/(\d+\.\d+\.\d+|test) \(Cloud Posse; \+https:\/\/atmos\.tools\)"'
- "stacksBaseAbsolutePath"
- "terraformDirAbsolutePath"
- "helmfileDirAbsolutePath"
- 'examples[/\\]+demo-stacks[/\\]+stacks[/\\]+\*\*[/\\]+_defaults.yaml'
- 'examples[/\\]+demo-stacks[/\\]+stacks[/\\]+deploy[/\\]+\*\*[/\\]+\*'
stdout:
- '"append_user_agent": "Atmos/(\d+\.\d+\.\d+|test) \(Cloud Posse; \+https:\/\/atmos\.tools\)"'
stderr:
Expand Down