grntest 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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