Skip to content

Commit 3f683c7

Browse files
g-cqdclaude
andcommitted
Phase 3: Split CSVDecoderTests.swift into focused test suites
Split the monolithic 2079-line test file into 8 focused test files: - CSVDecoderBasicTests.swift - Simple decoding, types (UUID, URL, Decimal) - CSVDecoderStrategyTests.swift - Date/number/boolean strategies - CSVDecoderRFC4180Tests.swift - Quoted fields, line endings, strict mode - CSVDecoderStreamingTests.swift - Async/stream decoding - CSVDecoderParallelTests.swift - SIMD scanner, parallel decode, backpressure - CSVDecoderKeyMappingTests.swift - Key strategies, column/index mapping - CSVDecoderErrorTests.swift - Error locations, suggestions, diagnostics - CSVDecoderNestedTests.swift - Nested type decoding strategies Benefits: - Faster test runs when working on specific features - Clearer ownership and maintenance - Easier CI parallelization All 181 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9fc5ff7 commit 3f683c7

10 files changed

Lines changed: 2292 additions & 2096 deletions

MIGRATION_PLAN.md

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# CSV Parsing Locale-Aware Migration Plan
22

3-
## Status: Phase 1 Complete, Phase 2 In Progress
3+
## Status: Phase 1 Complete, Phase 2 Complete, Phase 3 Planned
44

55
## Overview
66

@@ -43,24 +43,57 @@ Migrate from hardcoded parsing logic to Foundation's locale-aware FormatStyle/Pa
4343

4444
## Phase 3: CSVCoder File Organization
4545

46-
### 3.1 Source File Splitting
47-
Split large files into focused, single-responsibility modules:
46+
### 3.1 Current File Analysis
4847

49-
| Current File | Split Into |
50-
|--------------|------------|
51-
| `CSVDecoder.swift` (500+ lines) | `CSVDecoder.swift` (core), `CSVDecoderConfiguration.swift`, `CSVDecodingStrategies.swift` |
52-
| `CSVSingleValueDecoder.swift` (450+ lines) | `CSVSingleValueDecoder.swift`, `NumberParsing.swift`, `DateParsing.swift` |
53-
| `CSVRowDecoder.swift` (700+ lines) | `CSVRowDecoder.swift`, `CSVKeyedDecodingContainer.swift` |
48+
| File | Lines | Bytes | Notes |
49+
|------|-------|-------|-------|
50+
| `CSVRowDecoder.swift` | 875 | 35KB | Largest source file |
51+
| `CSVDecoder+Parallel.swift` | 559 | 20KB | Well-scoped extension |
52+
| `CSVDecoder.swift` | 504 | 20KB | Config + strategies + decoder |
53+
| `CSVSingleValueDecoder.swift` | 464 | 17KB | Moderate complexity |
54+
| `CSVDecoderTests.swift` | 2079 | 67KB | **Priority: split tests** |
55+
| `CSVEncoderTests.swift` | 957 | 32KB | Lower priority |
5456

55-
### 3.2 Test File Splitting
56-
| Current File | Split Into |
57-
|--------------|------------|
58-
| `CSVDecoderTests.swift` (2000+ lines) | `CSVDecoderBasicTests.swift`, `CSVDecoderStrategyTests.swift`, `CSVDecoderEdgeCaseTests.swift`, `CSVDecoderStreamingTests.swift` |
59-
| `CSVEncoderTests.swift` | Similar split by functionality |
57+
### 3.2 Source File Strategy
6058

