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.
- checksums.yaml +4 -4
- data/README.md +83 -10
- data/docs/release-v0.2.0-alpha.md +80 -0
- data/docs/release-v0.3.0-alpha.md +74 -0
- data/lib/csvtool/application/use_cases/run_extraction.rb +17 -17
- data/lib/csvtool/application/use_cases/run_row_extraction.rb +111 -0
- data/lib/csvtool/application/use_cases/run_row_randomization.rb +105 -0
- data/lib/csvtool/cli.rb +10 -2
- data/lib/csvtool/domain/{extraction_session → column_session}/column_selection.rb +1 -1
- data/lib/csvtool/domain/{extraction_session/extraction_session.rb → column_session/column_session.rb} +2 -2
- data/lib/csvtool/domain/{extraction_session → column_session}/csv_source.rb +1 -1
- data/lib/csvtool/domain/{extraction_session → column_session}/extraction_options.rb +1 -1
- data/lib/csvtool/domain/{extraction_session → column_session}/extraction_value.rb +1 -1
- data/lib/csvtool/domain/{extraction_session → column_session}/output_destination.rb +1 -1
- data/lib/csvtool/domain/{extraction_session → column_session}/preview.rb +1 -1
- data/lib/csvtool/domain/{extraction_session → column_session}/separator.rb +1 -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/domain/row_session/row_output_destination.rb +31 -0
- data/lib/csvtool/domain/row_session/row_range.rb +39 -0
- data/lib/csvtool/domain/row_session/row_session.rb +25 -0
- data/lib/csvtool/domain/row_session/row_source.rb +16 -0
- data/lib/csvtool/infrastructure/csv/row_randomizer.rb +83 -0
- data/lib/csvtool/infrastructure/csv/row_streamer.rb +27 -0
- data/lib/csvtool/infrastructure/output/csv_row_console_writer.rb +34 -0
- data/lib/csvtool/infrastructure/output/csv_row_file_writer.rb +45 -0
- data/lib/csvtool/interface/cli/errors/presenter.rb +20 -0
- data/lib/csvtool/interface/cli/menu_loop.rb +13 -5
- 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_extraction_test.rb +140 -0
- data/test/csvtool/application/use_cases/run_row_randomization_test.rb +124 -0
- data/test/csvtool/cli_test.rb +237 -6
- data/test/csvtool/cli_unit_test.rb +24 -1
- data/test/csvtool/domain/{extraction_session → column_session}/column_selection_test.rb +2 -2
- data/test/csvtool/domain/column_session/column_session_test.rb +35 -0
- data/test/csvtool/domain/column_session/csv_source_test.rb +14 -0
- data/test/csvtool/domain/{extraction_session → column_session}/extraction_options_test.rb +3 -3
- data/test/csvtool/domain/{extraction_session → column_session}/extraction_value_test.rb +2 -2
- data/test/csvtool/domain/{extraction_session → column_session}/output_destination_test.rb +3 -3
- data/test/csvtool/domain/column_session/preview_test.rb +18 -0
- data/test/csvtool/domain/{extraction_session → column_session}/separator_test.rb +3 -3
- 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/domain/row_session/row_output_destination_test.rb +23 -0
- data/test/csvtool/domain/row_session/row_range_test.rb +30 -0
- data/test/csvtool/domain/row_session/row_session_test.rb +22 -0
- data/test/csvtool/domain/row_session/row_source_test.rb +12 -0
- data/test/csvtool/infrastructure/csv/row_randomizer_test.rb +37 -0
- data/test/csvtool/infrastructure/csv/row_streamer_test.rb +41 -0
- data/test/csvtool/infrastructure/output/csv_row_console_writer_test.rb +24 -0
- data/test/csvtool/infrastructure/output/csv_row_file_writer_test.rb +40 -0
- data/test/csvtool/interface/cli/errors/presenter_test.rb +10 -0
- data/test/csvtool/interface/cli/menu_loop_test.rb +68 -12
- 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_bad_tail.csv +5 -0
- data/test/fixtures/sample_people_no_headers.csv +3 -0
- metadata +53 -17
- data/test/csvtool/domain/extraction_session/csv_source_test.rb +0 -14
- data/test/csvtool/domain/extraction_session/extraction_session_test.rb +0 -35
- 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
|
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
|
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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\
|
|
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
|
-
"
|
|
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("
|
|
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/
|
|
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::
|
|
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/
|
|
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::
|
|
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::
|
|
15
|
+
Csvtool::Domain::ColumnSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 0)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|