csvops 0.7.0.alpha → 0.9.0.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -20
  3. data/docs/architecture.md +67 -4
  4. data/docs/cli-output-conventions.md +49 -0
  5. data/docs/release-v0.8.0-alpha.md +88 -0
  6. data/docs/release-v0.9.0-alpha.md +80 -0
  7. data/lib/csvtool/application/use_cases/run_csv_stats.rb +64 -0
  8. data/lib/csvtool/cli.rb +136 -12
  9. data/lib/csvtool/domain/csv_stats_session/stats_options.rb +11 -0
  10. data/lib/csvtool/domain/csv_stats_session/stats_session.rb +25 -0
  11. data/lib/csvtool/domain/csv_stats_session/stats_source.rb +17 -0
  12. data/lib/csvtool/infrastructure/csv/csv_stats_scanner.rb +67 -0
  13. data/lib/csvtool/infrastructure/output/csv_stats_file_writer.rb +26 -0
  14. data/lib/csvtool/interface/cli/menu_loop.rb +9 -5
  15. data/lib/csvtool/interface/cli/output/color_policy.rb +25 -0
  16. data/lib/csvtool/interface/cli/output/colorizer.rb +27 -0
  17. data/lib/csvtool/interface/cli/output/formatters/csv_row_formatter.rb +19 -0
  18. data/lib/csvtool/interface/cli/output/formatters/stats_formatter.rb +57 -0
  19. data/lib/csvtool/interface/cli/output/streams.rb +22 -0
  20. data/lib/csvtool/interface/cli/output/table_renderer.rb +70 -0
  21. data/lib/csvtool/interface/cli/workflows/builders/csv_stats_session_builder.rb +28 -0
  22. data/lib/csvtool/interface/cli/workflows/presenters/cross_csv_dedupe_presenter.rb +17 -5
  23. data/lib/csvtool/interface/cli/workflows/presenters/csv_parity_presenter.rb +15 -4
  24. data/lib/csvtool/interface/cli/workflows/presenters/csv_split_presenter.rb +15 -6
  25. data/lib/csvtool/interface/cli/workflows/presenters/csv_stats_presenter.rb +43 -0
  26. data/lib/csvtool/interface/cli/workflows/presenters/row_extraction_presenter.rb +5 -4
  27. data/lib/csvtool/interface/cli/workflows/presenters/row_randomization_presenter.rb +5 -4
  28. data/lib/csvtool/interface/cli/workflows/run_cross_csv_dedupe_workflow.rb +9 -8
  29. data/lib/csvtool/interface/cli/workflows/run_csv_parity_workflow.rb +6 -5
  30. data/lib/csvtool/interface/cli/workflows/run_csv_split_workflow.rb +11 -10
  31. data/lib/csvtool/interface/cli/workflows/run_csv_stats_workflow.rb +78 -0
  32. data/lib/csvtool/interface/cli/workflows/run_extraction_workflow.rb +9 -8
  33. data/lib/csvtool/interface/cli/workflows/run_row_extraction_workflow.rb +7 -6
  34. data/lib/csvtool/interface/cli/workflows/run_row_randomization_workflow.rb +8 -7
  35. data/lib/csvtool/interface/cli/workflows/steps/csv_stats/build_session_step.rb +25 -0
  36. data/lib/csvtool/interface/cli/workflows/steps/csv_stats/collect_destination_step.rb +27 -0
  37. data/lib/csvtool/interface/cli/workflows/steps/csv_stats/collect_inputs_step.rb +31 -0
  38. data/lib/csvtool/interface/cli/workflows/steps/csv_stats/execute_step.rb +27 -0
  39. data/lib/csvtool/version.rb +1 -1
  40. data/test/csvtool/application/use_cases/run_csv_stats_test.rb +165 -0
  41. data/test/csvtool/cli_test.rb +376 -68
  42. data/test/csvtool/cli_unit_test.rb +5 -5
  43. data/test/csvtool/infrastructure/csv/csv_stats_scanner_test.rb +68 -0
  44. data/test/csvtool/infrastructure/output/csv_stats_file_writer_test.rb +38 -0
  45. data/test/csvtool/interface/cli/menu_loop_test.rb +34 -11
  46. data/test/csvtool/interface/cli/output/color_policy_test.rb +40 -0
  47. data/test/csvtool/interface/cli/output/colorizer_test.rb +28 -0
  48. data/test/csvtool/interface/cli/output/formatters/csv_row_formatter_test.rb +22 -0
  49. data/test/csvtool/interface/cli/output/formatters/stats_formatter_test.rb +51 -0
  50. data/test/csvtool/interface/cli/output/streams_test.rb +25 -0
  51. data/test/csvtool/interface/cli/output/table_renderer_test.rb +36 -0
  52. data/test/csvtool/interface/cli/workflows/builders/csv_stats_session_builder_test.rb +19 -0
  53. data/test/csvtool/interface/cli/workflows/presenters/cross_csv_dedupe_presenter_test.rb +4 -1
  54. data/test/csvtool/interface/cli/workflows/presenters/csv_parity_presenter_test.rb +5 -1
  55. data/test/csvtool/interface/cli/workflows/presenters/csv_split_presenter_test.rb +22 -4
  56. data/test/csvtool/interface/cli/workflows/presenters/csv_stats_presenter_test.rb +39 -0
  57. data/test/csvtool/interface/cli/workflows/run_cross_csv_dedupe_workflow_test.rb +10 -7
  58. data/test/csvtool/interface/cli/workflows/run_csv_parity_workflow_test.rb +3 -1
  59. data/test/csvtool/interface/cli/workflows/run_csv_split_workflow_test.rb +5 -3
  60. data/test/csvtool/interface/cli/workflows/run_csv_stats_workflow_test.rb +151 -0
  61. data/test/csvtool/interface/cli/workflows/steps/csv_stats/build_session_step_test.rb +36 -0
  62. data/test/csvtool/interface/cli/workflows/steps/csv_stats/collect_destination_step_test.rb +49 -0
  63. data/test/csvtool/interface/cli/workflows/steps/csv_stats/collect_inputs_step_test.rb +61 -0
  64. data/test/csvtool/interface/cli/workflows/steps/csv_stats/execute_step_test.rb +65 -0
  65. metadata +39 -1
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../test_helper"
4
4
  require "csvtool/cli"
