grntest 1.0.0
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/Gemfile +20 -0
- data/README.md +208 -0
- data/Rakefile +43 -0
- data/bin/grntest +20 -0
- data/doc/text/gpl-3.0.txt +674 -0
- data/doc/text/news.md +5 -0
- data/grntest.gemspec +53 -0
- data/lib/grntest/tester.rb +2073 -0
- data/lib/grntest/version.rb +18 -0
- data/test/run-test.rb +29 -0
- data/test/test-executor.rb +193 -0
- metadata +176 -0
@@ -0,0 +1,2073 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright (C) 2012 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 "English"
|
19
|
+
require "optparse"
|
20
|
+
require "pathname"
|
21
|
+
require "fileutils"
|
22
|
+
require "tempfile"
|
23
|
+
require "shellwords"
|
24
|
+
require "open-uri"
|
25
|
+
require "cgi/util"
|
26
|
+
|
27
|
+
require "json"
|
28
|
+
require "msgpack"
|
29
|
+
|
30
|
+
require "grntest/version"
|
31
|
+
|
32
|
+
module Grntest
|
33
|
+
class Tester
|
34
|
+
class Error < StandardError
|
35
|
+
end
|
36
|
+
|
37
|
+
class NotExist < Error
|
38
|
+
attr_reader :path
|
39
|
+
def initialize(path)
|
40
|
+
@path = path
|
41
|
+
super("<#{path}> doesn't exist.")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class ParseError < Error
|
46
|
+
attr_reader :type, :content, :reason
|
47
|
+
def initialize(type, content, reason)
|
48
|
+
@type = type
|
49
|
+
@content = content
|
50
|
+
@reason = reason
|
51
|
+
super("failed to parse <#{@type}> content: #{reason}: <#{content}>")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
def run(argv=nil)
|
57
|
+
argv ||= ARGV.dup
|
58
|
+
tester = new
|
59
|
+
catch do |tag|
|
60
|
+
parser = create_option_parser(tester, tag)
|
61
|
+
targets = parser.parse!(argv)
|
62
|
+
tester.run(*targets)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def create_option_parser(tester, tag)
|
68
|
+
parser = OptionParser.new
|
69
|
+
parser.banner += " TEST_FILE_OR_DIRECTORY..."
|
70
|
+
|
71
|
+
parser.on("--groonga=COMMAND",
|
72
|
+
"Use COMMAND as groonga command",
|
73
|
+
"(#{tester.groonga})") do |command|
|
74
|
+
tester.groonga = command
|
75
|
+
end
|
76
|
+
|
77
|
+
parser.on("--groonga-httpd=COMMAND",
|
78
|
+
"Use COMMAND as groonga-httpd command for groonga-httpd tests",
|
79
|
+
"(#{tester.groonga_httpd})") do |command|
|
80
|
+
tester.groonga_httpd = command
|
81
|
+
end
|
82
|
+
|
83
|
+
parser.on("--groonga-suggest-create-dataset=COMMAND",
|
84
|
+
"Use COMMAND as groonga_suggest_create_dataset command",
|
85
|
+
"(#{tester.groonga_suggest_create_dataset})") do |command|
|
86
|
+
tester.groonga_suggest_create_dataset = command
|
87
|
+
end
|
88
|
+
|
89
|
+
available_interfaces = [:stdio, :http]
|
90
|
+
available_interface_labels = available_interfaces.join(", ")
|
91
|
+
parser.on("--interface=INTERFACE", available_interfaces,
|
92
|
+
"Use INTERFACE for communicating groonga",
|
93
|
+
"[#{available_interface_labels}]",
|
94
|
+
"(#{tester.interface})") do |interface|
|
95
|
+
tester.interface = interface
|
96
|
+
end
|
97
|
+
|
98
|
+
available_output_types = ["json", "msgpack"]
|
99
|
+
available_output_type_labels = available_output_types.join(", ")
|
100
|
+
parser.on("--output-type=TYPE", available_output_types,
|
101
|
+
"Use TYPE as the output type",
|
102
|
+
"[#{available_output_type_labels}]",
|
103
|
+
"(#{tester.output_type})") do |type|
|
104
|
+
tester.output_type = type
|
105
|
+
end
|
106
|
+
|
107
|
+
available_testees = ["groonga", "groonga-httpd"]
|
108
|
+
available_testee_labels = available_testees.join(", ")
|
109
|
+
parser.on("--testee=TESTEE", available_testees,
|
110
|
+
"Test against TESTEE",
|
111
|
+
"[#{available_testee_labels}]",
|
112
|
+
"(#{tester.testee})") do |testee|
|
113
|
+
tester.testee = testee
|
114
|
+
if tester.testee == "groonga-httpd"
|
115
|
+
tester.interface = :http
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
parser.on("--base-directory=DIRECTORY",
|
120
|
+
"Use DIRECTORY as a base directory of relative path",
|
121
|
+
"(#{tester.base_directory})") do |directory|
|
122
|
+
tester.base_directory = Pathname(directory)
|
123
|
+
end
|
124
|
+
|
125
|
+
parser.on("--diff=DIFF",
|
126
|
+
"Use DIFF as diff command",
|
127
|
+
"(#{tester.diff})") do |diff|
|
128
|
+
tester.diff = diff
|
129
|
+
tester.diff_options.clear
|
130
|
+
end
|
131
|
+
|
132
|
+
diff_option_is_specified = false
|
133
|
+
parser.on("--diff-option=OPTION",
|
134
|
+
"Use OPTION as diff command",
|
135
|
+
"(#{tester.diff_options.join(' ')})") do |option|
|
136
|
+
tester.diff_options.clear if diff_option_is_specified
|
137
|
+
tester.diff_options << option
|
138
|
+
diff_option_is_specified = true
|
139
|
+
end
|
140
|
+
|
141
|
+
available_reporters = [:mark, :stream, :inplace]
|
142
|
+
available_reporter_labels = available_reporters.join(", ")
|
143
|
+
parser.on("--reporter=REPORTER", available_reporters,
|
144
|
+
"Report test result by REPORTER",
|
145
|
+
"[#{available_reporter_labels}]",
|
146
|
+
"(auto)") do |reporter|
|
147
|
+
tester.reporter = reporter
|
148
|
+
end
|
149
|
+
|
150
|
+
parser.on("--test=NAME",
|
151
|
+
"Run only test that name is NAME",
|
152
|
+
"If NAME is /.../, NAME is treated as regular expression",
|
153
|
+
"This option can be used multiple times") do |name|
|
154
|
+
tester.test_patterns << parse_name_or_pattern(name)
|
155
|
+
end
|
156
|
+
|
157
|
+
parser.on("--test-suite=NAME",
|
158
|
+
"Run only test suite that name is NAME",
|
159
|
+
"If NAME is /.../, NAME is treated as regular expression",
|
160
|
+
"This option can be used multiple times") do |name|
|
161
|
+
tester.test_suite_patterns << parse_name_or_pattern(name)
|
162
|
+
end
|
163
|
+
|
164
|
+
parser.on("--exclude-test=NAME",
|
165
|
+
"Exclude test that name is NAME",
|
166
|
+
"If NAME is /.../, NAME is treated as regular expression",
|
167
|
+
"This option can be used multiple times") do |name|
|
168
|
+
tester.exclude_test_patterns << parse_name_or_pattern(name)
|
169
|
+
end
|
170
|
+
|
171
|
+
parser.on("--exclude-test-suite=NAME",
|
172
|
+
"Exclude test suite that name is NAME",
|
173
|
+
"If NAME is /.../, NAME is treated as regular expression",
|
174
|
+
"This option can be used multiple times") do |name|
|
175
|
+
tester.exclude_test_suite_patterns << parse_name_or_pattern(name)
|
176
|
+
end
|
177
|
+
|
178
|
+
parser.on("--n-workers=N", Integer,
|
179
|
+
"Use N workers to run tests") do |n|
|
180
|
+
tester.n_workers = n
|
181
|
+
end
|
182
|
+
|
183
|
+
parser.on("--gdb[=COMMAND]",
|
184
|
+
"Run groonga on gdb and use COMMAND as gdb",
|
185
|
+
"(#{tester.default_gdb})") do |command|
|
186
|
+
tester.gdb = command || tester.default_gdb
|
187
|
+
end
|
188
|
+
|
189
|
+
parser.on("--[no-]keep-database",
|
190
|
+
"Keep used database for debug after test is finished",
|
191
|
+
"(#{tester.keep_database?})") do |boolean|
|
192
|
+
tester.keep_database = boolean
|
193
|
+
end
|
194
|
+
|
195
|
+
parser.on("--output=OUTPUT",
|
196
|
+
"Output to OUTPUT",
|
197
|
+
"(stdout)") do |output|
|
198
|
+
tester.output = File.open(output, "w:ascii-8bit")
|
199
|
+
end
|
200
|
+
|
201
|
+
parser.on("--[no-]use-color",
|
202
|
+
"Enable colorized output",
|
203
|
+
"(auto)") do |use_color|
|
204
|
+
tester.use_color = use_color
|
205
|
+
end
|
206
|
+
|
207
|
+
parser.on("--version",
|
208
|
+
"Show version and exit") do
|
209
|
+
puts(VERSION)
|
210
|
+
throw(tag, true)
|
211
|
+
end
|
212
|
+
|
213
|
+
parser
|
214
|
+
end
|
215
|
+
|
216
|
+
def parse_name_or_pattern(name)
|
217
|
+
if /\A\/(.+)\/\z/ =~ name
|
218
|
+
Regexp.new($1, Regexp::IGNORECASE)
|
219
|
+
else
|
220
|
+
name
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
attr_accessor :groonga, :groonga_httpd, :groonga_suggest_create_dataset
|
226
|
+
attr_accessor :interface, :output_type, :testee
|
227
|
+
attr_accessor :base_directory, :diff, :diff_options
|
228
|
+
attr_accessor :n_workers
|
229
|
+
attr_accessor :output
|
230
|
+
attr_accessor :gdb, :default_gdb
|
231
|
+
attr_writer :reporter, :keep_database, :use_color
|
232
|
+
attr_reader :test_patterns, :test_suite_patterns
|
233
|
+
attr_reader :exclude_test_patterns, :exclude_test_suite_patterns
|
234
|
+
def initialize
|
235
|
+
@groonga = "groonga"
|
236
|
+
@groonga_httpd = "groonga-httpd"
|
237
|
+
@groonga_suggest_create_dataset = "groonga-suggest-create-dataset"
|
238
|
+
@interface = :stdio
|
239
|
+
@output_type = "json"
|
240
|
+
@testee = "groonga"
|
241
|
+
@base_directory = Pathname(".")
|
242
|
+
@reporter = nil
|
243
|
+
@n_workers = 1
|
244
|
+
@output = $stdout
|
245
|
+
@keep_database = false
|
246
|
+
@use_color = nil
|
247
|
+
@test_patterns = []
|
248
|
+
@test_suite_patterns = []
|
249
|
+
@exclude_test_patterns = []
|
250
|
+
@exclude_test_suite_patterns = []
|
251
|
+
detect_suitable_diff
|
252
|
+
initialize_debuggers
|
253
|
+
end
|
254
|
+
|
255
|
+
def run(*targets)
|
256
|
+
succeeded = true
|
257
|
+
return succeeded if targets.empty?
|
258
|
+
|
259
|
+
test_suites = load_tests(*targets)
|
260
|
+
run_test_suites(test_suites)
|
261
|
+
end
|
262
|
+
|
263
|
+
def reporter
|
264
|
+
if @reporter.nil?
|
265
|
+
if @n_workers == 1
|
266
|
+
:mark
|
267
|
+
else
|
268
|
+
:inplace
|
269
|
+
end
|
270
|
+
else
|
271
|
+
@reporter
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def keep_database?
|
276
|
+
@keep_database
|
277
|
+
end
|
278
|
+
|
279
|
+
def use_color?
|
280
|
+
if @use_color.nil?
|
281
|
+
@use_color = guess_color_availability
|
282
|
+
end
|
283
|
+
@use_color
|
284
|
+
end
|
285
|
+
|
286
|
+
def target_test?(test_name)
|
287
|
+
selected_test?(test_name) and not excluded_test?(test_name)
|
288
|
+
end
|
289
|
+
|
290
|
+
def selected_test?(test_name)
|
291
|
+
@test_patterns.all? do |pattern|
|
292
|
+
pattern === test_name
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def excluded_test?(test_name)
|
297
|
+
@exclude_test_patterns.any? do |pattern|
|
298
|
+
pattern === test_name
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def target_test_suite?(test_suite_name)
|
303
|
+
selected_test_suite?(test_suite_name) and
|
304
|
+
not excluded_test_suite?(test_suite_name)
|
305
|
+
end
|
306
|
+
|
307
|
+
def selected_test_suite?(test_suite_name)
|
308
|
+
@test_suite_patterns.all? do |pattern|
|
309
|
+
pattern === test_suite_name
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def excluded_test_suite?(test_suite_name)
|
314
|
+
@exclude_test_suite_patterns.any? do |pattern|
|
315
|
+
pattern === test_suite_name
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
private
|
320
|
+
def load_tests(*targets)
|
321
|
+
default_group_name = "."
|
322
|
+
tests = {default_group_name => []}
|
323
|
+
targets.each do |target|
|
324
|
+
target_path = Pathname(target)
|
325
|
+
next unless target_path.exist?
|
326
|
+
if target_path.directory?
|
327
|
+
load_tests_under_directory(tests, target_path)
|
328
|
+
else
|
329
|
+
tests[default_group_name] << target_path
|
330
|
+
end
|
331
|
+
end
|
332
|
+
tests
|
333
|
+
end
|
334
|
+
|
335
|
+
def load_tests_under_directory(tests, test_directory_path)
|
336
|
+
test_file_paths = Pathname.glob(test_directory_path + "**" + "*.test")
|
337
|
+
test_file_paths.each do |test_file_path|
|
338
|
+
directory_path = test_file_path.dirname
|
339
|
+
directory = directory_path.relative_path_from(test_directory_path).to_s
|
340
|
+
tests[directory] ||= []
|
341
|
+
tests[directory] << test_file_path
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def run_test_suites(test_suites)
|
346
|
+
runner = TestSuitesRunner.new(self)
|
347
|
+
runner.run(test_suites)
|
348
|
+
end
|
349
|
+
|
350
|
+
def detect_suitable_diff
|
351
|
+
if command_exist?("cut-diff")
|
352
|
+
@diff = "cut-diff"
|
353
|
+
@diff_options = ["--context-lines", "10"]
|
354
|
+
else
|
355
|
+
@diff = "diff"
|
356
|
+
@diff_options = ["-u"]
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def initialize_debuggers
|
361
|
+
@gdb = nil
|
362
|
+
@default_gdb = "gdb"
|
363
|
+
end
|
364
|
+
|
365
|
+
def command_exist?(name)
|
366
|
+
ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
|
367
|
+
absolute_path = File.join(path, name)
|
368
|
+
return true if File.executable?(absolute_path)
|
369
|
+
end
|
370
|
+
false
|
371
|
+
end
|
372
|
+
|
373
|
+
def guess_color_availability
|
374
|
+
return false unless @output.tty?
|
375
|
+
case ENV["TERM"]
|
376
|
+
when /term(?:-(?:256)?color)?\z/, "screen"
|
377
|
+
true
|
378
|
+
else
|
379
|
+
return true if ENV["EMACS"] == "t"
|
380
|
+
false
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
class Result
|
385
|
+
attr_accessor :elapsed_time
|
386
|
+
def initialize
|
387
|
+
@elapsed_time = 0
|
388
|
+
end
|
389
|
+
|
390
|
+
def measure
|
391
|
+
start_time = Time.now
|
392
|
+
yield
|
393
|
+
ensure
|
394
|
+
@elapsed_time = Time.now - start_time
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
class WorkerResult < Result
|
399
|
+
attr_reader :n_tests, :n_passed_tests, :n_not_checked_tests
|
400
|
+
attr_reader :failed_tests
|
401
|
+
def initialize
|
402
|
+
super
|
403
|
+
@n_tests = 0
|
404
|
+
@n_passed_tests = 0
|
405
|
+
@n_not_checked_tests = 0
|
406
|
+
@failed_tests = []
|
407
|
+
end
|
408
|
+
|
409
|
+
def n_failed_tests
|
410
|
+
@failed_tests.size
|
411
|
+
end
|
412
|
+
|
413
|
+
def test_finished
|
414
|
+
@n_tests += 1
|
415
|
+
end
|
416
|
+
|
417
|
+
def test_passed
|
418
|
+
@n_passed_tests += 1
|
419
|
+
end
|
420
|
+
|
421
|
+
def test_failed(name)
|
422
|
+
@failed_tests << name
|
423
|
+
end
|
424
|
+
|
425
|
+
def test_not_checked
|
426
|
+
@n_not_checked_tests += 1
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
class Worker
|
431
|
+
attr_reader :id, :tester, :test_suites_rusult, :reporter
|
432
|
+
attr_reader :suite_name, :test_script_path, :test_name, :status, :result
|
433
|
+
def initialize(id, tester, test_suites_result, reporter)
|
434
|
+
@id = id
|
435
|
+
@tester = tester
|
436
|
+
@test_suites_result = test_suites_result
|
437
|
+
@reporter = reporter
|
438
|
+
@suite_name = nil
|
439
|
+
@test_script_path = nil
|
440
|
+
@test_name = nil
|
441
|
+
@interruptted = false
|
442
|
+
@status = "not running"
|
443
|
+
@result = WorkerResult.new
|
444
|
+
end
|
445
|
+
|
446
|
+
def interrupt
|
447
|
+
@interruptted = true
|
448
|
+
end
|
449
|
+
|
450
|
+
def interruptted?
|
451
|
+
@interruptted
|
452
|
+
end
|
453
|
+
|
454
|
+
def run(queue)
|
455
|
+
succeeded = true
|
456
|
+
|
457
|
+
@result.measure do
|
458
|
+
@reporter.start_worker(self)
|
459
|
+
catch do |tag|
|
460
|
+
loop do
|
461
|
+
suite_name, test_script_path, test_name = queue.pop
|
462
|
+
break if test_script_path.nil?
|
463
|
+
|
464
|
+
unless @suite_name == suite_name
|
465
|
+
@reporter.finish_suite(self) if @suite_name
|
466
|
+
@suite_name = suite_name
|
467
|
+
@reporter.start_suite(self)
|
468
|
+
end
|
469
|
+
@test_script_path = test_script_path
|
470
|
+
@test_name = test_name
|
471
|
+
runner = TestRunner.new(@tester, self)
|
472
|
+
succeeded = false unless runner.run
|
473
|
+
|
474
|
+
break if interruptted?
|
475
|
+
end
|
476
|
+
@status = "finished"
|
477
|
+
@reporter.finish_suite(@suite_name) if @suite_name
|
478
|
+
@suite_name = nil
|
479
|
+
end
|
480
|
+
end
|
481
|
+
@reporter.finish_worker(self)
|
482
|
+
|
483
|
+
succeeded
|
484
|
+
end
|
485
|
+
|
486
|
+
def start_test
|
487
|
+
@status = "running"
|
488
|
+
@test_result = nil
|
489
|
+
@reporter.start_test(self)
|
490
|
+
end
|
491
|
+
|
492
|
+
def pass_test(result)
|
493
|
+
@status = "passed"
|
494
|
+
@result.test_passed
|
495
|
+
@reporter.pass_test(self, result)
|
496
|
+
end
|
497
|
+
|
498
|
+
def fail_test(result)
|
499
|
+
@status = "failed"
|
500
|
+
@result.test_failed(test_name)
|
501
|
+
@reporter.fail_test(self, result)
|
502
|
+
end
|
503
|
+
|
504
|
+
def not_check_test(result)
|
505
|
+
@status = "not checked"
|
506
|
+
@result.test_not_checked
|
507
|
+
@reporter.not_check_test(self, result)
|
508
|
+
end
|
509
|
+
|
510
|
+
def finish_test(result)
|
511
|
+
@result.test_finished
|
512
|
+
@reporter.finish_test(self, result)
|
513
|
+
@test_script_path = nil
|
514
|
+
@test_name = nil
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
class TestSuitesResult < Result
|
519
|
+
attr_accessor :workers
|
520
|
+
attr_accessor :n_total_tests
|
521
|
+
def initialize
|
522
|
+
super
|
523
|
+
@workers = []
|
524
|
+
@n_total_tests = 0
|
525
|
+
end
|
526
|
+
|
527
|
+
def pass_ratio
|
528
|
+
n_target_tests = n_tests - n_not_checked_tests
|
529
|
+
if n_target_tests.zero?
|
530
|
+
0
|
531
|
+
else
|
532
|
+
(n_passed_tests / n_target_tests.to_f) * 100
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
def n_tests
|
537
|
+
collect_count(:n_tests)
|
538
|
+
end
|
539
|
+
|
540
|
+
def n_passed_tests
|
541
|
+
collect_count(:n_passed_tests)
|
542
|
+
end
|
543
|
+
|
544
|
+
def n_failed_tests
|
545
|
+
collect_count(:n_failed_tests)
|
546
|
+
end
|
547
|
+
|
548
|
+
def n_not_checked_tests
|
549
|
+
collect_count(:n_not_checked_tests)
|
550
|
+
end
|
551
|
+
|
552
|
+
private
|
553
|
+
def collect_count(item)
|
554
|
+
counts = @workers.collect do |worker|
|
555
|
+
worker.result.send(item)
|
556
|
+
end
|
557
|
+
counts.inject(&:+)
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
class TestSuitesRunner
|
562
|
+
def initialize(tester)
|
563
|
+
@tester = tester
|
564
|
+
@reporter = create_reporter
|
565
|
+
@result = TestSuitesResult.new
|
566
|
+
end
|
567
|
+
|
568
|
+
def run(test_suites)
|
569
|
+
succeeded = true
|
570
|
+
|
571
|
+
@result.measure do
|
572
|
+
succeeded = run_test_suites(test_suites)
|
573
|
+
end
|
574
|
+
@reporter.finish(@result)
|
575
|
+
|
576
|
+
succeeded
|
577
|
+
end
|
578
|
+
|
579
|
+
private
|
580
|
+
def run_test_suites(test_suites)
|
581
|
+
queue = Queue.new
|
582
|
+
test_suites.each do |suite_name, test_script_paths|
|
583
|
+
next unless @tester.target_test_suite?(suite_name)
|
584
|
+
test_script_paths.each do |test_script_path|
|
585
|
+
test_name = test_script_path.basename(".*").to_s
|
586
|
+
next unless @tester.target_test?(test_name)
|
587
|
+
queue << [suite_name, test_script_path, test_name]
|
588
|
+
@result.n_total_tests += 1
|
589
|
+
end
|
590
|
+
end
|
591
|
+
@tester.n_workers.times do
|
592
|
+
queue << nil
|
593
|
+
end
|
594
|
+
|
595
|
+
workers = []
|
596
|
+
@tester.n_workers.times do |i|
|
597
|
+
workers << Worker.new(i, @tester, @result, @reporter)
|
598
|
+
end
|
599
|
+
@result.workers = workers
|
600
|
+
@reporter.start(@result)
|
601
|
+
|
602
|
+
succeeded = true
|
603
|
+
worker_threads = []
|
604
|
+
@tester.n_workers.times do |i|
|
605
|
+
worker = workers[i]
|
606
|
+
worker_threads << Thread.new do
|
607
|
+
succeeded = false unless worker.run(queue)
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
begin
|
612
|
+
worker_threads.each(&:join)
|
613
|
+
rescue Interrupt
|
614
|
+
workers.each do |worker|
|
615
|
+
worker.interrupt
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
succeeded
|
620
|
+
end
|
621
|
+
|
622
|
+
def create_reporter
|
623
|
+
case @tester.reporter
|
624
|
+
when :mark
|
625
|
+
MarkReporter.new(@tester)
|
626
|
+
when :stream
|
627
|
+
StreamReporter.new(@tester)
|
628
|
+
when :inplace
|
629
|
+
InplaceReporter.new(@tester)
|
630
|
+
end
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
class TestResult < Result
|
635
|
+
attr_accessor :worker_id, :test_name
|
636
|
+
attr_accessor :expected, :actual
|
637
|
+
def initialize(worker)
|
638
|
+
super()
|
639
|
+
@worker_id = worker.id
|
640
|
+
@test_name = worker.test_name
|
641
|
+
@actual = nil
|
642
|
+
@expected = nil
|
643
|
+
end
|
644
|
+
|
645
|
+
def status
|
646
|
+
if @expected
|
647
|
+
if @actual == @expected
|
648
|
+
:success
|
649
|
+
else
|
650
|
+
:failure
|
651
|
+
end
|
652
|
+
else
|
653
|
+
:not_checked
|
654
|
+
end
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
class TestRunner
|
659
|
+
MAX_N_COLUMNS = 79
|
660
|
+
|
661
|
+
def initialize(tester, worker)
|
662
|
+
@tester = tester
|
663
|
+
@worker = worker
|
664
|
+
@max_n_columns = MAX_N_COLUMNS
|
665
|
+
@id = nil
|
666
|
+
end
|
667
|
+
|
668
|
+
def run
|
669
|
+
succeeded = true
|
670
|
+
|
671
|
+
@worker.start_test
|
672
|
+
result = TestResult.new(@worker)
|
673
|
+
result.measure do
|
674
|
+
result.actual = execute_groonga_script
|
675
|
+
end
|
676
|
+
result.actual = normalize_result(result.actual)
|
677
|
+
result.expected = read_expected_result
|
678
|
+
case result.status
|
679
|
+
when :success
|
680
|
+
@worker.pass_test(result)
|
681
|
+
remove_reject_file
|
682
|
+
when :failure
|
683
|
+
@worker.fail_test(result)
|
684
|
+
output_reject_file(result.actual)
|
685
|
+
succeeded = false
|
686
|
+
else
|
687
|
+
@worker.not_check_test(result)
|
688
|
+
output_actual_file(result.actual)
|
689
|
+
end
|
690
|
+
@worker.finish_test(result)
|
691
|
+
|
692
|
+
succeeded
|
693
|
+
end
|
694
|
+
|
695
|
+
private
|
696
|
+
def execute_groonga_script
|
697
|
+
create_temporary_directory do |directory_path|
|
698
|
+
db_dir = directory_path + "db"
|
699
|
+
FileUtils.mkdir_p(db_dir.to_s)
|
700
|
+
db_path = db_dir + "db"
|
701
|
+
context = Executor::Context.new
|
702
|
+
context.temporary_directory_path = directory_path
|
703
|
+
context.db_path = db_path
|
704
|
+
context.base_directory = @tester.base_directory.expand_path
|
705
|
+
context.groonga_suggest_create_dataset =
|
706
|
+
@tester.groonga_suggest_create_dataset
|
707
|
+
context.output_type = @tester.output_type
|
708
|
+
run_groonga(context) do |executor|
|
709
|
+
executor.execute(test_script_path)
|
710
|
+
end
|
711
|
+
context.result
|
712
|
+
end
|
713
|
+
end
|
714
|
+
|
715
|
+
def create_temporary_directory
|
716
|
+
path = "tmp/grntest"
|
717
|
+
path << ".#{@worker.id}" if @tester.n_workers > 1
|
718
|
+
FileUtils.rm_rf(path, :secure => true)
|
719
|
+
FileUtils.mkdir_p(path)
|
720
|
+
begin
|
721
|
+
yield(Pathname(path).expand_path)
|
722
|
+
ensure
|
723
|
+
if @tester.keep_database? and File.exist?(path)
|
724
|
+
FileUtils.rm_rf(keep_database_path, :secure => true)
|
725
|
+
FileUtils.mv(path, keep_database_path)
|
726
|
+
else
|
727
|
+
FileUtils.rm_rf(path, :secure => true)
|
728
|
+
end
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
732
|
+
def keep_database_path
|
733
|
+
test_script_path.to_s.gsub(/\//, ".")
|
734
|
+
end
|
735
|
+
|
736
|
+
def run_groonga(context, &block)
|
737
|
+
case @tester.interface
|
738
|
+
when :stdio
|
739
|
+
run_groonga_stdio(context, &block)
|
740
|
+
when :http
|
741
|
+
run_groonga_http(context, &block)
|
742
|
+
end
|
743
|
+
end
|
744
|
+
|
745
|
+
def run_groonga_stdio(context)
|
746
|
+
pid = nil
|
747
|
+
begin
|
748
|
+
open_pipe do |input_read, input_write, output_read, output_write|
|
749
|
+
groonga_input = input_write
|
750
|
+
groonga_output = output_read
|
751
|
+
|
752
|
+
input_fd = input_read.to_i
|
753
|
+
output_fd = output_write.to_i
|
754
|
+
env = {}
|
755
|
+
spawn_options = {
|
756
|
+
input_fd => input_fd,
|
757
|
+
output_fd => output_fd
|
758
|
+
}
|
759
|
+
command_line = groonga_command_line(context, spawn_options)
|
760
|
+
command_line += [
|
761
|
+
"--input-fd", input_fd.to_s,
|
762
|
+
"--output-fd", output_fd.to_s,
|
763
|
+
"-n",
|
764
|
+
context.relative_db_path.to_s,
|
765
|
+
]
|
766
|
+
pid = Process.spawn(env, *command_line, spawn_options)
|
767
|
+
executor = StandardIOExecutor.new(groonga_input,
|
768
|
+
groonga_output,
|
769
|
+
context)
|
770
|
+
executor.ensure_groonga_ready
|
771
|
+
yield(executor)
|
772
|
+
end
|
773
|
+
ensure
|
774
|
+
Process.waitpid(pid) if pid
|
775
|
+
end
|
776
|
+
end
|
777
|
+
|
778
|
+
def open_pipe
|
779
|
+
IO.pipe("ASCII-8BIT") do |input_read, input_write|
|
780
|
+
IO.pipe("ASCII-8BIT") do |output_read, output_write|
|
781
|
+
yield(input_read, input_write, output_read, output_write)
|
782
|
+
end
|
783
|
+
end
|
784
|
+
end
|
785
|
+
|
786
|
+
def command_command_line(command, context, spawn_options)
|
787
|
+
command_line = []
|
788
|
+
if @tester.gdb
|
789
|
+
if libtool_wrapper?(command)
|
790
|
+
command_line << find_libtool(command)
|
791
|
+
command_line << "--mode=execute"
|
792
|
+
end
|
793
|
+
command_line << @tester.gdb
|
794
|
+
gdb_command_path = context.temporary_directory_path + "groonga.gdb"
|
795
|
+
File.open(gdb_command_path, "w") do |gdb_command|
|
796
|
+
gdb_command.puts(<<-EOC)
|
797
|
+
break main
|
798
|
+
run
|
799
|
+
print chdir("#{context.temporary_directory_path}")
|
800
|
+
EOC
|
801
|
+
end
|
802
|
+
command_line << "--command=#{gdb_command_path}"
|
803
|
+
command_line << "--quiet"
|
804
|
+
command_line << "--args"
|
805
|
+
else
|
806
|
+
spawn_options[:chdir] = context.temporary_directory_path.to_s
|
807
|
+
end
|
808
|
+
command_line << command
|
809
|
+
command_line
|
810
|
+
end
|
811
|
+
|
812
|
+
def groonga_command_line(context, spawn_options)
|
813
|
+
command_line = command_command_line(@tester.groonga, context,
|
814
|
+
spawn_options)
|
815
|
+
command_line << "--log-path=#{context.log_path}"
|
816
|
+
command_line << "--working-directory=#{context.temporary_directory_path}"
|
817
|
+
command_line
|
818
|
+
end
|
819
|
+
|
820
|
+
def libtool_wrapper?(command)
|
821
|
+
return false unless File.exist?(command)
|
822
|
+
File.open(command, "r") do |command_file|
|
823
|
+
first_line = command_file.gets
|
824
|
+
first_line.start_with?("#!")
|
825
|
+
end
|
826
|
+
end
|
827
|
+
|
828
|
+
def find_libtool(command)
|
829
|
+
command_path = Pathname.new(command)
|
830
|
+
directory = command_path.dirname
|
831
|
+
until directory.root?
|
832
|
+
libtool = directory + "libtool"
|
833
|
+
return libtool.to_s if libtool.executable?
|
834
|
+
directory = directory.parent
|
835
|
+
end
|
836
|
+
"libtool"
|
837
|
+
end
|
838
|
+
|
839
|
+
def run_groonga_http(context)
|
840
|
+
host = "127.0.0.1"
|
841
|
+
port = 50041 + @worker.id
|
842
|
+
pid_file_path = context.temporary_directory_path + "groonga.pid"
|
843
|
+
|
844
|
+
env = {}
|
845
|
+
spawn_options = {}
|
846
|
+
command_line = groonga_http_command(host, port, pid_file_path, context,
|
847
|
+
spawn_options)
|
848
|
+
pid = nil
|
849
|
+
begin
|
850
|
+
pid = Process.spawn(env, *command_line, spawn_options)
|
851
|
+
begin
|
852
|
+
executor = HTTPExecutor.new(host, port, context)
|
853
|
+
begin
|
854
|
+
executor.ensure_groonga_ready
|
855
|
+
rescue
|
856
|
+
if Process.waitpid(pid, Process::WNOHANG)
|
857
|
+
pid = nil
|
858
|
+
raise
|
859
|
+
end
|
860
|
+
raise unless @tester.gdb
|
861
|
+
retry
|
862
|
+
end
|
863
|
+
yield(executor)
|
864
|
+
ensure
|
865
|
+
executor.send_command("shutdown")
|
866
|
+
wait_groonga_http_shutdown(pid_file_path)
|
867
|
+
end
|
868
|
+
ensure
|
869
|
+
Process.waitpid(pid) if pid
|
870
|
+
end
|
871
|
+
end
|
872
|
+
|
873
|
+
def wait_groonga_http_shutdown(pid_file_path)
|
874
|
+
total_sleep_time = 0
|
875
|
+
sleep_time = 0.1
|
876
|
+
while pid_file_path.exist?
|
877
|
+
sleep(sleep_time)
|
878
|
+
total_sleep_time += sleep_time
|
879
|
+
break if total_sleep_time > 1.0
|
880
|
+
end
|
881
|
+
end
|
882
|
+
|
883
|
+
def groonga_http_command(host, port, pid_file_path, context, spawn_options)
|
884
|
+
case @tester.testee
|
885
|
+
when "groonga"
|
886
|
+
command_line = groonga_command_line(context, spawn_options)
|
887
|
+
command_line += [
|
888
|
+
"--pid-path", pid_file_path.to_s,
|
889
|
+
"--bind-address", host,
|
890
|
+
"--port", port.to_s,
|
891
|
+
"--protocol", "http",
|
892
|
+
"-s",
|
893
|
+
"-n",
|
894
|
+
context.relative_db_path.to_s,
|
895
|
+
]
|
896
|
+
when "groonga-httpd"
|
897
|
+
command_line = command_command_line(@tester.groonga_httpd, context,
|
898
|
+
spawn_options)
|
899
|
+
config_file_path = create_config_file(context, host, port,
|
900
|
+
pid_file_path)
|
901
|
+
command_line += [
|
902
|
+
"-c", config_file_path.to_s,
|
903
|
+
"-p", "#{context.temporary_directory_path}/",
|
904
|
+
]
|
905
|
+
end
|
906
|
+
command_line
|
907
|
+
end
|
908
|
+
|
909
|
+
def create_config_file(context, host, port, pid_file_path)
|
910
|
+
create_empty_database(context.db_path.to_s)
|
911
|
+
config_file_path =
|
912
|
+
context.temporary_directory_path + "groonga-httpd.conf"
|
913
|
+
config_file_path.open("w") do |config_file|
|
914
|
+
config_file.puts(<<EOF)
|
915
|
+
daemon off;
|
916
|
+
master_process off;
|
917
|
+
worker_processes 1;
|
918
|
+
working_directory #{context.temporary_directory_path};
|
919
|
+
error_log groonga-httpd-access.log;
|
920
|
+
pid #{pid_file_path};
|
921
|
+
events {
|
922
|
+
worker_connections 1024;
|
923
|
+
}
|
924
|
+
|
925
|
+
http {
|
926
|
+
server {
|
927
|
+
access_log groonga-httpd-access.log;
|
928
|
+
listen #{port};
|
929
|
+
server_name #{host};
|
930
|
+
location /d/ {
|
931
|
+
groonga_database #{context.relative_db_path};
|
932
|
+
groonga on;
|
933
|
+
}
|
934
|
+
}
|
935
|
+
}
|
936
|
+
EOF
|
937
|
+
end
|
938
|
+
config_file_path
|
939
|
+
end
|
940
|
+
|
941
|
+
def create_empty_database(db_path)
|
942
|
+
output_fd = Tempfile.new("create-empty-database")
|
943
|
+
create_database_command = [
|
944
|
+
@tester.groonga,
|
945
|
+
"--output-fd", output_fd.to_i.to_s,
|
946
|
+
"-n", db_path,
|
947
|
+
"shutdown"
|
948
|
+
]
|
949
|
+
system(*create_database_command)
|
950
|
+
output_fd.close(true)
|
951
|
+
end
|
952
|
+
|
953
|
+
def normalize_result(result)
|
954
|
+
normalized_result = ""
|
955
|
+
result.each do |tag, content, options|
|
956
|
+
case tag
|
957
|
+
when :input
|
958
|
+
normalized_result << content
|
959
|
+
when :output
|
960
|
+
normalized_result << normalize_output(content, options)
|
961
|
+
when :error
|
962
|
+
normalized_result << normalize_raw_content(content)
|
963
|
+
end
|
964
|
+
end
|
965
|
+
normalized_result
|
966
|
+
end
|
967
|
+
|
968
|
+
def normalize_raw_content(content)
|
969
|
+
"#{content}\n".force_encoding("ASCII-8BIT")
|
970
|
+
end
|
971
|
+
|
972
|
+
def normalize_output(content, options)
|
973
|
+
type = options[:type]
|
974
|
+
case type
|
975
|
+
when "json", "msgpack"
|
976
|
+
status = nil
|
977
|
+
values = nil
|
978
|
+
begin
|
979
|
+
status, *values = parse_result(content.chomp, type)
|
980
|
+
rescue ParseError
|
981
|
+
return $!.message
|
982
|
+
end
|
983
|
+
normalized_status = normalize_status(status)
|
984
|
+
normalized_output_content = [normalized_status, *values]
|
985
|
+
normalized_output = JSON.generate(normalized_output_content)
|
986
|
+
if normalized_output.bytesize > @max_n_columns
|
987
|
+
normalized_output = JSON.pretty_generate(normalized_output_content)
|
988
|
+
end
|
989
|
+
normalize_raw_content(normalized_output)
|
990
|
+
else
|
991
|
+
normalize_raw_content(content)
|
992
|
+
end
|
993
|
+
end
|
994
|
+
|
995
|
+
def parse_result(result, type)
|
996
|
+
case type
|
997
|
+
when "json"
|
998
|
+
begin
|
999
|
+
JSON.parse(result)
|
1000
|
+
rescue JSON::ParserError
|
1001
|
+
raise ParseError.new(type, result, $!.message)
|
1002
|
+
end
|
1003
|
+
when "msgpack"
|
1004
|
+
begin
|
1005
|
+
MessagePack.unpack(result.chomp)
|
1006
|
+
rescue MessagePack::UnpackError, NoMemoryError
|
1007
|
+
raise ParseError.new(type, result, $!.message)
|
1008
|
+
end
|
1009
|
+
else
|
1010
|
+
raise ParseError.new(type, result, "unknown type")
|
1011
|
+
end
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
def normalize_status(status)
|
1015
|
+
return_code, started_time, elapsed_time, *rest = status
|
1016
|
+
_ = started_time = elapsed_time # for suppress warnings
|
1017
|
+
if return_code.zero?
|
1018
|
+
[0, 0.0, 0.0]
|
1019
|
+
else
|
1020
|
+
message, backtrace = rest
|
1021
|
+
_ = backtrace # for suppress warnings
|
1022
|
+
[[return_code, 0.0, 0.0], message]
|
1023
|
+
end
|
1024
|
+
end
|
1025
|
+
|
1026
|
+
def test_script_path
|
1027
|
+
@worker.test_script_path
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
def have_extension?
|
1031
|
+
not test_script_path.extname.empty?
|
1032
|
+
end
|
1033
|
+
|
1034
|
+
def related_file_path(extension)
|
1035
|
+
path = Pathname(test_script_path.to_s.gsub(/\.[^.]+\z/, ".#{extension}"))
|
1036
|
+
return nil if test_script_path == path
|
1037
|
+
path
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
def read_expected_result
|
1041
|
+
return nil unless have_extension?
|
1042
|
+
result_path = related_file_path("expected")
|
1043
|
+
return nil if result_path.nil?
|
1044
|
+
return nil unless result_path.exist?
|
1045
|
+
result_path.open("r:ascii-8bit") do |result_file|
|
1046
|
+
result_file.read
|
1047
|
+
end
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
def remove_reject_file
|
1051
|
+
return unless have_extension?
|
1052
|
+
reject_path = related_file_path("reject")
|
1053
|
+
return if reject_path.nil?
|
1054
|
+
FileUtils.rm_rf(reject_path.to_s, :secure => true)
|
1055
|
+
end
|
1056
|
+
|
1057
|
+
def output_reject_file(actual_result)
|
1058
|
+
output_actual_result(actual_result, "reject")
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
def output_actual_file(actual_result)
|
1062
|
+
output_actual_result(actual_result, "actual")
|
1063
|
+
end
|
1064
|
+
|
1065
|
+
def output_actual_result(actual_result, suffix)
|
1066
|
+
result_path = related_file_path(suffix)
|
1067
|
+
return if result_path.nil?
|
1068
|
+
result_path.open("w:ascii-8bit") do |result_file|
|
1069
|
+
result_file.print(actual_result)
|
1070
|
+
end
|
1071
|
+
end
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
class Executor
|
1075
|
+
class Context
|
1076
|
+
attr_writer :logging
|
1077
|
+
attr_accessor :base_directory, :temporary_directory_path, :db_path
|
1078
|
+
attr_accessor :groonga_suggest_create_dataset
|
1079
|
+
attr_accessor :result
|
1080
|
+
attr_accessor :output_type
|
1081
|
+
def initialize
|
1082
|
+
@logging = true
|
1083
|
+
@base_directory = Pathname(".")
|
1084
|
+
@temporary_directory_path = Pathname("tmp")
|
1085
|
+
@db_path = Pathname("db")
|
1086
|
+
@groonga_suggest_create_dataset = "groonga-suggest-create-dataset"
|
1087
|
+
@n_nested = 0
|
1088
|
+
@result = []
|
1089
|
+
@output_type = "json"
|
1090
|
+
@log = nil
|
1091
|
+
end
|
1092
|
+
|
1093
|
+
def logging?
|
1094
|
+
@logging
|
1095
|
+
end
|
1096
|
+
|
1097
|
+
def execute
|
1098
|
+
@n_nested += 1
|
1099
|
+
yield
|
1100
|
+
ensure
|
1101
|
+
@n_nested -= 1
|
1102
|
+
end
|
1103
|
+
|
1104
|
+
def top_level?
|
1105
|
+
@n_nested == 1
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
def log_path
|
1109
|
+
@temporary_directory_path + "groonga.log"
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
def log
|
1113
|
+
@log ||= File.open(log_path.to_s, "a+")
|
1114
|
+
end
|
1115
|
+
|
1116
|
+
def relative_db_path
|
1117
|
+
@db_path.relative_path_from(@temporary_directory_path)
|
1118
|
+
end
|
1119
|
+
end
|
1120
|
+
|
1121
|
+
attr_reader :context
|
1122
|
+
def initialize(context=nil)
|
1123
|
+
@loading = false
|
1124
|
+
@pending_command = ""
|
1125
|
+
@pending_load_command = nil
|
1126
|
+
@current_command_name = nil
|
1127
|
+
@output_type = nil
|
1128
|
+
@context = context || Context.new
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
def execute(script_path)
|
1132
|
+
unless script_path.exist?
|
1133
|
+
raise NotExist.new(script_path)
|
1134
|
+
end
|
1135
|
+
|
1136
|
+
@context.execute do
|
1137
|
+
script_path.open("r:ascii-8bit") do |script_file|
|
1138
|
+
script_file.each_line do |line|
|
1139
|
+
begin
|
1140
|
+
if @loading
|
1141
|
+
execute_line_on_loading(line)
|
1142
|
+
else
|
1143
|
+
execute_line_with_continuation_line_support(line)
|
1144
|
+
end
|
1145
|
+
rescue Error
|
1146
|
+
line_info = "#{script_path}:#{script_file.lineno}:#{line.chomp}"
|
1147
|
+
log_error("#{line_info}: #{$!.message}")
|
1148
|
+
raise unless @context.top_level?
|
1149
|
+
end
|
1150
|
+
end
|
1151
|
+
end
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
@context.result
|
1155
|
+
end
|
1156
|
+
|
1157
|
+
private
|
1158
|
+
def execute_line_on_loading(line)
|
1159
|
+
log_input(line)
|
1160
|
+
@pending_load_command << line
|
1161
|
+
if line == "]\n"
|
1162
|
+
execute_command(@pending_load_command)
|
1163
|
+
@pending_load_command = nil
|
1164
|
+
@loading = false
|
1165
|
+
end
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
def execute_line_with_continuation_line_support(line)
|
1169
|
+
if /\\$/ =~ line
|
1170
|
+
@pending_command << $PREMATCH
|
1171
|
+
else
|
1172
|
+
if @pending_command.empty?
|
1173
|
+
execute_line(line)
|
1174
|
+
else
|
1175
|
+
@pending_command << line
|
1176
|
+
execute_line(@pending_command)
|
1177
|
+
@pending_command = ""
|
1178
|
+
end
|
1179
|
+
end
|
1180
|
+
end
|
1181
|
+
|
1182
|
+
def execute_line(line)
|
1183
|
+
case line
|
1184
|
+
when /\A\s*\z/
|
1185
|
+
# do nothing
|
1186
|
+
when /\A\s*\#/
|
1187
|
+
comment_content = $POSTMATCH
|
1188
|
+
execute_comment(comment_content)
|
1189
|
+
else
|
1190
|
+
execute_command_line(line)
|
1191
|
+
end
|
1192
|
+
end
|
1193
|
+
|
1194
|
+
def execute_comment(content)
|
1195
|
+
command, *options = Shellwords.split(content)
|
1196
|
+
case command
|
1197
|
+
when "disable-logging"
|
1198
|
+
@context.logging = false
|
1199
|
+
when "enable-logging"
|
1200
|
+
@context.logging = true
|
1201
|
+
when "suggest-create-dataset"
|
1202
|
+
dataset_name = options.first
|
1203
|
+
return if dataset_name.nil?
|
1204
|
+
execute_suggest_create_dataset(dataset_name)
|
1205
|
+
when "include"
|
1206
|
+
path = options.first
|
1207
|
+
return if path.nil?
|
1208
|
+
execute_script(Pathname(path))
|
1209
|
+
end
|
1210
|
+
end
|
1211
|
+
|
1212
|
+
def execute_suggest_create_dataset(dataset_name)
|
1213
|
+
command_line = [@context.groonga_suggest_create_dataset,
|
1214
|
+
@context.db_path.to_s,
|
1215
|
+
dataset_name]
|
1216
|
+
packed_command_line = command_line.join(" ")
|
1217
|
+
log_input("#{packed_command_line}\n")
|
1218
|
+
begin
|
1219
|
+
IO.popen(command_line, "r:ascii-8bit") do |io|
|
1220
|
+
log_output(io.read)
|
1221
|
+
end
|
1222
|
+
rescue SystemCallError
|
1223
|
+
raise Error.new("failed to run groonga-suggest-create-dataset: " +
|
1224
|
+
"<#{packed_command_line}>: #{$!}")
|
1225
|
+
end
|
1226
|
+
end
|
1227
|
+
|
1228
|
+
def execute_script(script_path)
|
1229
|
+
executor = create_sub_executor(@context)
|
1230
|
+
if script_path.relative?
|
1231
|
+
script_path = @context.base_directory + script_path
|
1232
|
+
end
|
1233
|
+
executor.execute(script_path)
|
1234
|
+
end
|
1235
|
+
|
1236
|
+
def execute_command_line(command_line)
|
1237
|
+
extract_command_info(command_line)
|
1238
|
+
log_input(command_line)
|
1239
|
+
if multiline_load_command?
|
1240
|
+
@loading = true
|
1241
|
+
@pending_load_command = command_line.dup
|
1242
|
+
else
|
1243
|
+
execute_command(command_line)
|
1244
|
+
end
|
1245
|
+
end
|
1246
|
+
|
1247
|
+
def extract_command_info(command_line)
|
1248
|
+
@current_command, *@current_arguments = Shellwords.split(command_line)
|
1249
|
+
if @current_command == "dump"
|
1250
|
+
@output_type = "groonga-command"
|
1251
|
+
else
|
1252
|
+
@output_type = @context.output_type
|
1253
|
+
@current_arguments.each_with_index do |word, i|
|
1254
|
+
if /\A--output_type(?:=(.+))?\z/ =~ word
|
1255
|
+
@output_type = $1 || words[i + 1]
|
1256
|
+
break
|
1257
|
+
end
|
1258
|
+
end
|
1259
|
+
end
|
1260
|
+
end
|
1261
|
+
|
1262
|
+
def have_output_type_argument?
|
1263
|
+
@current_arguments.any? do |argument|
|
1264
|
+
/\A--output_type(?:=.+)?\z/ =~ argument
|
1265
|
+
end
|
1266
|
+
end
|
1267
|
+
|
1268
|
+
def multiline_load_command?
|
1269
|
+
@current_command == "load" and
|
1270
|
+
not @current_arguments.include?("--values")
|
1271
|
+
end
|
1272
|
+
|
1273
|
+
def execute_command(command)
|
1274
|
+
log_output(send_command(command))
|
1275
|
+
log_error(read_error_log)
|
1276
|
+
end
|
1277
|
+
|
1278
|
+
def read_error_log
|
1279
|
+
log = read_all_readable_content(context.log, :first_timeout => 0)
|
1280
|
+
normalized_error_log = ""
|
1281
|
+
log.each_line do |line|
|
1282
|
+
timestamp, log_level, message = line.split(/\|\s*/, 3)
|
1283
|
+
_ = timestamp # suppress warning
|
1284
|
+
next unless error_log_level?(log_level)
|
1285
|
+
next if backtrace_log_message?(message)
|
1286
|
+
normalized_error_log << "\#|#{log_level}| #{message}"
|
1287
|
+
end
|
1288
|
+
normalized_error_log.chomp
|
1289
|
+
end
|
1290
|
+
|
1291
|
+
def read_all_readable_content(output, options={})
|
1292
|
+
content = ""
|
1293
|
+
first_timeout = options[:first_timeout] || 1
|
1294
|
+
timeout = first_timeout
|
1295
|
+
while IO.select([output], [], [], timeout)
|
1296
|
+
break if output.eof?
|
1297
|
+
content << output.readpartial(65535)
|
1298
|
+
timeout = 0
|
1299
|
+
end
|
1300
|
+
content
|
1301
|
+
end
|
1302
|
+
|
1303
|
+
def error_log_level?(log_level)
|
1304
|
+
["E", "A", "C", "e"].include?(log_level)
|
1305
|
+
end
|
1306
|
+
|
1307
|
+
def backtrace_log_message?(message)
|
1308
|
+
message.start_with?("/")
|
1309
|
+
end
|
1310
|
+
|
1311
|
+
def log(tag, content, options={})
|
1312
|
+
return unless @context.logging?
|
1313
|
+
log_force(tag, content, options)
|
1314
|
+
end
|
1315
|
+
|
1316
|
+
def log_force(tag, content, options)
|
1317
|
+
return if content.empty?
|
1318
|
+
@context.result << [tag, content, options]
|
1319
|
+
end
|
1320
|
+
|
1321
|
+
def log_input(content)
|
1322
|
+
log(:input, content)
|
1323
|
+
end
|
1324
|
+
|
1325
|
+
def log_output(content)
|
1326
|
+
log(:output, content,
|
1327
|
+
:command => @current_command,
|
1328
|
+
:type => @output_type)
|
1329
|
+
@current_command = nil
|
1330
|
+
@output_type = nil
|
1331
|
+
end
|
1332
|
+
|
1333
|
+
def log_error(content)
|
1334
|
+
log_force(:error, content, {})
|
1335
|
+
end
|
1336
|
+
end
|
1337
|
+
|
1338
|
+
class StandardIOExecutor < Executor
|
1339
|
+
def initialize(input, output, context=nil)
|
1340
|
+
super(context)
|
1341
|
+
@input = input
|
1342
|
+
@output = output
|
1343
|
+
end
|
1344
|
+
|
1345
|
+
def send_command(command_line)
|
1346
|
+
unless have_output_type_argument?
|
1347
|
+
command_line = command_line.sub(/$/, " --output_type #{@output_type}")
|
1348
|
+
end
|
1349
|
+
begin
|
1350
|
+
@input.print(command_line)
|
1351
|
+
@input.flush
|
1352
|
+
rescue SystemCallError
|
1353
|
+
message = "failed to write to groonga: <#{command_line}>: #{$!}"
|
1354
|
+
raise Error.new(message)
|
1355
|
+
end
|
1356
|
+
read_output
|
1357
|
+
end
|
1358
|
+
|
1359
|
+
def ensure_groonga_ready
|
1360
|
+
@input.print("status\n")
|
1361
|
+
@input.flush
|
1362
|
+
@output.gets
|
1363
|
+
end
|
1364
|
+
|
1365
|
+
def create_sub_executor(context)
|
1366
|
+
self.class.new(@input, @output, context)
|
1367
|
+
end
|
1368
|
+
|
1369
|
+
private
|
1370
|
+
def read_output
|
1371
|
+
read_all_readable_content(@output)
|
1372
|
+
end
|
1373
|
+
end
|
1374
|
+
|
1375
|
+
class HTTPExecutor < Executor
|
1376
|
+
def initialize(host, port, context=nil)
|
1377
|
+
super(context)
|
1378
|
+
@host = host
|
1379
|
+
@port = port
|
1380
|
+
end
|
1381
|
+
|
1382
|
+
def send_command(command_line)
|
1383
|
+
converter = CommandFormatConverter.new(command_line)
|
1384
|
+
url = "http://#{@host}:#{@port}#{converter.to_url}"
|
1385
|
+
begin
|
1386
|
+
open(url) do |response|
|
1387
|
+
"#{response.read}\n"
|
1388
|
+
end
|
1389
|
+
rescue OpenURI::HTTPError
|
1390
|
+
message = "Failed to get response from groonga: #{$!}: <#{url}>"
|
1391
|
+
raise Error.new(message)
|
1392
|
+
end
|
1393
|
+
end
|
1394
|
+
|
1395
|
+
def ensure_groonga_ready
|
1396
|
+
n_retried = 0
|
1397
|
+
begin
|
1398
|
+
send_command("status")
|
1399
|
+
rescue SystemCallError
|
1400
|
+
n_retried += 1
|
1401
|
+
sleep(0.1)
|
1402
|
+
retry if n_retried < 10
|
1403
|
+
raise
|
1404
|
+
end
|
1405
|
+
end
|
1406
|
+
|
1407
|
+
def create_sub_executor(context)
|
1408
|
+
self.class.new(@host, @port, context)
|
1409
|
+
end
|
1410
|
+
end
|
1411
|
+
|
1412
|
+
class CommandFormatConverter
|
1413
|
+
def initialize(gqtp_command)
|
1414
|
+
@gqtp_command = gqtp_command
|
1415
|
+
end
|
1416
|
+
|
1417
|
+
def to_url
|
1418
|
+
command = nil
|
1419
|
+
arguments = nil
|
1420
|
+
load_values = ""
|
1421
|
+
@gqtp_command.each_line.with_index do |line, i|
|
1422
|
+
if i.zero?
|
1423
|
+
command, *arguments = Shellwords.split(line)
|
1424
|
+
else
|
1425
|
+
load_values << line
|
1426
|
+
end
|
1427
|
+
end
|
1428
|
+
arguments.concat(["--values", load_values]) unless load_values.empty?
|
1429
|
+
|
1430
|
+
named_arguments = convert_to_named_arguments(command, arguments)
|
1431
|
+
build_url(command, named_arguments)
|
1432
|
+
end
|
1433
|
+
|
1434
|
+
private
|
1435
|
+
def convert_to_named_arguments(command, arguments)
|
1436
|
+
named_arguments = {}
|
1437
|
+
|
1438
|
+
last_argument_name = nil
|
1439
|
+
n_non_named_arguments = 0
|
1440
|
+
arguments.each do |argument|
|
1441
|
+
if /\A--/ =~ argument
|
1442
|
+
last_argument_name = $POSTMATCH
|
1443
|
+
next
|
1444
|
+
end
|
1445
|
+
|
1446
|
+
if last_argument_name.nil?
|
1447
|
+
argument_name = arguments_name(command)[n_non_named_arguments]
|
1448
|
+
n_non_named_arguments += 1
|
1449
|
+
else
|
1450
|
+
argument_name = last_argument_name
|
1451
|
+
last_argument_name = nil
|
1452
|
+
end
|
1453
|
+
|
1454
|
+
named_arguments[argument_name] = argument
|
1455
|
+
end
|
1456
|
+
|
1457
|
+
named_arguments
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
def arguments_name(command)
|
1461
|
+
case command
|
1462
|
+
when "table_create"
|
1463
|
+
["name", "flags", "key_type", "value_type", "default_tokenizer"]
|
1464
|
+
when "column_create"
|
1465
|
+
["table", "name", "flags", "type", "source"]
|
1466
|
+
when "load"
|
1467
|
+
["values", "table", "columns", "ifexists", "input_type"]
|
1468
|
+
when "select"
|
1469
|
+
["table"]
|
1470
|
+
when "suggest"
|
1471
|
+
[
|
1472
|
+
"types", "table", "column", "query", "sortby",
|
1473
|
+
"output_columns", "offset", "limit", "frequency_threshold",
|
1474
|
+
"conditional_probability_threshold", "prefix_search"
|
1475
|
+
]
|
1476
|
+
when "truncate"
|
1477
|
+
["table"]
|
1478
|
+
when "get"
|
1479
|
+
["table", "key", "output_columns", "id"]
|
1480
|
+
else
|
1481
|
+
nil
|
1482
|
+
end
|
1483
|
+
end
|
1484
|
+
|
1485
|
+
def build_url(command, named_arguments)
|
1486
|
+
url = "/d/#{command}"
|
1487
|
+
query_parameters = []
|
1488
|
+
named_arguments.each do |name, argument|
|
1489
|
+
query_parameters << "#{CGI.escape(name)}=#{CGI.escape(argument)}"
|
1490
|
+
end
|
1491
|
+
unless query_parameters.empty?
|
1492
|
+
url << "?"
|
1493
|
+
url << query_parameters.join("&")
|
1494
|
+
end
|
1495
|
+
url
|
1496
|
+
end
|
1497
|
+
end
|
1498
|
+
|
1499
|
+
class BaseReporter
|
1500
|
+
def initialize(tester)
|
1501
|
+
@tester = tester
|
1502
|
+
@term_width = guess_term_width
|
1503
|
+
@output = @tester.output
|
1504
|
+
@mutex = Mutex.new
|
1505
|
+
reset_current_column
|
1506
|
+
end
|
1507
|
+
|
1508
|
+
private
|
1509
|
+
def synchronize
|
1510
|
+
@mutex.synchronize do
|
1511
|
+
yield
|
1512
|
+
end
|
1513
|
+
end
|
1514
|
+
|
1515
|
+
def report_summary(result)
|
1516
|
+
puts(colorize(statistics(result), result))
|
1517
|
+
pass_ratio = result.pass_ratio
|
1518
|
+
elapsed_time = result.elapsed_time
|
1519
|
+
summary = "%.4g%% passed in %.4fs." % [pass_ratio, elapsed_time]
|
1520
|
+
puts(colorize(summary, result))
|
1521
|
+
end
|
1522
|
+
|
1523
|
+
def statistics(result)
|
1524
|
+
items = [
|
1525
|
+
"#{result.n_tests} tests",
|
1526
|
+
"#{result.n_passed_tests} passes",
|
1527
|
+
"#{result.n_failed_tests} failures",
|
1528
|
+
"#{result.n_not_checked_tests} not checked_tests",
|
1529
|
+
]
|
1530
|
+
"#{throughput(result)}: " + items.join(", ")
|
1531
|
+
end
|
1532
|
+
|
1533
|
+
def throughput(result)
|
1534
|
+
if result.elapsed_time.zero?
|
1535
|
+
tests_per_second = 0
|
1536
|
+
else
|
1537
|
+
tests_per_second = result.n_tests / result.elapsed_time
|
1538
|
+
end
|
1539
|
+
"%.2f tests/sec" % tests_per_second
|
1540
|
+
end
|
1541
|
+
|
1542
|
+
def report_failure(result)
|
1543
|
+
report_marker(result)
|
1544
|
+
report_diff(result.expected, result.actual)
|
1545
|
+
report_marker(result)
|
1546
|
+
end
|
1547
|
+
|
1548
|
+
def report_actual(result)
|
1549
|
+
report_marker(result)
|
1550
|
+
puts(result.actual)
|
1551
|
+
report_marker(result)
|
1552
|
+
end
|
1553
|
+
|
1554
|
+
def report_marker(result)
|
1555
|
+
puts(colorize("=" * @term_width, result))
|
1556
|
+
end
|
1557
|
+
|
1558
|
+
def report_diff(expected, actual)
|
1559
|
+
create_temporary_file("expected", expected) do |expected_file|
|
1560
|
+
create_temporary_file("actual", actual) do |actual_file|
|
1561
|
+
diff_options = @tester.diff_options.dup
|
1562
|
+
diff_options.concat(["--label", "(actual)", actual_file.path,
|
1563
|
+
"--label", "(expected)", expected_file.path])
|
1564
|
+
system(@tester.diff, *diff_options)
|
1565
|
+
end
|
1566
|
+
end
|
1567
|
+
end
|
1568
|
+
|
1569
|
+
def report_test(worker, result)
|
1570
|
+
report_marker(result)
|
1571
|
+
print("[#{worker.id}] ") if @tester.n_workers > 1
|
1572
|
+
puts(worker.suite_name)
|
1573
|
+
print(" #{worker.test_name}")
|
1574
|
+
report_test_result(result, worker.status)
|
1575
|
+
end
|
1576
|
+
|
1577
|
+
def report_test_result(result, label)
|
1578
|
+
message = test_result_message(result, label)
|
1579
|
+
message_width = string_width(message)
|
1580
|
+
rest_width = @term_width - @current_column
|
1581
|
+
if rest_width > message_width
|
1582
|
+
print(" " * (rest_width - message_width))
|
1583
|
+
end
|
1584
|
+
puts(message)
|
1585
|
+
end
|
1586
|
+
|
1587
|
+
def test_result_message(result, label)
|
1588
|
+
elapsed_time = result.elapsed_time
|
1589
|
+
formatted_elapsed_time = "%.4fs" % elapsed_time
|
1590
|
+
formatted_elapsed_time = colorize(formatted_elapsed_time,
|
1591
|
+
elapsed_time_status(elapsed_time))
|
1592
|
+
" #{formatted_elapsed_time} [#{colorize(label, result)}]"
|
1593
|
+
end
|
1594
|
+
|
1595
|
+
LONG_ELAPSED_TIME = 1.0
|
1596
|
+
def long_elapsed_time?(elapsed_time)
|
1597
|
+
elapsed_time >= LONG_ELAPSED_TIME
|
1598
|
+
end
|
1599
|
+
|
1600
|
+
def elapsed_time_status(elapsed_time)
|
1601
|
+
if long_elapsed_time?(elapsed_time)
|
1602
|
+
elapsed_time_status = :failure
|
1603
|
+
else
|
1604
|
+
elapsed_time_status = :not_checked
|
1605
|
+
end
|
1606
|
+
end
|
1607
|
+
|
1608
|
+
def justify(message, width)
|
1609
|
+
return " " * width if message.nil?
|
1610
|
+
return message.ljust(width) if message.bytesize <= width
|
1611
|
+
half_width = width / 2.0
|
1612
|
+
elision_mark = "..."
|
1613
|
+
left = message[0, half_width.ceil - elision_mark.size]
|
1614
|
+
right = message[(message.size - half_width.floor)..-1]
|
1615
|
+
"#{left}#{elision_mark}#{right}"
|
1616
|
+
end
|
1617
|
+
|
1618
|
+
def print(message)
|
1619
|
+
@current_column += string_width(message.to_s)
|
1620
|
+
@output.print(message)
|
1621
|
+
end
|
1622
|
+
|
1623
|
+
def puts(*messages)
|
1624
|
+
reset_current_column
|
1625
|
+
@output.puts(*messages)
|
1626
|
+
end
|
1627
|
+
|
1628
|
+
def reset_current_column
|
1629
|
+
@current_column = 0
|
1630
|
+
end
|
1631
|
+
|
1632
|
+
def create_temporary_file(key, content)
|
1633
|
+
file = Tempfile.new("groonga-test-#{key}")
|
1634
|
+
file.print(content)
|
1635
|
+
file.close
|
1636
|
+
yield(file)
|
1637
|
+
end
|
1638
|
+
|
1639
|
+
def guess_term_width
|
1640
|
+
Integer(guess_term_width_from_env || guess_term_width_from_stty || 79)
|
1641
|
+
rescue ArgumentError
|
1642
|
+
0
|
1643
|
+
end
|
1644
|
+
|
1645
|
+
def guess_term_width_from_env
|
1646
|
+
ENV["COLUMNS"] || ENV["TERM_WIDTH"]
|
1647
|
+
end
|
1648
|
+
|
1649
|
+
def guess_term_width_from_stty
|
1650
|
+
case `stty -a`
|
1651
|
+
when /(\d+) columns/
|
1652
|
+
$1
|
1653
|
+
when /columns (\d+)/
|
1654
|
+
$1
|
1655
|
+
else
|
1656
|
+
nil
|
1657
|
+
end
|
1658
|
+
rescue SystemCallError
|
1659
|
+
nil
|
1660
|
+
end
|
1661
|
+
|
1662
|
+
def string_width(string)
|
1663
|
+
string.gsub(/\e\[[0-9;]+m/, "").size
|
1664
|
+
end
|
1665
|
+
|
1666
|
+
def result_status(result)
|
1667
|
+
if result.respond_to?(:status)
|
1668
|
+
result.status
|
1669
|
+
else
|
1670
|
+
if result.n_failed_tests > 0
|
1671
|
+
:failure
|
1672
|
+
elsif result.n_not_checked_tests > 0
|
1673
|
+
:not_checked
|
1674
|
+
else
|
1675
|
+
:success
|
1676
|
+
end
|
1677
|
+
end
|
1678
|
+
end
|
1679
|
+
|
1680
|
+
def colorize(message, result_or_status)
|
1681
|
+
return message unless @tester.use_color?
|
1682
|
+
if result_or_status.is_a?(Symbol)
|
1683
|
+
status = result_or_status
|
1684
|
+
else
|
1685
|
+
status = result_status(result_or_status)
|
1686
|
+
end
|
1687
|
+
case status
|
1688
|
+
when :success
|
1689
|
+
"%s%s%s" % [success_color, message, reset_color]
|
1690
|
+
when :failure
|
1691
|
+
"%s%s%s" % [failure_color, message, reset_color]
|
1692
|
+
when :not_checked
|
1693
|
+
"%s%s%s" % [not_checked_color, message, reset_color]
|
1694
|
+
else
|
1695
|
+
message
|
1696
|
+
end
|
1697
|
+
end
|
1698
|
+
|
1699
|
+
def success_color
|
1700
|
+
escape_sequence({
|
1701
|
+
:color => :green,
|
1702
|
+
:color_256 => [0, 3, 0],
|
1703
|
+
:background => true,
|
1704
|
+
},
|
1705
|
+
{
|
1706
|
+
:color => :white,
|
1707
|
+
:color_256 => [5, 5, 5],
|
1708
|
+
:bold => true,
|
1709
|
+
})
|
1710
|
+
end
|
1711
|
+
|
1712
|
+
def failure_color
|
1713
|
+
escape_sequence({
|
1714
|
+
:color => :red,
|
1715
|
+
:color_256 => [3, 0, 0],
|
1716
|
+
:background => true,
|
1717
|
+
},
|
1718
|
+
{
|
1719
|
+
:color => :white,
|
1720
|
+
:color_256 => [5, 5, 5],
|
1721
|
+
:bold => true,
|
1722
|
+
})
|
1723
|
+
end
|
1724
|
+
|
1725
|
+
def not_checked_color
|
1726
|
+
escape_sequence({
|
1727
|
+
:color => :cyan,
|
1728
|
+
:color_256 => [0, 1, 1],
|
1729
|
+
:background => true,
|
1730
|
+
},
|
1731
|
+
{
|
1732
|
+
:color => :white,
|
1733
|
+
:color_256 => [5, 5, 5],
|
1734
|
+
:bold => true,
|
1735
|
+
})
|
1736
|
+
end
|
1737
|
+
|
1738
|
+
def reset_color
|
1739
|
+
escape_sequence(:reset)
|
1740
|
+
end
|
1741
|
+
|
1742
|
+
COLOR_NAMES = [
|
1743
|
+
:black, :red, :green, :yellow,
|
1744
|
+
:blue, :magenta, :cyan, :white,
|
1745
|
+
]
|
1746
|
+
def escape_sequence(*commands)
|
1747
|
+
sequence = []
|
1748
|
+
commands.each do |command|
|
1749
|
+
case command
|
1750
|
+
when :reset
|
1751
|
+
sequence << "0"
|
1752
|
+
when :bold
|
1753
|
+
sequence << "1"
|
1754
|
+
when :italic
|
1755
|
+
sequence << "3"
|
1756
|
+
when :underline
|
1757
|
+
sequence << "4"
|
1758
|
+
when Hash
|
1759
|
+
foreground_p = !command[:background]
|
1760
|
+
if available_colors == 256
|
1761
|
+
sequence << (foreground_p ? "38" : "48")
|
1762
|
+
sequence << "5"
|
1763
|
+
sequence << pack_256_color(*command[:color_256])
|
1764
|
+
else
|
1765
|
+
color_parameter = foreground_p ? 3 : 4
|
1766
|
+
color_parameter += 6 if command[:intensity]
|
1767
|
+
color = COLOR_NAMES.index(command[:color])
|
1768
|
+
sequence << "#{color_parameter}#{color}"
|
1769
|
+
end
|
1770
|
+
end
|
1771
|
+
end
|
1772
|
+
"\e[#{sequence.join(';')}m"
|
1773
|
+
end
|
1774
|
+
|
1775
|
+
def pack_256_color(red, green, blue)
|
1776
|
+
red * 36 + green * 6 + blue + 16
|
1777
|
+
end
|
1778
|
+
|
1779
|
+
def available_colors
|
1780
|
+
case ENV["COLORTERM"]
|
1781
|
+
when "gnome-terminal"
|
1782
|
+
256
|
1783
|
+
else
|
1784
|
+
case ENV["TERM"]
|
1785
|
+
when /-256color\z/
|
1786
|
+
256
|
1787
|
+
else
|
1788
|
+
8
|
1789
|
+
end
|
1790
|
+
end
|
1791
|
+
end
|
1792
|
+
end
|
1793
|
+
|
1794
|
+
class MarkReporter < BaseReporter
|
1795
|
+
def initialize(tester)
|
1796
|
+
super
|
1797
|
+
end
|
1798
|
+
|
1799
|
+
def start(result)
|
1800
|
+
end
|
1801
|
+
|
1802
|
+
def start_worker(worker)
|
1803
|
+
end
|
1804
|
+
|
1805
|
+
def start_suite(worker)
|
1806
|
+
end
|
1807
|
+
|
1808
|
+
def start_test(worker)
|
1809
|
+
end
|
1810
|
+
|
1811
|
+
def pass_test(worker, result)
|
1812
|
+
synchronize do
|
1813
|
+
report_test_result_mark(".", result)
|
1814
|
+
end
|
1815
|
+
end
|
1816
|
+
|
1817
|
+
def fail_test(worker, result)
|
1818
|
+
synchronize do
|
1819
|
+
report_test_result_mark("F", result)
|
1820
|
+
puts
|
1821
|
+
report_test(worker, result)
|
1822
|
+
report_failure(result)
|
1823
|
+
end
|
1824
|
+
end
|
1825
|
+
|
1826
|
+
def not_check_test(worker, result)
|
1827
|
+
synchronize do
|
1828
|
+
report_test_result_mark("N", result)
|
1829
|
+
puts
|
1830
|
+
report_test(worker, result)
|
1831
|
+
report_actual(result)
|
1832
|
+
end
|
1833
|
+
end
|
1834
|
+
|
1835
|
+
def finish_test(worker, result)
|
1836
|
+
end
|
1837
|
+
|
1838
|
+
def finish_suite(worker)
|
1839
|
+
end
|
1840
|
+
|
1841
|
+
def finish_worker(worker_id)
|
1842
|
+
end
|
1843
|
+
|
1844
|
+
def finish(result)
|
1845
|
+
puts
|
1846
|
+
puts
|
1847
|
+
report_summary(result)
|
1848
|
+
end
|
1849
|
+
|
1850
|
+
private
|
1851
|
+
def report_test_result_mark(mark, result)
|
1852
|
+
print(colorize(mark, result))
|
1853
|
+
if @term_width <= @current_column
|
1854
|
+
puts
|
1855
|
+
else
|
1856
|
+
@output.flush
|
1857
|
+
end
|
1858
|
+
end
|
1859
|
+
end
|
1860
|
+
|
1861
|
+
class StreamReporter < BaseReporter
|
1862
|
+
def initialize(tester)
|
1863
|
+
super
|
1864
|
+
end
|
1865
|
+
|
1866
|
+
def start(result)
|
1867
|
+
end
|
1868
|
+
|
1869
|
+
def start_worker(worker)
|
1870
|
+
end
|
1871
|
+
|
1872
|
+
def start_suite(worker)
|
1873
|
+
if worker.suite_name.bytesize <= @term_width
|
1874
|
+
puts(worker.suite_name)
|
1875
|
+
else
|
1876
|
+
puts(justify(worker.suite_name, @term_width))
|
1877
|
+
end
|
1878
|
+
@output.flush
|
1879
|
+
end
|
1880
|
+
|
1881
|
+
def start_test(worker)
|
1882
|
+
print(" #{worker.test_name}")
|
1883
|
+
@output.flush
|
1884
|
+
end
|
1885
|
+
|
1886
|
+
def pass_test(worker, result)
|
1887
|
+
report_test_result(result, worker.status)
|
1888
|
+
end
|
1889
|
+
|
1890
|
+
def fail_test(worker, result)
|
1891
|
+
report_test_result(result, worker.status)
|
1892
|
+
report_failure(result)
|
1893
|
+
end
|
1894
|
+
|
1895
|
+
def not_check_test(worker, result)
|
1896
|
+
report_test_result(result, worker.status)
|
1897
|
+
report_actual(result)
|
1898
|
+
end
|
1899
|
+
|
1900
|
+
def finish_test(worker, result)
|
1901
|
+
end
|
1902
|
+
|
1903
|
+
def finish_suite(worker)
|
1904
|
+
end
|
1905
|
+
|
1906
|
+
def finish_worker(worker_id)
|
1907
|
+
end
|
1908
|
+
|
1909
|
+
def finish(result)
|
1910
|
+
puts
|
1911
|
+
report_summary(result)
|
1912
|
+
end
|
1913
|
+
end
|
1914
|
+
|
1915
|
+
class InplaceReporter < BaseReporter
|
1916
|
+
def initialize(tester)
|
1917
|
+
super
|
1918
|
+
@last_redraw_time = Time.now
|
1919
|
+
@minimum_redraw_interval = 0.1
|
1920
|
+
end
|
1921
|
+
|
1922
|
+
def start(result)
|
1923
|
+
@test_suites_result = result
|
1924
|
+
end
|
1925
|
+
|
1926
|
+
def start_worker(worker)
|
1927
|
+
end
|
1928
|
+
|
1929
|
+
def start_suite(worker)
|
1930
|
+
redraw
|
1931
|
+
end
|
1932
|
+
|
1933
|
+
def start_test(worker)
|
1934
|
+
redraw
|
1935
|
+
end
|
1936
|
+
|
1937
|
+
def pass_test(worker, result)
|
1938
|
+
redraw
|
1939
|
+
end
|
1940
|
+
|
1941
|
+
def fail_test(worker, result)
|
1942
|
+
redraw do
|
1943
|
+
report_test(worker, result)
|
1944
|
+
report_failure(result)
|
1945
|
+
end
|
1946
|
+
end
|
1947
|
+
|
1948
|
+
def not_check_test(worker, result)
|
1949
|
+
redraw do
|
1950
|
+
report_test(worker, result)
|
1951
|
+
report_actual(result)
|
1952
|
+
end
|
1953
|
+
end
|
1954
|
+
|
1955
|
+
def finish_test(worker, result)
|
1956
|
+
redraw
|
1957
|
+
end
|
1958
|
+
|
1959
|
+
def finish_suite(worker)
|
1960
|
+
redraw
|
1961
|
+
end
|
1962
|
+
|
1963
|
+
def finish_worker(worker)
|
1964
|
+
redraw
|
1965
|
+
end
|
1966
|
+
|
1967
|
+
def finish(result)
|
1968
|
+
draw
|
1969
|
+
puts
|
1970
|
+
report_summary(result)
|
1971
|
+
end
|
1972
|
+
|
1973
|
+
private
|
1974
|
+
def draw
|
1975
|
+
@test_suites_result.workers.each do |worker|
|
1976
|
+
draw_status_line(worker)
|
1977
|
+
draw_test_line(worker)
|
1978
|
+
end
|
1979
|
+
draw_progress_line
|
1980
|
+
end
|
1981
|
+
|
1982
|
+
def draw_status_line(worker)
|
1983
|
+
clear_line
|
1984
|
+
left = "[#{colorize(worker.id, worker.result)}] "
|
1985
|
+
right = " [#{worker.status}]"
|
1986
|
+
rest_width = @term_width - @current_column
|
1987
|
+
center_width = rest_width - string_width(left) - string_width(right)
|
1988
|
+
center = justify(worker.suite_name, center_width)
|
1989
|
+
puts("#{left}#{center}#{right}")
|
1990
|
+
end
|
1991
|
+
|
1992
|
+
def draw_test_line(worker)
|
1993
|
+
clear_line
|
1994
|
+
if worker.test_name
|
1995
|
+
label = " #{worker.test_name}"
|
1996
|
+
else
|
1997
|
+
label = " #{statistics(worker.result)}"
|
1998
|
+
end
|
1999
|
+
puts(justify(label, @term_width))
|
2000
|
+
end
|
2001
|
+
|
2002
|
+
def draw_progress_line
|
2003
|
+
n_done_tests = @test_suites_result.n_tests
|
2004
|
+
n_total_tests = @test_suites_result.n_total_tests
|
2005
|
+
finished_test_ratio = n_done_tests.to_f / n_total_tests
|
2006
|
+
|
2007
|
+
start_mark = "|"
|
2008
|
+
finish_mark = "|"
|
2009
|
+
statistics = " [%3d%%]" % (finished_test_ratio * 100)
|
2010
|
+
|
2011
|
+
progress_width = @term_width
|
2012
|
+
progress_width -= start_mark.bytesize
|
2013
|
+
progress_width -= finish_mark.bytesize
|
2014
|
+
progress_width -= statistics.bytesize
|
2015
|
+
finished_mark = "-"
|
2016
|
+
if n_done_tests == n_total_tests
|
2017
|
+
progress = colorize(finished_mark * progress_width,
|
2018
|
+
@test_suites_result)
|
2019
|
+
else
|
2020
|
+
current_mark = ">"
|
2021
|
+
finished_marks_width = (progress_width * finished_test_ratio).ceil
|
2022
|
+
finished_marks_width -= current_mark.bytesize
|
2023
|
+
finished_marks_width = [0, finished_marks_width].max
|
2024
|
+
progress = finished_mark * finished_marks_width + current_mark
|
2025
|
+
progress = colorize(progress, @test_suites_result)
|
2026
|
+
progress << " " * (progress_width - string_width(progress))
|
2027
|
+
end
|
2028
|
+
puts("#{start_mark}#{progress}#{finish_mark}#{statistics}")
|
2029
|
+
end
|
2030
|
+
|
2031
|
+
def redraw
|
2032
|
+
synchronize do
|
2033
|
+
unless block_given?
|
2034
|
+
return if Time.now - @last_redraw_time < @minimum_redraw_interval
|
2035
|
+
end
|
2036
|
+
draw
|
2037
|
+
if block_given?
|
2038
|
+
yield
|
2039
|
+
else
|
2040
|
+
up_n_lines(n_using_lines)
|
2041
|
+
end
|
2042
|
+
@last_redraw_time = Time.now
|
2043
|
+
end
|
2044
|
+
end
|
2045
|
+
|
2046
|
+
def up_n_lines(n)
|
2047
|
+
print("\e[1A" * n)
|
2048
|
+
end
|
2049
|
+
|
2050
|
+
def clear_line
|
2051
|
+
print(" " * @term_width)
|
2052
|
+
print("\r")
|
2053
|
+
reset_current_column
|
2054
|
+
end
|
2055
|
+
|
2056
|
+
def n_using_lines
|
2057
|
+
n_worker_lines * n_workers + n_progress_lines
|
2058
|
+
end
|
2059
|
+
|
2060
|
+
def n_worker_lines
|
2061
|
+
2
|
2062
|
+
end
|
2063
|
+
|
2064
|
+
def n_progress_lines
|
2065
|
+
1
|
2066
|
+
end
|
2067
|
+
|
2068
|
+
def n_workers
|
2069
|
+
@tester.n_workers
|
2070
|
+
end
|
2071
|
+
end
|
2072
|
+
end
|
2073
|
+
end
|