csvops 0.5.0.alpha → 0.7.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +88 -7
  3. data/docs/architecture.md +119 -5
  4. data/docs/release-v0.6.0-alpha.md +84 -0
  5. data/docs/release-v0.7.0-alpha.md +87 -0
  6. data/lib/csvtool/application/use_cases/run_csv_parity.rb +70 -0
  7. data/lib/csvtool/application/use_cases/run_csv_split.rb +97 -0
  8. data/lib/csvtool/cli.rb +9 -1
  9. data/lib/csvtool/domain/csv_parity_session/parity_options.rb +22 -0
  10. data/lib/csvtool/domain/csv_parity_session/parity_session.rb +20 -0
  11. data/lib/csvtool/domain/csv_parity_session/source_pair.rb +19 -0
  12. data/lib/csvtool/domain/csv_split_session/split_options.rb +27 -0
  13. data/lib/csvtool/domain/csv_split_session/split_session.rb +20 -0
  14. data/lib/csvtool/domain/csv_split_session/split_source.rb +17 -0
  15. data/lib/csvtool/infrastructure/csv/csv_parity_comparator.rb +71 -0
  16. data/lib/csvtool/infrastructure/csv/csv_splitter.rb +64 -0
  17. data/lib/csvtool/infrastructure/output/csv_split_manifest_writer.rb +20 -0
  18. data/lib/csvtool/interface/cli/errors/presenter.rb +12 -0
  19. data/lib/csvtool/interface/cli/menu_loop.rb +8 -2
  20. data/lib/csvtool/interface/cli/prompts/chunk_size_prompt.rb +21 -0
  21. data/lib/csvtool/interface/cli/prompts/split_manifest_prompt.rb +30 -0
  22. data/lib/csvtool/interface/cli/prompts/split_output_prompt.rb +38 -0
  23. data/lib/csvtool/interface/cli/workflows/builders/csv_parity_session_builder.rb +33 -0
  24. data/lib/csvtool/interface/cli/workflows/builders/csv_split_session_builder.rb +44 -0
  25. data/lib/csvtool/interface/cli/workflows/presenters/csv_parity_presenter.rb +38 -0
  26. data/lib/csvtool/interface/cli/workflows/presenters/csv_split_presenter.rb +26 -0
  27. data/lib/csvtool/interface/cli/workflows/run_csv_parity_workflow.rb +66 -0
  28. data/lib/csvtool/interface/cli/workflows/run_csv_split_workflow.rb +89 -0
  29. data/lib/csvtool/interface/cli/workflows/steps/csv_split/build_session_step.rb +30 -0
  30. data/lib/csvtool/interface/cli/workflows/steps/csv_split/collect_inputs_step.rb +43 -0
  31. data/lib/csvtool/interface/cli/workflows/steps/csv_split/collect_manifest_step.rb +30 -0
  32. data/lib/csvtool/interface/cli/workflows/steps/csv_split/collect_output_step.rb +31 -0
  33. data/lib/csvtool/interface/cli/workflows/steps/csv_split/execute_step.rb +36 -0
  34. data/lib/csvtool/interface/cli/workflows/steps/parity/build_session_step.rb +25 -0
  35. data/lib/csvtool/interface/cli/workflows/steps/parity/collect_inputs_step.rb +32 -0
  36. data/lib/csvtool/interface/cli/workflows/steps/parity/execute_step.rb +26 -0
  37. data/lib/csvtool/version.rb +1 -1
  38. data/test/csvtool/application/use_cases/run_csv_parity_test.rb +160 -0
  39. data/test/csvtool/application/use_cases/run_csv_split_test.rb +124 -0
  40. data/test/csvtool/cli_test.rb +222 -21
  41. data/test/csvtool/cli_unit_test.rb +4 -4
  42. data/test/csvtool/domain/csv_parity_session/parity_options_test.rb +17 -0
  43. data/test/csvtool/domain/csv_parity_session/parity_session_test.rb +18 -0
  44. data/test/csvtool/domain/csv_parity_session/source_pair_test.rb +11 -0
  45. data/test/csvtool/infrastructure/csv/csv_parity_comparator_test.rb +78 -0
  46. data/test/csvtool/infrastructure/csv/csv_splitter_test.rb +68 -0
  47. data/test/csvtool/infrastructure/output/csv_split_manifest_writer_test.rb +25 -0
  48. data/test/csvtool/interface/cli/errors/presenter_test.rb +2 -0
  49. data/test/csvtool/interface/cli/menu_loop_test.rb +87 -93
  50. data/test/csvtool/interface/cli/prompts/chunk_size_prompt_test.rb +17 -0
  51. data/test/csvtool/interface/cli/prompts/split_manifest_prompt_test.rb +42 -0
  52. data/test/csvtool/interface/cli/prompts/split_output_prompt_test.rb +22 -0
  53. data/test/csvtool/interface/cli/workflows/builders/csv_parity_session_builder_test.rb +20 -0
  54. data/test/csvtool/interface/cli/workflows/builders/csv_split_session_builder_test.rb +30 -0
  55. data/test/csvtool/interface/cli/workflows/presenters/csv_parity_presenter_test.rb +43 -0
  56. data/test/csvtool/interface/cli/workflows/presenters/csv_split_presenter_test.rb +26 -0
  57. data/test/csvtool/interface/cli/workflows/run_csv_parity_workflow_test.rb +94 -0
  58. data/test/csvtool/interface/cli/workflows/run_csv_split_workflow_test.rb +200 -0
  59. data/test/csvtool/interface/cli/workflows/steps/csv_split/build_session_step_test.rb +40 -0
  60. data/test/csvtool/interface/cli/workflows/steps/csv_split/collect_inputs_step_test.rb +64 -0
  61. data/test/csvtool/interface/cli/workflows/steps/csv_split/collect_manifest_step_test.rb +30 -0
  62. data/test/csvtool/interface/cli/workflows/steps/csv_split/collect_output_step_test.rb +32 -0
  63. data/test/csvtool/interface/cli/workflows/steps/csv_split/execute_step_test.rb +83 -0
  64. data/test/csvtool/interface/cli/workflows/steps/parity/build_session_step_test.rb +41 -0
  65. data/test/csvtool/interface/cli/workflows/steps/parity/collect_inputs_step_test.rb +30 -0
  66. data/test/csvtool/interface/cli/workflows/steps/parity/execute_step_test.rb +40 -0
  67. data/test/fixtures/parity_duplicates_left.csv +4 -0
  68. data/test/fixtures/parity_duplicates_right.csv +3 -0
  69. data/test/fixtures/parity_people_header_mismatch.csv +4 -0
  70. data/test/fixtures/parity_people_many_reordered.csv +13 -0
  71. data/test/fixtures/parity_people_mismatch.csv +4 -0
  72. data/test/fixtures/parity_people_reordered.csv +4 -0
  73. data/test/fixtures/parity_people_reordered.tsv +4 -0
  74. data/test/fixtures/split_people_25.csv +26 -0
  75. metadata +64 -1
