lib/grntest/tester.rb in grntest-1.0.0 vs lib/grntest/tester.rb in grntest-1.0.1

- old
+ new

@@ -120,10 +120,17 @@ "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 @@ -222,11 +229,11 @@ end end attr_accessor :groonga, :groonga_httpd, :groonga_suggest_create_dataset attr_accessor :interface, :output_type, :testee - attr_accessor :base_directory, :diff, :diff_options + attr_accessor :base_directory, :database_path, :diff, :diff_options attr_accessor :n_workers attr_accessor :output attr_accessor :gdb, :default_gdb attr_writer :reporter, :keep_database, :use_color attr_reader :test_patterns, :test_suite_patterns @@ -237,10 +244,11 @@ @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 @@ -286,11 +294,12 @@ def target_test?(test_name) selected_test?(test_name) and not excluded_test?(test_name) end def selected_test?(test_name) - @test_patterns.all? do |pattern| + return true if @test_patterns.empty? + @test_patterns.any? do |pattern| pattern === test_name end end def excluded_test?(test_name) @@ -303,11 +312,12 @@ selected_test_suite?(test_suite_name) and not excluded_test_suite?(test_suite_name) end def selected_test_suite?(test_suite_name) - @test_suite_patterns.all? do |pattern| + 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) @@ -394,16 +404,18 @@ @elapsed_time = Time.now - start_time end end class WorkerResult < Result - attr_reader :n_tests, :n_passed_tests, :n_not_checked_tests + attr_reader :n_tests, :n_passed_tests, :n_leaked_tests + attr_reader :n_not_checked_tests attr_reader :failed_tests def initialize super @n_tests = 0 @n_passed_tests = 0 + @n_leaked_tests = 0 @n_not_checked_tests = 0 @failed_tests = [] end def n_failed_tests @@ -420,10 +432,14 @@ def test_failed(name) @failed_tests << name end + def test_leaked(name) + @n_leaked_tests += 1 + end + def test_not_checked @n_not_checked_tests += 1 end end @@ -499,14 +515,20 @@ @status = "failed" @result.test_failed(test_name) @reporter.fail_test(self, result) end - def not_check_test(result) + def leaked_test(result) + @status = "leaked(#{result.n_leaked_objects})" + @result.test_leaked(test_name) + @reporter.leaked_test(self, result) + end + + def not_checked_test(result) @status = "not checked" @result.test_not_checked - @reporter.not_check_test(self, result) + @reporter.not_checked_test(self, result) end def finish_test(result) @result.test_finished @reporter.finish_test(self, result) @@ -543,10 +565,14 @@ def n_failed_tests collect_count(:n_failed_tests) end + def n_leaked_tests + collect_count(:n_leaked_tests) + end + def n_not_checked_tests collect_count(:n_not_checked_tests) end private @@ -631,28 +657,37 @@ end end class TestResult < Result attr_accessor :worker_id, :test_name - attr_accessor :expected, :actual + attr_accessor :expected, :actual, :n_leaked_objects def initialize(worker) super() @worker_id = worker.id @test_name = worker.test_name @actual = nil @expected = nil + @n_leaked_objects = 0 end def status if @expected if @actual == @expected - :success + if @n_leaked_objects.zero? + :success + else + :leaked + end else :failure end else - :not_checked + if @n_leaked_objects.zero? + :not_checked + else + :leaked + end end end end class TestRunner @@ -671,45 +706,53 @@ @worker.start_test result = TestResult.new(@worker) result.measure do result.actual = execute_groonga_script end - result.actual = normalize_result(result.actual) + normalize_actual_result(result) result.expected = read_expected_result case result.status when :success @worker.pass_test(result) remove_reject_file when :failure @worker.fail_test(result) output_reject_file(result.actual) succeeded = false + when :leaked + @worker.leaked_test(result) + succeeded = false else - @worker.not_check_test(result) + @worker.not_checked_test(result) output_actual_file(result.actual) end @worker.finish_test(result) succeeded end private def execute_groonga_script create_temporary_directory do |directory_path| - db_dir = directory_path + "db" - FileUtils.mkdir_p(db_dir.to_s) - db_path = db_dir + "db" + 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) context.result end end def create_temporary_directory @@ -732,10 +775,14 @@ 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 + case @tester.interface when :stdio run_groonga_stdio(context, &block) when :http run_groonga_http(context, &block) @@ -758,11 +805,10 @@ } command_line = groonga_command_line(context, spawn_options) command_line += [ "--input-fd", input_fd.to_s, "--output-fd", output_fd.to_s, - "-n", context.relative_db_path.to_s, ] pid = Process.spawn(env, *command_line, spawn_options) executor = StandardIOExecutor.new(groonga_input, groonga_output, @@ -888,11 +934,10 @@ "--pid-path", pid_file_path.to_s, "--bind-address", host, "--port", port.to_s, "--protocol", "http", "-s", - "-n", context.relative_db_path.to_s, ] when "groonga-httpd" command_line = command_command_line(@tester.groonga_httpd, context, spawn_options) @@ -905,11 +950,10 @@ end command_line end def create_config_file(context, host, port, pid_file_path) - create_empty_database(context.db_path.to_s) config_file_path = context.temporary_directory_path + "groonga-httpd.conf" config_file_path.open("w") do |config_file| config_file.puts(<<EOF) daemon off; @@ -948,23 +992,25 @@ ] system(*create_database_command) output_fd.close(true) end - def normalize_result(result) + def normalize_actual_result(result) normalized_result = "" - result.each do |tag, content, options| + 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 - normalized_result + result.actual = normalized_result end def normalize_raw_content(content) "#{content}\n".force_encoding("ASCII-8BIT") end @@ -1067,10 +1113,21 @@ 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 @@ -1179,35 +1236,83 @@ end end def execute_line(line) case line + when /\A\#@/ + directive_content = $POSTMATCH + execute_directive(line, directive_content) when /\A\s*\z/ # do nothing when /\A\s*\#/ - comment_content = $POSTMATCH - execute_comment(comment_content) + # ignore comment else execute_command_line(line) end end - def execute_comment(content) + 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(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" - dataset_name = options.first - return if dataset_name.nil? - execute_suggest_create_dataset(dataset_name) + execute_directive_suggest_create_dataset(line, content, options) when "include" - path = options.first - return if path.nil? - execute_script(Pathname(path)) + execute_directive_include(line, content, options) + when "copy-path" + execute_directive_copy_path(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, @@ -1225,14 +1330,11 @@ end end def execute_script(script_path) executor = create_sub_executor(@context) - if script_path.relative? - script_path = @context.base_directory + script_path - end - executor.execute(script_path) + executor.execute(resolve_path(script_path)) end def execute_command_line(command_line) extract_command_info(command_line) log_input(command_line) @@ -1511,34 +1613,49 @@ 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 statistics_header + items = [ + "tests/sec", + "tests", + "passes", + "failures", + "leaked", + "!checked", + ] + " " + ((["%-9s"] * items.size).join(" | ") % items) + " |" + end + def statistics(result) items = [ - "#{result.n_tests} tests", - "#{result.n_passed_tests} passes", - "#{result.n_failed_tests} failures", - "#{result.n_not_checked_tests} not checked_tests", + "%9.2f" % throughput(result), + "%9d" % result.n_tests, + "%9d" % result.n_passed_tests, + "%9d" % result.n_failed_tests, + "%9d" % result.n_leaked_tests, + "%9d" % result.n_not_checked_tests, ] - "#{throughput(result)}: " + items.join(", ") + " " + 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 - "%.2f tests/sec" % tests_per_second + tests_per_second end def report_failure(result) report_marker(result) report_diff(result.expected, result.actual) @@ -1557,12 +1674,12 @@ 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", "(actual)", actual_file.path, - "--label", "(expected)", expected_file.path]) + diff_options.concat(["--label", "(expected)", expected_file.path, + "--label", "(actual)", actual_file.path]) system(@tester.diff, *diff_options) end end end @@ -1667,10 +1784,12 @@ 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_not_checked_tests > 0 :not_checked else :success end @@ -1687,10 +1806,12 @@ 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 :not_checked "%s%s%s" % [not_checked_color, message, reset_color] else message end @@ -1720,10 +1841,23 @@ :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 not_checked_color escape_sequence({ :color => :cyan, :color_256 => [0, 1, 1], :background => true, @@ -1821,12 +1955,18 @@ report_test(worker, result) report_failure(result) end end - def not_check_test(worker, result) + def leaked_test(worker, result) synchronize do + report_test_result_mark("L(#{result.n_leaked_objects})", result) + end + end + + def not_checked_test(worker, result) + synchronize do report_test_result_mark("N", result) puts report_test(worker, result) report_actual(result) end @@ -1847,10 +1987,13 @@ 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 @@ -1890,12 +2033,16 @@ def fail_test(worker, result) report_test_result(result, worker.status) report_failure(result) end - def not_check_test(worker, result) + def leaked_test(worker, result) report_test_result(result, worker.status) + end + + def not_checked_test(worker, result) + report_test_result(result, worker.status) report_actual(result) end def finish_test(worker, result) end @@ -1943,13 +2090,20 @@ report_test(worker, result) report_failure(result) end end - def not_check_test(worker, result) + def leaked_test(worker, result) redraw do report_test(worker, result) + report_marker(result) + end + end + + def not_checked_test(worker, result) + redraw do + report_test(worker, result) report_actual(result) end end def finish_test(worker, result) @@ -1970,17 +2124,22 @@ 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 @@ -1992,19 +2151,23 @@ def draw_test_line(worker) clear_line if worker.test_name label = " #{worker.test_name}" else - label = " #{statistics(worker.result)}" + 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 - finished_test_ratio = n_done_tests.to_f / 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) @@ -2052,10 +2215,14 @@ print("\r") reset_current_column end def n_using_lines - n_worker_lines * n_workers + n_progress_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