must_be 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/README.md +348 -0
- data/Rakefile +27 -0
- data/VERSION +1 -0
- data/doc/readme/examples.rb +262 -0
- data/doc/readme/run_examples.rb +47 -0
- data/lib/must_be/attr_typed.rb +78 -0
- data/lib/must_be/basic.rb +120 -0
- data/lib/must_be/containers.rb +291 -0
- data/lib/must_be/containers_registered_classes.rb +83 -0
- data/lib/must_be/core.rb +247 -0
- data/lib/must_be/nonstandard_control_flow.rb +159 -0
- data/lib/must_be/proxy.rb +62 -0
- data/lib/must_be.rb +9 -0
- data/must_be.gemspec +71 -0
- data/spec/must_be/attr_typed_spec.rb +225 -0
- data/spec/must_be/basic_spec.rb +578 -0
- data/spec/must_be/containers_spec.rb +952 -0
- data/spec/must_be/core_spec.rb +675 -0
- data/spec/must_be/nonstandard_control_flow_spec.rb +845 -0
- data/spec/must_be/proxy_spec.rb +194 -0
- data/spec/notify_matcher_spec.rb +59 -0
- data/spec/spec_helper.rb +180 -0
- data/spec/typical_usage_spec.rb +176 -0
- metadata +98 -0
data/.gitignore
ADDED
data/README.md
ADDED
@@ -0,0 +1,348 @@
|
|
1
|
+
# must_be Runtime Assertions
|
2
|
+
|
3
|
+
Ruby doesn't have a static type system. Tests and specs are well and good. But at the end of the day, Cthulhu reigns and sanity needs checking. must_be provides runtime assertions in all kinds of delicious Ruby flavors.
|
4
|
+
|
5
|
+
You can configure must_be to notify you of trouble in any way imaginable. Just set `MustBe.notifier` to any proc or other call worthy object. By default must_be raises an error, but logging and debugging notifiers come included. Furthermore, you can disable must_be at any time if its performance penalty proves unsatisfactory.
|
6
|
+
|
7
|
+
# Examples
|
8
|
+
|
9
|
+
After installing:
|
10
|
+
|
11
|
+
> sudo gem install must_be
|
12
|
+
|
13
|
+
Begin with customary oblations:
|
14
|
+
|
15
|
+
require 'rubygems'
|
16
|
+
require 'must_be'
|
17
|
+
|
18
|
+
Now `Object` has a number of `must`* and `must_not`* methods. Here are several examples of each. When an example notifies, the message corresponding to its `MustBe::Note` error is listed on the following comment line (`#=>` or `#~>` for a regexp match).
|
19
|
+
|
20
|
+
## Basic
|
21
|
+
|
22
|
+
For your everyday, average sanity:
|
23
|
+
|
24
|
+
87.must_be_a(Float, Integer)
|
25
|
+
87.must_be_a(Symbol, String)
|
26
|
+
#=> 87.must_be_a(Symbol, String), but is a Fixnum
|
27
|
+
|
28
|
+
87.must_not_be_a(Symbol, String, "message")
|
29
|
+
87.must_not_be_a(Float, Integer, "message")
|
30
|
+
#=> 87.must_not_be_a(Float, Integer, "message"), but is a Fixnum
|
31
|
+
|
32
|
+
`must_be_in` uses `include?` to check. Multiple arguments are grouped together as an array:
|
33
|
+
|
34
|
+
2.must_be_in(1, 2, 3)
|
35
|
+
2.must_be_in([1, 2, 3])
|
36
|
+
2.must_be_in(1 => 3, 2 => 4)
|
37
|
+
2.must_be_in(1..3)
|
38
|
+
2.must_be_in
|
39
|
+
#=> 2.must_be_in
|
40
|
+
|
41
|
+
2.must_not_be_in
|
42
|
+
2.must_not_be_in(4, 5)
|
43
|
+
2.must_not_be_in(1 => 3, 2 => 4)
|
44
|
+
#=> 2.must_not_be_in({1=>3, 2=>4})
|
45
|
+
|
46
|
+
`must_be_boolean` ensures the value is `true` or `false`:
|
47
|
+
|
48
|
+
true.must_be_boolean
|
49
|
+
false.must_be_boolean
|
50
|
+
nil.must_be_boolean
|
51
|
+
#=> nil.must_be_boolean
|
52
|
+
|
53
|
+
Sometimes close is good enough:
|
54
|
+
|
55
|
+
2.must_be_close(2.0)
|
56
|
+
2.must_be_close(2.01)
|
57
|
+
2.must_be_close(2.1)
|
58
|
+
#=> 2.must_be_close(2.1, 0.1), difference is 0.1
|
59
|
+
|
60
|
+
2.must_be_close(2.1, 6)
|
61
|
+
2.must_be_close(9.0, 6)
|
62
|
+
#=> 2.must_be_close(9.0, 6), difference is 7.0
|
63
|
+
|
64
|
+
`must_be` uses case (`===`) equality:
|
65
|
+
|
66
|
+
34.must_be(Integer, :all)
|
67
|
+
:all.must_be(Integer, :all)
|
68
|
+
:none.must_be(Integer, :all)
|
69
|
+
#=> :none.must_be(Integer, :all), but matches Symbol
|
70
|
+
|
71
|
+
5.must_be(1..5)
|
72
|
+
5.must_be(1...5)
|
73
|
+
#=> 5.must_be(1...5), but matches Fixnum
|
74
|
+
|
75
|
+
5.must_not_be(1...5)
|
76
|
+
3.must_not_be(1...5)
|
77
|
+
#=> 3.must_not_be(1...5), but matches Fixnum
|
78
|
+
|
79
|
+
true.must_be
|
80
|
+
nil.must_be
|
81
|
+
#=> nil.must_be, but matches NilClass
|
82
|
+
false.must_be
|
83
|
+
#=> false.must_be, but matches FalseClass
|
84
|
+
|
85
|
+
nil.must_not_be
|
86
|
+
:hello.must_not_be
|
87
|
+
#=> :hello.must_not_be, but matches Symbol
|
88
|
+
|
89
|
+
:yep.must_be(lambda {|v| v == :yep})
|
90
|
+
:nope.must_be(lambda {|v| v == :yep})
|
91
|
+
#~> :nope.must_be\(#<Proc:0x[^.]+?>\), but matches Symbol
|
92
|
+
|
93
|
+
:yep.must_not_be(lambda {|v| v == :nope})
|
94
|
+
:nope.must_not_be(lambda {|v| v == :nope})
|
95
|
+
#~> :nope.must_not_be\(#<Proc:0x[^.]+?>\), but matches Symbol
|
96
|
+
|
97
|
+
## Proxy
|
98
|
+
|
99
|
+
`must` either takes a block, or it returns a proxy for checking predicates:
|
100
|
+
|
101
|
+
7.must {|x| x > 3}
|
102
|
+
7.must {|x| x < 3}
|
103
|
+
#=> 7.must {}
|
104
|
+
|
105
|
+
7.must_not {|x| x < 3}
|
106
|
+
7.must_not {|x| x > 3}
|
107
|
+
#=> 7.must_not {}
|
108
|
+
|
109
|
+
7.must == 7
|
110
|
+
7.must > 3
|
111
|
+
7.must.even?
|
112
|
+
#=> 7.must.even?
|
113
|
+
|
114
|
+
7.must_not == 8
|
115
|
+
7.must_not < 3
|
116
|
+
7.must_not.odd?
|
117
|
+
#=> 7.must_not.odd?
|
118
|
+
|
119
|
+
## Module#attr_typed
|
120
|
+
|
121
|
+
`attr_typed` is like `attr_accessor` but with type checking:
|
122
|
+
|
123
|
+
class Typed
|
124
|
+
attr_typed :v, Symbol
|
125
|
+
attr_typed :n, Fixnum, Bignum
|
126
|
+
attr_typed :o, &:odd?
|
127
|
+
end
|
128
|
+
|
129
|
+
t = Typed.new
|
130
|
+
t.v = :hello
|
131
|
+
t.v = "world"
|
132
|
+
#=> attribute `v' must be a Symbol, but value "world" is a String
|
133
|
+
|
134
|
+
t.n = 411
|
135
|
+
t.n = 4.1
|
136
|
+
#=> attribute `n' must be a Fixnum or Bignum, but value 4.1 is a Float
|
137
|
+
|
138
|
+
t.o = 7
|
139
|
+
t.o = 8
|
140
|
+
#=> attribute `o' cannot be 8
|
141
|
+
|
142
|
+
## Containers
|
143
|
+
|
144
|
+
It's good to be sure that an array or hash contains the right stuff:
|
145
|
+
|
146
|
+
["okay", :ready, "go"].must_only_contain(Symbol, String)
|
147
|
+
["okay", :ready, 4].must_only_contain(Symbol, String)
|
148
|
+
#=> must_only_contain: 4.must_be(Symbol, String), but matches Fixnum in container ["okay", :ready, 4]
|
149
|
+
|
150
|
+
["okay", :ready, "go"].must_not_contain(Numeric)
|
151
|
+
["okay", :ready, 4].must_not_contain(Numeric)
|
152
|
+
#=> must_not_contain: 4.must_not_be(Numeric), but matches Fixnum in container ["okay", :ready, 4]
|
153
|
+
|
154
|
+
[].must_only_contain(:yes, :no)
|
155
|
+
[:yep].must_only_contain(:yes, :no)
|
156
|
+
#=> must_only_contain: :yep.must_be(:yes, :no), but matches Symbol in container [:yep]
|
157
|
+
|
158
|
+
[].must_not_contain(:yes, :no)
|
159
|
+
[:yes, :no].must_not_contain(:yes, :no)
|
160
|
+
#=> must_not_contain: :yes.must_not_be(:yes, :no), but matches Symbol in container [:yes, :no]
|
161
|
+
|
162
|
+
[0, [], ""].must_only_contain
|
163
|
+
[nil].must_only_contain
|
164
|
+
#=> must_only_contain: nil.must_be, but matches NilClass in container [nil]
|
165
|
+
|
166
|
+
[nil, false].must_not_contain
|
167
|
+
[0].must_not_contain
|
168
|
+
#=> must_not_contain: 0.must_not_be, but matches Fixnum in container [0]
|
169
|
+
|
170
|
+
{:welcome => :home}.must_only_contain(Symbol => Symbol)
|
171
|
+
{:symbol => :s, :fixnum => 5}.must_only_contain(Symbol => [Symbol, Fixnum])
|
172
|
+
{5 => :s, 6 => 5, :t => 5, :s => :s}.must_only_contain([Symbol, Fixnum] => [Symbol, Fixnum])
|
173
|
+
{6 => 5}.must_only_contain(Symbol => Fixnum, Fixnum => Symbol)
|
174
|
+
#=> must_only_contain: pair {6=>5} does not match [{Symbol=>Fixnum, Fixnum=>Symbol}] in container {6=>5}
|
175
|
+
|
176
|
+
{:welcome => nil}.must_not_contain(nil => Object)
|
177
|
+
{nil => :welcome}.must_not_contain(nil => Object)
|
178
|
+
#=> must_not_contain: pair {nil=>:welcome} matches [{nil=>Object}] in container {nil=>:welcome}
|
179
|
+
|
180
|
+
{:welcome => :home}.must_only_contain
|
181
|
+
{:welcome => nil}.must_only_contain
|
182
|
+
#=> must_only_contain: pair {:welcome=>nil} does not match [] in container {:welcome=>nil}
|
183
|
+
|
184
|
+
{nil => false, false => nil}.must_not_contain
|
185
|
+
{nil => 0}.must_not_contain
|
186
|
+
#=> must_not_contain: pair {nil=>0} matches [] in container {nil=>0}
|
187
|
+
|
188
|
+
It's better to be sure that a array or hash always contains the right stuff:
|
189
|
+
|
190
|
+
numbers = [].must_only_ever_contain(Numeric)
|
191
|
+
numbers << 3
|
192
|
+
numbers << :float
|
193
|
+
#=> must_only_ever_contain: Array#<<(:float): :float.must_be(Numeric), but matches Symbol in container [3, :float]
|
194
|
+
|
195
|
+
financials = [1, 4, 9].must_never_ever_contain(Float)
|
196
|
+
financials.map!{|x| Math.sqrt(x)}
|
197
|
+
#=> must_never_ever_contain: Array#map! {}: 3.0.must_not_be(Float), but matches Float in container [1.0, 2.0, 3.0]
|
198
|
+
|
199
|
+
## MustBe::MustOnlyEverContain.register
|
200
|
+
|
201
|
+
It's best to be sure that your custom container contains the right stuff:
|
202
|
+
|
203
|
+
class Box
|
204
|
+
attr_accessor :contents
|
205
|
+
|
206
|
+
def self.[](contents = nil)
|
207
|
+
new(contents)
|
208
|
+
end
|
209
|
+
|
210
|
+
def initialize(contents = nil)
|
211
|
+
@contents = nil
|
212
|
+
end
|
213
|
+
|
214
|
+
def each
|
215
|
+
yield(contents) unless contents.nil?
|
216
|
+
end
|
217
|
+
|
218
|
+
def empty!
|
219
|
+
self.contents = nil
|
220
|
+
end
|
221
|
+
|
222
|
+
def inspect
|
223
|
+
"Box[#{contents.inspect}]"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
MustOnlyEverContain.register(Box) do
|
228
|
+
def contents=(contents)
|
229
|
+
must_check_member(contents)
|
230
|
+
super
|
231
|
+
end
|
232
|
+
|
233
|
+
must_check_contents_after :empty!
|
234
|
+
end
|
235
|
+
|
236
|
+
box = Box[:hello].must_only_ever_contain(Symbol)
|
237
|
+
box.contents = :world
|
238
|
+
box.contents = 987
|
239
|
+
#=> must_only_ever_contain: Box#contents=(987): 987.must_be(Symbol), but matches Fixnum in container Box[987]
|
240
|
+
|
241
|
+
box = Box[2].must_never_ever_contain(nil)
|
242
|
+
box.contents = 64
|
243
|
+
box.empty!
|
244
|
+
#=> must_never_ever_contain: Box#empty!: nil.must_not_be(nil), but matches NilClass in container Box[nil]
|
245
|
+
|
246
|
+
## Core
|
247
|
+
|
248
|
+
Define new must-assertions by using `must_notify`:
|
249
|
+
|
250
|
+
must_notify("message")
|
251
|
+
#=> message
|
252
|
+
|
253
|
+
must_notify(:receiver, :method, [:args], nil, ", additional message")
|
254
|
+
#=> :receiver.method(:args), additional message
|
255
|
+
|
256
|
+
note = MustBe::Note.new("message")
|
257
|
+
must_notify(note)
|
258
|
+
#=> message
|
259
|
+
|
260
|
+
Use `must_check` to temporarily override `MustBe.notify` usually as part of a bigger, better must-assertion.
|
261
|
+
|
262
|
+
note = must_check {}
|
263
|
+
note.must == nil
|
264
|
+
|
265
|
+
note = must_check { :it.must_not_be }
|
266
|
+
must_notify(note)
|
267
|
+
#=> :it.must_not_be, but matches Symbol
|
268
|
+
|
269
|
+
def add_more_if_notifies
|
270
|
+
must_check(lambda do
|
271
|
+
yield
|
272
|
+
end) do |note|
|
273
|
+
MustBe::Note.new(note.message+" more")
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
result = add_more_if_notifies { 5 }
|
278
|
+
result.must == 5
|
279
|
+
|
280
|
+
add_more_if_notifies { must_notify("learn") }
|
281
|
+
#=> learn more
|
282
|
+
|
283
|
+
## Nonstandard Control Flow
|
284
|
+
|
285
|
+
Last, but not least, we have must-assertions for raising and throwing.
|
286
|
+
|
287
|
+
def rescuing
|
288
|
+
yield
|
289
|
+
rescue
|
290
|
+
raise if $!.is_a? MustBe::Note
|
291
|
+
end
|
292
|
+
|
293
|
+
rescuing { must_raise(/match/) { raise ArgumentError, "should match" } }
|
294
|
+
must_raise { "But it doesn't!" }
|
295
|
+
#=> main.must_raise {}, but nothing was raised
|
296
|
+
|
297
|
+
must_not_raise { "I'm fine."}
|
298
|
+
rescuing { must_not_raise { raise } }
|
299
|
+
#=> main.must_not_raise {}, but raised RuntimeError
|
300
|
+
|
301
|
+
catch(:ball) { must_throw { throw :ball } }
|
302
|
+
catch(:ball) { must_throw(:party) { throw :ball } }
|
303
|
+
#=> main.must_throw(:party) {}, but threw :ball
|
304
|
+
|
305
|
+
catch(:ball) { must_not_throw(:ball, :fiercely) { throw :ball, :gently } }
|
306
|
+
catch(:ball) { must_not_throw { throw :ball } }
|
307
|
+
#=> main.must_not_throw {}, but threw :ball
|
308
|
+
|
309
|
+
# Configuration
|
310
|
+
|
311
|
+
Set `MustBe.notifier` as needed. It takes a `MustBe::Note` as an argument and raises an exception unless its return value is false or nil:
|
312
|
+
|
313
|
+
MustBe.notifier = lambda do |note|
|
314
|
+
puts note
|
315
|
+
false
|
316
|
+
end
|
317
|
+
|
318
|
+
You can freely `MustBe.disable` and `MustBe.enable` as needed:
|
319
|
+
|
320
|
+
MustBe.disable
|
321
|
+
|
322
|
+
3.must == 4
|
323
|
+
# no message
|
324
|
+
|
325
|
+
MustBe.enable
|
326
|
+
|
327
|
+
3.must == 4
|
328
|
+
#=> 3.must.==(4)
|
329
|
+
|
330
|
+
Before requiring must_be, you can use `ENV['MUST_BE__NOTIFIER']` to set the notifier:
|
331
|
+
|
332
|
+
# Default: just raises the note.
|
333
|
+
ENV['MUST_BE__NOTIFIER'] = :raise
|
334
|
+
|
335
|
+
# Puts the note together with its backtrace.
|
336
|
+
ENV['MUST_BE__NOTIFIER'] = :log
|
337
|
+
|
338
|
+
# Invokes ruby-debug.
|
339
|
+
ENV['MUST_BE__NOTIFIER'] = :debug
|
340
|
+
|
341
|
+
# Calls MustBe.disable.
|
342
|
+
ENV['MUST_BE__NOTIFIER'] = :disable
|
343
|
+
|
344
|
+
By default `MustBe` is mixed into `Object`. If you want to mix `MustBe` selectively, set:
|
345
|
+
|
346
|
+
ENV['MUST_BE__DO_NOT_AUTOMATICALLY_INCLUDE_IN_OBJECT'] = "anything"
|
347
|
+
|
348
|
+
# Enjoy must_be.
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec/rake/spectask'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'jeweler'
|
5
|
+
Jeweler::Tasks.new do |gemspec|
|
6
|
+
gemspec.name = "must_be"
|
7
|
+
gemspec.summary = "must_be Runtime Assertions"
|
8
|
+
gemspec.description = "must_be provides runtime assertions which can easily be disabled in production environments. Likewise, the notifier can be customized to raise errors, log failure, enter the debugger, or anything else."
|
9
|
+
gemspec.email = "wtaysom@gmail.com"
|
10
|
+
gemspec.homepage = "http://github.com/wtaysom/must_be"
|
11
|
+
gemspec.authors = ["William Taysom"]
|
12
|
+
end
|
13
|
+
Jeweler::GemcutterTasks.new
|
14
|
+
rescue LoadError
|
15
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Run the spec suite against rcov"
|
19
|
+
Spec::Rake::SpecTask.new(:rcov_helper) do |t|
|
20
|
+
t.rcov = true
|
21
|
+
t.rcov_opts = ['--exclude', '/Library/Ruby/Gems/']
|
22
|
+
end
|
23
|
+
|
24
|
+
desc "Run the spec suite against rcov and open coverage results"
|
25
|
+
task :rcov => :rcov_helper do
|
26
|
+
`open coverage/index.html`
|
27
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
@@ -0,0 +1,262 @@
|
|
1
|
+
## Basic
|
2
|
+
|
3
|
+
87.must_be_a(Float, Integer)
|
4
|
+
87.must_be_a(Symbol, String)
|
5
|
+
#=> 87.must_be_a(Symbol, String), but is a Fixnum
|
6
|
+
|
7
|
+
87.must_not_be_a(Symbol, String, "message")
|
8
|
+
87.must_not_be_a(Float, Integer, "message")
|
9
|
+
#=> 87.must_not_be_a(Float, Integer, "message"), but is a Fixnum
|
10
|
+
|
11
|
+
2.must_be_in(1, 2, 3)
|
12
|
+
2.must_be_in([1, 2, 3])
|
13
|
+
2.must_be_in(1 => 3, 2 => 4)
|
14
|
+
2.must_be_in(1..3)
|
15
|
+
2.must_be_in
|
16
|
+
#=> 2.must_be_in
|
17
|
+
|
18
|
+
2.must_not_be_in
|
19
|
+
2.must_not_be_in(4, 5)
|
20
|
+
2.must_not_be_in(1 => 3, 2 => 4)
|
21
|
+
#=> 2.must_not_be_in({1=>3, 2=>4})
|
22
|
+
|
23
|
+
true.must_be_boolean
|
24
|
+
false.must_be_boolean
|
25
|
+
nil.must_be_boolean
|
26
|
+
#=> nil.must_be_boolean
|
27
|
+
|
28
|
+
2.must_be_close(2.0)
|
29
|
+
2.must_be_close(2.01)
|
30
|
+
2.must_be_close(2.1)
|
31
|
+
#=> 2.must_be_close(2.1, 0.1), difference is 0.1
|
32
|
+
|
33
|
+
2.must_be_close(2.1, 6)
|
34
|
+
2.must_be_close(9.0, 6)
|
35
|
+
#=> 2.must_be_close(9.0, 6), difference is 7.0
|
36
|
+
|
37
|
+
34.must_be(Integer, :all)
|
38
|
+
:all.must_be(Integer, :all)
|
39
|
+
:none.must_be(Integer, :all)
|
40
|
+
#=> :none.must_be(Integer, :all), but matches Symbol
|
41
|
+
|
42
|
+
5.must_be(1..5)
|
43
|
+
5.must_be(1...5)
|
44
|
+
#=> 5.must_be(1...5), but matches Fixnum
|
45
|
+
|
46
|
+
5.must_not_be(1...5)
|
47
|
+
3.must_not_be(1...5)
|
48
|
+
#=> 3.must_not_be(1...5), but matches Fixnum
|
49
|
+
|
50
|
+
true.must_be
|
51
|
+
nil.must_be
|
52
|
+
#=> nil.must_be, but matches NilClass
|
53
|
+
false.must_be
|
54
|
+
#=> false.must_be, but matches FalseClass
|
55
|
+
|
56
|
+
nil.must_not_be
|
57
|
+
:hello.must_not_be
|
58
|
+
#=> :hello.must_not_be, but matches Symbol
|
59
|
+
|
60
|
+
:yep.must_be(lambda {|v| v == :yep})
|
61
|
+
:nope.must_be(lambda {|v| v == :yep})
|
62
|
+
#~> :nope.must_be\(#<Proc:0x[^.]+?>\), but matches Symbol
|
63
|
+
|
64
|
+
:yep.must_not_be(lambda {|v| v == :nope})
|
65
|
+
:nope.must_not_be(lambda {|v| v == :nope})
|
66
|
+
#~> :nope.must_not_be\(#<Proc:0x[^.]+?>\), but matches Symbol
|
67
|
+
|
68
|
+
## Proxy
|
69
|
+
|
70
|
+
7.must {|x| x > 3}
|
71
|
+
7.must {|x| x < 3}
|
72
|
+
#=> 7.must {}
|
73
|
+
|
74
|
+
7.must_not {|x| x < 3}
|
75
|
+
7.must_not {|x| x > 3}
|
76
|
+
#=> 7.must_not {}
|
77
|
+
|
78
|
+
7.must == 7
|
79
|
+
7.must > 3
|
80
|
+
7.must.even?
|
81
|
+
#=> 7.must.even?
|
82
|
+
|
83
|
+
7.must_not == 8
|
84
|
+
7.must_not < 3
|
85
|
+
7.must_not.odd?
|
86
|
+
#=> 7.must_not.odd?
|
87
|
+
|
88
|
+
## `Module#attr_typed`
|
89
|
+
|
90
|
+
class Typed
|
91
|
+
attr_typed :v, Symbol
|
92
|
+
attr_typed :n, Fixnum, Bignum
|
93
|
+
attr_typed :o, &:odd?
|
94
|
+
end
|
95
|
+
|
96
|
+
t = Typed.new
|
97
|
+
t.v = :hello
|
98
|
+
t.v = "world"
|
99
|
+
#=> attribute `v' must be a Symbol, but value "world" is a String
|
100
|
+
|
101
|
+
t.n = 411
|
102
|
+
t.n = 4.1
|
103
|
+
#=> attribute `n' must be a Fixnum or Bignum, but value 4.1 is a Float
|
104
|
+
|
105
|
+
t.o = 7
|
106
|
+
t.o = 8
|
107
|
+
#=> attribute `o' cannot be 8
|
108
|
+
|
109
|
+
## Containers
|
110
|
+
|
111
|
+
["okay", :ready, "go"].must_only_contain(Symbol, String)
|
112
|
+
["okay", :ready, 4].must_only_contain(Symbol, String)
|
113
|
+
#=> must_only_contain: 4.must_be(Symbol, String), but matches Fixnum in container ["okay", :ready, 4]
|
114
|
+
|
115
|
+
["okay", :ready, "go"].must_not_contain(Numeric)
|
116
|
+
["okay", :ready, 4].must_not_contain(Numeric)
|
117
|
+
#=> must_not_contain: 4.must_not_be(Numeric), but matches Fixnum in container ["okay", :ready, 4]
|
118
|
+
|
119
|
+
[].must_only_contain(:yes, :no)
|
120
|
+
[:yep].must_only_contain(:yes, :no)
|
121
|
+
#=> must_only_contain: :yep.must_be(:yes, :no), but matches Symbol in container [:yep]
|
122
|
+
|
123
|
+
[].must_not_contain(:yes, :no)
|
124
|
+
[:yes, :no].must_not_contain(:yes, :no)
|
125
|
+
#=> must_not_contain: :yes.must_not_be(:yes, :no), but matches Symbol in container [:yes, :no]
|
126
|
+
|
127
|
+
[0, [], ""].must_only_contain
|
128
|
+
[nil].must_only_contain
|
129
|
+
#=> must_only_contain: nil.must_be, but matches NilClass in container [nil]
|
130
|
+
|
131
|
+
[nil, false].must_not_contain
|
132
|
+
[0].must_not_contain
|
133
|
+
#=> must_not_contain: 0.must_not_be, but matches Fixnum in container [0]
|
134
|
+
|
135
|
+
{:welcome => :home}.must_only_contain(Symbol => Symbol)
|
136
|
+
{:symbol => :s, :fixnum => 5}.must_only_contain(Symbol => [Symbol, Fixnum])
|
137
|
+
{5 => :s, 6 => 5, :t => 5, :s => :s}.must_only_contain([Symbol, Fixnum] => [Symbol, Fixnum])
|
138
|
+
{6 => 5}.must_only_contain(Symbol => Fixnum, Fixnum => Symbol)
|
139
|
+
#=> must_only_contain: pair {6=>5} does not match [{Symbol=>Fixnum, Fixnum=>Symbol}] in container {6=>5}
|
140
|
+
|
141
|
+
{:welcome => nil}.must_not_contain(nil => Object)
|
142
|
+
{nil => :welcome}.must_not_contain(nil => Object)
|
143
|
+
#=> must_not_contain: pair {nil=>:welcome} matches [{nil=>Object}] in container {nil=>:welcome}
|
144
|
+
|
145
|
+
{:welcome => :home}.must_only_contain
|
146
|
+
{:welcome => nil}.must_only_contain
|
147
|
+
#=> must_only_contain: pair {:welcome=>nil} does not match [] in container {:welcome=>nil}
|
148
|
+
|
149
|
+
{nil => false, false => nil}.must_not_contain
|
150
|
+
{nil => 0}.must_not_contain
|
151
|
+
#=> must_not_contain: pair {nil=>0} matches [] in container {nil=>0}
|
152
|
+
|
153
|
+
numbers = [].must_only_ever_contain(Numeric)
|
154
|
+
numbers << 3
|
155
|
+
numbers << :float
|
156
|
+
#=> must_only_ever_contain: Array#<<(:float): :float.must_be(Numeric), but matches Symbol in container [3, :float]
|
157
|
+
|
158
|
+
financials = [1, 4, 9].must_never_ever_contain(Float)
|
159
|
+
financials.map!{|x| Math.sqrt(x)}
|
160
|
+
#=> must_never_ever_contain: Array#map! {}: 3.0.must_not_be(Float), but matches Float in container [1.0, 2.0, 3.0]
|
161
|
+
|
162
|
+
## MustBe::MustOnlyEverContain.register
|
163
|
+
|
164
|
+
class Box
|
165
|
+
attr_accessor :contents
|
166
|
+
|
167
|
+
def self.[](contents = nil)
|
168
|
+
new(contents)
|
169
|
+
end
|
170
|
+
|
171
|
+
def initialize(contents = nil)
|
172
|
+
@contents = nil
|
173
|
+
end
|
174
|
+
|
175
|
+
def each
|
176
|
+
yield(contents) unless contents.nil?
|
177
|
+
end
|
178
|
+
|
179
|
+
def empty!
|
180
|
+
self.contents = nil
|
181
|
+
end
|
182
|
+
|
183
|
+
def inspect
|
184
|
+
"Box[#{contents.inspect}]"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
MustOnlyEverContain.register(Box) do
|
189
|
+
def contents=(contents)
|
190
|
+
must_check_member(contents)
|
191
|
+
super
|
192
|
+
end
|
193
|
+
|
194
|
+
must_check_contents_after :empty!
|
195
|
+
end
|
196
|
+
|
197
|
+
box = Box[:hello].must_only_ever_contain(Symbol)
|
198
|
+
box.contents = :world
|
199
|
+
box.contents = 987
|
200
|
+
#=> must_only_ever_contain: Box#contents=(987): 987.must_be(Symbol), but matches Fixnum in container Box[987]
|
201
|
+
|
202
|
+
box = Box[2].must_never_ever_contain(nil)
|
203
|
+
box.contents = 64
|
204
|
+
box.empty!
|
205
|
+
#=> must_never_ever_contain: Box#empty!: nil.must_not_be(nil), but matches NilClass in container Box[nil]
|
206
|
+
|
207
|
+
## Core
|
208
|
+
|
209
|
+
must_notify("message")
|
210
|
+
#=> message
|
211
|
+
|
212
|
+
must_notify(:receiver, :method, [:args], nil, ", additional message")
|
213
|
+
#=> :receiver.method(:args), additional message
|
214
|
+
|
215
|
+
note = MustBe::Note.new("message")
|
216
|
+
must_notify(note)
|
217
|
+
#=> message
|
218
|
+
|
219
|
+
note = must_check {}
|
220
|
+
note.must == nil
|
221
|
+
|
222
|
+
note = must_check { :it.must_not_be }
|
223
|
+
must_notify(note)
|
224
|
+
#=> :it.must_not_be, but matches Symbol
|
225
|
+
|
226
|
+
def add_more_if_notifies
|
227
|
+
must_check(lambda do
|
228
|
+
yield
|
229
|
+
end) do |note|
|
230
|
+
MustBe::Note.new(note.message+" more")
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
result = add_more_if_notifies { 5 }
|
235
|
+
result.must == 5
|
236
|
+
|
237
|
+
add_more_if_notifies { must_notify("learn") }
|
238
|
+
#=> learn more
|
239
|
+
|
240
|
+
## Nonstandard Control Flow
|
241
|
+
|
242
|
+
def rescuing
|
243
|
+
yield
|
244
|
+
rescue
|
245
|
+
raise if $!.is_a? MustBe::Note
|
246
|
+
end
|
247
|
+
|
248
|
+
rescuing { must_raise(/match/) { raise ArgumentError, "should match" } }
|
249
|
+
must_raise { "But it doesn't!" }
|
250
|
+
#=> main.must_raise {}, but nothing was raised
|
251
|
+
|
252
|
+
must_not_raise { "I'm fine."}
|
253
|
+
rescuing { must_not_raise { raise } }
|
254
|
+
#=> main.must_not_raise {}, but raised RuntimeError
|
255
|
+
|
256
|
+
catch(:ball) { must_throw { throw :ball } }
|
257
|
+
catch(:ball) { must_throw(:party) { throw :ball } }
|
258
|
+
#=> main.must_throw(:party) {}, but threw :ball
|
259
|
+
|
260
|
+
catch(:ball) { must_not_throw(:ball, :fiercely) { throw :ball, :gently } }
|
261
|
+
catch(:ball) { must_not_throw { throw :ball } }
|
262
|
+
#=> main.must_not_throw {}, but threw :ball
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'lib/must_be'
|
2
|
+
|
3
|
+
$expecting_note = false
|
4
|
+
|
5
|
+
class UnexpectedNoteError < StandardError; end
|
6
|
+
class MismatchedNoteError < StandardError; end
|
7
|
+
|
8
|
+
MustBe.notifier = lambda do |note|
|
9
|
+
unless $expecting_note
|
10
|
+
$note = note
|
11
|
+
raise UnexpectedNoteError
|
12
|
+
end
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def log(message, details)
|
17
|
+
puts "expected: #{message.is_a?(Regexp) ? message.inspect : message}"
|
18
|
+
puts details
|
19
|
+
end
|
20
|
+
|
21
|
+
def check_note(message)
|
22
|
+
$expecting_note = true
|
23
|
+
yield
|
24
|
+
log(message, "but did not notify")
|
25
|
+
rescue Note => note
|
26
|
+
unless message === note.message
|
27
|
+
log(message, " found: #{note.message}")
|
28
|
+
raise MismatchedNoteError
|
29
|
+
end
|
30
|
+
ensure
|
31
|
+
$expecting_note = false
|
32
|
+
end
|
33
|
+
|
34
|
+
example_text = IO.read(File.dirname(__FILE__)+"/examples.rb")
|
35
|
+
example_text.gsub!(/^(.*)\n#=> (.*)$/, "check_note(%{\\2}) {\n\\1}")
|
36
|
+
example_text.gsub!(/^(.*)\n#~> (.*)$/, "check_note(%r{\\2}) {\n\\1}")
|
37
|
+
|
38
|
+
begin
|
39
|
+
eval(example_text)
|
40
|
+
puts "Examples are okay."
|
41
|
+
rescue UnexpectedNoteError => ex
|
42
|
+
eval_frame = ex.backtrace.last
|
43
|
+
eval_frame =~ /:(\d+)$/
|
44
|
+
eval_line = $1.to_i
|
45
|
+
puts "example.rb:#{eval_line}: unexpected note: #=> #{$note}"
|
46
|
+
rescue MismatchedNoteError
|
47
|
+
end
|