@@ -17,128 +17,122 @@ class MenuLoopTest < Minitest::Test
17
17
  end
18
18
 
19
19
  def test_routes_extract_column_then_exit
20
- column_action = FakeAction.new
21
- rows_action = FakeAction.new
22
- randomize_rows_action = FakeAction.new
23
- dedupe_action = FakeAction.new
24
- stdout = StringIO.new
25
- menu = Csvtool::Interface::CLI::MenuLoop.new(
26
- stdin: StringIO.new("1\n5\n"),
27
- stdout: stdout,
28
- menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
29
- extract_column_action: column_action,
30
- extract_rows_action: rows_action,
31
- randomize_rows_action: randomize_rows_action,
32
- dedupe_action: dedupe_action
33
- )
34
-
20
+ menu, actions, = build_menu("1\n7\n")
35
21
  status = menu.run
36
22
 
37
23
  assert_equal 0, status
38
- assert_equal 1, column_action.runs
39
- assert_equal 0, rows_action.runs
40
- assert_equal 0, randomize_rows_action.runs
41
- assert_equal 0, dedupe_action.runs
42
- assert_includes stdout.string, "CSV Tool Menu"
24
+ assert_equal 1, actions[:column].runs
25
+ assert_equal 0, actions[:rows].runs
26
+ assert_equal 0, actions[:randomize].runs
27
+ assert_equal 0, actions[:dedupe].runs
28
+ assert_equal 0, actions[:parity].runs
29
+ assert_equal 0, actions[:split].runs
43
30
  end
