diff options
Diffstat (limited to 'test/irb')
29 files changed, 6441 insertions, 1699 deletions
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..df3753dae7 --- /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 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 new file mode 100644 index 0000000000..1614b42adb --- /dev/null +++ b/test/irb/helper.rb @@ -0,0 +1,219 @@ +require "test/unit" +require "pathname" +require "rubygems" + +begin + require_relative "../lib/helper" + require_relative "../lib/envutil" +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 = []) + @line_no = 0 + @list = list + end + + def gets + @list[@line_no]&.tap {@line_no += 1} + end + + def eof? + @line_no >= @list.size + end + + def encoding + Encoding.default_external + end + + def reset + @line_no = 0 + end + end + + def ruby_core? + !Pathname(__dir__).join("../../", "irb.gemspec").exist? + end + + def save_encodings + @default_encoding = [Encoding.default_external, Encoding.default_internal] + @stdio_encodings = [STDIN, STDOUT, STDERR].map {|io| [io.external_encoding, io.internal_encoding] } + end + + def restore_encodings + EnvUtil.suppress_warning do + Encoding.default_external, Encoding.default_internal = *@default_encoding + [STDIN, STDOUT, STDERR].zip(@stdio_encodings) do |io, encs| + io.set_encoding(*encs) + end + end + end + + def without_rdoc(&block) + ::Kernel.send(:alias_method, :irb_original_require, :require) + + ::Kernel.define_method(:require) do |name| + raise LoadError, "cannot load such file -- rdoc (test)" if name.match?("rdoc") || name.match?(/^rdoc\/.*/) + ::Kernel.send(:irb_original_require, name) + end + + yield + ensure + EnvUtil.suppress_warning { + ::Kernel.send(:alias_method, :require, :irb_original_require) + ::Kernel.undef_method :irb_original_require + } + 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") + + PTY.spawn(@envs.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 + 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{irb- .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_cmd.rb b/test/irb/test_cmd.rb deleted file mode 100644 index 5b63e56800..0000000000 --- a/test/irb/test_cmd.rb +++ /dev/null @@ -1,429 +0,0 @@ -# frozen_string_literal: false -require "test/unit" -require "irb" -require "irb/extend-command" - -module TestIRB - class ExtendCommand < Test::Unit::TestCase - class TestInputMethod < ::IRB::InputMethod - attr_reader :list, :line_no - - def initialize(list = []) - super("test") - @line_no = 0 - @list = list - end - - def gets - @list[@line_no]&.tap {@line_no += 1} - end - - def eof? - @line_no >= @list.size - end - - def encoding - Encoding.default_external - end - - def reset - @line_no = 0 - end - end - - def setup - @pwd = Dir.pwd - @tmpdir = File.join(Dir.tmpdir, "test_reline_config_#{$$}") - begin - Dir.mkdir(@tmpdir) - rescue Errno::EEXIST - FileUtils.rm_rf(@tmpdir) - Dir.mkdir(@tmpdir) - end - Dir.chdir(@tmpdir) - @home_backup = ENV["HOME"] - ENV["HOME"] = @tmpdir - @xdg_config_home_backup = ENV.delete("XDG_CONFIG_HOME") - @default_encoding = [Encoding.default_external, Encoding.default_internal] - @stdio_encodings = [STDIN, STDOUT, STDERR].map {|io| [io.external_encoding, io.internal_encoding] } - IRB.instance_variable_get(:@CONF).clear - end - - def teardown - ENV["XDG_CONFIG_HOME"] = @xdg_config_home_backup - ENV["HOME"] = @home_backup - Dir.chdir(@pwd) - FileUtils.rm_rf(@tmpdir) - EnvUtil.suppress_warning { - Encoding.default_external, Encoding.default_internal = *@default_encoding - [STDIN, STDOUT, STDERR].zip(@stdio_encodings) do |io, encs| - io.set_encoding(*encs) - end - } - end - - def test_irb_info_multiline - FileUtils.touch("#{@tmpdir}/.inputrc") - FileUtils.touch("#{@tmpdir}/.irbrc") - IRB.setup(__FILE__, argv: []) - IRB.conf[:USE_MULTILINE] = true - IRB.conf[:USE_SINGLELINE] = false - IRB.conf[:VERBOSE] = false - workspace = IRB::WorkSpace.new(self) - irb = IRB::Irb.new(workspace, TestInputMethod.new([])) - IRB.conf[:MAIN_CONTEXT] = irb.context - expected = %r{ - Ruby\sversion: .+\n - IRB\sversion:\sirb .+\n - InputMethod:\sAbstract\sInputMethod\n - \.irbrc\spath: .+\n - RUBY_PLATFORM: .+ - }x - assert_match expected, irb.context.main.irb_info.to_s - end - - def test_irb_info_singleline - FileUtils.touch("#{@tmpdir}/.inputrc") - FileUtils.touch("#{@tmpdir}/.irbrc") - IRB.setup(__FILE__, argv: []) - IRB.conf[:USE_MULTILINE] = false - IRB.conf[:USE_SINGLELINE] = true - IRB.conf[:VERBOSE] = false - workspace = IRB::WorkSpace.new(self) - irb = IRB::Irb.new(workspace, TestInputMethod.new([])) - IRB.conf[:MAIN_CONTEXT] = irb.context - expected = %r{ - Ruby\sversion: .+\n - IRB\sversion:\sirb .+\n - InputMethod:\sAbstract\sInputMethod\n - \.irbrc\spath: .+\n - RUBY_PLATFORM: .+ - }x - assert_match expected, irb.context.main.irb_info.to_s - end - - def test_irb_info_multiline_without_rc_files - inputrc_backup = ENV["INPUTRC"] - ENV["INPUTRC"] = "unknown_inpurc" - ext_backup = IRB::IRBRC_EXT - IRB.__send__(:remove_const, :IRBRC_EXT) - IRB.const_set(:IRBRC_EXT, "unknown_ext") - IRB.setup(__FILE__, argv: []) - IRB.conf[:USE_MULTILINE] = true - IRB.conf[:USE_SINGLELINE] = false - IRB.conf[:VERBOSE] = false - workspace = IRB::WorkSpace.new(self) - irb = IRB::Irb.new(workspace, TestInputMethod.new([])) - IRB.conf[:MAIN_CONTEXT] = irb.context - expected = %r{ - Ruby\sversion: .+\n - IRB\sversion:\sirb .+\n - InputMethod:\sAbstract\sInputMethod\n - RUBY_PLATFORM: .+\n - \z - }x - assert_match expected, irb.context.main.irb_info.to_s - ensure - ENV["INPUTRC"] = inputrc_backup - IRB.__send__(:remove_const, :IRBRC_EXT) - IRB.const_set(:IRBRC_EXT, ext_backup) - end - - def test_irb_info_singleline_without_rc_files - inputrc_backup = ENV["INPUTRC"] - ENV["INPUTRC"] = "unknown_inpurc" - ext_backup = IRB::IRBRC_EXT - IRB.__send__(:remove_const, :IRBRC_EXT) - IRB.const_set(:IRBRC_EXT, "unknown_ext") - IRB.setup(__FILE__, argv: []) - IRB.conf[:USE_MULTILINE] = false - IRB.conf[:USE_SINGLELINE] = true - IRB.conf[:VERBOSE] = false - workspace = IRB::WorkSpace.new(self) - irb = IRB::Irb.new(workspace, TestInputMethod.new([])) - IRB.conf[:MAIN_CONTEXT] = irb.context - expected = %r{ - Ruby\sversion: .+\n - IRB\sversion:\sirb .+\n - InputMethod:\sAbstract\sInputMethod\n - RUBY_PLATFORM: .+\n - \z - }x - assert_match expected, irb.context.main.irb_info.to_s - ensure - ENV["INPUTRC"] = inputrc_backup - IRB.__send__(:remove_const, :IRBRC_EXT) - IRB.const_set(:IRBRC_EXT, ext_backup) - end - - def test_measure - IRB.init_config(nil) - IRB.conf[:PROMPT] = { - DEFAULT: { - PROMPT_I: '> ', - PROMPT_S: '> ', - PROMPT_C: '> ', - PROMPT_N: '> ' - } - } - IRB.conf[:VERBOSE] = false - IRB.conf[:PROMPT_MODE] = :DEFAULT - IRB.conf[:MEASURE] = false - input = TestInputMethod.new([ - "3\n", - "measure\n", - "3\n", - "measure :off\n", - "3\n", - ]) - c = Class.new(Object) - irb = IRB::Irb.new(IRB::WorkSpace.new(c.new), input) - irb.context.return_format = "=> %s\n" - out, err = capture_output do - irb.eval_input - end - assert_empty err - assert_match(/\A=> 3\nTIME is added\.\n=> nil\nprocessing time: .+\n=> 3\n=> nil\n=> 3\n/, out) - assert_empty(c.class_variables) - end - - def test_measure_enabled_by_rc - IRB.init_config(nil) - IRB.conf[:PROMPT] = { - DEFAULT: { - PROMPT_I: '> ', - PROMPT_S: '> ', - PROMPT_C: '> ', - PROMPT_N: '> ' - } - } - IRB.conf[:VERBOSE] = false - IRB.conf[:PROMPT_MODE] = :DEFAULT - IRB.conf[:MEASURE] = true - input = TestInputMethod.new([ - "3\n", - "measure :off\n", - "3\n", - ]) - irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) - irb.context.return_format = "=> %s\n" - out, err = capture_output do - irb.eval_input - end - assert_empty err - assert_match(/\Aprocessing time: .+\n=> 3\n=> nil\n=> 3\n/, out) - end - - def test_measure_enabled_by_rc_with_custom - IRB.init_config(nil) - IRB.conf[:PROMPT] = { - DEFAULT: { - PROMPT_I: '> ', - PROMPT_S: '> ', - PROMPT_C: '> ', - PROMPT_N: '> ' - } - } - IRB.conf[:VERBOSE] = false - IRB.conf[:PROMPT_MODE] = :DEFAULT - IRB.conf[:MEASURE] = true - IRB.conf[:MEASURE_PROC][:CUSTOM] = proc { |line, line_no, &block| - time = Time.now - result = block.() - puts 'custom processing time: %fs' % (Time.now - time) if IRB.conf[:MEASURE] - result - } - input = TestInputMethod.new([ - "3\n", - "measure :off\n", - "3\n", - ]) - irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) - irb.context.return_format = "=> %s\n" - out, err = capture_output do - irb.eval_input - end - assert_empty err - assert_match(/\Acustom processing time: .+\n=> 3\n=> nil\n=> 3\n/, out) - end - - def test_measure_with_custom - IRB.init_config(nil) - IRB.conf[:PROMPT] = { - DEFAULT: { - PROMPT_I: '> ', - PROMPT_S: '> ', - PROMPT_C: '> ', - PROMPT_N: '> ' - } - } - IRB.conf[:VERBOSE] = false - IRB.conf[:PROMPT_MODE] = :DEFAULT - IRB.conf[:MEASURE] = false - IRB.conf[:MEASURE_PROC][:CUSTOM] = proc { |line, line_no, &block| - time = Time.now - result = block.() - puts 'custom processing time: %fs' % (Time.now - time) if IRB.conf[:MEASURE] - result - } - input = TestInputMethod.new([ - "3\n", - "measure\n", - "3\n", - "measure :off\n", - "3\n", - ]) - irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) - irb.context.return_format = "=> %s\n" - out, err = capture_output do - irb.eval_input - end - assert_empty err - 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 - IRB.init_config(nil) - IRB.conf[:PROMPT] = { - DEFAULT: { - PROMPT_I: '> ', - PROMPT_S: '> ', - PROMPT_C: '> ', - PROMPT_N: '> ' - } - } - IRB.conf[:VERBOSE] = false - IRB.conf[:PROMPT_MODE] = :DEFAULT - IRB.conf[:MEASURE] = false - input = TestInputMethod.new([ - "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", - "3\n", - "measure :off\n", - "3\n", - ]) - c = Class.new(Object) - irb = IRB::Irb.new(IRB::WorkSpace.new(c.new), input) - irb.context.return_format = "=> %s\n" - out, err = capture_output do - irb.eval_input - end - 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_empty(c.class_variables) - end - - def test_irb_source - IRB.init_config(nil) - File.write("#{@tmpdir}/a.rb", "a = 'hi'\n") - input = TestInputMethod.new([ - "a = 'bug17564'\n", - "a\n", - "irb_source '#{@tmpdir}/a.rb'\n", - "a\n", - ]) - IRB.conf[:VERBOSE] = false - IRB.conf[:PROMPT_MODE] = :SIMPLE - irb = IRB::Irb.new(IRB::WorkSpace.new(self), input) - IRB.conf[:MAIN_CONTEXT] = irb.context - out, err = capture_output do - irb.eval_input - end - assert_empty err - assert_pattern_list([ - /=> "bug17564"\n/, - /=> "bug17564"\n/, - / => "hi"\n/, - / => nil\n/, - /=> "hi"\n/, - ], out) - end - - def test_irb_load - IRB.init_config(nil) - File.write("#{@tmpdir}/a.rb", "a = 'hi'\n") - input = TestInputMethod.new([ - "a = 'bug17564'\n", - "a\n", - "irb_load '#{@tmpdir}/a.rb'\n", - "a\n", - ]) - IRB.conf[:VERBOSE] = false - IRB.conf[:PROMPT_MODE] = :SIMPLE - irb = IRB::Irb.new(IRB::WorkSpace.new(self), input) - IRB.conf[:MAIN_CONTEXT] = irb.context - out, err = capture_output do - irb.eval_input - end - assert_empty err - assert_pattern_list([ - /=> "bug17564"\n/, - /=> "bug17564"\n/, - / => "hi"\n/, - / => nil\n/, - /=> "bug17564"\n/, - ], out) - end - - def test_ls - input = TestInputMethod.new([ - "ls Object.new.tap { |o| o.instance_variable_set(:@a, 1) }\n", - ]) - IRB.init_config(nil) - workspace = IRB::WorkSpace.new(self) - IRB.conf[:VERBOSE] = false - irb = IRB::Irb.new(workspace, input) - IRB.conf[:MAIN_CONTEXT] = irb.context - irb.context.return_format = "=> %s\n" - out, err = capture_output do - irb.eval_input - end - assert_empty err - assert_match(/^instance variables:\s+@a\n/m, out) - end - - def test_show_source - input = TestInputMethod.new([ - "show_source 'IRB.conf'\n", - ]) - IRB.init_config(nil) - workspace = IRB::WorkSpace.new(self) - IRB.conf[:VERBOSE] = false - irb = IRB::Irb.new(workspace, input) - IRB.conf[:MAIN_CONTEXT] = irb.context - irb.context.return_format = "=> %s\n" - out, err = capture_output do - irb.eval_input - end - assert_empty err - assert_match(%r[/irb\.rb], out) - end - - def test_whereami - input = TestInputMethod.new([ - "whereami\n", - ]) - IRB.init_config(nil) - workspace = IRB::WorkSpace.new(self) - IRB.conf[:VERBOSE] = false - irb = IRB::Irb.new(workspace, input) - IRB.conf[:MAIN_CONTEXT] = irb.context - irb.context.return_format = "=> %s\n" - out, err = capture_output do - irb.eval_input - end - assert_empty err - assert_match(/^From: .+ @ line \d+ :\n/, out) - end - end -end diff --git a/test/irb/test_color.rb b/test/irb/test_color.rb index a28ae06117..9d78f5233e 100644 --- a/test/irb/test_color.rb +++ b/test/irb/test_color.rb @@ -1,11 +1,11 @@ # frozen_string_literal: false -require 'test/unit' require 'irb/color' -require 'rubygems' require 'stringio' +require_relative "helper" + module TestIRB - class TestColor < Test::Unit::TestCase + class ColorTest < TestCase CLEAR = "\e[0m" BOLD = "\e[1m" UNDERLINE = "\e[4m" @@ -17,6 +17,41 @@ module TestIRB MAGENTA = "\e[35m" CYAN = "\e[36m" + def setup + super + if IRB.respond_to?(:conf) + @colorize, IRB.conf[:USE_COLORIZE] = IRB.conf[:USE_COLORIZE], true + end + end + + def teardown + if instance_variable_defined?(:@colorize) + IRB.conf[:USE_COLORIZE] = @colorize + end + super + end + + def test_colorize + text = "text" + { + [:BOLD] => "#{BOLD}#{text}#{CLEAR}", + [:UNDERLINE] => "#{UNDERLINE}#{text}#{CLEAR}", + [:REVERSE] => "#{REVERSE}#{text}#{CLEAR}", + [:RED] => "#{RED}#{text}#{CLEAR}", + [:GREEN] => "#{GREEN}#{text}#{CLEAR}", + [:YELLOW] => "#{YELLOW}#{text}#{CLEAR}", + [:BLUE] => "#{BLUE}#{text}#{CLEAR}", + [:MAGENTA] => "#{MAGENTA}#{text}#{CLEAR}", + [:CYAN] => "#{CYAN}#{text}#{CLEAR}", + }.each do |seq, result| + assert_equal_with_term(result, text, seq: seq) + + assert_equal_with_term(text, text, seq: seq, tty: false) + assert_equal_with_term(text, text, seq: seq, colorable: false) + assert_equal_with_term(result, text, seq: seq, tty: false, colorable: true) + end + end + def test_colorize_code # Common behaviors. Warn parser error, but do not warn compile error. tests = { @@ -54,6 +89,7 @@ module TestIRB ":class" => "#{YELLOW}:#{CLEAR}#{YELLOW}class#{CLEAR}", "[:end, 2]" => "[#{YELLOW}:#{CLEAR}#{YELLOW}end#{CLEAR}, #{BLUE}#{BOLD}2#{CLEAR}]", "[:>, 3]" => "[#{YELLOW}:#{CLEAR}#{YELLOW}>#{CLEAR}, #{BLUE}#{BOLD}3#{CLEAR}]", + "[:`, 4]" => "[#{YELLOW}:#{CLEAR}#{YELLOW}`#{CLEAR}, #{BLUE}#{BOLD}4#{CLEAR}]", ":Hello ? world : nil" => "#{YELLOW}:#{CLEAR}#{YELLOW}Hello#{CLEAR} ? world : #{CYAN}#{BOLD}nil#{CLEAR}", 'raise "foo#{bar}baz"' => "raise #{RED}#{BOLD}\"#{CLEAR}#{RED}foo#{CLEAR}#{RED}\#{#{CLEAR}bar#{RED}}#{CLEAR}#{RED}baz#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}", '["#{obj.inspect}"]' => "[#{RED}#{BOLD}\"#{CLEAR}#{RED}\#{#{CLEAR}obj.inspect#{RED}}#{CLEAR}#{RED}#{BOLD}\"#{CLEAR}]", @@ -63,20 +99,20 @@ 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}", + "foo\n__END__\nbar" => "foo\n#{GREEN}__END__#{CLEAR}\nbar", + "foo\n<<A\0\0bar\nA\nbaz" => "foo\n#{RED}<<A#{CLEAR}^@^@bar\n#{RED}A#{CLEAR}\nbaz", + "<<A+1\nA" => "#{RED}<<A#{CLEAR}+#{BLUE}#{BOLD}1#{CLEAR}\n#{RED}A#{CLEAR}", } - # specific to Ruby 2.7+ - if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7.0') - tests.merge!({ - "4.5.6" => "#{MAGENTA}#{BOLD}4.5#{CLEAR}#{RED}#{REVERSE}.6#{CLEAR}", - "\e[0m\n" => "#{RED}#{REVERSE}^[#{CLEAR}[#{BLUE}#{BOLD}0#{CLEAR}#{RED}#{REVERSE}m#{CLEAR}\n", - "<<EOS\nhere\nEOS" => "#{RED}<<EOS#{CLEAR}\n#{RED}here#{CLEAR}\n#{RED}EOS#{CLEAR}", - }) - end + tests.merge!({ + "4.5.6" => "#{MAGENTA}#{BOLD}4.5#{CLEAR}#{RED}#{REVERSE}.6#{CLEAR}", + "\e[0m\n" => "#{RED}#{REVERSE}^[#{CLEAR}[#{BLUE}#{BOLD}0#{CLEAR}#{RED}#{REVERSE}m#{CLEAR}\n", + "<<EOS\nhere\nEOS" => "#{RED}<<EOS#{CLEAR}\n#{RED}here#{CLEAR}\n#{RED}EOS#{CLEAR}", + }) # specific to Ruby 3.0+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0') @@ -90,12 +126,15 @@ module TestIRB "class bad; end" => "#{GREEN}class#{CLEAR} #{RED}#{REVERSE}bad#{CLEAR}; #{GREEN}end#{CLEAR}", "def req(@a) end" => "#{GREEN}def#{CLEAR} #{BLUE}#{BOLD}req#{CLEAR}(#{RED}#{REVERSE}@a#{CLEAR}) #{GREEN}end#{CLEAR}", }) - else - tests.merge!({ - "[1]]]\u0013" => "[1]]]^S", + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2.0') + tests.merge!({ + "def req(true) end" => "#{GREEN}def#{CLEAR} #{BLUE}#{BOLD}req#{CLEAR}(#{RED}#{REVERSE}true#{CLEAR}#{RED}#{REVERSE})#{CLEAR} #{RED}#{REVERSE}end#{CLEAR}", }) + end + else tests.merge!({ - "def req(true) end" => "def req(true) end", + "[1]]]\u0013" => "[#{BLUE}#{BOLD}1#{CLEAR}]#{RED}#{REVERSE}]#{CLEAR}]^S", + "def req(true) end" => "#{GREEN}def#{CLEAR} #{BLUE}#{BOLD}req#{CLEAR}(#{RED}#{REVERSE}true#{CLEAR}) end", "nil = 1" => "#{CYAN}#{BOLD}nil#{CLEAR} = #{BLUE}#{BOLD}1#{CLEAR}", "alias $x $1" => "#{GREEN}alias#{CLEAR} #{GREEN}#{BOLD}$x#{CLEAR} $1", "class bad; end" => "#{GREEN}class#{CLEAR} bad; #{GREEN}end#{CLEAR}", @@ -104,31 +143,46 @@ module TestIRB end tests.each do |code, result| - if colorize_code_supported? - actual = with_term { IRB::Color.colorize_code(code, complete: true) } - assert_equal(result, actual, "Case: IRB::Color.colorize_code(#{code.dump}, complete: true)\nResult: #{humanized_literal(actual)}") + assert_equal_with_term(result, code, complete: true) + assert_equal_with_term(result, code, complete: false) - actual = with_term { IRB::Color.colorize_code(code, complete: false) } - assert_equal(result, actual, "Case: IRB::Color.colorize_code(#{code.dump}, complete: false)\nResult: #{humanized_literal(actual)}") - else - actual = with_term { IRB::Color.colorize_code(code) } - assert_equal(code, actual) - end + assert_equal_with_term(code, code, complete: true, tty: false) + assert_equal_with_term(code, code, complete: false, tty: false) + + assert_equal_with_term(code, code, complete: true, colorable: false) + + assert_equal_with_term(code, code, complete: false, colorable: false) + + assert_equal_with_term(result, code, complete: true, tty: false, colorable: true) + + assert_equal_with_term(result, code, complete: false, tty: false, colorable: true) end end - def test_colorize_code_complete_true - unless complete_option_supported? - skip '`complete: true` is the same as `complete: false` in Ruby 2.6-' - end + def test_colorize_code_with_local_variables + code = "a /(b +1)/i" + result_without_lvars = "a #{RED}#{BOLD}/#{CLEAR}#{RED}(b +1)#{CLEAR}#{RED}#{BOLD}/i#{CLEAR}" + result_with_lvar = "a /(b #{BLUE}#{BOLD}+1#{CLEAR})/i" + result_with_lvars = "a /(b +#{BLUE}#{BOLD}1#{CLEAR})/i" + assert_equal_with_term(result_without_lvars, code) + assert_equal_with_term(result_with_lvar, code, local_variables: ['a']) + assert_equal_with_term(result_with_lvars, code, local_variables: ['a', 'b']) + end + + def test_colorize_code_complete_true # `complete: true` behaviors. Warn end-of-file. { "'foo' + 'bar" => "#{RED}#{BOLD}'#{CLEAR}#{RED}foo#{CLEAR}#{RED}#{BOLD}'#{CLEAR} + #{RED}#{BOLD}'#{CLEAR}#{RED}#{REVERSE}bar#{CLEAR}", "('foo" => "(#{RED}#{BOLD}'#{CLEAR}#{RED}#{REVERSE}foo#{CLEAR}", }.each do |code, result| - actual = with_term { IRB::Color.colorize_code(code, complete: true) } - assert_equal(result, actual, "Case: colorize_code(#{code.dump}, complete: true)\nResult: #{humanized_literal(actual)}") + assert_equal_with_term(result, code, complete: true) + + assert_equal_with_term(code, code, complete: true, tty: false) + + assert_equal_with_term(code, code, complete: true, colorable: false) + + assert_equal_with_term(result, code, complete: true, tty: false, colorable: true) end end @@ -138,18 +192,13 @@ module TestIRB "'foo' + 'bar" => "#{RED}#{BOLD}'#{CLEAR}#{RED}foo#{CLEAR}#{RED}#{BOLD}'#{CLEAR} + #{RED}#{BOLD}'#{CLEAR}#{RED}bar#{CLEAR}", "('foo" => "(#{RED}#{BOLD}'#{CLEAR}#{RED}foo#{CLEAR}", }.each do |code, result| - if colorize_code_supported? - actual = with_term { IRB::Color.colorize_code(code, complete: false) } - assert_equal(result, actual, "Case: colorize_code(#{code.dump}, complete: false)\nResult: #{humanized_literal(actual)}") - - unless complete_option_supported? - actual = with_term { IRB::Color.colorize_code(code, complete: true) } - assert_equal(result, actual, "Case: colorize_code(#{code.dump}, complete: false)\nResult: #{humanized_literal(actual)}") - end - else - actual = with_term { IRB::Color.colorize_code(code) } - assert_equal(code, actual) - end + assert_equal_with_term(result, code, complete: false) + + assert_equal_with_term(code, code, complete: false, tty: false) + + assert_equal_with_term(code, code, complete: false, colorable: false) + + assert_equal_with_term(result, code, complete: false, tty: false, colorable: true) end end @@ -175,20 +224,10 @@ module TestIRB private - # `#colorize_code` is supported only for Ruby 2.5+. It just returns the original code in 2.4-. - def colorize_code_supported? - Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5.0') - end - - # `complete: true` is the same as `complete: false` in Ruby 2.6- - def complete_option_supported? - Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7.0') - end - - def with_term + def with_term(tty: true) stdout = $stdout io = StringIO.new - def io.tty?; true; end + def io.tty?; true; end if tty $stdout = io env = ENV.to_h.dup @@ -200,6 +239,23 @@ module TestIRB ENV.replace(env) if env end + def assert_equal_with_term(result, code, seq: nil, tty: true, **opts) + actual = with_term(tty: tty) do + if seq + IRB::Color.colorize(code, seq, **opts) + else + IRB::Color.colorize_code(code, **opts) + end + end + message = -> { + args = [code.dump] + args << seq.inspect if seq + opts.each {|kwd, val| args << "#{kwd}: #{val}"} + "Case: colorize#{seq ? "" : "_code"}(#{args.join(', ')})\nResult: #{humanized_literal(actual)}" + } + assert_equal(result, actual, message) + end + def humanized_literal(str) str .gsub(CLEAR, '@@@{CLEAR}') diff --git a/test/irb/test_color_printer.rb b/test/irb/test_color_printer.rb index 1afc7ccf55..c2c624d868 100644 --- a/test/irb/test_color_printer.rb +++ b/test/irb/test_color_printer.rb @@ -1,11 +1,11 @@ # frozen_string_literal: false -require 'test/unit' require 'irb/color_printer' -require 'rubygems' require 'stringio' +require_relative "helper" + module TestIRB - class TestColorPrinter < Test::Unit::TestCase + class ColorPrinterTest < TestCase CLEAR = "\e[0m" BOLD = "\e[1m" RED = "\e[31m" @@ -14,6 +14,10 @@ module TestIRB CYAN = "\e[36m" def setup + super + if IRB.respond_to?(:conf) + @colorize, IRB.conf[:USE_COLORIZE] = IRB.conf[:USE_COLORIZE], true + end @get_screen_size = Reline.method(:get_screen_size) Reline.instance_eval { undef :get_screen_size } def Reline.get_screen_size @@ -24,18 +28,19 @@ module TestIRB def teardown Reline.instance_eval { undef :get_screen_size } Reline.define_singleton_method(:get_screen_size, @get_screen_size) + if instance_variable_defined?(:@colorize) + IRB.conf[:USE_COLORIZE] = @colorize + end + super end IRBTestColorPrinter = Struct.new(:a) def test_color_printer - unless ripper_lexer_scan_supported? - skip 'Ripper::Lexer#scan is supported in Ruby 2.7+' - end { 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| @@ -46,10 +51,6 @@ module TestIRB private - def ripper_lexer_scan_supported? - Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7.0') - end - def with_term stdout = $stdout io = StringIO.new diff --git a/test/irb/test_command.rb b/test/irb/test_command.rb new file mode 100644 index 0000000000..8bf95c1074 --- /dev/null +++ b/test/irb/test_command.rb @@ -0,0 +1,1043 @@ +# frozen_string_literal: false +require "irb" + +require_relative "helper" + +module TestIRB + class CommandTestCase < TestCase + def setup + @pwd = Dir.pwd + @tmpdir = File.join(Dir.tmpdir, "test_reline_config_#{$$}") + begin + Dir.mkdir(@tmpdir) + rescue Errno::EEXIST + FileUtils.rm_rf(@tmpdir) + Dir.mkdir(@tmpdir) + end + Dir.chdir(@tmpdir) + @home_backup = ENV["HOME"] + ENV["HOME"] = @tmpdir + @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 + + def teardown + ENV["XDG_CONFIG_HOME"] = @xdg_config_home_backup + ENV["HOME"] = @home_backup + Dir.chdir(@pwd) + FileUtils.rm_rf(@tmpdir) + restore_encodings + end + + def execute_lines(*lines, conf: {}, main: self, irb_path: nil) + 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 + end + + class FrozenObjectTest < CommandTestCase + def test_calling_command_on_a_frozen_main + main = Object.new.freeze + + out, err = execute_lines( + "irb_info", + main: main + ) + assert_empty(err) + assert_match(/RUBY_PLATFORM/, out) + end + end + + class InfoTest < CommandTestCase + def setup + super + @locals_backup = ENV.delete("LANG"), ENV.delete("LC_ALL") + end + + def teardown + super + ENV["LANG"], ENV["LC_ALL"] = @locals_backup + end + + def test_irb_info_multiline + FileUtils.touch("#{@tmpdir}/.inputrc") + FileUtils.touch("#{@tmpdir}/.irbrc") + FileUtils.touch("#{@tmpdir}/_irbrc") + + out, err = execute_lines( + "irb_info", + conf: { USE_MULTILINE: true, USE_SINGLELINE: false } + ) + + expected = %r{ + Ruby\sversion:\s.+\n + IRB\sversion:\sirb\s.+\n + InputMethod:\sAbstract\sInputMethod\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' : ''} + }x + + assert_empty err + assert_match expected, out + end + + def test_irb_info_singleline + FileUtils.touch("#{@tmpdir}/.inputrc") + FileUtils.touch("#{@tmpdir}/.irbrc") + + out, err = execute_lines( + "irb_info", + conf: { USE_MULTILINE: false, USE_SINGLELINE: true } + ) + + expected = %r{ + Ruby\sversion:\s.+\n + IRB\sversion:\sirb\s.+\n + InputMethod:\sAbstract\sInputMethod\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' : ''} + }x + + assert_empty err + assert_match expected, out + end + + def test_irb_info_multiline_without_rc_files + inputrc_backup = ENV["INPUTRC"] + ENV["INPUTRC"] = "unknown_inpurc" + ext_backup = IRB::IRBRC_EXT + IRB.__send__(:remove_const, :IRBRC_EXT) + IRB.const_set(:IRBRC_EXT, "unknown_ext") + + out, err = execute_lines( + "irb_info", + conf: { USE_MULTILINE: true, USE_SINGLELINE: false } + ) + + expected = %r{ + 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' : ''} + }x + + assert_empty err + assert_match expected, out + ensure + ENV["INPUTRC"] = inputrc_backup + IRB.__send__(:remove_const, :IRBRC_EXT) + IRB.const_set(:IRBRC_EXT, ext_backup) + end + + def test_irb_info_singleline_without_rc_files + inputrc_backup = ENV["INPUTRC"] + ENV["INPUTRC"] = "unknown_inpurc" + ext_backup = IRB::IRBRC_EXT + IRB.__send__(:remove_const, :IRBRC_EXT) + IRB.const_set(:IRBRC_EXT, "unknown_ext") + + out, err = execute_lines( + "irb_info", + conf: { USE_MULTILINE: false, USE_SINGLELINE: true } + ) + + expected = %r{ + 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' : ''} + }x + + assert_empty err + assert_match expected, out + ensure + ENV["INPUTRC"] = inputrc_backup + IRB.__send__(:remove_const, :IRBRC_EXT) + IRB.const_set(:IRBRC_EXT, ext_backup) + end + + def test_irb_info_lang + FileUtils.touch("#{@tmpdir}/.inputrc") + FileUtils.touch("#{@tmpdir}/.irbrc") + ENV["LANG"] = "ja_JP.UTF-8" + ENV["LC_ALL"] = "en_US.UTF-8" + + out, err = execute_lines( + "irb_info", + conf: { USE_MULTILINE: true, USE_SINGLELINE: false } + ) + + expected = %r{ + Ruby\sversion: .+\n + IRB\sversion:\sirb .+\n + InputMethod:\sAbstract\sInputMethod\n + Completion: .+\n + \.irbrc\spaths: .+\n + RUBY_PLATFORM: .+\n + LANG\senv:\sja_JP\.UTF-8\n + LC_ALL\senv:\sen_US\.UTF-8\n + East\sAsian\sAmbiguous\sWidth:\s\d\n + }x + + assert_empty err + assert_match expected, out + end + end + + class CustomCommandTestCase < CommandTestCase + def setup + @commands_backup = IRB::Command.commands + IRB::Command.class_variable_set(:@@command_override_policies, nil) + end + + def teardown + IRB::Command.class_variable_set(:@@command_override_policies, nil) + IRB::Command.instance_variable_set(:@commands, @commands_backup) + end + end + + class CommandArgTest < CustomCommandTestCase + class PrintArgCommand < IRB::Command::Base + category 'CommandTest' + description 'print_command_arg' + def execute(arg) + puts "arg=#{arg.inspect}" + end + end + + def test_arg + IRB::Command._register_with_aliases(:print_arg, PrintArgCommand, [:pa, IRB::Command::OVERRIDE_ALL]) + out, err = execute_lines("print_arg\n") + assert_empty err + assert_include(out, 'arg=""') + + out, err = execute_lines("print_arg \n") + assert_empty err + assert_include(out, 'arg=""') + + out, err = execute_lines("print_arg a r g\n") + assert_empty err + assert_include(out, 'arg="a r g"') + + out, err = execute_lines("print_arg a r g \n") + assert_empty err + assert_include(out, 'arg="a r g"') + + out, err = execute_lines("pa a r g \n") + assert_empty err + assert_include(out, 'arg="a r g"') + end + end + + class ExtendCommandBundleCompatibilityTest < CustomCommandTestCase + class FooBarCommand < IRB::Command::Base + category 'FooBarCategory' + description 'foobar_description' + def execute(_arg) + puts "FooBar executed" + end + end + + def test_def_extend_command + IRB::ExtendCommandBundle.def_extend_command(:foobar, FooBarCommand, nil, [:fbalias, IRB::Command::OVERRIDE_ALL]) + out, err = execute_lines("foobar\n") + assert_empty err + assert_include(out, "FooBar executed") + + out, err = execute_lines("fbalias\n") + assert_empty err + assert_include(out, "FooBar executed") + + out, err = execute_lines("show_cmds\n") + assert_include(out, "FooBarCategory") + assert_include(out, "foobar_description") + end + end + + class MeasureTest < CommandTestCase + def test_measure + 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", + "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(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 + + def test_measure_enabled_by_rc + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: true + } + + out, err = execute_lines( + "3\n", + "measure :off\n", + "3\n", + conf: conf, + ) + + assert_empty err + assert_match(/\Aprocessing time: .+\n=> 3\n=> nil\n=> 3\n/, out) + end + + def test_measure_enabled_by_rc_with_custom + measuring_proc = proc { |line, line_no, &block| + time = Time.now + result = block.() + puts 'custom processing time: %fs' % (Time.now - time) if IRB.conf[:MEASURE] + result + } + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: true, + MEASURE_PROC: { CUSTOM: measuring_proc } + } + + out, err = execute_lines( + "3\n", + "measure :off\n", + "3\n", + conf: conf, + ) + assert_empty err + assert_match(/\Acustom processing time: .+\n=> 3\n=> nil\n=> 3\n/, out) + end + + def test_measure_with_custom + measuring_proc = proc { |line, line_no, &block| + time = Time.now + result = block.() + puts 'custom processing time: %fs' % (Time.now - time) if IRB.conf[:MEASURE] + result + } + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: false, + MEASURE_PROC: { CUSTOM: measuring_proc } + } + out, err = execute_lines( + "3\n", + "measure\n", + "3\n", + "measure :off\n", + "3\n", + conf: conf + ) + + assert_empty err + assert_match(/\A=> 3\nCUSTOM is added\.\n=> nil\ncustom processing time: .+\n=> 3\n=> nil\n=> 3\n/, out) + end + + def test_measure_toggle + conf = { + PROMPT: { + DEFAULT: { + PROMPT_I: '> ', + PROMPT_S: '> ', + PROMPT_C: '> ' + } + }, + PROMPT_MODE: :DEFAULT, + MEASURE: false, + MEASURE_PROC: { + FOO: proc { |&block| puts 'foo'; block.call }, + BAR: proc { |&block| puts 'bar'; block.call } + } + } + out, err = execute_lines( + "measure :foo\n", + "1\n", + "measure :on, :bar\n", + "2\n", + "measure :off, :foo\n", + "3\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 do\n", + "3\n", + conf: conf, + main: c + ) + + assert_match(/to add custom measure/, err) + assert_match(/\A=> 3\n=> nil\n=> 3\n/, out) + assert_empty(c.class_variables) + end + end + + class IrbSourceTest < CommandTestCase + def test_irb_source + File.write("#{@tmpdir}/a.rb", "a = 'hi'\n") + out, err = execute_lines( + "a = 'bug17564'\n", + "a\n", + "irb_source '#{@tmpdir}/a.rb'\n", + "a\n", + ) + assert_empty err + assert_pattern_list([ + /=> "bug17564"\n/, + /=> "bug17564"\n/, + / => "hi"\n/, + / => nil\n/, + /=> "hi"\n/, + ], out) + end + + def test_irb_source_without_argument + out, err = execute_lines( + "irb_source\n", + ) + assert_empty err + assert_match(/Please specify the file name./, out) + end + end + + class IrbLoadTest < CommandTestCase + def test_irb_load + File.write("#{@tmpdir}/a.rb", "a = 'hi'\n") + out, err = execute_lines( + "a = 'bug17564'\n", + "a\n", + "irb_load '#{@tmpdir}/a.rb'\n", + "a\n", + ) + assert_empty err + assert_pattern_list([ + /=> "bug17564"\n/, + /=> "bug17564"\n/, + / => "hi"\n/, + / => nil\n/, + /=> "bug17564"\n/, + ], out) + end + + def test_irb_load_without_argument + out, err = execute_lines( + "irb_load\n", + ) + + assert_empty err + assert_match(/Please specify the file name./, out) + end + end + + class WorkspaceCommandTestCase < CommandTestCase + def setup + super + # create Foo under the test class's namespace so it doesn't pollute global namespace + self.class.class_eval <<~RUBY + class Foo; end + RUBY + end + end + + class CwwsTest < WorkspaceCommandTestCase + def test_cwws_returns_the_current_workspace_object + out, err = execute_lines( + "cwws", + "self.class" + ) + + assert_empty err + assert_include(out, self.class.name) + 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", + "self.class", + "popws", + "self.class" + ) + assert_empty err + + 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", + "self.singleton_class.ancestors" + ) + assert_empty err + assert_include(out, "IRB::ExtendCommandBundle") + end + + def test_pushws_prints_workspace_stack_when_no_arg_is_given + out, err = execute_lines( + "pushws", + ) + assert_empty err + assert_include(out, "[#<TestIRB::PushwsTe...>]") + end + + def test_pushws_without_argument_swaps_the_top_two_workspaces + out, err = execute_lines( + "pushws #{self.class}::Foo.new", + "self.class", + "pushws", + "self.class" + ) + assert_empty err + assert_match(/=> #{self.class}::Foo\n/, out) + assert_match(/=> #{self.class}\n$/, out) + end + end + + class WorkspacesTest < WorkspaceCommandTestCase + def test_workspaces_returns_the_stack_of_workspaces + out, err = execute_lines( + "pushws #{self.class}::Foo.new\n", + "workspaces", + ) + + assert_empty err + assert_match(/\[#<TestIRB::Workspac...>, #<TestIRB::Workspac...>\]\n/, out) + end + end + + class PopwsTest < WorkspaceCommandTestCase + def test_popws_replaces_the_current_workspace_with_the_previous_one + out, err = execute_lines( + "pushws Foo.new\n", + "popws\n", + "cwws\n", + "_.class", + ) + assert_empty err + assert_include(out, "=> #{self.class}") + end + + def test_popws_prints_help_message_if_the_workspace_is_empty + out, err = execute_lines( + "popws\n", + ) + assert_empty err + assert_match(/\[#<TestIRB::PopwsTes...>\]\n/, out) + end + end + + class ChwsTest < WorkspaceCommandTestCase + def test_chws_replaces_the_current_workspace + out, err = execute_lines( + "chws #{self.class}::Foo.new\n", + "cwws\n", + "_.class", + ) + assert_empty err + assert_include(out, "=> #{self.class}::Foo") + end + + def test_chws_does_nothing_when_receiving_no_argument + out, err = execute_lines( + "chws\n", + "cwws\n", + "_.class", + ) + assert_empty err + assert_include(out, "=> #{self.class}") + end + end + + class WhereamiTest < CommandTestCase + def test_whereami + out, err = execute_lines( + "whereami\n", + ) + assert_empty err + assert_match(/^From: .+ @ line \d+ :\n/, out) + end + + def test_whereami_alias + out, err = execute_lines( + "@\n", + ) + assert_empty err + assert_match(/^From: .+ @ line \d+ :\n/, out) + end + end + + class LsTest < CommandTestCase + def test_ls + out, err = execute_lines( + "class P\n", + " def m() end\n", + " def m2() end\n", + "end\n", + + "class C < P\n", + " def m1() end\n", + " def m2() end\n", + "end\n", + + "module M\n", + " def m1() end\n", + " def m3() end\n", + "end\n", + + "module M2\n", + " include M\n", + " def m4() end\n", + "end\n", + + "obj = C.new\n", + "obj.instance_variable_set(:@a, 1)\n", + "obj.extend M2\n", + "def obj.m5() end\n", + "ls obj\n", + ) + + assert_empty err + assert_match(/^instance variables:\s+@a\n/m, out) + assert_match(/P#methods:\s+m\n/m, out) + assert_match(/C#methods:\s+m2\n/m, out) + assert_match(/M#methods:\s+m1\s+m3\n/m, out) + assert_match(/M2#methods:\s+m4\n/m, out) + assert_match(/C.methods:\s+m5\n/m, out) + end + + def test_ls_class + out, err = execute_lines( + "module M1\n", + " def m2; end\n", + " def m3; end\n", + "end\n", + + "class C1\n", + " def m1; end\n", + " def m2; end\n", + "end\n", + + "class C2 < C1\n", + " include M1\n", + " def m3; end\n", + " def m4; end\n", + " def self.m3; end\n", + " def self.m5; end\n", + "end\n", + "ls C2" + ) + + assert_empty err + assert_match(/C2.methods:\s+m3\s+m5\n/, out) + assert_match(/C2#methods:\s+m3\s+m4\n.*M1#methods:\s+m2\n.*C1#methods:\s+m1\n/, out) + assert_not_match(/Module#methods/, out) + assert_not_match(/Class#methods/, out) + end + + def test_ls_module + out, err = execute_lines( + "module M1\n", + " def m1; end\n", + " def m2; end\n", + "end\n", + + "module M2\n", + " include M1\n", + " def m1; end\n", + " def m3; end\n", + " def self.m4; end\n", + "end\n", + "ls M2" + ) + + assert_empty err + assert_match(/M2\.methods:\s+m4\n/, out) + assert_match(/M2#methods:\s+m1\s+m3\n.*M1#methods:\s+m2\n/, out) + assert_not_match(/Module#methods/, out) + end + + def test_ls_instance + out, err = execute_lines( + "class Foo; def bar; end; end\n", + "ls Foo.new" + ) + + assert_empty err + assert_match(/Foo#methods:\s+bar/, out) + # don't duplicate + assert_not_match(/Foo#methods:\s+bar\n.*Foo#methods/, out) + end + + def test_ls_grep + out, err = execute_lines("ls 42\n") + assert_empty err + assert_match(/times/, out) + assert_match(/polar/, out) + + [ + "ls 42, grep: /times/\n", + "ls 42 -g times\n", + "ls 42 -G times\n", + ].each do |line| + out, err = execute_lines(line) + assert_empty err + assert_match(/times/, out) + assert_not_match(/polar/, out) + end + end + + def test_ls_grep_empty + out, err = execute_lines("ls\n") + assert_empty err + assert_match(/assert/, out) + assert_match(/refute/, out) + + [ + "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(/assert/, out) + assert_not_match(/refute/, out) + end + end + + def test_ls_with_no_singleton_class + out, err = execute_lines( + "ls 42", + ) + assert_empty err + assert_match(/Comparable#methods:\s+/, out) + assert_match(/Numeric#methods:\s+/, out) + assert_match(/Integer#methods:\s+/, out) + end + end + + class ShowDocTest < CommandTestCase + def test_show_doc + out, err = execute_lines( + "show_doc 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_not_include err, "[Deprecation]" + 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/command/help.rb" } + end + + def test_show_doc_without_rdoc + out, err = without_rdoc do + execute_lines( + "show_doc String#gsub\n", + "\n", + ) + end + + # if it fails to require rdoc, it only returns the command object + assert_match(/=> nil\n/, out) + 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/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["VISUAL"] = ": code" + ENV["EDITOR"] = ": code2" + end + + def teardown + ENV["VISUAL"] = @original_visual + ENV["EDITOR"] = @original_editor + end + + def test_edit_without_arg + out, err = execute_lines( + "edit", + irb_path: __FILE__ + ) + + assert_empty err + assert_match("path: #{__FILE__}", out) + 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__}" + ) + + assert_empty err + assert_match("path: #{__FILE__}", out) + assert_match("command: ': code'", out) + end + + def test_edit_with_non_existing_path + out, err = execute_lines( + "edit test_cmd_non_existing_path.rb" + ) + + assert_empty err + assert_match(/Can not find file: test_cmd_non_existing_path\.rb/, out) + end + + def test_edit_with_constant + out, err = execute_lines( + "edit IRB::Irb" + ) + + assert_empty err + assert_match(/path: .*\/lib\/irb\.rb/, out) + assert_match("command: ': code'", out) + end + + def test_edit_with_class_method + out, err = execute_lines( + "edit IRB.start" + ) + + assert_empty err + assert_match(/path: .*\/lib\/irb\.rb/, out) + assert_match("command: ': code'", out) + end + + def test_edit_with_instance_method + out, err = execute_lines( + "edit IRB::Irb#run" + ) + + assert_empty err + 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 535690ae22..5fe7952b3d 100644 --- a/test/irb/test_completion.rb +++ b/test/irb/test_completion.rb @@ -1,87 +1,311 @@ # frozen_string_literal: false -require "test/unit" +require "pathname" require "irb" +require_relative "helper" + module TestIRB - class TestCompletion < Test::Unit::TestCase - def test_nonstring_module_name - begin - require "irb/completion" - bug5938 = '[ruby-core:42244]' - bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] - cmds = bundle_exec + %W[-W0 -rirb -rirb/completion -e IRB.setup(__FILE__) - -e IRB.conf[:MAIN_CONTEXT]=IRB::Irb.new.context - -e module\sFoo;def\sself.name;//;end;end - -e IRB::InputCompletor::CompletionProc.call("[1].first.") - -- -f --] - status = assert_in_out_err(cmds, "", //, [], bug5938) - assert(status.success?, bug5938) - rescue LoadError - skip "cannot load irb/completion" - end + class CompletionTest < TestCase + def completion_candidates(target, bind) + IRB::RegexpCompletor.new.completion_candidates('', target, '', bind: bind) end - def test_complete_numeric - assert_include(IRB::InputCompletor.retrieve_completion_data("1r.positi", bind: binding), "1r.positive?") - assert_empty(IRB::InputCompletor.retrieve_completion_data("1i.positi", bind: binding)) + def doc_namespace(target, bind) + IRB::RegexpCompletor.new.doc_namespace('', target, '', bind: bind) end - def test_complete_symbol - _ = :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)) + 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 - def test_complete_symbol_failure - assert_nil(IRB::InputCompletor::PerfectMatchedProc.(":aiueo", bind: binding)) + class MethodCompletionTest < CompletionTest + def test_complete_string + assert_include(completion_candidates("'foo'.up", binding), "'foo'.upcase") + # completing 'foo bar'.up + 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(completion_candidates("/foo/.ma", binding), "/foo/.match") + # completing /foo bar/.ma + 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(completion_candidates("[].an", binding), "[].any?") + assert_equal("Array.any?", doc_namespace("[].any?", binding)) + end + + def test_complete_hash_and_proc + # hash + assert_include(completion_candidates("{}.an", binding), "{}.any?") + assert_equal(["Hash.any?", "Proc.any?"], doc_namespace("{}.any?", binding)) + + # proc + assert_include(completion_candidates("{}.bin", binding), "{}.binding") + assert_equal(["Hash.binding", "Proc.binding"], doc_namespace("{}.binding", binding)) + end + + def test_complete_numeric + assert_include(completion_candidates("1.positi", binding), "1.positive?") + assert_equal("Integer.positive?", doc_namespace("1.positive?", binding)) + + assert_include(completion_candidates("1r.positi", binding), "1r.positive?") + assert_equal("Rational.positive?", doc_namespace("1r.positive?", binding)) + + assert_include(completion_candidates("0xFFFF.positi", binding), "0xFFFF.positive?") + assert_equal("Integer.positive?", doc_namespace("0xFFFF.positive?", binding)) + + assert_empty(completion_candidates("1i.positi", binding)) + end + + def test_complete_symbol + 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(completion_candidates("String.ne", binding), "String.new") + assert_equal("String.new", doc_namespace("String.new", binding)) + end end - def test_complete_reserved_words - candidates = IRB::InputCompletor.retrieve_completion_data("de", bind: binding) - %w[def defined?].each do |word| - assert_include candidates, word + class RequireComepletionTest < CompletionTest + def test_complete_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::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 - candidates = IRB::InputCompletor.retrieve_completion_data("__", bind: binding) - %w[__ENCODING__ __LINE__ __FILE__].each do |word| - assert_include candidates, word + def test_complete_require_with_pathname_in_load_path + temp_dir = Dir.mktmpdir + File.write(File.join(temp_dir, "foo.rb"), "test") + test_path = Pathname.new(temp_dir) + $LOAD_PATH << test_path + + candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'foo", "", bind: binding) + assert_include candidates, "'foo" + ensure + $LOAD_PATH.pop if test_path + FileUtils.remove_entry(temp_dir) if temp_dir + end + + def test_complete_require_with_string_convertable_in_load_path + temp_dir = Dir.mktmpdir + File.write(File.join(temp_dir, "foo.rb"), "test") + object = Object.new + object.define_singleton_method(:to_s) { temp_dir } + $LOAD_PATH << object + + candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'foo", "", bind: binding) + assert_include candidates, "'foo" + ensure + $LOAD_PATH.pop if object + FileUtils.remove_entry(temp_dir) if temp_dir + end + + def test_complete_require_with_malformed_object_in_load_path + object = Object.new + def object.to_s; raise; end + $LOAD_PATH << object + + assert_nothing_raised do + IRB::RegexpCompletor.new.completion_candidates("require ", "'foo", "", bind: binding) + end + ensure + $LOAD_PATH.pop if object + end + + def test_complete_require_library_name_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::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::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 end end - def test_complete_predicate? - candidates = IRB::InputCompletor.retrieve_completion_data("1.posi", bind: binding) - assert_include candidates, '1.positive?' + 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 + str_example = '' + @str_example = '' + private_methods = '' + methods = '' + global_variables = '' + local_variables = '' + instance_variables = '' + + # suppress "assigned but unused variable" warning + str_example.clear + @str_example.clear + private_methods.clear + methods.clear + global_variables.clear + local_variables.clear + instance_variables.clear - namespace = IRB::InputCompletor.retrieve_completion_data("1.positive?", bind: binding, doc_namespace: true) - assert_equal "Integer.positive?", namespace + 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(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 + xzy, xzy_1, xzy2 = '', '', '' + + xzy.clear + xzy_1.clear + xzy2.clear + + candidates = completion_candidates("xz", binding) + assert_equal(%w[xzy xzy2 xzy_1], candidates) + end end - def test_complete_require - candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "") - %w['irb/init 'irb/ruby-lex].each do |word| - assert_include candidates, word + class ConstantCompletionTest < CompletionTest + class Foo + B3 = 1 + B1 = 1 + B2 = 1 end - # Test cache - candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "") - %w['irb/init 'irb/ruby-lex].each do |word| - assert_include candidates, word + + def test_complete_constants + 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"], completion_candidates("::Fo", binding)) + assert_equal("Forwardable", doc_namespace("::Forwardable", binding)) end end - def test_complete_require_relative - candidates = Dir.chdir(__dir__ + "/../..") do - IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "") + 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 + symbols = %w"UTF-16LE UTF-7".map do |enc| + "K".force_encoding(enc).to_sym + rescue end - %w['lib/irb/init 'lib/irb/ruby-lex].each do |word| + 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(completion_candidates(":", binding)) + end + + def test_complete_invalid_three_colons + assert_empty(completion_candidates(":::A", binding)) + assert_empty(completion_candidates(":::", binding)) + end + + def test_complete_absolute_constants_with_special_characters + 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 = completion_candidates("de", binding) + %w[def defined?].each do |word| assert_include candidates, word end - # Test cache - candidates = Dir.chdir(__dir__ + "/../..") do - IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "") - end - %w['lib/irb/init 'lib/irb/ruby-lex].each do |word| + + candidates = completion_candidates("__", binding) + %w[__ENCODING__ __LINE__ __FILE__].each do |word| assert_include candidates, word end end + + def test_complete_methods + obj = Object.new + obj.singleton_class.class_eval { + def public_hoge; end + private def private_hoge; end + + # Support for overriding #methods etc. + def methods; end + def private_methods; end + def global_variables; end + def local_variables; end + def instance_variables; end + } + bind = obj.instance_exec { binding } + + 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 + + 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 71e8ad1c0d..cd3f2c8f62 100644 --- a/test/irb/test_context.rb +++ b/test/irb/test_context.rb @@ -1,45 +1,16 @@ # frozen_string_literal: false -require 'test/unit' require 'tempfile' require 'irb' -require 'rubygems' if defined?(Gem) -module TestIRB - class TestContext < Test::Unit::TestCase - class TestInputMethod < ::IRB::InputMethod - attr_reader :list, :line_no - - def initialize(list = []) - super("test") - @line_no = 0 - @list = list - end - - def gets - @list[@line_no]&.tap {@line_no += 1} - end - - def eof? - @line_no >= @list.size - end - - def encoding - Encoding.default_external - end - - def reset - @line_no = 0 - end - - def winsize - [10, 20] - end - end +require_relative "helper" +module TestIRB + 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) @@ -48,53 +19,17 @@ module TestIRB def Reline.get_screen_size [36, 80] end + save_encodings end def teardown Reline.instance_eval { undef :get_screen_size } Reline.define_singleton_method(:get_screen_size, @get_screen_size) + 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 - skip if RUBY_ENGINE == 'truffleruby' - assert_raise_with_message(EncodingError, /invalid symbol/) { - @context.evaluate(%q[{"\xAE": 1}], 1) - # The backtrace of this invalid encoding hash doesn't contain lineno. - } - end - - def test_evaluate_with_onigmo_warning - skip if RUBY_ENGINE == 'truffleruby' - assert_warning("(irb):1: warning: character class has duplicated range: /[aa]/\n") do - @context.evaluate('/[aa]/', 1) - end - end def test_eval_input - skip if RUBY_ENGINE == 'truffleruby' verbose, $VERBOSE = $VERBOSE, nil input = TestInputMethod.new([ "raise 'Foo'\n", @@ -107,17 +42,32 @@ 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 def test_eval_input_raise2x - skip if RUBY_ENGINE == 'truffleruby' input = TestInputMethod.new([ "raise 'Foo'\n", "raise 'Bar'\n", @@ -128,80 +78,145 @@ 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_eval_object_without_inspect_method - verbose, $VERBOSE = $VERBOSE, nil - all_assertions do |all| - IRB::Inspector::INSPECTORS.invert.each_value do |mode| - all.for(mode) do - input = TestInputMethod.new([ - "[BasicObject.new, Class.new]\n", - ]) - irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) - irb.context.inspect_mode = mode - out, err = capture_output do - irb.eval_input - end - assert_empty err - assert_match(/\(Object doesn't support #inspect\)\n(=> )?\n/, out) + 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 + require 'stringio' + input = TestInputMethod.new(["n=1"]) + input.instance_variable_set(:@stdout, StringIO.new) + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) + irb.context.echo_on_assignment = :truncate + irb.context.prompt_mode = :DEFAULT + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_equal "=> 1\n", out + end + + { + successful: [ + [false, "class Foo < Struct.new(:bar); end; Foo.new(123)\n", /#<struct bar=123>/], + [:p, "class Foo < Struct.new(:bar); end; Foo.new(123)\n", /#<struct bar=123>/], + [true, "class Foo < Struct.new(:bar); end; Foo.new(123)\n", /#<struct #<Class:.*>::Foo bar=123>/], + [:yaml, "123", /--- 123\n/], + [: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/], + [:marshal, "[Object.new, Class.new]", /#<TypeError: can't dump anonymous class #<Class:/] + ] + }.each do |scenario, cases| + cases.each do |inspect_mode, input, expected| + define_method "test_#{inspect_mode}_inspect_mode_#{scenario}" do + verbose, $VERBOSE = $VERBOSE, nil + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), TestInputMethod.new([input])) + irb.context.inspect_mode = inspect_mode + out, err = capture_output do + irb.eval_input end + assert_empty err + assert_match(expected, out) + ensure + $VERBOSE = verbose end end + end + + def test_object_inspection_handles_basic_object + verbose, $VERBOSE = $VERBOSE, nil + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), TestInputMethod.new(["BasicObject.new"])) + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_not_match(/NoMethodError/, out) + assert_match(/#<BasicObject:.*>/, out) ensure $VERBOSE = verbose end - def test_default_config - assert_equal(true, @context.use_colorize?) - end + def test_object_inspection_falls_back_to_kernel_inspect_when_errored + verbose, $VERBOSE = $VERBOSE, nil + main = Object.new + main.singleton_class.module_eval <<~RUBY + class Foo + def inspect + raise "foo" + end + end + RUBY - 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" - ) + irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new(["Foo.new"])) + out, err = capture_output do + irb.eval_input end + assert_empty err + assert_match(/An error occurred when inspecting the object: #<RuntimeError: foo>/, out) + assert_match(/Result of Kernel#inspect: #<#<Class:.*>::Foo:/, out) + ensure + $VERBOSE = verbose + 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" - ) + def test_object_inspection_prints_useful_info_when_kernel_inspect_also_errored + verbose, $VERBOSE = $VERBOSE, nil + main = Object.new + main.singleton_class.module_eval <<~RUBY + class Foo + def initialize + # Kernel#inspect goes through instance variables with #inspect + # So this will cause Kernel#inspect to fail + @foo = BasicObject.new + end + + def inspect + raise "foo" + end + end + RUBY + + irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new(["Foo.new"])) + out, err = capture_output do + irb.eval_input 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) + ensure + $VERBOSE = verbose + end + + def test_default_config + assert_equal(true, @context.use_autocomplete?) end def test_echo_on_assignment @@ -220,7 +235,7 @@ module TestIRB # The default irb.context.echo = true irb.context.echo_on_assignment = false - out, err = capture_io do + out, err = capture_output do irb.eval_input end assert_empty err @@ -230,7 +245,7 @@ module TestIRB input.reset irb.context.echo = true irb.context.echo_on_assignment = true - out, err = capture_io do + out, err = capture_output do irb.eval_input end assert_empty err @@ -240,7 +255,7 @@ module TestIRB input.reset irb.context.echo = false irb.context.echo_on_assignment = false - out, err = capture_io do + out, err = capture_output do irb.eval_input end assert_empty err @@ -250,7 +265,7 @@ module TestIRB input.reset irb.context.echo = false irb.context.echo_on_assignment = true - out, err = capture_io do + out, err = capture_output do irb.eval_input end assert_empty err @@ -268,7 +283,7 @@ module TestIRB irb.context.echo = true irb.context.echo_on_assignment = false - out, err = capture_io do + out, err = capture_output do irb.eval_input end assert_empty err @@ -277,7 +292,7 @@ module TestIRB input.reset irb.context.echo = true irb.context.echo_on_assignment = :truncate - out, err = capture_io do + out, err = capture_output do irb.eval_input end assert_empty err @@ -286,7 +301,7 @@ module TestIRB input.reset irb.context.echo = true irb.context.echo_on_assignment = true - out, err = capture_io do + out, err = capture_output do irb.eval_input end assert_empty err @@ -295,7 +310,7 @@ module TestIRB input.reset irb.context.echo = false irb.context.echo_on_assignment = false - out, err = capture_io do + out, err = capture_output do irb.eval_input end assert_empty err @@ -304,7 +319,7 @@ module TestIRB input.reset irb.context.echo = false irb.context.echo_on_assignment = :truncate - out, err = capture_io do + out, err = capture_output do irb.eval_input end assert_empty err @@ -313,7 +328,7 @@ module TestIRB input.reset irb.context.echo = false irb.context.echo_on_assignment = true - out, err = capture_io do + out, err = capture_output do irb.eval_input end assert_empty err @@ -321,99 +336,103 @@ module TestIRB end def test_omit_multiline_on_assignment - input = TestInputMethod.new([ - "class A; def inspect; ([?* * 1000] * 3).join(%{\\n}); end; end; a = A.new\n", - "a\n" - ]) - value = ([?* * 1000] * 3).join(%{\n}) - value_first_line = (?* * 1000).to_s - irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) - irb.context.return_format = "=> %s\n" - - irb.context.echo = true - irb.context.echo_on_assignment = false - out, err = capture_io do - irb.eval_input - end - assert_empty err - assert_equal("=> \n#{value}\n", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) - - input.reset - irb.context.echo = true - irb.context.echo_on_assignment = :truncate - out, err = capture_io do - irb.eval_input - end - assert_empty err - assert_equal("=> #{value_first_line[0..(input.winsize.last - 9)]}...\e[0m\n=> \n#{value}\n", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) - - input.reset - irb.context.echo = true - irb.context.echo_on_assignment = true - out, err = capture_io do - irb.eval_input - end - assert_empty err - assert_equal("=> \n#{value}\n=> \n#{value}\n", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) - - input.reset - irb.context.echo = false - irb.context.echo_on_assignment = false - out, err = capture_io do - irb.eval_input - end - assert_empty err - assert_equal("", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) - - input.reset - irb.context.echo = false - irb.context.echo_on_assignment = :truncate - out, err = capture_io do - irb.eval_input - end - assert_empty err - assert_equal("", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) - - input.reset - irb.context.echo = false - irb.context.echo_on_assignment = true - out, err = capture_io do - irb.eval_input + without_colorize do + input = TestInputMethod.new([ + "class A; def inspect; ([?* * 1000] * 3).join(%{\\n}); end; end; a = A.new\n", + "a\n" + ]) + value = ([?* * 1000] * 3).join(%{\n}) + value_first_line = (?* * 1000).to_s + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) + irb.context.return_format = "=> %s\n" + + irb.context.echo = true + irb.context.echo_on_assignment = false + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_equal("=> \n#{value}\n", out) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) + + input.reset + irb.context.echo = true + irb.context.echo_on_assignment = :truncate + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_equal("=> #{value_first_line[0..(input.winsize.last - 9)]}...\n=> \n#{value}\n", out) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) + + input.reset + irb.context.echo = true + irb.context.echo_on_assignment = true + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_equal("=> \n#{value}\n=> \n#{value}\n", out) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) + + input.reset + irb.context.echo = false + irb.context.echo_on_assignment = false + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_equal("", out) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) + + input.reset + irb.context.echo = false + irb.context.echo_on_assignment = :truncate + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_equal("", out) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) + + input.reset + irb.context.echo = false + irb.context.echo_on_assignment = true + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_equal("", out) + irb.context.evaluate_expression('A.remove_method(:inspect)', 0) end - assert_empty err - assert_equal("", out) - irb.context.evaluate('A.remove_method(:inspect)', 0) end def test_echo_on_assignment_conf # Default IRB.conf[:ECHO] = nil IRB.conf[:ECHO_ON_ASSIGNMENT] = nil - input = TestInputMethod.new() - irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) + without_colorize do + input = TestInputMethod.new() + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) - assert(irb.context.echo?, "echo? should be true by default") - assert_equal(:truncate, irb.context.echo_on_assignment?, "echo_on_assignment? should be :truncate by default") + assert(irb.context.echo?, "echo? should be true by default") + assert_equal(:truncate, irb.context.echo_on_assignment?, "echo_on_assignment? should be :truncate by default") - # Explicitly set :ECHO to false - IRB.conf[:ECHO] = false - irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) + # Explicitly set :ECHO to false + IRB.conf[:ECHO] = false + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) - refute(irb.context.echo?, "echo? should be false when IRB.conf[:ECHO] is set to false") - assert_equal(:truncate, irb.context.echo_on_assignment?, "echo_on_assignment? should be :truncate by default") + refute(irb.context.echo?, "echo? should be false when IRB.conf[:ECHO] is set to false") + assert_equal(:truncate, irb.context.echo_on_assignment?, "echo_on_assignment? should be :truncate by default") - # Explicitly set :ECHO_ON_ASSIGNMENT to true - IRB.conf[:ECHO] = nil - IRB.conf[:ECHO_ON_ASSIGNMENT] = false - irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) + # Explicitly set :ECHO_ON_ASSIGNMENT to true + IRB.conf[:ECHO] = nil + IRB.conf[:ECHO_ON_ASSIGNMENT] = false + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) - assert(irb.context.echo?, "echo? should be true by default") - refute(irb.context.echo_on_assignment?, "echo_on_assignment? should be false when IRB.conf[:ECHO_ON_ASSIGNMENT] is set to false") + assert(irb.context.echo?, "echo? should be true by default") + refute(irb.context.echo_on_assignment?, "echo_on_assignment? should be false when IRB.conf[:ECHO_ON_ASSIGNMENT] is set to false") + end end def test_multiline_output_on_default_inspector @@ -421,34 +440,57 @@ module TestIRB def main.inspect "abc\ndef" end - input = TestInputMethod.new([ - "self" - ]) - irb = IRB::Irb.new(IRB::WorkSpace.new(main), input) - irb.context.return_format = "=> %s\n" - # The default - irb.context.newline_before_multiline_output = true - out, err = capture_io do - irb.eval_input + without_colorize do + input = TestInputMethod.new([ + "self" + ]) + irb = IRB::Irb.new(IRB::WorkSpace.new(main), input) + irb.context.return_format = "=> %s\n" + + # The default + irb.context.newline_before_multiline_output = true + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_equal("=> \nabc\ndef\n", + out) + + # No newline before multiline output + input.reset + irb.context.newline_before_multiline_output = false + out, err = capture_output do + irb.eval_input + end + assert_empty err + assert_equal("=> abc\ndef\n", out) end - assert_empty err - assert_equal("=> \nabc\ndef\n", - out) + end - # No newline before multiline output - input.reset - irb.context.newline_before_multiline_output = false - out, err = capture_io do + def test_default_return_format + IRB.conf[:PROMPT][:MY_PROMPT] = { + :PROMPT_I => "%03n> ", + :PROMPT_S => "%03n> ", + :PROMPT_C => "%03n> " + # without :RETURN + # :RETURN => "%s\n" + } + IRB.conf[:PROMPT_MODE] = :MY_PROMPT + input = TestInputMethod.new([ + "3" + ]) + irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input) + out, err = capture_output do irb.eval_input end assert_empty err - assert_equal("=> abc\ndef\n", + assert_equal("3\n", out) end def test_eval_input_with_exception - skip if RUBY_ENGINE == 'truffleruby' + pend if RUBY_ENGINE == 'truffleruby' verbose, $VERBOSE = $VERBOSE, nil input = TestInputMethod.new([ "def hoge() fuga; end; def fuga() raise; end; hoge\n", @@ -458,27 +500,35 @@ module TestIRB irb.eval_input end assert_empty err - if '2.5.0' <= RUBY_VERSION && 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 def test_eval_input_with_invalid_byte_sequence_exception - skip if RUBY_ENGINE == 'truffleruby' verbose, $VERBOSE = $VERBOSE, nil input = TestInputMethod.new([ %Q{def hoge() fuga; end; def fuga() raise "A\\xF3B"; end; hoge\n}, @@ -488,27 +538,37 @@ module TestIRB irb.eval_input end assert_empty err - if '2.5.0' <= RUBY_VERSION && 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 def test_eval_input_with_long_exception - skip if RUBY_ENGINE == 'truffleruby' + pend if RUBY_ENGINE == 'truffleruby' verbose, $VERBOSE = $VERBOSE, nil nesting = 20 generated_code = '' @@ -524,48 +584,48 @@ module TestIRB irb.eval_input end assert_empty err - if '2.5.0' <= RUBY_VERSION && RUBY_VERSION < '3.0.0' && STDOUT.tty? + if RUBY_VERSION < '3.0.0' && STDOUT.tty? expected = [ :*, /Traceback \(most recent call last\):\n/, - :*, /\t... 5 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/, + :*, /\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/, ] 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/, - :*, /\t... 5 levels...\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 assert_pattern_list(expected, out) @@ -573,6 +633,43 @@ module TestIRB $VERBOSE = verbose end + 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.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.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.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 input = TestInputMethod.new([ "\n", @@ -593,5 +690,40 @@ module TestIRB :*, /\b6\n/, ], 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 + original_value = IRB.conf[:USE_COLORIZE] + IRB.conf[:USE_COLORIZE] = false + yield + ensure + IRB.conf[:USE_COLORIZE] = original_value + 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..eca40c5702 --- /dev/null +++ b/test/irb/test_debugger_integration.rb @@ -0,0 +1,480 @@ +# 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_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 81b7fe8679..63be35fdaa 100644 --- a/test/irb/test_history.rb +++ b/test/irb/test_history.rb @@ -1,51 +1,47 @@ # frozen_string_literal: false -require 'test/unit' 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 < Test::Unit::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 TestInputMethod < ::IRB::InputMethod - HISTORY = Array.new + 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 - attr_reader :list, :line_no - - def initialize(list = []) - super("test") - @line_no = 0 - @list = list - end - - def gets - @list[@line_no]&.tap {@line_no += 1} - end - - def eof? - @line_no >= @list.size - end - - def encoding - Encoding.default_external - end - - def reset - @line_no = 0 - end + class TestInputMethodWithReadlineHistory < TestInputMethod + # When IRB.conf[:USE_MULTILINE] is false, IRB::ReadlineInputMethod uses Readline::HISTORY + HISTORY = Readline::HISTORY - def winsize - [10, 20] - end + include IRB::HistorySavingAbility end def test_history_save_1 @@ -127,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 - assert_history(<<~EXPECTED_HISTORY, <<~INITIAL_HISTORY, <<~INPUT) do |history_file| + 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 + 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 @@ -143,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 @@ -158,35 +224,31 @@ module TestIRB end 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 - 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 = TestInputMethod.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 - 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: @@ -194,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 @@ -208,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 2c50b5da3a..f11d7398c8 100644 --- a/test/irb/test_init.rb +++ b/test/irb/test_init.rb @@ -1,10 +1,30 @@ # frozen_string_literal: false -require "test/unit" require "irb" require "fileutils" +require_relative "helper" + module TestIRB - class TestInit < Test::Unit::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 = 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 argv = ["foo", "bar"] with_argv(argv) do @@ -19,57 +39,224 @@ module TestIRB assert_equal orig, $0 end - def test_rc_file - backup_irbrc = ENV.delete("IRBRC") # This is for RVM... - backup_xdg_config_home = ENV.delete("XDG_CONFIG_HOME") - backup_home = ENV["HOME"] - Dir.mktmpdir("test_irb_init_#{$$}") do |tmpdir| - ENV["HOME"] = tmpdir - - 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")) + def test_rc_files + tmpdir = @tmpdir + Dir.chdir(tmpdir) do + 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 - ensure - ENV["HOME"] = backup_home - ENV["XDG_CONFIG_HOME"] = backup_xdg_config_home - ENV["IRBRC"] = backup_irbrc - end - - def test_rc_file_in_subdir - backup_irbrc = ENV.delete("IRBRC") # This is for RVM... - backup_xdg_config_home = ENV.delete("XDG_CONFIG_HOME") - backup_home = ENV["HOME"] - Dir.mktmpdir("test_irb_init_#{$$}") do |tmpdir| - ENV["HOME"] = tmpdir - - 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")) + end + + def test_duplicated_rc_files + tmpdir = @tmpdir + Dir.chdir(tmpdir) do + 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 - ensure - ENV["HOME"] = backup_home - ENV["XDG_CONFIG_HOME"] = backup_xdg_config_home - ENV["IRBRC"] = backup_irbrc 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 + + def test_no_color_environment_variable + orig_no_color = ENV['NO_COLOR'] + orig_use_colorize = IRB.conf[:USE_COLORIZE] + IRB.conf[:USE_COLORIZE] = true + + assert IRB.conf[:USE_COLORIZE] + + ENV['NO_COLOR'] = 'true' + IRB.setup(__FILE__) + refute IRB.conf[:USE_COLORIZE] + + ENV['NO_COLOR'] = '' + IRB.setup(__FILE__) + assert IRB.conf[:USE_COLORIZE] + + ENV['NO_COLOR'] = nil + IRB.setup(__FILE__) + assert IRB.conf[:USE_COLORIZE] + ensure + ENV['NO_COLOR'] = orig_no_color + IRB.conf[:USE_COLORIZE] = orig_use_colorize + end + + def test_use_autocomplete_environment_variable + orig_use_autocomplete_env = ENV['IRB_USE_AUTOCOMPLETE'] + orig_use_autocomplete_conf = IRB.conf[:USE_AUTOCOMPLETE] + + ENV['IRB_USE_AUTOCOMPLETE'] = nil + IRB.setup(__FILE__) + assert IRB.conf[:USE_AUTOCOMPLETE] + + ENV['IRB_USE_AUTOCOMPLETE'] = '' + IRB.setup(__FILE__) + assert IRB.conf[:USE_AUTOCOMPLETE] + + ENV['IRB_USE_AUTOCOMPLETE'] = 'false' + IRB.setup(__FILE__) + refute IRB.conf[:USE_AUTOCOMPLETE] + + ENV['IRB_USE_AUTOCOMPLETE'] = 'true' + IRB.setup(__FILE__) + assert IRB.conf[:USE_AUTOCOMPLETE] + ensure + ENV["IRB_USE_AUTOCOMPLETE"] = orig_use_autocomplete_env + 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) + assert_nil IRB.conf[:SCRIPT] + assert_equal(['-f'], argv) + + argv = %w[--noscript -- a] + IRB.setup(eval("__FILE__"), argv: argv) + assert_nil IRB.conf[:SCRIPT] + assert_equal(['a'], argv) + + argv = %w[--noscript a] + IRB.setup(eval("__FILE__"), argv: argv) + assert_nil IRB.conf[:SCRIPT] + assert_equal(['a'], argv) + + argv = %w[--script --noscript a] + IRB.setup(eval("__FILE__"), argv: argv) + assert_nil IRB.conf[:SCRIPT] + assert_equal(['a'], argv) + + argv = %w[--noscript --script a] + IRB.setup(eval("__FILE__"), argv: argv) + assert_equal('a', IRB.conf[:SCRIPT]) + assert_equal([], argv) + end + + def test_dash + argv = %w[-] + IRB.setup(eval("__FILE__"), argv: argv) + assert_equal('-', IRB.conf[:SCRIPT]) + assert_equal([], argv) + + argv = %w[-- -] + IRB.setup(eval("__FILE__"), argv: argv) + assert_equal('-', IRB.conf[:SCRIPT]) + assert_equal([], argv) + + argv = %w[-- - -f] + IRB.setup(eval("__FILE__"), argv: argv) + assert_equal('-', IRB.conf[:SCRIPT]) + 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) @@ -80,4 +267,44 @@ module TestIRB ARGV.replace(orig) end end + + class InitIntegrationTest < IntegrationTestCase + def test_load_error_in_rc_file_is_warned + write_rc <<~'IRBRC' + require "file_that_does_not_exist" + IRBRC + + write_ruby <<~'RUBY' + binding.irb + RUBY + + 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 + + write_ruby <<~'RUBY' + binding.irb + RUBY + + 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 new file mode 100644 index 0000000000..ce317b4b32 --- /dev/null +++ b/test/irb/test_input_method.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: false + +require "irb" +require "rdoc" +require_relative "helper" + +module TestIRB + class InputMethodTest < TestCase + def setup + @conf_backup = IRB.conf.dup + IRB.conf[:LC_MESSAGES] = IRB::Locale.new + save_encodings + end + + 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 + 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::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 + original_show_doc_proc = Reline.dialog_proc(:show_doc)&.dialog_proc + empty_proc = Proc.new {} + Reline.add_dialog_proc(:show_doc, empty_proc) + + IRB.conf[:USE_AUTOCOMPLETE] = false + + IRB::RelineInputMethod.new(IRB::RegexpCompletor.new) + + refute Reline.autocompletion + assert_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 + + def test_initialization_with_use_autocomplete + original_show_doc_proc = Reline.dialog_proc(:show_doc)&.dialog_proc + empty_proc = Proc.new {} + Reline.add_dialog_proc(:show_doc, empty_proc) + + IRB.conf[:USE_AUTOCOMPLETE] = true + + IRB::RelineInputMethod.new(IRB::RegexpCompletor.new) + + assert Reline.autocompletion + 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 + + def test_initialization_with_use_autocomplete_but_without_rdoc + original_show_doc_proc = Reline.dialog_proc(:show_doc)&.dialog_proc + empty_proc = Proc.new {} + Reline.add_dialog_proc(:show_doc, empty_proc) + + IRB.conf[:USE_AUTOCOMPLETE] = true + + without_rdoc do + IRB::RelineInputMethod.new(IRB::RegexpCompletor.new) + end + + assert Reline.autocompletion + # doesn't register show_doc dialog + assert_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 + 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..84b9ee3644 --- /dev/null +++ b/test/irb/test_irb.rb @@ -0,0 +1,806 @@ +# 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.4." + 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 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 +end diff --git a/test/irb/test_locale.rb b/test/irb/test_locale.rb new file mode 100644 index 0000000000..930a38834c --- /dev/null +++ b/test/irb/test_locale.rb @@ -0,0 +1,118 @@ +require "irb" +require "stringio" + +require_relative "helper" + +module TestIRB + class LocaleTestCase < TestCase + def test_initialize_with_en + locale = IRB::Locale.new("en_US.UTF-8") + + assert_equal("en", locale.lang) + assert_equal("US", locale.territory) + assert_equal("UTF-8", locale.encoding.name) + assert_equal(nil, locale.modifier) + end + + def test_initialize_with_ja + locale = IRB::Locale.new("ja_JP.UTF-8") + + assert_equal("ja", locale.lang) + assert_equal("JP", locale.territory) + assert_equal("UTF-8", locale.encoding.name) + assert_equal(nil, locale.modifier) + end + + def test_initialize_with_legacy_ja_encoding_ujis + original_stderr = $stderr + $stderr = StringIO.new + + locale = IRB::Locale.new("ja_JP.ujis") + + assert_equal("ja", locale.lang) + assert_equal("JP", locale.territory) + assert_equal(Encoding::EUC_JP, locale.encoding) + assert_equal(nil, locale.modifier) + + assert_include $stderr.string, "ja_JP.ujis is obsolete. use ja_JP.EUC-JP" + ensure + $stderr = original_stderr + end + + def test_initialize_with_legacy_ja_encoding_euc + original_stderr = $stderr + $stderr = StringIO.new + + locale = IRB::Locale.new("ja_JP.euc") + + assert_equal("ja", locale.lang) + assert_equal("JP", locale.territory) + assert_equal(Encoding::EUC_JP, locale.encoding) + assert_equal(nil, locale.modifier) + + assert_include $stderr.string, "ja_JP.euc is obsolete. use ja_JP.EUC-JP" + ensure + $stderr = original_stderr + end + + %w(IRB_LANG LC_MESSAGES LC_ALL LANG).each do |env_var| + define_method "test_initialize_with_#{env_var.downcase}" do + original_values = { + "IRB_LANG" => ENV["IRB_LANG"], + "LC_MESSAGES" => ENV["LC_MESSAGES"], + "LC_ALL" => ENV["LC_ALL"], + "LANG" => ENV["LANG"], + } + + ENV["IRB_LANG"] = ENV["LC_MESSAGES"] = ENV["LC_ALL"] = ENV["LANG"] = nil + ENV[env_var] = "zh_TW.UTF-8" + + locale = IRB::Locale.new + + assert_equal("zh", locale.lang) + assert_equal("TW", locale.territory) + assert_equal("UTF-8", locale.encoding.name) + assert_equal(nil, locale.modifier) + ensure + original_values.each do |key, value| + ENV[key] = value + end + end + end + + def test_load + # reset Locale's internal cache + IRB::Locale.class_variable_set(:@@loaded, []) + # Because error.rb files define the same class, loading them causes method redefinition warnings. + original_verbose = $VERBOSE + $VERBOSE = nil + + jp_local = IRB::Locale.new("ja_JP.UTF-8") + jp_local.load("irb/error.rb") + msg = IRB::CantReturnToNormalMode.new.message + assert_equal("Normalモードに戻れません.", msg) + + # reset Locale's internal cache + IRB::Locale.class_variable_set(:@@loaded, []) + + en_local = IRB::Locale.new("en_US.UTF-8") + en_local.load("irb/error.rb") + msg = IRB::CantReturnToNormalMode.new.message + assert_equal("Can't return to normal mode.", msg) + ensure + # before turning warnings back on, load the error.rb file again to avoid warnings in other tests + IRB::Locale.new.load("irb/error.rb") + $VERBOSE = original_verbose + end + + def test_find + jp_local = IRB::Locale.new("ja_JP.UTF-8") + path = jp_local.find("irb/error.rb") + assert_include(path, "/lib/irb/lc/ja/error.rb") + + en_local = IRB::Locale.new("en_US.UTF-8") + path = en_local.find("irb/error.rb") + assert_include(path, "/lib/irb/lc/error.rb") + 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 aa634c02a2..fec31f384f 100644 --- a/test/irb/test_option.rb +++ b/test/irb/test_option.rb @@ -1,8 +1,8 @@ # frozen_string_literal: false -require 'test/unit' +require_relative "helper" module TestIRB - class TestOption < Test::Unit::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_exception.rb b/test/irb/test_raise_exception.rb new file mode 100644 index 0000000000..44a5ae87e1 --- /dev/null +++ b/test/irb/test_raise_exception.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: false +require "tmpdir" + +require_relative "helper" + +module TestIRB + 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>/, []) + 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") + def e.message; raise; end + def e.inspect; raise; end + raise e +IRB + end + + def test_raise_exception_with_invalid_byte_sequence + pend if RUBY_ENGINE == 'truffleruby' || /mswin|mingw/ =~ RUBY_PLATFORM + bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] + assert_in_out_err(bundle_exec + %w[-rirb -W0 -e IRB.start(__FILE__) -- -f --], <<~IRB, /A\\xF3B \(StandardError\)/, []) + raise StandardError, "A\\xf3B" + IRB + end + + def test_raise_exception_with_different_encoding_containing_invalid_byte_sequence + backup_home = ENV["HOME"] + Dir.mktmpdir("test_irb_raise_no_backtrace_exception_#{$$}") do |tmpdir| + ENV["HOME"] = tmpdir + + bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] + File.open("#{tmpdir}/euc.rb", 'w') do |f| + f.write(<<~EOF) + # encoding: euc-jp + + def raise_euc_with_invalid_byte_sequence + raise "\xA4\xA2\\xFF" + end + EOF + end + env = {} + %w(LC_MESSAGES LC_ALL LC_CTYPE LANG).each {|n| env[n] = "ja_JP.UTF-8" } + # 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\)/ + assert_in_out_err(args, <<~IRB, error, [], encoding: "UTF-8") + require_relative 'euc' + raise_euc_with_invalid_byte_sequence + IRB + end + ensure + ENV["HOME"] = backup_home + end + end +end diff --git a/test/irb/test_raise_no_backtrace_exception.rb b/test/irb/test_raise_no_backtrace_exception.rb deleted file mode 100644 index 40ee0c52bf..0000000000 --- a/test/irb/test_raise_no_backtrace_exception.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: false -require 'test/unit' - -module TestIRB - class TestRaiseNoBacktraceException < Test::Unit::TestCase - def test_raise_exception - skip if RUBY_ENGINE == 'truffleruby' - 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/, []) - e = Exception.new("foo") - puts e.inspect - def e.backtrace; nil; end - raise e -IRB - end - - def test_raise_exception_with_invalid_byte_sequence - skip if RUBY_ENGINE == 'truffleruby' - bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] - assert_in_out_err(bundle_exec + %w[-rirb -W0 -e IRB.start(__FILE__) -- -f --], <<~IRB, /A\\xF3B \(StandardError\)/, []) - raise StandardError, "A\\xf3B" - IRB - end - end -end diff --git a/test/irb/test_ruby_lex.rb b/test/irb/test_ruby_lex.rb index 556afbd776..4e406a8ce0 100644 --- a/test/irb/test_ruby_lex.rb +++ b/test/irb/test_ruby_lex.rb @@ -1,586 +1,242 @@ -$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) -require 'irb/ruby-lex' -require 'test/unit' -require 'ostruct' +# frozen_string_literal: true +require "irb" -module TestIRB - class TestRubyLex < Test::Unit::TestCase - Row = Struct.new(:content, :current_line_spaces, :new_line_spaces, :nesting_level) - - class MockIO_AutoIndent - def initialize(params, &assertion) - @params = params - @assertion = assertion - end - - def auto_indent(&block) - result = block.call(*@params) - @assertion.call(result) - end - end - - def assert_indenting(lines, correct_space_count, add_new_line) - lines = lines + [""] if add_new_line - last_line_index = lines.length - 1 - byte_pointer = lines.last.length - - ruby_lex = RubyLex.new() - io = MockIO_AutoIndent.new([lines, last_line_index, byte_pointer, add_new_line]) do |auto_indent| - error_message = "Calculated the wrong number of spaces for:\n #{lines.join("\n")}" - assert_equal(correct_space_count, auto_indent, error_message) - end - ruby_lex.set_input(io) - context = OpenStruct.new(auto_indent_mode: true) - ruby_lex.set_auto_indent(context) - end - - def assert_nesting_level(lines, expected) - ruby_lex = RubyLex.new() - io = proc{ lines.join("\n") } - ruby_lex.set_input(io, io) - ruby_lex.lex - error_message = "Calculated the wrong number of nesting level for:\n #{lines.join("\n")}" - assert_equal(expected, ruby_lex.instance_variable_get(:@indent), error_message) - 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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]), nil, 0), - Row.new(%q(hello), nil, 0), - Row.new(%q(FOO), nil, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - end - end - - def test_endless_range_at_end_of_line - if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.6.0') - skip 'Endless range is available in 2.6.0 or later' - end - 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_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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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), - ] +require_relative "helper" - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - assert_nesting_level(lines, row.nesting_level) - end +module TestIRB + class RubyLexTest < TestCase + def setup + save_encodings 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - assert_nesting_level(lines, row.nesting_level) - end + def teardown + restore_encodings 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - assert_nesting_level(lines, row.nesting_level) - end + def test_local_variables_dependent_code + lines = ["a /1#/ do", "2"] + assert_indent_level(lines, 1) + assert_code_block_open(lines, true) + assert_indent_level(lines, 0, local_variables: ['a']) + assert_code_block_open(lines, false, local_variables: ['a']) 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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_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_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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_heredoc_with_indent - input_with_correct_indents = [ - Row.new(%q(<<~Q), nil, 0, 0), - Row.new(%q({), nil, 0, 0), - Row.new(%q( #), nil, 0, 0), - Row.new(%q(}), nil, 0, 0), - ] - - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - 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 - 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), - ] + 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 - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - assert_nesting_level(lines, row.nesting_level) - end - end + # recoverable syntax error code is not terminated + assert_code_block_open(['a+'], true) - def test_broken_heredoc - if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7.0') - skip 'This test needs Ripper::Lexer#scan to take broken tokens' - end - input_with_correct_indents = [ - Row.new(%q(def foo), nil, 2, 1), - Row.new(%q( <<~Q), nil, 2, 1), - Row.new(%q( Qend), nil, 2, 1), - ] + # unrecoverable syntax error code is terminated + assert_code_block_open(['.; a+'], false) - lines = [] - input_with_correct_indents.each do |row| - lines << row.content - assert_indenting(lines, row.current_line_spaces, false) - assert_indenting(lines, row.new_line_spaces, true) - assert_nesting_level(lines, row.nesting_level) - end - end - - PromptRow = Struct.new(:prompt, :content) - - 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) - skip if RUBY_ENGINE == 'truffleruby' - ruby_lex = RubyLex.new() - 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 - 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.set_input(io) - end - - def test_dyanmic_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_dyanmic_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 - if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7.0') - skip 'This test needs Ripper::Lexer#scan to take broken tokens' - end - - 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[0]], "There is already another token in the position of #{t.inspect}.") - pos_to_index[t[0]] = i + assert_nil(pos_to_index[t.pos], "There is already another token in the position of #{t.inspect}.") + pos_to_index[t.pos] = i } end def test_broken_percent_literal_in_method - if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7.0') - skip 'This test needs Ripper::Lexer#scan to take broken tokens' - end - - tokens = RubyLex.ripper_lex_without_warning(<<~EOC.chomp) + tokens = IRB::RubyLex.ripper_lex_without_warning(<<~EOC.chomp) def foo %wwww end EOC pos_to_index = {} tokens.each_with_index { |t, i| - assert_nil(pos_to_index[t[0]], "There is already another token in the position of #{t.inspect}.") - pos_to_index[t[0]] = i + assert_nil(pos_to_index[t.pos], "There is already another token in the position of #{t.inspect}.") + pos_to_index[t.pos] = i } end + + def test_unterminated_code + ['do', '<<A'].each do |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') + end + end + + def test_unterminated_heredoc_string_literal + ['<<A;<<B', "<<A;<<B\n", "%W[\#{<<A;<<B", "%W[\#{<<A;<<B\n"].each do |code| + 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_indent_level_with_heredoc_and_embdoc + reference_code = <<~EOC.chomp + if true + hello + p( + ) + EOC + code_with_heredoc = <<~EOC.chomp + if true + <<~A + A + p( + ) + EOC + code_with_embdoc = <<~EOC.chomp + if true + =begin + =end + p( + ) + EOC + 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 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 + + 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 + + _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 f028fc3aa2..199ce95a37 100644 --- a/test/irb/test_workspace.rb +++ b/test/irb/test_workspace.rb @@ -1,13 +1,13 @@ # frozen_string_literal: false -require 'test/unit' require 'tempfile' -require 'rubygems' require 'irb' require 'irb/workspace' require 'irb/color' +require_relative "helper" + module TestIRB - class TestWorkSpace < Test::Unit::TestCase + class WorkSpaceTest < TestCase def test_code_around_binding IRB.conf[:USE_COLORIZE] = false Tempfile.create('irb') do |f| @@ -39,8 +39,8 @@ module TestIRB end def test_code_around_binding_with_existing_unreadable_file - skip 'chmod cannot make file unreadable on windows' if windows? - skip 'skipped in root privilege' if Process.uid == 0 + pend 'chmod cannot make file unreadable on windows' if windows? + pend 'skipped in root privilege' if Process.uid == 0 Tempfile.create('irb') do |f| code = "IRB::WorkSpace.new(binding)\n" @@ -80,6 +80,22 @@ module TestIRB assert_equal(nil, workspace.code_around_binding) end + + def test_toplevel_binding_local_variables + bug17623 = '[ruby-core:102468]' + bundle_exec = ENV.key?('BUNDLE_GEMFILE') ? ['-rbundler/setup'] : [] + top_srcdir = "#{__dir__}/../.." + irb_path = nil + %w[exe libexec].find do |dir| + 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) + version = 'xyz' # typical rubygems loading file + load('#{irb_path}') + RUBY + end + private def with_script_lines diff --git a/test/irb/yamatanooroti/test_rendering.rb b/test/irb/yamatanooroti/test_rendering.rb index 8f55b38a93..44e07a3a12 100644 --- a/test/irb/yamatanooroti/test_rendering.rb +++ b/test/irb/yamatanooroti/test_rendering.rb @@ -2,164 +2,516 @@ require 'irb' begin require 'yamatanooroti' +rescue LoadError, NameError + # On Ruby repository, this test suite doesn't run because Ruby repo doesn't + # have the yamatanooroti gem. + return +end - class IRB::TestRendering < Yamatanooroti::TestCase - def setup - @pwd = Dir.pwd - suffix = '%010d' % Random.rand(0..65535) - @tmpdir = File.join(File.expand_path(Dir.tmpdir), "test_irb_#{$$}_#{suffix}") - begin - Dir.mkdir(@tmpdir) - rescue Errno::EEXIST - FileUtils.rm_rf(@tmpdir) - Dir.mkdir(@tmpdir) - end - @irbrc_backup = ENV['IRBRC'] - @irbrc_file = ENV['IRBRC'] = File.join(@tmpdir, 'temporaty_irbrc') - File.unlink(@irbrc_file) if File.exist?(@irbrc_file) - end - - def teardown +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}") + begin + Dir.mkdir(@tmpdir) + rescue Errno::EEXIST FileUtils.rm_rf(@tmpdir) - ENV['IRBRC'] = @irbrc_backup - ENV.delete('RELINE_TEST_PROMPT') if ENV['RELINE_TEST_PROMPT'] + Dir.mkdir(@tmpdir) end + @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 test_launch - write_irbrc <<~'LINES' - puts 'start IRB' - LINES - start_terminal(25, 80, %W{ruby -I#{@pwd}/lib -I#{@pwd}/../reline/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') - write(<<~EOC) - 'Hello, World!' - EOC - close - assert_screen(<<~EOC) - start IRB - irb(main):001:0> 'Hello, World!' - => "Hello, World!" - irb(main):002:0> - EOC - end + def teardown + FileUtils.rm_rf(@tmpdir) + ENV['IRBRC'] = @irbrc_backup + ENV['TERM'] = @original_term + ENV['HOME'] = @home_backup + ENV['XDG_CONFIG_HOME'] = @xdg_config_home_backup + end - def test_multiline_paste - write_irbrc <<~'LINES' - puts 'start IRB' - LINES - start_terminal(25, 80, %W{ruby -I#{@pwd}/lib -I#{@pwd}/../reline/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') - write(<<~EOC) - class A - def inspect; '#<A>'; end - def a; self; end - def b; true; end - end - - a = A.new - - a - .a - .b - EOC - close + def test_launch + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(25, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write(<<~EOC) + 'Hello, World!' + EOC + close + assert_screen(<<~EOC) + start IRB + irb(main):001> 'Hello, World!' + => "Hello, World!" + 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 + + def test_multiline_paste + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(25, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write(<<~EOC) + class A + def inspect; '#<A>'; end + def a; self; end + def b; true; end + end + + a = A.new + + a + .a + .b + .itself + EOC + close + assert_screen(<<~EOC) + start IRB + 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> + irb(main):007> a = A.new + => #<A> + irb(main):008> + irb(main):009> a + irb(main):010> .a + irb(main):011> .b + irb(main):012> .itself + => true + irb(main):013> + EOC + end + + def test_evaluate_each_toplevel_statement_by_multiline_paste + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(40, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write(<<~EOC) + class A + def inspect; '#<A>'; end + def b; self; end + def c; true; end + end + + a = A.new + + a + .b + # aaa + .c + + (a) + &.b() + + class A def b; self; end; def c; true; end; end; + a = A.new + a + .b + # aaa + .c + (a) + &.b() + .itself + EOC + close + assert_screen(<<~EOC) + start IRB + 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> + irb(main):007> a = A.new + => #<A> + irb(main):008> + irb(main):009> a + irb(main):010> .b + irb(main):011> # aaa + irb(main):012> .c + => true + irb(main):013> + irb(main):014> (a) + irb(main):015> &.b() + => #<A> + 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):019> a + irb(main):020> .b + irb(main):021> # aaa + irb(main):022> .c + => true + irb(main):023> (a) + irb(main):024> &.b() + irb(main):025> .itself + => #<A> + irb(main):026> + EOC + end + + def test_symbol_with_backtick + write_irbrc <<~'LINES' + puts 'start IRB' + LINES + start_terminal(40, 80, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write(<<~EOC) + :` + EOC + close + assert_screen(<<~EOC) + start IRB + irb(main):001> :` + => :` + irb(main):002> + EOC + end + + 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_S => "%03n> ", + :PROMPT_C => "%03n> " + } + IRB.conf[:PROMPT_MODE] = :MY_PROMPT + puts 'start IRB' + LINES + 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 + + # 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 - 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 - => :b - irb(main):006:0> - irb(main):007:0> a = A.new - => #<A> - irb(main):008:0> - irb(main):009:0> a - irb(main):010:0> .a - irb(main):011:0> .b - => true - irb(main):012:0> + 001> IRB + IRBPress Opti + IRB EOC - end - - def test_evaluate_each_toplevel_statement_by_multiline_paste - write_irbrc <<~'LINES' - puts 'start IRB' - LINES - start_terminal(40, 80, %W{ruby -I#{@pwd}/lib -I#{@pwd}/../reline/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') - write(<<~EOC) - class A - def inspect; '#<A>'; end - def b; self; end - def c; true; end - end - - a = A.new - - a - .b - # aaa - .c - - (a) - &.b() - - - class A def b; self; end; def c; true; end; end; - a = A.new - a - .b - # aaa - .c - (a) - &.b() - EOC - close + else 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 - => :c - irb(main):006:0> - irb(main):007:0> 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 - => true - irb(main):013:0> - irb(main):014:0> (a) - irb(main):015:0> &.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 - => #<A> - irb(main):020:0> a - irb(main):021:0> .b - irb(main):022:0> # aaa - irb(main):023:0> .c - => true - irb(main):024:0> (a) - irb(main):025:0> &.b() - => #<A> - irb(main):026:0> + 001> IRB + IRBPress Alt+ + IRB EOC end + end - private def write_irbrc(content) - File.open(@irbrc_file, 'w') do |f| - f.write content - end + def test_autocomplete_with_showdoc_in_gaps_on_narrow_screen_left + 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_S => "%03n> ", + :PROMPT_C => "%03n> " + } + IRB.conf[:PROMPT_MODE] = :MY_PROMPT + puts 'start IRB' + LINES + start_terminal(4, 12, %W{ruby -I#{@pwd}/lib #{@pwd}/exe/irb}, startup_message: 'start IRB') + write("IR") + write("\C-i") + sleep 0.2 + close + assert_screen(<<~EOC) + start IRB + 001> IRB + PressIRB + IRB + EOC + end + + def test_assignment_expression_truncate + 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 + code = "a /'/i if false; a=1; x=1000.times.to_a#'.size" + write(code + "\n") + close + assert_screen(<<~EOC) + start IRB + irb(main):001> #{code} + => + [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) + File.open(@irbrc_file, 'w') do |f| + f.write content end end -rescue LoadError, NameError - # On Ruby repository, this test suit doesn't run because Ruby repo doesn't - # have the yamatanooroti gem. end |