5
+ require "json"
5
6
  require "tmpdir"
6
7
  require "fileutils"
7
8
 
@@ -12,11 +13,75 @@ class TestCli < Minitest::Test
12
13
 
13
14
  def test_menu_can_exit_cleanly
14
15
  output = StringIO.new
15
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("7\n"), stdout: output, stderr: StringIO.new)
16
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("8\n"), stdout: output, stderr: output)
16
17
  assert_equal 0, status
17
18
  assert_includes output.string, "CSV Tool Menu"
18
19
  end
19
20
 
21
+ def test_stats_workflow_shell_can_run_and_return_to_menu
22
+ output = StringIO.new
23
+ input = [
24
+ "7",
25
+ fixture_path("sample_people.csv"),
26
+ "",
27
+ "",
28
+ "",
29
+ "8"
30
+ ].join("\n") + "\n"
31
+
32
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
33
+
34
+ assert_equal 0, status
35
+ assert_includes output.string, "CSV Stats Summary"
36
+ assert_includes output.string, "Metric"
37
+ assert_includes output.string, "Rows"
38
+ assert_includes output.string, "Columns"
39
+ assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
40
+ end
41
+
42
+ def test_stats_workflow_missing_file_returns_to_menu
43
+ output = StringIO.new
44
+ input = [
45
+ "7",
46
+ "/tmp/does-not-exist.csv",
47
+ "",
48
+ "",
49
+ "",
50
+ "8"
51
+ ].join("\n") + "\n"
52
+
53
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
54
+
55
+ assert_equal 0, status
56
+ assert_includes output.string, "File not found: /tmp/does-not-exist.csv"
57
+ assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
58
+ end
59
+
60
+ def test_stats_workflow_can_write_output_to_file
61
+ output = StringIO.new
62
+
63
+ Dir.mktmpdir do |dir|
64
+ output_path = File.join(dir, "stats.csv")
65
+ input = [
66
+ "7",
67
+ fixture_path("sample_people.csv"),
68
+ "",
69
+ "",
70
+ "2",
71
+ output_path,
72
+ "8"
73
+ ].join("\n") + "\n"
74
+
75
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
76
+
77
+ assert_equal 0, status
78
+ assert_includes output.string, "Wrote output to #{output_path}"
79
+ csv_text = File.read(output_path)
80
+ assert_includes csv_text, "metric,value"
81
+ assert_includes csv_text, "row_count,3"
82
+ end
83
+ end
84
+
20
85
  def test_split_workflow_splits_csv_in_menu_flow
