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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +56 -108
  3. data/docs/architecture.md +266 -0
  4. data/docs/release-v0.3.0-alpha.md +74 -0
  5. data/docs/release-v0.4.0-alpha.md +87 -0
  6. data/lib/csvtool/application/use_cases/run_cross_csv_dedupe.rb +93 -0
  7. data/lib/csvtool/application/use_cases/run_extraction.rb +3 -3
  8. data/lib/csvtool/application/use_cases/run_row_extraction.rb +3 -3
  9. data/lib/csvtool/application/use_cases/run_row_randomization.rb +105 -0
  10. data/lib/csvtool/cli.rb +9 -1
  11. data/lib/csvtool/domain/cross_csv_dedupe_session/column_selector.rb +44 -0
  12. data/lib/csvtool/domain/cross_csv_dedupe_session/cross_csv_dedupe_session.rb +46 -0
  13. data/lib/csvtool/domain/cross_csv_dedupe_session/csv_profile.rb +24 -0
  14. data/lib/csvtool/domain/cross_csv_dedupe_session/key_mapping.rb +22 -0
  15. data/lib/csvtool/domain/cross_csv_dedupe_session/match_options.rb +29 -0
  16. data/lib/csvtool/domain/row_randomization_session/randomization_options.rb +17 -0
  17. data/lib/csvtool/domain/row_randomization_session/randomization_session.rb +25 -0
  18. data/lib/csvtool/domain/row_randomization_session/randomization_source.rb +24 -0
  19. data/lib/csvtool/domain/row_session/row_source.rb +3 -0
  20. data/lib/csvtool/domain/{column_session → shared}/output_destination.rb +1 -1
  21. data/lib/csvtool/infrastructure/csv/cross_csv_deduper.rb +85 -0
  22. data/lib/csvtool/infrastructure/csv/row_randomizer.rb +83 -0
  23. data/lib/csvtool/infrastructure/csv/selector_validator.rb +30 -0
  24. data/lib/csvtool/interface/cli/errors/presenter.rb +4 -0
  25. data/lib/csvtool/interface/cli/menu_loop.rb +8 -2
  26. data/lib/csvtool/interface/cli/prompts/headers_present_prompt.rb +22 -0
  27. data/lib/csvtool/interface/cli/prompts/seed_prompt.rb +29 -0
  28. data/lib/csvtool/interface/cli/workflows/run_cross_csv_dedupe_workflow.rb +163 -0
  29. data/lib/csvtool/version.rb +1 -1
  30. data/test/csvtool/application/use_cases/run_cross_csv_dedupe_test.rb +113 -0
  31. data/test/csvtool/application/use_cases/run_row_randomization_test.rb +124 -0
  32. data/test/csvtool/cli_test.rb +231 -12
  33. data/test/csvtool/cli_unit_test.rb +27 -2
  34. data/test/csvtool/domain/column_session/column_session_test.rb +2 -2
  35. data/test/csvtool/domain/column_session/csv_source_test.rb +10 -0
  36. data/test/csvtool/domain/cross_csv_dedupe_session/column_selector_test.rb +42 -0
  37. data/test/csvtool/domain/cross_csv_dedupe_session/cross_csv_dedupe_session_test.rb +75 -0
  38. data/test/csvtool/domain/cross_csv_dedupe_session/csv_profile_test.rb +26 -0
  39. data/test/csvtool/domain/cross_csv_dedupe_session/key_mapping_test.rb +31 -0
  40. data/test/csvtool/domain/cross_csv_dedupe_session/match_options_test.rb +52 -0
  41. data/test/csvtool/domain/row_randomization_session/randomization_options_test.rb +20 -0
  42. data/test/csvtool/domain/row_randomization_session/randomization_session_test.rb +26 -0
  43. data/test/csvtool/domain/row_randomization_session/randomization_source_test.rb +42 -0
  44. data/test/csvtool/domain/row_session/row_session_test.rb +2 -2
  45. data/test/csvtool/domain/row_session/row_source_test.rb +16 -0
  46. data/test/csvtool/domain/shared/output_destination_test.rb +24 -0
  47. data/test/csvtool/infrastructure/csv/cross_csv_deduper_test.rb +155 -0
  48. data/test/csvtool/infrastructure/csv/row_randomizer_test.rb +37 -0
  49. data/test/csvtool/infrastructure/csv/selector_validator_test.rb +72 -0
  50. data/test/csvtool/interface/cli/errors/presenter_test.rb +2 -0
  51. data/test/csvtool/interface/cli/menu_loop_test.rb +78 -10
  52. data/test/csvtool/interface/cli/prompts/headers_present_prompt_test.rb +14 -0
  53. data/test/csvtool/interface/cli/prompts/seed_prompt_test.rb +39 -0
  54. data/test/csvtool/interface/cli/workflows/run_cross_csv_dedupe_workflow_test.rb +246 -0
  55. data/test/fixtures/dedupe_reference.csv +3 -0
  56. data/test/fixtures/dedupe_reference.tsv +3 -0
  57. data/test/fixtures/dedupe_reference_all.csv +5 -0
  58. data/test/fixtures/dedupe_reference_no_headers.csv +2 -0
  59. data/test/fixtures/dedupe_reference_none.csv +2 -0
  60. data/test/fixtures/dedupe_reference_normalization.csv +3 -0
  61. data/test/fixtures/dedupe_source.csv +6 -0
  62. data/test/fixtures/dedupe_source.tsv +6 -0
  63. data/test/fixtures/dedupe_source_no_headers.csv +5 -0
  64. data/test/fixtures/dedupe_source_normalization.csv +4 -0
  65. data/test/fixtures/sample_people_no_headers.csv +3 -0
  66. metadata +50 -6
  67. data/lib/csvtool/domain/row_session/row_output_destination.rb +0 -31
  68. data/test/csvtool/domain/column_session/output_destination_test.rb +0 -18
  69. 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/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,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\n3\n"),
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\n3\n"),
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\n3\n"),
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 3."
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