mspec 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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