grntest 1.0.2 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/doc/text/news.md +11 -1
- data/lib/grntest/base-result.rb +32 -0
- data/lib/grntest/error.rb +39 -0
- data/lib/grntest/execution-context.rb +89 -0
- data/lib/grntest/executors.rb +19 -0
- data/lib/grntest/executors/base-executor.rb +332 -0
- data/lib/grntest/executors/http-executor.rb +60 -0
- data/lib/grntest/executors/standard-io-executor.rb +71 -0
- data/lib/grntest/reporters.rb +37 -0
- data/lib/grntest/reporters/base-reporter.rb +375 -0
- data/lib/grntest/reporters/inplace-reporter.rb +208 -0
- data/lib/grntest/reporters/mark-reporter.rb +112 -0
- data/lib/grntest/reporters/stream-reporter.rb +86 -0
- data/lib/grntest/response-parser.rb +64 -0
- data/lib/grntest/test-runner.rb +511 -0
- data/lib/grntest/test-suites-runner.rb +141 -0
- data/lib/grntest/tester.rb +2 -1975
- data/lib/grntest/version.rb +1 -1
- data/lib/grntest/worker.rb +164 -0
- data/test/executors/test-base-executor.rb +42 -0
- data/test/executors/test-standard-io-executor.rb +61 -0
- metadata +22 -10
- data/test/test-executor.rb +0 -207
@@ -0,0 +1,511 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
require "pathname"
|
19
|
+
require "fileutils"
|
20
|
+
require "tempfile"
|
21
|
+
|
22
|
+
require "json"
|
23
|
+
|
24
|
+
require "grntest/error"
|
25
|
+
require "grntest/executors"
|
26
|
+
require "grntest/base-result"
|
27
|
+
|
28
|
+
module Grntest
|
29
|
+
class TestResult < BaseResult
|
30
|
+
attr_accessor :worker_id, :test_name
|
31
|
+
attr_accessor :expected, :actual, :n_leaked_objects
|
32
|
+
attr_writer :omitted
|
33
|
+
def initialize(worker)
|
34
|
+
super()
|
35
|
+
@worker_id = worker.id
|
36
|
+
@test_name = worker.test_name
|
37
|
+
@actual = nil
|
38
|
+
@expected = nil
|
39
|
+
@n_leaked_objects = 0
|
40
|
+
@omitted = false
|
41
|
+
end
|
42
|
+
|
43
|
+
def status
|
44
|
+
return :omitted if omitted?
|
45
|
+
|
46
|
+
if @expected
|
47
|
+
if @actual == @expected
|
48
|
+
if leaked?
|
49
|
+
:leaked
|
50
|
+
else
|
51
|
+
:success
|
52
|
+
end
|
53
|
+
else
|
54
|
+
:failure
|
55
|
+
end
|
56
|
+
else
|
57
|
+
if leaked?
|
58
|
+
:leaked
|
59
|
+
else
|
60
|
+
:not_checked
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def omitted?
|
66
|
+
@omitted
|
67
|
+
end
|
68
|
+
|
69
|
+
def leaked?
|
70
|
+
not @n_leaked_objects.zero?
|
71
|
+
end
|
72
|
+
|
73
|
+
def checked?
|
74
|
+
not @expected.nil?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class TestRunner
|
79
|
+
MAX_N_COLUMNS = 79
|
80
|
+
|
81
|
+
def initialize(tester, worker)
|
82
|
+
@tester = tester
|
83
|
+
@worker = worker
|
84
|
+
@max_n_columns = MAX_N_COLUMNS
|
85
|
+
@id = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def run
|
89
|
+
succeeded = true
|
90
|
+
|
91
|
+
@worker.on_test_start
|
92
|
+
result = TestResult.new(@worker)
|
93
|
+
result.measure do
|
94
|
+
execute_groonga_script(result)
|
95
|
+
end
|
96
|
+
normalize_actual_result(result)
|
97
|
+
result.expected = read_expected_result
|
98
|
+
case result.status
|
99
|
+
when :success
|
100
|
+
@worker.on_test_success(result)
|
101
|
+
remove_reject_file
|
102
|
+
when :failure
|
103
|
+
@worker.on_test_failure(result)
|
104
|
+
output_reject_file(result.actual)
|
105
|
+
succeeded = false
|
106
|
+
when :leaked
|
107
|
+
@worker.on_test_leak(result)
|
108
|
+
succeeded = false
|
109
|
+
when :omitted
|
110
|
+
@worker.on_test_omission(result)
|
111
|
+
else
|
112
|
+
@worker.on_test_no_check(result)
|
113
|
+
output_actual_file(result.actual)
|
114
|
+
end
|
115
|
+
@worker.on_test_finish(result)
|
116
|
+
|
117
|
+
succeeded
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
def execute_groonga_script(result)
|
122
|
+
create_temporary_directory do |directory_path|
|
123
|
+
if @tester.database_path
|
124
|
+
db_path = Pathname(@tester.database_path).expand_path
|
125
|
+
else
|
126
|
+
db_dir = directory_path + "db"
|
127
|
+
FileUtils.mkdir_p(db_dir.to_s)
|
128
|
+
db_path = db_dir + "db"
|
129
|
+
end
|
130
|
+
context = ExecutionContext.new
|
131
|
+
context.temporary_directory_path = directory_path
|
132
|
+
context.db_path = db_path
|
133
|
+
context.base_directory = @tester.base_directory.expand_path
|
134
|
+
context.groonga_suggest_create_dataset =
|
135
|
+
@tester.groonga_suggest_create_dataset
|
136
|
+
context.output_type = @tester.output_type
|
137
|
+
run_groonga(context) do |executor|
|
138
|
+
executor.execute(test_script_path)
|
139
|
+
end
|
140
|
+
check_memory_leak(context)
|
141
|
+
result.omitted = context.omitted?
|
142
|
+
result.actual = context.result
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def create_temporary_directory
|
147
|
+
path = "tmp/grntest"
|
148
|
+
path << ".#{@worker.id}" if @tester.n_workers > 1
|
149
|
+
FileUtils.rm_rf(path, :secure => true)
|
150
|
+
FileUtils.mkdir_p(path)
|
151
|
+
begin
|
152
|
+
yield(Pathname(path).expand_path)
|
153
|
+
ensure
|
154
|
+
if @tester.keep_database? and File.exist?(path)
|
155
|
+
FileUtils.rm_rf(keep_database_path, :secure => true)
|
156
|
+
FileUtils.mv(path, keep_database_path)
|
157
|
+
else
|
158
|
+
FileUtils.rm_rf(path, :secure => true)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def keep_database_path
|
164
|
+
test_script_path.to_s.gsub(/\//, ".")
|
165
|
+
end
|
166
|
+
|
167
|
+
def run_groonga(context, &block)
|
168
|
+
unless @tester.database_path
|
169
|
+
create_empty_database(context.db_path.to_s)
|
170
|
+
end
|
171
|
+
|
172
|
+
catch do |tag|
|
173
|
+
context.abort_tag = tag
|
174
|
+
case @tester.interface
|
175
|
+
when :stdio
|
176
|
+
run_groonga_stdio(context, &block)
|
177
|
+
when :http
|
178
|
+
run_groonga_http(context, &block)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def run_groonga_stdio(context)
|
184
|
+
pid = nil
|
185
|
+
begin
|
186
|
+
open_pipe do |input_read, input_write, output_read, output_write|
|
187
|
+
groonga_input = input_write
|
188
|
+
groonga_output = output_read
|
189
|
+
|
190
|
+
input_fd = input_read.to_i
|
191
|
+
output_fd = output_write.to_i
|
192
|
+
env = {}
|
193
|
+
spawn_options = {
|
194
|
+
input_fd => input_fd,
|
195
|
+
output_fd => output_fd
|
196
|
+
}
|
197
|
+
command_line = groonga_command_line(context, spawn_options)
|
198
|
+
command_line += [
|
199
|
+
"--input-fd", input_fd.to_s,
|
200
|
+
"--output-fd", output_fd.to_s,
|
201
|
+
context.relative_db_path.to_s,
|
202
|
+
]
|
203
|
+
pid = Process.spawn(env, *command_line, spawn_options)
|
204
|
+
executor = Executors::StandardIOExecutor.new(groonga_input,
|
205
|
+
groonga_output,
|
206
|
+
context)
|
207
|
+
executor.ensure_groonga_ready
|
208
|
+
yield(executor)
|
209
|
+
end
|
210
|
+
ensure
|
211
|
+
Process.waitpid(pid) if pid
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def open_pipe
|
216
|
+
IO.pipe("ASCII-8BIT") do |input_read, input_write|
|
217
|
+
IO.pipe("ASCII-8BIT") do |output_read, output_write|
|
218
|
+
yield(input_read, input_write, output_read, output_write)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def command_command_line(command, context, spawn_options)
|
224
|
+
command_line = []
|
225
|
+
if @tester.gdb
|
226
|
+
if libtool_wrapper?(command)
|
227
|
+
command_line << find_libtool(command)
|
228
|
+
command_line << "--mode=execute"
|
229
|
+
end
|
230
|
+
command_line << @tester.gdb
|
231
|
+
gdb_command_path = context.temporary_directory_path + "groonga.gdb"
|
232
|
+
File.open(gdb_command_path, "w") do |gdb_command|
|
233
|
+
gdb_command.puts(<<-EOC)
|
234
|
+
break main
|
235
|
+
run
|
236
|
+
print chdir("#{context.temporary_directory_path}")
|
237
|
+
EOC
|
238
|
+
end
|
239
|
+
command_line << "--command=#{gdb_command_path}"
|
240
|
+
command_line << "--quiet"
|
241
|
+
command_line << "--args"
|
242
|
+
else
|
243
|
+
spawn_options[:chdir] = context.temporary_directory_path.to_s
|
244
|
+
end
|
245
|
+
command_line << command
|
246
|
+
command_line
|
247
|
+
end
|
248
|
+
|
249
|
+
def groonga_command_line(context, spawn_options)
|
250
|
+
command_line = command_command_line(@tester.groonga, context,
|
251
|
+
spawn_options)
|
252
|
+
command_line << "--log-path=#{context.log_path}"
|
253
|
+
command_line << "--working-directory=#{context.temporary_directory_path}"
|
254
|
+
command_line
|
255
|
+
end
|
256
|
+
|
257
|
+
def libtool_wrapper?(command)
|
258
|
+
return false unless File.exist?(command)
|
259
|
+
File.open(command, "r") do |command_file|
|
260
|
+
first_line = command_file.gets
|
261
|
+
first_line.start_with?("#!")
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def find_libtool(command)
|
266
|
+
command_path = Pathname.new(command)
|
267
|
+
directory = command_path.dirname
|
268
|
+
until directory.root?
|
269
|
+
libtool = directory + "libtool"
|
270
|
+
return libtool.to_s if libtool.executable?
|
271
|
+
directory = directory.parent
|
272
|
+
end
|
273
|
+
"libtool"
|
274
|
+
end
|
275
|
+
|
276
|
+
def run_groonga_http(context)
|
277
|
+
host = "127.0.0.1"
|
278
|
+
port = 50041 + @worker.id
|
279
|
+
pid_file_path = context.temporary_directory_path + "groonga.pid"
|
280
|
+
|
281
|
+
env = {}
|
282
|
+
spawn_options = {}
|
283
|
+
command_line = groonga_http_command(host, port, pid_file_path, context,
|
284
|
+
spawn_options)
|
285
|
+
pid = nil
|
286
|
+
begin
|
287
|
+
pid = Process.spawn(env, *command_line, spawn_options)
|
288
|
+
begin
|
289
|
+
executor = Executors::HTTPExecutor.new(host, port, context)
|
290
|
+
begin
|
291
|
+
executor.ensure_groonga_ready
|
292
|
+
rescue
|
293
|
+
if Process.waitpid(pid, Process::WNOHANG)
|
294
|
+
pid = nil
|
295
|
+
raise
|
296
|
+
end
|
297
|
+
raise unless @tester.gdb
|
298
|
+
retry
|
299
|
+
end
|
300
|
+
yield(executor)
|
301
|
+
ensure
|
302
|
+
executor.send_command("shutdown")
|
303
|
+
wait_groonga_http_shutdown(pid_file_path)
|
304
|
+
end
|
305
|
+
ensure
|
306
|
+
Process.waitpid(pid) if pid
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def wait_groonga_http_shutdown(pid_file_path)
|
311
|
+
total_sleep_time = 0
|
312
|
+
sleep_time = 0.1
|
313
|
+
while pid_file_path.exist?
|
314
|
+
sleep(sleep_time)
|
315
|
+
total_sleep_time += sleep_time
|
316
|
+
break if total_sleep_time > 1.0
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def groonga_http_command(host, port, pid_file_path, context, spawn_options)
|
321
|
+
case @tester.testee
|
322
|
+
when "groonga"
|
323
|
+
command_line = groonga_command_line(context, spawn_options)
|
324
|
+
command_line += [
|
325
|
+
"--pid-path", pid_file_path.to_s,
|
326
|
+
"--bind-address", host,
|
327
|
+
"--port", port.to_s,
|
328
|
+
"--protocol", "http",
|
329
|
+
"-s",
|
330
|
+
context.relative_db_path.to_s,
|
331
|
+
]
|
332
|
+
when "groonga-httpd"
|
333
|
+
command_line = command_command_line(@tester.groonga_httpd, context,
|
334
|
+
spawn_options)
|
335
|
+
config_file_path = create_config_file(context, host, port,
|
336
|
+
pid_file_path)
|
337
|
+
command_line += [
|
338
|
+
"-c", config_file_path.to_s,
|
339
|
+
"-p", "#{context.temporary_directory_path}/",
|
340
|
+
]
|
341
|
+
end
|
342
|
+
command_line
|
343
|
+
end
|
344
|
+
|
345
|
+
def create_config_file(context, host, port, pid_file_path)
|
346
|
+
config_file_path =
|
347
|
+
context.temporary_directory_path + "groonga-httpd.conf"
|
348
|
+
config_file_path.open("w") do |config_file|
|
349
|
+
config_file.puts(<<EOF)
|
350
|
+
daemon off;
|
351
|
+
master_process off;
|
352
|
+
worker_processes 1;
|
353
|
+
working_directory #{context.temporary_directory_path};
|
354
|
+
error_log groonga-httpd-access.log;
|
355
|
+
pid #{pid_file_path};
|
356
|
+
events {
|
357
|
+
worker_connections 1024;
|
358
|
+
}
|
359
|
+
|
360
|
+
http {
|
361
|
+
server {
|
362
|
+
access_log groonga-httpd-access.log;
|
363
|
+
listen #{port};
|
364
|
+
server_name #{host};
|
365
|
+
location /d/ {
|
366
|
+
groonga_database #{context.relative_db_path};
|
367
|
+
groonga on;
|
368
|
+
}
|
369
|
+
}
|
370
|
+
}
|
371
|
+
EOF
|
372
|
+
end
|
373
|
+
config_file_path
|
374
|
+
end
|
375
|
+
|
376
|
+
def create_empty_database(db_path)
|
377
|
+
output_fd = Tempfile.new("create-empty-database")
|
378
|
+
create_database_command = [
|
379
|
+
@tester.groonga,
|
380
|
+
"--output-fd", output_fd.to_i.to_s,
|
381
|
+
"-n", db_path,
|
382
|
+
"shutdown"
|
383
|
+
]
|
384
|
+
system(*create_database_command)
|
385
|
+
output_fd.close(true)
|
386
|
+
end
|
387
|
+
|
388
|
+
def normalize_actual_result(result)
|
389
|
+
normalized_result = ""
|
390
|
+
result.actual.each do |tag, content, options|
|
391
|
+
case tag
|
392
|
+
when :input
|
393
|
+
normalized_result << content
|
394
|
+
when :output
|
395
|
+
normalized_result << normalize_output(content, options)
|
396
|
+
when :error
|
397
|
+
normalized_result << normalize_raw_content(content)
|
398
|
+
when :n_leaked_objects
|
399
|
+
result.n_leaked_objects = content
|
400
|
+
end
|
401
|
+
end
|
402
|
+
result.actual = normalized_result
|
403
|
+
end
|
404
|
+
|
405
|
+
def normalize_raw_content(content)
|
406
|
+
"#{content}\n".force_encoding("ASCII-8BIT")
|
407
|
+
end
|
408
|
+
|
409
|
+
def normalize_output(content, options)
|
410
|
+
type = options[:type]
|
411
|
+
case type
|
412
|
+
when "json", "msgpack"
|
413
|
+
status = nil
|
414
|
+
values = nil
|
415
|
+
begin
|
416
|
+
status, *values = ResponseParser.parse(content.chomp, type)
|
417
|
+
rescue ParseError
|
418
|
+
return $!.message
|
419
|
+
end
|
420
|
+
normalized_status = normalize_status(status)
|
421
|
+
normalized_output_content = [normalized_status, *values]
|
422
|
+
normalized_output = JSON.generate(normalized_output_content)
|
423
|
+
if normalized_output.bytesize > @max_n_columns
|
424
|
+
normalized_output = JSON.pretty_generate(normalized_output_content)
|
425
|
+
end
|
426
|
+
normalize_raw_content(normalized_output)
|
427
|
+
when "xml"
|
428
|
+
normalized_xml = normalize_output_xml(content, options)
|
429
|
+
normalize_raw_content(normalized_xml)
|
430
|
+
else
|
431
|
+
normalize_raw_content(content)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
def normalize_output_xml(content, options)
|
436
|
+
content.sub(/^<RESULT .+?>/) do |result|
|
437
|
+
result.gsub(/( (?:UP|ELAPSED))="\d+\.\d+(?:e[+-]?\d+)?"/, '\1="0.0"')
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
def normalize_status(status)
|
442
|
+
return_code, started_time, elapsed_time, *rest = status
|
443
|
+
_ = started_time = elapsed_time # for suppress warnings
|
444
|
+
if return_code.zero?
|
445
|
+
[0, 0.0, 0.0]
|
446
|
+
else
|
447
|
+
message, backtrace = rest
|
448
|
+
_ = backtrace # for suppress warnings
|
449
|
+
[[return_code, 0.0, 0.0], message]
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
def test_script_path
|
454
|
+
@worker.test_script_path
|
455
|
+
end
|
456
|
+
|
457
|
+
def have_extension?
|
458
|
+
not test_script_path.extname.empty?
|
459
|
+
end
|
460
|
+
|
461
|
+
def related_file_path(extension)
|
462
|
+
path = Pathname(test_script_path.to_s.gsub(/\.[^.]+\z/, ".#{extension}"))
|
463
|
+
return nil if test_script_path == path
|
464
|
+
path
|
465
|
+
end
|
466
|
+
|
467
|
+
def read_expected_result
|
468
|
+
return nil unless have_extension?
|
469
|
+
result_path = related_file_path("expected")
|
470
|
+
return nil if result_path.nil?
|
471
|
+
return nil unless result_path.exist?
|
472
|
+
result_path.open("r:ascii-8bit") do |result_file|
|
473
|
+
result_file.read
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
def remove_reject_file
|
478
|
+
return unless have_extension?
|
479
|
+
reject_path = related_file_path("reject")
|
480
|
+
return if reject_path.nil?
|
481
|
+
FileUtils.rm_rf(reject_path.to_s, :secure => true)
|
482
|
+
end
|
483
|
+
|
484
|
+
def output_reject_file(actual_result)
|
485
|
+
output_actual_result(actual_result, "reject")
|
486
|
+
end
|
487
|
+
|
488
|
+
def output_actual_file(actual_result)
|
489
|
+
output_actual_result(actual_result, "actual")
|
490
|
+
end
|
491
|
+
|
492
|
+
def output_actual_result(actual_result, suffix)
|
493
|
+
result_path = related_file_path(suffix)
|
494
|
+
return if result_path.nil?
|
495
|
+
result_path.open("w:ascii-8bit") do |result_file|
|
496
|
+
result_file.print(actual_result)
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
def check_memory_leak(context)
|
501
|
+
context.log.each_line do |line|
|
502
|
+
timestamp, log_level, message = line.split(/\|\s*/, 3)
|
503
|
+
_ = timestamp # suppress warning
|
504
|
+
next unless /^grn_fin \((\d+)\)$/ =~ message
|
505
|
+
n_leaked_objects = $1.to_i
|
506
|
+
next if n_leaked_objects.zero?
|
507
|
+
context.result << [:n_leaked_objects, n_leaked_objects, {}]
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|