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.
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