lib/grntest/tester.rb in grntest-1.0.2 vs lib/grntest/tester.rb in grntest-1.0.3
- old
+ new
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
#
-# Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
+# Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
@@ -13,48 +13,18 @@
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-require "English"
require "optparse"
require "pathname"
-require "fileutils"
-require "tempfile"
-require "shellwords"
-require "open-uri"
-require "json"
-require "msgpack"
-
-require "groonga/command"
-
require "grntest/version"
+require "grntest/test-suites-runner"
module Grntest
class Tester
- class Error < StandardError
- end
-
- class NotExist < Error
- attr_reader :path
- def initialize(path)
- @path = path
- super("<#{path}> doesn't exist.")
- end
- end
-
- class ParseError < Error
- attr_reader :type, :content, :reason
- def initialize(type, content, reason)
- @type = type
- @content = content
- @reason = reason
- super("failed to parse <#{@type}> content: #{reason}: <#{content}>")
- end
- end
-
class << self
def run(argv=nil)
argv ||= ARGV.dup
tester = new
catch do |tag|
@@ -387,1952 +357,9 @@
when /term(?:-(?:256)?color)?\z/, "screen"
true
else
return true if ENV["EMACS"] == "t"
false
- end
- end
-
- class Result
- attr_accessor :elapsed_time
- def initialize
- @elapsed_time = 0
- end
-
- def measure
- start_time = Time.now
- yield
- ensure
- @elapsed_time = Time.now - start_time
- end
- end
-
- class WorkerResult < Result
- attr_reader :n_tests, :n_passed_tests, :n_leaked_tests
- attr_reader :n_omitted_tests, :n_not_checked_tests
- attr_reader :failed_tests
- def initialize
- super
- @n_tests = 0
- @n_passed_tests = 0
- @n_leaked_tests = 0
- @n_omitted_tests = 0
- @n_not_checked_tests = 0
- @failed_tests = []
- end
-
- def n_failed_tests
- @failed_tests.size
- end
-
- def on_test_finish
- @n_tests += 1
- end
-
- def on_test_success
- @n_passed_tests += 1
- end
-
- def on_test_failure(name)
- @failed_tests << name
- end
-
- def on_test_leak(name)
- @n_leaked_tests += 1
- end
-
- def on_test_omission
- @n_omitted_tests += 1
- end
-
- def on_test_no_check
- @n_not_checked_tests += 1
- end
- end
-
- class Worker
- attr_reader :id, :tester, :test_suites_rusult, :reporter
- attr_reader :suite_name, :test_script_path, :test_name, :status, :result
- def initialize(id, tester, test_suites_result, reporter)
- @id = id
- @tester = tester
- @test_suites_result = test_suites_result
- @reporter = reporter
- @suite_name = nil
- @test_script_path = nil
- @test_name = nil
- @interruptted = false
- @status = "not running"
- @result = WorkerResult.new
- end
-
- def interrupt
- @interruptted = true
- end
-
- def interruptted?
- @interruptted
- end
-
- def run(queue)
- succeeded = true
-
- @result.measure do
- @reporter.on_worker_start(self)
- catch do |tag|
- loop do
- suite_name, test_script_path, test_name = queue.pop
- break if test_script_path.nil?
-
- unless @suite_name == suite_name
- @reporter.on_suite_finish(self) if @suite_name
- @suite_name = suite_name
- @reporter.on_suite_start(self)
- end
- @test_script_path = test_script_path
- @test_name = test_name
- runner = TestRunner.new(@tester, self)
- succeeded = false unless runner.run
-
- break if interruptted?
- end
- @status = "finished"
- @reporter.on_suite_finish(@suite_name) if @suite_name
- @suite_name = nil
- end
- end
- @reporter.on_worker_finish(self)
-
- succeeded
- end
-
- def on_test_start
- @status = "running"
- @test_result = nil
- @reporter.on_test_start(self)
- end
-
- def on_test_success(result)
- @status = "passed"
- @result.on_test_success
- @reporter.on_test_success(self, result)
- end
-
- def on_test_failure(result)
- @status = "failed"
- @result.on_test_failure(test_name)
- @reporter.on_test_failure(self, result)
- end
-
- def on_test_leak(result)
- @status = "leaked(#{result.n_leaked_objects})"
- @result.on_test_leak(test_name)
- @reporter.on_test_leak(self, result)
- end
-
- def on_test_omission(result)
- @status = "omitted"
- @result.on_test_omission
- @reporter.on_test_omission(self, result)
- end
-
- def on_test_no_check(result)
- @status = "not checked"
- @result.on_test_no_check
- @reporter.on_test_no_check(self, result)
- end
-
- def on_test_finish(result)
- @result.on_test_finish
- @reporter.on_test_finish(self, result)
- @test_script_path = nil
- @test_name = nil
- end
- end
-
- class TestSuitesResult < Result
- attr_accessor :workers
- attr_accessor :n_total_tests
- def initialize
- super
- @workers = []
- @n_total_tests = 0
- end
-
- def pass_ratio
- n_target_tests = n_tests - n_not_checked_tests
- if n_target_tests.zero?
- 0
- else
- (n_passed_tests / n_target_tests.to_f) * 100
- end
- end
-
- def n_tests
- collect_count(:n_tests)
- end
-
- def n_passed_tests
- collect_count(:n_passed_tests)
- end
-
- def n_failed_tests
- collect_count(:n_failed_tests)
- end
-
- def n_leaked_tests
- collect_count(:n_leaked_tests)
- end
-
- def n_omitted_tests
- collect_count(:n_omitted_tests)
- end
-
- def n_not_checked_tests
- collect_count(:n_not_checked_tests)
- end
-
- private
- def collect_count(item)
- counts = @workers.collect do |worker|
- worker.result.send(item)
- end
- counts.inject(&:+)
- end
- end
-
- class TestSuitesRunner
- def initialize(tester)
- @tester = tester
- @reporter = create_reporter
- @result = TestSuitesResult.new
- end
-
- def run(test_suites)
- succeeded = true
-
- @result.measure do
- succeeded = run_test_suites(test_suites)
- end
- @reporter.on_finish(@result)
-
- succeeded
- end
-
- private
- def run_test_suites(test_suites)
- queue = Queue.new
- test_suites.each do |suite_name, test_script_paths|
- next unless @tester.target_test_suite?(suite_name)
- test_script_paths.each do |test_script_path|
- test_name = test_script_path.basename(".*").to_s
- next unless @tester.target_test?(test_name)
- queue << [suite_name, test_script_path, test_name]
- @result.n_total_tests += 1
- end
- end
- @tester.n_workers.times do
- queue << nil
- end
-
- workers = []
- @tester.n_workers.times do |i|
- workers << Worker.new(i, @tester, @result, @reporter)
- end
- @result.workers = workers
- @reporter.on_start(@result)
-
- succeeded = true
- worker_threads = []
- @tester.n_workers.times do |i|
- worker = workers[i]
- worker_threads << Thread.new do
- succeeded = false unless worker.run(queue)
- end
- end
-
- begin
- worker_threads.each(&:join)
- rescue Interrupt
- workers.each do |worker|
- worker.interrupt
- end
- end
-
- succeeded
- end
-
- def create_reporter
- case @tester.reporter
- when :mark
- MarkReporter.new(@tester)
- when :stream
- StreamReporter.new(@tester)
- when :inplace
- InplaceReporter.new(@tester)
- end
- end
- end
-
- class TestResult < Result
- attr_accessor :worker_id, :test_name
- attr_accessor :expected, :actual, :n_leaked_objects
- attr_writer :omitted
- def initialize(worker)
- super()
- @worker_id = worker.id
- @test_name = worker.test_name
- @actual = nil
- @expected = nil
- @n_leaked_objects = 0
- @omitted = false
- end
-
- def status
- return :omitted if omitted?
-
- if @expected
- if @actual == @expected
- if @n_leaked_objects.zero?
- :success
- else
- :leaked
- end
- else
- :failure
- end
- else
- if @n_leaked_objects.zero?
- :not_checked
- else
- :leaked
- end
- end
- end
-
- def omitted?
- @omitted
- end
- end
-
- class ResponseParser
- class << self
- def parse(content, type)
- parser = new(type)
- parser.parse(content)
- end
- end
-
- def initialize(type)
- @type = type
- end
-
- def parse(content)
- case @type
- when "json", "msgpack"
- parse_result(content.chomp)
- else
- content
- end
- end
-
- def parse_result(result)
- case @type
- when "json"
- begin
- JSON.parse(result)
- rescue JSON::ParserError
- raise ParseError.new(@type, result, $!.message)
- end
- when "msgpack"
- begin
- MessagePack.unpack(result.chomp)
- rescue MessagePack::UnpackError, NoMemoryError
- raise ParseError.new(@type, result, $!.message)
- end
- else
- raise ParseError.new(@type, result, "unknown type")
- end
- end
- end
-
- class TestRunner
- MAX_N_COLUMNS = 79
-
- def initialize(tester, worker)
- @tester = tester
- @worker = worker
- @max_n_columns = MAX_N_COLUMNS
- @id = nil
- end
-
- def run
- succeeded = true
-
- @worker.on_test_start
- result = TestResult.new(@worker)
- result.measure do
- execute_groonga_script(result)
- end
- normalize_actual_result(result)
- result.expected = read_expected_result
- case result.status
- when :success
- @worker.on_test_success(result)
- remove_reject_file
- when :failure
- @worker.on_test_failure(result)
- output_reject_file(result.actual)
- succeeded = false
- when :leaked
- @worker.on_test_leak(result)
- succeeded = false
- when :omitted
- @worker.on_test_omission(result)
- else
- @worker.on_test_no_check(result)
- output_actual_file(result.actual)
- end
- @worker.on_test_finish(result)
-
- succeeded
- end
-
- private
- def execute_groonga_script(result)
- create_temporary_directory do |directory_path|
- if @tester.database_path
- db_path = Pathname(@tester.database_path).expand_path
- else
- db_dir = directory_path + "db"
- FileUtils.mkdir_p(db_dir.to_s)
- db_path = db_dir + "db"
- end
- context = Executor::Context.new
- context.temporary_directory_path = directory_path
- context.db_path = db_path
- context.base_directory = @tester.base_directory.expand_path
- context.groonga_suggest_create_dataset =
- @tester.groonga_suggest_create_dataset
- context.output_type = @tester.output_type
- run_groonga(context) do |executor|
- executor.execute(test_script_path)
- end
- check_memory_leak(context)
- result.omitted = context.omitted?
- result.actual = context.result
- end
- end
-
- def create_temporary_directory
- path = "tmp/grntest"
- path << ".#{@worker.id}" if @tester.n_workers > 1
- FileUtils.rm_rf(path, :secure => true)
- FileUtils.mkdir_p(path)
- begin
- yield(Pathname(path).expand_path)
- ensure
- if @tester.keep_database? and File.exist?(path)
- FileUtils.rm_rf(keep_database_path, :secure => true)
- FileUtils.mv(path, keep_database_path)
- else
- FileUtils.rm_rf(path, :secure => true)
- end
- end
- end
-
- def keep_database_path
- test_script_path.to_s.gsub(/\//, ".")
- end
-
- def run_groonga(context, &block)
- unless @tester.database_path
- create_empty_database(context.db_path.to_s)
- end
-
- catch do |tag|
- context.abort_tag = tag
- case @tester.interface
- when :stdio
- run_groonga_stdio(context, &block)
- when :http
- run_groonga_http(context, &block)
- end
- end
- end
-
- def run_groonga_stdio(context)
- pid = nil
- begin
- open_pipe do |input_read, input_write, output_read, output_write|
- groonga_input = input_write
- groonga_output = output_read
-
- input_fd = input_read.to_i
- output_fd = output_write.to_i
- env = {}
- spawn_options = {
- input_fd => input_fd,
- output_fd => output_fd
- }
- command_line = groonga_command_line(context, spawn_options)
- command_line += [
- "--input-fd", input_fd.to_s,
- "--output-fd", output_fd.to_s,
- context.relative_db_path.to_s,
- ]
- pid = Process.spawn(env, *command_line, spawn_options)
- executor = StandardIOExecutor.new(groonga_input,
- groonga_output,
- context)
- executor.ensure_groonga_ready
- yield(executor)
- end
- ensure
- Process.waitpid(pid) if pid
- end
- end
-
- def open_pipe
- IO.pipe("ASCII-8BIT") do |input_read, input_write|
- IO.pipe("ASCII-8BIT") do |output_read, output_write|
- yield(input_read, input_write, output_read, output_write)
- end
- end
- end
-
- def command_command_line(command, context, spawn_options)
- command_line = []
- if @tester.gdb
- if libtool_wrapper?(command)
- command_line << find_libtool(command)
- command_line << "--mode=execute"
- end
- command_line << @tester.gdb
- gdb_command_path = context.temporary_directory_path + "groonga.gdb"
- File.open(gdb_command_path, "w") do |gdb_command|
- gdb_command.puts(<<-EOC)
-break main
-run
-print chdir("#{context.temporary_directory_path}")
-EOC
- end
- command_line << "--command=#{gdb_command_path}"
- command_line << "--quiet"
- command_line << "--args"
- else
- spawn_options[:chdir] = context.temporary_directory_path.to_s
- end
- command_line << command
- command_line
- end
-
- def groonga_command_line(context, spawn_options)
- command_line = command_command_line(@tester.groonga, context,
- spawn_options)
- command_line << "--log-path=#{context.log_path}"
- command_line << "--working-directory=#{context.temporary_directory_path}"
- command_line
- end
-
- def libtool_wrapper?(command)
- return false unless File.exist?(command)
- File.open(command, "r") do |command_file|
- first_line = command_file.gets
- first_line.start_with?("#!")
- end
- end
-
- def find_libtool(command)
- command_path = Pathname.new(command)
- directory = command_path.dirname
- until directory.root?
- libtool = directory + "libtool"
- return libtool.to_s if libtool.executable?
- directory = directory.parent
- end
- "libtool"
- end
-
- def run_groonga_http(context)
- host = "127.0.0.1"
- port = 50041 + @worker.id
- pid_file_path = context.temporary_directory_path + "groonga.pid"
-
- env = {}
- spawn_options = {}
- command_line = groonga_http_command(host, port, pid_file_path, context,
- spawn_options)
- pid = nil
- begin
- pid = Process.spawn(env, *command_line, spawn_options)
- begin
- executor = HTTPExecutor.new(host, port, context)
- begin
- executor.ensure_groonga_ready
- rescue
- if Process.waitpid(pid, Process::WNOHANG)
- pid = nil
- raise
- end
- raise unless @tester.gdb
- retry
- end
- yield(executor)
- ensure
- executor.send_command("shutdown")
- wait_groonga_http_shutdown(pid_file_path)
- end
- ensure
- Process.waitpid(pid) if pid
- end
- end
-
- def wait_groonga_http_shutdown(pid_file_path)
- total_sleep_time = 0
- sleep_time = 0.1
- while pid_file_path.exist?
- sleep(sleep_time)
- total_sleep_time += sleep_time
- break if total_sleep_time > 1.0
- end
- end
-
- def groonga_http_command(host, port, pid_file_path, context, spawn_options)
- case @tester.testee
- when "groonga"
- command_line = groonga_command_line(context, spawn_options)
- command_line += [
- "--pid-path", pid_file_path.to_s,
- "--bind-address", host,
- "--port", port.to_s,
- "--protocol", "http",
- "-s",
- context.relative_db_path.to_s,
- ]
- when "groonga-httpd"
- command_line = command_command_line(@tester.groonga_httpd, context,
- spawn_options)
- config_file_path = create_config_file(context, host, port,
- pid_file_path)
- command_line += [
- "-c", config_file_path.to_s,
- "-p", "#{context.temporary_directory_path}/",
- ]
- end
- command_line
- end
-
- def create_config_file(context, host, port, pid_file_path)
- config_file_path =
- context.temporary_directory_path + "groonga-httpd.conf"
- config_file_path.open("w") do |config_file|
- config_file.puts(<<EOF)
-daemon off;
-master_process off;
-worker_processes 1;
-working_directory #{context.temporary_directory_path};
-error_log groonga-httpd-access.log;
-pid #{pid_file_path};
-events {
- worker_connections 1024;
-}
-
-http {
- server {
- access_log groonga-httpd-access.log;
- listen #{port};
- server_name #{host};
- location /d/ {
- groonga_database #{context.relative_db_path};
- groonga on;
- }
- }
-}
-EOF
- end
- config_file_path
- end
-
- def create_empty_database(db_path)
- output_fd = Tempfile.new("create-empty-database")
- create_database_command = [
- @tester.groonga,
- "--output-fd", output_fd.to_i.to_s,
- "-n", db_path,
- "shutdown"
- ]
- system(*create_database_command)
- output_fd.close(true)
- end
-
- def normalize_actual_result(result)
- normalized_result = ""
- result.actual.each do |tag, content, options|
- case tag
- when :input
- normalized_result << content
- when :output
- normalized_result << normalize_output(content, options)
- when :error
- normalized_result << normalize_raw_content(content)
- when :n_leaked_objects
- result.n_leaked_objects = content
- end
- end
- result.actual = normalized_result
- end
-
- def normalize_raw_content(content)
- "#{content}\n".force_encoding("ASCII-8BIT")
- end
-
- def normalize_output(content, options)
- type = options[:type]
- case type
- when "json", "msgpack"
- status = nil
- values = nil
- begin
- status, *values = ResponseParser.parse(content.chomp, type)
- rescue ParseError
- return $!.message
- end
- normalized_status = normalize_status(status)
- normalized_output_content = [normalized_status, *values]
- normalized_output = JSON.generate(normalized_output_content)
- if normalized_output.bytesize > @max_n_columns
- normalized_output = JSON.pretty_generate(normalized_output_content)
- end
- normalize_raw_content(normalized_output)
- else
- normalize_raw_content(content)
- end
- end
-
- def normalize_status(status)
- return_code, started_time, elapsed_time, *rest = status
- _ = started_time = elapsed_time # for suppress warnings
- if return_code.zero?
- [0, 0.0, 0.0]
- else
- message, backtrace = rest
- _ = backtrace # for suppress warnings
- [[return_code, 0.0, 0.0], message]
- end
- end
-
- def test_script_path
- @worker.test_script_path
- end
-
- def have_extension?
- not test_script_path.extname.empty?
- end
-
- def related_file_path(extension)
- path = Pathname(test_script_path.to_s.gsub(/\.[^.]+\z/, ".#{extension}"))
- return nil if test_script_path == path
- path
- end
-
- def read_expected_result
- return nil unless have_extension?
- result_path = related_file_path("expected")
- return nil if result_path.nil?
- return nil unless result_path.exist?
- result_path.open("r:ascii-8bit") do |result_file|
- result_file.read
- end
- end
-
- def remove_reject_file
- return unless have_extension?
- reject_path = related_file_path("reject")
- return if reject_path.nil?
- FileUtils.rm_rf(reject_path.to_s, :secure => true)
- end
-
- def output_reject_file(actual_result)
- output_actual_result(actual_result, "reject")
- end
-
- def output_actual_file(actual_result)
- output_actual_result(actual_result, "actual")
- end
-
- def output_actual_result(actual_result, suffix)
- result_path = related_file_path(suffix)
- return if result_path.nil?
- result_path.open("w:ascii-8bit") do |result_file|
- result_file.print(actual_result)
- end
- end
-
- def check_memory_leak(context)
- context.log.each_line do |line|
- timestamp, log_level, message = line.split(/\|\s*/, 3)
- _ = timestamp # suppress warning
- next unless /^grn_fin \((\d+)\)$/ =~ message
- n_leaked_objects = $1.to_i
- next if n_leaked_objects.zero?
- context.result << [:n_leaked_objects, n_leaked_objects, {}]
- end
- end
- end
-
- class Executor
- class Context
- attr_writer :logging
- attr_accessor :base_directory, :temporary_directory_path, :db_path
- attr_accessor :groonga_suggest_create_dataset
- attr_accessor :result
- attr_accessor :output_type
- attr_accessor :on_error
- attr_accessor :abort_tag
- def initialize
- @logging = true
- @base_directory = Pathname(".")
- @temporary_directory_path = Pathname("tmp")
- @db_path = Pathname("db")
- @groonga_suggest_create_dataset = "groonga-suggest-create-dataset"
- @n_nested = 0
- @result = []
- @output_type = "json"
- @log = nil
- @on_error = :default
- @abort_tag = nil
- @omitted = false
- end
-
- def logging?
- @logging
- end
-
- def execute
- @n_nested += 1
- yield
- ensure
- @n_nested -= 1
- end
-
- def top_level?
- @n_nested == 1
- end
-
- def log_path
- @temporary_directory_path + "groonga.log"
- end
-
- def log
- @log ||= File.open(log_path.to_s, "a+")
- end
-
- def relative_db_path
- @db_path.relative_path_from(@temporary_directory_path)
- end
-
- def omitted?
- @omitted
- end
-
- def error
- case @on_error
- when :omit
- omit
- end
- end
-
- def omit
- @omitted = true
- abort
- end
-
- def abort
- throw @abort_tag
- end
- end
-
- module ReturnCode
- SUCCESS = 0
- end
-
- attr_reader :context
- def initialize(context=nil)
- @loading = false
- @pending_command = ""
- @pending_load_command = nil
- @current_command_name = nil
- @output_type = nil
- @long_timeout = default_long_timeout
- @context = context || Context.new
- end
-
- def execute(script_path)
- unless script_path.exist?
- raise NotExist.new(script_path)
- end
-
- @context.execute do
- script_path.open("r:ascii-8bit") do |script_file|
- parser = create_parser
- script_file.each_line do |line|
- begin
- parser << line
- rescue Error, Groonga::Command::ParseError
- line_info = "#{script_path}:#{script_file.lineno}:#{line.chomp}"
- log_error("#{line_info}: #{$!.message}")
- if $!.is_a?(Groonga::Command::ParseError)
- @context.abort
- else
- log_error("#{line_info}: #{$!.message}")
- raise unless @context.top_level?
- end
- end
- end
- end
- end
-
- @context.result
- end
-
- private
- def create_parser
- parser = Groonga::Command::Parser.new
- parser.on_command do |command|
- execute_command(command)
- end
- parser.on_load_complete do |command|
- execute_command(command)
- end
- parser.on_comment do |comment|
- if /\A@/ =~ comment
- directive_content = $POSTMATCH
- execute_directive("\##{comment}", directive_content)
- end
- end
- parser
- end
-
- def resolve_path(path)
- if path.relative?
- @context.base_directory + path
- else
- path
- end
- end
-
- def execute_directive_suggest_create_dataset(line, content, options)
- dataset_name = options.first
- if dataset_name.nil?
- log_input(line)
- log_error("#|e| [suggest-create-dataset] dataset name is missing")
- return
- end
- execute_suggest_create_dataset(dataset_name)
- end
-
- def execute_directive_include(line, content, options)
- path = options.first
- if path.nil?
- log_input(line)
- log_error("#|e| [include] path is missing")
- return
- end
- execute_script(Pathname(path))
- end
-
- def execute_directive_copy_path(line, content, options)
- source, destination, = options
- if source.nil? or destination.nil?
- log_input(line)
- if source.nil?
- log_error("#|e| [copy-path] source is missing")
- end
- if destiantion.nil?
- log_error("#|e| [copy-path] destination is missing")
- end
- return
- end
- source = resolve_path(Pathname(source))
- destination = resolve_path(Pathname(destination))
- FileUtils.cp_r(source.to_s, destination.to_s)
- end
-
- def execute_directive_long_timeout(line, content, options)
- long_timeout, = options
- invalid_value_p = false
- case long_timeout
- when "default"
- @long_timeout = default_long_timeout
- when nil
- invalid_value_p = true
- else
- begin
- @long_timeout = Float(long_timeout)
- rescue ArgumentError
- invalid_value_p = true
- end
- end
-
- if invalid_value_p
- log_input(line)
- message = "long-timeout must be number or 'default': <#{long_timeout}>"
- log_error("#|e| [long-timeout] #{message}")
- end
- end
-
- def execute_directive_on_error(line, content, options)
- action, = options
- invalid_value_p = false
- valid_actions = ["default", "omit"]
- if valid_actions.include?(action)
- @context.on_error = action.to_sym
- else
- invalid_value_p = true
- end
-
- if invalid_value_p
- log_input(line)
- valid_actions_label = "[#{valid_actions.join(', ')}]"
- message = "on-error must be one of #{valid_actions_label}"
- log_error("#|e| [on-error] #{message}: <#{action}>")
- end
- end
-
- def execute_directive_omit(line, content, options)
- reason, = options
- @output_type = "raw"
- log_output("omit: #{reason}")
- @context.omit
- end
-
- def execute_directive(line, content)
- command, *options = Shellwords.split(content)
- case command
- when "disable-logging"
- @context.logging = false
- when "enable-logging"
- @context.logging = true
- when "suggest-create-dataset"
- execute_directive_suggest_create_dataset(line, content, options)
- when "include"
- execute_directive_include(line, content, options)
- when "copy-path"
- execute_directive_copy_path(line, content, options)
- when "long-timeout"
- execute_directive_long_timeout(line, content, options)
- when "on-error"
- execute_directive_on_error(line, content, options)
- when "omit"
- execute_directive_omit(line, content, options)
- else
- log_input(line)
- log_error("#|e| unknown directive: <#{command}>")
- end
- end
-
- def execute_suggest_create_dataset(dataset_name)
- command_line = [@context.groonga_suggest_create_dataset,
- @context.db_path.to_s,
- dataset_name]
- packed_command_line = command_line.join(" ")
- log_input("#{packed_command_line}\n")
- begin
- IO.popen(command_line, "r:ascii-8bit") do |io|
- log_output(io.read)
- end
- rescue SystemCallError
- raise Error.new("failed to run groonga-suggest-create-dataset: " +
- "<#{packed_command_line}>: #{$!}")
- end
- end
-
- def execute_script(script_path)
- executor = create_sub_executor(@context)
- executor.execute(resolve_path(script_path))
- end
-
- def extract_command_info(command)
- @current_command = command
- if @current_command.name == "dump"
- @output_type = "groonga-command"
- else
- @output_type = @current_command[:output_type] || @context.output_type
- end
- end
-
- def execute_command(command)
- extract_command_info(command)
- log_input("#{command.original_source}\n")
- response = send_command(command)
- type = @output_type
- log_output(response)
- log_error(read_error_log)
-
- @context.error if error_response?(response, type)
- end
-
- def read_error_log
- log = read_all_readable_content(context.log, :first_timeout => 0)
- normalized_error_log = ""
- log.each_line do |line|
- timestamp, log_level, message = line.split(/\|\s*/, 3)
- _ = timestamp # suppress warning
- next unless error_log_level?(log_level)
- next if backtrace_log_message?(message)
- normalized_error_log << "\#|#{log_level}| #{message}"
- end
- normalized_error_log.chomp
- end
-
- def read_all_readable_content(output, options={})
- content = ""
- first_timeout = options[:first_timeout] || 1
- timeout = first_timeout
- while IO.select([output], [], [], timeout)
- break if output.eof?
- request_bytes = 1024
- read_content = output.readpartial(request_bytes)
- content << read_content
- timeout = 0 if read_content.bytesize < request_bytes
- end
- content
- end
-
- def error_log_level?(log_level)
- ["E", "A", "C", "e"].include?(log_level)
- end
-
- def backtrace_log_message?(message)
- message.start_with?("/")
- end
-
- def error_response?(response, type)
- status = nil
- begin
- status, = ResponseParser.parse(response, type)
- rescue ParseError
- return false
- end
-
- return_code, = status
- return_code != ReturnCode::SUCCESS
- end
-
- def log(tag, content, options={})
- return unless @context.logging?
- log_force(tag, content, options)
- end
-
- def log_force(tag, content, options)
- return if content.empty?
- @context.result << [tag, content, options]
- end
-
- def log_input(content)
- log(:input, content)
- end
-
- def log_output(content)
- log(:output, content,
- :command => @current_command,
- :type => @output_type)
- @current_command = nil
- @output_type = nil
- end
-
- def log_error(content)
- log_force(:error, content, {})
- end
-
- def default_long_timeout
- 180
- end
- end
-
- class StandardIOExecutor < Executor
- def initialize(input, output, context=nil)
- super(context)
- @input = input
- @output = output
- end
-
- def send_command(command)
- command_line = @current_command.original_source
- unless @current_command.has_key?(:output_type)
- command_line = command_line.sub(/$/, " --output_type #{@output_type}")
- end
- begin
- @input.print(command_line)
- @input.print("\n")
- @input.flush
- rescue SystemCallError
- message = "failed to write to groonga: <#{command_line}>: #{$!}"
- raise Error.new(message)
- end
- read_output
- end
-
- def ensure_groonga_ready
- @input.print("status\n")
- @input.flush
- @output.gets
- end
-
- def create_sub_executor(context)
- self.class.new(@input, @output, context)
- end
-
- private
- def read_output
- options = {}
- options[:first_timeout] = @long_timeout if may_slow_command?
- read_all_readable_content(@output, options)
- end
-
- MAY_SLOW_COMMANDS = [
- "column_create",
- "register",
- ]
- def may_slow_command?
- MAY_SLOW_COMMANDS.include?(@current_command)
- end
- end
-
- class HTTPExecutor < Executor
- def initialize(host, port, context=nil)
- super(context)
- @host = host
- @port = port
- end
-
- def send_command(command_line)
- converter = CommandFormatConverter.new(command_line)
- url = "http://#{@host}:#{@port}#{converter.to_url}"
- begin
- open(url) do |response|
- "#{response.read}\n"
- end
- rescue OpenURI::HTTPError
- message = "Failed to get response from groonga: #{$!}: <#{url}>"
- raise Error.new(message)
- end
- end
-
- def ensure_groonga_ready
- n_retried = 0
- begin
- send_command("status")
- rescue SystemCallError
- n_retried += 1
- sleep(0.1)
- retry if n_retried < 10
- raise
- end
- end
-
- def create_sub_executor(context)
- self.class.new(@host, @port, context)
- end
- end
-
- class CommandFormatConverter
- def initialize(gqtp_command)
- @gqtp_command = gqtp_command
- end
-
- def to_url
- command = Groonga::Command::Parser.parse(@gqtp_command)
- command.to_uri_format
- end
- end
-
- class BaseReporter
- def initialize(tester)
- @tester = tester
- @term_width = guess_term_width
- @output = @tester.output
- @mutex = Mutex.new
- reset_current_column
- end
-
- private
- def synchronize
- @mutex.synchronize do
- yield
- end
- end
-
- def report_summary(result)
- puts(statistics_header)
- puts(colorize(statistics(result), result))
- pass_ratio = result.pass_ratio
- elapsed_time = result.elapsed_time
- summary = "%.4g%% passed in %.4fs." % [pass_ratio, elapsed_time]
- puts(colorize(summary, result))
- end
-
- def columns
- [
- # label, format value
- ["tests/sec", lambda {|result| "%9.2f" % throughput(result)}],
- [" tests", lambda {|result| "%8d" % result.n_tests}],
- [" passes", lambda {|result| "%8d" % result.n_passed_tests}],
- ["failures", lambda {|result| "%8d" % result.n_failed_tests}],
- [" leaked", lambda {|result| "%8d" % result.n_leaked_tests}],
- [" omitted", lambda {|result| "%8d" % result.n_omitted_tests}],
- ["!checked", lambda {|result| "%8d" % result.n_not_checked_tests}],
- ]
- end
-
- def statistics_header
- labels = columns.collect do |label, format_value|
- label
- end
- " " + labels.join(" | ") + " |"
- end
-
- def statistics(result)
- items = columns.collect do |label, format_value|
- format_value.call(result)
- end
- " " + items.join(" | ") + " |"
- end
-
- def throughput(result)
- if result.elapsed_time.zero?
- tests_per_second = 0
- else
- tests_per_second = result.n_tests / result.elapsed_time
- end
- tests_per_second
- end
-
- def report_failure(result)
- report_marker(result)
- report_diff(result.expected, result.actual)
- report_marker(result)
- end
-
- def report_actual(result)
- report_marker(result)
- puts(result.actual)
- report_marker(result)
- end
-
- def report_marker(result)
- puts(colorize("=" * @term_width, result))
- end
-
- def report_diff(expected, actual)
- create_temporary_file("expected", expected) do |expected_file|
- create_temporary_file("actual", actual) do |actual_file|
- diff_options = @tester.diff_options.dup
- diff_options.concat(["--label", "(expected)", expected_file.path,
- "--label", "(actual)", actual_file.path])
- system(@tester.diff, *diff_options)
- end
- end
- end
-
- def report_test(worker, result)
- report_marker(result)
- print("[#{worker.id}] ") if @tester.n_workers > 1
- puts(worker.suite_name)
- print(" #{worker.test_name}")
- report_test_result(result, worker.status)
- end
-
- def report_test_result(result, label)
- message = test_result_message(result, label)
- message_width = string_width(message)
- rest_width = @term_width - @current_column
- if rest_width > message_width
- print(" " * (rest_width - message_width))
- end
- puts(message)
- end
-
- def test_result_message(result, label)
- elapsed_time = result.elapsed_time
- formatted_elapsed_time = "%.4fs" % elapsed_time
- formatted_elapsed_time = colorize(formatted_elapsed_time,
- elapsed_time_status(elapsed_time))
- " #{formatted_elapsed_time} [#{colorize(label, result)}]"
- end
-
- LONG_ELAPSED_TIME = 1.0
- def long_elapsed_time?(elapsed_time)
- elapsed_time >= LONG_ELAPSED_TIME
- end
-
- def elapsed_time_status(elapsed_time)
- if long_elapsed_time?(elapsed_time)
- elapsed_time_status = :failure
- else
- elapsed_time_status = :not_checked
- end
- end
-
- def justify(message, width)
- return " " * width if message.nil?
- return message.ljust(width) if message.bytesize <= width
- half_width = width / 2.0
- elision_mark = "..."
- left = message[0, half_width.ceil - elision_mark.size]
- right = message[(message.size - half_width.floor)..-1]
- "#{left}#{elision_mark}#{right}"
- end
-
- def print(message)
- @current_column += string_width(message.to_s)
- @output.print(message)
- end
-
- def puts(*messages)
- reset_current_column
- @output.puts(*messages)
- end
-
- def reset_current_column
- @current_column = 0
- end
-
- def create_temporary_file(key, content)
- file = Tempfile.new("groonga-test-#{key}")
- file.print(content)
- file.close
- yield(file)
- end
-
- def guess_term_width
- Integer(guess_term_width_from_env || guess_term_width_from_stty || 79)
- rescue ArgumentError
- 0
- end
-
- def guess_term_width_from_env
- ENV["COLUMNS"] || ENV["TERM_WIDTH"]
- end
-
- def guess_term_width_from_stty
- return nil unless STDIN.tty?
-
- case tty_info
- when /(\d+) columns/
- $1
- when /columns (\d+)/
- $1
- else
- nil
- end
- end
-
- def tty_info
- begin
- `stty -a`
- rescue SystemCallError
- nil
- end
- end
-
- def string_width(string)
- string.gsub(/\e\[[0-9;]+m/, "").size
- end
-
- def result_status(result)
- if result.respond_to?(:status)
- result.status
- else
- if result.n_failed_tests > 0
- :failure
- elsif result.n_leaked_tests > 0
- :leaked
- elsif result.n_omitted_tests > 0
- :omitted
- elsif result.n_not_checked_tests > 0
- :not_checked
- else
- :success
- end
- end
- end
-
- def colorize(message, result_or_status)
- return message unless @tester.use_color?
- if result_or_status.is_a?(Symbol)
- status = result_or_status
- else
- status = result_status(result_or_status)
- end
- case status
- when :success
- "%s%s%s" % [success_color, message, reset_color]
- when :failure
- "%s%s%s" % [failure_color, message, reset_color]
- when :leaked
- "%s%s%s" % [leaked_color, message, reset_color]
- when :omitted
- "%s%s%s" % [omitted_color, message, reset_color]
- when :not_checked
- "%s%s%s" % [not_checked_color, message, reset_color]
- else
- message
- end
- end
-
- def success_color
- escape_sequence({
- :color => :green,
- :color_256 => [0, 3, 0],
- :background => true,
- },
- {
- :color => :white,
- :color_256 => [5, 5, 5],
- :bold => true,
- })
- end
-
- def failure_color
- escape_sequence({
- :color => :red,
- :color_256 => [3, 0, 0],
- :background => true,
- },
- {
- :color => :white,
- :color_256 => [5, 5, 5],
- :bold => true,
- })
- end
-
- def leaked_color
- escape_sequence({
- :color => :magenta,
- :color_256 => [3, 0, 3],
- :background => true,
- },
- {
- :color => :white,
- :color_256 => [5, 5, 5],
- :bold => true,
- })
- end
-
- def omitted_color
- escape_sequence({
- :color => :blue,
- :color_256 => [0, 0, 1],
- :background => true,
- },
- {
- :color => :white,
- :color_256 => [5, 5, 5],
- :bold => true,
- })
- end
-
- def not_checked_color
- escape_sequence({
- :color => :cyan,
- :color_256 => [0, 1, 1],
- :background => true,
- },
- {
- :color => :white,
- :color_256 => [5, 5, 5],
- :bold => true,
- })
- end
-
- def reset_color
- escape_sequence(:reset)
- end
-
- COLOR_NAMES = [
- :black, :red, :green, :yellow,
- :blue, :magenta, :cyan, :white,
- ]
- def escape_sequence(*commands)
- sequence = []
- commands.each do |command|
- case command
- when :reset
- sequence << "0"
- when :bold
- sequence << "1"
- when :italic
- sequence << "3"
- when :underline
- sequence << "4"
- when Hash
- foreground_p = !command[:background]
- if available_colors == 256
- sequence << (foreground_p ? "38" : "48")
- sequence << "5"
- sequence << pack_256_color(*command[:color_256])
- else
- color_parameter = foreground_p ? 3 : 4
- color_parameter += 6 if command[:intensity]
- color = COLOR_NAMES.index(command[:color])
- sequence << "#{color_parameter}#{color}"
- end
- end
- end
- "\e[#{sequence.join(';')}m"
- end
-
- def pack_256_color(red, green, blue)
- red * 36 + green * 6 + blue + 16
- end
-
- def available_colors
- case ENV["COLORTERM"]
- when "gnome-terminal"
- 256
- else
- case ENV["TERM"]
- when /-256color\z/
- 256
- else
- 8
- end
- end
- end
- end
-
- class MarkReporter < BaseReporter
- def initialize(tester)
- super
- end
-
- def on_start(result)
- end
-
- def on_worker_start(worker)
- end
-
- def on_suite_start(worker)
- end
-
- def on_test_start(worker)
- end
-
- def on_test_success(worker, result)
- synchronize do
- report_test_result_mark(".", result)
- end
- end
-
- def on_test_failure(worker, result)
- synchronize do
- report_test_result_mark("F", result)
- puts
- report_test(worker, result)
- report_failure(result)
- end
- end
-
- def on_test_leak(worker, result)
- synchronize do
- report_test_result_mark("L(#{result.n_leaked_objects})", result)
- end
- end
-
- def on_test_omission(worker, result)
- synchronize do
- report_test_result_mark("O", result)
- puts
- report_test(worker, result)
- report_actual(result)
- end
- end
-
- def on_test_no_check(worker, result)
- synchronize do
- report_test_result_mark("N", result)
- puts
- report_test(worker, result)
- report_actual(result)
- end
- end
-
- def on_test_finish(worker, result)
- end
-
- def on_suite_finish(worker)
- end
-
- def on_worker_finish(worker_id)
- end
-
- def on_finish(result)
- puts
- puts
- report_summary(result)
- end
-
- private
- def report_test_result_mark(mark, result)
- if @term_width < @current_column + mark.bytesize
- puts
- end
- print(colorize(mark, result))
- if @term_width <= @current_column
- puts
- else
- @output.flush
- end
- end
- end
-
- class StreamReporter < BaseReporter
- def initialize(tester)
- super
- end
-
- def on_start(result)
- end
-
- def on_worker_start(worker)
- end
-
- def on_suite_start(worker)
- if worker.suite_name.bytesize <= @term_width
- puts(worker.suite_name)
- else
- puts(justify(worker.suite_name, @term_width))
- end
- @output.flush
- end
-
- def on_test_start(worker)
- print(" #{worker.test_name}")
- @output.flush
- end
-
- def on_test_success(worker, result)
- report_test_result(result, worker.status)
- end
-
- def on_test_failure(worker, result)
- report_test_result(result, worker.status)
- report_failure(result)
- end
-
- def on_test_leak(worker, result)
- report_test_result(result, worker.status)
- end
-
- def on_test_omission(worker, result)
- report_test_result(result, worker.status)
- report_actual(result)
- end
-
- def on_test_no_check(worker, result)
- report_test_result(result, worker.status)
- report_actual(result)
- end
-
- def on_test_finish(worker, result)
- end
-
- def on_suite_finish(worker)
- end
-
- def on_worker_finish(worker_id)
- end
-
- def on_finish(result)
- puts
- report_summary(result)
- end
- end
-
- class InplaceReporter < BaseReporter
- def initialize(tester)
- super
- @last_redraw_time = Time.now
- @minimum_redraw_interval = 0.1
- end
-
- def on_start(result)
- @test_suites_result = result
- end
-
- def on_worker_start(worker)
- end
-
- def on_suite_start(worker)
- redraw
- end
-
- def on_test_start(worker)
- redraw
- end
-
- def on_test_success(worker, result)
- redraw
- end
-
- def on_test_failure(worker, result)
- redraw do
- report_test(worker, result)
- report_failure(result)
- end
- end
-
- def on_test_leak(worker, result)
- redraw do
- report_test(worker, result)
- report_marker(result)
- end
- end
-
- def on_test_omission(worker, result)
- redraw do
- report_test(worker, result)
- report_actual(result)
- end
- end
-
- def on_test_no_check(worker, result)
- redraw do
- report_test(worker, result)
- report_actual(result)
- end
- end
-
- def on_test_finish(worker, result)
- redraw
- end
-
- def on_suite_finish(worker)
- redraw
- end
-
- def on_worker_finish(worker)
- redraw
- end
-
- def on_finish(result)
- draw
- puts
- report_summary(result)
- end
-
- private
- def draw
- draw_statistics_header_line
- @test_suites_result.workers.each do |worker|
- draw_status_line(worker)
- draw_test_line(worker)
- end
- draw_progress_line
- end
-
- def draw_statistics_header_line
- puts(statistics_header)
- end
-
- def draw_status_line(worker)
- clear_line
- left = "[#{colorize(worker.id, worker.result)}] "
- right = " [#{worker.status}]"
- rest_width = @term_width - @current_column
- center_width = rest_width - string_width(left) - string_width(right)
- center = justify(worker.suite_name, center_width)
- puts("#{left}#{center}#{right}")
- end
-
- def draw_test_line(worker)
- clear_line
- if worker.test_name
- label = " #{worker.test_name}"
- else
- label = statistics(worker.result)
- end
- puts(justify(label, @term_width))
- end
-
- def draw_progress_line
- n_done_tests = @test_suites_result.n_tests
- n_total_tests = @test_suites_result.n_total_tests
- if n_total_tests.zero?
- finished_test_ratio = 0.0
- else
- finished_test_ratio = n_done_tests.to_f / n_total_tests
- end
-
- start_mark = "|"
- finish_mark = "|"
- statistics = " [%3d%%]" % (finished_test_ratio * 100)
-
- progress_width = @term_width
- progress_width -= start_mark.bytesize
- progress_width -= finish_mark.bytesize
- progress_width -= statistics.bytesize
- finished_mark = "-"
- if n_done_tests == n_total_tests
- progress = colorize(finished_mark * progress_width,
- @test_suites_result)
- else
- current_mark = ">"
- finished_marks_width = (progress_width * finished_test_ratio).ceil
- finished_marks_width -= current_mark.bytesize
- finished_marks_width = [0, finished_marks_width].max
- progress = finished_mark * finished_marks_width + current_mark
- progress = colorize(progress, @test_suites_result)
- progress << " " * (progress_width - string_width(progress))
- end
- puts("#{start_mark}#{progress}#{finish_mark}#{statistics}")
- end
-
- def redraw
- synchronize do
- unless block_given?
- return if Time.now - @last_redraw_time < @minimum_redraw_interval
- end
- draw
- if block_given?
- yield
- else
- up_n_lines(n_using_lines)
- end
- @last_redraw_time = Time.now
- end
- end
-
- def up_n_lines(n)
- print("\e[1A" * n)
- end
-
- def clear_line
- print(" " * @term_width)
- print("\r")
- reset_current_column
- end
-
- def n_using_lines
- n_statistics_header_line + n_worker_lines * n_workers + n_progress_lines
- end
-
- def n_statistics_header_line
- 1
- end
-
- def n_worker_lines
- 2
- end
-
- def n_progress_lines
- 1
- end
-
- def n_workers
- @tester.n_workers
end
end
end
end