csvops 0.7.0.alpha → 0.9.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 +80 -20
- data/docs/architecture.md +67 -4
- data/docs/cli-output-conventions.md +49 -0
- data/docs/release-v0.8.0-alpha.md +88 -0
- data/docs/release-v0.9.0-alpha.md +80 -0
- data/lib/csvtool/application/use_cases/run_csv_stats.rb +64 -0
- data/lib/csvtool/cli.rb +136 -12
- data/lib/csvtool/domain/csv_stats_session/stats_options.rb +11 -0
- data/lib/csvtool/domain/csv_stats_session/stats_session.rb +25 -0
- data/lib/csvtool/domain/csv_stats_session/stats_source.rb +17 -0
- data/lib/csvtool/infrastructure/csv/csv_stats_scanner.rb +67 -0
- data/lib/csvtool/infrastructure/output/csv_stats_file_writer.rb +26 -0
- data/lib/csvtool/interface/cli/menu_loop.rb +9 -5
- data/lib/csvtool/interface/cli/output/color_policy.rb +25 -0
- data/lib/csvtool/interface/cli/output/colorizer.rb +27 -0
- data/lib/csvtool/interface/cli/output/formatters/csv_row_formatter.rb +19 -0
- data/lib/csvtool/interface/cli/output/formatters/stats_formatter.rb +57 -0
- data/lib/csvtool/interface/cli/output/streams.rb +22 -0
- data/lib/csvtool/interface/cli/output/table_renderer.rb +70 -0
- data/lib/csvtool/interface/cli/workflows/builders/csv_stats_session_builder.rb +28 -0
- data/lib/csvtool/interface/cli/workflows/presenters/cross_csv_dedupe_presenter.rb +17 -5
- data/lib/csvtool/interface/cli/workflows/presenters/csv_parity_presenter.rb +15 -4
- data/lib/csvtool/interface/cli/workflows/presenters/csv_split_presenter.rb +15 -6
- data/lib/csvtool/interface/cli/workflows/presenters/csv_stats_presenter.rb +43 -0
- data/lib/csvtool/interface/cli/workflows/presenters/row_extraction_presenter.rb +5 -4
- data/lib/csvtool/interface/cli/workflows/presenters/row_randomization_presenter.rb +5 -4
- data/lib/csvtool/interface/cli/workflows/run_cross_csv_dedupe_workflow.rb +9 -8
- data/lib/csvtool/interface/cli/workflows/run_csv_parity_workflow.rb +6 -5
- data/lib/csvtool/interface/cli/workflows/run_csv_split_workflow.rb +11 -10
- data/lib/csvtool/interface/cli/workflows/run_csv_stats_workflow.rb +78 -0
- data/lib/csvtool/interface/cli/workflows/run_extraction_workflow.rb +9 -8
- data/lib/csvtool/interface/cli/workflows/run_row_extraction_workflow.rb +7 -6
- data/lib/csvtool/interface/cli/workflows/run_row_randomization_workflow.rb +8 -7
- data/lib/csvtool/interface/cli/workflows/steps/csv_stats/build_session_step.rb +25 -0
- data/lib/csvtool/interface/cli/workflows/steps/csv_stats/collect_destination_step.rb +27 -0
- data/lib/csvtool/interface/cli/workflows/steps/csv_stats/collect_inputs_step.rb +31 -0
- data/lib/csvtool/interface/cli/workflows/steps/csv_stats/execute_step.rb +27 -0
- data/lib/csvtool/version.rb +1 -1
- data/test/csvtool/application/use_cases/run_csv_stats_test.rb +165 -0
- data/test/csvtool/cli_test.rb +376 -68
- data/test/csvtool/cli_unit_test.rb +5 -5
- data/test/csvtool/infrastructure/csv/csv_stats_scanner_test.rb +68 -0
- data/test/csvtool/infrastructure/output/csv_stats_file_writer_test.rb +38 -0
- data/test/csvtool/interface/cli/menu_loop_test.rb +34 -11
- data/test/csvtool/interface/cli/output/color_policy_test.rb +40 -0
- data/test/csvtool/interface/cli/output/colorizer_test.rb +28 -0
- data/test/csvtool/interface/cli/output/formatters/csv_row_formatter_test.rb +22 -0
- data/test/csvtool/interface/cli/output/formatters/stats_formatter_test.rb +51 -0
- data/test/csvtool/interface/cli/output/streams_test.rb +25 -0
- data/test/csvtool/interface/cli/output/table_renderer_test.rb +36 -0
- data/test/csvtool/interface/cli/workflows/builders/csv_stats_session_builder_test.rb +19 -0
- data/test/csvtool/interface/cli/workflows/presenters/cross_csv_dedupe_presenter_test.rb +4 -1
- data/test/csvtool/interface/cli/workflows/presenters/csv_parity_presenter_test.rb +5 -1
- data/test/csvtool/interface/cli/workflows/presenters/csv_split_presenter_test.rb +22 -4
- data/test/csvtool/interface/cli/workflows/presenters/csv_stats_presenter_test.rb +39 -0
- data/test/csvtool/interface/cli/workflows/run_cross_csv_dedupe_workflow_test.rb +10 -7
- data/test/csvtool/interface/cli/workflows/run_csv_parity_workflow_test.rb +3 -1
- data/test/csvtool/interface/cli/workflows/run_csv_split_workflow_test.rb +5 -3
- data/test/csvtool/interface/cli/workflows/run_csv_stats_workflow_test.rb +151 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_stats/build_session_step_test.rb +36 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_stats/collect_destination_step_test.rb +49 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_stats/collect_inputs_step_test.rb +61 -0
- data/test/csvtool/interface/cli/workflows/steps/csv_stats/execute_step_test.rb +65 -0
- metadata +39 -1
|
@@ -16,7 +16,7 @@ class CliUnitTest < Minitest::Test
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def test_menu_command_can_exit_zero
|
|
19
|
-
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("
|
|
19
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("8\n"), stdout: StringIO.new, stderr: StringIO.new)
|
|
20
20
|
assert_equal 0, status
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ class CliUnitTest < Minitest::Test
|
|
|
28
28
|
def test_menu_routes_to_row_range_shell
|
|
29
29
|
stdout = StringIO.new
|
|
30
30
|
fixture = File.expand_path("../fixtures/sample_people.csv", __dir__)
|
|
31
|
-
input = ["2", fixture, "", "2", "3", "", "
|
|
31
|
+
input = ["2", fixture, "", "2", "3", "", "8"].join("\n") + "\n"
|
|
32
32
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: stdout, stderr: StringIO.new)
|
|
33
33
|
assert_equal 0, status
|
|
34
34
|
assert_includes stdout.string, "name,city"
|
|
@@ -39,7 +39,7 @@ class CliUnitTest < Minitest::Test
|
|
|
39
39
|
def test_menu_routes_to_randomize_rows_shell
|
|
40
40
|
stdout = StringIO.new
|
|
41
41
|
fixture = File.expand_path("../fixtures/sample_people.csv", __dir__)
|
|
42
|
-
input = ["3", fixture, "", "", "", "", "
|
|
42
|
+
input = ["3", fixture, "", "", "", "", "8"].join("\n") + "\n"
|
|
43
43
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: stdout, stderr: StringIO.new)
|
|
44
44
|
assert_equal 0, status
|
|
45
45
|
assert_includes stdout.string, "name,city"
|
|
@@ -52,12 +52,12 @@ class CliUnitTest < Minitest::Test
|
|
|
52
52
|
stdout = StringIO.new
|
|
53
53
|
source_fixture = File.expand_path("../fixtures/dedupe_source.csv", __dir__)
|
|
54
54
|
reference_fixture = File.expand_path("../fixtures/dedupe_reference.csv", __dir__)
|
|
55
|
-
input = ["4", source_fixture, "", "", reference_fixture, "", "", "customer_id", "external_id", "", "", "", "
|
|
55
|
+
input = ["4", source_fixture, "", "", reference_fixture, "", "", "customer_id", "external_id", "", "", "", "8"].join("\n") + "\n"
|
|
56
56
|
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: stdout, stderr: StringIO.new)
|
|
57
57
|
assert_equal 0, status
|
|
58
58
|
assert_includes stdout.string, "customer_id,name"
|
|
59
59
|
assert_includes stdout.string, "1,Alice"
|
|
60
60
|
assert_includes stdout.string, "3,Cara"
|
|
61
|
-
assert_includes stdout.string, "Summary
|
|
61
|
+
assert_includes stdout.string, "Summary"
|
|
62
62
|
end
|
|
63
63
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csv"
|
|
5
|
+
require "csvtool/infrastructure/csv/csv_stats_scanner"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
|
|
8
|
+
class CsvStatsScannerTest < Minitest::Test
|
|
9
|
+
def fixture_path(name)
|
|
10
|
+
File.expand_path("../../../fixtures/#{name}", __dir__)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_scans_headers_mode_with_streaming_foreach
|
|
14
|
+
source = fixture_path("sample_people_blanks.csv")
|
|
15
|
+
csv = Object.new
|
|
16
|
+
received = nil
|
|
17
|
+
|
|
18
|
+
define_singleton_foreach(csv) do |path, headers:, col_sep:, &block|
|
|
19
|
+
received = { path: path, headers: headers, col_sep: col_sep }
|
|
20
|
+
::CSV.foreach(path, headers: headers, col_sep: col_sep, &block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
result = Csvtool::Infrastructure::CSV::CsvStatsScanner.new(csv: csv).call(
|
|
24
|
+
file_path: source,
|
|
25
|
+
col_sep: ",",
|
|
26
|
+
headers_present: true
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
assert_equal({ path: source, headers: true, col_sep: "," }, received)
|
|
30
|
+
assert_equal 5, result[:row_count]
|
|
31
|
+
assert_equal 2, result[:column_count]
|
|
32
|
+
assert_equal ["name", "city"], result[:headers]
|
|
33
|
+
assert_equal [
|
|
34
|
+
{ name: "name", blank_count: 2, non_blank_count: 3 },
|
|
35
|
+
{ name: "city", blank_count: 1, non_blank_count: 4 }
|
|
36
|
+
], result[:column_stats]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_scans_large_file_in_single_pass_shape
|
|
40
|
+
Dir.mktmpdir do |dir|
|
|
41
|
+
path = File.join(dir, "large.csv")
|
|
42
|
+
File.open(path, "w") do |f|
|
|
43
|
+
f.puts("id,value")
|
|
44
|
+
20_000.times { |i| f.puts("#{i},v#{i}") }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
result = Csvtool::Infrastructure::CSV::CsvStatsScanner.new.call(
|
|
48
|
+
file_path: path,
|
|
49
|
+
col_sep: ",",
|
|
50
|
+
headers_present: true
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assert_equal 20_000, result[:row_count]
|
|
54
|
+
assert_equal 2, result[:column_count]
|
|
55
|
+
assert_equal ["id", "value"], result[:headers]
|
|
56
|
+
assert_equal [
|
|
57
|
+
{ name: "id", blank_count: 0, non_blank_count: 20_000 },
|
|
58
|
+
{ name: "value", blank_count: 0, non_blank_count: 20_000 }
|
|
59
|
+
], result[:column_stats]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def define_singleton_foreach(obj, &implementation)
|
|
66
|
+
obj.define_singleton_method(:foreach, &implementation)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/infrastructure/output/csv_stats_file_writer"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
class InfrastructureCsvStatsFileWriterTest < Minitest::Test
|
|
8
|
+
def test_writes_stats_as_metric_value_csv
|
|
9
|
+
writer = Csvtool::Infrastructure::Output::CsvStatsFileWriter.new
|
|
10
|
+
|
|
11
|
+
Dir.mktmpdir do |dir|
|
|
12
|
+
path = File.join(dir, "stats.csv")
|
|
13
|
+
writer.call(
|
|
14
|
+
path: path,
|
|
15
|
+
data: {
|
|
16
|
+
row_count: 3,
|
|
17
|
+
column_count: 2,
|
|
18
|
+
headers: ["name", "city"],
|
|
19
|
+
column_stats: [
|
|
20
|
+
{ name: "name", non_blank_count: 3, blank_count: 0 },
|
|
21
|
+
{ name: "city", non_blank_count: 2, blank_count: 1 }
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
assert_equal [
|
|
27
|
+
"metric,value",
|
|
28
|
+
"row_count,3",
|
|
29
|
+
"column_count,2",
|
|
30
|
+
"headers,name|city",
|
|
31
|
+
"column.name.non_blank,3",
|
|
32
|
+
"column.name.blank,0",
|
|
33
|
+
"column.city.non_blank,2",
|
|
34
|
+
"column.city.blank,1"
|
|
35
|
+
], File.read(path).lines.map(&:strip)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -17,7 +17,7 @@ class MenuLoopTest < Minitest::Test
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def test_routes_extract_column_then_exit
|
|
20
|
-
menu, actions, = build_menu("1\
|
|
20
|
+
menu, actions, = build_menu("1\n8\n")
|
|
21
21
|
status = menu.run
|
|
22
22
|
|
|
23
23
|
assert_equal 0, status
|
|
@@ -27,10 +27,11 @@ class MenuLoopTest < Minitest::Test
|
|
|
27
27
|
assert_equal 0, actions[:dedupe].runs
|
|
28
28
|
assert_equal 0, actions[:parity].runs
|
|
29
29
|
assert_equal 0, actions[:split].runs
|
|
30
|
+
assert_equal 0, actions[:stats].runs
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def test_routes_extract_rows_then_exit
|
|
33
|
-
menu, actions, = build_menu("2\
|
|
34
|
+
menu, actions, = build_menu("2\n8\n")
|
|
34
35
|
status = menu.run
|
|
35
36
|
|
|
36
37
|
assert_equal 0, status
|
|
@@ -40,10 +41,11 @@ class MenuLoopTest < Minitest::Test
|
|
|
40
41
|
assert_equal 0, actions[:dedupe].runs
|
|
41
42
|
assert_equal 0, actions[:parity].runs
|
|
42
43
|
assert_equal 0, actions[:split].runs
|
|
44
|
+
assert_equal 0, actions[:stats].runs
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
def test_routes_randomize_rows_then_exit
|
|
46
|
-
menu, actions, = build_menu("3\
|
|
48
|
+
menu, actions, = build_menu("3\n8\n")
|
|
47
49
|
status = menu.run
|
|
48
50
|
|
|
49
51
|
assert_equal 0, status
|
|
@@ -53,10 +55,11 @@ class MenuLoopTest < Minitest::Test
|
|
|
53
55
|
assert_equal 0, actions[:dedupe].runs
|
|
54
56
|
assert_equal 0, actions[:parity].runs
|
|
55
57
|
assert_equal 0, actions[:split].runs
|
|
58
|
+
assert_equal 0, actions[:stats].runs
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
def test_routes_dedupe_then_exit
|
|
59
|
-
menu, actions, = build_menu("4\
|
|
62
|
+
menu, actions, = build_menu("4\n8\n")
|
|
60
63
|
status = menu.run
|
|
61
64
|
|
|
62
65
|
assert_equal 0, status
|
|
@@ -66,10 +69,11 @@ class MenuLoopTest < Minitest::Test
|
|
|
66
69
|
assert_equal 1, actions[:dedupe].runs
|
|
67
70
|
assert_equal 0, actions[:parity].runs
|
|
68
71
|
assert_equal 0, actions[:split].runs
|
|
72
|
+
assert_equal 0, actions[:stats].runs
|
|
69
73
|
end
|
|
70
74
|
|
|
71
75
|
def test_routes_parity_then_exit
|
|
72
|
-
menu, actions, = build_menu("5\
|
|
76
|
+
menu, actions, = build_menu("5\n8\n")
|
|
73
77
|
status = menu.run
|
|
74
78
|
|
|
75
79
|
assert_equal 0, status
|
|
@@ -79,10 +83,11 @@ class MenuLoopTest < Minitest::Test
|
|
|
79
83
|
assert_equal 0, actions[:dedupe].runs
|
|
80
84
|
assert_equal 1, actions[:parity].runs
|
|
81
85
|
assert_equal 0, actions[:split].runs
|
|
86
|
+
assert_equal 0, actions[:stats].runs
|
|
82
87
|
end
|
|
83
88
|
|
|
84
89
|
def test_routes_split_then_exit
|
|
85
|
-
menu, actions, stdout = build_menu("6\
|
|
90
|
+
menu, actions, stdout = build_menu("6\n8\n")
|
|
86
91
|
status = menu.run
|
|
87
92
|
|
|
88
93
|
assert_equal 0, status
|
|
@@ -92,20 +97,36 @@ class MenuLoopTest < Minitest::Test
|
|
|
92
97
|
assert_equal 0, actions[:dedupe].runs
|
|
93
98
|
assert_equal 0, actions[:parity].runs
|
|
94
99
|
assert_equal 1, actions[:split].runs
|
|
100
|
+
assert_equal 0, actions[:stats].runs
|
|
95
101
|
assert_includes stdout.string, "CSV Tool Menu"
|
|
96
102
|
end
|
|
97
103
|
|
|
104
|
+
def test_routes_stats_then_exit
|
|
105
|
+
menu, actions, = build_menu("7\n8\n")
|
|
106
|
+
status = menu.run
|
|
107
|
+
|
|
108
|
+
assert_equal 0, status
|
|
109
|
+
assert_equal 0, actions[:column].runs
|
|
110
|
+
assert_equal 0, actions[:rows].runs
|
|
111
|
+
assert_equal 0, actions[:randomize].runs
|
|
112
|
+
assert_equal 0, actions[:dedupe].runs
|
|
113
|
+
assert_equal 0, actions[:parity].runs
|
|
114
|
+
assert_equal 0, actions[:split].runs
|
|
115
|
+
assert_equal 1, actions[:stats].runs
|
|
116
|
+
end
|
|
117
|
+
|
|
98
118
|
def test_invalid_choice_shows_prompt
|
|
99
|
-
menu, actions, stdout = build_menu("x\
|
|
119
|
+
menu, actions, stdout = build_menu("x\n8\n")
|
|
100
120
|
menu.run
|
|
101
121
|
|
|
102
|
-
assert_includes stdout.string, "Please choose 1, 2, 3, 4, 5, 6, or
|
|
122
|
+
assert_includes stdout.string, "Please choose 1, 2, 3, 4, 5, 6, 7, or 8."
|
|
103
123
|
assert_equal 0, actions[:column].runs
|
|
104
124
|
assert_equal 0, actions[:rows].runs
|
|
105
125
|
assert_equal 0, actions[:randomize].runs
|
|
106
126
|
assert_equal 0, actions[:dedupe].runs
|
|
107
127
|
assert_equal 0, actions[:parity].runs
|
|
108
128
|
assert_equal 0, actions[:split].runs
|
|
129
|
+
assert_equal 0, actions[:stats].runs
|
|
109
130
|
end
|
|
110
131
|
|
|
111
132
|
private
|
|
@@ -117,20 +138,22 @@ class MenuLoopTest < Minitest::Test
|
|
|
117
138
|
randomize: FakeAction.new,
|
|
118
139
|
dedupe: FakeAction.new,
|
|
119
140
|
parity: FakeAction.new,
|
|
120
|
-
split: FakeAction.new
|
|
141
|
+
split: FakeAction.new,
|
|
142
|
+
stats: FakeAction.new
|
|
121
143
|
}
|
|
122
144
|
stdout = StringIO.new
|
|
123
145
|
|
|
124
146
|
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
125
147
|
stdin: StringIO.new(input),
|
|
126
148
|
stdout: stdout,
|
|
127
|
-
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Validate parity", "Split CSV into chunks", "Exit"],
|
|
149
|
+
menu_options: ["Extract column", "Extract rows (range)", "Randomize rows", "Dedupe using another CSV", "Validate parity", "Split CSV into chunks", "CSV stats summary", "Exit"],
|
|
128
150
|
extract_column_action: actions[:column],
|
|
129
151
|
extract_rows_action: actions[:rows],
|
|
130
152
|
randomize_rows_action: actions[:randomize],
|
|
131
153
|
dedupe_action: actions[:dedupe],
|
|
132
154
|
parity_action: actions[:parity],
|
|
133
|
-
split_action: actions[:split]
|
|
155
|
+
split_action: actions[:split],
|
|
156
|
+
stats_action: actions[:stats]
|
|
134
157
|
)
|
|
135
158
|
|
|
136
159
|
[menu, actions, stdout]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/output/color_policy"
|
|
5
|
+
|
|
6
|
+
class ColorPolicyTest < Minitest::Test
|
|
7
|
+
class TtyIO
|
|
8
|
+
def tty? = true
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class NonTtyIO
|
|
12
|
+
def tty? = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_auto_uses_tty
|
|
16
|
+
enabled = Csvtool::Interface::CLI::Output::ColorPolicy.new(mode: "auto", io: TtyIO.new, env: {}).enabled?
|
|
17
|
+
disabled = Csvtool::Interface::CLI::Output::ColorPolicy.new(mode: "auto", io: NonTtyIO.new, env: {}).enabled?
|
|
18
|
+
|
|
19
|
+
assert_equal true, enabled
|
|
20
|
+
assert_equal false, disabled
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_never_disables_color
|
|
24
|
+
policy = Csvtool::Interface::CLI::Output::ColorPolicy.new(mode: "never", io: TtyIO.new, env: {})
|
|
25
|
+
|
|
26
|
+
assert_equal false, policy.enabled?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_always_enables_color_even_with_no_color
|
|
30
|
+
policy = Csvtool::Interface::CLI::Output::ColorPolicy.new(mode: "always", io: NonTtyIO.new, env: { "NO_COLOR" => "1" })
|
|
31
|
+
|
|
32
|
+
assert_equal true, policy.enabled?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_no_color_disables_auto
|
|
36
|
+
policy = Csvtool::Interface::CLI::Output::ColorPolicy.new(mode: "auto", io: TtyIO.new, env: { "NO_COLOR" => "1" })
|
|
37
|
+
|
|
38
|
+
assert_equal false, policy.enabled?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/output/colorizer"
|
|
5
|
+
|
|
6
|
+
class ColorizerTest < Minitest::Test
|
|
7
|
+
class FakePolicy
|
|
8
|
+
def initialize(enabled)
|
|
9
|
+
@enabled = enabled
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def enabled?
|
|
13
|
+
@enabled
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_wraps_text_when_enabled
|
|
18
|
+
colorizer = Csvtool::Interface::CLI::Output::Colorizer.new(policy: FakePolicy.new(true))
|
|
19
|
+
|
|
20
|
+
assert_equal "\e[31mMISMATCH\e[0m", colorizer.call("MISMATCH", code: "31")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_returns_text_when_disabled
|
|
24
|
+
colorizer = Csvtool::Interface::CLI::Output::Colorizer.new(policy: FakePolicy.new(false))
|
|
25
|
+
|
|
26
|
+
assert_equal "MATCH", colorizer.call("MATCH", code: "32")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/output/formatters/csv_row_formatter"
|
|
5
|
+
|
|
6
|
+
class CsvRowFormatterTest < Minitest::Test
|
|
7
|
+
def test_formats_row_with_separator
|
|
8
|
+
formatter = Csvtool::Interface::CLI::Output::Formatters::CsvRowFormatter.new
|
|
9
|
+
|
|
10
|
+
row = formatter.call(fields: ["Alice", "London"], col_sep: ",")
|
|
11
|
+
|
|
12
|
+
assert_equal "Alice,London", row
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_quotes_values_when_needed
|
|
16
|
+
formatter = Csvtool::Interface::CLI::Output::Formatters::CsvRowFormatter.new
|
|
17
|
+
|
|
18
|
+
row = formatter.call(fields: ["Alice, Jr", "London"], col_sep: ",")
|
|
19
|
+
|
|
20
|
+
assert_equal '"Alice, Jr",London', row
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/output/formatters/stats_formatter"
|
|
5
|
+
require "csvtool/interface/cli/output/table_renderer"
|
|
6
|
+
require "json"
|
|
7
|
+
|
|
8
|
+
class StatsFormatterTest < Minitest::Test
|
|
9
|
+
def sample_data
|
|
10
|
+
{
|
|
11
|
+
row_count: 3,
|
|
12
|
+
column_count: 2,
|
|
13
|
+
headers: ["name", "city"],
|
|
14
|
+
column_stats: [
|
|
15
|
+
{ name: "name", non_blank_count: 3, blank_count: 0 },
|
|
16
|
+
{ name: "city", non_blank_count: 2, blank_count: 1 }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def formatter
|
|
22
|
+
Csvtool::Interface::CLI::Output::Formatters::StatsFormatter.new(
|
|
23
|
+
table_renderer: Csvtool::Interface::CLI::Output::TableRenderer.new
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_formats_text_summary
|
|
28
|
+
text = formatter.call(data: sample_data, format: "text", max_width: 80)
|
|
29
|
+
|
|
30
|
+
assert_includes text, "CSV Stats Summary"
|
|
31
|
+
assert_includes text, "Metric"
|
|
32
|
+
assert_includes text, "Column completeness:"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_formats_json_summary
|
|
36
|
+
json = formatter.call(data: sample_data, format: "json", max_width: 80)
|
|
37
|
+
|
|
38
|
+
parsed = JSON.parse(json, symbolize_names: true)
|
|
39
|
+
assert_equal 3, parsed[:row_count]
|
|
40
|
+
assert_equal 2, parsed[:column_count]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_formats_csv_summary
|
|
44
|
+
csv = formatter.call(data: sample_data, format: "csv", max_width: 80)
|
|
45
|
+
|
|
46
|
+
lines = csv.lines.map(&:chomp)
|
|
47
|
+
assert_equal "metric,value", lines.first
|
|
48
|
+
assert_includes lines, "row_count,3"
|
|
49
|
+
assert_includes lines, "column.city.blank,1"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/output/streams"
|
|
5
|
+
|
|
6
|
+
class StreamsTest < Minitest::Test
|
|
7
|
+
def test_builds_data_and_ui_streams
|
|
8
|
+
data = StringIO.new
|
|
9
|
+
ui = StringIO.new
|
|
10
|
+
|
|
11
|
+
streams = Csvtool::Interface::CLI::Output::Streams.build(data: data, ui: ui)
|
|
12
|
+
|
|
13
|
+
assert_same data, streams.data
|
|
14
|
+
assert_same ui, streams.ui
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_defaults_ui_to_data_stream
|
|
18
|
+
data = StringIO.new
|
|
19
|
+
|
|
20
|
+
streams = Csvtool::Interface::CLI::Output::Streams.build(data: data)
|
|
21
|
+
|
|
22
|
+
assert_same data, streams.data
|
|
23
|
+
assert_same data, streams.ui
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/output/table_renderer"
|
|
5
|
+
|
|
6
|
+
class TableRendererTest < Minitest::Test
|
|
7
|
+
def test_renders_aligned_table
|
|
8
|
+
renderer = Csvtool::Interface::CLI::Output::TableRenderer.new
|
|
9
|
+
|
|
10
|
+
text = renderer.render(
|
|
11
|
+
headers: ["Metric", "Value"],
|
|
12
|
+
rows: [["Rows", "3"], ["Columns", "2"]],
|
|
13
|
+
max_width: 80
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
lines = text.lines.map(&:chomp)
|
|
17
|
+
assert_equal "Metric | Value", lines[0]
|
|
18
|
+
assert_equal "--------+------", lines[1]
|
|
19
|
+
assert_equal "Rows | 3 ", lines[2]
|
|
20
|
+
assert_equal "Columns | 2 ", lines[3]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_truncates_cells_when_width_is_narrow
|
|
24
|
+
renderer = Csvtool::Interface::CLI::Output::TableRenderer.new
|
|
25
|
+
|
|
26
|
+
text = renderer.render(
|
|
27
|
+
headers: ["Column", "Non-blank", "Blank"],
|
|
28
|
+
rows: [["very_long_column_name", "123456", "0"]],
|
|
29
|
+
max_width: 26
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
lines = text.lines.map(&:chomp)
|
|
33
|
+
assert lines.all? { |line| line.length <= 26 }
|
|
34
|
+
assert_includes lines[2], "..."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/builders/csv_stats_session_builder"
|
|
5
|
+
require "csvtool/domain/shared/output_destination"
|
|
6
|
+
|
|
7
|
+
class CsvStatsSessionBuilderTest < Minitest::Test
|
|
8
|
+
def test_builds_stats_session
|
|
9
|
+
builder = Csvtool::Interface::CLI::Workflows::Builders::CsvStatsSessionBuilder.new
|
|
10
|
+
destination = Csvtool::Domain::Shared::OutputDestination.console
|
|
11
|
+
|
|
12
|
+
session = builder.call(file_path: "/tmp/data.csv", col_sep: ";", headers_present: false, destination: destination)
|
|
13
|
+
|
|
14
|
+
assert_equal "/tmp/data.csv", session.source.path
|
|
15
|
+
assert_equal ";", session.source.separator
|
|
16
|
+
assert_equal false, session.source.headers_present
|
|
17
|
+
assert_equal true, session.output_destination.console?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -14,7 +14,10 @@ class CrossCsvDedupePresenterTest < Minitest::Test
|
|
|
14
14
|
|
|
15
15
|
assert_includes out.string, "\nid,name\n"
|
|
16
16
|
assert_includes out.string, "1,Alice"
|
|
17
|
-
assert_includes out.string, "Summary
|
|
17
|
+
assert_includes out.string, "Summary"
|
|
18
|
+
assert_includes out.string, "Source rows"
|
|
19
|
+
assert_includes out.string, "Removed rows"
|
|
20
|
+
assert_includes out.string, "Kept rows"
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def test_prints_zero_and_all_removed_messages
|
|
@@ -17,7 +17,11 @@ class CsvParityPresenterTest < Minitest::Test
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
assert_includes out.string, "MATCH"
|
|
20
|
-
assert_includes out.string, "
|
|
20
|
+
assert_includes out.string, "Metric"
|
|
21
|
+
assert_includes out.string, "Left rows"
|
|
22
|
+
assert_includes out.string, "Right rows"
|
|
23
|
+
assert_includes out.string, "Left only"
|
|
24
|
+
assert_includes out.string, "Right only"
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def test_prints_mismatch_examples
|
|
@@ -17,10 +17,28 @@ class CsvSplitPresenterTest < Minitest::Test
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
assert_includes out.string, "Split complete."
|
|
20
|
-
assert_includes out.string, "
|
|
21
|
-
assert_includes out.string, "
|
|
22
|
-
assert_includes out.string, "
|
|
23
|
-
assert_includes out.string, "
|
|
20
|
+
assert_includes out.string, "Metric"
|
|
21
|
+
assert_includes out.string, "Chunk size"
|
|
22
|
+
assert_includes out.string, "Data rows"
|
|
23
|
+
assert_includes out.string, "Chunks written"
|
|
24
|
+
assert_includes out.string, "Manifest"
|
|
24
25
|
assert_includes out.string, "/tmp/people_part_001.csv"
|
|
25
26
|
end
|
|
27
|
+
|
|
28
|
+
def test_truncates_summary_table_for_narrow_width
|
|
29
|
+
out = StringIO.new
|
|
30
|
+
presenter = Csvtool::Interface::CLI::Workflows::Presenters::CsvSplitPresenter.new(stdout: out, max_width: 26)
|
|
31
|
+
|
|
32
|
+
presenter.print_summary(
|
|
33
|
+
chunk_size: 10,
|
|
34
|
+
data_rows: 25,
|
|
35
|
+
chunk_count: 3,
|
|
36
|
+
manifest_path: "/tmp/very/long/path/manifest.csv",
|
|
37
|
+
chunk_paths: []
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
lines = out.string.lines.map(&:chomp)
|
|
41
|
+
assert lines.all? { |line| line.empty? || line.length <= 26 }
|
|
42
|
+
assert_includes out.string, "..."
|
|
43
|
+
end
|
|
26
44
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/workflows/presenters/csv_stats_presenter"
|
|
5
|
+
|
|
6
|
+
class CsvStatsPresenterTest < Minitest::Test
|
|
7
|
+
def test_prints_summary_with_headers_and_column_stats
|
|
8
|
+
out = StringIO.new
|
|
9
|
+
presenter = Csvtool::Interface::CLI::Workflows::Presenters::CsvStatsPresenter.new(stdout: out)
|
|
10
|
+
|
|
11
|
+
presenter.print_summary(
|
|
12
|
+
row_count: 3,
|
|
13
|
+
column_count: 2,
|
|
14
|
+
headers: ["name", "city"],
|
|
15
|
+
column_stats: [
|
|
16
|
+
{ name: "name", non_blank_count: 3, blank_count: 0 },
|
|
17
|
+
{ name: "city", non_blank_count: 2, blank_count: 1 }
|
|
18
|
+
]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
assert_includes out.string, "CSV Stats Summary"
|
|
22
|
+
assert_includes out.string, "Metric"
|
|
23
|
+
assert_includes out.string, "Rows"
|
|
24
|
+
assert_includes out.string, "Columns"
|
|
25
|
+
assert_includes out.string, "Headers"
|
|
26
|
+
assert_includes out.string, "Column completeness:"
|
|
27
|
+
assert_includes out.string, "name"
|
|
28
|
+
assert_includes out.string, "city"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_prints_file_written_message
|
|
32
|
+
out = StringIO.new
|
|
33
|
+
presenter = Csvtool::Interface::CLI::Workflows::Presenters::CsvStatsPresenter.new(stdout: out)
|
|
34
|
+
|
|
35
|
+
presenter.print_file_written("/tmp/stats.csv")
|
|
36
|
+
|
|
37
|
+
assert_includes out.string, "Wrote output to /tmp/stats.csv"
|
|
38
|
+
end
|
|
39
|
+
end
|