RSpec formatters 2

Our very first contest challenged the contestants to create new formatters for the RSpec testing library. After it closed, I received a lot of comments from people that missed the contest but would have loved to participate.

We decided to do it again. The rules are the same as last time:

This week, the challenge is to create your own formatter for RSpec 2. Your solution should solve a problem you’re facing with the existing formatters (like, I don’t know how long my specs are going to take or I don’t notice when my suite is done running) or you can do something completely crazy and funny. With rainbows, or something like that. Oh, and remember: You’re not limited to terminal output, do whatever you can think of.

A good starting point would be to check out the existing formatters. Also, don’t forget to take a look at the entries in the previous RSpec formatters contest.

As always, when you’re done, put your solution in a Gist, including a README file to explain what it does, how it works and why it should win. Of course, you’re encouraged to put a link to a demo video of your formatter in action in your Gist too.

Prize

This week’s winner gets to choose between $25 of 6sync hosting credit or one month of free Github Micro!

This contest is finished

Congratulations to this week's winners! The entries and the contestant names are shown below.

  • README.md

    Chuck testr RSpec formatter

    This formatter is a game changer... Nope! It's just Chuck TestaR

    Requirements

    It requires rspec and growl_notify gem. It works best on OS X.

    Installation

    Since we're using growl notifications you may want to download two additional images (not included in the gist):

    curl http://img.skitch.com/20111031-8x26je3q4x41gy17qhh63j539f.png > chuck-normal.png
    curl http://img.skitch.com/20111031-gmsuuk85ti7tircttd1237u7u7.png > chuck-nope.png
    

    After that, you're good to go!

    Usage

    $ rspec -r ./chuck_testar.rb -f ChuckTestar -c chuck_testar_spec.rb
    
    portable.rb
    module Portable
      def self.platform
        if RbConfig::CONFIG['host_os'] =~ /mswin|windows|cygwin/i
          'windows'
        elsif RbConfig::CONFIG['host_os'] =~ /darwin/
          'osx'
        else
          'linux'
        end
      end
    end
    
    chuck_testar.rb
    require 'rspec/core/formatters/base_text_formatter'
    require './notifications'
    require './portable'
    
    class ChuckTestar < RSpec::Core::Formatters::BaseTextFormatter
      def icon(name)
        File.join(File.dirname(__FILE__), name)
      end
    
      def notify(text, icon_filename)
        if Object.const_defined? "GrowlNotify"
          GrowlNotify.normal({
            :title => 'RSpec',
            :description => text,
            :icon => icon(icon_filename)
          })
        end
      end
    
      def say(text)
        if Portable.platform == 'linux'
          `echo "#{text}" | espeak`
        elsif Portable.platform == 'osx'
          `say "#{text}"`
        end
      end
    
      def notify_with_voice!(text, icon_filename)
        notify(text, icon_filename)
        say(text)
      end
    
      def delay(seconds)
        sleep(seconds)
      end
    
      def start(example_count)
        super(example_count)
        output.print green('Y')
      end
    
      def stop
        output.print green('p')
        output.print green("\n\nYour tests pass!\n")
        notify_with_voice!('Your tests pass', 'chuck-normal.png')
        delay(2)
        if @failed_examples.length > 0
          output.print magenta("\n\nNope! It's just Chuck Testa!")
          notify_with_voice!("Nope! It\'s just Chuck Testa!", 'chuck-nope.png')
          delay(1)
        end
      end
    
      def example_passed(example)
        super(example)
        output.print green('e')
      end
    
      def example_pending(example)
        super(example)
        output.print green('e')
      end
    
      def example_failed(example)
        super(example)
        output.print green('e')
      end
    
      def start_dump
        super()
        output.puts
      end
    end
    
    notifications.rb
    begin
      require "growl_notify"
    
      GrowlNotify.config do |config|
        config.notifications = ["Chuck Testar"]
        config.default_notifications = ["Chuck Testar"]
        config.application_name = "Chuck Testar RSpec formatter" # this shoes up in the growl applications list in systems settings
      end
    rescue LoadError
        puts "Please install growl_notify gem to enable growl notifications"
    end
    
    chuck_testar_spec.rb
    require './chuck_testar'
    
    describe ChuckTestar do
      let(:output)    { StringIO.new }
      let(:formatter) { ChuckTestar.new(output) }
    
      before do
        formatter.stub(:delay => true)
      end
    
      describe "output" do
        let(:example) {
          double("example 1",
           :execution_result => {:status => 'failed', :exception => Exception.new }
          )
        }
    
        it "displays 'Y' at the start of the suite" do
          formatter.start(1)
          output.string.should =~ /Y/
        end
    
        it "displays 'e' when examples pass" do
          formatter.example_passed(example)
          output.string.should =~ /e/
        end
    
        it "displays 'e' when examples fails" do
          formatter.example_failed(example)
          output.string.should =~ /e/
        end
    
        it "displays 'p' at the end of tests suite" do
          formatter.stub(:notify_with_voice! => true)
          formatter.stop
          output.string.should =~ /p/
        end
      end
    
      describe "an failing example" do
        it "fails" do
          false.should be_true
        end
      end
    
      describe "notifications" do
        before do
          formatter.stub(:say => true)
        end
    
        it "display success notification when spec pass" do
          formatter.example_passed(example)
          GrowlNotify.should_receive(:normal).with(:title => 'RSpec', :description => 'Your tests pass', :icon => formatter.icon('chuck-normal.png'))
          formatter.stop
        end
    
        it "display success and fail notifications when spec fail" do
          formatter.example_failed(example)
          GrowlNotify.should_receive(:normal).with(:title => 'RSpec', :description => 'Your tests pass', :icon => formatter.icon('chuck-normal.png'))
          GrowlNotify.should_receive(:normal).with(:title => 'RSpec', :description => "Nope! It's just Chuck Testa!", :icon => formatter.icon('chuck-nope.png'))
          formatter.stop
        end
      end
    end
    
    View full entry
    Finished in 1st place with a final score of 4.3/5. (View the Gist)
  • README.md

    Nyan Cat RSpec Formatter!

    -_-_-_-_-_-_-_,------,  
    _-_-_-_-_-_-_-|   /\_/\  
    -_-_-_-_-_-_-~|__( ^ .^) 
    _-_-_-_-_-_-_-""  ""      
    

    This is my take on the Nyan Cat RSpec Formatter. It simply creates a rainbow trail of test results. It also counts the number of examples as they execute and highlights failed and pending specs.

    The rainbow changes colors as it runs. You must try it:

    rspec nyan_formatter_spec.rb --require ./nyan_formatter.rb  --format NyanFormatter
    

    (Scroll down for screenshots)

    rspec_nyan_cat_formatter.png
    rspec_nyan_cat_formatter_failing.png
    nyan_formatter_spec.rb
    require 'stringio'
    require File.join(File.dirname(__FILE__), 'nyan_formatter.rb')
    
    describe NyanFormatter do
    
      before do
        @output = StringIO.new
        @formatter = NyanFormatter.new(@output)
        @formatter.start(2)
        @example = RSpec::Core::ExampleGroup.describe.example
        sleep(0.2)  # Just to slow it down a little :-)
      end
    
      describe 'passed, pending and failed' do
    
        before do
          @formatter.stub!(:tick)
        end
    
        describe 'example_passed' do
    
          it 'should call the increment method' do
            @formatter.should_receive :tick
            @formatter.example_passed(@example)
          end
    
          it 'should relax Nyan Cat' do
            @formatter.example_passed(@example)
            @formatter.nyan_cat.should == '~|_(^.^)'
          end
    
        end
    
        describe 'example_pending' do
    
          it 'should call the tick method' do
            @formatter.should_receive :tick
            @formatter.example_pending(@example)
          end
    
          it 'should increment the pending count' do
            lambda { @formatter.example_pending(@example)}.
              should change(@formatter, :pending_count).by(1)
          end
    
          it 'should alert Nyan Cat' do
            @formatter.example_pending(@example)
            @formatter.nyan_cat.should == '~|_(o.o)'
          end
    
        end
    
        describe 'example_failed' do
    
          it 'should call the increment method' do
            @formatter.should_receive :tick
            @formatter.example_failed(@example)
          end
    
          it 'should increment the failure count' do
            lambda { @formatter.example_failed(@example)}.
              should change(@formatter, :failure_count).by(1)
          end
    
          it 'should alert Nyan Cat' do
            @formatter.example_failed(@example)
            @formatter.nyan_cat.should == '~|_(o.o)'
          end
    
        end
      end
    
      describe 'tick' do
    
        before do
          @formatter.stub!(:current).and_return(1)
          @formatter.stub!(:example_count).and_return(2)
          @formatter.tick
        end
    
        it 'should change title' do
          @formatter.title.should == '  1/2'
        end
    
        it 'should calculate the percentage done' do
          @formatter.percentage.should == 50
        end
    
        it 'should increment the current' do
          @formatter.current.should == 1
        end
    
        it 'should store the marks in an array' do
          @formatter.example_results.should include('=')
        end
    
      end
    
      describe 'rainbowify' do
    
        it 'should increment the color index count' do
          lambda { @formatter.rainbowify('=') }.should change(@formatter, :color_index).by(1)
        end
    
      end
    
      describe 'highlight' do
    
        it 'should rainbowify passing examples' do
          @formatter.highlight('=').should == "\e[38;5;154m=\e[0m"
        end
    
        it 'should mark failing examples as red' do
          @formatter.highlight('*').should == "\e[31m*\e[0m"
        end
    
        it 'should mark pending examples as yellow' do
          @formatter.highlight('!').should == "\e[33m!\e[0m"
        end
    
      end
    
      describe 'start' do
    
        it 'should set the total amount of specs' do
          @formatter.example_count.should == 2
        end
    
        it 'should set the current to 0' do
          @formatter.current.should == 0
        end
    
      end
    
    end
    
    nyan_formatter.rb
    # -*- coding: utf-8 -*-
    require 'rspec/core/formatters/base_text_formatter'
    
    class NyanFormatter < RSpec::Core::Formatters::BaseTextFormatter
    
      ESC     = "\e["
      NND     = "#{ESC}0m"
      PASS    = '='
      FAIL    = '*'
      ERROR   = '!'
      PENDING = '路'
    
      attr_reader :title, :current, :example_results, :color_index
    
      def start(example_count)
        super(example_count)
        @current, @color_index = 0,0
        @bar_length = 70
        @example_results = []
      end
    
      def example_passed(example)
        super(example)
        tick PASS
      end
    
      def example_pending(example)
        super(example)
        @pending_count =+1
        tick PENDING
      end
    
      def example_failed(example)
        super(example)
        @failure_count =+1
        tick FAIL
      end
    
      def start_dump
        @current = @example_count
      end
    
      def dump_summary(duration, example_count, failure_count, pending_count)
        dump_profile if profile_examples? && failure_count == 0
        summary = "\nNyan Cat flew #{format_seconds(duration)} seconds".split(//).map { |c| rainbowify(c) }
        output.puts summary.join
        output.puts colorise_summary(summary_line(example_count, failure_count, pending_count))
        dump_commands_to_rerun_failed_examples
      end
    
      def dump_failures
        # noop
      end
    
      # Increments the example count and displays the current progress
      #
      # Returns nothing
      def tick(mark = PASS)
        @example_results << mark
        @current =  (@current > @example_count) ? @example_count : @current + 1
        @title = "  #{current}/#{example_count}"
        dump_progress
      end
    
      # Creates a rainbow trail
      #
      # Returns the sprintf format of the Nyan cat
      def nyan_trail
        width =  percentage * @bar_length / 100
        marker = @example_results.map{ |mark| highlight(mark) }.join
        sprintf("%s#{nyan_cat}%s", marker, " " * (@bar_length - width) )
      end
    
      # Calculates the percentage completed any given point
      #
      # Returns Fixnum of the percentage
      def percentage
        @example_count.zero? ? 100 : @current * 100 / @example_count
      end
    
      # Ascii Nyan Cat. If tests are complete, Nyan Cat goes to sleep. If
      # there are failing or pending examples, Nyan Cat is concerned.
      #
      # Returns String Nyan Cat
      def nyan_cat
        if @failure_count > 0 || @pending_count > 0
          '~|_(o.o)'
        elsif (@current == @example_count)
          '~|_(-.-)'
        else
          '~|_(^.^)'
        end
      end
    
      # Displays the current progress in all Nyan Cat glory
      #
      def dump_progress
        max_width = 80
        line  = sprintf("%-8s %s", @title[0,(7)] + ":", nyan_trail)
        tail  = (@current == @example_count) ? "\n" : "\r"
    
        if line.length == max_width - 1
          output.print line + tail
          output.flush
        elsif line.length >= max_width
          @bar_length = [@bar_length - (line.length - max_width + 1), 0].max
          @bar_length == 0 ? output.print( rainbowify(line + tail) ) : dump_progress
        else
          @bar_length += max_width - line.length + 1
          dump_progress
        end
      end
    
      # Colorizes the string with raindow colors of the rainbow
      #
      def rainbowify(string)
        c = colors[@color_index % colors.size]
        @color_index += 1
        "#{ESC}38;5;#{c}m#{string}#{NND}"
      end
    
      # Calculates the colors of the rainbow
      #
      def colors
        @colors ||= (0...(6 * 7)).map do |n|
          pi_3 = Math::PI / 3
          n *= 1.0 / 6
          r  = (3 * Math.sin(n           ) + 3).to_i
          g  = (3 * Math.sin(n + 2 * pi_3) + 3).to_i
          b  = (3 * Math.sin(n + 4 * pi_3) + 3).to_i
          36 * r + 6 * g + b + 16
        end
      end
    
      # Determines how to color the example.  If pass, it is rainbowified, otherwise
      # we assign red if failed or yellow if an error occurred.
      #
      def highlight(mark = PASS)
        case mark
          when PASS;  rainbowify mark
          when FAIL;  red mark
          when ERROR; yellow mark
          else mark
        end
      end
    
    end
    
    View full entry
    Finished in 2nd place with a final score of 3.8/5. (View the Gist)
  • BackwardsFormatter.rb
    require 'rspec/core/formatters/base_text_formatter'
    
    class BackwardsFormatter < RSpec::Core::Formatters::BaseTextFormatter
      def example_failed example
        super example
        dump_backwards_failures
      end
    
      def dump_failures; end
      def dump_summary *args; end
    
      def dump_backwards_failures
        100.times { output.puts }
        output.puts summary_line(example_count, failed_examples.count, pending_examples.count)
        failed_examples.reverse.each_with_index do |example,i|
          dump_failure example, failed_examples.count - i - 1
          dump_backtrace example
        end
      end
    end
    
    View full entry
    Finished in 3rd place with a final score of 2.1/5. (View the Gist)