|
3 | 3 | // TableProTests |
4 | 4 | // |
5 | 5 |
|
6 | | -import XCTest |
| 6 | +import Foundation |
| 7 | +import Testing |
| 8 | + |
7 | 9 | @testable import TablePro |
8 | 10 |
|
9 | | -final class StringHexDumpTests: XCTestCase { |
| 11 | +@Suite("String+HexDump") |
| 12 | +struct StringHexDumpTests { |
| 13 | + // MARK: - Hex Dump |
10 | 14 |
|
11 | | - func testEmptyStringReturnsNil() { |
12 | | - XCTAssertNil("".formattedAsHexDump()) |
| 15 | + @Test("Empty string returns nil") |
| 16 | + func emptyStringReturnsNil() { |
| 17 | + #expect("".formattedAsHexDump() == nil) |
13 | 18 | } |
14 | 19 |
|
15 | | - func testBasicASCII() { |
| 20 | + @Test("Basic ASCII produces correct hex and ASCII column") |
| 21 | + func basicASCII() { |
16 | 22 | let result = "Hello".formattedAsHexDump() |
17 | | - XCTAssertNotNil(result) |
18 | | - // "Hello" = 48 65 6C 6C 6F |
19 | | - XCTAssertTrue(result!.contains("48 65 6C 6C 6F")) |
20 | | - XCTAssertTrue(result!.contains("|Hello|")) |
21 | | - } |
22 | | - |
23 | | - func testFullLine() { |
24 | | - // 16 bytes: "0123456789ABCDEF" |
25 | | - let input = "0123456789ABCDEF" |
26 | | - let result = input.formattedAsHexDump()! |
27 | | - // Should have offset 00000000 |
28 | | - XCTAssertTrue(result.hasPrefix("00000000")) |
29 | | - // Should contain ASCII representation |
30 | | - XCTAssertTrue(result.contains("|0123456789ABCDEF|")) |
31 | | - } |
32 | | - |
33 | | - func testMultipleLines() { |
34 | | - // 20 bytes = 1 full line (16) + 1 partial line (4) |
35 | | - let input = "ABCDEFGHIJKLMNOPQRST" |
36 | | - let result = input.formattedAsHexDump()! |
37 | | - let lines = result.split(separator: "\n") |
38 | | - XCTAssertEqual(lines.count, 2) |
39 | | - XCTAssertTrue(lines[0].hasPrefix("00000000")) |
40 | | - XCTAssertTrue(lines[1].hasPrefix("00000010")) |
41 | | - } |
42 | | - |
43 | | - func testNonPrintableCharsShowAsDots() { |
44 | | - // Create string with non-printable characters via isoLatin1 |
| 23 | + #expect(result != nil) |
| 24 | + #expect(result?.contains("48 65 6C 6C 6F") == true) |
| 25 | + #expect(result?.contains("|Hello|") == true) |
| 26 | + } |
| 27 | + |
| 28 | + @Test("Full 16-byte line has correct offset and ASCII") |
| 29 | + func fullLine() { |
| 30 | + let result = "0123456789ABCDEF".formattedAsHexDump() |
| 31 | + #expect(result?.hasPrefix("00000000") == true) |
| 32 | + #expect(result?.contains("|0123456789ABCDEF|") == true) |
| 33 | + } |
| 34 | + |
| 35 | + @Test("Multiple lines have correct offsets") |
| 36 | + func multipleLines() { |
| 37 | + let result = "ABCDEFGHIJKLMNOPQRST".formattedAsHexDump() |
| 38 | + let lines = result?.split(separator: "\n") ?? [] |
| 39 | + #expect(lines.count == 2) |
| 40 | + #expect(lines[0].hasPrefix("00000000")) |
| 41 | + #expect(lines[1].hasPrefix("00000010")) |
| 42 | + } |
| 43 | + |
| 44 | + @Test("Non-printable characters show as dots in ASCII column") |
| 45 | + func nonPrintableCharsShowAsDots() { |
45 | 46 | let bytes: [UInt8] = [0x00, 0x01, 0x02, 0x41, 0x42, 0x7F, 0xFF] |
46 | | - let input = String(bytes: bytes, encoding: .isoLatin1)! |
47 | | - let result = input.formattedAsHexDump()! |
48 | | - // 0x00, 0x01, 0x02 → dots; 0x41, 0x42 → "AB"; 0x7F, 0xFF → dots |
49 | | - XCTAssertTrue(result.contains("|...AB..|")) |
| 47 | + guard let input = String(bytes: bytes, encoding: .isoLatin1) else { |
| 48 | + Issue.record("Failed to create isoLatin1 string") |
| 49 | + return |
| 50 | + } |
| 51 | + let result = input.formattedAsHexDump() |
| 52 | + #expect(result?.contains("|...AB..|") == true) |
50 | 53 | } |
51 | 54 |
|
52 | | - func testTruncation() { |
53 | | - // Create a string larger than maxBytes |
| 55 | + @Test("Truncation adds summary line") |
| 56 | + func truncation() { |
54 | 57 | let input = String(repeating: "A", count: 100) |
55 | | - let result = input.formattedAsHexDump(maxBytes: 32)! |
56 | | - XCTAssertTrue(result.contains("truncated")) |
57 | | - XCTAssertTrue(result.contains("100 bytes total")) |
58 | | - // Should only have 2 full lines (32 bytes / 16) + truncation line |
59 | | - let lines = result.split(separator: "\n") |
60 | | - XCTAssertEqual(lines.count, 3) |
| 58 | + let result = input.formattedAsHexDump(maxBytes: 32) |
| 59 | + #expect(result?.contains("truncated") == true) |
| 60 | + #expect(result?.contains("100 bytes total") == true) |
| 61 | + let lines = result?.split(separator: "\n") ?? [] |
| 62 | + #expect(lines.count == 3) |
61 | 63 | } |
62 | 64 |
|
63 | | - func testOffsetFormatting() { |
| 65 | + @Test("Offset formatting across multiple lines") |
| 66 | + func offsetFormatting() { |
64 | 67 | let input = String(repeating: "X", count: 48) |
65 | | - let result = input.formattedAsHexDump()! |
66 | | - let lines = result.split(separator: "\n") |
67 | | - XCTAssertEqual(lines.count, 3) |
68 | | - XCTAssertTrue(lines[0].hasPrefix("00000000")) |
69 | | - XCTAssertTrue(lines[1].hasPrefix("00000010")) |
70 | | - XCTAssertTrue(lines[2].hasPrefix("00000020")) |
71 | | - } |
72 | | - |
73 | | - func testSingleByte() { |
74 | | - let result = "A".formattedAsHexDump()! |
75 | | - XCTAssertTrue(result.contains("41")) |
76 | | - XCTAssertTrue(result.contains("|A|")) |
| 68 | + let lines = input.formattedAsHexDump()?.split(separator: "\n") ?? [] |
| 69 | + #expect(lines.count == 3) |
| 70 | + #expect(lines[0].hasPrefix("00000000")) |
| 71 | + #expect(lines[1].hasPrefix("00000010")) |
| 72 | + #expect(lines[2].hasPrefix("00000020")) |
| 73 | + } |
| 74 | + |
| 75 | + @Test("Single byte") |
| 76 | + func singleByte() { |
| 77 | + let result = "A".formattedAsHexDump() |
| 78 | + #expect(result?.contains("41") == true) |
| 79 | + #expect(result?.contains("|A|") == true) |
| 80 | + } |
| 81 | + |
| 82 | + // MARK: - Compact Hex |
| 83 | + |
| 84 | + @Test("Compact hex basic ASCII") |
| 85 | + func compactHexBasic() { |
| 86 | + #expect("Hello".formattedAsCompactHex() == "0x48656C6C6F") |
| 87 | + } |
| 88 | + |
| 89 | + @Test("Compact hex empty string returns nil") |
| 90 | + func compactHexEmpty() { |
| 91 | + #expect("".formattedAsCompactHex() == nil) |
| 92 | + } |
| 93 | + |
| 94 | + @Test("Compact hex truncation adds ellipsis") |
| 95 | + func compactHexTruncation() { |
| 96 | + let input = String(repeating: "A", count: 100) |
| 97 | + #expect(input.formattedAsCompactHex(maxBytes: 4) == "0x41414141…") |
| 98 | + } |
| 99 | + |
| 100 | + @Test("Compact hex non-printable bytes") |
| 101 | + func compactHexNonPrintable() { |
| 102 | + let bytes: [UInt8] = [0x00, 0xFF] |
| 103 | + guard let input = String(bytes: bytes, encoding: .isoLatin1) else { |
| 104 | + Issue.record("Failed to create isoLatin1 string") |
| 105 | + return |
| 106 | + } |
| 107 | + #expect(input.formattedAsCompactHex() == "0x00FF") |
| 108 | + } |
| 109 | + |
| 110 | + // MARK: - Editable Hex |
| 111 | + |
| 112 | + @Test("Editable hex basic ASCII") |
| 113 | + func editableHexBasic() { |
| 114 | + #expect("Hello".formattedAsEditableHex() == "48 65 6C 6C 6F") |
| 115 | + } |
| 116 | + |
| 117 | + @Test("Editable hex empty string returns nil") |
| 118 | + func editableHexEmpty() { |
| 119 | + #expect("".formattedAsEditableHex() == nil) |
| 120 | + } |
| 121 | + |
| 122 | + @Test("Editable hex non-printable bytes") |
| 123 | + func editableHexNonPrintable() { |
| 124 | + let bytes: [UInt8] = [0x00, 0x01, 0xFF] |
| 125 | + guard let input = String(bytes: bytes, encoding: .isoLatin1) else { |
| 126 | + Issue.record("Failed to create isoLatin1 string") |
| 127 | + return |
| 128 | + } |
| 129 | + #expect(input.formattedAsEditableHex() == "00 01 FF") |
| 130 | + } |
| 131 | + |
| 132 | + @Test("Editable hex truncation adds ellipsis") |
| 133 | + func editableHexTruncation() { |
| 134 | + let input = String(repeating: "A", count: 100) |
| 135 | + let result = input.formattedAsEditableHex(maxBytes: 3) |
| 136 | + #expect(result?.hasPrefix("41 41 41") == true) |
| 137 | + #expect(result?.hasSuffix("…") == true) |
| 138 | + } |
| 139 | + |
| 140 | + // MARK: - Parse Hex |
| 141 | + |
| 142 | + @Test("Parse space-separated hex") |
| 143 | + @MainActor |
| 144 | + func parseHexSpaceSeparated() { |
| 145 | + #expect(BlobFormattingService.shared.parseHex("48 65 6C 6C 6F") == "Hello") |
| 146 | + } |
| 147 | + |
| 148 | + @Test("Parse continuous hex") |
| 149 | + @MainActor |
| 150 | + func parseHexContinuous() { |
| 151 | + #expect(BlobFormattingService.shared.parseHex("48656C6C6F") == "Hello") |
| 152 | + } |
| 153 | + |
| 154 | + @Test("Parse hex with 0x prefix") |
| 155 | + @MainActor |
| 156 | + func parseHexWithPrefix() { |
| 157 | + #expect(BlobFormattingService.shared.parseHex("0x48656C6C6F") == "Hello") |
| 158 | + } |
| 159 | + |
| 160 | + @Test("Parse hex rejects odd-length input") |
| 161 | + @MainActor |
| 162 | + func parseHexInvalidOddLength() { |
| 163 | + #expect(BlobFormattingService.shared.parseHex("486") == nil) |
| 164 | + } |
| 165 | + |
| 166 | + @Test("Parse hex rejects invalid characters") |
| 167 | + @MainActor |
| 168 | + func parseHexInvalidChars() { |
| 169 | + #expect(BlobFormattingService.shared.parseHex("ZZZZ") == nil) |
| 170 | + } |
| 171 | + |
| 172 | + @Test("Parse hex rejects empty string") |
| 173 | + @MainActor |
| 174 | + func parseHexEmpty() { |
| 175 | + #expect(BlobFormattingService.shared.parseHex("") == nil) |
| 176 | + } |
| 177 | + |
| 178 | + @Test("Round-trip: raw → editable hex → parse back to raw") |
| 179 | + @MainActor |
| 180 | + func parseHexRoundTrip() { |
| 181 | + let bytes: [UInt8] = [0x00, 0x01, 0x7F, 0x80, 0xFF] |
| 182 | + guard let original = String(bytes: bytes, encoding: .isoLatin1), |
| 183 | + let hex = original.formattedAsEditableHex(), |
| 184 | + let roundTripped = BlobFormattingService.shared.parseHex(hex) else { |
| 185 | + Issue.record("Round-trip conversion failed") |
| 186 | + return |
| 187 | + } |
| 188 | + #expect(roundTripped == original) |
77 | 189 | } |
78 | 190 | } |
0 commit comments