package field import ( "errors" "reflect" "strings" "testing" ) const solvedPuzzle = "123456789456789123789123456234567891567891234891234567345678912678912345912345678" func classicCells(t *testing.T, digits string) (Properties, [][]Cell) { t.Helper() if len(digits) != 81 { t.Fatalf("test puzzle has %d characters", len(digits)) } props := Properties{Rows: 9, Columns: 9, BlockRows: 3, BlockColumns: 3, BlockSizeRow: 3, BlockSizeColumn: 3} cells := make([][]Cell, props.Rows) for row := range cells { cells[row] = make([]Cell, props.Columns) for column := range cells[row] { position := NewPosition(row, column, row/3, column/3, row%3, column%3) cells[row][column] = *NewCell(int(digits[row*9+column]-'0'), position) } } return props, cells } func mustField(t *testing.T, digits string) *Field { t.Helper() props, cells := classicCells(t, digits) result, err := New(props, cells) if err != nil { t.Fatalf("New() error = %v", err) } return result } func TestNewValidatesStructureAndValues(t *testing.T) { props, cells := classicCells(t, strings.Repeat("0", 81)) tests := []struct { name string mutate func(*Properties, [][]Cell) }{ {name: "missing row", mutate: func(_ *Properties, cells [][]Cell) { cells[0] = cells[0][:8] }}, {name: "wrong position", mutate: func(_ *Properties, cells [][]Cell) { cells[0][0].pos = NewPosition(1, 0, 0, 0, 0, 0) }}, {name: "number out of range", mutate: func(_ *Properties, cells [][]Cell) { cells[0][0].number = 10 }}, {name: "invalid blocks", mutate: func(props *Properties, _ [][]Cell) { props.BlockRows = 2 }}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { localProps := props localCells := make([][]Cell, len(cells)) for row := range cells { localCells[row] = append([]Cell(nil), cells[row]...) } test.mutate(&localProps, localCells) if _, err := New(localProps, localCells); !errors.Is(err, ErrInvalidField) { t.Fatalf("New() error = %v, want ErrInvalidField", err) } }) } duplicate := "113456789" + solvedPuzzle[9:] duplicateProps, duplicateCells := classicCells(t, duplicate) if _, err := New(duplicateProps, duplicateCells); !errors.Is(err, ErrInvalidField) { t.Fatalf("New() duplicate error = %v, want ErrInvalidField", err) } } func TestGetCellBoundsAndForEachCellPointers(t *testing.T) { f := mustField(t, strings.Repeat("0", 81)) for _, coordinates := range [][2]int{{-1, 0}, {0, -1}, {9, 0}, {0, 9}} { if _, err := f.GetCell(coordinates[0], coordinates[1]); !errors.Is(err, ErrOutOfBounds) { t.Errorf("GetCell(%d, %d) error = %v", coordinates[0], coordinates[1], err) } } want, _ := f.GetCell(4, 5) var found *Cell f.ForEachCell(func(cell *Cell) { if cell.GetPosition().GetRow() == 4 && cell.GetPosition().GetColumn() == 5 { found = cell } }) if found != want { t.Fatalf("ForEachCell() returned %p, want stored cell %p", found, want) } } func TestValidityAndSolvedState(t *testing.T) { incomplete := mustField(t, strings.Repeat("0", 81)) if !incomplete.IsValid() || incomplete.IsSolved() { t.Fatalf("empty field: IsValid=%v IsSolved=%v", incomplete.IsValid(), incomplete.IsSolved()) } solved := mustField(t, solvedPuzzle) if !solved.IsValid() || !solved.IsSolved() { t.Fatalf("solved field: IsValid=%v IsSolved=%v", solved.IsValid(), solved.IsSolved()) } row, _ := solved.GetRow(0) block, _ := solved.GetBlock(0, 0) if !row.IsSolved() || !block.IsSolved() { t.Fatal("completed row and block must be solved") } solved.cells[0][0].number = solved.cells[0][1].number if solved.IsValid() || solved.IsSolved() { t.Fatal("field with a duplicate must be invalid and unsolved") } } func TestChangesAreValidatedAndAtomic(t *testing.T) { f := mustField(t, strings.Repeat("0", 81)) first, _ := f.GetCell(0, 0) second, _ := f.GetCell(0, 1) err := f.AddChanges([]ExternalChange{ {Cell: first, Action: ActionSetNumber, Value: 1, From: 0}, {Cell: second, Action: ActionSetNumber, Value: 1, From: 0}, }) if !errors.Is(err, ErrInvalidChange) { t.Fatalf("AddChanges() error = %v, want ErrInvalidChange", err) } if first.GetNumber() != 0 || second.GetNumber() != 0 || len(f.GetChanges()) != 0 { t.Fatal("failed change batch was not rolled back") } foreign := NewCell(0, NewPosition(0, 0, 0, 0, 0, 0)) if err := f.AddChange(&ExternalChange{Cell: foreign, Action: ActionSetNumber, Value: 1}); !errors.Is(err, ErrInvalidChange) { t.Fatalf("foreign cell error = %v, want ErrInvalidChange", err) } if err := (*Field)(nil).AddChanges(nil); !errors.Is(err, ErrInvalidField) { t.Fatalf("nil field error = %v, want ErrInvalidField", err) } } func TestNotesStaySortedUniqueAndArePropagated(t *testing.T) { f := mustField(t, strings.Repeat("0", 81)) target, _ := f.GetCell(0, 0) rowPeer, _ := f.GetCell(0, 1) columnPeer, _ := f.GetCell(1, 0) blockPeer, _ := f.GetCell(1, 1) unrelated, _ := f.GetCell(4, 4) for _, note := range []int{5, 1, 5} { if err := target.GetNotes().Add(f, target, note, "test", nil); err != nil { t.Fatalf("add target note: %v", err) } } if got, want := target.GetNotes().Get(), []int{1, 5}; !reflect.DeepEqual(got, want) { t.Fatalf("notes = %v, want %v", got, want) } for _, cell := range []*Cell{rowPeer, columnPeer, blockPeer, unrelated} { if err := cell.GetNotes().Add(f, cell, 5, "test", nil); err != nil { t.Fatalf("add peer note: %v", err) } } if err := target.SetNumber(f, 5, "test", nil); err != nil { t.Fatalf("SetNumber() error = %v", err) } for _, peer := range []*Cell{target, rowPeer, columnPeer, blockPeer} { if peer.GetNotes().Has(5) { row, column := peer.GetPosition().GetCoords() t.Fatalf("note 5 was not removed at %d/%d", row, column) } } if !unrelated.GetNotes().Has(5) { t.Fatal("note was removed from an unrelated cell") } if f.IsValid() == false { t.Fatal("field must remain valid after candidate propagation") } filled, _ := f.GetCell(0, 2) if err := filled.SetNumber(f, 2, "test", nil); err != nil { t.Fatal(err) } if err := filled.GetNotes().Add(f, filled, 3, "test", nil); !errors.Is(err, ErrInvalidChange) { t.Fatalf("note on filled cell error = %v, want ErrInvalidChange", err) } if err := rowPeer.GetNotes().Add(f, rowPeer, 2, "test", nil); !errors.Is(err, ErrInvalidChange) { t.Fatalf("invalid peer note error = %v, want ErrInvalidChange", err) } } func TestRemoveNumberRecordsCorrectAction(t *testing.T) { f := mustField(t, strings.Repeat("0", 81)) cell, _ := f.GetCell(0, 0) if err := cell.SetNumber(f, 1, "set", nil); err != nil { t.Fatal(err) } if err := cell.RemoveNumber(f, "remove", nil); err != nil { t.Fatal(err) } changes := f.GetChanges() if changes[len(changes)-1].Action != ActionRemoveNumber { t.Fatalf("last action = %v, want ActionRemoveNumber", changes[len(changes)-1].Action) } } func TestStringRendersDigitsAndNotes(t *testing.T) { f := mustField(t, "900000000"+strings.Repeat("0", 72)) cell, _ := f.GetCell(0, 1) if err := cell.GetNotes().Add(f, cell, 1, "test", nil); err != nil { t.Fatal(err) } if !strings.HasPrefix(f.String(), "9 · ·") { t.Fatalf("String() = %q", f.String()) } if got := f.StringNotesForNumber(1); got != "0/1" { t.Fatalf("StringNotesForNumber() = %q, want 0/1", got) } } func TestGettersReturnDefensiveCopies(t *testing.T) { f := mustField(t, strings.Repeat("0", 81)) props, err := f.GetProperties() if err != nil { t.Fatal(err) } props.Rows = 4 stored, _ := f.GetProperties() if stored.Rows != 9 { t.Fatal("GetProperties exposed internal properties") } cell, _ := f.GetCell(0, 0) if err := cell.GetNotes().Add(f, cell, 1, "test", nil); err != nil { t.Fatal(err) } notes := cell.GetNotes().Get() notes[0] = 9 if !cell.GetNotes().Has(1) || cell.GetNotes().Has(9) { t.Fatal("Notes.Get exposed its backing slice") } changes := f.GetChanges() changes[0].Value = 9 if f.GetChanges()[0].Value != 1 { t.Fatal("GetChanges exposed its backing slice") } }