44
31
 
45
32
  def test_routes_extract_rows_then_exit
46
- column_action = FakeAction.new
47
- rows_action = FakeAction.new
48
- randomize_rows_action = FakeAction.new
49
- dedupe_action = FakeAction.new
50
- stdout = StringIO.new
51
- menu = Csvtool::Interface::CLI::MenuLoop.new(
52
- stdin: StringIO.new("2\n5\n"),
53
- stdout: stdout,
54
- menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
55
- extract_column_action: column_action,
56
- extract_rows_action: rows_action,
57
- randomize_rows_action: randomize_rows_action,
58
- dedupe_action: dedupe_action
59
- )
60
-
33
+ menu, actions, = build_menu("2\n7\n")
61
34
  status = menu.run
62
35
 
63
36
  assert_equal 0, status
64
- assert_equal 0, column_action.runs
65
- assert_equal 1, rows_action.runs
66
- assert_equal 0, randomize_rows_action.runs
67
- assert_equal 0, dedupe_action.runs
37
+ assert_equal 0, actions[:column].runs
38
+ assert_equal 1, actions[:rows].runs
39
+ assert_equal 0, actions[:randomize].runs
40
+ assert_equal 0, actions[:dedupe].runs
41
+ assert_equal 0, actions[:parity].runs
42
+ assert_equal 0, actions[:split].runs
68
43
  end
69
44
 
70
45
  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
-
46
+ menu, actions, = build_menu("3\n7\n")
86
47
  status = menu.run
87
48
 
88
49
  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
50
+ assert_equal 0, actions[:column].runs
51
+ assert_equal 0, actions[:rows].runs
52
+ assert_equal 1, actions[:randomize].runs
53
+ assert_equal 0, actions[:dedupe].runs
54
+ assert_equal 0, actions[:parity].runs
55
+ assert_equal 0, actions[:split].runs
93
56
  end
94
57
 
95
58
  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
- )
59
+ menu, actions, = build_menu("4\n7\n")
60
+ status = menu.run
61
+
62
+ assert_equal 0, status
63
+ assert_equal 0, actions[:column].runs
64
+ assert_equal 0, actions[:rows].runs
65
+ assert_equal 0, actions[:randomize].runs
66
+ assert_equal 1, actions[:dedupe].runs
67
+ assert_equal 0, actions[:parity].runs
68
+ assert_equal 0, actions[:split].runs
69
+ end
70
+
71
+ def test_routes_parity_then_exit
72
+ menu, actions, = build_menu("5\n7\n")
73
+ status = menu.run
74
+
75
+ assert_equal 0, status
76
+ assert_equal 0, actions[:column].runs
77
+ assert_equal 0, actions[:rows].runs
78
+ assert_equal 0, actions[:randomize].runs
79
+ assert_equal 0, actions[:dedupe].runs
80
+ assert_equal 1, actions[:parity].runs
81
+ assert_equal 0, actions[:split].runs
82
+ end
110
83
 
84
+ def test_routes_split_then_exit
85
+ menu, actions, stdout = build_menu("6\n7\n")
111
86
  status = menu.run
112
87
 
113
88
  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
89
+ assert_equal 0, actions[:column].runs
90
+ assert_equal 0, actions[:rows].runs
91
+ assert_equal 0, actions[:randomize].runs
92
+ assert_equal 0, actions[:dedupe].runs
93
+ assert_equal 0, actions[:parity].runs
94
+ assert_equal 1, actions[:split].runs
95
+ assert_includes stdout.string, "CSV Tool Menu"
118
96
  end
