• Jump To … +
    autorun.rb data_structures.rb defaults.rb dsl.rb evaluators.rb notifiers.rb schedulers.rb
  • ¶

    Notifiers

  • ¶

    Without a notifier, there is no way for XSpec to interact with the outside world. A notifier handles progress updates as tests are executed, and summarizing the run when it finished.

    module XSpec
      module Notifier
  • ¶

    A formatter must implement at least four methods. run_start and run_finish are called at the beginning and end of the full spec run respectively, while evaluate_start and evaluate_finish are called for each test. See API docs for more information.

        module Empty
          def run_start(*_); end
          def evaluate_start(*_); end
          def evaluate_finish(*_); end
          def run_finish(*_); true; end
        end
  • ¶

    Many notifiers play nice with others, and can be composed together in a way that each notifier will have its callback run in turn.

        module Composable
          def +(other)
            Composite.new(self, other)
          end
        end
    
        class Composite
          include Composable
    
          def initialize(*notifiers)
            @notifiers = notifiers
          end
    
          def run_start(*args)
            notifiers.each {|x| x.run_start(*args) }
          end
    
          def evaluate_start(*args)
            notifiers.each {|x| x.evaluate_start(*args) }
          end
    
          def evaluate_finish(*args)
            notifiers.each {|x| x.evaluate_finish(*args) }
          end
    
          def run_finish
            notifiers.map(&:run_finish).all?
          end
    
          protected
    
          attr_reader :notifiers
        end
  • ¶

    Outputs a single character for each executed unit of work representing the result.

        class Character
          include Empty
          include Composable
    
          def initialize(out = $stdout)
            @out = out
          end
    
          def evaluate_finish(result)
            @out.print label_for_failure(result.errors[0])
            @failed ||= result.errors.any?
          end
    
          def run_finish
            @out.puts
            !@failed
          end
    
          protected
    
          def label_for_failure(f)
            case f
              when CodeException then 'E'
              when Failure then 'F'
              else '.'
            end
          end
        end
  • ¶

    Renders a histogram of test durations after the entire run is complete.

        class TimingsAtEnd
          include Empty
          include Composable
    
          DEFAULT_SPLITS = [0.001, 0.005, 0.01, 0.1, 1.0, Float::INFINITY]
    
          def initialize(out:    $stdout,
                         splits: DEFAULT_SPLITS,
                         width:  20)
    
            @timings = {}
            @splits  = splits
            @width   = width
            @out     = out
          end
    
          def evaluate_finish(result)
            timings[result] = result.duration
          end
    
          def run_finish
            buckets = bucket_from_splits(timings, splits)
            max     = buckets.values.max
    
            out.puts "           Timings:"
            buckets.each do |(split, count)|
              label = split.infinite? ? "∞" : split
    
              out.puts "    %6s %-#{width}s %i" % [
                label,
                '#' * (count / max.to_f * width.to_f).ceil,
                count
              ]
            end
            out.puts
    
            true
          end
    
        private
    
          attr_reader :timings, :splits, :width, :out
    
          def bucket_from_splits(timings, splits)
            initial_buckets = splits.each_with_object({}) do |b, a|
              a[b] = 0
            end
    
            buckets = timings.each_with_object(initial_buckets) do |(_, d), a|
              split = splits.detect {|x| d < x }
              a[split] += 1
            end
    
            remove_trailing_zero_counts(buckets)
          end
    
          def remove_trailing_zero_counts(buckets)
            Hash[
              buckets
                .to_a
                .reverse
                .drop_while {|_, x| x == 0 }
                .reverse
            ]
          end
        end
  • ¶

    Provides convenience methods for working with short ids.

        module ShortIdSupport
          def run_start(config)
            super
            @short_id = config.fetch(:short_id)
          end
    
          def short_id_for(x)
            @short_id.(x)
          end
        end
  • ¶

    Outputs error messages and backtraces after the entire run is complete.

        class FailuresAtEnd
          include Empty
          include Composable
          include ShortIdSupport
    
          def initialize(out = $stdout)
            @errors = []
            @out    = out
          end
    
          def evaluate_finish(result)
            self.errors += result.errors
          end
    
          def run_finish
            return true if errors.empty?
    
            out.puts
            errors.each do |error|
              out.puts "%s - %s\n%s\n\n" % [
                short_id_for(error.unit_of_work),
                error.unit_of_work.full_name,
                error.message.lines.map {|x| "  #{x}"}.join("")
              ]
              clean_backtrace(error.caller).each do |line|
                out.puts "  %s" % line
              end
              out.puts
            end
    
            false
          end
    
          private
  • ¶

    A standard backtrace contains many entries for XSpec itself which are not useful for debugging your tests, so they are stripped out.

          def clean_backtrace(backtrace)
            lib_dir = File.dirname(File.expand_path('..', __FILE__))
    
            backtrace.reject {|x|
              File.dirname(x).start_with?(lib_dir)
            }
          end
    
          protected
    
          attr_accessor :out, :errors
        end
  • ¶

    Includes nicely formatted names and durations of each test in the output, with color.

        class ColoredDocumentation
          include Empty
          include Composable
          include ShortIdSupport
    
          VT100_COLORS = {
            :black   => 30,
            :red     => 31,
            :green   => 32,
            :yellow  => 33,
            :blue    => 34,
            :magenta => 35,
            :cyan    => 36,
            :white   => 37
          }
    
          def initialize(out = $stdout)
            self.indent          = 2
            self.last_seen_names = []
            self.failed          = false
            self.out             = out
          end
    
          def evaluate_finish(result)
            output_context_header! result.parents.map(&:name).compact
    
            spaces = ' ' * (last_seen_names.size * indent)
    
            self.failed ||= result.errors.any?
    
            out.puts "%s%s" % [spaces, decorate(result)]
          end
    
          def run_finish
            out.puts
            !failed
          end
    
          protected
    
          attr_accessor :last_seen_names, :indent, :failed, :out
    
          def color_code_for(color)
            VT100_COLORS.fetch(color)
          end
    
          def colorize(text, color)
            "\e[#{color_code_for(color)}m#{text}\e[0m"
          end
    
          def decorate(result)
            name = result.name
            out = if result.errors.any?
              colorize(append_failed(name), :red)
            else
              colorize(name , :green)
            end
            "%.3fs %s %s" % [
              result.duration,
              short_id_for(result),
              out,
            ]
          end
    
          def output_context_header!(parent_names)
            if parent_names != last_seen_names
              tail = parent_names - last_seen_names
    
              out.puts
              if tail.any?
                existing_indent = parent_names.size - tail.size
                tail.each_with_index do |name, i|
                  out.puts '%s%s' % [' ' * ((existing_indent + i) * indent), name]
                end
              end
    
              self.last_seen_names = parent_names
            end
          end
    
          def append_failed(name)
            [name, "FAILED"].compact.join(' - ')
          end
        end
  • ¶

    Includes nicely formatted names and durations of each test in the output.

        class Documentation < ColoredDocumentation
          def colorize(name, _)
            name
          end
        end
  • ¶

    Serializes all calls to a child notifier. Used in threaded scheduler so that notifier implementors do not need to worry about thread safety.

        class Synchronized
          def initialize(notifier)
            @notifier = notifier
            @mutex    = Mutex.new
          end
    
          def run_start(*args)
            @mutex.synchronize { @notifier.run_start(*args) }
          end
    
          def evaluate_start(*args)
            @mutex.synchronize { @notifier.evaluate_start(*args) }
          end
    
          def evaluate_finish(*args)
            @mutex.synchronize { @notifier.evaluate_finish(*args) }
          end
    
          def run_finish(*args)
            @mutex.synchronize { @notifier.run_finish(*args) }
          end
        end
  • ¶

    A notifier that does not do anything and always returns successful. Useful as a parent class for other notifiers or for testing.

        class Null
          include Composable
          include Empty
        end
    
        DEFAULT =
          ColoredDocumentation.new +
          TimingsAtEnd.new +
          FailuresAtEnd.new
      end
    end