SusSourceSusAssertions

class Assertions

Represents a collection of test assertions and their results. Tracks passed, failed, skipped, and errored assertions.

Nested

Definitions

def self.default(**options)

Create a new assertions instance with default options.

Signature

parameter options Hash

Options to pass to #initialize.

returns Assertions

A new assertions instance.

Implementation

def self.default(**options)
	self.new(**options)
end

def initialize(identity: nil, target: nil, output: Output.buffered, inverted: false, orientation: true, isolated: false, distinct: false, measure: false, verbose: false)

Initialize a new assertions instance.

Signature

parameter identity Identity, nil

The identity used to identify this set of assertions.

parameter target Object, nil

The specific target of the assertions, e.g. the test case or nested test assertions.

parameter output Output

The output buffer used to capture output from the assertions.

parameter inverted Boolean

Whether the assertions are inverted with respect to the parent.

parameter orientation Boolean

Whether the assertions are positive or negative in general.

parameter isolated Boolean

Whether this set of assertions is isolated from the parent.

parameter distinct Boolean

Whether this set of assertions should be treated as a single statement.

parameter measure Boolean

Whether to measure execution time.

parameter verbose Boolean

Whether to output verbose information.

Implementation

def initialize(identity: nil, target: nil, output: Output.buffered, inverted: false, orientation: true, isolated: false, distinct: false, measure: false, verbose: false)
	# In theory, the target could carry the identity of the assertion group, but it's not really necessary, so we just handle it explicitly and pass it into any nested assertions.
	@identity = identity
	@target = target
	@output = output
	@inverted = inverted
	@orientation = orientation
	@isolated = isolated
	@distinct = distinct
	@verbose = verbose
	
	if measure
		@clock = Clock.start!
	else
		@clock = nil
	end
	
	@passed = Array.new
	@failed = Array.new
	@deferred = Array.new
	@skipped = Array.new
	@errored = Array.new
	
	@count = 0
end

attr :identity

Signature

attribute Identity, nil

The identity that is used to identify this set of assertions.

attr :target

Signature

attribute Object, nil

The specific target of the assertions, e.g. the test case or nested test assertions.

attr :output

Signature

attribute Output

The output buffer used to capture output from the assertions.

attr :level

Signature

attribute Integer, nil

The nesting level of this set of assertions.

attr :inverted

Signature

attribute Boolean

Whether this set of assertions is inverted, i.e. the assertions are expected to fail relative to the parent. Used for grouping assertions and ensuring they are added to the parent passed/failed array correctly.

attr :orientation

Signature

attribute Boolean

The absolute orientation of this set of assertions, i.e. whether the assertions are expected to pass or fail regardless of the parent. Used for correctly formatting the output.

attr :isolated

Signature

attribute Boolean

Whether this set of assertions is isolated from the parent. This is used to ensure that any deferred assertions are completed before the parent is completed. This is used by receive assertions which are deferred until the user code of the test has completed.

attr :distinct

Signature

attribute Boolean

Distinct is used to identify a set of assertions as a single statement for the purpose of user feedback. It's used by top level ensure statements to ensure that error messages are captured and reported on those statements.

attr :verbose

Signature

attribute Boolean

Whether to output verbose information.

attr :clock

Signature

attribute Clock, nil

The clock used to measure execution time, if measurement is enabled.

attr :passed

Signature

attribute Array

Nested assertions that have passed.

attr :failed

Signature

attribute Array

Nested assertions that have failed.

attr :deferred

Signature

attribute Array

Nested assertions that have been deferred.

attr :skipped

Signature

attribute Array

Nested assertions that have been skipped.

attr :errored

Signature

attribute Array

Nested assertions that have errored.

attr :count

Signature

attribute Integer

The total number of assertions performed.

def inspect

Signature

returns String

A string representation of the assertions instance.

Implementation

def inspect
	"\#<#{self.class} #{@passed.size} passed #{@failed.size} failed #{@deferred.size} deferred #{@skipped.size} skipped #{@errored.size} errored>"
