[ruby/irb] Extract integration testing helpers out of debug command

tests
(https://github.com/ruby/irb/pull/660)

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.

https://github.com/ruby/irb/commit/73b7a895f8
This commit is contained in:
Stan Lo 2023-08-02 19:33:38 +01:00 committed by git
parent dc54574ade
commit 8ecd300e1e
2 changed files with 113 additions and 103 deletions

View File

@ -7,6 +7,11 @@ begin
rescue LoadError # ruby/ruby defines helpers differently
end
begin
require "pty"
rescue LoadError # some platforms don't support PTY
end
module IRB
class InputMethod; end
end
@ -73,4 +78,109 @@ module TestIRB
}
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#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})
@ruby_file.write(program)
@ruby_file.close
end
def integration_envs
{}
end
end
end

View File

@ -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 @@ module TestIRB
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