csvops 0.1.0.alpha → 0.3.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -10
  3. data/docs/release-v0.2.0-alpha.md +80 -0
  4. data/docs/release-v0.3.0-alpha.md +74 -0
  5. data/lib/csvtool/application/use_cases/run_extraction.rb +17 -17
  6. data/lib/csvtool/application/use_cases/run_row_extraction.rb +111 -0
  7. data/lib/csvtool/application/use_cases/run_row_randomization.rb +105 -0
  8. data/lib/csvtool/cli.rb +10 -2
  9. data/lib/csvtool/domain/{extraction_session → column_session}/column_selection.rb +1 -1
  10. data/lib/csvtool/domain/{extraction_session/extraction_session.rb → column_session/column_session.rb} +2 -2
  11. data/lib/csvtool/domain/{extraction_session → column_session}/csv_source.rb +1 -1
  12. data/lib/csvtool/domain/{extraction_session → column_session}/extraction_options.rb +1 -1
  13. data/lib/csvtool/domain/{extraction_session → column_session}/extraction_value.rb +1 -1
  14. data/lib/csvtool/domain/{extraction_session → column_session}/output_destination.rb +1 -1
  15. data/lib/csvtool/domain/{extraction_session → column_session}/preview.rb +1 -1
  16. data/lib/csvtool/domain/{extraction_session → column_session}/separator.rb +1 -1
  17. data/lib/csvtool/domain/row_randomization_session/randomization_options.rb +17 -0
  18. data/lib/csvtool/domain/row_randomization_session/randomization_output_destination.rb +31 -0
  19. data/lib/csvtool/domain/row_randomization_session/randomization_session.rb +25 -0
  20. data/lib/csvtool/domain/row_randomization_session/randomization_source.rb +23 -0
  21. data/lib/csvtool/domain/row_session/row_output_destination.rb +31 -0
  22. data/lib/csvtool/domain/row_session/row_range.rb +39 -0
  23. data/lib/csvtool/domain/row_session/row_session.rb +25 -0
  24. data/lib/csvtool/domain/row_session/row_source.rb +16 -0
  25. data/lib/csvtool/infrastructure/csv/row_randomizer.rb +83 -0
  26. data/lib/csvtool/infrastructure/csv/row_streamer.rb +27 -0
  27. data/lib/csvtool/infrastructure/output/csv_row_console_writer.rb +34 -0
  28. data/lib/csvtool/infrastructure/output/csv_row_file_writer.rb +45 -0
  29. data/lib/csvtool/interface/cli/errors/presenter.rb +20 -0
  30. data/lib/csvtool/interface/cli/menu_loop.rb +13 -5
  31. data/lib/csvtool/interface/cli/prompts/headers_present_prompt.rb +22 -0
  32. data/lib/csvtool/interface/cli/prompts/seed_prompt.rb +29 -0
  33. data/lib/csvtool/version.rb +1 -1
  34. data/test/csvtool/application/use_cases/run_row_extraction_test.rb +140 -0
  35. data/test/csvtool/application/use_cases/run_row_randomization_test.rb +124 -0
  36. data/test/csvtool/cli_test.rb +237 -6
  37. data/test/csvtool/cli_unit_test.rb +24 -1
  38. data/test/csvtool/domain/{extraction_session → column_session}/column_selection_test.rb +2 -2
  39. data/test/csvtool/domain/column_session/column_session_test.rb +35 -0
  40. data/test/csvtool/domain/column_session/csv_source_test.rb +14 -0
  41. data/test/csvtool/domain/{extraction_session → column_session}/extraction_options_test.rb +3 -3
  42. data/test/csvtool/domain/{extraction_session → column_session}/extraction_value_test.rb +2 -2
  43. data/test/csvtool/domain/{extraction_session → column_session}/output_destination_test.rb +3 -3
  44. data/test/csvtool/domain/column_session/preview_test.rb +18 -0
  45. data/test/csvtool/domain/{extraction_session → column_session}/separator_test.rb +3 -3
  46. data/test/csvtool/domain/row_randomization_session/randomization_options_test.rb +20 -0
  47. data/test/csvtool/domain/row_randomization_session/randomization_output_destination_test.rb +21 -0
  48. data/test/csvtool/domain/row_randomization_session/randomization_session_test.rb +26 -0
  49. data/test/csvtool/domain/row_randomization_session/randomization_source_test.rb +28 -0
  50. data/test/csvtool/domain/row_session/row_output_destination_test.rb +23 -0
  51. data/test/csvtool/domain/row_session/row_range_test.rb +30 -0
  52. data/test/csvtool/domain/row_session/row_session_test.rb +22 -0
  53. data/test/csvtool/domain/row_session/row_source_test.rb +12 -0
  54. data/test/csvtool/infrastructure/csv/row_randomizer_test.rb +37 -0
  55. data/test/csvtool/infrastructure/csv/row_streamer_test.rb +41 -0
  56. data/test/csvtool/infrastructure/output/csv_row_console_writer_test.rb +24 -0
  57. data/test/csvtool/infrastructure/output/csv_row_file_writer_test.rb +40 -0
  58. data/test/csvtool/interface/cli/errors/presenter_test.rb +10 -0
  59. data/test/csvtool/interface/cli/menu_loop_test.rb +68 -12
  60. data/test/csvtool/interface/cli/prompts/headers_present_prompt_test.rb +14 -0
  61. data/test/csvtool/interface/cli/prompts/seed_prompt_test.rb +39 -0
  62. data/test/fixtures/sample_people_bad_tail.csv +5 -0
  63. data/test/fixtures/sample_people_no_headers.csv +3 -0
  64. metadata +53 -17
  65. data/test/csvtool/domain/extraction_session/csv_source_test.rb +0 -14
  66. data/test/csvtool/domain/extraction_session/extraction_session_test.rb +0 -35
  67. data/test/csvtool/domain/extraction_session/preview_test.rb +0 -18
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../test_helper"
4
+ require "csvtool/application/use_cases/run_row_extraction"
5
+ require "tmpdir"
6
+
7
+ class RunRowExtractionTest < Minitest::Test
8
+ def test_use_case_prints_selected_row_range_with_header
9
+ out = StringIO.new
10
+ fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
11
+ input = [fixture, "", "2", "3", ""].join("\n") + "\n"
12
+
13
+ use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
14
+ use_case.call
15
+
16
+ assert_includes out.string, "name,city"
17
+ assert_includes out.string, "Bob,Paris"
18
+ assert_includes out.string, "Cara,Berlin"
19
+ refute_includes out.string, "Alice,London"
20
+ end
21
+
22
+ def test_rejects_non_numeric_start_row
23
+ out = StringIO.new
24
+ fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
25
+ input = [fixture, "", "abc", "3", ""].join("\n") + "\n"
26
+
27
+ use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
28
+ use_case.call
29
+
30
+ assert_includes out.string, "Start row must be a positive integer."
31
+ refute_includes out.string, "name,city"
32
+ end
33
+
34
+ def test_rejects_non_numeric_end_row
35
+ out = StringIO.new
36
+ fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
37
+ input = [fixture, "", "1", "xyz", ""].join("\n") + "\n"
38
+
39
+ use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
40
+ use_case.call
41
+
42
+ assert_includes out.string, "End row must be a positive integer."
43
+ refute_includes out.string, "name,city"
44
+ end
45
+
46
+ def test_rejects_end_before_start
47
+ out = StringIO.new
48
+ fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
49
+ input = [fixture, "", "3", "2", ""].join("\n") + "\n"
50
+
51
+ use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
52
+ use_case.call
53
+
54
+ assert_includes out.string, "End row must be greater than or equal to start row."
55
+ refute_includes out.string, "name,city"
56
+ end
57
+
58
+ def test_handles_out_of_bounds_start_row
59
+ out = StringIO.new
60
+ fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
61
+ input = [fixture, "", "10", "12", ""].join("\n") + "\n"
62
+
63
+ use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
64
+ use_case.call
65
+
66
+ assert_includes out.string, "Row range is out of bounds. File has 3 data rows."
67
+ refute_includes out.string, "name,city"
68
+ end
69
+
70
+ def test_use_case_supports_tsv_separator
71
+ out = StringIO.new
72
+ fixture = File.expand_path("../../../fixtures/sample_people.tsv", __dir__)
73
+ input = [fixture, "2", "2", "3", ""].join("\n") + "\n"
74
+
75
+ use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
76
+ use_case.call
77
+
78
+ assert_includes out.string, "name,city"
79
+ assert_includes out.string, "Bob,Paris"
80
+ assert_includes out.string, "Cara,Berlin"
81
+ end
82
+
83
+ def test_use_case_supports_custom_separator
84
+ out = StringIO.new
85
+ fixture = File.expand_path("../../../fixtures/sample_people_colon.txt", __dir__)
86
+ input = [fixture, "5", ":", "2", "3", ""].join("\n") + "\n"
87
+
88
+ use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
89
+ use_case.call
90
+
91
+ assert_includes out.string, "name,city"
92
+ assert_includes out.string, "Bob,Paris"
93
+ assert_includes out.string, "Cara,Berlin"
94
+ end
95
+
96
+ def test_use_case_can_write_selected_rows_to_file
97
+ out = StringIO.new
98
+ fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
99
+
100
+ Dir.mktmpdir do |dir|
101
+ output_path = File.join(dir, "rows.csv")
102
+ input = [fixture, "", "2", "3", "2", output_path].join("\n") + "\n"
103
+
104
+ use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
105
+ use_case.call
106
+
107
+ assert_equal "name,city\nBob,Paris\nCara,Berlin\n", File.read(output_path)
108
+ assert_includes out.string, "Wrote output to #{output_path}"
109
+ end
110
+ end
111
+
112
+ def test_stops_parsing_after_end_row_for_console_output
113
+ out = StringIO.new
114
+ fixture = File.expand_path("../../../fixtures/sample_people_bad_tail.csv", __dir__)
115
+ input = [fixture, "", "1", "2", ""].join("\n") + "\n"
116
+
117
+ use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
118
+ use_case.call
119
+
120
+ assert_includes out.string, "name,city"
121
+ assert_includes out.string, "Alice,London"
122
+ assert_includes out.string, "Bob,Paris"
123
+ refute_includes out.string, "Could not parse CSV file."
124
+ end
125
+
126
+ def test_out_of_bounds_file_mode_reports_error
127
+ out = StringIO.new
128
+ fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
129
+
130
+ Dir.mktmpdir do |dir|
131
+ output_path = File.join(dir, "rows.csv")
132
+ input = [fixture, "", "10", "12", "2", output_path].join("\n") + "\n"
133
+
134
+ use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
135
+ use_case.call
136
+ end
137
+
138
+ assert_includes out.string, "Row range is out of bounds. File has 3 data rows."
139
+ end
140
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../test_helper"
4
+ require "csvtool/application/use_cases/run_row_randomization"
5
+ require "tmpdir"
6
+
7
+ class RunRowRandomizationTest < Minitest::Test
8
+ def test_prints_header_then_all_randomized_rows
9
+ fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
10
+ output = StringIO.new
11
+ input = StringIO.new("#{fixture}\n\n\n\n\n")
12
+
13
+ Csvtool::Application::UseCases::RunRowRandomization.new(stdin: input, stdout: output).call
14
+
15
+ assert_includes output.string, "CSV file path:"
16
+ header_index = output.string.index("name,city")
17
+ assert header_index
18
+ %w[Alice,London Bob,Paris Cara,Berlin].each do |row|
19
+ row_index = output.string.index(row)
20
+ assert row_index
21
+ assert_operator header_index, :<, row_index
22
+ end
23
+ end
24
+
25
+ def test_missing_file_shows_friendly_error
26
+ output = StringIO.new
27
+ input = StringIO.new("/tmp/does-not-exist.csv\n")
28
+
29
+ Csvtool::Application::UseCases::RunRowRandomization.new(stdin: input, stdout: output).call
30
+
31
+ assert_includes output.string, "File not found: /tmp/does-not-exist.csv"
32
+ end
33
+
34
+ def test_can_write_randomized_rows_to_file
35
+ fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
36
+ output = StringIO.new
37
+
38
+ Dir.mktmpdir do |dir|
39
+ output_path = File.join(dir, "randomized.csv")
40
+ input = StringIO.new("#{fixture}\n\n\n\n2\n#{output_path}\n")
41
+
42
+ Csvtool::Application::UseCases::RunRowRandomization.new(stdin: input, stdout: output).call
43
+
44
+ written = File.read(output_path).lines.map(&:strip)
45
+ assert_equal "name,city", written.first
46
+ assert_equal ["Alice,London", "Bob,Paris", "Cara,Berlin"].sort, written[1..].sort
47
+ assert_includes output.string, "Wrote output to #{output_path}"
48
+ end
49
+ end
50
+
51
+ def test_supports_tsv_separator
52
+ fixture = File.expand_path("../../../fixtures/sample_people.tsv", __dir__)
53
+ output = StringIO.new
54
+ input = StringIO.new("#{fixture}\n2\n\n\n\n")
55
+
56
+ Csvtool::Application::UseCases::RunRowRandomization.new(stdin: input, stdout: output).call
57
+
58
+ assert_includes output.string, "name\tcity"
59
+ assert_includes output.string, "Alice\tLondon"
60
+ assert_includes output.string, "Bob\tParis"
61
+ assert_includes output.string, "Cara\tBerlin"
62
+ end
63
+
64
+ def test_supports_custom_separator
65
+ fixture = File.expand_path("../../../fixtures/sample_people_colon.txt", __dir__)
66
+ output = StringIO.new
67
+ input = StringIO.new("#{fixture}\n5\n:\n\n\n\n")
68
+
69
+ Csvtool::Application::UseCases::RunRowRandomization.new(stdin: input, stdout: output).call
70
+
71
+ assert_includes output.string, "name:city"
72
+ assert_includes output.string, "Alice:London"
73
+ assert_includes output.string, "Bob:Paris"
74
+ assert_includes output.string, "Cara:Berlin"
75
+ end
76
+
77
+ def test_headerless_mode_randomizes_all_rows
78
+ fixture = File.expand_path("../../../fixtures/sample_people_no_headers.csv", __dir__)
79
+ output = StringIO.new
80
+ input = StringIO.new("#{fixture}\n\nn\n\n\n")
81
+
82
+ Csvtool::Application::UseCases::RunRowRandomization.new(stdin: input, stdout: output).call
83
+
84
+ refute_includes output.string, "name,city"
85
+ assert_includes output.string, "Alice,London"
86
+ assert_includes output.string, "Bob,Paris"
87
+ assert_includes output.string, "Cara,Berlin"
88
+ end
89
+
90
+ def test_same_seed_produces_same_output_order
91
+ fixture = File.expand_path("../../../fixtures/sample_people_many.csv", __dir__)
92
+ input_data = "#{fixture}\n\n\n123\n\n"
93
+
94
+ out1 = StringIO.new
95
+ out2 = StringIO.new
96
+
97
+ Csvtool::Application::UseCases::RunRowRandomization.new(stdin: StringIO.new(input_data), stdout: out1).call
98
+ Csvtool::Application::UseCases::RunRowRandomization.new(stdin: StringIO.new(input_data), stdout: out2).call
99
+
100
+ rows1 = out1.string.lines.map(&:strip).select { |line| line.include?(",") && !line.start_with?("name,city") }
101
+ rows2 = out2.string.lines.map(&:strip).select { |line| line.include?(",") && !line.start_with?("name,city") }
102
+ assert_equal rows1, rows2
103
+ end
104
+
105
+ def test_invalid_seed_shows_friendly_error
106
+ fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
107
+ output = StringIO.new
108
+ input = StringIO.new("#{fixture}\n\n\nabc\n")
109
+
110
+ Csvtool::Application::UseCases::RunRowRandomization.new(stdin: input, stdout: output).call
111
+
112
+ assert_includes output.string, "Seed must be an integer."
113
+ end
114
+
115
+ def test_malformed_csv_shows_friendly_error
116
+ fixture = File.expand_path("../../../fixtures/sample_people_bad_tail.csv", __dir__)
117
+ output = StringIO.new
118
+ input = StringIO.new("#{fixture}\n\n\n\n\n")
119
+
120
+ Csvtool::Application::UseCases::RunRowRandomization.new(stdin: input, stdout: output).call
121
+
122
+ assert_includes output.string, "Could not parse CSV file."
123
+ end
124
+ end
@@ -11,7 +11,7 @@ class TestCli < Minitest::Test
11
11
 
