@@ -16,6 +16,7 @@ import (
16
16
17
17
"github.com/charmbracelet/lipgloss"
18
18
"github.com/creack/pty"
19
+ "github.com/go-git/go-git/v5"
19
20
"github.com/hexops/gotextdiff"
20
21
"github.com/hexops/gotextdiff/myers"
21
22
"github.com/hexops/gotextdiff/span"
@@ -26,9 +27,11 @@ import (
26
27
)
27
28
28
29
// 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
+ )
32
35
33
36
// Define styles using lipgloss
34
37
var (
@@ -172,7 +175,78 @@ func (pm *PathManager) Apply() error {
172
175
173
176
// Determine if running in a CI environment
174
177
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
176
250
}
177
251
178
252
// sanitizeTestName converts t.Name() into a valid filename.
@@ -561,11 +635,11 @@ func verifyFileContains(t *testing.T, filePatterns map[string][]MatchPattern) bo
561
635
}
562
636
563
637
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
565
639
if err != nil {
566
640
panic (fmt .Sprintf ("Failed to create snapshot directory: %v" , err ))
567
641
}
568
- err = os .WriteFile (fullPath , []byte (output ), 0644 ) // Write snapshot
642
+ err = os .WriteFile (fullPath , []byte (output ), 0o644 ) // Write snapshot
569
643
if err != nil {
570
644
panic (fmt .Sprintf ("Failed to write snapshot file: %v" , err ))
571
645
}
@@ -645,6 +719,17 @@ func verifySnapshot(t *testing.T, tc TestCase, stdoutOutput, stderrOutput string
645
719
return true
646
720
}
647
721
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
+
648
733
testName := sanitizeTestName (t .Name ())
649
734
stdoutFileName := fmt .Sprintf ("%s.stdout.golden" , testName )
650
735
stderrFileName := fmt .Sprintf ("%s.stderr.golden" , testName )
@@ -675,7 +760,6 @@ $ go test -run=%q -regenerate-snapshots`, stdoutPath, t.Name())
675
760
if isCIEnvironment () || ! term .IsTerminal (int (os .Stdout .Fd ())) {
676
761
// Generate a colorized diff for better readability
677
762
diff = generateUnifiedDiff (filteredStdoutActual , filteredStdoutExpected )
678
-
679
763
} else {
680
764
diff = colorizeDiffWithThreshold (filteredStdoutActual , filteredStdoutExpected , 10 )
681
765
}
@@ -706,6 +790,29 @@ $ go test -run=%q -regenerate-snapshots`, stderrPath, t.Name())
706
790
return true
707
791
}
708
792
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
+
709
816
func TestUnmarshalMatchPattern (t * testing.T ) {
710
817
yamlData := `
711
818
expect:
0 commit comments