csvops 0.5.0.alpha → 0.7.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 +88 -7
- data/docs/architecture.md +119 -5
- data/docs/release-v0.6.0-alpha.md +84 -0
- data/docs/release-v0.7.0-alpha.md +87 -0
- data/lib/csvtool/application/use_cases/run_csv_parity.rb +70 -0
- data/lib/csvtool/application/use_cases/run_csv_split.rb +97 -0
- data/lib/csvtool/cli.rb +9 -1
- data/lib/csvtool/domain/csv_parity_session/parity_options.rb +22 -0
- data/lib/csvtool/domain/csv_parity_session/parity_session.rb +20 -0
- data/lib/csvtool/domain/csv_parity_session/source_pair.rb +19 -0
- 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/infrastructure/csv/csv_parity_comparator.rb +71 -0
- data/lib/csvtool/infrastructure/csv/csv_splitter.rb +64 -0
- data/lib/csvtool/infrastructure/output/csv_split_manifest_writer.rb +20 -0
- data/lib/csvtool/interface/cli/errors/presenter.rb +12 -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_parity_session_builder.rb +33 -0
- data/lib/csvtool/interface/cli/workflows/builders/csv_split_session_builder.rb +44 -0
- data/lib/csvtool/interface/cli/workflows/presenters/csv_parity_presenter.rb +38 -0
- data/lib/csvtool/interface/cli/workflows/presenters/csv_split_presenter.rb +26 -0
- data/lib/csvtool/interface/cli/workflows/run_csv_parity_workflow.rb +66 -0
- data/lib/csvtool/interface/cli/workflows/run_csv_split_workflow.rb +89 -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/parity/build_session_step.rb +25 -0
- data/lib/csvtool/interface/cli/workflows/steps/parity/collect_inputs_step.rb +32 -0
- data/lib/csvtool/interface/cli/workflows/steps/parity/execute_step.rb +26 -0
- data/lib/csvtool/version.rb +1 -1
- data/test/csvtool/application/use_cases/run_csv_parity_test.rb +160 -0
- data/test/csvtool/application/use_cases/run_csv_split_test.rb +124 -0
- data/test/csvtool/cli_test.rb +222 -21
- data/test/csvtool/cli_unit_test.rb +4 -4
- data/test/csvtool/domain/csv_parity_session/parity_options_test.rb +17 -0
- data/test/csvtool/domain/csv_parity_session/parity_session_test.rb +18 -0
- data/test/csvtool/domain/csv_parity_session/source_pair_test.rb +11 -0
- data/test/csvtool/infrastructure/csv/csv_parity_comparator_test.rb +78 -0
- data/test/csvtool/infrastructure/csv/csv_splitter_test.rb +68 -0
- data/test/csvtool/infrastructure/output/csv_split_manifest_writer_test.rb +25 -0
- data/test/csvtool/interface/cli/errors/presenter_test.rb +2 -0
- data/test/csvtool/interface/cli/menu_loop_test.rb +87 -93
- 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_parity_session_builder_test.rb +20 -0
- data/test/csvtool/interface/cli/workflows/builders/csv_split_session_builder_test.rb +30 -0
- data/test/csvtool/interface/cli/workflows/presenters/csv_parity_presenter_test.rb +43 -0
- data/test/csvtool/interface/cli/workflows/presenters/csv_split_presenter_test.rb +26 -0
- data/test/csvtool/interface/cli/workflows/run_csv_parity_workflow_test.rb +94 -0
- data/test/csvtool/interface/cli/workflows/run_csv_split_workflow_test.rb +200 -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/parity/build_session_step_test.rb +41 -0
- data/test/csvtool/interface/cli/workflows/steps/parity/collect_inputs_step_test.rb +30 -0
- data/test/csvtool/interface/cli/workflows/steps/parity/execute_step_test.rb +40 -0
- data/test/fixtures/parity_duplicates_left.csv +4 -0
- data/test/fixtures/parity_duplicates_right.csv +3 -0
- data/test/fixtures/parity_people_header_mismatch.csv +4 -0
- data/test/fixtures/parity_people_many_reordered.csv +13 -0
- data/test/fixtures/parity_people_mismatch.csv +4 -0
- data/test/fixtures/parity_people_reordered.csv +4 -0
- data/test/fixtures/parity_people_reordered.tsv +4 -0
- data/test/fixtures/split_people_25.csv +26 -0
- metadata +64 -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,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,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/parity/build_session_step"
|
|
5
|
+
|
|
6
|
+
class ParityBuildSessionStepTest < Minitest::Test
|
|
7
|
+
class FakeBuilder
|
|
8
|
+
attr_reader :args
|
|
9
|
+
|
|
10
|
+
def call(left_path:, right_path:, col_sep:, headers_present:)
|
|
11
|
+
@args = {
|
|
12
|
+
left_path: left_path,
|
|
13
|
+
right_path: right_path,
|
|
14
|
+
col_sep: col_sep,
|
|
15
|
+
headers_present: headers_present
|
|
16
|
+
}
|
|
17
|
+
:session
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_builds_session_from_context
|
|
22
|
+
builder = FakeBuilder.new
|
|
23
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::Parity::BuildSessionStep.new
|
|
24
|
+
context = {
|
|
25
|
+
session_builder: builder,
|
|
26
|
+
left_path: "/tmp/left.csv",
|
|
27
|
+
right_path: "/tmp/right.csv",
|
|
28
|
+
col_sep: "\t",
|
|
29
|
+
headers_present: false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
result = step.call(context)
|
|
33
|
+
|
|
34
|
+
assert_nil result
|
|
35
|
+
assert_equal :session, context[:session]
|
|
36
|
+
assert_equal "/tmp/left.csv", builder.args[:left_path]
|
|
37
|
+
assert_equal "/tmp/right.csv", builder.args[:right_path]
|
|
38
|
+
assert_equal "\t", builder.args[:col_sep]
|
|
39
|
+
assert_equal false, builder.args[:headers_present]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/parity/collect_inputs_step"
|
|
5
|
+
|
|
6
|
+
class ParityCollectInputsStepTest < Minitest::Test
|
|
7
|
+
def test_collects_inputs_into_context
|
|
8
|
+
file_prompt = Object.new
|
|
9
|
+
separator_prompt = Object.new
|
|
10
|
+
headers_prompt = Object.new
|
|
11
|
+
def file_prompt.call(label:) = label.include?("Left") ? "/tmp/left.csv" : "/tmp/right.csv"
|
|
12
|
+
def separator_prompt.call = ","
|
|
13
|
+
def headers_prompt.call = true
|
|
14
|
+
|
|
15
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::Parity::CollectInputsStep.new(
|
|
16
|
+
file_path_prompt: file_prompt,
|
|
17
|
+
separator_prompt: separator_prompt,
|
|
18
|
+
headers_present_prompt: headers_prompt
|
|
19
|
+
)
|
|
20
|
+
context = {}
|
|
21
|
+
|
|
22
|
+
result = step.call(context)
|
|
23
|
+
|
|
24
|
+
assert_nil result
|
|
25
|
+
assert_equal "/tmp/left.csv", context[:left_path]
|
|
26
|
+
assert_equal "/tmp/right.csv", context[:right_path]
|
|
27
|
+
assert_equal ",", context[:col_sep]
|
|
28
|
+
assert_equal true, context[:headers_present]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/parity/execute_step"
|
|
5
|
+
|
|
6
|
+
class ParityExecuteStepTest < Minitest::Test
|
|
7
|
+
Result = Struct.new(:ok, :data) do
|
|
8
|
+
def ok? = ok
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class FakeUseCase
|
|
12
|
+
attr_reader :session
|
|
13
|
+
|
|
14
|
+
def call(session:)
|
|
15
|
+
@session = session
|
|
16
|
+
Result.new(true, { match: true })
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class FakePresenter
|
|
21
|
+
attr_reader :data
|
|
22
|
+
|
|
23
|
+
def print_summary(data)
|
|
24
|
+
@data = data
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_calls_use_case_and_presenter
|
|
29
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::Parity::ExecuteStep.new
|
|
30
|
+
use_case = FakeUseCase.new
|
|
31
|
+
presenter = FakePresenter.new
|
|
32
|
+
context = { use_case: use_case, session: :session, presenter: presenter, handle_error: ->(_r) {} }
|
|
33
|
+
|
|
34
|
+
result = step.call(context)
|
|
35
|
+
|
|
36
|
+
assert_nil result
|
|
37
|
+
assert_equal :session, use_case.session
|
|
38
|
+
assert_equal true, presenter.data[:match]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name,city
|
|
2
|
+
Name01,City01
|
|
3
|
+
Name02,City02
|
|
4
|
+
Name03,City03
|
|
5
|
+
Name04,City04
|
|
6
|
+
Name05,City05
|
|
7
|
+
Name06,City06
|
|
8
|
+
Name07,City07
|
|
9
|
+
Name08,City08
|
|
10
|
+
Name09,City09
|
|
11
|
+
Name10,City10
|
|
12
|
+
Name11,City11
|
|
13
|
+
Name12,City12
|
|
14
|
+
Name13,City13
|
|
15
|
+
Name14,City14
|
|
16
|
+
Name15,City15
|
|
17
|
+
Name16,City16
|
|
18
|
+
Name17,City17
|
|
19
|
+
Name18,City18
|
|
20
|
+
Name19,City19
|
|
21
|
+
Name20,City20
|
|
22
|
+
Name21,City21
|
|
23
|
+
Name22,City22
|
|
24
|
+
Name23,City23
|
|
25
|
+
Name24,City24
|
|
26
|
+
Name25,City25
|