rfix 2.0.4 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/exe/rfix +11 -90
  3. data/lib/rfix.rb +10 -9
  4. data/lib/rfix/branch/reference.rb +2 -2
  5. data/lib/rfix/branch/upstream.rb +2 -4
  6. data/lib/rfix/cli/command.rb +14 -1
  7. data/lib/rfix/cli/command/all.rb +21 -0
  8. data/lib/rfix/cli/command/base.rb +30 -19
  9. data/lib/rfix/cli/command/branch.rb +2 -0
  10. data/lib/rfix/cli/command/help.rb +2 -0
  11. data/lib/rfix/cli/command/info.rb +6 -1
  12. data/lib/rfix/cli/command/local.rb +2 -0
  13. data/lib/rfix/cli/command/origin.rb +2 -0
  14. data/lib/rfix/cli/command/setup.rb +2 -0
  15. data/lib/rfix/cli/command/status.rb +39 -0
  16. data/lib/rfix/collector.rb +69 -0
  17. data/lib/rfix/diff.rb +69 -0
  18. data/lib/rfix/extension/comment_config.rb +15 -0
  19. data/lib/rfix/extension/offense.rb +17 -14
  20. data/lib/rfix/extension/pastel.rb +7 -4
  21. data/lib/rfix/extension/progresbar.rb +15 -0
  22. data/lib/rfix/extension/strings.rb +10 -2
  23. data/lib/rfix/file.rb +5 -3
  24. data/lib/rfix/file/base.rb +21 -14
  25. data/lib/rfix/file/deleted.rb +2 -0
  26. data/lib/rfix/file/ignored.rb +2 -0
  27. data/lib/rfix/file/null.rb +17 -0
  28. data/lib/rfix/file/tracked.rb +39 -23
  29. data/lib/rfix/file/undefined.rb +17 -0
  30. data/lib/rfix/file/untracked.rb +3 -1
  31. data/lib/rfix/formatter.rb +67 -71
  32. data/lib/rfix/highlighter.rb +1 -3
  33. data/lib/rfix/rake/gemfile.rb +26 -23
  34. data/lib/rfix/repository.rb +59 -96
  35. data/lib/rfix/types.rb +24 -14
  36. data/lib/rfix/version.rb +1 -1
  37. data/rfix.gemspec +11 -3
  38. data/vendor/cli-ui/Gemfile +17 -0
  39. data/vendor/cli-ui/Gemfile.lock +60 -0
  40. data/vendor/cli-ui/LICENSE.txt +21 -0
  41. data/vendor/cli-ui/README.md +224 -0
  42. data/vendor/cli-ui/Rakefile +20 -0
  43. data/vendor/cli-ui/bin/console +14 -0
  44. data/vendor/cli-ui/cli-ui.gemspec +25 -0
  45. data/vendor/cli-ui/dev.yml +14 -0
  46. data/vendor/cli-ui/lib/cli/ui.rb +233 -0
  47. data/vendor/cli-ui/lib/cli/ui/ansi.rb +157 -0
  48. data/vendor/cli-ui/lib/cli/ui/color.rb +84 -0
  49. data/vendor/cli-ui/lib/cli/ui/formatter.rb +192 -0
  50. data/vendor/cli-ui/lib/cli/ui/frame.rb +269 -0
  51. data/vendor/cli-ui/lib/cli/ui/frame/frame_stack.rb +98 -0
  52. data/vendor/cli-ui/lib/cli/ui/frame/frame_style.rb +120 -0
  53. data/vendor/cli-ui/lib/cli/ui/frame/frame_style/box.rb +166 -0
  54. data/vendor/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +139 -0
  55. data/vendor/cli-ui/lib/cli/ui/glyph.rb +84 -0
  56. data/vendor/cli-ui/lib/cli/ui/os.rb +67 -0
  57. data/vendor/cli-ui/lib/cli/ui/printer.rb +59 -0
  58. data/vendor/cli-ui/lib/cli/ui/progress.rb +90 -0
  59. data/vendor/cli-ui/lib/cli/ui/prompt.rb +297 -0
  60. data/vendor/cli-ui/lib/cli/ui/prompt/interactive_options.rb +484 -0
  61. data/vendor/cli-ui/lib/cli/ui/prompt/options_handler.rb +29 -0
  62. data/vendor/cli-ui/lib/cli/ui/spinner.rb +66 -0
  63. data/vendor/cli-ui/lib/cli/ui/spinner/async.rb +40 -0
  64. data/vendor/cli-ui/lib/cli/ui/spinner/spin_group.rb +263 -0
  65. data/vendor/cli-ui/lib/cli/ui/stdout_router.rb +232 -0
  66. data/vendor/cli-ui/lib/cli/ui/terminal.rb +46 -0
  67. data/vendor/cli-ui/lib/cli/ui/truncater.rb +102 -0
  68. data/vendor/cli-ui/lib/cli/ui/version.rb +5 -0
  69. data/vendor/cli-ui/lib/cli/ui/widgets.rb +77 -0
  70. data/vendor/cli-ui/lib/cli/ui/widgets/base.rb +27 -0
  71. data/vendor/cli-ui/lib/cli/ui/widgets/status.rb +61 -0
  72. data/vendor/cli-ui/lib/cli/ui/wrap.rb +56 -0
  73. data/vendor/cli-ui/test/cli/ui/ansi_test.rb +32 -0
  74. data/vendor/cli-ui/test/cli/ui/cli_ui_test.rb +23 -0
  75. data/vendor/cli-ui/test/cli/ui/color_test.rb +40 -0
  76. data/vendor/cli-ui/test/cli/ui/formatter_test.rb +79 -0
  77. data/vendor/cli-ui/test/cli/ui/glyph_test.rb +68 -0
  78. data/vendor/cli-ui/test/cli/ui/printer_test.rb +103 -0
  79. data/vendor/cli-ui/test/cli/ui/progress_test.rb +46 -0
  80. data/vendor/cli-ui/test/cli/ui/prompt/options_handler_test.rb +39 -0
  81. data/vendor/cli-ui/test/cli/ui/prompt_test.rb +348 -0
  82. data/vendor/cli-ui/test/cli/ui/spinner/spin_group_test.rb +39 -0
  83. data/vendor/cli-ui/test/cli/ui/spinner_test.rb +141 -0
  84. data/vendor/cli-ui/test/cli/ui/stdout_router_test.rb +32 -0
  85. data/vendor/cli-ui/test/cli/ui/terminal_test.rb +26 -0
  86. data/vendor/cli-ui/test/cli/ui/truncater_test.rb +31 -0
  87. data/vendor/cli-ui/test/cli/ui/widgets/status_test.rb +49 -0
  88. data/vendor/cli-ui/test/cli/ui/widgets_test.rb +15 -0
  89. data/vendor/cli-ui/test/test_helper.rb +53 -0
  90. data/vendor/cli-ui/tmp/cache/bootsnap/compile-cache/d9/c036af0f3dc494 +0 -0
  91. data/vendor/cli-ui/tmp/cache/bootsnap/load-path-cache +0 -0
  92. data/vendor/dry-cli/lib/dry/cli/command.rb +2 -1
  93. data/vendor/dry-cli/tmp/cache/bootsnap/compile-cache/ff/a22a5daafbd74c +0 -0
  94. data/vendor/dry-cli/tmp/cache/bootsnap/load-path-cache +0 -0
  95. data/vendor/strings-ansi/tmp/cache/bootsnap/compile-cache/79/49cf49407b370e +0 -0
  96. data/vendor/strings-ansi/tmp/cache/bootsnap/load-path-cache +0 -0
  97. metadata +170 -9
  98. data/lib/rfix/extension/string.rb +0 -12
  99. 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