clone

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

main
puzzletea / export / pack / packexport_test.go
packexport_test.go 5.1K
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 }42 if !strings.Contains(string(jsonlA), pdfexport.ExportSchemaV2) {43 t.Fatalf("jsonl schema marker missing %q", pdfexport.ExportSchemaV2)44 }4546 info, err := os.Stat(specA.PDFOutputPath)47 if err != nil {48 t.Fatal(err)49 }50 if info.Size() == 0 {51 t.Fatal("expected non-empty pdf output")52 }53}5455func TestDefaultSpecUsesPackSchemaV2(t *testing.T) {56 spec := DefaultSpec(t.TempDir())57 if spec.Schema != PackSpecSchemaV2 {58 t.Fatalf("Schema = %q, want %q", spec.Schema, PackSpecSchemaV2)59 }60}6162func TestRunSkipsZeroCountModes(t *testing.T) {63 restore := snapshotNowFn(t, time.Date(2026, 3, 22, 18, 0, 0, 0, time.UTC))64 defer restore()6566 dir := t.TempDir()67 spec := smallTestSpec(dir, "single")68 spec.Counts[puzzle.CanonicalGameID("Word Search")][puzzle.CanonicalModeID("Easy 10x10")] = 06970 result, err := Run(t.Context(), spec)71 if err != nil {72 t.Fatalf("run failed: %v", err)73 }74 if result.TotalCount != 1 {75 t.Fatalf("TotalCount = %d, want 1", result.TotalCount)76 }7778 data, err := os.ReadFile(spec.JSONLOutputPath)79 if err != nil {80 t.Fatal(err)81 }82 lines := strings.Split(strings.TrimSpace(string(data)), "\n")83 if got := len(lines); got != 1 {84 t.Fatalf("jsonl line count = %d, want 1", got)85 }86 if strings.Contains(string(data), "Word Search") {87 t.Fatal("did not expect zero-count mode to appear in output")88 }89}9091func TestValidateSpecRejectsUnknownTargets(t *testing.T) {92 dir := t.TempDir()9394 tests := []struct {95 name string96 spec Spec97 want string98 }{99 {100 name: "unknown game",101 spec: func() Spec {102 spec := smallTestSpec(dir, "unknown-game")103 spec.Counts = zeroCounts()104 spec.Counts[puzzle.GameID("missing-game")] = map[puzzle.ModeID]int{105 puzzle.ModeID("missing-mode"): 1,106 }107 return spec108 }(),109 want: "not a known export target",110 },111 {112 name: "unknown mode",113 spec: func() Spec {114 spec := smallTestSpec(dir, "unknown-mode")115 spec.Counts = zeroCounts()116 spec.Counts[puzzle.CanonicalGameID("Sudoku")][puzzle.ModeID("missing-mode")] = 1117 return spec118 }(),119 want: "not a known export variant",120 },121 {122 name: "unsupported schema",123 spec: func() Spec {124 spec := smallTestSpec(dir, "unsupported-schema")125 spec.Schema = "puzzletea.pack.v1"126 return spec127 }(),128 want: "unsupported pack spec schema",129 },130 }131132 for _, tt := range tests {133 t.Run(tt.name, func(t *testing.T) {134 err := ValidateSpec(tt.spec)135 if err == nil {136 t.Fatal("expected validation error")137 }138 if !strings.Contains(err.Error(), tt.want) {139 t.Fatalf("error = %q, want substring %q", err.Error(), tt.want)140 }141 })142 }143}144145func TestRunDoesNotPublishOutputsAfterCancellation(t *testing.T) {146 restoreNow := snapshotNowFn(t, time.Date(2026, 3, 22, 18, 0, 0, 0, time.UTC))147 defer restoreNow()148149 prevWritePDF := writePDFFn150 t.Cleanup(func() {151 writePDFFn = prevWritePDF152 })153154 dir := t.TempDir()155 spec := smallTestSpec(dir, "canceled")156 ctx, cancel := context.WithCancel(t.Context())157 writePDFFn = func(outputPath string, _ []pdfexport.PackDocument, _ []pdfexport.Puzzle, _ pdfexport.RenderConfig) error {158 if err := os.WriteFile(outputPath, []byte("%PDF-test"), 0o644); err != nil {159 return err160 }161 cancel()162 return nil163 }164165 _, err := Run(ctx, spec)166 if !errors.Is(err, context.Canceled) {167 t.Fatalf("Run error = %v, want context.Canceled", err)168 }169 if _, err := os.Stat(spec.PDFOutputPath); !errors.Is(err, os.ErrNotExist) {170 t.Fatalf("expected no published pdf output, stat err = %v", err)171 }172 if _, err := os.Stat(spec.JSONLOutputPath); !errors.Is(err, os.ErrNotExist) {173 t.Fatalf("expected no published jsonl output, stat err = %v", err)174 }175}176177func snapshotNowFn(t *testing.T, now time.Time) func() {178 t.Helper()179180 prev := nowFn181 nowFn = func() time.Time { return now }182 return func() { nowFn = prev }183}184185func smallTestSpec(dir, suffix string) Spec {186 spec := DefaultSpec(dir)187 spec.Counts = zeroCounts()188 spec.Title = "Deterministic Sampler"189 spec.Seed = "packexport-test-seed"190 spec.PDFOutputPath = filepath.Join(dir, "pack-"+suffix+".pdf")191 spec.JSONLOutputPath = filepath.Join(dir, "pack-"+suffix+".jsonl")192 spec.Counts[puzzle.CanonicalGameID("Sudoku")][puzzle.CanonicalModeID("Easy")] = 1193 spec.Counts[puzzle.CanonicalGameID("Word Search")][puzzle.CanonicalModeID("Easy 10x10")] = 1194 return spec195}