assert2 0.3.6 → 0.3.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,246 @@
1
+ =begin
2
+ One Yury Kotlyarov recently posted this Rails project as a question:
3
+
4
+ http://github.com/yura/howto-rspec-custom-matchers/tree/master
5
+
6
+ It asks: How to write an RSpec matcher that specifies an HTML
7
+ <form> contains certain fields, and enforces their properties
8
+ and nested structure? He proposed [the equivalent of] this:
9
+
10
+ get :new # a Rails "functional" test - on a controller
11
+
12
+ assert_xhtml do
13
+ form :action => '/users' do
14
+ fieldset do
15
+ legend 'Personal Information'
16
+ label 'First name'
17
+ input :type => 'text', :name => 'user[first_name]'
18
+ end
19
+ end
20
+ end
21
+
22
+ The form in question is a familiar user login page:
23
+
24
+ <form action="/users">
25
+ <fieldset>
26
+ <legend>Personal Information</legend>
27
+ <ol>
28
+ <li id="control_user_first_name">
29
+ <label for="user_first_name">First name</label>
30
+ <input type="text" name="user[first_name]" id="user_first_name" />
31
+ </li>
32
+ </ol>
33
+ </fieldset>
34
+ </form>
35
+
36
+ If that form were full of <%= eRB %> tags, testing it would be
37
+ mission-critical. (Adding such eRB tags is left as an exercise for
38
+ the reader!)
39
+
40
+ This post creates a custom matcher that satisfies the following
41
+ requirements:
42
+
43
+ - the specification <em>looks like</em> the target code
44
+ * (except that it's in Ruby;)
45
+ - the specification can declare any HTML element type
46
+ _without_ cluttering our namespaces
47
+ - our matcher can match attributes exactly
48
+ - our matcher strips leading and trailing blanks from text
49
+ - the matcher enforces node order. if the specification puts
50
+ a list in collating order, for example, the HTML's order
51
+ must match
52
+ - the specification only requires the attributes and structural
53
+ elements that its matcher demands; we skip the rest -
54
+ such as the <ol> and <li> fields. They can change
55
+ freely as our website upgrades
56
+ - at fault time, the matcher prints out the failing elements
57
+ and their immediate context.
58
+
59
+ First, we take care of the paperwork. This spec works with Yuri's
60
+ sample website. I add Nokogiri, for our XML engine:
61
+ =end
62
+
63
+ require 'nokogiri'
64
+
65
+ =begin
66
+ That block after "response.body.should be_html_with" answers
67
+ Yuri's question. Any HTML we can think of, we can specify
68
+ it in there.
69
+
70
+ If we inject a fault, such as :name => 'user[first_nome]', we
71
+ get this diagnostic:
72
+
73
+ <input type="text" name="user[first_nome]">
74
+ does not match
75
+ <fieldset>
76
+ <legend>Personal Information</legend>
77
+ <ol>
78
+ <li id="control_user_first_name">
79
+ <label for="user_first_name">First name</label>
80
+ <input type="text" name="user[first_name]" id="user_first_name">
81
+ </li>
82
+ </ol>
83
+ </fieldset>
84
+
85
+ The diagnostic only reported the fault's immediate
86
+ context - the <fieldset> where the matcher sought the
87
+ errant <input> field. It would not, for example, spew
88
+ an entire website into our faces.
89
+
90
+ To support that specification, we will create a new
91
+ RSpec "matcher":
92
+ =end
93
+
94
+ class BeHtmlWith
95
+
96
+ def matches?(stwing, &block)
97
+ # @scope.wrap_expectation self do
98
+ begin
99
+ bwock = block || @block || proc{}
100
+ builder = Nokogiri::HTML::Builder.new(&bwock)
101
+ match = builder.doc.root
102
+ doc = Nokogiri::HTML(stwing)
103
+ @last_match = 0
104
+ @failure_message = match_nodes(match, doc)
105
+ return @failure_message.nil?
106
+ end
107
+ # end
108
+ end
109
+
110
+ =begin
111
+ The trick up our sleeve is Nokogiri::HTML::Builder. We passed
112
+ the matching block into it - that's where all the 'form',
113
+ 'fieldset', 'input', etc. elements came from. And this trick
114
+ exposes both our target page and our matched elements to the
115
+ full power of Nokogiri. Schema validation, for example, would
116
+ be very easy.
117
+
118
+ The matches? method works by building two DOMs, and forcing
119
+ our page's DOM to satisfy each element, attribute, and text
120
+ in our specification's DOM.
121
+
122
+ To match nodes, we first find all nodes, by name, below
123
+ the current node. Note that match_nodes() recurses. Then
124
+ we throw away all nodes that don't satisfy our matching
125
+ criteria.
126
+
127
+ We pick the first node that passes that check, and
128
+ then recursively match its children to each child,
129
+ if any, from our matching node.
130
+ =end
131
+
132
+ # TODO does a multi-modal top axis work?
133
+
134
+ def match_nodes(match, doc)
135
+ node = doc.xpath("descendant::#{match.name.sub(/\!$/, '')}").
136
+ select{|n| resemble(match, n) }.
137
+ first or return complaint(match, doc)
138
+
139
+ this_match = node.xpath('preceding::*').length
140
+
141
+ if @last_match > this_match
142
+ return complaint(match, doc, 'node is out of specified order!')
143
+ end
144
+
145
+ @last_match = this_match
146
+
147
+ # http://www.zvon.org/xxl/XPathTutorial/Output/example18.html
148
+ # The preceding axis contains all nodes in the same document
149
+ # as the context node that are before the context node in
150
+ # document order, excluding any ancestors and excluding
151
+ # attribute nodes and namespace nodes
152
+
153
+ #p [node.name, node.text]
154
+ # p node.path if lastest
155
+ #p node.text
156
+ # p lastest.path if lastest
157
+
158
+ # TODO try xpath('*')
159
+ match.children.grep(Nokogiri::XML::Element).each do |child|
160
+ issue = match_nodes(child, node) and
161
+ return issue
162
+ end
163
+
164
+ return nil
165
+ end
166
+
167
+ =begin
168
+ At any point in that recursion, if we can't find a match,
169
+ we build a string describing that situation, and pass it
170
+ back up the call stack. This immediately stops any iterating
171
+ and recursing underway!
172
+
173
+ Two nodes "resemble" each other if their names are the
174
+ same (naturally!); if your matching element's
175
+ attributes are a subset of your page's element's
176
+ attributes, and if their text is similar:
177
+ =end
178
+
179
+ def resemble(match, node)
180
+ keys = match.attributes.keys
181
+ node_keys = valuate(node.attributes.select{|k,v| keys.include? k })
182
+ match_keys = valuate(match.attributes)
183
+ node_keys == match_keys or return false
184
+
185
+ # TODO try
186
+ # match_text = match.xpath('text()').map{|x|x.to_s}
187
+ # node_text = match.xpath('text()').map{|x|x.to_s}
188
+
189
+ match_text = match.children.grep(Nokogiri::XML::Text).map{|t| t.to_s.strip }
190
+ node_text = node .children.grep(Nokogiri::XML::Text).map{|t| t.to_s.strip }
191
+ match_text.empty? or 0 == ( match_text - node_text ).length
192
+ end
193
+
194
+ =begin
195
+ That method cannot simply compare node.text, because Nokogiri
196
+ conglomerates all that node's descendants' texts together, and
197
+ these would gum up our search. So those elaborate lines with
198
+ grep() and map() serve to extract all the current node's
199
+ immediate textual children, then compare them as sets.
200
+
201
+ Put another way, <form> does not appear to contain "First name".
202
+ Specifications can only match text by declaring their immediate
203
+ parent.
204
+
205
+ The remaining support methods are self-explanatory. They
206
+ prepare Node attributes for comparison, build our diagnostics,
207
+ and plug our matcher object into RSpec:
208
+ =end
209
+
210
+ def valuate(attributes)
211
+ attributes.inject({}) do |h,(k,v)|
212
+ h.merge(k => v.value)
213
+ end # this converts objects to strings, so our Hashes
214
+ end # can compare for equality
215
+
216
+ def complaint(node, match, berate = nil)
217
+ "\n #{berate}".rstrip +
218
+ "\n\n#{node.to_html}\n" +
219
+ " does not match\n\n" +
220
+ match.to_html
221
+ end
222
+
223
+ attr_accessor :failure_message
224
+
225
+ def negative_failure_message
226
+ "yack yack yack"
227
+ end
228
+
229
+ def initialize(scope, &block)
230
+ @scope, @block = scope, block
231
+ end
232
+ end
233
+
234
+ module Test::Unit::Assertions
235
+ def assert_xhtml(xhtml = @response.body, &block) # TODO merge
236
+ _assert_xml(xhtml) # , XML::HTMLParser)
237
+ if block
238
+ # require 'should_be_html_with_spec'
239
+ matcher = BeHtmlWith.new(self, &block)
240
+ matcher.matches?(xhtml, &block)
241
+ message = matcher.failure_message
242
+ flunk message if message.to_s != ''
243
+ end
244
+ return @xdoc
245
+ end
246
+ end
@@ -0,0 +1,246 @@
1
+ =begin
2
+ One Yury Kotlyarov recently posted this Rails project as a question:
3
+
4
+ http://github.com/yura/howto-rspec-custom-matchers/tree/master
5
+
6
+ It asks: How to write an RSpec matcher that specifies an HTML
7
+ <form> contains certain fields, and enforces their properties
8
+ and nested structure? He proposed [the equivalent of] this:
9
+
10
+ get :new # a Rails "functional" test - on a controller
11
+
12
+ assert_xhtml do
13
+ form :action => '/users' do
14
+ fieldset do
15
+ legend 'Personal Information'
16
+ label 'First name'
17
+ input :type => 'text', :name => 'user[first_name]'
18
+ end
19
+ end
20
+ end
21
+
22
+ The form in question is a familiar user login page:
23
+
24
+ <form action="/users">
25
+ <fieldset>
26
+ <legend>Personal Information</legend>
27
+ <ol>
28
+ <li id="control_user_first_name">
29
+ <label for="user_first_name">First name</label>
30
+ <input type="text" name="user[first_name]" id="user_first_name" />
31
+ </li>
32
+ </ol>
33
+ </fieldset>
34
+ </form>
35
+
36
+ If that form were full of <%= eRB %> tags, testing it would be
37
+ mission-critical. (Adding such eRB tags is left as an exercise for
38
+ the reader!)
39
+
40
+ This post creates a custom matcher that satisfies the following
41
+ requirements:
42
+
43
+ - the specification <em>looks like</em> the target code
44
+ * (except that it's in Ruby;)
45
+ - the specification can declare any HTML element type
46
+ _without_ cluttering our namespaces
47
+ - our matcher can match attributes exactly
48
+ - our matcher strips leading and trailing blanks from text
49
+ - the matcher enforces node order. if the specification puts
50
+ a list in collating order, for example, the HTML's order
51
+ must match
52
+ - the specification only requires the attributes and structural
53
+ elements that its matcher demands; we skip the rest -
54
+ such as the <ol> and <li> fields. They can change
55
+ freely as our website upgrades
56
+ - at fault time, the matcher prints out the failing elements
57
+ and their immediate context.
58
+
59
+ First, we take care of the paperwork. This spec works with Yuri's
60
+ sample website. I add Nokogiri, for our XML engine:
61
+ =end
62
+
63
+ require 'nokogiri'
64
+
65
+ =begin
66
+ That block after "response.body.should be_html_with" answers
67
+ Yuri's question. Any HTML we can think of, we can specify
68
+ it in there.
69
+
70
+ If we inject a fault, such as :name => 'user[first_nome]', we
71
+ get this diagnostic:
72
+
73
+ <input type="text" name="user[first_nome]">
74
+ does not match
75
+ <fieldset>
76
+ <legend>Personal Information</legend>
77
+ <ol>
78
+ <li id="control_user_first_name">
79
+ <label for="user_first_name">First name</label>
80
+ <input type="text" name="user[first_name]" id="user_first_name">
81
+ </li>
82
+ </ol>
83
+ </fieldset>
84
+
85
+ The diagnostic only reported the fault's immediate
86
+ context - the <fieldset> where the matcher sought the
87
+ errant <input> field. It would not, for example, spew
88
+ an entire website into our faces.
89
+
90
+ To support that specification, we will create a new
91
+ RSpec "matcher":
92
+ =end
93
+
94
+ class BeHtmlWith
95
+
96
+ def matches?(stwing, &block)
97
+ # @scope.wrap_expectation self do
98
+ begin
99
+ bwock = block || @block || proc{}
100
+ builder = Nokogiri::HTML::Builder.new(&bwock)
101
+ match = builder.doc.root
102
+ doc = Nokogiri::HTML(stwing)
103
+ @last_match = 0
104
+ @failure_message = match_nodes(match, doc)
105
+ return @failure_message.nil?
106
+ end
107
+ # end
108
+ end
109
+
110
+ =begin
111
+ The trick up our sleeve is Nokogiri::HTML::Builder. We passed
112
+ the matching block into it - that's where all the 'form',
113
+ 'fieldset', 'input', etc. elements came from. And this trick
114
+ exposes both our target page and our matched elements to the
115
+ full power of Nokogiri. Schema validation, for example, would
116
+ be very easy.
117
+
118
+ The matches? method works by building two DOMs, and forcing
119
+ our page's DOM to satisfy each element, attribute, and text
120
+ in our specification's DOM.
121
+
122
+ To match nodes, we first find all nodes, by name, below
123
+ the current node. Note that match_nodes() recurses. Then
124
+ we throw away all nodes that don't satisfy our matching
125
+ criteria.
126
+
127
+ We pick the first node that passes that check, and
128
+ then recursively match its children to each child,
129
+ if any, from our matching node.
130
+ =end
131
+
132
+ # TODO does a multi-modal top axis work?
133
+
134
+ def match_nodes(match, doc)
135
+ node = doc.xpath("descendant::#{match.name.sub(/\!$/, '')}").
136
+ select{|n| resemble(match, n) }.
137
+ first or return complaint(match, doc)
138
+
139
+ this_match = node.xpath('preceding::*').length
140
+
141
+ if @last_match > this_match
142
+ return complaint(match, doc, 'node is out of specified order!')
143
+ end
144
+
145
+ @last_match = this_match
146
+
147
+ # http://www.zvon.org/xxl/XPathTutorial/Output/example18.html
148
+ # The preceding axis contains all nodes in the same document
149
+ # as the context node that are before the context node in
150
+ # document order, excluding any ancestors and excluding
151
+ # attribute nodes and namespace nodes
152
+
153
+ #p [node.name, node.text]
154
+ # p node.path if lastest
155
+ #p node.text
156
+ # p lastest.path if lastest
157
+
158
+ # TODO try xpath('*')
159
+ match.children.grep(Nokogiri::XML::Element).each do |child|
160
+ issue = match_nodes(child, node) and
161
+ return issue
162
+ end
163
+
164
+ return nil
165
+ end
166
+
167
+ =begin
168
+ At any point in that recursion, if we can't find a match,
169
+ we build a string describing that situation, and pass it
170
+ back up the call stack. This immediately stops any iterating
171
+ and recursing underway!
172
+
173
+ Two nodes "resemble" each other if their names are the
174
+ same (naturally!); if your matching element's
175
+ attributes are a subset of your page's element's
176
+ attributes, and if their text is similar:
177
+ =end
178
+
179
+ def resemble(match, node)
180
+ keys = match.attributes.keys
181
+ node_keys = valuate(node.attributes.select{|k,v| keys.include? k })
182
+ match_keys = valuate(match.attributes)
183
+ node_keys == match_keys or return false
184
+
185
+ # TODO try
186
+ # match_text = match.xpath('text()').map{|x|x.to_s}
187
+ # node_text = match.xpath('text()').map{|x|x.to_s}
188
+
189
+ match_text = match.children.grep(Nokogiri::XML::Text).map{|t| t.to_s.strip }
190
+ node_text = node .children.grep(Nokogiri::XML::Text).map{|t| t.to_s.strip }
191
+ match_text.empty? or 0 == ( match_text - node_text ).length
192
+ end
193
+
194
+ =begin
195
+ That method cannot simply compare node.text, because Nokogiri
196
+ conglomerates all that node's descendants' texts together, and
197
+ these would gum up our search. So those elaborate lines with
198
+ grep() and map() serve to extract all the current node's
199
+ immediate textual children, then compare them as sets.
200
+
201
+ Put another way, <form> does not appear to contain "First name".
202
+ Specifications can only match text by declaring their immediate
203
+ parent.
204
+
205
+ The remaining support methods are self-explanatory. They
206
+ prepare Node attributes for comparison, build our diagnostics,
207
+ and plug our matcher object into RSpec:
208
+ =end
209
+
210
+ def valuate(attributes)
211
+ attributes.inject({}) do |h,(k,v)|
212
+ h.merge(k => v.value)
213
+ end # this converts objects to strings, so our Hashes
214
+ end # can compare for equality
215
+
216
+ def complaint(node, match, berate = nil)
217
+ "\n #{berate}".rstrip +
218
+ "\n\n#{node.to_html}\n" +
219
+ " does not match\n\n" +
220
+ match.to_html
221
+ end
222
+
223
+ attr_accessor :failure_message
224
+
225
+ def negative_failure_message
226
+ "yack yack yack"
227
+ end
228
+
229
+ def initialize(scope, &block)
230
+ @scope, @block = scope, block
231
+ end
232
+ end
233
+
234
+ module Test::Unit::Assertions
235
+ def assert_xhtml(xhtml = @response.body, &block) # TODO merge
236
+ _assert_xml(xhtml) # , XML::HTMLParser)
237
+ if block
238
+ # require 'should_be_html_with_spec'
239
+ matcher = BeHtmlWith.new(self, &block)
240
+ matcher.matches?(xhtml, &block)
241
+ message = matcher.failure_message
242
+ flunk message if message.any?
243
+ end
244
+ return @xdoc
245
+ end
246
+ end
data/lib/assert2/xpath.rb CHANGED
@@ -3,6 +3,7 @@ require 'assert2'
3
3
  require 'rexml/document'
4
4
  require 'rexml/entity'
5
5
  require 'rexml/formatters/pretty'
6
+ require 'nokogiri' # must be installed to use xpath{}!
6
7
 
7
8
  module Test; module Unit; module Assertions
8
9
 
@@ -30,6 +31,30 @@ module Test; module Unit; module Assertions
30
31
  end
31
32
  end
32
33
 
34
+ def assert_xhtml_(xhtml)
35
+ return _assert_xml_(xhtml) # , XML::HTMLParser)
36
+ end
37
+
38
+ def _assert_xml_(xml) #, parser = XML::Parser)
39
+ if false
40
+ xp = parser.new()
41
+ xp.string = xml
42
+
43
+ if XML.respond_to? :'default_pedantic_parser='
44
+ XML.default_pedantic_parser = true
45
+ else
46
+ XML::Parser.default_pedantic_parser = true
47
+ end # CONSIDER uh, figure out the best libxml-ruby??
48
+
49
+ @xdoc = xp.parse.root
50
+ else
51
+ # TODO figure out how entities are supposed to work!!
52
+ xml = xml.gsub('&mdash;', '--')
53
+ doc = Nokogiri::XML(xml)
54
+ @xdoc = doc.root
55
+ end
56
+ end
57
+
33
58
  class AssertXPathArguments
34
59
 
35
60
  def initialize(path = '', id = nil, options = {})
@@ -102,6 +127,35 @@ module Test; module Unit; module Assertions
102
127
  @xdoc = former_xdoc
103
128
  end # TODO trap LibXML::XML::XPath::InvalidPath and explicate it's an XPath problem
104
129
 
130
+ def xpath_(path, id = nil, options = {}, &block)
131
+ former_xdoc = @xdoc
132
+ apa = AssertXPathArguments.new(path, id, options)
133
+ node = @xdoc.xpath(apa.xpath) #, nil, apa.subs)
134
+ # TODO advise Nokogiri to provide substitution arguments
135
+
136
+ add_diagnostic :clear do
137
+ diagnostic = "xpath: #{ apa.xpath.inspect }\n"
138
+ diagnostic << "arguments: #{ apa.subs.pretty_inspect }\n" if apa.subs.any?
139
+ diagnostic + "xml context:\n" + indent_xml
140
+ end
141
+
142
+ if node
143
+ def node.[](symbol)
144
+ return attributes[symbol.to_s]
145
+ end
146
+ end
147
+
148
+ if block
149
+ assert_('this xpath cannot find a node', :keep_diagnostics => true){ node }
150
+ assert_ nil, :args => [@xdoc = node], :keep_diagnostics => true, &block # TODO need the _ ?
151
+ end
152
+
153
+ return node
154
+ # TODO raid http://thebogles.com/blog/an-hpricot-style-interface-to-libxml/
155
+ ensure
156
+ @xdoc = former_xdoc
157
+ end # TODO trap LibXML::XML::XPath::InvalidPath and explicate it's an XPath problem
158
+
105
159
  def indent_xml(node = @xdoc)
106
160
  bar = REXML::Formatters::Pretty.new
107
161
  out = String.new
data/lib/assert2.rb CHANGED
@@ -81,6 +81,7 @@ module Test; module Unit; module Assertions
81
81
  end
82
82
 
83
83
  module Coulor #:nodoc:
84
+ # TODO shell into term-ansicolor!
84
85
  def colorize(we_color)
85
86
  @@we_color = we_color
86
87
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: assert2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.6
4
+ version: 0.3.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Phlip
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-02-15 00:00:00 -08:00
12
+ date: 2009-03-11 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -23,14 +23,15 @@ extra_rdoc_files: []
23
23
 
24
24
  files:
25
25
  - lib/assert2
26
+ - lib/assert2/xhtml.rb~
26
27
  - lib/assert2/flunk.rb
27
28
  - lib/assert2/rubynode_reflector.rb
28
29
  - lib/assert2/xpath.rb
29
30
  - lib/assert2/ripper_reflector.rb
30
31
  - lib/assert2/ripdoc.html.erb
31
32
  - lib/assert2/ripdoc.rb
33
+ - lib/assert2/xhtml.rb
32
34
  - lib/assert2.rb
33
- - lib/assert2.rb~
34
35
  has_rdoc: false
35
36
  homepage: http://assert2.rubyforge.org/
36
37
  post_install_message:
data/lib/assert2.rb~ DELETED
@@ -1,344 +0,0 @@
1
- require 'test/unit'
2
-
3
- # FIXME the first failing assertion of a batch should suggest you get with Ruby1.9...
4
- # TODO install Coulor (flibberty)
5
- # TODO add :verbose => option to assert{}
6
- # TODO pay for Staff Benda Bilili ALBUM: Tr�s Tr�s Fort (Promo Sampler) !
7
- # TODO evaluate parts[3]
8
- # ERGO if the block is a block, decorate with do-end
9
- # ERGO decorate assert_latest's block at fault time
10
-
11
- #~ if RUBY_VERSION > '1.8.6'
12
- #~ puts "\nWarning: This version of assert{ 2.0 } requires\n" +
13
- #~ "RubyNode, which only works on Ruby versions < 1.8.7.\n" +
14
- #~ "Upgrade to Ruby1.9, and try 'gem install assert21'\n\n"
15
- #~ end
16
-
17
- #~ def colorize(whatever)
18
- #~ # FIXME stop ignoring this and start colorizing v2.1!
19
- #~ end
20
-
21
- if RUBY_VERSION < '1.9.0'
22
- require 'assert2/rubynode_reflector'
23
- else
24
- require 'assert2/ripper_reflector'
25
- end
26
-
27
- # CONSIDER fix if an assertion contains more than one command - reflect it all!
28
-
29
- module Test; module Unit; module Assertions
30
-
31
- FlunkError = if defined? Test::Unit::AssertionFailedError
32
- Test::Unit::AssertionFailedError
33
- else
34
- MiniTest::Assertion
35
- end
36
-
37
- def add_diagnostic(whatever = nil, &block)
38
- @__additional_diagnostics ||= [] # TODO move that inside the reflector object, and persist it thru a test case event
39
-
40
- if whatever == :clear
41
- @__additional_diagnostics = []
42
- whatever = nil
43
- end
44
-
45
- @__additional_diagnostics += [whatever, block] # note .compact will take care of them if they don't exist
46
- end
47
-
48
- def assert(*args, &block)
49
- # This assertion calls a block, and faults if it returns
50
- # +false+ or +nil+. The fault diagnostic will reflect the
51
- # assertion's complete source - with comments - and will
52
- # reevaluate the every variable and expression in the
53
- # block.
54
- #
55
- # The first argument can be a diagnostic string:
56
- #
57
- # assert("foo failed"){ foo() }
58
- #
59
- # The fault diagnostic will print that line.
60
- #
61
- # The next time you think to write any of these assertions...
62
- #
63
- # - +assert+
64
- # - +assert_equal+
65
- # - +assert_instance_of+
66
- # - +assert_kind_of+
67
- # - +assert_operator+
68
- # - +assert_match+
69
- # - +assert_not_nil+
70
- #
71
- # use <code>assert{ 2.1 }</code> instead.
72
- #
73
- # If no block is provided, the assertion calls +assert_classic+,
74
- # which simulates RubyUnit's standard <code>assert()</code>.
75
- if block
76
- assert_ *args, &block
77
- else
78
- assert_classic *args
79
- end
80
- return true # or die trying ;-)
81
- end
82
-
83
- module Coulor #:nodoc:
84
- def colorize(we_color)
85
- @@we_color = we_color
86
- end
87
- unless defined? BOLD
88
- BOLD = "\e[1m"
89
- CLEAR = "\e[0m"
90
- end # ERGO modularize these; anneal with Win32
91
- def colour(text, colour_code)
92
- return colour_code + text + CLEAR if colorize?
93
- return text
94
- end
95
- def colorize? # ERGO how other libraries set these options transparent??
96
- we_color = (@@we_color rescue true) # ERGO parens needed?
97
- return false if ENV['EMACS'] == 't'
98
- return (we_color == :always or we_color && $stdout.tty?)
99
- end
100
- def bold(text)
101
- return BOLD + text + CLEAR if colorize?
102
- return text
103
- end
104
- def green(text); colour(text, "\e[32m"); end
105
- def red(text); colour(text, "\e[31m"); end
106
- def magenta(text); colour(text, "\e[35m"); end
107
- def blue(text); colour(text, "\e[34m"); end
108
- def orange(text); colour(text, "\e[3Bm"); end
109
- end
110
-
111
- class RubyReflector
112
- attr_accessor :captured_block_vars,
113
- :args
114
-
115
- include Coulor
116
-
117
- def split_and_read(called)
118
- if called + ':' =~ /([^:]+):(\d+):/
119
- file, line = $1, $2.to_i
120
- return File.readlines(file)[line - 1 .. -1]
121
- end
122
-
123
- return nil
124
- end
125
-
126
- def __evaluate_diagnostics
127
- @__additional_diagnostics.each_with_index do |d, x|
128
- @__additional_diagnostics[x] = d.call if d.respond_to? :call
129
- end
130
- end # CONSIDER pass the same args as blocks take?
131
-
132
- def __build_message(reflection)
133
- __evaluate_diagnostics
134
- return (@__additional_diagnostics.uniq + [reflection]).compact.join("\n")
135
- end # TODO move this fluff to the ruby_reflector!
136
-
137
- def format_inspection(inspection, spaces)
138
- spaces = ' ' * spaces
139
- inspection = inspection.gsub('\n'){ "\\n\" +\n \"" } if inspection =~ /^".*"$/
140
- inspection = inspection.gsub("\n"){ "\n" + spaces }
141
- return inspection.lstrip
142
- end
143
-
144
- def format_assertion_result(assertion_source, inspection)
145
- spaces = " --> ".length
146
- inspection = format_inspection(inspection, spaces)
147
- return assertion_source.rstrip + "\n --> #{inspection.lstrip}\n"
148
- end
149
-
150
- def format_capture(width, snip, value)
151
- return "#{ format_snip(width, snip) } --> #{ format_value(width, value) }"
152
- end
153
-
154
- def format_value(width, value) # TODO width is a de-facto instance variable
155
- width += 4
156
- source = value.pretty_inspect.rstrip
157
- return format_inspection(source, width)
158
- end
159
-
160
- def measure_capture(kap)
161
- return kap.split("\n").inject(0){|x, v| v.strip.length > x ? v.strip.length : x } if kap.match("\n")
162
- kap.length
163
- # TODO need the if?
164
- end
165
-
166
- end
167
-
168
- def colorize(to_color)
169
- RubyReflector.new.colorize(to_color)
170
- end
171
-
172
- # TODO work with raw MiniTest
173
-
174
- # This is a copy of the classic assert, so your pre-existing
175
- # +assert+ calls will not change their behavior
176
- #
177
- if defined? MiniTest::Assertion
178
- def assert_classic(test, msg=nil)
179
- msg ||= "Failed assertion, no message given."
180
- self._assertions += 1
181
- unless test then
182
- msg = msg.call if Proc === msg
183
- raise MiniTest::Assertion, msg
184
- end
185
- true
186
- end
187
-
188
- def add_assertion
189
- self._assertions += 1
190
- end
191
- else
192
- def assert_classic(boolean, message=nil)
193
- #_wrap_assertion do
194
- assert_block("assert<classic> should not be called with a block.") { !block_given? }
195
- assert_block(build_message(message, "<?> is not true.", boolean)) { boolean }
196
- #end
197
- end
198
- end
199
-
200
- # The new <code>assert()</code> calls this to interpret
201
- # blocks of assertive statements.
202
- #
203
- def assert_(diagnostic = nil, options = {}, &block)
204
- options[:keep_diagnostics] or add_diagnostic :clear
205
-
206
- begin
207
- if got = block.call(*options[:args])
208
- add_assertion
209
- return got
210
- end
211
- rescue FlunkError
212
- raise # asserts inside assertions that fail do not decorate the outer assertion
213
- rescue => got
214
- add_exception got
215
- end
216
-
217
- flunk diagnose(diagnostic, got, caller[1], options, block)
218
- end
219
-
220
- def add_exception(ex)
221
- ex.backtrace[0..10].each do |line|
222
- add_diagnostic ' ' + line
223
- end
224
- end
225
-
226
- # This assertion replaces:
227
- #
228
- # - +assert_nil+
229
- # - +assert_no_match+
230
- # - +assert_not_equal+
231
- #
232
- # It faults, and prints its block's contents and values,
233
- # if its block returns non-+false+ and non-+nil+.
234
- #
235
- def deny(diagnostic = nil, options = {}, &block)
236
- # "None shall pass!" --the Black Knight
237
-
238
- options[:keep_diagnostics] or add_diagnostic :clear
239
-
240
- begin
241
- got = block.call(*options[:args]) or (add_assertion and return true)
242
- rescue FlunkError
243
- raise
244
- rescue => got
245
- add_exception got
246
- end
247
-
248
- flunk diagnose(diagnostic, got, caller[0], options, block)
249
- end # "You're a looney!" -- King Arthur
250
-
251
- def deny_(diagnostic = nil, options = {}, &block)
252
- # "None shall pass!" --the Black Knight
253
-
254
- options[:keep_diagnostics] or add_diagnostic :clear
255
-
256
- begin
257
- got = block.call(*options[:args]) or (add_assertion and return true)
258
- rescue FlunkError
259
- raise
260
- rescue => got
261
- add_exception got
262
- end
263
-
264
- flunk diagnose(diagnostic, got, caller[0], options, block)
265
- end # "You're a looney!" -- King Arthur
266
-
267
- # FIXME document why this deny_ is here, and how to alias it back to deny
268
-
269
- alias denigh deny # to line assert{ ... } and
270
- # denigh{ ... } statements up neatly!
271
-
272
- #~ def __reflect_assertion(called, options, block, got)
273
- #~ effect = RubyReflector.new(called)
274
- #~ effect.args = *options[:args]
275
- #~ return effect.reflect_assertion(block, got)
276
- #~ end
277
-
278
- #~ def __reflect_assertion(called, options, block, got)
279
- #~ effect = RubyReflector.new(called)
280
- #~ effect.args = *options[:args]
281
- #~ effect.block = block
282
- #~ return effect.reflect_assertion(block, got) # TODO merge this and its copies into assert2_utilities
283
- #~ end
284
-
285
- #!doc!
286
- def diagnose(diagnostic = nil, got = nil, called = caller[0],
287
- options = {}, block = nil) # TODO make this directly callable
288
- rf = RubyReflector.new
289
- rf.diagnose(diagnostic, got, called, options, block, @__additional_diagnostics)
290
- #~ options = { :args => [] }.merge(options)
291
- #~ # CONSIDER only capture the block_vars if there be args?
292
- #~ @__additional_diagnostics.unshift diagnostic
293
- #~ return __build_message(__reflect_assertion(called, options, block, got))
294
- end
295
-
296
- if RubyReflector::HAS_RUBYNODE
297
- # wrap this common idiom:
298
- # foo = assemble()
299
- # deny{ foo.bar() }
300
- # foo.activate()
301
- # assert{ foo.bar() }
302
- #
303
- # that becomes:
304
- # foo = assemble()
305
- #
306
- # assert_yin_yang proc{ foo.bar() } do
307
- # foo.activate()
308
- # end
309
- #
310
- def assert_yin_yang(*args, &block)
311
- # prock(s), diagnostic = nil, &block)
312
- procks, diagnostic = args.partition{|p| p.respond_to? :call }
313
- block ||= procks.shift
314
- source = reflect_source(&block)
315
- fuss = [diagnostic, "fault before calling:", source].compact.join("\n")
316
- procks.each do |prock| deny(fuss, &prock); end
317
- block.call
318
- fuss = [diagnostic, "fault after calling:", source].compact.join("\n")
319
- procks.each do |prock| assert(fuss, &prock); end
320
- end
321
-
322
- # the prock assertion must pass on both sides of the called block
323
- #
324
- def deny_yin_yang(*args, &block)
325
- # prock(s), diagnostic = nil, &block)
326
- procks, diagnostic = args.partition{|p| p.respond_to? :call }
327
- block ||= procks.shift
328
- source = reflect_source(&block)
329
- fuss = [diagnostic, "fault before calling:", source].compact.join("\n")
330
- procks.each do |prock| assert(fuss, &prock); end
331
- block.call
332
- fuss = [diagnostic, "fault after calling:", source].compact.join("\n")
333
- procks.each do |prock| assert(fuss, &prock); end
334
- end
335
-
336
- end
337
-
338
- end ; end ; end
339
-
340
- class File
341
- def self.write(filename, contents)
342
- open(filename, 'w'){|f| f.write(contents) }
343
- end
344
- end