expectation 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/lib/contracts.rb +46 -64
- data/lib/contracts/expects.rb +46 -0
- data/lib/contracts/nothrows.rb +18 -0
- data/lib/contracts/returns.rb +26 -0
- data/lib/contracts/runtime.rb +47 -0
- data/lib/core/exception.rb +4 -4
- data/lib/expectation.rb +24 -67
- data/lib/expectation/assertions.rb +6 -6
- data/lib/expectation/matcher.rb +82 -0
- data/lib/expectation/multi_matcher.rb +25 -0
- data/lib/expectation/version.rb +3 -2
- data/test/contracts_test.rb +97 -21
- data/test/expection_test.rb +23 -1
- data/test/matching_test.rb +10 -8
- data/test/test_helper.rb +14 -9
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e82b68f20c078cadb99ae70a9fe2d3f73288680
|
4
|
+
data.tar.gz: 071efc275974c5a339a4f4178cdaf6dd042f398d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7cb4aa141fe72c1ca03c748add680c855b8c20e2b946386c3380a342515054fc8de7b32e8f5c32cc3e46f7a18ee1c9a0c16f4e816bed7503a146ad65f455d59b
|
7
|
+
data.tar.gz: 6b9ebdad0ceadeb168cd26587a5ca0407b7e8c5151fa6daebced22366d0d1cd10ddee17033862ae2c612bf03c71ae4f82f6d33ea9d1c9f8474aafc8580fa1a01
|
data/README.md
CHANGED
data/lib/contracts.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#--
|
2
2
|
# Author:: radiospiel (mailto:eno@radiospiel.org)
|
3
3
|
# Copyright:: Copyright (c) 2011, 2012 radiospiel
|
4
|
-
# License:: Distributes under the terms of the Modified BSD License,
|
4
|
+
# License:: Distributes under the terms of the Modified BSD License,
|
5
|
+
# see LICENSE.BSD for details.
|
5
6
|
#++
|
6
7
|
|
7
8
|
# The Contract module provides annotation support. That basically
|
@@ -24,15 +25,23 @@
|
|
24
25
|
# end
|
25
26
|
# end
|
26
27
|
#
|
28
|
+
require "logger"
|
29
|
+
|
27
30
|
module Contracts
|
28
31
|
class Error < ArgumentError; end
|
29
32
|
|
33
|
+
(class << self; self; end).class_eval do
|
34
|
+
attr :logger, true
|
35
|
+
end
|
36
|
+
self.logger = Logger.new(STDOUT)
|
37
|
+
|
30
38
|
def self.current_contracts
|
31
39
|
Thread.current[:current_contracts] ||= []
|
32
40
|
end
|
33
41
|
|
34
42
|
def self.consume_current_contracts
|
35
|
-
r
|
43
|
+
r = Thread.current[:current_contracts]
|
44
|
+
Thread.current[:current_contracts] = nil
|
36
45
|
r
|
37
46
|
end
|
38
47
|
|
@@ -54,22 +63,45 @@ module Contracts
|
|
54
63
|
end
|
55
64
|
|
56
65
|
def invoke(receiver, *args, &blk)
|
66
|
+
#
|
67
|
+
# Some contracts might need a per-invocation scope. If that is the take
|
68
|
+
# their before_call method will return their specific scope, and we'll
|
69
|
+
# carry that over to the after_call and on_exception calls.
|
70
|
+
#
|
71
|
+
# Since this is potentially costly we do rather not create a combined
|
72
|
+
# scope object unless we really need it; also there is an optimized
|
73
|
+
# code path for after_call's in effect. (Not for on_exception though;
|
74
|
+
# since they should only occur in exceptional situations they can carry
|
75
|
+
# a bit of performance penalty just fine.)
|
76
|
+
#
|
77
|
+
# TODO: This could be improved by having a each annotation take care of
|
78
|
+
# each individual call; a first experiment to do that, however, failed.
|
79
|
+
annotation_scopes = nil
|
80
|
+
|
57
81
|
@before_annotations.each do |annotation|
|
58
|
-
annotation.before_call(receiver, *args, &blk)
|
82
|
+
next unless annotation_scope = annotation.before_call(receiver, *args, &blk)
|
83
|
+
annotation_scopes ||= {}
|
84
|
+
annotation_scopes[annotation.object_id] = annotation_scope
|
59
85
|
end
|
60
86
|
|
61
87
|
# instance methods are UnboundMethod, class methods are Method.
|
62
88
|
rv = @method.is_a?(Method) ? @method.call(*args, &blk)
|
63
89
|
: @method.bind(receiver).call(*args, &blk)
|
64
90
|
|
65
|
-
|
66
|
-
|
91
|
+
if annotation_scopes
|
92
|
+
@after_annotations.each do |annotation|
|
93
|
+
annotation.after_call(annotation_scopes[annotation.object_id], rv, receiver, *args, &blk)
|
94
|
+
end
|
95
|
+
else
|
96
|
+
@after_annotations.each do |annotation|
|
97
|
+
annotation.after_call(nil, rv, receiver, *args, &blk)
|
98
|
+
end
|
67
99
|
end
|
68
100
|
|
69
101
|
return rv
|
70
102
|
rescue StandardError => exc
|
71
103
|
@exception_annotations.each do |annotation|
|
72
|
-
annotation.on_exception(exc, receiver, *args, &blk)
|
104
|
+
annotation.on_exception(annotation_scopes && annotation_scopes[annotation.object_id], exc, receiver, *args, &blk)
|
73
105
|
end
|
74
106
|
raise exc
|
75
107
|
end
|
@@ -122,7 +154,7 @@ module Contracts
|
|
122
154
|
#
|
123
155
|
# Returns a description of the method; i.e. Class#name or Class.name
|
124
156
|
def method_name
|
125
|
-
if method.is_a?(Method)
|
157
|
+
if method.is_a?(Method) # A singleton method?
|
126
158
|
# The method owner is the singleton class of the class. Sadly, the
|
127
159
|
# the singleton class has no name; hence we try to construct the name
|
128
160
|
# from its to_s description.
|
@@ -132,66 +164,16 @@ module Contracts
|
|
132
164
|
"#{method.owner}##{method.name}"
|
133
165
|
end
|
134
166
|
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
require "expectation"
|
139
|
-
|
140
|
-
class Contracts::Expects < Contracts::Base
|
141
|
-
attr :expectations
|
142
|
-
|
143
|
-
def initialize(expectations)
|
144
|
-
@expectations = expectations
|
145
|
-
end
|
146
|
-
|
147
|
-
def before_call(receiver, *args, &blk)
|
148
|
-
@parameter_names ||= method.parameters.map(&:last)
|
149
167
|
|
150
|
-
|
151
|
-
next unless expectation = expectations[parameter_name]
|
168
|
+
private
|
152
169
|
|
153
|
-
|
170
|
+
def error!(message)
|
171
|
+
fail Contracts::Error, message, caller[6..-1]
|
154
172
|
end
|
155
|
-
rescue Expectation::Error
|
156
|
-
raise Contracts::Error, "#{$!} in call to `#{method_name}`", caller[5..-1]
|
157
173
|
end
|
158
174
|
end
|
159
175
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
class Contracts::Returns < Contracts::Base
|
168
|
-
attr :expectation
|
169
|
-
|
170
|
-
def initialize(expectation)
|
171
|
-
@expectation = expectation
|
172
|
-
end
|
173
|
-
|
174
|
-
def after_call(rv, receiver, *args, &blk)
|
175
|
-
Expectation.match! rv, expectation
|
176
|
-
rescue Expectation::Error
|
177
|
-
raise Contracts::Error, "#{$!} in return of `#{method_name}`", caller[5..-1]
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
module Contracts::ClassMethods
|
182
|
-
def Returns(expectation)
|
183
|
-
Contracts::Returns.new(expectation)
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
class Contracts::Nothrows < Contracts::Base
|
188
|
-
def on_exception(rv, method, receiver, *args, &blk)
|
189
|
-
raise Contracts::Error, "Nothrow method `#{method_name}` raised exception: #{$!}", caller[5..-1]
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
module Contracts::ClassMethods
|
194
|
-
def Nothrow
|
195
|
-
Contracts::Nothrows.new
|
196
|
-
end
|
197
|
-
end
|
176
|
+
require_relative "contracts/expects.rb"
|
177
|
+
require_relative "contracts/nothrows.rb"
|
178
|
+
require_relative "contracts/returns.rb"
|
179
|
+
require_relative "contracts/runtime.rb"
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#--
|
2
|
+
# Author:: radiospiel (mailto:eno@radiospiel.org)
|
3
|
+
# Copyright:: Copyright (c) 2011, 2012 radiospiel
|
4
|
+
# License:: Distributes under the terms of the Modified BSD License,
|
5
|
+
# see LICENSE.BSD for details.
|
6
|
+
#++
|
7
|
+
|
8
|
+
# The Contract module provides support for Expects annotations.
|
9
|
+
|
10
|
+
require "expectation"
|
11
|
+
|
12
|
+
class Contracts::Expects < Contracts::Base
|
13
|
+
attr_reader :expectations
|
14
|
+
|
15
|
+
def initialize(expectations)
|
16
|
+
@expectations = expectations
|
17
|
+
end
|
18
|
+
|
19
|
+
def before_call(_receiver, *args, &_blk)
|
20
|
+
args.each_with_index do |value, idx|
|
21
|
+
next unless expectation = expectations_ary[idx]
|
22
|
+
Expectation::Matcher.match! value, expectation
|
23
|
+
end
|
24
|
+
|
25
|
+
nil
|
26
|
+
rescue Expectation::Error
|
27
|
+
error! "#{$ERROR_INFO} in call to `#{method_name}`"
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def expectations_ary
|
33
|
+
@expectations_ary ||= begin
|
34
|
+
method.parameters.map do |_flag, parameter_name|
|
35
|
+
expectations[parameter_name]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module Contracts::ClassMethods
|
42
|
+
def Expects(expectation)
|
43
|
+
expect! expectation => Hash
|
44
|
+
Contracts::Expects.new(expectation)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#--
|
2
|
+
# Author:: radiospiel (mailto:eno@radiospiel.org)
|
3
|
+
# Copyright:: Copyright (c) 2011, 2012 radiospiel
|
4
|
+
# License:: Distributes under the terms of the Modified BSD License,
|
5
|
+
# see LICENSE.BSD for details.
|
6
|
+
#++
|
7
|
+
|
8
|
+
class Contracts::Nothrows < Contracts::Base
|
9
|
+
def on_exception(_, _rv, _method, _receiver, *_args, &_blk)
|
10
|
+
error! "Nothrow method `#{method_name}` raised exception: #{$ERROR_INFO}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Contracts::ClassMethods
|
15
|
+
def Nothrow
|
16
|
+
Contracts::Nothrows.new
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
#--
|
2
|
+
# Author:: radiospiel (mailto:eno@radiospiel.org)
|
3
|
+
# Copyright:: Copyright (c) 2011, 2012 radiospiel
|
4
|
+
# License:: Distributes under the terms of the Modified BSD License,
|
5
|
+
# see LICENSE.BSD for details.
|
6
|
+
#++
|
7
|
+
|
8
|
+
class Contracts::Returns < Contracts::Base
|
9
|
+
attr_reader :expectation
|
10
|
+
|
11
|
+
def initialize(expectation)
|
12
|
+
@expectation = expectation
|
13
|
+
end
|
14
|
+
|
15
|
+
def after_call(_, rv, _receiver, *_args, &_blk)
|
16
|
+
Expectation::Matcher.match! rv, expectation
|
17
|
+
rescue Expectation::Error
|
18
|
+
error! "#{$ERROR_INFO} in return of `#{method_name}`"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Contracts::ClassMethods
|
23
|
+
def Returns(expectation)
|
24
|
+
Contracts::Returns.new(expectation)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#--
|
2
|
+
# Author:: radiospiel (mailto:eno@radiospiel.org)
|
3
|
+
# Copyright:: Copyright (c) 2011, 2012 radiospiel
|
4
|
+
# License:: Distributes under the terms of the Modified BSD License,
|
5
|
+
# see LICENSE.BSD for details.
|
6
|
+
#++
|
7
|
+
|
8
|
+
class Contracts::Runtime < Contracts::Base
|
9
|
+
attr_reader :expected_runtime, :max
|
10
|
+
|
11
|
+
def initialize(expected_runtime, options)
|
12
|
+
@expected_runtime = expected_runtime
|
13
|
+
@max = options[:max]
|
14
|
+
|
15
|
+
expect! max.nil? || expected_runtime <= max
|
16
|
+
end
|
17
|
+
|
18
|
+
def before_call(_receiver, *_args, &_blk)
|
19
|
+
Time.now
|
20
|
+
end
|
21
|
+
|
22
|
+
def after_call(starts_at, _rv, _receiver, *_args, &_blk)
|
23
|
+
runtime = Time.now - starts_at
|
24
|
+
|
25
|
+
if max && runtime >= max
|
26
|
+
error! "#{method_name} took longer than allowed: %.02f secs > %.02f secs." % [runtime, expected_runtime]
|
27
|
+
end
|
28
|
+
|
29
|
+
if runtime >= expected_runtime
|
30
|
+
Contracts.logger.warn "#{method_name} took longer than expected: %.02f secs > %.02f secs." % [runtime, expected_runtime]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def logger
|
35
|
+
self.class.logger
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module Contracts::ClassMethods
|
40
|
+
include Contracts
|
41
|
+
|
42
|
+
+Expects(expected_runtime: Numeric)
|
43
|
+
+Expects(options: { max: [Numeric, nil] })
|
44
|
+
def Runtime(expected_runtime, options = {})
|
45
|
+
Contracts::Runtime.new expected_runtime, options
|
46
|
+
end
|
47
|
+
end
|
data/lib/core/exception.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
#--
|
2
2
|
# Author:: radiospiel (mailto:eno@radiospiel.org)
|
3
3
|
# Copyright:: Copyright (c) 2011, 2012 radiospiel
|
4
|
-
# License:: Distributes under the terms of the Modified BSD License,
|
4
|
+
# License:: Distributes under the terms of the Modified BSD License,
|
5
|
+
# see LICENSE.BSD for details.
|
5
6
|
#++
|
6
7
|
|
7
8
|
class Exception
|
8
|
-
# Create an ArgumentError with an adjusted backtrace. We don't want to
|
9
|
+
# Create an ArgumentError with an adjusted backtrace. We don't want to
|
9
10
|
# see the user all the annotation internals.
|
10
11
|
def reraise_with_current_backtrace!
|
11
12
|
set_backtrace caller[2..-1]
|
12
|
-
|
13
|
+
fail self
|
13
14
|
end
|
14
15
|
end
|
15
|
-
|
data/lib/expectation.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
#--
|
2
2
|
# Author:: radiospiel (mailto:eno@radiospiel.org)
|
3
3
|
# Copyright:: Copyright (c) 2011, 2012 radiospiel
|
4
|
-
# License:: Distributes under the terms of the Modified BSD License,
|
4
|
+
# License:: Distributes under the terms of the Modified BSD License,
|
5
|
+
# see LICENSE.BSD for details.
|
5
6
|
#++
|
6
7
|
|
7
8
|
module Expectation; end
|
8
9
|
|
9
10
|
require_relative "core/exception"
|
11
|
+
require_relative "expectation/matcher"
|
12
|
+
require_relative "expectation/multi_matcher"
|
10
13
|
require_relative "expectation/assertions"
|
11
14
|
|
12
15
|
# The Expectation module implements methods to verify one or more values
|
@@ -16,13 +19,13 @@ require_relative "expectation/assertions"
|
|
16
19
|
#
|
17
20
|
# == Example
|
18
21
|
#
|
19
|
-
# This function expects a String argument starting with <tt>"http:"</tt>,
|
20
|
-
# an Integer or Float argument, and a Hash with a String entry at key
|
22
|
+
# This function expects a String argument starting with <tt>"http:"</tt>,
|
23
|
+
# an Integer or Float argument, and a Hash with a String entry at key
|
21
24
|
# <tt>:foo</tt>, and either an Array or +nil+ at key <tt>:bar</tt>.
|
22
|
-
#
|
25
|
+
#
|
23
26
|
# def function(a, b, options = {})
|
24
|
-
# expect! a => /^http:/,
|
25
|
-
# b => [Integer, Float],
|
27
|
+
# expect! a => /^http:/,
|
28
|
+
# b => [Integer, Float],
|
26
29
|
# options => {
|
27
30
|
# :foo => String,
|
28
31
|
# :bar => [ Array, nil ]
|
@@ -30,77 +33,31 @@ require_relative "expectation/assertions"
|
|
30
33
|
# end
|
31
34
|
|
32
35
|
module Expectation
|
33
|
-
|
34
|
-
attr :value, :expectation, :info
|
35
|
-
|
36
|
-
def initialize(value, expectation, info = nil)
|
37
|
-
@value, @expectation, @info =
|
38
|
-
value, expectation, info
|
39
|
-
end
|
40
|
-
|
41
|
-
def to_s
|
42
|
-
message = "#{value.inspect} does not match #{expectation.inspect}"
|
43
|
-
message += ", #{info}" if info
|
44
|
-
message
|
45
|
-
end
|
46
|
-
end
|
36
|
+
Error = Expectation::Matcher::Mismatch
|
47
37
|
|
48
38
|
#
|
49
|
-
# Verifies a number of expectations. If one or more expectations are
|
50
|
-
# not met it raises an
|
51
|
-
|
39
|
+
# Verifies a number of expectations. If one or more expectations are
|
40
|
+
# not met it raises an Error (on the first failing expectation).
|
41
|
+
#
|
42
|
+
# In contrast to the global expect! function this method does not
|
43
|
+
# adjust an Error's backtrace.
|
44
|
+
def self.expect!(*expectations, &block)
|
52
45
|
expectations.each do |expectation|
|
53
46
|
if expectation.is_a?(Hash)
|
54
47
|
expectation.all? do |actual, exp|
|
55
|
-
match! actual, exp
|
48
|
+
Matcher.match! actual, exp
|
56
49
|
end
|
57
50
|
else
|
58
|
-
match! expectation, :truish
|
51
|
+
Matcher.match! expectation, :truish
|
59
52
|
end
|
60
53
|
end
|
61
54
|
|
62
|
-
match! block, :__block if block
|
63
|
-
rescue Error
|
64
|
-
$!.reraise_with_current_backtrace!
|
65
|
-
end
|
66
|
-
|
67
|
-
#
|
68
|
-
# Does a value match an expectation?
|
69
|
-
def match?(value, expectation)
|
70
|
-
match! value, expectation
|
71
|
-
true
|
72
|
-
rescue Error
|
73
|
-
false
|
74
|
-
end
|
75
|
-
|
76
|
-
# Matches a value against an expectation. Raises an Expectation::Error
|
77
|
-
# if the expectation could not be matched.
|
78
|
-
def match!(value, expectation, key=nil)
|
79
|
-
match = case expectation
|
80
|
-
when :truish then !!value
|
81
|
-
when :fail then false
|
82
|
-
when Array then expectation.any? { |e| _match?(value, e) }
|
83
|
-
when Proc then expectation.arity == 0 ? expectation.call : expectation.call(value)
|
84
|
-
when Regexp then value.is_a?(String) && expectation =~ value
|
85
|
-
when :__block then value.call
|
86
|
-
when Hash then Hash === value &&
|
87
|
-
expectation.each { |key, exp| match! value[key], exp, key }
|
88
|
-
else expectation === value
|
89
|
-
end
|
90
|
-
|
91
|
-
return if match
|
92
|
-
|
93
|
-
raise Error.new(value, expectation, key && "at key #{key.inspect}")
|
94
|
-
end
|
95
|
-
|
96
|
-
private
|
97
|
-
|
98
|
-
def _match?(value, expectation)
|
99
|
-
match! value, expectation
|
100
|
-
true
|
101
|
-
rescue Error
|
102
|
-
false
|
55
|
+
Matcher.match! block, :__block if block
|
103
56
|
end
|
104
57
|
end
|
105
58
|
|
106
|
-
|
59
|
+
def expect!(*args, &block)
|
60
|
+
Expectation.expect! *args, &block
|
61
|
+
rescue Expectation::Error
|
62
|
+
$ERROR_INFO.reraise_with_current_backtrace!
|
63
|
+
end
|
@@ -1,15 +1,15 @@
|
|
1
1
|
#--
|
2
2
|
# Author:: radiospiel (mailto:eno@radiospiel.org)
|
3
3
|
# Copyright:: Copyright (c) 2011, 2012 radiospiel
|
4
|
-
# License:: Distributes under the terms of the Modified BSD License,
|
4
|
+
# License:: Distributes under the terms of the Modified BSD License,
|
5
|
+
# see LICENSE.BSD for details.
|
5
6
|
#++
|
6
7
|
|
7
|
-
|
8
8
|
# The Expectation::Assertions module provides expect! and inexpect!
|
9
9
|
# assertions to use from within test cases.
|
10
10
|
#
|
11
11
|
# == Example
|
12
|
-
#
|
12
|
+
#
|
13
13
|
# class ExpectationTest < Test::Unit::TestCase
|
14
14
|
# include Expectation::Assertions
|
15
15
|
#
|
@@ -25,12 +25,12 @@ module Expectation::Assertions
|
|
25
25
|
begin
|
26
26
|
Expectation.expect!(*expectation, &block)
|
27
27
|
rescue Expectation::Error
|
28
|
-
exc =
|
28
|
+
exc = $ERROR_INFO
|
29
29
|
end
|
30
30
|
|
31
31
|
assert_block(exc && exc.message) { !exc }
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
# verifies the failure of the passed in expectations
|
35
35
|
def inexpect!(*expectation, &block)
|
36
36
|
exc = nil
|
@@ -38,7 +38,7 @@ module Expectation::Assertions
|
|
38
38
|
begin
|
39
39
|
Expectation.expect!(*expectation, &block)
|
40
40
|
rescue Expectation::Error
|
41
|
-
exc =
|
41
|
+
exc = $ERROR_INFO
|
42
42
|
end
|
43
43
|
|
44
44
|
assert_block("Expectation(s) should fail, but didn't") { exc }
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#--
|
2
|
+
# Author:: radiospiel (mailto:eno@radiospiel.org)
|
3
|
+
# Copyright:: Copyright (c) 2011, 2012 radiospiel
|
4
|
+
# License:: Distributes under the terms of the Modified BSD License,
|
5
|
+
# see LICENSE.BSD for details.
|
6
|
+
#++
|
7
|
+
|
8
|
+
# The Expectation::Matcher module implements the logic to match a value
|
9
|
+
# against a pattern.
|
10
|
+
|
11
|
+
module Expectation::Matcher
|
12
|
+
class Mismatch < ArgumentError
|
13
|
+
attr_reader :value, :expectation, :info
|
14
|
+
|
15
|
+
def initialize(value, expectation, info = nil)
|
16
|
+
@value = value
|
17
|
+
@expectation = expectation
|
18
|
+
@info = info
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
message = "#{value.inspect} does not match #{expectation.inspect}"
|
23
|
+
case info
|
24
|
+
when nil then message
|
25
|
+
when Fixnum then "#{message}, at index #{info}"
|
26
|
+
else "#{message}, at key #{info.inspect}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
extend self
|
32
|
+
|
33
|
+
#
|
34
|
+
# Does a value match an expectation?
|
35
|
+
def match?(value, expectation)
|
36
|
+
match! value, expectation
|
37
|
+
true
|
38
|
+
rescue Mismatch
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Matches a value against an expectation. Raises an Expectation::Mismatch
|
44
|
+
# if the expectation could not be matched.
|
45
|
+
#
|
46
|
+
# The info parameter is used to add some position information to
|
47
|
+
# any Mismatch raised.
|
48
|
+
def match!(value, expectation, info = nil)
|
49
|
+
match = case expectation
|
50
|
+
when :truish then !!value
|
51
|
+
when :fail then false
|
52
|
+
when Array then
|
53
|
+
if expectation.length == 1
|
54
|
+
# Array as "array of elements matching an expectation"; for example
|
55
|
+
# [1,2,3] => [Fixnum]
|
56
|
+
e = expectation.first
|
57
|
+
value.each_with_index { |v, idx| match!(v, e, idx) }
|
58
|
+
else
|
59
|
+
# Array as "object matching one of given expectations
|
60
|
+
expectation.any? { |e| _match?(value, e) }
|
61
|
+
end
|
62
|
+
when Proc then expectation.arity == 0 ? expectation.call : expectation.call(value)
|
63
|
+
when Regexp then value.is_a?(String) && expectation =~ value
|
64
|
+
when :__block then value.call
|
65
|
+
when Hash then Hash === value &&
|
66
|
+
expectation.each { |key, exp| match! value[key], exp, key }
|
67
|
+
else expectation === value
|
68
|
+
end
|
69
|
+
|
70
|
+
return if match
|
71
|
+
fail Mismatch.new(value, expectation, info)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def _match?(value, expectation)
|
77
|
+
match! value, expectation
|
78
|
+
true
|
79
|
+
rescue Mismatch
|
80
|
+
false
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#--
|
2
|
+
# Author:: radiospiel (mailto:eno@radiospiel.org)
|
3
|
+
# Copyright:: Copyright (c) 2011, 2012 radiospiel
|
4
|
+
# License:: Distributes under the terms of the Modified BSD License,
|
5
|
+
# see LICENSE.BSD for details.
|
6
|
+
#++
|
7
|
+
|
8
|
+
# The Expectation::MultiMatcher class provides support for T1 | T2 matches.
|
9
|
+
|
10
|
+
class Expectation::MultiMatcher < Array
|
11
|
+
def initialize(lhs, rhs)
|
12
|
+
push lhs
|
13
|
+
push rhs
|
14
|
+
end
|
15
|
+
|
16
|
+
def |(other)
|
17
|
+
push other
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Module
|
22
|
+
def |(other)
|
23
|
+
Expectation::MultiMatcher.new(self, other)
|
24
|
+
end
|
25
|
+
end
|
data/lib/expectation/version.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
#--
|
2
2
|
# Author:: radiospiel (mailto:eno@radiospiel.org)
|
3
3
|
# Copyright:: Copyright (c) 2011, 2012 radiospiel
|
4
|
-
# License:: Distributes under the terms of the Modified BSD License,
|
4
|
+
# License:: Distributes under the terms of the Modified BSD License,
|
5
|
+
# see LICENSE.BSD for details.
|
5
6
|
module Expectation
|
6
7
|
# The expectation gem's version.
|
7
|
-
VERSION=
|
8
|
+
VERSION = '1.1.0'
|
8
9
|
end
|
data/test/contracts_test.rb
CHANGED
@@ -4,30 +4,14 @@
|
|
4
4
|
require_relative 'test_helper'
|
5
5
|
|
6
6
|
require "contracts"
|
7
|
+
Contracts.logger.level = Logger::ERROR
|
7
8
|
|
8
9
|
class ContractsTest < Test::Unit::TestCase
|
9
|
-
class
|
10
|
-
|
11
|
-
|
12
|
-
+Expects(a: 1)
|
13
|
-
def sum(a, b, c)
|
14
|
-
a + b + c
|
15
|
-
end
|
16
|
-
|
17
|
-
+Returns(2)
|
18
|
-
def returns_arg(r)
|
19
|
-
r
|
20
|
-
end
|
21
|
-
|
22
|
-
+Expects(v: Fixnum)
|
23
|
-
def throw_on_one(v)
|
24
|
-
raise if v == 1
|
25
|
-
end
|
10
|
+
class Base
|
11
|
+
end
|
26
12
|
|
27
|
-
|
28
|
-
|
29
|
-
raise if v == 1
|
30
|
-
end
|
13
|
+
class Foo < Base
|
14
|
+
include Contracts
|
31
15
|
end
|
32
16
|
|
33
17
|
attr :foo
|
@@ -36,6 +20,15 @@ class ContractsTest < Test::Unit::TestCase
|
|
36
20
|
@foo = Foo.new
|
37
21
|
end
|
38
22
|
|
23
|
+
# -- Expects contracts ------------------------------------------------------
|
24
|
+
|
25
|
+
class Foo
|
26
|
+
+Expects(a: 1)
|
27
|
+
def sum(a, b, c)
|
28
|
+
a + b + c
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
39
32
|
def test_contracts_check_number_of_arguments
|
40
33
|
e = assert_raise(ArgumentError) {
|
41
34
|
foo.sum(1) # scripts/test:67:in `f': wrong number of arguments (1 for 3) (ArgumentError)
|
@@ -54,6 +47,30 @@ class ContractsTest < Test::Unit::TestCase
|
|
54
47
|
assert e.backtrace.first.include?("test/contracts_test.rb:")
|
55
48
|
end
|
56
49
|
|
50
|
+
class Foo
|
51
|
+
attr :a, :b
|
52
|
+
+Expects(b: String)
|
53
|
+
def with_default_arg(a, b="check")
|
54
|
+
@a, @b = a, b
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_default_args
|
59
|
+
foo.with_default_arg :one
|
60
|
+
|
61
|
+
assert_equal(foo.a, :one)
|
62
|
+
assert_equal(foo.b, "check")
|
63
|
+
end
|
64
|
+
|
65
|
+
# -- Returns contracts ------------------------------------------------------
|
66
|
+
|
67
|
+
class Foo
|
68
|
+
+Returns(2)
|
69
|
+
def returns_arg(r)
|
70
|
+
r
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
57
74
|
def test_returns_contract
|
58
75
|
assert_nothing_raised() {
|
59
76
|
foo.returns_arg(2)
|
@@ -65,6 +82,37 @@ class ContractsTest < Test::Unit::TestCase
|
|
65
82
|
assert e.backtrace.first.include?("test/contracts_test.rb:")
|
66
83
|
end
|
67
84
|
|
85
|
+
# -- test for call to super -------------------------------------------------
|
86
|
+
|
87
|
+
class Base
|
88
|
+
attr :checked
|
89
|
+
|
90
|
+
def check
|
91
|
+
@checked = true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class Foo
|
96
|
+
+Returns(true)
|
97
|
+
def check
|
98
|
+
super
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_calls_super
|
103
|
+
foo.check
|
104
|
+
assert foo.checked
|
105
|
+
end
|
106
|
+
|
107
|
+
# -- Nothrow contracts ------------------------------------------------------
|
108
|
+
|
109
|
+
class Foo
|
110
|
+
+Nothrow()
|
111
|
+
def unexpected_throw_on_one(v)
|
112
|
+
raise if v == 1
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
68
116
|
def test_nothrow_contract
|
69
117
|
assert_nothing_raised() {
|
70
118
|
foo.unexpected_throw_on_one(2)
|
@@ -76,6 +124,13 @@ class ContractsTest < Test::Unit::TestCase
|
|
76
124
|
assert e.backtrace.first.include?("test/contracts_test.rb:")
|
77
125
|
end
|
78
126
|
|
127
|
+
class Foo
|
128
|
+
+Expects(v: Fixnum)
|
129
|
+
def throw_on_one(v)
|
130
|
+
raise if v == 1
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
79
134
|
def test_still_throws_fine
|
80
135
|
assert_nothing_raised() {
|
81
136
|
foo.throw_on_one(2)
|
@@ -85,4 +140,25 @@ class ContractsTest < Test::Unit::TestCase
|
|
85
140
|
foo.throw_on_one(1)
|
86
141
|
}
|
87
142
|
end
|
143
|
+
|
144
|
+
# -- Runtime contracts ------------------------------------------------------
|
145
|
+
|
146
|
+
require "timecop"
|
147
|
+
class Foo
|
148
|
+
+Runtime(0.01, max: 0.05)
|
149
|
+
def wait_for(time)
|
150
|
+
Timecop.travel(Time.now + time)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def test_runtime
|
155
|
+
foo.wait_for 0.001
|
156
|
+
foo.wait_for 0.02
|
157
|
+
|
158
|
+
e = assert_raise(Contracts::Error) {
|
159
|
+
foo.wait_for 10
|
160
|
+
}
|
161
|
+
|
162
|
+
Timecop.return
|
163
|
+
end
|
88
164
|
end
|
data/test/expection_test.rb
CHANGED
@@ -9,7 +9,7 @@ class ExpectationTest < Test::Unit::TestCase
|
|
9
9
|
end
|
10
10
|
|
11
11
|
#
|
12
|
-
# This test covers the usual use case: expectations are
|
12
|
+
# This test covers the usual use case: expectations are
|
13
13
|
# passed in a single Hash.
|
14
14
|
def test_hash_expectation
|
15
15
|
assert_expectation! "1" => /1/
|
@@ -36,6 +36,28 @@ class ExpectationTest < Test::Unit::TestCase
|
|
36
36
|
assert_failed_expectation! do nil end
|
37
37
|
end
|
38
38
|
|
39
|
+
def test_array_expectations
|
40
|
+
assert_expectation! 1 => [Fixnum, String]
|
41
|
+
assert_expectation! 1 => [String, Fixnum]
|
42
|
+
assert_failed_expectation! 1 => [NilClass, String]
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_multi_expectations
|
46
|
+
assert_expectation! 1 => Fixnum | String
|
47
|
+
assert_expectation! 1 => String | 1
|
48
|
+
assert_failed_expectation! 1 => NilClass | String
|
49
|
+
assert_expectation! 1 => NilClass | String | 1
|
50
|
+
end
|
51
|
+
|
52
|
+
module I; end
|
53
|
+
class X; include I; end
|
54
|
+
class Y; end
|
55
|
+
|
56
|
+
def test_interface_expectations
|
57
|
+
assert_expectation! X.new => I
|
58
|
+
assert_failed_expectation! Y.new => I
|
59
|
+
end
|
60
|
+
|
39
61
|
def test_exception_message
|
40
62
|
e = assert_failed_expectation!({ 1 => 2 })
|
41
63
|
assert e.message.include?("1 does not match 2")
|
data/test/matching_test.rb
CHANGED
@@ -5,21 +5,23 @@ require_relative 'test_helper'
|
|
5
5
|
|
6
6
|
class MatchingTest < Test::Unit::TestCase
|
7
7
|
def assert_match(value, expectation)
|
8
|
-
assert_equal true, Expectation.match?(value, expectation)
|
8
|
+
assert_equal true, Expectation::Matcher.match?(value, expectation)
|
9
9
|
end
|
10
10
|
|
11
11
|
def assert_mismatch(value, expectation)
|
12
|
-
assert_equal false, Expectation.match?(value, expectation)
|
12
|
+
assert_equal false, Expectation::Matcher.match?(value, expectation)
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_mismatches_raise_exceptions
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
assert_match 1, 1
|
17
|
+
assert_mismatch 1, 2
|
18
|
+
end
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
def test_array_matches
|
21
|
+
assert_match [1], [Integer]
|
22
|
+
assert_mismatch [1], [String]
|
23
|
+
assert_match [1, "2"], [[Integer, String]]
|
24
|
+
assert_mismatch [1, "2", /abc/], [[Integer, String]]
|
23
25
|
end
|
24
26
|
|
25
27
|
def test_int_expectations
|
data/test/test_helper.rb
CHANGED
@@ -1,18 +1,23 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'bundler/setup'
|
3
3
|
|
4
|
-
|
5
|
-
require 'simplecov
|
4
|
+
if ENV["COVERAGE"]
|
5
|
+
require 'simplecov'
|
6
|
+
require 'simplecov-console'
|
7
|
+
end
|
8
|
+
|
6
9
|
require 'test/unit'
|
7
10
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
+
if ENV["COVERAGE"]
|
12
|
+
SimpleCov.start do
|
13
|
+
add_filter "test/*.rb"
|
14
|
+
end
|
11
15
|
|
12
|
-
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
13
|
-
|
14
|
-
|
15
|
-
]
|
16
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
17
|
+
SimpleCov::Formatter::HTMLFormatter,
|
18
|
+
SimpleCov::Formatter::Console,
|
19
|
+
]
|
20
|
+
end
|
16
21
|
|
17
22
|
require "expectation"
|
18
23
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: expectation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- radiospiel
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-11-
|
11
|
+
date: 2015-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Defensive programming with expectations
|
14
14
|
email: eno@radiospiel.org
|
@@ -18,9 +18,15 @@ extra_rdoc_files: []
|
|
18
18
|
files:
|
19
19
|
- README.md
|
20
20
|
- lib/contracts.rb
|
21
|
+
- lib/contracts/expects.rb
|
22
|
+
- lib/contracts/nothrows.rb
|
23
|
+
- lib/contracts/returns.rb
|
24
|
+
- lib/contracts/runtime.rb
|
21
25
|
- lib/core/exception.rb
|
22
26
|
- lib/expectation.rb
|
23
27
|
- lib/expectation/assertions.rb
|
28
|
+
- lib/expectation/matcher.rb
|
29
|
+
- lib/expectation/multi_matcher.rb
|
24
30
|
- lib/expectation/version.rb
|
25
31
|
- test/assertions_test.rb
|
26
32
|
- test/contracts_module_test.rb
|