grntest 1.0.0

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