21
86
  output = StringIO.new
22
87
  Dir.mktmpdir do |dir|
@@ -32,13 +97,13 @@ class TestCli < Minitest::Test
32
97
  "",
33
98
  "",
34
99
  "",
35
- "7"
100
+ "8"
36
101
  ].join("\n") + "\n"
37
102
 
38
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
103
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
39
104
 
40
105
  assert_equal 0, status
41
- assert_includes output.string, "Chunks written: 3"
106
+ assert_includes output.string, "Chunks written"
42
107
  assert File.file?(File.join(dir, "people_part_001.csv"))
43
108
  assert File.file?(File.join(dir, "people_part_002.csv"))
44
109
  assert File.file?(File.join(dir, "people_part_003.csv"))
@@ -53,10 +118,10 @@ class TestCli < Minitest::Test
53
118
  "",
54
119
  "",
55
120
  "0",
56
- "7"
121
+ "8"
57
122
  ].join("\n") + "\n"
58
123
 
59
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
124
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
60
125
 
61
126
  assert_equal 0, status
62
127
  assert_includes output.string, "Chunk size must be a positive integer."
@@ -73,11 +138,11 @@ class TestCli < Minitest::Test
73
138
  "",
74
139
  "y",
75
140
  "",
76
- "7"
141
+ "8"
77
142
  ].join("\n") + "\n"
78
143
 
79
144
  output = StringIO.new
80
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
145
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
81
146
 
82
147
  assert_equal 0, status
83
148
  assert_match(/\nAlice\nBob\nCara\n/, output.string)
@@ -96,6 +161,244 @@ class TestCli < Minitest::Test
96
161
  assert_equal "Alice\nBob\nCara\n", output.string
97
162
  end
98
163
 
