clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

feat/app-decomposition
puzzletea / export / pack / packexport_test.go
packexport_test.go 4.5K
1package packexport23import (4 "context"5 "errors"6 "os"7 "path/filepath"8 "strings"9 "testing"10 "time"1112 "github.com/FelineStateMachine/puzzletea/export/pdf"13 "github.com/FelineStateMachine/puzzletea/puzzle"14)1516func TestRunDeterministicWithFixedSpecAndSeed(t *testing.T) {17 restore := snapshotNowFn(t, time.Date(2026, 3, 22, 18, 0, 0, 0, time.UTC))18 defer restore()1920 dir := t.TempDir()21 specA := smallTestSpec(dir, "a")22 specB := smallTestSpec(dir, "b")2324 if _, err := Run(t.Context(), specA); err != nil {25 t.Fatalf("first run failed: %v", err)26 }27 if _, err := Run(t.Context(), specB); err != nil {28 t.Fatalf("second run failed: %v", err)29 }3031 jsonlA, err := os.ReadFile(specA.JSONLOutputPath)32 if err != nil {33 t.Fatal(err)34 }35 jsonlB, err := os.ReadFile(specB.JSONLOutputPath)36 if err != nil {37 t.Fatal(err)38 }39 if string(jsonlA) != string(jsonlB) {40 t.Fatal("expected identical jsonl output for identical spec and seed")41 }4243 info, err := os.Stat(specA.PDFOutputPath)44 if err != nil {45 t.Fatal(err)46 }47 if info.Size() == 0 {48 t.Fatal("expected non-empty pdf output")49 }50}5152func TestRunSkipsZeroCountModes(t *testing.T) {53 restore := snapshotNowFn(t, time.Date(2026, 3, 22, 18, 0, 0, 0, time.UTC))54 defer restore()5556 dir := t.TempDir()57 spec := smallTestSpec(dir, "single")58 spec.Counts[puzzle.CanonicalGameID("Word Search")][puzzle.CanonicalModeID("Easy 10x10")] = 05960 result, err := Run(t.Context(), spec)61 if err != nil {62 t.Fatalf("run failed: %v", err)63 }64 if result.TotalCount != 1 {65 t.Fatalf("TotalCount = %d, want 1", result.TotalCount)66 }6768 data, err := os.ReadFile(spec.JSONLOutputPath)69 if err != nil {70 t.Fatal(err)71 }72 lines := strings.Split(strings.TrimSpace(string(data)), "\n")73 if got := len(lines); got != 1 {74 t.Fatalf("jsonl line count = %d, want 1", got)75 }76 if strings.Contains(string(data), "Word Search") {77 t.Fatal("did not expect zero-count mode to appear in output")78 }79}8081func TestValidateSpecRejectsUnknownTargets(t *testing.T) {82 dir := t.TempDir()8384 tests := []struct {85 name string86 spec Spec87 want string88 }{89 {90 name: "unknown game",91 spec: func() Spec {92 spec := smallTestSpec(dir, "unknown-game")93 spec.Counts = zeroCounts()94 spec.Counts[puzzle.GameID("missing-game")] = map[puzzle.ModeID]int{95 puzzle.ModeID("missing-mode"): 1,96 }97 return spec98 }(),99 want: "not a known export target",100 },101 {102 name: "unknown mode",103 spec: func() Spec {104 spec := smallTestSpec(dir, "unknown-mode")105 spec.Counts = zeroCounts()106 spec.Counts[puzzle.CanonicalGameID("Sudoku")][puzzle.ModeID("missing-mode")] = 1107 return spec108 }(),109 want: "not a known export mode",110 },111 }112113 for _, tt := range tests {114 t.Run(tt.name, func(t *testing.T) {115 err := ValidateSpec(tt.spec)116 if err == nil {117 t.Fatal("expected validation error")118 }119 if !strings.Contains(err.Error(), tt.want) {120 t.Fatalf("error = %q, want substring %q", err.Error(), tt.want)121 }122 })123 }124}125126func TestRunDoesNotPublishOutputsAfterCancellation(t *testing.T) {127 restoreNow := snapshotNowFn(t, time.Date(2026, 3, 22, 18, 0, 0, 0, time.UTC))128 defer restoreNow()129130 prevWritePDF := writePDFFn131 t.Cleanup(func() {132 writePDFFn = prevWritePDF133 })134135 dir := t.TempDir()136 spec := smallTestSpec(dir, "canceled")137 ctx, cancel := context.WithCancel(t.Context())138 writePDFFn = func(outputPath string, _ []pdfexport.PackDocument, _ []pdfexport.Puzzle, _ pdfexport.RenderConfig) error {139 if err := os.WriteFile(outputPath, []byte("%PDF-test"), 0o644); err != nil {140 return err141 }142 cancel()143 return nil144 }145146 _, err := Run(ctx, spec)147 if !errors.Is(err, context.Canceled) {148 t.Fatalf("Run error = %v, want context.Canceled", err)149 }150 if _, err := os.Stat(spec.PDFOutputPath); !errors.Is(err, os.ErrNotExist) {151 t.Fatalf("expected no published pdf output, stat err = %v", err)152 }153 if _, err := os.Stat(spec.JSONLOutputPath); !errors.Is(err, os.ErrNotExist) {154 t.Fatalf("expected no published jsonl output, stat err = %v", err)155 }156}157158func snapshotNowFn(t *testing.T, now time.Time) func() {159 t.Helper()160161 prev := nowFn162 nowFn = func() time.Time { return now }163 return func() { nowFn = prev }164}165166func smallTestSpec(dir, suffix string) Spec {167 spec := DefaultSpec(dir)168 spec.Counts = zeroCounts()169 spec.Title = "Deterministic Sampler"170 spec.Seed = "packexport-test-seed"171 spec.PDFOutputPath = filepath.Join(dir, "pack-"+suffix+".pdf")172 spec.JSONLOutputPath = filepath.Join(dir, "pack-"+suffix+".jsonl")173 spec.Counts[puzzle.CanonicalGameID("Sudoku")][puzzle.CanonicalModeID("Easy")] = 1174 spec.Counts[puzzle.CanonicalGameID("Word Search")][puzzle.CanonicalModeID("Easy 10x10")] = 1175 return spec176}