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, " 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]) } }