164
+ def test_stats_command_writes_summary_to_stdout_only
165
+ stdout = StringIO.new
166
+ stderr = StringIO.new
167
+
168
+ status = Csvtool::CLI.start(
169
+ ["stats", fixture_path("sample_people.csv")],
170
+ stdin: StringIO.new,
171
+ stdout: stdout,
172
+ stderr: stderr
173
+ )
174
+
175
+ assert_equal 0, status
176
+ assert_includes stdout.string, "CSV Stats Summary"
177
+ assert_includes stdout.string, "Metric"
178
+ assert_includes stdout.string, "Rows"
179
+ assert_includes stdout.string, "Columns"
180
+ assert_includes stdout.string, "Headers"
181
+ assert_equal "", stderr.string
182
+ end
183
+
184
+ def test_stats_command_writes_errors_to_stderr_only
185
+ stdout = StringIO.new
186
+ stderr = StringIO.new
187
+
188
+ status = Csvtool::CLI.start(
189
+ ["stats", "/tmp/not-there.csv"],
190
+ stdin: StringIO.new,
191
+ stdout: stdout,
192
+ stderr: stderr
193
+ )
194
+
195
+ assert_equal 1, status
196
+ assert_equal "", stdout.string
197
+ assert_includes stderr.string, "File not found: /tmp/not-there.csv"
198
+ end
199
+
200
+ def test_stats_command_supports_json_format
201
+ stdout = StringIO.new
202
+ stderr = StringIO.new
203
+
204
+ status = Csvtool::CLI.start(
205
+ ["stats", fixture_path("sample_people.csv"), "--format", "json"],
206
+ stdin: StringIO.new,
207
+ stdout: stdout,
208
+ stderr: stderr
209
+ )
210
+
211
+ assert_equal 0, status
212
+ data = JSON.parse(stdout.string, symbolize_names: true)
213
+ assert_equal 3, data[:row_count]
214
+ assert_equal 2, data[:column_count]
215
+ assert_equal ["name", "city"], data[:headers]
216
+ assert_equal "", stderr.string
217
+ end
218
+
219
+ def test_stats_command_supports_csv_format
220
+ stdout = StringIO.new
221
+ stderr = StringIO.new
222
+
223
+ status = Csvtool::CLI.start(
224
+ ["stats", fixture_path("sample_people.csv"), "--format=csv"],
225
+ stdin: StringIO.new,
226
+ stdout: stdout,
227
+ stderr: stderr
228
+ )
229
+
230
+ assert_equal 0, status
231
+ lines = stdout.string.lines.map(&:strip)
232
+ assert_equal "metric,value", lines[0]
233
+ assert_includes lines, "row_count,3"
234
+ assert_includes lines, "column_count,2"
235
+ assert_includes lines, "headers,name|city"
236
+ assert_equal "", stderr.string
237
+ end
238
+
239
+ def test_stats_command_rejects_unknown_format
240
+ stdout = StringIO.new
241
+ stderr = StringIO.new
242
+
243
+ status = Csvtool::CLI.start(
244
+ ["stats", fixture_path("sample_people.csv"), "--format", "yaml"],
245
+ stdin: StringIO.new,
246
+ stdout: stdout,
247
+ stderr: stderr
248
+ )
249
+
250
+ assert_equal 1, status
251
+ assert_equal "", stdout.string
252
+ assert_includes stderr.string, "Invalid format: yaml"
253
+ end
254
+
255
+ def test_stats_command_color_auto_without_tty_has_no_ansi
256
+ stdout = StringIO.new
257
+ stderr = StringIO.new
258
+
259
+ status = Csvtool::CLI.start(
260
+ ["stats", fixture_path("sample_people.csv"), "--color", "auto"],
261
+ stdin: StringIO.new,
262
+ stdout: stdout,
263
+ stderr: stderr
264
+ )
265
+
266
+ assert_equal 0, status
267
+ refute_match(/\e\[[0-9;]*m/, stdout.string)
268
+ assert_equal "", stderr.string
269
+ end
270
+
271
+ def test_stats_command_color_always_adds_ansi
272
+ stdout = StringIO.new
273
+ stderr = StringIO.new
274
+
275
+ status = Csvtool::CLI.start(
276
+ ["stats", fixture_path("sample_people.csv"), "--color=always"],
277
+ stdin: StringIO.new,
278
+ stdout: stdout,
279
+ stderr: stderr
280
+ )
281
+
282
+ assert_equal 0, status
283
+ assert_match(/\e\[[0-9;]*m/, stdout.string)
284
+ assert_equal "", stderr.string
285
+ end
286
+
287
+ def test_stats_command_color_never_disables_ansi
288
+ stdout = StringIO.new
289
+ stderr = StringIO.new
290
+
291
+ status = Csvtool::CLI.start(
292
+ ["stats", fixture_path("sample_people.csv"), "--color", "never"],
293
+ stdin: StringIO.new,
294
+ stdout: stdout,
295
+ stderr: stderr
296
+ )
297
+
298
+ assert_equal 0, status
299
+ refute_match(/\e\[[0-9;]*m/, stdout.string)
300
+ assert_equal "", stderr.string
301
+ end
302
+
303
+ def test_stats_command_no_color_env_disables_auto
304
+ stdout = StringIO.new
305
+ stderr = StringIO.new
306
+
307
+ status = Csvtool::CLI.start(
308
+ ["stats", fixture_path("sample_people.csv"), "--color", "auto"],
309
+ stdin: StringIO.new,
310
+ stdout: stdout,
311
+ stderr: stderr,
312
+ env: { "NO_COLOR" => "1" }
313
+ )
314
+
315
+ assert_equal 0, status
316
+ refute_match(/\e\[[0-9;]*m/, stdout.string)
317
+ end
318
+
319
+ def test_stats_command_color_always_overrides_no_color_env
320
+ stdout = StringIO.new
321
+ stderr = StringIO.new
322
+
323
+ status = Csvtool::CLI.start(
324
+ ["stats", fixture_path("sample_people.csv"), "--color", "always"],
325
+ stdin: StringIO.new,
326
+ stdout: stdout,
327
+ stderr: stderr,
328
+ env: { "NO_COLOR" => "1" }
329
+ )
330
+
331
+ assert_equal 0, status
332
+ assert_match(/\e\[[0-9;]*m/, stdout.string)
333
+ end
334
+
335
+ def test_stats_command_rejects_unknown_color_mode
336
+ stdout = StringIO.new
337
+ stderr = StringIO.new
338
+
339
+ status = Csvtool::CLI.start(
340
+ ["stats", fixture_path("sample_people.csv"), "--color", "sometimes"],
341
+ stdin: StringIO.new,
342
+ stdout: stdout,
343
+ stderr: stderr
344
+ )
345
+
346
+ assert_equal 1, status
347
+ assert_equal "", stdout.string
348
+ assert_includes stderr.string, "Invalid color mode: sometimes"
349
+ end
350
+
351
+ def test_menu_workflow_routes_ui_to_stderr_and_data_to_stdout
352
+ stdout = StringIO.new
353
+ stderr = StringIO.new
354
+ input = [
355
+ "7",
356
+ fixture_path("sample_people.csv"),
357
+ "",
358
+ "",
359
+ "",
360
+ "8"
361
+ ].join("\n") + "\n"
362
+
363
+ status = Csvtool::CLI.start(
364
+ ["menu"],
365
+ stdin: StringIO.new(input),
366
+ stdout: stdout,
367
+ stderr: stderr
368
+ )
369
+
370
+ assert_equal 0, status
371
+ assert_includes stderr.string, "CSV Tool Menu"
372
+ assert_includes stderr.string, "CSV file path:"
373
+ assert_includes stdout.string, "CSV Stats Summary"
374
+ refute_includes stdout.string, "CSV Tool Menu"
375
+ refute_includes stdout.string, "CSV file path:"
376
+ end
377
+
378
+ def test_stats_command_table_respects_narrow_terminal_width
379
+ stdout = StringIO.new
380
+ stderr = StringIO.new
381
+
382
+ Dir.mktmpdir do |dir|
383
+ path = File.join(dir, "long_headers.csv")
384
+ File.write(path, "very_long_column_name,another_really_long_column_name\nvalue_a,value_b\n")
385
+
386
+ status = Csvtool::CLI.start(
387
+ ["stats", path],
388
+ stdin: StringIO.new,
389
+ stdout: stdout,
390
+ stderr: stderr,
391
+ env: { "COLUMNS" => "32" }
392
+ )
393
+
394
+ assert_equal 0, status
395
+ lines = stdout.string.lines.map(&:chomp)
396
+ assert lines.all? { |line| line.length <= 32 }
397
+ assert_includes stdout.string, "..."
398
+ assert_equal "", stderr.string
399
+ end
400
+ end
401
+
99
402
  def test_row_range_workflow_prints_selected_rows
100
403
  output = StringIO.new
101
404
  input = [
@@ -105,10 +408,10 @@ class TestCli < Minitest::Test
105
408
  "2",
106
409
  "3",
107
410
  "",
108
- "7"
411
+ "8"
109
412
  ].join("\n") + "\n"
110
413
 
111
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
414
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
112
415
 
113
416
  assert_equal 0, status
114
417
  assert_includes output.string, "name,city"
@@ -126,10 +429,10 @@ class TestCli < Minitest::Test
126
429
  "0",
127
430
  "3",
128
431
  "",
129
- "7"
432
+ "8"
130
433
  ].join("\n") + "\n"
131
434
 
132
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
435
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
133
436
 
134
437
  assert_equal 0, status
135
438
  assert_includes output.string, "Start row must be a positive integer."
@@ -145,10 +448,10 @@ class TestCli < Minitest::Test
145
448
  "2",
146
449
  "3",
147
450
  "",
148
- "7"
451
+ "8"
149
452
  ].join("\n") + "\n"
150
453
 
151
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
454
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
152
455
 
153
456
  assert_equal 0, status
154
457
  assert_includes output.string, "name,city"
@@ -166,10 +469,10 @@ class TestCli < Minitest::Test
166
469
  "2",
167
470
  "3",
168
471
  "",
169
- "7"
472
+ "8"
170
473
  ].join("\n") + "\n"
171
474
 
172
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
475
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
173
476
 
174
477
  assert_equal 0, status
175
478
  assert_includes output.string, "name,city"
@@ -191,10 +494,10 @@ class TestCli < Minitest::Test
191
494
  "3",
192
495
  "2",
193
496
  output_path,
194
- "7"
497
+ "8"
195
498
  ].join("\n") + "\n"
196
499
 
197
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
500
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
198
501
  assert_equal 0, status
199
502
  assert_equal "name,city\nBob,Paris\nCara,Berlin\n", File.read(output_path)
200
503
  end
@@ -211,10 +514,10 @@ class TestCli < Minitest::Test
211
514
  "1",
212
515
  "2",
213
516
  "",
214
- "7"
517
+ "8"
215
518
  ].join("\n") + "\n"