end

def message

Signature

returns Hash

A hash containing the output text and location of the assertions.

Implementation

def message
	{
		text: @output.string,
		location: @identity&.to_location
	}
end

def total

Signature

returns Integer

The total number of assertions (passed, failed, deferred, skipped, and errored).

Implementation

def total
	@passed.size + @failed.size + @deferred.size + @skipped.size + @errored.size
end

def print(output, verbose: @verbose)

Print a summary of the assertions to the output.

Signature

parameter output Output

The output target.

parameter verbose Boolean

Whether to include verbose information.

Implementation

def print(output, verbose: @verbose)
	if verbose && @target
		@target.print(output)
		output.write(": ")
	end
	
	if @count.zero?
		output.write("0 assertions")
	else
		if @passed.any?
			output.write(:passed, @passed.size, " passed", :reset, " ")
		end
		
		if @failed.any?
			output.write(:failed, @failed.size, " failed", :reset, " ")
		end
		
		if @deferred.any?
			output.write(:deferred, @deferred.size, " deferred", :reset, " ")
		end
		
		if @skipped.any?
			output.write(:skipped, @skipped.size, " skipped", :reset, " ")
		end
		
		if @errored.any?
			output.write(:errored, @errored.size, " errored", :reset, " ")
		end
		
		output.write("out of ", self.total, " total (", @count, " assertions)")
	end
end

def puts(*message)

Print a message to the output buffer.

Signature

parameter message Array

The message parts to print.

Implementation

def puts(*message)
	@output.puts(:indent, *message)
end

def empty?

Signature

returns Boolean

Whether there are no assertions (passed, failed, deferred, skipped, or errored).

Implementation

def empty?
	@passed.empty? and @failed.empty? and @deferred.empty? and @skipped.empty? and @errored.empty?
end

def passed?

Signature

returns Boolean

Whether all assertions passed and none errored.

Implementation

def passed?
	if @inverted
		# Inverted assertions:
		@failed.any? and @errored.empty?
	else
		# Normal assertions:
		@failed.empty? and @errored.empty?
	end
end

def failed?

Signature

returns Boolean

Whether any assertions failed or errored.

Implementation

def failed?
	!self.passed?
end

def errored?

Signature

returns Boolean

Whether any assertions errored.

Implementation

def errored?
	@errored.any?
end

def assert(condition, message = nil)

Make an assertion about a condition.

Signature

parameter condition Boolean

The condition to assert.

parameter message String | Nil

Optional message describing the assertion.

Implementation

def assert(condition, message = nil)
	@count += 1
	
	identity = @identity&.scoped
	backtrace = Output::Backtrace.first(identity)
	assert = Assert.new(identity, backtrace, self)
	
	if condition
		@passed << assert
		@output.assert(condition, @orientation, message || "assertion passed", backtrace)
	else
		@failed << assert
		@output.assert(condition, @orientation, message || "assertion failed", backtrace)
	end
end

def each_failure(&block)

Iterate over all failures in this assertions instance.

Signature

yields {|failure| ...}

Each failure (failed assertion or error).

returns Enumerator

An enumerator if no block is given.

Implementation

def each_failure(&block)
	return to_enum(__method__) unless block_given?
	
	if self.failed? and @distinct
		return yield(self)
	end
	
	@failed.each do |assertions|
		assertions.each_failure(&block)
	end
	
	@errored.each do |assertions|
		assertions.each_failure(&block)
	end
end

def skip(reason)

Skip this set of assertions with a reason.

Signature

parameter reason String

The reason for skipping.

Implementation

def skip(reason)
	@output.skip(reason, @identity&.scoped)
	
	@skipped << self
end

def inform(message = nil)

Print an informational message during test execution.

Signature

parameter message String | Nil

The message to print, or a block that returns a message.

Implementation