61-
### 3.3 Naming Conventions
62-
- Source: `CSV<Component>.swift` or `<Component>+CSV.swift` for extensions
63-
- Tests: `<Component>Tests.swift` matching source file names
59+
**Recommended: Minimal restructuring**
60+
61+
After analysis, the existing source structure is already well-organized:
62+
- Extensions (`+Parallel`, `+Streaming`, `+Backpressure`) are properly split
63+
- `LocaleUtilities.swift` was added in Phase 1 for parsing utilities
64+
- `CSVRowDecoder.swift` has tightly coupled parsing logic (not worth splitting)
65+
66+
**Optional future refinements:**
67+
- [ ] Extract `CSVDecoderConfiguration.swift` (lines 14-93 of CSVDecoder.swift) - ~80 lines
68+
- [ ] Extract `CSVDecodingStrategies.swift` (strategy enums) - ~115 lines
69+
- [ ] Consider making `LocaleUtilities` public for external consumption
70+
71+
### 3.3 Test File Splitting (Recommended)
72+
73+
Split `CSVDecoderTests.swift` (2079 lines, 100+ tests) into focused suites:
74+
75+
| New File | Test Groups | Lines |
76+
|----------|-------------|-------|
77+
| `CSVDecoderBasicTests.swift` | Simple decode, delimiters, types (UUID, URL, Decimal) | ~200 |
78+
| `CSVDecoderStrategyTests.swift` | Date/number/boolean strategies | ~230 |
79+
| `CSVDecoderRFC4180Tests.swift` | Quoted fields, line endings, strict/lenient mode | ~350 |
80+
| `CSVDecoderStreamingTests.swift` | Stream decode, async collect | ~160 |
81+
| `CSVDecoderParallelTests.swift` | SIMD scanner, parallel decode, batched | ~250 |
82+
| `CSVDecoderKeyMappingTests.swift` | Key strategies, column mapping, index mapping | ~200 |
83+
| `CSVDecoderErrorTests.swift` | Error locations, suggestions, diagnostics | ~150 |
84+
| `CSVDecoderNestedTests.swift` | Nested decoding strategies | ~100 |
85+
| `LocaleAwareDecodingTests.swift` | Already separate (Phase 1) | 295 |
86+
87+
**Benefits:**
88+
- Faster test runs when working on specific features
89+
- Clearer ownership and maintenance
90+
- Easier CI parallelization
91+
92+
### 3.4 Implementation Priority
93+
94+
1. **High**: Test file splitting (CSVDecoderTests.swift → 8 files)
95+
2. **Medium**: Extract configuration/strategies from CSVDecoder.swift
96+
3. **Low**: Make LocaleUtilities public API
6497

6598
---
6699

@@ -81,7 +114,33 @@ Split large files into focused, single-responsibility modules:
81114
- ✅ Updated `CSVSingleValueDecoder.swift` and `CSVRowDecoder.swift` to use new strategies
82115
- ✅ Added 13 new tests in `LocaleAwareDecodingTests.swift`
83116
- ✅ All 181 tests passing
84-
- Phase 1 complete, starting Phase 2
117+
- Phase 1 complete
118+
119+
### Entry 3: 2024-12-28
120+
- ✅ Phase 2 complete: LotoFuel assessment done
121+
- CSVDataNormalizer kept for pre-decode validation
122+
- FuelioCSVReader kept with current strategies (fixed Fuelio format)
123+
- LotoFuel updated to CSVCoder revision 9fc5ff7
124+
- All 24 LotoFuelServices tests passing
125+
- ✅ Phase 3 planning complete:
126+
- Analyzed all source files (6.9K lines total)
127+
- Analyzed test files (3.3K lines, CSVDecoderTests.swift = 2K lines)
128+
- Recommended test file splitting over source splitting
129+
- Source structure already well-organized with +Parallel, +Streaming extensions
130+
131+
### Entry 4: 2024-12-28
132+
- ✅ Phase 3 test file splitting implemented:
133+
- Split CSVDecoderTests.swift (2079 lines) into 8 focused test files:
134+
- CSVDecoderBasicTests.swift (~200 lines) - Simple decoding, types
135+
- CSVDecoderStrategyTests.swift (~200 lines) - Date/number/boolean strategies
136+
- CSVDecoderRFC4180Tests.swift (~320 lines) - Quoted fields, strict/lenient mode
137+
- CSVDecoderStreamingTests.swift (~230 lines) - Async/stream decoding
138+
- CSVDecoderParallelTests.swift (~340 lines) - SIMD, parallel decode
139+
- CSVDecoderKeyMappingTests.swift (~330 lines) - Key strategies, index mapping
140+
- CSVDecoderErrorTests.swift (~170 lines) - Error diagnostics
141+
- CSVDecoderNestedTests.swift (~160 lines) - Nested decoding strategies
142+
- All 181 tests pass
143+
- Total test files: 10 (8 decoder + 1 encoder + 1 locale-aware)
85144