216
519
 
217
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
520
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
218
521
 
219
522
  assert_equal 0, status
220
523
  assert_includes output.string, "Alice,London"
@@ -231,10 +534,10 @@ class TestCli < Minitest::Test
231
534
  "",
232
535
  "",
233
536
  "",
234
- "7"
537
+ "8"
235
538
  ].join("\n") + "\n"
236
539
 
237
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
540
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
238
541
 
239
542
  assert_equal 0, status
240
543
  assert_includes output.string, "name,city"
@@ -256,10 +559,10 @@ class TestCli < Minitest::Test
256
559
  "",
257
560
  "2",
258
561
  output_path,
259
- "7"
562
+ "8"
260
563
  ].join("\n") + "\n"
261
564
 
262
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
565
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
263
566
 
264
567
  assert_equal 0, status
265
568
  assert_includes output.string, "Wrote output to #{output_path}"
@@ -278,10 +581,10 @@ class TestCli < Minitest::Test
278
581
  "",
279
582
  "",
280
583
  "",
281
- "7"
584
+ "8"
282
585
  ].join("\n") + "\n"
283
586
 
284
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
587
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
285
588
 
286
589
  assert_equal 0, status
287
590
  assert_includes output.string, "name\tcity"
@@ -297,10 +600,10 @@ class TestCli < Minitest::Test
297
600
  "n",
