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
|
@@ -16,7 +16,7 @@ class CliUnitTest < Minitest::Test
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def test_menu_command_can_exit_zero
|
|
19
|
-
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("
|
|
19
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("6\n"), stdout: StringIO.new, stderr: StringIO.new)
|
|
20
20
|
assert_equal 0, status
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ class CliUnitTest < Minitest::Test
|
|
|
28
28
|
def test_menu_routes_to_row_range_shell
|
|
29
29
|
stdout = StringIO.new
|
|
30
30
|
fixture = File.expand_path("../fixtures/sample_people.csv", __dir__)
|
|
31
|
-
input = ["2", fixture, "", "2", "3", "", "
|
|
31
|
+
input = ["2", fixture, "", "2", "3", "", "6"].join("\n") + "\n"
|
|
32
32
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: stdout, stderr: StringIO.new)
|
|
33
33
|
assert_equal 0, status
|
|
34
34
|
assert_includes stdout.string, "name,city"
|
|
@@ -39,7 +39,7 @@ class CliUnitTest < Minitest::Test
|
|
|
39
39
|
def test_menu_routes_to_randomize_rows_shell
|
|
40
40
|
stdout = StringIO.new
|
|
41
41
|
fixture = File.expand_path("../fixtures/sample_people.csv", __dir__)
|
|
42
|
-
input = ["3", fixture, "", "", "", "", "
|
|
42
|
+
input = ["3", fixture, "", "", "", "", "6"].join("\n") + "\n"
|
|
43
43
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: stdout, stderr: StringIO.new)
|
|
44
44
|
assert_equal 0, status
|
|
45
45
|
assert_includes stdout.string, "name,city"
|
|
@@ -52,7 +52,7 @@ class CliUnitTest < Minitest::Test
|
|
|
52
52
|
stdout = StringIO.new
|
|
53
53
|
source_fixture = File.expand_path("../fixtures/dedupe_source.csv", __dir__)
|
|
54
54
|
reference_fixture = File.expand_path("../fixtures/dedupe_reference.csv", __dir__)
|
|
55
|
-
input = ["4", source_fixture, "", "", reference_fixture, "", "", "customer_id", "external_id", "", "", "", "
|
|
55
|
+
input = ["4", source_fixture, "", "", reference_fixture, "", "", "customer_id", "external_id", "", "", "", "6"].join("\n") + "\n"
|
|
56
56
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: stdout, stderr: StringIO.new)
|
|
57
57
|
assert_equal 0, status
|
|
58
58
|
assert_includes stdout.string, "customer_id,name"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/csv_parity_session/parity_options"
|
|
5
|
+
|
|
6
|
+
class ParityOptionsTest < Minitest::Test
|
|
7
|
+
def test_requires_separator
|
|
8
|
+
assert_raises(ArgumentError) do
|
|
9
|
+
Csvtool::Domain::CsvParitySession::ParityOptions.new(separator: "", headers_present: true)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_exposes_headers_present
|
|
14
|
+
options = Csvtool::Domain::CsvParitySession::ParityOptions.new(separator: ",", headers_present: false)
|
|
15
|
+
assert_equal false, options.headers_present?
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/csv_parity_session/source_pair"
|
|
5
|
+
require "csvtool/domain/csv_parity_session/parity_options"
|
|
6
|
+
require "csvtool/domain/csv_parity_session/parity_session"
|
|
7
|
+
|
|
8
|
+
class ParitySessionTest < Minitest::Test
|
|
9
|
+
def test_stores_source_pair_and_options
|
|
10
|
+
source_pair = Csvtool::Domain::CsvParitySession::SourcePair.new(left_path: "/tmp/l.csv", right_path: "/tmp/r.csv")
|
|
11
|
+
options = Csvtool::Domain::CsvParitySession::ParityOptions.new(separator: ",", headers_present: true)
|
|
12
|
+
|
|
13
|
+
session = Csvtool::Domain::CsvParitySession::ParitySession.start(source_pair: source_pair, options: options)
|
|
14
|
+
|
|
15
|
+
assert_equal source_pair, session.source_pair
|
|
16
|
+
assert_equal options, session.options
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/csv_parity_session/source_pair"
|
|
5
|
+
|
|
6
|
+
class SourcePairTest < Minitest::Test
|
|
7
|
+
def test_requires_paths
|
|
8
|
+
assert_raises(ArgumentError) { Csvtool::Domain::CsvParitySession::SourcePair.new(left_path: "", right_path: "/tmp/r.csv") }
|
|
9
|
+
assert_raises(ArgumentError) { Csvtool::Domain::CsvParitySession::SourcePair.new(left_path: "/tmp/l.csv", right_path: "") }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/infrastructure/csv/csv_parity_comparator"
|
|
5
|
+
|
|
6
|
+
class CsvParityComparatorTest < Minitest::Test
|
|
7
|
+
def fixture_path(name)
|
|
8
|
+
File.expand_path("../../../fixtures/#{name}", __dir__)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_reports_match_when_rows_are_equal_ignoring_order
|
|
12
|
+
comparator = Csvtool::Infrastructure::CSV::CsvParityComparator.new
|
|
13
|
+
|
|
14
|
+
result = comparator.call(
|
|
15
|
+
left_path: fixture_path("sample_people.csv"),
|
|
16
|
+
right_path: fixture_path("parity_people_reordered.csv"),
|
|
17
|
+
col_sep: ",",
|
|
18
|
+
headers_present: true
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
assert_equal true, result[:match]
|
|
22
|
+
assert_equal 0, result[:left_only_count]
|
|
23
|
+
assert_equal 0, result[:right_only_count]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_reports_mismatch_counts_for_different_rows
|
|
27
|
+
comparator = Csvtool::Infrastructure::CSV::CsvParityComparator.new
|
|
28
|
+
|
|
29
|
+
result = comparator.call(
|
|
30
|
+
left_path: fixture_path("sample_people.csv"),
|
|
31
|
+
right_path: fixture_path("parity_people_mismatch.csv"),
|
|
32
|
+
col_sep: ",",
|
|
33
|
+
headers_present: true
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
assert_equal false, result[:match]
|
|
37
|
+
assert_equal 1, result[:left_only_count]
|
|
38
|
+
assert_equal 1, result[:right_only_count]
|
|
39
|
+
assert_equal "Cara,Berlin", result[:left_only_examples][0][:row]
|
|
40
|
+
assert_equal 1, result[:left_only_examples][0][:count_delta]
|
|
41
|
+
assert_equal "Dina,Rome", result[:right_only_examples][0][:row]
|
|
42
|
+
assert_equal 1, result[:right_only_examples][0][:count_delta]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_respects_duplicate_counts
|
|
46
|
+
comparator = Csvtool::Infrastructure::CSV::CsvParityComparator.new
|
|
47
|
+
|
|
48
|
+
result = comparator.call(
|
|
49
|
+
left_path: fixture_path("parity_duplicates_left.csv"),
|
|
50
|
+
right_path: fixture_path("parity_duplicates_right.csv"),
|
|
51
|
+
col_sep: ",",
|
|
52
|
+
headers_present: true
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert_equal false, result[:match]
|
|
56
|
+
assert_equal 1, result[:left_only_count]
|
|
57
|
+
assert_equal 0, result[:right_only_count]
|
|
58
|
+
assert_equal "1,Alice", result[:left_only_examples][0][:row]
|
|
59
|
+
assert_equal 1, result[:left_only_examples][0][:count_delta]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_preserves_exact_semantics_for_larger_fixture_with_different_order
|
|
63
|
+
comparator = Csvtool::Infrastructure::CSV::CsvParityComparator.new
|
|
64
|
+
|
|
65
|
+
result = comparator.call(
|
|
66
|
+
left_path: fixture_path("sample_people_many.csv"),
|
|
67
|
+
right_path: fixture_path("parity_people_many_reordered.csv"),
|
|
68
|
+
col_sep: ",",
|
|
69
|
+
headers_present: true
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
assert_equal true, result[:match]
|
|
73
|
+
assert_equal 12, result[:left_rows]
|
|
74
|
+
assert_equal 12, result[:right_rows]
|
|
75
|
+
assert_equal 0, result[:left_only_count]
|
|
76
|
+
assert_equal 0, result[:right_only_count]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -24,6 +24,7 @@ class ErrorsPresenterTest < Minitest::Test
|
|
|
24
24
|
presenter.invalid_end_row
|
|
25
25
|
presenter.invalid_row_range_order
|
|
26
26
|
presenter.row_range_out_of_bounds(3)
|
|
27
|
+
presenter.header_mismatch
|
|
27
28
|
|
|
28
29
|
text = out.string
|
|
29
30
|
assert_includes text, "File not found: /tmp/x.csv"
|
|
@@ -42,5 +43,6 @@ class ErrorsPresenterTest < Minitest::Test
|
|
|
42
43
|
assert_includes text, "End row must be a positive integer."
|
|
43
44
|
assert_includes text, "End row must be greater than or equal to start row."
|
|
44
45
|
assert_includes text, "Row range is out of bounds. File has 3 data rows."
|
|
46
|
+
assert_includes text, "CSV headers do not match."
|
|
45
47
|
end
|
|
46
48
|
end
|
|
@@ -21,15 +21,17 @@ class MenuLoopTest < Minitest::Test
|
|
|
21
21
|
rows_action = FakeAction.new
|
|
22
22
|
randomize_rows_action = FakeAction.new
|
|
23
23
|
dedupe_action = FakeAction.new
|
|
24
|
+
parity_action = FakeAction.new
|
|
24
25
|
stdout = StringIO.new
|
|
25
26
|
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
26
|
-
stdin: StringIO.new("1\
|
|
27
|
+
stdin: StringIO.new("1\n6\n"),
|
|
27
28
|
stdout: stdout,
|
|
28
|
-
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
|
|
29
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Validate parity", "Exit"],
|
|
29
30
|
extract_column_action: column_action,
|
|
30
31
|
extract_rows_action: rows_action,
|
|
31
32
|
randomize_rows_action: randomize_rows_action,
|
|
32
|
-
dedupe_action: dedupe_action
|
|
33
|
+
dedupe_action: dedupe_action,
|
|
34
|
+
parity_action: parity_action
|
|
33
35
|
)
|
|
34
36
|
|
|
35
37
|
status = menu.run
|
|
@@ -39,6 +41,7 @@ class MenuLoopTest < Minitest::Test
|
|
|
39
41
|
assert_equal 0, rows_action.runs
|
|
40
42
|
assert_equal 0, randomize_rows_action.runs
|
|
41
43
|
assert_equal 0, dedupe_action.runs
|
|
44
|
+
assert_equal 0, parity_action.runs
|
|
42
45
|
assert_includes stdout.string, "CSV Tool Menu"
|
|
43
46
|
end
|
|
44
47
|
|
|
@@ -47,15 +50,17 @@ class MenuLoopTest < Minitest::Test
|
|
|
47
50
|
rows_action = FakeAction.new
|
|
48
51
|
randomize_rows_action = FakeAction.new
|
|
49
52
|
dedupe_action = FakeAction.new
|
|
53
|
+
parity_action = FakeAction.new
|
|
50
54
|
stdout = StringIO.new
|
|
51
55
|
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
52
|
-
stdin: StringIO.new("2\
|
|
56
|
+
stdin: StringIO.new("2\n6\n"),
|
|
53
57
|
stdout: stdout,
|
|
54
|
-
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
|
|
58
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Validate parity", "Exit"],
|
|
55
59
|
extract_column_action: column_action,
|
|
56
60
|
extract_rows_action: rows_action,
|
|
57
61
|
randomize_rows_action: randomize_rows_action,
|
|
58
|
-
dedupe_action: dedupe_action
|
|
62
|
+
dedupe_action: dedupe_action,
|
|
63
|
+
parity_action: parity_action
|
|
59
64
|
)
|
|
60
65
|
|
|
61
66
|
status = menu.run
|
|
@@ -65,6 +70,7 @@ class MenuLoopTest < Minitest::Test
|
|
|
65
70
|
assert_equal 1, rows_action.runs
|
|
66
71
|
assert_equal 0, randomize_rows_action.runs
|
|
67
72
|
assert_equal 0, dedupe_action.runs
|
|
73
|
+
assert_equal 0, parity_action.runs
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
def test_routes_randomize_rows_then_exit
|
|
@@ -72,15 +78,17 @@ class MenuLoopTest < Minitest::Test
|
|
|
72
78
|
rows_action = FakeAction.new
|
|
73
79
|
randomize_rows_action = FakeAction.new
|
|
74
80
|
dedupe_action = FakeAction.new
|
|
81
|
+
parity_action = FakeAction.new
|
|
75
82
|
stdout = StringIO.new
|
|
76
83
|
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
77
|
-
stdin: StringIO.new("3\
|
|
84
|
+
stdin: StringIO.new("3\n6\n"),
|
|
78
85
|
stdout: stdout,
|
|
79
|
-
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
|
|
86
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Validate parity", "Exit"],
|
|
80
87
|
extract_column_action: column_action,
|
|
81
88
|
extract_rows_action: rows_action,
|
|
82
89
|
randomize_rows_action: randomize_rows_action,
|
|
83
|
-
dedupe_action: dedupe_action
|
|
90
|
+
dedupe_action: dedupe_action,
|
|
91
|
+
parity_action: parity_action
|
|
84
92
|
)
|
|
85
93
|
|
|
86
94
|
status = menu.run
|
|
@@ -90,6 +98,7 @@ class MenuLoopTest < Minitest::Test
|
|
|
90
98
|
assert_equal 0, rows_action.runs
|
|
91
99
|
assert_equal 1, randomize_rows_action.runs
|
|
92
100
|
assert_equal 0, dedupe_action.runs
|
|
101
|
+
assert_equal 0, parity_action.runs
|
|
93
102
|
end
|
|
94
103
|
|
|
95
104
|
def test_routes_dedupe_then_exit
|
|
@@ -97,15 +106,17 @@ class MenuLoopTest < Minitest::Test
|
|
|
97
106
|
rows_action = FakeAction.new
|
|
98
107
|
randomize_rows_action = FakeAction.new
|
|
99
108
|
dedupe_action = FakeAction.new
|
|
109
|
+
parity_action = FakeAction.new
|
|
100
110
|
stdout = StringIO.new
|
|
101
111
|
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
102
|
-
stdin: StringIO.new("4\
|
|
112
|
+
stdin: StringIO.new("4\n6\n"),
|
|
103
113
|
stdout: stdout,
|
|
104
|
-
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
|
|
114
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Validate parity", "Exit"],
|
|
105
115
|
extract_column_action: column_action,
|
|
106
116
|
extract_rows_action: rows_action,
|
|
107
117
|
randomize_rows_action: randomize_rows_action,
|
|
108
|
-
dedupe_action: dedupe_action
|
|
118
|
+
dedupe_action: dedupe_action,
|
|
119
|
+
parity_action: parity_action
|
|
109
120
|
)
|
|
110
121
|
|
|
111
122
|
status = menu.run
|
|
@@ -115,6 +126,35 @@ class MenuLoopTest < Minitest::Test
|
|
|
115
126
|
assert_equal 0, rows_action.runs
|
|
116
127
|
assert_equal 0, randomize_rows_action.runs
|
|
117
128
|
assert_equal 1, dedupe_action.runs
|
|
129
|
+
assert_equal 0, parity_action.runs
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_routes_parity_then_exit
|
|
133
|
+
column_action = FakeAction.new
|
|
134
|
+
rows_action = FakeAction.new
|
|
135
|
+
randomize_rows_action = FakeAction.new
|
|
136
|
+
dedupe_action = FakeAction.new
|
|
137
|
+
parity_action = FakeAction.new
|
|
138
|
+
stdout = StringIO.new
|
|
139
|
+
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
140
|
+
stdin: StringIO.new("5\n6\n"),
|
|
141
|
+
stdout: stdout,
|
|
142
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Validate parity", "Exit"],
|
|
143
|
+
extract_column_action: column_action,
|
|
144
|
+
extract_rows_action: rows_action,
|
|
145
|
+
randomize_rows_action: randomize_rows_action,
|
|
146
|
+
dedupe_action: dedupe_action,
|
|
147
|
+
parity_action: parity_action
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
status = menu.run
|
|
151
|
+
|
|
152
|
+
assert_equal 0, status
|
|
153
|
+
assert_equal 0, column_action.runs
|
|
154
|
+
assert_equal 0, rows_action.runs
|
|
155
|
+
assert_equal 0, randomize_rows_action.runs
|
|
156
|
+
assert_equal 0, dedupe_action.runs
|
|
157
|
+
assert_equal 1, parity_action.runs
|
|
118
158
|
end
|
|
119
159
|
|
|
120
160
|
def test_invalid_choice_shows_prompt
|
|
@@ -122,23 +162,26 @@ class MenuLoopTest < Minitest::Test
|
|
|
122
162
|
rows_action = FakeAction.new
|
|
123
163
|
randomize_rows_action = FakeAction.new
|
|
124
164
|
dedupe_action = FakeAction.new
|
|
165
|
+
parity_action = FakeAction.new
|
|
125
166
|
stdout = StringIO.new
|
|
126
167
|
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
127
|
-
stdin: StringIO.new("x\
|
|
168
|
+
stdin: StringIO.new("x\n6\n"),
|
|
128
169
|
stdout: stdout,
|
|
129
|
-
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
|
|
170
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Validate parity", "Exit"],
|
|
130
171
|
extract_column_action: column_action,
|
|
131
172
|
extract_rows_action: rows_action,
|
|
132
173
|
randomize_rows_action: randomize_rows_action,
|
|
133
|
-
dedupe_action: dedupe_action
|
|
174
|
+
dedupe_action: dedupe_action,
|
|
175
|
+
parity_action: parity_action
|
|
134
176
|
)
|
|
135
177
|
|
|
136
178
|
menu.run
|
|
137
179
|
|
|
138
|
-
assert_includes stdout.string, "Please choose 1, 2, 3, 4, or
|
|
180
|
+
assert_includes stdout.string, "Please choose 1, 2, 3, 4, 5, or 6."
|
|
139
181
|
assert_equal 0, column_action.runs
|
|
140
182
|
assert_equal 0, rows_action.runs
|
|
141
183
|
assert_equal 0, randomize_rows_action.runs
|
|
142
184
|
assert_equal 0, dedupe_action.runs
|
|
185
|
+
assert_equal 0, parity_action.runs
|
|
143
186
|
end
|
|
144
187
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/builders/csv_parity_session_builder"
|
|
5
|
+
|
|
6
|
+
class CsvParitySessionBuilderTest < Minitest::Test
|
|
7
|
+
def test_builds_parity_session
|
|
8
|
+
session = Csvtool::Interface::CLI::Workflows::Builders::CsvParitySessionBuilder.new.call(
|
|
9
|
+
left_path: "/tmp/left.csv",
|
|
10
|
+
right_path: "/tmp/right.csv",
|
|
11
|
+
col_sep: "\t",
|
|
12
|
+
headers_present: false
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
assert_equal "/tmp/left.csv", session.source_pair.left_path
|
|
16
|
+
assert_equal "/tmp/right.csv", session.source_pair.right_path
|
|
17
|
+
assert_equal "\t", session.options.separator
|
|
18
|
+
assert_equal false, session.options.headers_present?
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -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,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,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
|