86145
---
87146

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
//
2+
// CSVDecoderBasicTests.swift
3+
// CSVCoder
4+
//
5+
// Basic decoding tests: simple records, delimiters, types.
6+
//
7+
8+
import Testing
9+
@testable import CSVCoder
10+
import Foundation
11+
12+
@Suite("CSVDecoder Basic Tests")
13+
struct CSVDecoderBasicTests {
14+
15+
struct SimpleRecord: Codable, Equatable {
16+
let name: String
17+
let age: Int
18+
let score: Double
19+
}
20+
21+
@Test("Decode simple records")
22+
func decodeSimpleRecords() throws {
23+
let csv = """
24+
name,age,score
25+
Alice,30,95.5
26+
Bob,25,88.0
27+
"""
28+
29+
let decoder = CSVDecoder()
30+
let records = try decoder.decode([SimpleRecord].self, from: csv)
31+
32+
#expect(records.count == 2)
33+
#expect(records[0] == SimpleRecord(name: "Alice", age: 30, score: 95.5))
34+
#expect(records[1] == SimpleRecord(name: "Bob", age: 25, score: 88.0))
35+
}
36+
37+
struct NameAge: Codable, Equatable {
38+
let name: String
39+
let age: Int
40+
}
41+
42+
@Test("Decode with semicolon delimiter")
43+
func decodeWithSemicolonDelimiter() throws {
44+
let csv = """
45+
name;age
46+
Charlie;35
47+
"""
48+
49+
let config = CSVDecoder.Configuration(delimiter: ";")
50+
let decoder = CSVDecoder(configuration: config)
51+
let records = try decoder.decode([NameAge].self, from: csv)
52+
53+
#expect(records.count == 1)
54+
#expect(records[0].name == "Charlie")
55+
#expect(records[0].age == 35)
56+
}
57+
58+
struct DateRecord: Codable {
59+
let event: String
60+
let date: Date
61+
}
62+
63+
@Test("Decode dates with format")
64+
func decodeDatesWithFormat() throws {
65+
let csv = """
66+
event,date
67+
Meeting,25/12/2024
68+
"""
69+
70+
let config = CSVDecoder.Configuration(
71+
dateDecodingStrategy: .formatted("dd/MM/yyyy")
72+
)
73+
let decoder = CSVDecoder(configuration: config)
74+
let records = try decoder.decode([DateRecord].self, from: csv)
75+
76+
#expect(records.count == 1)
77+
#expect(records[0].event == "Meeting")
78+
}
79+
80+
@Test("Decode from single row dictionary")
81+
func decodeFromDictionary() throws {
82+
let row = ["name": "Eve", "age": "28", "score": "92.5"]
83+
84+
let decoder = CSVDecoder()
85+
let record = try decoder.decode(SimpleRecord.self, from: row)
86+
87+
#expect(record.name == "Eve")
88+
#expect(record.age == 28)
89+
#expect(record.score == 92.5)
90+
}
91+
92+
@Test("Handle quoted fields with delimiters")
93+
func handleQuotedFields() throws {
94+
let csv = """
95+
name,description
96+
Test,"Value,with,commas"
97+
"""
98+
99+
let decoder = CSVDecoder()
100+
101+
struct QuotedRecord: Codable {
102+
let name: String
103+
let description: String
104+
}
105+
106+
let records = try decoder.decode([QuotedRecord].self, from: csv)
107+
108+
#expect(records.count == 1)
109+
#expect(records[0].description == "Value,with,commas")
110+
}
111+
112+
@Test("Handle empty values")
113+
func handleEmptyValues() throws {
114+
let csv = """
115+
name,value
116+
Test,
117+
"""
118+
119+
struct OptionalRecord: Codable {
120+
let name: String
121+
let value: String?
122+
}
123+
124+
let decoder = CSVDecoder()
125+
let records = try decoder.decode([OptionalRecord].self, from: csv)
126+
127+
#expect(records.count == 1)
128+
#expect(records[0].value == nil)
129+
}
130+
131+
@Test("Decode UInt8 values")
132+
func decodeUInt8Values() throws {
133+
let csv = """
134+
id,value
135+
1,42
136+
2,255
137+
"""
138+
139+
struct ByteRecord: Codable {
140+
let id: Int
141+
let value: UInt8
142+
}
143+
144+
let decoder = CSVDecoder()
145+
let records = try decoder.decode([ByteRecord].self, from: csv)
146+
147+
#expect(records.count == 2)
148+
#expect(records[0].value == 42)
149+
#expect(records[1].value == 255)
150+
}
151+
152+
@Test("Decode Decimal values")
153+
func decodeDecimalValues() throws {
154+
let csv = """
155+
price,quantity
156+
19.99,100
157+
0.001,999999
158+
"""
159+
160+
struct PriceRecord: Codable, Equatable {
161+
let price: Decimal
162+
let quantity: Int
163+
}
164+
165+
let decoder = CSVDecoder()
166+
let records = try decoder.decode([PriceRecord].self, from: csv)
167+
168+
#expect(records.count == 2)
169+
#expect(records[0].price == Decimal(string: "19.99"))
170+
#expect(records[1].price == Decimal(string: "0.001"))
171+
}
172+
173+
@Test("Decode UUID values")
174+
func decodeUUIDValues() throws {
175+
let uuid1 = UUID()
176+
let uuid2 = UUID()
177+
let csv = """
178+
id,name
179+
\(uuid1.uuidString),Item1
180+
\(uuid2.uuidString),Item2
181+
"""
182+
183+
struct UUIDRecord: Codable {
184+
let id: UUID
185+
let name: String
186+
}
187+
188+
let decoder = CSVDecoder()
189+
let records = try decoder.decode([UUIDRecord].self, from: csv)
190+
191+
#expect(records.count == 2)
192+
#expect(records[0].id == uuid1)
193+
#expect(records[1].id == uuid2)
194+
}
195+
196+
@Test("Decode URL values")
197+
func decodeURLValues() throws {
198+
let csv = """
199+
name,website
200+
Example,https://example.com
201+
Test,https://test.com/path?query=1
202+
"""
203+
204+
struct URLRecord: Codable {
205+
let name: String
206+
let website: URL
207+
}
208+
209+
let decoder = CSVDecoder()
210+
let records = try decoder.decode([URLRecord].self, from: csv)
211+
212+
#expect(records.count == 2)
213+
#expect(records[0].website == URL(string: "https://example.com"))
214+
#expect(records[1].website == URL(string: "https://test.com/path?query=1"))
215+
}
216+
217+
// MARK: - Type Inference Tests
218+
219+
@Test("Type inference decode from string")
220+
func typeInferenceFromString() throws {
221+
let csv = """
222+
name,age,score
223+
Alice,30,95.5
224+
"""
225+
226+
let decoder = CSVDecoder()
227+
let records: [SimpleRecord] = try decoder.decode(from: csv)
228+
229+
#expect(records.count == 1)
230+
#expect(records[0].name == "Alice")
231+
}
232+
233+
@Test("Type inference decode from Data")
234+
func typeInferenceFromData() throws {
235+
let csv = """
236+
name,age,score
237+
Bob,25,88.0
238+
"""
239+
let data = csv.data(using: .utf8)!
240+
241+
let decoder = CSVDecoder()
242+
let records: [SimpleRecord] = try decoder.decode(from: data)
243+
244+
#expect(records.count == 1)
245+
#expect(records[0].name == "Bob")
246+
}
247+
248+
@Test("Type inference decode from dictionary")
249+
func typeInferenceFromDictionary() throws {
250+
let row = ["name": "Carol", "age": "35", "score": "92.0"]
251+
252+
let decoder = CSVDecoder()
253+
let record: SimpleRecord = try decoder.decode(from: row)
254+
255+
#expect(record.name == "Carol")
256+
#expect(record.age == 35)
257+
}
258+
}

0 commit comments

Comments
 (0)