grntest 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|