Repository: MarvinJWendt/testza (go)

Issue:
Snapshot saves slice capacities, failing when executing on different machines
I have snapshots that have a `[]byte` slice member as part of a struct that is snapshotted. This saves the slice capacity to the snapshot file, resulting in failures when tests run on different machines (as capacity is expanded differently).

For example:

```
           7| ( 4. -)   Content: ([]uint8) (len=116 cap=128) {
           8| ( 4. +)   Content: ([]uint8) (len=116 cap=116) {
```

These slices are effectively identical, varying only by capacity.

I'm not sure if this behaviour is desired in general (even though it's not for me) or expected. I'm happy to look at creating a fix, let me know.

Thanks for the really useful library!

Pull request:
Remove capacity from snapshots
Changes the snapshot functionality such that capacities for slices are no longer emitted into the created snapshot files. Slice capacities can vary across different runtimes, creating the potential for intermittent snapshot comparison failures.

While this doesn't break the API surface, it does change behaviour is such a way that will cause existing tests that save capacities to fail when they update the library version. Given the test instability that `cap` can introduce, my preference would be to go ahead outside of a `v2` style version upgrade, but I'd like to get your feedback.

Another point of possible contention is that Arrays also have a capacity which is not variable, and there is a possibility that varying such a capacity would be a semantic error that a user of the library would want to catch.

Fixes #184

## Notes

In order to make this change safely, I've introduced an additional test to expose the behaviour, and pulled the `Sdump()` calls into a separate function. This allows for the use ofof `go-spew`'s `ConfigState` struct instead of the global config. This makes varying configuration more straightforward, as it doesn't need to be restored, and removes the possibility of strange errors caused by concurrency in tests.

Patch:
diff --git a/snapshot.go b/snapshot.go
index 47c7171..8167675 100644
--- a/snapshot.go
+++ b/snapshot.go
@@ -33,14 +33,11 @@ func snapshotCreateForDir(dir string, name string, snapshotObject any) error {
 		return fmt.Errorf("creating snapshot failed: %w", err)
 	}
 
-	originalSpewConfig := spew.Config.DisablePointerAddresses
-	spew.Config.DisablePointerAddresses = true
-	dump := strings.ReplaceAll(spew.Sdump(snapshotObject), "\r\n", "\n")
+	dump := strings.ReplaceAll(createSnapshotText(snapshotObject), "\r\n", "\n")
 	err = os.WriteFile(path.Clean(dir+name+".testza"), []byte(dump), 0755)
 	if err != nil {
 		return fmt.Errorf("creating snapshot failed: %w", err)
 	}
-	spew.Config.DisablePointerAddresses = originalSpewConfig
 
 	return nil
 }
