Approval-based snapshot testing library for Go (mirror)

feat: `SnapFunc` function and general cleanup/improvement

+147 -139
+2 -1
__snapshots__/test_accept.snap
··· 1 1 --- 2 - version: 0.1.0 3 2 test_name: TestAccept 3 + file_path: 4 + func_name: 4 5 --- 5 6 new content to accept
+8
__snapshots__/test_map.snap
··· 1 + --- 2 + test_name: TestMap 3 + file_path: /home/patrick/projects/freeze/freeze.go 4 + func_name: 5 + --- 6 + map[string]interface{}{ 7 + "foo": "bar", 8 + }
-7
__snapshots__/test_map.snap.new
··· 1 - --- 2 - version: 0.1.0 3 - test_name: TestMap 4 - --- 5 - map[string]interface{}{ 6 - "foo": "bar", 7 - }
+9
__snapshots__/test_snap_custom_type.snap
··· 1 + --- 2 + test_name: TestSnapCustomType 3 + file_path: /home/patrick/projects/freeze/freeze.go 4 + func_name: 5 + --- 6 + freeze_test.CustomStruct{ 7 + Name: "Alice", 8 + Age: 30, 9 + }
-8
__snapshots__/test_snap_custom_type.snap.new
··· 1 - --- 2 - version: 0.1.0 3 - test_name: TestSnapCustomType 4 - --- 5 - freeze_test.CustomStruct{ 6 - Name: "Alice", 7 - Age: 30, 8 - }
+6
__snapshots__/test_snap_func.snap
··· 1 + --- 2 + test_name: TestSnapFunc 3 + file_path: /home/patrick/projects/freeze/freeze_test.go 4 + func_name: testHelperFunction 5 + --- 6 + "helper result"
+6
__snapshots__/test_snap_func_another_helper.snap
··· 1 + --- 2 + test_name: TestSnapFuncAnotherHelper 3 + file_path: /home/patrick/projects/freeze/freeze_test.go 4 + func_name: calculateSomething 5 + --- 6 + 10
+2 -1
__snapshots__/test_snap_multiple.snap.new __snapshots__/test_snap_multiple.snap
··· 1 1 --- 2 - version: 0.1.0 3 2 test_name: TestSnapMultiple 3 + file_path: /home/patrick/projects/freeze/freeze.go 4 + func_name: 4 5 --- 5 6 "value1" 6 7 "value2"
+6
__snapshots__/test_snap_string.snap
··· 1 + --- 2 + test_name: TestSnapString 3 + file_path: /home/patrick/projects/freeze/freeze.go 4 + func_name: 5 + --- 6 + hello world
-5
__snapshots__/test_snap_string.snap.new
··· 1 - --- 2 - version: 0.1.0 3 - test_name: TestSnapString 4 - --- 5 - hello world
+4 -36
api.go
··· 38 38 return api.NewSnapshotBox(snap) 39 39 } 40 40 41 - func DiffSnapshotBox(old, new *Snapshot) string { 42 - return api.DiffSnapshotBox(old, new) 43 - } 44 - 45 - func Red(s string) string { 46 - return api.Red(s) 47 - } 48 - 49 - func Green(s string) string { 50 - return api.Green(s) 51 - } 52 - 53 - func Yellow(s string) string { 54 - return api.Yellow(s) 55 - } 56 - 57 - func Blue(s string) string { 58 - return api.Blue(s) 59 - } 60 - 61 - func Gray(s string) string { 62 - return api.Gray(s) 63 - } 64 - 65 - func Bold(s string) string { 66 - return api.Bold(s) 67 - } 68 - 69 - func TerminalWidth() int { 70 - return api.TerminalWidth() 71 - } 72 - 73 - func ClearScreen() { 74 - api.ClearScreen() 41 + func NewSnapshotBoxFunc(snap *Snapshot) string { 42 + return api.NewSnapshotBoxFunc(snap) 75 43 } 76 44 77 - func ClearLine() { 78 - api.ClearLine() 45 + func DiffSnapshotBox(old, new *Snapshot) string { 46 + return api.DiffSnapshotBox(old, new) 79 47 }
+29 -11
freeze.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 + "runtime" 5 6 6 7 "github.com/kortschak/utter" 7 8 "github.com/ptdewey/freeze/internal/diff" ··· 9 10 "github.com/ptdewey/freeze/internal/pretty" 10 11 "github.com/ptdewey/freeze/internal/review" 11 12 ) 12 - 13 - const version = "0.1.0" 14 13 15 14 // TODO: probably make this (and other things) configurable 16 15 func init() { ··· 34 33 snapWithTitle(t, title, content) 35 34 } 36 35 36 + func SnapFunc(t testingT, values ...any) { 37 + t.Helper() 38 + content := formatValues(values...) 39 + snapWithTitle(t, t.Name(), content, t.Name()) 40 + } 41 + 42 + func SnapFuncWithName(t testingT, funcName string, values ...any) { 43 + t.Helper() 44 + content := formatValues(values...) 45 + snapWithTitle(t, t.Name(), content, funcName) 46 + } 47 + 37 48 func snap(t testingT, content string) { 38 49 t.Helper() 39 50 testName := t.Name() 40 51 snapWithTitle(t, testName, content) 41 52 } 42 53 43 - func snapWithTitle(t testingT, title string, content string) { 54 + func snapWithTitle(t testingT, title string, content string, funcName ...string) { 44 55 t.Helper() 45 56 57 + _, filePath, _, _ := runtime.Caller(2) 58 + 46 59 snapshot := &files.Snapshot{ 47 - Version: version, 48 - Name: title, 49 - Content: content, 60 + Name: title, 61 + FilePath: filePath, 62 + Content: content, 63 + } 64 + 65 + if len(funcName) > 0 && funcName[0] != "" { 66 + snapshot.FuncName = funcName[0] 50 67 } 51 68 52 69 accepted, err := files.ReadAccepted(title) ··· 71 88 return 72 89 } 73 90 74 - fmt.Println(pretty.NewSnapshotBox(snapshot)) 91 + if len(funcName) > 0 && funcName[0] != "" { 92 + fmt.Println(pretty.NewSnapshotBoxFunc(snapshot)) 93 + } else { 94 + fmt.Println(pretty.NewSnapshotBox(snapshot)) 95 + } 75 96 t.Error("new snapshot created - run 'freeze review' to accept") 76 97 } 77 98 ··· 96 117 } 97 118 98 119 func formatValue(v any) string { 99 - // if v == nil { 100 - // return "<nil>" 101 - // } 102 - 103 120 return utter.Sdump(v) 104 121 } 105 122 123 + // DOCS: 106 124 func Review() error { 107 125 return review.Review() 108 126 }
+19 -30
freeze_test.go
··· 7 7 "testing" 8 8 9 9 "github.com/ptdewey/freeze" 10 + "github.com/ptdewey/freeze/internal/api" 10 11 ) 11 12 12 13 func TestSnapString(t *testing.T) { ··· 40 41 }) 41 42 } 42 43 44 + func testHelperFunction() string { 45 + return "helper result" 46 + } 47 + 48 + func TestSnapFunc(t *testing.T) { 49 + freeze.SnapFuncWithName(t, "testHelperFunction", testHelperFunction()) 50 + } 51 + 52 + func TestSnapFuncAnotherHelper(t *testing.T) { 53 + freeze.SnapFuncWithName(t, "calculateSomething", calculateSomething(5)) 54 + } 55 + 56 + func calculateSomething(n int) int { 57 + return n * 2 58 + } 59 + 43 60 func TestSerializeDeserialize(t *testing.T) { 44 61 snap := &freeze.Snapshot{ 45 - Version: "1.0.0", 46 62 Name: "TestExample", 47 63 Content: "test content\nmultiline", 48 64 } 49 65 50 66 serialized := snap.Serialize() 51 - expected := "---\nversion: 1.0.0\ntest_name: TestExample\n---\ntest content\nmultiline" 67 + expected := "---\ntest_name: TestExample\nfile_path: \nfunc_name: \n---\ntest content\nmultiline" 52 68 if serialized != expected { 53 69 t.Errorf("expected:\n%s\ngot:\n%s", expected, serialized) 54 70 } ··· 58 74 t.Fatalf("failed to deserialize: %v", err) 59 75 } 60 76 61 - if deserialized.Version != snap.Version { 62 - t.Errorf("version mismatch: %s != %s", deserialized.Version, snap.Version) 63 - } 64 77 if deserialized.Name != snap.Name { 65 78 t.Errorf("test name mismatch: %s != %s", deserialized.Name, snap.Name) 66 79 } ··· 71 84 72 85 func TestFileOperations(t *testing.T) { 73 86 snap := &freeze.Snapshot{ 74 - Version: "0.1.0", 75 87 Name: "TestFileOps", 76 88 Content: "file test content", 77 89 } 78 90 79 - if err := freeze.SaveSnapshot(snap, "test"); err != nil { 91 + if err := api.SaveSnapshot(snap, "test"); err != nil { 80 92 t.Fatalf("failed to save snapshot: %v", err) 81 93 } 82 94 ··· 151 163 152 164 func TestDiffSnapshotBox(t *testing.T) { 153 165 old := &freeze.Snapshot{ 154 - Version: "0.1.0", 155 166 Name: "TestDiff", 156 167 Content: "old content", 157 168 } 158 169 159 170 new := &freeze.Snapshot{ 160 - Version: "0.1.0", 161 171 Name: "TestDiff", 162 172 Content: "new content", 163 173 } ··· 174 184 175 185 func TestNewSnapshotBox(t *testing.T) { 176 186 snap := &freeze.Snapshot{ 177 - Version: "0.1.0", 178 187 Name: "TestNew", 179 188 Content: "test content", 180 189 } ··· 186 195 187 196 if !contains(box, "New Snapshot") { 188 197 t.Error("NewSnapshotBox missing header") 189 - } 190 - } 191 - 192 - func TestFormatFunctions(t *testing.T) { 193 - tests := []struct { 194 - name string 195 - fn func(string) string 196 - text string 197 - }{ 198 - {"Red", freeze.Red, "error"}, 199 - {"Green", freeze.Green, "success"}, 200 - {"Yellow", freeze.Yellow, "warning"}, 201 - {"Blue", freeze.Blue, "info"}, 202 - } 203 - 204 - for _, tt := range tests { 205 - result := tt.fn(tt.text) 206 - if result == "" { 207 - t.Errorf("%s returned empty string", tt.name) 208 - } 209 198 } 210 199 } 211 200
+1 -1
go.mod
··· 2 2 3 3 go 1.25.2 4 4 5 - require github.com/kortschak/utter v1.7.0 // indirect 5 + require github.com/kortschak/utter v1.7.0
+4
internal/api/api.go
··· 40 40 return pretty.NewSnapshotBox(snap) 41 41 } 42 42 43 + func NewSnapshotBoxFunc(snap *Snapshot) string { 44 + return pretty.NewSnapshotBoxFunc(snap) 45 + } 46 + 43 47 func DiffSnapshotBox(old, new *Snapshot) string { 44 48 diffLines := convertDiffLines(diff.Histogram(old.Content, new.Content)) 45 49 return pretty.DiffSnapshotBox(old, new, diffLines)
+10 -7
internal/files/files.go
··· 9 9 ) 10 10 11 11 type Snapshot struct { 12 - Version string 13 - Name string 14 - Content string 12 + Name string 13 + FilePath string 14 + FuncName string 15 + Content string 15 16 } 16 17 17 18 func (s *Snapshot) Serialize() string { 18 - header := fmt.Sprintf("---\nversion: %s\ntest_name: %s\n---\n", s.Version, s.Name) 19 + header := fmt.Sprintf("---\ntest_name: %s\nfile_path: %s\nfunc_name: %s\n---\n", s.Name, s.FilePath, s.FuncName) 19 20 return header + s.Content 20 21 } 21 22 ··· 32 33 Content: content, 33 34 } 34 35 35 - for _, line := range strings.Split(header, "\n") { 36 + for line := range strings.SplitSeq(header, "\n") { 36 37 line = strings.TrimSpace(line) 37 38 if line == "" { 38 39 continue ··· 45 46 46 47 key, value := kv[0], kv[1] 47 48 switch key { 48 - case "version": 49 - snap.Version = value 50 49 case "test_name": 51 50 snap.Name = value 51 + case "file_path": 52 + snap.FilePath = value 53 + case "func_name": 54 + snap.FuncName = value 52 55 } 53 56 } 54 57
+10 -23
internal/files/files_test.go
··· 34 34 35 35 func TestSerializeDeserialize(t *testing.T) { 36 36 snap := &files.Snapshot{ 37 - Version: "1.0.0", 38 - Name: "TestExample", 39 - Content: "test content\nmultiline", 37 + Name: "TestExample", 38 + FilePath: "/path/to/test.go", 39 + Content: "test content\nmultiline", 40 40 } 41 41 42 42 serialized := snap.Serialize() 43 - expected := "---\nversion: 1.0.0\ntest_name: TestExample\n---\ntest content\nmultiline" 43 + expected := "---\ntest_name: TestExample\nfile_path: /path/to/test.go\nfunc_name: \n---\ntest content\nmultiline" 44 44 if serialized != expected { 45 45 t.Errorf("Serialize():\nexpected:\n%s\n\ngot:\n%s", expected, serialized) 46 46 } ··· 50 50 t.Fatalf("Deserialize failed: %v", err) 51 51 } 52 52 53 - if deserialized.Version != snap.Version { 54 - t.Errorf("Version mismatch: %s != %s", deserialized.Version, snap.Version) 55 - } 56 53 if deserialized.Name != snap.Name { 57 54 t.Errorf("Name mismatch: %s != %s", deserialized.Name, snap.Name) 55 + } 56 + if deserialized.FilePath != snap.FilePath { 57 + t.Errorf("FilePath mismatch: %s != %s", deserialized.FilePath, snap.FilePath) 58 58 } 59 59 if deserialized.Content != snap.Content { 60 60 t.Errorf("Content mismatch: %s != %s", deserialized.Content, snap.Content) ··· 85 85 tests := []struct { 86 86 name string 87 87 input string 88 - wantVer string 89 88 wantTest string 90 89 wantContent string 91 90 }{ 92 91 { 93 92 "simple", 94 - "---\nversion: 1.0\ntest_name: Test\n---\ncontent", 95 - "1.0", 93 + "---\ntest_name: Test\nfile_path: /path\nfunc_name: \n---\ncontent", 96 94 "Test", 97 95 "content", 98 96 }, 99 97 { 100 98 "multiline content", 101 - "---\nversion: 0.1\ntest_name: MyTest\n---\nline1\nline2\nline3", 102 - "0.1", 99 + "---\ntest_name: MyTest\nfile_path: /path\nfunc_name: \n---\nline1\nline2\nline3", 103 100 "MyTest", 104 101 "line1\nline2\nline3", 105 102 }, 106 103 { 107 104 "with extra fields", 108 - "---\nversion: 1.0\ntest_name: Test\nextra: ignored\n---\ncontent", 109 - "1.0", 105 + "---\ntest_name: Test\nfile_path: /path\nfunc_name: \nextra: ignored\n---\ncontent", 110 106 "Test", 111 107 "content", 112 108 }, ··· 118 114 if err != nil { 119 115 t.Fatalf("Deserialize failed: %v", err) 120 116 } 121 - if snap.Version != tt.wantVer { 122 - t.Errorf("Version = %s, want %s", snap.Version, tt.wantVer) 123 - } 124 117 if snap.Name != tt.wantTest { 125 118 t.Errorf("Name = %s, want %s", snap.Name, tt.wantTest) 126 119 } ··· 133 126 134 127 func TestSaveAndReadSnapshot(t *testing.T) { 135 128 snap := &files.Snapshot{ 136 - Version: "0.1.0", 137 129 Name: "TestSaveRead", 138 130 Content: "saved content", 139 131 } ··· 150 142 if read.Content != snap.Content { 151 143 t.Errorf("Content mismatch: %s != %s", read.Content, snap.Content) 152 144 } 153 - if read.Version != snap.Version { 154 - t.Errorf("Version mismatch: %s != %s", read.Version, snap.Version) 155 - } 156 145 157 146 cleanupSnapshot(t, "TestSaveRead", "test") 158 147 } ··· 166 155 167 156 func TestAcceptSnapshot(t *testing.T) { 168 157 newSnap := &files.Snapshot{ 169 - Version: "0.1.0", 170 158 Name: "TestAccept", 171 159 Content: "new content to accept", 172 160 } ··· 198 186 199 187 func TestRejectSnapshot(t *testing.T) { 200 188 snap := &files.Snapshot{ 201 - Version: "0.1.0", 202 189 Name: "TestReject", 203 190 Content: "content to reject", 204 191 }
+26 -8
internal/pretty/boxes.go
··· 22 22 DiffNew 23 23 ) 24 24 25 - func NewSnapshotBox(snap *files.Snapshot) string { 25 + func newSnapshotBoxInternal(snap *files.Snapshot, isFuncSnapshot bool) string { 26 26 width := TerminalWidth() 27 27 28 28 var sb strings.Builder 29 29 sb.WriteString("─── " + "New Snapshot " + strings.Repeat("─", width-15) + "\n\n") 30 - sb.WriteString(fmt.Sprintf(" test: %s\n", Blue("\""+snap.Name+"\""))) 31 - sb.WriteString(fmt.Sprintf(" snapshot: %s\n\n", Gray(files.SnapshotFileName(snap.Name)))) 30 + 31 + if isFuncSnapshot && snap.FuncName != "" { 32 + sb.WriteString(fmt.Sprintf(" func: %s\n", Blue("\""+snap.FuncName+"\""))) 33 + sb.WriteString(fmt.Sprintf(" test: %s\n", Blue("\""+snap.Name+"\""))) 34 + } else { 35 + sb.WriteString(fmt.Sprintf(" test: %s\n", Blue("\""+snap.Name+"\""))) 36 + } 37 + 38 + sb.WriteString(fmt.Sprintf(" snapshot: %s\n", Gray(files.SnapshotFileName(snap.Name)+".snap.new"))) 39 + if snap.FilePath != "" { 40 + sb.WriteString(fmt.Sprintf(" file: %s\n", Gray(snap.FilePath))) 41 + } 42 + sb.WriteString("\n") 32 43 33 44 lines := strings.Split(snap.Content, "\n") 34 45 numLines := len(lines) ··· 55 66 return sb.String() 56 67 } 57 68 58 - // TODO: needs to get overhauled with styling like above 59 - // - should show line numbers, line numbers with diffs should be the same 60 - // - should show test name and path in the header section 61 - // TODO: additional styling 62 - // show helper text to say + is new results, - is old snapshot 69 + func NewSnapshotBox(snap *files.Snapshot) string { 70 + return newSnapshotBoxInternal(snap, false) 71 + } 72 + 73 + func NewSnapshotBoxFunc(snap *files.Snapshot) string { 74 + return newSnapshotBoxInternal(snap, true) 75 + } 76 + 77 + // TODO: diff should show old and new line numbers 63 78 func DiffSnapshotBox(old, new *files.Snapshot, diffLines []DiffLine) string { 64 79 width := TerminalWidth() 65 80 66 81 var sb strings.Builder 67 82 sb.WriteString(strings.Repeat("─", width) + "\n") 68 83 sb.WriteString(fmt.Sprintf(" %s\n", Blue("Snapshot Diff"))) 84 + if new.FilePath != "" { 85 + sb.WriteString(fmt.Sprintf(" file: %s\n", Gray(new.FilePath))) 86 + } 69 87 sb.WriteString(strings.Repeat("─", width) + "\n") 70 88 71 89 for _, dl := range diffLines {
+5 -1
internal/review/review.go
··· 75 75 diffLines := computeDiffLines(accepted, newSnap) 76 76 fmt.Println(pretty.DiffSnapshotBox(accepted, newSnap, diffLines)) 77 77 } else { 78 - fmt.Println(pretty.NewSnapshotBox(newSnap)) 78 + if newSnap.FuncName != "" { 79 + fmt.Println(pretty.NewSnapshotBoxFunc(newSnap)) 80 + } else { 81 + fmt.Println(pretty.NewSnapshotBox(newSnap)) 82 + } 79 83 } 80 84 81 85 for {