rfix 2.0.4 → 3.0.0
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/exe/rfix +11 -90
- data/lib/rfix.rb +10 -9
- data/lib/rfix/branch/reference.rb +2 -2
- data/lib/rfix/branch/upstream.rb +2 -4
- data/lib/rfix/cli/command.rb +14 -1
- data/lib/rfix/cli/command/all.rb +21 -0
- data/lib/rfix/cli/command/base.rb +30 -19
- data/lib/rfix/cli/command/branch.rb +2 -0
- data/lib/rfix/cli/command/help.rb +2 -0
- data/lib/rfix/cli/command/info.rb +6 -1
- data/lib/rfix/cli/command/local.rb +2 -0
- data/lib/rfix/cli/command/origin.rb +2 -0
- data/lib/rfix/cli/command/setup.rb +2 -0
- data/lib/rfix/cli/command/status.rb +39 -0
- data/lib/rfix/collector.rb +69 -0
- data/lib/rfix/diff.rb +69 -0
- data/lib/rfix/extension/comment_config.rb +15 -0
- data/lib/rfix/extension/offense.rb +17 -14
- data/lib/rfix/extension/pastel.rb +7 -4
- data/lib/rfix/extension/progresbar.rb +15 -0
- data/lib/rfix/extension/strings.rb +10 -2
- data/lib/rfix/file.rb +5 -3
- data/lib/rfix/file/base.rb +21 -14
- data/lib/rfix/file/deleted.rb +2 -0
- data/lib/rfix/file/ignored.rb +2 -0
- data/lib/rfix/file/null.rb +17 -0
- data/lib/rfix/file/tracked.rb +39 -23
- data/lib/rfix/file/undefined.rb +17 -0
- data/lib/rfix/file/untracked.rb +3 -1
- data/lib/rfix/formatter.rb +67 -71
- data/lib/rfix/highlighter.rb +1 -3
- data/lib/rfix/rake/gemfile.rb +26 -23
- data/lib/rfix/repository.rb +59 -96
- data/lib/rfix/types.rb +24 -14
- data/lib/rfix/version.rb +1 -1
- data/rfix.gemspec +11 -3
- data/vendor/cli-ui/Gemfile +17 -0
- data/vendor/cli-ui/Gemfile.lock +60 -0
- data/vendor/cli-ui/LICENSE.txt +21 -0
- data/vendor/cli-ui/README.md +224 -0
- data/vendor/cli-ui/Rakefile +20 -0
- data/vendor/cli-ui/bin/console +14 -0
- data/vendor/cli-ui/cli-ui.gemspec +25 -0
- data/vendor/cli-ui/dev.yml +14 -0
- data/vendor/cli-ui/lib/cli/ui.rb +233 -0
- data/vendor/cli-ui/lib/cli/ui/ansi.rb +157 -0
- data/vendor/cli-ui/lib/cli/ui/color.rb +84 -0
- data/vendor/cli-ui/lib/cli/ui/formatter.rb +192 -0
- data/vendor/cli-ui/lib/cli/ui/frame.rb +269 -0
- data/vendor/cli-ui/lib/cli/ui/frame/frame_stack.rb +98 -0
- data/vendor/cli-ui/lib/cli/ui/frame/frame_style.rb +120 -0
- data/vendor/cli-ui/lib/cli/ui/frame/frame_style/box.rb +166 -0
- data/vendor/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +139 -0
- data/vendor/cli-ui/lib/cli/ui/glyph.rb +84 -0
- data/vendor/cli-ui/lib/cli/ui/os.rb +67 -0
- data/vendor/cli-ui/lib/cli/ui/printer.rb +59 -0
- data/vendor/cli-ui/lib/cli/ui/progress.rb +90 -0
- data/vendor/cli-ui/lib/cli/ui/prompt.rb +297 -0
- data/vendor/cli-ui/lib/cli/ui/prompt/interactive_options.rb +484 -0
- data/vendor/cli-ui/lib/cli/ui/prompt/options_handler.rb +29 -0
- data/vendor/cli-ui/lib/cli/ui/spinner.rb +66 -0
- data/vendor/cli-ui/lib/cli/ui/spinner/async.rb +40 -0
- data/vendor/cli-ui/lib/cli/ui/spinner/spin_group.rb +263 -0
- data/vendor/cli-ui/lib/cli/ui/stdout_router.rb +232 -0
- data/vendor/cli-ui/lib/cli/ui/terminal.rb +46 -0
- data/vendor/cli-ui/lib/cli/ui/truncater.rb +102 -0
- data/vendor/cli-ui/lib/cli/ui/version.rb +5 -0
- data/vendor/cli-ui/lib/cli/ui/widgets.rb +77 -0
- data/vendor/cli-ui/lib/cli/ui/widgets/base.rb +27 -0
- data/vendor/cli-ui/lib/cli/ui/widgets/status.rb +61 -0
- data/vendor/cli-ui/lib/cli/ui/wrap.rb +56 -0
- data/vendor/cli-ui/test/cli/ui/ansi_test.rb +32 -0
- data/vendor/cli-ui/test/cli/ui/cli_ui_test.rb +23 -0
- data/vendor/cli-ui/test/cli/ui/color_test.rb +40 -0
- data/vendor/cli-ui/test/cli/ui/formatter_test.rb +79 -0
- data/vendor/cli-ui/test/cli/ui/glyph_test.rb +68 -0
- data/vendor/cli-ui/test/cli/ui/printer_test.rb +103 -0
- data/vendor/cli-ui/test/cli/ui/progress_test.rb +46 -0
- data/vendor/cli-ui/test/cli/ui/prompt/options_handler_test.rb +39 -0
- data/vendor/cli-ui/test/cli/ui/prompt_test.rb +348 -0
- data/vendor/cli-ui/test/cli/ui/spinner/spin_group_test.rb +39 -0
- data/vendor/cli-ui/test/cli/ui/spinner_test.rb +141 -0
- data/vendor/cli-ui/test/cli/ui/stdout_router_test.rb +32 -0
- data/vendor/cli-ui/test/cli/ui/terminal_test.rb +26 -0
- data/vendor/cli-ui/test/cli/ui/truncater_test.rb +31 -0
- data/vendor/cli-ui/test/cli/ui/widgets/status_test.rb +49 -0
- data/vendor/cli-ui/test/cli/ui/widgets_test.rb +15 -0
- data/vendor/cli-ui/test/test_helper.rb +53 -0
- data/vendor/cli-ui/tmp/cache/bootsnap/compile-cache/d9/c036af0f3dc494 +0 -0
- data/vendor/cli-ui/tmp/cache/bootsnap/load-path-cache +0 -0
- data/vendor/dry-cli/lib/dry/cli/command.rb +2 -1
- data/vendor/dry-cli/tmp/cache/bootsnap/compile-cache/ff/a22a5daafbd74c +0 -0
- data/vendor/dry-cli/tmp/cache/bootsnap/load-path-cache +0 -0
- data/vendor/strings-ansi/tmp/cache/bootsnap/compile-cache/79/49cf49407b370e +0 -0
- data/vendor/strings-ansi/tmp/cache/bootsnap/load-path-cache +0 -0
- metadata +170 -9
- data/lib/rfix/extension/string.rb +0 -12
- data/lib/rfix/indicator.rb +0 -19
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
module CLI
|
|
4
|
+
module UI
|
|
5
|
+
class FormatterTest < MiniTest::Test
|
|
6
|
+
def test_format
|
|
7
|
+
input = 'a{{blue:b {{*}}{{bold:c {{red:d}}}}{{bold: e}}}} f'
|
|
8
|
+
expected = "\e[0ma\e[0;94mb \e[0;33m⭑\e[0;94;1mc \e[0;94;1;31md\e[0;94;1m e\e[0m f"
|
|
9
|
+
actual = CLI::UI::Formatter.new(input).format
|
|
10
|
+
assert_equal(expected, actual)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_format_widget
|
|
14
|
+
input = 'a{{@widget/status:0:0:0:0}}b'
|
|
15
|
+
expected = "a\e[0m\e[1m∅\e[0mb"
|
|
16
|
+
actual = CLI::UI::Formatter.new(input).format(enable_color: false)
|
|
17
|
+
assert_equal(expected, actual)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_format_no_color
|
|
21
|
+
input = 'a{{blue:b {{*}}{{bold:c {{red:d}}}}{{bold: e}}}} f {{bold:'
|
|
22
|
+
expected = 'ab ⭑c d e f '
|
|
23
|
+
actual = CLI::UI::Formatter.new(input).format(enable_color: false)
|
|
24
|
+
assert_equal(expected, actual)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_format_trailing
|
|
28
|
+
input = 'a{{bold:a {{blue:'
|
|
29
|
+
expected = "\e[0ma\e[0;1ma \e[0;1;94m"
|
|
30
|
+
actual = CLI::UI::Formatter.new(input).format
|
|
31
|
+
assert_equal(expected, actual)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_invalid_funcname
|
|
35
|
+
input = '{{nope:text}}'
|
|
36
|
+
ex = assert_raises(CLI::UI::Formatter::FormatError) do
|
|
37
|
+
CLI::UI::Formatter.new(input).format
|
|
38
|
+
end
|
|
39
|
+
expected = 'invalid format specifier: nope'
|
|
40
|
+
assert_equal(input, ex.input)
|
|
41
|
+
assert_equal(-1, ex.index)
|
|
42
|
+
assert_equal(expected, ex.message)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_invalid_glyph
|
|
46
|
+
input = '{{&}}'
|
|
47
|
+
ex = assert_raises(CLI::UI::Formatter::FormatError) do
|
|
48
|
+
CLI::UI::Formatter.new(input).format
|
|
49
|
+
end
|
|
50
|
+
expected = "invalid glyph handle at index 3: '&'"
|
|
51
|
+
assert_equal(input, ex.input)
|
|
52
|
+
assert_equal(3, ex.index)
|
|
53
|
+
assert_equal(expected, ex.message)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_mixed_non_syntax
|
|
57
|
+
input = '{{bold:{{foo {{green:bar}} }}}}'
|
|
58
|
+
expected = "\e[0;1m{{foo \e[0;1;32mbar\e[0;1m }}\e[0m"
|
|
59
|
+
actual = CLI::UI::Formatter.new(input).format
|
|
60
|
+
assert_equal(expected, actual)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_incomplete_non_syntax
|
|
64
|
+
input = '{{foo'
|
|
65
|
+
expected = "\e[0m{{foo"
|
|
66
|
+
actual = CLI::UI::Formatter.new(input).format
|
|
67
|
+
assert_equal(expected, actual)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_reset_after_glyph
|
|
71
|
+
input = '{{*}} foobar'
|
|
72
|
+
expected = "\e[0;33m⭑\e[0m foobar"
|
|
73
|
+
|
|
74
|
+
actual = CLI::UI::Formatter.new(input).format
|
|
75
|
+
assert_equal(expected, actual)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
module CLI
|
|
4
|
+
module UI
|
|
5
|
+
class GlyphTest < MiniTest::Test
|
|
6
|
+
def test_glyphs
|
|
7
|
+
assert_equal("\x1b[33m⭑\x1b[0m", Glyph::STAR.to_s)
|
|
8
|
+
assert_equal("\x1b[94m𝒾\x1b[0m", Glyph::INFO.to_s)
|
|
9
|
+
assert_equal("\x1b[94m?\x1b[0m", Glyph::QUESTION.to_s)
|
|
10
|
+
assert_equal("\x1b[32m✓\x1b[0m", Glyph::CHECK.to_s)
|
|
11
|
+
assert_equal("\x1b[31m✗\x1b[0m", Glyph::X.to_s)
|
|
12
|
+
assert_equal("\x1b[97m🐛\x1b[0m", Glyph::BUG.to_s)
|
|
13
|
+
assert_equal("\x1b[33m»\x1b[0m", Glyph::CHEVRON.to_s)
|
|
14
|
+
|
|
15
|
+
assert_equal(Glyph::STAR, Glyph.lookup('*'))
|
|
16
|
+
assert_equal(Glyph::INFO, Glyph.lookup('i'))
|
|
17
|
+
assert_equal(Glyph::QUESTION, Glyph.lookup('?'))
|
|
18
|
+
assert_equal(Glyph::CHECK, Glyph.lookup('v'))
|
|
19
|
+
assert_equal(Glyph::X, Glyph.lookup('x'))
|
|
20
|
+
assert_equal(Glyph::BUG, Glyph.lookup('b'))
|
|
21
|
+
assert_equal(Glyph::CHEVRON, Glyph.lookup('>'))
|
|
22
|
+
|
|
23
|
+
assert_raises(Glyph::InvalidGlyphHandle) do
|
|
24
|
+
Glyph.lookup('$')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_plain_glyphs
|
|
29
|
+
with_os_mock_and_reload(
|
|
30
|
+
CLI::UI::OS::Windows,
|
|
31
|
+
:Glyph,
|
|
32
|
+
File.join(File.dirname(__FILE__), '../../../lib/cli/ui/glyph.rb')
|
|
33
|
+
) do
|
|
34
|
+
assert_equal("\x1b[33m*\x1b[0m", Glyph::STAR.to_s)
|
|
35
|
+
assert_equal("\x1b[94mi\x1b[0m", Glyph::INFO.to_s)
|
|
36
|
+
assert_equal("\x1b[94m?\x1b[0m", Glyph::QUESTION.to_s)
|
|
37
|
+
assert_equal("\x1b[32m√\x1b[0m", Glyph::CHECK.to_s)
|
|
38
|
+
assert_equal("\x1b[31mX\x1b[0m", Glyph::X.to_s)
|
|
39
|
+
assert_equal("\x1b[97m!\x1b[0m", Glyph::BUG.to_s)
|
|
40
|
+
assert_equal("\x1b[33m»\x1b[0m", Glyph::CHEVRON.to_s)
|
|
41
|
+
|
|
42
|
+
assert_equal(Glyph::STAR, Glyph.lookup('*'))
|
|
43
|
+
assert_equal(Glyph::INFO, Glyph.lookup('i'))
|
|
44
|
+
assert_equal(Glyph::QUESTION, Glyph.lookup('?'))
|
|
45
|
+
assert_equal(Glyph::CHECK, Glyph.lookup('v'))
|
|
46
|
+
assert_equal(Glyph::X, Glyph.lookup('x'))
|
|
47
|
+
assert_equal(Glyph::BUG, Glyph.lookup('b'))
|
|
48
|
+
assert_equal(Glyph::CHEVRON, Glyph.lookup('>'))
|
|
49
|
+
|
|
50
|
+
assert_raises(Glyph::InvalidGlyphHandle) do
|
|
51
|
+
Glyph.lookup('$')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_useful_exception
|
|
57
|
+
e = begin
|
|
58
|
+
Glyph.lookup('$')
|
|
59
|
+
rescue => e
|
|
60
|
+
e
|
|
61
|
+
end
|
|
62
|
+
assert_match(/invalid glyph handle: \$/, e.message) # error
|
|
63
|
+
assert_match(/Glyph\.available/, e.message) # where to find colors
|
|
64
|
+
assert_match(/\*/, e.message) # list of valid colors
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
module CLI
|
|
4
|
+
module UI
|
|
5
|
+
class PrinterTest < MiniTest::Test
|
|
6
|
+
def test_puts_color
|
|
7
|
+
out, _ = capture_io do
|
|
8
|
+
CLI::UI::StdoutRouter.ensure_activated
|
|
9
|
+
assert(Printer.puts('foo', frame_color: :red))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
assert_equal("\e[0mfoo\n", out)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# NOTE: The spacing in the assertion of this test is important for
|
|
16
|
+
# downstream projects and should be maintained.
|
|
17
|
+
def test_puts_color_frame
|
|
18
|
+
Frame.open('test') do
|
|
19
|
+
out, _ = capture_io do
|
|
20
|
+
CLI::UI::StdoutRouter.ensure_activated
|
|
21
|
+
assert(Printer.puts('foo', frame_color: :red))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
assert_equal("\e[31m┃ \e[0m\e[0mfoo\n", out)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_frame_with_long_texts
|
|
29
|
+
overlong_preamble = 'This is a long preamble! '
|
|
30
|
+
overlong_preamble *= (CLI::UI::Terminal.width / overlong_preamble.length).floor + 1
|
|
31
|
+
|
|
32
|
+
overlong_suffix = 'This overlaps the suffix! '
|
|
33
|
+
overlong_suffix *= (CLI::UI::Terminal.width / overlong_suffix.length).floor + 1
|
|
34
|
+
|
|
35
|
+
Frame.open(overlong_preamble, success_text: overlong_suffix) do
|
|
36
|
+
out, _ = capture_io do
|
|
37
|
+
CLI::UI::StdoutRouter.ensure_activated
|
|
38
|
+
assert(Printer.puts('foo', frame_color: :red))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
assert_equal("\e[31m┃ \e[0m\e[0mfoo\n", out)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_puts_stream
|
|
46
|
+
_, err = capture_io do
|
|
47
|
+
assert(Printer.puts('foo', to: $stderr, format: false))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
assert_equal("foo\n", err)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_puts_format
|
|
54
|
+
out, _ = capture_io do
|
|
55
|
+
assert(Printer.puts('{{x}} foo'))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
assert_equal("\e[0;31m✗\e[0m foo\n", out)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_puts_pipe
|
|
62
|
+
IO.pipe do |r, w|
|
|
63
|
+
assert(Printer.puts('foo', to: w, format: false))
|
|
64
|
+
assert_equal("foo\n", r.gets)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_puts_pipe_closed
|
|
69
|
+
IO.pipe do |_r, w|
|
|
70
|
+
w.close
|
|
71
|
+
assert_raises(IOError) do
|
|
72
|
+
Printer.puts('foo', to: w, graceful: false)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_puts_graceful
|
|
78
|
+
IO.pipe do |r, w|
|
|
79
|
+
w.close
|
|
80
|
+
refute(Printer.puts('foo', to: w, graceful: true))
|
|
81
|
+
assert_nil(r.gets)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_encoding
|
|
86
|
+
msg = 'é'.force_encoding(Encoding::ISO_8859_1)
|
|
87
|
+
out, _ = capture_io do
|
|
88
|
+
assert(Printer.puts(msg, encoding: nil, format: false))
|
|
89
|
+
end
|
|
90
|
+
refute_equal(msg + "\n", out) # It doesn't work
|
|
91
|
+
assert_equal(msg.encode(Encoding::UTF_8) + "\n", out)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_encoding_ut8
|
|
95
|
+
msg = 'é'.force_encoding(Encoding::ISO_8859_1)
|
|
96
|
+
out, _ = capture_io do
|
|
97
|
+
assert(Printer.puts(msg, format: false))
|
|
98
|
+
end
|
|
99
|
+
assert_equal(msg + "\n", out)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
module CLI
|
|
4
|
+
module UI
|
|
5
|
+
class ProgressTest < MiniTest::Test
|
|
6
|
+
def test_tick_with_percent
|
|
7
|
+
assert_bar(set_percent: 0.1, expected_filled: 1, expected_unfilled: 9, suffix: ' 10% ')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_tick_with_set_percent
|
|
11
|
+
assert_bar(set_percent: 0.9, expected_filled: 9, expected_unfilled: 1, suffix: ' 90% ')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_tick_with_set_percent_above_100_percent_is_set_to_100_percent
|
|
15
|
+
assert_bar(set_percent: 2.0, expected_filled: 10, suffix: ' 100%')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_tick_with_percent_change_to_above_100_percent_is_set_to_100_percent
|
|
19
|
+
assert_bar(percent: 2.0, expected_filled: 10, suffix: ' 100%')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_tick_with_set_percent_and_percent_raises
|
|
23
|
+
assert_raises(ArgumentError) do
|
|
24
|
+
bar = Progress.new(width: 10)
|
|
25
|
+
bar.tick(percent: 0.5, set_percent: 0.9)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def assert_bar(percent: nil, set_percent: nil, expected_filled: 0, expected_unfilled: 0, suffix: '')
|
|
30
|
+
expected_bar = "\e[0m\e[46m#{" " * expected_filled}\e[1;47m#{" " * expected_unfilled}\e[0m#{suffix}"
|
|
31
|
+
|
|
32
|
+
params = {}
|
|
33
|
+
params[:percent] = percent if percent
|
|
34
|
+
params[:set_percent] = set_percent if set_percent
|
|
35
|
+
|
|
36
|
+
out, = capture_io do
|
|
37
|
+
bar = Progress.new(width: 10 + suffix.size) # each 10% is one box with this width
|
|
38
|
+
bar.tick(**params)
|
|
39
|
+
assert_equal(expected_bar, bar.to_s)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
assert_equal(expected_bar + "\e[1A\e[1G\n", out)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
require 'cli/ui/prompt/options_handler'
|
|
3
|
+
|
|
4
|
+
module CLI
|
|
5
|
+
module UI
|
|
6
|
+
module Prompt
|
|
7
|
+
class OptionsHandlerTest < MiniTest::Test
|
|
8
|
+
def test_initialize
|
|
9
|
+
handler = OptionsHandler.new
|
|
10
|
+
|
|
11
|
+
assert_empty(handler.options)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_option
|
|
15
|
+
handler = OptionsHandler.new
|
|
16
|
+
|
|
17
|
+
handler.option('a') {}
|
|
18
|
+
handler.option('b') {}
|
|
19
|
+
handler.option('c') {}
|
|
20
|
+
|
|
21
|
+
assert_equal(['a', 'b', 'c'], handler.options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_call
|
|
25
|
+
handler = OptionsHandler.new
|
|
26
|
+
procedure_called = false
|
|
27
|
+
procedure = proc do |selection|
|
|
28
|
+
procedure_called = true
|
|
29
|
+
selection
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
handler.option('a', &procedure)
|
|
33
|
+
assert_equal('a', handler.call('a'))
|
|
34
|
+
assert(procedure_called)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
require 'test_helper'
|
|
3
|
+
require 'readline'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
require 'open3'
|
|
6
|
+
|
|
7
|
+
module CLI
|
|
8
|
+
module UI
|
|
9
|
+
class PromptTest < MiniTest::Test
|
|
10
|
+
# ^C is not handled; raises Interrupt, which may be handled by caller.
|
|
11
|
+
def test_confirm_sigint
|
|
12
|
+
jruby_skip('SIGINT shuts down the JVM instead of raising Interrupt')
|
|
13
|
+
|
|
14
|
+
run_in_process(<<~RUBY)
|
|
15
|
+
begin
|
|
16
|
+
CLI::UI::Prompt.confirm('question')
|
|
17
|
+
rescue Interrupt
|
|
18
|
+
puts 'sentinel'
|
|
19
|
+
end
|
|
20
|
+
RUBY
|
|
21
|
+
|
|
22
|
+
wait_for_output_to_include('question')
|
|
23
|
+
kill_process
|
|
24
|
+
|
|
25
|
+
assert_output_includes('sentinel')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# ^C is not handled; raises Interrupt, which may be handled by caller.
|
|
29
|
+
def test_ask_free_form_sigint
|
|
30
|
+
jruby_skip('SIGINT shuts down the JVM instead of raising Interrupt')
|
|
31
|
+
|
|
32
|
+
run_in_process(<<~RUBY)
|
|
33
|
+
begin
|
|
34
|
+
CLI::UI::Prompt.ask('question')
|
|
35
|
+
rescue Interrupt
|
|
36
|
+
puts 'sentinel'
|
|
37
|
+
end
|
|
38
|
+
RUBY
|
|
39
|
+
|
|
40
|
+
wait_for_output_to_include('question')
|
|
41
|
+
kill_process
|
|
42
|
+
|
|
43
|
+
assert_output_includes('sentinel')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# ^C is not handled; raises Interrupt, which may be handled by caller.
|
|
47
|
+
def test_ask_interactive_sigint
|
|
48
|
+
jruby_skip('SIGINT shuts down the JVM instead of raising Interrupt')
|
|
49
|
+
|
|
50
|
+
run_in_process(<<~RUBY)
|
|
51
|
+
begin
|
|
52
|
+
CLI::UI::Prompt.ask('question', options: %w(a b))
|
|
53
|
+
rescue Interrupt
|
|
54
|
+
puts 'sentinel'
|
|
55
|
+
end
|
|
56
|
+
RUBY
|
|
57
|
+
|
|
58
|
+
wait_for_output_to_include('question')
|
|
59
|
+
kill_process
|
|
60
|
+
|
|
61
|
+
assert_output_includes('sentinel')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_confirm_happy_path
|
|
65
|
+
run_in_process('puts CLI::UI::Prompt.confirm("q")')
|
|
66
|
+
write('y')
|
|
67
|
+
assert_output_includes('true')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_confirm_default_no
|
|
71
|
+
run_in_process('puts CLI::UI::Prompt.confirm("q", default: false)')
|
|
72
|
+
write("\n")
|
|
73
|
+
assert_output_includes('false')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_confirm_invalid
|
|
77
|
+
run_in_process('puts CLI::UI::Prompt.confirm("q")')
|
|
78
|
+
write('ryn')
|
|
79
|
+
assert_output_includes('true')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_confirm_no_match_internal
|
|
83
|
+
run_in_process('puts CLI::UI::Prompt.confirm("q", default: false)')
|
|
84
|
+
write('xn')
|
|
85
|
+
assert_output_includes('false')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_output_includes_instructions
|
|
89
|
+
run_in_process('CLI::UI::Prompt.confirm("q")')
|
|
90
|
+
write('y')
|
|
91
|
+
assert_output_includes('(Choose with ↑ ↓ ⏎)')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_windows_instructions
|
|
95
|
+
# Windows doesn't detect presses on the arrow keys when picking an option, so we don't show the instruction text
|
|
96
|
+
# for them.
|
|
97
|
+
run_in_process(<<~RUBY)
|
|
98
|
+
CLI::UI::OS # Force the file to load before redefining ::current
|
|
99
|
+
module CLI
|
|
100
|
+
module UI
|
|
101
|
+
module OS
|
|
102
|
+
def self.current
|
|
103
|
+
CLI::UI::OS::Windows
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
CLI::UI::Prompt.confirm("q")
|
|
109
|
+
RUBY
|
|
110
|
+
write('y')
|
|
111
|
+
assert_output_includes("(Navigate up with 'k' and down with 'j', press Enter to select)")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_ask_free_form_happy_path
|
|
115
|
+
run_in_process('puts "--#{CLI::UI::Prompt.ask("q")}--"')
|
|
116
|
+
write("asdf\n")
|
|
117
|
+
assert_output_includes('--asdf--')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_ask_free_form_empty_answer_allowed
|
|
121
|
+
run_in_process('puts "--#{CLI::UI::Prompt.ask("q")}--"')
|
|
122
|
+
write("\n")
|
|
123
|
+
assert_output_includes('----')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_ask_free_form_empty_answer_rejected
|
|
127
|
+
run_in_process('puts "--#{CLI::UI::Prompt.ask("q", allow_empty: false)}--"')
|
|
128
|
+
write("\nasdf\n")
|
|
129
|
+
assert_output_includes('--asdf--')
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_ask_free_form_no_filename_completion
|
|
133
|
+
run_in_process('puts "--#{CLI::UI::Prompt.ask("q")}--"')
|
|
134
|
+
write("/dev/nul\t\n")
|
|
135
|
+
assert_output_includes('--/dev/nul--')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_ask_free_form_filename_completion
|
|
139
|
+
run_in_process('puts "--#{CLI::UI::Prompt.ask("q", is_file: true)}--"')
|
|
140
|
+
write("/dev/nul\t\n")
|
|
141
|
+
assert_output_includes('--/dev/null--')
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def test_ask_free_form_default
|
|
145
|
+
run_in_process('puts "--#{CLI::UI::Prompt.ask("q", default: "asdf")}--"')
|
|
146
|
+
write("\n")
|
|
147
|
+
assert_output_includes('--asdf--')
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_ask_free_form_default_nondefault
|
|
151
|
+
run_in_process('puts "--#{CLI::UI::Prompt.ask("q", default: "asdf")}--"')
|
|
152
|
+
write("zxcv\n")
|
|
153
|
+
assert_output_includes('--zxcv--')
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def test_ask_invalid_kwargs
|
|
157
|
+
kwargsets = [
|
|
158
|
+
{ options: ['a'], default: 'a' },
|
|
159
|
+
{ options: ['a'], is_file: true },
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
kwargsets.each do |kwargs|
|
|
163
|
+
error = assert_raises(ArgumentError) { Prompt.ask('q', **kwargs) }
|
|
164
|
+
assert_equal('conflicting arguments: options provided with default or is_file', error.message)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
error = assert_raises(ArgumentError) do
|
|
168
|
+
Prompt.ask('q', default: 'a', allow_empty: false)
|
|
169
|
+
end
|
|
170
|
+
assert_equal('conflicting arguments: default enabled but allow_empty is false', error.message)
|
|
171
|
+
|
|
172
|
+
error = assert_raises(ArgumentError) do
|
|
173
|
+
Prompt.ask('q', default: 'b') {}
|
|
174
|
+
end
|
|
175
|
+
assert_equal('conflicting arguments: options provided with default or is_file', error.message)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def test_ask_interactive_conflicting_arguments
|
|
179
|
+
error = assert_raises(ArgumentError) do
|
|
180
|
+
Prompt.ask('q', options: %w(a b)) { |h| h.option('a') }
|
|
181
|
+
end
|
|
182
|
+
assert_equal('conflicting arguments: options and block given', error.message)
|
|
183
|
+
|
|
184
|
+
error = assert_raises(ArgumentError) do
|
|
185
|
+
Prompt.ask('q', options: %w(a b), multiple: true, default: %w(b c)) { |h| h.option('a') }
|
|
186
|
+
end
|
|
187
|
+
assert_equal('conflicting arguments: default should only include elements present in options', error.message)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_ask_interactive_insufficient_options
|
|
191
|
+
exception = assert_raises(ArgumentError) do
|
|
192
|
+
Prompt.ask('q', options: %w())
|
|
193
|
+
end
|
|
194
|
+
assert_equal('insufficient options', exception.message)
|
|
195
|
+
|
|
196
|
+
exception = assert_raises(ArgumentError) do
|
|
197
|
+
Prompt.ask('q') { |_h| {} }
|
|
198
|
+
end
|
|
199
|
+
assert_equal('insufficient options', exception.message)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def test_ask_interactive_with_block
|
|
203
|
+
run_in_process(<<~RUBY)
|
|
204
|
+
puts(CLI::UI::Prompt.ask('q') do |h|
|
|
205
|
+
h.option('a') { |_a| 'a was selected' }
|
|
206
|
+
h.option('b') { |_a| 'b was selected' }
|
|
207
|
+
end)
|
|
208
|
+
RUBY
|
|
209
|
+
write('1')
|
|
210
|
+
|
|
211
|
+
assert_output_includes('a was selected')
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def test_ask_interactive_with_vim_bound_arrows
|
|
215
|
+
run_in_process('puts "--#{CLI::UI::Prompt.ask("q", options: %w(a b))}--"')
|
|
216
|
+
write('j ')
|
|
217
|
+
assert_output_includes('--b--')
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def test_ask_interactive_escape
|
|
221
|
+
run_in_process(<<~RUBY)
|
|
222
|
+
begin
|
|
223
|
+
CLI::UI::Prompt.ask("q", options: %w(a b))
|
|
224
|
+
rescue Interrupt # jruby can rescue this one since we raise it rather than receiving it as a signal
|
|
225
|
+
puts 'sentinel'
|
|
226
|
+
end
|
|
227
|
+
RUBY
|
|
228
|
+
|
|
229
|
+
write("\e;")
|
|
230
|
+
assert_output_includes('sentinel')
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def test_ask_interactive_invalid_input
|
|
234
|
+
run_in_process('puts "--#{CLI::UI::Prompt.ask("q", options: %w(a b))}--"')
|
|
235
|
+
write('3nan2')
|
|
236
|
+
assert_output_includes('--b--')
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def test_ask_interactive_with_blank_option
|
|
240
|
+
run_in_process(<<~RUBY)
|
|
241
|
+
puts(CLI::UI::Prompt.ask('q') do |h|
|
|
242
|
+
h.option('a') { |_a| 'a was selected' }
|
|
243
|
+
h.option('') { |_a| 'b was selected' }
|
|
244
|
+
end)
|
|
245
|
+
RUBY
|
|
246
|
+
write('jj ')
|
|
247
|
+
assert_output_includes('a was selected')
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_ask_interactive_filter_options
|
|
251
|
+
run_in_process('puts "--#{CLI::UI::Prompt.ask("q", options: %w(abcd xyz))}--"')
|
|
252
|
+
write("fz\n")
|
|
253
|
+
assert_output_includes('--xyz--')
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def test_ask_interactive_line_selection
|
|
257
|
+
run_in_process('puts "--#{CLI::UI::Prompt.ask("q", options: (1..15).map(&:to_s))}--"')
|
|
258
|
+
write("e10\n")
|
|
259
|
+
assert_output_includes('--10--')
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def test_ask_multiple
|
|
263
|
+
run_in_process('puts CLI::UI::Prompt.ask("q", options: (1..15).map(&:to_s), multiple: true).inspect')
|
|
264
|
+
write('1350')
|
|
265
|
+
assert_output_includes(['1', '3', '5'].inspect)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def test_ask_multiple_with_handler
|
|
269
|
+
run_in_process(<<~RUBY)
|
|
270
|
+
puts(CLI::UI::Prompt.ask('q', multiple: true) do |handler|
|
|
271
|
+
('1'..'10').each do |i|
|
|
272
|
+
handler.option(i) { i }
|
|
273
|
+
end
|
|
274
|
+
end.inspect)
|
|
275
|
+
RUBY
|
|
276
|
+
write('1350')
|
|
277
|
+
assert_output_includes(['1', '3', '5'].inspect)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def test_ask_multiple_with_default_values
|
|
281
|
+
run_in_process(
|
|
282
|
+
'puts CLI::UI::Prompt.ask("q", options: (1..15).map(&:to_s), multiple: true, default: %w(2 3)).inspect'
|
|
283
|
+
)
|
|
284
|
+
write('120')
|
|
285
|
+
assert_output_includes(['1', '3'].inspect)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
private
|
|
289
|
+
|
|
290
|
+
def run_in_process(code)
|
|
291
|
+
@stdin, @stdout, @stderr, @wait_thr = Open3.popen3(
|
|
292
|
+
'ruby',
|
|
293
|
+
'-r',
|
|
294
|
+
'bundler/setup',
|
|
295
|
+
'-r',
|
|
296
|
+
'cli/ui',
|
|
297
|
+
'-e',
|
|
298
|
+
"$stdout.sync = true; $stderr.sync = true; #{code}"
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def wait_for_output_to_include(text)
|
|
303
|
+
@output = ''
|
|
304
|
+
until @output.include?(text)
|
|
305
|
+
begin
|
|
306
|
+
@output += @stdout.read_nonblock(100)
|
|
307
|
+
rescue IO::WaitReadable
|
|
308
|
+
IO.select([@stdout])
|
|
309
|
+
retry
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def write(text)
|
|
315
|
+
@stdin.write(text)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def kill_process
|
|
319
|
+
Process.kill('INT', @wait_thr[:pid])
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def clean_up
|
|
323
|
+
@wait_thr.value
|
|
324
|
+
yield if block_given?
|
|
325
|
+
ensure
|
|
326
|
+
@stdin.close
|
|
327
|
+
@stderr.close
|
|
328
|
+
@stdout.close
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def assert_output_includes(text)
|
|
332
|
+
clean_up do
|
|
333
|
+
assert_includes(@stdout.read, text)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def assert_error_includes(text)
|
|
338
|
+
clean_up do
|
|
339
|
+
assert_includes(@stderr.read, text)
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def jruby_skip(message)
|
|
344
|
+
skip(message) if RUBY_ENGINE.include?('jruby')
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|