grntest 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/doc/text/news.md +11 -1
- data/lib/grntest/base-result.rb +32 -0
- data/lib/grntest/error.rb +39 -0
- data/lib/grntest/execution-context.rb +89 -0
- data/lib/grntest/executors.rb +19 -0
- data/lib/grntest/executors/base-executor.rb +332 -0
- data/lib/grntest/executors/http-executor.rb +60 -0
- data/lib/grntest/executors/standard-io-executor.rb +71 -0
- data/lib/grntest/reporters.rb +37 -0
- data/lib/grntest/reporters/base-reporter.rb +375 -0
- data/lib/grntest/reporters/inplace-reporter.rb +208 -0
- data/lib/grntest/reporters/mark-reporter.rb +112 -0
- data/lib/grntest/reporters/stream-reporter.rb +86 -0
- data/lib/grntest/response-parser.rb +64 -0
- data/lib/grntest/test-runner.rb +511 -0
- data/lib/grntest/test-suites-runner.rb +141 -0
- data/lib/grntest/tester.rb +2 -1975
- data/lib/grntest/version.rb +1 -1
- data/lib/grntest/worker.rb +164 -0
- data/test/executors/test-base-executor.rb +42 -0
- data/test/executors/test-standard-io-executor.rb +61 -0
- metadata +22 -10
- data/test/test-executor.rb +0 -207
@@ -0,0 +1,141 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
|
4
|
+
#
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
require "thread"
|
19
|
+
|
20
|
+
require "grntest/reporters"
|
21
|
+
require "grntest/worker"
|
22
|
+
require "grntest/base-result"
|
23
|
+
|
24
|
+
module Grntest
|
25
|
+
class TestSuitesResult < BaseResult
|
26
|
+
attr_accessor :workers
|
27
|
+
attr_accessor :n_total_tests
|
28
|
+
def initialize
|
29
|
+
super
|
30
|
+
@workers = []
|
31
|
+
@n_total_tests = 0
|
32
|
+
end
|
33
|
+
|
34
|
+
def pass_ratio
|
35
|
+
n_target_tests = n_tests - n_not_checked_tests
|
36
|
+
if n_target_tests.zero?
|
37
|
+
0
|
38
|
+
else
|
39
|
+
(n_passed_tests / n_target_tests.to_f) * 100
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def n_tests
|
44
|
+
collect_count(:n_tests)
|
45
|
+
end
|
46
|
+
|
47
|
+
def n_passed_tests
|
48
|
+
collect_count(:n_passed_tests)
|
49
|
+
end
|
50
|
+
|
51
|
+
def n_failed_tests
|
52
|
+
collect_count(:n_failed_tests)
|
53
|
+
end
|
54
|
+
|
55
|
+
def n_leaked_tests
|
56
|
+
collect_count(:n_leaked_tests)
|
57
|
+
end
|
58
|
+
|
59
|
+
def n_omitted_tests
|
60
|
+
collect_count(:n_omitted_tests)
|
61
|
+
end
|
62
|
+
|
63
|
+
def n_not_checked_tests
|
64
|
+
collect_count(:n_not_checked_tests)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def collect_count(item)
|
69
|
+
counts = @workers.collect do |worker|
|
70
|
+
worker.result.send(item)
|
71
|
+
end
|
72
|
+
counts.inject(&:+)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class TestSuitesRunner
|
77
|
+
def initialize(tester)
|
78
|
+
@tester = tester
|
79
|
+
@reporter = create_reporter
|
80
|
+
@result = TestSuitesResult.new
|
81
|
+
end
|
82
|
+
|
83
|
+
def run(test_suites)
|
84
|
+
succeeded = true
|
85
|
+
|
86
|
+
@result.measure do
|
87
|
+
succeeded = run_test_suites(test_suites)
|
88
|
+
end
|
89
|
+
@reporter.on_finish(@result)
|
90
|
+
|
91
|
+
succeeded
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
def run_test_suites(test_suites)
|
96
|
+
queue = Queue.new
|
97
|
+
test_suites.each do |suite_name, test_script_paths|
|
98
|
+
next unless @tester.target_test_suite?(suite_name)
|
99
|
+
test_script_paths.each do |test_script_path|
|
100
|
+
test_name = test_script_path.basename(".*").to_s
|
101
|
+
next unless @tester.target_test?(test_name)
|
102
|
+
queue << [suite_name, test_script_path, test_name]
|
103
|
+
@result.n_total_tests += 1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
@tester.n_workers.times do
|
107
|
+
queue << nil
|
108
|
+
end
|
109
|
+
|
110
|
+
workers = []
|
111
|
+
@tester.n_workers.times do |i|
|
112
|
+
workers << Worker.new(i, @tester, @result, @reporter)
|
113
|
+
end
|
114
|
+
@result.workers = workers
|
115
|
+
@reporter.on_start(@result)
|
116
|
+
|
117
|
+
succeeded = true
|
118
|
+
worker_threads = []
|
119
|
+
@tester.n_workers.times do |i|
|
120
|
+
worker = workers[i]
|
121
|
+
worker_threads << Thread.new do
|
122
|
+
succeeded = false unless worker.run(queue)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
begin
|
127
|
+
worker_threads.each(&:join)
|
128
|
+
rescue Interrupt
|
129
|
+
workers.each do |worker|
|
130
|
+
worker.interrupt
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
succeeded
|
135
|
+
end
|
136
|
+
|
137
|
+
def create_reporter
|
138
|
+
Reporters.create_repoter(@tester)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/grntest/tester.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
#
|
3
|
-
# Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
|
3
|
+
# Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
|
4
4
|
#
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
6
6
|
# it under the terms of the GNU General Public License as published by
|
@@ -15,44 +15,14 @@
|
|
15
15
|
# You should have received a copy of the GNU General Public License
|
16
16
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
17
|
|
18
|
-
require "English"
|
19
18
|
require "optparse"
|
20
19
|
require "pathname"
|
21
|
-
require "fileutils"
|
22
|
-
require "tempfile"
|
23
|
-
require "shellwords"
|
24
|
-
require "open-uri"
|
25
|
-
|
26
|
-
require "json"
|
27
|
-
require "msgpack"
|
28
|
-
|
29
|
-
require "groonga/command"
|
30
20
|
|
31
21
|
require "grntest/version"
|
22
|
+
require "grntest/test-suites-runner"
|
32
23
|
|
33
24
|
module Grntest
|
34
25
|
class Tester
|
35
|
-
class Error < StandardError
|
36
|
-
end
|
37
|
-
|
38
|
-
class NotExist < Error
|
39
|
-
attr_reader :path
|
40
|
-
def initialize(path)
|
41
|
-
@path = path
|
42
|
-
super("<#{path}> doesn't exist.")
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
class ParseError < Error
|
47
|
-
attr_reader :type, :content, :reason
|
48
|
-
def initialize(type, content, reason)
|
49
|
-
@type = type
|
50
|
-
@content = content
|
51
|
-
@reason = reason
|
52
|
-
super("failed to parse <#{@type}> content: #{reason}: <#{content}>")
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
26
|
class << self
|
57
27
|
def run(argv=nil)
|
58
28
|
argv ||= ARGV.dup
|
@@ -391,1948 +361,5 @@ module Grntest
|
|
391
361
|
false
|
392
362
|
end
|
393
363
|
end
|
394
|
-
|
395
|
-
class Result
|
396
|
-
attr_accessor :elapsed_time
|
397
|
-
def initialize
|
398
|
-
@elapsed_time = 0
|
399
|
-
end
|
400
|
-
|
401
|
-
def measure
|
402
|
-
start_time = Time.now
|
403
|
-
yield
|
404
|
-
ensure
|
405
|
-
@elapsed_time = Time.now - start_time
|
406
|
-
end
|
407
|
-
end
|
408
|
-
|
409
|
-
class WorkerResult < Result
|
410
|
-
attr_reader :n_tests, :n_passed_tests, :n_leaked_tests
|
411
|
-
attr_reader :n_omitted_tests, :n_not_checked_tests
|
412
|
-
attr_reader :failed_tests
|
413
|
-
def initialize
|
414
|
-
super
|
415
|
-
@n_tests = 0
|
416
|
-
@n_passed_tests = 0
|
417
|
-
@n_leaked_tests = 0
|
418
|
-
@n_omitted_tests = 0
|
419
|
-
@n_not_checked_tests = 0
|
420
|
-
@failed_tests = []
|
421
|
-
end
|
422
|
-
|
423
|
-
def n_failed_tests
|
424
|
-
@failed_tests.size
|
425
|
-
end
|
426
|
-
|
427
|
-
def on_test_finish
|
428
|
-
@n_tests += 1
|
429
|
-
end
|
430
|
-
|
431
|
-
def on_test_success
|
432
|
-
@n_passed_tests += 1
|
433
|
-
end
|
434
|
-
|
435
|
-
def on_test_failure(name)
|
436
|
-
@failed_tests << name
|
437
|
-
end
|
438
|
-
|
439
|
-
def on_test_leak(name)
|
440
|
-
@n_leaked_tests += 1
|
441
|
-
end
|
442
|
-
|
443
|
-
def on_test_omission
|
444
|
-
@n_omitted_tests += 1
|
445
|
-
end
|
446
|
-
|
447
|
-
def on_test_no_check
|
448
|
-
@n_not_checked_tests += 1
|
449
|
-
end
|
450
|
-
end
|
451
|
-
|
452
|
-
class Worker
|
453
|
-
attr_reader :id, :tester, :test_suites_rusult, :reporter
|
454
|
-
attr_reader :suite_name, :test_script_path, :test_name, :status, :result
|
455
|
-
def initialize(id, tester, test_suites_result, reporter)
|
456
|
-
@id = id
|
457
|
-
@tester = tester
|
458
|
-
@test_suites_result = test_suites_result
|
459
|
-
@reporter = reporter
|
460
|
-
@suite_name = nil
|
461
|
-
@test_script_path = nil
|
462
|
-
@test_name = nil
|
463
|
-
@interruptted = false
|
464
|
-
@status = "not running"
|
465
|
-
@result = WorkerResult.new
|
466
|
-
end
|
467
|
-
|
468
|
-
def interrupt
|
469
|
-
@interruptted = true
|
470
|
-
end
|
471
|
-
|
472
|
-
def interruptted?
|
473
|
-
@interruptted
|
474
|
-
end
|
475
|
-
|
476
|
-
def run(queue)
|
477
|
-
succeeded = true
|
478
|
-
|
479
|
-
@result.measure do
|
480
|
-
@reporter.on_worker_start(self)
|
481
|
-
catch do |tag|
|
482
|
-
loop do
|
483
|
-
suite_name, test_script_path, test_name = queue.pop
|
484
|
-
break if test_script_path.nil?
|
485
|
-
|
486
|
-
unless @suite_name == suite_name
|
487
|
-
@reporter.on_suite_finish(self) if @suite_name
|
488
|
-
@suite_name = suite_name
|
489
|
-
@reporter.on_suite_start(self)
|
490
|
-
end
|
491
|
-
@test_script_path = test_script_path
|
492
|
-
@test_name = test_name
|
493
|
-
runner = TestRunner.new(@tester, self)
|
494
|
-
succeeded = false unless runner.run
|
495
|
-
|
496
|
-
break if interruptted?
|
497
|
-
end
|
498
|
-
@status = "finished"
|
499
|
-
@reporter.on_suite_finish(@suite_name) if @suite_name
|
500
|
-
@suite_name = nil
|
501
|
-
end
|
502
|
-
end
|
503
|
-
@reporter.on_worker_finish(self)
|
504
|
-
|
505
|
-
succeeded
|
506
|
-
end
|
507
|
-
|
508
|
-
def on_test_start
|
509
|
-
@status = "running"
|
510
|
-
@test_result = nil
|
511
|
-
@reporter.on_test_start(self)
|
512
|
-
end
|
513
|
-
|
514
|
-
def on_test_success(result)
|
515
|
-
@status = "passed"
|
516
|
-
@result.on_test_success
|
517
|
-
@reporter.on_test_success(self, result)
|
518
|
-
end
|
519
|
-
|
520
|
-
def on_test_failure(result)
|
521
|
-
@status = "failed"
|
522
|
-
@result.on_test_failure(test_name)
|
523
|
-
@reporter.on_test_failure(self, result)
|
524
|
-
end
|
525
|
-
|
526
|
-
def on_test_leak(result)
|
527
|
-
@status = "leaked(#{result.n_leaked_objects})"
|
528
|
-
@result.on_test_leak(test_name)
|
529
|
-
@reporter.on_test_leak(self, result)
|
530
|
-
end
|
531
|
-
|
532
|
-
def on_test_omission(result)
|
533
|
-
@status = "omitted"
|
534
|
-
@result.on_test_omission
|
535
|
-
@reporter.on_test_omission(self, result)
|
536
|
-
end
|
537
|
-
|
538
|
-
def on_test_no_check(result)
|
539
|
-
@status = "not checked"
|
540
|
-
@result.on_test_no_check
|
541
|
-
@reporter.on_test_no_check(self, result)
|
542
|
-
end
|
543
|
-
|
544
|
-
def on_test_finish(result)
|
545
|
-
@result.on_test_finish
|
546
|
-
@reporter.on_test_finish(self, result)
|
547
|
-
@test_script_path = nil
|
548
|
-
@test_name = nil
|
549
|
-
end
|
550
|
-
end
|
551
|
-
|
552
|
-
class TestSuitesResult < Result
|
553
|
-
attr_accessor :workers
|
554
|
-
attr_accessor :n_total_tests
|
555
|
-
def initialize
|
556
|
-
super
|
557
|
-
@workers = []
|
558
|
-
@n_total_tests = 0
|
559
|
-
end
|
560
|
-
|
561
|
-
def pass_ratio
|
562
|
-
n_target_tests = n_tests - n_not_checked_tests
|
563
|
-
if n_target_tests.zero?
|
564
|
-
0
|
565
|
-
else
|
566
|
-
(n_passed_tests / n_target_tests.to_f) * 100
|
567
|
-
end
|
568
|
-
end
|
569
|
-
|
570
|
-
def n_tests
|
571
|
-
collect_count(:n_tests)
|
572
|
-
end
|
573
|
-
|
574
|
-
def n_passed_tests
|
575
|
-
collect_count(:n_passed_tests)
|
576
|
-
end
|
577
|
-
|
578
|
-
def n_failed_tests
|
579
|
-
collect_count(:n_failed_tests)
|
580
|
-
end
|
581
|
-
|
582
|
-
def n_leaked_tests
|
583
|
-
collect_count(:n_leaked_tests)
|
584
|
-
end
|
585
|
-
|
586
|
-
def n_omitted_tests
|
587
|
-
collect_count(:n_omitted_tests)
|
588
|
-
end
|
589
|
-
|
590
|
-
def n_not_checked_tests
|
591
|
-
collect_count(:n_not_checked_tests)
|
592
|
-
end
|
593
|
-
|
594
|
-
private
|
595
|
-
def collect_count(item)
|
596
|
-
counts = @workers.collect do |worker|
|
597
|
-
worker.result.send(item)
|
598
|
-
end
|
599
|
-
counts.inject(&:+)
|
600
|
-
end
|
601
|
-
end
|
602
|
-
|
603
|
-
class TestSuitesRunner
|
604
|
-
def initialize(tester)
|
605
|
-
@tester = tester
|
606
|
-
@reporter = create_reporter
|
607
|
-
@result = TestSuitesResult.new
|
608
|
-
end
|
609
|
-
|
610
|
-
def run(test_suites)
|
611
|
-
succeeded = true
|
612
|
-
|
613
|
-
@result.measure do
|
614
|
-
succeeded = run_test_suites(test_suites)
|
615
|
-
end
|
616
|
-
@reporter.on_finish(@result)
|
617
|
-
|
618
|
-
succeeded
|
619
|
-
end
|
620
|
-
|
621
|
-
private
|
622
|
-
def run_test_suites(test_suites)
|
623
|
-
queue = Queue.new
|
624
|
-
test_suites.each do |suite_name, test_script_paths|
|
625
|
-
next unless @tester.target_test_suite?(suite_name)
|
626
|
-
test_script_paths.each do |test_script_path|
|
627
|
-
test_name = test_script_path.basename(".*").to_s
|
628
|
-
next unless @tester.target_test?(test_name)
|
629
|
-
queue << [suite_name, test_script_path, test_name]
|
630
|
-
@result.n_total_tests += 1
|
631
|
-
end
|
632
|
-
end
|
633
|
-
@tester.n_workers.times do
|
634
|
-
queue << nil
|
635
|
-
end
|
636
|
-
|
637
|
-
workers = []
|
638
|
-
@tester.n_workers.times do |i|
|
639
|
-
workers << Worker.new(i, @tester, @result, @reporter)
|
640
|
-
end
|
641
|
-
@result.workers = workers
|
642
|
-
@reporter.on_start(@result)
|
643
|
-
|
644
|
-
succeeded = true
|
645
|
-
worker_threads = []
|
646
|
-
@tester.n_workers.times do |i|
|
647
|
-
worker = workers[i]
|
648
|
-
worker_threads << Thread.new do
|
649
|
-
succeeded = false unless worker.run(queue)
|
650
|
-
end
|
651
|
-
end
|
652
|
-
|
653
|
-
begin
|
654
|
-
worker_threads.each(&:join)
|
655
|
-
rescue Interrupt
|
656
|
-
workers.each do |worker|
|
657
|
-
worker.interrupt
|
658
|
-
end
|
659
|
-
end
|
660
|
-
|
661
|
-
succeeded
|
662
|
-
end
|
663
|
-
|
664
|
-
def create_reporter
|
665
|
-
case @tester.reporter
|
666
|
-
when :mark
|
667
|
-
MarkReporter.new(@tester)
|
668
|
-
when :stream
|
669
|
-
StreamReporter.new(@tester)
|
670
|
-
when :inplace
|
671
|
-
InplaceReporter.new(@tester)
|
672
|
-
end
|
673
|
-
end
|
674
|
-
end
|
675
|
-
|
676
|
-
class TestResult < Result
|
677
|
-
attr_accessor :worker_id, :test_name
|
678
|
-
attr_accessor :expected, :actual, :n_leaked_objects
|
679
|
-
attr_writer :omitted
|
680
|
-
def initialize(worker)
|
681
|
-
super()
|
682
|
-
@worker_id = worker.id
|
683
|
-
@test_name = worker.test_name
|
684
|
-
@actual = nil
|
685
|
-
@expected = nil
|
686
|
-
@n_leaked_objects = 0
|
687
|
-
@omitted = false
|
688
|
-
end
|
689
|
-
|
690
|
-
def status
|
691
|
-
return :omitted if omitted?
|
692
|
-
|
693
|
-
if @expected
|
694
|
-
if @actual == @expected
|
695
|
-
if @n_leaked_objects.zero?
|
696
|
-
:success
|
697
|
-
else
|
698
|
-
:leaked
|
699
|
-
end
|
700
|
-
else
|
701
|
-
:failure
|
702
|
-
end
|
703
|
-
else
|
704
|
-
if @n_leaked_objects.zero?
|
705
|
-
:not_checked
|
706
|
-
else
|
707
|
-
:leaked
|
708
|
-
end
|
709
|
-
end
|
710
|
-
end
|
711
|
-
|
712
|
-
def omitted?
|
713
|
-
@omitted
|
714
|
-
end
|
715
|
-
end
|
716
|
-
|
717
|
-
class ResponseParser
|
718
|
-
class << self
|
719
|
-
def parse(content, type)
|
720
|
-
parser = new(type)
|
721
|
-
parser.parse(content)
|
722
|
-
end
|
723
|
-
end
|
724
|
-
|
725
|
-
def initialize(type)
|
726
|
-
@type = type
|
727
|
-
end
|
728
|
-
|
729
|
-
def parse(content)
|
730
|
-
case @type
|
731
|
-
when "json", "msgpack"
|
732
|
-
parse_result(content.chomp)
|
733
|
-
else
|
734
|
-
content
|
735
|
-
end
|
736
|
-
end
|
737
|
-
|
738
|
-
def parse_result(result)
|
739
|
-
case @type
|
740
|
-
when "json"
|
741
|
-
begin
|
742
|
-
JSON.parse(result)
|
743
|
-
rescue JSON::ParserError
|
744
|
-
raise ParseError.new(@type, result, $!.message)
|
745
|
-
end
|
746
|
-
when "msgpack"
|
747
|
-
begin
|
748
|
-
MessagePack.unpack(result.chomp)
|
749
|
-
rescue MessagePack::UnpackError, NoMemoryError
|
750
|
-
raise ParseError.new(@type, result, $!.message)
|
751
|
-
end
|
752
|
-
else
|
753
|
-
raise ParseError.new(@type, result, "unknown type")
|
754
|
-
end
|
755
|
-
end
|
756
|
-
end
|
757
|
-
|
758
|
-
class TestRunner
|
759
|
-
MAX_N_COLUMNS = 79
|
760
|
-
|
761
|
-
def initialize(tester, worker)
|
762
|
-
@tester = tester
|
763
|
-
@worker = worker
|
764
|
-
@max_n_columns = MAX_N_COLUMNS
|
765
|
-
@id = nil
|
766
|
-
end
|
767
|
-
|
768
|
-
def run
|
769
|
-
succeeded = true
|
770
|
-
|
771
|
-
@worker.on_test_start
|
772
|
-
result = TestResult.new(@worker)
|
773
|
-
result.measure do
|
774
|
-
execute_groonga_script(result)
|
775
|
-
end
|
776
|
-
normalize_actual_result(result)
|
777
|
-
result.expected = read_expected_result
|
778
|
-
case result.status
|
779
|
-
when :success
|
780
|
-
@worker.on_test_success(result)
|
781
|
-
remove_reject_file
|
782
|
-
when :failure
|
783
|
-
@worker.on_test_failure(result)
|
784
|
-
output_reject_file(result.actual)
|
785
|
-
succeeded = false
|
786
|
-
when :leaked
|
787
|
-
@worker.on_test_leak(result)
|
788
|
-
succeeded = false
|
789
|
-
when :omitted
|
790
|
-
@worker.on_test_omission(result)
|
791
|
-
else
|
792
|
-
@worker.on_test_no_check(result)
|
793
|
-
output_actual_file(result.actual)
|
794
|
-
end
|
795
|
-
@worker.on_test_finish(result)
|
796
|
-
|
797
|
-
succeeded
|
798
|
-
end
|
799
|
-
|
800
|
-
private
|
801
|
-
def execute_groonga_script(result)
|
802
|
-
create_temporary_directory do |directory_path|
|
803
|
-
if @tester.database_path
|
804
|
-
db_path = Pathname(@tester.database_path).expand_path
|
805
|
-
else
|
806
|
-
db_dir = directory_path + "db"
|
807
|
-
FileUtils.mkdir_p(db_dir.to_s)
|
808
|
-
db_path = db_dir + "db"
|
809
|
-
end
|
810
|
-
context = Executor::Context.new
|
811
|
-
context.temporary_directory_path = directory_path
|
812
|
-
context.db_path = db_path
|
813
|
-
context.base_directory = @tester.base_directory.expand_path
|
814
|
-
context.groonga_suggest_create_dataset =
|
815
|
-
@tester.groonga_suggest_create_dataset
|
816
|
-
context.output_type = @tester.output_type
|
817
|
-
run_groonga(context) do |executor|
|
818
|
-
executor.execute(test_script_path)
|
819
|
-
end
|
820
|
-
check_memory_leak(context)
|
821
|
-
result.omitted = context.omitted?
|
822
|
-
result.actual = context.result
|
823
|
-
end
|
824
|
-
end
|
825
|
-
|
826
|
-
def create_temporary_directory
|
827
|
-
path = "tmp/grntest"
|
828
|
-
path << ".#{@worker.id}" if @tester.n_workers > 1
|
829
|
-
FileUtils.rm_rf(path, :secure => true)
|
830
|
-
FileUtils.mkdir_p(path)
|
831
|
-
begin
|
832
|
-
yield(Pathname(path).expand_path)
|
833
|
-
ensure
|
834
|
-
if @tester.keep_database? and File.exist?(path)
|
835
|
-
FileUtils.rm_rf(keep_database_path, :secure => true)
|
836
|
-
FileUtils.mv(path, keep_database_path)
|
837
|
-
else
|
838
|
-
FileUtils.rm_rf(path, :secure => true)
|
839
|
-
end
|
840
|
-
end
|
841
|
-
end
|
842
|
-
|
843
|
-
def keep_database_path
|
844
|
-
test_script_path.to_s.gsub(/\//, ".")
|
845
|
-
end
|
846
|
-
|
847
|
-
def run_groonga(context, &block)
|
848
|
-
unless @tester.database_path
|
849
|
-
create_empty_database(context.db_path.to_s)
|
850
|
-
end
|
851
|
-
|
852
|
-
catch do |tag|
|
853
|
-
context.abort_tag = tag
|
854
|
-
case @tester.interface
|
855
|
-
when :stdio
|
856
|
-
run_groonga_stdio(context, &block)
|
857
|
-
when :http
|
858
|
-
run_groonga_http(context, &block)
|
859
|
-
end
|
860
|
-
end
|
861
|
-
end
|
862
|
-
|
863
|
-
def run_groonga_stdio(context)
|
864
|
-
pid = nil
|
865
|
-
begin
|
866
|
-
open_pipe do |input_read, input_write, output_read, output_write|
|
867
|
-
groonga_input = input_write
|
868
|
-
groonga_output = output_read
|
869
|
-
|
870
|
-
input_fd = input_read.to_i
|
871
|
-
output_fd = output_write.to_i
|
872
|
-
env = {}
|
873
|
-
spawn_options = {
|
874
|
-
input_fd => input_fd,
|
875
|
-
output_fd => output_fd
|
876
|
-
}
|
877
|
-
command_line = groonga_command_line(context, spawn_options)
|
878
|
-
command_line += [
|
879
|
-
"--input-fd", input_fd.to_s,
|
880
|
-
"--output-fd", output_fd.to_s,
|
881
|
-
context.relative_db_path.to_s,
|
882
|
-
]
|
883
|
-
pid = Process.spawn(env, *command_line, spawn_options)
|
884
|
-
executor = StandardIOExecutor.new(groonga_input,
|
885
|
-
groonga_output,
|
886
|
-
context)
|
887
|
-
executor.ensure_groonga_ready
|
888
|
-
yield(executor)
|
889
|
-
end
|
890
|
-
ensure
|
891
|
-
Process.waitpid(pid) if pid
|
892
|
-
end
|
893
|
-
end
|
894
|
-
|
895
|
-
def open_pipe
|
896
|
-
IO.pipe("ASCII-8BIT") do |input_read, input_write|
|
897
|
-
IO.pipe("ASCII-8BIT") do |output_read, output_write|
|
898
|
-
yield(input_read, input_write, output_read, output_write)
|
899
|
-
end
|
900
|
-
end
|
901
|
-
end
|
902
|
-
|
903
|
-
def command_command_line(command, context, spawn_options)
|
904
|
-
command_line = []
|
905
|
-
if @tester.gdb
|
906
|
-
if libtool_wrapper?(command)
|
907
|
-
command_line << find_libtool(command)
|
908
|
-
command_line << "--mode=execute"
|
909
|
-
end
|
910
|
-
command_line << @tester.gdb
|
911
|
-
gdb_command_path = context.temporary_directory_path + "groonga.gdb"
|
912
|
-
File.open(gdb_command_path, "w") do |gdb_command|
|
913
|
-
gdb_command.puts(<<-EOC)
|
914
|
-
break main
|
915
|
-
run
|
916
|
-
print chdir("#{context.temporary_directory_path}")
|
917
|
-
EOC
|
918
|
-
end
|
919
|
-
command_line << "--command=#{gdb_command_path}"
|
920
|
-
command_line << "--quiet"
|
921
|
-
command_line << "--args"
|
922
|
-
else
|
923
|
-
spawn_options[:chdir] = context.temporary_directory_path.to_s
|
924
|
-
end
|
925
|
-
command_line << command
|
926
|
-
command_line
|
927
|
-
end
|
928
|
-
|
929
|
-
def groonga_command_line(context, spawn_options)
|
930
|
-
command_line = command_command_line(@tester.groonga, context,
|
931
|
-
spawn_options)
|
932
|
-
command_line << "--log-path=#{context.log_path}"
|
933
|
-
command_line << "--working-directory=#{context.temporary_directory_path}"
|
934
|
-
command_line
|
935
|
-
end
|
936
|
-
|
937
|
-
def libtool_wrapper?(command)
|
938
|
-
return false unless File.exist?(command)
|
939
|
-
File.open(command, "r") do |command_file|
|
940
|
-
first_line = command_file.gets
|
941
|
-
first_line.start_with?("#!")
|
942
|
-
end
|
943
|
-
end
|
944
|
-
|
945
|
-
def find_libtool(command)
|
946
|
-
command_path = Pathname.new(command)
|
947
|
-
directory = command_path.dirname
|
948
|
-
until directory.root?
|
949
|
-
libtool = directory + "libtool"
|
950
|
-
return libtool.to_s if libtool.executable?
|
951
|
-
directory = directory.parent
|
952
|
-
end
|
953
|
-
"libtool"
|
954
|
-
end
|
955
|
-
|
956
|
-
def run_groonga_http(context)
|
957
|
-
host = "127.0.0.1"
|
958
|
-
port = 50041 + @worker.id
|
959
|
-
pid_file_path = context.temporary_directory_path + "groonga.pid"
|
960
|
-
|
961
|
-
env = {}
|
962
|
-
spawn_options = {}
|
963
|
-
command_line = groonga_http_command(host, port, pid_file_path, context,
|
964
|
-
spawn_options)
|
965
|
-
pid = nil
|
966
|
-
begin
|
967
|
-
pid = Process.spawn(env, *command_line, spawn_options)
|
968
|
-
begin
|
969
|
-
executor = HTTPExecutor.new(host, port, context)
|
970
|
-
begin
|
971
|
-
executor.ensure_groonga_ready
|
972
|
-
rescue
|
973
|
-
if Process.waitpid(pid, Process::WNOHANG)
|
974
|
-
pid = nil
|
975
|
-
raise
|
976
|
-
end
|
977
|
-
raise unless @tester.gdb
|
978
|
-
retry
|
979
|
-
end
|
980
|
-
yield(executor)
|
981
|
-
ensure
|
982
|
-
executor.send_command("shutdown")
|
983
|
-
wait_groonga_http_shutdown(pid_file_path)
|
984
|
-
end
|
985
|
-
ensure
|
986
|
-
Process.waitpid(pid) if pid
|
987
|
-
end
|
988
|
-
end
|
989
|
-
|
990
|
-
def wait_groonga_http_shutdown(pid_file_path)
|
991
|
-
total_sleep_time = 0
|
992
|
-
sleep_time = 0.1
|
993
|
-
while pid_file_path.exist?
|
994
|
-
sleep(sleep_time)
|
995
|
-
total_sleep_time += sleep_time
|
996
|
-
break if total_sleep_time > 1.0
|
997
|
-
end
|
998
|
-
end
|
999
|
-
|
1000
|
-
def groonga_http_command(host, port, pid_file_path, context, spawn_options)
|
1001
|
-
case @tester.testee
|
1002
|
-
when "groonga"
|
1003
|
-
command_line = groonga_command_line(context, spawn_options)
|
1004
|
-
command_line += [
|
1005
|
-
"--pid-path", pid_file_path.to_s,
|
1006
|
-
"--bind-address", host,
|
1007
|
-
"--port", port.to_s,
|
1008
|
-
"--protocol", "http",
|
1009
|
-
"-s",
|
1010
|
-
context.relative_db_path.to_s,
|
1011
|
-
]
|
1012
|
-
when "groonga-httpd"
|
1013
|
-
command_line = command_command_line(@tester.groonga_httpd, context,
|
1014
|
-
spawn_options)
|
1015
|
-
config_file_path = create_config_file(context, host, port,
|
1016
|
-
pid_file_path)
|
1017
|
-
command_line += [
|
1018
|
-
"-c", config_file_path.to_s,
|
1019
|
-
"-p", "#{context.temporary_directory_path}/",
|
1020
|
-
]
|
1021
|
-
end
|
1022
|
-
command_line
|
1023
|
-
end
|
1024
|
-
|
1025
|
-
def create_config_file(context, host, port, pid_file_path)
|
1026
|
-
config_file_path =
|
1027
|
-
context.temporary_directory_path + "groonga-httpd.conf"
|
1028
|
-
config_file_path.open("w") do |config_file|
|
1029
|
-
config_file.puts(<<EOF)
|
1030
|
-
daemon off;
|
1031
|
-
master_process off;
|
1032
|
-
worker_processes 1;
|
1033
|
-
working_directory #{context.temporary_directory_path};
|
1034
|
-
error_log groonga-httpd-access.log;
|
1035
|
-
pid #{pid_file_path};
|
1036
|
-
events {
|
1037
|
-
worker_connections 1024;
|
1038
|
-
}
|
1039
|
-
|
1040
|
-
http {
|
1041
|
-
server {
|
1042
|
-
access_log groonga-httpd-access.log;
|
1043
|
-
listen #{port};
|
1044
|
-
server_name #{host};
|
1045
|
-
location /d/ {
|
1046
|
-
groonga_database #{context.relative_db_path};
|
1047
|
-
groonga on;
|
1048
|
-
}
|
1049
|
-
}
|
1050
|
-
}
|
1051
|
-
EOF
|
1052
|
-
end
|
1053
|
-
config_file_path
|
1054
|
-
end
|
1055
|
-
|
1056
|
-
def create_empty_database(db_path)
|
1057
|
-
output_fd = Tempfile.new("create-empty-database")
|
1058
|
-
create_database_command = [
|
1059
|
-
@tester.groonga,
|
1060
|
-
"--output-fd", output_fd.to_i.to_s,
|
1061
|
-
"-n", db_path,
|
1062
|
-
"shutdown"
|
1063
|
-
]
|
1064
|
-
system(*create_database_command)
|
1065
|
-
output_fd.close(true)
|
1066
|
-
end
|
1067
|
-
|
1068
|
-
def normalize_actual_result(result)
|
1069
|
-
normalized_result = ""
|
1070
|
-
result.actual.each do |tag, content, options|
|
1071
|
-
case tag
|
1072
|
-
when :input
|
1073
|
-
normalized_result << content
|
1074
|
-
when :output
|
1075
|
-
normalized_result << normalize_output(content, options)
|
1076
|
-
when :error
|
1077
|
-
normalized_result << normalize_raw_content(content)
|
1078
|
-
when :n_leaked_objects
|
1079
|
-
result.n_leaked_objects = content
|
1080
|
-
end
|
1081
|
-
end
|
1082
|
-
result.actual = normalized_result
|
1083
|
-
end
|
1084
|
-
|
1085
|
-
def normalize_raw_content(content)
|
1086
|
-
"#{content}\n".force_encoding("ASCII-8BIT")
|
1087
|
-
end
|
1088
|
-
|
1089
|
-
def normalize_output(content, options)
|
1090
|
-
type = options[:type]
|
1091
|
-
case type
|
1092
|
-
when "json", "msgpack"
|
1093
|
-
status = nil
|
1094
|
-
values = nil
|
1095
|
-
begin
|
1096
|
-
status, *values = ResponseParser.parse(content.chomp, type)
|
1097
|
-
rescue ParseError
|
1098
|
-
return $!.message
|
1099
|
-
end
|
1100
|
-
normalized_status = normalize_status(status)
|
1101
|
-
normalized_output_content = [normalized_status, *values]
|
1102
|
-
normalized_output = JSON.generate(normalized_output_content)
|
1103
|
-
if normalized_output.bytesize > @max_n_columns
|
1104
|
-
normalized_output = JSON.pretty_generate(normalized_output_content)
|
1105
|
-
end
|
1106
|
-
normalize_raw_content(normalized_output)
|
1107
|
-
else
|
1108
|
-
normalize_raw_content(content)
|
1109
|
-
end
|
1110
|
-
end
|
1111
|
-
|
1112
|
-
def normalize_status(status)
|
1113
|
-
return_code, started_time, elapsed_time, *rest = status
|
1114
|
-
_ = started_time = elapsed_time # for suppress warnings
|
1115
|
-
if return_code.zero?
|
1116
|
-
[0, 0.0, 0.0]
|
1117
|
-
else
|
1118
|
-
message, backtrace = rest
|
1119
|
-
_ = backtrace # for suppress warnings
|
1120
|
-
[[return_code, 0.0, 0.0], message]
|
1121
|
-
end
|
1122
|
-
end
|
1123
|
-
|
1124
|
-
def test_script_path
|
1125
|
-
@worker.test_script_path
|
1126
|
-
end
|
1127
|
-
|
1128
|
-
def have_extension?
|
1129
|
-
not test_script_path.extname.empty?
|
1130
|
-
end
|
1131
|
-
|
1132
|
-
def related_file_path(extension)
|
1133
|
-
path = Pathname(test_script_path.to_s.gsub(/\.[^.]+\z/, ".#{extension}"))
|
1134
|
-
return nil if test_script_path == path
|
1135
|
-
path
|
1136
|
-
end
|
1137
|
-
|
1138
|
-
def read_expected_result
|
1139
|
-
return nil unless have_extension?
|
1140
|
-
result_path = related_file_path("expected")
|
1141
|
-
return nil if result_path.nil?
|
1142
|
-
return nil unless result_path.exist?
|
1143
|
-
result_path.open("r:ascii-8bit") do |result_file|
|
1144
|
-
result_file.read
|
1145
|
-
end
|
1146
|
-
end
|
1147
|
-
|
1148
|
-
def remove_reject_file
|
1149
|
-
return unless have_extension?
|
1150
|
-
reject_path = related_file_path("reject")
|
1151
|
-
return if reject_path.nil?
|
1152
|
-
FileUtils.rm_rf(reject_path.to_s, :secure => true)
|
1153
|
-
end
|
1154
|
-
|
1155
|
-
def output_reject_file(actual_result)
|
1156
|
-
output_actual_result(actual_result, "reject")
|
1157
|
-
end
|
1158
|
-
|
1159
|
-
def output_actual_file(actual_result)
|
1160
|
-
output_actual_result(actual_result, "actual")
|
1161
|
-
end
|
1162
|
-
|
1163
|
-
def output_actual_result(actual_result, suffix)
|
1164
|
-
result_path = related_file_path(suffix)
|
1165
|
-
return if result_path.nil?
|
1166
|
-
result_path.open("w:ascii-8bit") do |result_file|
|
1167
|
-
result_file.print(actual_result)
|
1168
|
-
end
|
1169
|
-
end
|
1170
|
-
|
1171
|
-
def check_memory_leak(context)
|
1172
|
-
context.log.each_line do |line|
|
1173
|
-
timestamp, log_level, message = line.split(/\|\s*/, 3)
|
1174
|
-
_ = timestamp # suppress warning
|
1175
|
-
next unless /^grn_fin \((\d+)\)$/ =~ message
|
1176
|
-
n_leaked_objects = $1.to_i
|
1177
|
-
next if n_leaked_objects.zero?
|
1178
|
-
context.result << [:n_leaked_objects, n_leaked_objects, {}]
|
1179
|
-
end
|
1180
|
-
end
|
1181
|
-
end
|
1182
|
-
|
1183
|
-
class Executor
|
1184
|
-
class Context
|
1185
|
-
attr_writer :logging
|
1186
|
-
attr_accessor :base_directory, :temporary_directory_path, :db_path
|
1187
|
-
attr_accessor :groonga_suggest_create_dataset
|
1188
|
-
attr_accessor :result
|
1189
|
-
attr_accessor :output_type
|
1190
|
-
attr_accessor :on_error
|
1191
|
-
attr_accessor :abort_tag
|
1192
|
-
def initialize
|
1193
|
-
@logging = true
|
1194
|
-
@base_directory = Pathname(".")
|
1195
|
-
@temporary_directory_path = Pathname("tmp")
|
1196
|
-
@db_path = Pathname("db")
|
1197
|
-
@groonga_suggest_create_dataset = "groonga-suggest-create-dataset"
|
1198
|
-
@n_nested = 0
|
1199
|
-
@result = []
|
1200
|
-
@output_type = "json"
|
1201
|
-
@log = nil
|
1202
|
-
@on_error = :default
|
1203
|
-
@abort_tag = nil
|
1204
|
-
@omitted = false
|
1205
|
-
end
|
1206
|
-
|
1207
|
-
def logging?
|
1208
|
-
@logging
|
1209
|
-
end
|
1210
|
-
|
1211
|
-
def execute
|
1212
|
-
@n_nested += 1
|
1213
|
-
yield
|
1214
|
-
ensure
|
1215
|
-
@n_nested -= 1
|
1216
|
-
end
|
1217
|
-
|
1218
|
-
def top_level?
|
1219
|
-
@n_nested == 1
|
1220
|
-
end
|
1221
|
-
|
1222
|
-
def log_path
|
1223
|
-
@temporary_directory_path + "groonga.log"
|
1224
|
-
end
|
1225
|
-
|
1226
|
-
def log
|
1227
|
-
@log ||= File.open(log_path.to_s, "a+")
|
1228
|
-
end
|
1229
|
-
|
1230
|
-
def relative_db_path
|
1231
|
-
@db_path.relative_path_from(@temporary_directory_path)
|
1232
|
-
end
|
1233
|
-
|
1234
|
-
def omitted?
|
1235
|
-
@omitted
|
1236
|
-
end
|
1237
|
-
|
1238
|
-
def error
|
1239
|
-
case @on_error
|
1240
|
-
when :omit
|
1241
|
-
omit
|
1242
|
-
end
|
1243
|
-
end
|
1244
|
-
|
1245
|
-
def omit
|
1246
|
-
@omitted = true
|
1247
|
-
abort
|
1248
|
-
end
|
1249
|
-
|
1250
|
-
def abort
|
1251
|
-
throw @abort_tag
|
1252
|
-
end
|
1253
|
-
end
|
1254
|
-
|
1255
|
-
module ReturnCode
|
1256
|
-
SUCCESS = 0
|
1257
|
-
end
|
1258
|
-
|
1259
|
-
attr_reader :context
|
1260
|
-
def initialize(context=nil)
|
1261
|
-
@loading = false
|
1262
|
-
@pending_command = ""
|
1263
|
-
@pending_load_command = nil
|
1264
|
-
@current_command_name = nil
|
1265
|
-
@output_type = nil
|
1266
|
-
@long_timeout = default_long_timeout
|
1267
|
-
@context = context || Context.new
|
1268
|
-
end
|
1269
|
-
|
1270
|
-
def execute(script_path)
|
1271
|
-
unless script_path.exist?
|
1272
|
-
raise NotExist.new(script_path)
|
1273
|
-
end
|
1274
|
-
|
1275
|
-
@context.execute do
|
1276
|
-
script_path.open("r:ascii-8bit") do |script_file|
|
1277
|
-
parser = create_parser
|
1278
|
-
script_file.each_line do |line|
|
1279
|
-
begin
|
1280
|
-
parser << line
|
1281
|
-
rescue Error, Groonga::Command::ParseError
|
1282
|
-
line_info = "#{script_path}:#{script_file.lineno}:#{line.chomp}"
|
1283
|
-
log_error("#{line_info}: #{$!.message}")
|
1284
|
-
if $!.is_a?(Groonga::Command::ParseError)
|
1285
|
-
@context.abort
|
1286
|
-
else
|
1287
|
-
log_error("#{line_info}: #{$!.message}")
|
1288
|
-
raise unless @context.top_level?
|
1289
|
-
end
|
1290
|
-
end
|
1291
|
-
end
|
1292
|
-
end
|
1293
|
-
end
|
1294
|
-
|
1295
|
-
@context.result
|
1296
|
-
end
|
1297
|
-
|
1298
|
-
private
|
1299
|
-
def create_parser
|
1300
|
-
parser = Groonga::Command::Parser.new
|
1301
|
-
parser.on_command do |command|
|
1302
|
-
execute_command(command)
|
1303
|
-
end
|
1304
|
-
parser.on_load_complete do |command|
|
1305
|
-
execute_command(command)
|
1306
|
-
end
|
1307
|
-
parser.on_comment do |comment|
|
1308
|
-
if /\A@/ =~ comment
|
1309
|
-
directive_content = $POSTMATCH
|
1310
|
-
execute_directive("\##{comment}", directive_content)
|
1311
|
-
end
|
1312
|
-
end
|
1313
|
-
parser
|
1314
|
-
end
|
1315
|
-
|
1316
|
-
def resolve_path(path)
|
1317
|
-
if path.relative?
|
1318
|
-
@context.base_directory + path
|
1319
|
-
else
|
1320
|
-
path
|
1321
|
-
end
|
1322
|
-
end
|
1323
|
-
|
1324
|
-
def execute_directive_suggest_create_dataset(line, content, options)
|
1325
|
-
dataset_name = options.first
|
1326
|
-
if dataset_name.nil?
|
1327
|
-
log_input(line)
|
1328
|
-
log_error("#|e| [suggest-create-dataset] dataset name is missing")
|
1329
|
-
return
|
1330
|
-
end
|
1331
|
-
execute_suggest_create_dataset(dataset_name)
|
1332
|
-
end
|
1333
|
-
|
1334
|
-
def execute_directive_include(line, content, options)
|
1335
|
-
path = options.first
|
1336
|
-
if path.nil?
|
1337
|
-
log_input(line)
|
1338
|
-
log_error("#|e| [include] path is missing")
|
1339
|
-
return
|
1340
|
-
end
|
1341
|
-
execute_script(Pathname(path))
|
1342
|
-
end
|
1343
|
-
|
1344
|
-
def execute_directive_copy_path(line, content, options)
|
1345
|
-
source, destination, = options
|
1346
|
-
if source.nil? or destination.nil?
|
1347
|
-
log_input(line)
|
1348
|
-
if source.nil?
|
1349
|
-
log_error("#|e| [copy-path] source is missing")
|
1350
|
-
end
|
1351
|
-
if destiantion.nil?
|
1352
|
-
log_error("#|e| [copy-path] destination is missing")
|
1353
|
-
end
|
1354
|
-
return
|
1355
|
-
end
|
1356
|
-
source = resolve_path(Pathname(source))
|
1357
|
-
destination = resolve_path(Pathname(destination))
|
1358
|
-
FileUtils.cp_r(source.to_s, destination.to_s)
|
1359
|
-
end
|
1360
|
-
|
1361
|
-
def execute_directive_long_timeout(line, content, options)
|
1362
|
-
long_timeout, = options
|
1363
|
-
invalid_value_p = false
|
1364
|
-
case long_timeout
|
1365
|
-
when "default"
|
1366
|
-
@long_timeout = default_long_timeout
|
1367
|
-
when nil
|
1368
|
-
invalid_value_p = true
|
1369
|
-
else
|
1370
|
-
begin
|
1371
|
-
@long_timeout = Float(long_timeout)
|
1372
|
-
rescue ArgumentError
|
1373
|
-
invalid_value_p = true
|
1374
|
-
end
|
1375
|
-
end
|
1376
|
-
|
1377
|
-
if invalid_value_p
|
1378
|
-
log_input(line)
|
1379
|
-
message = "long-timeout must be number or 'default': <#{long_timeout}>"
|
1380
|
-
log_error("#|e| [long-timeout] #{message}")
|
1381
|
-
end
|
1382
|
-
end
|
1383
|
-
|
1384
|
-
def execute_directive_on_error(line, content, options)
|
1385
|
-
action, = options
|
1386
|
-
invalid_value_p = false
|
1387
|
-
valid_actions = ["default", "omit"]
|
1388
|
-
if valid_actions.include?(action)
|
1389
|
-
@context.on_error = action.to_sym
|
1390
|
-
else
|
1391
|
-
invalid_value_p = true
|
1392
|
-
end
|
1393
|
-
|
1394
|
-
if invalid_value_p
|
1395
|
-
log_input(line)
|
1396
|
-
valid_actions_label = "[#{valid_actions.join(', ')}]"
|
1397
|
-
message = "on-error must be one of #{valid_actions_label}"
|
1398
|
-
log_error("#|e| [on-error] #{message}: <#{action}>")
|
1399
|
-
end
|
1400
|
-
end
|
1401
|
-
|
1402
|
-
def execute_directive_omit(line, content, options)
|
1403
|
-
reason, = options
|
1404
|
-
@output_type = "raw"
|
1405
|
-
log_output("omit: #{reason}")
|
1406
|
-
@context.omit
|
1407
|
-
end
|
1408
|
-
|
1409
|
-
def execute_directive(line, content)
|
1410
|
-
command, *options = Shellwords.split(content)
|
1411
|
-
case command
|
1412
|
-
when "disable-logging"
|
1413
|
-
@context.logging = false
|
1414
|
-
when "enable-logging"
|
1415
|
-
@context.logging = true
|
1416
|
-
when "suggest-create-dataset"
|
1417
|
-
execute_directive_suggest_create_dataset(line, content, options)
|
1418
|
-
when "include"
|
1419
|
-
execute_directive_include(line, content, options)
|
1420
|
-
when "copy-path"
|
1421
|
-
execute_directive_copy_path(line, content, options)
|
1422
|
-
when "long-timeout"
|
1423
|
-
execute_directive_long_timeout(line, content, options)
|
1424
|
-
when "on-error"
|
1425
|
-
execute_directive_on_error(line, content, options)
|
1426
|
-
when "omit"
|
1427
|
-
execute_directive_omit(line, content, options)
|
1428
|
-
else
|
1429
|
-
log_input(line)
|
1430
|
-
log_error("#|e| unknown directive: <#{command}>")
|
1431
|
-
end
|
1432
|
-
end
|
1433
|
-
|
1434
|
-
def execute_suggest_create_dataset(dataset_name)
|
1435
|
-
command_line = [@context.groonga_suggest_create_dataset,
|
1436
|
-
@context.db_path.to_s,
|
1437
|
-
dataset_name]
|
1438
|
-
packed_command_line = command_line.join(" ")
|
1439
|
-
log_input("#{packed_command_line}\n")
|
1440
|
-
begin
|
1441
|
-
IO.popen(command_line, "r:ascii-8bit") do |io|
|
1442
|
-
log_output(io.read)
|
1443
|
-
end
|
1444
|
-
rescue SystemCallError
|
1445
|
-
raise Error.new("failed to run groonga-suggest-create-dataset: " +
|
1446
|
-
"<#{packed_command_line}>: #{$!}")
|
1447
|
-
end
|
1448
|
-
end
|
1449
|
-
|
1450
|
-
def execute_script(script_path)
|
1451
|
-
executor = create_sub_executor(@context)
|
1452
|
-
executor.execute(resolve_path(script_path))
|
1453
|
-
end
|
1454
|
-
|
1455
|
-
def extract_command_info(command)
|
1456
|
-
@current_command = command
|
1457
|
-
if @current_command.name == "dump"
|
1458
|
-
@output_type = "groonga-command"
|
1459
|
-
else
|
1460
|
-
@output_type = @current_command[:output_type] || @context.output_type
|
1461
|
-
end
|
1462
|
-
end
|
1463
|
-
|
1464
|
-
def execute_command(command)
|
1465
|
-
extract_command_info(command)
|
1466
|
-
log_input("#{command.original_source}\n")
|
1467
|
-
response = send_command(command)
|
1468
|
-
type = @output_type
|
1469
|
-
log_output(response)
|
1470
|
-
log_error(read_error_log)
|
1471
|
-
|
1472
|
-
@context.error if error_response?(response, type)
|
1473
|
-
end
|
1474
|
-
|
1475
|
-
def read_error_log
|
1476
|
-
log = read_all_readable_content(context.log, :first_timeout => 0)
|
1477
|
-
normalized_error_log = ""
|
1478
|
-
log.each_line do |line|
|
1479
|
-
timestamp, log_level, message = line.split(/\|\s*/, 3)
|
1480
|
-
_ = timestamp # suppress warning
|
1481
|
-
next unless error_log_level?(log_level)
|
1482
|
-
next if backtrace_log_message?(message)
|
1483
|
-
normalized_error_log << "\#|#{log_level}| #{message}"
|
1484
|
-
end
|
1485
|
-
normalized_error_log.chomp
|
1486
|
-
end
|
1487
|
-
|
1488
|
-
def read_all_readable_content(output, options={})
|
1489
|
-
content = ""
|
1490
|
-
first_timeout = options[:first_timeout] || 1
|
1491
|
-
timeout = first_timeout
|
1492
|
-
while IO.select([output], [], [], timeout)
|
1493
|
-
break if output.eof?
|
1494
|
-
request_bytes = 1024
|
1495
|
-
read_content = output.readpartial(request_bytes)
|
1496
|
-
content << read_content
|
1497
|
-
timeout = 0 if read_content.bytesize < request_bytes
|
1498
|
-
end
|
1499
|
-
content
|
1500
|
-
end
|
1501
|
-
|
1502
|
-
def error_log_level?(log_level)
|
1503
|
-
["E", "A", "C", "e"].include?(log_level)
|
1504
|
-
end
|
1505
|
-
|
1506
|
-
def backtrace_log_message?(message)
|
1507
|
-
message.start_with?("/")
|
1508
|
-
end
|
1509
|
-
|
1510
|
-
def error_response?(response, type)
|
1511
|
-
status = nil
|
1512
|
-
begin
|
1513
|
-
status, = ResponseParser.parse(response, type)
|
1514
|
-
rescue ParseError
|
1515
|
-
return false
|
1516
|
-
end
|
1517
|
-
|
1518
|
-
return_code, = status
|
1519
|
-
return_code != ReturnCode::SUCCESS
|
1520
|
-
end
|
1521
|
-
|
1522
|
-
def log(tag, content, options={})
|
1523
|
-
return unless @context.logging?
|
1524
|
-
log_force(tag, content, options)
|
1525
|
-
end
|
1526
|
-
|
1527
|
-
def log_force(tag, content, options)
|
1528
|
-
return if content.empty?
|
1529
|
-
@context.result << [tag, content, options]
|
1530
|
-
end
|
1531
|
-
|
1532
|
-
def log_input(content)
|
1533
|
-
log(:input, content)
|
1534
|
-
end
|
1535
|
-
|
1536
|
-
def log_output(content)
|
1537
|
-
log(:output, content,
|
1538
|
-
:command => @current_command,
|
1539
|
-
:type => @output_type)
|
1540
|
-
@current_command = nil
|
1541
|
-
@output_type = nil
|
1542
|
-
end
|
1543
|
-
|
1544
|
-
def log_error(content)
|
1545
|
-
log_force(:error, content, {})
|
1546
|
-
end
|
1547
|
-
|
1548
|
-
def default_long_timeout
|
1549
|
-
180
|
1550
|
-
end
|
1551
|
-
end
|
1552
|
-
|
1553
|
-
class StandardIOExecutor < Executor
|
1554
|
-
def initialize(input, output, context=nil)
|
1555
|
-
super(context)
|
1556
|
-
@input = input
|
1557
|
-
@output = output
|
1558
|
-
end
|
1559
|
-
|
1560
|
-
def send_command(command)
|
1561
|
-
command_line = @current_command.original_source
|
1562
|
-
unless @current_command.has_key?(:output_type)
|
1563
|
-
command_line = command_line.sub(/$/, " --output_type #{@output_type}")
|
1564
|
-
end
|
1565
|
-
begin
|
1566
|
-
@input.print(command_line)
|
1567
|
-
@input.print("\n")
|
1568
|
-
@input.flush
|
1569
|
-
rescue SystemCallError
|
1570
|
-
message = "failed to write to groonga: <#{command_line}>: #{$!}"
|
1571
|
-
raise Error.new(message)
|
1572
|
-
end
|
1573
|
-
read_output
|
1574
|
-
end
|
1575
|
-
|
1576
|
-
def ensure_groonga_ready
|
1577
|
-
@input.print("status\n")
|
1578
|
-
@input.flush
|
1579
|
-
@output.gets
|
1580
|
-
end
|
1581
|
-
|
1582
|
-
def create_sub_executor(context)
|
1583
|
-
self.class.new(@input, @output, context)
|
1584
|
-
end
|
1585
|
-
|
1586
|
-
private
|
1587
|
-
def read_output
|
1588
|
-
options = {}
|
1589
|
-
options[:first_timeout] = @long_timeout if may_slow_command?
|
1590
|
-
read_all_readable_content(@output, options)
|
1591
|
-
end
|
1592
|
-
|
1593
|
-
MAY_SLOW_COMMANDS = [
|
1594
|
-
"column_create",
|
1595
|
-
"register",
|
1596
|
-
]
|
1597
|
-
def may_slow_command?
|
1598
|
-
MAY_SLOW_COMMANDS.include?(@current_command)
|
1599
|
-
end
|
1600
|
-
end
|
1601
|
-
|
1602
|
-
class HTTPExecutor < Executor
|
1603
|
-
def initialize(host, port, context=nil)
|
1604
|
-
super(context)
|
1605
|
-
@host = host
|
1606
|
-
@port = port
|
1607
|
-
end
|
1608
|
-
|
1609
|
-
def send_command(command_line)
|
1610
|
-
converter = CommandFormatConverter.new(command_line)
|
1611
|
-
url = "http://#{@host}:#{@port}#{converter.to_url}"
|
1612
|
-
begin
|
1613
|
-
open(url) do |response|
|
1614
|
-
"#{response.read}\n"
|
1615
|
-
end
|
1616
|
-
rescue OpenURI::HTTPError
|
1617
|
-
message = "Failed to get response from groonga: #{$!}: <#{url}>"
|
1618
|
-
raise Error.new(message)
|
1619
|
-
end
|
1620
|
-
end
|
1621
|
-
|
1622
|
-
def ensure_groonga_ready
|
1623
|
-
n_retried = 0
|
1624
|
-
begin
|
1625
|
-
send_command("status")
|
1626
|
-
rescue SystemCallError
|
1627
|
-
n_retried += 1
|
1628
|
-
sleep(0.1)
|
1629
|
-
retry if n_retried < 10
|
1630
|
-
raise
|
1631
|
-
end
|
1632
|
-
end
|
1633
|
-
|
1634
|
-
def create_sub_executor(context)
|
1635
|
-
self.class.new(@host, @port, context)
|
1636
|
-
end
|
1637
|
-
end
|
1638
|
-
|
1639
|
-
class CommandFormatConverter
|
1640
|
-
def initialize(gqtp_command)
|
1641
|
-
@gqtp_command = gqtp_command
|
1642
|
-
end
|
1643
|
-
|
1644
|
-
def to_url
|
1645
|
-
command = Groonga::Command::Parser.parse(@gqtp_command)
|
1646
|
-
command.to_uri_format
|
1647
|
-
end
|
1648
|
-
end
|
1649
|
-
|
1650
|
-
class BaseReporter
|
1651
|
-
def initialize(tester)
|
1652
|
-
@tester = tester
|
1653
|
-
@term_width = guess_term_width
|
1654
|
-
@output = @tester.output
|
1655
|
-
@mutex = Mutex.new
|
1656
|
-
reset_current_column
|
1657
|
-
end
|
1658
|
-
|
1659
|
-
private
|
1660
|
-
def synchronize
|
1661
|
-
@mutex.synchronize do
|
1662
|
-
yield
|
1663
|
-
end
|
1664
|
-
end
|
1665
|
-
|
1666
|
-
def report_summary(result)
|
1667
|
-
puts(statistics_header)
|
1668
|
-
puts(colorize(statistics(result), result))
|
1669
|
-
pass_ratio = result.pass_ratio
|
1670
|
-
elapsed_time = result.elapsed_time
|
1671
|
-
summary = "%.4g%% passed in %.4fs." % [pass_ratio, elapsed_time]
|
1672
|
-
puts(colorize(summary, result))
|
1673
|
-
end
|
1674
|
-
|
1675
|
-
def columns
|
1676
|
-
[
|
1677
|
-
# label, format value
|
1678
|
-
["tests/sec", lambda {|result| "%9.2f" % throughput(result)}],
|
1679
|
-
[" tests", lambda {|result| "%8d" % result.n_tests}],
|
1680
|
-
[" passes", lambda {|result| "%8d" % result.n_passed_tests}],
|
1681
|
-
["failures", lambda {|result| "%8d" % result.n_failed_tests}],
|
1682
|
-
[" leaked", lambda {|result| "%8d" % result.n_leaked_tests}],
|
1683
|
-
[" omitted", lambda {|result| "%8d" % result.n_omitted_tests}],
|
1684
|
-
["!checked", lambda {|result| "%8d" % result.n_not_checked_tests}],
|
1685
|
-
]
|
1686
|
-
end
|
1687
|
-
|
1688
|
-
def statistics_header
|
1689
|
-
labels = columns.collect do |label, format_value|
|
1690
|
-
label
|
1691
|
-
end
|
1692
|
-
" " + labels.join(" | ") + " |"
|
1693
|
-
end
|
1694
|
-
|
1695
|
-
def statistics(result)
|
1696
|
-
items = columns.collect do |label, format_value|
|
1697
|
-
format_value.call(result)
|
1698
|
-
end
|
1699
|
-
" " + items.join(" | ") + " |"
|
1700
|
-
end
|
1701
|
-
|
1702
|
-
def throughput(result)
|
1703
|
-
if result.elapsed_time.zero?
|
1704
|
-
tests_per_second = 0
|
1705
|
-
else
|
1706
|
-
tests_per_second = result.n_tests / result.elapsed_time
|
1707
|
-
end
|
1708
|
-
tests_per_second
|
1709
|
-
end
|
1710
|
-
|
1711
|
-
def report_failure(result)
|
1712
|
-
report_marker(result)
|
1713
|
-
report_diff(result.expected, result.actual)
|
1714
|
-
report_marker(result)
|
1715
|
-
end
|
1716
|
-
|
1717
|
-
def report_actual(result)
|
1718
|
-
report_marker(result)
|
1719
|
-
puts(result.actual)
|
1720
|
-
report_marker(result)
|
1721
|
-
end
|
1722
|
-
|
1723
|
-
def report_marker(result)
|
1724
|
-
puts(colorize("=" * @term_width, result))
|
1725
|
-
end
|
1726
|
-
|
1727
|
-
def report_diff(expected, actual)
|
1728
|
-
create_temporary_file("expected", expected) do |expected_file|
|
1729
|
-
create_temporary_file("actual", actual) do |actual_file|
|
1730
|
-
diff_options = @tester.diff_options.dup
|
1731
|
-
diff_options.concat(["--label", "(expected)", expected_file.path,
|
1732
|
-
"--label", "(actual)", actual_file.path])
|
1733
|
-
system(@tester.diff, *diff_options)
|
1734
|
-
end
|
1735
|
-
end
|
1736
|
-
end
|
1737
|
-
|
1738
|
-
def report_test(worker, result)
|
1739
|
-
report_marker(result)
|
1740
|
-
print("[#{worker.id}] ") if @tester.n_workers > 1
|
1741
|
-
puts(worker.suite_name)
|
1742
|
-
print(" #{worker.test_name}")
|
1743
|
-
report_test_result(result, worker.status)
|
1744
|
-
end
|
1745
|
-
|
1746
|
-
def report_test_result(result, label)
|
1747
|
-
message = test_result_message(result, label)
|
1748
|
-
message_width = string_width(message)
|
1749
|
-
rest_width = @term_width - @current_column
|
1750
|
-
if rest_width > message_width
|
1751
|
-
print(" " * (rest_width - message_width))
|
1752
|
-
end
|
1753
|
-
puts(message)
|
1754
|
-
end
|
1755
|
-
|
1756
|
-
def test_result_message(result, label)
|
1757
|
-
elapsed_time = result.elapsed_time
|
1758
|
-
formatted_elapsed_time = "%.4fs" % elapsed_time
|
1759
|
-
formatted_elapsed_time = colorize(formatted_elapsed_time,
|
1760
|
-
elapsed_time_status(elapsed_time))
|
1761
|
-
" #{formatted_elapsed_time} [#{colorize(label, result)}]"
|
1762
|
-
end
|
1763
|
-
|
1764
|
-
LONG_ELAPSED_TIME = 1.0
|
1765
|
-
def long_elapsed_time?(elapsed_time)
|
1766
|
-
elapsed_time >= LONG_ELAPSED_TIME
|
1767
|
-
end
|
1768
|
-
|
1769
|
-
def elapsed_time_status(elapsed_time)
|
1770
|
-
if long_elapsed_time?(elapsed_time)
|
1771
|
-
elapsed_time_status = :failure
|
1772
|
-
else
|
1773
|
-
elapsed_time_status = :not_checked
|
1774
|
-
end
|
1775
|
-
end
|
1776
|
-
|
1777
|
-
def justify(message, width)
|
1778
|
-
return " " * width if message.nil?
|
1779
|
-
return message.ljust(width) if message.bytesize <= width
|
1780
|
-
half_width = width / 2.0
|
1781
|
-
elision_mark = "..."
|
1782
|
-
left = message[0, half_width.ceil - elision_mark.size]
|
1783
|
-
right = message[(message.size - half_width.floor)..-1]
|
1784
|
-
"#{left}#{elision_mark}#{right}"
|
1785
|
-
end
|
1786
|
-
|
1787
|
-
def print(message)
|
1788
|
-
@current_column += string_width(message.to_s)
|
1789
|
-
@output.print(message)
|
1790
|
-
end
|
1791
|
-
|
1792
|
-
def puts(*messages)
|
1793
|
-
reset_current_column
|
1794
|
-
@output.puts(*messages)
|
1795
|
-
end
|
1796
|
-
|
1797
|
-
def reset_current_column
|
1798
|
-
@current_column = 0
|
1799
|
-
end
|
1800
|
-
|
1801
|
-
def create_temporary_file(key, content)
|
1802
|
-
file = Tempfile.new("groonga-test-#{key}")
|
1803
|
-
file.print(content)
|
1804
|
-
file.close
|
1805
|
-
yield(file)
|
1806
|
-
end
|
1807
|
-
|
1808
|
-
def guess_term_width
|
1809
|
-
Integer(guess_term_width_from_env || guess_term_width_from_stty || 79)
|
1810
|
-
rescue ArgumentError
|
1811
|
-
0
|
1812
|
-
end
|
1813
|
-
|
1814
|
-
def guess_term_width_from_env
|
1815
|
-
ENV["COLUMNS"] || ENV["TERM_WIDTH"]
|
1816
|
-
end
|
1817
|
-
|
1818
|
-
def guess_term_width_from_stty
|
1819
|
-
return nil unless STDIN.tty?
|
1820
|
-
|
1821
|
-
case tty_info
|
1822
|
-
when /(\d+) columns/
|
1823
|
-
$1
|
1824
|
-
when /columns (\d+)/
|
1825
|
-
$1
|
1826
|
-
else
|
1827
|
-
nil
|
1828
|
-
end
|
1829
|
-
end
|
1830
|
-
|
1831
|
-
def tty_info
|
1832
|
-
begin
|
1833
|
-
`stty -a`
|
1834
|
-
rescue SystemCallError
|
1835
|
-
nil
|
1836
|
-
end
|
1837
|
-
end
|
1838
|
-
|
1839
|
-
def string_width(string)
|
1840
|
-
string.gsub(/\e\[[0-9;]+m/, "").size
|
1841
|
-
end
|
1842
|
-
|
1843
|
-
def result_status(result)
|
1844
|
-
if result.respond_to?(:status)
|
1845
|
-
result.status
|
1846
|
-
else
|
1847
|
-
if result.n_failed_tests > 0
|
1848
|
-
:failure
|
1849
|
-
elsif result.n_leaked_tests > 0
|
1850
|
-
:leaked
|
1851
|
-
elsif result.n_omitted_tests > 0
|
1852
|
-
:omitted
|
1853
|
-
elsif result.n_not_checked_tests > 0
|
1854
|
-
:not_checked
|
1855
|
-
else
|
1856
|
-
:success
|
1857
|
-
end
|
1858
|
-
end
|
1859
|
-
end
|
1860
|
-
|
1861
|
-
def colorize(message, result_or_status)
|
1862
|
-
return message unless @tester.use_color?
|
1863
|
-
if result_or_status.is_a?(Symbol)
|
1864
|
-
status = result_or_status
|
1865
|
-
else
|
1866
|
-
status = result_status(result_or_status)
|
1867
|
-
end
|
1868
|
-
case status
|
1869
|
-
when :success
|
1870
|
-
"%s%s%s" % [success_color, message, reset_color]
|
1871
|
-
when :failure
|
1872
|
-
"%s%s%s" % [failure_color, message, reset_color]
|
1873
|
-
when :leaked
|
1874
|
-
"%s%s%s" % [leaked_color, message, reset_color]
|
1875
|
-
when :omitted
|
1876
|
-
"%s%s%s" % [omitted_color, message, reset_color]
|
1877
|
-
when :not_checked
|
1878
|
-
"%s%s%s" % [not_checked_color, message, reset_color]
|
1879
|
-
else
|
1880
|
-
message
|
1881
|
-
end
|
1882
|
-
end
|
1883
|
-
|
1884
|
-
def success_color
|
1885
|
-
escape_sequence({
|
1886
|
-
:color => :green,
|
1887
|
-
:color_256 => [0, 3, 0],
|
1888
|
-
:background => true,
|
1889
|
-
},
|
1890
|
-
{
|
1891
|
-
:color => :white,
|
1892
|
-
:color_256 => [5, 5, 5],
|
1893
|
-
:bold => true,
|
1894
|
-
})
|
1895
|
-
end
|
1896
|
-
|
1897
|
-
def failure_color
|
1898
|
-
escape_sequence({
|
1899
|
-
:color => :red,
|
1900
|
-
:color_256 => [3, 0, 0],
|
1901
|
-
:background => true,
|
1902
|
-
},
|
1903
|
-
{
|
1904
|
-
:color => :white,
|
1905
|
-
:color_256 => [5, 5, 5],
|
1906
|
-
:bold => true,
|
1907
|
-
})
|
1908
|
-
end
|
1909
|
-
|
1910
|
-
def leaked_color
|
1911
|
-
escape_sequence({
|
1912
|
-
:color => :magenta,
|
1913
|
-
:color_256 => [3, 0, 3],
|
1914
|
-
:background => true,
|
1915
|
-
},
|
1916
|
-
{
|
1917
|
-
:color => :white,
|
1918
|
-
:color_256 => [5, 5, 5],
|
1919
|
-
:bold => true,
|
1920
|
-
})
|
1921
|
-
end
|
1922
|
-
|
1923
|
-
def omitted_color
|
1924
|
-
escape_sequence({
|
1925
|
-
:color => :blue,
|
1926
|
-
:color_256 => [0, 0, 1],
|
1927
|
-
:background => true,
|
1928
|
-
},
|
1929
|
-
{
|
1930
|
-
:color => :white,
|
1931
|
-
:color_256 => [5, 5, 5],
|
1932
|
-
:bold => true,
|
1933
|
-
})
|
1934
|
-
end
|
1935
|
-
|
1936
|
-
def not_checked_color
|
1937
|
-
escape_sequence({
|
1938
|
-
:color => :cyan,
|
1939
|
-
:color_256 => [0, 1, 1],
|
1940
|
-
:background => true,
|
1941
|
-
},
|
1942
|
-
{
|
1943
|
-
:color => :white,
|
1944
|
-
:color_256 => [5, 5, 5],
|
1945
|
-
:bold => true,
|
1946
|
-
})
|
1947
|
-
end
|
1948
|
-
|
1949
|
-
def reset_color
|
1950
|
-
escape_sequence(:reset)
|
1951
|
-
end
|
1952
|
-
|
1953
|
-
COLOR_NAMES = [
|
1954
|
-
:black, :red, :green, :yellow,
|
1955
|
-
:blue, :magenta, :cyan, :white,
|
1956
|
-
]
|
1957
|
-
def escape_sequence(*commands)
|
1958
|
-
sequence = []
|
1959
|
-
commands.each do |command|
|
1960
|
-
case command
|
1961
|
-
when :reset
|
1962
|
-
sequence << "0"
|
1963
|
-
when :bold
|
1964
|
-
sequence << "1"
|
1965
|
-
when :italic
|
1966
|
-
sequence << "3"
|
1967
|
-
when :underline
|
1968
|
-
sequence << "4"
|
1969
|
-
when Hash
|
1970
|
-
foreground_p = !command[:background]
|
1971
|
-
if available_colors == 256
|
1972
|
-
sequence << (foreground_p ? "38" : "48")
|
1973
|
-
sequence << "5"
|
1974
|
-
sequence << pack_256_color(*command[:color_256])
|
1975
|
-
else
|
1976
|
-
color_parameter = foreground_p ? 3 : 4
|
1977
|
-
color_parameter += 6 if command[:intensity]
|
1978
|
-
color = COLOR_NAMES.index(command[:color])
|
1979
|
-
sequence << "#{color_parameter}#{color}"
|
1980
|
-
end
|
1981
|
-
end
|
1982
|
-
end
|
1983
|
-
"\e[#{sequence.join(';')}m"
|
1984
|
-
end
|
1985
|
-
|
1986
|
-
def pack_256_color(red, green, blue)
|
1987
|
-
red * 36 + green * 6 + blue + 16
|
1988
|
-
end
|
1989
|
-
|
1990
|
-
def available_colors
|
1991
|
-
case ENV["COLORTERM"]
|
1992
|
-
when "gnome-terminal"
|
1993
|
-
256
|
1994
|
-
else
|
1995
|
-
case ENV["TERM"]
|
1996
|
-
when /-256color\z/
|
1997
|
-
256
|
1998
|
-
else
|
1999
|
-
8
|
2000
|
-
end
|
2001
|
-
end
|
2002
|
-
end
|
2003
|
-
end
|
2004
|
-
|
2005
|
-
class MarkReporter < BaseReporter
|
2006
|
-
def initialize(tester)
|
2007
|
-
super
|
2008
|
-
end
|
2009
|
-
|
2010
|
-
def on_start(result)
|
2011
|
-
end
|
2012
|
-
|
2013
|
-
def on_worker_start(worker)
|
2014
|
-
end
|
2015
|
-
|
2016
|
-
def on_suite_start(worker)
|
2017
|
-
end
|
2018
|
-
|
2019
|
-
def on_test_start(worker)
|
2020
|
-
end
|
2021
|
-
|
2022
|
-
def on_test_success(worker, result)
|
2023
|
-
synchronize do
|
2024
|
-
report_test_result_mark(".", result)
|
2025
|
-
end
|
2026
|
-
end
|
2027
|
-
|
2028
|
-
def on_test_failure(worker, result)
|
2029
|
-
synchronize do
|
2030
|
-
report_test_result_mark("F", result)
|
2031
|
-
puts
|
2032
|
-
report_test(worker, result)
|
2033
|
-
report_failure(result)
|
2034
|
-
end
|
2035
|
-
end
|
2036
|
-
|
2037
|
-
def on_test_leak(worker, result)
|
2038
|
-
synchronize do
|
2039
|
-
report_test_result_mark("L(#{result.n_leaked_objects})", result)
|
2040
|
-
end
|
2041
|
-
end
|
2042
|
-
|
2043
|
-
def on_test_omission(worker, result)
|
2044
|
-
synchronize do
|
2045
|
-
report_test_result_mark("O", result)
|
2046
|
-
puts
|
2047
|
-
report_test(worker, result)
|
2048
|
-
report_actual(result)
|
2049
|
-
end
|
2050
|
-
end
|
2051
|
-
|
2052
|
-
def on_test_no_check(worker, result)
|
2053
|
-
synchronize do
|
2054
|
-
report_test_result_mark("N", result)
|
2055
|
-
puts
|
2056
|
-
report_test(worker, result)
|
2057
|
-
report_actual(result)
|
2058
|
-
end
|
2059
|
-
end
|
2060
|
-
|
2061
|
-
def on_test_finish(worker, result)
|
2062
|
-
end
|
2063
|
-
|
2064
|
-
def on_suite_finish(worker)
|
2065
|
-
end
|
2066
|
-
|
2067
|
-
def on_worker_finish(worker_id)
|
2068
|
-
end
|
2069
|
-
|
2070
|
-
def on_finish(result)
|
2071
|
-
puts
|
2072
|
-
puts
|
2073
|
-
report_summary(result)
|
2074
|
-
end
|
2075
|
-
|
2076
|
-
private
|
2077
|
-
def report_test_result_mark(mark, result)
|
2078
|
-
if @term_width < @current_column + mark.bytesize
|
2079
|
-
puts
|
2080
|
-
end
|
2081
|
-
print(colorize(mark, result))
|
2082
|
-
if @term_width <= @current_column
|
2083
|
-
puts
|
2084
|
-
else
|
2085
|
-
@output.flush
|
2086
|
-
end
|
2087
|
-
end
|
2088
|
-
end
|
2089
|
-
|
2090
|
-
class StreamReporter < BaseReporter
|
2091
|
-
def initialize(tester)
|
2092
|
-
super
|
2093
|
-
end
|
2094
|
-
|
2095
|
-
def on_start(result)
|
2096
|
-
end
|
2097
|
-
|
2098
|
-
def on_worker_start(worker)
|
2099
|
-
end
|
2100
|
-
|
2101
|
-
def on_suite_start(worker)
|
2102
|
-
if worker.suite_name.bytesize <= @term_width
|
2103
|
-
puts(worker.suite_name)
|
2104
|
-
else
|
2105
|
-
puts(justify(worker.suite_name, @term_width))
|
2106
|
-
end
|
2107
|
-
@output.flush
|
2108
|
-
end
|
2109
|
-
|
2110
|
-
def on_test_start(worker)
|
2111
|
-
print(" #{worker.test_name}")
|
2112
|
-
@output.flush
|
2113
|
-
end
|
2114
|
-
|
2115
|
-
def on_test_success(worker, result)
|
2116
|
-
report_test_result(result, worker.status)
|
2117
|
-
end
|
2118
|
-
|
2119
|
-
def on_test_failure(worker, result)
|
2120
|
-
report_test_result(result, worker.status)
|
2121
|
-
report_failure(result)
|
2122
|
-
end
|
2123
|
-
|
2124
|
-
def on_test_leak(worker, result)
|
2125
|
-
report_test_result(result, worker.status)
|
2126
|
-
end
|
2127
|
-
|
2128
|
-
def on_test_omission(worker, result)
|
2129
|
-
report_test_result(result, worker.status)
|
2130
|
-
report_actual(result)
|
2131
|
-
end
|
2132
|
-
|
2133
|
-
def on_test_no_check(worker, result)
|
2134
|
-
report_test_result(result, worker.status)
|
2135
|
-
report_actual(result)
|
2136
|
-
end
|
2137
|
-
|
2138
|
-
def on_test_finish(worker, result)
|
2139
|
-
end
|
2140
|
-
|
2141
|
-
def on_suite_finish(worker)
|
2142
|
-
end
|
2143
|
-
|
2144
|
-
def on_worker_finish(worker_id)
|
2145
|
-
end
|
2146
|
-
|
2147
|
-
def on_finish(result)
|
2148
|
-
puts
|
2149
|
-
report_summary(result)
|
2150
|
-
end
|
2151
|
-
end
|
2152
|
-
|
2153
|
-
class InplaceReporter < BaseReporter
|
2154
|
-
def initialize(tester)
|
2155
|
-
super
|
2156
|
-
@last_redraw_time = Time.now
|
2157
|
-
@minimum_redraw_interval = 0.1
|
2158
|
-
end
|
2159
|
-
|
2160
|
-
def on_start(result)
|
2161
|
-
@test_suites_result = result
|
2162
|
-
end
|
2163
|
-
|
2164
|
-
def on_worker_start(worker)
|
2165
|
-
end
|
2166
|
-
|
2167
|
-
def on_suite_start(worker)
|
2168
|
-
redraw
|
2169
|
-
end
|
2170
|
-
|
2171
|
-
def on_test_start(worker)
|
2172
|
-
redraw
|
2173
|
-
end
|
2174
|
-
|
2175
|
-
def on_test_success(worker, result)
|
2176
|
-
redraw
|
2177
|
-
end
|
2178
|
-
|
2179
|
-
def on_test_failure(worker, result)
|
2180
|
-
redraw do
|
2181
|
-
report_test(worker, result)
|
2182
|
-
report_failure(result)
|
2183
|
-
end
|
2184
|
-
end
|
2185
|
-
|
2186
|
-
def on_test_leak(worker, result)
|
2187
|
-
redraw do
|
2188
|
-
report_test(worker, result)
|
2189
|
-
report_marker(result)
|
2190
|
-
end
|
2191
|
-
end
|
2192
|
-
|
2193
|
-
def on_test_omission(worker, result)
|
2194
|
-
redraw do
|
2195
|
-
report_test(worker, result)
|
2196
|
-
report_actual(result)
|
2197
|
-
end
|
2198
|
-
end
|
2199
|
-
|
2200
|
-
def on_test_no_check(worker, result)
|
2201
|
-
redraw do
|
2202
|
-
report_test(worker, result)
|
2203
|
-
report_actual(result)
|
2204
|
-
end
|
2205
|
-
end
|
2206
|
-
|
2207
|
-
def on_test_finish(worker, result)
|
2208
|
-
redraw
|
2209
|
-
end
|
2210
|
-
|
2211
|
-
def on_suite_finish(worker)
|
2212
|
-
redraw
|
2213
|
-
end
|
2214
|
-
|
2215
|
-
def on_worker_finish(worker)
|
2216
|
-
redraw
|
2217
|
-
end
|
2218
|
-
|
2219
|
-
def on_finish(result)
|
2220
|
-
draw
|
2221
|
-
puts
|
2222
|
-
report_summary(result)
|
2223
|
-
end
|
2224
|
-
|
2225
|
-
private
|
2226
|
-
def draw
|
2227
|
-
draw_statistics_header_line
|
2228
|
-
@test_suites_result.workers.each do |worker|
|
2229
|
-
draw_status_line(worker)
|
2230
|
-
draw_test_line(worker)
|
2231
|
-
end
|
2232
|
-
draw_progress_line
|
2233
|
-
end
|
2234
|
-
|
2235
|
-
def draw_statistics_header_line
|
2236
|
-
puts(statistics_header)
|
2237
|
-
end
|
2238
|
-
|
2239
|
-
def draw_status_line(worker)
|
2240
|
-
clear_line
|
2241
|
-
left = "[#{colorize(worker.id, worker.result)}] "
|
2242
|
-
right = " [#{worker.status}]"
|
2243
|
-
rest_width = @term_width - @current_column
|
2244
|
-
center_width = rest_width - string_width(left) - string_width(right)
|
2245
|
-
center = justify(worker.suite_name, center_width)
|
2246
|
-
puts("#{left}#{center}#{right}")
|
2247
|
-
end
|
2248
|
-
|
2249
|
-
def draw_test_line(worker)
|
2250
|
-
clear_line
|
2251
|
-
if worker.test_name
|
2252
|
-
label = " #{worker.test_name}"
|
2253
|
-
else
|
2254
|
-
label = statistics(worker.result)
|
2255
|
-
end
|
2256
|
-
puts(justify(label, @term_width))
|
2257
|
-
end
|
2258
|
-
|
2259
|
-
def draw_progress_line
|
2260
|
-
n_done_tests = @test_suites_result.n_tests
|
2261
|
-
n_total_tests = @test_suites_result.n_total_tests
|
2262
|
-
if n_total_tests.zero?
|
2263
|
-
finished_test_ratio = 0.0
|
2264
|
-
else
|
2265
|
-
finished_test_ratio = n_done_tests.to_f / n_total_tests
|
2266
|
-
end
|
2267
|
-
|
2268
|
-
start_mark = "|"
|
2269
|
-
finish_mark = "|"
|
2270
|
-
statistics = " [%3d%%]" % (finished_test_ratio * 100)
|
2271
|
-
|
2272
|
-
progress_width = @term_width
|
2273
|
-
progress_width -= start_mark.bytesize
|
2274
|
-
progress_width -= finish_mark.bytesize
|
2275
|
-
progress_width -= statistics.bytesize
|
2276
|
-
finished_mark = "-"
|
2277
|
-
if n_done_tests == n_total_tests
|
2278
|
-
progress = colorize(finished_mark * progress_width,
|
2279
|
-
@test_suites_result)
|
2280
|
-
else
|
2281
|
-
current_mark = ">"
|
2282
|
-
finished_marks_width = (progress_width * finished_test_ratio).ceil
|
2283
|
-
finished_marks_width -= current_mark.bytesize
|
2284
|
-
finished_marks_width = [0, finished_marks_width].max
|
2285
|
-
progress = finished_mark * finished_marks_width + current_mark
|
2286
|
-
progress = colorize(progress, @test_suites_result)
|
2287
|
-
progress << " " * (progress_width - string_width(progress))
|
2288
|
-
end
|
2289
|
-
puts("#{start_mark}#{progress}#{finish_mark}#{statistics}")
|
2290
|
-
end
|
2291
|
-
|
2292
|
-
def redraw
|
2293
|
-
synchronize do
|
2294
|
-
unless block_given?
|
2295
|
-
return if Time.now - @last_redraw_time < @minimum_redraw_interval
|
2296
|
-
end
|
2297
|
-
draw
|
2298
|
-
if block_given?
|
2299
|
-
yield
|
2300
|
-
else
|
2301
|
-
up_n_lines(n_using_lines)
|
2302
|
-
end
|
2303
|
-
@last_redraw_time = Time.now
|
2304
|
-
end
|
2305
|
-
end
|
2306
|
-
|
2307
|
-
def up_n_lines(n)
|
2308
|
-
print("\e[1A" * n)
|
2309
|
-
end
|
2310
|
-
|
2311
|
-
def clear_line
|
2312
|
-
print(" " * @term_width)
|
2313
|
-
print("\r")
|
2314
|
-
reset_current_column
|
2315
|
-
end
|
2316
|
-
|
2317
|
-
def n_using_lines
|
2318
|
-
n_statistics_header_line + n_worker_lines * n_workers + n_progress_lines
|
2319
|
-
end
|
2320
|
-
|
2321
|
-
def n_statistics_header_line
|
2322
|
-
1
|
2323
|
-
end
|
2324
|
-
|
2325
|
-
def n_worker_lines
|
2326
|
-
2
|
2327
|
-
end
|
2328
|
-
|
2329
|
-
def n_progress_lines
|
2330
|
-
1
|
2331
|
-
end
|
2332
|
-
|
2333
|
-
def n_workers
|
2334
|
-
@tester.n_workers
|
2335
|
-
end
|
2336
|
-
end
|
2337
364
|
end
|
2338
365
|
end
|