GridSilhouetteTests.swift (5842B)
1 import Foundation 2 import Testing 3 4 @testable import Crossmate 5 6 @Suite("Grid silhouette codec") 7 struct GridSilhouetteTests { 8 9 @Test("encodes a small symmetric grid to a stable segment") 10 func encodesSymmetricSegment() { 11 // 3×3 with only the centre blocked. Symmetric, so the first ⌈9/2⌉ = 5 12 // cells (00001) pack into one byte 0x08 → base64url "CA"; side 3 → "3". 13 var blocks = [Bool](repeating: false, count: 9) 14 blocks[4] = true 15 #expect(GridSilhouette.encode(side: 3, blocks: blocks) == "s3CA") 16 } 17 18 @Test("tags an asymmetric grid for a full dump") 19 func tagsAsymmetricGrid() { 20 // A single corner block has no 180° partner → not symmetric → "f". 21 var blocks = [Bool](repeating: false, count: 9) 22 blocks[0] = true 23 let segment = GridSilhouette.encode(side: 3, blocks: blocks) 24 #expect(segment?.first == "f") 25 #expect(GridSilhouette.decode(segment ?? "") == GridSilhouette.Grid(side: 3, blocks: blocks)) 26 } 27 28 @Test("round-trips a symmetric grid through decode") 29 func roundTripsSymmetric() { 30 var blocks = [Bool](repeating: false, count: 9) 31 blocks[4] = true 32 let grid = GridSilhouette.Grid(side: 3, blocks: blocks) 33 let segment = GridSilhouette.encode(side: grid.width, blocks: grid.blocks) 34 #expect(GridSilhouette.decode(segment ?? "") == grid) 35 } 36 37 @Test("round-trips a realistic 15×15 and stays compact") 38 func roundTrips15x15() { 39 // A symmetric scatter of blocks across a standard 15×15. 40 let side = 15 41 let n = side * side 42 var blocks = [Bool](repeating: false, count: n) 43 for k in [0, 4, 16, 30, 47, 88, 100, 112] { 44 blocks[k] = true 45 blocks[n - 1 - k] = true // keep it symmetric 46 } 47 let grid = GridSilhouette.Grid(side: side, blocks: blocks) 48 let segment = GridSilhouette.encode(side: side, blocks: blocks) 49 #expect(segment?.first == "s") 50 // ~2 tag/size chars + ⌈113 bits / 6⌉ ≈ 19 payload chars. 51 #expect((segment?.count ?? .max) <= 24) 52 #expect(GridSilhouette.decode(segment ?? "") == grid) 53 } 54 55 @Test("encodes a rectangular grid under the uppercase two-digit form") 56 func encodesRectangularSegment() { 57 // 2×3, all empty. Width≠height → uppercase `S`, width "2", height "3"; 58 // symmetric (all false), so ⌈6/2⌉ = 3 cells pack into 0x00 → "AA". 59 let grid = GridSilhouette.Grid(width: 2, height: 3, blocks: [Bool](repeating: false, count: 6)) 60 let segment = GridSilhouette.encode(width: 2, height: 3, blocks: grid.blocks) 61 #expect(segment == "S23AA") 62 #expect(GridSilhouette.decode(segment ?? "") == grid) 63 } 64 65 @Test("tags an asymmetric rectangular grid with uppercase F") 66 func tagsAsymmetricRectangle() { 67 // A single corner block has no 180° partner → full dump → "F". 68 var blocks = [Bool](repeating: false, count: 6) 69 blocks[0] = true 70 let segment = GridSilhouette.encode(width: 2, height: 3, blocks: blocks) 71 #expect(segment?.first == "F") 72 #expect( 73 GridSilhouette.decode(segment ?? "") 74 == GridSilhouette.Grid(width: 2, height: 3, blocks: blocks) 75 ) 76 } 77 78 @Test("round-trips a realistic 21×22 Sunday grid") 79 func roundTrips21x22() { 80 // The shape that motivated rectangular support: a non-square Sunday. 81 let width = 21 82 let height = 22 83 let n = width * height 84 var blocks = [Bool](repeating: false, count: n) 85 for k in [0, 5, 21, 64, 200, 230, 300, 410] { 86 blocks[k] = true 87 blocks[n - 1 - k] = true // keep it 180°-symmetric 88 } 89 let grid = GridSilhouette.Grid(width: width, height: height, blocks: blocks) 90 let segment = GridSilhouette.encode(width: width, height: height, blocks: blocks) 91 #expect(segment?.first == "S") 92 // width 21 → "l", height 22 → "m". 93 #expect(segment?.dropFirst().prefix(2) == "lm") 94 #expect(GridSilhouette.decode(segment ?? "") == grid) 95 } 96 97 @Test("encodes the largest supported side via base-36") 98 func encodesLargeSide() { 99 let side = GridSilhouette.maxSide // 35 → base-36 'z' 100 let blocks = [Bool](repeating: false, count: side * side) 101 let segment = GridSilhouette.encode(side: side, blocks: blocks) 102 #expect(segment?.dropFirst().first == "z") 103 #expect(GridSilhouette.decode(segment ?? "") == GridSilhouette.Grid(side: side, blocks: blocks)) 104 } 105 106 @Test("refuses a mismatched block count or out-of-range dimension") 107 func refusesUnsupportedGrids() { 108 #expect(GridSilhouette.encode(side: 3, blocks: [Bool](repeating: false, count: 6)) == nil) 109 #expect(GridSilhouette.encode(side: 1, blocks: [false]) == nil) 110 #expect(GridSilhouette.encode(side: 36, blocks: [Bool](repeating: false, count: 36 * 36)) == nil) 111 #expect(GridSilhouette.encode(width: 2, height: 36, blocks: [Bool](repeating: false, count: 72)) == nil) 112 #expect(GridSilhouette.encode(width: 2, height: 3, blocks: [Bool](repeating: false, count: 5)) == nil) 113 } 114 115 @Test("rejects malformed segments") 116 func rejectsMalformedSegments() { 117 #expect(GridSilhouette.decode("") == nil) 118 #expect(GridSilhouette.decode("x3CA") == nil) // unknown tag 119 #expect(GridSilhouette.decode("s") == nil) // no size/payload 120 #expect(GridSilhouette.decode("s3") == nil) // empty payload, no bits 121 #expect(GridSilhouette.decode("s1CA") == nil) // side 1 is below minSide 122 #expect(GridSilhouette.decode("S2") == nil) // rectangular tag, missing height 123 #expect(GridSilhouette.decode("S21AA") == nil) // height 1 is below minSide 124 } 125 }