csvops 0.5.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 +45 -3
- data/docs/architecture.md +61 -4
- data/docs/release-v0.6.0-alpha.md +84 -0
- data/lib/csvtool/application/use_cases/run_csv_parity.rb +70 -0
- data/lib/csvtool/cli.rb +5 -1
- data/lib/csvtool/domain/csv_parity_session/parity_options.rb +22 -0
- data/lib/csvtool/domain/csv_parity_session/parity_session.rb +20 -0
- data/lib/csvtool/domain/csv_parity_session/source_pair.rb +19 -0
- data/lib/csvtool/infrastructure/csv/csv_parity_comparator.rb +71 -0
- 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/workflows/builders/csv_parity_session_builder.rb +33 -0
- data/lib/csvtool/interface/cli/workflows/presenters/csv_parity_presenter.rb +38 -0
- data/lib/csvtool/interface/cli/workflows/run_csv_parity_workflow.rb +66 -0
- data/lib/csvtool/interface/cli/workflows/steps/parity/build_session_step.rb +25 -0
- data/lib/csvtool/interface/cli/workflows/steps/parity/collect_inputs_step.rb +32 -0
- data/lib/csvtool/interface/cli/workflows/steps/parity/execute_step.rb +26 -0
- data/lib/csvtool/version.rb +1 -1
- data/test/csvtool/application/use_cases/run_csv_parity_test.rb +160 -0
- data/test/csvtool/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/interface/cli/errors/presenter_test.rb +2 -0
- data/test/csvtool/interface/cli/menu_loop_test.rb +59 -16
- data/test/csvtool/interface/cli/workflows/builders/csv_parity_session_builder_test.rb +20 -0
- data/test/csvtool/interface/cli/workflows/presenters/csv_parity_presenter_test.rb +43 -0
- data/test/csvtool/interface/cli/workflows/run_csv_parity_workflow_test.rb +94 -0
- data/test/csvtool/interface/cli/workflows/steps/parity/build_session_step_test.rb +41 -0
- data/test/csvtool/interface/cli/workflows/steps/parity/collect_inputs_step_test.rb +30 -0
- data/test/csvtool/interface/cli/workflows/steps/parity/execute_step_test.rb +40 -0
- data/test/fixtures/parity_duplicates_left.csv +4 -0
- data/test/fixtures/parity_duplicates_right.csv +3 -0
- data/test/fixtures/parity_people_header_mismatch.csv +4 -0
- data/test/fixtures/parity_people_many_reordered.csv +13 -0
- data/test/fixtures/parity_people_mismatch.csv +4 -0
- data/test/fixtures/parity_people_reordered.csv +4 -0
- data/test/fixtures/parity_people_reordered.tsv +4 -0
- metadata +31 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csvtool/application/use_cases/run_csv_parity"
|
|
4
|
+
require "csvtool/interface/cli/errors/presenter"
|
|
5
|
+
require "csvtool/interface/cli/prompts/file_path_prompt"
|
|
6
|
+
require "csvtool/interface/cli/prompts/separator_prompt"
|
|
7
|
+
require "csvtool/interface/cli/prompts/headers_present_prompt"
|
|
8
|
+
require "csvtool/interface/cli/workflows/builders/csv_parity_session_builder"
|
|
9
|
+
require "csvtool/interface/cli/workflows/presenters/csv_parity_presenter"
|
|
10
|
+
require "csvtool/interface/cli/workflows/support/result_error_handler"
|
|
11
|
+
require "csvtool/interface/cli/workflows/steps/workflow_step_pipeline"
|
|
12
|
+
require "csvtool/interface/cli/workflows/steps/parity/collect_inputs_step"
|
|
13
|
+
require "csvtool/interface/cli/workflows/steps/parity/build_session_step"
|
|
14
|
+
require "csvtool/interface/cli/workflows/steps/parity/execute_step"
|
|
15
|
+
|
|
16
|
+
module Csvtool
|
|
17
|
+
module Interface
|
|
18
|
+
module CLI
|
|
19
|
+
module Workflows
|
|
20
|
+
class RunCsvParityWorkflow
|
|
21
|
+
def initialize(stdin:, stdout:, use_case: Application::UseCases::RunCsvParity.new)
|
|
22
|
+
@stdin = stdin
|
|
23
|
+
@stdout = stdout
|
|
24
|
+
@use_case = use_case
|
|
25
|
+
@errors = Interface::CLI::Errors::Presenter.new(stdout: stdout)
|
|
26
|
+
@session_builder = Builders::CsvParitySessionBuilder.new
|
|
27
|
+
@presenter = Presenters::CsvParityPresenter.new(stdout: stdout)
|
|
28
|
+
@result_error_handler = Support::ResultErrorHandler.new(errors: @errors)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call
|
|
32
|
+
context = {
|
|
33
|
+
use_case: @use_case,
|
|
34
|
+
session_builder: @session_builder,
|
|
35
|
+
presenter: @presenter,
|
|
36
|
+
handle_error: method(:handle_error)
|
|
37
|
+
}
|
|
38
|
+
pipeline = Steps::WorkflowStepPipeline.new(steps: [
|
|
39
|
+
Steps::Parity::CollectInputsStep.new(
|
|
40
|
+
file_path_prompt: Interface::CLI::Prompts::FilePathPrompt.new(stdin: @stdin, stdout: @stdout),
|
|
41
|
+
separator_prompt: Interface::CLI::Prompts::SeparatorPrompt.new(stdin: @stdin, stdout: @stdout, errors: @errors),
|
|
42
|
+
headers_present_prompt: Interface::CLI::Prompts::HeadersPresentPrompt.new(stdin: @stdin, stdout: @stdout)
|
|
43
|
+
),
|
|
44
|
+
Steps::Parity::BuildSessionStep.new,
|
|
45
|
+
Steps::Parity::ExecuteStep.new
|
|
46
|
+
])
|
|
47
|
+
pipeline.call(context)
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def handle_error(result)
|
|
54
|
+
@result_error_handler.call(result, {
|
|
55
|
+
file_not_found: ->(r, errors) { errors.file_not_found(r.data[:path]) },
|
|
56
|
+
could_not_parse_csv: ->(_r, errors) { errors.could_not_parse_csv },
|
|
57
|
+
cannot_read_file: ->(r, errors) { errors.cannot_read_file(r.data[:path]) },
|
|
58
|
+
no_headers: ->(_r, errors) { errors.no_headers },
|
|
59
|
+
header_mismatch: ->(_r, errors) { errors.header_mismatch }
|
|
60
|
+
})
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -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
|
+
module Parity
|
|
9
|
+
class BuildSessionStep
|
|
10
|
+
def call(context)
|
|
11
|
+
context[:session] = context.fetch(:session_builder).call(
|
|
12
|
+
left_path: context.fetch(:left_path),
|
|
13
|
+
right_path: context.fetch(:right_path),
|
|
14
|
+
col_sep: context.fetch(:col_sep),
|
|
15
|
+
headers_present: context.fetch(:headers_present)
|
|
16
|
+
)
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csvtool
|
|
4
|
+
module Interface
|
|
5
|
+
module CLI
|
|
6
|
+
module Workflows
|
|
7
|
+
module Steps
|
|
8
|
+
module Parity
|
|
9
|
+
class CollectInputsStep
|
|
10
|
+
def initialize(file_path_prompt:, separator_prompt:, headers_present_prompt:)
|
|
11
|
+
@file_path_prompt = file_path_prompt
|
|
12
|
+
@separator_prompt = separator_prompt
|
|
13
|
+
@headers_present_prompt = headers_present_prompt
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(context)
|
|
17
|
+
context[:left_path] = @file_path_prompt.call(label: "Left CSV file path: ")
|
|
18
|
+
context[:right_path] = @file_path_prompt.call(label: "Right CSV file path: ")
|
|
19
|
+
col_sep = @separator_prompt.call
|
|
20
|
+
return :halt if col_sep.nil?
|
|
21
|
+
|
|
22
|
+
context[:col_sep] = col_sep
|
|
23
|
+
context[:headers_present] = @headers_present_prompt.call
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csvtool
|
|
4
|
+
module Interface
|
|
5
|
+
module CLI
|
|
6
|
+
module Workflows
|
|
7
|
+
module Steps
|
|
8
|
+
module Parity
|
|
9
|
+
class ExecuteStep
|
|
10
|
+
def call(context)
|
|
11
|
+
result = context.fetch(:use_case).call(session: context.fetch(:session))
|
|
12
|
+
unless result.ok?
|
|
13
|
+
context.fetch(:handle_error).call(result)
|
|
14
|
+
return :halt
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
context.fetch(:presenter).print_summary(result.data)
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/csvtool/version.rb
CHANGED
|
@@ -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
|
data/test/csvtool/cli_test.rb
CHANGED
|
@@ -11,7 +11,7 @@ class TestCli < Minitest::Test
|
|
|
11
11
|
|
|
12
12
|
def test_menu_can_exit_cleanly
|
|
13
13
|
output = StringIO.new
|
|
14
|
-
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("
|
|
14
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("6\n"), stdout: output, stderr: StringIO.new)
|
|
15
15
|
assert_equal 0, status
|
|
16
16
|
assert_includes output.string, "CSV Tool Menu"
|
|
17
17
|
end
|
|
@@ -26,7 +26,7 @@ class TestCli < Minitest::Test
|
|
|
26
26
|
"",
|
|
27
27
|
"y",
|
|
28
28
|
"",
|
|
29
|
-
"
|
|
29
|
+
"6"
|
|
30
30
|
].join("\n") + "\n"
|
|
31
31
|
|
|
32
32
|
output = StringIO.new
|
|
@@ -58,7 +58,7 @@ class TestCli < Minitest::Test
|
|
|
58
58
|
"2",
|
|
59
59
|
"3",
|
|
60
60
|
"",
|
|
61
|
-
"
|
|
61
|
+
"6"
|
|
62
62
|
].join("\n") + "\n"
|
|
63
63
|
|
|
64
64
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -79,7 +79,7 @@ class TestCli < Minitest::Test
|
|
|
79
79
|
"0",
|
|
80
80
|
"3",
|
|
81
81
|
"",
|
|
82
|
-
"
|
|
82
|
+
"6"
|
|
83
83
|
].join("\n") + "\n"
|
|
84
84
|
|
|
85
85
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -98,7 +98,7 @@ class TestCli < Minitest::Test
|
|
|
98
98
|
"2",
|
|
99
99
|
"3",
|
|
100
100
|
"",
|
|
101
|
-
"
|
|
101
|
+
"6"
|
|
102
102
|
].join("\n") + "\n"
|
|
103
103
|
|
|
104
104
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -119,7 +119,7 @@ class TestCli < Minitest::Test
|
|
|
119
119
|
"2",
|
|
120
120
|
"3",
|
|
121
121
|
"",
|
|
122
|
-
"
|
|
122
|
+
"6"
|
|
123
123
|
].join("\n") + "\n"
|
|
124
124
|
|
|
125
125
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -144,7 +144,7 @@ class TestCli < Minitest::Test
|
|
|
144
144
|
"3",
|
|
145
145
|
"2",
|
|
146
146
|
output_path,
|
|
147
|
-
"
|
|
147
|
+
"6"
|
|
148
148
|
].join("\n") + "\n"
|
|
149
149
|
|
|
150
150
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -164,7 +164,7 @@ class TestCli < Minitest::Test
|
|
|
164
164
|
"1",
|
|
165
165
|
"2",
|
|
166
166
|
"",
|
|
167
|
-
"
|
|
167
|
+
"6"
|
|
168
168
|
].join("\n") + "\n"
|
|
169
169
|
|
|
170
170
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -184,7 +184,7 @@ class TestCli < Minitest::Test
|
|
|
184
184
|
"",
|
|
185
185
|
"",
|
|
186
186
|
"",
|
|
187
|
-
"
|
|
187
|
+
"6"
|
|
188
188
|
].join("\n") + "\n"
|
|
189
189
|
|
|
190
190
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -209,7 +209,7 @@ class TestCli < Minitest::Test
|
|
|
209
209
|
"",
|
|
210
210
|
"2",
|
|
211
211
|
output_path,
|
|
212
|
-
"
|
|
212
|
+
"6"
|
|
213
213
|
].join("\n") + "\n"
|
|
214
214
|
|
|
215
215
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -231,7 +231,7 @@ class TestCli < Minitest::Test
|
|
|
231
231
|
"",
|
|
232
232
|
"",
|
|
233
233
|
"",
|
|
234
|
-
"
|
|
234
|
+
"6"
|
|
235
235
|
].join("\n") + "\n"
|
|
236
236
|
|
|
237
237
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -250,7 +250,7 @@ class TestCli < Minitest::Test
|
|
|
250
250
|
"n",
|
|
251
251
|
"",
|
|
252
252
|
"",
|
|
253
|
-
"
|
|
253
|
+
"6"
|
|
254
254
|
].join("\n") + "\n"
|
|
255
255
|
|
|
256
256
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -270,7 +270,7 @@ class TestCli < Minitest::Test
|
|
|
270
270
|
"",
|
|
271
271
|
"",
|
|
272
272
|
"abc",
|
|
273
|
-
"
|
|
273
|
+
"6"
|
|
274
274
|
].join("\n") + "\n"
|
|
275
275
|
|
|
276
276
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -295,7 +295,7 @@ class TestCli < Minitest::Test
|
|
|
295
295
|
"",
|
|
296
296
|
"",
|
|
297
297
|
"",
|
|
298
|
-
"
|
|
298
|
+
"6"
|
|
299
299
|
].join("\n") + "\n"
|
|
300
300
|
|
|
301
301
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -329,7 +329,7 @@ class TestCli < Minitest::Test
|
|
|
329
329
|
"",
|
|
330
330
|
"2",
|
|
331
331
|
output_path,
|
|
332
|
-
"
|
|
332
|
+
"6"
|
|
333
333
|
].join("\n") + "\n"
|
|
334
334
|
|
|
335
335
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -356,7 +356,7 @@ class TestCli < Minitest::Test
|
|
|
356
356
|
"",
|
|
357
357
|
"",
|
|
358
358
|
"",
|
|
359
|
-
"
|
|
359
|
+
"6"
|
|
360
360
|
].join("\n") + "\n"
|
|
361
361
|
|
|
362
362
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -382,7 +382,7 @@ class TestCli < Minitest::Test
|
|
|
382
382
|
"",
|
|
383
383
|
"",
|
|
384
384
|
"",
|
|
385
|
-
"
|
|
385
|
+
"6"
|
|
386
386
|
].join("\n") + "\n"
|
|
387
387
|
|
|
388
388
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -394,6 +394,160 @@ class TestCli < Minitest::Test
|
|
|
394
394
|
assert_includes output.string, "Summary: source_rows=5 removed_rows=3 kept_rows=2"
|
|
395
395
|
end
|
|
396
396
|
|
|
397
|
+
def test_parity_workflow_reports_match_and_returns_to_menu
|
|
398
|
+
output = StringIO.new
|
|
399
|
+
input = [
|
|
400
|
+
"5",
|
|
401
|
+
fixture_path("sample_people.csv"),
|
|
402
|
+
fixture_path("sample_people.csv"),
|
|
403
|
+
"",
|
|
404
|
+
"",
|
|
405
|
+
"6"
|
|
406
|
+
].join("\n") + "\n"
|
|
407
|
+
|
|
408
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
409
|
+
|
|
410
|
+
assert_equal 0, status
|
|
411
|
+
assert_includes output.string, "Left CSV file path:"
|
|
412
|
+
assert_includes output.string, "Right CSV file path:"
|
|
413
|
+
assert_includes output.string, "MATCH"
|
|
414
|
+
assert_includes output.string, "Summary: left_rows=3 right_rows=3 left_only=0 right_only=0"
|
|
415
|
+
assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def test_parity_workflow_supports_tsv_separator
|
|
419
|
+
output = StringIO.new
|
|
420
|
+
input = [
|
|
421
|
+
"5",
|
|
422
|
+
fixture_path("sample_people.tsv"),
|
|
423
|
+
fixture_path("parity_people_reordered.tsv"),
|
|
424
|
+
"2",
|
|
425
|
+
"",
|
|
426
|
+
"6"
|
|
427
|
+
].join("\n") + "\n"
|
|
428
|
+
|
|
429
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
430
|
+
|
|
431
|
+
assert_equal 0, status
|
|
432
|
+
assert_includes output.string, "MATCH"
|
|
433
|
+
assert_includes output.string, "Summary: left_rows=3 right_rows=3 left_only=0 right_only=0"
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def test_parity_workflow_headerless_mode_compares_all_rows
|
|
437
|
+
output = StringIO.new
|
|
438
|
+
input = [
|
|
439
|
+
"5",
|
|
440
|
+
fixture_path("sample_people_no_headers.csv"),
|
|
441
|
+
fixture_path("sample_people_no_headers.csv"),
|
|
442
|
+
"",
|
|
443
|
+
"n",
|
|
444
|
+
"6"
|
|
445
|
+
].join("\n") + "\n"
|
|
446
|
+
|
|
447
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
448
|
+
|
|
449
|
+
assert_equal 0, status
|
|
450
|
+
assert_includes output.string, "MATCH"
|
|
451
|
+
assert_includes output.string, "Summary: left_rows=3 right_rows=3 left_only=0 right_only=0"
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def test_parity_workflow_reports_header_mismatch_in_headered_mode
|
|
455
|
+
output = StringIO.new
|
|
456
|
+
input = [
|
|
457
|
+
"5",
|
|
458
|
+
fixture_path("sample_people.csv"),
|
|
459
|
+
fixture_path("parity_people_header_mismatch.csv"),
|
|
460
|
+
"",
|
|
461
|
+
"",
|
|
462
|
+
"6"
|
|
463
|
+
].join("\n") + "\n"
|
|
464
|
+
|
|
465
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
466
|
+
|
|
467
|
+
assert_equal 0, status
|
|
468
|
+
assert_includes output.string, "CSV headers do not match."
|
|
469
|
+
assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def test_parity_workflow_prints_mismatch_examples_and_counts
|
|
473
|
+
output = StringIO.new
|
|
474
|
+
input = [
|
|
475
|
+
"5",
|
|
476
|
+
fixture_path("sample_people.csv"),
|
|
477
|
+
fixture_path("parity_people_mismatch.csv"),
|
|
478
|
+
"",
|
|
479
|
+
"",
|
|
480
|
+
"6"
|
|
481
|
+
].join("\n") + "\n"
|
|
482
|
+
|
|
483
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
484
|
+
|
|
485
|
+
assert_equal 0, status
|
|
486
|
+
assert_includes output.string, "MISMATCH"
|
|
487
|
+
assert_includes output.string, "Summary: left_rows=3 right_rows=3 left_only=1 right_only=1"
|
|
488
|
+
assert_includes output.string, "Left-only examples:"
|
|
489
|
+
assert_includes output.string, "Cara,Berlin (count +1)"
|
|
490
|
+
assert_includes output.string, "Right-only examples:"
|
|
491
|
+
assert_includes output.string, "Dina,Rome (count +1)"
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def test_parity_workflow_missing_left_file_returns_to_menu
|
|
495
|
+
output = StringIO.new
|
|
496
|
+
input = [
|
|
497
|
+
"5",
|
|
498
|
+
"/tmp/not-there-left.csv",
|
|
499
|
+
fixture_path("sample_people.csv"),
|
|
500
|
+
"",
|
|
501
|
+
"",
|
|
502
|
+
"6"
|
|
503
|
+
].join("\n") + "\n"
|
|
504
|
+
|
|
505
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
506
|
+
|
|
507
|
+
assert_equal 0, status
|
|
508
|
+
assert_includes output.string, "File not found: /tmp/not-there-left.csv"
|
|
509
|
+
assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
|
|
510
|
+
refute_includes output.string, "Traceback"
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def test_parity_workflow_missing_right_file_returns_to_menu
|
|
514
|
+
output = StringIO.new
|
|
515
|
+
input = [
|
|
516
|
+
"5",
|
|
517
|
+
fixture_path("sample_people.csv"),
|
|
518
|
+
"/tmp/not-there-right.csv",
|
|
519
|
+
"",
|
|
520
|
+
"",
|
|
521
|
+
"6"
|
|
522
|
+
].join("\n") + "\n"
|
|
523
|
+
|
|
524
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
525
|
+
|
|
526
|
+
assert_equal 0, status
|
|
527
|
+
assert_includes output.string, "File not found: /tmp/not-there-right.csv"
|
|
528
|
+
assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
|
|
529
|
+
refute_includes output.string, "Traceback"
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def test_parity_workflow_malformed_csv_returns_to_menu
|
|
533
|
+
output = StringIO.new
|
|
534
|
+
input = [
|
|
535
|
+
"5",
|
|
536
|
+
fixture_path("sample_people.csv"),
|
|
537
|
+
fixture_path("sample_people_bad_tail.csv"),
|
|
538
|
+
"",
|
|
539
|
+
"",
|
|
540
|
+
"6"
|
|
541
|
+
].join("\n") + "\n"
|
|
542
|
+
|
|
543
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
544
|
+
|
|
545
|
+
assert_equal 0, status
|
|
546
|
+
assert_includes output.string, "Could not parse CSV file."
|
|
547
|
+
assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
|
|
548
|
+
refute_includes output.string, "Traceback"
|
|
549
|
+
end
|
|
550
|
+
|
|
397
551
|
def test_end_to_end_file_output_writes_expected_csv
|
|
398
552
|
output = StringIO.new
|
|
399
553
|
output_path = nil
|
|
@@ -410,7 +564,7 @@ class TestCli < Minitest::Test
|
|
|
410
564
|
"y",
|
|
411
565
|
"2",
|
|
412
566
|
output_path,
|
|
413
|
-
"
|
|
567
|
+
"6"
|
|
414
568
|
].join("\n") + "\n"
|
|
415
569
|
|
|
416
570
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
@@ -430,7 +584,7 @@ class TestCli < Minitest::Test
|
|
|
430
584
|
"1",
|
|
431
585
|
"",
|
|
432
586
|
"n",
|
|
433
|
-
"
|
|
587
|
+
"6"
|
|
434
588
|
].join("\n") + "\n"
|
|
435
589
|
|
|
436
590
|
output = StringIO.new
|
|
@@ -445,7 +599,7 @@ class TestCli < Minitest::Test
|
|
|
445
599
|
output = StringIO.new
|
|
446
600
|
status = Csvtool::CLI.start(
|
|
447
601
|
["menu"],
|
|
448
|
-
stdin: StringIO.new("1\n/tmp/does-not-exist.csv\n4\n"),
|
|
602
|
+
stdin: StringIO.new("1\n/tmp/does-not-exist.csv\n4\n6\n"),
|
|
449
603
|
stdout: output,
|
|
450
604
|
stderr: StringIO.new
|
|
451
605
|
)
|
|
@@ -466,7 +620,7 @@ class TestCli < Minitest::Test
|
|
|
466
620
|
"y",
|
|
467
621
|
"2",
|
|
468
622
|
"/tmp/not-a-dir/out.csv",
|
|
469
|
-
"
|
|
623
|
+
"6"
|
|
470
624
|
].join("\n") + "\n"
|
|
471
625
|
|
|
472
626
|
output = StringIO.new
|