Skip to content

Commit e7dfaea

Browse files
committed
CSVWriter tests updated
1 parent c6c9c68 commit e7dfaea

File tree

8 files changed

+351
-277
lines changed

8 files changed

+351
-277
lines changed

Sources/Active/Writer/Writer.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ extension CSVWriter {
6969
public func endFile() throws {
7070
guard self.stream.streamStatus != .closed else { return }
7171

72-
if self.fieldIndex >= 0 {
72+
if self.fieldIndex > 0 {
7373
try self.endRow()
7474
}
7575

@@ -82,7 +82,7 @@ extension CSVWriter {
8282
/// - parameter field: The `String` to concatenate to the current CSV row.
8383
/// - throws: `CSVError<CSVWriter>` exclusively.
8484
public func write(field: String) throws {
85-
guard self.expectedFields <= 0 || self.fieldIndex >= self.expectedFields else {
85+
guard self.expectedFields <= 0 || self.fieldIndex <= self.expectedFields else {
8686
throw Error.fieldOverflow(expectedFields: self.expectedFields)
8787
}
8888

@@ -100,7 +100,7 @@ extension CSVWriter {
100100
/// - parameter fields: A collection representing several fields (usually `[String]`).
101101
/// - throws: `CSVWriter.Error` exclusively.
102102
public func write<C:Collection>(fields: C) throws where C.Element == String {
103-
guard self.expectedFields <= 0 || (self.fieldIndex + fields.count) >= self.expectedFields else {
103+
guard self.expectedFields <= 0 || (self.fieldIndex + fields.count) <= self.expectedFields else {
104104
throw Error.fieldOverflow(expectedFields: self.expectedFields)
105105
}
106106

@@ -122,7 +122,6 @@ extension CSVWriter {
122122
return try self.writeEmptyRow()
123123
}
124124

125-
// If this is the first row, fill the `expectedFields` variable.
126125
if self.expectedFields > 0 {
127126
try stride(from: self.fieldIndex, to: self.expectedFields, by: 1).forEach { [f = self.settings.delimiters.field] _ in
128127
try self.lowlevelWrite(delimiter: f)

Tests/CodableCSVTests/ActiveTests/ReaderTests.swift

Lines changed: 121 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,54 @@ final class ReaderTests: XCTestCase {
1616
override func setUp() {
1717
self.continueAfterFailure = false
1818
}
19+
}
20+
21+
// MARK: -
22+
23+
extension ReaderTests {
24+
/// The test data used for this file.
25+
private enum TestData {
26+
/// A CSV row representing a header row (4 fields).
27+
static let headers = ["seq", "Name", "Country", "Number Pair"]
28+
/// Small amount of regular CSV rows (4 fields per row).
29+
static let content = [["1", "Marcos", "Spain", "99"],
30+
["2", "Marine-Anaïs", "France", "88"],
31+
["3", "Alex", "Germany", "77"],
32+
["4", "Pei", "China", "66"]]
33+
/// A bunch of rows each one containing an edge case.
34+
static let edgeCases = [["", "Marcos", "Spaiñ", "99"],
35+
["2", "Marine-Anaïs", #""Fra""nce""#, ""],
36+
["", "", "", ""],
37+
["4", "Pei", "China", #""\#n""#],
38+
["", "", "", #""\#r\#n""#],
39+
["5", #""A\#rh,me\#nd""#, "Egypt", #""\#r""#],
40+
["6", #""Man""olo""#, "México", "100_000"]]
41+
/// Exactly the same data as `contentEdgeCases`, but the quotes delimiting the beginning and end of a field have been removed.
42+
///
43+
/// It is tipically used to check the result of parsing `contentEdgeCases`.
44+
static let unescapedEdgeCases = [
45+
["", "Marcos", "Spaiñ", "99"],
46+
["2", "Marine-Anaïs", #"Fra"nce"#, ""],
47+
["", "", "", ""],
48+
["4", "Pei", "China", "\n"],
49+
["", "", "", "\r\n"],
50+
["5", "A\rh,me\nd", "Egypt", "\r"],
51+
["6", #"Man"olo"#, "México", "100_000"]]
52+
/// Encodes the test data into a Swift `String`.
53+
/// - parameter sample:
54+
/// - parameter delimiters: Unicode scalars to use to mark fields and rows.
55+
/// - returns: Swift String representing the CSV file.
56+
static func toCSV(_ sample: [[String]], delimiters: Delimiter.Pair) -> String {
57+
let (f, r) = (String(delimiters.field.rawValue), String(delimiters.row.rawValue))
58+
return sample.map { $0.joined(separator: f) }.joined(separator: r).appending(r)
59+
}
60+
}
1961

2062
private typealias Encoded = (string: String, data: Data)
2163
}
2264

65+
// MARK: -
66+
2367
extension ReaderTests {
2468
/// Tests the correct parsing of an empty CSV.
2569
func testEmpty() throws {
@@ -30,8 +74,14 @@ extension ReaderTests {
3074

3175
/// Tests the correct parsing of a single value CSV.
3276
func testSingleValue() throws {
77+
let delimiters: Delimiter.Pair = (",", "\n")
3378
let input = [["Marine-Anaïs"]]
34-
let parsed = try CSVReader.parse(input: input.toCSV() as String) { $0.headerStrategy = .none }
79+
80+
let parsed = try CSVReader.parse(input: TestData.toCSV(input, delimiters: delimiters)) {
81+
$0.delimiters = delimiters
82+
$0.headerStrategy = .none
83+
}
84+
3585
XCTAssertTrue(parsed.headers.isEmpty)
3686
XCTAssertEqual(parsed.rows, input)
3787
}
@@ -45,8 +95,7 @@ extension ReaderTests {
4595
let trimStrategy: [CharacterSet] = [.init(), .whitespaces]
4696
let presamples: [Bool] = [true, false]
4797
// The data used for testing.
48-
let headers = TestData.headers
49-
let content = TestData.content
98+
let (headers, content) = (TestData.headers, TestData.content)
5099

51100
// The actual testing implementation.
52101
let work: (_ configuration: CSVReader.Configuration, _ encoded: Encoded) throws -> Void = {
@@ -79,7 +128,9 @@ extension ReaderTests {
79128
case .firstLine: input = [headers] + content
80129
// case .unknown: return XCTFail("Testing header inference is not yet supported")
81130
}
82-
let encoded: Encoded = (input.toCSV(delimiters: pair), input.toCSV(delimiters: pair))
131+
132+
let string = TestData.toCSV(input, delimiters: pair)
133+
let encoded: Encoded = (string, string.data(using: .utf8)!)
83134

84135
for t in trimStrategy {
85136
var toTrim = t
@@ -112,9 +163,8 @@ extension ReaderTests {
112163
let trimStrategy: [CharacterSet] = [.init(), /*.whitespaces*/] // The whitespaces remove the row or field delimiters.
113164
let presamples: [Bool] = [true, false]
114165
// The data used for testing.
115-
let headers = TestData.headers
116-
let content = TestData.contentEdgeCases
117-
let unescapedContent = TestData.contentUnescapedEdgeCases
166+
let (headers, content) = (TestData.headers, TestData.edgeCases)
167+
let unescapedContent = TestData.unescapedEdgeCases
118168

119169
// The actual testing implementation.
120170
let work: (_ configuration: CSVReader.Configuration, _ encoded: Encoded) throws -> Void = {
@@ -146,7 +196,9 @@ extension ReaderTests {
146196
case .firstLine: input = [headers] + content
147197
// case .unknown: return XCTFail("Testing header inference is not yet supported")
148198
}
149-
let encoded: Encoded = (input.toCSV(delimiters: pair), input.toCSV(delimiters: pair))
199+
200+
let string = TestData.toCSV(input, delimiters: pair)
201+
let encoded: Encoded = (string, string.data(using: .utf8)!)
150202

151203
for t in trimStrategy {
152204
var toTrim = t
@@ -177,8 +229,7 @@ extension ReaderTests {
177229
let trimStrategy: [CharacterSet] = [.init(), .whitespaces]
178230
let presamples: [Bool] = [true, false]
179231
// The data used for testing.
180-
let headers = TestData.headers
181-
let content = TestData.content
232+
let (headers, content) = (TestData.headers, TestData.content)
182233
let input = ([headers] + content).mappingRandomFields(count: 5) { [quote = Character("\"")] in
183234
guard !$0.hasPrefix(String(quote)) else { return $0 }
184235

@@ -203,7 +254,9 @@ extension ReaderTests {
203254
for r in rowDelimiters {
204255
for f in fieldDelimiters {
205256
let pair: Delimiter.Pair = (f, r)
206-
let encoded: Encoded = (input.toCSV(delimiters: pair), input.toCSV(delimiters: pair))
257+
258+
let string = TestData.toCSV(input, delimiters: pair)
259+
let encoded: Encoded = (string, string.data(using: .utf8)!)
207260

208261
for t in trimStrategy {
209262
var toTrim = t
@@ -232,14 +285,15 @@ extension ReaderTests {
232285
let fieldDelimiters: [Delimiter.Field] = [",", ";", "\t"]
233286
let presamples: [Bool] = [true, false]
234287
// The data used for testing.
235-
let headers = TestData.headers
236-
let content = TestData.content
288+
let (headers, content) = (TestData.headers, TestData.content)
237289
let input = ([headers] + content).removingRandomFields(count: 2)
238290
// Iterate through all configuration values.
239291
for r in rowDelimiters {
240292
for f in fieldDelimiters {
241293
let pair: Delimiter.Pair = (f, r)
242-
let encoded: Encoded = (input.toCSV(delimiters: pair), input.toCSV(delimiters: pair))
294+
295+
let string = TestData.toCSV(input, delimiters: pair)
296+
let encoded: Encoded = (string, string.data(using: .utf8)!)
243297

244298
for p in presamples {
245299
var c = CSVReader.Configuration()
@@ -253,3 +307,56 @@ extension ReaderTests {
253307
}
254308
}
255309
}
310+
311+
// MARK: -
312+
313+
fileprivate extension Array where Element == [String] {
314+
/// Removes a random field from a random row.
315+
/// - parameter num: The number of random fields to remove.
316+
mutating func removeRandomFields(count: Int = 1) {
317+
guard !self.isEmpty && !self.first!.isEmpty else {
318+
fatalError("The receiving rows cannot be empty.")
319+
}
320+
321+
for _ in 0..<count {
322+
let selectedRow = Int.random(in: 0..<self.count)
323+
let selectedField = Int.random(in: 0..<self[selectedRow].count)
324+
325+
let _ = self[selectedRow].remove(at: selectedField)
326+
}
327+
}
328+
329+
/// Copies the receiving array and removes from it a random field from a random row.
330+
/// - parameter num: The number of random fields to remove.
331+
/// - returns: A copy of the receiving array lacking `count` number of fields.
332+
func removingRandomFields(count: Int = 1) -> [[String]] {
333+
var result = self
334+
result.removeRandomFields(count: count)
335+
return result
336+
}
337+
338+
/// Transform a random field into the value returned in the argument closure.
339+
/// - parameter num: The number of random fields to modify.
340+
mutating func mapRandomFields(count: Int = 1, _ transform: (String) -> String) {
341+
guard !self.isEmpty && !self.first!.isEmpty else {
342+
fatalError("The receiving rows cannot be empty.")
343+
}
344+
345+
for _ in 0..<count {
346+
let selectedRow = Int.random(in: 0..<self.count)
347+
let selectedField = Int.random(in: 0..<self[selectedRow].count)
348+
349+
self[selectedRow][selectedField] = transform(self[selectedRow][selectedField])
350+
}
351+
}
352+
353+
/// Copies the receiving array and transforms a random field from it into another value.
354+
/// - parameter num: The number of random fields to modify.
355+
/// - returns: A copy of the receiving array with the `count` number of fields modified.
356+
func mappingRandomFields(count: Int = 1, _ transform: (String) -> String) -> [[String]] {
357+
var result = self
358+
result.mapRandomFields(count: count, transform)
359+
return result
360+
}
361+
}
362+

0 commit comments

Comments
 (0)