require "optparse" require 'chronic_duration' ## # :include: README.txt module Minitest VERSION = '5.0.14' # :nodoc: DATE = '2013-08-07' # :nodoc: @@installed_at_exit ||= false @@after_run = [] @extensions = [] mc = (class << self; self; end) ## # Filter object for backtraces. mc.send :attr_accessor, :backtrace_filter ## # Reporter object to be used for all runs. # # NOTE: This accessor is only available during setup, not during runs. mc.send :attr_accessor, :reporter ## # Names of known extension plugins. mc.send :attr_accessor, :extensions @@trace_set = false mc.send :attr_accessor, :trace_set ## # Registers Minitest to run at process exit def self.autorun at_exit { next if $! and not $!.kind_of? SystemExit exit_code = nil at_exit { @@after_run.reverse_each(&:call) exit exit_code || false } exit_code = Minitest.run ARGV } unless @@installed_at_exit @@installed_at_exit = true end ## # A simple hook allowing you to run a block of code after everything # is done running. Eg: # # Minitest.after_run { p $debugging_info } def self.after_run &block @@after_run << block end def self.init_plugins options # :nodoc: self.extensions.each do |name| msg = "plugin_#{name}_init" send msg, options if self.respond_to? msg end end def self.load_plugins # :nodoc: return unless self.extensions.empty? seen = {} Gem.find_files("minitest/*_plugin.rb").each do |plugin_path| name = File.basename plugin_path, "_plugin.rb" next if seen[name] seen[name] = true require plugin_path self.extensions << name end end ## # This is the top-level run method. Everything starts from here. It # tells each Runnable sub-class to run, and each of those are # responsible for doing whatever they do. # # The overall structure of a run looks like this: # # Minitest.autorun # Minitest.run(args) # __run(reporter, options) # Runnable.runnables.each # runnable.run(reporter, options) # self.runnable_methods.each # self.new(runnable_method).run def self.run args = [] self.load_plugins options = process_args args reporter = CompositeReporter.new reporter << ProgressReporter.new(options[:io], options) reporter << SummaryReporter.new(options[:io], options) self.reporter = reporter # this makes it available to plugins self.init_plugins options self.reporter = nil # runnables shouldn't depend on the reporter, ever reporter.start __run reporter, options reporter.report reporter.passed? end ## # Internal run method. Responsible for telling all Runnable # sub-classes to run. # # NOTE: this method is redefined in parallel_each.rb, which is # loaded if a Runnable calls parallelize_me!. def self.__run reporter, options Runnable.runnables.each do |runnable| runnable.run reporter, options end end ## # Trace file source to :io (default $stdout) # # spec_opts = {} # # @param :trace [Array] the files to trace # @param :io [IO] io to print to def self.trace_specs spec_opts # ensure trace is set once return if trace_set self.trace_set = true targets = [] files = {} last_file = '' last_line = -1 files_to_trace = spec_opts.fetch(:trace, []); io = spec_opts.fetch(:io, $stdout) color = spec_opts.fetch(:color, "\e[32m") # ANSI.green default # target only existing readable files files_to_trace.each { |f| targets.push(f) if File.exists?(f) && File .readable?(f) } return if targets.empty? set_trace_func(lambda do |event, file, line, id, binding, classname| return unless targets.include? file # never repeat a line return if file == last_file && line == last_line file_sym = file.intern files[file_sym] = IO.readlines(file) if files[file_sym].nil? lines = files[file_sym] # arrays are 0 indexed and line numbers start at one. io.print color if color # ANSI code io.puts lines[ line - 1] io.print "\e[0m" if color # ANSI.clear last_file = file last_line = line end) end ## # Run specs. Does not print dots (ProgressReporter) # # spec_opts # @param :io [Array] defaults to $stdout # @param :trace [Array] files to trace def self.run_specs spec_opts={} options = { :io => spec_opts.fetch(:io, $stdout) } reporter = Minitest::CompositeReporter.new reporter << Minitest::SummaryReporter.new(options[:io], options) reporter.start trace_specs spec_opts begin Minitest.__run reporter, options reporter.reporters.each { |r| r.report } rescue Minitest::Runnable::ExitAfterFirstFail # Minitest calls .report on exception end end def self.process_args args = [] # :nodoc: options = { :io => $stdout } orig_args = args.dup OptionParser.new do |opts| opts.banner = "minitest options:" opts.version = Minitest::VERSION opts.on "-h", "--help", "Display this help." do puts opts exit end opts.on "-s", "--seed SEED", Integer, "Sets random seed" do |m| options[:seed] = m.to_i end opts.on "-v", "--verbose", "Verbose. Show progress processing files." do options[:verbose] = true end opts.on "-n", "--name PATTERN","Filter run on /pattern/ or string." do |a| options[:filter] = a end unless extensions.empty? opts.separator "" opts.separator "Known extensions: #{extensions.join(', ')}" extensions.each do |meth| msg = "plugin_#{meth}_options" send msg, opts, options if self.respond_to?(msg) end end begin opts.parse! args rescue OptionParser::InvalidOption => e puts puts e puts puts opts exit 1 end orig_args -= args end unless options[:seed] then srand options[:seed] = srand % 0xFFFF orig_args << "--seed" << options[:seed].to_s end srand options[:seed] options[:args] = orig_args.map { |s| s =~ /[\s|&<>$()]/ ? s.inspect : s }.join " " options end def self.filter_backtrace bt # :nodoc: backtrace_filter.filter bt end ## # Represents anything "runnable", like Test, Spec, Benchmark, or # whatever you can dream up. # # Subclasses of this are automatically registered and available in # Runnable.runnables. class Runnable ## # Number of assertions executed in this run. attr_accessor :assertions ## # An assertion raised during the run, if any. attr_accessor :failures ## # Name of the run. def name @NAME end ## # Set the name of the run. def name= o @NAME = o end def self.inherited klass # :nodoc: self.runnables << klass super end ## # Returns all instance methods matching the pattern +re+. def self.methods_matching re public_instance_methods(true).grep(re).map(&:to_s) end def self.reset # :nodoc: @@runnables = [] end reset class ExitAfterFirstFail < RuntimeError; end def self.check_failures result, reporter if !result.failures.empty? begin reporter.reporters.each { |r| r.report } ensure raise ExitAfterFirstFail end end end ## # Responsible for running all runnable methods in a given class, # each in its own instance. Each instance is passed to the # reporter to record. def self.run reporter, options = {} filter = options[:filter] || '/./' filter = Regexp.new $1 if filter =~ /\/(.*)\// filtered_methods = self.runnable_methods.find_all { |m| filter === m || filter === "#{self}##{m}" } begin # before_first method1 = self.new(filtered_methods.first) # run method and capture exceptions. method1.capture_exceptions do method1.before_first_method end # save exceptions to reporter and check for failures with_info_handler reporter do # only record if failures not empty, otherwise # the count (runs) are messed up. each call to .record # increases count by one. if !method1.failures.empty? reporter.record method1 check_failures method1, reporter end # run the other methods filtered_methods.each do |method_name| result = self.new(method_name).run raise "#{self}#run _must_ return self" unless self === result reporter.record result check_failures result, reporter end end ensure # ensure after last runs # after_last # init method1 again method1 = self.new(filtered_methods.first) method1.capture_exceptions do method1.after_last_method end with_info_handler reporter do if !method1.failures.empty? reporter.record method1 check_failures method1, reporter end end end end def before_first_method; end def after_last_method; end def self.with_info_handler reporter, &block # :nodoc: handler = lambda do unless reporter.passed? then warn "Current results:" warn "" warn reporter.reporters.first warn "" end end on_signal "INFO", handler, &block end def self.on_signal name, action # :nodoc: supported = Signal.list[name] old_trap = trap name do old_trap.call if old_trap.respond_to? :call action.call end if supported yield ensure trap name, old_trap if supported end ## # Each subclass of Runnable is responsible for overriding this # method to return all runnable methods. See #methods_matching. def self.runnable_methods raise NotImplementedError, "subclass responsibility" end ## # Returns all subclasses of Runnable. def self.runnables @@runnables end def marshal_dump # :nodoc: [self.name, self.failures, self.assertions] end def marshal_load ary # :nodoc: self.name, self.failures, self.assertions = ary end def failure # :nodoc: self.failures.first end def initialize name # :nodoc: self.name = name self.failures = [] self.assertions = 0 end ## # Runs a single method. Needs to return self. def run raise NotImplementedError, "subclass responsibility" end ## # Did this run pass? # # Note: skipped runs are not considered passing, but they don't # cause the process to exit non-zero. def passed? raise NotImplementedError, "subclass responsibility" end ## # Returns a single character string to print based on the result # of the run. Eg ".", "F", or "E". def result_code raise NotImplementedError, "subclass responsibility" end ## # Was this run skipped? See #passed? for more information. def skipped? raise NotImplementedError, "subclass responsibility" end end ## # Defines the API for Reporters. Subclass this and override whatever # you want. Go nuts. class AbstractReporter ## # Starts reporting on the run. def start end ## # Record a result and output the Runnable#result_code. Stores the # result of the run if the run did not pass. def record result end ## # Outputs the summary of the run. def report end ## # Did this run pass? def passed? true end end class Reporter < AbstractReporter # :nodoc: ## # The IO used to report. attr_accessor :io ## # Command-line options for this run. attr_accessor :options def initialize io = $stdout, options = {} # :nodoc: self.io = io self.options = options end end ## # A very simple reporter that prints the "dots" during the run. # # This is added to the top-level CompositeReporter at the start of # the run. If you want to change the output of minitest via a # plugin, pull this out of the composite and replace it with your # own. class ProgressReporter < Reporter def record result # :nodoc: io.print "%s#%s = %.2f s = " % [result.class, result.name, result.time] if options[:verbose] io.print result.result_code io.puts if options[:verbose] end end ## # A reporter that gathers statistics about a test run. Does not do # any IO because meant to be used as a parent class for a reporter # that does. # # If you want to create an entirely different type of output (eg, # CI, HTML, etc), this is the place to start. class StatisticsReporter < Reporter # :stopdoc: attr_accessor :assertions attr_accessor :count attr_accessor :results attr_accessor :start_time attr_accessor :total_time attr_accessor :failures attr_accessor :errors attr_accessor :skips # :startdoc: def initialize io = $stdout, options = {} # :nodoc: super self.assertions = 0 self.count = 0 self.results = [] self.start_time = nil self.total_time = nil self.failures = nil self.errors = nil self.skips = nil end def passed? # :nodoc: results.all?(&:skipped?) end def start # :nodoc: self.start_time = Time.now end def record result # :nodoc: self.count += 1 self.assertions += result.assertions results << result if not result.passed? or result.skipped? end def report # :nodoc: aggregate = results.group_by { |r| r.failure.class } aggregate.default = [] # dumb. group_by should provide this self.total_time = Time.now - start_time self.failures = aggregate[Assertion].size self.errors = aggregate[UnexpectedError].size self.skips = aggregate[Skip].size end end ## # A reporter that prints the header, summary, and failure details at # the end of the run. # # This is added to the top-level CompositeReporter at the start of # the run. If you want to change the output of minitest via a # plugin, pull this out of the composite and replace it with your # own. class SummaryReporter < StatisticsReporter # :stopdoc: attr_accessor :sync attr_accessor :old_sync # :startdoc: def start # :nodoc: super self.sync = io.respond_to? :"sync=" # stupid emacs self.old_sync, io.sync = io.sync, true if self.sync end def report # :nodoc: super io.sync = self.old_sync io.puts # finish the dots io.puts io.puts statistics io.puts aggregated_results io.puts summary end def statistics # :nodoc: "Finished in #{ChronicDuration.output(total_time.round) || '0s'}" end def aggregated_results # :nodoc: filtered_results = results.dup filtered_results.reject!(&:skipped?) unless options[:verbose] filtered_results.each_with_index.map do |result, i| "\n%3d) %s" % [i+1, result] end.join("\n") + "\n" end def summary # :nodoc: "%d runs, %d assertions, %d failures, %d errors, %d skips" % [count, assertions, failures, errors, skips] end end ## # Dispatch to multiple reporters as one. class CompositeReporter < AbstractReporter ## # The list of reporters to dispatch to. attr_accessor :reporters def initialize *reporters # :nodoc: self.reporters = reporters end ## # Add another reporter to the mix. def << reporter self.reporters << reporter end def passed? # :nodoc: self.reporters.all?(&:passed?) end def start # :nodoc: self.reporters.each(&:start) end def record result # :nodoc: self.reporters.each do |reporter| reporter.record result end end def report # :nodoc: self.reporters.each(&:report) end end ## # Represents run failures. class Assertion < Exception def error # :nodoc: self end ## # Where was this run before an assertion was raised? def location last_before_assertion = "" self.backtrace.reverse_each do |s| break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/ last_before_assertion = s end last_before_assertion.sub(/:in .*$/, "") end def result_code # :nodoc: result_label[0, 1] end def result_label # :nodoc: "Failure" end end ## # Assertion raised when skipping a run. class Skip < Assertion def result_label # :nodoc: "Skipped" end end ## # Assertion wrapping an unexpected error that was raised during a run. class UnexpectedError < Assertion attr_accessor :exception # :nodoc: def initialize exception # :nodoc: super self.exception = exception end def backtrace # :nodoc: self.exception.backtrace end def error # :nodoc: self.exception end def message # :nodoc: bt = Minitest::filter_backtrace(self.backtrace).join "\n " "#{self.exception.class}: #{self.exception.message}\n #{bt}" end def result_label # :nodoc: "Error" end end ## # Provides a simple set of guards that you can use in your tests # to skip execution if it is not applicable. These methods are # mixed into Test as both instance and class methods so you # can use them inside or outside of the test methods. # # def test_something_for_mri # skip "bug 1234" if jruby? # # ... # end # # if windows? then # # ... lots of test methods ... # end module Guard ## # Is this running on jruby? def jruby? platform = RUBY_PLATFORM "java" == platform end ## # Is this running on maglev? def maglev? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE "maglev" == platform end ## # Is this running on mri? def mri? platform = RUBY_DESCRIPTION /^ruby/ =~ platform end ## # Is this running on rubinius? def rubinius? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE "rbx" == platform end ## # Is this running on windows? def windows? platform = RUBY_PLATFORM /mswin|mingw/ =~ platform end end class BacktraceFilter # :nodoc: def filter bt return ["No backtrace"] unless bt return bt.dup if $DEBUG new_bt = bt.take_while { |line| line !~ /lib\/minitest/ } new_bt = bt.select { |line| line !~ /lib\/minitest/ } if new_bt.empty? new_bt = bt.dup if new_bt.empty? new_bt end end self.backtrace_filter = BacktraceFilter.new end require "minitest/test" require "minitest/unit" unless defined?(MiniTest) # compatibility layer only