csvops 0.3.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +56 -142
  3. data/docs/architecture.md +266 -0
  4. data/docs/release-v0.4.0-alpha.md +87 -0
  5. data/lib/csvtool/application/use_cases/run_cross_csv_dedupe.rb +93 -0
  6. data/lib/csvtool/application/use_cases/run_extraction.rb +3 -3
  7. data/lib/csvtool/application/use_cases/run_row_extraction.rb +3 -3
  8. data/lib/csvtool/application/use_cases/run_row_randomization.rb +3 -3
  9. data/lib/csvtool/cli.rb +5 -1
  10. data/lib/csvtool/domain/cross_csv_dedupe_session/column_selector.rb +44 -0
  11. data/lib/csvtool/domain/cross_csv_dedupe_session/cross_csv_dedupe_session.rb +46 -0
  12. data/lib/csvtool/domain/cross_csv_dedupe_session/csv_profile.rb +24 -0
  13. data/lib/csvtool/domain/cross_csv_dedupe_session/key_mapping.rb +22 -0
  14. data/lib/csvtool/domain/cross_csv_dedupe_session/match_options.rb +29 -0
  15. data/lib/csvtool/domain/row_randomization_session/randomization_source.rb +1 -0
  16. data/lib/csvtool/domain/row_session/row_source.rb +3 -0
  17. data/lib/csvtool/domain/{column_session → shared}/output_destination.rb +1 -1
  18. data/lib/csvtool/infrastructure/csv/cross_csv_deduper.rb +85 -0
  19. data/lib/csvtool/infrastructure/csv/selector_validator.rb +30 -0
  20. data/lib/csvtool/interface/cli/menu_loop.rb +5 -2
  21. data/lib/csvtool/interface/cli/workflows/run_cross_csv_dedupe_workflow.rb +163 -0
  22. data/lib/csvtool/version.rb +1 -1
  23. data/test/csvtool/application/use_cases/run_cross_csv_dedupe_test.rb +113 -0
  24. data/test/csvtool/cli_test.rb +130 -16
  25. data/test/csvtool/cli_unit_test.rb +16 -3
  26. data/test/csvtool/domain/column_session/column_session_test.rb +2 -2
  27. data/test/csvtool/domain/column_session/csv_source_test.rb +10 -0
  28. data/test/csvtool/domain/cross_csv_dedupe_session/column_selector_test.rb +42 -0
  29. data/test/csvtool/domain/cross_csv_dedupe_session/cross_csv_dedupe_session_test.rb +75 -0
  30. data/test/csvtool/domain/cross_csv_dedupe_session/csv_profile_test.rb +26 -0
  31. data/test/csvtool/domain/cross_csv_dedupe_session/key_mapping_test.rb +31 -0
  32. data/test/csvtool/domain/cross_csv_dedupe_session/match_options_test.rb +52 -0
  33. data/test/csvtool/domain/row_randomization_session/randomization_session_test.rb +2 -2
  34. data/test/csvtool/domain/row_randomization_session/randomization_source_test.rb +15 -1
  35. data/test/csvtool/domain/row_session/row_session_test.rb +2 -2
  36. data/test/csvtool/domain/row_session/row_source_test.rb +16 -0
  37. data/test/csvtool/domain/shared/output_destination_test.rb +24 -0
  38. data/test/csvtool/infrastructure/csv/cross_csv_deduper_test.rb +155 -0
  39. data/test/csvtool/infrastructure/csv/selector_validator_test.rb +72 -0
  40. data/test/csvtool/interface/cli/menu_loop_test.rb +50 -13
  41. data/test/csvtool/interface/cli/workflows/run_cross_csv_dedupe_workflow_test.rb +246 -0
  42. data/test/fixtures/dedupe_reference.csv +3 -0
  43. data/test/fixtures/dedupe_reference.tsv +3 -0
  44. data/test/fixtures/dedupe_reference_all.csv +5 -0
  45. data/test/fixtures/dedupe_reference_no_headers.csv +2 -0
  46. data/test/fixtures/dedupe_reference_none.csv +2 -0
  47. data/test/fixtures/dedupe_reference_normalization.csv +3 -0
  48. data/test/fixtures/dedupe_source.csv +6 -0
  49. data/test/fixtures/dedupe_source.tsv +6 -0
  50. data/test/fixtures/dedupe_source_no_headers.csv +5 -0
  51. data/test/fixtures/dedupe_source_normalization.csv +4 -0
  52. metadata +34 -8
  53. data/lib/csvtool/domain/row_randomization_session/randomization_output_destination.rb +0 -31
  54. data/lib/csvtool/domain/row_session/row_output_destination.rb +0 -31
  55. data/test/csvtool/domain/column_session/output_destination_test.rb +0 -18
  56. data/test/csvtool/domain/row_randomization_session/randomization_output_destination_test.rb +0 -21
  57. data/test/csvtool/domain/row_session/row_output_destination_test.rb +0 -23
