scampi 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,166 @@
1
+ module Scampi
2
+ class Context
3
+ attr_reader :name, :block
4
+
5
+ def initialize(name, &block)
6
+ @name = name
7
+ @block = block
8
+ @items = []
9
+ @before = []
10
+ @after = []
11
+ end
12
+
13
+ # Phase 1: evaluate block to discover specs and children.
14
+ # Nothing is executed — it and describe just queue.
15
+ def register
16
+ tap do
17
+ if name =~ RestrictContext
18
+ instance_eval(&block)
19
+ else
20
+ self
21
+ end
22
+ end
23
+ end
24
+
25
+ # Count total specs recursively.
26
+ def count
27
+ @items.sum { |item|
28
+ case item[0]
29
+ when :spec then 1
30
+ when :child then item[1].count
31
+ else 0
32
+ end
33
+ }
34
+ end
35
+
36
+ # Phase 2: run all registered specs in order, emitting TAP subtests.
37
+ # +indent+ is the nesting depth (0 = inside a top-level describe).
38
+ # Returns true if all specs/children passed, false otherwise.
39
+ def execute(indent = 0)
40
+ prefix = " " * indent
41
+ inner = " " * (indent + 1)
42
+
43
+ plan = @items.count { |item| item[0] == :spec || item[0] == :child }
44
+
45
+ puts "#{prefix}# Subtest: #{@name}"
46
+ puts "#{inner}1..#{plan}"
47
+
48
+ befores = []
49
+ afters = []
50
+ local_n = 0
51
+ all_passed = true
52
+
53
+ @items.each { |item|
54
+ case item[0]
55
+ when :before
56
+ befores << item[1]
57
+ when :after
58
+ afters << item[1]
59
+ when :spec
60
+ Counter[:specifications] += 1
61
+ local_n += 1
62
+ passed = run_requirement(item[1], item[2], befores, afters, indent + 1, local_n)
63
+ all_passed = false unless passed
64
+ when :child
65
+ local_n += 1
66
+ child_passed = item[1].execute(indent + 1)
67
+ if child_passed
68
+ puts "#{inner}#{"ok".green} #{local_n} - #{item[1].name}"
69
+ else
70
+ puts "#{inner}#{"not ok".red} #{local_n} - #{item[1].name}"
71
+ end
72
+ all_passed = false unless child_passed
73
+ end
74
+ }
75
+
76
+ all_passed
77
+ end
78
+
79
+ def before(&block); @items << [:before, block]; @before << block; end
80
+ def after(&block); @items << [:after, block]; @after << block; end
81
+
82
+ def behaves_like(*names)
83
+ names.each { |name| instance_eval(&Shared[name]) }
84
+ end
85
+
86
+ def it(description, &block)
87
+ return unless description =~ RestrictName
88
+ block ||= proc { should.flunk "not implemented" }
89
+ @items << [:spec, description, block]
90
+ end
91
+
92
+ def should(*args, &block)
93
+ if Counter[:depth] == 0
94
+ it('should ' + args.first, &block)
95
+ else
96
+ super(*args, &block)
97
+ end
98
+ end
99
+
100
+ def run_requirement(description, spec, befores, afters, indent = 0, local_n = 1)
101
+ Scampi.handle_requirement(description, indent, local_n) do
102
+ begin
103
+ Counter[:depth] += 1
104
+ rescued = false
105
+ begin
106
+ befores.each { |block| instance_eval(&block) }
107
+ prev_req = Counter[:requirements]
108
+ instance_eval(&spec)
109
+ rescue Object => e
110
+ rescued = true
111
+ raise e
112
+ ensure
113
+ if Counter[:requirements] == prev_req and not rescued
114
+ raise Error.new(:missing,
115
+ "empty specification: #{@name} #{description}")
116
+ end
117
+ begin
118
+ afters.each { |block| instance_eval(&block) }
119
+ rescue Object => e
120
+ raise e unless rescued
121
+ end
122
+ end
123
+ rescue SystemExit, Interrupt
124
+ raise
125
+ rescue Object => e
126
+ ErrorLog << "#{e.class}: #{e.message}\n"
127
+ e.backtrace.find_all { |line| line !~ /bin\/scampi|\/scampi\.rb:\d+/ }.
128
+ each_with_index { |line, i|
129
+ ErrorLog << "\t#{line}#{i==0 ? ": #@name - #{description}" : ""}\n"
130
+ }
131
+ ErrorLog << "\n"
132
+
133
+ if e.kind_of? Error
134
+ Counter[e.count_as] += 1
135
+ e.count_as.to_s.upcase
136
+ else
137
+ Counter[:errors] += 1
138
+ "ERROR: #{e.class}"
139
+ end
140
+ else
141
+ ""
142
+ ensure
143
+ Counter[:depth] -= 1
144
+ end
145
+ end
146
+ end
147
+
148
+ def describe(*args, &block)
149
+ context = Scampi::Context.new(args.join(' '), &block)
150
+ (parent_context = self).methods(false).each { |e|
151
+ (class << context; self; end).send(:define_method, e) { |*args2, &block2|
152
+ parent_context.send(e, *args2, &block2)
153
+ }
154
+ }
155
+ @before.each { |b| context.before(&b) }
156
+ @after.each { |b| context.after(&b) }
157
+ context.register
158
+ @items << [:child, context]
159
+ context
160
+ end
161
+
162
+ def raise?(*args, &block) = block.raise?(*args)
163
+ def throw?(*args, &block) = block.throw?(*args)
164
+ def change?(&block) = lambda{}.change?(&block)
165
+ end
166
+ end
@@ -0,0 +1,10 @@
1
+ module Scampi
2
+ class Error < RuntimeError
3
+ attr_accessor :count_as
4
+
5
+ def initialize(count_as, message)
6
+ @count_as = count_as
7
+ super message
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,61 @@
1
+ class Object
2
+ def true? = false
3
+ def false? = false
4
+ end
5
+
6
+ class TrueClass
7
+ def true? = true
8
+ end
9
+
10
+ class FalseClass
11
+ def false? = true
12
+ end
13
+
14
+ class Proc
15
+ def raise?(*exceptions)
16
+ call
17
+ rescue *(exceptions.empty? ? RuntimeError : exceptions) => e
18
+ e
19
+ else
20
+ false
21
+ end
22
+
23
+ def throw?(sym)
24
+ catch(sym) {
25
+ call
26
+ return false
27
+ }
28
+ return true
29
+ end
30
+
31
+ def change?
32
+ pre_result = yield
33
+ call
34
+ post_result = yield
35
+ pre_result != post_result
36
+ end
37
+ end
38
+
39
+ class Numeric
40
+ def close?(to, delta)
41
+ (to.to_f - self).abs <= delta.to_f rescue false
42
+ end
43
+ end
44
+
45
+ class Object
46
+ def should(*args, &block)
47
+ Scampi::Should.new(self).be(*args, &block)
48
+ end
49
+ end
50
+
51
+ module Kernel
52
+ private
53
+
54
+ def describe(*args, &block)
55
+ Scampi.queue << Scampi::Context.new(args.join(' '), &block)
56
+ end
57
+
58
+ def shared(name, &block)
59
+ Scampi::Shared[name] = block
60
+ end
61
+ end
@@ -0,0 +1,67 @@
1
+ module Scampi
2
+ class Should
3
+ # Kills ==, ===, =~, eql?, equal?, frozen?, instance_of?, is_a?,
4
+ # kind_of?, nil?, respond_to?, tainted?
5
+ instance_methods.each { |name| undef_method name if name =~ /\?|^\W+$/ }
6
+
7
+ def initialize(object)
8
+ @object = object
9
+ @negated = false
10
+ end
11
+
12
+ def not(*args, &block)
13
+ @negated = !@negated
14
+
15
+ if args.empty?
16
+ self
17
+ else
18
+ be(*args, &block)
19
+ end
20
+ end
21
+
22
+ def be(*args, &block)
23
+ if args.empty?
24
+ self
25
+ else
26
+ unless block_given?
27
+ block = args.shift
28
+ end
29
+ satisfy(*args, &block)
30
+ end
31
+ end
32
+
33
+ alias a be
34
+ alias an be
35
+
36
+ def satisfy(description="", &block)
37
+ r = yield(@object)
38
+ if Scampi::Counter[:depth] > 0
39
+ Scampi::Counter[:requirements] += 1
40
+ raise Scampi::Error.new(:failed, description) unless @negated ^ r
41
+ r
42
+ else
43
+ @negated ? !r : !!r
44
+ end
45
+ end
46
+
47
+ def method_missing(name, *args, &block)
48
+ name = "#{name}?" if name.to_s =~ /\w[^?]\z/
49
+
50
+ desc = @negated ? "not ".dup : "".dup
51
+ desc << @object.inspect << "." << name.to_s
52
+ desc << "(" << args.map{|x|x.inspect}.join(", ") << ") failed"
53
+
54
+ satisfy(desc) { |x| x.__send__(name, *args, &block) }
55
+ end
56
+
57
+ def equal(value) = self == value
58
+ def match(value) = self =~ value
59
+
60
+ def identical_to(value) = self.equal? value
61
+ alias same_as identical_to
62
+
63
+ def flunk(reason="Flunked")
64
+ raise Scampi::Error.new(:failed, reason)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,5 @@
1
+ module Scampi
2
+ VERSION = "0.1.1"
3
+ end
4
+
5
+ require_relative '../rubygems_plugin'
@@ -0,0 +1,5 @@
1
+ module Scampi
2
+ VERSION = "<%= version %>"
3
+ end
4
+
5
+ require_relative '../rubygems_plugin'
data/lib/scampi.rb ADDED
@@ -0,0 +1,97 @@
1
+ # Scampi -- small RSpec clone with TAP output.
2
+ #
3
+ # Forked from Bacon by Christian Neukirchen.
4
+ # "Truth will sooner come out from error than from confusion." ---Francis Bacon
5
+
6
+ # Copyright (C) 2007, 2008, 2012 Christian Neukirchen <purl.org/net/chneukirchen>
7
+ #
8
+ # Scampi is freely distributable under the terms of an MIT-style license.
9
+ # See COPYING or http://www.opensource.org/licenses/mit-license.php.
10
+
11
+ require_relative 'scampi/version'
12
+ require 'colorize_extended'
13
+
14
+ module Scampi
15
+ Counter = Hash.new(0)
16
+ ErrorLog = "".dup
17
+ Shared = Hash.new { |_, name|
18
+ raise NameError, "no such context: #{name.inspect}"
19
+ }
20
+
21
+ RestrictName = // unless defined? RestrictName
22
+ RestrictContext = // unless defined? RestrictContext
23
+
24
+ Backtraces = true unless defined? Backtraces
25
+
26
+ @queue = []
27
+ @ran = false
28
+
29
+ def self.queue
30
+ @queue
31
+ end
32
+
33
+ def self.run
34
+ return if @ran
35
+ @ran = true
36
+
37
+ # Register: evaluate all describe blocks to discover specs
38
+ @queue.each(&:register)
39
+
40
+ # TAP version + plan (one entry per top-level describe)
41
+ puts "TAP version 14"
42
+ puts "1..#{@queue.size}"
43
+
44
+ # Execute all specs as subtests
45
+ @queue.each_with_index do |context, i|
46
+ passed = context.execute(0)
47
+ n = i + 1
48
+ if passed
49
+ puts "#{"ok".green} #{n} - #{context.name}"
50
+ else
51
+ puts "#{"not ok".red} #{n} - #{context.name}"
52
+ end
53
+ end
54
+
55
+ # Summary comment
56
+ tests, assertions, failures, errors =
57
+ Counter.values_at(:specifications, :requirements, :failed, :errors)
58
+ puts "# #{tests} tests, #{assertions} assertions, #{failures} failures, #{errors} errors"
59
+ end
60
+
61
+ def self.summary_on_exit
62
+ return if Counter[:installed_summary] > 0
63
+ @timer = Time.now
64
+ at_exit {
65
+ run
66
+ if $!
67
+ raise $!
68
+ elsif Counter[:errors] + Counter[:failed] > 0
69
+ exit 1
70
+ end
71
+ }
72
+ Counter[:installed_summary] += 1
73
+ end
74
+ class << self; alias summary_at_exit summary_on_exit; end
75
+
76
+ # TAP output
77
+
78
+ def self.handle_requirement(description, indent = 0, local_n = 1)
79
+ ErrorLog.replace ""
80
+ error = yield
81
+ prefix = " " * indent
82
+ if error.empty?
83
+ puts "#{prefix}#{"ok".green} #{local_n} - #{description}"
84
+ true
85
+ else
86
+ puts "#{prefix}#{"not ok".red} #{local_n} - #{description}: #{error}"
87
+ puts ErrorLog.strip.gsub(/^/, "#{prefix}# ") if Backtraces
88
+ false
89
+ end
90
+ end
91
+ end
92
+
93
+ require_relative 'scampi/error'
94
+ require_relative 'scampi/context'
95
+ require_relative 'scampi/should'
96
+ require_relative 'scampi/monkey_patches'
97
+ require_relative 'rubygems_plugin'
data/scampi.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ require_relative 'lib/scampi/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "scampi"
5
+ s.version = Scampi::VERSION
6
+ s.platform = Gem::Platform::RUBY
7
+ s.summary = "a small RSpec clone with built-in TAP support"
8
+ s.license = "MIT"
9
+
10
+ s.description = <<-EOF
11
+ Scampi is a small RSpec clone weighing less than 350 LoC but
12
+ nevertheless providing all essential features. Includes a
13
+ TAP (Test Anything Protocol) harness and assertion library.
14
+
15
+ http://github.com/general-intelligence-systems/scampi
16
+ EOF
17
+
18
+ s.files = `git ls-files`.split("\n") - [".gitignore"]
19
+ s.bindir = 'exe'
20
+ s.executables = ['scampi']
21
+ s.require_path = 'lib'
22
+ s.extra_rdoc_files = ['README.rdoc']
23
+ s.test_files = []
24
+
25
+ s.add_dependency 'colorize-extended'
26
+
27
+ s.author = 'Nathan K'
28
+ s.email = 'nathankidd@hey.com'
29
+ s.homepage = 'http://github.com/general-intelligence-systems/scampi'
30
+ end