csvops 0.2.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.
- checksums.yaml +4 -4
- data/README.md +39 -5
- data/docs/release-v0.3.0-alpha.md +74 -0
- data/lib/csvtool/application/use_cases/run_row_randomization.rb +105 -0
- data/lib/csvtool/cli.rb +5 -1
- data/lib/csvtool/domain/row_randomization_session/randomization_options.rb +17 -0
- data/lib/csvtool/domain/row_randomization_session/randomization_output_destination.rb +31 -0
- data/lib/csvtool/domain/row_randomization_session/randomization_session.rb +25 -0
- data/lib/csvtool/domain/row_randomization_session/randomization_source.rb +23 -0
- data/lib/csvtool/infrastructure/csv/row_randomizer.rb +83 -0
- data/lib/csvtool/interface/cli/errors/presenter.rb +4 -0
- data/lib/csvtool/interface/cli/menu_loop.rb +5 -2
- data/lib/csvtool/interface/cli/prompts/headers_present_prompt.rb +22 -0
- data/lib/csvtool/interface/cli/prompts/seed_prompt.rb +29 -0
- data/lib/csvtool/version.rb +1 -1
- data/test/csvtool/application/use_cases/run_row_randomization_test.rb +124 -0
- data/test/csvtool/cli_test.rb +117 -12
- data/test/csvtool/cli_unit_test.rb +14 -2
- data/test/csvtool/domain/row_randomization_session/randomization_options_test.rb +20 -0
- data/test/csvtool/domain/row_randomization_session/randomization_output_destination_test.rb +21 -0
- data/test/csvtool/domain/row_randomization_session/randomization_session_test.rb +26 -0
- data/test/csvtool/domain/row_randomization_session/randomization_source_test.rb +28 -0
- data/test/csvtool/infrastructure/csv/row_randomizer_test.rb +37 -0
- data/test/csvtool/interface/cli/errors/presenter_test.rb +2 -0
- data/test/csvtool/interface/cli/menu_loop_test.rb +41 -10
- data/test/csvtool/interface/cli/prompts/headers_present_prompt_test.rb +14 -0
- data/test/csvtool/interface/cli/prompts/seed_prompt_test.rb +39 -0
- data/test/fixtures/sample_people_no_headers.csv +3 -0
- metadata +19 -1
|
@@ -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
|
data/test/csvtool/cli_test.rb
CHANGED
|
@@ -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("
|
|
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
|
-
"
|
|
29
|
+
"4"
|
|
30
30
|
].join("\n") + "\n"
|
|
31
31
|
|
|
32
32
|
output = StringIO.new
|
|
@@ -58,7 +58,7 @@ class TestCli < Minitest::Test
|
|
|
58
58
|
"2",
|
|
59
59
|
"3",
|
|
60
60
|
"",
|
|
61
|
-
"
|
|
61
|
+
"4"
|
|
62
62
|
].join("\n") + "\n"
|
|
63
63
|
|
|
64
64
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -79,7 +79,7 @@ class TestCli < Minitest::Test
|
|
|
79
79
|
"0",
|
|
80
80
|
"3",
|
|
81
81
|
"",
|
|
82
|
-
"
|
|
82
|
+
"4"
|
|
83
83
|
].join("\n") + "\n"
|
|
84
84
|
|
|
85
85
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -98,7 +98,7 @@ class TestCli < Minitest::Test
|
|
|
98
98
|
"2",
|
|
99
99
|
"3",
|
|
100
100
|
"",
|
|
101
|
-
"
|
|
101
|
+
"4"
|
|
102
102
|
].join("\n") + "\n"
|
|
103
103
|
|
|
104
104
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -119,7 +119,7 @@ class TestCli < Minitest::Test
|
|
|
119
119
|
"2",
|
|
120
120
|
"3",
|
|
121
121
|
"",
|
|
122
|
-
"
|
|
122
|
+
"4"
|
|
123
123
|
].join("\n") + "\n"
|
|
124
124
|
|
|
125
125
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -144,7 +144,7 @@ class TestCli < Minitest::Test
|
|
|
144
144
|
"3",
|
|
145
145
|
"2",
|
|
146
146
|
output_path,
|
|
147
|
-
"
|
|
147
|
+
"4"
|
|
148
148
|
].join("\n") + "\n"
|
|
149
149
|
|
|
150
150
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -164,7 +164,7 @@ class TestCli < Minitest::Test
|
|
|
164
164
|
"1",
|
|
165
165
|
"2",
|
|
166
166
|
"",
|
|
167
|
-
"
|
|
167
|
+
"4"
|
|
168
168
|
].join("\n") + "\n"
|
|
169
169
|
|
|
170
170
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -175,6 +175,111 @@ class TestCli < Minitest::Test
|
|
|
175
175
|
refute_includes output.string, "Could not parse CSV file."
|
|
176
176
|
end
|
|
177
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
|
+
|
|
178
283
|
def test_end_to_end_file_output_writes_expected_csv
|
|
179
284
|
output = StringIO.new
|
|
180
285
|
output_path = nil
|
|
@@ -191,7 +296,7 @@ class TestCli < Minitest::Test
|
|
|
191
296
|
"y",
|
|
192
297
|
"2",
|
|
193
298
|
output_path,
|
|
194
|
-
"
|
|
299
|
+
"4"
|
|
195
300
|
].join("\n") + "\n"
|
|
196
301
|
|
|
197
302
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -211,7 +316,7 @@ class TestCli < Minitest::Test
|
|
|
211
316
|
"1",
|
|
212
317
|
"",
|
|
213
318
|
"n",
|
|
214
|
-
"
|
|
319
|
+
"4"
|
|
215
320
|
].join("\n") + "\n"
|
|
216
321
|
|
|
217
322
|
output = StringIO.new
|
|
@@ -226,7 +331,7 @@ class TestCli < Minitest::Test
|
|
|
226
331
|
output = StringIO.new
|
|
227
332
|
status = Csvtool::CLI.start(
|
|
228
333
|
["menu"],
|
|
229
|
-
stdin: StringIO.new("1\n/tmp/does-not-exist.csv\
|
|
334
|
+
stdin: StringIO.new("1\n/tmp/does-not-exist.csv\n4\n"),
|
|
230
335
|
stdout: output,
|
|
231
336
|
stderr: StringIO.new
|
|
232
337
|
)
|
|
@@ -247,7 +352,7 @@ class TestCli < Minitest::Test
|
|
|
247
352
|
"y",
|
|
248
353
|
"2",
|
|
249
354
|
"/tmp/not-a-dir/out.csv",
|
|
250
|
-
"
|
|
355
|
+
"4"
|
|
251
356
|
].join("\n") + "\n"
|
|
252
357
|
|
|
253
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("
|
|
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
|
|
|
@@ -28,11 +28,23 @@ class CliUnitTest < Minitest::Test
|
|
|
28
28
|
def test_menu_routes_to_row_range_shell
|
|
29
29
|
stdout = StringIO.new
|
|
30
30
|
fixture = File.expand_path("../fixtures/sample_people.csv", __dir__)
|
|
31
|
-
input = ["2", fixture, "", "2", "3", "", "
|
|
31
|
+
input = ["2", fixture, "", "2", "3", "", "4"].join("\n") + "\n"
|
|
32
32
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: stdout, stderr: StringIO.new)
|
|
33
33
|
assert_equal 0, status
|
|
34
34
|
assert_includes stdout.string, "name,city"
|
|
35
35
|
assert_includes stdout.string, "Bob,Paris"
|
|
36
36
|
assert_includes stdout.string, "Cara,Berlin"
|
|
37
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
|
|
38
50
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/row_randomization_session/randomization_options"
|
|
5
|
+
|
|
6
|
+
class RandomizationOptionsTest < Minitest::Test
|
|
7
|
+
def test_accepts_nil_or_integer_seed
|
|
8
|
+
with_seed = Csvtool::Domain::RowRandomizationSession::RandomizationOptions.new(seed: 42)
|
|
9
|
+
without_seed = Csvtool::Domain::RowRandomizationSession::RandomizationOptions.new(seed: nil)
|
|
10
|
+
|
|
11
|
+
assert_equal 42, with_seed.seed
|
|
12
|
+
assert_nil without_seed.seed
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_rejects_non_integer_seed
|
|
16
|
+
assert_raises(ArgumentError) do
|
|
17
|
+
Csvtool::Domain::RowRandomizationSession::RandomizationOptions.new(seed: "abc")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/row_randomization_session/randomization_output_destination"
|
|
5
|
+
|
|
6
|
+
class RandomizationOutputDestinationTest < Minitest::Test
|
|
7
|
+
def test_console_and_file_modes
|
|
8
|
+
console = Csvtool::Domain::RowRandomizationSession::RandomizationOutputDestination.console
|
|
9
|
+
file = Csvtool::Domain::RowRandomizationSession::RandomizationOutputDestination.file(path: "/tmp/out.csv")
|
|
10
|
+
|
|
11
|
+
assert_equal false, console.file?
|
|
12
|
+
assert_equal true, file.file?
|
|
13
|
+
assert_equal "/tmp/out.csv", file.path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_rejects_empty_file_path
|
|
17
|
+
assert_raises(ArgumentError) do
|
|
18
|
+
Csvtool::Domain::RowRandomizationSession::RandomizationOutputDestination.file(path: "")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/row_randomization_session/randomization_session"
|
|
5
|
+
require "csvtool/domain/row_randomization_session/randomization_source"
|
|
6
|
+
require "csvtool/domain/row_randomization_session/randomization_options"
|
|
7
|
+
require "csvtool/domain/row_randomization_session/randomization_output_destination"
|
|
8
|
+
|
|
9
|
+
class RandomizationSessionTest < Minitest::Test
|
|
10
|
+
def test_with_output_destination_returns_updated_session
|
|
11
|
+
source = Csvtool::Domain::RowRandomizationSession::RandomizationSource.new(
|
|
12
|
+
path: "/tmp/in.csv",
|
|
13
|
+
separator: ",",
|
|
14
|
+
headers_present: true
|
|
15
|
+
)
|
|
16
|
+
options = Csvtool::Domain::RowRandomizationSession::RandomizationOptions.new(seed: 7)
|
|
17
|
+
session = Csvtool::Domain::RowRandomizationSession::RandomizationSession.start(source: source, options: options)
|
|
18
|
+
destination = Csvtool::Domain::RowRandomizationSession::RandomizationOutputDestination.console
|
|
19
|
+
|
|
20
|
+
updated = session.with_output_destination(destination)
|
|
21
|
+
|
|
22
|
+
assert_equal source, updated.source
|
|
23
|
+
assert_equal options, updated.options
|
|
24
|
+
assert_equal destination, updated.output_destination
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/row_randomization_session/randomization_source"
|
|
5
|
+
|
|
6
|
+
class RandomizationSourceTest < Minitest::Test
|
|
7
|
+
def test_holds_path_separator_and_headers_mode
|
|
8
|
+
source = Csvtool::Domain::RowRandomizationSession::RandomizationSource.new(
|
|
9
|
+
path: "/tmp/a.csv",
|
|
10
|
+
separator: ",",
|
|
11
|
+
headers_present: true
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
assert_equal "/tmp/a.csv", source.path
|
|
15
|
+
assert_equal ",", source.separator
|
|
16
|
+
assert_equal true, source.headers_present?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_rejects_empty_separator
|
|
20
|
+
assert_raises(ArgumentError) do
|
|
21
|
+
Csvtool::Domain::RowRandomizationSession::RandomizationSource.new(
|
|
22
|
+
path: "/tmp/a.csv",
|
|
23
|
+
separator: "",
|
|
24
|
+
headers_present: true
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/infrastructure/csv/row_randomizer"
|
|
5
|
+
|
|
6
|
+
class InfrastructureRowRandomizerTest < Minitest::Test
|
|
7
|
+
def fixture_path(name)
|
|
8
|
+
File.expand_path("../../../fixtures/#{name}", __dir__)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_randomizes_rows_and_preserves_membership
|
|
12
|
+
randomizer = Csvtool::Infrastructure::CSV::RowRandomizer.new
|
|
13
|
+
|
|
14
|
+
rows = randomizer.call(file_path: fixture_path("sample_people.csv"), col_sep: ",", headers: true, seed: 1234)
|
|
15
|
+
|
|
16
|
+
assert_equal 3, rows.length
|
|
17
|
+
assert_equal [%w[Alice London], %w[Bob Paris], %w[Cara Berlin]].sort, rows.sort
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_same_seed_returns_same_order
|
|
21
|
+
randomizer = Csvtool::Infrastructure::CSV::RowRandomizer.new
|
|
22
|
+
|
|
23
|
+
one = randomizer.call(file_path: fixture_path("sample_people_many.csv"), col_sep: ",", headers: true, seed: 42)
|
|
24
|
+
two = randomizer.call(file_path: fixture_path("sample_people_many.csv"), col_sep: ",", headers: true, seed: 42)
|
|
25
|
+
|
|
26
|
+
assert_equal one, two
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_different_seed_changes_order
|
|
30
|
+
randomizer = Csvtool::Infrastructure::CSV::RowRandomizer.new
|
|
31
|
+
|
|
32
|
+
one = randomizer.call(file_path: fixture_path("sample_people_many.csv"), col_sep: ",", headers: true, seed: 42)
|
|
33
|
+
two = randomizer.call(file_path: fixture_path("sample_people_many.csv"), col_sep: ",", headers: true, seed: 43)
|
|
34
|
+
|
|
35
|
+
refute_equal one, two
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -18,6 +18,7 @@ class ErrorsPresenterTest < Minitest::Test
|
|
|
18
18
|
presenter.invalid_output_destination
|
|
19
19
|
presenter.empty_custom_separator
|
|
20
20
|
presenter.invalid_separator_choice
|
|
21
|
+
presenter.invalid_seed
|
|
21
22
|
presenter.canceled
|
|
22
23
|
presenter.invalid_start_row
|
|
23
24
|
presenter.invalid_end_row
|
|
@@ -35,6 +36,7 @@ class ErrorsPresenterTest < Minitest::Test
|
|
|
35
36
|
assert_includes text, "Invalid output destination."
|
|
36
37
|
assert_includes text, "Separator cannot be empty."
|
|
37
38
|
assert_includes text, "Invalid separator choice."
|
|
39
|
+
assert_includes text, "Seed must be an integer."
|
|
38
40
|
assert_includes text, "Canceled."
|
|
39
41
|
assert_includes text, "Start row must be a positive integer."
|
|
40
42
|
assert_includes text, "End row must be a positive integer."
|
|
@@ -19,13 +19,15 @@ class MenuLoopTest < Minitest::Test
|
|
|
19
19
|
def test_routes_extract_column_then_exit
|
|
20
20
|
column_action = FakeAction.new
|
|
21
21
|
rows_action = FakeAction.new
|
|
22
|
+
randomize_rows_action = FakeAction.new
|
|
22
23
|
stdout = StringIO.new
|
|
23
24
|
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
24
|
-
stdin: StringIO.new("1\
|
|
25
|
+
stdin: StringIO.new("1\n4\n"),
|
|
25
26
|
stdout: stdout,
|
|
26
|
-
menu_options: ["Extract column", "Extract rows (range)", "Exit"],
|
|
27
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Exit"],
|
|
27
28
|
extract_column_action: column_action,
|
|
28
|
-
extract_rows_action: rows_action
|
|
29
|
+
extract_rows_action: rows_action,
|
|
30
|
+
randomize_rows_action: randomize_rows_action
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
status = menu.run
|
|
@@ -33,19 +35,22 @@ class MenuLoopTest < Minitest::Test
|
|
|
33
35
|
assert_equal 0, status
|
|
34
36
|
assert_equal 1, column_action.runs
|
|
35
37
|
assert_equal 0, rows_action.runs
|
|
38
|
+
assert_equal 0, randomize_rows_action.runs
|
|
36
39
|
assert_includes stdout.string, "CSV Tool Menu"
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
def test_routes_extract_rows_then_exit
|
|
40
43
|
column_action = FakeAction.new
|
|
41
44
|
rows_action = FakeAction.new
|
|
45
|
+
randomize_rows_action = FakeAction.new
|
|
42
46
|
stdout = StringIO.new
|
|
43
47
|
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
44
|
-
stdin: StringIO.new("2\
|
|
48
|
+
stdin: StringIO.new("2\n4\n"),
|
|
45
49
|
stdout: stdout,
|
|
46
|
-
menu_options: ["Extract column", "Extract rows (range)", "Exit"],
|
|
50
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Exit"],
|
|
47
51
|
extract_column_action: column_action,
|
|
48
|
-
extract_rows_action: rows_action
|
|
52
|
+
extract_rows_action: rows_action,
|
|
53
|
+
randomize_rows_action: randomize_rows_action
|
|
49
54
|
)
|
|
50
55
|
|
|
51
56
|
status = menu.run
|
|
@@ -53,24 +58,50 @@ class MenuLoopTest < Minitest::Test
|
|
|
53
58
|
assert_equal 0, status
|
|
54
59
|
assert_equal 0, column_action.runs
|
|
55
60
|
assert_equal 1, rows_action.runs
|
|
61
|
+
assert_equal 0, randomize_rows_action.runs
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_routes_randomize_rows_then_exit
|
|
65
|
+
column_action = FakeAction.new
|
|
66
|
+
rows_action = FakeAction.new
|
|
67
|
+
randomize_rows_action = FakeAction.new
|
|
68
|
+
stdout = StringIO.new
|
|
69
|
+
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
70
|
+
stdin: StringIO.new("3\n4\n"),
|
|
71
|
+
stdout: stdout,
|
|
72
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Exit"],
|
|
73
|
+
extract_column_action: column_action,
|
|
74
|
+
extract_rows_action: rows_action,
|
|
75
|
+
randomize_rows_action: randomize_rows_action
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
status = menu.run
|
|
79
|
+
|
|
80
|
+
assert_equal 0, status
|
|
81
|
+
assert_equal 0, column_action.runs
|
|
82
|
+
assert_equal 0, rows_action.runs
|
|
83
|
+
assert_equal 1, randomize_rows_action.runs
|
|
56
84
|
end
|
|
57
85
|
|
|
58
86
|
def test_invalid_choice_shows_prompt
|
|
59
87
|
column_action = FakeAction.new
|
|
60
88
|
rows_action = FakeAction.new
|
|
89
|
+
randomize_rows_action = FakeAction.new
|
|
61
90
|
stdout = StringIO.new
|
|
62
91
|
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
63
|
-
stdin: StringIO.new("x\
|
|
92
|
+
stdin: StringIO.new("x\n4\n"),
|
|
64
93
|
stdout: stdout,
|
|
65
|
-
menu_options: ["Extract column", "Extract rows (range)", "Exit"],
|
|
94
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Exit"],
|
|
66
95
|
extract_column_action: column_action,
|
|
67
|
-
extract_rows_action: rows_action
|
|
96
|
+
extract_rows_action: rows_action,
|
|
97
|
+
randomize_rows_action: randomize_rows_action
|
|
68
98
|
)
|
|
69
99
|
|
|
70
100
|
menu.run
|
|
71
101
|
|
|
72
|
-
assert_includes stdout.string, "Please choose 1, 2, or
|
|
102
|
+
assert_includes stdout.string, "Please choose 1, 2, 3, or 4."
|
|
73
103
|
assert_equal 0, column_action.runs
|
|
74
104
|
assert_equal 0, rows_action.runs
|
|
105
|
+
assert_equal 0, randomize_rows_action.runs
|
|
75
106
|
end
|
|
76
107
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/prompts/headers_present_prompt"
|
|
5
|
+
|
|
6
|
+
class HeadersPresentPromptTest < Minitest::Test
|
|
7
|
+
def test_defaults_to_true_and_accepts_negative_inputs
|
|
8
|
+
yes_prompt = Csvtool::Interface::CLI::Prompts::HeadersPresentPrompt.new(stdin: StringIO.new("\n"), stdout: StringIO.new)
|
|
9
|
+
no_prompt = Csvtool::Interface::CLI::Prompts::HeadersPresentPrompt.new(stdin: StringIO.new("n\n"), stdout: StringIO.new)
|
|
10
|
+
|
|
11
|
+
assert_equal true, yes_prompt.call
|
|
12
|
+
assert_equal false, no_prompt.call
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/prompts/seed_prompt"
|
|
5
|
+
|
|
6
|
+
class SeedPromptTest < Minitest::Test
|
|
7
|
+
class FakeErrors
|
|
8
|
+
attr_reader :calls
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@calls = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def invalid_seed
|
|
15
|
+
@calls << :invalid_seed
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_blank_returns_nil
|
|
20
|
+
errors = FakeErrors.new
|
|
21
|
+
prompt = Csvtool::Interface::CLI::Prompts::SeedPrompt.new(stdin: StringIO.new("\n"), stdout: StringIO.new, errors: errors)
|
|
22
|
+
assert_nil prompt.call
|
|
23
|
+
assert_empty errors.calls
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_integer_returns_seed
|
|
27
|
+
errors = FakeErrors.new
|
|
28
|
+
prompt = Csvtool::Interface::CLI::Prompts::SeedPrompt.new(stdin: StringIO.new("42\n"), stdout: StringIO.new, errors: errors)
|
|
29
|
+
assert_equal 42, prompt.call
|
|
30
|
+
assert_empty errors.calls
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_invalid_reports_error
|
|
34
|
+
errors = FakeErrors.new
|
|
35
|
+
prompt = Csvtool::Interface::CLI::Prompts::SeedPrompt.new(stdin: StringIO.new("abc\n"), stdout: StringIO.new, errors: errors)
|
|
36
|
+
assert_equal Csvtool::Interface::CLI::Prompts::SeedPrompt::INVALID, prompt.call
|
|
37
|
+
assert_includes errors.calls, :invalid_seed
|
|
38
|
+
end
|
|
39
|
+
end
|