@@ -4,7 +4,7 @@ require_relative "../../../test_helper"
4
4
  require "csvtool/domain/row_randomization_session/randomization_session"
5
5
  require "csvtool/domain/row_randomization_session/randomization_source"
6
6
  require "csvtool/domain/row_randomization_session/randomization_options"
7
- require "csvtool/domain/row_randomization_session/randomization_output_destination"
7
+ require "csvtool/domain/shared/output_destination"
8
8
 
9
9
  class RandomizationSessionTest < Minitest::Test
10
10
  def test_with_output_destination_returns_updated_session
@@ -15,7 +15,7 @@ class RandomizationSessionTest < Minitest::Test
15
15
  )
16
16
  options = Csvtool::Domain::RowRandomizationSession::RandomizationOptions.new(seed: 7)
17
17
  session = Csvtool::Domain::RowRandomizationSession::RandomizationSession.start(source: source, options: options)
18
- destination = Csvtool::Domain::RowRandomizationSession::RandomizationOutputDestination.console
18
+ destination = Csvtool::Domain::Shared::OutputDestination.console
19
19
 
20
20
  updated = session.with_output_destination(destination)
21
21
 
@@ -17,12 +17,26 @@ class RandomizationSourceTest < Minitest::Test
17
17
  end
18
18
 
19
19
  def test_rejects_empty_separator
20
- assert_raises(ArgumentError) do
20
+ error = assert_raises(ArgumentError) do
21
21
  Csvtool::Domain::RowRandomizationSession::RandomizationSource.new(
22
22
  path: "/tmp/a.csv",
23
23
  separator: "",
24
24
  headers_present: true
25
25
  )
26
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
27
41
  end
28
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/row_session/row_output_destination"
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::RowSession::RowOutputDestination.console
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,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
@@ -20,14 +20,16 @@ class MenuLoopTest < Minitest::Test
20
20
  column_action = FakeAction.new
21
21
  rows_action = FakeAction.new
22
22
  randomize_rows_action = FakeAction.new
23
+ dedupe_action = FakeAction.new
23
24
  stdout = StringIO.new
24
25
  menu = Csvtool::Interface::CLI::MenuLoop.new(
25
- stdin: StringIO.new("1\n4\n"),
26
+ stdin: StringIO.new("1\n5\n"),
26
27
  stdout: stdout,
27
- menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Exit"],
28
+ menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
28
29
  extract_column_action: column_action,
29
30
  extract_rows_action: rows_action,
30
- randomize_rows_action: randomize_rows_action
31
+ randomize_rows_action: randomize_rows_action,
32
+ dedupe_action: dedupe_action
31
33
  )
32
34
 
33
35
  status = menu.run
@@ -36,6 +38,7 @@ class MenuLoopTest < Minitest::Test
36
38
  assert_equal 1, column_action.runs
37
39
  assert_equal 0, rows_action.runs
