csvops 0.2.0.alpha → 0.4.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 +56 -108
- data/docs/architecture.md +266 -0
- data/docs/release-v0.3.0-alpha.md +74 -0
- data/docs/release-v0.4.0-alpha.md +87 -0
- data/lib/csvtool/application/use_cases/run_cross_csv_dedupe.rb +93 -0
- data/lib/csvtool/application/use_cases/run_extraction.rb +3 -3
- data/lib/csvtool/application/use_cases/run_row_extraction.rb +3 -3
- data/lib/csvtool/application/use_cases/run_row_randomization.rb +105 -0
- data/lib/csvtool/cli.rb +9 -1
- data/lib/csvtool/domain/cross_csv_dedupe_session/column_selector.rb +44 -0
- data/lib/csvtool/domain/cross_csv_dedupe_session/cross_csv_dedupe_session.rb +46 -0
- data/lib/csvtool/domain/cross_csv_dedupe_session/csv_profile.rb +24 -0
- data/lib/csvtool/domain/cross_csv_dedupe_session/key_mapping.rb +22 -0
- data/lib/csvtool/domain/cross_csv_dedupe_session/match_options.rb +29 -0
- data/lib/csvtool/domain/row_randomization_session/randomization_options.rb +17 -0
- data/lib/csvtool/domain/row_randomization_session/randomization_session.rb +25 -0
- data/lib/csvtool/domain/row_randomization_session/randomization_source.rb +24 -0
- data/lib/csvtool/domain/row_session/row_source.rb +3 -0
- data/lib/csvtool/domain/{column_session → shared}/output_destination.rb +1 -1
- data/lib/csvtool/infrastructure/csv/cross_csv_deduper.rb +85 -0
- data/lib/csvtool/infrastructure/csv/row_randomizer.rb +83 -0
- data/lib/csvtool/infrastructure/csv/selector_validator.rb +30 -0
- data/lib/csvtool/interface/cli/errors/presenter.rb +4 -0
- data/lib/csvtool/interface/cli/menu_loop.rb +8 -2
- data/lib/csvtool/interface/cli/prompts/headers_present_prompt.rb +22 -0
- data/lib/csvtool/interface/cli/prompts/seed_prompt.rb +29 -0
- data/lib/csvtool/interface/cli/workflows/run_cross_csv_dedupe_workflow.rb +163 -0
- data/lib/csvtool/version.rb +1 -1
- data/test/csvtool/application/use_cases/run_cross_csv_dedupe_test.rb +113 -0
- data/test/csvtool/application/use_cases/run_row_randomization_test.rb +124 -0
- data/test/csvtool/cli_test.rb +231 -12
- data/test/csvtool/cli_unit_test.rb +27 -2
- data/test/csvtool/domain/column_session/column_session_test.rb +2 -2
- data/test/csvtool/domain/column_session/csv_source_test.rb +10 -0
- data/test/csvtool/domain/cross_csv_dedupe_session/column_selector_test.rb +42 -0
- data/test/csvtool/domain/cross_csv_dedupe_session/cross_csv_dedupe_session_test.rb +75 -0
- data/test/csvtool/domain/cross_csv_dedupe_session/csv_profile_test.rb +26 -0
- data/test/csvtool/domain/cross_csv_dedupe_session/key_mapping_test.rb +31 -0
- data/test/csvtool/domain/cross_csv_dedupe_session/match_options_test.rb +52 -0
- data/test/csvtool/domain/row_randomization_session/randomization_options_test.rb +20 -0
- data/test/csvtool/domain/row_randomization_session/randomization_session_test.rb +26 -0
- data/test/csvtool/domain/row_randomization_session/randomization_source_test.rb +42 -0
- data/test/csvtool/domain/row_session/row_session_test.rb +2 -2
- data/test/csvtool/domain/row_session/row_source_test.rb +16 -0
- data/test/csvtool/domain/shared/output_destination_test.rb +24 -0
- data/test/csvtool/infrastructure/csv/cross_csv_deduper_test.rb +155 -0
- data/test/csvtool/infrastructure/csv/row_randomizer_test.rb +37 -0
- data/test/csvtool/infrastructure/csv/selector_validator_test.rb +72 -0
- data/test/csvtool/interface/cli/errors/presenter_test.rb +2 -0
- data/test/csvtool/interface/cli/menu_loop_test.rb +78 -10
- data/test/csvtool/interface/cli/prompts/headers_present_prompt_test.rb +14 -0
- data/test/csvtool/interface/cli/prompts/seed_prompt_test.rb +39 -0
- data/test/csvtool/interface/cli/workflows/run_cross_csv_dedupe_workflow_test.rb +246 -0
- data/test/fixtures/dedupe_reference.csv +3 -0
- data/test/fixtures/dedupe_reference.tsv +3 -0
- data/test/fixtures/dedupe_reference_all.csv +5 -0
- data/test/fixtures/dedupe_reference_no_headers.csv +2 -0
- data/test/fixtures/dedupe_reference_none.csv +2 -0
- data/test/fixtures/dedupe_reference_normalization.csv +3 -0
- data/test/fixtures/dedupe_source.csv +6 -0
- data/test/fixtures/dedupe_source.tsv +6 -0
- data/test/fixtures/dedupe_source_no_headers.csv +5 -0
- data/test/fixtures/dedupe_source_normalization.csv +4 -0
- data/test/fixtures/sample_people_no_headers.csv +3 -0
- metadata +50 -6
- data/lib/csvtool/domain/row_session/row_output_destination.rb +0 -31
- data/test/csvtool/domain/column_session/output_destination_test.rb +0 -18
- data/test/csvtool/domain/row_session/row_output_destination_test.rb +0 -23
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/row_randomization_session/randomization_source"
|
|
5
|
+
|
|
6
|
+
class RandomizationSourceTest < Minitest::Test
|
|
7
|
+
def test_holds_path_separator_and_headers_mode
|
|
8
|
+
source = Csvtool::Domain::RowRandomizationSession::RandomizationSource.new(
|
|
9
|
+
path: "/tmp/a.csv",
|
|
10
|
+
separator: ",",
|
|
11
|
+
headers_present: true
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
assert_equal "/tmp/a.csv", source.path
|
|
15
|
+
assert_equal ",", source.separator
|
|
16
|
+
assert_equal true, source.headers_present?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_rejects_empty_separator
|
|
20
|
+
error = assert_raises(ArgumentError) do
|
|
21
|
+
Csvtool::Domain::RowRandomizationSession::RandomizationSource.new(
|
|
22
|
+
path: "/tmp/a.csv",
|
|
23
|
+
separator: "",
|
|
24
|
+
headers_present: true
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
assert_equal "separator cannot be empty", error.message
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_rejects_empty_path
|
|
32
|
+
error = assert_raises(ArgumentError) do
|
|
33
|
+
Csvtool::Domain::RowRandomizationSession::RandomizationSource.new(
|
|
34
|
+
path: "",
|
|
35
|
+
separator: ",",
|
|
36
|
+
headers_present: true
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
assert_equal "path cannot be empty", error.message
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -4,7 +4,7 @@ require_relative "../../../test_helper"
|
|
|
4
4
|
require "csvtool/domain/row_session/row_session"
|
|
5
5
|
require "csvtool/domain/row_session/row_source"
|
|
6
6
|
require "csvtool/domain/row_session/row_range"
|
|
7
|
-
require "csvtool/domain/
|
|
7
|
+
require "csvtool/domain/shared/output_destination"
|
|
8
8
|
|
|
9
9
|
class RowSessionTest < Minitest::Test
|
|
10
10
|
def test_starts_and_sets_output_destination
|
|
@@ -12,7 +12,7 @@ class RowSessionTest < Minitest::Test
|
|
|
12
12
|
row_range = Csvtool::Domain::RowSession::RowRange.new(start_row: 1, end_row: 2)
|
|
13
13
|
|
|
14
14
|
session = Csvtool::Domain::RowSession::RowSession.start(source: source, row_range: row_range)
|
|
15
|
-
destination = Csvtool::Domain::
|
|
15
|
+
destination = Csvtool::Domain::Shared::OutputDestination.console
|
|
16
16
|
updated = session.with_output_destination(destination)
|
|
17
17
|
|
|
18
18
|
assert_equal source, updated.source
|
|
@@ -9,4 +9,20 @@ class RowSourceTest < Minitest::Test
|
|
|
9
9
|
assert_equal "/tmp/a.csv", source.path
|
|
10
10
|
assert_equal "\t", source.separator
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
def test_rejects_empty_path
|
|
14
|
+
error = assert_raises(ArgumentError) do
|
|
15
|
+
Csvtool::Domain::RowSession::RowSource.new(path: "", separator: ",")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
assert_equal "path cannot be empty", error.message
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_rejects_empty_separator
|
|
22
|
+
error = assert_raises(ArgumentError) do
|
|
23
|
+
Csvtool::Domain::RowSession::RowSource.new(path: "/tmp/a.csv", separator: "")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
assert_equal "separator cannot be empty", error.message
|
|
27
|
+
end
|
|
12
28
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/shared/output_destination"
|
|
5
|
+
|
|
6
|
+
class SharedOutputDestinationTest < Minitest::Test
|
|
7
|
+
def test_builds_console_and_file_destinations
|
|
8
|
+
console = Csvtool::Domain::Shared::OutputDestination.console
|
|
9
|
+
file = Csvtool::Domain::Shared::OutputDestination.file(path: "/tmp/out.csv")
|
|
10
|
+
|
|
11
|
+
assert_equal true, console.console?
|
|
12
|
+
assert_equal false, console.file?
|
|
13
|
+
assert_equal true, file.file?
|
|
14
|
+
assert_equal "/tmp/out.csv", file.path
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_rejects_empty_file_path
|
|
18
|
+
error = assert_raises(ArgumentError) do
|
|
19
|
+
Csvtool::Domain::Shared::OutputDestination.file(path: "")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
assert_equal "file output path cannot be empty", error.message
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/infrastructure/csv/cross_csv_deduper"
|
|
5
|
+
require "csvtool/domain/cross_csv_dedupe_session/column_selector"
|
|
6
|
+
require "csvtool/domain/cross_csv_dedupe_session/match_options"
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
|
|
9
|
+
class InfrastructureCrossCsvDeduperTest < Minitest::Test
|
|
10
|
+
def fixture_path(name)
|
|
11
|
+
File.expand_path("../../../fixtures/#{name}", __dir__)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_filters_source_rows_by_reference_column_values
|
|
15
|
+
deduper = Csvtool::Infrastructure::CSV::CrossCsvDeduper.new
|
|
16
|
+
|
|
17
|
+
result = deduper.call(
|
|
18
|
+
source_path: fixture_path("dedupe_source.csv"),
|
|
19
|
+
reference_path: fixture_path("dedupe_reference.csv"),
|
|
20
|
+
source_selector: header_selector("customer_id"),
|
|
21
|
+
reference_selector: header_selector("external_id"),
|
|
22
|
+
source_col_sep: ",",
|
|
23
|
+
reference_col_sep: ","
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
assert_equal ["customer_id", "name"], result[:headers]
|
|
27
|
+
assert_equal 5, result[:source_rows]
|
|
28
|
+
assert_equal 3, result[:removed_rows]
|
|
29
|
+
assert_equal 2, result[:kept_rows_count]
|
|
30
|
+
assert_equal [%w[1 Alice], %w[3 Cara]], result[:kept_rows]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_normalization_trim_on_case_off
|
|
34
|
+
deduper = Csvtool::Infrastructure::CSV::CrossCsvDeduper.new
|
|
35
|
+
|
|
36
|
+
result = deduper.call(
|
|
37
|
+
source_path: fixture_path("dedupe_source_normalization.csv"),
|
|
38
|
+
reference_path: fixture_path("dedupe_reference_normalization.csv"),
|
|
39
|
+
source_selector: header_selector("customer_id"),
|
|
40
|
+
reference_selector: header_selector("external_id"),
|
|
41
|
+
match_options: match_options(trim_whitespace: true, case_insensitive: false)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assert_equal 3, result[:kept_rows_count]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_normalization_trim_on_case_on
|
|
48
|
+
deduper = Csvtool::Infrastructure::CSV::CrossCsvDeduper.new
|
|
49
|
+
|
|
50
|
+
result = deduper.call(
|
|
51
|
+
source_path: fixture_path("dedupe_source_normalization.csv"),
|
|
52
|
+
reference_path: fixture_path("dedupe_reference_normalization.csv"),
|
|
53
|
+
source_selector: header_selector("customer_id"),
|
|
54
|
+
reference_selector: header_selector("external_id"),
|
|
55
|
+
match_options: match_options(trim_whitespace: true, case_insensitive: true)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
assert_equal 1, result[:kept_rows_count]
|
|
59
|
+
assert_equal [%w[B2 Bob]], result[:kept_rows]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_normalization_trim_off_case_on
|
|
63
|
+
deduper = Csvtool::Infrastructure::CSV::CrossCsvDeduper.new
|
|
64
|
+
|
|
65
|
+
result = deduper.call(
|
|
66
|
+
source_path: fixture_path("dedupe_source_normalization.csv"),
|
|
67
|
+
reference_path: fixture_path("dedupe_reference_normalization.csv"),
|
|
68
|
+
source_selector: header_selector("customer_id"),
|
|
69
|
+
reference_selector: header_selector("external_id"),
|
|
70
|
+
match_options: match_options(trim_whitespace: false, case_insensitive: true)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
assert_equal 2, result[:kept_rows_count]
|
|
74
|
+
assert_equal [[" A1 ", "Alice"], %w[B2 Bob]], result[:kept_rows]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_normalization_trim_off_case_off
|
|
78
|
+
deduper = Csvtool::Infrastructure::CSV::CrossCsvDeduper.new
|
|
79
|
+
|
|
80
|
+
result = deduper.call(
|
|
81
|
+
source_path: fixture_path("dedupe_source_normalization.csv"),
|
|
82
|
+
reference_path: fixture_path("dedupe_reference_normalization.csv"),
|
|
83
|
+
source_selector: header_selector("customer_id"),
|
|
84
|
+
reference_selector: header_selector("external_id"),
|
|
85
|
+
match_options: match_options(trim_whitespace: false, case_insensitive: false)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
assert_equal 3, result[:kept_rows_count]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_each_retained_streams_rows_and_reports_stats
|
|
92
|
+
deduper = Csvtool::Infrastructure::CSV::CrossCsvDeduper.new
|
|
93
|
+
yielded_rows = []
|
|
94
|
+
|
|
95
|
+
result = deduper.each_retained(
|
|
96
|
+
source_path: fixture_path("dedupe_source.csv"),
|
|
97
|
+
reference_path: fixture_path("dedupe_reference.csv"),
|
|
98
|
+
source_selector: header_selector("customer_id"),
|
|
99
|
+
reference_selector: header_selector("external_id")
|
|
100
|
+
) { |fields| yielded_rows << fields }
|
|
101
|
+
|
|
102
|
+
assert_equal [%w[1 Alice], %w[3 Cara]], yielded_rows
|
|
103
|
+
assert_equal 5, result[:source_rows]
|
|
104
|
+
assert_equal 3, result[:removed_rows]
|
|
105
|
+
assert_equal 2, result[:kept_rows_count]
|
|
106
|
+
refute_includes result.keys, :kept_rows
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_each_retained_supports_large_inputs_with_streaming
|
|
110
|
+
deduper = Csvtool::Infrastructure::CSV::CrossCsvDeduper.new
|
|
111
|
+
|
|
112
|
+
Dir.mktmpdir do |dir|
|
|
113
|
+
source_path = File.join(dir, "source.csv")
|
|
114
|
+
reference_path = File.join(dir, "reference.csv")
|
|
115
|
+
|
|
116
|
+
File.open(source_path, "w") do |file|
|
|
117
|
+
file.puts "id,name"
|
|
118
|
+
10_000.times { |index| file.puts "#{index},name#{index}" }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
File.open(reference_path, "w") do |file|
|
|
122
|
+
file.puts "external_id"
|
|
123
|
+
10_000.times do |index|
|
|
124
|
+
file.puts index.to_s if (index % 2).zero?
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
yielded_count = 0
|
|
129
|
+
result = deduper.each_retained(
|
|
130
|
+
source_path: source_path,
|
|
131
|
+
reference_path: reference_path,
|
|
132
|
+
source_selector: header_selector("id"),
|
|
133
|
+
reference_selector: header_selector("external_id")
|
|
134
|
+
) { |_fields| yielded_count += 1 }
|
|
135
|
+
|
|
136
|
+
assert_equal 10_000, result[:source_rows]
|
|
137
|
+
assert_equal 5_000, result[:removed_rows]
|
|
138
|
+
assert_equal 5_000, result[:kept_rows_count]
|
|
139
|
+
assert_equal 5_000, yielded_count
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def header_selector(name)
|
|
146
|
+
Csvtool::Domain::CrossCsvDedupeSession::ColumnSelector.from_input(headers_present: true, input: name)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def match_options(trim_whitespace:, case_insensitive:)
|
|
150
|
+
Csvtool::Domain::CrossCsvDedupeSession::MatchOptions.new(
|
|
151
|
+
trim_whitespace: trim_whitespace,
|
|
152
|
+
case_insensitive: case_insensitive
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/infrastructure/csv/row_randomizer"
|
|
5
|
+
|
|
6
|
+
class InfrastructureRowRandomizerTest < Minitest::Test
|
|
7
|
+
def fixture_path(name)
|
|
8
|
+
File.expand_path("../../../fixtures/#{name}", __dir__)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_randomizes_rows_and_preserves_membership
|
|
12
|
+
randomizer = Csvtool::Infrastructure::CSV::RowRandomizer.new
|
|
13
|
+
|
|
14
|
+
rows = randomizer.call(file_path: fixture_path("sample_people.csv"), col_sep: ",", headers: true, seed: 1234)
|
|
15
|
+
|
|
16
|
+
assert_equal 3, rows.length
|
|
17
|
+
assert_equal [%w[Alice London], %w[Bob Paris], %w[Cara Berlin]].sort, rows.sort
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_same_seed_returns_same_order
|
|
21
|
+
randomizer = Csvtool::Infrastructure::CSV::RowRandomizer.new
|
|
22
|
+
|
|
23
|
+
one = randomizer.call(file_path: fixture_path("sample_people_many.csv"), col_sep: ",", headers: true, seed: 42)
|
|
24
|
+
two = randomizer.call(file_path: fixture_path("sample_people_many.csv"), col_sep: ",", headers: true, seed: 42)
|
|
25
|
+
|
|
26
|
+
assert_equal one, two
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_different_seed_changes_order
|
|
30
|
+
randomizer = Csvtool::Infrastructure::CSV::RowRandomizer.new
|
|
31
|
+
|
|
32
|
+
one = randomizer.call(file_path: fixture_path("sample_people_many.csv"), col_sep: ",", headers: true, seed: 42)
|
|
33
|
+
two = randomizer.call(file_path: fixture_path("sample_people_many.csv"), col_sep: ",", headers: true, seed: 43)
|
|
34
|
+
|
|
35
|
+
refute_equal one, two
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/infrastructure/csv/selector_validator"
|
|
5
|
+
require "csvtool/domain/cross_csv_dedupe_session/csv_profile"
|
|
6
|
+
require "csvtool/domain/cross_csv_dedupe_session/column_selector"
|
|
7
|
+
|
|
8
|
+
class InfrastructureSelectorValidatorTest < Minitest::Test
|
|
9
|
+
def fixture_path(name)
|
|
10
|
+
File.expand_path("../../../fixtures/#{name}", __dir__)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_accepts_header_selector_when_column_exists
|
|
14
|
+
validator = Csvtool::Infrastructure::CSV::SelectorValidator.new
|
|
15
|
+
profile = Csvtool::Domain::CrossCsvDedupeSession::CsvProfile.new(
|
|
16
|
+
path: fixture_path("dedupe_source.csv"),
|
|
17
|
+
separator: ",",
|
|
18
|
+
headers_present: true
|
|
19
|
+
)
|
|
20
|
+
selector = Csvtool::Domain::CrossCsvDedupeSession::ColumnSelector.from_input(
|
|
21
|
+
headers_present: true,
|
|
22
|
+
input: "customer_id"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
assert_equal true, validator.valid?(profile: profile, selector: selector)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_rejects_header_selector_when_column_missing
|
|
29
|
+
validator = Csvtool::Infrastructure::CSV::SelectorValidator.new
|
|
30
|
+
profile = Csvtool::Domain::CrossCsvDedupeSession::CsvProfile.new(
|
|
31
|
+
path: fixture_path("dedupe_source.csv"),
|
|
32
|
+
separator: ",",
|
|
33
|
+
headers_present: true
|
|
34
|
+
)
|
|
35
|
+
selector = Csvtool::Domain::CrossCsvDedupeSession::ColumnSelector.from_input(
|
|
36
|
+
headers_present: true,
|
|
37
|
+
input: "missing"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
assert_equal false, validator.valid?(profile: profile, selector: selector)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_accepts_index_selector_when_in_range
|
|
44
|
+
validator = Csvtool::Infrastructure::CSV::SelectorValidator.new
|
|
45
|
+
profile = Csvtool::Domain::CrossCsvDedupeSession::CsvProfile.new(
|
|
46
|
+
path: fixture_path("dedupe_source_no_headers.csv"),
|
|
47
|
+
separator: ",",
|
|
48
|
+
headers_present: false
|
|
49
|
+
)
|
|
50
|
+
selector = Csvtool::Domain::CrossCsvDedupeSession::ColumnSelector.from_input(
|
|
51
|
+
headers_present: false,
|
|
52
|
+
input: "2"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert_equal true, validator.valid?(profile: profile, selector: selector)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_rejects_index_selector_when_out_of_range
|
|
59
|
+
validator = Csvtool::Infrastructure::CSV::SelectorValidator.new
|
|
60
|
+
profile = Csvtool::Domain::CrossCsvDedupeSession::CsvProfile.new(
|
|
61
|
+
path: fixture_path("dedupe_source_no_headers.csv"),
|
|
62
|
+
separator: ",",
|
|
63
|
+
headers_present: false
|
|
64
|
+
)
|
|
65
|
+
selector = Csvtool::Domain::CrossCsvDedupeSession::ColumnSelector.from_input(
|
|
66
|
+
headers_present: false,
|
|
67
|
+
input: "9"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert_equal false, validator.valid?(profile: profile, selector: selector)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -18,6 +18,7 @@ class ErrorsPresenterTest < Minitest::Test
|
|
|
18
18
|
presenter.invalid_output_destination
|
|
19
19
|
presenter.empty_custom_separator
|
|
20
20
|
presenter.invalid_separator_choice
|
|
21
|
+
presenter.invalid_seed
|
|
21
22
|
presenter.canceled
|
|
22
23
|
presenter.invalid_start_row
|
|
23
24
|
presenter.invalid_end_row
|
|
@@ -35,6 +36,7 @@ class ErrorsPresenterTest < Minitest::Test
|
|
|
35
36
|
assert_includes text, "Invalid output destination."
|
|
36
37
|
assert_includes text, "Separator cannot be empty."
|
|
37
38
|
assert_includes text, "Invalid separator choice."
|
|
39
|
+
assert_includes text, "Seed must be an integer."
|
|
38
40
|
assert_includes text, "Canceled."
|
|
39
41
|
assert_includes text, "Start row must be a positive integer."
|
|
40
42
|
assert_includes text, "End row must be a positive integer."
|
|
@@ -19,13 +19,17 @@ class MenuLoopTest < Minitest::Test
|
|
|
19
19
|
def test_routes_extract_column_then_exit
|
|
20
20
|
column_action = FakeAction.new
|
|
21
21
|
rows_action = FakeAction.new
|
|
22
|
+
randomize_rows_action = FakeAction.new
|
|
23
|
+
dedupe_action = FakeAction.new
|
|
22
24
|
stdout = StringIO.new
|
|
23
25
|
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
24
|
-
stdin: StringIO.new("1\
|
|
26
|
+
stdin: StringIO.new("1\n5\n"),
|
|
25
27
|
stdout: stdout,
|
|
26
|
-
menu_options: ["Extract column", "Extract rows (range)", "Exit"],
|
|
28
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
|
|
27
29
|
extract_column_action: column_action,
|
|
28
|
-
extract_rows_action: rows_action
|
|
30
|
+
extract_rows_action: rows_action,
|
|
31
|
+
randomize_rows_action: randomize_rows_action,
|
|
32
|
+
dedupe_action: dedupe_action
|
|
29
33
|
)
|
|
30
34
|
|
|
31
35
|
status = menu.run
|
|
@@ -33,19 +37,25 @@ class MenuLoopTest < Minitest::Test
|
|
|
33
37
|
assert_equal 0, status
|
|
34
38
|
assert_equal 1, column_action.runs
|
|
35
39
|
assert_equal 0, rows_action.runs
|
|
40
|
+
assert_equal 0, randomize_rows_action.runs
|
|
41
|
+
assert_equal 0, dedupe_action.runs
|
|
36
42
|
assert_includes stdout.string, "CSV Tool Menu"
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
def test_routes_extract_rows_then_exit
|
|
40
46
|
column_action = FakeAction.new
|
|
41
47
|
rows_action = FakeAction.new
|
|
48
|
+
randomize_rows_action = FakeAction.new
|
|
49
|
+
dedupe_action = FakeAction.new
|
|
42
50
|
stdout = StringIO.new
|
|
43
51
|
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
44
|
-
stdin: StringIO.new("2\
|
|
52
|
+
stdin: StringIO.new("2\n5\n"),
|
|
45
53
|
stdout: stdout,
|
|
46
|
-
menu_options: ["Extract column", "Extract rows (range)", "Exit"],
|
|
54
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
|
|
47
55
|
extract_column_action: column_action,
|
|
48
|
-
extract_rows_action: rows_action
|
|
56
|
+
extract_rows_action: rows_action,
|
|
57
|
+
randomize_rows_action: randomize_rows_action,
|
|
58
|
+
dedupe_action: dedupe_action
|
|
49
59
|
)
|
|
50
60
|
|
|
51
61
|
status = menu.run
|
|
@@ -53,24 +63,82 @@ class MenuLoopTest < Minitest::Test
|
|
|
53
63
|
assert_equal 0, status
|
|
54
64
|
assert_equal 0, column_action.runs
|
|
55
65
|
assert_equal 1, rows_action.runs
|
|
66
|
+
assert_equal 0, randomize_rows_action.runs
|
|
67
|
+
assert_equal 0, dedupe_action.runs
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_routes_randomize_rows_then_exit
|
|
71
|
+
column_action = FakeAction.new
|
|
72
|
+
rows_action = FakeAction.new
|
|
73
|
+
randomize_rows_action = FakeAction.new
|
|
74
|
+
dedupe_action = FakeAction.new
|
|
75
|
+
stdout = StringIO.new
|
|
76
|
+
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
77
|
+
stdin: StringIO.new("3\n5\n"),
|
|
78
|
+
stdout: stdout,
|
|
79
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
|
|
80
|
+
extract_column_action: column_action,
|
|
81
|
+
extract_rows_action: rows_action,
|
|
82
|
+
randomize_rows_action: randomize_rows_action,
|
|
83
|
+
dedupe_action: dedupe_action
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
status = menu.run
|
|
87
|
+
|
|
88
|
+
assert_equal 0, status
|
|
89
|
+
assert_equal 0, column_action.runs
|
|
90
|
+
assert_equal 0, rows_action.runs
|
|
91
|
+
assert_equal 1, randomize_rows_action.runs
|
|
92
|
+
assert_equal 0, dedupe_action.runs
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_routes_dedupe_then_exit
|
|
96
|
+
column_action = FakeAction.new
|
|
97
|
+
rows_action = FakeAction.new
|
|
98
|
+
randomize_rows_action = FakeAction.new
|
|
99
|
+
dedupe_action = FakeAction.new
|
|
100
|
+
stdout = StringIO.new
|
|
101
|
+
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
102
|
+
stdin: StringIO.new("4\n5\n"),
|
|
103
|
+
stdout: stdout,
|
|
104
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
|
|
105
|
+
extract_column_action: column_action,
|
|
106
|
+
extract_rows_action: rows_action,
|
|
107
|
+
randomize_rows_action: randomize_rows_action,
|
|
108
|
+
dedupe_action: dedupe_action
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
status = menu.run
|
|
112
|
+
|
|
113
|
+
assert_equal 0, status
|
|
114
|
+
assert_equal 0, column_action.runs
|
|
115
|
+
assert_equal 0, rows_action.runs
|
|
116
|
+
assert_equal 0, randomize_rows_action.runs
|
|
117
|
+
assert_equal 1, dedupe_action.runs
|
|
56
118
|
end
|
|
57
119
|
|
|
58
120
|
def test_invalid_choice_shows_prompt
|
|
59
121
|
column_action = FakeAction.new
|
|
60
122
|
rows_action = FakeAction.new
|
|
123
|
+
randomize_rows_action = FakeAction.new
|
|
124
|
+
dedupe_action = FakeAction.new
|
|
61
125
|
stdout = StringIO.new
|
|
62
126
|
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
63
|
-
stdin: StringIO.new("x\
|
|
127
|
+
stdin: StringIO.new("x\n5\n"),
|
|
64
128
|
stdout: stdout,
|
|
65
|
-
menu_options: ["Extract column", "Extract rows (range)", "Exit"],
|
|
129
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
|
|
66
130
|
extract_column_action: column_action,
|
|
67
|
-
extract_rows_action: rows_action
|
|
131
|
+
extract_rows_action: rows_action,
|
|
132
|
+
randomize_rows_action: randomize_rows_action,
|
|
133
|
+
dedupe_action: dedupe_action
|
|
68
134
|
)
|
|
69
135
|
|
|
70
136
|
menu.run
|
|
71
137
|
|
|
72
|
-
assert_includes stdout.string, "Please choose 1, 2, or
|
|
138
|
+
assert_includes stdout.string, "Please choose 1, 2, 3, 4, or 5."
|
|
73
139
|
assert_equal 0, column_action.runs
|
|
74
140
|
assert_equal 0, rows_action.runs
|
|
141
|
+
assert_equal 0, randomize_rows_action.runs
|
|
142
|
+
assert_equal 0, dedupe_action.runs
|
|
75
143
|
end
|
|
76
144
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/prompts/headers_present_prompt"
|
|
5
|
+
|
|
6
|
+
class HeadersPresentPromptTest < Minitest::Test
|
|
7
|
+
def test_defaults_to_true_and_accepts_negative_inputs
|
|
8
|
+
yes_prompt = Csvtool::Interface::CLI::Prompts::HeadersPresentPrompt.new(stdin: StringIO.new("\n"), stdout: StringIO.new)
|
|
9
|
+
no_prompt = Csvtool::Interface::CLI::Prompts::HeadersPresentPrompt.new(stdin: StringIO.new("n\n"), stdout: StringIO.new)
|
|
10
|
+
|
|
11
|
+
assert_equal true, yes_prompt.call
|
|
12
|
+
assert_equal false, no_prompt.call
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/prompts/seed_prompt"
|
|
5
|
+
|
|
6
|
+
class SeedPromptTest < Minitest::Test
|
|
7
|
+
class FakeErrors
|
|
8
|
+
attr_reader :calls
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@calls = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def invalid_seed
|
|
15
|
+
@calls << :invalid_seed
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_blank_returns_nil
|
|
20
|
+
errors = FakeErrors.new
|
|
21
|
+
prompt = Csvtool::Interface::CLI::Prompts::SeedPrompt.new(stdin: StringIO.new("\n"), stdout: StringIO.new, errors: errors)
|
|
22
|
+
assert_nil prompt.call
|
|
23
|
+
assert_empty errors.calls
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_integer_returns_seed
|
|
27
|
+
errors = FakeErrors.new
|
|
28
|
+
prompt = Csvtool::Interface::CLI::Prompts::SeedPrompt.new(stdin: StringIO.new("42\n"), stdout: StringIO.new, errors: errors)
|
|
29
|
+
assert_equal 42, prompt.call
|
|
30
|
+
assert_empty errors.calls
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_invalid_reports_error
|
|
34
|
+
errors = FakeErrors.new
|
|
35
|
+
prompt = Csvtool::Interface::CLI::Prompts::SeedPrompt.new(stdin: StringIO.new("abc\n"), stdout: StringIO.new, errors: errors)
|
|
36
|
+
assert_equal Csvtool::Interface::CLI::Prompts::SeedPrompt::INVALID, prompt.call
|
|
37
|
+
assert_includes errors.calls, :invalid_seed
|
|
38
|
+
end
|
|
39
|
+
end
|