csvops 0.4.0.alpha → 0.6.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 +60 -12
- data/docs/architecture.md +208 -21
- data/docs/release-v0.5.0-alpha.md +89 -0
- data/docs/release-v0.6.0-alpha.md +84 -0
- data/lib/csvtool/application/use_cases/run_cross_csv_dedupe.rb +17 -14
- data/lib/csvtool/application/use_cases/run_csv_parity.rb +70 -0
- 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 +11 -7
- 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/infrastructure/csv/csv_parity_comparator.rb +71 -0
- 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/errors/presenter.rb +4 -0
- data/lib/csvtool/interface/cli/menu_loop.rb +5 -2
- 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/csv_parity_session_builder.rb +33 -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/csv_parity_presenter.rb +38 -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_csv_parity_workflow.rb +66 -0
- 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/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/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_csv_parity_test.rb +160 -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/cli_test.rb +175 -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/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/errors/presenter_test.rb +2 -0
- data/test/csvtool/interface/cli/menu_loop_test.rb +59 -16
- 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/csv_parity_session_builder_test.rb +20 -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/csv_parity_presenter_test.rb +43 -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_csv_parity_workflow_test.rb +94 -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/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/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
- 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
- metadata +90 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/presenters/csv_parity_presenter"
|
|
5
|
+
|
|
6
|
+
class CsvParityPresenterTest < Minitest::Test
|
|
7
|
+
def test_prints_match_summary
|
|
8
|
+
out = StringIO.new
|
|
9
|
+
presenter = Csvtool::Interface::CLI::Workflows::Presenters::CsvParityPresenter.new(stdout: out)
|
|
10
|
+
|
|
11
|
+
presenter.print_summary(
|
|
12
|
+
match: true,
|
|
13
|
+
left_rows: 3,
|
|
14
|
+
right_rows: 3,
|
|
15
|
+
left_only_count: 0,
|
|
16
|
+
right_only_count: 0
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
assert_includes out.string, "MATCH"
|
|
20
|
+
assert_includes out.string, "Summary: left_rows=3 right_rows=3 left_only=0 right_only=0"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_prints_mismatch_examples
|
|
24
|
+
out = StringIO.new
|
|
25
|
+
presenter = Csvtool::Interface::CLI::Workflows::Presenters::CsvParityPresenter.new(stdout: out)
|
|
26
|
+
|
|
27
|
+
presenter.print_summary(
|
|
28
|
+
match: false,
|
|
29
|
+
left_rows: 3,
|
|
30
|
+
right_rows: 3,
|
|
31
|
+
left_only_count: 1,
|
|
32
|
+
right_only_count: 1,
|
|
33
|
+
left_only_examples: [{ row: "Cara,Berlin", count_delta: 1 }],
|
|
34
|
+
right_only_examples: [{ row: "Dina,Rome", count_delta: 1 }]
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
assert_includes out.string, "MISMATCH"
|
|
38
|
+
assert_includes out.string, "Left-only examples:"
|
|
39
|
+
assert_includes out.string, "Cara,Berlin (count +1)"
|
|
40
|
+
assert_includes out.string, "Right-only examples:"
|
|
41
|
+
assert_includes out.string, "Dina,Rome (count +1)"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/presenters/row_extraction_presenter"
|
|
5
|
+
|
|
6
|
+
class RowExtractionPresenterTest < Minitest::Test
|
|
7
|
+
def test_prints_header_once_then_rows
|
|
8
|
+
out = StringIO.new
|
|
9
|
+
presenter = Csvtool::Interface::CLI::Workflows::Presenters::RowExtractionPresenter.new(
|
|
10
|
+
stdout: out,
|
|
11
|
+
headers: ["name", "city"],
|
|
12
|
+
col_sep: ","
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
presenter.print_row(["Alice", "London"])
|
|
16
|
+
presenter.print_row(["Bob", "Paris"])
|
|
17
|
+
|
|
18
|
+
assert_equal "name,city\nAlice,London\nBob,Paris\n", out.string
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_prints_file_written_message
|
|
22
|
+
out = StringIO.new
|
|
23
|
+
presenter = Csvtool::Interface::CLI::Workflows::Presenters::RowExtractionPresenter.new(
|
|
24
|
+
stdout: out,
|
|
25
|
+
headers: ["name"],
|
|
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,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,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/run_csv_parity_workflow"
|
|
5
|
+
|
|
6
|
+
class RunCsvParityWorkflowTest < Minitest::Test
|
|
7
|
+
class FakeUseCase
|
|
8
|
+
attr_reader :calls
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@calls = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(session:)
|
|
15
|
+
@calls << session
|
|
16
|
+
Struct.new(:ok?, :data).new(true, {
|
|
17
|
+
match: true,
|
|
18
|
+
left_rows: 3,
|
|
19
|
+
right_rows: 3,
|
|
20
|
+
left_only_count: 0,
|
|
21
|
+
right_only_count: 0
|
|
22
|
+
})
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class MismatchUseCase
|
|
27
|
+
def call(session:)
|
|
28
|
+
Struct.new(:ok?, :data).new(true, {
|
|
29
|
+
match: false,
|
|
30
|
+
left_rows: 3,
|
|
31
|
+
right_rows: 3,
|
|
32
|
+
left_only_count: 1,
|
|
33
|
+
right_only_count: 1,
|
|
34
|
+
left_only_examples: [{ row: "Cara,Berlin", count_delta: 1 }],
|
|
35
|
+
right_only_examples: [{ row: "Dina,Rome", count_delta: 1 }]
|
|
36
|
+
})
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class CannotReadUseCase
|
|
41
|
+
def call(session:)
|
|
42
|
+
Struct.new(:ok?, :error, :data).new(false, :cannot_read_file, { path: "/tmp/protected.csv" })
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_prompts_for_paths_and_calls_use_case
|
|
47
|
+
stdout = StringIO.new
|
|
48
|
+
use_case = FakeUseCase.new
|
|
49
|
+
input = StringIO.new("/tmp/left.csv\n/tmp/right.csv\n2\ny\n")
|
|
50
|
+
|
|
51
|
+
Csvtool::Interface::CLI::Workflows::RunCsvParityWorkflow
|
|
52
|
+
.new(stdin: input, stdout: stdout, use_case: use_case)
|
|
53
|
+
.call
|
|
54
|
+
|
|
55
|
+
call = use_case.calls.first
|
|
56
|
+
assert_equal "/tmp/left.csv", call.source_pair.left_path
|
|
57
|
+
assert_equal "/tmp/right.csv", call.source_pair.right_path
|
|
58
|
+
assert_equal "\t", call.options.separator
|
|
59
|
+
assert_equal true, call.options.headers_present?
|
|
60
|
+
assert_includes stdout.string, "Left CSV file path: "
|
|
61
|
+
assert_includes stdout.string, "Right CSV file path: "
|
|
62
|
+
assert_includes stdout.string, "Choose separator:"
|
|
63
|
+
assert_includes stdout.string, "Headers present? [Y/n]: "
|
|
64
|
+
assert_includes stdout.string, "MATCH"
|
|
65
|
+
assert_includes stdout.string, "Summary: left_rows=3 right_rows=3 left_only=0 right_only=0"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_prints_mismatch_examples_when_not_equal
|
|
69
|
+
stdout = StringIO.new
|
|
70
|
+
input = StringIO.new("/tmp/left.csv\n/tmp/right.csv\n\ny\n")
|
|
71
|
+
|
|
72
|
+
Csvtool::Interface::CLI::Workflows::RunCsvParityWorkflow
|
|
73
|
+
.new(stdin: input, stdout: stdout, use_case: MismatchUseCase.new)
|
|
74
|
+
.call
|
|
75
|
+
|
|
76
|
+
assert_includes stdout.string, "MISMATCH"
|
|
77
|
+
assert_includes stdout.string, "Left-only examples:"
|
|
78
|
+
assert_includes stdout.string, "Cara,Berlin (count +1)"
|
|
79
|
+
assert_includes stdout.string, "Right-only examples:"
|
|
80
|
+
assert_includes stdout.string, "Dina,Rome (count +1)"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_prints_cannot_read_error_without_stacktrace
|
|
84
|
+
stdout = StringIO.new
|
|
85
|
+
input = StringIO.new("/tmp/left.csv\n/tmp/right.csv\n\ny\n")
|
|
86
|
+
|
|
87
|
+
Csvtool::Interface::CLI::Workflows::RunCsvParityWorkflow
|
|
88
|
+
.new(stdin: input, stdout: stdout, use_case: CannotReadUseCase.new)
|
|
89
|
+
.call
|
|
90
|
+
|
|
91
|
+
assert_includes stdout.string, "Cannot read file: /tmp/protected.csv"
|
|
92
|
+
refute_includes stdout.string, "Traceback"
|
|
93
|
+
end
|
|
94
|
+
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,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,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
|