38
40
  assert_equal 0, randomize_rows_action.runs
41
+ assert_equal 0, dedupe_action.runs
39
42
  assert_includes stdout.string, "CSV Tool Menu"
40
43
  end
41
44
 
@@ -43,14 +46,16 @@ class MenuLoopTest < Minitest::Test
43
46
  column_action = FakeAction.new
44
47
  rows_action = FakeAction.new
45
48
  randomize_rows_action = FakeAction.new
49
+ dedupe_action = FakeAction.new
46
50
  stdout = StringIO.new
47
51
  menu = Csvtool::Interface::CLI::MenuLoop.new(
48
- stdin: StringIO.new("2\n4\n"),
52
+ stdin: StringIO.new("2\n5\n"),
49
53
  stdout: stdout,
50
- menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Exit"],
54
+ menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
51
55
  extract_column_action: column_action,
52
56
  extract_rows_action: rows_action,
53
- randomize_rows_action: randomize_rows_action
57
+ randomize_rows_action: randomize_rows_action,
58
+ dedupe_action: dedupe_action
54
59
  )
55
60
 
56
61
  status = menu.run
@@ -59,20 +64,23 @@ class MenuLoopTest < Minitest::Test
59
64
  assert_equal 0, column_action.runs
60
65
  assert_equal 1, rows_action.runs
61
66
  assert_equal 0, randomize_rows_action.runs
67
+ assert_equal 0, dedupe_action.runs
62
68
  end
63
69
 
64
70
  def test_routes_randomize_rows_then_exit
65
71
  column_action = FakeAction.new
66
72
  rows_action = FakeAction.new
67
73
  randomize_rows_action = FakeAction.new
74
+ dedupe_action = FakeAction.new
68
75
  stdout = StringIO.new
69
76
  menu = Csvtool::Interface::CLI::MenuLoop.new(
70
- stdin: StringIO.new("3\n4\n"),
77
+ stdin: StringIO.new("3\n5\n"),
71
78
  stdout: stdout,
72
- menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Exit"],
79
+ menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
73
80
  extract_column_action: column_action,
74
81
  extract_rows_action: rows_action,
75
- randomize_rows_action: randomize_rows_action
82
+ randomize_rows_action: randomize_rows_action,
83
+ dedupe_action: dedupe_action
76
84
  )
77
85
 
78
86
  status = menu.run
@@ -81,27 +89,56 @@ class MenuLoopTest < Minitest::Test
81
89
  assert_equal 0, column_action.runs
82
90
  assert_equal 0, rows_action.runs
83
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
84
118
  end
85
119
 
86
120
  def test_invalid_choice_shows_prompt
87
121
  column_action = FakeAction.new
88
122
  rows_action = FakeAction.new
89
123
  randomize_rows_action = FakeAction.new
124
+ dedupe_action = FakeAction.new
90
125
  stdout = StringIO.new
91
126
  menu = Csvtool::Interface::CLI::MenuLoop.new(
92
- stdin: StringIO.new("x\n4\n"),
127
+ stdin: StringIO.new("x\n5\n"),
93
128
  stdout: stdout,
94
- menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Exit"],
129
+ menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
95
130
  extract_column_action: column_action,
96
131
  extract_rows_action: rows_action,
97
- randomize_rows_action: randomize_rows_action
132
+ randomize_rows_action: randomize_rows_action,
133
+ dedupe_action: dedupe_action
98
134
  )
99
135
 
100
136
  menu.run
101
137
 
102
- assert_includes stdout.string, "Please choose 1, 2, 3, or 4."
138
+ assert_includes stdout.string, "Please choose 1, 2, 3, 4, or 5."
103
139
  assert_equal 0, column_action.runs
104
140
  assert_equal 0, rows_action.runs
105
141
  assert_equal 0, randomize_rows_action.runs
142
+ assert_equal 0, dedupe_action.runs
106
143
  end
107
144
  end