assert2 0.3.6 → 0.3.8

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,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