module Scampi
The top-level Scampi module. Manages the global test queue, counters, error log, and TAP output.
Nested
Definitions
Counter = Hash.new(0)
Global counters tracking specifications, requirements, failures, errors, nesting depth, and whether the summary hook has been installed.
ErrorLog = "".dup
Mutable string that accumulates error backtraces for TAP diagnostic output.
Shared
Registry of shared context blocks, keyed by name.
Implementation
Shared = Hash.new { |_, name|
raise NameError, "no such context: #{name.inspect}"
}
def self.queue
The global queue of test items (contexts and raw specs).
Signature
-
returns
Array
Implementation
def self.queue
@queue
end
def self.run
Run all queued tests and emit TAP version 14 output.
Implementation
def self.run
return if @ran
@ran = true
# Register: evaluate all describe blocks to discover specs
@queue.each { |item| item.register if item.is_a?(Context) }
# TAP version + plan
puts "TAP version 14"
puts "1..#{@queue.size}"
# Execute: contexts become subtests, raw specs become flat lines
@queue.each_with_index do |item, i|
n = i + 1
if item.is_a?(Context)
passed = item.execute(0)
if passed
puts "#{"ok".green} #{n} - #{item.name}"
else
puts "#{"not ok".red} #{n} - #{item.name}"
end
else
_, description, block = item
Counter[:specifications] += 1
passed = run_bare_spec(description, block, n)
end
end
# Summary comment
tests, assertions, failures, errors =
Counter.values_at(:specifications, :requirements, :failed, :errors)
puts "# #{tests} tests, #{assertions} assertions, #{failures} failures, #{errors} errors"
end
def self.summary_on_exit
Install an at_exit hook that runs all queued tests and sets the
exit code to 1 if there were any failures or errors.
Implementation
def self.summary_on_exit
return if Counter[:installed_summary] > 0
@timer = Time.now
at_exit {
run
if $!
raise $!
elsif Counter[:errors] + Counter[:failed] > 0
exit 1
end
}
Counter[:installed_summary] += 1
end
def self.handle_requirement(description, indent = 0, local_n = 1)
Execute a single requirement block and emit the TAP ok/not-ok line.
The block should return an empty string on success, or an error description string on failure.
Signature
-
parameter
descriptionString Human-readable spec description.
-
parameter
indentInteger Nesting depth for TAP subtest indentation.
-
parameter
local_nInteger The spec number within the current context.
-
returns
Boolean Whether the requirement passed.
Implementation
def self.handle_requirement(description, indent = 0, local_n = 1)
ErrorLog.replace ""
error = yield
prefix = " " * indent
if error.empty?
puts "#{prefix}#{"ok".green} #{local_n} - #{description}"
true
else
puts "#{prefix}#{"not ok".red} #{local_n} - #{description}: #{error}"
puts ErrorLog.strip.gsub(/^/, "#{prefix}# ") if Backtraces
false
end
end
def self.run_bare_spec(description, block, n)
Run a single spec that lives outside any describe block.
Signature
-
parameter
descriptionString Spec description.
-
parameter
blockProc The spec body.
-
parameter
nInteger The spec number in the top-level plan.
-
returns
Boolean Whether the spec passed.
Implementation
def self.run_bare_spec(description, block, n)
handle_requirement(description, 0, n) do
begin
Counter[:depth] += 1
rescued = false
begin
prev_req = Counter[:requirements]
block.call
rescue Object => e
rescued = true
raise e
ensure
if Counter[:requirements] == prev_req and not rescued
raise Error.new(:missing, "empty specification: #{description}")
end
end
rescue SystemExit, Interrupt
raise
rescue Object => e
ErrorLog << "#{e.class}: #{e.message}\n"
e.backtrace.find_all { |line| line !~ /bin\/scampi|\/scampi\.rb:\d+/ }.
each_with_index { |line, i|
ErrorLog << "\t#{line}#{i==0 ? ": #{description}" : ""}\n"
}
ErrorLog << "\n"
if e.kind_of? Error
Counter[e.count_as] += 1
e.count_as.to_s.upcase
else
Counter[:errors] += 1
"ERROR: #{e.class}"
end
else
""
ensure
Counter[:depth] -= 1
end
end
end