12
12
  def test_menu_can_exit_cleanly
13
13
  output = StringIO.new
14
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("2\n"), stdout: output, stderr: StringIO.new)
14
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("4\n"), stdout: output, stderr: StringIO.new)
15
15
  assert_equal 0, status
16
16
  assert_includes output.string, "CSV Tool Menu"
17
17
  end
@@ -26,7 +26,7 @@ class TestCli < Minitest::Test
26
26
  "",
27
27
  "y",
28
28
  "",
29
- "2"
29
+ "4"
30
30
  ].join("\n") + "\n"
31
31
 
32
32
  output = StringIO.new
@@ -49,6 +49,237 @@ class TestCli < Minitest::Test
49
49
  assert_equal "Alice\nBob\nCara\n", output.string
50
50
  end
51
51
 
52
+ def test_row_range_workflow_prints_selected_rows
53
+ output = StringIO.new
54
+ input = [
55
+ "2",
56
+ fixture_path("sample_people.csv"),
57
+ "",
58
+ "2",
59
+ "3",
60
+ "",
61
+ "4"
62
+ ].join("\n") + "\n"
63
+
64
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
65
+
66
+ assert_equal 0, status
67
+ assert_includes output.string, "name,city"
68
+ assert_includes output.string, "Bob,Paris"
69
+ assert_includes output.string, "Cara,Berlin"
70
+ refute_includes output.string, "Alice,London"
71
+ end
72
+
73
+ def test_row_range_invalid_inputs_return_to_menu
74
+ output = StringIO.new
75
+ input = [
76
+ "2",
77
+ fixture_path("sample_people.csv"),
78
+ "",
79
+ "0",
80
+ "3",
81
+ "",
82
+ "4"
83
+ ].join("\n") + "\n"
84
+
85
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
86
+
87
+ assert_equal 0, status
88
+ assert_includes output.string, "Start row must be a positive integer."
89
+ assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
90
+ end
91
+
92
+ def test_row_range_workflow_supports_tsv_separator
93
+ output = StringIO.new
94
+ input = [
95
+ "2",
96
+ fixture_path("sample_people.tsv"),
97
+ "2",
98
+ "2",
99
+ "3",
100
+ "",
101
+ "4"
102
+ ].join("\n") + "\n"
103
+
104
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
105
+
106
+ assert_equal 0, status
107
+ assert_includes output.string, "name,city"
108
+ assert_includes output.string, "Bob,Paris"
109
+ assert_includes output.string, "Cara,Berlin"
110
+ end
111
+
112
+ def test_row_range_workflow_supports_custom_separator
113
+ output = StringIO.new
114
+ input = [
115
+ "2",
116
+ fixture_path("sample_people_colon.txt"),
117
+ "5",
118
+ ":",
119
+ "2",
120
+ "3",
121
+ "",
122
+ "4"
123
+ ].join("\n") + "\n"
124
+
125
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
126
+
127
+ assert_equal 0, status
128
+ assert_includes output.string, "name,city"
129
+ assert_includes output.string, "Bob,Paris"
130
+ assert_includes output.string, "Cara,Berlin"
131
+ end
132
+
133
+ def test_row_range_workflow_can_write_selected_rows_to_file
134
+ output = StringIO.new
135
+ output_path = nil
136
+
137
+ Dir.mktmpdir do |dir|
138
+ output_path = File.join(dir, "row_range.csv")
139
+ input = [
140
+ "2",
141
+ fixture_path("sample_people.csv"),
142
+ "",
143
+ "2",
144
+ "3",
145
+ "2",
146
+ output_path,
147
+ "4"
148
+ ].join("\n") + "\n"
149
+
150
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
151
+ assert_equal 0, status
152
+ assert_equal "name,city\nBob,Paris\nCara,Berlin\n", File.read(output_path)
153
+ end
154
+
155
+ assert_includes output.string, "Wrote output to #{output_path}"
156
+ end
157
+
158
+ def test_row_range_workflow_stops_before_malformed_tail
159
+ output = StringIO.new
160
+ input = [
161
+ "2",
162
+ fixture_path("sample_people_bad_tail.csv"),
163
+ "",
164
+ "1",
165
+ "2",
166
+ "",
167
+ "4"
168
+ ].join("\n") + "\n"
169
+
170
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
171
+
172
+ assert_equal 0, status
173
+ assert_includes output.string, "Alice,London"
174
+ assert_includes output.string, "Bob,Paris"
175
+ refute_includes output.string, "Could not parse CSV file."
176
+ end
177
+
178
+ def test_randomize_rows_workflow_prints_header_and_all_data_rows
179
+ output = StringIO.new
180
+ input = [
181
+ "3",
182
+ fixture_path("sample_people.csv"),
183
+ "",
184
+ "",
185
+ "",
186
+ "",
187
+ "4"
188
+ ].join("\n") + "\n"
189
+
190
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
191
+
192
+ assert_equal 0, status
193
+ assert_includes output.string, "name,city"
194
+ assert_includes output.string, "Alice,London"
195
+ assert_includes output.string, "Bob,Paris"
196
+ assert_includes output.string, "Cara,Berlin"
197
+ end
198
+
199
+ def test_randomize_rows_workflow_can_write_to_file
200
+ output = StringIO.new
201
+
202
+ Dir.mktmpdir do |dir|
203
+ output_path = File.join(dir, "randomized_rows.csv")
204
+ input = [
205
+ "3",
206
+ fixture_path("sample_people.csv"),
207
+ "",
208
+ "",
209
+ "",
210
+ "2",
211
+ output_path,
212
+ "4"
213
+ ].join("\n") + "\n"
214
+
215
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
216
+
217
+ assert_equal 0, status
218
+ assert_includes output.string, "Wrote output to #{output_path}"
219
+ lines = File.read(output_path).lines.map(&:strip)
220
+ assert_equal "name,city", lines.first
221
+ assert_equal ["Alice,London", "Bob,Paris", "Cara,Berlin"].sort, lines[1..].sort
222
+ end
223
+ end
224
+
225
+ def test_randomize_rows_workflow_supports_tsv_separator
226
+ output = StringIO.new
227
+ input = [
228
+ "3",
229
+ fixture_path("sample_people.tsv"),
230
+ "2",
231
+ "",
232
+ "",
233
+ "",
234
+ "4"
235
+ ].join("\n") + "\n"
236
+
237
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
238
+
239
+ assert_equal 0, status
240
+ assert_includes output.string, "name\tcity"
241
+ assert_includes output.string, "Alice\tLondon"
242
+ end
243
+
244
+ def test_randomize_rows_workflow_headerless_mode_randomizes_all_rows
245
+ output = StringIO.new
246
+ input = [
247
+ "3",
248
+ fixture_path("sample_people_no_headers.csv"),
249
+ "",
250
+ "n",
251
+ "",
252
+ "",
253
+ "4"
254
+ ].join("\n") + "\n"
255
+
256
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
257
+
258
+ assert_equal 0, status
259
+ refute_includes output.string, "name,city"
260
+ assert_includes output.string, "Alice,London"
261
+ assert_includes output.string, "Bob,Paris"
262
+ assert_includes output.string, "Cara,Berlin"
263
+ end
264
+
265
+ def test_randomize_rows_invalid_seed_returns_to_menu
266
+ output = StringIO.new
267
+ input = [
268
+ "3",
269
+ fixture_path("sample_people.csv"),
270
+ "",
271
+ "",
272
+ "abc",
273
+ "4"
274
+ ].join("\n") + "\n"
275
+
276
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
277
+
278
+ assert_equal 0, status
279
+ assert_includes output.string, "Seed must be an integer."
280
+ assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
281
+ end
282
+
52
283
  def test_end_to_end_file_output_writes_expected_csv
