csvops 0.1.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 +7 -0
- data/Gemfile +8 -0
- data/README.md +209 -0
- data/Rakefile +10 -0
- data/bin/csvtool +8 -0
- data/bin/tool +8 -0
- data/csvops.gemspec +25 -0
- data/docs/release-v0.1.0-alpha.md +73 -0
- data/exe/csvtool +8 -0
- data/lib/csvtool/application/use_cases/run_extraction.rb +125 -0
- data/lib/csvtool/cli.rb +87 -0
- data/lib/csvtool/domain/extraction_session/column_selection.rb +17 -0
- data/lib/csvtool/domain/extraction_session/csv_source.rb +18 -0
- data/lib/csvtool/domain/extraction_session/extraction_options.rb +22 -0
- data/lib/csvtool/domain/extraction_session/extraction_session.rb +61 -0
- data/lib/csvtool/domain/extraction_session/extraction_value.rb +15 -0
- data/lib/csvtool/domain/extraction_session/output_destination.rb +35 -0
- data/lib/csvtool/domain/extraction_session/preview.rb +23 -0
- data/lib/csvtool/domain/extraction_session/separator.rb +17 -0
- data/lib/csvtool/infrastructure/csv/header_reader.rb +17 -0
- data/lib/csvtool/infrastructure/csv/value_streamer.rb +20 -0
- data/lib/csvtool/infrastructure/output/console_writer.rb +20 -0
- data/lib/csvtool/infrastructure/output/csv_file_writer.rb +30 -0
- data/lib/csvtool/interface/cli/errors/presenter.rb +59 -0
- data/lib/csvtool/interface/cli/menu_loop.rb +41 -0
- data/lib/csvtool/interface/cli/prompts/column_selector_prompt.rb +54 -0
- data/lib/csvtool/interface/cli/prompts/confirm_prompt.rb +29 -0
- data/lib/csvtool/interface/cli/prompts/file_path_prompt.rb +21 -0
- data/lib/csvtool/interface/cli/prompts/output_destination_prompt.rb +40 -0
- data/lib/csvtool/interface/cli/prompts/separator_prompt.rb +44 -0
- data/lib/csvtool/interface/cli/prompts/skip_blanks_prompt.rb +22 -0
- data/lib/csvtool/services/preview_builder.rb +20 -0
- data/lib/csvtool/version.rb +5 -0
- data/test/csvtool/application/use_cases/run_extraction_test.rb +31 -0
- data/test/csvtool/cli_test.rb +134 -0
- data/test/csvtool/cli_unit_test.rb +27 -0
- data/test/csvtool/domain/extraction_session/column_selection_test.rb +11 -0
- data/test/csvtool/domain/extraction_session/csv_source_test.rb +14 -0
- data/test/csvtool/domain/extraction_session/extraction_options_test.rb +18 -0
- data/test/csvtool/domain/extraction_session/extraction_session_test.rb +35 -0
- data/test/csvtool/domain/extraction_session/extraction_value_test.rb +11 -0
- data/test/csvtool/domain/extraction_session/output_destination_test.rb +18 -0
- data/test/csvtool/domain/extraction_session/preview_test.rb +18 -0
- data/test/csvtool/domain/extraction_session/separator_test.rb +15 -0
- data/test/csvtool/infrastructure/csv/header_reader_test.rb +16 -0
- data/test/csvtool/infrastructure/csv/value_streamer_test.rb +22 -0
- data/test/csvtool/infrastructure/output/console_writer_test.rb +19 -0
- data/test/csvtool/infrastructure/output/csv_file_writer_test.rb +35 -0
- data/test/csvtool/interface/cli/errors/presenter_test.rb +36 -0
- data/test/csvtool/interface/cli/menu_loop_test.rb +51 -0
- data/test/csvtool/interface/cli/prompts/column_selector_prompt_test.rb +23 -0
- data/test/csvtool/interface/cli/prompts/confirm_prompt_test.rb +23 -0
- data/test/csvtool/interface/cli/prompts/file_path_prompt_test.rb +11 -0
- data/test/csvtool/interface/cli/prompts/output_destination_prompt_test.rb +28 -0
- data/test/csvtool/interface/cli/prompts/separator_prompt_test.rb +31 -0
- data/test/csvtool/interface/cli/prompts/skip_blanks_prompt_test.rb +13 -0
- data/test/csvtool/services/preview_builder_test.rb +22 -0
- data/test/fixtures/empty.csv +0 -0
- data/test/fixtures/sample_people.csv +4 -0
- data/test/fixtures/sample_people.tsv +4 -0
- data/test/fixtures/sample_people_blanks.csv +6 -0
- data/test/fixtures/sample_people_colon.txt +4 -0
- data/test/fixtures/sample_people_many.csv +13 -0
- data/test/test_helper.rb +6 -0
- metadata +150 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../test_helper"
|
|
4
|
+
require "csvtool/cli"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
class TestCli < Minitest::Test
|
|
8
|
+
def fixture_path(name)
|
|
9
|
+
File.expand_path("../fixtures/#{name}", __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_menu_can_exit_cleanly
|
|
13
|
+
output = StringIO.new
|
|
14
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("2\n"), stdout: output, stderr: StringIO.new)
|
|
15
|
+
assert_equal 0, status
|
|
16
|
+
assert_includes output.string, "CSV Tool Menu"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_end_to_end_console_happy_path_prints_expected_values
|
|
20
|
+
input = [
|
|
21
|
+
"1",
|
|
22
|
+
fixture_path("sample_people.csv"),
|
|
23
|
+
"1",
|
|
24
|
+
"",
|
|
25
|
+
"1",
|
|
26
|
+
"",
|
|
27
|
+
"y",
|
|
28
|
+
"",
|
|
29
|
+
"2"
|
|
30
|
+
].join("\n") + "\n"
|
|
31
|
+
|
|
32
|
+
output = StringIO.new
|
|
33
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
34
|
+
|
|
35
|
+
assert_equal 0, status
|
|
36
|
+
assert_match(/\nAlice\nBob\nCara\n/, output.string)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_column_command_prints_expected_values
|
|
40
|
+
output = StringIO.new
|
|
41
|
+
status = Csvtool::CLI.start(
|
|
42
|
+
["column", fixture_path("sample_people.csv"), "name"],
|
|
43
|
+
stdin: StringIO.new,
|
|
44
|
+
stdout: output,
|
|
45
|
+
stderr: StringIO.new
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
assert_equal 0, status
|
|
49
|
+
assert_equal "Alice\nBob\nCara\n", output.string
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_end_to_end_file_output_writes_expected_csv
|
|
53
|
+
output = StringIO.new
|
|
54
|
+
output_path = nil
|
|
55
|
+
|
|
56
|
+
Dir.mktmpdir do |dir|
|
|
57
|
+
output_path = File.join(dir, "names.csv")
|
|
58
|
+
input = [
|
|
59
|
+
"1",
|
|
60
|
+
fixture_path("sample_people.csv"),
|
|
61
|
+
"1",
|
|
62
|
+
"",
|
|
63
|
+
"1",
|
|
64
|
+
"",
|
|
65
|
+
"y",
|
|
66
|
+
"2",
|
|
67
|
+
output_path,
|
|
68
|
+
"2"
|
|
69
|
+
].join("\n") + "\n"
|
|
70
|
+
|
|
71
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
72
|
+
assert_equal 0, status
|
|
73
|
+
assert_equal "name\nAlice\nBob\nCara\n", File.read(output_path)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
assert_includes output.string, "Wrote output to #{output_path}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_preview_cancel_returns_to_menu
|
|
80
|
+
input = [
|
|
81
|
+
"1",
|
|
82
|
+
fixture_path("sample_people_many.csv"),
|
|
83
|
+
"1",
|
|
84
|
+
"",
|
|
85
|
+
"1",
|
|
86
|
+
"",
|
|
87
|
+
"n",
|
|
88
|
+
"2"
|
|
89
|
+
].join("\n") + "\n"
|
|
90
|
+
|
|
91
|
+
output = StringIO.new
|
|
92
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
93
|
+
|
|
94
|
+
assert_equal 0, status
|
|
95
|
+
assert_includes output.string, "Canceled."
|
|
96
|
+
assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_missing_file_shows_error_and_returns_to_menu
|
|
100
|
+
output = StringIO.new
|
|
101
|
+
status = Csvtool::CLI.start(
|
|
102
|
+
["menu"],
|
|
103
|
+
stdin: StringIO.new("1\n/tmp/does-not-exist.csv\n2\n"),
|
|
104
|
+
stdout: output,
|
|
105
|
+
stderr: StringIO.new
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
assert_equal 0, status
|
|
109
|
+
assert_includes output.string, "File not found: /tmp/does-not-exist.csv"
|
|
110
|
+
assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_invalid_output_path_shows_error_and_returns_to_menu
|
|
114
|
+
input = [
|
|
115
|
+
"1",
|
|
116
|
+
fixture_path("sample_people.csv"),
|
|
117
|
+
"1",
|
|
118
|
+
"",
|
|
119
|
+
"1",
|
|
120
|
+
"",
|
|
121
|
+
"y",
|
|
122
|
+
"2",
|
|
123
|
+
"/tmp/not-a-dir/out.csv",
|
|
124
|
+
"2"
|
|
125
|
+
].join("\n") + "\n"
|
|
126
|
+
|
|
127
|
+
output = StringIO.new
|
|
128
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new(input), stdout: output, stderr: StringIO.new)
|
|
129
|
+
|
|
130
|
+
assert_equal 0, status
|
|
131
|
+
assert_includes output.string, "Cannot write output file: /tmp/not-a-dir/out.csv"
|
|
132
|
+
assert_operator output.string.scan("CSV Tool Menu").length, :>=, 2
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../test_helper"
|
|
4
|
+
require "csvtool/cli"
|
|
5
|
+
|
|
6
|
+
class CliUnitTest < Minitest::Test
|
|
7
|
+
def test_unknown_command_prints_usage_and_returns_one
|
|
8
|
+
stdout = StringIO.new
|
|
9
|
+
stderr = StringIO.new
|
|
10
|
+
|
|
11
|
+
status = Csvtool::CLI.start(["unknown"], stdin: StringIO.new, stdout: stdout, stderr: stderr)
|
|
12
|
+
|
|
13
|
+
assert_equal 1, status
|
|
14
|
+
assert_includes stderr.string, "csvtool menu"
|
|
15
|
+
assert_includes stderr.string, "csvtool column <file> <column>"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_menu_command_can_exit_zero
|
|
19
|
+
status = Csvtool::CLI.start(["menu"], stdin: StringIO.new("2\n"), stdout: StringIO.new, stderr: StringIO.new)
|
|
20
|
+
assert_equal 0, status
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_column_command_requires_file_and_column_args
|
|
24
|
+
status = Csvtool::CLI.start(["column"], stdin: StringIO.new, stdout: StringIO.new, stderr: StringIO.new)
|
|
25
|
+
assert_equal 1, status
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/extraction_session/column_selection"
|
|
5
|
+
|
|
6
|
+
class ColumnSelectionTest < Minitest::Test
|
|
7
|
+
def test_stores_name
|
|
8
|
+
selection = Csvtool::Domain::ExtractionSession::ColumnSelection.new(name: "city")
|
|
9
|
+
assert_equal "city", selection.name
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/extraction_session/csv_source"
|
|
5
|
+
require "csvtool/domain/extraction_session/separator"
|
|
6
|
+
|
|
7
|
+
class CsvSourceTest < Minitest::Test
|
|
8
|
+
def test_stores_path_and_separator
|
|
9
|
+
separator = Csvtool::Domain::ExtractionSession::Separator.new(",")
|
|
10
|
+
source = Csvtool::Domain::ExtractionSession::CsvSource.new(path: "/tmp/a.csv", separator: separator)
|
|
11
|
+
assert_equal "/tmp/a.csv", source.path
|
|
12
|
+
assert_equal separator, source.separator
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/extraction_session/extraction_options"
|
|
5
|
+
|
|
6
|
+
class ExtractionOptionsTest < Minitest::Test
|
|
7
|
+
def test_exposes_options
|
|
8
|
+
options = Csvtool::Domain::ExtractionSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 10)
|
|
9
|
+
assert_equal true, options.skip_blanks?
|
|
10
|
+
assert_equal 10, options.preview_limit
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_non_positive_preview_limit_raises
|
|
14
|
+
assert_raises(ArgumentError) do
|
|
15
|
+
Csvtool::Domain::ExtractionSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 0)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/extraction_session/extraction_session"
|
|
5
|
+
require "csvtool/domain/extraction_session/csv_source"
|
|
6
|
+
require "csvtool/domain/extraction_session/separator"
|
|
7
|
+
require "csvtool/domain/extraction_session/column_selection"
|
|
8
|
+
require "csvtool/domain/extraction_session/extraction_options"
|
|
9
|
+
require "csvtool/domain/extraction_session/preview"
|
|
10
|
+
require "csvtool/domain/extraction_session/extraction_value"
|
|
11
|
+
require "csvtool/domain/extraction_session/output_destination"
|
|
12
|
+
|
|
13
|
+
class ExtractionSessionTest < Minitest::Test
|
|
14
|
+
def test_state_transitions
|
|
15
|
+
session = Csvtool::Domain::ExtractionSession::ExtractionSession.start(
|
|
16
|
+
source: Csvtool::Domain::ExtractionSession::CsvSource.new(
|
|
17
|
+
path: "/tmp/in.csv",
|
|
18
|
+
separator: Csvtool::Domain::ExtractionSession::Separator.new(",")
|
|
19
|
+
),
|
|
20
|
+
column_selection: Csvtool::Domain::ExtractionSession::ColumnSelection.new(name: "name"),
|
|
21
|
+
options: Csvtool::Domain::ExtractionSession::ExtractionOptions.new(skip_blanks: true, preview_limit: 10)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
preview = Csvtool::Domain::ExtractionSession::Preview.new(
|
|
25
|
+
values: [Csvtool::Domain::ExtractionSession::ExtractionValue.new("Alice")]
|
|
26
|
+
)
|
|
27
|
+
session = session.with_preview(preview).confirm!.with_output_destination(
|
|
28
|
+
Csvtool::Domain::ExtractionSession::OutputDestination.console
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
assert_equal true, session.confirmed?
|
|
32
|
+
assert_equal preview, session.preview
|
|
33
|
+
assert_equal true, session.output_destination.console?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/extraction_session/extraction_value"
|
|
5
|
+
|
|
6
|
+
class ExtractionValueTest < Minitest::Test
|
|
7
|
+
def test_stringifies_value
|
|
8
|
+
value = Csvtool::Domain::ExtractionSession::ExtractionValue.new(123)
|
|
9
|
+
assert_equal "123", value.value
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/extraction_session/output_destination"
|
|
5
|
+
|
|
6
|
+
class OutputDestinationTest < Minitest::Test
|
|
7
|
+
def test_console_factory
|
|
8
|
+
destination = Csvtool::Domain::ExtractionSession::OutputDestination.console
|
|
9
|
+
assert_equal true, destination.console?
|
|
10
|
+
assert_equal false, destination.file?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_file_factory
|
|
14
|
+
destination = Csvtool::Domain::ExtractionSession::OutputDestination.file(path: "/tmp/out.csv")
|
|
15
|
+
assert_equal true, destination.file?
|
|
16
|
+
assert_equal "/tmp/out.csv", destination.path
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/extraction_session/preview"
|
|
5
|
+
require "csvtool/domain/extraction_session/extraction_value"
|
|
6
|
+
|
|
7
|
+
class PreviewTest < Minitest::Test
|
|
8
|
+
def test_exposes_size_and_string_values
|
|
9
|
+
values = [
|
|
10
|
+
Csvtool::Domain::ExtractionSession::ExtractionValue.new("Alice"),
|
|
11
|
+
Csvtool::Domain::ExtractionSession::ExtractionValue.new("Bob")
|
|
12
|
+
]
|
|
13
|
+
preview = Csvtool::Domain::ExtractionSession::Preview.new(values: values)
|
|
14
|
+
|
|
15
|
+
assert_equal 2, preview.size
|
|
16
|
+
assert_equal %w[Alice Bob], preview.to_strings
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/domain/extraction_session/separator"
|
|
5
|
+
|
|
6
|
+
class SeparatorTest < Minitest::Test
|
|
7
|
+
def test_stores_value
|
|
8
|
+
separator = Csvtool::Domain::ExtractionSession::Separator.new(",")
|
|
9
|
+
assert_equal ",", separator.value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_empty_value_raises
|
|
13
|
+
assert_raises(ArgumentError) { Csvtool::Domain::ExtractionSession::Separator.new("") }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/infrastructure/csv/header_reader"
|
|
5
|
+
|
|
6
|
+
class InfrastructureHeaderReaderTest < Minitest::Test
|
|
7
|
+
def fixture_path(name)
|
|
8
|
+
File.expand_path("../../../fixtures/#{name}", __dir__)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_returns_headers
|
|
12
|
+
reader = Csvtool::Infrastructure::CSV::HeaderReader.new
|
|
13
|
+
headers = reader.call(file_path: fixture_path("sample_people.csv"), col_sep: ",")
|
|
14
|
+
assert_equal %w[name city], headers
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/infrastructure/csv/value_streamer"
|
|
5
|
+
|
|
6
|
+
class InfrastructureValueStreamerTest < Minitest::Test
|
|
7
|
+
def fixture_path(name)
|
|
8
|
+
File.expand_path("../../../fixtures/#{name}", __dir__)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_filters_blanks_when_enabled
|
|
12
|
+
streamer = Csvtool::Infrastructure::CSV::ValueStreamer.new
|
|
13
|
+
values = []
|
|
14
|
+
streamer.each(
|
|
15
|
+
file_path: fixture_path("sample_people_blanks.csv"),
|
|
16
|
+
column_name: "name",
|
|
17
|
+
col_sep: ",",
|
|
18
|
+
skip_blanks: true
|
|
19
|
+
) { |v| values << v }
|
|
20
|
+
assert_equal %w[Alice Bob Cara], values
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/infrastructure/output/console_writer"
|
|
5
|
+
|
|
6
|
+
class InfrastructureConsoleWriterTest < Minitest::Test
|
|
7
|
+
class FakeStreamer
|
|
8
|
+
def each(file_path:, column_name:, col_sep:, skip_blanks:)
|
|
9
|
+
%w[Alice Bob].each { |value| yield value }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_writes_streamed_values_to_stdout
|
|
14
|
+
out = StringIO.new
|
|
15
|
+
writer = Csvtool::Infrastructure::Output::ConsoleWriter.new(stdout: out, value_streamer: FakeStreamer.new)
|
|
16
|
+
writer.call(file_path: "x.csv", column_name: "name", col_sep: ",", skip_blanks: true)
|
|
17
|
+
assert_equal "Alice\nBob\n", out.string
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/infrastructure/output/csv_file_writer"
|
|
5
|
+
require "csvtool/interface/cli/errors/presenter"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
|
|
8
|
+
class InfrastructureCsvFileWriterTest < Minitest::Test
|
|
9
|
+
class FakeStreamer
|
|
10
|
+
def each(file_path:, column_name:, col_sep:, skip_blanks:)
|
|
11
|
+
%w[Alice Bob].each { |value| yield value }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_writes_header_and_values
|
|
16
|
+
stdout = StringIO.new
|
|
17
|
+
writer = Csvtool::Infrastructure::Output::CsvFileWriter.new(
|
|
18
|
+
stdout: stdout,
|
|
19
|
+
errors: Csvtool::Interface::CLI::Errors::Presenter.new(stdout: stdout),
|
|
20
|
+
value_streamer: FakeStreamer.new
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
Dir.mktmpdir do |dir|
|
|
24
|
+
output_path = File.join(dir, "names.csv")
|
|
25
|
+
writer.call(
|
|
26
|
+
file_path: "ignored.csv",
|
|
27
|
+
column_name: "name",
|
|
28
|
+
col_sep: ",",
|
|
29
|
+
skip_blanks: true,
|
|
30
|
+
output_path: output_path
|
|
31
|
+
)
|
|
32
|
+
assert_equal "name\nAlice\nBob\n", File.read(output_path)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/errors/presenter"
|
|
5
|
+
|
|
6
|
+
class ErrorsPresenterTest < Minitest::Test
|
|
7
|
+
def test_all_messages_are_presented
|
|
8
|
+
out = StringIO.new
|
|
9
|
+
presenter = Csvtool::Interface::CLI::Errors::Presenter.new(stdout: out)
|
|
10
|
+
|
|
11
|
+
presenter.file_not_found("/tmp/x.csv")
|
|
12
|
+
presenter.no_headers
|
|
13
|
+
presenter.column_not_found
|
|
14
|
+
presenter.could_not_parse_csv
|
|
15
|
+
presenter.cannot_read_file("/tmp/y.csv")
|
|
16
|
+
presenter.cannot_write_output_file("/tmp/z.csv", Errno::ENOENT)
|
|
17
|
+
presenter.empty_output_path
|
|
18
|
+
presenter.invalid_output_destination
|
|
19
|
+
presenter.empty_custom_separator
|
|
20
|
+
presenter.invalid_separator_choice
|
|
21
|
+
presenter.canceled
|
|
22
|
+
|
|
23
|
+
text = out.string
|
|
24
|
+
assert_includes text, "File not found: /tmp/x.csv"
|
|
25
|
+
assert_includes text, "No headers found."
|
|
26
|
+
assert_includes text, "Column not found."
|
|
27
|
+
assert_includes text, "Could not parse CSV file."
|
|
28
|
+
assert_includes text, "Cannot read file: /tmp/y.csv"
|
|
29
|
+
assert_includes text, "Cannot write output file: /tmp/z.csv (Errno::ENOENT)"
|
|
30
|
+
assert_includes text, "Output file path cannot be empty."
|
|
31
|
+
assert_includes text, "Invalid output destination."
|
|
32
|
+
assert_includes text, "Separator cannot be empty."
|
|
33
|
+
assert_includes text, "Invalid separator choice."
|
|
34
|
+
assert_includes text, "Canceled."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/menu_loop"
|
|
5
|
+
|
|
6
|
+
class MenuLoopTest < Minitest::Test
|
|
7
|
+
class FakeAction
|
|
8
|
+
attr_reader :runs
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@runs = 0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
@runs += 1
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_routes_extract_then_exit
|
|
20
|
+
action = FakeAction.new
|
|
21
|
+
stdout = StringIO.new
|
|
22
|
+
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
23
|
+
stdin: StringIO.new("1\n2\n"),
|
|
24
|
+
stdout: stdout,
|
|
25
|
+
menu_options: ["Extract column", "Exit"],
|
|
26
|
+
extract_action: action
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
status = menu.run
|
|
30
|
+
|
|
31
|
+
assert_equal 0, status
|
|
32
|
+
assert_equal 1, action.runs
|
|
33
|
+
assert_includes stdout.string, "CSV Tool Menu"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_invalid_choice_shows_prompt
|
|
37
|
+
action = FakeAction.new
|
|
38
|
+
stdout = StringIO.new
|
|
39
|
+
menu = Csvtool::Interface::CLI::MenuLoop.new(
|
|
40
|
+
stdin: StringIO.new("x\n2\n"),
|
|
41
|
+
stdout: stdout,
|
|
42
|
+
menu_options: ["Extract column", "Exit"],
|
|
43
|
+
extract_action: action
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
menu.run
|
|
47
|
+
|
|
48
|
+
assert_includes stdout.string, "Please choose 1 or 2."
|
|
49
|
+
assert_equal 0, action.runs
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/prompts/column_selector_prompt"
|
|
5
|
+
|
|
6
|
+
class ColumnSelectorPromptTest < Minitest::Test
|
|
7
|
+
class FakeErrors
|
|
8
|
+
attr_reader :calls
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@calls = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def column_not_found = @calls << :column_not_found
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_selects_filtered_header
|
|
18
|
+
errors = FakeErrors.new
|
|
19
|
+
prompt = Csvtool::Interface::CLI::Prompts::ColumnSelectorPrompt.new(stdin: StringIO.new("ci\n1\n"), stdout: StringIO.new, errors: errors)
|
|
20
|
+
assert_equal "city", prompt.call(%w[name city])
|
|
21
|
+
assert_empty errors.calls
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/prompts/confirm_prompt"
|
|
5
|
+
|
|
6
|
+
class ConfirmPromptTest < Minitest::Test
|
|
7
|
+
class FakeErrors
|
|
8
|
+
attr_reader :calls
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@calls = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def canceled = @calls << :canceled
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_calls_canceled_on_no
|
|
18
|
+
errors = FakeErrors.new
|
|
19
|
+
prompt = Csvtool::Interface::CLI::Prompts::ConfirmPrompt.new(stdin: StringIO.new("n\n"), stdout: StringIO.new, errors: errors)
|
|
20
|
+
assert_equal false, prompt.call(%w[a b])
|
|
21
|
+
assert_includes errors.calls, :canceled
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/prompts/file_path_prompt"
|
|
5
|
+
|
|
6
|
+
class FilePathPromptTest < Minitest::Test
|
|
7
|
+
def test_returns_trimmed_path
|
|
8
|
+
prompt = Csvtool::Interface::CLI::Prompts::FilePathPrompt.new(stdin: StringIO.new(" /tmp/a.csv \n"), stdout: StringIO.new)
|
|
9
|
+
assert_equal "/tmp/a.csv", prompt.call
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/prompts/output_destination_prompt"
|
|
5
|
+
require "csvtool/interface/cli/errors/presenter"
|
|
6
|
+
|
|
7
|
+
class OutputDestinationPromptTest < Minitest::Test
|
|
8
|
+
def test_defaults_to_console
|
|
9
|
+
stdout = StringIO.new
|
|
10
|
+
prompt = Csvtool::Interface::CLI::Prompts::OutputDestinationPrompt.new(
|
|
11
|
+
stdin: StringIO.new("\n"),
|
|
12
|
+
stdout: stdout,
|
|
13
|
+
errors: Csvtool::Interface::CLI::Errors::Presenter.new(stdout: stdout)
|
|
14
|
+
)
|
|
15
|
+
assert_equal({ mode: :console }, prompt.call)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_file_mode_requires_path
|
|
19
|
+
stdout = StringIO.new
|
|
20
|
+
prompt = Csvtool::Interface::CLI::Prompts::OutputDestinationPrompt.new(
|
|
21
|
+
stdin: StringIO.new("2\n\n"),
|
|
22
|
+
stdout: stdout,
|
|
23
|
+
errors: Csvtool::Interface::CLI::Errors::Presenter.new(stdout: stdout)
|
|
24
|
+
)
|
|
25
|
+
assert_nil prompt.call
|
|
26
|
+
assert_includes stdout.string, "Output file path cannot be empty."
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/prompts/separator_prompt"
|
|
5
|
+
|
|
6
|
+
class SeparatorPromptTest < Minitest::Test
|
|
7
|
+
class FakeErrors
|
|
8
|
+
attr_reader :calls
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@calls = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def empty_custom_separator = @calls << :empty_custom_separator
|
|
15
|
+
def invalid_separator_choice = @calls << :invalid_separator_choice
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_defaults_to_comma
|
|
19
|
+
errors = FakeErrors.new
|
|
20
|
+
prompt = Csvtool::Interface::CLI::Prompts::SeparatorPrompt.new(stdin: StringIO.new("\n"), stdout: StringIO.new, errors: errors)
|
|
21
|
+
assert_equal ",", prompt.call
|
|
22
|
+
assert_empty errors.calls
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_custom_empty_reports_error
|
|
26
|
+
errors = FakeErrors.new
|
|
27
|
+
prompt = Csvtool::Interface::CLI::Prompts::SeparatorPrompt.new(stdin: StringIO.new("5\n\n"), stdout: StringIO.new, errors: errors)
|
|
28
|
+
assert_nil prompt.call
|
|
29
|
+
assert_includes errors.calls, :empty_custom_separator
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../test_helper"
|
|
4
|
+
require "csvtool/interface/cli/prompts/skip_blanks_prompt"
|
|
5
|
+
|
|
6
|
+
class SkipBlanksPromptTest < Minitest::Test
|
|
7
|
+
def test_default_true_and_no_false
|
|
8
|
+
prompt_yes = Csvtool::Interface::CLI::Prompts::SkipBlanksPrompt.new(stdin: StringIO.new("\n"), stdout: StringIO.new)
|
|
9
|
+
prompt_no = Csvtool::Interface::CLI::Prompts::SkipBlanksPrompt.new(stdin: StringIO.new("n\n"), stdout: StringIO.new)
|
|
10
|
+
assert_equal true, prompt_yes.call
|
|
11
|
+
assert_equal false, prompt_no.call
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../test_helper"
|
|
4
|
+
require "csvtool/services/preview_builder"
|
|
5
|
+
|
|
6
|
+
class PreviewBuilderTest < Minitest::Test
|
|
7
|
+
class FakeStreamer
|
|
8
|
+
def initialize(values)
|
|
9
|
+
@values = values
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def each(file_path:, column_name:, col_sep:, skip_blanks:)
|
|
13
|
+
@values.each { |value| yield value }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_builds_preview_up_to_limit
|
|
18
|
+
builder = Csvtool::Services::PreviewBuilder.new(value_streamer: FakeStreamer.new(%w[a b c d]))
|
|
19
|
+
preview = builder.call(file_path: "x.csv", column_name: "name", col_sep: ",", skip_blanks: true, limit: 2)
|
|
20
|
+
assert_equal %w[a b], preview
|
|
21
|
+
end
|
|
22
|
+
end
|
|
File without changes
|