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.
@@ -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