diff --git a/README.md b/README.md index 62e79be..8f6b685 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ TODO: TODO: +## Disclaimer + +- This package was largely vibe coded, your mileage may vary (but this library provides more of what I want than the ones below). + ## Other Libraries - [go-snaps](https://github.com/gkampitakis/go-snaps) diff --git a/__snapshots__/test_map.new b/__snapshots__/test_map.accepted similarity index 100% rename from __snapshots__/test_map.new rename to __snapshots__/test_map.accepted diff --git a/__snapshots__/test_snap_custom_type.new b/__snapshots__/test_snap_custom_type.accepted similarity index 100% rename from __snapshots__/test_snap_custom_type.new rename to __snapshots__/test_snap_custom_type.accepted diff --git a/__snapshots__/test_snap_multiple.accepted b/__snapshots__/test_snap_multiple.accepted new file mode 100644 index 0000000..bae6dcb --- /dev/null +++ b/__snapshots__/test_snap_multiple.accepted @@ -0,0 +1,13 @@ +--- +version: 0.1.0 +test_name: TestSnapMultiple +--- +value1 +value2 +42 +foo +bar +baz +wibble +wobble +tick \ No newline at end of file diff --git a/__snapshots__/test_snap_multiple.new b/__snapshots__/test_snap_multiple.new index bae6dcb..3e6866c 100644 --- a/__snapshots__/test_snap_multiple.new +++ b/__snapshots__/test_snap_multiple.new @@ -10,4 +10,4 @@ bar baz wibble wobble -tick \ No newline at end of file +tock \ No newline at end of file diff --git a/__snapshots__/test_snap_string.new b/__snapshots__/test_snap_string.accepted similarity index 100% rename from __snapshots__/test_snap_string.new rename to __snapshots__/test_snap_string.accepted diff --git a/api.go b/api.go new file mode 100644 index 0000000..071c933 --- /dev/null +++ b/api.go @@ -0,0 +1,82 @@ +package freeze + +import ( + "github.com/ptdewey/freeze/internal/diff" + "github.com/ptdewey/freeze/internal/files" + "github.com/ptdewey/freeze/internal/pretty" +) + +type Snapshot = files.Snapshot + +type DiffLine = diff.DiffLine + +const ( + DiffShared = diff.DiffShared + DiffOld = diff.DiffOld + DiffNew = diff.DiffNew +) + +func Deserialize(raw string) (*Snapshot, error) { + return files.Deserialize(raw) +} + +func SaveSnapshot(snap *Snapshot, state string) error { + return files.SaveSnapshot(snap, state) +} + +func ReadSnapshot(testName string, state string) (*Snapshot, error) { + return files.ReadSnapshot(testName, state) +} + +func SnapshotFileName(testName string) string { + return files.SnapshotFileName(testName) +} + +func Histogram(old, new string) []DiffLine { + return diff.Histogram(old, new) +} + +func NewSnapshotBox(snap *Snapshot) string { + return pretty.NewSnapshotBox(snap) +} + +func DiffSnapshotBox(old, new *Snapshot) string { + diffLines := convertDiffLines(diff.Histogram(old.Content, new.Content)) + return pretty.DiffSnapshotBox(old, new, diffLines) +} + +func Red(s string) string { + return pretty.Red(s) +} + +func Green(s string) string { + return pretty.Green(s) +} + +func Yellow(s string) string { + return pretty.Yellow(s) +} + +func Blue(s string) string { + return pretty.Blue(s) +} + +func Gray(s string) string { + return pretty.Gray(s) +} + +func Bold(s string) string { + return pretty.Bold(s) +} + +func TerminalWidth() int { + return pretty.TerminalWidth() +} + +func ClearScreen() { + pretty.ClearScreen() +} + +func ClearLine() { + pretty.ClearLine() +} diff --git a/freeze.go b/freeze.go index 648f1e6..b874a96 100644 --- a/freeze.go +++ b/freeze.go @@ -3,6 +3,10 @@ package freeze import ( "fmt" "reflect" + + "github.com/ptdewey/freeze/internal/diff" + "github.com/ptdewey/freeze/internal/files" + "github.com/ptdewey/freeze/internal/pretty" ) const version = "0.1.0" @@ -33,37 +37,50 @@ func snap(t testingT, content string) { func snapWithTitle(t testingT, title string, content string) { t.Helper() - snapshot := &Snapshot{ + snapshot := &files.Snapshot{ Version: version, TestName: title, Content: content, } - accepted, err := readAccepted(title) + accepted, err := files.ReadAccepted(title) if err == nil { if accepted.Content == content { return } - if err := SaveSnapshot(snapshot, "new"); err != nil { + if err := files.SaveSnapshot(snapshot, "new"); err != nil { t.Error("failed to save snapshot:", err) return } - fmt.Println(DiffSnapshotBox(accepted, snapshot)) + diffLines := convertDiffLines(diff.Histogram(accepted.Content, snapshot.Content)) + fmt.Println(pretty.DiffSnapshotBox(accepted, snapshot, diffLines)) t.Error("snapshot mismatch - run 'freeze review' to update") return } - if err := SaveSnapshot(snapshot, "new"); err != nil { + if err := files.SaveSnapshot(snapshot, "new"); err != nil { t.Error("failed to save snapshot:", err) return } - fmt.Println(NewSnapshotBox(snapshot)) + fmt.Println(pretty.NewSnapshotBox(snapshot)) t.Error("new snapshot created - run 'freeze review' to accept") } +func convertDiffLines(diffLines []diff.DiffLine) []pretty.DiffLine { + result := make([]pretty.DiffLine, len(diffLines)) + for i, dl := range diffLines { + result[i] = pretty.DiffLine{ + Number: dl.Number, + Line: dl.Line, + Kind: pretty.DiffKind(dl.Kind), + } + } + return result +} + func formatValues(values ...any) string { if len(values) == 0 { return "" @@ -102,7 +119,7 @@ func formatValue(v any) string { case reflect.String: return v.(string) case reflect.Struct, reflect.Slice, reflect.Array, reflect.Map: - // TODO: make this better probably + // TODO: make this better probably (utter?) return fmt.Sprintf("%#v", v) default: return fmt.Sprint(v) diff --git a/freeze_test.go b/freeze_test.go index 1e0e543..b45a470 100644 --- a/freeze_test.go +++ b/freeze_test.go @@ -14,7 +14,7 @@ func TestSnapString(t *testing.T) { } func TestSnapMultiple(t *testing.T) { - freeze.Snap(t, "value1", "value2", 42, "foo", "bar", "baz", "wibble", "wobble", "tick") + freeze.Snap(t, "value1", "value2", 42, "foo", "bar", "baz", "wibble", "wobble", "tock") } type CustomStruct struct { diff --git a/diff.go b/internal/diff/diff.go similarity index 99% rename from diff.go rename to internal/diff/diff.go index b7fc76a..2186381 100644 --- a/diff.go +++ b/internal/diff/diff.go @@ -1,4 +1,4 @@ -package freeze +package diff import ( "strings" diff --git a/files.go b/internal/files/files.go similarity index 75% rename from files.go rename to internal/files/files.go index a4644d2..afb43a3 100644 --- a/files.go +++ b/internal/files/files.go @@ -1,4 +1,4 @@ -package freeze +package files import ( "fmt" @@ -8,6 +8,53 @@ import ( "strings" ) +type Snapshot struct { + Version string + TestName string + Content string +} + +func (s *Snapshot) Serialize() string { + header := fmt.Sprintf("---\nversion: %s\ntest_name: %s\n---\n", s.Version, s.TestName) + return header + s.Content +} + +func Deserialize(raw string) (*Snapshot, error) { + parts := strings.SplitN(raw, "---\n", 3) + if len(parts) < 3 { + return nil, fmt.Errorf("invalid snapshot format") + } + + header := parts[1] + content := parts[2] + + snap := &Snapshot{ + Content: content, + } + + for _, line := range strings.Split(header, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + kv := strings.SplitN(line, ": ", 2) + if len(kv) != 2 { + continue + } + + key, value := kv[0], kv[1] + switch key { + case "version": + snap.Version = value + case "test_name": + snap.TestName = value + } + } + + return snap, nil +} + func findProjectRoot() (string, error) { cwd, err := os.Getwd() if err != nil { @@ -34,9 +81,6 @@ func getSnapshotDir() (string, error) { return "", err } - // TODO: pull this from config. - // config should allow having snapshot dir at project root (with or w/o subdirs) - // or in a __snapshots__ dir inside of each package dir snapshotDir := filepath.Join(root, "__snapshots__") if err := os.MkdirAll(snapshotDir, 0755); err != nil { return "", err @@ -89,11 +133,11 @@ func ReadSnapshot(testName string, state string) (*Snapshot, error) { return Deserialize(string(data)) } -func readAccepted(testName string) (*Snapshot, error) { +func ReadAccepted(testName string) (*Snapshot, error) { return ReadSnapshot(testName, "accepted") } -func readNew(testName string) (*Snapshot, error) { +func ReadNew(testName string) (*Snapshot, error) { return ReadSnapshot(testName, "new") } diff --git a/format.go b/internal/pretty/boxes.go similarity index 52% rename from format.go rename to internal/pretty/boxes.go index a46ea8f..e2e3e40 100644 --- a/format.go +++ b/internal/pretty/boxes.go @@ -1,91 +1,34 @@ -package freeze +package pretty import ( "fmt" - "os" "strconv" "strings" -) -const ( - colorRed = "\033[31m" - colorGreen = "\033[32m" - colorYellow = "\033[33m" - colorBlue = "\033[34m" - colorGray = "\033[90m" - colorReset = "\033[0m" - colorBold = "\033[1m" + "github.com/ptdewey/freeze/internal/files" ) -func TerminalWidth() int { - width := os.Getenv("COLUMNS") - if w, err := strconv.Atoi(width); err == nil && w > 0 { - return w - } - return 80 -} - -func ClearScreen() { - fmt.Print("\033[2J") - fmt.Print("\033[H") -} - -func ClearLine() { - fmt.Print("\033[K") -} - -func Red(s string) string { - if !hasColor() { - return s - } - return colorRed + s + colorReset -} - -func Green(s string) string { - if !hasColor() { - return s - } - return colorGreen + s + colorReset -} - -func Yellow(s string) string { - if !hasColor() { - return s - } - return colorYellow + s + colorReset -} - -func Blue(s string) string { - if !hasColor() { - return s - } - return colorBlue + s + colorReset +type DiffLine struct { + Number int + Line string + Kind DiffKind } -func Gray(s string) string { - if !hasColor() { - return s - } - return colorGray + s + colorReset -} +type DiffKind int -func Bold(s string) string { - if !hasColor() { - return s - } - return colorBold + s + colorReset -} - -func hasColor() bool { - return os.Getenv("NO_COLOR") == "" -} +const ( + DiffShared DiffKind = iota + DiffOld + DiffNew +) -func NewSnapshotBox(snap *Snapshot) string { +func NewSnapshotBox(snap *files.Snapshot) string { width := TerminalWidth() var sb strings.Builder sb.WriteString(strings.Repeat("─", width+2) + "\n") - // TODO: add file path to a new line below this + // TODO: "New Snapshot" should be above this line, in default color. + // - color should be on test name and path sb.WriteString(fmt.Sprintf(" %s \n", Blue("New Snapshot -- \""+snap.TestName+"\""))) lines := strings.Split(snap.Content, "\n") @@ -113,12 +56,12 @@ func NewSnapshotBox(snap *Snapshot) string { return sb.String() } -// TODO: this probably needs the styling overhaul from above -func DiffSnapshotBox(old, new *Snapshot) string { +// TODO: needs to get overhauled with styling like above +// - should show line numbers, line numbers with diffs should be the same +// - should show test name and path in the header section +func DiffSnapshotBox(old, new *files.Snapshot, diffLines []DiffLine) string { width := TerminalWidth() - diffLines := Histogram(old.Content, new.Content) - var sb strings.Builder sb.WriteString(strings.Repeat("─", width) + "\n") sb.WriteString(fmt.Sprintf(" %s\n", Blue("Snapshot Diff"))) @@ -150,19 +93,3 @@ func DiffSnapshotBox(old, new *Snapshot) string { sb.WriteString(strings.Repeat("─", width) + "\n") return sb.String() } - -func FormatHeader(text string) string { - return Bold(Blue(text)) -} - -func FormatSuccess(text string) string { - return Green(text) -} - -func FormatError(text string) string { - return Red(text) -} - -func FormatWarning(text string) string { - return Yellow(text) -} diff --git a/internal/pretty/pretty.go b/internal/pretty/pretty.go new file mode 100644 index 0000000..59b8af5 --- /dev/null +++ b/internal/pretty/pretty.go @@ -0,0 +1,96 @@ +package pretty + +import ( + "fmt" + "os" + "strconv" +) + +const ( + colorRed = "\033[31m" + colorGreen = "\033[32m" + colorYellow = "\033[33m" + colorBlue = "\033[34m" + colorGray = "\033[90m" + colorReset = "\033[0m" + colorBold = "\033[1m" +) + +func TerminalWidth() int { + width := os.Getenv("COLUMNS") + if w, err := strconv.Atoi(width); err == nil && w > 0 { + return w + } + return 80 +} + +func ClearScreen() { + fmt.Print("\033[2J") + fmt.Print("\033[H") +} + +func ClearLine() { + fmt.Print("\033[K") +} + +func Red(s string) string { + if !hasColor() { + return s + } + return colorRed + s + colorReset +} + +func Green(s string) string { + if !hasColor() { + return s + } + return colorGreen + s + colorReset +} + +func Yellow(s string) string { + if !hasColor() { + return s + } + return colorYellow + s + colorReset +} + +func Blue(s string) string { + if !hasColor() { + return s + } + return colorBlue + s + colorReset +} + +func Gray(s string) string { + if !hasColor() { + return s + } + return colorGray + s + colorReset +} + +func Bold(s string) string { + if !hasColor() { + return s + } + return colorBold + s + colorReset +} + +func hasColor() bool { + return os.Getenv("NO_COLOR") == "" +} + +func Header(text string) string { + return Bold(Blue(text)) +} + +func Success(text string) string { + return Green(text) +} + +func Error(text string) string { + return Red(text) +} + +func Warning(text string) string { + return Yellow(text) +} diff --git a/review.go b/review.go index 66c3c0d..e49a215 100644 --- a/review.go +++ b/review.go @@ -5,6 +5,10 @@ import ( "fmt" "os" "strings" + + "github.com/ptdewey/freeze/internal/diff" + "github.com/ptdewey/freeze/internal/files" + "github.com/ptdewey/freeze/internal/pretty" ) type ReviewChoice int @@ -13,22 +17,35 @@ const ( Accept ReviewChoice = iota Reject Skip - ToggleDiff + // ToggleDiff Quit ) +func computeDiffLines(old, new *files.Snapshot) []pretty.DiffLine { + diffLines := diff.Histogram(old.Content, new.Content) + result := make([]pretty.DiffLine, len(diffLines)) + for i, dl := range diffLines { + result[i] = pretty.DiffLine{ + Number: dl.Number, + Line: dl.Line, + Kind: pretty.DiffKind(dl.Kind), + } + } + return result +} + func Review() error { - snapshots, err := ListNewSnapshots() + snapshots, err := files.ListNewSnapshots() if err != nil { return err } if len(snapshots) == 0 { - fmt.Println(FormatSuccess("✓ No new snapshots to review")) + fmt.Println(pretty.Success("✓ No new snapshots to review")) return nil } - fmt.Println(FormatHeader("🐦 Freeze - Snapshot Review")) + fmt.Println(pretty.Header("🐦 Freeze - Snapshot Review")) fmt.Printf("Found %d new snapshot(s) to review\n\n", len(snapshots)) return reviewLoop(snapshots) @@ -39,22 +56,24 @@ func reviewLoop(snapshots []string) error { showDiff := false for i, testName := range snapshots { - fmt.Printf("\n[%d/%d] %s\n", i+1, len(snapshots), FormatHeader(testName)) + fmt.Printf("\n[%d/%d] %s\n", i+1, len(snapshots), pretty.Header(testName)) - newSnap, err := ReadSnapshot(testName, "new") + newSnap, err := files.ReadSnapshot(testName, "new") if err != nil { - fmt.Println(FormatError("✗ Failed to read new snapshot: " + err.Error())) + fmt.Println(pretty.Error("✗ Failed to read new snapshot: " + err.Error())) continue } - accepted, acceptErr := ReadSnapshot(testName, "accepted") + accepted, acceptErr := files.ReadSnapshot(testName, "accepted") if acceptErr == nil && showDiff { - fmt.Println(DiffSnapshotBox(accepted, newSnap)) + diffLines := computeDiffLines(accepted, newSnap) + fmt.Println(pretty.DiffSnapshotBox(accepted, newSnap, diffLines)) } else if acceptErr == nil { - fmt.Println(DiffSnapshotBox(accepted, newSnap)) + diffLines := computeDiffLines(accepted, newSnap) + fmt.Println(pretty.DiffSnapshotBox(accepted, newSnap, diffLines)) } else { - fmt.Println(NewSnapshotBox(newSnap)) + fmt.Println(pretty.NewSnapshotBox(newSnap)) } for { @@ -65,30 +84,28 @@ func reviewLoop(snapshots []string) error { switch choice { case Accept: - if err := AcceptSnapshot(testName); err != nil { - fmt.Println(FormatError("✗ Failed to accept snapshot: " + err.Error())) + if err := files.AcceptSnapshot(testName); err != nil { + fmt.Println(pretty.Error("✗ Failed to accept snapshot: " + err.Error())) } else { - fmt.Println(FormatSuccess("✓ Snapshot accepted")) + fmt.Println(pretty.Success("✓ Snapshot accepted")) } - break case Reject: - if err := RejectSnapshot(testName); err != nil { - fmt.Println(FormatError("✗ Failed to reject snapshot: " + err.Error())) + if err := files.RejectSnapshot(testName); err != nil { + fmt.Println(pretty.Error("✗ Failed to reject snapshot: " + err.Error())) } else { - fmt.Println(FormatWarning("⊘ Snapshot rejected")) + fmt.Println(pretty.Warning("⊘ Snapshot rejected")) } - break case Skip: - fmt.Println(FormatWarning("⊘ Snapshot skipped")) - break - case ToggleDiff: - showDiff = !showDiff - if acceptErr == nil { - fmt.Println(DiffSnapshotBox(accepted, newSnap)) - } else { - fmt.Println(NewSnapshotBox(newSnap)) - } - continue + fmt.Println(pretty.Warning("⊘ Snapshot skipped")) + // case ToggleDiff: + // showDiff = !showDiff + // if acceptErr == nil { + // diffLines := computeDiffLines(accepted, newSnap) + // fmt.Println(pretty.DiffSnapshotBox(accepted, newSnap, diffLines)) + // } else { + // fmt.Println(pretty.NewSnapshotBox(newSnap)) + // } + // continue case Quit: fmt.Println("\nReview interrupted") return nil @@ -97,12 +114,13 @@ func reviewLoop(snapshots []string) error { } } - fmt.Println("\n" + FormatSuccess("✓ Review complete")) + fmt.Println("\n" + pretty.Success("✓ Review complete")) return nil } func askChoice(reader *bufio.Reader, current, total int) (ReviewChoice, error) { - fmt.Printf("\nOptions: [a]ccept [r]eject [s]kip [d]iff [q]uit: ") + // fmt.Printf("\nOptions: [a]ccept [r]eject [s]kip [d]iff [q]uit: ") + fmt.Printf("\nOptions: [a]ccept [r]eject [s]kip [q]uit: ") input, err := reader.ReadString('\n') if err != nil { @@ -118,44 +136,44 @@ func askChoice(reader *bufio.Reader, current, total int) (ReviewChoice, error) { return Reject, nil case "s", "skip": return Skip, nil - case "d", "diff": - return ToggleDiff, nil + // case "d", "diff": + // return ToggleDiff, nil case "q", "quit": return Quit, nil default: - fmt.Println(FormatWarning("Invalid option, please try again")) + fmt.Println(pretty.Warning("Invalid option, please try again")) return askChoice(reader, current, total) } } func AcceptAll() error { - snapshots, err := ListNewSnapshots() + snapshots, err := files.ListNewSnapshots() if err != nil { return err } for _, testName := range snapshots { - if err := AcceptSnapshot(testName); err != nil { + if err := files.AcceptSnapshot(testName); err != nil { return err } } - fmt.Printf(FormatSuccess("✓ Accepted %d snapshot(s)\n"), len(snapshots)) + fmt.Printf(pretty.Success("✓ Accepted %d snapshot(s)\n"), len(snapshots)) return nil } func RejectAll() error { - snapshots, err := ListNewSnapshots() + snapshots, err := files.ListNewSnapshots() if err != nil { return err } for _, testName := range snapshots { - if err := RejectSnapshot(testName); err != nil { + if err := files.RejectSnapshot(testName); err != nil { return err } } - fmt.Printf(FormatWarning("⊘ Rejected %d snapshot(s)\n"), len(snapshots)) + fmt.Printf(pretty.Warning("⊘ Rejected %d snapshot(s)\n"), len(snapshots)) return nil } diff --git a/snapshot.go b/snapshot.go deleted file mode 100644 index 1b59f94..0000000 --- a/snapshot.go +++ /dev/null @@ -1,53 +0,0 @@ -package freeze - -import ( - "fmt" - "strings" -) - -type Snapshot struct { - Version string - TestName string - Content string -} - -func (s *Snapshot) Serialize() string { - header := fmt.Sprintf("---\nversion: %s\ntest_name: %s\n---\n", s.Version, s.TestName) - return header + s.Content -} - -func Deserialize(raw string) (*Snapshot, error) { - parts := strings.SplitN(raw, "---\n", 3) - if len(parts) < 3 { - return nil, fmt.Errorf("invalid snapshot format") - } - - header := parts[1] - content := parts[2] - - snap := &Snapshot{ - Content: content, - } - - for _, line := range strings.Split(header, "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - kv := strings.SplitN(line, ": ", 2) - if len(kv) != 2 { - continue - } - - key, value := kv[0], kv[1] - switch key { - case "version": - snap.Version = value - case "test_name": - snap.TestName = value - } - } - - return snap, nil -}