csvops 0.4.0.alpha → 0.5.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 +15 -9
- data/docs/architecture.md +148 -18
- data/docs/release-v0.5.0-alpha.md +89 -0
- data/lib/csvtool/application/use_cases/run_cross_csv_dedupe.rb +17 -14
- data/lib/csvtool/application/use_cases/run_extraction.rb +63 -88
- data/lib/csvtool/application/use_cases/run_row_extraction.rb +45 -73
- data/lib/csvtool/application/use_cases/run_row_randomization.rb +56 -73
- data/lib/csvtool/cli.rb +6 -6
- data/lib/csvtool/infrastructure/output/csv_cross_csv_dedupe_file_writer.rb +23 -0
- data/lib/csvtool/infrastructure/output/csv_file_writer.rb +1 -7
- data/lib/csvtool/infrastructure/output/csv_randomized_row_file_writer.rb +23 -0
- data/lib/csvtool/infrastructure/output/csv_row_file_writer.rb +2 -9
- data/lib/csvtool/interface/cli/prompts/dedupe_key_selector_prompt.rb +30 -0
- data/lib/csvtool/interface/cli/prompts/file_path_prompt.rb +4 -2
- data/lib/csvtool/interface/cli/prompts/headers_present_prompt.rb +4 -2
- data/lib/csvtool/interface/cli/prompts/separator_prompt.rb +4 -2
- data/lib/csvtool/interface/cli/prompts/yes_no_prompt.rb +26 -0
- data/lib/csvtool/interface/cli/workflows/builders/column_session_builder.rb +32 -0
- data/lib/csvtool/interface/cli/workflows/builders/cross_csv_dedupe_session_builder.rb +35 -0
- data/lib/csvtool/interface/cli/workflows/builders/row_extraction_session_builder.rb +22 -0
- data/lib/csvtool/interface/cli/workflows/builders/row_randomization_session_builder.rb +28 -0
- data/lib/csvtool/interface/cli/workflows/presenters/column_extraction_presenter.rb +25 -0
- data/lib/csvtool/interface/cli/workflows/presenters/cross_csv_dedupe_presenter.rb +39 -0
- data/lib/csvtool/interface/cli/workflows/presenters/row_extraction_presenter.rb +34 -0
- data/lib/csvtool/interface/cli/workflows/presenters/row_randomization_presenter.rb +34 -0
- data/lib/csvtool/interface/cli/workflows/run_cross_csv_dedupe_workflow.rb +48 -125
- data/lib/csvtool/interface/cli/workflows/run_extraction_workflow.rb +88 -0
- data/lib/csvtool/interface/cli/workflows/run_row_extraction_workflow.rb +86 -0
- data/lib/csvtool/interface/cli/workflows/run_row_randomization_workflow.rb +80 -0
- data/lib/csvtool/interface/cli/workflows/steps/cross_csv_dedupe/collect_options_step.rb +55 -0
- data/lib/csvtool/interface/cli/workflows/steps/cross_csv_dedupe/collect_profiles_step.rb +52 -0
- data/lib/csvtool/interface/cli/workflows/steps/cross_csv_dedupe/execute_step.rb +34 -0
- data/lib/csvtool/interface/cli/workflows/steps/extraction/build_preview_step.rb +40 -0
- data/lib/csvtool/interface/cli/workflows/steps/extraction/collect_destination_step.rb +28 -0
- data/lib/csvtool/interface/cli/workflows/steps/extraction/collect_inputs_step.rb +47 -0
- data/lib/csvtool/interface/cli/workflows/steps/extraction/execute_step.rb +32 -0
- data/lib/csvtool/interface/cli/workflows/steps/row_extraction/collect_destination_step.rb +33 -0
- data/lib/csvtool/interface/cli/workflows/steps/row_extraction/collect_range_step.rb +35 -0
- data/lib/csvtool/interface/cli/workflows/steps/row_extraction/collect_source_step.rb +32 -0
- data/lib/csvtool/interface/cli/workflows/steps/row_extraction/execute_step.rb +43 -0
- data/lib/csvtool/interface/cli/workflows/steps/row_extraction/read_headers_step.rb +29 -0
- data/lib/csvtool/interface/cli/workflows/steps/row_randomization/collect_destination_step.rb +34 -0
- data/lib/csvtool/interface/cli/workflows/steps/row_randomization/collect_inputs_step.rb +49 -0
- data/lib/csvtool/interface/cli/workflows/steps/row_randomization/execute_step.rb +37 -0
- data/lib/csvtool/interface/cli/workflows/steps/workflow_step_pipeline.rb +25 -0
- data/lib/csvtool/interface/cli/workflows/support/output_destination_mapper.rb +23 -0
- data/lib/csvtool/interface/cli/workflows/support/result_error_handler.rb +22 -0
- data/lib/csvtool/version.rb +1 -1
- data/test/csvtool/application/use_cases/io_boundary_test.rb +26 -0
- data/test/csvtool/application/use_cases/run_cross_csv_dedupe_test.rb +28 -0
- data/test/csvtool/application/use_cases/run_extraction_test.rb +72 -16
- data/test/csvtool/application/use_cases/run_row_extraction_test.rb +82 -102
- data/test/csvtool/application/use_cases/run_row_randomization_test.rb +96 -86
- data/test/csvtool/infrastructure/output/csv_cross_csv_dedupe_file_writer_test.rb +32 -0
- data/test/csvtool/infrastructure/output/csv_file_writer_test.rb +0 -4
- data/test/csvtool/infrastructure/output/csv_randomized_row_file_writer_test.rb +32 -0
- data/test/csvtool/infrastructure/output/csv_row_file_writer_test.rb +1 -4
- data/test/csvtool/interface/cli/prompts/dedupe_key_selector_prompt_test.rb +30 -0
- data/test/csvtool/interface/cli/prompts/file_path_prompt_test.rb +9 -0
- data/test/csvtool/interface/cli/prompts/headers_present_prompt_test.rb +10 -0
- data/test/csvtool/interface/cli/prompts/separator_prompt_test.rb +10 -0
- data/test/csvtool/interface/cli/prompts/yes_no_prompt_test.rb +22 -0
- data/test/csvtool/interface/cli/workflows/builders/column_session_builder_test.rb +17 -0
- data/test/csvtool/interface/cli/workflows/builders/cross_csv_dedupe_session_builder_test.rb +36 -0
- data/test/csvtool/interface/cli/workflows/builders/row_extraction_session_builder_test.rb +21 -0
- data/test/csvtool/interface/cli/workflows/builders/row_randomization_session_builder_test.rb +26 -0
- data/test/csvtool/interface/cli/workflows/presenters/column_extraction_presenter_test.rb +24 -0
- data/test/csvtool/interface/cli/workflows/presenters/cross_csv_dedupe_presenter_test.rb +30 -0
- data/test/csvtool/interface/cli/workflows/presenters/row_extraction_presenter_test.rb +33 -0
- data/test/csvtool/interface/cli/workflows/presenters/row_randomization_presenter_test.rb +33 -0
- data/test/csvtool/interface/cli/workflows/run_extraction_workflow_test.rb +56 -0
- data/test/csvtool/interface/cli/workflows/run_row_extraction_workflow_test.rb +83 -0
- data/test/csvtool/interface/cli/workflows/run_row_randomization_workflow_test.rb +69 -0
- data/test/csvtool/interface/cli/workflows/steps/cross_csv_dedupe/collect_options_step_test.rb +41 -0
- data/test/csvtool/interface/cli/workflows/steps/extraction/collect_inputs_step_test.rb +66 -0
- data/test/csvtool/interface/cli/workflows/steps/row_extraction/collect_source_step_test.rb +39 -0
- data/test/csvtool/interface/cli/workflows/steps/row_extraction/execute_step_test.rb +91 -0
- data/test/csvtool/interface/cli/workflows/steps/row_extraction/read_headers_step_test.rb +57 -0
- data/test/csvtool/interface/cli/workflows/steps/row_randomization/collect_inputs_step_test.rb +37 -0
- data/test/csvtool/interface/cli/workflows/steps/workflow_step_pipeline_test.rb +30 -0
- data/test/csvtool/interface/cli/workflows/support/output_destination_mapper_test.rb +23 -0
- data/test/csvtool/interface/cli/workflows/support/result_error_handler_test.rb +34 -0
- metadata +60 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/presenters/row_randomization_presenter"
|
|
5
|
+
|
|
6
|
+
class RowRandomizationPresenterTest < Minitest::Test
|
|
7
|
+
def test_prints_console_start_and_rows
|
|
8
|
+
out = StringIO.new
|
|
9
|
+
presenter = Csvtool::Interface::CLI::Workflows::Presenters::RowRandomizationPresenter.new(
|
|
10
|
+
stdout: out,
|
|
11
|
+
headers: ["name", "city"],
|
|
12
|
+
col_sep: ","
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
presenter.print_console_start
|
|
16
|
+
presenter.print_row(["Alice", "London"])
|
|
17
|
+
|
|
18
|
+
assert_equal "\nname,city\nAlice,London\n", out.string
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_prints_file_written_message
|
|
22
|
+
out = StringIO.new
|
|
23
|
+
presenter = Csvtool::Interface::CLI::Workflows::Presenters::RowRandomizationPresenter.new(
|
|
24
|
+
stdout: out,
|
|
25
|
+
headers: nil,
|
|
26
|
+
col_sep: ","
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
presenter.print_file_written("/tmp/out.csv")
|
|
30
|
+
|
|
31
|
+
assert_includes out.string, "Wrote output to /tmp/out.csv"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/run_extraction_workflow"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
class RunExtractionWorkflowTest < Minitest::Test
|
|
8
|
+
def fixture_path(name)
|
|
9
|
+
File.expand_path("../../../../fixtures/#{name}", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_missing_file_path_reports_error
|
|
13
|
+
out = StringIO.new
|
|
14
|
+
workflow = Csvtool::Interface::CLI::Workflows::RunExtractionWorkflow.new(
|
|
15
|
+
stdin: StringIO.new("/tmp/not-present.csv\n\n"),
|
|
16
|
+
stdout: out
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
workflow.call
|
|
20
|
+
|
|
21
|
+
assert_includes out.string, "File not found: /tmp/not-present.csv"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_workflow_can_run_console_happy_path
|
|
25
|
+
out = StringIO.new
|
|
26
|
+
fixture = fixture_path("sample_people.csv")
|
|
27
|
+
input = ["#{fixture}", "1", "", "1", "", "y", ""].join("\n") + "\n"
|
|
28
|
+
|
|
29
|
+
Csvtool::Interface::CLI::Workflows::RunExtractionWorkflow.new(
|
|
30
|
+
stdin: StringIO.new(input),
|
|
31
|
+
stdout: out
|
|
32
|
+
).call
|
|
33
|
+
|
|
34
|
+
assert_includes out.string, "Alice"
|
|
35
|
+
assert_includes out.string, "Bob"
|
|
36
|
+
assert_includes out.string, "Cara"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_workflow_can_write_output_file
|
|
40
|
+
out = StringIO.new
|
|
41
|
+
|
|
42
|
+
Dir.mktmpdir do |dir|
|
|
43
|
+
output_path = File.join(dir, "names.csv")
|
|
44
|
+
fixture = fixture_path("sample_people.csv")
|
|
45
|
+
input = ["#{fixture}", "1", "", "1", "", "y", "2", output_path].join("\n") + "\n"
|
|
46
|
+
|
|
47
|
+
Csvtool::Interface::CLI::Workflows::RunExtractionWorkflow.new(
|
|
48
|
+
stdin: StringIO.new(input),
|
|
49
|
+
stdout: out
|
|
50
|
+
).call
|
|
51
|
+
|
|
52
|
+
assert_includes out.string, "Wrote output to #{output_path}"
|
|
53
|
+
assert_equal "name\nAlice\nBob\nCara\n", File.read(output_path)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/run_row_extraction_workflow"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
class RunRowExtractionWorkflowTest < Minitest::Test
|
|
8
|
+
def fixture_path(name)
|
|
9
|
+
File.expand_path("../../../../fixtures/#{name}", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_missing_file_path_reports_error
|
|
13
|
+
out = StringIO.new
|
|
14
|
+
workflow = Csvtool::Interface::CLI::Workflows::RunRowExtractionWorkflow.new(
|
|
15
|
+
stdin: StringIO.new("/tmp/not-present.csv\n\n"),
|
|
16
|
+
stdout: out
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
workflow.call
|
|
20
|
+
|
|
21
|
+
assert_includes out.string, "File not found: /tmp/not-present.csv"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_workflow_can_run_console_happy_path
|
|
25
|
+
out = StringIO.new
|
|
26
|
+
fixture = fixture_path("sample_people.csv")
|
|
27
|
+
input = [fixture, "", "2", "3", ""].join("\n") + "\n"
|
|
28
|
+
|
|
29
|
+
Csvtool::Interface::CLI::Workflows::RunRowExtractionWorkflow.new(
|
|
30
|
+
stdin: StringIO.new(input),
|
|
31
|
+
stdout: out
|
|
32
|
+
).call
|
|
33
|
+
|
|
34
|
+
assert_includes out.string, "name,city"
|
|
35
|
+
assert_includes out.string, "Bob,Paris"
|
|
36
|
+
assert_includes out.string, "Cara,Berlin"
|
|
37
|
+
refute_includes out.string, "Alice,London"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_workflow_can_write_output_file
|
|
41
|
+
out = StringIO.new
|
|
42
|
+
|
|
43
|
+
Dir.mktmpdir do |dir|
|
|
44
|
+
output_path = File.join(dir, "rows.csv")
|
|
45
|
+
fixture = fixture_path("sample_people.csv")
|
|
46
|
+
input = [fixture, "", "2", "3", "2", output_path].join("\n") + "\n"
|
|
47
|
+
|
|
48
|
+
Csvtool::Interface::CLI::Workflows::RunRowExtractionWorkflow.new(
|
|
49
|
+
stdin: StringIO.new(input),
|
|
50
|
+
stdout: out
|
|
51
|
+
).call
|
|
52
|
+
|
|
53
|
+
assert_includes out.string, "Wrote output to #{output_path}"
|
|
54
|
+
assert_equal "name,city\nBob,Paris\nCara,Berlin\n", File.read(output_path)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_rejects_non_numeric_start_row
|
|
59
|
+
out = StringIO.new
|
|
60
|
+
fixture = fixture_path("sample_people.csv")
|
|
61
|
+
input = [fixture, "", "abc", "3", ""].join("\n") + "\n"
|
|
62
|
+
|
|
63
|
+
Csvtool::Interface::CLI::Workflows::RunRowExtractionWorkflow.new(
|
|
64
|
+
stdin: StringIO.new(input),
|
|
65
|
+
stdout: out
|
|
66
|
+
).call
|
|
67
|
+
|
|
68
|
+
assert_includes out.string, "Start row must be a positive integer."
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_reports_out_of_bounds_range
|
|
72
|
+
out = StringIO.new
|
|
73
|
+
fixture = fixture_path("sample_people.csv")
|
|
74
|
+
input = [fixture, "", "10", "12", ""].join("\n") + "\n"
|
|
75
|
+
|
|
76
|
+
Csvtool::Interface::CLI::Workflows::RunRowExtractionWorkflow.new(
|
|
77
|
+
stdin: StringIO.new(input),
|
|
78
|
+
stdout: out
|
|
79
|
+
).call
|
|
80
|
+
|
|
81
|
+
assert_includes out.string, "Row range is out of bounds. File has 3 data rows."
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/run_row_randomization_workflow"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
class RunRowRandomizationWorkflowTest < Minitest::Test
|
|
8
|
+
def fixture_path(name)
|
|
9
|
+
File.expand_path("../../../../fixtures/#{name}", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_missing_file_shows_friendly_error
|
|
13
|
+
output = StringIO.new
|
|
14
|
+
input = StringIO.new("/tmp/does-not-exist.csv\n\n")
|
|
15
|
+
|
|
16
|
+
Csvtool::Interface::CLI::Workflows::RunRowRandomizationWorkflow.new(stdin: input, stdout: output).call
|
|
17
|
+
|
|
18
|
+
assert_includes output.string, "File not found: /tmp/does-not-exist.csv"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_workflow_prints_header_then_all_randomized_rows
|
|
22
|
+
output = StringIO.new
|
|
23
|
+
input = StringIO.new([fixture_path("sample_people.csv"), "", "", "", ""].join("\n") + "\n")
|
|
24
|
+
|
|
25
|
+
Csvtool::Interface::CLI::Workflows::RunRowRandomizationWorkflow.new(stdin: input, stdout: output).call
|
|
26
|
+
|
|
27
|
+
assert_includes output.string, "name,city"
|
|
28
|
+
assert_includes output.string, "Alice,London"
|
|
29
|
+
assert_includes output.string, "Bob,Paris"
|
|
30
|
+
assert_includes output.string, "Cara,Berlin"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_workflow_can_write_randomized_rows_to_file
|
|
34
|
+
output = StringIO.new
|
|
35
|
+
|
|
36
|
+
Dir.mktmpdir do |dir|
|
|
37
|
+
output_path = File.join(dir, "randomized.csv")
|
|
38
|
+
input = StringIO.new([fixture_path("sample_people.csv"), "", "", "", "2", output_path].join("\n") + "\n")
|
|
39
|
+
|
|
40
|
+
Csvtool::Interface::CLI::Workflows::RunRowRandomizationWorkflow.new(stdin: input, stdout: output).call
|
|
41
|
+
|
|
42
|
+
written = File.read(output_path).lines.map(&:strip)
|
|
43
|
+
assert_equal "name,city", written.first
|
|
44
|
+
assert_equal ["Alice,London", "Bob,Paris", "Cara,Berlin"].sort, written[1..].sort
|
|
45
|
+
assert_includes output.string, "Wrote output to #{output_path}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_workflow_supports_headerless_mode
|
|
50
|
+
output = StringIO.new
|
|
51
|
+
input = StringIO.new([fixture_path("sample_people_no_headers.csv"), "", "n", "", ""].join("\n") + "\n")
|
|
52
|
+
|
|
53
|
+
Csvtool::Interface::CLI::Workflows::RunRowRandomizationWorkflow.new(stdin: input, stdout: output).call
|
|
54
|
+
|
|
55
|
+
refute_includes output.string, "name,city"
|
|
56
|
+
assert_includes output.string, "Alice,London"
|
|
57
|
+
assert_includes output.string, "Bob,Paris"
|
|
58
|
+
assert_includes output.string, "Cara,Berlin"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_invalid_seed_shows_friendly_error
|
|
62
|
+
output = StringIO.new
|
|
63
|
+
input = StringIO.new([fixture_path("sample_people.csv"), "", "", "abc"].join("\n") + "\n")
|
|
64
|
+
|
|
65
|
+
Csvtool::Interface::CLI::Workflows::RunRowRandomizationWorkflow.new(stdin: input, stdout: output).call
|
|
66
|
+
|
|
67
|
+
assert_includes output.string, "Seed must be an integer."
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/cross_csv_dedupe/collect_options_step"
|
|
5
|
+
require "csvtool/domain/cross_csv_dedupe_session/csv_profile"
|
|
6
|
+
|
|
7
|
+
class CrossCsvDedupeCollectOptionsStepTest < Minitest::Test
|
|
8
|
+
class FakeErrors
|
|
9
|
+
attr_reader :column_not_found_called
|
|
10
|
+
|
|
11
|
+
def column_not_found
|
|
12
|
+
@column_not_found_called = true
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_halts_when_source_selector_invalid
|
|
17
|
+
selector_prompt = Object.new
|
|
18
|
+
yes_no_prompt = Object.new
|
|
19
|
+
output_destination_prompt = Object.new
|
|
20
|
+
session_builder = Object.new
|
|
21
|
+
mapper = Object.new
|
|
22
|
+
errors = FakeErrors.new
|
|
23
|
+
|
|
24
|
+
def selector_prompt.call(label:, headers_present:) = nil
|
|
25
|
+
|
|
26
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::CrossCsvDedupe::CollectOptionsStep.new(
|
|
27
|
+
selector_prompt: selector_prompt,
|
|
28
|
+
yes_no_prompt: yes_no_prompt,
|
|
29
|
+
output_destination_prompt: output_destination_prompt,
|
|
30
|
+
errors: errors
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
source = Csvtool::Domain::CrossCsvDedupeSession::CsvProfile.new(path: "/tmp/a.csv", separator: ",", headers_present: true)
|
|
34
|
+
reference = Csvtool::Domain::CrossCsvDedupeSession::CsvProfile.new(path: "/tmp/b.csv", separator: ",", headers_present: true)
|
|
35
|
+
|
|
36
|
+
result = step.call(source: source, reference: reference, session_builder: session_builder, output_destination_mapper: mapper)
|
|
37
|
+
|
|
38
|
+
assert_equal :halt, result
|
|
39
|
+
assert_equal true, errors.column_not_found_called
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/extraction/collect_inputs_step"
|
|
5
|
+
|
|
6
|
+
class ExtractionCollectInputsStepTest < Minitest::Test
|
|
7
|
+
Result = Struct.new(:ok, :data) do
|
|
8
|
+
def ok? = ok
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class FakeUseCase
|
|
12
|
+
def initialize(result)
|
|
13
|
+
@result = result
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def read_headers(file_path:, col_sep:)
|
|
17
|
+
@result
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_halts_when_separator_missing
|
|
22
|
+
file_prompt = Object.new
|
|
23
|
+
separator_prompt = Object.new
|
|
24
|
+
selector_prompt = Object.new
|
|
25
|
+
skip_prompt = Object.new
|
|
26
|
+
def file_prompt.call = "/tmp/data.csv"
|
|
27
|
+
def separator_prompt.call = nil
|
|
28
|
+
|
|
29
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::Extraction::CollectInputsStep.new(
|
|
30
|
+
file_path_prompt: file_prompt,
|
|
31
|
+
separator_prompt: separator_prompt,
|
|
32
|
+
column_selector_prompt: selector_prompt,
|
|
33
|
+
skip_blanks_prompt: skip_prompt
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
assert_equal :halt, step.call(
|
|
37
|
+
use_case: FakeUseCase.new(Result.new(true, { headers: [] })),
|
|
38
|
+
session_builder: Object.new,
|
|
39
|
+
handle_error: ->(_r) {}
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_halts_when_header_read_fails
|
|
44
|
+
file_prompt = Object.new
|
|
45
|
+
separator_prompt = Object.new
|
|
46
|
+
selector_prompt = Object.new
|
|
47
|
+
skip_prompt = Object.new
|
|
48
|
+
builder = Object.new
|
|
49
|
+
handled = []
|
|
50
|
+
def file_prompt.call = "/tmp/data.csv"
|
|
51
|
+
def separator_prompt.call = ","
|
|
52
|
+
|
|
53
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::Extraction::CollectInputsStep.new(
|
|
54
|
+
file_path_prompt: file_prompt,
|
|
55
|
+
separator_prompt: separator_prompt,
|
|
56
|
+
column_selector_prompt: selector_prompt,
|
|
57
|
+
skip_blanks_prompt: skip_prompt
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
fail_result = Result.new(false, {})
|
|
61
|
+
result = step.call(use_case: FakeUseCase.new(fail_result), session_builder: builder, handle_error: ->(r) { handled << r })
|
|
62
|
+
|
|
63
|
+
assert_equal :halt, result
|
|
64
|
+
assert_equal [fail_result], handled
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/row_extraction/collect_source_step"
|
|
5
|
+
|
|
6
|
+
class CollectSourceStepTest < Minitest::Test
|
|
7
|
+
def test_collects_file_and_separator
|
|
8
|
+
file_prompt = Object.new
|
|
9
|
+
separator_prompt = Object.new
|
|
10
|
+
def file_prompt.call = "/tmp/data.csv"
|
|
11
|
+
def separator_prompt.call = ","
|
|
12
|
+
|
|
13
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::RowExtraction::CollectSourceStep.new(
|
|
14
|
+
file_path_prompt: file_prompt,
|
|
15
|
+
separator_prompt: separator_prompt
|
|
16
|
+
)
|
|
17
|
+
context = {}
|
|
18
|
+
|
|
19
|
+
result = step.call(context)
|
|
20
|
+
|
|
21
|
+
assert_nil result
|
|
22
|
+
assert_equal "/tmp/data.csv", context[:file_path]
|
|
23
|
+
assert_equal ",", context[:col_sep]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_halts_when_separator_missing
|
|
27
|
+
file_prompt = Object.new
|
|
28
|
+
separator_prompt = Object.new
|
|
29
|
+
def file_prompt.call = "/tmp/data.csv"
|
|
30
|
+
def separator_prompt.call = nil
|
|
31
|
+
|
|
32
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::RowExtraction::CollectSourceStep.new(
|
|
33
|
+
file_path_prompt: file_prompt,
|
|
34
|
+
separator_prompt: separator_prompt
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
assert_equal :halt, step.call({})
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/row_extraction/execute_step"
|
|
5
|
+
|
|
6
|
+
class ExecuteStepTest < Minitest::Test
|
|
7
|
+
Result = Struct.new(:ok, :data) do
|
|
8
|
+
def ok? = ok
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class FakeUseCase
|
|
12
|
+
def initialize(result)
|
|
13
|
+
@result = result
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def extract(session:, headers:, on_row:)
|
|
17
|
+
@called = true
|
|
18
|
+
on_row.call(["Bob", "Paris"]) if @result.ok?
|
|
19
|
+
@result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :called
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class FakePresenter
|
|
26
|
+
attr_reader :rows, :written
|
|
27
|
+
|
|
28
|
+
def initialize(stdout:, headers:, col_sep:)
|
|
29
|
+
@rows = []
|
|
30
|
+
@written = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def print_row(fields)
|
|
34
|
+
@rows << fields
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def print_file_written(path)
|
|
38
|
+
@written = path
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class FakeErrors
|
|
43
|
+
attr_reader :out_of_bounds
|
|
44
|
+
|
|
45
|
+
def row_range_out_of_bounds(count)
|
|
46
|
+
@out_of_bounds = count
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_prints_rows_and_reports_out_of_bounds
|
|
51
|
+
errors = FakeErrors.new
|
|
52
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::RowExtraction::ExecuteStep.new(
|
|
53
|
+
stdout: StringIO.new,
|
|
54
|
+
errors: errors,
|
|
55
|
+
presenter_class: FakePresenter
|
|
56
|
+
)
|
|
57
|
+
use_case = FakeUseCase.new(Result.new(true, { matched: false, row_count: 3, wrote_rows: false }))
|
|
58
|
+
context = {
|
|
59
|
+
session: Object.new,
|
|
60
|
+
headers: ["name", "city"],
|
|
61
|
+
use_case: use_case,
|
|
62
|
+
handle_error: ->(_r) { raise "unexpected" }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
result = step.call(context)
|
|
66
|
+
|
|
67
|
+
assert_nil result
|
|
68
|
+
assert_equal 3, errors.out_of_bounds
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_halts_on_use_case_failure
|
|
72
|
+
handled = []
|
|
73
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::RowExtraction::ExecuteStep.new(
|
|
74
|
+
stdout: StringIO.new,
|
|
75
|
+
errors: FakeErrors.new,
|
|
76
|
+
presenter_class: FakePresenter
|
|
77
|
+
)
|
|
78
|
+
fail_result = Result.new(false, {})
|
|
79
|
+
use_case = FakeUseCase.new(fail_result)
|
|
80
|
+
|
|
81
|
+
result = step.call(
|
|
82
|
+
session: Object.new,
|
|
83
|
+
headers: ["name", "city"],
|
|
84
|
+
use_case: use_case,
|
|
85
|
+
handle_error: ->(r) { handled << r }
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
assert_equal :halt, result
|
|
89
|
+
assert_equal [fail_result], handled
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/row_extraction/read_headers_step"
|
|
5
|
+
|
|
6
|
+
class ReadHeadersStepTest < Minitest::Test
|
|
7
|
+
Result = Struct.new(:ok, :data) do
|
|
8
|
+
def ok? = ok
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class FakeUseCase
|
|
12
|
+
def initialize(result)
|
|
13
|
+
@result = result
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def read_headers(file_path:, col_sep:)
|
|
17
|
+
@file_path = file_path
|
|
18
|
+
@col_sep = col_sep
|
|
19
|
+
@result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :file_path, :col_sep
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_sets_headers_on_success
|
|
26
|
+
use_case = FakeUseCase.new(Result.new(true, { headers: ["name", "city"] }))
|
|
27
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::RowExtraction::ReadHeadersStep.new
|
|
28
|
+
context = {
|
|
29
|
+
use_case: use_case,
|
|
30
|
+
file_path: "/tmp/data.csv",
|
|
31
|
+
col_sep: ",",
|
|
32
|
+
handle_error: ->(_result) { raise "should not be called" }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
result = step.call(context)
|
|
36
|
+
|
|
37
|
+
assert_nil result
|
|
38
|
+
assert_equal ["name", "city"], context[:headers]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_halts_and_routes_error_when_failure
|
|
42
|
+
failing_result = Result.new(false, {})
|
|
43
|
+
use_case = FakeUseCase.new(failing_result)
|
|
44
|
+
handled = []
|
|
45
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::RowExtraction::ReadHeadersStep.new
|
|
46
|
+
|
|
47
|
+
result = step.call(
|
|
48
|
+
use_case: use_case,
|
|
49
|
+
file_path: "/tmp/data.csv",
|
|
50
|
+
col_sep: ",",
|
|
51
|
+
handle_error: ->(r) { handled << r }
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
assert_equal :halt, result
|
|
55
|
+
assert_equal [failing_result], handled
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/row_randomization/collect_inputs_step"
|
|
5
|
+
require "csvtool/interface/cli/prompts/seed_prompt"
|
|
6
|
+
|
|
7
|
+
class RowRandomizationCollectInputsStepTest < Minitest::Test
|
|
8
|
+
Result = Struct.new(:ok, :data) do
|
|
9
|
+
def ok? = ok
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class FakeUseCase
|
|
13
|
+
def read_headers(file_path:, col_sep:, headers_present:)
|
|
14
|
+
Result.new(true, { headers: ["name"] })
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_halts_when_seed_invalid
|
|
19
|
+
file_prompt = Object.new
|
|
20
|
+
separator_prompt = Object.new
|
|
21
|
+
headers_prompt = Object.new
|
|
22
|
+
seed_prompt = Object.new
|
|
23
|
+
def file_prompt.call = "/tmp/data.csv"
|
|
24
|
+
def separator_prompt.call = ","
|
|
25
|
+
def headers_prompt.call = true
|
|
26
|
+
def seed_prompt.call = Csvtool::Interface::CLI::Prompts::SeedPrompt::INVALID
|
|
27
|
+
|
|
28
|
+
step = Csvtool::Interface::CLI::Workflows::Steps::RowRandomization::CollectInputsStep.new(
|
|
29
|
+
file_path_prompt: file_prompt,
|
|
30
|
+
separator_prompt: separator_prompt,
|
|
31
|
+
headers_present_prompt: headers_prompt,
|
|
32
|
+
seed_prompt: seed_prompt
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
assert_equal :halt, step.call(use_case: FakeUseCase.new, handle_error: ->(_r) {})
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/steps/workflow_step_pipeline"
|
|
5
|
+
|
|
6
|
+
class WorkflowStepPipelineTest < Minitest::Test
|
|
7
|
+
def test_runs_all_steps_when_no_halt
|
|
8
|
+
calls = []
|
|
9
|
+
step_1 = ->(_ctx) { calls << :one; nil }
|
|
10
|
+
step_2 = ->(_ctx) { calls << :two; nil }
|
|
11
|
+
pipeline = Csvtool::Interface::CLI::Workflows::Steps::WorkflowStepPipeline.new(steps: [step_1, step_2])
|
|
12
|
+
|
|
13
|
+
result = pipeline.call({})
|
|
14
|
+
|
|
15
|
+
assert_equal true, result
|
|
16
|
+
assert_equal %i[one two], calls
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_stops_on_halt
|
|
20
|
+
calls = []
|
|
21
|
+
step_1 = ->(_ctx) { calls << :one; :halt }
|
|
22
|
+
step_2 = ->(_ctx) { calls << :two; nil }
|
|
23
|
+
pipeline = Csvtool::Interface::CLI::Workflows::Steps::WorkflowStepPipeline.new(steps: [step_1, step_2])
|
|
24
|
+
|
|
25
|
+
result = pipeline.call({})
|
|
26
|
+
|
|
27
|
+
assert_equal false, result
|
|
28
|
+
assert_equal [:one], calls
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/support/output_destination_mapper"
|
|
5
|
+
|
|
6
|
+
class OutputDestinationMapperTest < Minitest::Test
|
|
7
|
+
def test_maps_console_destination
|
|
8
|
+
mapper = Csvtool::Interface::CLI::Workflows::Support::OutputDestinationMapper.new
|
|
9
|
+
|
|
10
|
+
destination = mapper.call({ mode: :console })
|
|
11
|
+
|
|
12
|
+
assert_equal true, destination.console?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_maps_file_destination
|
|
16
|
+
mapper = Csvtool::Interface::CLI::Workflows::Support::OutputDestinationMapper.new
|
|
17
|
+
|
|
18
|
+
destination = mapper.call({ mode: :file, path: "/tmp/out.csv" })
|
|
19
|
+
|
|
20
|
+
assert_equal true, destination.file?
|
|
21
|
+
assert_equal "/tmp/out.csv", destination.path
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/support/result_error_handler"
|
|
5
|
+
|
|
6
|
+
class ResultErrorHandlerTest < Minitest::Test
|
|
7
|
+
Result = Struct.new(:error)
|
|
8
|
+
|
|
9
|
+
def test_dispatches_mapped_error_action
|
|
10
|
+
calls = []
|
|
11
|
+
errors = Object.new
|
|
12
|
+
handler = Csvtool::Interface::CLI::Workflows::Support::ResultErrorHandler.new(errors: errors)
|
|
13
|
+
result = Result.new(:no_headers)
|
|
14
|
+
|
|
15
|
+
handler.call(result, {
|
|
16
|
+
no_headers: ->(_r, _e) { calls << :called }
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
assert_equal [:called], calls
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_ignores_unmapped_error
|
|
23
|
+
calls = []
|
|
24
|
+
errors = Object.new
|
|
25
|
+
handler = Csvtool::Interface::CLI::Workflows::Support::ResultErrorHandler.new(errors: errors)
|
|
26
|
+
result = Result.new(:unknown)
|
|
27
|
+
|
|
28
|
+
handler.call(result, {
|
|
29
|
+
no_headers: ->(_r, _e) { calls << :called }
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
assert_empty calls
|
|
33
|
+
end
|
|
34
|
+
end
|