csvops 0.6.0.alpha → 0.8.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 +103 -24
- data/docs/architecture.md +121 -4
- data/docs/release-v0.7.0-alpha.md +87 -0
- data/docs/release-v0.8.0-alpha.md +88 -0
- data/lib/csvtool/application/use_cases/run_csv_split.rb +97 -0
- data/lib/csvtool/application/use_cases/run_csv_stats.rb +64 -0
- data/lib/csvtool/cli.rb +9 -1
- data/lib/csvtool/domain/csv_split_session/split_options.rb +27 -0
- data/lib/csvtool/domain/csv_split_session/split_session.rb +20 -0
- data/lib/csvtool/domain/csv_split_session/split_source.rb +17 -0
- data/lib/csvtool/domain/csv_stats_session/stats_options.rb +11 -0
- data/lib/csvtool/domain/csv_stats_session/stats_session.rb +25 -0
- data/lib/csvtool/domain/csv_stats_session/stats_source.rb +17 -0
- data/lib/csvtool/infrastructure/csv/csv_splitter.rb +64 -0
- data/lib/csvtool/infrastructure/csv/csv_stats_scanner.rb +67 -0
- data/lib/csvtool/infrastructure/output/csv_split_manifest_writer.rb +20 -0
- data/lib/csvtool/infrastructure/output/csv_stats_file_writer.rb +26 -0
- data/lib/csvtool/interface/cli/errors/presenter.rb +8 -0
- data/lib/csvtool/interface/cli/menu_loop.rb +8 -2
- data/lib/csvtool/interface/cli/prompts/chunk_size_prompt.rb +21 -0
- data/lib/csvtool/interface/cli/prompts/split_manifest_prompt.rb +30 -0
- data/lib/csvtool/interface/cli/prompts/split_output_prompt.rb +38 -0
- data/lib/csvtool/interface/cli/workflows/builders/csv_split_session_builder.rb +44 -0
- data/lib/csvtool/interface/cli/workflows/builders/csv_stats_session_builder.rb +28 -0
- data/lib/csvtool/interface/cli/workflows/presenters/csv_split_presenter.rb +26 -0
- data/lib/csvtool/interface/cli/workflows/presenters/csv_stats_presenter.rb +34 -0
- data/lib/csvtool/interface/cli/workflows/run_csv_split_workflow.rb +89 -0
- data/lib/csvtool/interface/cli/workflows/run_csv_stats_workflow.rb +77 -0
- data/lib/csvtool/interface/cli/workflows/steps/csv_split/build_session_step.rb +30 -0
- data/lib/csvtool/interface/cli/workflows/steps/csv_split/collect_inputs_step.rb +43 -0
- data/lib/csvtool/interface/cli/workflows/steps/csv_split/collect_manifest_step.rb +30 -0
- data/lib/csvtool/interface/cli/workflows/steps/csv_split/collect_output_step.rb +31 -0
- data/lib/csvtool/interface/cli/workflows/steps/csv_split/execute_step.rb +36 -0
- data/lib/csvtool/interface/cli/workflows/steps/csv_stats/build_session_step.rb +25 -0
- data/lib/csvtool/interface/cli/workflows/steps/csv_stats/collect_destination_step.rb +27 -0
- data/lib/csvtool/interface/cli/workflows/steps/csv_stats/collect_inputs_step.rb +31 -0
- data/lib/csvtool/interface/cli/workflows/steps/csv_stats/execute_step.rb +27 -0
- data/lib/csvtool/version.rb +1 -1
- data/test/csvtool/application/use_cases/run_csv_split_test.rb +124 -0
- data/test/csvtool/application/use_cases/run_csv_stats_test.rb +165 -0
- data/test/csvtool/cli_test.rb +139 -29
- data/test/csvtool/infrastructure/csv/csv_splitter_test.rb +68 -0
- data/test/csvtool/infrastructure/csv/csv_stats_scanner_test.rb +68 -0
- data/test/csvtool/infrastructure/output/csv_split_manifest_writer_test.rb +25 -0
- data/test/csvtool/infrastructure/output/csv_stats_file_writer_test.rb +38 -0
- data/test/csvtool/interface/cli/menu_loop_test.rb +104 -130
- data/test/csvtool/interface/cli/prompts/chunk_size_prompt_test.rb +17 -0
- data/test/csvtool/interface/cli/prompts/split_manifest_prompt_test.rb +42 -0
- data/test/csvtool/interface/cli/prompts/split_output_prompt_test.rb +22 -0
- data/test/csvtool/interface/cli/workflows/builders/csv_split_session_builder_test.rb +30 -0
- data/test/csvtool/interface/cli/workflows/builders/csv_stats_session_builder_test.rb +19 -0
- data/test/csvtool/interface/cli/workflows/presenters/csv_split_presenter_test.rb +26 -0
- data/test/csvtool/interface/cli/workflows/presenters/csv_stats_presenter_test.rb +37 -0
- data/test/csvtool/interface/cli/workflows/run_csv_split_workflow_test.rb +200 -0
- data/test/csvtool/interface/cli/workflows/run_csv_stats_workflow_test.rb +146 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_split/build_session_step_test.rb +40 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_split/collect_inputs_step_test.rb +64 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_split/collect_manifest_step_test.rb +30 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_split/collect_output_step_test.rb +32 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_split/execute_step_test.rb +83 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_stats/build_session_step_test.rb +36 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_stats/collect_destination_step_test.rb +49 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_stats/collect_inputs_step_test.rb +61 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_stats/execute_step_test.rb +65 -0
- data/test/fixtures/split_people_25.csv +26 -0
- metadata +58 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/run_csv_split_workflow"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
class RunCsvSplitWorkflowTest < Minitest::Test
|
|
9
|
+
def fixture_path(name)
|
|
10
|
+
File.expand_path("../../../../fixtures/#{name}", __dir__)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_workflow_splits_into_chunk_files
|
|
14
|
+
out = StringIO.new
|
|
15
|
+
|
|
16
|
+
Dir.mktmpdir do |dir|
|
|
17
|
+
source_path = File.join(dir, "people.csv")
|
|
18
|
+
FileUtils.cp(fixture_path("split_people_25.csv"), source_path)
|
|
19
|
+
input = [source_path, "", "", "10", "", "", "", ""].join("\n") + "\n"
|
|
20
|
+
|
|
21
|
+
Csvtool::Interface::CLI::Workflows::RunCsvSplitWorkflow.new(
|
|
22
|
+
stdin: StringIO.new(input),
|
|
23
|
+
stdout: out
|
|
24
|
+
).call
|
|
25
|
+
|
|
26
|
+
assert_includes out.string, "Split complete."
|
|
27
|
+
assert_includes out.string, "Chunk size: 10"
|
|
28
|
+
assert_includes out.string, "Chunks written: 3"
|
|
29
|
+
assert File.file?(File.join(dir, "people_part_001.csv"))
|
|
30
|
+
assert File.file?(File.join(dir, "people_part_002.csv"))
|
|
31
|
+
assert File.file?(File.join(dir, "people_part_003.csv"))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_workflow_supports_tsv_separator
|
|
36
|
+
out = StringIO.new
|
|
37
|
+
|
|
38
|
+
Dir.mktmpdir do |dir|
|
|
39
|
+
source_path = File.join(dir, "people.tsv")
|
|
40
|
+
FileUtils.cp(fixture_path("sample_people.tsv"), source_path)
|
|
41
|
+
input = [source_path, "2", "", "2", "", "", "", ""].join("\n") + "\n"
|
|
42
|
+
|
|
43
|
+
Csvtool::Interface::CLI::Workflows::RunCsvSplitWorkflow.new(
|
|
44
|
+
stdin: StringIO.new(input),
|
|
45
|
+
stdout: out
|
|
46
|
+
).call
|
|
47
|
+
|
|
48
|
+
chunk_path = File.join(dir, "people_part_001.tsv")
|
|
49
|
+
assert File.file?(chunk_path)
|
|
50
|
+
assert_includes File.read(chunk_path), "name\tcity"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_workflow_supports_headerless_mode
|
|
55
|
+
out = StringIO.new
|
|
56
|
+
|
|
57
|
+
Dir.mktmpdir do |dir|
|
|
58
|
+
source_path = File.join(dir, "people.csv")
|
|
59
|
+
FileUtils.cp(fixture_path("sample_people_no_headers.csv"), source_path)
|
|
60
|
+
input = [source_path, "", "n", "2", "", "", "", ""].join("\n") + "\n"
|
|
61
|
+
|
|
62
|
+
Csvtool::Interface::CLI::Workflows::RunCsvSplitWorkflow.new(
|
|
63
|
+
stdin: StringIO.new(input),
|
|
64
|
+
stdout: out
|
|
65
|
+
).call
|
|
66
|
+
|
|
67
|
+
chunk_path = File.join(dir, "people_part_001.csv")
|
|
68
|
+
lines = File.read(chunk_path).lines.map(&:strip)
|
|
69
|
+
assert_equal "Alice,London", lines.first
|
|
70
|
+
assert_equal "Bob,Paris", lines.last
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_workflow_supports_custom_separator
|
|
75
|
+
out = StringIO.new
|
|
76
|
+
|
|
77
|
+
Dir.mktmpdir do |dir|
|
|
78
|
+
source_path = File.join(dir, "people.txt")
|
|
79
|
+
FileUtils.cp(fixture_path("sample_people_colon.txt"), source_path)
|
|
80
|
+
input = [source_path, "5", ":", "", "2", "", "", "", ""].join("\n") + "\n"
|
|
81
|
+
|
|
82
|
+
Csvtool::Interface::CLI::Workflows::RunCsvSplitWorkflow.new(
|
|
83
|
+
stdin: StringIO.new(input),
|
|
84
|
+
stdout: out
|
|
85
|
+
).call
|
|
86
|
+
|
|
87
|
+
chunk_path = File.join(dir, "people_part_001.txt")
|
|
88
|
+
assert File.file?(chunk_path)
|
|
89
|
+
assert_includes File.read(chunk_path), "name:city"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_workflow_uses_custom_output_directory_and_prefix
|
|
94
|
+
out = StringIO.new
|
|
95
|
+
|
|
96
|
+
Dir.mktmpdir do |dir|
|
|
97
|
+
source_path = File.join(dir, "people.csv")
|
|
98
|
+
output_dir = File.join(dir, "chunks")
|
|
99
|
+
Dir.mkdir(output_dir)
|
|
100
|
+
FileUtils.cp(fixture_path("split_people_25.csv"), source_path)
|
|
101
|
+
input = [source_path, "", "", "10", output_dir, "batch", "", ""].join("\n") + "\n"
|
|
102
|
+
|
|
103
|
+
Csvtool::Interface::CLI::Workflows::RunCsvSplitWorkflow.new(
|
|
104
|
+
stdin: StringIO.new(input),
|
|
105
|
+
stdout: out
|
|
106
|
+
).call
|
|
107
|
+
|
|
108
|
+
assert File.file?(File.join(output_dir, "batch_part_001.csv"))
|
|
109
|
+
assert File.file?(File.join(output_dir, "batch_part_002.csv"))
|
|
110
|
+
assert File.file?(File.join(output_dir, "batch_part_003.csv"))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_workflow_does_not_overwrite_existing_file_without_confirmation
|
|
115
|
+
out = StringIO.new
|
|
116
|
+
|
|
117
|
+
Dir.mktmpdir do |dir|
|
|
118
|
+
source_path = File.join(dir, "people.csv")
|
|
119
|
+
FileUtils.cp(fixture_path("split_people_25.csv"), source_path)
|
|
120
|
+
existing = File.join(dir, "people_part_001.csv")
|
|
121
|
+
File.write(existing, "sentinel\n")
|
|
122
|
+
input = [source_path, "", "", "10", "", "", "", ""].join("\n") + "\n"
|
|
123
|
+
|
|
124
|
+
Csvtool::Interface::CLI::Workflows::RunCsvSplitWorkflow.new(
|
|
125
|
+
stdin: StringIO.new(input),
|
|
126
|
+
stdout: out
|
|
127
|
+
).call
|
|
128
|
+
|
|
129
|
+
assert_includes out.string, "Output file already exists: #{existing}"
|
|
130
|
+
assert_equal "sentinel\n", File.read(existing)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_workflow_reports_missing_file
|
|
135
|
+
out = StringIO.new
|
|
136
|
+
input = ["/tmp/does-not-exist.csv", "", "", "10", "", "", "", ""].join("\n") + "\n"
|
|
137
|
+
|
|
138
|
+
Csvtool::Interface::CLI::Workflows::RunCsvSplitWorkflow.new(
|
|
139
|
+
stdin: StringIO.new(input),
|
|
140
|
+
stdout: out
|
|
141
|
+
).call
|
|
142
|
+
|
|
143
|
+
assert_includes out.string, "File not found: /tmp/does-not-exist.csv"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def test_workflow_rejects_invalid_chunk_size
|
|
147
|
+
out = StringIO.new
|
|
148
|
+
input = [fixture_path("sample_people.csv"), "", "", "abc"].join("\n") + "\n"
|
|
149
|
+
|
|
150
|
+
Csvtool::Interface::CLI::Workflows::RunCsvSplitWorkflow.new(
|
|
151
|
+
stdin: StringIO.new(input),
|
|
152
|
+
stdout: out
|
|
153
|
+
).call
|
|
154
|
+
|
|
155
|
+
assert_includes out.string, "Chunk size must be a positive integer."
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def test_workflow_creates_output_directory_when_missing
|
|
159
|
+
out = StringIO.new
|
|
160
|
+
|
|
161
|
+
Dir.mktmpdir do |dir|
|
|
162
|
+
source_path = File.join(dir, "people.csv")
|
|
163
|
+
output_dir = File.join(dir, "chunks")
|
|
164
|
+
FileUtils.cp(fixture_path("split_people_25.csv"), source_path)
|
|
165
|
+
input = [source_path, "", "", "10", output_dir, "people", "", ""].join("\n") + "\n"
|
|
166
|
+
|
|
167
|
+
Csvtool::Interface::CLI::Workflows::RunCsvSplitWorkflow.new(
|
|
168
|
+
stdin: StringIO.new(input),
|
|
169
|
+
stdout: out
|
|
170
|
+
).call
|
|
171
|
+
|
|
172
|
+
assert Dir.exist?(output_dir)
|
|
173
|
+
assert File.file?(File.join(output_dir, "people_part_001.csv"))
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def test_workflow_can_write_manifest_when_enabled
|
|
178
|
+
out = StringIO.new
|
|
179
|
+
|
|
180
|
+
Dir.mktmpdir do |dir|
|
|
181
|
+
source_path = File.join(dir, "people.csv")
|
|
182
|
+
FileUtils.cp(fixture_path("split_people_25.csv"), source_path)
|
|
183
|
+
manifest_path = File.join(dir, "manifest.csv")
|
|
184
|
+
input = [source_path, "", "", "10", "", "", "", "y", manifest_path].join("\n") + "\n"
|
|
185
|
+
|
|
186
|
+
Csvtool::Interface::CLI::Workflows::RunCsvSplitWorkflow.new(
|
|
187
|
+
stdin: StringIO.new(input),
|
|
188
|
+
stdout: out
|
|
189
|
+
).call
|
|
190
|
+
|
|
191
|
+
assert File.file?(manifest_path)
|
|
192
|
+
lines = File.read(manifest_path).lines.map(&:strip)
|
|
193
|
+
assert_equal "chunk_index,chunk_path,row_count", lines.first
|
|
194
|
+
assert_includes lines[1], ",10"
|
|
195
|
+
assert_includes lines[2], ",10"
|
|
196
|
+
assert_includes lines[3], ",5"
|
|
197
|
+
assert_includes out.string, "Manifest: #{manifest_path}"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/run_csv_stats_workflow"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
class RunCsvStatsWorkflowTest < Minitest::Test
|
|
8
|
+
def fixture_path(name)
|
|
9
|
+
File.expand_path("../../../../fixtures/#{name}", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_workflow_prints_core_stats_summary
|
|
13
|
+
out = StringIO.new
|
|
14
|
+
input = [fixture_path("sample_people.csv"), "", ""].join("\n") + "\n"
|
|
15
|
+
|
|
16
|
+
Csvtool::Interface::CLI::Workflows::RunCsvStatsWorkflow.new(
|
|
17
|
+
stdin: StringIO.new(input),
|
|
18
|
+
stdout: out
|
|
19
|
+
).call
|
|
20
|
+
|
|
21
|
+
assert_includes out.string, "CSV Stats Summary"
|
|
22
|
+
assert_includes out.string, "Rows: 3"
|
|
23
|
+
assert_includes out.string, "Columns: 2"
|
|
24
|
+
assert_includes out.string, "Headers: name, city"
|
|
25
|
+
assert_includes out.string, "Column completeness:"
|
|
26
|
+
assert_includes out.string, "name: non_blank=3 blank=0"
|
|
27
|
+
assert_includes out.string, "city: non_blank=3 blank=0"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_workflow_supports_tsv_separator
|
|
31
|
+
out = StringIO.new
|
|
32
|
+
input = [fixture_path("sample_people.tsv"), "2", ""].join("\n") + "\n"
|
|
33
|
+
|
|
34
|
+
Csvtool::Interface::CLI::Workflows::RunCsvStatsWorkflow.new(
|
|
35
|
+
stdin: StringIO.new(input),
|
|
36
|
+
stdout: out
|
|
37
|
+
).call
|
|
38
|
+
|
|
39
|
+
assert_includes out.string, "Rows: 3"
|
|
40
|
+
assert_includes out.string, "Columns: 2"
|
|
41
|
+
assert_includes out.string, "Headers: name, city"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_workflow_supports_headerless_mode
|
|
45
|
+
out = StringIO.new
|
|
46
|
+
input = [fixture_path("sample_people_no_headers.csv"), "", "n"].join("\n") + "\n"
|
|
47
|
+
|
|
48
|
+
Csvtool::Interface::CLI::Workflows::RunCsvStatsWorkflow.new(
|
|
49
|
+
stdin: StringIO.new(input),
|
|
50
|
+
stdout: out
|
|
51
|
+
).call
|
|
52
|
+
|
|
53
|
+
assert_includes out.string, "Rows: 3"
|
|
54
|
+
assert_includes out.string, "Columns: 2"
|
|
55
|
+
refute_includes out.string, "Headers:"
|
|
56
|
+
assert_includes out.string, "column_1: non_blank=3 blank=0"
|
|
57
|
+
assert_includes out.string, "column_2: non_blank=3 blank=0"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_workflow_supports_custom_separator
|
|
61
|
+
out = StringIO.new
|
|
62
|
+
input = [fixture_path("sample_people_colon.txt"), "5", ":", ""].join("\n") + "\n"
|
|
63
|
+
|
|
64
|
+
Csvtool::Interface::CLI::Workflows::RunCsvStatsWorkflow.new(
|
|
65
|
+
stdin: StringIO.new(input),
|
|
66
|
+
stdout: out
|
|
67
|
+
).call
|
|
68
|
+
|
|
69
|
+
assert_includes out.string, "Rows: 3"
|
|
70
|
+
assert_includes out.string, "Columns: 2"
|
|
71
|
+
assert_includes out.string, "Headers: name, city"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_workflow_prints_column_completeness_for_blank_values
|
|
75
|
+
out = StringIO.new
|
|
76
|
+
input = [fixture_path("sample_people_blanks.csv"), "", ""].join("\n") + "\n"
|
|
77
|
+
|
|
78
|
+
Csvtool::Interface::CLI::Workflows::RunCsvStatsWorkflow.new(
|
|
79
|
+
stdin: StringIO.new(input),
|
|
80
|
+
stdout: out
|
|
81
|
+
).call
|
|
82
|
+
|
|
83
|
+
assert_includes out.string, "name: non_blank=3 blank=2"
|
|
84
|
+
assert_includes out.string, "city: non_blank=4 blank=1"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_workflow_reports_missing_file
|
|
88
|
+
out = StringIO.new
|
|
89
|
+
input = ["/tmp/does-not-exist.csv", "", ""].join("\n") + "\n"
|
|
90
|
+
|
|
91
|
+
Csvtool::Interface::CLI::Workflows::RunCsvStatsWorkflow.new(
|
|
92
|
+
stdin: StringIO.new(input),
|
|
93
|
+
stdout: out
|
|
94
|
+
).call
|
|
95
|
+
|
|
96
|
+
assert_includes out.string, "File not found: /tmp/does-not-exist.csv"
|
|
97
|
+
refute_includes out.string, "Traceback"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_workflow_reports_parse_error
|
|
101
|
+
out = StringIO.new
|
|
102
|
+
input = [fixture_path("sample_people_bad_tail.csv"), "", ""].join("\n") + "\n"
|
|
103
|
+
|
|
104
|
+
Csvtool::Interface::CLI::Workflows::RunCsvStatsWorkflow.new(
|
|
105
|
+
stdin: StringIO.new(input),
|
|
106
|
+
stdout: out
|
|
107
|
+
).call
|
|
108
|
+
|
|
109
|
+
assert_includes out.string, "Could not parse CSV file."
|
|
110
|
+
refute_includes out.string, "Traceback"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_workflow_can_write_stats_to_file
|
|
114
|
+
out = StringIO.new
|
|
115
|
+
|
|
116
|
+
Dir.mktmpdir do |dir|
|
|
117
|
+
output_path = File.join(dir, "stats.csv")
|
|
118
|
+
input = [fixture_path("sample_people.csv"), "", "", "2", output_path].join("\n") + "\n"
|
|
119
|
+
|
|
120
|
+
Csvtool::Interface::CLI::Workflows::RunCsvStatsWorkflow.new(
|
|
121
|
+
stdin: StringIO.new(input),
|
|
122
|
+
stdout: out
|
|
123
|
+
).call
|
|
124
|
+
|
|
125
|
+
assert_includes out.string, "Wrote output to #{output_path}"
|
|
126
|
+
csv_text = File.read(output_path)
|
|
127
|
+
assert_includes csv_text, "metric,value"
|
|
128
|
+
assert_includes csv_text, "row_count,3"
|
|
129
|
+
assert_includes csv_text, "column_count,2"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_workflow_reports_cannot_write_output_file
|
|
134
|
+
out = StringIO.new
|
|
135
|
+
output_path = "/tmp/does-not-exist-dir/stats.csv"
|
|
136
|
+
input = [fixture_path("sample_people.csv"), "", "", "2", output_path].join("\n") + "\n"
|
|
137
|
+
|
|
138
|
+
Csvtool::Interface::CLI::Workflows::RunCsvStatsWorkflow.new(
|
|
139
|
+
stdin: StringIO.new(input),
|
|
140
|
+
stdout: out
|
|
141
|
+
).call
|
|
142
|
+
|
|
143
|
+
assert_includes out.string, "Cannot write output file: #{output_path} (Errno::ENOENT)"
|
|
144
|
+
refute_includes out.string, "Traceback"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/csv_split/build_session_step"
|
|
5
|
+
|
|
6
|
+
class BuildSessionStepTest < Minitest::Test
|
|
7
|
+
class FakeBuilder
|
|
8
|
+
attr_reader :params
|
|
9
|
+
|
|
10
|
+
def call(**params)
|
|
11
|
+
@params = params
|
|
12
|
+
:session
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_builds_session_from_context
|
|
17
|
+
builder = FakeBuilder.new
|
|
18
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::CsvSplit::BuildSessionStep.new
|
|
19
|
+
context = {
|
|
20
|
+
session_builder: builder,
|
|
21
|
+
file_path: "/tmp/data.csv",
|
|
22
|
+
col_sep: ",",
|
|
23
|
+
headers_present: true,
|
|
24
|
+
chunk_size: 10,
|
|
25
|
+
output_directory: "/tmp/out",
|
|
26
|
+
file_prefix: "batch",
|
|
27
|
+
overwrite_existing: true,
|
|
28
|
+
write_manifest: true,
|
|
29
|
+
manifest_path: "/tmp/out/manifest.csv"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
result = step.call(context)
|
|
33
|
+
|
|
34
|
+
assert_nil result
|
|
35
|
+
assert_equal :session, context[:session]
|
|
36
|
+
assert_equal "/tmp/data.csv", builder.params[:file_path]
|
|
37
|
+
assert_equal true, builder.params[:write_manifest]
|
|
38
|
+
assert_equal "/tmp/out/manifest.csv", builder.params[:manifest_path]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/csv_split/collect_inputs_step"
|
|
5
|
+
|
|
6
|
+
class CollectInputsStepTest < Minitest::Test
|
|
7
|
+
class FakePrompt
|
|
8
|
+
def initialize(value)
|
|
9
|
+
@value = value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(label: nil)
|
|
13
|
+
@value
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class FakeErrors
|
|
18
|
+
attr_reader :invalid_chunk_size_calls
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@invalid_chunk_size_calls = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def invalid_chunk_size
|
|
25
|
+
@invalid_chunk_size_calls += 1
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_collects_inputs_into_context
|
|
30
|
+
errors = FakeErrors.new
|
|
31
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::CsvSplit::CollectInputsStep.new(
|
|
32
|
+
file_path_prompt: FakePrompt.new("/tmp/data.csv"),
|
|
33
|
+
separator_prompt: FakePrompt.new(","),
|
|
34
|
+
headers_present_prompt: FakePrompt.new(true),
|
|
35
|
+
chunk_size_prompt: FakePrompt.new("10"),
|
|
36
|
+
errors: errors
|
|
37
|
+
)
|
|
38
|
+
context = {}
|
|
39
|
+
|
|
40
|
+
result = step.call(context)
|
|
41
|
+
|
|
42
|
+
assert_nil result
|
|
43
|
+
assert_equal "/tmp/data.csv", context[:file_path]
|
|
44
|
+
assert_equal ",", context[:col_sep]
|
|
45
|
+
assert_equal true, context[:headers_present]
|
|
46
|
+
assert_equal 10, context[:chunk_size]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_halts_on_invalid_chunk_size
|
|
50
|
+
errors = FakeErrors.new
|
|
51
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::CsvSplit::CollectInputsStep.new(
|
|
52
|
+
file_path_prompt: FakePrompt.new("/tmp/data.csv"),
|
|
53
|
+
separator_prompt: FakePrompt.new(","),
|
|
54
|
+
headers_present_prompt: FakePrompt.new(true),
|
|
55
|
+
chunk_size_prompt: FakePrompt.new("abc"),
|
|
56
|
+
errors: errors
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
result = step.call({})
|
|
60
|
+
|
|
61
|
+
assert_equal :halt, result
|
|
62
|
+
assert_equal 1, errors.invalid_chunk_size_calls
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/csv_split/collect_manifest_step"
|
|
5
|
+
|
|
6
|
+
class CollectManifestStepTest < Minitest::Test
|
|
7
|
+
class FakeSplitManifestPrompt
|
|
8
|
+
attr_reader :default_path
|
|
9
|
+
|
|
10
|
+
def call(default_path:)
|
|
11
|
+
@default_path = default_path
|
|
12
|
+
{ write_manifest: true, manifest_path: "/tmp/out/custom_manifest.csv" }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_sets_manifest_values_in_context
|
|
17
|
+
prompt = FakeSplitManifestPrompt.new
|
|
18
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::CsvSplit::CollectManifestStep.new(
|
|
19
|
+
split_manifest_prompt: prompt
|
|
20
|
+
)
|
|
21
|
+
context = { output_directory: "/tmp/out", file_prefix: "batch" }
|
|
22
|
+
|
|
23
|
+
result = step.call(context)
|
|
24
|
+
|
|
25
|
+
assert_nil result
|
|
26
|
+
assert_equal "/tmp/out/batch_manifest.csv", prompt.default_path
|
|
27
|
+
assert_equal true, context[:write_manifest]
|
|
28
|
+
assert_equal "/tmp/out/custom_manifest.csv", context[:manifest_path]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/csv_split/collect_output_step"
|
|
5
|
+
|
|
6
|
+
class CollectOutputStepTest < Minitest::Test
|
|
7
|
+
class FakeSplitOutputPrompt
|
|
8
|
+
attr_reader :received
|
|
9
|
+
|
|
10
|
+
def call(default_directory:, default_prefix:)
|
|
11
|
+
@received = { default_directory: default_directory, default_prefix: default_prefix }
|
|
12
|
+
{ output_directory: "/tmp/out", file_prefix: "batch", overwrite_existing: true }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_sets_output_values_in_context
|
|
17
|
+
prompt = FakeSplitOutputPrompt.new
|
|
18
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::CsvSplit::CollectOutputStep.new(
|
|
19
|
+
split_output_prompt: prompt
|
|
20
|
+
)
|
|
21
|
+
context = { file_path: "/tmp/people.csv" }
|
|
22
|
+
|
|
23
|
+
result = step.call(context)
|
|
24
|
+
|
|
25
|
+
assert_nil result
|
|
26
|
+
assert_equal "/tmp", prompt.received[:default_directory]
|
|
27
|
+
assert_equal "people", prompt.received[:default_prefix]
|
|
28
|
+
assert_equal "/tmp/out", context[:output_directory]
|
|
29
|
+
assert_equal "batch", context[:file_prefix]
|
|
30
|
+
assert_equal true, context[:overwrite_existing]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/csv_split/execute_step"
|
|
5
|
+
|
|
6
|
+
class SplitExecuteStepTest < Minitest::Test
|
|
7
|
+
Result = Struct.new(:ok, :data) do
|
|
8
|
+
def ok? = ok
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class FakeUseCase
|
|
12
|
+
def initialize(headers_result:, run_result:)
|
|
13
|
+
@headers_result = headers_result
|
|
14
|
+
@run_result = run_result
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def read_headers(file_path:, col_sep:, headers_present:)
|
|
18
|
+
@headers_called = [file_path, col_sep, headers_present]
|
|
19
|
+
@headers_result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(session:)
|
|
23
|
+
@session = session
|
|
24
|
+
@run_result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :headers_called, :session
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class FakePresenter
|
|
31
|
+
attr_reader :summary
|
|
32
|
+
|
|
33
|
+
def print_summary(data)
|
|
34
|
+
@summary = data
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_handles_header_failure
|
|
39
|
+
headers_fail = Result.new(false, { path: "/tmp/missing.csv" })
|
|
40
|
+
use_case = FakeUseCase.new(headers_result: headers_fail, run_result: Result.new(true, {}))
|
|
41
|
+
handled = []
|
|
42
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::CsvSplit::ExecuteStep.new
|
|
43
|
+
|
|
44
|
+
result = step.call(
|
|
45
|
+
file_path: "/tmp/missing.csv",
|
|
46
|
+
col_sep: ",",
|
|
47
|
+
headers_present: true,
|
|
48
|
+
chunk_size: 10,
|
|
49
|
+
use_case: use_case,
|
|
50
|
+
session: Object.new,
|
|
51
|
+
presenter: FakePresenter.new,
|
|
52
|
+
handle_error: ->(r) { handled << r }
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert_equal :halt, result
|
|
56
|
+
assert_equal [headers_fail], handled
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_prints_summary_on_success
|
|
60
|
+
use_case = FakeUseCase.new(
|
|
61
|
+
headers_result: Result.new(true, { headers: %w[name city] }),
|
|
62
|
+
run_result: Result.new(true, { chunk_count: 3, chunk_paths: ["/tmp/a.csv"], data_rows: 25 })
|
|
63
|
+
)
|
|
64
|
+
presenter = FakePresenter.new
|
|
65
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::CsvSplit::ExecuteStep.new
|
|
66
|
+
|
|
67
|
+
result = step.call(
|
|
68
|
+
file_path: "/tmp/people.csv",
|
|
69
|
+
col_sep: ",",
|
|
70
|
+
headers_present: true,
|
|
71
|
+
chunk_size: 10,
|
|
72
|
+
use_case: use_case,
|
|
73
|
+
session: :session,
|
|
74
|
+
presenter: presenter,
|
|
75
|
+
handle_error: ->(_r) { raise "unexpected" }
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
assert_nil result
|
|
79
|
+
assert_equal :session, use_case.session
|
|
80
|
+
assert_equal 10, presenter.summary[:chunk_size]
|
|
81
|
+
assert_equal 3, presenter.summary[:chunk_count]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/csv_stats/build_session_step"
|
|
5
|
+
|
|
6
|
+
class CsvStatsBuildSessionStepTest < Minitest::Test
|
|
7
|
+
class FakeBuilder
|
|
8
|
+
attr_reader :params
|
|
9
|
+
|
|
10
|
+
def call(**params)
|
|
11
|
+
@params = params
|
|
12
|
+
:session
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_builds_session_from_context
|
|
17
|
+
builder = FakeBuilder.new
|
|
18
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::CsvStats::BuildSessionStep.new
|
|
19
|
+
context = {
|
|
20
|
+
session_builder: builder,
|
|
21
|
+
file_path: "/tmp/data.csv",
|
|
22
|
+
col_sep: "\t",
|
|
23
|
+
headers_present: true,
|
|
24
|
+
output_destination: :destination
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
result = step.call(context)
|
|
28
|
+
|
|
29
|
+
assert_nil result
|
|
30
|
+
assert_equal :session, context[:session]
|
|
31
|
+
assert_equal "/tmp/data.csv", builder.params[:file_path]
|
|
32
|
+
assert_equal "\t", builder.params[:col_sep]
|
|
33
|
+
assert_equal true, builder.params[:headers_present]
|
|
34
|
+
assert_equal :destination, builder.params[:destination]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/csv_stats/collect_destination_step"
|
|
5
|
+
|
|
6
|
+
class CsvStatsCollectDestinationStepTest < Minitest::Test
|
|
7
|
+
class FakePrompt
|
|
8
|
+
def initialize(result)
|
|
9
|
+
@result = result
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
@result
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class FakeMapper
|
|
18
|
+
attr_reader :input
|
|
19
|
+
|
|
20
|
+
def call(input)
|
|
21
|
+
@input = input
|
|
22
|
+
:mapped_destination
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_collects_and_maps_destination
|
|
27
|
+
mapper = FakeMapper.new
|
|
28
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::CsvStats::CollectDestinationStep.new(
|
|
29
|
+
output_destination_prompt: FakePrompt.new({ mode: :file, path: "/tmp/out.csv" })
|
|
30
|
+
)
|
|
31
|
+
context = { output_destination_mapper: mapper }
|
|
32
|
+
|
|
33
|
+
result = step.call(context)
|
|
34
|
+
|
|
35
|
+
assert_nil result
|
|
36
|
+
assert_equal({ mode: :file, path: "/tmp/out.csv" }, mapper.input)
|
|
37
|
+
assert_equal :mapped_destination, context[:output_destination]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_halts_when_destination_prompt_returns_nil
|
|
41
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::CsvStats::CollectDestinationStep.new(
|
|
42
|
+
output_destination_prompt: FakePrompt.new(nil)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
result = step.call(output_destination_mapper: FakeMapper.new)
|
|
46
|
+
|
|
47
|
+
assert_equal :halt, result
|
|
48
|
+
end
|
|
49
|
+
end
|