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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +103 -24
  3. data/docs/architecture.md +121 -4
  4. data/docs/release-v0.7.0-alpha.md +87 -0
  5. data/docs/release-v0.8.0-alpha.md +88 -0
  6. data/lib/csvtool/application/use_cases/run_csv_split.rb +97 -0
  7. data/lib/csvtool/application/use_cases/run_csv_stats.rb +64 -0
  8. data/lib/csvtool/cli.rb +9 -1
  9. data/lib/csvtool/domain/csv_split_session/split_options.rb +27 -0
  10. data/lib/csvtool/domain/csv_split_session/split_session.rb +20 -0
  11. data/lib/csvtool/domain/csv_split_session/split_source.rb +17 -0
  12. data/lib/csvtool/domain/csv_stats_session/stats_options.rb +11 -0
  13. data/lib/csvtool/domain/csv_stats_session/stats_session.rb +25 -0
  14. data/lib/csvtool/domain/csv_stats_session/stats_source.rb +17 -0
  15. data/lib/csvtool/infrastructure/csv/csv_splitter.rb +64 -0
  16. data/lib/csvtool/infrastructure/csv/csv_stats_scanner.rb +67 -0
  17. data/lib/csvtool/infrastructure/output/csv_split_manifest_writer.rb +20 -0
  18. data/lib/csvtool/infrastructure/output/csv_stats_file_writer.rb +26 -0
  19. data/lib/csvtool/interface/cli/errors/presenter.rb +8 -0
  20. data/lib/csvtool/interface/cli/menu_loop.rb +8 -2
  21. data/lib/csvtool/interface/cli/prompts/chunk_size_prompt.rb +21 -0
  22. data/lib/csvtool/interface/cli/prompts/split_manifest_prompt.rb +30 -0
  23. data/lib/csvtool/interface/cli/prompts/split_output_prompt.rb +38 -0
  24. data/lib/csvtool/interface/cli/workflows/builders/csv_split_session_builder.rb +44 -0
  25. data/lib/csvtool/interface/cli/workflows/builders/csv_stats_session_builder.rb +28 -0
  26. data/lib/csvtool/interface/cli/workflows/presenters/csv_split_presenter.rb +26 -0
  27. data/lib/csvtool/interface/cli/workflows/presenters/csv_stats_presenter.rb +34 -0
  28. data/lib/csvtool/interface/cli/workflows/run_csv_split_workflow.rb +89 -0
  29. data/lib/csvtool/interface/cli/workflows/run_csv_stats_workflow.rb +77 -0
  30. data/lib/csvtool/interface/cli/workflows/steps/csv_split/build_session_step.rb +30 -0
  31. data/lib/csvtool/interface/cli/workflows/steps/csv_split/collect_inputs_step.rb +43 -0
  32. data/lib/csvtool/interface/cli/workflows/steps/csv_split/collect_manifest_step.rb +30 -0
  33. data/lib/csvtool/interface/cli/workflows/steps/csv_split/collect_output_step.rb +31 -0
  34. data/lib/csvtool/interface/cli/workflows/steps/csv_split/execute_step.rb +36 -0
  35. data/lib/csvtool/interface/cli/workflows/steps/csv_stats/build_session_step.rb +25 -0
  36. data/lib/csvtool/interface/cli/workflows/steps/csv_stats/collect_destination_step.rb +27 -0
  37. data/lib/csvtool/interface/cli/workflows/steps/csv_stats/collect_inputs_step.rb +31 -0
  38. data/lib/csvtool/interface/cli/workflows/steps/csv_stats/execute_step.rb +27 -0
  39. data/lib/csvtool/version.rb +1 -1
  40. data/test/csvtool/application/use_cases/run_csv_split_test.rb +124 -0
  41. data/test/csvtool/application/use_cases/run_csv_stats_test.rb +165 -0
  42. data/test/csvtool/cli_test.rb +139 -29
  43. data/test/csvtool/infrastructure/csv/csv_splitter_test.rb +68 -0
  44. data/test/csvtool/infrastructure/csv/csv_stats_scanner_test.rb +68 -0
  45. data/test/csvtool/infrastructure/output/csv_split_manifest_writer_test.rb +25 -0
  46. data/test/csvtool/infrastructure/output/csv_stats_file_writer_test.rb +38 -0
  47. data/test/csvtool/interface/cli/menu_loop_test.rb +104 -130
  48. data/test/csvtool/interface/cli/prompts/chunk_size_prompt_test.rb +17 -0
  49. data/test/csvtool/interface/cli/prompts/split_manifest_prompt_test.rb +42 -0
  50. data/test/csvtool/interface/cli/prompts/split_output_prompt_test.rb +22 -0
  51. data/test/csvtool/interface/cli/workflows/builders/csv_split_session_builder_test.rb +30 -0
  52. data/test/csvtool/interface/cli/workflows/builders/csv_stats_session_builder_test.rb +19 -0
  53. data/test/csvtool/interface/cli/workflows/presenters/csv_split_presenter_test.rb +26 -0
  54. data/test/csvtool/interface/cli/workflows/presenters/csv_stats_presenter_test.rb +37 -0
  55. data/test/csvtool/interface/cli/workflows/run_csv_split_workflow_test.rb +200 -0
  56. data/test/csvtool/interface/cli/workflows/run_csv_stats_workflow_test.rb +146 -0
  57. data/test/csvtool/interface/cli/workflows/steps/csv_split/build_session_step_test.rb +40 -0
  58. data/test/csvtool/interface/cli/workflows/steps/csv_split/collect_inputs_step_test.rb +64 -0
  59. data/test/csvtool/interface/cli/workflows/steps/csv_split/collect_manifest_step_test.rb +30 -0
  60. data/test/csvtool/interface/cli/workflows/steps/csv_split/collect_output_step_test.rb +32 -0
  61. data/test/csvtool/interface/cli/workflows/steps/csv_split/execute_step_test.rb +83 -0
  62. data/test/csvtool/interface/cli/workflows/steps/csv_stats/build_session_step_test.rb +36 -0
  63. data/test/csvtool/interface/cli/workflows/steps/csv_stats/collect_destination_step_test.rb +49 -0
  64. data/test/csvtool/interface/cli/workflows/steps/csv_stats/collect_inputs_step_test.rb +61 -0
  65. data/test/csvtool/interface/cli/workflows/steps/csv_stats/execute_step_test.rb +65 -0
  66. data/test/fixtures/split_people_25.csv +26 -0
  67. 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