expectation 1.0.0 → 1.1.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.
- 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
|