Refactor Sudoku field and solver implementation

This commit is contained in:
2026-09-16 22:52:44 +02:00
parent 8a5ae8c640
commit f7f9c16184
24 changed files with 1574 additions and 603 deletions
+110
View File
@@ -0,0 +1,110 @@
package parser
import (
"crypto/sha1"
"errors"
"fmt"
"strings"
"testing"
)
const validSolvedPuzzle = "123456789456789123789123456234567891567891234891234567345678912678912345912345678"
func bankRecord(digits string, rating float64) string {
hash := fmt.Sprintf("%x", sha1.Sum([]byte(digits)))[:12]
return fmt.Sprintf("%s %s %.1f", hash, digits, rating)
}
func TestPuzzleBankParsesLFCRLFAndFinalLine(t *testing.T) {
first := "0" + validSolvedPuzzle[1:]
data := bankRecord(first, 1.2) + "\r\n\n" + bankRecord(validSolvedPuzzle, 2.5)
parser := &PuzzleBank{}
if err := parser.Parse([]byte(data)); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if got := len(parser.GetAllFields()); got != 2 {
t.Fatalf("field count = %d, want 2", got)
}
second, err := parser.GetField(1)
if err != nil {
t.Fatal(err)
}
if second.GetRating() != 2.5 || !second.IsSolved() {
t.Fatalf("second field: rating=%v solved=%v", second.GetRating(), second.IsSolved())
}
}
func TestPuzzleBankRejectsMalformedInput(t *testing.T) {
valid := bankRecord(validSolvedPuzzle, 1.2)
tests := map[string]string{
"empty": "\n\r\n",
"columns": "broken",
"hash length": "abc " + validSolvedPuzzle + " 1.2",
"hash mismatch": "ffffffffffff" + valid[12:],
"bad puzzle": bankRecord("x"+validSolvedPuzzle[1:], 1.2),
"bad rating": strings.TrimSuffix(valid, "1.2") + "nope",
}
for name, input := range tests {
t.Run(name, func(t *testing.T) {
if err := (&PuzzleBank{}).Parse([]byte(input)); err == nil || (name != "empty" && !errors.Is(err, ErrInvalidInput)) {
t.Fatalf("Parse() error = %v", err)
}
})
}
}
func TestPuzzleBankParseIsAtomicAndDoesNotAppend(t *testing.T) {
parser := &PuzzleBank{}
valid := bankRecord(validSolvedPuzzle, 1.2)
if err := parser.Parse([]byte(valid)); err != nil {
t.Fatal(err)
}
original, _ := parser.GetField(0)
if err := parser.Parse([]byte(valid + "\ninvalid")); err == nil {
t.Fatal("invalid Parse() error = nil")
}
afterFailure, _ := parser.GetField(0)
if afterFailure != original || len(parser.GetAllFields()) != 1 {
t.Fatal("failed parse changed previously parsed fields")
}
if err := parser.Parse([]byte(valid)); err != nil {
t.Fatal(err)
}
if len(parser.GetAllFields()) != 1 {
t.Fatal("repeated Parse() appended fields")
}
}
func TestParserHelperBoundsAndDefensiveSlice(t *testing.T) {
parser := &PuzzleString{}
if err := parser.Parse([]byte(validSolvedPuzzle)); err != nil {
t.Fatal(err)
}
for _, index := range []int{-1, 1} {
if _, err := parser.GetField(index); !errors.Is(err, ErrFieldIndex) {
t.Fatalf("GetField(%d) error = %v", index, err)
}
}
fields := parser.GetAllFields()
fields[0] = nil
if stored, err := parser.GetField(0); err != nil || stored == nil {
t.Fatal("GetAllFields exposed its backing slice")
}
}
func TestPuzzleStringValidatesInput(t *testing.T) {
parser := &PuzzleString{}
if err := parser.Parse([]byte(" \n" + validSolvedPuzzle + "\n")); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if fields := parser.GetAllFields(); len(fields) != 1 || !fields[0].IsSolved() {
t.Fatal("valid puzzle string was not parsed")
}
invalid := []string{"", validSolvedPuzzle[:80], "x" + validSolvedPuzzle[1:], "11" + validSolvedPuzzle[2:]}
for _, input := range invalid {
if err := (&PuzzleString{}).Parse([]byte(input)); err == nil {
t.Fatalf("Parse(%q) error = nil", input)
}
}
}