Skip to content

Commit

Permalink
Extract integration testing helpers out of debug command tests
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
st0012 committed Jul 31, 2023
1 parent 82d1687 commit 3626ca0
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 103 deletions.
110 changes: 110 additions & 0 deletions test/irb/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
106 changes: 3 additions & 103 deletions test/irb/test_debug_cmd.rb
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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

0 comments on commit 3626ca0

Please sign in to comment.