119
97
 
120
98
  def test_invalid_choice_shows_prompt
121
- column_action = FakeAction.new
122
- rows_action = FakeAction.new
123
- randomize_rows_action = FakeAction.new
124
- dedupe_action = FakeAction.new
99
+ menu, actions, stdout = build_menu("x\n7\n")
100
+ menu.run
101
+
102
+ assert_includes stdout.string, "Please choose 1, 2, 3, 4, 5, 6, or 7."
103
+ assert_equal 0, actions[:column].runs
104
+ assert_equal 0, actions[:rows].runs
105
+ assert_equal 0, actions[:randomize].runs
106
+ assert_equal 0, actions[:dedupe].runs
107
+ assert_equal 0, actions[:parity].runs
108
+ assert_equal 0, actions[:split].runs
109
+ end
110
+
111
+ private
112
+
113
+ def build_menu(input)
114
+ actions = {
115
+ column: FakeAction.new,
116
+ rows: FakeAction.new,
117
+ randomize: FakeAction.new,
118
+ dedupe: FakeAction.new,
119
+ parity: FakeAction.new,
120
+ split: FakeAction.new
121
+ }
125
122
  stdout = StringIO.new
123
+
126
124
  menu = Csvtool::Interface::CLI::MenuLoop.new(
127
- stdin: StringIO.new("x\n5\n"),
125
+ stdin: StringIO.new(input),
128
126
  stdout: stdout,
129
- menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Exit"],
130
- extract_column_action: column_action,
131
- extract_rows_action: rows_action,
132
- randomize_rows_action: randomize_rows_action,
133
- dedupe_action: dedupe_action
127
+ menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Validate parity", "Split CSV into chunks", "Exit"],
128
+ extract_column_action: actions[:column],
129
+ extract_rows_action: actions[:rows],
130
+ randomize_rows_action: actions[:randomize],
131
+ dedupe_action: actions[:dedupe],
132
+ parity_action: actions[:parity],
133
+ split_action: actions[:split]
134
134
  )
135
135
 
136
- menu.run
137
-
138
- assert_includes stdout.string, "Please choose 1, 2, 3, 4, or 5."
139
- assert_equal 0, column_action.runs
140
- assert_equal 0, rows_action.runs
141
- assert_equal 0, randomize_rows_action.runs
142
- assert_equal 0, dedupe_action.runs
136
+ [menu, actions, stdout]
143
137
  end