298
601
  "",
299
602
  "",
300
- "7"
603
+ "8"
301
604
  ].join("\n") + "\n"
302
605
 
303
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
606
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
304
607
 
305
608
  assert_equal 0, status
306
609
  refute_includes output.string, "name,city"
@@ -317,10 +620,10 @@ class TestCli < Minitest::Test
317
620
  "",
318
621
  "",
319
622
  "abc",
320
- "7"
623
+ "8"
321
624
  ].join("\n") + "\n"
322
625
 
323
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
626
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
324
627
 
325
628
  assert_equal 0, status
326
629
  assert_includes output.string, "Seed must be an integer."
@@ -342,10 +645,10 @@ class TestCli < Minitest::Test
342
645
  "",
343
646
  "",
344
647
  "",
345
- "7"
648
+ "8"
346
649
  ].join("\n") + "\n"
347
650
 
348
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
651
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
349
652
 
350
653
  assert_equal 0, status
351
654
  assert_includes output.string, "Reference CSV file path:"
@@ -354,7 +657,7 @@ class TestCli < Minitest::Test
354
657
  assert_includes output.string, "customer_id,name"
355
658
  assert_includes output.string, "1,Alice"
356
659
  assert_includes output.string, "3,Cara"
357
- assert_includes output.string, "Summary: source_rows=5 removed_rows=3 kept_rows=2"
660
+ assert_includes output.string, "Summary"
358
661
  end
359
662
 
360
663
  def test_dedupe_workflow_can_write_to_file
@@ -376,15 +679,15 @@ class TestCli < Minitest::Test
376
679
  "",
