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.
- data/lib/assert2/xhtml.rb +246 -0
- data/lib/assert2/xhtml.rb~ +246 -0
- data/lib/assert2/xpath.rb +54 -0
- data/lib/assert2.rb +1 -0
- metadata +4 -3
- data/lib/assert2.rb~ +0 -344
@@ -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('—', '--')
|
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
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.
|
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-
|
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
|