def inform(message = nil)
	if message.nil? and block_given?
		begin
			message = yield
		rescue => error
			message = error.full_message
		end
	end
	
	@output.inform(message, @identity&.scoped)
end

def defer(&block)

Add a deferred assertion that will be resolved later.

Signature

yields {|assertions| ...}

The block that will be called to resolve the deferred assertion.

Implementation

def defer(&block)
	@deferred << block
end

def deferred?

Signature

returns Boolean

Whether there are any deferred assertions.

Implementation

def deferred?
	@deferred.any?
end

def resolve!

Resolve all deferred assertions in order.

Implementation

def resolve!
	@output.indented do
		while block = @deferred.shift
			block.call(self)
		end
	end
end

def error!(error)

Record an error that occurred during test execution.

Signature

parameter error Exception

The exception that was raised.

Implementation

def error!(error)
	identity = @identity&.scoped(error.backtrace_locations)
	
	@errored << Error.new(identity, error)
	
	# TODO consider passing `identity`.
	@output.error(error, @identity)
end

def nested(target, identity: @identity, isolated: false, distinct: false, inverted: false, **options)

Create a nested set of assertions.

Signature

parameter target Object

The target object for the nested assertions.

parameter identity Identity, nil

The identity for the nested assertions.

parameter isolated Boolean

Whether the nested assertions are isolated from the parent.

parameter distinct Boolean

Whether the nested assertions should be treated as a single statement.

parameter inverted Boolean

Whether the nested assertions are inverted.

parameter options Hash

Additional options to pass to the nested assertions instance.

yields {|assertions| ...}

The nested assertions instance.

returns Object

The result of the block.

Implementation

def nested(target, identity: @identity, isolated: false, distinct: false, inverted: false, **options)
	result = nil
	
	# Isolated assertions need to have buffered output so they can be replayed if they fail:
	if isolated or distinct
		output = @output.buffered
	else
		output = @output
	end
	
	# Inverting a nested assertions causes the orientation to flip:
	if inverted
		orientation = !@orientation
	else
		orientation = @orientation
	end
	
	output.puts(:indent, target)
	
	assertions = self.class.new(identity: identity, target: target, output: output, isolated: isolated, inverted: inverted, orientation: orientation, distinct: distinct, verbose: @verbose, **options)
	
	output.indented do
		begin
			result = yield(assertions)
		rescue StandardError => error
			assertions.error!(error)
		end
	end
	
	# Some assertions are deferred until the end of the test, e.g. expecting a method to be called. This scope is managed by the {add} method. If there are no deferred assertions, then we can add the child assertions right away. Otherwise, we append the child assertions to our own list of deferred assertions. When an assertions instance is marked as `isolated`, it will force all deferred assertions to be resolved. It's also at this time, we should conclude measuring the duration of the test.
	assertions.resolve_into(self)
	
	return result
end

def add(assertions)

Add child assertions that were nested to this instance.

Signature

parameter assertions Assertions

The child assertions to add.

Implementation

def add(assertions)
	# All child assertions should be resolved by this point:
	raise "Nested assertions must be fully resolved!" if assertions.deferred?
	
	if assertions.append?
		# If we are isolated, we merge all child assertions into the parent as a single entity:
		append!(assertions)
	else
		# Otherwise, we append all child assertions into the parent assertions:
		merge!(assertions)
	end
end

def append?

Whether the child assertions should be merged into the parent assertions.

Implementation

def append?
	@isolated || @inverted || @distinct
end

def merge!(assertions)

Concatenate the child assertions into this instance.

Implementation

def merge!(assertions)
	@count += assertions.count
	@passed.concat(assertions.passed)
	@failed.concat(assertions.failed)
	@deferred.concat(assertions.deferred)
	@skipped.concat(assertions.skipped)
	@errored.concat(assertions.errored)
	
	# if @verbose
	# 	@output.write(:indent)
	# 	self.print(@output, verbose: false)
	# 	@output.puts
	# end
end