mspec 1.3.1 → 1.4.0

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.
data/README CHANGED
@@ -91,6 +91,99 @@ All of these should be applied to a block created with `lambda` or `proc`:
91
91
  is associated with. The exception class can be given for finer-grained
92
92
  control (inheritance works normally so Exception would catch everything.)
93
93
 
94
+ == Nested 'describe' blocks
95
+
96
+ MSpec supports nesting one 'describe' block inside another. The examples in
97
+ the nested block are evaluated with all the before/after blocks of all the
98
+ containing 'describe' blocks. The following example illustrates this:
99
+
100
+ describe "Some#method" do
101
+ before :each do
102
+ @obj = 1
103
+ end
104
+
105
+ describe "when passed String" do
106
+ before :each do
107
+ @meth = :to_s
108
+ end
109
+
110
+ it "returns false" do
111
+ # when this example is evaluated, @obj = 1 and @meth = :to_s
112
+ end
113
+ end
114
+ end
115
+
116
+ The output when using the SpecdocFormatter (selected with -fs to the runners)
117
+ will be as follows:
118
+
119
+ Some#method when passed String
120
+ - returns false
121
+
122
+
123
+ == Shared 'describe' blocks
124
+
125
+ MSpec supports RSpec-style shared 'describe' blocks. MSpec also provides a
126
+ convenience method to assist in writing specs for the numerous aliased methods
127
+ that Ruby provides. The following example illustrates shared blocks:
128
+
129
+ describe :someclass_some_method, :shared => true do
130
+ it "does something" do
131
+ end
132
+ end
133
+
134
+ describe "SomeClass#some_method" do
135
+ it_should_behave_like "someclass_some_method"
136
+ end
137
+
138
+ The first argument to 'describe' for a shared block is an object that
139
+ duck-types as a String. The representation of the object must be unique. This
140
+ example uses a symbol. This was the convention for the previous facility that
141
+ MSpec provided for aliased method (#it_behaves_like). However, this convention
142
+ is not set in stone (but the uniqueness requirement is). Note that the
143
+ argument to the #it_should_behave_like is a String because at this time RSpec
144
+ will not find the shared block by the symbol.
145
+
146
+ MSpec continues to support the #it_behaves_like convenience method for
147
+ specifying aliased methods. The syntax is as follows:
148
+
149
+ it_behaves_like :symbol_matching_shared_describe, :method [, :object]
150
+
151
+ describe :someclass_some_method, :shared => true do
152
+ it "returns true" do
153
+ obj.send(@method).should be_true
154
+ end
155
+
156
+ it "returns something else" do
157
+ @object.send(@method).should be_something_else
158
+ end
159
+ end
160
+
161
+ # example #1
162
+ describe "SomeClass#some_method" do
163
+ it_behaves_like :someclass_some_method, :other_method
164
+ end
165
+
166
+ # example #2
167
+ describe "SomeOtherClass#some_method" do
168
+ it_behaves_like :someclass_some_method, :some_method, OtherClass
169
+ end
170
+
171
+ The first form above (#1) is used for typical aliases. That is, methods with
172
+ different names on the same class that behave identically. The
173
+ #it_behaves_like helper creates a before(:all) block that sets @method to
174
+ :other_method. The form of the first example block in the shared block
175
+ illustrates the typical form of a spec for an aliased method.
176
+
177
+ The second form above (#2) is used for methods on different classes that are
178
+ essentially aliases, even though Ruby does not provide a syntax for specifying
179
+ such methods as aliases. Examples are the methods on File, FileTest, and
180
+ File::Stat. In this case, the #it_behaves_like helper sets both @method and
181
+ @object in the before(:all) block (@method = :some_method, @object =
182
+ OtherClass in this example).
183
+
184
+ For shared specs that fall outside of either of these two narrow categories,
185
+ use nested or shared 'describe' blocks as appropriate and use the
186
+ #it_should_behave_like method directly.
94
187
 
95
188
  == Guards
96
189
 
@@ -61,8 +61,10 @@ class MSpecCI < MSpecScript
61
61
  MSpec.register_tags_patterns config[:tags_patterns]
62
62
  MSpec.register_files files
63
63
  TagFilter.new(:exclude, "fails").register
64
+ TagFilter.new(:exclude, "critical").register
64
65
  TagFilter.new(:exclude, "unstable").register
65
66
  TagFilter.new(:exclude, "incomplete").register
67
+ TagFilter.new(:exclude, "unsupported").register
66
68
 
67
69
  MSpec.process
68
70
  exit MSpec.exit_code
@@ -6,12 +6,13 @@ require 'mspec/matchers/be_false'
6
6
  require 'mspec/matchers/be_kind_of'
7
7
  require 'mspec/matchers/be_nil'
8
8
  require 'mspec/matchers/be_true'
9
- require 'mspec/matchers/equal'
9
+ require 'mspec/matchers/complain'
10
10
  require 'mspec/matchers/eql'
11
+ require 'mspec/matchers/equal'
12
+ require 'mspec/matchers/equal_element'
13
+ require 'mspec/matchers/equal_utf16'
11
14
  require 'mspec/matchers/include'
12
15
  require 'mspec/matchers/match_yaml'
13
16
  require 'mspec/matchers/raise_error'
14
17
  require 'mspec/matchers/output'
15
18
  require 'mspec/matchers/output_to_fd'
16
- require 'mspec/matchers/complain'
17
- require 'mspec/matchers/equal_utf16'
@@ -0,0 +1,78 @@
1
+ class EqualElementMatcher
2
+ def initialize(element, attributes = nil, content = nil, options = {})
3
+ @element = element
4
+ @attributes = attributes
5
+ @content = content
6
+ @options = options
7
+ end
8
+
9
+ def matches?(actual)
10
+ @actual = actual
11
+
12
+ matched = true
13
+
14
+ if @options[:not_closed]
15
+ matched &&= actual =~ /^#{Regexp.quote("<" + @element)}.*#{Regexp.quote(">" + (@content || ''))}$/
16
+ else
17
+ matched &&= actual =~ /^#{Regexp.quote("<" + @element)}/
18
+ matched &&= actual =~ /#{Regexp.quote("</" + @element + ">")}$/
19
+ matched &&= actual =~ /#{Regexp.quote(">" + @content + "</")}/ if @content
20
+ end
21
+
22
+ if @attributes
23
+ if @attributes.empty?
24
+ matched &&= actual.scan(/\w+\=\"(.*)\"/).size == 0
25
+ else
26
+ @attributes.each do |key, value|
27
+ if value == true
28
+ matched &&= (actual.scan(/#{Regexp.quote(key)}(\s|>)/).size == 1)
29
+ else
30
+ matched &&= (actual.scan(%Q{ #{key}="#{value}"}).size == 1)
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ !!matched
37
+ end
38
+
39
+ def failure_message
40
+ ["Expected #{@actual.pretty_inspect}",
41
+ "to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"]
42
+ end
43
+
44
+ def negative_failure_message
45
+ ["Expected #{@actual.pretty_inspect}",
46
+ "not to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"]
47
+ end
48
+
49
+ def attributes_for_failure_message
50
+ if @attributes
51
+ if @attributes.empty?
52
+ "no attributes"
53
+ else
54
+ @attributes.inject([]) { |memo, n| memo << %Q{#{n[0]}="#{n[1]}"} }.join(" ")
55
+ end
56
+ else
57
+ "any attributes"
58
+ end
59
+ end
60
+
61
+ def content_for_failure_message
62
+ if @content
63
+ if @content.empty?
64
+ "no content"
65
+ else
66
+ "#{@content.inspect} as content"
67
+ end
68
+ else
69
+ "any content"
70
+ end
71
+ end
72
+ end
73
+
74
+ class Object
75
+ def equal_element(*args)
76
+ EqualElementMatcher.new(*args)
77
+ end
78
+ end
@@ -13,84 +13,170 @@ require 'mspec/runner/example'
13
13
  # is evaluated, just as +it+ refers to the example itself.
14
14
  #++
15
15
  class ContextState
16
- attr_reader :state
17
-
18
- def initialize
19
- @start = []
20
- @before = []
21
- @after = []
22
- @finish = []
23
- @spec = []
16
+ attr_reader :state, :parent, :parents, :children, :examples, :to_s
17
+
18
+ def initialize(mod, options=nil)
19
+ @to_s = mod.to_s
20
+ if options.is_a? Hash
21
+ @options = options
22
+ else
23
+ @to_s += "#{".:#".include?(options[0,1]) ? "" : " "}#{options}" if options
24
+ @options = { }
25
+ end
26
+ @options[:shared] ||= false
27
+
28
+ @parsed = false
29
+ @before = { :all => [], :each => [] }
30
+ @after = { :all => [], :each => [] }
31
+ @pre = {}
32
+ @post = {}
33
+ @examples = []
34
+ @parent = nil
35
+ @parents = [self]
36
+ @children = []
37
+
24
38
  @mock_verify = lambda { Mock.verify_count }
25
39
  @mock_cleanup = lambda { Mock.cleanup }
26
40
  @expectation_missing = lambda { raise ExpectationNotFoundError }
27
41
  end
28
42
 
29
- def before(at=:each, &block)
30
- case at
31
- when :each
32
- @before << block
33
- when :all
34
- @start << block
35
- end
43
+ # Returns true if this is a shared +ContextState+. Essentially, when
44
+ # created with: describe "Something", :shared => true { ... }
45
+ def shared?
46
+ return @options[:shared]
36
47
  end
37
48
 
38
- def after(at=:each, &block)
39
- case at
40
- when :each
41
- @after << block
42
- when :all
43
- @finish << block
49
+ # Set the parent (enclosing) +ContextState+ for this state. Creates
50
+ # the +parents+ list.
51
+ def parent=(parent)
52
+ @description = nil
53
+ @parent = parent
54
+ parent.child self if parent and not shared?
55
+
56
+ state = parent
57
+ while state
58
+ parents.unshift state
59
+ state = state.parent
44
60
  end
45
61
  end
46
62
 
63
+ # Add the ContextState instance +child+ to the list of nested
64
+ # describe blocks.
65
+ def child(child)
66
+ @children << child
67
+ end
68
+
69
+ # Returns a list of all before(+what+) blocks from self and any parents.
70
+ def pre(what)
71
+ @pre[what] ||= parents.inject([]) { |l, s| l.push(*s.before(what)) }
72
+ end
73
+
74
+ # Returns a list of all after(+what+) blocks from self and any parents.
75
+ # The list is in reverse order. In other words, the blocks defined in
76
+ # inner describes are in the list before those defined in outer describes,
77
+ # and in a particular describe block those defined later are in the list
78
+ # before those defined earlier.
79
+ def post(what)
80
+ @post[what] ||= parents.inject([]) { |l, s| l.unshift(*s.after(what)) }
81
+ end
82
+
83
+ # Records before(:each) and before(:all) blocks.
84
+ def before(what, &block)
85
+ block ? @before[what].push(block) : @before[what]
86
+ end
87
+
88
+ # Records after(:each) and after(:all) blocks.
89
+ def after(what, &block)
90
+ block ? @after[what].unshift(block) : @after[what]
91
+ end
92
+
93
+ # Creates an ExampleState instance for the block and stores it
94
+ # in a list of examples to evaluate unless the example is filtered.
47
95
  def it(desc, &block)
48
- state = ExampleState.new @describe, desc
49
- @spec << [desc, block, state] unless state.filtered?
96
+ @examples << ExampleState.new(self, desc, block)
97
+ end
98
+
99
+ # Evaluates the block and resets the toplevel +ContextState+ to #parent.
100
+ def describe(&block)
101
+ @parsed = protect @to_s, block, false
102
+ MSpec.register_current parent
103
+ MSpec.register_shared self if shared?
104
+ end
105
+
106
+ # Returns a description string generated from self and all parents
107
+ def description
108
+ @description ||= parents.inject([]) { |l,s| l << s.to_s }.join(" ")
50
109
  end
51
110
 
52
- def describe(mod, desc=nil, &block)
53
- @describe = desc ? "#{mod} #{desc}" : mod.to_s
54
- @block = block
111
+ # Injects the before/after blocks and examples from the shared
112
+ # describe block into this +ContextState+ instance.
113
+ def it_should_behave_like(desc)
114
+ unless state = MSpec.retrieve_shared(desc)
115
+ raise Exception, "Unable to find shared 'describe' for #{desc}"
116
+ end
117
+
118
+ state.examples.each { |ex| ex.context = self; @examples << ex }
119
+ state.before(:all).each { |b| before :all, &b }
120
+ state.before(:each).each { |b| before :each, &b }
121
+ state.after(:each).each { |b| after :each, &b }
122
+ state.after(:all).each { |b| after :all, &b }
55
123
  end
56
124
 
125
+ # Evaluates each block in +blocks+ using the +MSpec.protect+ method
126
+ # so that exceptions are handled and tallied. Returns true and does
127
+ # NOT evaluate any blocks if +check+ is true and +MSpec.pretend_mode?+
128
+ # is true.
57
129
  def protect(what, blocks, check=true)
58
130
  return true if check and MSpec.pretend_mode?
59
131
  Array(blocks).all? { |block| MSpec.protect what, &block }
60
132
  end
61
133
 
134
+ # Removes filtered examples. Returns true if there are examples
135
+ # left to evaluate.
136
+ def filter_examples
137
+ @examples.reject! { |ex| ex.filtered? }
138
+ not @examples.empty?
139
+ end
140
+
141
+ # Evaluates the examples in a +ContextState+. Invokes the MSpec events
142
+ # for :enter, :before, :after, :leave.
62
143
  def process
63
- protect @describe, @block, false
64
- return unless @spec.any? { |desc, spec, state| state.unfiltered? }
65
-
66
- MSpec.shuffle @spec if MSpec.randomize?
67
- MSpec.actions :enter, @describe
68
-
69
- if protect "before :all", @start
70
- @spec.each do |desc, spec, state|
71
- @state = state
72
- MSpec.actions :before, state
73
-
74
- if protect("before :each", @before)
75
- MSpec.clear_expectations
76
- passed = protect nil, spec
77
- if spec
78
- MSpec.actions :example, state, spec
79
- protect nil, @expectation_missing unless MSpec.expectation? or not passed
144
+ MSpec.register_current self
145
+
146
+ if @parsed and filter_examples
147
+ MSpec.shuffle @examples if MSpec.randomize?
148
+ MSpec.actions :enter, description
149
+
150
+ if protect "before :all", pre(:all)
151
+ @examples.each do |state|
152
+ @state = state
153
+ example = state.example
154
+ MSpec.actions :before, state
155
+
156
+ if protect "before :each", pre(:each)
157
+ MSpec.clear_expectations
158
+ if example
159
+ passed = protect nil, example
160
+ MSpec.actions :example, state, example
161
+ protect nil, @expectation_missing unless MSpec.expectation? or not passed
162
+ end
163
+ protect "after :each", post(:each)
164
+ protect "Mock.verify_count", @mock_verify
80
165
  end
81
- protect "after :each", @after
82
- protect "Mock.verify_count", @mock_verify
83
- end
84
166
 
167
+ protect "Mock.cleanup", @mock_cleanup
168
+ MSpec.actions :after, state
169
+ @state = nil
170
+ end
171
+ protect "after :all", post(:all)
172
+ else
85
173
  protect "Mock.cleanup", @mock_cleanup
86
- MSpec.actions :after, state
87
- @state = nil
88
174
  end
89
- protect "after :all", @finish
90
- else
91
- protect "Mock.cleanup", @mock_cleanup
175
+
176
+ MSpec.actions :leave
92
177
  end
93
178
 
94
- MSpec.actions :leave
179
+ MSpec.register_current nil
180
+ children.each { |child| child.process }
95
181
  end
96
182
  end
@@ -3,35 +3,32 @@ require 'mspec/runner/mspec'
3
3
  # Holds some of the state of the example (i.e. +it+ block) that is
4
4
  # being evaluated. See also +ContextState+.
5
5
  class ExampleState
6
- def initialize(describe, it)
7
- @describe = describe
8
- @it = it
9
- @unfiltered = nil
10
- end
6
+ attr_reader :context, :it, :example
11
7
 
12
- def describe
13
- @describe
8
+ def initialize(context, it, example=nil)
9
+ @context = context
10
+ @it = it
11
+ @example = example
14
12
  end
15
13
 
16
- def it
17
- @it
14
+ def context=(context)
15
+ @description = nil
16
+ @context = context
18
17
  end
19
18
 
20
- def description
21
- @description ||= "#{@describe} #{@it}"
19
+ def describe
20
+ @context.description
22
21
  end
23
22
 
24
- def unfiltered?
25
- unless @unfiltered
26
- incl = MSpec.retrieve(:include) || []
27
- excl = MSpec.retrieve(:exclude) || []
28
- @unfiltered = incl.empty? || incl.any? { |f| f === description }
29
- @unfiltered &&= excl.empty? || !excl.any? { |f| f === description }
30
- end
31
- @unfiltered
23
+ def description
24
+ @description ||= "#{describe} #{@it}"
32
25
  end
33
26
 
34
27
  def filtered?
35
- not unfiltered?
28
+ incl = MSpec.retrieve(:include) || []
29
+ excl = MSpec.retrieve(:exclude) || []
30
+ included = incl.empty? || incl.any? { |f| f === description }
31
+ included &&= excl.empty? || !excl.any? { |f| f === description }
32
+ not included
36
33
  end
37
34
  end