144
138
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../../test_helper"
4
+ require "csvtool/interface/cli/prompts/chunk_size_prompt"
5
+
6
+ class ChunkSizePromptTest < Minitest::Test
7
+ def test_returns_entered_value
8
+ out = StringIO.new
9
+ prompt = Csvtool::Interface::CLI::Prompts::ChunkSizePrompt.new(
10
+ stdin: StringIO.new("25\n"),
11
+ stdout: out
12
+ )
13
+
14
+ assert_equal "25", prompt.call
15
+ assert_includes out.string, "Rows per chunk: "
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../../test_helper"
4
+ require "csvtool/interface/cli/prompts/split_manifest_prompt"
5
+
6
+ class SplitManifestPromptTest < Minitest::Test
7
+ class FakeYesNoPrompt
8
+ def initialize(value)
9
+ @value = value
10
+ end
11
+
12
+ def call(label:, default:)
13
+ @value.nil? ? default : @value
14
+ end
15
+ end
16
+
17
+ def test_returns_disabled_when_user_declines_manifest
18
+ prompt = Csvtool::Interface::CLI::Prompts::SplitManifestPrompt.new(
19
+ stdin: StringIO.new,
20
+ stdout: StringIO.new,
21
+ yes_no_prompt: FakeYesNoPrompt.new(false)
22
+ )
23
+
24
+ result = prompt.call(default_path: "/tmp/manifest.csv")
25
+
26
+ assert_equal false, result[:write_manifest]
27
+ assert_nil result[:manifest_path]
28
+ end
29
+
30
+ def test_uses_default_manifest_path_when_blank
31
+ prompt = Csvtool::Interface::CLI::Prompts::SplitManifestPrompt.new(
32
+ stdin: StringIO.new("\n"),
33
+ stdout: StringIO.new,
34
+ yes_no_prompt: FakeYesNoPrompt.new(true)
35
+ )
36
+
37
+ result = prompt.call(default_path: "/tmp/manifest.csv")
38
+
39
+ assert_equal true, result[:write_manifest]
40
+ assert_equal "/tmp/manifest.csv", result[:manifest_path]
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../../test_helper"
4
+ require "csvtool/interface/cli/prompts/split_output_prompt"
5
+ require "csvtool/interface/cli/prompts/yes_no_prompt"
6
+
7
+ class SplitOutputPromptTest < Minitest::Test
8
+ def test_uses_defaults_for_blank_values
9
+ out = StringIO.new
10
+ prompt = Csvtool::Interface::CLI::Prompts::SplitOutputPrompt.new(
11
+ stdin: StringIO.new("\n\n\n"),
12
+ stdout: out,
13
+ yes_no_prompt: Csvtool::Interface::CLI::Prompts::YesNoPrompt.new(stdin: StringIO.new("\n"), stdout: StringIO.new)
14
+ )
15
+
16
+ result = prompt.call(default_directory: "/tmp/out", default_prefix: "people")
17
+
18
+ assert_equal "/tmp/out", result[:output_directory]
19
+ assert_equal "people", result[:file_prefix]
20
+ assert_equal false, result[:overwrite_existing]
21
+ end
22
+ 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,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../../../test_helper"
4
+ require "csvtool/interface/cli/workflows/builders/csv_split_session_builder"
5
+
6
+ class CsvSplitSessionBuilderTest < Minitest::Test
7
+ def test_builds_split_session
8
+ session = Csvtool::Interface::CLI::Workflows::Builders::CsvSplitSessionBuilder.new.call(
9
+ file_path: "/tmp/people.csv",
10
+ col_sep: ",",
11
+ headers_present: true,
12
+ chunk_size: 10,
13
+ output_directory: "/tmp/out",
14
+ file_prefix: "batch",
15
+ overwrite_existing: true,
16
+ write_manifest: true,
17
+ manifest_path: "/tmp/out/manifest.csv"
18
+ )
19
+
20
+ assert_equal "/tmp/people.csv", session.source.path
21
+ assert_equal ",", session.source.separator
22
+ assert_equal true, session.source.headers_present
23
+ assert_equal 10, session.options.chunk_size
24
+ assert_equal "/tmp/out", session.options.output_directory
25
+ assert_equal "batch", session.options.file_prefix
26
+ assert_equal true, session.options.overwrite_existing
27
+ assert_equal true, session.options.write_manifest
28
+ assert_equal "/tmp/out/manifest.csv", session.options.manifest_path
29
+ end
30
+ 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,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../../../test_helper"
4
+ require "csvtool/interface/cli/workflows/presenters/csv_split_presenter"
5
+
6
+ class CsvSplitPresenterTest < Minitest::Test
7
+ def test_prints_summary_and_chunk_paths
8
+ out = StringIO.new
9
+ presenter = Csvtool::Interface::CLI::Workflows::Presenters::CsvSplitPresenter.new(stdout: out)
10
+
11
+ presenter.print_summary(
12
+ chunk_size: 10,
13
+ data_rows: 25,
14
+ chunk_count: 3,
15
+ manifest_path: "/tmp/manifest.csv",
16
+ chunk_paths: ["/tmp/people_part_001.csv", "/tmp/people_part_002.csv", "/tmp/people_part_003.csv"]
17
+ )
18
+
19
+ assert_includes out.string, "Split complete."
20
+ assert_includes out.string, "Chunk size: 10"
21
+ assert_includes out.string, "Data rows: 25"
22
+ assert_includes out.string, "Chunks written: 3"
23
+ assert_includes out.string, "Manifest: /tmp/manifest.csv"
24
+ assert_includes out.string, "/tmp/people_part_001.csv"
25
+ end
26
+ 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