From 3626ca0e848373523ddc013cc70b8a84286989e1 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 31 Jul 2023 16:15:10 +0100 Subject: [PATCH] Extract integration testing helpers out of debug command tests The ability to run a test case in a subprocess is useful for testing many other features, like nested IRB sessions. So I think it's worth extracting them into a new test case class. --- test/irb/helper.rb | 110 +++++++++++++++++++++++++++++++++++++ test/irb/test_debug_cmd.rb | 106 +---------------------------------- 2 files changed, 113 insertions(+), 103 deletions(-) diff --git a/test/irb/helper.rb b/test/irb/helper.rb index 55f9e083e..eacca18f0 100644 --- a/test/irb/helper.rb +++ b/test/irb/helper.rb @@ -7,6 +7,11 @@ rescue LoadError # ruby/ruby defines helpers differently end +begin + require "pty" +rescue LoadError # some Ruby implementations don't have PTY +end + module IRB class InputMethod; end end @@ -73,4 +78,109 @@ def without_rdoc(&block) } end end + + class IntegrationTestCase + LIB = File.expand_path("../../lib", __dir__) + TIMEOUT_SEC = 3 + + def setup + unless defined?(PTY) + omit "Integration tests require PTY." + end + end + + def run_ruby_file(&block) + cmd = [EnvUtil.rubybin, "-I", LIB, @ruby_file.to_path] + tmp_dir = Dir.mktmpdir + + @commands = [] + lines = [] + + yield + + PTY.spawn(integration_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 + File.unlink(@ruby_file) if @ruby_file + FileUtils.remove_entry tmp_dir + end + + # read.gets could raise exceptions on some platforms + # https://github.com/ruby/ruby/blob/master/ext/pty/pty.c#L729-L736 + def safe_gets(read) + read.gets + rescue Errno::EIO + nil + end + + def kill_safely pid + return if wait_pid pid, TIMEOUT_SEC + + Process.kill :TERM, pid + return if wait_pid pid, 0.2 + + Process.kill :KILL, pid + Process.waitpid(pid) + rescue Errno::EPERM, Errno::ESRCH + end + + def wait_pid pid, sec + total_sec = 0.0 + wait_sec = 0.001 # 1ms + + while total_sec < sec + if Process.waitpid(pid, Process::WNOHANG) == pid + return true + end + sleep wait_sec + total_sec += wait_sec + wait_sec *= 2 + end + + false + rescue Errno::ECHILD + true + end + + def type(command) + @commands << command + end + + def write_ruby(program) + @ruby_file = Tempfile.create(%w{irb- .rb}) + @ruby_file.write(program) + @ruby_file.close + end + + def integration_envs + {} + end + end end diff --git a/test/irb/test_debug_cmd.rb b/test/irb/test_debug_cmd.rb index 3bc00638d..aa0321b66 100644 --- a/test/irb/test_debug_cmd.rb +++ b/test/irb/test_debug_cmd.rb @@ -1,24 +1,12 @@ # frozen_string_literal: true -begin - require "pty" -rescue LoadError - return -end - require "tempfile" require "tmpdir" require_relative "helper" module TestIRB - LIB = File.expand_path("../../lib", __dir__) - - class DebugCommandTestCase < TestCase - IRB_AND_DEBUGGER_OPTIONS = { - "NO_COLOR" => "true", "RUBY_DEBUG_HISTORY_FILE" => '' - } - + class DebugCommandTest < IntegrationTestCase def setup if ruby_core? omit "This test works only under ruby/irb" @@ -204,96 +192,8 @@ def test_catch private - TIMEOUT_SEC = 3 - - def run_ruby_file(&block) - cmd = [EnvUtil.rubybin, "-I", LIB, @ruby_file.to_path] - tmp_dir = Dir.mktmpdir - - @commands = [] - lines = [] - - yield - - PTY.spawn(IRB_AND_DEBUGGER_OPTIONS.merge("TERM" => "dumb"), *cmd) do |read, write, pid| - Timeout.timeout(TIMEOUT_SEC) do - while line = safe_gets(read) - lines << line - - # means the breakpoint is triggered - if line.match?(/binding\.irb/) - while command = @commands.shift - write.puts(command) - end - end - end - end - ensure - read.close - write.close - kill_safely(pid) - end - - lines.join - rescue Timeout::Error - message = <<~MSG - Test timedout. - - #{'=' * 30} OUTPUT #{'=' * 30} - #{lines.map { |l| " #{l}" }.join} - #{'=' * 27} END OF OUTPUT #{'=' * 27} - MSG - assert_block(message) { false } - ensure - File.unlink(@ruby_file) if @ruby_file - FileUtils.remove_entry tmp_dir - end - - # read.gets could raise exceptions on some platforms - # https://github.com/ruby/ruby/blob/master/ext/pty/pty.c#L729-L736 - def safe_gets(read) - read.gets - rescue Errno::EIO - nil - end - - def kill_safely pid - return if wait_pid pid, TIMEOUT_SEC - - Process.kill :TERM, pid - return if wait_pid pid, 0.2 - - Process.kill :KILL, pid - Process.waitpid(pid) - rescue Errno::EPERM, Errno::ESRCH - end - - def wait_pid pid, sec - total_sec = 0.0 - wait_sec = 0.001 # 1ms - - while total_sec < sec - if Process.waitpid(pid, Process::WNOHANG) == pid - return true - end - sleep wait_sec - total_sec += wait_sec - wait_sec *= 2 - end - - false - rescue Errno::ECHILD - true - end - - def type(command) - @commands << command - end - - def write_ruby(program) - @ruby_file = Tempfile.create(%w{irb- .rb}) - @ruby_file.write(program) - @ruby_file.close + def integration_envs + { "NO_COLOR" => "true", "RUBY_DEBUG_HISTORY_FILE" => '' } end end end