53
284
  output = StringIO.new
54
285
  output_path = nil
@@ -65,7 +296,7 @@ class TestCli < Minitest::Test
65
296
  "y",
66
297
  "2",
67
298
  output_path,
68
- "2"
299
+ "4"
69
300
  ].join("\n") + "\n"
70
301
 
71
302
  status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
@@ -85,7 +316,7 @@ class TestCli < Minitest::Test
85
316
  "1",
86
317
  "",
87
318
  "n",
88
- "2"
319
+ "4"
89
320
  ].join("\n") + "\n"
90
321
 
91
322
  output = StringIO.new
@@ -100,7 +331,7 @@ class TestCli < Minitest::Test
100
331
  output = StringIO.new
101
332
  status = Csvtool::CLI.start(
102
333
  ["menu"],
103
- stdin: StringIO.new("1\n/tmp/does-not-exist.csv\n2\n"),
334
+ stdin: StringIO.new("1\n/tmp/does-not-exist.csv\n4\n"),
104
335
  stdout: output,
105
336
  stderr: StringIO.new
106
337
  )
@@ -121,7 +352,7 @@ class TestCli < Minitest::Test
121
352
  "y",
122
353
  "2",
123
354
  "/tmp/not-a-dir/out.csv",
124
- "2"
355
+ "4"
125
356
  ].join("\n") + "\n"
