diff options
Diffstat (limited to 'test/irb')
29 files changed, 4827 insertions, 1780 deletions
diff --git a/test/irb/command/test_custom_command.rb b/test/irb/command/test_custom_command.rb new file mode 100644 index 0000000000..3a3ad11d5a --- /dev/null +++ b/test/irb/command/test_custom_command.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true +require "irb" + +require_relative "../helper" + +module TestIRB + class CustomCommandIntegrationTest < TestIRB::IntegrationTestCase + def test_command_registration_can_happen_after_irb_require + write_ruby <<~RUBY + require "irb" + require "irb/command" + + class PrintCommand < IRB::Command::Base + category 'CommandTest' + description 'print_command' + def execute(*) + puts "Hello from PrintCommand" + end + end + + IRB::Command.register(:print!, PrintCommand) + + binding.irb + RUBY + + output = run_ruby_file do + type "print!" + type "exit" + end + + assert_include(output, "Hello from PrintCommand") + end + + def test_command_registration_accepts_string_too + write_ruby <<~RUBY + require "irb/command" + + class PrintCommand < IRB::Command::Base + category 'CommandTest' + description 'print_command' + def execute(*) + puts "Hello from PrintCommand" + end + end + + IRB::Command.register("print!", PrintCommand) + + binding.irb + RUBY + + output = run_ruby_file do + type "print!" + type "exit" + end + + assert_include(output, "Hello from PrintCommand") + end + + def test_arguments_propagation + write_ruby <<~RUBY + require "irb/command" + + class PrintArgCommand < IRB::Command::Base + category 'CommandTest' + description 'print_command_arg' + def execute(arg) + $nth_execution ||= 0 + puts "\#{$nth_execution} arg=\#{arg.inspect}" + $nth_execution += 1 + end + end + + IRB::Command.register(:print_arg, PrintArgCommand) + + binding.irb + RUBY + + output = run_ruby_file do + type "print_arg" + type "print_arg \n" + type "print_arg a r g" + type "print_arg a r g \n" + type "exit" + end + + assert_include(output, "0 arg=\"\"") + assert_include(output, "1 arg=\"\"") + assert_include(output, "2 arg=\"a r g\"") + assert_include(output, "3 arg=\"a r g\"") + end + + def test_def_extend_command_still_works + write_ruby <<~RUBY + require "irb" + + class FooBarCommand < IRB::Command::Base + category 'FooBarCategory' + description 'foobar_description' + def execute(*) + $nth_execution ||= 1 + puts "\#{$nth_execution} FooBar executed" + $nth_execution += 1 + end + end + + IRB::ExtendCommandBundle.def_extend_command(:foobar, FooBarCommand, nil, [:fbalias, IRB::Command::OVERRIDE_ALL]) + + binding.irb + RUBY + + output = run_ruby_file do + type "foobar" + type "fbalias" + type "help foobar" + type "exit" + end + + assert_include(output, "1 FooBar executed") + assert_include(output, "2 FooBar executed") + assert_include(output, "foobar_description") + end + + def test_no_meta_command_also_works + write_ruby <<~RUBY + require "irb/command" + + class NoMetaCommand < IRB::Command::Base + def execute(*) + puts "This command does not override meta attributes" + end + end + + IRB::Command.register(:no_meta, NoMetaCommand) + + binding.irb + RUBY + + output = run_ruby_file do + type "no_meta" + type "help no_meta" + type "exit" + end + + assert_include(output, "This command does not override meta attributes") + assert_include(output, "No description provided.") + assert_not_include(output, "Maybe IRB bug") + end + end +end diff --git a/test/irb/command/test_disable_irb.rb b/test/irb/command/test_disable_irb.rb new file mode 100644 index 0000000000..14a20043d5 --- /dev/null +++ b/test/irb/command/test_disable_irb.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: false +require 'irb' + +require_relative "../helper" + +module TestIRB + class DisableIRBTest < IntegrationTestCase + def test_disable_irb_disable_further_irb_breakpoints + write_ruby <<~'ruby' + puts "First line" + puts "Second line" + binding.irb + puts "Third line" + binding.irb + puts "Fourth line" + ruby + + output = run_ruby_file do + type "disable_irb" + end + + assert_match(/First line\r\n/, output) + assert_match(/Second line\r\n/, output) + assert_match(/Third line\r\n/, output) + assert_match(/Fourth line\r\n/, output) + end + end +end diff --git a/test/irb/command/test_force_exit.rb b/test/irb/command/test_force_exit.rb new file mode 100644 index 0000000000..191a786872 --- /dev/null +++ b/test/irb/command/test_force_exit.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: false +require 'irb' + +require_relative "../helper" + +module TestIRB + class ForceExitTest < IntegrationTestCase + def test_forced_exit_finishes_process_immediately + write_ruby <<~'ruby' + puts "First line" + puts "Second line" + binding.irb + puts "Third line" + binding.irb + puts "Fourth line" + ruby + + output = run_ruby_file do + type "123" + type "456" + type "exit!" + end + + assert_match(/First line\r\n/, output) + assert_match(/Second line\r\n/, output) + assert_match(/irb\(main\):001> 123/, output) + assert_match(/irb\(main\):002> 456/, output) + refute_match(/Third line\r\n/, output) + refute_match(/Fourth line\r\n/, output) + end + + def test_forced_exit_in_nested_sessions + write_ruby <<~'ruby' + def foo + binding.irb + end + + binding.irb + binding.irb + ruby + + output = run_ruby_file do + type "123" + type "foo" + type "exit!" + end + + assert_match(/irb\(main\):001> 123/, output) + end + end +end diff --git a/test/irb/command/test_help.rb b/test/irb/command/test_help.rb new file mode 100644 index 0000000000..b34832b022 --- /dev/null +++ b/test/irb/command/test_help.rb @@ -0,0 +1,75 @@ +require "tempfile" +require_relative "../helper" + +module TestIRB + class HelpTest < IntegrationTestCase + def setup + super + + write_rc <<~'RUBY' + IRB.conf[:USE_PAGER] = false + RUBY + + write_ruby <<~'RUBY' + binding.irb + RUBY + end + + def test_help + out = run_ruby_file do + type "help" + type "exit" + end + + assert_match(/List all available commands/, out) + assert_match(/Start the debugger of debug\.gem/, out) + end + + def test_command_help + out = run_ruby_file do + type "help ls" + type "exit" + end + + assert_match(/Usage: ls \[obj\]/, out) + end + + def test_command_help_not_found + out = run_ruby_file do + type "help foo" + type "exit" + end + + assert_match(/Can't find command `foo`\. Please check the command name and try again\./, out) + end + + def test_show_cmds + out = run_ruby_file do + type "help" + type "exit" + end + + assert_match(/List all available commands/, out) + assert_match(/Start the debugger of debug\.gem/, out) + end + + def test_help_lists_user_aliases + out = run_ruby_file do + type "help" + type "exit" + end + + assert_match(/\$\s+Alias for `show_source`/, out) + assert_match(/@\s+Alias for `whereami`/, out) + end + + def test_help_lists_helper_methods + out = run_ruby_file do + type "help" + type "exit" + end + + assert_match(/Helper methods\s+conf\s+Returns the current IRB context/, out) + end + end +end diff --git a/test/irb/command/test_multi_irb_commands.rb b/test/irb/command/test_multi_irb_commands.rb new file mode 100644 index 0000000000..e313c0c5d2 --- /dev/null +++ b/test/irb/command/test_multi_irb_commands.rb @@ -0,0 +1,50 @@ +require "tempfile" +require_relative "../helper" + +module TestIRB + class MultiIRBTest < IntegrationTestCase + def setup + super + + write_ruby <<~'RUBY' + binding.irb + RUBY + end + + def test_jobs_command_with_print_deprecated_warning + out = run_ruby_file do + type "jobs" + type "exit" + end + + assert_match(/Multi-irb commands are deprecated and will be removed in IRB 2\.0\.0\. Please use workspace commands instead\./, out) + assert_match(%r|If you have any use case for multi-irb, please leave a comment at https://github.com/ruby/irb/issues/653|, out) + assert_match(/#0->irb on main \(#<Thread:0x.+ run>: running\)/, out) + end + + def test_irb_jobs_and_kill_commands + out = run_ruby_file do + type "irb" + type "jobs" + type "kill 1" + type "exit" + end + + assert_match(/#0->irb on main \(#<Thread:0x.+ sleep_forever>: stop\)/, out) + assert_match(/#1->irb#1 on main \(#<Thread:0x.+ run>: running\)/, out) + end + + def test_irb_fg_jobs_and_kill_commands + out = run_ruby_file do + type "irb" + type "fg 0" + type "jobs" + type "kill 1" + type "exit" + end + + assert_match(/#0->irb on main \(#<Thread:0x.+ run>: running\)/, out) + assert_match(/#1->irb#1 on main \(#<Thread:0x.+ sleep_forever>: stop\)/, out) + end + end +end diff --git a/test/irb/command/test_show_source.rb b/test/irb/command/test_show_source.rb new file mode 100644 index 0000000000..d014c78fc4 --- /dev/null +++ b/test/irb/command/test_show_source.rb @@ -0,0 +1,397 @@ +# frozen_string_literal: false +require 'irb' + +require_relative "../helper" + +module TestIRB + class ShowSourceTest < IntegrationTestCase + def setup + super + + write_rc <<~'RUBY' + IRB.conf[:USE_PAGER] = false + RUBY + end + + def test_show_source + write_ruby <<~'RUBY' + binding.irb + RUBY + + out = run_ruby_file do + type "show_source IRB.conf" + type "exit" + end + + assert_match(%r[/irb\/init\.rb], out) + end + + def test_show_source_alias + write_ruby <<~'RUBY' + binding.irb + RUBY + + out = run_ruby_file do + type "$ IRB.conf" + type "exit" + end + + assert_match(%r[/irb\/init\.rb], out) + end + + def test_show_source_with_missing_signature + write_ruby <<~'RUBY' + binding.irb + RUBY + + out = run_ruby_file do + type "show_source foo" + type "exit" + end + + assert_match(%r[Couldn't locate a definition for foo], out) + end + + def test_show_source_with_missing_constant + write_ruby <<~'RUBY' + binding.irb + RUBY + + out = run_ruby_file do + type "show_source Foo" + type "exit" + end + + assert_match(%r[Couldn't locate a definition for Foo], out) + end + + def test_show_source_string + write_ruby <<~'RUBY' + binding.irb + RUBY + + out = run_ruby_file do + type "show_source 'IRB.conf'" + type "exit" + end + + assert_match(%r[/irb\/init\.rb], out) + end + + def test_show_source_method_s + write_ruby <<~RUBY + class Baz + def foo + end + end + + class Bar < Baz + def foo + super + end + end + + binding.irb + RUBY + + out = run_ruby_file do + type "show_source Bar#foo -s" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}:2\s+def foo\r\n end\r\n], out) + end + + def test_show_source_method_s_with_incorrect_signature + write_ruby <<~RUBY + class Baz + def foo + end + end + + class Bar < Baz + def foo + super + end + end + + binding.irb + RUBY + + out = run_ruby_file do + type "show_source Bar#fooo -s" + type "exit" + end + + assert_match(%r[Error: Couldn't locate a super definition for Bar#fooo], out) + end + + def test_show_source_private_method + write_ruby <<~RUBY + class Bar + private def foo + end + end + binding.irb + RUBY + + out = run_ruby_file do + type "show_source Bar#foo" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}:2\s+private def foo\r\n end\r\n], out) + end + + def test_show_source_private_singleton_method + write_ruby <<~RUBY + class Bar + private def foo + end + end + binding.irb + RUBY + + out = run_ruby_file do + type "bar = Bar.new" + type "show_source bar.foo" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}:2\s+private def foo\r\n end\r\n], out) + end + + def test_show_source_method_multiple_s + write_ruby <<~RUBY + class Baz + def foo + end + end + + class Bar < Baz + def foo + super + end + end + + class Bob < Bar + def foo + super + end + end + + binding.irb + RUBY + + out = run_ruby_file do + type "show_source Bob#foo -ss" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}:2\s+def foo\r\n end\r\n], out) + end + + def test_show_source_method_no_instance_method + write_ruby <<~RUBY + class Baz + end + + class Bar < Baz + def foo + super + end + end + + binding.irb + RUBY + + out = run_ruby_file do + type "show_source Bar#foo -s" + type "exit" + end + + assert_match(%r[Error: Couldn't locate a super definition for Bar#foo], out) + end + + def test_show_source_method_exceeds_super_chain + write_ruby <<~RUBY + class Baz + def foo + end + end + + class Bar < Baz + def foo + super + end + end + + binding.irb + RUBY + + out = run_ruby_file do + type "show_source Bar#foo -ss" + type "exit" + end + + assert_match(%r[Error: Couldn't locate a super definition for Bar#foo], out) + end + + def test_show_source_method_accidental_characters + write_ruby <<~'RUBY' + class Baz + def foo + end + end + + class Bar < Baz + def foo + super + end + end + + binding.irb + RUBY + + out = run_ruby_file do + type "show_source Bar#foo -sddddd" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}:2\s+def foo\r\n end], out) + end + + def test_show_source_receiver_super + write_ruby <<~RUBY + class Baz + def foo + end + end + + class Bar < Baz + def foo + super + end + end + + binding.irb + RUBY + + out = run_ruby_file do + type "bar = Bar.new" + type "show_source bar.foo -s" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}:2\s+def foo\r\n end], out) + end + + def test_show_source_with_double_colons + write_ruby <<~RUBY + class Foo + end + + class Foo + class Bar + end + end + + binding.irb + RUBY + + out = run_ruby_file do + type "show_source ::Foo" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}:1\s+class Foo\r\nend], out) + + out = run_ruby_file do + type "show_source ::Foo::Bar" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}:5\s+class Bar\r\n end], out) + end + + def test_show_source_keep_script_lines + pend unless defined?(RubyVM.keep_script_lines) + + write_ruby <<~RUBY + binding.irb + RUBY + + out = run_ruby_file do + type "def foo; end" + type "show_source foo" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}\(irb\):1\s+def foo; end], out) + end + + def test_show_source_unavailable_source + write_ruby <<~RUBY + binding.irb + RUBY + + out = run_ruby_file do + type "RubyVM.keep_script_lines = false if defined?(RubyVM.keep_script_lines)" + type "def foo; end" + type "show_source foo" + type "exit" + end + assert_match(%r[#{@ruby_file.to_path}\(irb\):2\s+Source not available], out) + end + + def test_show_source_shows_binary_source + write_ruby <<~RUBY + # io-console is an indirect dependency of irb + require "io/console" + + binding.irb + RUBY + + out = run_ruby_file do + # IO::ConsoleMode is defined in io-console gem's C extension + type "show_source IO::ConsoleMode" + type "exit" + end + + # A safeguard to make sure the test subject is actually defined + refute_match(/NameError/, out) + assert_match(%r[Defined in binary file:.+io/console], out) + end + + def test_show_source_with_constant_lookup + write_ruby <<~RUBY + X = 1 + module M + Y = 1 + Z = 2 + end + class A + Z = 1 + Array = 1 + class B + include M + Object.new.instance_eval { binding.irb } + end + end + RUBY + + out = run_ruby_file do + type "show_source X" + type "show_source Y" + type "show_source Z" + type "show_source Array" + type "exit" + end + + assert_match(%r[#{@ruby_file.to_path}:1\s+X = 1], out) + assert_match(%r[#{@ruby_file.to_path}:3\s+Y = 1], out) + assert_match(%r[#{@ruby_file.to_path}:7\s+Z = 1], out) + assert_match(%r[#{@ruby_file.to_path}:8\s+Array = 1], out) + end + end +end diff --git a/test/irb/helper.rb b/test/irb/helper.rb index 55f9e083eb..acaf6277f3 100644 --- a/test/irb/helper.rb +++ b/test/irb/helper.rb @@ -1,5 +1,6 @@ require "test/unit" require "pathname" +require "rubygems" begin require_relative "../lib/helper" @@ -7,17 +8,22 @@ begin rescue LoadError # ruby/ruby defines helpers differently end +begin + require "pty" +rescue LoadError # some platforms don't support PTY +end + module IRB class InputMethod; end end module TestIRB + RUBY_3_4 = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0.dev") class TestCase < Test::Unit::TestCase class TestInputMethod < ::IRB::InputMethod attr_reader :list, :line_no def initialize(list = []) - super("test") @line_no = 0 @list = list end @@ -73,4 +79,143 @@ module TestIRB } end end + + class IntegrationTestCase < TestCase + LIB = File.expand_path("../../lib", __dir__) + TIMEOUT_SEC = 3 + + def setup + @envs = {} + @tmpfiles = [] + + unless defined?(PTY) + omit "Integration tests require PTY." + end + + if ruby_core? + omit "This test works only under ruby/irb" + end + + write_rc <<~RUBY + IRB.conf[:USE_PAGER] = false + RUBY + end + + def teardown + @tmpfiles.each do |tmpfile| + File.unlink(tmpfile) + end + end + + def run_ruby_file(&block) + cmd = [EnvUtil.rubybin, "-I", LIB, @ruby_file.to_path] + tmp_dir = Dir.mktmpdir + + @commands = [] + lines = [] + + yield + + # Test should not depend on user's irbrc file + @envs["HOME"] ||= tmp_dir + @envs["XDG_CONFIG_HOME"] ||= tmp_dir + @envs["IRBRC"] = nil unless @envs.key?("IRBRC") + + envs_for_spawn = @envs.merge('TERM' => 'dumb', 'TEST_IRB_FORCE_INTERACTIVE' => 'true') + + PTY.spawn(envs_for_spawn, *cmd) do |read, write, pid| + Timeout.timeout(TIMEOUT_SEC) do + while line = safe_gets(read) + lines << line + + # means the breakpoint is triggered + if line.match?(/binding\.irb/) + while command = @commands.shift + write.puts(command) + end + end + end + end + ensure + read.close + write.close + kill_safely(pid) + end + + lines.join + rescue Timeout::Error + message = <<~MSG + Test timedout. + + #{'=' * 30} OUTPUT #{'=' * 30} + #{lines.map { |l| " #{l}" }.join} + #{'=' * 27} END OF OUTPUT #{'=' * 27} + MSG + assert_block(message) { false } + ensure + FileUtils.remove_entry tmp_dir + end + + # read.gets could raise exceptions on some platforms + # https://github.com/ruby/ruby/blob/master/ext/pty/pty.c#L721-L728 + def safe_gets(read) + read.gets + rescue Errno::EIO + nil + end + + def kill_safely pid + return if wait_pid pid, TIMEOUT_SEC + + Process.kill :TERM, pid + return if wait_pid pid, 0.2 + + Process.kill :KILL, pid + Process.waitpid(pid) + rescue Errno::EPERM, Errno::ESRCH + end + + def wait_pid pid, sec + total_sec = 0.0 + wait_sec = 0.001 # 1ms + + while total_sec < sec + if Process.waitpid(pid, Process::WNOHANG) == pid + return true + end + sleep wait_sec + total_sec += wait_sec + wait_sec *= 2 + end + + false + rescue Errno::ECHILD + true + end + + def type(command) + @commands << command + end + + def write_ruby(program) + @ruby_file = Tempfile.create(%w{irbtest- .rb}) + @tmpfiles << @ruby_file + @ruby_file.write(program) + @ruby_file.close + end + + def write_rc(content) + # Append irbrc content if a tempfile for it already exists + if @irbrc + @irbrc = File.open(@irbrc, "a") + else + @irbrc = Tempfile.new('irbrc') + @tmpfiles << @irbrc + end + + @irbrc.write(content) + @irbrc.close + @envs['IRBRC'] = @irbrc.path + end + end end diff --git a/test/irb/test_color.rb b/test/irb/test_color.rb index 652396c89e..9d78f5233e 100644 --- a/test/irb/test_color.rb +++ b/test/irb/test_color.rb @@ -1,12 +1,11 @@ # frozen_string_literal: false require 'irb/color' -require 'rubygems' require 'stringio' require_relative "helper" module TestIRB - class TestColor < TestCase + class ColorTest < TestCase CLEAR = "\e[0m" BOLD = "\e[1m" UNDERLINE = "\e[4m" @@ -100,7 +99,7 @@ module TestIRB "foo %i[bar]" => "foo #{YELLOW}%i[#{CLEAR}#{YELLOW}bar#{CLEAR}#{YELLOW}]#{CLEAR}", "foo :@bar, baz, :@@qux, :$quux" => "foo #{YELLOW}:#{CLEAR}#{YELLOW}@bar#{CLEAR}, baz, #{YELLOW}:#{CLEAR}#{YELLOW}@@qux#{CLEAR}, #{YELLOW}:#{CLEAR}#{YELLOW}$quux#{CLEAR}", "`echo`" => "#{RED}#{BOLD}`#{CLEAR}#{RED}echo#{CLEAR}#{RED}#{BOLD}`#{CLEAR}", - "\t" => "\t", # not ^I + "\t" => Reline::Unicode.escape_for_print("\t") == ' ' ? ' ' : "\t", # not ^I "foo(*%W(bar))" => "foo(*#{RED}#{BOLD}%W(#{CLEAR}#{RED}bar#{CLEAR}#{RED}#{BOLD})#{CLEAR})", "$stdout" => "#{GREEN}#{BOLD}$stdout#{CLEAR}", "__END__" => "#{GREEN}__END__#{CLEAR}", diff --git a/test/irb/test_color_printer.rb b/test/irb/test_color_printer.rb index a717940d81..c2c624d868 100644 --- a/test/irb/test_color_printer.rb +++ b/test/irb/test_color_printer.rb @@ -1,12 +1,11 @@ # frozen_string_literal: false require 'irb/color_printer' -require 'rubygems' require 'stringio' require_relative "helper" module TestIRB - class TestColorPrinter < TestCase + class ColorPrinterTest < TestCase CLEAR = "\e[0m" BOLD = "\e[1m" RED = "\e[31m" @@ -41,7 +40,7 @@ module TestIRB { 1 => "#{BLUE}#{BOLD}1#{CLEAR}\n", "a\nb" => %[#{RED}#{BOLD}"#{CLEAR}#{RED}a\\nb#{CLEAR}#{RED}#{BOLD}"#{CLEAR}\n], - IRBTestColorPrinter.new('test') => "#{GREEN}#<struct TestIRB::TestColorPrinter::IRBTestColorPrinter#{CLEAR} a#{GREEN}=#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}#{RED}test#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}#{GREEN}>#{CLEAR}\n", + IRBTestColorPrinter.new('test') => "#{GREEN}#<struct TestIRB::ColorPrinterTest::IRBTestColorPrinter#{CLEAR} a#{GREEN}=#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}#{RED}test#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}#{GREEN}>#{CLEAR}\n", Ripper::Lexer.new('1').scan => "[#{GREEN}#<Ripper::Lexer::Elem:#{CLEAR} on_int@1:0 END token: #{RED}#{BOLD}\"#{CLEAR}#{RED}1#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}#{GREEN}>#{CLEAR}]\n", Class.new{define_method(:pretty_print){|q| q.text("[__FILE__, __LINE__, __ENCODING__]")}}.new => "[#{CYAN}#{BOLD}__FILE__#{CLEAR}, #{CYAN}#{BOLD}__LINE__#{CLEAR}, #{CYAN}#{BOLD}__ENCODING__#{CLEAR}]\n", }.each do |object, result| diff --git a/test/irb/test_cmd.rb b/test/irb/test_command.rb index 448f42587a..8cb8928adb 100644 --- a/test/irb/test_cmd.rb +++ b/test/irb/test_command.rb @@ -1,7 +1,5 @@ # frozen_string_literal: false -require "rubygems" require "irb" -require "irb/extend-command" require_relative "helper" @@ -22,6 +20,7 @@ module TestIRB @xdg_config_home_backup = ENV.delete("XDG_CONFIG_HOME") save_encodings IRB.instance_variable_get(:@CONF).clear + IRB.instance_variable_set(:@existing_rc_name_generators, nil) @is_win = (RbConfig::CONFIG['host_os'] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/) end @@ -34,16 +33,17 @@ module TestIRB end def execute_lines(*lines, conf: {}, main: self, irb_path: nil) - IRB.init_config(nil) - IRB.conf[:VERBOSE] = false - IRB.conf[:PROMPT_MODE] = :SIMPLE - IRB.conf.merge!(conf) - input = TestInputMethod.new(lines) - irb = IRB::Irb.new(IRB::WorkSpace.new(main), input) - irb.context.return_format = "=> %s\n" - irb.context.irb_path = irb_path if irb_path - IRB.conf[:MAIN_CONTEXT] = irb.context capture_output do + IRB.init_config(nil) + IRB.conf[:VERBOSE] = false + IRB.conf[:PROMPT_MODE] = :SIMPLE + IRB.conf[:USE_PAGER] = false + IRB.conf.merge!(conf) + input = TestInputMethod.new(lines) + irb = IRB::Irb.new(IRB::WorkSpace.new(main), input) + irb.context.return_format = "=> %s\n" + irb.context.irb_path = irb_path if irb_path + IRB.conf[:MAIN_CONTEXT] = irb.context irb.eval_input end end @@ -57,28 +57,11 @@ module TestIRB "irb_info", main: main ) - assert_empty err + assert_empty(err) assert_match(/RUBY_PLATFORM/, out) end end - class CommnadAliasTest < CommandTestCase - def test_vars_with_aliases - @foo = "foo" - $bar = "bar" - out, err = execute_lines( - "@foo\n", - "$bar\n", - ) - assert_empty err - assert_match(/"foo"/, out) - assert_match(/"bar"/, out) - ensure - remove_instance_variable(:@foo) - $bar = nil - end - end - class InfoTest < CommandTestCase def setup super @@ -93,6 +76,7 @@ module TestIRB def test_irb_info_multiline FileUtils.touch("#{@tmpdir}/.inputrc") FileUtils.touch("#{@tmpdir}/.irbrc") + FileUtils.touch("#{@tmpdir}/_irbrc") out, err = execute_lines( "irb_info", @@ -103,7 +87,8 @@ module TestIRB Ruby\sversion:\s.+\n IRB\sversion:\sirb\s.+\n InputMethod:\sAbstract\sInputMethod\n - \.irbrc\spath:\s.+\n + Completion: .+\n + \.irbrc\spaths:.*\.irbrc.*_irbrc\n RUBY_PLATFORM:\s.+\n East\sAsian\sAmbiguous\sWidth:\s\d\n #{@is_win ? 'Code\spage:\s\d+\n' : ''} @@ -126,7 +111,8 @@ module TestIRB Ruby\sversion:\s.+\n IRB\sversion:\sirb\s.+\n InputMethod:\sAbstract\sInputMethod\n - \.irbrc\spath:\s.+\n + Completion: .+\n + \.irbrc\spaths:\s.+\n RUBY_PLATFORM:\s.+\n East\sAsian\sAmbiguous\sWidth:\s\d\n #{@is_win ? 'Code\spage:\s\d+\n' : ''} @@ -152,6 +138,7 @@ module TestIRB Ruby\sversion:\s.+\n IRB\sversion:\sirb\s.+\n InputMethod:\sAbstract\sInputMethod\n + Completion: .+\n RUBY_PLATFORM:\s.+\n East\sAsian\sAmbiguous\sWidth:\s\d\n #{@is_win ? 'Code\spage:\s\d+\n' : ''} @@ -181,6 +168,7 @@ module TestIRB Ruby\sversion:\s.+\n IRB\sversion:\sirb\s.+\n InputMethod:\sAbstract\sInputMethod\n + Completion: .+\n RUBY_PLATFORM:\s.+\n East\sAsian\sAmbiguous\sWidth:\s\d\n #{@is_win ? 'Code\spage:\s\d+\n' : ''} @@ -209,7 +197,8 @@ module TestIRB Ruby\sversion: .+\n IRB\sversion:\sirb .+\n InputMethod:\sAbstract\sInputMethod\n - \.irbrc\spath: .+\n + Completion: .+\n + \.irbrc\spaths: .+\n RUBY_PLATFORM: .+\n LANG\senv:\sja_JP\.UTF-8\n LC_ALL\senv:\sen_US\.UTF-8\n @@ -228,8 +217,7 @@ module TestIRB DEFAULT: { PROMPT_I: '> ', PROMPT_S: '> ', - PROMPT_C: '> ', - PROMPT_N: '> ' + PROMPT_C: '> ' } }, PROMPT_MODE: :DEFAULT, @@ -238,17 +226,47 @@ module TestIRB c = Class.new(Object) out, err = execute_lines( - "3\n", "measure\n", "3\n", "measure :off\n", "3\n", + "measure :on\n", + "3\n", + "measure :off\n", + "3\n", conf: conf, main: c ) assert_empty err - assert_match(/\A=> 3\nTIME is added\.\n=> nil\nprocessing time: .+\n=> 3\n=> nil\n=> 3\n/, out) + assert_match(/\A(TIME is added\.\n=> nil\nprocessing time: .+\n=> 3\n=> nil\n=> 3\n){2}/, out) + assert_empty(c.class_variables) + end + + def test_measure_keeps_previous_value + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: false + } + + c = Class.new(Object) + out, err = execute_lines( + "measure\n", + "3\n", + "_\n", + conf: conf, + main: c + ) + + assert_empty err + assert_match(/\ATIME is added\.\n=> nil\nprocessing time: .+\n=> 3\nprocessing time: .+\n=> 3/, out) assert_empty(c.class_variables) end @@ -258,8 +276,7 @@ module TestIRB DEFAULT: { PROMPT_I: '> ', PROMPT_S: '> ', - PROMPT_C: '> ', - PROMPT_N: '> ' + PROMPT_C: '> ' } }, PROMPT_MODE: :DEFAULT, @@ -289,8 +306,7 @@ module TestIRB DEFAULT: { PROMPT_I: '> ', PROMPT_S: '> ', - PROMPT_C: '> ', - PROMPT_N: '> ' + PROMPT_C: '> ' } }, PROMPT_MODE: :DEFAULT, @@ -320,8 +336,7 @@ module TestIRB DEFAULT: { PROMPT_I: '> ', PROMPT_S: '> ', - PROMPT_C: '> ', - PROMPT_N: '> ' + PROMPT_C: '> ' } }, PROMPT_MODE: :DEFAULT, @@ -341,42 +356,61 @@ module TestIRB assert_match(/\A=> 3\nCUSTOM is added\.\n=> nil\ncustom processing time: .+\n=> 3\n=> nil\n=> 3\n/, out) end - def test_measure_with_proc + def test_measure_toggle conf = { PROMPT: { DEFAULT: { PROMPT_I: '> ', PROMPT_S: '> ', - PROMPT_C: '> ', - PROMPT_N: '> ' + PROMPT_C: '> ' } }, PROMPT_MODE: :DEFAULT, MEASURE: false, + MEASURE_PROC: { + FOO: proc { |&block| puts 'foo'; block.call }, + BAR: proc { |&block| puts 'bar'; block.call } + } } - c = Class.new(Object) out, err = execute_lines( + "measure :foo\n", + "1\n", + "measure :on, :bar\n", + "2\n", + "measure :off, :foo\n", "3\n", - "measure { |context, code, line_no, &block|\n", - " result = block.()\n", - " puts 'aaa' if IRB.conf[:MEASURE]\n", - " result\n", - "}\n", - "3\n", - "measure { |context, code, line_no, &block|\n", - " result = block.()\n", - " puts 'bbb' if IRB.conf[:MEASURE]\n", - " result\n", - "}\n", + "measure :off, :bar\n", + "4\n", + conf: conf + ) + + assert_empty err + assert_match(/\AFOO is added\.\n=> nil\nfoo\n=> 1\nBAR is added\.\n=> nil\nbar\nfoo\n=> 2\n=> nil\nbar\n=> 3\n=> nil\n=> 4\n/, out) + end + + def test_measure_with_proc_warning + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: false, + } + c = Class.new(Object) + out, err = execute_lines( "3\n", - "measure :off\n", + "measure do\n", "3\n", conf: conf, main: c ) - assert_empty err - assert_match(/\A=> 3\nBLOCK is added\.\n=> nil\naaa\n=> 3\nBLOCK is added.\naaa\n=> nil\nbbb\n=> 3\n=> nil\n=> 3\n/, out) + assert_match(/to add custom measure/, err) + assert_match(/\A=> 3\n=> nil\n=> 3\n/, out) assert_empty(c.class_variables) end end @@ -438,99 +472,6 @@ module TestIRB end end - class ShowSourceTest < CommandTestCase - def test_show_source - out, err = execute_lines( - "show_source IRB.conf\n", - ) - assert_empty err - assert_match(%r[/irb\.rb], out) - end - - def test_show_source_method - out, err = execute_lines( - "p show_source('IRB.conf')\n", - ) - assert_empty err - assert_match(%r[/irb\.rb], out) - end - - def test_show_source_string - out, err = execute_lines( - "show_source 'IRB.conf'\n", - ) - assert_empty err - assert_match(%r[/irb\.rb], out) - end - - def test_show_source_alias - out, err = execute_lines( - "$ 'IRB.conf'\n", - conf: { COMMAND_ALIASES: { :'$' => :show_source } } - ) - assert_empty err - assert_match(%r[/irb\.rb], out) - end - - def test_show_source_end_finder - pend if RUBY_ENGINE == 'truffleruby' - eval(code = <<-EOS, binding, __FILE__, __LINE__ + 1) - def show_source_test_method - unless true - end - end unless defined?(show_source_test_method) - EOS - - out, err = execute_lines( - "show_source '#{self.class.name}#show_source_test_method'\n", - ) - - assert_empty err - assert_include(out, code) - end - - def test_show_source_private_instance - pend if RUBY_ENGINE == 'truffleruby' - eval(code = <<-EOS, binding, __FILE__, __LINE__ + 1) - class PrivateInstanceTest - private def show_source_test_method - unless true - end - end unless private_method_defined?(:show_source_test_method) - end - EOS - - out, err = execute_lines( - "show_source '#{self.class.name}::PrivateInstanceTest#show_source_test_method'\n", - ) - - assert_empty err - assert_include(out, code.lines[1..-2].join) - end - - - def test_show_source_private - pend if RUBY_ENGINE == 'truffleruby' - eval(code = <<-EOS, binding, __FILE__, __LINE__ + 1) - class PrivateTest - private def show_source_test_method - unless true - end - end unless private_method_defined?(:show_source_test_method) - end - - Instance = PrivateTest.new unless defined?(Instance) - EOS - - out, err = execute_lines( - "show_source '#{self.class.name}::Instance.show_source_test_method'\n", - ) - - assert_empty err - assert_include(out, code.lines[1..-4].join) - end - end - class WorkspaceCommandTestCase < CommandTestCase def setup super @@ -544,62 +485,67 @@ module TestIRB class CwwsTest < WorkspaceCommandTestCase def test_cwws_returns_the_current_workspace_object out, err = execute_lines( - "cwws.class", + "cwws" ) assert_empty err - assert_include(out, self.class.name) + assert_include(out, "Current workspace: #{self}") end end class PushwsTest < WorkspaceCommandTestCase def test_pushws_switches_to_new_workspace_and_pushes_the_current_one_to_the_stack out, err = execute_lines( - "pushws #{self.class}::Foo.new\n", - "cwws.class", + "pushws #{self.class}::Foo.new", + "self.class", + "popws", + "self.class" ) assert_empty err - assert_include(out, "#{self.class}::Foo") + + assert_match(/=> #{self.class}::Foo\n/, out) + assert_match(/=> #{self.class}\n$/, out) end def test_pushws_extends_the_new_workspace_with_command_bundle out, err = execute_lines( - "pushws Object.new\n", + "pushws Object.new", "self.singleton_class.ancestors" ) assert_empty err assert_include(out, "IRB::ExtendCommandBundle") end - def test_pushws_prints_help_message_when_no_arg_is_given + def test_pushws_prints_workspace_stack_when_no_arg_is_given out, err = execute_lines( - "pushws\n", + "pushws", ) assert_empty err - assert_match(/No other workspace/, out) + assert_include(out, "[#<TestIRB::PushwsTe...>]") end - end - class WorkspacesTest < WorkspaceCommandTestCase - def test_workspaces_returns_the_array_of_non_main_workspaces + def test_pushws_without_argument_swaps_the_top_two_workspaces out, err = execute_lines( - "pushws #{self.class}::Foo.new\n", - "workspaces.map { |w| w.class.name }", + "pushws #{self.class}::Foo.new", + "self.class", + "pushws", + "self.class" ) - assert_empty err - # self.class::Foo would be the current workspace - # self.class would be the old workspace that's pushed to the stack - assert_include(out, "=> [\"#{self.class}\"]") + assert_match(/=> #{self.class}::Foo\n/, out) + assert_match(/=> #{self.class}\n$/, out) end + end - def test_workspaces_returns_empty_array_when_no_workspaces_were_added + class WorkspacesTest < WorkspaceCommandTestCase + def test_workspaces_returns_the_stack_of_workspaces out, err = execute_lines( - "workspaces.map(&:to_s)", + "pushws #{self.class}::Foo.new\n", + "workspaces", ) assert_empty err - assert_include(out, "=> []") + assert_match(/\[#<TestIRB::Workspac...>, #<TestIRB::Workspac...>\]\n/, out) end end @@ -608,7 +554,8 @@ module TestIRB out, err = execute_lines( "pushws Foo.new\n", "popws\n", - "cwws.class", + "cwws\n", + "self.class", ) assert_empty err assert_include(out, "=> #{self.class}") @@ -619,7 +566,7 @@ module TestIRB "popws\n", ) assert_empty err - assert_match(/workspace stack empty/, out) + assert_match(/\[#<TestIRB::PopwsTes...>\]\n/, out) end end @@ -627,19 +574,20 @@ module TestIRB def test_chws_replaces_the_current_workspace out, err = execute_lines( "chws #{self.class}::Foo.new\n", - "cwws.class", + "cwws\n", + "self.class\n" ) assert_empty err + assert_include(out, "Current workspace: #<#{self.class.name}::Foo") assert_include(out, "=> #{self.class}::Foo") end def test_chws_does_nothing_when_receiving_no_argument out, err = execute_lines( "chws\n", - "cwws.class", ) assert_empty err - assert_include(out, "=> #{self.class}") + assert_include(out, "Current workspace: #{self}") end end @@ -661,19 +609,6 @@ module TestIRB end end - - class ShowCmdsTest < CommandTestCase - def test_show_cmds - out, err = execute_lines( - "show_cmds\n" - ) - - assert_empty err - assert_match(/List all available commands and their description/, out) - assert_match(/Start the debugger of debug\.gem/, out) - end - end - class LsTest < CommandTestCase def test_ls out, err = execute_lines( @@ -797,18 +732,18 @@ module TestIRB def test_ls_grep_empty out, err = execute_lines("ls\n") assert_empty err - assert_match(/whereami/, out) - assert_match(/show_source/, out) + assert_match(/assert/, out) + assert_match(/refute/, out) [ - "ls grep: /whereami/\n", - "ls -g whereami\n", - "ls -G whereami\n", + "ls grep: /assert/\n", + "ls -g assert\n", + "ls -G assert\n", ].each do |line| out, err = execute_lines(line) assert_empty err - assert_match(/whereami/, out) - assert_not_match(/show_source/, out) + assert_match(/assert/, out) + assert_not_match(/refute/, out) end end @@ -824,22 +759,6 @@ module TestIRB end class ShowDocTest < CommandTestCase - def test_help - out, err = execute_lines( - "help String#gsub\n", - "\n", - ) - - # the former is what we'd get without document content installed, like on CI - # the latter is what we may get locally - possible_rdoc_output = [/Nothing known about String#gsub/, /gsub\(pattern\)/] - assert_include err, "[Deprecation] The `help` command will be repurposed to display command help in the future.\n" - assert(possible_rdoc_output.any? { |output| output.match?(out) }, "Expect the `help` command to match one of the possible outputs. Got:\n#{out}") - ensure - # this is the only way to reset the redefined method without coupling the test with its implementation - EnvUtil.suppress_warning { load "irb/cmd/help.rb" } - end - def test_show_doc out, err = execute_lines( "show_doc String#gsub\n", @@ -853,7 +772,7 @@ module TestIRB assert(possible_rdoc_output.any? { |output| output.match?(out) }, "Expect the `show_doc` command to match one of the possible outputs. Got:\n#{out}") ensure # this is the only way to reset the redefined method without coupling the test with its implementation - EnvUtil.suppress_warning { load "irb/cmd/help.rb" } + EnvUtil.suppress_warning { load "irb/command/help.rb" } end def test_show_doc_without_rdoc @@ -869,18 +788,21 @@ module TestIRB assert_include(err, "Can't display document because `rdoc` is not installed.\n") ensure # this is the only way to reset the redefined method without coupling the test with its implementation - EnvUtil.suppress_warning { load "irb/cmd/help.rb" } + EnvUtil.suppress_warning { load "irb/command/help.rb" } end end class EditTest < CommandTestCase def setup + @original_visual = ENV["VISUAL"] @original_editor = ENV["EDITOR"] # noop the command so nothing gets executed - ENV["EDITOR"] = ": code" + ENV["VISUAL"] = ": code" + ENV["EDITOR"] = ": code2" end def teardown + ENV["VISUAL"] = @original_visual ENV["EDITOR"] = @original_editor end @@ -895,6 +817,16 @@ module TestIRB assert_match("command: ': code'", out) end + def test_edit_without_arg_and_non_existing_irb_path + out, err = execute_lines( + "edit", + irb_path: '/path/to/file.rb(irb)' + ) + + assert_empty err + assert_match(/Can not find file: \/path\/to\/file\.rb\(irb\)/, out) + end + def test_edit_with_path out, err = execute_lines( "edit #{__FILE__}" @@ -943,5 +875,97 @@ module TestIRB assert_match(/path: .*\/lib\/irb\.rb/, out) assert_match("command: ': code'", out) end + + def test_edit_with_editor_env_var + ENV.delete("VISUAL") + + out, err = execute_lines( + "edit", + irb_path: __FILE__ + ) + + assert_empty err + assert_match("path: #{__FILE__}", out) + assert_match("command: ': code2'", out) + end + end + + class HistoryCmdTest < CommandTestCase + def teardown + TestInputMethod.send(:remove_const, "HISTORY") if defined?(TestInputMethod::HISTORY) + super + end + + def test_history + TestInputMethod.const_set("HISTORY", %w[foo bar baz]) + + out, err = without_rdoc do + execute_lines("history") + end + + assert_include(out, <<~EOF) + 2: baz + 1: bar + 0: foo + EOF + assert_empty err + end + + def test_multiline_history_with_truncation + TestInputMethod.const_set("HISTORY", ["foo", "bar", <<~INPUT]) + [].each do |x| + puts x + end + INPUT + + out, err = without_rdoc do + execute_lines("hist") + end + + assert_include(out, <<~EOF) + 2: [].each do |x| + puts x + ... + 1: bar + 0: foo + EOF + assert_empty err + end + + def test_history_grep + TestInputMethod.const_set("HISTORY", ["foo", "bar", <<~INPUT]) + [].each do |x| + puts x + end + INPUT + + out, err = without_rdoc do + execute_lines("hist -g each\n") + end + + assert_include(out, <<~EOF) + 2: [].each do |x| + puts x + ... + EOF + assert_empty err + end + + end + + class HelperMethodInsallTest < CommandTestCase + def test_helper_method_install + IRB::ExtendCommandBundle.module_eval do + def foobar + "test_helper_method_foobar" + end + end + + out, err = execute_lines("foobar.upcase") + assert_empty err + assert_include(out, '=> "TEST_HELPER_METHOD_FOOBAR"') + ensure + IRB::ExtendCommandBundle.remove_method :foobar + end end end diff --git a/test/irb/test_completion.rb b/test/irb/test_completion.rb index e259428d76..5fe7952b3d 100644 --- a/test/irb/test_completion.rb +++ b/test/irb/test_completion.rb @@ -5,77 +5,90 @@ require "irb" require_relative "helper" module TestIRB - class TestCompletion < TestCase - def setup - # make sure require completion candidates are not cached - IRB::InputCompletor.class_variable_set(:@@files_from_load_path, nil) + class CompletionTest < TestCase + def completion_candidates(target, bind) + IRB::RegexpCompletor.new.completion_candidates('', target, '', bind: bind) + end + + def doc_namespace(target, bind) + IRB::RegexpCompletor.new.doc_namespace('', target, '', bind: bind) + end + + class CommandCompletionTest < CompletionTest + def test_command_completion + assert_include(IRB::RegexpCompletor.new.completion_candidates('', 'show_s', '', bind: binding), 'show_source') + assert_not_include(IRB::RegexpCompletor.new.completion_candidates(';', 'show_s', '', bind: binding), 'show_source') + end end - class TestMethodCompletion < TestCompletion + class MethodCompletionTest < CompletionTest def test_complete_string - assert_include(IRB::InputCompletor.retrieve_completion_data("'foo'.up", bind: binding), "'foo'.upcase") + assert_include(completion_candidates("'foo'.up", binding), "'foo'.upcase") # completing 'foo bar'.up - assert_include(IRB::InputCompletor.retrieve_completion_data("bar'.up", bind: binding), "bar'.upcase") - assert_equal("String.upcase", IRB::InputCompletor.retrieve_completion_data("'foo'.upcase", bind: binding, doc_namespace: true)) + assert_include(completion_candidates("bar'.up", binding), "bar'.upcase") + assert_equal("String.upcase", doc_namespace("'foo'.upcase", binding)) end def test_complete_regexp - assert_include(IRB::InputCompletor.retrieve_completion_data("/foo/.ma", bind: binding), "/foo/.match") + assert_include(completion_candidates("/foo/.ma", binding), "/foo/.match") # completing /foo bar/.ma - assert_include(IRB::InputCompletor.retrieve_completion_data("bar/.ma", bind: binding), "bar/.match") - assert_equal("Regexp.match", IRB::InputCompletor.retrieve_completion_data("/foo/.match", bind: binding, doc_namespace: true)) + assert_include(completion_candidates("bar/.ma", binding), "bar/.match") + assert_equal("Regexp.match", doc_namespace("/foo/.match", binding)) end def test_complete_array - assert_include(IRB::InputCompletor.retrieve_completion_data("[].an", bind: binding), "[].any?") - assert_equal("Array.any?", IRB::InputCompletor.retrieve_completion_data("[].any?", bind: binding, doc_namespace: true)) + assert_include(completion_candidates("[].an", binding), "[].any?") + assert_equal("Array.any?", doc_namespace("[].any?", binding)) end def test_complete_hash_and_proc # hash - assert_include(IRB::InputCompletor.retrieve_completion_data("{}.an", bind: binding), "{}.any?") - assert_equal(["Proc.any?", "Hash.any?"], IRB::InputCompletor.retrieve_completion_data("{}.any?", bind: binding, doc_namespace: true)) + assert_include(completion_candidates("{}.an", binding), "{}.any?") + assert_equal(["Hash.any?", "Proc.any?"], doc_namespace("{}.any?", binding)) # proc - assert_include(IRB::InputCompletor.retrieve_completion_data("{}.bin", bind: binding), "{}.binding") - assert_equal(["Proc.binding", "Hash.binding"], IRB::InputCompletor.retrieve_completion_data("{}.binding", bind: binding, doc_namespace: true)) + assert_include(completion_candidates("{}.bin", binding), "{}.binding") + assert_equal(["Hash.binding", "Proc.binding"], doc_namespace("{}.binding", binding)) end def test_complete_numeric - assert_include(IRB::InputCompletor.retrieve_completion_data("1.positi", bind: binding), "1.positive?") - assert_equal("Integer.positive?", IRB::InputCompletor.retrieve_completion_data("1.positive?", bind: binding, doc_namespace: true)) + assert_include(completion_candidates("1.positi", binding), "1.positive?") + assert_equal("Integer.positive?", doc_namespace("1.positive?", binding)) - assert_include(IRB::InputCompletor.retrieve_completion_data("1r.positi", bind: binding), "1r.positive?") - assert_equal("Rational.positive?", IRB::InputCompletor.retrieve_completion_data("1r.positive?", bind: binding, doc_namespace: true)) + assert_include(completion_candidates("1r.positi", binding), "1r.positive?") + assert_equal("Rational.positive?", doc_namespace("1r.positive?", binding)) - assert_include(IRB::InputCompletor.retrieve_completion_data("0xFFFF.positi", bind: binding), "0xFFFF.positive?") - assert_equal("Integer.positive?", IRB::InputCompletor.retrieve_completion_data("0xFFFF.positive?", bind: binding, doc_namespace: true)) + assert_include(completion_candidates("0xFFFF.positi", binding), "0xFFFF.positive?") + assert_equal("Integer.positive?", doc_namespace("0xFFFF.positive?", binding)) - assert_empty(IRB::InputCompletor.retrieve_completion_data("1i.positi", bind: binding)) + assert_empty(completion_candidates("1i.positi", binding)) end def test_complete_symbol - assert_include(IRB::InputCompletor.retrieve_completion_data(":foo.to_p", bind: binding), ":foo.to_proc") - assert_equal("Symbol.to_proc", IRB::InputCompletor.retrieve_completion_data(":foo.to_proc", bind: binding, doc_namespace: true)) + assert_include(completion_candidates(":foo.to_p", binding), ":foo.to_proc") + assert_equal("Symbol.to_proc", doc_namespace(":foo.to_proc", binding)) end def test_complete_class - assert_include(IRB::InputCompletor.retrieve_completion_data("String.ne", bind: binding), "String.new") - assert_equal("String.new", IRB::InputCompletor.retrieve_completion_data("String.new", bind: binding, doc_namespace: true)) + assert_include(completion_candidates("String.ne", binding), "String.new") + assert_equal("String.new", doc_namespace("String.new", binding)) end end - class TestRequireComepletion < TestCompletion + class RequireComepletionTest < CompletionTest def test_complete_require - candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "") + candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'irb", "", bind: binding) %w['irb/init 'irb/ruby-lex].each do |word| assert_include candidates, word end # Test cache - candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "") + candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'irb", "", bind: binding) %w['irb/init 'irb/ruby-lex].each do |word| assert_include candidates, word end + # Test string completion not disturbed by require completion + candidates = IRB::RegexpCompletor.new.completion_candidates("'string ", "'.", "", bind: binding) + assert_include candidates, "'.upcase" end def test_complete_require_with_pathname_in_load_path @@ -84,7 +97,7 @@ module TestIRB test_path = Pathname.new(temp_dir) $LOAD_PATH << test_path - candidates = IRB::InputCompletor::CompletionProc.("'foo", "require ", "") + candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'foo", "", bind: binding) assert_include candidates, "'foo" ensure $LOAD_PATH.pop if test_path @@ -98,7 +111,7 @@ module TestIRB object.define_singleton_method(:to_s) { temp_dir } $LOAD_PATH << object - candidates = IRB::InputCompletor::CompletionProc.("'foo", "require ", "") + candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'foo", "", bind: binding) assert_include candidates, "'foo" ensure $LOAD_PATH.pop if object @@ -111,27 +124,28 @@ module TestIRB $LOAD_PATH << object assert_nothing_raised do - IRB::InputCompletor::CompletionProc.("'foo", "require ", "") + IRB::RegexpCompletor.new.completion_candidates("require ", "'foo", "", bind: binding) end ensure $LOAD_PATH.pop if object end def test_complete_require_library_name_first - candidates = IRB::InputCompletor::CompletionProc.("'csv", "require ", "") - assert_equal "'csv", candidates.first + # Test that library name is completed first with subdirectories + candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'irb", "", bind: binding) + assert_equal "'irb", candidates.first end def test_complete_require_relative candidates = Dir.chdir(__dir__ + "/../..") do - IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "") + IRB::RegexpCompletor.new.completion_candidates("require_relative ", "'lib/irb", "", bind: binding) end %w['lib/irb/init 'lib/irb/ruby-lex].each do |word| assert_include candidates, word end # Test cache candidates = Dir.chdir(__dir__ + "/../..") do - IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "") + IRB::RegexpCompletor.new.completion_candidates("require_relative ", "'lib/irb", "", bind: binding) end %w['lib/irb/init 'lib/irb/ruby-lex].each do |word| assert_include candidates, word @@ -139,7 +153,7 @@ module TestIRB end end - class TestVariableCompletion < TestCompletion + class VariableCompletionTest < CompletionTest def test_complete_variable # Bug fix issues https://github.com/ruby/irb/issues/368 # Variables other than `str_example` and `@str_example` are defined to ensure that irb completion does not cause unintended behavior @@ -160,13 +174,13 @@ module TestIRB local_variables.clear instance_variables.clear - assert_include(IRB::InputCompletor.retrieve_completion_data("str_examp", bind: binding), "str_example") - assert_equal("String", IRB::InputCompletor.retrieve_completion_data("str_example", bind: binding, doc_namespace: true)) - assert_equal("String.to_s", IRB::InputCompletor.retrieve_completion_data("str_example.to_s", bind: binding, doc_namespace: true)) + assert_include(completion_candidates("str_examp", binding), "str_example") + assert_equal("String", doc_namespace("str_example", binding)) + assert_equal("String.to_s", doc_namespace("str_example.to_s", binding)) - assert_include(IRB::InputCompletor.retrieve_completion_data("@str_examp", bind: binding), "@str_example") - assert_equal("String", IRB::InputCompletor.retrieve_completion_data("@str_example", bind: binding, doc_namespace: true)) - assert_equal("String.to_s", IRB::InputCompletor.retrieve_completion_data("@str_example.to_s", bind: binding, doc_namespace: true)) + assert_include(completion_candidates("@str_examp", binding), "@str_example") + assert_equal("String", doc_namespace("@str_example", binding)) + assert_equal("String.to_s", doc_namespace("@str_example.to_s", binding)) end def test_complete_sort_variables @@ -176,12 +190,12 @@ module TestIRB xzy_1.clear xzy2.clear - candidates = IRB::InputCompletor.retrieve_completion_data("xz", bind: binding, doc_namespace: false) + candidates = completion_candidates("xz", binding) assert_equal(%w[xzy xzy2 xzy_1], candidates) end end - class TestConstantCompletion < TestCompletion + class ConstantCompletionTest < CompletionTest class Foo B3 = 1 B1 = 1 @@ -189,136 +203,56 @@ module TestIRB end def test_complete_constants - assert_equal(["Foo"], IRB::InputCompletor.retrieve_completion_data("Fo", bind: binding)) - assert_equal(["Foo::B1", "Foo::B2", "Foo::B3"], IRB::InputCompletor.retrieve_completion_data("Foo::B", bind: binding)) - assert_equal(["Foo::B1.positive?"], IRB::InputCompletor.retrieve_completion_data("Foo::B1.pos", bind: binding)) + assert_equal(["Foo"], completion_candidates("Fo", binding)) + assert_equal(["Foo::B1", "Foo::B2", "Foo::B3"], completion_candidates("Foo::B", binding)) + assert_equal(["Foo::B1.positive?"], completion_candidates("Foo::B1.pos", binding)) - assert_equal(["::Forwardable"], IRB::InputCompletor.retrieve_completion_data("::Fo", bind: binding)) - assert_equal("Forwardable", IRB::InputCompletor.retrieve_completion_data("::Forwardable", bind: binding, doc_namespace: true)) + assert_equal(["::Forwardable"], completion_candidates("::Fo", binding)) + assert_equal("Forwardable", doc_namespace("::Forwardable", binding)) end end - class TestPerfectMatching < TestCompletion - def setup - # trigger PerfectMatchedProc to set up RDocRIDriver constant - IRB::InputCompletor::PerfectMatchedProc.("foo", bind: binding) - - @original_use_stdout = IRB::InputCompletor::RDocRIDriver.use_stdout - # force the driver to use stdout so it doesn't start a pager and interrupt tests - IRB::InputCompletor::RDocRIDriver.use_stdout = true - end - - def teardown - IRB::InputCompletor::RDocRIDriver.use_stdout = @original_use_stdout - end - - def test_perfectly_matched_namespace_triggers_document_display - omit unless has_rdoc_content? - - out, err = capture_output do - IRB::InputCompletor::PerfectMatchedProc.("String", bind: binding) - end - - assert_empty(err) - - assert_include(out, " S\bSt\btr\bri\bin\bng\bg") - end - - def test_perfectly_matched_multiple_namespaces_triggers_document_display - result = nil - out, err = capture_output do - result = IRB::InputCompletor::PerfectMatchedProc.("{}.nil?", bind: binding) - end - - assert_empty(err) - - # check if there're rdoc contents (e.g. CI doesn't generate them) - if has_rdoc_content? - # if there's rdoc content, we can verify by checking stdout - # rdoc generates control characters for formatting method names - assert_include(out, "P\bPr\bro\boc\bc.\b.n\bni\bil\bl?\b?") # Proc.nil? - assert_include(out, "H\bHa\bas\bsh\bh.\b.n\bni\bil\bl?\b?") # Hash.nil? - else - # this is a hacky way to verify the rdoc rendering code path because CI doesn't have rdoc content - # if there are multiple namespaces to be rendered, PerfectMatchedProc renders the result with a document - # which always returns the bytes rendered, even if it's 0 - assert_equal(0, result) - end - end - - def test_not_matched_namespace_triggers_nothing - result = nil - out, err = capture_output do - result = IRB::InputCompletor::PerfectMatchedProc.("Stri", bind: binding) - end - - assert_empty(err) - assert_empty(out) - assert_nil(result) - end - - def test_perfect_matching_stops_without_rdoc - result = nil - - out, err = capture_output do - without_rdoc do - result = IRB::InputCompletor::PerfectMatchedProc.("String", bind: binding) - end - end - - assert_empty(err) - assert_not_match(/from ruby core/, out) - assert_nil(result) - end - - def test_perfect_matching_handles_nil_namespace - out, err = capture_output do - # symbol literal has `nil` doc namespace so it's a good test subject - assert_nil(IRB::InputCompletor::PerfectMatchedProc.(":aiueo", bind: binding)) - end - - assert_empty(err) - assert_empty(out) - end - - private - - def has_rdoc_content? - File.exist?(RDoc::RI::Paths::BASE) - end + def test_not_completing_empty_string + assert_equal([], completion_candidates("", binding)) + assert_equal([], completion_candidates(" ", binding)) + assert_equal([], completion_candidates("\t", binding)) + assert_equal(nil, doc_namespace("", binding)) end def test_complete_symbol - %w"UTF-16LE UTF-7".each do |enc| + symbols = %w"UTF-16LE UTF-7".map do |enc| "K".force_encoding(enc).to_sym rescue end - _ = :aiueo - assert_include(IRB::InputCompletor.retrieve_completion_data(":a", bind: binding), ":aiueo") - assert_empty(IRB::InputCompletor.retrieve_completion_data(":irb_unknown_symbol_abcdefg", bind: binding)) + symbols += [:aiueo, :"aiu eo"] + candidates = completion_candidates(":a", binding) + assert_include(candidates, ":aiueo") + assert_not_include(candidates, ":aiu eo") + assert_empty(completion_candidates(":irb_unknown_symbol_abcdefg", binding)) # Do not complete empty symbol for performance reason - assert_empty(IRB::InputCompletor.retrieve_completion_data(":", bind: binding)) + assert_empty(completion_candidates(":", binding)) end def test_complete_invalid_three_colons - assert_empty(IRB::InputCompletor.retrieve_completion_data(":::A", bind: binding)) - assert_empty(IRB::InputCompletor.retrieve_completion_data(":::", bind: binding)) + assert_empty(completion_candidates(":::A", binding)) + assert_empty(completion_candidates(":::", binding)) end def test_complete_absolute_constants_with_special_characters - assert_empty(IRB::InputCompletor.retrieve_completion_data("::A:", bind: binding)) - assert_empty(IRB::InputCompletor.retrieve_completion_data("::A.", bind: binding)) - assert_empty(IRB::InputCompletor.retrieve_completion_data("::A(", bind: binding)) - assert_empty(IRB::InputCompletor.retrieve_completion_data("::A)", bind: binding)) + assert_empty(completion_candidates("::A:", binding)) + assert_empty(completion_candidates("::A.", binding)) + assert_empty(completion_candidates("::A(", binding)) + assert_empty(completion_candidates("::A)", binding)) + assert_empty(completion_candidates("::A[", binding)) end def test_complete_reserved_words - candidates = IRB::InputCompletor.retrieve_completion_data("de", bind: binding) + candidates = completion_candidates("de", binding) %w[def defined?].each do |word| assert_include candidates, word end - candidates = IRB::InputCompletor.retrieve_completion_data("__", bind: binding) + candidates = completion_candidates("__", binding) %w[__ENCODING__ __LINE__ __FILE__].each do |word| assert_include candidates, word end @@ -339,11 +273,39 @@ module TestIRB } bind = obj.instance_exec { binding } - assert_include(IRB::InputCompletor.retrieve_completion_data("public_hog", bind: bind), "public_hoge") - assert_include(IRB::InputCompletor.retrieve_completion_data("public_hoge", bind: bind, doc_namespace: true), "public_hoge") + assert_include(completion_candidates("public_hog", bind), "public_hoge") + assert_include(doc_namespace("public_hoge", bind), "public_hoge") + + assert_include(completion_candidates("private_hog", bind), "private_hoge") + assert_include(doc_namespace("private_hoge", bind), "private_hoge") + end + end + + class DeprecatedInputCompletorTest < TestCase + def setup + save_encodings + @verbose, $VERBOSE = $VERBOSE, nil + IRB.init_config(nil) + IRB.conf[:VERBOSE] = false + IRB.conf[:MAIN_CONTEXT] = IRB::Context.new(IRB::WorkSpace.new(binding)) + end + + def teardown + restore_encodings + $VERBOSE = @verbose + end + + def test_completion_proc + assert_include(IRB::InputCompletor::CompletionProc.call('1.ab'), '1.abs') + assert_include(IRB::InputCompletor::CompletionProc.call('1.ab', '', ''), '1.abs') + end - assert_include(IRB::InputCompletor.retrieve_completion_data("private_hog", bind: bind), "private_hoge") - assert_include(IRB::InputCompletor.retrieve_completion_data("private_hoge", bind: bind, doc_namespace: true), "private_hoge") + def test_retrieve_completion_data + assert_include(IRB::InputCompletor.retrieve_completion_data('1.ab'), '1.abs') + assert_equal(IRB::InputCompletor.retrieve_completion_data('1.abs', doc_namespace: true), 'Integer.abs') + bind = eval('a = 1; binding') + assert_include(IRB::InputCompletor.retrieve_completion_data('a.ab', bind: bind), 'a.abs') + assert_equal(IRB::InputCompletor.retrieve_completion_data('a.abs', bind: bind, doc_namespace: true), 'Integer.abs') end end end diff --git a/test/irb/test_context.rb b/test/irb/test_context.rb index c4ca56aba6..cd3f2c8f62 100644 --- a/test/irb/test_context.rb +++ b/test/irb/test_context.rb @@ -1,16 +1,16 @@ # frozen_string_literal: false require 'tempfile' require 'irb' -require 'rubygems' if defined?(Gem) require_relative "helper" module TestIRB - class TestContext < TestCase + class ContextTest < TestCase def setup IRB.init_config(nil) IRB.conf[:USE_SINGLELINE] = false IRB.conf[:VERBOSE] = false + IRB.conf[:USE_PAGER] = false workspace = IRB::WorkSpace.new(Object.new) @context = IRB::Context.new(nil, workspace, TestInputMethod.new) @@ -28,41 +28,6 @@ module TestIRB restore_encodings end - def test_last_value - assert_nil(@context.last_value) - assert_nil(@context.evaluate('_', 1)) - obj = Object.new - @context.set_last_value(obj) - assert_same(obj, @context.last_value) - assert_same(obj, @context.evaluate('_', 1)) - end - - def test_evaluate_with_exception - assert_nil(@context.evaluate("$!", 1)) - e = assert_raise_with_message(RuntimeError, 'foo') { - @context.evaluate("raise 'foo'", 1) - } - assert_equal('foo', e.message) - assert_same(e, @context.evaluate('$!', 1, exception: e)) - e = assert_raise(SyntaxError) { - @context.evaluate("1,2,3", 1, exception: e) - } - assert_match(/\A\(irb\):1:/, e.message) - assert_not_match(/rescue _\.class/, e.message) - end - - def test_evaluate_with_encoding_error_without_lineno - assert_raise_with_message(EncodingError, /invalid symbol/) { - @context.evaluate(%q[:"\xAE"], 1) - # The backtrace of this invalid encoding hash doesn't contain lineno. - } - end - - def test_evaluate_still_emits_warning - assert_warning("(irb):1: warning: END in method; use at_exit\n") do - @context.evaluate(%q[def foo; END {}; end], 1) - end - end def test_eval_input verbose, $VERBOSE = $VERBOSE, nil @@ -77,11 +42,27 @@ module TestIRB irb.eval_input end assert_empty err - assert_pattern_list([:*, /\(irb\):1:in `<main>': Foo \(RuntimeError\)\n/, - :*, /#<RuntimeError: Foo>\n/, - :*, /0$/, - :*, /0$/, - /\s*/], out) + + expected_output = + if RUBY_3_4 + [ + :*, /\(irb\):1:in '<main>': Foo \(RuntimeError\)\n/, + :*, /#<RuntimeError: Foo>\n/, + :*, /0$/, + :*, /0$/, + /\s*/ + ] + else + [ + :*, /\(irb\):1:in `<main>': Foo \(RuntimeError\)\n/, + :*, /#<RuntimeError: Foo>\n/, + :*, /0$/, + :*, /0$/, + /\s*/ + ] + end + + assert_pattern_list(expected_output, out) ensure $VERBOSE = verbose end @@ -97,11 +78,33 @@ module TestIRB irb.eval_input end assert_empty err - assert_pattern_list([ - :*, /\(irb\):1:in `<main>': Foo \(RuntimeError\)\n/, - :*, /\(irb\):2:in `<main>': Bar \(RuntimeError\)\n/, - :*, /#<RuntimeError: Bar>\n/, - ], out) + expected_output = + if RUBY_3_4 + [ + :*, /\(irb\):1:in '<main>': Foo \(RuntimeError\)\n/, + :*, /\(irb\):2:in '<main>': Bar \(RuntimeError\)\n/, + :*, /#<RuntimeError: Bar>\n/, + ] + else + [ + :*, /\(irb\):1:in `<main>': Foo \(RuntimeError\)\n/, + :*, /\(irb\):2:in `<main>': Bar \(RuntimeError\)\n/, + :*, /#<RuntimeError: Bar>\n/, + ] + end + assert_pattern_list(expected_output, out) + end + + def test_prompt_n_deprecation + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), TestInputMethod.new) + + _, err = capture_output do + irb.context.prompt_n = "foo" + irb.context.prompt_n + end + + assert_include err, "IRB::Context#prompt_n is deprecated" + assert_include err, "IRB::Context#prompt_n= is deprecated" end def test_output_to_pipe @@ -127,9 +130,9 @@ module TestIRB [:marshal, "123", Marshal.dump(123)], ], failed: [ - [false, "BasicObject.new", /#<NoMethodError: undefined method `to_s' for/], - [:p, "class Foo; undef inspect ;end; Foo.new", /#<NoMethodError: undefined method `inspect' for/], - [:yaml, "BasicObject.new", /#<NoMethodError: undefined method `inspect' for/], + [false, "BasicObject.new", /#<NoMethodError: undefined method (`|')to_s' for/], + [:p, "class Foo; undef inspect ;end; Foo.new", /#<NoMethodError: undefined method (`|')inspect' for/], + [:yaml, "BasicObject.new", /#<NoMethodError: undefined method (`|')inspect' for/], [:marshal, "[Object.new, Class.new]", /#<TypeError: can't dump anonymous class #<Class:/] ] }.each do |scenario, cases| @@ -207,7 +210,7 @@ module TestIRB end assert_empty err assert_match(/An error occurred when inspecting the object: #<RuntimeError: foo>/, out) - assert_match(/An error occurred when running Kernel#inspect: #<NoMethodError: undefined method `inspect' for/, out) + assert_match(/An error occurred when running Kernel#inspect: #<NoMethodError: undefined method (`|')inspect' for/, out) ensure $VERBOSE = verbose end @@ -216,59 +219,6 @@ module TestIRB assert_equal(true, @context.use_autocomplete?) end - def test_assignment_expression - input = TestInputMethod.new - irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) - [ - "foo = bar", - "@foo = bar", - "$foo = bar", - "@@foo = bar", - "::Foo = bar", - "a::Foo = bar", - "Foo = bar", - "foo.bar = 1", - "foo[1] = bar", - "foo += bar", - "foo -= bar", - "foo ||= bar", - "foo &&= bar", - "foo, bar = 1, 2", - "foo.bar=(1)", - "foo; foo = bar", - "foo; foo = bar; ;\n ;", - "foo\nfoo = bar", - ].each do |exp| - assert( - irb.assignment_expression?(exp), - "#{exp.inspect}: should be an assignment expression" - ) - end - - [ - "foo", - "foo.bar", - "foo[0]", - "foo = bar; foo", - "foo = bar\nfoo", - ].each do |exp| - refute( - irb.assignment_expression?(exp), - "#{exp.inspect}: should not be an assignment expression" - ) - end - end - - def test_assignment_expression_with_local_variable - input = TestInputMethod.new - irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) - code = "a /1;x=1#/" - refute(irb.assignment_expression?(code), "#{code}: should not be an assignment expression") - irb.context.workspace.binding.eval('a = 1') - assert(irb.assignment_expression?(code), "#{code}: should be an assignment expression") - refute(irb.assignment_expression?(""), "empty code should not be an assignment expression") - end - def test_echo_on_assignment input = TestInputMethod.new([ "a = 1\n", @@ -403,7 +353,7 @@ module TestIRB end assert_empty err assert_equal("=> \n#{value}\n", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) input.reset irb.context.echo = true @@ -413,7 +363,7 @@ module TestIRB end assert_empty err assert_equal("=> #{value_first_line[0..(input.winsize.last - 9)]}...\n=> \n#{value}\n", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) input.reset irb.context.echo = true @@ -423,7 +373,7 @@ module TestIRB end assert_empty err assert_equal("=> \n#{value}\n=> \n#{value}\n", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) input.reset irb.context.echo = false @@ -433,7 +383,7 @@ module TestIRB end assert_empty err assert_equal("", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) input.reset irb.context.echo = false @@ -443,7 +393,7 @@ module TestIRB end assert_empty err assert_equal("", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) input.reset irb.context.echo = false @@ -453,7 +403,7 @@ module TestIRB end assert_empty err assert_equal("", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) end end @@ -521,7 +471,6 @@ module TestIRB def test_default_return_format IRB.conf[:PROMPT][:MY_PROMPT] = { :PROMPT_I => "%03n> ", - :PROMPT_N => "%03n> ", :PROMPT_S => "%03n> ", :PROMPT_C => "%03n> " # without :RETURN @@ -551,22 +500,30 @@ module TestIRB irb.eval_input end assert_empty err - if RUBY_VERSION < '3.0.0' && STDOUT.tty? - expected = [ - :*, /Traceback \(most recent call last\):\n/, - :*, /\t 2: from \(irb\):1:in `<main>'\n/, - :*, /\t 1: from \(irb\):1:in `hoge'\n/, - :*, /\(irb\):1:in `fuga': unhandled exception\n/, - ] - else - expected = [ - :*, /\(irb\):1:in `fuga': unhandled exception\n/, - :*, /\tfrom \(irb\):1:in `hoge'\n/, - :*, /\tfrom \(irb\):1:in `<main>'\n/, - :* - ] - end - assert_pattern_list(expected, out) + expected_output = + if RUBY_3_4 + [ + :*, /\(irb\):1:in 'fuga': unhandled exception\n/, + :*, /\tfrom \(irb\):1:in 'hoge'\n/, + :*, /\tfrom \(irb\):1:in '<main>'\n/, + :* + ] + elsif RUBY_VERSION < '3.0.0' && STDOUT.tty? + [ + :*, /Traceback \(most recent call last\):\n/, + :*, /\t 2: from \(irb\):1:in `<main>'\n/, + :*, /\t 1: from \(irb\):1:in `hoge'\n/, + :*, /\(irb\):1:in `fuga': unhandled exception\n/, + ] + else + [ + :*, /\(irb\):1:in `fuga': unhandled exception\n/, + :*, /\tfrom \(irb\):1:in `hoge'\n/, + :*, /\tfrom \(irb\):1:in `<main>'\n/, + :* + ] + end + assert_pattern_list(expected_output, out) ensure $VERBOSE = verbose end @@ -581,22 +538,31 @@ module TestIRB irb.eval_input end assert_empty err - if RUBY_VERSION < '3.0.0' && STDOUT.tty? - expected = [ - :*, /Traceback \(most recent call last\):\n/, - :*, /\t 2: from \(irb\):1:in `<main>'\n/, - :*, /\t 1: from \(irb\):1:in `hoge'\n/, - :*, /\(irb\):1:in `fuga': A\\xF3B \(RuntimeError\)\n/, - ] - else - expected = [ - :*, /\(irb\):1:in `fuga': A\\xF3B \(RuntimeError\)\n/, - :*, /\tfrom \(irb\):1:in `hoge'\n/, - :*, /\tfrom \(irb\):1:in `<main>'\n/, - :* - ] - end - assert_pattern_list(expected, out) + expected_output = + if RUBY_3_4 + [ + :*, /\(irb\):1:in 'fuga': A\\xF3B \(RuntimeError\)\n/, + :*, /\tfrom \(irb\):1:in 'hoge'\n/, + :*, /\tfrom \(irb\):1:in '<main>'\n/, + :* + ] + elsif RUBY_VERSION < '3.0.0' && STDOUT.tty? + [ + :*, /Traceback \(most recent call last\):\n/, + :*, /\t 2: from \(irb\):1:in `<main>'\n/, + :*, /\t 1: from \(irb\):1:in `hoge'\n/, + :*, /\(irb\):1:in `fuga': A\\xF3B \(RuntimeError\)\n/, + ] + else + [ + :*, /\(irb\):1:in `fuga': A\\xF3B \(RuntimeError\)\n/, + :*, /\tfrom \(irb\):1:in `hoge'\n/, + :*, /\tfrom \(irb\):1:in `<main>'\n/, + :* + ] + end + + assert_pattern_list(expected_output, out) ensure $VERBOSE = verbose end @@ -622,43 +588,43 @@ module TestIRB expected = [ :*, /Traceback \(most recent call last\):\n/, :*, /\t... \d+ levels...\n/, - :*, /\t16: from \(irb\):1:in `a4'\n/, - :*, /\t15: from \(irb\):1:in `a5'\n/, - :*, /\t14: from \(irb\):1:in `a6'\n/, - :*, /\t13: from \(irb\):1:in `a7'\n/, - :*, /\t12: from \(irb\):1:in `a8'\n/, - :*, /\t11: from \(irb\):1:in `a9'\n/, - :*, /\t10: from \(irb\):1:in `a10'\n/, - :*, /\t 9: from \(irb\):1:in `a11'\n/, - :*, /\t 8: from \(irb\):1:in `a12'\n/, - :*, /\t 7: from \(irb\):1:in `a13'\n/, - :*, /\t 6: from \(irb\):1:in `a14'\n/, - :*, /\t 5: from \(irb\):1:in `a15'\n/, - :*, /\t 4: from \(irb\):1:in `a16'\n/, - :*, /\t 3: from \(irb\):1:in `a17'\n/, - :*, /\t 2: from \(irb\):1:in `a18'\n/, - :*, /\t 1: from \(irb\):1:in `a19'\n/, - :*, /\(irb\):1:in `a20': unhandled exception\n/, + :*, /\t16: from \(irb\):1:in (`|')a4'\n/, + :*, /\t15: from \(irb\):1:in (`|')a5'\n/, + :*, /\t14: from \(irb\):1:in (`|')a6'\n/, + :*, /\t13: from \(irb\):1:in (`|')a7'\n/, + :*, /\t12: from \(irb\):1:in (`|')a8'\n/, + :*, /\t11: from \(irb\):1:in (`|')a9'\n/, + :*, /\t10: from \(irb\):1:in (`|')a10'\n/, + :*, /\t 9: from \(irb\):1:in (`|')a11'\n/, + :*, /\t 8: from \(irb\):1:in (`|')a12'\n/, + :*, /\t 7: from \(irb\):1:in (`|')a13'\n/, + :*, /\t 6: from \(irb\):1:in (`|')a14'\n/, + :*, /\t 5: from \(irb\):1:in (`|')a15'\n/, + :*, /\t 4: from \(irb\):1:in (`|')a16'\n/, + :*, /\t 3: from \(irb\):1:in (`|')a17'\n/, + :*, /\t 2: from \(irb\):1:in (`|')a18'\n/, + :*, /\t 1: from \(irb\):1:in (`|')a19'\n/, + :*, /\(irb\):1:in (`|')a20': unhandled exception\n/, ] else expected = [ - :*, /\(irb\):1:in `a20': unhandled exception\n/, - :*, /\tfrom \(irb\):1:in `a19'\n/, - :*, /\tfrom \(irb\):1:in `a18'\n/, - :*, /\tfrom \(irb\):1:in `a17'\n/, - :*, /\tfrom \(irb\):1:in `a16'\n/, - :*, /\tfrom \(irb\):1:in `a15'\n/, - :*, /\tfrom \(irb\):1:in `a14'\n/, - :*, /\tfrom \(irb\):1:in `a13'\n/, - :*, /\tfrom \(irb\):1:in `a12'\n/, - :*, /\tfrom \(irb\):1:in `a11'\n/, - :*, /\tfrom \(irb\):1:in `a10'\n/, - :*, /\tfrom \(irb\):1:in `a9'\n/, - :*, /\tfrom \(irb\):1:in `a8'\n/, - :*, /\tfrom \(irb\):1:in `a7'\n/, - :*, /\tfrom \(irb\):1:in `a6'\n/, - :*, /\tfrom \(irb\):1:in `a5'\n/, - :*, /\tfrom \(irb\):1:in `a4'\n/, + :*, /\(irb\):1:in (`|')a20': unhandled exception\n/, + :*, /\tfrom \(irb\):1:in (`|')a19'\n/, + :*, /\tfrom \(irb\):1:in (`|')a18'\n/, + :*, /\tfrom \(irb\):1:in (`|')a17'\n/, + :*, /\tfrom \(irb\):1:in (`|')a16'\n/, + :*, /\tfrom \(irb\):1:in (`|')a15'\n/, + :*, /\tfrom \(irb\):1:in (`|')a14'\n/, + :*, /\tfrom \(irb\):1:in (`|')a13'\n/, + :*, /\tfrom \(irb\):1:in (`|')a12'\n/, + :*, /\tfrom \(irb\):1:in (`|')a11'\n/, + :*, /\tfrom \(irb\):1:in (`|')a10'\n/, + :*, /\tfrom \(irb\):1:in (`|')a9'\n/, + :*, /\tfrom \(irb\):1:in (`|')a8'\n/, + :*, /\tfrom \(irb\):1:in (`|')a7'\n/, + :*, /\tfrom \(irb\):1:in (`|')a6'\n/, + :*, /\tfrom \(irb\):1:in (`|')a5'\n/, + :*, /\tfrom \(irb\):1:in (`|')a4'\n/, :*, /\t... \d+ levels...\n/, ] end @@ -670,21 +636,38 @@ module TestIRB def test_prompt_main_escape main = Struct.new(:to_s).new("main\a\t\r\n") irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new) - assert_equal("irb(main )>", irb.prompt('irb(%m)>', nil, 1, 1)) + assert_equal("irb(main )>", irb.send(:format_prompt, 'irb(%m)>', nil, 1, 1)) end def test_prompt_main_inspect_escape main = Struct.new(:inspect).new("main\\n\nmain") irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new) - assert_equal("irb(main\\n main)>", irb.prompt('irb(%M)>', nil, 1, 1)) + assert_equal("irb(main\\n main)>", irb.send(:format_prompt, 'irb(%M)>', nil, 1, 1)) end def test_prompt_main_truncate main = Struct.new(:to_s).new("a" * 100) def main.inspect; to_s.inspect; end irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new) - assert_equal('irb(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa...)>', irb.prompt('irb(%m)>', nil, 1, 1)) - assert_equal('irb("aaaaaaaaaaaaaaaaaaaaaaaaaaaa...)>', irb.prompt('irb(%M)>', nil, 1, 1)) + assert_equal('irb(aaaaaaaaaaaaaaaaaaaaaaaaaaaaa...)>', irb.send(:format_prompt, 'irb(%m)>', nil, 1, 1)) + assert_equal('irb("aaaaaaaaaaaaaaaaaaaaaaaaaaaa...)>', irb.send(:format_prompt, 'irb(%M)>', nil, 1, 1)) + end + + def test_prompt_main_raise + main = Object.new + def main.to_s; raise TypeError; end + def main.inspect; raise ArgumentError; end + irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new) + assert_equal("irb(!TypeError)>", irb.send(:format_prompt, 'irb(%m)>', nil, 1, 1)) + assert_equal("irb(!ArgumentError)>", irb.send(:format_prompt, 'irb(%M)>', nil, 1, 1)) + end + + def test_prompt_format + main = 'main' + irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new) + assert_equal('%% main %m %main %%m >', irb.send(:format_prompt, '%%%% %m %%m %%%m %%%%m %l', '>', 1, 1)) + assert_equal('42,%i, 42,%3i,042,%03i', irb.send(:format_prompt, '%i,%%i,%3i,%%3i,%03i,%%03i', nil, 42, 1)) + assert_equal('42,%n, 42,%3n,042,%03n', irb.send(:format_prompt, '%n,%%n,%3n,%%3n,%03n,%%03n', nil, 1, 42)) end def test_lineno @@ -708,6 +691,31 @@ module TestIRB ], out) end + def test_irb_path_setter + @context.irb_path = __FILE__ + assert_equal(__FILE__, @context.irb_path) + assert_equal("#{__FILE__}(irb)", @context.instance_variable_get(:@eval_path)) + @context.irb_path = 'file/does/not/exist' + assert_equal('file/does/not/exist', @context.irb_path) + assert_equal('file/does/not/exist', @context.instance_variable_get(:@eval_path)) + @context.irb_path = "#{__FILE__}(irb)" + assert_equal("#{__FILE__}(irb)", @context.irb_path) + assert_equal("#{__FILE__}(irb)", @context.instance_variable_get(:@eval_path)) + end + + def test_build_completor + verbose, $VERBOSE = $VERBOSE, nil + original_completor = IRB.conf[:COMPLETOR] + IRB.conf[:COMPLETOR] = :regexp + assert_equal 'IRB::RegexpCompletor', @context.send(:build_completor).class.name + IRB.conf[:COMPLETOR] = :unknown + assert_equal 'IRB::RegexpCompletor', @context.send(:build_completor).class.name + # :type is tested in test_type_completor.rb + ensure + $VERBOSE = verbose + IRB.conf[:COMPLETOR] = original_completor + end + private def without_colorize diff --git a/test/irb/test_debug_cmd.rb b/test/irb/test_debug_cmd.rb deleted file mode 100644 index 3bc00638dd..0000000000 --- a/test/irb/test_debug_cmd.rb +++ /dev/null @@ -1,299 +0,0 @@ -# frozen_string_literal: true - -begin - require "pty" -rescue LoadError - return -end - -require "tempfile" -require "tmpdir" - -require_relative "helper" - -module TestIRB - LIB = File.expand_path("../../lib", __dir__) - - class DebugCommandTestCase < TestCase - IRB_AND_DEBUGGER_OPTIONS = { - "NO_COLOR" => "true", "RUBY_DEBUG_HISTORY_FILE" => '' - } - - def setup - if ruby_core? - omit "This test works only under ruby/irb" - end - - if RUBY_ENGINE == 'truffleruby' - omit "This test runs with ruby/debug, which doesn't work with truffleruby" - end - end - - def test_backtrace - write_ruby <<~'RUBY' - def foo - binding.irb - end - foo - RUBY - - output = run_ruby_file do - type "backtrace" - type "q!" - end - - assert_match(/\(rdbg:irb\) backtrace/, output) - assert_match(/Object#foo at #{@ruby_file.to_path}/, output) - end - - def test_debug - write_ruby <<~'ruby' - binding.irb - puts "hello" - ruby - - output = run_ruby_file do - type "debug" - type "next" - type "continue" - end - - assert_match(/\(rdbg\) next/, output) - assert_match(/=> 2\| puts "hello"/, output) - end - - def test_next - write_ruby <<~'ruby' - binding.irb - puts "hello" - ruby - - output = run_ruby_file do - type "next" - type "continue" - end - - assert_match(/\(rdbg:irb\) next/, output) - assert_match(/=> 2\| puts "hello"/, output) - end - - def test_break - write_ruby <<~'RUBY' - binding.irb - puts "Hello" - RUBY - - output = run_ruby_file do - type "break 2" - type "continue" - type "continue" - end - - assert_match(/\(rdbg:irb\) break/, output) - assert_match(/=> 2\| puts "Hello"/, output) - end - - def test_delete - write_ruby <<~'RUBY' - binding.irb - puts "Hello" - binding.irb - puts "World" - RUBY - - output = run_ruby_file do - type "break 4" - type "continue" - type "delete 0" - type "continue" - end - - assert_match(/\(rdbg:irb\) delete/, output) - assert_match(/deleted: #0 BP - Line/, output) - end - - def test_step - write_ruby <<~'RUBY' - def foo - puts "Hello" - end - binding.irb - foo - RUBY - - output = run_ruby_file do - type "step" - type "step" - type "continue" - end - - assert_match(/\(rdbg:irb\) step/, output) - assert_match(/=> 5\| foo/, output) - assert_match(/=> 2\| puts "Hello"/, output) - end - - def test_continue - write_ruby <<~'RUBY' - binding.irb - puts "Hello" - binding.irb - puts "World" - RUBY - - output = run_ruby_file do - type "continue" - type "continue" - end - - assert_match(/\(rdbg:irb\) continue/, output) - assert_match(/=> 3: binding.irb/, output) - end - - def test_finish - write_ruby <<~'RUBY' - def foo - binding.irb - puts "Hello" - end - foo - RUBY - - output = run_ruby_file do - type "finish" - type "continue" - end - - assert_match(/\(rdbg:irb\) finish/, output) - assert_match(/=> 4\| end/, output) - end - - def test_info - write_ruby <<~'RUBY' - def foo - a = "He" + "llo" - binding.irb - end - foo - RUBY - - output = run_ruby_file do - type "info" - type "continue" - end - - assert_match(/\(rdbg:irb\) info/, output) - assert_match(/%self = main/, output) - assert_match(/a = "Hello"/, output) - end - - def test_catch - write_ruby <<~'RUBY' - binding.irb - 1 / 0 - RUBY - - output = run_ruby_file do - type "catch ZeroDivisionError" - type "continue" - type "continue" - end - - assert_match(/\(rdbg:irb\) catch/, output) - assert_match(/Stop by #0 BP - Catch "ZeroDivisionError"/, output) - end - - private - - TIMEOUT_SEC = 3 - - def run_ruby_file(&block) - cmd = [EnvUtil.rubybin, "-I", LIB, @ruby_file.to_path] - tmp_dir = Dir.mktmpdir - - @commands = [] - lines = [] - - yield - - PTY.spawn(IRB_AND_DEBUGGER_OPTIONS.merge("TERM" => "dumb"), *cmd) do |read, write, pid| - Timeout.timeout(TIMEOUT_SEC) do - while line = safe_gets(read) - lines << line - - # means the breakpoint is triggered - if line.match?(/binding\.irb/) - while command = @commands.shift - write.puts(command) - end - end - end - end - ensure - read.close - write.close - kill_safely(pid) - end - - lines.join - rescue Timeout::Error - message = <<~MSG - Test timedout. - - #{'=' * 30} OUTPUT #{'=' * 30} - #{lines.map { |l| " #{l}" }.join} - #{'=' * 27} END OF OUTPUT #{'=' * 27} - MSG - assert_block(message) { false } - ensure - File.unlink(@ruby_file) if @ruby_file - FileUtils.remove_entry tmp_dir - end - - # read.gets could raise exceptions on some platforms - # https://github.com/ruby/ruby/blob/master/ext/pty/pty.c#L729-L736 - def safe_gets(read) - read.gets - rescue Errno::EIO - nil - end - - def kill_safely pid - return if wait_pid pid, TIMEOUT_SEC - - Process.kill :TERM, pid - return if wait_pid pid, 0.2 - - Process.kill :KILL, pid - Process.waitpid(pid) - rescue Errno::EPERM, Errno::ESRCH - end - - def wait_pid pid, sec - total_sec = 0.0 - wait_sec = 0.001 # 1ms - - while total_sec < sec - if Process.waitpid(pid, Process::WNOHANG) == pid - return true - end - sleep wait_sec - total_sec += wait_sec - wait_sec *= 2 - end - - false - rescue Errno::ECHILD - true - end - - def type(command) - @commands << command - end - - def write_ruby(program) - @ruby_file = Tempfile.create(%w{irb- .rb}) - @ruby_file.write(program) - @ruby_file.close - end - end -end diff --git a/test/irb/test_debugger_integration.rb b/test/irb/test_debugger_integration.rb new file mode 100644 index 0000000000..8b1bddea17 --- /dev/null +++ b/test/irb/test_debugger_integration.rb @@ -0,0 +1,496 @@ +# frozen_string_literal: true + +require "tempfile" +require "tmpdir" + +require_relative "helper" + +module TestIRB + class DebuggerIntegrationTest < IntegrationTestCase + def setup + super + + if RUBY_ENGINE == 'truffleruby' + omit "This test runs with ruby/debug, which doesn't work with truffleruby" + end + + @envs.merge!("NO_COLOR" => "true", "RUBY_DEBUG_HISTORY_FILE" => '') + end + + def test_backtrace + write_ruby <<~'RUBY' + def foo + binding.irb + end + foo + RUBY + + output = run_ruby_file do + type "backtrace" + type "exit!" + end + + assert_match(/irb\(main\):001> backtrace/, output) + assert_match(/Object#foo at #{@ruby_file.to_path}/, output) + end + + def test_debug + write_ruby <<~'ruby' + binding.irb + puts "hello" + ruby + + output = run_ruby_file do + type "debug" + type "next" + type "continue" + end + + assert_match(/irb\(main\):001> debug/, output) + assert_match(/irb:rdbg\(main\):002> next/, output) + assert_match(/=> 2\| puts "hello"/, output) + end + + def test_debug_command_only_runs_once + write_ruby <<~'ruby' + binding.irb + ruby + + output = run_ruby_file do + type "debug" + type "debug" + type "continue" + end + + assert_match(/irb\(main\):001> debug/, output) + assert_match(/irb:rdbg\(main\):002> debug/, output) + assert_match(/IRB is already running with a debug session/, output) + end + + def test_debug_command_can_only_be_called_from_binding_irb + write_ruby <<~'ruby' + require "irb" + # trick test framework + puts "binding.irb" + IRB.start + ruby + + output = run_ruby_file do + type "debug" + type "exit" + end + + assert_include(output, "Debugging commands are only available when IRB is started with binding.irb") + end + + def test_next + write_ruby <<~'ruby' + binding.irb + puts "hello" + ruby + + output = run_ruby_file do + type "next" + type "continue" + end + + assert_match(/irb\(main\):001> next/, output) + assert_match(/=> 2\| puts "hello"/, output) + end + + def test_break + write_ruby <<~'RUBY' + binding.irb + puts "Hello" + RUBY + + output = run_ruby_file do + type "break 2" + type "continue" + type "continue" + end + + assert_match(/irb\(main\):001> break/, output) + assert_match(/=> 2\| puts "Hello"/, output) + end + + def test_delete + write_ruby <<~'RUBY' + binding.irb + puts "Hello" + binding.irb + puts "World" + RUBY + + output = run_ruby_file do + type "break 4" + type "continue" + type "delete 0" + type "continue" + end + + assert_match(/irb:rdbg\(main\):003> delete/, output) + assert_match(/deleted: #0 BP - Line/, output) + end + + def test_step + write_ruby <<~'RUBY' + def foo + puts "Hello" + end + binding.irb + foo + RUBY + + output = run_ruby_file do + type "step" + type "step" + type "continue" + end + + assert_match(/irb\(main\):001> step/, output) + assert_match(/=> 5\| foo/, output) + assert_match(/=> 2\| puts "Hello"/, output) + end + + def test_long_stepping + write_ruby <<~'RUBY' + class Foo + def foo(num) + bar(num + 10) + end + + def bar(num) + num + end + end + + binding.irb + Foo.new.foo(100) + RUBY + + output = run_ruby_file do + type "step" + type "step" + type "step" + type "step" + type "num" + type "continue" + end + + assert_match(/irb\(main\):001> step/, output) + assert_match(/irb:rdbg\(main\):002> step/, output) + assert_match(/irb:rdbg\(#<Foo:.*>\):003> step/, output) + assert_match(/irb:rdbg\(#<Foo:.*>\):004> step/, output) + assert_match(/irb:rdbg\(#<Foo:.*>\):005> num/, output) + assert_match(/=> 110/, output) + end + + def test_continue + write_ruby <<~'RUBY' + binding.irb + puts "Hello" + binding.irb + puts "World" + RUBY + + output = run_ruby_file do + type "continue" + type "continue" + end + + assert_match(/irb\(main\):001> continue/, output) + assert_match(/=> 3: binding.irb/, output) + assert_match(/irb:rdbg\(main\):002> continue/, output) + end + + def test_finish + write_ruby <<~'RUBY' + def foo + binding.irb + puts "Hello" + end + foo + RUBY + + output = run_ruby_file do + type "finish" + type "continue" + end + + assert_match(/irb\(main\):001> finish/, output) + assert_match(/=> 4\| end/, output) + end + + def test_info + write_ruby <<~'RUBY' + def foo + a = "He" + "llo" + binding.irb + end + foo + RUBY + + output = run_ruby_file do + type "info" + type "continue" + end + + assert_match(/irb\(main\):001> info/, output) + assert_match(/%self = main/, output) + assert_match(/a = "Hello"/, output) + end + + def test_catch + write_ruby <<~'RUBY' + binding.irb + 1 / 0 + RUBY + + output = run_ruby_file do + type "catch ZeroDivisionError" + type "continue" + type "continue" + end + + assert_match(/irb\(main\):001> catch/, output) + assert_match(/Stop by #0 BP - Catch "ZeroDivisionError"/, output) + end + + def test_exit + write_ruby <<~'RUBY' + binding.irb + puts "he" + "llo" + RUBY + + output = run_ruby_file do + type "debug" + type "exit" + end + + assert_match(/irb:rdbg\(main\):002>/, output) + assert_match(/hello/, output) + end + + def test_force_exit + write_ruby <<~'RUBY' + binding.irb + puts "he" + "llo" + RUBY + + output = run_ruby_file do + type "debug" + type "exit!" + end + + assert_match(/irb:rdbg\(main\):002>/, output) + assert_not_match(/hello/, output) + end + + def test_quit + write_ruby <<~'RUBY' + binding.irb + puts "he" + "llo" + RUBY + + output = run_ruby_file do + type "debug" + type "quit!" + end + + assert_match(/irb:rdbg\(main\):002>/, output) + assert_not_match(/hello/, output) + end + + def test_prompt_line_number_continues + write_ruby <<~'ruby' + binding.irb + puts "Hello" + puts "World" + ruby + + output = run_ruby_file do + type "123" + type "456" + type "next" + type "info" + type "next" + type "continue" + end + + assert_match(/irb\(main\):003> next/, output) + assert_match(/irb:rdbg\(main\):004> info/, output) + assert_match(/irb:rdbg\(main\):005> next/, output) + end + + def test_prompt_irb_name_is_kept + write_rc <<~RUBY + IRB.conf[:IRB_NAME] = "foo" + RUBY + + write_ruby <<~'ruby' + binding.irb + puts "Hello" + ruby + + output = run_ruby_file do + type "next" + type "continue" + end + + assert_match(/foo\(main\):001> next/, output) + assert_match(/foo:rdbg\(main\):002> continue/, output) + end + + def test_irb_commands_are_available_after_moving_around_with_the_debugger + write_ruby <<~'ruby' + class Foo + def bar + puts "bar" + end + end + + binding.irb + Foo.new.bar + ruby + + output = run_ruby_file do + # Due to the way IRB defines its commands, moving into the Foo instance from main is necessary for proper testing. + type "next" + type "step" + type "irb_info" + type "continue" + end + + assert_include(output, "InputMethod: RelineInputMethod") + end + + def test_help_command_is_delegated_to_the_debugger + write_ruby <<~'ruby' + binding.irb + ruby + + output = run_ruby_file do + type "debug" + type "help" + type "continue" + end + + assert_include(output, "### Frame control") + end + + def test_help_display_different_content_when_debugger_is_enabled + write_ruby <<~'ruby' + binding.irb + ruby + + output = run_ruby_file do + type "debug" + type "help" + type "continue" + end + + # IRB's commands should still be listed + assert_match(/help\s+List all available commands/, output) + # debug gem's commands should be appended at the end + assert_match(/Debugging \(from debug\.gem\)\s+### Control flow/, output) + end + + def test_input_is_evaluated_in_the_context_of_the_current_thread + write_ruby <<~'ruby' + current_thread = Thread.current + binding.irb + ruby + + output = run_ruby_file do + type "debug" + type '"Threads match: #{current_thread == Thread.current}"' + type "continue" + end + + assert_match(/irb\(main\):001> debug/, output) + assert_match(/Threads match: true/, output) + end + + def test_irb_switches_debugger_interface_if_debug_was_already_activated + write_ruby <<~'ruby' + require 'debug' + class Foo + def bar + puts "bar" + end + end + + binding.irb + Foo.new.bar + ruby + + output = run_ruby_file do + # Due to the way IRB defines its commands, moving into the Foo instance from main is necessary for proper testing. + type "next" + type "step" + type 'irb_info' + type "continue" + end + + assert_match(/irb\(main\):001> next/, output) + assert_include(output, "InputMethod: RelineInputMethod") + end + + def test_debugger_cant_be_activated_while_multi_irb_is_active + write_ruby <<~'ruby' + binding.irb + a = 1 + ruby + + output = run_ruby_file do + type "jobs" + type "next" + type "exit" + end + + assert_match(/irb\(main\):001> jobs/, output) + assert_include(output, "Can't start the debugger when IRB is running in a multi-IRB session.") + end + + def test_multi_irb_commands_are_not_available_after_activating_the_debugger + write_ruby <<~'ruby' + binding.irb + a = 1 + ruby + + output = run_ruby_file do + type "next" + type "jobs" + type "continue" + end + + assert_match(/irb\(main\):001> next/, output) + assert_include(output, "Multi-IRB commands are not available when the debugger is enabled.") + end + + def test_irb_passes_empty_input_to_debugger_to_repeat_the_last_command + write_ruby <<~'ruby' + binding.irb + puts "foo" + puts "bar" + puts "baz" + ruby + + output = run_ruby_file do + type "next" + type "" + # Test that empty input doesn't repeat expressions + type "123" + type "" + type "next" + type "" + type "" + end + + assert_include(output, "=> 2\| puts \"foo\"") + assert_include(output, "=> 3\| puts \"bar\"") + assert_include(output, "=> 4\| puts \"baz\"") + end + end +end diff --git a/test/irb/test_eval_history.rb b/test/irb/test_eval_history.rb new file mode 100644 index 0000000000..54913ceff5 --- /dev/null +++ b/test/irb/test_eval_history.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true +require "irb" + +require_relative "helper" + +module TestIRB + class EvalHistoryTest < TestCase + def setup + save_encodings + IRB.instance_variable_get(:@CONF).clear + end + + def teardown + restore_encodings + end + + def execute_lines(*lines, conf: {}, main: self, irb_path: nil) + IRB.init_config(nil) + IRB.conf[:VERBOSE] = false + IRB.conf[:PROMPT_MODE] = :SIMPLE + IRB.conf[:USE_PAGER] = false + IRB.conf.merge!(conf) + input = TestInputMethod.new(lines) + irb = IRB::Irb.new(IRB::WorkSpace.new(main), input) + irb.context.return_format = "=> %s\n" + irb.context.irb_path = irb_path if irb_path + IRB.conf[:MAIN_CONTEXT] = irb.context + capture_output do + irb.eval_input + end + end + + def test_eval_history_is_disabled_by_default + out, err = execute_lines( + "a = 1", + "__" + ) + + assert_empty(err) + assert_match(/undefined local variable or method (`|')__'/, out) + end + + def test_eval_history_can_be_retrieved_with_double_underscore + out, err = execute_lines( + "a = 1", + "__", + conf: { EVAL_HISTORY: 5 } + ) + + assert_empty(err) + assert_match("=> 1\n" + "=> 1 1\n", out) + end + + def test_eval_history_respects_given_limit + out, err = execute_lines( + "'foo'\n", + "'bar'\n", + "'baz'\n", + "'xyz'\n", + "__", + conf: { EVAL_HISTORY: 4 } + ) + + assert_empty(err) + # Because eval_history injects `__` into the history AND decide to ignore it, we only get <limit> - 1 results + assert_match("2 \"bar\"\n" + "3 \"baz\"\n" + "4 \"xyz\"\n", out) + end + end +end diff --git a/test/irb/test_evaluation.rb b/test/irb/test_evaluation.rb new file mode 100644 index 0000000000..adb69b2067 --- /dev/null +++ b/test/irb/test_evaluation.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "tempfile" + +require_relative "helper" + +module TestIRB + class EchoingTest < IntegrationTestCase + def test_irb_echos_by_default + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type "123123" + type "exit" + end + + assert_include(output, "=> 123123") + end + + def test_irb_doesnt_echo_line_with_semicolon + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type "123123;" + type "123123 ;" + type "123123; " + type <<~RUBY + if true + 123123 + end; + RUBY + type "'evaluation ends'" + type "exit" + end + + assert_include(output, "=> \"evaluation ends\"") + assert_not_include(output, "=> 123123") + end + end +end diff --git a/test/irb/test_helper_method.rb b/test/irb/test_helper_method.rb new file mode 100644 index 0000000000..291278c16a --- /dev/null +++ b/test/irb/test_helper_method.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true +require "irb" + +require_relative "helper" + +module TestIRB + class HelperMethodTestCase < TestCase + def setup + $VERBOSE = nil + @verbosity = $VERBOSE + save_encodings + IRB.instance_variable_get(:@CONF).clear + end + + def teardown + $VERBOSE = @verbosity + restore_encodings + end + + def execute_lines(*lines, conf: {}, main: self, irb_path: nil) + IRB.init_config(nil) + IRB.conf[:VERBOSE] = false + IRB.conf[:PROMPT_MODE] = :SIMPLE + IRB.conf.merge!(conf) + input = TestInputMethod.new(lines) + irb = IRB::Irb.new(IRB::WorkSpace.new(main), input) + irb.context.return_format = "=> %s\n" + irb.context.irb_path = irb_path if irb_path + IRB.conf[:MAIN_CONTEXT] = irb.context + IRB.conf[:USE_PAGER] = false + capture_output do + irb.eval_input + end + end + end + + module TestHelperMethod + class ConfTest < HelperMethodTestCase + def test_conf_returns_the_context_object + out, err = execute_lines("conf.ap_name") + + assert_empty err + assert_include out, "=> \"irb\"" + end + end + end + + class HelperMethodIntegrationTest < IntegrationTestCase + def test_arguments_propogation + write_ruby <<~RUBY + require "irb/helper_method" + + class MyHelper < IRB::HelperMethod::Base + description "This is a test helper" + + def execute( + required_arg, optional_arg = nil, *splat_arg, required_keyword_arg:, + optional_keyword_arg: nil, **double_splat_arg, &block_arg + ) + puts [required_arg, optional_arg, splat_arg, required_keyword_arg, optional_keyword_arg, double_splat_arg, block_arg.call].to_s + end + end + + IRB::HelperMethod.register(:my_helper, MyHelper) + + binding.irb + RUBY + + output = run_ruby_file do + type <<~INPUT + my_helper( + "required", "optional", "splat", required_keyword_arg: "required", + optional_keyword_arg: "optional", a: 1, b: 2 + ) { "block" } + INPUT + type "exit" + end + + assert_include(output, '["required", "optional", ["splat"], "required", "optional", {:a=>1, :b=>2}, "block"]') + end + + def test_helper_method_injection_can_happen_after_irb_require + write_ruby <<~RUBY + require "irb" + + class MyHelper < IRB::HelperMethod::Base + description "This is a test helper" + + def execute + puts "Hello from MyHelper" + end + end + + IRB::HelperMethod.register(:my_helper, MyHelper) + + binding.irb + RUBY + + output = run_ruby_file do + type "my_helper" + type "exit" + end + + assert_include(output, 'Hello from MyHelper') + end + + def test_helper_method_instances_are_memoized + write_ruby <<~RUBY + require "irb/helper_method" + + class MyHelper < IRB::HelperMethod::Base + description "This is a test helper" + + def execute(val) + @val ||= val + end + end + + IRB::HelperMethod.register(:my_helper, MyHelper) + + binding.irb + RUBY + + output = run_ruby_file do + type "my_helper(100)" + type "my_helper(200)" + type "exit" + end + + assert_include(output, '=> 100') + assert_not_include(output, '=> 200') + end + end +end diff --git a/test/irb/test_history.rb b/test/irb/test_history.rb index c74a3a2cb9..63be35fdaa 100644 --- a/test/irb/test_history.rb +++ b/test/irb/test_history.rb @@ -1,22 +1,45 @@ # frozen_string_literal: false require 'irb' -require 'irb/ext/save-history' require 'readline' +require "tempfile" require_relative "helper" +return if RUBY_PLATFORM.match?(/solaris|mswin|mingw/i) + module TestIRB - class TestHistory < TestCase + class HistoryTest < TestCase def setup - IRB.conf[:RC_NAME_GENERATOR] = nil + @original_verbose, $VERBOSE = $VERBOSE, nil + @tmpdir = Dir.mktmpdir("test_irb_history_") + @backup_home = ENV["HOME"] + @backup_xdg_config_home = ENV.delete("XDG_CONFIG_HOME") + @backup_irbrc = ENV.delete("IRBRC") + @backup_default_external = Encoding.default_external + ENV["HOME"] = @tmpdir + IRB.instance_variable_set(:@existing_rc_name_generators, nil) end def teardown - IRB.conf[:RC_NAME_GENERATOR] = nil + IRB.instance_variable_set(:@existing_rc_name_generators, nil) + ENV["HOME"] = @backup_home + ENV["XDG_CONFIG_HOME"] = @backup_xdg_config_home + ENV["IRBRC"] = @backup_irbrc + Encoding.default_external = @backup_default_external + $VERBOSE = @original_verbose + FileUtils.rm_rf(@tmpdir) + end + + class TestInputMethodWithRelineHistory < TestInputMethod + # When IRB.conf[:USE_MULTILINE] is true, IRB::RelineInputMethod uses Reline::History + HISTORY = Reline::History.new(Reline.core.config) + + include IRB::HistorySavingAbility end - class TestInputMethodWithHistory < TestInputMethod - HISTORY = Array.new + class TestInputMethodWithReadlineHistory < TestInputMethod + # When IRB.conf[:USE_MULTILINE] is false, IRB::ReadlineInputMethod uses Readline::HISTORY + HISTORY = Readline::HISTORY include IRB::HistorySavingAbility end @@ -100,10 +123,80 @@ module TestIRB INPUT end - def test_history_concurrent_use + def test_history_concurrent_use_reline + omit "Skip Editline" if /EditLine/n.match(Readline::VERSION) + IRB.conf[:SAVE_HISTORY] = 1 + history_concurrent_use_for_input_method(TestInputMethodWithRelineHistory) + end + + def test_history_concurrent_use_readline omit "Skip Editline" if /EditLine/n.match(Readline::VERSION) IRB.conf[:SAVE_HISTORY] = 1 - assert_history(<<~EXPECTED_HISTORY, <<~INITIAL_HISTORY, <<~INPUT) do |history_file| + history_concurrent_use_for_input_method(TestInputMethodWithReadlineHistory) + end + + def test_history_concurrent_use_not_present + IRB.conf[:LC_MESSAGES] = IRB::Locale.new + IRB.conf[:SAVE_HISTORY] = 1 + io = TestInputMethodWithRelineHistory.new + io.class::HISTORY.clear + io.load_history + io.class::HISTORY << 'line1' + io.class::HISTORY << 'line2' + + history_file = IRB.rc_file("_history") + assert_not_send [File, :file?, history_file] + File.write(history_file, "line0\n") + io.save_history + assert_equal(%w"line0 line1 line2", File.read(history_file).split) + end + + def test_history_different_encodings + IRB.conf[:SAVE_HISTORY] = 2 + Encoding.default_external = Encoding::US_ASCII + locale = IRB::Locale.new("C") + assert_history(<<~EXPECTED_HISTORY.encode(Encoding::US_ASCII), <<~INITIAL_HISTORY.encode(Encoding::UTF_8), <<~INPUT, locale: locale) + ???? + exit + EXPECTED_HISTORY + 😀 + INITIAL_HISTORY + exit + INPUT + end + + def test_history_does_not_raise_when_history_file_directory_does_not_exist + backup_history_file = IRB.conf[:HISTORY_FILE] + IRB.conf[:SAVE_HISTORY] = 1 + IRB.conf[:HISTORY_FILE] = "fake/fake/fake/history_file" + io = TestInputMethodWithRelineHistory.new + + assert_warn(/history file does not exist/) do + io.save_history + end + + # assert_warn reverts $VERBOSE to EnvUtil.original_verbose, which is true in some cases + # We want to keep $VERBOSE as nil until teardown is called + # TODO: check if this is an assert_warn issue + $VERBOSE = nil + ensure + IRB.conf[:HISTORY_FILE] = backup_history_file + end + + def test_no_home_no_history_file_does_not_raise_history_save + ENV['HOME'] = nil + io = TestInputMethodWithRelineHistory.new + assert_nil(IRB.rc_file('_history')) + assert_nothing_raised do + io.load_history + io.save_history + end + end + + private + + def history_concurrent_use_for_input_method(input_method) + assert_history(<<~EXPECTED_HISTORY, <<~INITIAL_HISTORY, <<~INPUT, input_method) do |history_file| exit 5 exit @@ -116,7 +209,7 @@ module TestIRB 5 exit INPUT - assert_history(<<~EXPECTED_HISTORY2, <<~INITIAL_HISTORY2, <<~INPUT2) + assert_history(<<~EXPECTED_HISTORY2, <<~INITIAL_HISTORY2, <<~INPUT2, input_method) exit EXPECTED_HISTORY2 1 @@ -131,60 +224,31 @@ module TestIRB end end - def test_history_concurrent_use_not_present - backup_home = ENV["HOME"] - backup_xdg_config_home = ENV.delete("XDG_CONFIG_HOME") - backup_irbrc = ENV.delete("IRBRC") - IRB.conf[:LC_MESSAGES] = IRB::Locale.new - IRB.conf[:SAVE_HISTORY] = 1 - Dir.mktmpdir("test_irb_history_") do |tmpdir| - ENV["HOME"] = tmpdir - io = TestInputMethodWithHistory.new - io.class::HISTORY.clear - io.load_history - io.class::HISTORY.concat(%w"line1 line2") - - history_file = IRB.rc_file("_history") - assert_not_send [File, :file?, history_file] - File.write(history_file, "line0\n") - io.save_history - assert_equal(%w"line0 line1 line2", File.read(history_file).split) - end - ensure - ENV["HOME"] = backup_home - ENV["XDG_CONFIG_HOME"] = backup_xdg_config_home - ENV["IRBRC"] = backup_irbrc - end - - private - - def assert_history(expected_history, initial_irb_history, input) - backup_verbose, $VERBOSE = $VERBOSE, nil - backup_home = ENV["HOME"] - backup_xdg_config_home = ENV.delete("XDG_CONFIG_HOME") - IRB.conf[:LC_MESSAGES] = IRB::Locale.new + def assert_history(expected_history, initial_irb_history, input, input_method = TestInputMethodWithRelineHistory, locale: IRB::Locale.new) + IRB.conf[:LC_MESSAGES] = locale actual_history = nil - Dir.mktmpdir("test_irb_history_") do |tmpdir| - ENV["HOME"] = tmpdir - File.open(IRB.rc_file("_history"), "w") do |f| - f.write(initial_irb_history) - end + history_file = IRB.rc_file("_history") + ENV["HOME"] = @tmpdir + File.open(history_file, "w") do |f| + f.write(initial_irb_history) + end - io = TestInputMethodWithHistory.new + io = input_method.new + io.class::HISTORY.clear + io.load_history + if block_given? + previous_history = [] + io.class::HISTORY.each { |line| previous_history << line } + yield history_file io.class::HISTORY.clear - io.load_history - if block_given? - history = io.class::HISTORY.dup - yield IRB.rc_file("_history") - io.class::HISTORY.replace(history) - end - io.class::HISTORY.concat(input.split) - io.save_history + previous_history.each { |line| io.class::HISTORY << line } + end + input.split.each { |line| io.class::HISTORY << line } + io.save_history - io.load_history - File.open(IRB.rc_file("_history"), "r") do |f| - actual_history = f.read - end + io.load_history + File.open(history_file, "r") do |f| + actual_history = f.read end assert_equal(expected_history, actual_history, <<~MESSAGE) expected: @@ -192,10 +256,6 @@ module TestIRB but actual: #{actual_history} MESSAGE - ensure - $VERBOSE = backup_verbose - ENV["HOME"] = backup_home - ENV["XDG_CONFIG_HOME"] = backup_xdg_config_home end def with_temp_stdio @@ -206,4 +266,226 @@ module TestIRB end end end -end if not RUBY_PLATFORM.match?(/solaris|mswin|mingw/i) + + class IRBHistoryIntegrationTest < IntegrationTestCase + def test_history_saving_with_debug + write_history "" + + write_ruby <<~'RUBY' + def foo + end + + binding.irb + + foo + RUBY + + output = run_ruby_file do + type "'irb session'" + type "next" + type "'irb:debug session'" + type "step" + type "irb_info" + type "puts Reline::HISTORY.to_a.to_s" + type "q!" + end + + assert_include(output, "InputMethod: RelineInputMethod") + # check that in-memory history is preserved across sessions + assert_include output, %q( + ["'irb session'", "next", "'irb:debug session'", "step", "irb_info", "puts Reline::HISTORY.to_a.to_s"] + ).strip + + assert_equal <<~HISTORY, @history_file.open.read + 'irb session' + next + 'irb:debug session' + step + irb_info + puts Reline::HISTORY.to_a.to_s + q! + HISTORY + end + + def test_history_saving_with_debug_without_prior_history + tmpdir = Dir.mktmpdir("test_irb_history_") + # Intentionally not creating the file so we test the reset counter logic + history_file = File.join(tmpdir, "irb_history") + + write_rc <<~RUBY + IRB.conf[:HISTORY_FILE] = "#{history_file}" + RUBY + + write_ruby <<~'RUBY' + def foo + end + + binding.irb + + foo + RUBY + + output = run_ruby_file do + type "'irb session'" + type "next" + type "'irb:debug session'" + type "step" + type "irb_info" + type "puts Reline::HISTORY.to_a.to_s" + type "q!" + end + + assert_include(output, "InputMethod: RelineInputMethod") + # check that in-memory history is preserved across sessions + assert_include output, %q( + ["'irb session'", "next", "'irb:debug session'", "step", "irb_info", "puts Reline::HISTORY.to_a.to_s"] + ).strip + + assert_equal <<~HISTORY, File.read(history_file) + 'irb session' + next + 'irb:debug session' + step + irb_info + puts Reline::HISTORY.to_a.to_s + q! + HISTORY + ensure + FileUtils.rm_rf(tmpdir) + end + + def test_history_saving_with_nested_sessions + write_history "" + + write_ruby <<~'RUBY' + def foo + binding.irb + end + + binding.irb + RUBY + + run_ruby_file do + type "'outer session'" + type "foo" + type "'inner session'" + type "exit" + type "'outer session again'" + type "exit" + end + + assert_equal <<~HISTORY, @history_file.open.read + 'outer session' + foo + 'inner session' + exit + 'outer session again' + exit + HISTORY + end + + def test_nested_history_saving_from_inner_session_with_exit! + write_history "" + + write_ruby <<~'RUBY' + def foo + binding.irb + end + + binding.irb + RUBY + + run_ruby_file do + type "'outer session'" + type "foo" + type "'inner session'" + type "exit!" + end + + assert_equal <<~HISTORY, @history_file.open.read + 'outer session' + foo + 'inner session' + exit! + HISTORY + end + + def test_nested_history_saving_from_outer_session_with_exit! + write_history "" + + write_ruby <<~'RUBY' + def foo + binding.irb + end + + binding.irb + RUBY + + run_ruby_file do + type "'outer session'" + type "foo" + type "'inner session'" + type "exit" + type "'outer session again'" + type "exit!" + end + + assert_equal <<~HISTORY, @history_file.open.read + 'outer session' + foo + 'inner session' + exit + 'outer session again' + exit! + HISTORY + end + + def test_history_saving_with_nested_sessions_and_prior_history + write_history <<~HISTORY + old_history_1 + old_history_2 + old_history_3 + HISTORY + + write_ruby <<~'RUBY' + def foo + binding.irb + end + + binding.irb + RUBY + + run_ruby_file do + type "'outer session'" + type "foo" + type "'inner session'" + type "exit" + type "'outer session again'" + type "exit" + end + + assert_equal <<~HISTORY, @history_file.open.read + old_history_1 + old_history_2 + old_history_3 + 'outer session' + foo + 'inner session' + exit + 'outer session again' + exit + HISTORY + end + + private + + def write_history(history) + @history_file = Tempfile.new('irb_history') + @history_file.write(history) + @history_file.close + write_rc <<~RUBY + IRB.conf[:HISTORY_FILE] = "#{@history_file.path}" + RUBY + end + end +end diff --git a/test/irb/test_init.rb b/test/irb/test_init.rb index d9e338da8d..c423fa112e 100644 --- a/test/irb/test_init.rb +++ b/test/irb/test_init.rb @@ -5,19 +5,24 @@ require "fileutils" require_relative "helper" module TestIRB - class TestInit < TestCase + class InitTest < TestCase def setup # IRBRC is for RVM... @backup_env = %w[HOME XDG_CONFIG_HOME IRBRC].each_with_object({}) do |env, hash| hash[env] = ENV.delete(env) end - ENV["HOME"] = @tmpdir = Dir.mktmpdir("test_irb_init_#{$$}") + ENV["HOME"] = @tmpdir = File.realpath(Dir.mktmpdir("test_irb_init_#{$$}")) + end + + def reset_rc_name_generators + IRB.instance_variable_set(:@existing_rc_name_generators, nil) end def teardown ENV.update(@backup_env) FileUtils.rm_rf(@tmpdir) IRB.conf.delete(:SCRIPT) + reset_rc_name_generators end def test_setup_with_argv_preserves_global_argv @@ -34,42 +39,80 @@ module TestIRB assert_equal orig, $0 end - def test_rc_file + def test_rc_files tmpdir = @tmpdir Dir.chdir(tmpdir) do - ENV["XDG_CONFIG_HOME"] = "#{tmpdir}/xdg" - IRB.conf[:RC_NAME_GENERATOR] = nil - assert_equal(tmpdir+"/.irb#{IRB::IRBRC_EXT}", IRB.rc_file) - assert_equal(tmpdir+"/.irb_history", IRB.rc_file("_history")) - assert_file.not_exist?(tmpdir+"/xdg") - IRB.conf[:RC_NAME_GENERATOR] = nil - FileUtils.touch(tmpdir+"/.irb#{IRB::IRBRC_EXT}") - assert_equal(tmpdir+"/.irb#{IRB::IRBRC_EXT}", IRB.rc_file) - assert_equal(tmpdir+"/.irb_history", IRB.rc_file("_history")) - assert_file.not_exist?(tmpdir+"/xdg") + home = ENV['HOME'] = "#{tmpdir}/home" + xdg_config_home = ENV['XDG_CONFIG_HOME'] = "#{tmpdir}/xdg" + reset_rc_name_generators + assert_empty(IRB.irbrc_files) + assert_equal("#{home}/.irb_history", IRB.rc_file('_history')) + FileUtils.mkdir_p(home) + FileUtils.mkdir_p("#{xdg_config_home}/irb") + FileUtils.mkdir_p("#{home}/.config/irb") + reset_rc_name_generators + assert_empty(IRB.irbrc_files) + assert_equal("#{xdg_config_home}/irb/irb_history", IRB.rc_file('_history')) + home_irbrc = "#{home}/.irbrc" + config_irbrc = "#{home}/.config/irb/irbrc" + xdg_config_irbrc = "#{xdg_config_home}/irb/irbrc" + [home_irbrc, config_irbrc, xdg_config_irbrc].each do |file| + FileUtils.touch(file) + end + current_dir_irbrcs = %w[.irbrc irbrc _irbrc $irbrc].map { |file| "#{tmpdir}/#{file}" } + current_dir_irbrcs.each { |file| FileUtils.touch(file) } + reset_rc_name_generators + assert_equal([xdg_config_irbrc, home_irbrc, *current_dir_irbrcs], IRB.irbrc_files) + assert_equal(xdg_config_irbrc.sub(/rc$/, '_history'), IRB.rc_file('_history')) + ENV['XDG_CONFIG_HOME'] = nil + reset_rc_name_generators + assert_equal([home_irbrc, config_irbrc, *current_dir_irbrcs], IRB.irbrc_files) + assert_equal(home_irbrc.sub(/rc$/, '_history'), IRB.rc_file('_history')) + ENV['XDG_CONFIG_HOME'] = '' + reset_rc_name_generators + assert_equal([home_irbrc, config_irbrc] + current_dir_irbrcs, IRB.irbrc_files) + assert_equal(home_irbrc.sub(/rc$/, '_history'), IRB.rc_file('_history')) + ENV['XDG_CONFIG_HOME'] = xdg_config_home + ENV['IRBRC'] = "#{tmpdir}/.irbrc" + reset_rc_name_generators + assert_equal([ENV['IRBRC'], xdg_config_irbrc, home_irbrc] + (current_dir_irbrcs - [ENV['IRBRC']]), IRB.irbrc_files) + assert_equal(ENV['IRBRC'] + '_history', IRB.rc_file('_history')) + ENV['IRBRC'] = ENV['HOME'] = ENV['XDG_CONFIG_HOME'] = nil + reset_rc_name_generators + assert_equal(current_dir_irbrcs, IRB.irbrc_files) + assert_nil(IRB.rc_file('_history')) end end - def test_rc_file_in_subdir + def test_duplicated_rc_files tmpdir = @tmpdir Dir.chdir(tmpdir) do - FileUtils.mkdir_p("#{tmpdir}/mydir") - Dir.chdir("#{tmpdir}/mydir") do - IRB.conf[:RC_NAME_GENERATOR] = nil - assert_equal(tmpdir+"/.irb#{IRB::IRBRC_EXT}", IRB.rc_file) - assert_equal(tmpdir+"/.irb_history", IRB.rc_file("_history")) - IRB.conf[:RC_NAME_GENERATOR] = nil - FileUtils.touch(tmpdir+"/.irb#{IRB::IRBRC_EXT}") - assert_equal(tmpdir+"/.irb#{IRB::IRBRC_EXT}", IRB.rc_file) - assert_equal(tmpdir+"/.irb_history", IRB.rc_file("_history")) + ENV['XDG_CONFIG_HOME'] = "#{ENV['HOME']}/.config" + FileUtils.mkdir_p("#{ENV['XDG_CONFIG_HOME']}/irb") + env_irbrc = ENV['IRBRC'] = "#{tmpdir}/_irbrc" + xdg_config_irbrc = "#{ENV['XDG_CONFIG_HOME']}/irb/irbrc" + home_irbrc = "#{ENV['HOME']}/.irbrc" + current_dir_irbrc = "#{tmpdir}/irbrc" + [env_irbrc, xdg_config_irbrc, home_irbrc, current_dir_irbrc].each do |file| + FileUtils.touch(file) end + reset_rc_name_generators + assert_equal([env_irbrc, xdg_config_irbrc, home_irbrc, current_dir_irbrc], IRB.irbrc_files) end end - def test_recovery_sigint + def test_sigint_restore_default pend "This test gets stuck on Solaris for unknown reason; contribution is welcome" if RUBY_PLATFORM =~ /solaris/ bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] - status = assert_in_out_err(bundle_exec + %w[-W0 -rirb -e binding.irb;loop{Process.kill("SIGINT",$$)} -- -f --], "exit\n", //, //) + # IRB should restore SIGINT handler + status = assert_in_out_err(bundle_exec + %w[-W0 -rirb -e Signal.trap("SIGINT","DEFAULT");binding.irb;loop{Process.kill("SIGINT",$$)} -- -f --], "exit\n", //, //) + Process.kill("SIGKILL", status.pid) if !status.exited? && !status.stopped? && !status.signaled? + end + + def test_sigint_restore_block + bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] + # IRB should restore SIGINT handler + status = assert_in_out_err(bundle_exec + %w[-W0 -rirb -e x=false;Signal.trap("SIGINT"){x=true};binding.irb;loop{Process.kill("SIGINT",$$);if(x);break;end} -- -f --], "exit\n", //, //) Process.kill("SIGKILL", status.pid) if !status.exited? && !status.stopped? && !status.signaled? end @@ -120,6 +163,50 @@ module TestIRB IRB.conf[:USE_AUTOCOMPLETE] = orig_use_autocomplete_conf end + def test_completor_environment_variable + orig_use_autocomplete_env = ENV['IRB_COMPLETOR'] + orig_use_autocomplete_conf = IRB.conf[:COMPLETOR] + + ENV['IRB_COMPLETOR'] = nil + IRB.setup(__FILE__) + assert_equal(:regexp, IRB.conf[:COMPLETOR]) + + ENV['IRB_COMPLETOR'] = 'regexp' + IRB.setup(__FILE__) + assert_equal(:regexp, IRB.conf[:COMPLETOR]) + + ENV['IRB_COMPLETOR'] = 'type' + IRB.setup(__FILE__) + assert_equal(:type, IRB.conf[:COMPLETOR]) + + ENV['IRB_COMPLETOR'] = 'regexp' + IRB.setup(__FILE__, argv: ['--type-completor']) + assert_equal :type, IRB.conf[:COMPLETOR] + + ENV['IRB_COMPLETOR'] = 'type' + IRB.setup(__FILE__, argv: ['--regexp-completor']) + assert_equal :regexp, IRB.conf[:COMPLETOR] + ensure + ENV['IRB_COMPLETOR'] = orig_use_autocomplete_env + IRB.conf[:COMPLETOR] = orig_use_autocomplete_conf + end + + def test_completor_setup_with_argv + orig_completor_conf = IRB.conf[:COMPLETOR] + + # Default is :regexp + IRB.setup(__FILE__, argv: []) + assert_equal :regexp, IRB.conf[:COMPLETOR] + + IRB.setup(__FILE__, argv: ['--type-completor']) + assert_equal :type, IRB.conf[:COMPLETOR] + + IRB.setup(__FILE__, argv: ['--regexp-completor']) + assert_equal :regexp, IRB.conf[:COMPLETOR] + ensure + IRB.conf[:COMPLETOR] = orig_completor_conf + end + def test_noscript argv = %w[--noscript -- -f] IRB.setup(eval("__FILE__"), argv: argv) @@ -164,6 +251,12 @@ module TestIRB assert_equal(['-f'], argv) end + def test_option_tracer + argv = %w[--tracer] + IRB.setup(eval("__FILE__"), argv: argv) + assert_equal(true, IRB.conf[:USE_TRACER]) + end + private def with_argv(argv) @@ -174,4 +267,120 @@ module TestIRB ARGV.replace(orig) end end + + class ConfigValidationTest < TestCase + def setup + @original_home = ENV["HOME"] + @original_irbrc = ENV["IRBRC"] + # To prevent the test from using the user's .irbrc file + ENV["HOME"] = @home = Dir.mktmpdir + super + end + + def teardown + super + ENV["IRBRC"] = @original_irbrc + ENV["HOME"] = @original_home + File.unlink(@irbrc) + Dir.rmdir(@home) + IRB.instance_variable_set(:@existing_rc_name_generators, nil) + end + + def test_irb_name_converts_non_string_values_to_string + assert_no_irb_validation_error(<<~'RUBY') + IRB.conf[:IRB_NAME] = :foo + RUBY + + assert_equal "foo", IRB.conf[:IRB_NAME] + end + + def test_irb_rc_name_only_takes_callable_objects + assert_irb_validation_error(<<~'RUBY', "IRB.conf[:IRB_RC] should be a callable object. Got :foo.") + IRB.conf[:IRB_RC] = :foo + RUBY + end + + def test_back_trace_limit_only_accepts_integers + assert_irb_validation_error(<<~'RUBY', "IRB.conf[:BACK_TRACE_LIMIT] should be an integer. Got \"foo\".") + IRB.conf[:BACK_TRACE_LIMIT] = "foo" + RUBY + end + + def test_prompt_only_accepts_hash + assert_irb_validation_error(<<~'RUBY', "IRB.conf[:PROMPT] should be a Hash. Got \"foo\".") + IRB.conf[:PROMPT] = "foo" + RUBY + end + + def test_eval_history_only_accepts_integers + assert_irb_validation_error(<<~'RUBY', "IRB.conf[:EVAL_HISTORY] should be an integer. Got \"foo\".") + IRB.conf[:EVAL_HISTORY] = "foo" + RUBY + end + + private + + def assert_irb_validation_error(rc_content, error_message) + write_rc rc_content + + assert_raise_with_message(TypeError, error_message) do + IRB.setup(__FILE__) + end + end + + def assert_no_irb_validation_error(rc_content) + write_rc rc_content + + assert_nothing_raised do + IRB.setup(__FILE__) + end + end + + def write_rc(content) + @irbrc = Tempfile.new('irbrc') + @irbrc.write(content) + @irbrc.close + ENV['IRBRC'] = @irbrc.path + end + end + + class InitIntegrationTest < IntegrationTestCase + def setup + super + + write_ruby <<~'RUBY' + binding.irb + RUBY + end + + def test_load_error_in_rc_file_is_warned + write_rc <<~'IRBRC' + require "file_that_does_not_exist" + IRBRC + + output = run_ruby_file do + type "'foobar'" + type "exit" + end + + # IRB session should still be started + assert_includes output, "foobar" + assert_includes output, 'cannot load such file -- file_that_does_not_exist (LoadError)' + end + + def test_normal_errors_in_rc_file_is_warned + write_rc <<~'IRBRC' + raise "I'm an error" + IRBRC + + output = run_ruby_file do + type "'foobar'" + type "exit" + end + + # IRB session should still be started + assert_includes output, "foobar" + assert_includes output, 'I\'m an error (RuntimeError)' + end + end end diff --git a/test/irb/test_input_method.rb b/test/irb/test_input_method.rb index fdfb566390..ce317b4b32 100644 --- a/test/irb/test_input_method.rb +++ b/test/irb/test_input_method.rb @@ -1,11 +1,11 @@ # frozen_string_literal: false require "irb" - +require "rdoc" require_relative "helper" module TestIRB - class TestRelineInputMethod < TestCase + class InputMethodTest < TestCase def setup @conf_backup = IRB.conf.dup IRB.conf[:LC_MESSAGES] = IRB::Locale.new @@ -15,16 +15,22 @@ module TestIRB def teardown IRB.conf.replace(@conf_backup) restore_encodings + # Reset Reline configuration overridden by RelineInputMethod. + Reline.instance_variable_set(:@core, nil) end + end + class RelineInputMethodTest < InputMethodTest def test_initialization - IRB::RelineInputMethod.new + Reline.completion_proc = nil + Reline.dig_perfect_match_proc = nil + IRB::RelineInputMethod.new(IRB::RegexpCompletor.new) assert_nil Reline.completion_append_character assert_equal '', Reline.completer_quote_characters - assert_equal IRB::InputCompletor::BASIC_WORD_BREAK_CHARACTERS, Reline.basic_word_break_characters - assert_equal IRB::InputCompletor::CompletionProc, Reline.completion_proc - assert_equal IRB::InputCompletor::PerfectMatchedProc, Reline.dig_perfect_match_proc + assert_equal IRB::InputMethod::BASIC_WORD_BREAK_CHARACTERS, Reline.basic_word_break_characters + assert_not_nil Reline.completion_proc + assert_not_nil Reline.dig_perfect_match_proc end def test_initialization_without_use_autocomplete @@ -34,7 +40,7 @@ module TestIRB IRB.conf[:USE_AUTOCOMPLETE] = false - IRB::RelineInputMethod.new + IRB::RelineInputMethod.new(IRB::RegexpCompletor.new) refute Reline.autocompletion assert_equal empty_proc, Reline.dialog_proc(:show_doc).dialog_proc @@ -49,10 +55,10 @@ module TestIRB IRB.conf[:USE_AUTOCOMPLETE] = true - IRB::RelineInputMethod.new + IRB::RelineInputMethod.new(IRB::RegexpCompletor.new) assert Reline.autocompletion - assert_equal IRB::RelineInputMethod::SHOW_DOC_DIALOG, Reline.dialog_proc(:show_doc).dialog_proc + assert_not_equal empty_proc, Reline.dialog_proc(:show_doc).dialog_proc ensure Reline.add_dialog_proc(:show_doc, original_show_doc_proc, Reline::DEFAULT_DIALOG_CONTEXT) end @@ -65,7 +71,7 @@ module TestIRB IRB.conf[:USE_AUTOCOMPLETE] = true without_rdoc do - IRB::RelineInputMethod.new + IRB::RelineInputMethod.new(IRB::RegexpCompletor.new) end assert Reline.autocompletion @@ -75,5 +81,93 @@ module TestIRB Reline.add_dialog_proc(:show_doc, original_show_doc_proc, Reline::DEFAULT_DIALOG_CONTEXT) end end -end + class DisplayDocumentTest < InputMethodTest + def setup + super + @driver = RDoc::RI::Driver.new(use_stdout: true) + end + + def display_document(target, bind, driver = nil) + input_method = IRB::RelineInputMethod.new(IRB::RegexpCompletor.new) + input_method.instance_variable_set(:@rdoc_ri_driver, driver) if driver + input_method.instance_variable_set(:@completion_params, ['', target, '', bind]) + input_method.display_document(target) + end + + def test_perfectly_matched_namespace_triggers_document_display + omit unless has_rdoc_content? + + out, err = capture_output do + display_document("String", binding, @driver) + end + + assert_empty(err) + + assert_include(out, " S\bSt\btr\bri\bin\bng\bg") + end + + def test_perfectly_matched_multiple_namespaces_triggers_document_display + result = nil + out, err = capture_output do + result = display_document("{}.nil?", binding, @driver) + end + + assert_empty(err) + + # check if there're rdoc contents (e.g. CI doesn't generate them) + if has_rdoc_content? + # if there's rdoc content, we can verify by checking stdout + # rdoc generates control characters for formatting method names + assert_include(out, "P\bPr\bro\boc\bc.\b.n\bni\bil\bl?\b?") # Proc.nil? + assert_include(out, "H\bHa\bas\bsh\bh.\b.n\bni\bil\bl?\b?") # Hash.nil? + else + # this is a hacky way to verify the rdoc rendering code path because CI doesn't have rdoc content + # if there are multiple namespaces to be rendered, PerfectMatchedProc renders the result with a document + # which always returns the bytes rendered, even if it's 0 + assert_equal(0, result) + end + end + + def test_not_matched_namespace_triggers_nothing + result = nil + out, err = capture_output do + result = display_document("Stri", binding, @driver) + end + + assert_empty(err) + assert_empty(out) + assert_nil(result) + end + + def test_perfect_matching_stops_without_rdoc + result = nil + + out, err = capture_output do + without_rdoc do + result = display_document("String", binding) + end + end + + assert_empty(err) + assert_not_match(/from ruby core/, out) + assert_nil(result) + end + + def test_perfect_matching_handles_nil_namespace + out, err = capture_output do + # symbol literal has `nil` doc namespace so it's a good test subject + assert_nil(display_document(":aiueo", binding, @driver)) + end + + assert_empty(err) + assert_empty(out) + end + + private + + def has_rdoc_content? + File.exist?(RDoc::RI::Paths::BASE) + end + end +end diff --git a/test/irb/test_irb.rb b/test/irb/test_irb.rb new file mode 100644 index 0000000000..ece7902909 --- /dev/null +++ b/test/irb/test_irb.rb @@ -0,0 +1,924 @@ +# frozen_string_literal: true +require "irb" + +require_relative "helper" + +module TestIRB + class InputTest < IntegrationTestCase + def test_symbol_aliases_are_handled_correctly + write_ruby <<~'RUBY' + class Foo + end + binding.irb + RUBY + + output = run_ruby_file do + type "$ Foo" + type "exit!" + end + + assert_include output, "From: #{@ruby_file.path}:1" + end + + def test_symbol_aliases_are_handled_correctly_with_singleline_mode + write_rc <<~RUBY + IRB.conf[:USE_SINGLELINE] = true + RUBY + + write_ruby <<~'RUBY' + class Foo + end + binding.irb + RUBY + + output = run_ruby_file do + type "irb_info" + type "$ Foo" + type "exit!" + end + + # Make sure it's tested in singleline mode + assert_include output, "InputMethod: ReadlineInputMethod" + assert_include output, "From: #{@ruby_file.path}:1" + end + + def test_underscore_stores_last_result + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type "1 + 1" + type "_ + 10" + type "exit!" + end + + assert_include output, "=> 12" + end + + def test_evaluate_with_encoding_error_without_lineno + if RUBY_ENGINE == 'truffleruby' + omit "Remove me after https://github.com/ruby/prism/issues/2129 is addressed and adopted in TruffleRuby" + end + + if RUBY_VERSION >= "3.3." + omit "Now raises SyntaxError" + end + + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type %q[:"\xAE"] + type "exit!" + end + + assert_include output, 'invalid symbol in encoding UTF-8 :"\xAE"' + # EncodingError would be wrapped with ANSI escape sequences, so we assert it separately + assert_include output, "EncodingError" + end + + def test_evaluate_still_emits_warning + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type %q[def foo; END {}; end] + type "exit!" + end + + assert_include output, '(irb):1: warning: END in method; use at_exit' + end + + def test_symbol_aliases_dont_affect_ruby_syntax + write_ruby <<~'RUBY' + $foo = "It's a foo" + @bar = "It's a bar" + binding.irb + RUBY + + output = run_ruby_file do + type "$foo" + type "@bar" + type "exit!" + end + + assert_include output, "=> \"It's a foo\"" + assert_include output, "=> \"It's a bar\"" + end + + def test_empty_input_echoing_behaviour + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type "" + type " " + type "exit" + end + + assert_not_match(/irb\(main\):001> (\r*\n)?=> nil/, output) + assert_match(/irb\(main\):002> (\r*\n)?=> nil/, output) + end + end + + class NestedBindingIrbTest < IntegrationTestCase + def test_current_context_restore + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type '$ctx = IRB.CurrentContext' + type 'binding.irb' + type 'p context_changed: IRB.CurrentContext != $ctx' + type 'exit' + type 'p context_restored: IRB.CurrentContext == $ctx' + type 'exit' + end + + assert_include output, '{:context_changed=>true}' + assert_include output, '{:context_restored=>true}' + end + end + + class IrbIOConfigurationTest < TestCase + Row = Struct.new(:content, :current_line_spaces, :new_line_spaces, :indent_level) + + class MockIO_AutoIndent + attr_reader :calculated_indent + + def initialize(*params) + @params = params + end + + def auto_indent(&block) + @calculated_indent = block.call(*@params) + end + end + + class MockIO_DynamicPrompt + attr_reader :prompt_list + + def initialize(params, &assertion) + @params = params + end + + def dynamic_prompt(&block) + @prompt_list = block.call(@params) + end + end + + def setup + save_encodings + @irb = build_irb + end + + def teardown + restore_encodings + end + + class AutoIndentationTest < IrbIOConfigurationTest + def test_auto_indent + input_with_correct_indents = [ + [%q(def each_top_level_statement), 0, 2], + [%q( initialize_input), 2, 2], + [%q( catch(:TERM_INPUT) do), 2, 4], + [%q( loop do), 4, 6], + [%q( begin), 6, 8], + [%q( prompt), 8, 8], + [%q( unless l = lex), 8, 10], + [%q( throw :TERM_INPUT if @line == ''), 10, 10], + [%q( else), 8, 10], + [%q( @line_no += l.count("\n")), 10, 10], + [%q( next if l == "\n"), 10, 10], + [%q( @line.concat l), 10, 10], + [%q( if @code_block_open or @ltype or @continue or @indent > 0), 10, 12], + [%q( next), 12, 12], + [%q( end), 10, 10], + [%q( end), 8, 8], + [%q( if @line != "\n"), 8, 10], + [%q( @line.force_encoding(@io.encoding)), 10, 10], + [%q( yield @line, @exp_line_no), 10, 10], + [%q( end), 8, 8], + [%q( break if @io.eof?), 8, 8], + [%q( @line = ''), 8, 8], + [%q( @exp_line_no = @line_no), 8, 8], + [%q( ), nil, 8], + [%q( @indent = 0), 8, 8], + [%q( rescue TerminateLineInput), 6, 8], + [%q( initialize_input), 8, 8], + [%q( prompt), 8, 8], + [%q( end), 6, 6], + [%q( end), 4, 4], + [%q( end), 2, 2], + [%q(end), 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents) + end + + def test_braces_on_their_own_line + input_with_correct_indents = [ + [%q(if true), 0, 2], + [%q( [), 2, 4], + [%q( ]), 2, 2], + [%q(end), 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents) + end + + def test_multiple_braces_in_a_line + input_with_correct_indents = [ + [%q([[[), 0, 6], + [%q( ]), 4, 4], + [%q( ]), 2, 2], + [%q(]), 0, 0], + [%q([<<FOO]), 0, 0], + [%q(hello), 0, 0], + [%q(FOO), 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents) + end + + def test_a_closed_brace_and_not_closed_brace_in_a_line + input_with_correct_indents = [ + [%q(p() {), 0, 2], + [%q(}), 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents) + end + + def test_symbols + input_with_correct_indents = [ + [%q(:a), 0, 0], + [%q(:A), 0, 0], + [%q(:+), 0, 0], + [%q(:@@a), 0, 0], + [%q(:@a), 0, 0], + [%q(:$a), 0, 0], + [%q(:def), 0, 0], + [%q(:`), 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents) + end + + def test_incomplete_coding_magic_comment + input_with_correct_indents = [ + [%q(#coding:u), 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents) + end + + def test_incomplete_encoding_magic_comment + input_with_correct_indents = [ + [%q(#encoding:u), 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents) + end + + def test_incomplete_emacs_coding_magic_comment + input_with_correct_indents = [ + [%q(# -*- coding: u), 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents) + end + + def test_incomplete_vim_coding_magic_comment + input_with_correct_indents = [ + [%q(# vim:set fileencoding=u), 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents) + end + + def test_mixed_rescue + input_with_correct_indents = [ + [%q(def m), 0, 2], + [%q( begin), 2, 4], + [%q( begin), 4, 6], + [%q( x = a rescue 4), 6, 6], + [%q( y = [(a rescue 5)]), 6, 6], + [%q( [x, y]), 6, 6], + [%q( rescue => e), 4, 6], + [%q( raise e rescue 8), 6, 6], + [%q( end), 4, 4], + [%q( rescue), 2, 4], + [%q( raise rescue 11), 4, 4], + [%q( end), 2, 2], + [%q(rescue => e), 0, 2], + [%q( raise e rescue 14), 2, 2], + [%q(end), 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents) + end + + def test_oneliner_method_definition + input_with_correct_indents = [ + [%q(class A), 0, 2], + [%q( def foo0), 2, 4], + [%q( 3), 4, 4], + [%q( end), 2, 2], + [%q( def foo1()), 2, 4], + [%q( 3), 4, 4], + [%q( end), 2, 2], + [%q( def foo2(a, b)), 2, 4], + [%q( a + b), 4, 4], + [%q( end), 2, 2], + [%q( def foo3 a, b), 2, 4], + [%q( a + b), 4, 4], + [%q( end), 2, 2], + [%q( def bar0() = 3), 2, 2], + [%q( def bar1(a) = a), 2, 2], + [%q( def bar2(a, b) = a + b), 2, 2], + [%q( def bar3() = :s), 2, 2], + [%q( def bar4() = Time.now), 2, 2], + [%q(end), 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents) + end + + def test_tlambda + input_with_correct_indents = [ + [%q(if true), 0, 2, 1], + [%q( -> {), 2, 4, 2], + [%q( }), 2, 2, 1], + [%q(end), 0, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_corresponding_syntax_to_keyword_do_in_class + input_with_correct_indents = [ + [%q(class C), 0, 2, 1], + [%q( while method_name do), 2, 4, 2], + [%q( 3), 4, 4, 2], + [%q( end), 2, 2, 1], + [%q( foo do), 2, 4, 2], + [%q( 3), 4, 4, 2], + [%q( end), 2, 2, 1], + [%q(end), 0, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_corresponding_syntax_to_keyword_do + input_with_correct_indents = [ + [%q(while i > 0), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + [%q(while true), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + [%q(while ->{i > 0}.call), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + [%q(while ->{true}.call), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + [%q(while i > 0 do), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + [%q(while true do), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + [%q(while ->{i > 0}.call do), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + [%q(while ->{true}.call do), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + [%q(foo do), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + [%q(foo true do), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + [%q(foo ->{true} do), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + [%q(foo ->{i > 0} do), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_corresponding_syntax_to_keyword_for + input_with_correct_indents = [ + [%q(for i in [1]), 0, 2, 1], + [%q( puts i), 2, 2, 1], + [%q(end), 0, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_corresponding_syntax_to_keyword_for_with_do + input_with_correct_indents = [ + [%q(for i in [1] do), 0, 2, 1], + [%q( puts i), 2, 2, 1], + [%q(end), 0, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_typing_incomplete_include_interpreted_as_keyword_in + input_with_correct_indents = [ + [%q(module E), 0, 2, 1], + [%q(end), 0, 0, 0], + [%q(class A), 0, 2, 1], + [%q( in), 2, 2, 1] # scenario typing `include E` + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + + end + + def test_bracket_corresponding_to_times + input_with_correct_indents = [ + [%q(3.times { |i|), 0, 2, 1], + [%q( puts i), 2, 2, 1], + [%q(}), 0, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_do_corresponding_to_times + input_with_correct_indents = [ + [%q(3.times do |i|), 0, 2, 1], + [%q( puts i), 2, 2, 1], + [%q(end), 0, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_bracket_corresponding_to_loop + input_with_correct_indents = [ + ['loop {', 0, 2, 1], + [' 3', 2, 2, 1], + ['}', 0, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_do_corresponding_to_loop + input_with_correct_indents = [ + [%q(loop do), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_embdoc_indent + input_with_correct_indents = [ + [%q(=begin), 0, 0, 0], + [%q(a), 0, 0, 0], + [%q( b), 1, 1, 0], + [%q(=end), 0, 0, 0], + [%q(if 1), 0, 2, 1], + [%q( 2), 2, 2, 1], + [%q(=begin), 0, 0, 0], + [%q(a), 0, 0, 0], + [%q( b), 1, 1, 0], + [%q(=end), 0, 2, 1], + [%q( 3), 2, 2, 1], + [%q(end), 0, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_heredoc_with_indent + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7.0') + pend 'This test needs Ripper::Lexer#scan to take broken tokens' + end + input_with_correct_indents = [ + [%q(<<~Q+<<~R), 0, 2, 1], + [%q(a), 2, 2, 1], + [%q(a), 2, 2, 1], + [%q( b), 2, 2, 1], + [%q( b), 2, 2, 1], + [%q( Q), 0, 2, 1], + [%q( c), 4, 4, 1], + [%q( c), 4, 4, 1], + [%q( R), 0, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_oneliner_def_in_multiple_lines + input_with_correct_indents = [ + [%q(def a()=[), 0, 2, 1], + [%q( 1,), 2, 2, 1], + [%q(].), 0, 0, 0], + [%q(to_s), 0, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_broken_heredoc + input_with_correct_indents = [ + [%q(def foo), 0, 2, 1], + [%q( <<~Q), 2, 4, 2], + [%q( Qend), 4, 4, 2], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_pasted_code_keep_base_indent_spaces + input_with_correct_indents = [ + [%q( def foo), 0, 6, 1], + [%q( if bar), 6, 10, 2], + [%q( [1), 10, 12, 3], + [%q( ]+[["a), 10, 14, 4], + [%q(b" + `c), 0, 14, 4], + [%q(d` + /e), 0, 14, 4], + [%q(f/ + :"g), 0, 14, 4], + [%q(h".tap do), 0, 16, 5], + [%q( 1), 16, 16, 5], + [%q( end), 14, 14, 4], + [%q( ]), 12, 12, 3], + [%q( ]), 10, 10, 2], + [%q( end), 8, 6, 1], + [%q( end), 4, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_pasted_code_keep_base_indent_spaces_with_heredoc + input_with_correct_indents = [ + [%q( def foo), 0, 6, 1], + [%q( if bar), 6, 10, 2], + [%q( [1), 10, 12, 3], + [%q( ]+[["a), 10, 14, 4], + [%q(b" + <<~A + <<-B + <<C), 0, 16, 5], + [%q( a#{), 16, 18, 6], + [%q( 1), 18, 18, 6], + [%q( }), 16, 16, 5], + [%q( A), 14, 16, 5], + [%q( b#{), 16, 18, 6], + [%q( 1), 18, 18, 6], + [%q( }), 16, 16, 5], + [%q( B), 14, 0, 0], + [%q(c#{), 0, 2, 1], + [%q(1), 2, 2, 1], + [%q(}), 0, 0, 0], + [%q(C), 0, 14, 4], + [%q( ]), 12, 12, 3], + [%q( ]), 10, 10, 2], + [%q( end), 8, 6, 1], + [%q( end), 4, 0, 0], + ] + + assert_rows_with_correct_indents(input_with_correct_indents, assert_indent_level: true) + end + + def test_heredoc_keep_indent_spaces + (1..4).each do |indent| + row = Row.new(' ' * indent, nil, [4, indent].max, 2) + lines = ['def foo', ' <<~Q', row.content] + assert_row_indenting(lines, row) + assert_indent_level(lines, row.indent_level) + end + end + + private + + def assert_row_indenting(lines, row) + actual_current_line_spaces = calculate_indenting(lines, false) + + error_message = <<~MSG + Incorrect spaces calculation for line: + + ``` + > #{lines.last} + ``` + + All lines: + + ``` + #{lines.join("\n")} + ``` + MSG + assert_equal(row.current_line_spaces, actual_current_line_spaces, error_message) + + error_message = <<~MSG + Incorrect spaces calculation for line after the current line: + + ``` + #{lines.last} + > + ``` + + All lines: + + ``` + #{lines.join("\n")} + ``` + MSG + actual_next_line_spaces = calculate_indenting(lines, true) + assert_equal(row.new_line_spaces, actual_next_line_spaces, error_message) + end + + def assert_rows_with_correct_indents(rows_with_spaces, assert_indent_level: false) + lines = [] + rows_with_spaces.map do |row| + row = Row.new(*row) + lines << row.content + assert_row_indenting(lines, row) + + if assert_indent_level + assert_indent_level(lines, row.indent_level) + end + end + end + + def assert_indent_level(lines, expected) + code = lines.map { |l| "#{l}\n" }.join # code should end with "\n" + _tokens, opens, _ = @irb.scanner.check_code_state(code, local_variables: []) + indent_level = @irb.scanner.calc_indent_level(opens) + error_message = "Calculated the wrong number of indent level for:\n #{lines.join("\n")}" + assert_equal(expected, indent_level, error_message) + end + + def calculate_indenting(lines, add_new_line) + lines = lines + [""] if add_new_line + last_line_index = lines.length - 1 + byte_pointer = lines.last.length + + mock_io = MockIO_AutoIndent.new(lines, last_line_index, byte_pointer, add_new_line) + @irb.context.auto_indent_mode = true + @irb.context.io = mock_io + @irb.configure_io + + mock_io.calculated_indent + end + end + + class DynamicPromptTest < IrbIOConfigurationTest + def test_endless_range_at_end_of_line + input_with_prompt = [ + ['001:0: :> ', %q(a = 3..)], + ['002:0: :> ', %q()], + ] + + assert_dynamic_prompt(input_with_prompt) + end + + def test_heredoc_with_embexpr + input_with_prompt = [ + ['001:0:":* ', %q(<<A+%W[#{<<B)], + ['002:0:":* ', %q(#{<<C+%W[)], + ['003:0:":* ', %q(a)], + ['004:2:]:* ', %q(C)], + ['005:2:]:* ', %q(a)], + ['006:0:":* ', %q(]})], + ['007:0:":* ', %q(})], + ['008:0:":* ', %q(A)], + ['009:2:]:* ', %q(B)], + ['010:1:]:* ', %q(})], + ['011:0: :> ', %q(])], + ['012:0: :> ', %q()], + ] + + assert_dynamic_prompt(input_with_prompt) + end + + def test_heredoc_prompt_with_quotes + input_with_prompt = [ + ["001:1:':* ", %q(<<~'A')], + ["002:1:':* ", %q(#{foobar})], + ["003:0: :> ", %q(A)], + ["004:1:`:* ", %q(<<~`A`)], + ["005:1:`:* ", %q(whoami)], + ["006:0: :> ", %q(A)], + ['007:1:":* ', %q(<<~"A")], + ['008:1:":* ', %q(foobar)], + ['009:0: :> ', %q(A)], + ] + + assert_dynamic_prompt(input_with_prompt) + end + + def test_backtick_method + input_with_prompt = [ + ['001:0: :> ', %q(self.`(arg))], + ['002:0: :> ', %q()], + ['003:0: :> ', %q(def `(); end)], + ['004:0: :> ', %q()], + ] + + assert_dynamic_prompt(input_with_prompt) + end + + def test_dynamic_prompt + input_with_prompt = [ + ['001:1: :* ', %q(def hoge)], + ['002:1: :* ', %q( 3)], + ['003:0: :> ', %q(end)], + ] + + assert_dynamic_prompt(input_with_prompt) + end + + def test_dynamic_prompt_with_double_newline_breaking_code + input_with_prompt = [ + ['001:1: :* ', %q(if true)], + ['002:2: :* ', %q(%)], + ['003:1: :* ', %q(;end)], + ['004:1: :* ', %q(;hello)], + ['005:0: :> ', %q(end)], + ] + + assert_dynamic_prompt(input_with_prompt) + end + + def test_dynamic_prompt_with_multiline_literal + input_with_prompt = [ + ['001:1: :* ', %q(if true)], + ['002:2:]:* ', %q( %w[)], + ['003:2:]:* ', %q( a)], + ['004:1: :* ', %q( ])], + ['005:1: :* ', %q( b)], + ['006:2:]:* ', %q( %w[)], + ['007:2:]:* ', %q( c)], + ['008:1: :* ', %q( ])], + ['009:0: :> ', %q(end)], + ] + + assert_dynamic_prompt(input_with_prompt) + end + + def test_dynamic_prompt_with_blank_line + input_with_prompt = [ + ['001:1:]:* ', %q(%w[)], + ['002:1:]:* ', %q()], + ['003:0: :> ', %q(])], + ] + + assert_dynamic_prompt(input_with_prompt) + end + + def assert_dynamic_prompt(input_with_prompt) + expected_prompt_list, lines = input_with_prompt.transpose + def @irb.generate_prompt(opens, continue, line_offset) + ltype = @scanner.ltype_from_open_tokens(opens) + indent = @scanner.calc_indent_level(opens) + continue = opens.any? || continue + line_no = @line_no + line_offset + '%03d:%01d:%1s:%s ' % [line_no, indent, ltype, continue ? '*' : '>'] + end + io = MockIO_DynamicPrompt.new(lines) + @irb.context.io = io + @irb.configure_io + + error_message = <<~EOM + Expected dynamic prompt: + #{expected_prompt_list.join("\n")} + + Actual dynamic prompt: + #{io.prompt_list.join("\n")} + EOM + assert_equal(expected_prompt_list, io.prompt_list, error_message) + end + end + + private + + def build_binding + Object.new.instance_eval { binding } + end + + def build_irb + IRB.init_config(nil) + workspace = IRB::WorkSpace.new(build_binding) + + IRB.conf[:VERBOSE] = false + IRB::Irb.new(workspace, TestInputMethod.new) + end + end + + class BacktraceFilteringTest < TestIRB::IntegrationTestCase + def setup + super + # These tests are sensitive to warnings, so we disable them + original_rubyopt = [ENV["RUBYOPT"], @envs["RUBYOPT"]].compact.join(" ") + @envs["RUBYOPT"] = original_rubyopt + " -W0" + end + + def test_backtrace_filtering + write_ruby <<~'RUBY' + def foo + raise "error" + end + + def bar + foo + end + + binding.irb + RUBY + + output = run_ruby_file do + type "bar" + type "exit" + end + + assert_match(/irbtest-.*\.rb:2:in (`|'Object#)foo': error \(RuntimeError\)/, output) + frame_traces = output.split("\n").select { |line| line.strip.match?(/from /) }.map(&:strip) + + expected_traces = if RUBY_VERSION >= "3.3.0" + [ + /from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/, + /from .*\/irbtest-.*.rb\(irb\):1:in [`']<main>'/, + /from <internal:kernel>:\d+:in (`|'Kernel#)loop'/, + /from <internal:prelude>:\d+:in (`|'Binding#)irb'/, + /from .*\/irbtest-.*.rb:9:in [`']<main>'/ + ] + else + [ + /from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/, + /from .*\/irbtest-.*.rb\(irb\):1:in [`']<main>'/, + /from <internal:prelude>:\d+:in (`|'Binding#)irb'/, + /from .*\/irbtest-.*.rb:9:in [`']<main>'/ + ] + end + + expected_traces.reverse! if RUBY_VERSION < "3.0.0" + + expected_traces.each_with_index do |expected_trace, index| + assert_match(expected_trace, frame_traces[index]) + end + end + + def test_backtrace_filtering_with_backtrace_filter + write_rc <<~'RUBY' + class TestBacktraceFilter + def self.call(backtrace) + backtrace.reject { |line| line.include?("internal") } + end + end + + IRB.conf[:BACKTRACE_FILTER] = TestBacktraceFilter + RUBY + + write_ruby <<~'RUBY' + def foo + raise "error" + end + + def bar + foo + end + + binding.irb + RUBY + + output = run_ruby_file do + type "bar" + type "exit" + end + + assert_match(/irbtest-.*\.rb:2:in (`|'Object#)foo': error \(RuntimeError\)/, output) + frame_traces = output.split("\n").select { |line| line.strip.match?(/from /) }.map(&:strip) + + expected_traces = [ + /from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/, + /from .*\/irbtest-.*.rb\(irb\):1:in [`']<main>'/, + /from .*\/irbtest-.*.rb:9:in [`']<main>'/ + ] + + expected_traces.reverse! if RUBY_VERSION < "3.0.0" + + expected_traces.each_with_index do |expected_trace, index| + assert_match(expected_trace, frame_traces[index]) + end + end + end +end diff --git a/test/irb/test_nesting_parser.rb b/test/irb/test_nesting_parser.rb new file mode 100644 index 0000000000..2482d40081 --- /dev/null +++ b/test/irb/test_nesting_parser.rb @@ -0,0 +1,341 @@ +# frozen_string_literal: false +require 'irb' + +require_relative "helper" + +module TestIRB + class NestingParserTest < TestCase + def setup + save_encodings + end + + def teardown + restore_encodings + end + + def parse_by_line(code) + IRB::NestingParser.parse_by_line(IRB::RubyLex.ripper_lex_without_warning(code)) + end + + def test_open_tokens + code = <<~'EOS' + class A + def f + if true + tap do + { + x: " + #{p(1, 2, 3 + EOS + opens = IRB::NestingParser.open_tokens(IRB::RubyLex.ripper_lex_without_warning(code)) + assert_equal(%w[class def if do { " #{ (], opens.map(&:tok)) + end + + def test_parse_by_line + code = <<~EOS + (((((1+2 + ).to_s())).tap do ((( + EOS + _tokens, prev_opens, next_opens, min_depth = parse_by_line(code).last + assert_equal(%w[( ( ( ( (], prev_opens.map(&:tok)) + assert_equal(%w[( ( do ( ( (], next_opens.map(&:tok)) + assert_equal(2, min_depth) + end + + def test_ruby_syntax + code = <<~'EOS' + class A + 1 if 2 + 1 while 2 + 1 until 2 + 1 unless 2 + 1 rescue 2 + begin; rescue; ensure; end + tap do; rescue; ensure; end + class B; end + module C; end + def f; end + def `; end + def f() = 1 + %(); %w[]; %q(); %r{}; %i[] + "#{1}"; ''; /#{1}/; `#{1}` + :sym; :"sym"; :+; :`; :if + [1, 2, 3] + { x: 1, y: 2 } + (a, (*b, c), d), e = 1, 2, 3 + ->(a){}; ->(a) do end + -> a = -> b = :do do end do end + if 1; elsif 2; else; end + unless 1; end + while 1; end + until 1; end + for i in j; end + case 1; when 2; end + puts(1, 2, 3) + loop{|i|} + loop do |i| end + end + EOS + line_results = parse_by_line(code) + assert_equal(code.lines.size, line_results.size) + class_open, *inner_line_results, class_close = line_results + assert_equal(['class'], class_open[2].map(&:tok)) + inner_line_results.each {|result| assert_equal(['class'], result[2].map(&:tok)) } + assert_equal([], class_close[2].map(&:tok)) + end + + def test_multiline_string + code = <<~EOS + " + aaa + bbb + " + <<A + aaa + bbb + A + EOS + line_results = parse_by_line(code) + assert_equal(code.lines.size, line_results.size) + string_content_line, string_opens = line_results[1] + assert_equal("\naaa\nbbb\n", string_content_line.first.first.tok) + assert_equal("aaa\n", string_content_line.first.last) + assert_equal(['"'], string_opens.map(&:tok)) + heredoc_content_line, heredoc_opens = line_results[6] + assert_equal("aaa\nbbb\n", heredoc_content_line.first.first.tok) + assert_equal("bbb\n", heredoc_content_line.first.last) + assert_equal(['<<A'], heredoc_opens.map(&:tok)) + _line, _prev_opens, next_opens, _min_depth = line_results.last + assert_equal([], next_opens) + end + + def test_backslash_continued_nested_symbol + code = <<~'EOS' + x = <<A, :\ + heredoc #{ + here + } + A + =begin + embdoc + =end + # comment + + if # this is symbol :if + while + EOS + line_results = parse_by_line(code) + assert_equal(%w[: <<A #{], line_results[2][2].map(&:tok)) + assert_equal(%w[while], line_results.last[2].map(&:tok)) + end + + def test_oneliner_def + code = <<~EOC + if true + # normal oneliner def + def f = 1 + def f() = 1 + def f(*) = 1 + # keyword, backtick, op + def * = 1 + def ` = 1 + def if = 1 + def *() = 1 + def `() = 1 + def if() = 1 + # oneliner def with receiver + def a.* = 1 + def $a.* = 1 + def @a.` = 1 + def A.` = 1 + def ((a;b;c)).*() = 1 + def ((a;b;c)).if() = 1 + def ((a;b;c)).end() = 1 + # multiline oneliner def + def f = + 1 + def f() + = + 1 + # oneliner def with comment and embdoc + def # comment + =begin + embdoc + =end + ((a;b;c)) + . # comment + =begin + embdoc + =end + f (*) # comment + =begin + embdoc + =end + = + 1 + # nested oneliner def + def f(x = def f() = 1) = def f() = 1 + EOC + _tokens, _prev_opens, next_opens, min_depth = parse_by_line(code).last + assert_equal(['if'], next_opens.map(&:tok)) + assert_equal(1, min_depth) + end + + def test_heredoc_embexpr + code = <<~'EOS' + <<A+<<B+<<C+(<<D+(<<E) + #{ + <<~F+"#{<<~G} + #{ + here + } + F + G + " + } + A + B + C + D + E + ) + EOS + line_results = parse_by_line(code) + last_opens = line_results.last[-2] + assert_equal([], last_opens) + _tokens, _prev_opens, next_opens, _min_depth = line_results[4] + assert_equal(%w[( <<E <<D <<C <<B <<A #{ " <<~G <<~F #{], next_opens.map(&:tok)) + end + + def test_for_in + code = <<~EOS + for i in j + here + end + for i in j do + here + end + for i in + j do + here + end + for + # comment + i in j do + here + end + for (a;b;c).d in (a;b;c) do + here + end + for i in :in + :do do + here + end + for i in -> do end do + here + end + EOS + line_results = parse_by_line(code).select { |tokens,| tokens.map(&:last).include?('here') } + assert_equal(7, line_results.size) + line_results.each do |_tokens, _prev_opens, next_opens, _min_depth| + assert_equal(['for'], next_opens.map(&:tok)) + end + end + + def test_while_until + base_code = <<~'EOS' + while_or_until true + here + end + while_or_until a < c + here + end + while_or_until true do + here + end + while_or_until + # comment + (a + b) < + # comment + c do + here + end + while_or_until :\ + do do + here + end + while_or_until def do; end == :do do + here + end + while_or_until -> do end do + here + end + EOS + %w[while until].each do |keyword| + code = base_code.gsub('while_or_until', keyword) + line_results = parse_by_line(code).select { |tokens,| tokens.map(&:last).include?('here') } + assert_equal(7, line_results.size) + line_results.each do |_tokens, _prev_opens, next_opens, _min_depth| + assert_equal([keyword], next_opens.map(&:tok) ) + end + end + end + + def test_undef_alias + codes = [ + 'undef foo', + 'alias foo bar', + 'undef !', + 'alias + -', + 'alias $a $b', + 'undef do', + 'alias do do', + 'undef :do', + 'alias :do :do', + 'undef :"#{alias do do}"', + 'alias :"#{undef do}" do', + 'alias do :"#{undef do}"' + ] + code_with_comment = <<~EOS + undef # + # + do # + alias # + # + do # + # + do # + EOS + code_with_heredoc = <<~EOS + <<~A; alias + A + :"#{<<~A}" + A + do + EOS + [*codes, code_with_comment, code_with_heredoc].each do |code| + opens = IRB::NestingParser.open_tokens(IRB::RubyLex.ripper_lex_without_warning('(' + code + "\nif")) + assert_equal(%w[( if], opens.map(&:tok)) + end + end + + def test_case_in + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7.0') + pend 'This test requires ruby version that supports case-in syntax' + end + code = <<~EOS + case 1 + in 1 + here + in + 2 + here + end + EOS + line_results = parse_by_line(code).select { |tokens,| tokens.map(&:last).include?('here') } + assert_equal(2, line_results.size) + line_results.each do |_tokens, _prev_opens, next_opens, _min_depth| + assert_equal(['in'], next_opens.map(&:tok)) + end + end + end +end diff --git a/test/irb/test_option.rb b/test/irb/test_option.rb index e334cee6dd..fec31f384f 100644 --- a/test/irb/test_option.rb +++ b/test/irb/test_option.rb @@ -2,7 +2,7 @@ require_relative "helper" module TestIRB - class TestOption < TestCase + class OptionTest < TestCase def test_end_of_option bug4117 = '[ruby-core:33574]' bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] diff --git a/test/irb/test_raise_no_backtrace_exception.rb b/test/irb/test_raise_exception.rb index 9565419cdd..44a5ae87e1 100644 --- a/test/irb/test_raise_no_backtrace_exception.rb +++ b/test/irb/test_raise_exception.rb @@ -4,13 +4,31 @@ require "tmpdir" require_relative "helper" module TestIRB - class TestRaiseNoBacktraceException < TestCase - def test_raise_exception + class RaiseExceptionTest < TestCase + def test_raise_exception_with_nil_backtrace bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] - assert_in_out_err(bundle_exec + %w[-rirb -W0 -e IRB.start(__FILE__) -- -f --], <<-IRB, /Exception: foo/, []) + assert_in_out_err(bundle_exec + %w[-rirb -W0 -e IRB.start(__FILE__) -- -f --], <<-IRB, /#<Exception: foo>/, []) + raise Exception.new("foo").tap {|e| def e.backtrace; nil; end } +IRB + end + + def test_raise_exception_with_message_exception + bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] + expected = /#<Exception: foo>\nbacktraces are hidden because bar was raised when processing them/ + assert_in_out_err(bundle_exec + %w[-rirb -W0 -e IRB.start(__FILE__) -- -f --], <<-IRB, expected, []) + e = Exception.new("foo") + def e.message; raise 'bar'; end + raise e +IRB + end + + def test_raise_exception_with_message_inspect_exception + bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] + expected = /Uninspectable exception occurred/ + assert_in_out_err(bundle_exec + %w[-rirb -W0 -e IRB.start(__FILE__) -- -f --], <<-IRB, expected, []) e = Exception.new("foo") - puts e.inspect - def e.backtrace; nil; end + def e.message; raise; end + def e.inspect; raise; end raise e IRB end @@ -43,7 +61,7 @@ IRB # TruffleRuby warns when the locale does not exist env['TRUFFLERUBYOPT'] = "#{ENV['TRUFFLERUBYOPT']} --log.level=SEVERE" if RUBY_ENGINE == 'truffleruby' args = [env] + bundle_exec + %W[-rirb -C #{tmpdir} -W0 -e IRB.start(__FILE__) -- -f --] - error = /`raise_euc_with_invalid_byte_sequence': あ\\xFF \(RuntimeError\)/ + error = /raise_euc_with_invalid_byte_sequence': あ\\xFF \(RuntimeError\)/ assert_in_out_err(args, <<~IRB, error, [], encoding: "UTF-8") require_relative 'euc' raise_euc_with_invalid_byte_sequence diff --git a/test/irb/test_ruby_lex.rb b/test/irb/test_ruby_lex.rb index 1530a16d6a..4e406a8ce0 100644 --- a/test/irb/test_ruby_lex.rb +++ b/test/irb/test_ruby_lex.rb @@ -1,27 +1,10 @@ -$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) -require 'irb' -require 'rubygems' -require 'ostruct' +# frozen_string_literal: true +require "irb" require_relative "helper" module TestIRB - class TestRubyLex < TestCase - Row = Struct.new(:content, :current_line_spaces, :new_line_spaces, :nesting_level) - - class MockIO_AutoIndent - attr_reader :calculated_indent - - def initialize(*params) - @params = params - @calculated_indent - end - - def auto_indent(&block) - @calculated_indent = block.call(*@params) - end - end - + class RubyLexTest < TestCase def setup save_encodings end @@ -30,692 +13,77 @@ module TestIRB restore_encodings end - def calculate_indenting(lines, add_new_line) - lines = lines + [""] if add_new_line - last_line_index = lines.length - 1 - byte_pointer = lines.last.length - - context = build_context - context.auto_indent_mode = true - - ruby_lex = RubyLex.new(context) - mock_io = MockIO_AutoIndent.new(lines, last_line_index, byte_pointer, add_new_line) - - ruby_lex.configure_io(mock_io) - mock_io.calculated_indent - end - - def assert_row_indenting(lines, row) - actual_current_line_spaces = calculate_indenting(lines, false) - - error_message = <<~MSG - Incorrect spaces calculation for line: - - ``` - > #{lines.last} - ``` - - All lines: - - ``` - #{lines.join("\n")} - ``` - MSG - assert_equal(row.current_line_spaces, actual_current_line_spaces, error_message) - - error_message = <<~MSG - Incorrect spaces calculation for line after the current line: - - ``` - #{lines.last} - > - ``` - - All lines: - - ``` - #{lines.join("\n")} - ``` - MSG - actual_next_line_spaces = calculate_indenting(lines, true) - assert_equal(row.new_line_spaces, actual_next_line_spaces, error_message) - end - - def assert_nesting_level(lines, expected, local_variables: []) - indent, _code_block_open = check_state(lines, local_variables: local_variables) - error_message = "Calculated the wrong number of nesting level for:\n #{lines.join("\n")}" - assert_equal(expected, indent, error_message) - end - - def assert_code_block_open(lines, expected, local_variables: []) - _indent, code_block_open = check_state(lines, local_variables: local_variables) - error_message = "Wrong result of code_block_open for:\n #{lines.join("\n")}" - assert_equal(expected, code_block_open, error_message) - end - - def check_state(lines, local_variables: []) - context = build_context(local_variables) - ruby_lex = RubyLex.new(context) - _ltype, indent, _continue, code_block_open = ruby_lex.check_code_state(lines.join("\n")) - [indent, code_block_open] - end - - def test_auto_indent - input_with_correct_indents = [ - Row.new(%q(def each_top_level_statement), nil, 2), - Row.new(%q( initialize_input), nil, 2), - Row.new(%q( catch(:TERM_INPUT) do), nil, 4), - Row.new(%q( loop do), nil, 6), - Row.new(%q( begin), nil, 8), - Row.new(%q( prompt), nil, 8), - Row.new(%q( unless l = lex), nil, 10), - Row.new(%q( throw :TERM_INPUT if @line == ''), nil, 10), - Row.new(%q( else), 8, 10), - Row.new(%q( @line_no += l.count("\n")), nil, 10), - Row.new(%q( next if l == "\n"), nil, 10), - Row.new(%q( @line.concat l), nil, 10), - Row.new(%q( if @code_block_open or @ltype or @continue or @indent > 0), nil, 12), - Row.new(%q( next), nil, 12), - Row.new(%q( end), 10, 10), - Row.new(%q( end), 8, 8), - Row.new(%q( if @line != "\n"), nil, 10), - Row.new(%q( @line.force_encoding(@io.encoding)), nil, 10), - Row.new(%q( yield @line, @exp_line_no), nil, 10), - Row.new(%q( end), 8, 8), - Row.new(%q( break if @io.eof?), nil, 8), - Row.new(%q( @line = ''), nil, 8), - Row.new(%q( @exp_line_no = @line_no), nil, 8), - Row.new(%q( ), nil, 8), - Row.new(%q( @indent = 0), nil, 8), - Row.new(%q( rescue TerminateLineInput), 6, 8), - Row.new(%q( initialize_input), nil, 8), - Row.new(%q( prompt), nil, 8), - Row.new(%q( end), 6, 6), - Row.new(%q( end), 4, 4), - Row.new(%q( end), 2, 2), - Row.new(%q(end), 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - end - end - - def test_braces_on_their_own_line - input_with_correct_indents = [ - Row.new(%q(if true), nil, 2), - Row.new(%q( [), nil, 4), - Row.new(%q( ]), 2, 2), - Row.new(%q(end), 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - end - end - - def test_multiple_braces_in_a_line - input_with_correct_indents = [ - Row.new(%q([[[), nil, 6), - Row.new(%q( ]), 4, 4), - Row.new(%q( ]), 2, 2), - Row.new(%q(]), 0, 0), - Row.new(%q([<<FOO]), 0, 0), - Row.new(%q(hello), 0, 0), - Row.new(%q(FOO), nil, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - end - end - - def test_a_closed_brace_and_not_closed_brace_in_a_line - input_with_correct_indents = [ - Row.new(%q(p() {), nil, 2), - Row.new(%q(}), 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - end - end - - def test_symbols - input_with_correct_indents = [ - Row.new(%q(:a), nil, 0), - Row.new(%q(:A), nil, 0), - Row.new(%q(:+), nil, 0), - Row.new(%q(:@@a), nil, 0), - Row.new(%q(:@a), nil, 0), - Row.new(%q(:$a), nil, 0), - Row.new(%q(:def), nil, 0), - Row.new(%q(:`), nil, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - end - end - - def test_endless_range_at_end_of_line - input_with_prompt = [ - PromptRow.new('001:0: :> ', %q(a = 3..)), - PromptRow.new('002:0: :* ', %q()), - ] - - lines = input_with_prompt.map(&:content) - expected_prompt_list = input_with_prompt.map(&:prompt) - assert_dynamic_prompt(lines, expected_prompt_list) - end - - def test_heredoc_with_embexpr - input_with_prompt = [ - PromptRow.new('001:0:":* ', %q(<<A+%W[#{<<B)), - PromptRow.new('002:0:":* ', %q(#{<<C+%W[)), - PromptRow.new('003:0:":* ', %q(a)), - PromptRow.new('004:0:]:* ', %q(C)), - PromptRow.new('005:0:]:* ', %q(a)), - PromptRow.new('006:0:":* ', %q(]})), - PromptRow.new('007:0:":* ', %q(})), - PromptRow.new('008:0:":* ', %q(A)), - PromptRow.new('009:0:]:* ', %q(B)), - PromptRow.new('010:0:]:* ', %q(})), - PromptRow.new('011:0: :> ', %q(])), - PromptRow.new('012:0: :* ', %q()), - ] - - lines = input_with_prompt.map(&:content) - expected_prompt_list = input_with_prompt.map(&:prompt) - assert_dynamic_prompt(lines, expected_prompt_list) - end - - def test_heredoc_prompt_with_quotes - input_with_prompt = [ - PromptRow.new("001:0:':* ", %q(<<~'A')), - PromptRow.new("002:0:':* ", %q(#{foobar})), - PromptRow.new("003:0: :> ", %q(A)), - PromptRow.new("004:0:`:* ", %q(<<~`A`)), - PromptRow.new("005:0:`:* ", %q(whoami)), - PromptRow.new("006:0: :> ", %q(A)), - PromptRow.new('007:0:":* ', %q(<<~"A")), - PromptRow.new('008:0:":* ', %q(foobar)), - PromptRow.new('009:0: :> ', %q(A)), - ] - - lines = input_with_prompt.map(&:content) - expected_prompt_list = input_with_prompt.map(&:prompt) - assert_dynamic_prompt(lines, expected_prompt_list) - end - - def test_backtick_method - input_with_prompt = [ - PromptRow.new('001:0: :> ', %q(self.`(arg))), - PromptRow.new('002:0: :* ', %q()), - PromptRow.new('003:0: :> ', %q(def `(); end)), - PromptRow.new('004:0: :* ', %q()), - ] - - lines = input_with_prompt.map(&:content) - expected_prompt_list = input_with_prompt.map(&:prompt) - assert_dynamic_prompt(lines, expected_prompt_list) - end - - def test_incomplete_coding_magic_comment - input_with_correct_indents = [ - Row.new(%q(#coding:u), nil, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - end - end - - def test_incomplete_encoding_magic_comment - input_with_correct_indents = [ - Row.new(%q(#encoding:u), nil, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - end - end - - def test_incomplete_emacs_coding_magic_comment - input_with_correct_indents = [ - Row.new(%q(# -*- coding: u), nil, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - end - end - - def test_incomplete_vim_coding_magic_comment - input_with_correct_indents = [ - Row.new(%q(# vim:set fileencoding=u), nil, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - end - end - - def test_mixed_rescue - input_with_correct_indents = [ - Row.new(%q(def m), nil, 2), - Row.new(%q( begin), nil, 4), - Row.new(%q( begin), nil, 6), - Row.new(%q( x = a rescue 4), nil, 6), - Row.new(%q( y = [(a rescue 5)]), nil, 6), - Row.new(%q( [x, y]), nil, 6), - Row.new(%q( rescue => e), 4, 6), - Row.new(%q( raise e rescue 8), nil, 6), - Row.new(%q( end), 4, 4), - Row.new(%q( rescue), 2, 4), - Row.new(%q( raise rescue 11), nil, 4), - Row.new(%q( end), 2, 2), - Row.new(%q(rescue => e), 0, 2), - Row.new(%q( raise e rescue 14), nil, 2), - Row.new(%q(end), 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - end - end - - def test_oneliner_method_definition - input_with_correct_indents = [ - Row.new(%q(class A), nil, 2), - Row.new(%q( def foo0), nil, 4), - Row.new(%q( 3), nil, 4), - Row.new(%q( end), 2, 2), - Row.new(%q( def foo1()), nil, 4), - Row.new(%q( 3), nil, 4), - Row.new(%q( end), 2, 2), - Row.new(%q( def foo2(a, b)), nil, 4), - Row.new(%q( a + b), nil, 4), - Row.new(%q( end), 2, 2), - Row.new(%q( def foo3 a, b), nil, 4), - Row.new(%q( a + b), nil, 4), - Row.new(%q( end), 2, 2), - Row.new(%q( def bar0() = 3), nil, 2), - Row.new(%q( def bar1(a) = a), nil, 2), - Row.new(%q( def bar2(a, b) = a + b), nil, 2), - Row.new(%q( def bar3() = :s), nil, 2), - Row.new(%q( def bar4() = Time.now), nil, 2), - Row.new(%q(end), 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - end - end - - def test_tlambda - input_with_correct_indents = [ - Row.new(%q(if true), nil, 2, 1), - Row.new(%q( -> {), nil, 4, 2), - Row.new(%q( }), 2, 2, 1), - Row.new(%q(end), 0, 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) - end - end - - def test_corresponding_syntax_to_keyword_do_in_class - input_with_correct_indents = [ - Row.new(%q(class C), nil, 2, 1), - Row.new(%q( while method_name do), nil, 4, 2), - Row.new(%q( 3), nil, 4, 2), - Row.new(%q( end), 2, 2, 1), - Row.new(%q( foo do), nil, 4, 2), - Row.new(%q( 3), nil, 4, 2), - Row.new(%q( end), 2, 2, 1), - Row.new(%q(end), 0, 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) - end - end - - def test_corresponding_syntax_to_keyword_do - input_with_correct_indents = [ - Row.new(%q(while i > 0), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - Row.new(%q(while true), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - Row.new(%q(while ->{i > 0}.call), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - Row.new(%q(while ->{true}.call), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - Row.new(%q(while i > 0 do), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - Row.new(%q(while true do), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - Row.new(%q(while ->{i > 0}.call do), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - Row.new(%q(while ->{true}.call do), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - Row.new(%q(foo do), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - Row.new(%q(foo true do), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - Row.new(%q(foo ->{true} do), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - Row.new(%q(foo ->{i > 0} do), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) - end - end - - def test_corresponding_syntax_to_keyword_for - input_with_correct_indents = [ - Row.new(%q(for i in [1]), nil, 2, 1), - Row.new(%q( puts i), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) - end - end - - def test_corresponding_syntax_to_keyword_for_with_do - input_with_correct_indents = [ - Row.new(%q(for i in [1] do), nil, 2, 1), - Row.new(%q( puts i), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) - end - end - - def test_corresponding_syntax_to_keyword_in - input_with_correct_indents = [ - Row.new(%q(module E), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - Row.new(%q(class A), nil, 2, 1), - Row.new(%q( in), nil, 4, 1) - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) - end - end - - def test_bracket_corresponding_to_times - input_with_correct_indents = [ - Row.new(%q(3.times { |i|), nil, 2, 1), - Row.new(%q( puts i), nil, 2, 1), - Row.new(%q(}), 0, 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) - end - end - - def test_do_corresponding_to_times - input_with_correct_indents = [ - Row.new(%q(3.times do |i|), nil, 2, 1), - #Row.new(%q( puts i), nil, 2, 1), - #Row.new(%q(end), 0, 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) - end - end - - def test_bracket_corresponding_to_loop - input_with_correct_indents = [ - Row.new(%q(loop {), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(}), 0, 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) + def test_interpolate_token_with_heredoc_and_unclosed_embexpr + code = <<~'EOC' + ①+<<A-② + #{③*<<B/④ + #{⑤&<<C|⑥ + EOC + ripper_tokens = Ripper.tokenize(code) + rubylex_tokens = IRB::RubyLex.ripper_lex_without_warning(code) + # Assert no missing part + assert_equal(code, rubylex_tokens.map(&:tok).join) + # Assert ripper tokens are not removed + ripper_tokens.each do |tok| + assert(rubylex_tokens.any? { |t| t.tok == tok && t.tok != :on_ignored_by_ripper }) end - end - - def test_do_corresponding_to_loop - input_with_correct_indents = [ - Row.new(%q(loop do), nil, 2, 1), - Row.new(%q( 3), nil, 2, 1), - Row.new(%q(end), 0, 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) + # Assert interpolated token position + rubylex_tokens.each do |t| + row, col = t.pos + assert_equal t.tok, code.lines[row - 1].byteslice(col, t.tok.bytesize) end end def test_local_variables_dependent_code - pend if RUBY_ENGINE == 'truffleruby' lines = ["a /1#/ do", "2"] - assert_nesting_level(lines, 1) + assert_indent_level(lines, 1) assert_code_block_open(lines, true) - assert_nesting_level(lines, 0, local_variables: ['a']) + assert_indent_level(lines, 0, local_variables: ['a']) assert_code_block_open(lines, false, local_variables: ['a']) end - def test_heredoc_with_indent - input_with_correct_indents = [ - Row.new(%q(<<~Q), 0, 0, 0), - Row.new(%q({), 0, 0, 0), - Row.new(%q( #), 2, 0, 0), - Row.new(%q(}), 0, 0, 0) - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) - end + def test_literal_ends_with_space + assert_code_block_open(['% a'], true) + assert_code_block_open(['% a '], false) end - def test_oneliner_def_in_multiple_lines - input_with_correct_indents = [ - Row.new(%q(def a()=[), nil, 4, 2), - Row.new(%q( 1,), nil, 4, 1), - Row.new(%q(].), 0, 0, 0), - Row.new(%q(to_s), nil, 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) - end + def test_literal_ends_with_newline + assert_code_block_open(['%'], true) + assert_code_block_open(['%', ''], false) end - def test_broken_heredoc - input_with_correct_indents = [ - Row.new(%q(def foo), nil, 2, 1), - Row.new(%q( <<~Q), 2, 2, 1), - Row.new(%q( Qend), 2, 2, 1), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_row_indenting(lines, row) - assert_nesting_level(lines, row.nesting_level) - end + def test_should_continue + assert_should_continue(['a'], false) + assert_should_continue(['/a/'], false) + assert_should_continue(['a;'], false) + assert_should_continue(['<<A', 'A'], false) + assert_should_continue(['a...'], false) + assert_should_continue(['a\\'], true) + assert_should_continue(['a.'], true) + assert_should_continue(['a+'], true) + assert_should_continue(['a; #comment', '', '=begin', 'embdoc', '=end', ''], false) + assert_should_continue(['a+ #comment', '', '=begin', 'embdoc', '=end', ''], true) end - PromptRow = Struct.new(:prompt, :content) + def test_code_block_open_with_should_continue + # syntax ok + assert_code_block_open(['a'], false) # continue: false + assert_code_block_open(['a\\'], true) # continue: true - class MockIO_DynamicPrompt - def initialize(params, &assertion) - @params = params - @assertion = assertion - end - - def dynamic_prompt(&block) - result = block.call(@params) - @assertion.call(result) - end - end - - def assert_dynamic_prompt(lines, expected_prompt_list) - pend if RUBY_ENGINE == 'truffleruby' - context = build_context - ruby_lex = RubyLex.new(context) - dynamic_prompt_executed = false - io = MockIO_DynamicPrompt.new(lines) do |prompt_list| - error_message = <<~EOM - Expected dynamic prompt: - #{expected_prompt_list.join("\n")} - - Actual dynamic prompt: - #{prompt_list.join("\n")} - EOM - dynamic_prompt_executed = true - assert_equal(expected_prompt_list, prompt_list, error_message) - end - ruby_lex.set_prompt do |ltype, indent, continue, line_no| - '%03d:%01d:%1s:%s ' % [line_no, indent, ltype, continue ? '*' : '>'] - end - ruby_lex.configure_io(io) - assert dynamic_prompt_executed, "dynamic_prompt's assertions were not executed." - end - - def test_dynamic_prompt - input_with_prompt = [ - PromptRow.new('001:1: :* ', %q(def hoge)), - PromptRow.new('002:1: :* ', %q( 3)), - PromptRow.new('003:0: :> ', %q(end)), - ] - - lines = input_with_prompt.map(&:content) - expected_prompt_list = input_with_prompt.map(&:prompt) - assert_dynamic_prompt(lines, expected_prompt_list) - end - - def test_dynamic_prompt_with_double_newline_breaking_code - input_with_prompt = [ - PromptRow.new('001:1: :* ', %q(if true)), - PromptRow.new('002:1: :* ', %q(%)), - PromptRow.new('003:1: :* ', %q(;end)), - PromptRow.new('004:1: :* ', %q(;hello)), - PromptRow.new('005:0: :> ', %q(end)), - ] - - lines = input_with_prompt.map(&:content) - expected_prompt_list = input_with_prompt.map(&:prompt) - assert_dynamic_prompt(lines, expected_prompt_list) - end + # recoverable syntax error code is not terminated + assert_code_block_open(['a+'], true) - def test_dynamic_prompt_with_multiline_literal - input_with_prompt = [ - PromptRow.new('001:1: :* ', %q(if true)), - PromptRow.new('002:1:]:* ', %q( %w[)), - PromptRow.new('003:1:]:* ', %q( a)), - PromptRow.new('004:1: :* ', %q( ])), - PromptRow.new('005:1: :* ', %q( b)), - PromptRow.new('006:1:]:* ', %q( %w[)), - PromptRow.new('007:1:]:* ', %q( c)), - PromptRow.new('008:1: :* ', %q( ])), - PromptRow.new('009:0: :> ', %q(end)), - ] + # unrecoverable syntax error code is terminated + assert_code_block_open(['.; a+'], false) - lines = input_with_prompt.map(&:content) - expected_prompt_list = input_with_prompt.map(&:prompt) - assert_dynamic_prompt(lines, expected_prompt_list) - end - - def test_dynamic_prompt_with_blank_line - input_with_prompt = [ - PromptRow.new('001:0:]:* ', %q(%w[)), - PromptRow.new('002:0:]:* ', %q()), - PromptRow.new('003:0: :> ', %q(])), - ] - - lines = input_with_prompt.map(&:content) - expected_prompt_list = input_with_prompt.map(&:prompt) - assert_dynamic_prompt(lines, expected_prompt_list) + # other syntax error that failed to determine if it is recoverable or not + assert_code_block_open(['@; a'], false) + assert_code_block_open(['@; a+'], true) + assert_code_block_open(['@; (a'], true) end def test_broken_percent_literal - tokens = RubyLex.ripper_lex_without_warning('%wwww') + tokens = IRB::RubyLex.ripper_lex_without_warning('%wwww') pos_to_index = {} tokens.each_with_index { |t, i| assert_nil(pos_to_index[t.pos], "There is already another token in the position of #{t.inspect}.") @@ -724,7 +92,7 @@ module TestIRB end def test_broken_percent_literal_in_method - tokens = RubyLex.ripper_lex_without_warning(<<~EOC.chomp) + tokens = IRB::RubyLex.ripper_lex_without_warning(<<~EOC.chomp) def foo %wwww end @@ -738,7 +106,7 @@ module TestIRB def test_unterminated_code ['do', '<<A'].each do |code| - tokens = RubyLex.ripper_lex_without_warning(code) + tokens = IRB::RubyLex.ripper_lex_without_warning(code) assert_equal(code, tokens.map(&:tok).join, "Cannot reconstruct code from tokens") error_tokens = tokens.map(&:event).grep(/error/) assert_empty(error_tokens, 'Error tokens must be ignored if there is corresponding non-error token') @@ -746,15 +114,14 @@ module TestIRB end def test_unterminated_heredoc_string_literal - context = build_context ['<<A;<<B', "<<A;<<B\n", "%W[\#{<<A;<<B", "%W[\#{<<A;<<B\n"].each do |code| - tokens = RubyLex.ripper_lex_without_warning(code) - string_literal = RubyLex.new(context).check_string_literal(tokens) + tokens = IRB::RubyLex.ripper_lex_without_warning(code) + string_literal = IRB::NestingParser.open_tokens(tokens).last assert_equal('<<A', string_literal&.tok) end end - def test_corresponding_token_depth_with_heredoc_and_embdoc + def test_indent_level_with_heredoc_and_embdoc reference_code = <<~EOC.chomp if true hello @@ -775,64 +142,101 @@ module TestIRB p( ) EOC - context = build_context - [reference_code, code_with_heredoc, code_with_embdoc].each do |code| - lex = RubyLex.new(context) - lines = code.lines - lex.instance_variable_set('@tokens', RubyLex.ripper_lex_without_warning(code)) - assert_equal 2, lex.check_corresponding_token_depth(lines, lines.size) - end - end - - def test_find_prev_spaces_with_multiline_literal - lex = RubyLex.new(build_context) - reference_code = <<~EOC.chomp - if true - 1 - hello - 1 - world - end - EOC - code_with_percent_string = <<~EOC.chomp - if true - %w[ - hello - ] - world - end - EOC - code_with_quoted_string = <<~EOC.chomp - if true - ' - hello - ' - world - end - EOC - context = build_context - [reference_code, code_with_percent_string, code_with_quoted_string].each do |code| - lex = RubyLex.new(context) - lex.instance_variable_set('@tokens', RubyLex.ripper_lex_without_warning(code)) - prev_spaces = (1..code.lines.size).map { |index| lex.find_prev_spaces index } - assert_equal [0, 2, 2, 2, 2, 0], prev_spaces - end + expected = 1 + assert_indent_level(reference_code.lines, expected) + assert_indent_level(code_with_heredoc.lines, expected) + assert_indent_level(code_with_embdoc.lines, expected) + end + + def test_assignment_expression + ruby_lex = IRB::RubyLex.new + + [ + "foo = bar", + "@foo = bar", + "$foo = bar", + "@@foo = bar", + "::Foo = bar", + "a::Foo = bar", + "Foo = bar", + "foo.bar = 1", + "foo[1] = bar", + "foo += bar", + "foo -= bar", + "foo ||= bar", + "foo &&= bar", + "foo, bar = 1, 2", + "foo.bar=(1)", + "foo; foo = bar", + "foo; foo = bar; ;\n ;", + "foo\nfoo = bar", + ].each do |exp| + assert( + ruby_lex.assignment_expression?(exp, local_variables: []), + "#{exp.inspect}: should be an assignment expression" + ) + end + + [ + "foo", + "foo.bar", + "foo[0]", + "foo = bar; foo", + "foo = bar\nfoo", + ].each do |exp| + refute( + ruby_lex.assignment_expression?(exp, local_variables: []), + "#{exp.inspect}: should not be an assignment expression" + ) + end + end + + def test_assignment_expression_with_local_variable + ruby_lex = IRB::RubyLex.new + code = "a /1;x=1#/" + refute(ruby_lex.assignment_expression?(code, local_variables: []), "#{code}: should not be an assignment expression") + assert(ruby_lex.assignment_expression?(code, local_variables: [:a]), "#{code}: should be an assignment expression") + refute(ruby_lex.assignment_expression?("", local_variables: [:a]), "empty code should not be an assignment expression") + end + + def test_initialising_the_old_top_level_ruby_lex + assert_in_out_err(["--disable-gems", "-W:deprecated"], <<~RUBY, [], /warning: constant ::RubyLex is deprecated/) + require "irb" + ::RubyLex.new(nil) + RUBY end private - def build_context(local_variables = nil) - IRB.init_config(nil) - workspace = IRB::WorkSpace.new(TOPLEVEL_BINDING.dup) + def assert_indent_level(lines, expected, local_variables: []) + indent_level, _continue, _code_block_open = check_state(lines, local_variables: local_variables) + error_message = "Calculated the wrong number of indent level for:\n #{lines.join("\n")}" + assert_equal(expected, indent_level, error_message) + end - if local_variables - local_variables.each do |n| - workspace.binding.local_variable_set(n, nil) - end + def assert_should_continue(lines, expected, local_variables: []) + _indent_level, continue, _code_block_open = check_state(lines, local_variables: local_variables) + error_message = "Wrong result of should_continue for:\n #{lines.join("\n")}" + assert_equal(expected, continue, error_message) + end + + def assert_code_block_open(lines, expected, local_variables: []) + if RUBY_ENGINE == 'truffleruby' + omit "Remove me after https://github.com/ruby/prism/issues/2129 is addressed and adopted in TruffleRuby" end - IRB.conf[:VERBOSE] = false - IRB::Context.new(nil, workspace) + _indent_level, _continue, code_block_open = check_state(lines, local_variables: local_variables) + error_message = "Wrong result of code_block_open for:\n #{lines.join("\n")}" + assert_equal(expected, code_block_open, error_message) + end + + def check_state(lines, local_variables: []) + code = lines.map { |l| "#{l}\n" }.join # code should end with "\n" + ruby_lex = IRB::RubyLex.new + tokens, opens, terminated = ruby_lex.check_code_state(code, local_variables: local_variables) + indent_level = ruby_lex.calc_indent_level(opens) + continue = ruby_lex.should_continue?(tokens) + [indent_level, continue, !terminated] end end end diff --git a/test/irb/test_tracer.rb b/test/irb/test_tracer.rb new file mode 100644 index 0000000000..540f8be131 --- /dev/null +++ b/test/irb/test_tracer.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: false +require 'tempfile' +require 'irb' + +require_relative "helper" + +module TestIRB + class ContextWithTracerIntegrationTest < IntegrationTestCase + def setup + super + + omit "Tracer gem is not available when running on TruffleRuby" if RUBY_ENGINE == "truffleruby" + + @envs.merge!("NO_COLOR" => "true") + end + + def example_ruby_file + <<~'RUBY' + class Foo + def self.foo + 100 + end + end + + def bar(obj) + obj.foo + end + + binding.irb + RUBY + end + + def test_use_tracer_enabled_when_gem_is_unavailable + write_rc <<~RUBY + # Simulate the absence of the tracer gem + ::Kernel.send(:alias_method, :irb_original_require, :require) + + ::Kernel.define_method(:require) do |name| + raise LoadError, "cannot load such file -- tracer (test)" if name.match?("tracer") + ::Kernel.send(:irb_original_require, name) + end + + IRB.conf[:USE_TRACER] = true + RUBY + + write_ruby example_ruby_file + + output = run_ruby_file do + type "bar(Foo)" + type "exit" + end + + assert_include(output, "Tracer extension of IRB is enabled but tracer gem wasn't found.") + end + + def test_use_tracer_enabled_when_gem_is_available + write_rc <<~RUBY + IRB.conf[:USE_TRACER] = true + RUBY + + write_ruby example_ruby_file + + output = run_ruby_file do + type "bar(Foo)" + type "exit" + end + + assert_include(output, "Object#bar at") + assert_include(output, "Foo.foo at") + assert_include(output, "Foo.foo #=> 100") + assert_include(output, "Object#bar #=> 100") + + # Test that the tracer output does not include IRB's own files + assert_not_include(output, "irb/workspace.rb") + end + + def test_use_tracer_is_disabled_by_default + write_ruby example_ruby_file + + output = run_ruby_file do + type "bar(Foo)" + type "exit" + end + + assert_not_include(output, "#depth:") + assert_not_include(output, "Foo.foo") + end + + end +end diff --git a/test/irb/test_type_completor.rb b/test/irb/test_type_completor.rb new file mode 100644 index 0000000000..5ed8988b34 --- /dev/null +++ b/test/irb/test_type_completor.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# Run test only when Ruby >= 3.0 and repl_type_completor is available +return unless RUBY_VERSION >= '3.0.0' +return if RUBY_ENGINE == 'truffleruby' # needs endless method definition +begin + require 'repl_type_completor' +rescue LoadError + return +end + +require 'irb' +require 'tempfile' +require_relative './helper' + +module TestIRB + class TypeCompletorTest < TestCase + DummyContext = Struct.new(:irb_path) + + def setup + ReplTypeCompletor.load_rbs unless ReplTypeCompletor.rbs_loaded? + context = DummyContext.new('(irb)') + @completor = IRB::TypeCompletor.new(context) + end + + def empty_binding + binding + end + + def assert_completion(preposing, target, binding: empty_binding, include: nil, exclude: nil) + raise ArgumentError if include.nil? && exclude.nil? + candidates = @completor.completion_candidates(preposing, target, '', bind: binding) + assert ([*include] - candidates).empty?, "Expected #{candidates} to include #{include}" if include + assert (candidates & [*exclude]).empty?, "Expected #{candidates} not to include #{exclude}" if exclude + end + + def assert_doc_namespace(preposing, target, namespace, binding: empty_binding) + @completor.completion_candidates(preposing, target, '', bind: binding) + assert_equal namespace, @completor.doc_namespace(preposing, target, '', bind: binding) + end + + def test_type_completion + bind = eval('num = 1; binding') + assert_completion('num.times.map(&:', 'ab', binding: bind, include: 'abs') + assert_doc_namespace('num.chr.', 'upcase', 'String#upcase', binding: bind) + end + + def test_inspect + assert_match(/\AReplTypeCompletor.*\z/, @completor.inspect) + end + + def test_empty_completion + candidates = @completor.completion_candidates('(', ')', '', bind: binding) + assert_equal [], candidates + assert_doc_namespace('(', ')', nil) + end + + def test_command_completion + assert_include(@completor.completion_candidates('', 'show_s', '', bind: binding), 'show_source') + assert_not_include(@completor.completion_candidates(';', 'show_s', '', bind: binding), 'show_source') + end + end + + class TypeCompletorIntegrationTest < IntegrationTestCase + def test_type_completor + write_rc <<~RUBY + IRB.conf[:COMPLETOR] = :type + RUBY + + write_ruby <<~'RUBY' + binding.irb + RUBY + + output = run_ruby_file do + type "irb_info" + type "sleep 0.01 until ReplTypeCompletor.rbs_loaded?" + type "completor = IRB.CurrentContext.io.instance_variable_get(:@completor);" + type "n = 10" + type "puts completor.completion_candidates 'a = n.abs;', 'a.b', '', bind: binding" + type "puts completor.doc_namespace 'a = n.chr;', 'a.encoding', '', bind: binding" + type "exit!" + end + assert_match(/Completion: Autocomplete, ReplTypeCompletor/, output) + assert_match(/a\.bit_length/, output) + assert_match(/String#encoding/, output) + end + end +end diff --git a/test/irb/test_workspace.rb b/test/irb/test_workspace.rb index c6b8aa1e99..199ce95a37 100644 --- a/test/irb/test_workspace.rb +++ b/test/irb/test_workspace.rb @@ -1,6 +1,5 @@ # frozen_string_literal: false require 'tempfile' -require 'rubygems' require 'irb' require 'irb/workspace' require 'irb/color' @@ -8,7 +7,7 @@ require 'irb/color' require_relative "helper" module TestIRB - class TestWorkSpace < TestCase + class WorkSpaceTest < TestCase def test_code_around_binding IRB.conf[:USE_COLORIZE] = false Tempfile.create('irb') do |f| @@ -91,7 +90,7 @@ module TestIRB irb_path = "#{top_srcdir}/#{dir}/irb" File.exist?(irb_path) end or omit 'irb command not found' - assert_in_out_err(bundle_exec + ['-W0', "-C#{top_srcdir}", '-e', <<~RUBY , '--', '-f', '--'], 'binding.local_variables', /\[:_\]/, [], bug17623) + assert_in_out_err(bundle_exec + ['-W0', "-C#{top_srcdir}", '-e', <<~RUBY, '--', '-f', '--'], 'binding.local_variables', /\[:_\]/, [], bug17623) version = 'xyz' # typical rubygems loading file load('#{irb_path}') RUBY diff --git a/test/irb/yamatanooroti/test_rendering.rb b/test/irb/yamatanooroti/test_rendering.rb index 1586dc6504..44e07a3a12 100644 --- a/test/irb/yamatanooroti/test_rendering.rb +++ b/test/irb/yamatanooroti/test_rendering.rb @@ -8,8 +8,12 @@ rescue LoadError, NameError return end -class IRB::TestRendering < Yamatanooroti::TestCase +class IRB::RenderingTest < Yamatanooroti::TestCase def setup + @original_term = ENV['TERM'] + @home_backup = ENV['HOME'] + @xdg_config_home_backup = ENV['XDG_CONFIG_HOME'] + ENV['TERM'] = "xterm-256color" @pwd = Dir.pwd suffix = '%010d' % Random.rand(0..65535) @tmpdir = File.join(File.expand_path(Dir.tmpdir), "test_irb_#{$$}_#{suffix}") @@ -22,12 +26,16 @@ class IRB::TestRendering < Yamatanooroti::TestCase @irbrc_backup = ENV['IRBRC'] @irbrc_file = ENV['IRBRC'] = File.join(@tmpdir, 'temporaty_irbrc') File.unlink(@irbrc_file) if File.exist?(@irbrc_file) + ENV['HOME'] = File.join(@tmpdir, 'home') + ENV['XDG_CONFIG_HOME'] = File.join(@tmpdir, 'xdg_config_home') end def teardown FileUtils.rm_rf(@tmpdir) ENV['IRBRC'] = @irbrc_backup - ENV.delete('RELINE_TEST_PROMPT') if ENV['RELINE_TEST_PROMPT'] + ENV['TERM'] = @original_term + ENV['HOME'] = @home_backup + ENV['XDG_CONFIG_HOME'] = @xdg_config_home_backup end def test_launch @@ -41,9 +49,74 @@ class IRB::TestRendering < Yamatanooroti::TestCase close assert_screen(<<~EOC) start IRB - irb(main):001:0> 'Hello, World!' + irb(main):001> 'Hello, World!' => "Hello, World!" - irb(main):002:0> + irb(main):002> + EOC + end + + def test_configuration_file_is_skipped_with_dash_f + write_irbrc <<~'LINES' + puts '.irbrc file should be ignored when -f is used' + LINES + start_terminal(25, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb -f}, startup_message: '') + write(<<~EOC) + 'Hello, World!' + EOC + close + assert_screen(<<~EOC) + irb(main):001> 'Hello, World!' + => "Hello, World!" + irb(main):002> + EOC + end + + def test_configuration_file_is_skipped_with_dash_f_for_nested_sessions + write_irbrc <<~'LINES' + puts '.irbrc file should be ignored when -f is used' + LINES + start_terminal(25, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb -f}, startup_message: '') + write(<<~EOC) + 'Hello, World!' + binding.irb + exit! + EOC + close + assert_screen(<<~EOC) + irb(main):001> 'Hello, World!' + => "Hello, World!" + irb(main):002> binding.irb + irb(main):003> exit! + irb(main):001> + EOC + end + + def test_nomultiline + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(25, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb --nomultiline}, startup_message: 'start IRB') + write(<<~EOC) + if true + if false + a = "hello + world" + puts a + end + end + EOC + close + assert_screen(<<~EOC) + start IRB + irb(main):001> if true + irb(main):002* if false + irb(main):003* a = "hello + irb(main):004" world" + irb(main):005* puts a + irb(main):006* end + irb(main):007* end + => nil + irb(main):008> EOC end @@ -64,25 +137,27 @@ class IRB::TestRendering < Yamatanooroti::TestCase a .a .b + .itself EOC close assert_screen(<<~EOC) start IRB - irb(main):001:1* class A - irb(main):002:1* def inspect; '#<A>'; end - irb(main):003:1* def a; self; end - irb(main):004:1* def b; true; end - irb(main):005:0> end + irb(main):001* class A + irb(main):002* def inspect; '#<A>'; end + irb(main):003* def a; self; end + irb(main):004* def b; true; end + irb(main):005> end => :b - irb(main):006:0> - irb(main):007:0> a = A.new + irb(main):006> + irb(main):007> a = A.new => #<A> - irb(main):008:0> - irb(main):009:0> a - irb(main):010:0> .a - irb(main):011:0> .b + irb(main):008> + irb(main):009> a + irb(main):010> .a + irb(main):011> .b + irb(main):012> .itself => true - irb(main):012:0> + irb(main):013> EOC end @@ -108,7 +183,6 @@ class IRB::TestRendering < Yamatanooroti::TestCase (a) &.b() - class A def b; self; end; def c; true; end; end; a = A.new a @@ -117,44 +191,44 @@ class IRB::TestRendering < Yamatanooroti::TestCase .c (a) &.b() + .itself EOC close assert_screen(<<~EOC) start IRB - irb(main):001:1* class A - irb(main):002:1* def inspect; '#<A>'; end - irb(main):003:1* def b; self; end - irb(main):004:1* def c; true; end - irb(main):005:0> end + irb(main):001* class A + irb(main):002* def inspect; '#<A>'; end + irb(main):003* def b; self; end + irb(main):004* def c; true; end + irb(main):005> end => :c - irb(main):006:0> - irb(main):007:0> a = A.new + irb(main):006> + irb(main):007> a = A.new => #<A> - irb(main):008:0> - irb(main):009:0> a - irb(main):010:0> .b - irb(main):011:0> # aaa - irb(main):012:0> .c + irb(main):008> + irb(main):009> a + irb(main):010> .b + irb(main):011> # aaa + irb(main):012> .c => true - irb(main):013:0> - irb(main):014:0> (a) - irb(main):015:0> &.b() + irb(main):013> + irb(main):014> (a) + irb(main):015> &.b() => #<A> - irb(main):016:0> - irb(main):017:0> - irb(main):018:0> class A def b; self; end; def c; true; end; end; - => :c - irb(main):019:0> a = A.new + irb(main):016> + irb(main):017> class A def b; self; end; def c; true; end; end; + irb(main):018> a = A.new => #<A> - irb(main):020:0> a - irb(main):021:0> .b - irb(main):022:0> # aaa - irb(main):023:0> .c + irb(main):019> a + irb(main):020> .b + irb(main):021> # aaa + irb(main):022> .c => true - irb(main):024:0> (a) - irb(main):025:0> &.b() + irb(main):023> (a) + irb(main):024> &.b() + irb(main):025> .itself => #<A> - irb(main):026:0> + irb(main):026> EOC end @@ -169,41 +243,70 @@ class IRB::TestRendering < Yamatanooroti::TestCase close assert_screen(<<~EOC) start IRB - irb(main):001:0> :` + irb(main):001> :` => :` - irb(main):002:0> + irb(main):002> EOC end - def test_autocomplete_with_showdoc_in_gaps_on_narrow_screen_right - pend "Needs a dummy document to show doc" + def test_autocomplete_with_multiple_doc_namespaces write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(3, 50, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write("{}.__id_") + write("\C-i") + sleep 0.2 + close + screen = result.join("\n").sub(/\n*\z/, "\n") + assert_match(/start\ IRB\nirb\(main\):001> {}\.__id__\n }\.__id__(?:Press )?/, screen) + end + + def test_autocomplete_with_showdoc_in_gaps_on_narrow_screen_right + rdoc_dir = File.join(@tmpdir, 'rdoc') + system("bundle exec rdoc -r -o #{rdoc_dir}") + write_irbrc <<~LINES + IRB.conf[:EXTRA_DOC_DIRS] = ['#{rdoc_dir}'] IRB.conf[:PROMPT][:MY_PROMPT] = { :PROMPT_I => "%03n> ", - :PROMPT_N => "%03n> ", :PROMPT_S => "%03n> ", :PROMPT_C => "%03n> " } IRB.conf[:PROMPT_MODE] = :MY_PROMPT puts 'start IRB' LINES - start_terminal(4, 19, %W{ruby -I/home/aycabta/ruby/reline/lib -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') - write("Str\C-i") + start_terminal(4, 19, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write("IR") + write("\C-i") + sleep 0.2 close - assert_screen(<<~EOC) - 001> String - StringPress A - StructString - of byte - EOC + + # This is because on macOS we display different shortcut for displaying the full doc + # 'O' is for 'Option' and 'A' is for 'Alt' + if RUBY_PLATFORM =~ /darwin/ + assert_screen(<<~EOC) + start IRB + 001> IRB + IRBPress Opti + IRB + EOC + else + assert_screen(<<~EOC) + start IRB + 001> IRB + IRBPress Alt+ + IRB + EOC + end end def test_autocomplete_with_showdoc_in_gaps_on_narrow_screen_left - pend "Needs a dummy document to show doc" - write_irbrc <<~'LINES' + rdoc_dir = File.join(@tmpdir, 'rdoc') + system("bundle exec rdoc -r -o #{rdoc_dir}") + write_irbrc <<~LINES + IRB.conf[:EXTRA_DOC_DIRS] = ['#{rdoc_dir}'] IRB.conf[:PROMPT][:MY_PROMPT] = { :PROMPT_I => "%03n> ", - :PROMPT_N => "%03n> ", :PROMPT_S => "%03n> ", :PROMPT_C => "%03n> " } @@ -211,13 +314,15 @@ class IRB::TestRendering < Yamatanooroti::TestCase puts 'start IRB' LINES start_terminal(4, 12, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') - write("Str\C-i") + write("IR") + write("\C-i") + sleep 0.2 close assert_screen(<<~EOC) - 001> String - PressString - StrinStruct - of by + start IRB + 001> IRB + PressIRB + IRB EOC end @@ -232,14 +337,176 @@ class IRB::TestRendering < Yamatanooroti::TestCase close assert_screen(<<~EOC) start IRB - irb(main):001:0> #{code} + irb(main):001> #{code} => [0, ... - irb(main):002:0> + irb(main):002> + EOC + end + + def test_ctrl_c_is_handled + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(40, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + # Assignment expression code that turns into non-assignment expression after evaluation + write("\C-c") + close + assert_screen(<<~EOC) + start IRB + irb(main):001> + ^C + irb(main):001> EOC end + def test_show_cmds_with_pager_can_quit_with_ctrl_c + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(40, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write("help\n") + write("G") # move to the end of the screen + write("\C-c") # quit pager + write("'foo' + 'bar'\n") # eval something to make sure IRB resumes + close + + screen = result.join("\n").sub(/\n*\z/, "\n") + # IRB::Abort should be rescued + assert_not_match(/IRB::Abort/, screen) + # IRB should resume + assert_match(/foobar/, screen) + end + + def test_pager_page_content_pages_output_when_it_does_not_fit_in_the_screen_because_of_total_length + write_irbrc <<~'LINES' + puts 'start IRB' + require "irb/pager" + LINES + start_terminal(10, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write("IRB::Pager.page_content('a' * (80 * 8))\n") + write("'foo' + 'bar'\n") # eval something to make sure IRB resumes + close + + screen = result.join("\n").sub(/\n*\z/, "\n") + assert_match(/a{80}/, screen) + # because pager is invoked, foobar will not be evaluated + assert_not_match(/foobar/, screen) + end + + def test_pager_page_content_pages_output_when_it_does_not_fit_in_the_screen_because_of_screen_height + write_irbrc <<~'LINES' + puts 'start IRB' + require "irb/pager" + LINES + start_terminal(10, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write("IRB::Pager.page_content('a\n' * 8)\n") + write("'foo' + 'bar'\n") # eval something to make sure IRB resumes + close + + screen = result.join("\n").sub(/\n*\z/, "\n") + assert_match(/(a\n){8}/, screen) + # because pager is invoked, foobar will not be evaluated + assert_not_match(/foobar/, screen) + end + + def test_pager_page_content_doesnt_page_output_when_it_fits_in_the_screen + write_irbrc <<~'LINES' + puts 'start IRB' + require "irb/pager" + LINES + start_terminal(10, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write("IRB::Pager.page_content('a' * (80 * 7))\n") + write("'foo' + 'bar'\n") # eval something to make sure IRB resumes + close + + screen = result.join("\n").sub(/\n*\z/, "\n") + assert_match(/a{80}/, screen) + # because pager is not invoked, foobar will be evaluated + assert_match(/foobar/, screen) + end + + def test_long_evaluation_output_is_paged + write_irbrc <<~'LINES' + puts 'start IRB' + require "irb/pager" + LINES + start_terminal(10, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write("'a' * 80 * 11\n") + write("'foo' + 'bar'\n") # eval something to make sure IRB resumes + close + + screen = result.join("\n").sub(/\n*\z/, "\n") + assert_match(/(a{80}\n){8}/, screen) + # because pager is invoked, foobar will not be evaluated + assert_not_match(/foobar/, screen) + end + + def test_long_evaluation_output_is_preserved_after_paging + write_irbrc <<~'LINES' + puts 'start IRB' + require "irb/pager" + LINES + start_terminal(10, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write("'a' * 80 * 11\n") + write("q") # quit pager + write("'foo' + 'bar'\n") # eval something to make sure IRB resumes + close + + screen = result.join("\n").sub(/\n*\z/, "\n") + # confirm pager has exited + assert_match(/foobar/, screen) + # confirm output is preserved + assert_match(/(a{80}\n){6}/, screen) + end + + def test_debug_integration_hints_debugger_commands + write_irbrc <<~'LINES' + IRB.conf[:USE_COLORIZE] = false + LINES + script = Tempfile.create(["debug", ".rb"]) + script.write <<~RUBY + puts 'start IRB' + binding.irb + RUBY + script.close + start_terminal(40, 80, %W{ruby -I#{@pwd}/lib #{script.to_path}}, startup_message: 'start IRB') + write("debug\n") + write("pp 1\n") + write("pp 1") + close + + screen = result.join("\n").sub(/\n*\z/, "\n") + # submitted input shouldn't contain hint + assert_include(screen, "irb:rdbg(main):002> pp 1\n") + # unsubmitted input should contain hint + assert_include(screen, "irb:rdbg(main):003> pp 1 # debug command\n") + ensure + File.unlink(script) if script + end + + def test_debug_integration_doesnt_hint_non_debugger_commands + write_irbrc <<~'LINES' + IRB.conf[:USE_COLORIZE] = false + LINES + script = Tempfile.create(["debug", ".rb"]) + script.write <<~RUBY + puts 'start IRB' + binding.irb + RUBY + script.close + start_terminal(40, 80, %W{ruby -I#{@pwd}/lib #{script.to_path}}, startup_message: 'start IRB') + write("debug\n") + write("foo") + close + + screen = result.join("\n").sub(/\n*\z/, "\n") + assert_include(screen, "irb:rdbg(main):002> foo\n") + ensure + File.unlink(script) if script + end + private def write_irbrc(content) |