XDLimitsTests.swift (7292B)
1 import Foundation 2 import Testing 3 4 @testable import Crossmate 5 6 /// Adversarial boundary coverage for the parser's pre-allocation budgets: 7 /// grid geometry, clue count, per-clue text/metadata/answer lengths, accepted 8 /// answers, and rebus expansions. Each limit is tested from both sides — the 9 /// hostile case fails closed, the maximum legitimate case still parses. 10 @Suite("XD parsing limits") 11 struct XDLimitsTests { 12 13 /// Parses the source, returning the ParseError it threw (nil on success 14 /// or on a non-ParseError failure). 15 private func parseError(from source: String) -> XD.ParseError? { 16 do { 17 _ = try XD.parse(source) 18 return nil 19 } catch let error as XD.ParseError { 20 return error 21 } catch { 22 return nil 23 } 24 } 25 26 // MARK: - Grid geometry 27 28 @Test("A grid wider than the maximum is rejected before allocation") 29 func gridWiderThanMaximumIsRejected() { 30 let row = String(repeating: "A", count: XD.maxGridDimension + 1) 31 let source = """ 32 Title: Wide 33 34 35 \(row) 36 37 38 A1. Never reached 39 """ 40 41 guard case .some(.gridTooLarge) = parseError(from: source) else { 42 Issue.record("expected gridTooLarge") 43 return 44 } 45 } 46 47 @Test("A grid taller than the maximum is rejected before allocation") 48 func gridTallerThanMaximumIsRejected() { 49 let rows = Array(repeating: "A", count: XD.maxGridDimension + 1).joined(separator: "\n") 50 let source = """ 51 Title: Tall 52 53 54 \(rows) 55 56 57 D1. Never reached 58 """ 59 60 guard case .some(.gridTooLarge) = parseError(from: source) else { 61 Issue.record("expected gridTooLarge") 62 return 63 } 64 } 65 66 @Test("A grid at the maximum width still parses") 67 func gridAtMaximumWidthParses() throws { 68 let row = String(repeating: "A", count: XD.maxGridDimension) 69 let source = """ 70 Title: Widest 71 72 73 \(row) 74 75 76 A1. All As 77 """ 78 79 let xd = try XD.parse(source) 80 #expect(xd.width == XD.maxGridDimension) 81 #expect(xd.height == 1) 82 } 83 84 @Test("A grid at the maximum height still parses") 85 func gridAtMaximumHeightParses() throws { 86 let rows = Array(repeating: "A", count: XD.maxGridDimension).joined(separator: "\n") 87 let source = """ 88 Title: Tallest 89 90 91 \(rows) 92 93 94 D1. All As 95 """ 96 97 let xd = try XD.parse(source) 98 #expect(xd.width == 1) 99 #expect(xd.height == XD.maxGridDimension) 100 } 101 102 @Test("A maximum-width all-open row with an answer-less clue fails closed") 103 func answerlessOpenRowFailsClosed() { 104 // The audit's quadratic shape: one long open row whose only clue has 105 // no answer. The word-scoped passes visit the row once and the missing 106 // inferred solutions reject it promptly. 107 let row = String(repeating: ".", count: XD.maxGridDimension) 108 let source = """ 109 Title: Open Row 110 111 112 \(row) 113 114 115 A1. No answer given 116 """ 117 118 guard case .some(.missingInferredSolution) = parseError(from: source) else { 119 Issue.record("expected missingInferredSolution") 120 return 121 } 122 } 123 124 // MARK: - Clue budgets 125 126 @Test("More distinct clues than the maximum are rejected") 127 func tooManyDistinctCluesAreRejected() { 128 let clues = (1...(XD.maxClueCount + 1)) 129 .map { "A\($0). x" } 130 .joined(separator: "\n") 131 let source = """ 132 Title: Cluepocalypse 133 134 135 A 136 137 138 \(clues) 139 """ 140 141 guard case .some(.tooManyClues) = parseError(from: source) else { 142 Issue.record("expected tooManyClues") 143 return 144 } 145 } 146 147 @Test("A clue count at the maximum still parses") 148 func clueCountAtMaximumParses() throws { 149 let clues = (1...XD.maxClueCount) 150 .map { "A\($0). x" } 151 .joined(separator: "\n") 152 let source = """ 153 Title: Many Clues 154 155 156 A 157 158 159 \(clues) 160 """ 161 162 let xd = try XD.parse(source) 163 #expect(xd.acrossClues.count == XD.maxClueCount) 164 } 165 166 @Test("An over-long clue metadata value is rejected") 167 func overLongClueMetadataValueIsRejected() { 168 let value = String(repeating: "x", count: XD.maxClueTextLength + 1) 169 let source = """ 170 Title: Long Metadata 171 172 173 A 174 175 176 A1. Letter ~ A 177 A1 ^Note: \(value) 178 """ 179 180 guard case .some(.malformedClue) = parseError(from: source) else { 181 Issue.record("expected malformedClue") 182 return 183 } 184 } 185 186 @Test("An over-long answer reading is rejected") 187 func overLongAnswerReadingIsRejected() { 188 let answer = String(repeating: "A", count: XD.maxClueTextLength + 1) 189 let source = """ 190 Title: Long Answer 191 192 193 A 194 195 196 A1. Letter ~ \(answer) 197 """ 198 199 guard case .some(.malformedClue) = parseError(from: source) else { 200 Issue.record("expected malformedClue") 201 return 202 } 203 } 204 205 @Test("More accepted answers than the maximum are rejected") 206 func tooManyAcceptedAnswersAreRejected() { 207 let tokens = (1...(XD.maxAcceptedAnswersPerClue + 1)) 208 .map { "B\($0)" } 209 .joined(separator: " ") 210 let source = """ 211 Title: Accepting 212 213 214 A 215 216 217 A1. Letter ~ A 218 A1 ^Accept: \(tokens) 219 """ 220 221 guard case .some(.tooManyAcceptedAnswers) = parseError(from: source) else { 222 Issue.record("expected tooManyAcceptedAnswers") 223 return 224 } 225 } 226 227 @Test("An accepted-answer count at the maximum still parses") 228 func acceptedAnswersAtMaximumParse() throws { 229 let tokens = (1...XD.maxAcceptedAnswersPerClue) 230 .map { "B\($0)" } 231 .joined(separator: " ") 232 let source = """ 233 Title: Accepting 234 235 236 A 237 238 239 A1. Letter ~ A 240 A1 ^Accept: \(tokens) 241 """ 242 243 let xd = try XD.parse(source) 244 #expect(xd.acrossClues.first?.acceptedAnswers.count == XD.maxAcceptedAnswersPerClue) 245 } 246 247 // MARK: - Rebus budget 248 249 @Test("An over-long rebus expansion fails closed at its grid cell") 250 func overLongRebusValueFailsClosed() { 251 let value = String(repeating: "A", count: XD.maxRebusValueLength + 1) 252 let source = """ 253 Title: Big Rebus 254 Rebus: 1=\(value) 255 256 257 1 258 259 260 A1. Never reached 261 """ 262 263 guard case .some(.unknownGridCharacter) = parseError(from: source) else { 264 Issue.record("expected unknownGridCharacter") 265 return 266 } 267 } 268 269 @Test("A rebus expansion at the maximum length still parses") 270 func rebusValueAtMaximumParses() throws { 271 let value = String(repeating: "A", count: XD.maxRebusValueLength) 272 let source = """ 273 Title: Big Rebus 274 Rebus: 1=\(value) 275 276 277 1 278 279 280 A1. Long fill ~ \(value) 281 """ 282 283 let xd = try XD.parse(source) 284 guard case .open(let solution?, _, _) = xd.cells[0][0] else { 285 Issue.record("expected an open cell with a solution") 286 return 287 } 288 #expect(solution.count == XD.maxRebusValueLength) 289 } 290 }