126
357
 
127
358
  output = StringIO.new
@@ -16,7 +16,7 @@ class CliUnitTest < Minitest::Test
16
16
  end
17
17
 
18
18
  def test_menu_command_can_exit_zero
19
- status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("2\n"), stdout: StringIO.new, stderr: StringIO.new)
19
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("4\n"), stdout: StringIO.new, stderr: StringIO.new)
20
20
  assert_equal 0, status
21
21
  end
22
22
 
@@ -24,4 +24,27 @@ class CliUnitTest < Minitest::Test
24
24
  status = Csvtool::CLI.start(["column"], stdin: StringIO.new, stdout: StringIO.new, stderr: StringIO.new)
25
25
  assert_equal 1, status
26
26
  end
27
+
28
+ def test_menu_routes_to_row_range_shell
29
+ stdout = StringIO.new
30
+ fixture = File.expand_path("../fixtures/sample_people.csv", __dir__)
31
+ input = ["2", fixture, "", "2", "3", "", "4"].join("\n") + "\n"
32
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: stdout, stderr: StringIO.new)
33
+ assert_equal 0, status
34
+ assert_includes stdout.string, "name,city"
35
+ assert_includes stdout.string, "Bob,Paris"
36
+ assert_includes stdout.string, "Cara,Berlin"
37
+ end
38
+
39
+ def test_menu_routes_to_randomize_rows_shell
40
+ stdout = StringIO.new
41
+ fixture = File.expand_path("../fixtures/sample_people.csv", __dir__)
42
+ input = ["3", fixture, "", "", "", "", "4"].join("\n") + "\n"
43
+ status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: stdout, stderr: StringIO.new)
44
+ assert_equal 0, status
45
+ assert_includes stdout.string, "name,city"
46
+ assert_includes stdout.string, "Alice,London"
47
+ assert_includes stdout.string, "Bob,Paris"
48
+ assert_includes stdout.string, "Cara,Berlin"
49
+ end
27
50
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../../test_helper"
4
- require "csvtool/domain/extraction_session/column_selection"
4
+ require "csvtool/domain/column_session/column_selection"
5
5
 
