Skip to content

Commit a081ef9

Browse files
authored
Merge pull request #1771 from fsprojects/repo-assist/test-csvruntime-operations-2026-04-30-96fb9e025723b085
[Repo Assist] test: add 30 unit tests for CsvFile runtime transformation methods
2 parents 25a17e2 + b8f0724 commit a081ef9

2 files changed

Lines changed: 262 additions & 0 deletions

File tree

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
module FSharp.Data.Tests.CsvRuntimeOperations
2+
3+
open NUnit.Framework
4+
open FsUnit
5+
open System
6+
open System.IO
7+
open FSharp.Data
8+
open FSharp.Data.CsvExtensions
9+
10+
let private sampleCsv =
11+
"Name,Age,City\r\nAlice,30,London\r\nBob,25,Paris\r\nCarol,35,Berlin\r\nDave,22,Tokyo\r\nEve,28,Sydney"
12+
13+
// ============================================================
14+
// Filter
15+
// ============================================================
16+
17+
[<Test>]
18+
let ``Filter keeps only rows matching the predicate`` () =
19+
let csv = CsvFile.Parse(sampleCsv)
20+
let filtered = csv.Filter(fun row -> row.["Age"].AsInteger() >= 28)
21+
filtered.Rows |> Seq.length |> should equal 3
22+
23+
[<Test>]
24+
let ``Filter returns empty rows when no rows match`` () =
25+
let csv = CsvFile.Parse(sampleCsv)
26+
let filtered = csv.Filter(fun row -> row.["Age"].AsInteger() > 100)
27+
filtered.Rows |> Seq.length |> should equal 0
28+
29+
[<Test>]
30+
let ``Filter preserves header information`` () =
31+
let csv = CsvFile.Parse(sampleCsv)
32+
let filtered = csv.Filter(fun row -> row.["City"] = "London")
33+
filtered.Headers |> should equal (Some [| "Name"; "Age"; "City" |])
34+
35+
[<Test>]
36+
let ``Filter returns all rows when all match`` () =
37+
let csv = CsvFile.Parse(sampleCsv)
38+
let filtered = csv.Filter(fun row -> row.["Age"].AsInteger() > 0)
39+
filtered.Rows |> Seq.length |> should equal 5
40+
41+
// ============================================================
42+
// Take
43+
// ============================================================
44+
45+
[<Test>]
46+
let ``Take returns the first N rows`` () =
47+
let csv = CsvFile.Parse(sampleCsv)
48+
let taken = csv.Take 3
49+
let rows = taken.Rows |> Seq.toArray
50+
rows |> should haveLength 3
51+
rows.[0].["Name"] |> should equal "Alice"
52+
rows.[2].["Name"] |> should equal "Carol"
53+
54+
[<Test>]
55+
let ``Take 0 returns empty rows`` () =
56+
let csv = CsvFile.Parse(sampleCsv)
57+
csv.Take(0).Rows |> Seq.length |> should equal 0
58+
59+
[<Test>]
60+
let ``Take preserves headers`` () =
61+
let csv = CsvFile.Parse(sampleCsv)
62+
let taken = csv.Take 2
63+
taken.Headers |> should equal (Some [| "Name"; "Age"; "City" |])
64+
65+
// ============================================================
66+
// TakeWhile
67+
// ============================================================
68+
69+
[<Test>]
70+
let ``TakeWhile yields rows while predicate is true`` () =
71+
let csv = CsvFile.Parse(sampleCsv)
72+
// Ages: 30, 25, 35, 22, 28 — take while Age > 26; Bob=25 fails first
73+
let taken = csv.TakeWhile(fun row -> row.["Age"].AsInteger() > 26)
74+
let rows = taken.Rows |> Seq.toArray
75+
rows |> should haveLength 1
76+
rows.[0].["Name"] |> should equal "Alice"
77+
78+
[<Test>]
79+
let ``TakeWhile returns all rows when predicate always true`` () =
80+
let csv = CsvFile.Parse(sampleCsv)
81+
let taken = csv.TakeWhile(fun row -> row.["Age"].AsInteger() > 0)
82+
taken.Rows |> Seq.length |> should equal 5
83+
84+
[<Test>]
85+
let ``TakeWhile returns empty when first row fails predicate`` () =
86+
let csv = CsvFile.Parse(sampleCsv)
87+
let taken = csv.TakeWhile(fun row -> row.["Age"].AsInteger() > 100)
88+
taken.Rows |> Seq.length |> should equal 0
89+
90+
// ============================================================
91+
// Skip
92+
// ============================================================
93+
94+
[<Test>]
95+
let ``Skip omits the first N rows`` () =
96+
let csv = CsvFile.Parse(sampleCsv)
97+
let skipped = csv.Skip 3
98+
let rows = skipped.Rows |> Seq.toArray
99+
rows |> should haveLength 2
100+
rows.[0].["Name"] |> should equal "Dave"
101+
rows.[1].["Name"] |> should equal "Eve"
102+
103+
[<Test>]
104+
let ``Skip 0 returns all rows`` () =
105+
let csv = CsvFile.Parse(sampleCsv)
106+
csv.Skip(0).Rows |> Seq.length |> should equal 5
107+
108+
[<Test>]
109+
let ``Skip preserves headers`` () =
110+
let csv = CsvFile.Parse(sampleCsv)
111+
csv.Skip(2).Headers |> should equal (Some [| "Name"; "Age"; "City" |])
112+
113+
// ============================================================
114+
// SkipWhile
115+
// ============================================================
116+
117+
[<Test>]
118+
let ``SkipWhile skips rows while predicate is true then yields rest`` () =
119+
let csv = CsvFile.Parse(sampleCsv)
120+
// Ages: 30, 25, 35, 22, 28 — skip while Age >= 25 (Alice=30, Bob=25, Carol=35, then Dave=22 stops)
121+
let skipped = csv.SkipWhile(fun row -> row.["Age"].AsInteger() >= 25)
122+
let rows = skipped.Rows |> Seq.toArray
123+
rows |> should haveLength 2
124+
rows.[0].["Name"] |> should equal "Dave"
125+
126+
[<Test>]
127+
let ``SkipWhile returns all rows when first row fails predicate`` () =
128+
let csv = CsvFile.Parse(sampleCsv)
129+
let skipped = csv.SkipWhile(fun row -> row.["Age"].AsInteger() > 100)
130+
skipped.Rows |> Seq.length |> should equal 5
131+
132+
// ============================================================
133+
// Truncate
134+
// ============================================================
135+
136+
[<Test>]
137+
let ``Truncate returns at most N rows`` () =
138+
let csv = CsvFile.Parse(sampleCsv)
139+
let truncated = csv.Truncate 2
140+
truncated.Rows |> Seq.length |> should equal 2
141+
142+
[<Test>]
143+
let ``Truncate with count larger than row count returns all rows`` () =
144+
let csv = CsvFile.Parse(sampleCsv)
145+
let truncated = csv.Truncate 100
146+
truncated.Rows |> Seq.length |> should equal 5
147+
148+
[<Test>]
149+
let ``Truncate 0 returns empty rows`` () =
150+
let csv = CsvFile.Parse(sampleCsv)
151+
csv.Truncate(0).Rows |> Seq.length |> should equal 0
152+
153+
// ============================================================
154+
// Append
155+
// ============================================================
156+
157+
[<Test>]
158+
let ``Append adds rows from another sequence`` () =
159+
let csv = CsvFile.Parse(sampleCsv)
160+
let first2 = csv.Take 2
161+
let last3 = csv.Skip 2
162+
let combined = first2.Append(last3.Rows)
163+
combined.Rows |> Seq.length |> should equal 5
164+
165+
[<Test>]
166+
let ``Append preserves existing rows`` () =
167+
let csv = CsvFile.Parse(sampleCsv)
168+
let first1 = csv.Take 1
169+
let extra = csv.Take 1
170+
let combined = first1.Append(extra.Rows)
171+
let rows = combined.Rows |> Seq.toArray
172+
rows |> should haveLength 2
173+
rows.[0].["Name"] |> should equal "Alice"
174+
rows.[1].["Name"] |> should equal "Alice"
175+
176+
// ============================================================
177+
// Cache
178+
// ============================================================
179+
180+
[<Test>]
181+
let ``Cache preserves all rows`` () =
182+
let csv = CsvFile.Parse(sampleCsv)
183+
let cached = csv.Cache()
184+
cached.Rows |> Seq.length |> should equal 5
185+
186+
[<Test>]
187+
let ``Cache can be enumerated multiple times`` () =
188+
let csv = CsvFile.Parse(sampleCsv)
189+
let cached = csv.Cache()
190+
cached.Rows |> Seq.length |> should equal 5
191+
cached.Rows |> Seq.length |> should equal 5
192+
193+
// ============================================================
194+
// SaveToString
195+
// ============================================================
196+
197+
[<Test>]
198+
let ``SaveToString produces header row and all data rows`` () =
199+
let csv = CsvFile.Parse(sampleCsv)
200+
let result = csv.SaveToString()
201+
result |> should contain "Name,Age,City"
202+
result |> should contain "Alice,30,London"
203+
result |> should contain "Eve,28,Sydney"
204+
205+
[<Test>]
206+
let ``SaveToString round-trips CSV correctly`` () =
207+
let csv = CsvFile.Parse(sampleCsv)
208+
let serialised = csv.SaveToString()
209+
let reparsed = CsvFile.Parse(serialised)
210+
reparsed.Rows |> Seq.length |> should equal 5
211+
reparsed.Rows |> Seq.head |> fun r -> r.["Name"] |> should equal "Alice"
212+
213+
[<Test>]
214+
let ``SaveToString uses CRLF line endings per RFC 4180`` () =
215+
let csv = CsvFile.Parse(sampleCsv)
216+
let result = csv.SaveToString()
217+
result |> should contain "\r\n"
218+
219+
[<Test>]
220+
let ``SaveToString quotes fields containing the separator`` () =
221+
let csv = CsvFile.Parse("Name,Note\r\nAlice,\"hello, world\"")
222+
let result = csv.SaveToString()
223+
result |> should contain "\"hello, world\""
224+
225+
[<Test>]
226+
let ``SaveToString with custom separator uses it in output`` () =
227+
let csv = CsvFile.Parse(sampleCsv)
228+
let result = csv.SaveToString(separator = ';')
229+
result |> should startWith "Name;Age;City"
230+
231+
// ============================================================
232+
// Save to TextWriter
233+
// ============================================================
234+
235+
[<Test>]
236+
let ``Save to TextWriter produces the same output as SaveToString`` () =
237+
let csv = CsvFile.Parse(sampleCsv)
238+
use writer = new StringWriter()
239+
csv.Save(writer)
240+
let fromSave = writer.ToString()
241+
let fromSaveToString = csv.SaveToString()
242+
fromSave |> should equal fromSaveToString
243+
244+
// ============================================================
245+
// Chaining operations
246+
// ============================================================
247+
248+
[<Test>]
249+
let ``Chaining Filter and Take works correctly`` () =
250+
let csv = CsvFile.Parse(sampleCsv)
251+
let result = csv.Filter(fun row -> row.["Age"].AsInteger() >= 28).Take(2)
252+
let rows = result.Rows |> Seq.toArray
253+
rows |> should haveLength 2
254+
255+
[<Test>]
256+
let ``Chaining Skip and Truncate works correctly`` () =
257+
let csv = CsvFile.Parse(sampleCsv)
258+
let result = csv.Skip(1).Truncate(2)
259+
let rows = result.Rows |> Seq.toArray
260+
rows |> should haveLength 2
261+
rows.[0].["Name"] |> should equal "Bob"

tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<Compile Include="JsonSchema.fs" />
3838
<Compile Include="CsvReader.fs" />
3939
<Compile Include="CsvFile.fs" />
40+
<Compile Include="CsvRuntimeOperations.fs" />
4041
<Compile Include="CsvParserProperties.fs" />
4142
<Compile Include="StringExtensions.fs" />
4243
<Compile Include="HtmlCharRefs.fs" />

0 commit comments

Comments
 (0)