# -*- coding: utf-8 -*- # # Copyright (C) 2012-2015 Kouhei Sutou # # 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. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # 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 . require "optparse" require "pathname" require "grntest/version" require "grntest/test-suites-runner" module Grntest class Tester class << self def run(argv=nil) argv ||= ARGV.dup tester = new catch do |tag| parser = create_option_parser(tester, tag) targets = parser.parse!(argv) tester.run(*targets) end end private def create_option_parser(tester, tag) parser = OptionParser.new parser.banner += " TEST_FILE_OR_DIRECTORY..." parser.on("--groonga=COMMAND", "Use COMMAND as groonga command", "(#{tester.groonga})") do |command| tester.groonga = command end parser.on("--groonga-httpd=COMMAND", "Use COMMAND as groonga-httpd command for groonga-httpd tests", "(#{tester.groonga_httpd})") do |command| tester.groonga_httpd = command end parser.on("--groonga-suggest-create-dataset=COMMAND", "Use COMMAND as groonga_suggest_create_dataset command", "(#{tester.groonga_suggest_create_dataset})") do |command| tester.groonga_suggest_create_dataset = command end available_interfaces = [:stdio, :http] available_interface_labels = available_interfaces.join(", ") parser.on("--interface=INTERFACE", available_interfaces, "Use INTERFACE for communicating groonga", "[#{available_interface_labels}]", "(#{tester.interface})") do |interface| tester.interface = interface end available_output_types = ["json", "msgpack"] available_output_type_labels = available_output_types.join(", ") parser.on("--output-type=TYPE", available_output_types, "Use TYPE as the output type", "[#{available_output_type_labels}]", "(#{tester.output_type})") do |type| tester.output_type = type end available_testees = ["groonga", "groonga-httpd"] available_testee_labels = available_testees.join(", ") parser.on("--testee=TESTEE", available_testees, "Test against TESTEE", "[#{available_testee_labels}]", "(#{tester.testee})") do |testee| tester.testee = testee if tester.testee == "groonga-httpd" tester.interface = :http end end parser.on("--base-directory=DIRECTORY", "Use DIRECTORY as a base directory of relative path", "(#{tester.base_directory})") do |directory| tester.base_directory = Pathname(directory) end parser.on("--database=PATH", "Use existing database at PATH " + "instead of creating a new database", "(creating a new database)") do |path| tester.database_path = path end parser.on("--diff=DIFF", "Use DIFF as diff command", "(#{tester.diff})") do |diff| tester.diff = diff tester.diff_options.clear end diff_option_is_specified = false parser.on("--diff-option=OPTION", "Use OPTION as diff command", "(#{tester.diff_options.join(' ')})") do |option| tester.diff_options.clear if diff_option_is_specified tester.diff_options << option diff_option_is_specified = true end available_reporters = [:mark, :stream, :inplace] available_reporter_labels = available_reporters.join(", ") parser.on("--reporter=REPORTER", available_reporters, "Report test result by REPORTER", "[#{available_reporter_labels}]", "(auto)") do |reporter| tester.reporter = reporter end parser.on("--test=NAME", "Run only test that name is NAME", "If NAME is /.../, NAME is treated as regular expression", "This option can be used multiple times") do |name| tester.test_patterns << parse_name_or_pattern(name) end parser.on("--test-suite=NAME", "Run only test suite that name is NAME", "If NAME is /.../, NAME is treated as regular expression", "This option can be used multiple times") do |name| tester.test_suite_patterns << parse_name_or_pattern(name) end parser.on("--exclude-test=NAME", "Exclude test that name is NAME", "If NAME is /.../, NAME is treated as regular expression", "This option can be used multiple times") do |name| tester.exclude_test_patterns << parse_name_or_pattern(name) end parser.on("--exclude-test-suite=NAME", "Exclude test suite that name is NAME", "If NAME is /.../, NAME is treated as regular expression", "This option can be used multiple times") do |name| tester.exclude_test_suite_patterns << parse_name_or_pattern(name) end parser.on("--n-workers=N", Integer, "Use N workers to run tests") do |n| tester.n_workers = n end parser.on("--gdb[=COMMAND]", "Run groonga on gdb and use COMMAND as gdb", "(#{tester.default_gdb})") do |command| tester.gdb = command || tester.default_gdb end parser.on("--valgrind[=COMMAND]", "Run groonga on valgrind and use COMMAND as valgrind", "(#{tester.default_valgrind})") do |command| tester.valgrind = command || tester.default_valgrind end parser.on("--[no-]valgrind-gen-suppressions", "Generate suppressions for Valgrind", "(#{tester.valgrind_gen_suppressions?})") do |boolean| tester.valgrind_gen_suppressions = boolean end parser.on("--[no-]keep-database", "Keep used database for debug after test is finished", "(#{tester.keep_database?})") do |boolean| tester.keep_database = boolean end parser.on("--[no-]stop-on-failure", "Stop immediately on the first non success test", "(#{tester.stop_on_failure?})") do |boolean| tester.stop_on_failure = boolean end parser.on("--output=OUTPUT", "Output to OUTPUT", "(stdout)") do |output| tester.output = File.open(output, "w:ascii-8bit") end parser.on("--[no-]use-color", "Enable colorized output", "(auto)") do |use_color| tester.use_color = use_color end parser.on("--version", "Show version and exit") do puts(VERSION) throw(tag, true) end parser end def parse_name_or_pattern(name) if /\A\/(.+)\/\z/ =~ name Regexp.new($1, Regexp::IGNORECASE) else name end end end attr_accessor :groonga, :groonga_httpd, :groonga_suggest_create_dataset attr_accessor :interface, :output_type, :testee attr_accessor :base_directory, :database_path, :diff, :diff_options attr_accessor :n_workers attr_accessor :output attr_accessor :gdb, :default_gdb attr_accessor :valgrind, :default_valgrind attr_writer :valgrind_gen_suppressions attr_writer :reporter, :keep_database, :use_color attr_writer :stop_on_failure attr_reader :test_patterns, :test_suite_patterns attr_reader :exclude_test_patterns, :exclude_test_suite_patterns def initialize @groonga = "groonga" @groonga_httpd = "groonga-httpd" @groonga_suggest_create_dataset = "groonga-suggest-create-dataset" @interface = :stdio @output_type = "json" @testee = "groonga" @base_directory = Pathname(".") @database_path = nil @reporter = nil @n_workers = 1 @output = $stdout @keep_database = false @use_color = nil @stop_on_failure = false @test_patterns = [] @test_suite_patterns = [] @exclude_test_patterns = [] @exclude_test_suite_patterns = [] detect_suitable_diff initialize_debuggers initialize_memory_checkers end def run(*targets) succeeded = true return succeeded if targets.empty? test_suites = load_tests(*targets) run_test_suites(test_suites) end def reporter if @reporter.nil? if @n_workers == 1 :mark else :inplace end else @reporter end end def keep_database? @keep_database end def use_color? if @use_color.nil? @use_color = guess_color_availability end @use_color end def stop_on_failure? @stop_on_failure end def valgrind_gen_suppressions? @valgrind_gen_suppressions end def target_test?(test_name) selected_test?(test_name) and not excluded_test?(test_name) end def selected_test?(test_name) return true if @test_patterns.empty? @test_patterns.any? do |pattern| pattern === test_name end end def excluded_test?(test_name) @exclude_test_patterns.any? do |pattern| pattern === test_name end end def target_test_suite?(test_suite_name) selected_test_suite?(test_suite_name) and not excluded_test_suite?(test_suite_name) end def selected_test_suite?(test_suite_name) return true if @test_suite_patterns.empty? @test_suite_patterns.any? do |pattern| pattern === test_suite_name end end def excluded_test_suite?(test_suite_name) @exclude_test_suite_patterns.any? do |pattern| pattern === test_suite_name end end private def load_tests(*targets) default_group_name = "." tests = {default_group_name => []} targets.each do |target| target_path = Pathname(target) next unless target_path.exist? if target_path.directory? load_tests_under_directory(tests, target_path) else tests[default_group_name] << target_path end end tests end def load_tests_under_directory(tests, test_directory_path) test_file_paths = Pathname.glob(test_directory_path + "**" + "*.test") test_file_paths.each do |test_file_path| directory_path = test_file_path.dirname directory = directory_path.relative_path_from(test_directory_path).to_s tests[directory] ||= [] tests[directory] << test_file_path end end def run_test_suites(test_suites) runner = TestSuitesRunner.new(self) runner.run(test_suites) end def detect_suitable_diff if command_exist?("cut-diff") @diff = "cut-diff" @diff_options = ["--context-lines", "10"] else @diff = "diff" @diff_options = ["-u"] end end def initialize_debuggers @gdb = nil @default_gdb = "gdb" end def initialize_memory_checkers @vagrind = nil @default_valgrind = "valgrind" @vagrind_gen_suppressions = false end def command_exist?(name) ENV["PATH"].split(File::PATH_SEPARATOR).each do |path| absolute_path = File.join(path, name) return true if File.executable?(absolute_path) end false end def guess_color_availability return false unless @output.tty? case ENV["TERM"] when /term(?:-(?:256)?color)?\z/, "screen" true else return true if ENV["EMACS"] == "t" false end end end end