377
680
  "2",
378
681
  output_path,
379
- "7"
682
+ "8"
380
683
  ].join("\n") + "\n"
381
684
 
382
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
685
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
383
686
 
384
687
  assert_equal 0, status
385
688
  assert_includes output.string, "Wrote output to #{output_path}"
386
689
  assert_equal "customer_id,name\n1,Alice\n3,Cara\n", File.read(output_path)
387
- assert_includes output.string, "Summary: source_rows=5 removed_rows=3 kept_rows=2"
690
+ assert_includes output.string, "Summary"
388
691
  end
389
692
  end
390
693
 
@@ -403,10 +706,10 @@ class TestCli < Minitest::Test
403
706
  "",
404
707
  "",
405
708
  "",
406
- "7"
709
+ "8"
407
710
  ].join("\n") + "\n"
408
711
 
409
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
712
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
410
713
 
411
714
  assert_equal 0, status
412
715
  assert_includes output.string, "customer_id\tname"
@@ -429,16 +732,16 @@ class TestCli < Minitest::Test
429
732
  "",
430
733
  "",
431
734
  "",
432
- "7"
735
+ "8"
433
736
  ].join("\n") + "\n"
434
737
 
435
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
738
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
436
739
 
437
740
  assert_equal 0, status
438
741
  refute_includes output.string, "customer_id,name"
439
742
  assert_includes output.string, "1,Alice"
440
743
  assert_includes output.string, "3,Cara"
441
- assert_includes output.string, "Summary: source_rows=5 removed_rows=3 kept_rows=2"
744
+ assert_includes output.string, "Summary"
442
745
  end
443
746
 
444
747
  def test_parity_workflow_reports_match_and_returns_to_menu
@@ -449,16 +752,18 @@ class TestCli < Minitest::Test
449
752
  fixture_path("sample_people.csv"),
450
753
  "",
451
754
  "",
452
- "7"
755
+ "8"
453
756
  ].join("\n") + "\n"
454
757
 
455
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
758
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
456
759
 
457
760
  assert_equal 0, status
458
761
  assert_includes output.string, "Left CSV file path:"
459
762
  assert_includes output.string, "Right CSV file path:"
460
763
  assert_includes output.string, "MATCH"
461
- assert_includes output.string, "Summary: left_rows=3 right_rows=3 left_only=0 right_only=0"
764
+ assert_includes output.string, "Metric"
765
+ assert_includes output.string, "Left rows"
766
+ assert_includes output.string, "Right rows"
462
767
  assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
463
768
  end
464
769
 
@@ -470,14 +775,15 @@ class TestCli < Minitest::Test
470
775
  fixture_path("parity_people_reordered.tsv"),
471
776
  "2",
472
777
  "",
473
- "7"
778
+ "8"
474
779
  ].join("\n") + "\n"
475
780
 
476
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
781
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
477
782
 
478
783
  assert_equal 0, status
479
784
  assert_includes output.string, "MATCH"
480
- assert_includes output.string, "Summary: left_rows=3 right_rows=3 left_only=0 right_only=0"
785
+ assert_includes output.string, "Left rows"
786
+ assert_includes output.string, "Right rows"
481
787
  end
482
788
 
483
789
  def test_parity_workflow_headerless_mode_compares_all_rows
@@ -488,14 +794,15 @@ class TestCli < Minitest::Test
488
794
  fixture_path("sample_people_no_headers.csv"),
489
795
  "",
490
796
  "n",
491
- "7"
797
+ "8"
492
798
  ].join("\n") + "\n"
493
799
 
494
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
800
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
495
801
 
496
802
  assert_equal 0, status
497
803
  assert_includes output.string, "MATCH"
498
- assert_includes output.string, "Summary: left_rows=3 right_rows=3 left_only=0 right_only=0"
804
+ assert_includes output.string, "Left rows"
805
+ assert_includes output.string, "Right rows"
499
806
  end
500
807
 
501
808
  def test_parity_workflow_reports_header_mismatch_in_headered_mode
@@ -506,10 +813,10 @@ class TestCli < Minitest::Test
506
813
  fixture_path("parity_people_header_mismatch.csv"),
