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

- old
+ new

@@ -20,15 +20,16 @@ require "pathname" require "fileutils" require "tempfile" require "shellwords" require "open-uri" -require "cgi/util" require "json" require "msgpack" +require "groonga/command" + require "grntest/version" module Grntest class Tester class Error < StandardError @@ -405,42 +406,47 @@ end end class WorkerResult < Result attr_reader :n_tests, :n_passed_tests, :n_leaked_tests - attr_reader :n_not_checked_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 test_finished + def on_test_finish @n_tests += 1 end - def test_passed + def on_test_success @n_passed_tests += 1 end - def test_failed(name) + def on_test_failure(name) @failed_tests << name end - def test_leaked(name) + def on_test_leak(name) @n_leaked_tests += 1 end - def test_not_checked + def on_test_omission + @n_omitted_tests += 1 + end + + def on_test_no_check @n_not_checked_tests += 1 end end class Worker @@ -469,71 +475,77 @@ def run(queue) succeeded = true @result.measure do - @reporter.start_worker(self) + @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.finish_suite(self) if @suite_name + @reporter.on_suite_finish(self) if @suite_name @suite_name = suite_name - @reporter.start_suite(self) + @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.finish_suite(@suite_name) if @suite_name + @reporter.on_suite_finish(@suite_name) if @suite_name @suite_name = nil end end - @reporter.finish_worker(self) + @reporter.on_worker_finish(self) succeeded end - def start_test + def on_test_start @status = "running" @test_result = nil - @reporter.start_test(self) + @reporter.on_test_start(self) end - def pass_test(result) + def on_test_success(result) @status = "passed" - @result.test_passed - @reporter.pass_test(self, result) + @result.on_test_success + @reporter.on_test_success(self, result) end - def fail_test(result) + def on_test_failure(result) @status = "failed" - @result.test_failed(test_name) - @reporter.fail_test(self, result) + @result.on_test_failure(test_name) + @reporter.on_test_failure(self, result) end - def leaked_test(result) + def on_test_leak(result) @status = "leaked(#{result.n_leaked_objects})" - @result.test_leaked(test_name) - @reporter.leaked_test(self, result) + @result.on_test_leak(test_name) + @reporter.on_test_leak(self, result) end - def not_checked_test(result) + 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.test_not_checked - @reporter.not_checked_test(self, result) + @result.on_test_no_check + @reporter.on_test_no_check(self, result) end - def finish_test(result) - @result.test_finished - @reporter.finish_test(self, result) + def on_test_finish(result) + @result.on_test_finish + @reporter.on_test_finish(self, result) @test_script_path = nil @test_name = nil end end @@ -569,10 +581,14 @@ 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 @@ -595,11 +611,11 @@ succeeded = true @result.measure do succeeded = run_test_suites(test_suites) end - @reporter.finish(@result) + @reporter.on_finish(@result) succeeded end private @@ -621,11 +637,11 @@ workers = [] @tester.n_workers.times do |i| workers << Worker.new(i, @tester, @result, @reporter) end @result.workers = workers - @reporter.start(@result) + @reporter.on_start(@result) succeeded = true worker_threads = [] @tester.n_workers.times do |i| worker = workers[i] @@ -658,20 +674,24 @@ 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 @@ -686,12 +706,57 @@ 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 @@ -701,39 +766,41 @@ end def run succeeded = true - @worker.start_test + @worker.on_test_start result = TestResult.new(@worker) result.measure do - result.actual = execute_groonga_script + execute_groonga_script(result) end normalize_actual_result(result) result.expected = read_expected_result case result.status when :success - @worker.pass_test(result) + @worker.on_test_success(result) remove_reject_file when :failure - @worker.fail_test(result) + @worker.on_test_failure(result) output_reject_file(result.actual) succeeded = false when :leaked - @worker.leaked_test(result) + @worker.on_test_leak(result) succeeded = false + when :omitted + @worker.on_test_omission(result) else - @worker.not_checked_test(result) + @worker.on_test_no_check(result) output_actual_file(result.actual) end - @worker.finish_test(result) + @worker.on_test_finish(result) succeeded end private - def execute_groonga_script + 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" @@ -749,11 +816,12 @@ context.output_type = @tester.output_type run_groonga(context) do |executor| executor.execute(test_script_path) end check_memory_leak(context) - context.result + result.omitted = context.omitted? + result.actual = context.result end end def create_temporary_directory path = "tmp/grntest" @@ -779,15 +847,18 @@ 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) + 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 @@ -1020,11 +1091,11 @@ case type when "json", "msgpack" status = nil values = nil begin - status, *values = parse_result(content.chomp, type) + status, *values = ResponseParser.parse(content.chomp, type) rescue ParseError return $!.message end normalized_status = normalize_status(status) normalized_output_content = [normalized_status, *values] @@ -1036,29 +1107,10 @@ else normalize_raw_content(content) end end - def parse_result(result, type) - 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 - 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] @@ -1133,20 +1185,25 @@ 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 @@ -1171,87 +1228,93 @@ 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 - if @loading - execute_line_on_loading(line) - else - execute_line_with_continuation_line_support(line) - end - rescue Error + parser << line + rescue Error, Groonga::Command::ParseError line_info = "#{script_path}:#{script_file.lineno}:#{line.chomp}" log_error("#{line_info}: #{$!.message}") - raise unless @context.top_level? + 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 execute_line_on_loading(line) - log_input(line) - @pending_load_command << line - if line == "]\n" - execute_command(@pending_load_command) - @pending_load_command = nil - @loading = false + def create_parser + parser = Groonga::Command::Parser.new + parser.on_command do |command| + execute_command(command) end - end - - def execute_line_with_continuation_line_support(line) - if /\\$/ =~ line - @pending_command << $PREMATCH - else - if @pending_command.empty? - execute_line(line) - else - @pending_command << line - execute_line(@pending_command) - @pending_command = "" + 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 execute_line(line) - case line - when /\A\#@/ - directive_content = $POSTMATCH - execute_directive(line, directive_content) - when /\A\s*\z/ - # do nothing - when /\A\s*\#/ - # ignore comment - else - execute_command_line(line) - end - end - def resolve_path(path) if path.relative? @context.base_directory + path else path @@ -1293,10 +1356,58 @@ 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 @@ -1306,10 +1417,16 @@ 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 @@ -1333,50 +1450,28 @@ def execute_script(script_path) executor = create_sub_executor(@context) executor.execute(resolve_path(script_path)) end - def execute_command_line(command_line) - extract_command_info(command_line) - log_input(command_line) - if multiline_load_command? - @loading = true - @pending_load_command = command_line.dup - else - execute_command(command_line) - end - end - - def extract_command_info(command_line) - @current_command, *@current_arguments = Shellwords.split(command_line) - if @current_command == "dump" + def extract_command_info(command) + @current_command = command + if @current_command.name == "dump" @output_type = "groonga-command" else - @output_type = @context.output_type - @current_arguments.each_with_index do |word, i| - if /\A--output_type(?:=(.+))?\z/ =~ word - @output_type = $1 || words[i + 1] - break - end - end + @output_type = @current_command[:output_type] || @context.output_type end end - def have_output_type_argument? - @current_arguments.any? do |argument| - /\A--output_type(?:=.+)?\z/ =~ argument - end - end - - def multiline_load_command? - @current_command == "load" and - not @current_arguments.include?("--values") - end - def execute_command(command) - log_output(send_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 = "" @@ -1394,12 +1489,14 @@ content = "" first_timeout = options[:first_timeout] || 1 timeout = first_timeout while IO.select([output], [], [], timeout) break if output.eof? - content << output.readpartial(65535) - timeout = 0 + 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) @@ -1408,10 +1505,22 @@ 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 @@ -1433,25 +1542,31 @@ 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_line) - unless have_output_type_argument? + 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 @@ -1468,12 +1583,22 @@ self.class.new(@input, @output, context) end private def read_output - read_all_readable_content(@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) @@ -1515,89 +1640,13 @@ def initialize(gqtp_command) @gqtp_command = gqtp_command end def to_url - command = nil - arguments = nil - load_values = "" - @gqtp_command.each_line.with_index do |line, i| - if i.zero? - command, *arguments = Shellwords.split(line) - else - load_values << line - end - end - arguments.concat(["--values", load_values]) unless load_values.empty? - - named_arguments = convert_to_named_arguments(command, arguments) - build_url(command, named_arguments) + command = Groonga::Command::Parser.parse(@gqtp_command) + command.to_uri_format end - - private - def convert_to_named_arguments(command, arguments) - named_arguments = {} - - last_argument_name = nil - n_non_named_arguments = 0 - arguments.each do |argument| - if /\A--/ =~ argument - last_argument_name = $POSTMATCH - next - end - - if last_argument_name.nil? - argument_name = arguments_name(command)[n_non_named_arguments] - n_non_named_arguments += 1 - else - argument_name = last_argument_name - last_argument_name = nil - end - - named_arguments[argument_name] = argument - end - - named_arguments - end - - def arguments_name(command) - case command - when "table_create" - ["name", "flags", "key_type", "value_type", "default_tokenizer"] - when "column_create" - ["table", "name", "flags", "type", "source"] - when "load" - ["values", "table", "columns", "ifexists", "input_type"] - when "select" - ["table"] - when "suggest" - [ - "types", "table", "column", "query", "sortby", - "output_columns", "offset", "limit", "frequency_threshold", - "conditional_probability_threshold", "prefix_search" - ] - when "truncate" - ["table"] - when "get" - ["table", "key", "output_columns", "id"] - else - nil - end - end - - def build_url(command, named_arguments) - url = "/d/#{command}" - query_parameters = [] - named_arguments.each do |name, argument| - query_parameters << "#{CGI.escape(name)}=#{CGI.escape(argument)}" - end - unless query_parameters.empty? - url << "?" - url << query_parameters.join("&") - end - url - end end class BaseReporter def initialize(tester) @tester = tester @@ -1621,31 +1670,34 @@ 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", + 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}], ] - " " + ((["%-9s"] * items.size).join(" | ") % items) + " |" end + def statistics_header + labels = columns.collect do |label, format_value| + label + end + " " + labels.join(" | ") + " |" + end + def statistics(result) - items = [ - "%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, - ] + items = columns.collect do |label, format_value| + format_value.call(result) + end " " + items.join(" | ") + " |" end def throughput(result) if result.elapsed_time.zero? @@ -1762,22 +1814,30 @@ def guess_term_width_from_env ENV["COLUMNS"] || ENV["TERM_WIDTH"] end def guess_term_width_from_stty - case `stty -a` + return nil unless STDIN.tty? + + case tty_info when /(\d+) columns/ $1 when /columns (\d+)/ $1 else nil end - rescue SystemCallError - nil 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) @@ -1786,10 +1846,12 @@ 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 @@ -1808,10 +1870,12 @@ "%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 @@ -1854,10 +1918,23 @@ :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, @@ -1928,62 +2005,71 @@ class MarkReporter < BaseReporter def initialize(tester) super end - def start(result) + def on_start(result) end - def start_worker(worker) + def on_worker_start(worker) end - def start_suite(worker) + def on_suite_start(worker) end - def start_test(worker) + def on_test_start(worker) end - def pass_test(worker, result) + def on_test_success(worker, result) synchronize do report_test_result_mark(".", result) end end - def fail_test(worker, result) + 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 leaked_test(worker, result) + def on_test_leak(worker, result) synchronize do report_test_result_mark("L(#{result.n_leaked_objects})", result) end end - def not_checked_test(worker, result) + 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 finish_test(worker, result) + def on_test_finish(worker, result) end - def finish_suite(worker) + def on_suite_finish(worker) end - def finish_worker(worker_id) + def on_worker_finish(worker_id) end - def finish(result) + def on_finish(result) puts puts report_summary(result) end @@ -2004,58 +2090,63 @@ class StreamReporter < BaseReporter def initialize(tester) super end - def start(result) + def on_start(result) end - def start_worker(worker) + def on_worker_start(worker) end - def start_suite(worker) + 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 start_test(worker) + def on_test_start(worker) print(" #{worker.test_name}") @output.flush end - def pass_test(worker, result) + def on_test_success(worker, result) report_test_result(result, worker.status) end - def fail_test(worker, result) + def on_test_failure(worker, result) report_test_result(result, worker.status) report_failure(result) end - def leaked_test(worker, result) + def on_test_leak(worker, result) report_test_result(result, worker.status) end - def not_checked_test(worker, result) + def on_test_omission(worker, result) report_test_result(result, worker.status) report_actual(result) end - def finish_test(worker, result) + def on_test_no_check(worker, result) + report_test_result(result, worker.status) + report_actual(result) end - def finish_suite(worker) + def on_test_finish(worker, result) end - def finish_worker(worker_id) + def on_suite_finish(worker) end - def finish(result) + def on_worker_finish(worker_id) + end + + def on_finish(result) puts report_summary(result) end end @@ -2064,62 +2155,69 @@ super @last_redraw_time = Time.now @minimum_redraw_interval = 0.1 end - def start(result) + def on_start(result) @test_suites_result = result end - def start_worker(worker) + def on_worker_start(worker) end - def start_suite(worker) + def on_suite_start(worker) redraw end - def start_test(worker) + def on_test_start(worker) redraw end - def pass_test(worker, result) + def on_test_success(worker, result) redraw end - def fail_test(worker, result) + def on_test_failure(worker, result) redraw do report_test(worker, result) report_failure(result) end end - def leaked_test(worker, result) + def on_test_leak(worker, result) redraw do report_test(worker, result) report_marker(result) end end - def not_checked_test(worker, result) + def on_test_omission(worker, result) redraw do report_test(worker, result) report_actual(result) end end - def finish_test(worker, result) + 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 finish_suite(worker) + def on_suite_finish(worker) redraw end - def finish_worker(worker) + def on_worker_finish(worker) redraw end - def finish(result) + def on_finish(result) draw puts report_summary(result) end