Module: Scampi

Defined in:
lib/scampi.rb,
lib/scampi/error.rb,
lib/scampi/colors.rb,
lib/scampi/should.rb,
lib/scampi/context.rb,
lib/scampi/version.rb

Overview

Minimal ANSI coloring — trimmed from the colorize gem to only the two colors Scampi actually uses (green for "ok", red for "not ok"). Output is byte-for-byte identical to colorize's String#green / String#red.

Defined Under Namespace

Modules: Colors Classes: Context, Error, Should

Constant Summary collapse

Counter =

Global counters tracking specifications, requirements, failures, errors, nesting depth, and whether the summary hook has been installed.

Hash.new(0)
ErrorLog =

Mutable string that accumulates error backtraces for TAP diagnostic output.

"".dup
Shared =

Registry of shared context blocks, keyed by name.

Hash.new { |_, name|
  raise NameError, "no such context: #{name.inspect}"
}
RestrictName =

Regex filter for spec names. Only specs matching this pattern will run.

//
RestrictContext =

Regex filter for context names. Only contexts matching this pattern will run.

//
Backtraces =

Whether to include backtraces in TAP diagnostic output on failure.

true
VERSION =
"1.0.0"

Class Method Summary collapse

Class Method Details

.handle_requirement(description, indent = 0, local_n = 1) ⇒ Object

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.



140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/scampi.rb', line 140

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

.load_test_file(file) ⇒ Object

Load a test file, queuing whatever specs it defines.

Two styles are supported:

  1. Co-located __END__ tests -- the file's real code runs (Ruby stops parsing at __END__), then the section after __END__ is evaluated as spec code. Because DATA/__END__ is only populated for the directly-run script, we read and eval the tail ourselves, preserving the original file and line numbers for backtraces.

  2. Plain spec files -- files with describe/it at the top level and no __END__ are simply loaded.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/scampi.rb', line 97

def self.load_test_file(file)
  src = File.read(file)

  # Run the implementation code. Ruby ignores everything past __END__.
  old_verbose, $-w = $-w, nil
  load file
  $-w = old_verbose

  # If there's an __END__ section, eval its body as spec code.
  return unless src =~ /^__END__$/
  head, tail = src.split(/^__END__$\n?/, 2)
  return if tail.nil? || tail.strip.empty?

  lineno = head.count("\n") + 2  # first line after the __END__ marker
  eval(tail, TOPLEVEL_BINDING, file, lineno)
end

.queueObject

The global queue of test items (contexts and raw specs).



44
45
46
# File 'lib/scampi.rb', line 44

def self.queue
  @queue
end

.runObject

Run all queued tests and emit TAP version 14 output.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/scampi.rb', line 49

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

.run_bare_spec(description, block, n) ⇒ Object

Run a single spec that lives outside any describe block.



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/scampi.rb', line 160

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

.summary_on_exitObject Also known as: summary_at_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.



116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/scampi.rb', line 116

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