require "colorize" require "option_parser" module Spec # :nodoc: # The default spec runner. class_getter cli = CLI.new # :nodoc: enum InfoKind Comment Focus Order def color : Colorize::Color Colorize::ColorANSI::Cyan end end # :nodoc: class SpecError < Exception getter file : String getter line : Int32 def initialize(message, @file, @line) super(message) end end # :nodoc: class AssertionFailed < SpecError end # :nodoc: class ExamplePending < SpecError end # :nodoc: class NestingSpecError < SpecError end # :nodoc: def self.to_human(span : Time::Span) total_milliseconds = span.total_milliseconds if total_milliseconds < 1 return "#{(span.total_milliseconds * 1000).round.to_i} microseconds" end total_seconds = span.total_seconds if total_seconds < 1 return "#{span.total_milliseconds.round(2)} milliseconds" end if total_seconds < 60 return "#{total_seconds.round(2)} seconds" end minutes = span.minutes seconds = span.seconds "#{minutes}:#{seconds < 10 ? "0" : ""}#{seconds} minutes" end record SplitFilter, remainder : Int32, quotient : Int32 # Instructs the spec runner to execute the given block # before each spec in the spec suite. # # If multiple blocks are registered they run in the order # that they are given. # # For example: # # ``` # Spec.before_each { puts 1 } # Spec.before_each { puts 2 } # ``` # # will print, just before each spec, 1 and then 2. def self.before_each(&block) @@cli.root_context.before_each(&block) end # Instructs the spec runner to execute the given block # after each spec in the spec suite. # # If multiple blocks are registered they run in the reversed # order that they are given. # # For example: # # ``` # Spec.after_each { puts 1 } # Spec.after_each { puts 2 } # ``` # # will print, just after each spec, 2 and then 1. def self.after_each(&block) @@cli.root_context.after_each(&block) end # Instructs the spec runner to execute the given block # before the entire spec suite. # # If multiple blocks are registered they run in the order # that they are given. # # For example: # # ``` # Spec.before_suite { puts 1 } # Spec.before_suite { puts 2 } # ``` # # will print, just before the spec suite starts, 1 and then 2. def self.before_suite(&block) @@cli.root_context.before_all(&block) end # Instructs the spec runner to execute the given block # after the entire spec suite. # # If multiple blocks are registered they run in the reversed # order that they are given. # # For example: # # ``` # Spec.after_suite { puts 1 } # Spec.after_suite { puts 2 } # ``` # # will print, just after the spec suite ends, 2 and then 1. def self.after_suite(&block) @@cli.root_context.after_all(&block) end # Instructs the spec runner to execute the given block when each spec in the # spec suite runs. # # The block must call `run` on the given `Example::Procsy` object. # # If multiple blocks are registered they run in the reversed # order that they are given. # # ``` # require "spec" # # Spec.around_each do |example| # puts "runs before each sample" # example.run # puts "runs after each sample" # end # # it { } # it { } # ``` def self.around_each(&block : Example::Procsy ->) @@cli.root_context.around_each(&block) end # :nodoc: class CLI @aborted = false def abort! @aborted = true finish_run end @split_filter : SplitFilter? = nil def add_split_filter(filter) if filter r, _, m = filter.partition('%') @split_filter = SplitFilter.new(remainder: r.to_i, quotient: m.to_i) else @split_filter = nil end end @start_time : Time::Instant? = nil def run @start_time = Time.instant at_exit do |status| # Do not run specs if the process is exiting on an error next unless status == 0 begin if list_tags? execute_list_tags else execute_examples end rescue ex @stderr.print "Unhandled exception: " ex.inspect_with_backtrace(@stderr) @stderr.flush @aborted = true ensure finish_run unless list_tags? end end end def colorize(str, status : Status) str.colorize(status.color).toggle(@color) end def colorize(str, kind : InfoKind) str.colorize(kind.color).toggle(@color) end def execute_examples log_setup maybe_randomize run_filters root_context.run end def execute_list_tags run_filters tag_counts = collect_tags(root_context) print_list_tags(tag_counts) end private def collect_tags(context) : Hash(String, Int32) tag_counts = Hash(String, Int32).new(0) collect_tags(tag_counts, context, Set(String).new) tag_counts end private def collect_tags(tag_counts, context : Context, tags) if context.responds_to?(:tags) && (context_tags = context.tags) tags += context_tags end context.children.each do |child| collect_tags(tag_counts, child, tags) end end private def collect_tags(tag_counts, example : Example, tags) if example_tags = example.tags tags += example_tags end if tags.empty? tag_counts.update("untagged") { |count| count + 1 } else tags.tally(tag_counts) end end private def print_list_tags(tag_counts : Hash(String, Int32)) : Nil return if tag_counts.empty? longest_name_size = tag_counts.keys.max_of(&.size) tag_counts.to_a.sort_by! { |k, v| {-v, k} }.each do |tag_name, count| @stdout.puts "#{tag_name.rjust(longest_name_size)}: #{count}" end end # :nodoc: # # Workaround for #8914 private macro defined?(t) {% if t.resolve? %} {{ yield }} {% end %} end # :nodoc: def log_setup end # :nodoc: macro finished # :nodoc: # # Initialized the log module for the specs. # If the "log" module is required it is configured to emit no entries by default. def log_setup defined?(::Log) do if ::Log.responds_to?(:setup) ::Log.setup_from_env(default_level: :none) end end end end def finish_run elapsed_time = @start_time.not_nil!.elapsed root_context.finish(elapsed_time, @aborted) exit 1 if !root_context.succeeded || @aborted || (focus? && ENV["SPEC_FOCUS_NO_FAIL"]? != "1") end # :nodoc: def maybe_randomize if randomizer = @randomizer root_context.randomize(randomizer) end end # :nodoc: def run_filters root_context.run_filters(@pattern, @line, @locations, @split_filter, @focus, @tags, @anti_tags) end end @[Deprecated("This is an internal API.")] def self.finish_run @@cli.finish_run end @[Deprecated("This is an internal API.")] def self.add_split_filter(filter) @@cli.add_split_filter(filter) end end require "./*"