507
814
  "",
508
815
  "",
509
- "7"
816
+ "8"
510
817
  ].join("\n") + "\n"
511
818
 
512
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
819
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
513
820
 
514
821
  assert_equal 0, status
515
822
  assert_includes output.string, "CSV headers do not match."
@@ -524,14 +831,15 @@ class TestCli < Minitest::Test
524
831
  fixture_path("parity_people_mismatch.csv"),
525
832
  "",
526
833
  "",
527
- "7"
834
+ "8"
528
835
  ].join("\n") + "\n"
529
836
 
530
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
837
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
531
838
 
532
839
  assert_equal 0, status
533
840
  assert_includes output.string, "MISMATCH"
534
- assert_includes output.string, "Summary: left_rows=3 right_rows=3 left_only=1 right_only=1"
841
+ assert_includes output.string, "Left only"
842
+ assert_includes output.string, "Right only"
535
843
  assert_includes output.string, "Left-only examples:"
536
844
  assert_includes output.string, "Cara,Berlin (count +1)"
537
845
  assert_includes output.string, "Right-only examples:"
@@ -546,10 +854,10 @@ class TestCli < Minitest::Test
546
854
  fixture_path("sample_people.csv"),
547
855
  "",
548
856
  "",
549
- "7"
857
+ "8"
550
858
  ].join("\n") + "\n"
551
859
 
552
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
860
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
553
861
 
554
862
  assert_equal 0, status
555
863
  assert_includes output.string, "File not found: /tmp/not-there-left.csv"
@@ -565,10 +873,10 @@ class TestCli < Minitest::Test
565
873
  "/tmp/not-there-right.csv",
566
874
  "",
567
875
  "",
568
- "7"
876
+ "8"
569
877
  ].join("\n") + "\n"
570
878
 
571
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
879
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
572
880
 
573
881
  assert_equal 0, status
574
882
  assert_includes output.string, "File not found: /tmp/not-there-right.csv"
@@ -584,10 +892,10 @@ class TestCli < Minitest::Test
584
892
  fixture_path("sample_people_bad_tail.csv"),
585
893
  "",
586
894
  "",
587
- "7"
895
+ "8"
588
896
  ].join("\n") + "\n"
589
897
 
590
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
898
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
591
899
 
592
900
  assert_equal 0, status
593
901
  assert_includes output.string, "Could not parse CSV file."
@@ -611,10 +919,10 @@ class TestCli < Minitest::Test
611
919
  "y",
612
920
  "2",
613
921
  output_path,
614
- "7"
922
+ "8"
615
923
  ].join("\n") + "\n"
616
924
 
617
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
925
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
618
926
  assert_equal 0, status
619
927
  assert_equal "name\nAlice\nBob\nCara\n", File.read(output_path)
620
928
  end
@@ -631,11 +939,11 @@ class TestCli < Minitest::Test
631
939
  "1",
632
940
  "",
633
941
  "n",
634
- "7"
942
+ "8"
635
943
  ].join("\n") + "\n"
636
944
 
637
945
  output = StringIO.new
638
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
946
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
639
947
 
640
948
  assert_equal 0, status
641
949
  assert_includes output.string, "Canceled."
@@ -648,7 +956,7 @@ class TestCli < Minitest::Test
648
956
  ["menu"],
649
957
  stdin: StringIO.new("1\n/tmp/does-not-exist.csv\n4\n7\n"),
650
958
  stdout: output,
651
- stderr: StringIO.new
959
+ stderr: output
652
960
  )
653
961
 
654
962
  assert_equal 0, status
@@ -667,11 +975,11 @@ class TestCli < Minitest::Test
667
975
  "y",
668
976
  "2",
669
977
  "/tmp/not-a-dir/out.csv",
670
- "7"
978
+ "8"
671
979
  ].join("\n") + "\n"
672
980
 
673
981
  output = StringIO.new
674
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
982
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: output)
675
983
 
676
984
  assert_equal 0, status
677
985
  assert_includes output.string, "Cannot write output file: /tmp/not-a-dir/out.csv"