grntest 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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