189 lines
5.2 KiB
Go
189 lines
5.2 KiB
Go
package main
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestBullseyePattern(t *testing.T) {
|
||
p := bullseyePattern()
|
||
// Center should be black
|
||
if !p[2][2] {
|
||
t.Error("center should be black")
|
||
}
|
||
// Ring 1 should be white
|
||
if p[2][1] || p[1][2] || p[2][3] || p[3][2] {
|
||
t.Error("ring 1 should be white")
|
||
}
|
||
// Ring 2 (corners) should be black
|
||
if !p[0][0] || !p[0][4] || !p[4][0] || !p[4][4] {
|
||
t.Error("outer ring corners should be black")
|
||
}
|
||
// 4-fold rotational symmetry
|
||
for rot := 0; rot < 3; rot++ {
|
||
var rotated [bullseyeN][bullseyeN]bool
|
||
for r := 0; r < bullseyeN; r++ {
|
||
for c := 0; c < bullseyeN; c++ {
|
||
rotated[c][bullseyeN-1-r] = p[r][c]
|
||
}
|
||
}
|
||
if rotated != p {
|
||
t.Errorf("bullseye not symmetric under 90° rotation %d", rot+1)
|
||
}
|
||
p = rotated
|
||
}
|
||
}
|
||
|
||
func TestEncodeDecodeMarkerBits(t *testing.T) {
|
||
tests := []MarkerData{
|
||
{PageNum: 0, CornerID: CornerTL, NumFaces: 6, LongestMM: 50},
|
||
{PageNum: 3, CornerID: CornerBR, NumFaces: 12, LongestMM: 120},
|
||
{PageNum: 15, CornerID: CornerCenter, NumFaces: 31, LongestMM: 635},
|
||
{PageNum: 1, CornerID: CornerTR, NumFaces: 1, LongestMM: 5},
|
||
}
|
||
for _, tt := range tests {
|
||
bits := encodeMarkerBits(tt)
|
||
got, ok := decodeMarkerBits(bits)
|
||
if !ok {
|
||
t.Errorf("decode failed for %+v", tt)
|
||
continue
|
||
}
|
||
if got.PageNum != tt.PageNum || got.CornerID != tt.CornerID ||
|
||
got.NumFaces != tt.NumFaces || got.LongestMM != tt.LongestMM {
|
||
t.Errorf("roundtrip mismatch: input=%+v got=%+v", tt, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestEncodeDecodeParityCheck(t *testing.T) {
|
||
bits := encodeMarkerBits(MarkerData{PageNum: 1, CornerID: 2, NumFaces: 6, LongestMM: 50})
|
||
// Flip one bit — should fail parity
|
||
bits[10] = !bits[10]
|
||
_, ok := decodeMarkerBits(bits)
|
||
if ok {
|
||
t.Error("corrupted bits should fail parity check")
|
||
}
|
||
}
|
||
|
||
func TestEncodeMarkerGrid(t *testing.T) {
|
||
grid := encodeMarkerGrid(MarkerData{PageNum: 1, CornerID: CornerTL, NumFaces: 6, LongestMM: 50})
|
||
// Verify bullseye core in center
|
||
bull := bullseyePattern()
|
||
for r := 0; r < bullseyeN; r++ {
|
||
for c := 0; c < bullseyeN; c++ {
|
||
if grid[r+1][c+1] != bull[r][c] {
|
||
t.Errorf("bullseye mismatch at (%d,%d): got %v want %v", r, c, grid[r+1][c+1], bull[r][c])
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestDecodeFromGridWithRotation(t *testing.T) {
|
||
data := MarkerData{PageNum: 2, CornerID: CornerBL, NumFaces: 8, LongestMM: 100}
|
||
grid := encodeMarkerGrid(data)
|
||
|
||
// Unrotated should decode
|
||
got, rot, ok := decodeMarkerFromGrid(grid)
|
||
if !ok {
|
||
t.Fatal("decode failed for unrotated grid")
|
||
}
|
||
if rot != 0 {
|
||
t.Errorf("expected rotation 0, got %d", rot)
|
||
}
|
||
if got.CornerID != data.CornerID || got.PageNum != data.PageNum {
|
||
t.Errorf("data mismatch: got=%+v want=%+v", got, data)
|
||
}
|
||
|
||
// Rotate 90° and decode
|
||
rotated := rotateGrid90CW(grid)
|
||
got2, rot2, ok2 := decodeMarkerFromGrid(rotated)
|
||
if !ok2 {
|
||
t.Fatal("decode failed for 90° rotated grid")
|
||
}
|
||
if got2.CornerID != data.CornerID {
|
||
t.Errorf("rotated decode data mismatch: got=%+v", got2)
|
||
}
|
||
_ = rot2
|
||
}
|
||
|
||
func TestDataRingCellCount(t *testing.T) {
|
||
cells := dataRingCells()
|
||
// 7×7 perimeter = (7-1)*4 = 24
|
||
if len(cells) != 24 {
|
||
t.Errorf("expected 24 data ring cells, got %d", len(cells))
|
||
}
|
||
// All cells should be on the perimeter (row 0, row 6, col 0, or col 6)
|
||
for i, c := range cells {
|
||
if c[0] != 0 && c[0] != markerN-1 && c[1] != 0 && c[1] != markerN-1 {
|
||
t.Errorf("cell %d at (%d,%d) is not on perimeter", i, c[0], c[1])
|
||
}
|
||
}
|
||
// No duplicates
|
||
seen := map[[2]int]bool{}
|
||
for _, c := range cells {
|
||
if seen[c] {
|
||
t.Errorf("duplicate cell at (%d,%d)", c[0], c[1])
|
||
}
|
||
seen[c] = true
|
||
}
|
||
}
|
||
|
||
func TestRenderMarkerSVG(t *testing.T) {
|
||
var b strings.Builder
|
||
renderMarkerSVG(&b, 50, 50, MarkerData{PageNum: 1, CornerID: 0, NumFaces: 6, LongestMM: 50})
|
||
svg := b.String()
|
||
// Should contain black cells
|
||
if !strings.Contains(svg, `fill="black"`) {
|
||
t.Error("marker SVG should contain black cells")
|
||
}
|
||
if strings.Count(svg, "<rect") < 10 {
|
||
t.Errorf("expected at least 10 rect elements for marker, got %d", strings.Count(svg, "<rect"))
|
||
}
|
||
}
|
||
|
||
func TestRenderCalibBarsSVG(t *testing.T) {
|
||
var b strings.Builder
|
||
renderCalibBarsSVG(&b, 10, 200, calibBarSpecs())
|
||
svg := b.String()
|
||
if !strings.Contains(svg, "5mm") {
|
||
t.Error("missing 5mm bar label")
|
||
}
|
||
if !strings.Contains(svg, "50mm") {
|
||
t.Error("missing 50mm bar label")
|
||
}
|
||
// Encoded bars should have multiple rect elements (not just one per bar)
|
||
rectCount := strings.Count(svg, "<rect")
|
||
if rectCount < 8 {
|
||
t.Errorf("expected at least 8 rect elements for encoded bars, got %d", rectCount)
|
||
}
|
||
}
|
||
|
||
func TestCalibBarEncodeDecode(t *testing.T) {
|
||
for _, bar := range calibBarSpecs() {
|
||
cells := encodeCalibBar(bar.WidthMM)
|
||
if !cells[0] {
|
||
t.Errorf("bar %d: start cell should be black", bar.WidthMM)
|
||
}
|
||
if !cells[calibBarCells-1] {
|
||
t.Errorf("bar %d: stop cell should be black", bar.WidthMM)
|
||
}
|
||
val, ok := decodeCalibBar(cells)
|
||
if !ok {
|
||
t.Errorf("bar %d: decode failed", bar.WidthMM)
|
||
}
|
||
if val != bar.WidthMM {
|
||
t.Errorf("bar %d: decoded %d", bar.WidthMM, val)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestMarkerPositions(t *testing.T) {
|
||
pos := markerPositionsMM(215.9, 279.4)
|
||
if pos[0][0] > 20 || pos[0][1] > 20 {
|
||
t.Errorf("TL marker too far from corner: (%.1f, %.1f)", pos[0][0], pos[0][1])
|
||
}
|
||
if pos[3][0] < 195 || pos[3][1] < 260 {
|
||
t.Errorf("BR marker too far from corner: (%.1f, %.1f)", pos[3][0], pos[3][1])
|
||
}
|
||
}
|