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,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csvtool
|
|
4
|
+
module Interface
|
|
5
|
+
module CLI
|
|
6
|
+
module Workflows
|
|
7
|
+
module Steps
|
|
8
|
+
class WorkflowStepPipeline
|
|
9
|
+
def initialize(steps:)
|
|
10
|
+
@steps = steps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(context)
|
|
14
|
+
@steps.each do |step|
|
|
15
|
+
return false if step.call(context) == :halt
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csvtool/domain/shared/output_destination"
|
|
4
|
+
|
|
5
|
+
module Csvtool
|
|
6
|
+
module Interface
|
|
7
|
+
module CLI
|
|
8
|
+
module Workflows
|
|
9
|
+
module Support
|
|
10
|
+
class OutputDestinationMapper
|
|
11
|
+
def call(output_destination)
|
|
12
|
+
if output_destination[:mode] == :file
|
|
13
|
+
Domain::Shared::OutputDestination.file(path: output_destination[:path])
|
|
14
|
+
else
|
|
15
|
+
Domain::Shared::OutputDestination.console
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csvtool
|
|
4
|
+
module Interface
|
|
5
|
+
module CLI
|
|
6
|
+
module Workflows
|
|
7
|
+
module Support
|
|
8
|
+
class ResultErrorHandler
|
|
9
|
+
def initialize(errors:)
|
|
10
|
+
@errors = errors
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(result, mapping)
|
|
14
|
+
action = mapping[result.error]
|
|
15
|
+
action&.call(result, @errors)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/csvtool/version.rb
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
|
|
5
|
+
class UseCaseIoBoundaryTest < Minitest::Test
|
|
6
|
+
USE_CASE_GLOB = File.expand_path("../../../../lib/csvtool/application/use_cases/*.rb", __dir__)
|
|
7
|
+
FORBIDDEN_PATTERNS = [
|
|
8
|
+
/CSV\.open/,
|
|
9
|
+
/File\.open\([^)]*,\s*["']w/,
|
|
10
|
+
/File\.write\(/,
|
|
11
|
+
/IO\.write\(/
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def test_use_cases_do_not_perform_direct_file_writes
|
|
15
|
+
violations = []
|
|
16
|
+
|
|
17
|
+
Dir.glob(USE_CASE_GLOB).sort.each do |file_path|
|
|
18
|
+
content = File.read(file_path)
|
|
19
|
+
FORBIDDEN_PATTERNS.each do |pattern|
|
|
20
|
+
violations << "#{file_path}: #{pattern.inspect}" if content.match?(pattern)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
assert_equal [], violations, "Found forbidden direct write APIs in use cases:\n#{violations.join("\n")}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -11,6 +11,12 @@ require "csvtool/domain/shared/output_destination"
|
|
|
11
11
|
require "tmpdir"
|
|
12
12
|
|
|
13
13
|
class RunCrossCsvDedupeTest < Minitest::Test
|
|
14
|
+
class RaisingWriter
|
|
15
|
+
def call(**_kwargs)
|
|
16
|
+
raise Errno::ENOENT
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
14
20
|
def fixture_path(name)
|
|
15
21
|
File.expand_path("../../../fixtures/#{name}", __dir__)
|
|
16
22
|
end
|
|
@@ -78,6 +84,28 @@ class RunCrossCsvDedupeTest < Minitest::Test
|
|
|
78
84
|
assert_equal :column_not_found, result.error
|
|
79
85
|
end
|
|
80
86
|
|
|
87
|
+
def test_returns_cannot_write_output_file_when_writer_fails
|
|
88
|
+
use_case = Csvtool::Application::UseCases::RunCrossCsvDedupe.new(
|
|
89
|
+
csv_cross_csv_dedupe_file_writer: RaisingWriter.new
|
|
90
|
+
)
|
|
91
|
+
output_path = "/tmp/deduped.csv"
|
|
92
|
+
|
|
93
|
+
result = use_case.call(
|
|
94
|
+
session: build_session(
|
|
95
|
+
source_path: fixture_path("dedupe_source.csv"),
|
|
96
|
+
reference_path: fixture_path("dedupe_reference.csv"),
|
|
97
|
+
source_selector_input: "customer_id",
|
|
98
|
+
reference_selector_input: "external_id",
|
|
99
|
+
output_destination: Csvtool::Domain::Shared::OutputDestination.file(path: output_path)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
refute result.ok?
|
|
104
|
+
assert_equal :cannot_write_output_file, result.error
|
|
105
|
+
assert_equal output_path, result.data[:path]
|
|
106
|
+
assert_equal Errno::ENOENT, result.data[:error_class]
|
|
107
|
+
end
|
|
108
|
+
|
|
81
109
|
private
|
|
82
110
|
|
|
83
111
|
def build_session(source_path:, reference_path:, source_selector_input:, reference_selector_input:, output_destination:)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/application/use_cases/run_csv_parity"
|
|
5
|
+
require "csvtool/domain/csv_parity_session/source_pair"
|
|
6
|
+
require "csvtool/domain/csv_parity_session/parity_options"
|
|
7
|
+
require "csvtool/domain/csv_parity_session/parity_session"
|
|
8
|
+
|
|
9
|
+
class RunCsvParityTest < Minitest::Test
|
|
10
|
+
class EaccesComparator
|
|
11
|
+
def call(left_path:, right_path:, col_sep:, headers_present:, sample_limit: 5)
|
|
12
|
+
error = Errno::EACCES.new("/tmp/protected.csv")
|
|
13
|
+
def error.path
|
|
14
|
+
"/tmp/protected.csv"
|
|
15
|
+
end
|
|
16
|
+
raise error
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fixture_path(name)
|
|
21
|
+
File.expand_path("../../../fixtures/#{name}", __dir__)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def build_session(left_path:, right_path:, separator: ",", headers_present: true)
|
|
25
|
+
source_pair = Csvtool::Domain::CsvParitySession::SourcePair.new(
|
|
26
|
+
left_path: left_path,
|
|
27
|
+
right_path: right_path
|
|
28
|
+
)
|
|
29
|
+
options = Csvtool::Domain::CsvParitySession::ParityOptions.new(
|
|
30
|
+
separator: separator,
|
|
31
|
+
headers_present: headers_present
|
|
32
|
+
)
|
|
33
|
+
Csvtool::Domain::CsvParitySession::ParitySession.start(
|
|
34
|
+
source_pair: source_pair,
|
|
35
|
+
options: options
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_returns_match_for_equivalent_files
|
|
40
|
+
result = Csvtool::Application::UseCases::RunCsvParity.new.call(
|
|
41
|
+
session: build_session(
|
|
42
|
+
left_path: fixture_path("sample_people.csv"),
|
|
43
|
+
right_path: fixture_path("parity_people_reordered.csv")
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
assert_equal true, result.ok?
|
|
48
|
+
assert_equal true, result.data[:match]
|
|
49
|
+
assert_equal 0, result.data[:left_only_count]
|
|
50
|
+
assert_equal 0, result.data[:right_only_count]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_returns_mismatch_counts_for_non_equivalent_files
|
|
54
|
+
result = Csvtool::Application::UseCases::RunCsvParity.new.call(
|
|
55
|
+
session: build_session(
|
|
56
|
+
left_path: fixture_path("sample_people.csv"),
|
|
57
|
+
right_path: fixture_path("parity_people_mismatch.csv")
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
assert_equal true, result.ok?
|
|
62
|
+
assert_equal false, result.data[:match]
|
|
63
|
+
assert_equal 1, result.data[:left_only_count]
|
|
64
|
+
assert_equal 1, result.data[:right_only_count]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_duplicate_count_differences_are_detected
|
|
68
|
+
result = Csvtool::Application::UseCases::RunCsvParity.new.call(
|
|
69
|
+
session: build_session(
|
|
70
|
+
left_path: fixture_path("parity_duplicates_left.csv"),
|
|
71
|
+
right_path: fixture_path("parity_duplicates_right.csv")
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert_equal true, result.ok?
|
|
76
|
+
assert_equal false, result.data[:match]
|
|
77
|
+
assert_equal 1, result.data[:left_only_count]
|
|
78
|
+
assert_equal 0, result.data[:right_only_count]
|
|
79
|
+
assert_equal "1,Alice", result.data[:left_only_examples][0][:row]
|
|
80
|
+
assert_equal 1, result.data[:left_only_examples][0][:count_delta]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_headered_mode_fails_when_headers_do_not_match
|
|
84
|
+
result = Csvtool::Application::UseCases::RunCsvParity.new.call(
|
|
85
|
+
session: build_session(
|
|
86
|
+
left_path: fixture_path("sample_people.csv"),
|
|
87
|
+
right_path: fixture_path("parity_people_header_mismatch.csv")
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
assert_equal false, result.ok?
|
|
92
|
+
assert_equal :header_mismatch, result.error
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_headerless_mode_compares_all_rows_as_data
|
|
96
|
+
result = Csvtool::Application::UseCases::RunCsvParity.new.call(
|
|
97
|
+
session: build_session(
|
|
98
|
+
left_path: fixture_path("sample_people_no_headers.csv"),
|
|
99
|
+
right_path: fixture_path("sample_people_no_headers.csv"),
|
|
100
|
+
headers_present: false
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
assert_equal true, result.ok?
|
|
105
|
+
assert_equal true, result.data[:match]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_returns_file_not_found_for_left_side
|
|
109
|
+
result = Csvtool::Application::UseCases::RunCsvParity.new.call(
|
|
110
|
+
session: build_session(
|
|
111
|
+
left_path: "/tmp/nope-left.csv",
|
|
112
|
+
right_path: fixture_path("sample_people.csv")
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert_equal false, result.ok?
|
|
117
|
+
assert_equal :file_not_found, result.error
|
|
118
|
+
assert_equal "/tmp/nope-left.csv", result.data[:path]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_returns_file_not_found_for_right_side
|
|
122
|
+
result = Csvtool::Application::UseCases::RunCsvParity.new.call(
|
|
123
|
+
session: build_session(
|
|
124
|
+
left_path: fixture_path("sample_people.csv"),
|
|
125
|
+
right_path: "/tmp/nope-right.csv"
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
assert_equal false, result.ok?
|
|
130
|
+
assert_equal :file_not_found, result.error
|
|
131
|
+
assert_equal "/tmp/nope-right.csv", result.data[:path]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_returns_parse_error_for_malformed_csv
|
|
135
|
+
result = Csvtool::Application::UseCases::RunCsvParity.new.call(
|
|
136
|
+
session: build_session(
|
|
137
|
+
left_path: fixture_path("sample_people.csv"),
|
|
138
|
+
right_path: fixture_path("sample_people_bad_tail.csv")
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
assert_equal false, result.ok?
|
|
143
|
+
assert_equal :could_not_parse_csv, result.error
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def test_returns_cannot_read_file_when_eacces_is_raised
|
|
147
|
+
result = Csvtool::Application::UseCases::RunCsvParity.new(
|
|
148
|
+
comparator: EaccesComparator.new
|
|
149
|
+
).call(
|
|
150
|
+
session: build_session(
|
|
151
|
+
left_path: fixture_path("sample_people.csv"),
|
|
152
|
+
right_path: fixture_path("sample_people.csv")
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
assert_equal false, result.ok?
|
|
157
|
+
assert_equal :cannot_read_file, result.error
|
|
158
|
+
assert_equal "/tmp/protected.csv", result.data[:path]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -2,30 +2,86 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../../../test_helper"
|
|
4
4
|
require "csvtool/application/use_cases/run_extraction"
|
|
5
|
+
require "csvtool/domain/column_session/column_session"
|
|
6
|
+
require "csvtool/domain/column_session/csv_source"
|
|
7
|
+
require "csvtool/domain/column_session/separator"
|
|
8
|
+
require "csvtool/domain/column_session/column_selection"
|
|
9
|
+
require "csvtool/domain/column_session/extraction_options"
|
|
10
|
+
require "csvtool/domain/shared/output_destination"
|
|
11
|
+
require "tmpdir"
|
|
5
12
|
|
|
6
13
|
class RunExtractionTest < Minitest::Test
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
class RaisingWriter
|
|
15
|
+
def call(**_kwargs)
|
|
16
|
+
raise Errno::ENOENT
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fixture_path(name)
|
|
21
|
+
File.expand_path("../../../fixtures/#{name}", __dir__)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_read_headers_missing_file_returns_failure
|
|
25
|
+
result = Csvtool::Application::UseCases::RunExtraction.new.read_headers(
|
|
26
|
+
file_path: "/tmp/not-present.csv",
|
|
27
|
+
col_sep: ","
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
assert_equal false, result.ok?
|
|
31
|
+
assert_equal :file_not_found, result.error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_preview_returns_expected_values
|
|
35
|
+
use_case = Csvtool::Application::UseCases::RunExtraction.new
|
|
36
|
+
|
|
37
|
+
result = use_case.preview(
|
|
38
|
+
session: build_session(output_destination: Csvtool::Domain::Shared::OutputDestination.console)
|
|
12
39
|
)
|
|
13
40
|
|
|
14
|
-
|
|
41
|
+
assert_equal true, result.ok?
|
|
42
|
+
assert_equal %w[Alice Bob Cara], result.data[:preview_values]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_extract_writes_values_to_file
|
|
46
|
+
use_case = Csvtool::Application::UseCases::RunExtraction.new
|
|
47
|
+
|
|
48
|
+
Dir.mktmpdir do |dir|
|
|
49
|
+
output_path = File.join(dir, "names.csv")
|
|
50
|
+
result = use_case.extract(
|
|
51
|
+
session: build_session(output_destination: Csvtool::Domain::Shared::OutputDestination.file(path: output_path))
|
|
52
|
+
)
|
|
15
53
|
|
|
16
|
-
|
|
54
|
+
assert_equal true, result.ok?
|
|
55
|
+
assert_equal output_path, result.data[:output_path]
|
|
56
|
+
assert_equal "name\nAlice\nBob\nCara\n", File.read(output_path)
|
|
57
|
+
end
|
|
17
58
|
end
|
|
18
59
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
60
|
+
def test_extract_returns_cannot_write_output_file_when_writer_fails
|
|
61
|
+
use_case = Csvtool::Application::UseCases::RunExtraction.new(csv_file_writer: RaisingWriter.new)
|
|
62
|
+
|
|
63
|
+
result = use_case.extract(
|
|
64
|
+
session: build_session(output_destination: Csvtool::Domain::Shared::OutputDestination.file(path: "/tmp/names.csv"))
|
|
65
|
+
)
|
|
23
66
|
|
|
24
|
-
|
|
25
|
-
|
|
67
|
+
assert_equal false, result.ok?
|
|
68
|
+
assert_equal :cannot_write_output_file, result.error
|
|
69
|
+
assert_equal "/tmp/names.csv", result.data[:path]
|
|
70
|
+
assert_equal Errno::ENOENT, result.data[:error_class]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def build_session(output_destination:)
|
|
76
|
+
session = Csvtool::Domain::ColumnSession::ColumnSession.start(
|
|
77
|
+
source: Csvtool::Domain::ColumnSession::CsvSource.new(
|
|
78
|
+
path: fixture_path("sample_people.csv"),
|
|
79
|
+
separator: Csvtool::Domain::ColumnSession::Separator.new(",")
|
|
80
|
+
),
|
|
81
|
+
column_selection: Csvtool::Domain::ColumnSession::ColumnSelection.new(name: "name"),
|
|
82
|
+
options: Csvtool::Domain::ColumnSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 10)
|
|
83
|
+
)
|
|
26
84
|
|
|
27
|
-
|
|
28
|
-
assert_includes out.string, "Bob"
|
|
29
|
-
assert_includes out.string, "Cara"
|
|
85
|
+
session.with_output_destination(output_destination)
|
|
30
86
|
end
|
|
31
87
|
end
|
|
@@ -2,139 +2,119 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../../../test_helper"
|
|
4
4
|
require "csvtool/application/use_cases/run_row_extraction"
|
|
5
|
+
require "csvtool/domain/row_session/row_source"
|
|
6
|
+
require "csvtool/domain/row_session/row_range"
|
|
7
|
+
require "csvtool/domain/row_session/row_session"
|
|
8
|
+
require "csvtool/domain/shared/output_destination"
|
|
5
9
|
require "tmpdir"
|
|
6
10
|
|
|
7
11
|
class RunRowExtractionTest < Minitest::Test
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
14
|
-
use_case.call
|
|
15
|
-
|
|
16
|
-
assert_includes out.string, "name,city"
|
|
17
|
-
assert_includes out.string, "Bob,Paris"
|
|
18
|
-
assert_includes out.string, "Cara,Berlin"
|
|
19
|
-
refute_includes out.string, "Alice,London"
|
|
12
|
+
class RaisingWriter
|
|
13
|
+
def call(**_kwargs)
|
|
14
|
+
raise Errno::ENOENT
|
|
15
|
+
end
|
|
20
16
|
end
|
|
21
17
|
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
|
|
25
|
-
input = [fixture, "", "abc", "3", ""].join("\n") + "\n"
|
|
26
|
-
|
|
27
|
-
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
28
|
-
use_case.call
|
|
29
|
-
|
|
30
|
-
assert_includes out.string, "Start row must be a positive integer."
|
|
31
|
-
refute_includes out.string, "name,city"
|
|
18
|
+
def fixture_path(name)
|
|
19
|
+
File.expand_path("../../../fixtures/#{name}", __dir__)
|
|
32
20
|
end
|
|
33
21
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_rejects_end_before_start
|
|
47
|
-
out = StringIO.new
|
|
48
|
-
fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
|
|
49
|
-
input = [fixture, "", "3", "2", ""].join("\n") + "\n"
|
|
50
|
-
|
|
51
|
-
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
52
|
-
use_case.call
|
|
53
|
-
|
|
54
|
-
assert_includes out.string, "End row must be greater than or equal to start row."
|
|
55
|
-
refute_includes out.string, "name,city"
|
|
22
|
+
def build_session(file_path:, separator: ",", start_row:, end_row:, output: :console, output_path: nil)
|
|
23
|
+
source = Csvtool::Domain::RowSession::RowSource.new(path: file_path, separator: separator)
|
|
24
|
+
row_range = Csvtool::Domain::RowSession::RowRange.new(start_row: start_row, end_row: end_row)
|
|
25
|
+
session = Csvtool::Domain::RowSession::RowSession.start(source: source, row_range: row_range)
|
|
26
|
+
|
|
27
|
+
session.with_output_destination(
|
|
28
|
+
if output == :file
|
|
29
|
+
Csvtool::Domain::Shared::OutputDestination.file(path: output_path)
|
|
30
|
+
else
|
|
31
|
+
Csvtool::Domain::Shared::OutputDestination.console
|
|
32
|
+
end
|
|
33
|
+
)
|
|
56
34
|
end
|
|
57
35
|
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
fixture = File.expand_path("../../../fixtures/sample_people.csv", __dir__)
|
|
61
|
-
input = [fixture, "", "10", "12", ""].join("\n") + "\n"
|
|
36
|
+
def test_read_headers_returns_headers_for_valid_file
|
|
37
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new
|
|
62
38
|
|
|
63
|
-
|
|
64
|
-
use_case.call
|
|
39
|
+
result = use_case.read_headers(file_path: fixture_path("sample_people.csv"), col_sep: ",")
|
|
65
40
|
|
|
66
|
-
|
|
67
|
-
|
|
41
|
+
assert result.ok?
|
|
42
|
+
assert_equal ["name", "city"], result.data[:headers]
|
|
68
43
|
end
|
|
69
44
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
fixture = File.expand_path("../../../fixtures/sample_people.tsv", __dir__)
|
|
73
|
-
input = [fixture, "2", "2", "3", ""].join("\n") + "\n"
|
|
45
|
+
def test_read_headers_fails_when_file_is_missing
|
|
46
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new
|
|
74
47
|
|
|
75
|
-
|
|
76
|
-
use_case.call
|
|
48
|
+
result = use_case.read_headers(file_path: "/tmp/not-present.csv", col_sep: ",")
|
|
77
49
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
assert_includes out.string, "Cara,Berlin"
|
|
50
|
+
refute result.ok?
|
|
51
|
+
assert_equal :file_not_found, result.error
|
|
81
52
|
end
|
|
82
53
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
54
|
+
def test_extract_streams_rows_for_console_mode
|
|
55
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new
|
|
56
|
+
session = build_session(file_path: fixture_path("sample_people.csv"), start_row: 2, end_row: 3)
|
|
57
|
+
headers = ["name", "city"]
|
|
58
|
+
rows = []
|
|
87
59
|
|
|
88
|
-
|
|
89
|
-
use_case.call
|
|
60
|
+
result = use_case.extract(session: session, headers: headers, on_row: ->(fields) { rows << fields })
|
|
90
61
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
62
|
+
assert result.ok?
|
|
63
|
+
assert_equal true, result.data[:matched]
|
|
64
|
+
assert_equal 3, result.data[:row_count]
|
|
65
|
+
assert_equal [["Bob", "Paris"], ["Cara", "Berlin"]], rows
|
|
94
66
|
end
|
|
95
67
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
68
|
+
def test_extract_writes_rows_to_file_mode
|
|
69
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new
|
|
70
|
+
headers = ["name", "city"]
|
|
99
71
|
|
|
100
72
|
Dir.mktmpdir do |dir|
|
|
101
73
|
output_path = File.join(dir, "rows.csv")
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
74
|
+
session = build_session(
|
|
75
|
+
file_path: fixture_path("sample_people.csv"),
|
|
76
|
+
start_row: 2,
|
|
77
|
+
end_row: 3,
|
|
78
|
+
output: :file,
|
|
79
|
+
output_path: output_path
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
result = use_case.extract(session: session, headers: headers)
|
|
83
|
+
|
|
84
|
+
assert result.ok?
|
|
85
|
+
assert_equal true, result.data[:wrote_rows]
|
|
107
86
|
assert_equal "name,city\nBob,Paris\nCara,Berlin\n", File.read(output_path)
|
|
108
|
-
assert_includes out.string, "Wrote output to #{output_path}"
|
|
109
87
|
end
|
|
110
88
|
end
|
|
111
89
|
|
|
112
|
-
def
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
90
|
+
def test_extract_returns_cannot_write_output_file_when_writer_fails
|
|
91
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(csv_row_file_writer: RaisingWriter.new)
|
|
92
|
+
headers = ["name", "city"]
|
|
93
|
+
session = build_session(
|
|
94
|
+
file_path: fixture_path("sample_people.csv"),
|
|
95
|
+
start_row: 2,
|
|
96
|
+
end_row: 3,
|
|
97
|
+
output: :file,
|
|
98
|
+
output_path: "/tmp/rows.csv"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
result = use_case.extract(session: session, headers: headers)
|
|
102
|
+
|
|
103
|
+
refute result.ok?
|
|
104
|
+
assert_equal :cannot_write_output_file, result.error
|
|
105
|
+
assert_equal "/tmp/rows.csv", result.data[:path]
|
|
106
|
+
assert_equal Errno::ENOENT, result.data[:error_class]
|
|
124
107
|
end
|
|
125
108
|
|
|
126
|
-
def
|
|
127
|
-
|
|
128
|
-
|
|
109
|
+
def test_extract_reports_out_of_bounds_via_stats
|
|
110
|
+
use_case = Csvtool::Application::UseCases::RunRowExtraction.new
|
|
111
|
+
session = build_session(file_path: fixture_path("sample_people.csv"), start_row: 10, end_row: 12)
|
|
112
|
+
headers = ["name", "city"]
|
|
129
113
|
|
|
130
|
-
|
|
131
|
-
output_path = File.join(dir, "rows.csv")
|
|
132
|
-
input = [fixture, "", "10", "12", "2", output_path].join("\n") + "\n"
|
|
133
|
-
|
|
134
|
-
use_case = Csvtool::Application::UseCases::RunRowExtraction.new(stdin: StringIO.new(input), stdout: out)
|
|
135
|
-
use_case.call
|
|
136
|
-
end
|
|
114
|
+
result = use_case.extract(session: session, headers: headers)
|
|
137
115
|
|
|
138
|
-
|
|
116
|
+
assert result.ok?
|
|
117
|
+
assert_equal false, result.data[:matched]
|
|
118
|
+
assert_equal 3, result.data[:row_count]
|
|
139
119
|
end
|
|
140
120
|
end
|