6
6
  class ColumnSelectionTest < Minitest::Test
7
7
  def test_stores_name
8
- selection = Csvtool::Domain::ExtractionSession::ColumnSelection.new(name: "city")
8
+ selection = Csvtool::Domain::ColumnSession::ColumnSelection.new(name: "city")
9
9
  assert_equal "city", selection.name
10
10
  end
11
11
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../test_helper"
4
+ require "csvtool/domain/column_session/column_session"
5
+ require "csvtool/domain/column_session/csv_source"
6
+ require "csvtool/domain/column_session/separator"
7
+ require "csvtool/domain/column_session/column_selection"
8
+ require "csvtool/domain/column_session/extraction_options"
9
+ require "csvtool/domain/column_session/preview"
10
+ require "csvtool/domain/column_session/extraction_value"
11
+ require "csvtool/domain/column_session/output_destination"
12
+
13
+ class ColumnSessionTest < Minitest::Test
14
+ def test_state_transitions
15
+ session = Csvtool::Domain::ColumnSession::ColumnSession.start(
16
+ source: Csvtool::Domain::ColumnSession::CsvSource.new(
17
+ path: "/tmp/in.csv",
18
+ separator: Csvtool::Domain::ColumnSession::Separator.new(",")
19
+ ),
20
+ column_selection: Csvtool::Domain::ColumnSession::ColumnSelection.new(name: "name"),
21
+ options: Csvtool::Domain::ColumnSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 10)
22
+ )
23
+
24
+ preview = Csvtool::Domain::ColumnSession::Preview.new(
25
+ values: [Csvtool::Domain::ColumnSession::ExtractionValue.new("Alice")]
26
+ )
27
+ session = session.with_preview(preview).confirm!.with_output_destination(
28
+ Csvtool::Domain::ColumnSession::OutputDestination.console
29
+ )
30
+
31
+ assert_equal true, session.confirmed?
32
+ assert_equal preview, session.preview
33
+ assert_equal true, session.output_destination.console?
34
+ end
35
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../test_helper"
4
+ require "csvtool/domain/column_session/csv_source"
5
+ require "csvtool/domain/column_session/separator"
6
+
7
+ class CsvSourceTest < Minitest::Test
8
+ def test_stores_path_and_separator
9
+ separator = Csvtool::Domain::ColumnSession::Separator.new(",")
10
+ source = Csvtool::Domain::ColumnSession::CsvSource.new(path: "/tmp/a.csv", separator: separator)
11
+ assert_equal "/tmp/a.csv", source.path
12
+ assert_equal separator, source.separator
13
+ end
14
+ end
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../../test_helper"
4
- require "csvtool/domain/extraction_session/extraction_options"
4
+ require "csvtool/domain/column_session/extraction_options"
5
5
 
6
6
  class ExtractionOptionsTest < Minitest::Test
7
7
  def test_exposes_options
8
- options = Csvtool::Domain::ExtractionSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 10)
8
+ options = Csvtool::Domain::ColumnSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 10)
9
9
  assert_equal true, options.skip_blanks?
10
10
  assert_equal 10, options.preview_limit
11
11
  end
12
12
 
13
13
  def test_non_positive_preview_limit_raises
14
14
  assert_raises(ArgumentError) do
15
- Csvtool::Domain::ExtractionSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 0)
15
+ Csvtool::Domain::ColumnSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 0)
16
16
  end
17
17
  end
18
18
  end