@@ -64,14 +61,14 @@ var snapshotStringMatcher = regexp.MustCompile(`(?m)^\(.+?\)\s\(len=\d+\)\s(".+"
 func snapshotValidateFromDir(dir string, t testRunner, name string, actual any, msg ...any) error {
 	snapshotPath := path.Clean(dir + name + ".testza")
 	snapshotContent, err := os.ReadFile(snapshotPath)
-	snapshot := strings.ReplaceAll(string(snapshotContent), "\r\n", "\n")
 	if err != nil {
 		return fmt.Errorf("validating snapshot failed: %w", err)
 	}
-	originalSpewConfig := spew.Config.DisablePointerAddresses
-	spew.Config.DisablePointerAddresses = true
+	snapshot := strings.ReplaceAll(string(snapshotContent), "\r\n", "\n")
 
-	if spew.Sdump(actual) != snapshot {
+	actualSnapshot := createSnapshotText(actual)
+
+	if actualSnapshot != snapshot {
 		var diffObject *internal.Object
 		if strActual, ok := actual.(string); ok {
 			if match := snapshotStringMatcher.FindStringSubmatch(snapshot); len(match) > 0 {
@@ -83,7 +80,7 @@ func snapshotValidateFromDir(dir string, t testRunner, name string, actual any,
 		}
 
 		if diffObject == nil {
-			object := internal.NewDiffObject(snapshot, spew.Sdump(actual), true)
+			object := internal.NewDiffObject(snapshot, actualSnapshot, true)
 			diffObject = &object
 		}
 
@@ -102,15 +99,13 @@ func snapshotValidateFromDir(dir string, t testRunner, name string, actual any,
 				{
 					Name:      "Actual",
 					NameStyle: pterm.NewStyle(pterm.FgLightRed),
-					Data:      spew.Sdump(actual),
+					Data:      actualSnapshot,
 					DataStyle: pterm.NewStyle(pterm.FgRed),
 					Raw:       true,
 				},
 			})
 	}
 
-	spew.Config.DisablePointerAddresses = originalSpewConfig
-
 	return nil
 }
 
@@ -152,3 +147,12 @@ func SnapshotCreateOrValidate(t testRunner, name string, object any, msg ...any)
 
 	return nil
 }
+
+func createSnapshotText(object any) string {
+	cfg := spew.NewDefaultConfig()
+
+	cfg.DisablePointerAddresses = true
+	cfg.DisableCapacities = true
+
+	return cfg.Sdump(object)
+}


Test patch:
diff --git a/snapshot_test.go b/snapshot_test.go
index 9746b65..ff5b625 100644
--- a/snapshot_test.go
+++ b/snapshot_test.go
@@ -17,6 +17,17 @@ type snapshotObjectType struct {
 	Birthday time.Time
 }
 
+type snapshotComplexObjectType struct {
+	Name     string
+	Username string
+	Birthday time.Time
+	Nested   struct {
+		ChildName    string
+		PointerField *string
+		Set          []int
+	}
+}
+
 var snapshotObject = snapshotObjectType{
 	Name:     "Marvin Wendt",
 	Username: "MarvinJWendt",
@@ -27,6 +38,21 @@ var snapshotName = "TestSnapshotCreate_file_content"
 var snapshotString = `hello world`
 var snapshotNameString = "TestSnapshotCreate_file_content_string"
 
+var snapshotComplexObject = snapshotComplexObjectType{
+	Name:     "Marvin Wendt",
+	Username: "MarvinJWendt",
+	Birthday: time.Date(2001, time.January, 24, 0, 0, 0, 0, time.UTC),
+	Nested: struct {
+		ChildName    string
+		PointerField *string
+		Set          []int
+	}{
+		ChildName:    "as yet untitled",
+		PointerField: &snapshotString,
+		Set:          []int{1, 2, 3, 4, 3, 2, 1},
+	},
+}
+
 func TestSnapshotCreate_file_content(t *testing.T) {
 	err := testza.SnapshotCreate(snapshotName, snapshotObject)
 	testza.AssertNoError(t, err)
@@ -74,6 +100,11 @@ func TestSnapshotCreateOrValidate(t *testing.T) {
 	testza.AssertNoError(t, err)
 }
 
+func TestSnapshotCreateOrValidate_complex_object(t *testing.T) {
+	err := testza.SnapshotCreateOrValidate(t, t.Name(), snapshotComplexObject)
+	testza.AssertNoError(t, err)
+}
+
 func TestSnapshotCreateOrValidate_create_random(t *testing.T) {
 	name := t.Name() + testza.FuzzStringGenerateRandom(1, rand.Intn(10))[0]
 	err := testza.SnapshotCreateOrValidate(t, name, snapshotObject)
diff --git a/testdata/snapshots/TestSnapshotCreateOrValidate_complex_object.testza b/testdata/snapshots/TestSnapshotCreateOrValidate_complex_object.testza
new file mode 100755
index 0000000..0ff8649
--- /dev/null
+++ b/testdata/snapshots/TestSnapshotCreateOrValidate_complex_object.testza
@@ -0,0 +1,18 @@
+(testza_test.snapshotComplexObjectType) {
+ Name: (string) (len=12) "Marvin Wendt",
+ Username: (string) (len=12) "MarvinJWendt",
+ Birthday: (time.Time) 2001-01-24 00:00:00 +0000 UTC,
+ Nested: (struct { ChildName string; PointerField *string; Set []int }) {
+  ChildName: (string) (len=15) "as yet untitled",
+  PointerField: (*string)((len=11) "hello world"),
+  Set: ([]int) (len=7) {
+   (int) 1,
+   (int) 2,
+   (int) 3,
+   (int) 4,
+   (int) 3,
+   (int) 2,
+   (int) 1
+  }
+ }
+}