specky/ruby/specky_formatter.rb
author Mahlon E. Smith <mahlon@martini.nu>
Fri, 24 Dec 2010 20:01:10 -0800
branchvim-stuff
changeset 21 cd1f3381c1ed
parent 19 763cef799c74
child 22 ed72213b1788
permissions -rw-r--r--
Emit file and line for failure source (use gF to jump straight to it!). Show context lines for exception source. Put spec summary run at the top of the screen. Small documentation fixes.


require 'rspec/core/formatters/base_text_formatter'

### SpeckyFormatter: A basic RSpec 2.x text formatter, to be used
### with the 'Specky' vim plugin (or from the command line, if you
### dig it over the default 'documentation' format!)
###
### rspec -r /path/to/this/specky_formatter.rb -f SpeckyFormatter specs
###
class SpeckyFormatter < RSpec::Core::Formatters::BaseTextFormatter

	def initialize( *args )
		super
		@indent_level  = 0
		@failure_index = 0
		@failures      = []
		@txt           = ''
		@summary       = ''
	end


	########################################################################
	### R S P E C  H O O K S
	########################################################################

	### Example group hook -- increase indentation, emit description
	###
	def example_group_started( example_group )
		self.out '+', '-' * (example_group.description.length + 2), '+'
		self.out '| ', example_group.description, ' |'
		self.out '+', '-' * (example_group.description.length + 2), '+'
		@indent_level += 1
	end


	### Example group hook -- decrease indentation
	###
	def example_group_finished( example_group )
		@indent_level -= 1
	end


	### Called on example success
	###
	def example_passed( example )
		msg = self.format_example( example )
		msg << ')'
		self.out msg
	end


	### Called on a pending example
	###
	def example_pending( example )
		msg = self.format_example( example )
		pending_msg = example.metadata[ :execution_result ][ :pending_message ]
		msg << ", PENDING%s)" % [ ": #{pending_msg}" || '' ]
		self.out msg
	end


	### Called on example failure
	###
	def example_failed( example )
		@failure_index += 1
		msg = self.format_example( example )
		msg << ", FAILED - #%d)" % [ @failure_index ]
		self.out msg

		@failures << example
	end


	### Called after all examples are run.  Emit details for each failed example,
	### for Vim to fold.
	###
	def dump_failures
		self.out "\n" unless @failures.empty?

		@failures.each_with_index do |example, index|
			desc      = example.metadata[ :full_description ]
			exception = example.execution_result[ :exception ]
			file = line = nil

			if exception.backtrace.first =~ /(.*):(\d+)/
				file, line = $1, $2.to_i
			end
			self.out "FAILURE - #%d)" % [ index + 1 ]
			self.out "%s:%d" % [ file, line ]

			if RSpec::Core::PendingExampleFixedError === exception
				self.out "%s FIXED" % [ desc ]
				self.out "Expected pending '%s' to fail.  No error was raised." % [
					example.metadata[ :execution_result ][ :pending_message ]
				]
			else
				self.out desc
				self.out "Failure/Error: %s" %  [ read_failed_line( exception, example).strip ]
				exception.message.split("\n").each {|l| self.out l}

				# logic taken from the base class
				example.example_group.ancestors.push(example.example_group).each do |group|
					if group.metadata[:shared_group_name]
						self.out "Shared Example Group: \"#{group.metadata[:shared_group_name]}\" called from " +
							"#{backtrace_line(group.metadata[:example_group][:location])}"
						break
					end
				end
			end

			self.out exception_source( file, line ) if file && line
		end
	end


	### Emit the source of the exception, with context lines.
	###
	def exception_source( file, line )
		context = ''
		low, high = line - 3, line + 3

		File.open( file ).each_with_index do |cline, i|
			cline.chomp!.rstrip!
			next unless i >= low && i <= high
			context << "  %s%4d: %s\n" % [ ( i == line ? '>>' : ' |' ), i, cline ]
		end

		return context

	rescue
		'Unable to parse exception context lines.'
	end


	### Emit summary data for all examples.
	###
	def dump_summary( duration, example_count, failure_count, pending_count )
		succeeded = example_count - failure_count - pending_count
		@summary << "+%s+\n" % [ '-' * 49 ]
		@summary << "|%s-- Summary --%s|\n" % [ ' ' * 18, ' ' * 18 ]
		@summary << "+----------+-----------+--------+---------+-------+\n"
		@summary << "| Duration | Succeeded | Failed | Pending | Total |\n"
		@summary << "+----------+-----------+--------+---------+-------+\n"

		@summary << "| %7ss | %9s | %6s | %7s | %5s |\n" % [
			"%0.3f" % duration, succeeded, failure_count,
			pending_count, example_count
		]

		@summary << "+----------+-----------+--------+---------+-------+\n\n"
	end


	### End of run.  Dump it all out!
	###
	def close
		output.puts @summary
		output.puts @txt
	end


	#########
	protected
	#########

	### Send a string to the output IO object, after indentation.
	###
	def out( *msg )
		msg = msg.join
		@txt << "%s%s\n" % [ '  ' * @indent_level, msg ]
	end

	### Format the basic example information, along with the run duration.
	###
	def format_example( example )
		metadata    = example.metadata
		duration    = metadata[ :execution_result ][ :run_time ]
		description = metadata[ :description ]
		return "| %s (%0.3fs" % [ description, duration ]
	end
end # SpeckyFormatter