-
Notifications
You must be signed in to change notification settings - Fork 145
Expand file tree
/
Copy pathconsole_test_case.rb
More file actions
329 lines (278 loc) · 9.88 KB
/
console_test_case.rb
File metadata and controls
329 lines (278 loc) · 9.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
require_relative "test_case"
module DEBUGGER__
class ConsoleTestCase < TestCase
nr = ENV['RUBY_DEBUG_TEST_NO_REMOTE']
NO_REMOTE = nr == 'true' || nr == '1'
if !NO_REMOTE
warn "Tests on local and remote. You can disable remote tests with RUBY_DEBUG_TEST_NO_REMOTE=1."
end
if WITH_COVERAGE
require "simplecov"
Test::Unit.at_exit do
SimpleCov.start
SimpleCov.at_exit_behavior
end
end
# CIs usually doesn't allow overriding the HOME path
# we also don't need to worry about adding or being affected by ~/.rdbgrc on CI
# so we can just use the original home page there
USE_TMP_HOME =
!ENV["CI"] ||
begin
pwd = Dir.pwd
ruby = ENV['RUBY'] || RbConfig.ruby
home_cannot_change = false
PTY.spawn({ "HOME" => pwd }, ruby, '-e', 'puts ENV["HOME"]') do |r,|
home_cannot_change = r.gets.chomp != pwd
end
home_cannot_change
end
class << self
attr_reader :pty_home_dir
def startup
@pty_home_dir =
if USE_TMP_HOME
Dir.mktmpdir
else
Dir.home
end
end
def shutdown
if USE_TMP_HOME
FileUtils.remove_entry @pty_home_dir
end
end
end
def pty_home_dir
self.class.pty_home_dir
end
def create_message fail_msg, test_info
debugger_msg = <<~DEBUGGER_MSG.chomp
--------------------
| Debugger Session |
--------------------
> #{test_info.backlog.join('> ')}
DEBUGGER_MSG
debuggee_msg =
if test_info.mode != 'LOCAL'
<<~DEBUGGEE_MSG.chomp
--------------------
| Debuggee Session |
--------------------
> #{test_info.remote_info.debuggee_backlog.join('> ')}
DEBUGGEE_MSG
end
failure_msg = <<~FAILURE_MSG.chomp
-------------------
| Failure Message |
-------------------
#{fail_msg} on #{test_info.mode} mode
FAILURE_MSG
<<~MSG.chomp
#{debugger_msg}
#{debuggee_msg}
#{failure_msg}
MSG
end
def debug_code(program, remote: true, &test_steps)
Timeout.timeout(60) do
prepare_test_environment(program, test_steps) do
if remote && !NO_REMOTE && MULTITHREADED_TEST
begin
th = [
(new_thread { debug_code_on_local } unless remote == :remote_only),
new_thread { debug_code_on_unix_domain_socket },
new_thread { debug_code_on_tcpip },
].compact
th.each do |t|
if fail_msg = t.join.value
th.each{|t| t.raise Test::Unit::AssertionFailedError}
flunk fail_msg
end
end
rescue Exception => e
th.each(&:kill)
flunk "#{e.class.name}: #{e.message}"
ensure
th.each {|t| t.join}
end
elsif remote && !NO_REMOTE
debug_code_on_local unless remote == :remote_only
debug_code_on_unix_domain_socket
debug_code_on_tcpip
else
debug_code_on_local unless remote == :remote_only
end
end
end
end
def run_test_scenario cmd, test_info
PTY.spawn({ "HOME" => pty_home_dir }, cmd) do |read, write, pid|
test_info.backlog = []
test_info.last_backlog = []
begin
Timeout.timeout(TIMEOUT_SEC) do
while (line = read.gets)
debug_print line
test_info.backlog.push(line)
test_info.last_backlog.push(line)
case line.chomp
when /INTERNAL_INFO:\s(.*)/
# INTERNAL_INFO shouldn't be pushed into backlog and last_backlog
test_info.backlog.pop
test_info.last_backlog.pop
test_info.internal_info = JSON.parse(Regexp.last_match(1))
assertion = []
is_ask_cmd = false
loop do
assert_block(FailureMessage.new { create_message "Expected the REPL prompt to finish", test_info }) { !test_info.queue.empty? }
cmd = test_info.queue.pop
case cmd.to_s
when /Proc/
if is_ask_cmd
assertion.push cmd
else
cmd.call test_info
end
when /flunk_finish/
cmd.call test_info
when *ASK_CMD
write.puts cmd
is_ask_cmd = true
else
break
end
end
write.puts(cmd)
test_info.last_backlog.clear
when %r{\[y/n\]}i
assertion.each do |a|
a.call test_info
end
when test_info.prompt_pattern
# check if the previous command breaks the debugger before continuing
check_error(/REPL ERROR/, test_info)
end
end
check_error(/DEBUGGEE Exception/, test_info)
assert_empty_queue test_info
end
if r = test_info.remote_info
assert_program_finish test_info, r.pid, :debuggee
end
assert_program_finish test_info, pid, :debugger
# result of `gets` return this exception in some platform
# https://github.com/ruby/ruby/blob/master/ext/pty/pty.c#L729-L736
rescue Errno::EIO => e
check_error(/DEBUGGEE Exception/, test_info)
assert_empty_queue test_info, exception: e
if r = test_info.remote_info
assert_program_finish test_info, r.pid, :debuggee
end
assert_program_finish test_info, pid, :debugger
# result of `gets` return this exception in some platform
rescue Timeout::Error
assert_block(create_message("TIMEOUT ERROR (#{TIMEOUT_SEC} sec)", test_info)) { false }
rescue Test::Unit::AssertionFailedError
is_assertion_failure = true
raise
ensure
kill_remote_debuggee test_info, force: is_assertion_failure
# kill debug console process
read.close
write.close
kill_safely pid, force: is_assertion_failure
end
end
end
def assert_program_finish test_info, pid, name
assert_block(create_message("Expected the #{name} program to finish", test_info)) { wait_pid pid, TIMEOUT_SEC }
end
def prepare_test_environment(program, test_steps, &block)
ENV['RUBY_DEBUG_NO_COLOR'] = 'true'
ENV['RUBY_DEBUG_TEST_UI'] = 'terminal'
ENV['RUBY_DEBUG_NO_RELINE'] = 'true'
ENV['RUBY_DEBUG_HISTORY_FILE'] = ''
write_temp_file(strip_line_num(program))
@scenario = []
test_steps.call
@scenario.freeze
inject_lib_to_load_path
block.call
check_line_num!(program)
assert true
end
# use this to start a debug session with the test program
def manual_debug_code(program)
print("[Starting a Debug Session with @#{caller.first}]\n")
write_temp_file(strip_line_num(program))
remote_info = setup_unix_domain_socket_remote_debuggee
Timeout.timeout(TIMEOUT_SEC) do
while !File.exist?(remote_info.sock_path)
sleep 0.1
end
end
DEBUGGER__::Client.new([socket_path]).connect
ensure
kill_remote_debuggee remote_info
end
private def debug_code_on_local
test_info = TestInfo.new(dup_scenario, 'LOCAL', /\(rdbg\)/)
if WITH_COVERAGE
cmd = "#{RUBY} -I#{Dir.pwd}/lib -r#{__dir__}/simplecov_rdbg #{temp_file_path}"
else
cmd = "#{RDBG_EXECUTABLE} #{temp_file_path}"
end
run_test_scenario cmd, test_info
end
private def debug_code_on_unix_domain_socket
test_info = TestInfo.new(dup_scenario, 'UNIX Domain Socket', /\(rdbg:remote\)/)
test_info.remote_info = setup_unix_domain_socket_remote_debuggee
cmd = "#{RDBG_EXECUTABLE} -A #{test_info.remote_info.sock_path}"
run_test_scenario cmd, test_info
end
private def debug_code_on_tcpip
test_info = TestInfo.new(dup_scenario, 'TCP/IP', /\(rdbg:remote\)/)
test_info.remote_info = setup_tcpip_remote_debuggee
cmd = "#{RDBG_EXECUTABLE} -A #{test_info.remote_info.port}"
run_test_scenario cmd, test_info
end
def run_ruby program, options: nil, &test_steps
prepare_test_environment(program, test_steps) do
test_info = TestInfo.new(dup_scenario, 'LOCAL', /\(rdbg\)/)
cmd = "#{RUBY} #{options} -- #{temp_file_path}"
run_test_scenario cmd, test_info
end
end
def run_rdbg program, options: nil, rubyopt: nil, &test_steps
prepare_test_environment(program, test_steps) do
test_info = TestInfo.new(dup_scenario, 'LOCAL', /\(rdbg\)/)
cmd = "#{RDBG_EXECUTABLE} #{options} -- #{temp_file_path}"
cmd = "RUBYOPT=#{rubyopt} #{cmd}" if rubyopt
run_test_scenario cmd, test_info
end
end
def dup_scenario
@scenario.each_with_object(Queue.new){ |e, q| q << e }
end
def new_thread &block
Thread.new do
Thread.current[:is_subthread] = true
catch(:fail) do
block.call
end
end
end
def inject_lib_to_load_path
ENV['RUBYOPT'] = "-I #{__dir__}/../../lib"
end
def assert_empty_queue test_info, exception: nil
message = "Expected all commands/assertions to be executed. Still have #{test_info.queue.length} left."
if exception
message += "\nAssociated exception: #{exception.class} - #{exception.message}" +
exception.backtrace.map{|l| " #{l}\n"}.join
end
assert_block(FailureMessage.new { create_message message, test_info }) { test_info.queue.empty? }
end
end
end