PUZToXDConverterTests.swift (4490B)
1 import Foundation 2 import Testing 3 4 @testable import Crossmate 5 6 @Suite("PUZToXDConverter") 7 struct PUZToXDConverterTests { 8 @Test("Across Lite binary converts to XD and parses") 9 func basicPuzzleConvertsAndParses() throws { 10 let data = try puzData( 11 width: 3, 12 height: 3, 13 solution: "ABCDEFGHI", 14 title: "Mini PUZ", 15 author: "Tester", 16 copyright: "\u{00A9}2026", 17 clues: [ 18 "Across 1", 19 "Down 1", 20 "Down 2", 21 "Down 3", 22 "Across 4", 23 "Across 5" 24 ] 25 ) 26 27 let source = try PUZToXDConverter.convert(puzData: data) 28 let xd = try XD.parse(source) 29 let puzzle = Puzzle(xd: xd) 30 31 #expect(puzzle.title == "Mini PUZ") 32 #expect(puzzle.author == "Tester") 33 #expect(xd.copyright == "\u{00A9}2026") 34 #expect(puzzle.width == 3) 35 #expect(puzzle.height == 3) 36 #expect(puzzle.acrossClues.map(\.number) == [1, 4, 5]) 37 #expect(puzzle.downClues.map(\.number) == [1, 2, 3]) 38 #expect(xd.acrossClues.first?.answer == "ABC") 39 #expect(xd.downClues.first?.answer == "ADG") 40 } 41 42 @Test("Blocks are translated to XD blocks") 43 func blocksConvert() throws { 44 let data = try puzData( 45 width: 3, 46 height: 3, 47 solution: "ABC.D.EFG", 48 title: "Blocks", 49 author: "", 50 copyright: "", 51 clues: [ 52 "Across 1", 53 "Down 2", 54 "Across 4" 55 ] 56 ) 57 58 let source = try PUZToXDConverter.convert(puzData: data) 59 #expect(source.contains("ABC\n#D#\nEFG")) 60 61 let puzzle = Puzzle(xd: try XD.parse(source)) 62 #expect(puzzle.cells[1][0].isBlock) 63 #expect(puzzle.cells[1][2].isBlock) 64 } 65 66 @Test("All-uppercase PUZ title is displayed in title case") 67 func uppercaseTitleIsTitleCased() throws { 68 let data = try puzData( 69 width: 3, 70 height: 3, 71 solution: "ABCDEFGHI", 72 title: "THEMELESS MONDAY #875", 73 author: "", 74 copyright: "", 75 clues: [ 76 "Across 1", 77 "Down 1", 78 "Down 2", 79 "Down 3", 80 "Across 4", 81 "Across 5" 82 ] 83 ) 84 85 let puzzle = Puzzle(xd: try XD.parse(try PUZToXDConverter.convert(puzData: data))) 86 #expect(puzzle.title == "Themeless Monday #875") 87 } 88 89 @Test("Mixed-case PUZ title is preserved") 90 func mixedCaseTitleIsPreserved() throws { 91 let data = try puzData( 92 width: 3, 93 height: 3, 94 solution: "ABCDEFGHI", 95 title: "Mini PUZ", 96 author: "", 97 copyright: "", 98 clues: [ 99 "Across 1", 100 "Down 1", 101 "Down 2", 102 "Down 3", 103 "Across 4", 104 "Across 5" 105 ] 106 ) 107 108 let puzzle = Puzzle(xd: try XD.parse(try PUZToXDConverter.convert(puzData: data))) 109 #expect(puzzle.title == "Mini PUZ") 110 } 111 112 private func puzData( 113 width: UInt8, 114 height: UInt8, 115 solution: String, 116 title: String, 117 author: String, 118 copyright: String, 119 clues: [String], 120 notes: String = "" 121 ) throws -> Data { 122 let cellCount = Int(width) * Int(height) 123 let solutionBytes = Array(solution.utf8) 124 #expect(solutionBytes.count == cellCount) 125 126 var data = Data(repeating: 0, count: 0x34) 127 for (offset, byte) in "ACROSS&DOWN".utf8.enumerated() { 128 data[0x02 + offset] = byte 129 } 130 data[0x18] = UInt8(ascii: "1") 131 data[0x19] = UInt8(ascii: ".") 132 data[0x1A] = UInt8(ascii: "3") 133 data[0x2C] = width 134 data[0x2D] = height 135 data[0x2E] = UInt8(clues.count & 0xFF) 136 data[0x2F] = UInt8((clues.count >> 8) & 0xFF) 137 138 data.append(contentsOf: solutionBytes) 139 data.append(contentsOf: Array(repeating: UInt8(ascii: "-"), count: cellCount)) 140 141 for string in [title, author, copyright] + clues + [notes] { 142 data.append(try cp1252Data(string)) 143 data.append(0) 144 } 145 return data 146 } 147 148 private func cp1252Data(_ string: String) throws -> Data { 149 try #require(string.data(using: .windowsCP1252)) 150 } 151 }