contracts-lite 0.14.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 +7 -0
- data/CHANGELOG.markdown +80 -0
- data/Gemfile +16 -0
- data/LICENSE +23 -0
- data/README.md +102 -0
- data/TODO.markdown +6 -0
- data/TUTORIAL.md +747 -0
- data/benchmarks/bench.rb +67 -0
- data/benchmarks/hash.rb +69 -0
- data/benchmarks/invariants.rb +91 -0
- data/benchmarks/io.rb +62 -0
- data/benchmarks/wrap_test.rb +57 -0
- data/contracts.gemspec +13 -0
- data/lib/contracts.rb +231 -0
- data/lib/contracts/builtin_contracts.rb +541 -0
- data/lib/contracts/call_with.rb +97 -0
- data/lib/contracts/core.rb +52 -0
- data/lib/contracts/decorators.rb +47 -0
- data/lib/contracts/engine.rb +26 -0
- data/lib/contracts/engine/base.rb +136 -0
- data/lib/contracts/engine/eigenclass.rb +50 -0
- data/lib/contracts/engine/target.rb +70 -0
- data/lib/contracts/error_formatter.rb +121 -0
- data/lib/contracts/errors.rb +71 -0
- data/lib/contracts/formatters.rb +134 -0
- data/lib/contracts/invariants.rb +68 -0
- data/lib/contracts/method_handler.rb +195 -0
- data/lib/contracts/method_reference.rb +100 -0
- data/lib/contracts/support.rb +59 -0
- data/lib/contracts/validators.rb +139 -0
- data/lib/contracts/version.rb +3 -0
- data/script/rubocop +7 -0
- data/spec/builtin_contracts_spec.rb +461 -0
- data/spec/contracts_spec.rb +748 -0
- data/spec/error_formatter_spec.rb +68 -0
- data/spec/fixtures/fixtures.rb +710 -0
- data/spec/invariants_spec.rb +17 -0
- data/spec/module_spec.rb +18 -0
- data/spec/override_validators_spec.rb +162 -0
- data/spec/ruby_version_specific/contracts_spec_1.9.rb +24 -0
- data/spec/ruby_version_specific/contracts_spec_2.0.rb +55 -0
- data/spec/ruby_version_specific/contracts_spec_2.1.rb +63 -0
- data/spec/spec_helper.rb +102 -0
- data/spec/support.rb +10 -0
- data/spec/support_spec.rb +21 -0
- data/spec/validators_spec.rb +47 -0
- metadata +94 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
module Contracts
|
2
|
+
# MethodReference represents original method reference that was
|
3
|
+
# decorated by contracts.ruby. Used for instance methods.
|
4
|
+
class MethodReference
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
# name - name of the method
|
8
|
+
# method - method object
|
9
|
+
def initialize(name, method)
|
10
|
+
@name = name
|
11
|
+
@method = method
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns method_position, delegates to Support.method_position
|
15
|
+
def method_position
|
16
|
+
Support.method_position(@method)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Makes a method re-definition in proper way
|
20
|
+
def make_definition(this, &blk)
|
21
|
+
is_private = private?(this)
|
22
|
+
is_protected = protected?(this)
|
23
|
+
alias_target(this).send(:define_method, name, &blk)
|
24
|
+
make_private(this) if is_private
|
25
|
+
make_protected(this) if is_protected
|
26
|
+
end
|
27
|
+
|
28
|
+
# Aliases original method to a special unique name, which is known
|
29
|
+
# only to this class. Usually done right before re-defining the
|
30
|
+
# method.
|
31
|
+
def make_alias(this)
|
32
|
+
_aliased_name = aliased_name
|
33
|
+
original_name = name
|
34
|
+
|
35
|
+
alias_target(this).class_eval do
|
36
|
+
alias_method _aliased_name, original_name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Calls original method on specified `this` argument with
|
41
|
+
# specified arguments `args` and block `&blk`.
|
42
|
+
def send_to(this, *args, &blk)
|
43
|
+
this.send(aliased_name, *args, &blk)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Makes a method private
|
49
|
+
def make_private(this)
|
50
|
+
original_name = name
|
51
|
+
alias_target(this).class_eval { private original_name }
|
52
|
+
end
|
53
|
+
|
54
|
+
def private?(this)
|
55
|
+
this.private_instance_methods.map(&:to_sym).include?(name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def protected?(this)
|
59
|
+
this.protected_instance_methods.map(&:to_sym).include?(name)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Makes a method protected
|
63
|
+
def make_protected(this)
|
64
|
+
original_name = name
|
65
|
+
alias_target(this).class_eval { protected original_name }
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns alias target for instance methods, subject to be
|
69
|
+
# overriden in subclasses.
|
70
|
+
def alias_target(this)
|
71
|
+
this
|
72
|
+
end
|
73
|
+
|
74
|
+
def aliased_name
|
75
|
+
@_original_name ||= construct_unique_name
|
76
|
+
end
|
77
|
+
|
78
|
+
def construct_unique_name
|
79
|
+
:"__contracts_ruby_original_#{name}_#{Support.unique_id}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# The same as MethodReference, but used for singleton methods.
|
84
|
+
class SingletonMethodReference < MethodReference
|
85
|
+
private
|
86
|
+
|
87
|
+
def private?(this)
|
88
|
+
this.private_methods.map(&:to_sym).include?(name)
|
89
|
+
end
|
90
|
+
|
91
|
+
def protected?(this)
|
92
|
+
this.protected_methods.map(&:to_sym).include?(name)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Return alias target for singleton methods.
|
96
|
+
def alias_target(this)
|
97
|
+
Support.eigenclass_of this
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Contracts
|
2
|
+
module Support
|
3
|
+
class << self
|
4
|
+
def method_position(method)
|
5
|
+
return method.method_position if method.is_a?(MethodReference)
|
6
|
+
|
7
|
+
if RUBY_VERSION =~ /^1\.8/
|
8
|
+
if method.respond_to?(:__file__)
|
9
|
+
method.__file__ + ":" + method.__line__.to_s
|
10
|
+
else
|
11
|
+
method.inspect
|
12
|
+
end
|
13
|
+
else
|
14
|
+
file, line = method.source_location
|
15
|
+
file + ":" + line.to_s
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_name(method)
|
20
|
+
method.is_a?(Proc) ? "Proc" : method.name
|
21
|
+
end
|
22
|
+
|
23
|
+
# Generates unique id, which can be used as a part of identifier
|
24
|
+
#
|
25
|
+
# Example:
|
26
|
+
# Contracts::Support.unique_id # => "i53u6tiw5hbo"
|
27
|
+
def unique_id
|
28
|
+
# Consider using SecureRandom.hex here, and benchmark which one is better
|
29
|
+
(Time.now.to_f * 1000).to_i.to_s(36) + rand(1_000_000).to_s(36)
|
30
|
+
end
|
31
|
+
|
32
|
+
def contract_id(contract)
|
33
|
+
contract.object_id
|
34
|
+
end
|
35
|
+
|
36
|
+
def eigenclass_hierarchy_supported?
|
37
|
+
return false if RUBY_PLATFORM == "java" && RUBY_VERSION.to_f < 2.0
|
38
|
+
RUBY_VERSION.to_f > 1.8
|
39
|
+
end
|
40
|
+
|
41
|
+
def eigenclass_of(target)
|
42
|
+
class << target; self; end
|
43
|
+
end
|
44
|
+
|
45
|
+
def eigenclass?(target)
|
46
|
+
module_eigenclass?(target) ||
|
47
|
+
target <= eigenclass_of(Object)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Module eigenclass can be detected by its ancestor chain
|
53
|
+
# containing a Module
|
54
|
+
def module_eigenclass?(target)
|
55
|
+
target < Module
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module Contracts
|
2
|
+
module Validators
|
3
|
+
DEFAULT_VALIDATOR_STRATEGIES = {
|
4
|
+
# e.g. lambda {true}
|
5
|
+
Proc => lambda { |contract| contract },
|
6
|
+
|
7
|
+
# e.g. [Num, String]
|
8
|
+
# TODO: account for these errors too
|
9
|
+
Array => lambda do |contract|
|
10
|
+
lambda do |arg|
|
11
|
+
return false unless arg.is_a?(Array) && arg.length == contract.length
|
12
|
+
arg.zip(contract).all? do |_arg, _contract|
|
13
|
+
Contract.valid?(_arg, _contract)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end,
|
17
|
+
|
18
|
+
# e.g. { :a => Num, :b => String }
|
19
|
+
Hash => lambda do |contract|
|
20
|
+
lambda do |arg|
|
21
|
+
return false unless arg.is_a?(Hash)
|
22
|
+
contract.keys.all? do |k|
|
23
|
+
Contract.valid?(arg[k], contract[k])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end,
|
27
|
+
|
28
|
+
Range => lambda do |contract|
|
29
|
+
lambda do |arg|
|
30
|
+
contract.include?(arg)
|
31
|
+
end
|
32
|
+
end,
|
33
|
+
|
34
|
+
Regexp => lambda do |contract|
|
35
|
+
lambda do |arg|
|
36
|
+
arg =~ contract
|
37
|
+
end
|
38
|
+
end,
|
39
|
+
|
40
|
+
Contracts::Args => lambda do |contract|
|
41
|
+
lambda do |arg|
|
42
|
+
Contract.valid?(arg, contract.contract)
|
43
|
+
end
|
44
|
+
end,
|
45
|
+
|
46
|
+
Contracts::Func => lambda do |_|
|
47
|
+
lambda do |arg|
|
48
|
+
arg.is_a?(Method) || arg.is_a?(Proc)
|
49
|
+
end
|
50
|
+
end,
|
51
|
+
|
52
|
+
:valid => lambda do |contract|
|
53
|
+
lambda { |arg| contract.valid?(arg) }
|
54
|
+
end,
|
55
|
+
|
56
|
+
:class => lambda do |contract|
|
57
|
+
lambda { |arg| arg.is_a?(contract) }
|
58
|
+
end,
|
59
|
+
|
60
|
+
:default => lambda do |contract|
|
61
|
+
lambda { |arg| contract == arg }
|
62
|
+
end
|
63
|
+
}.freeze
|
64
|
+
|
65
|
+
# Allows to override validator with custom one.
|
66
|
+
# Example:
|
67
|
+
# Contract.override_validator(Array) do |contract|
|
68
|
+
# lambda do |arg|
|
69
|
+
# # .. implementation for Array contract ..
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# Contract.override_validator(:class) do |contract|
|
74
|
+
# lambda do |arg|
|
75
|
+
# arg.is_a?(contract) || arg.is_a?(RSpec::Mocks::Double)
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
def override_validator(name, &block)
|
79
|
+
validator_strategies[name] = block
|
80
|
+
end
|
81
|
+
|
82
|
+
# This is a little weird. For each contract
|
83
|
+
# we pre-make a proc to validate it so we
|
84
|
+
# don't have to go through this decision tree every time.
|
85
|
+
# Seems silly but it saves us a bunch of time (4.3sec vs 5.2sec)
|
86
|
+
def make_validator!(contract)
|
87
|
+
klass = contract.class
|
88
|
+
key = if validator_strategies.key?(klass)
|
89
|
+
klass
|
90
|
+
else
|
91
|
+
if contract.respond_to? :valid?
|
92
|
+
:valid
|
93
|
+
elsif klass == Class || klass == Module
|
94
|
+
:class
|
95
|
+
else
|
96
|
+
:default
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
validator_strategies[key].call(contract)
|
101
|
+
end
|
102
|
+
|
103
|
+
def make_validator(contract)
|
104
|
+
contract_id = Support.contract_id(contract)
|
105
|
+
|
106
|
+
if memoized_validators.key?(contract_id)
|
107
|
+
return memoized_validators[contract_id]
|
108
|
+
end
|
109
|
+
|
110
|
+
memoized_validators[contract_id] = make_validator!(contract)
|
111
|
+
end
|
112
|
+
|
113
|
+
# @private
|
114
|
+
def reset_validators
|
115
|
+
clean_memoized_validators
|
116
|
+
restore_validators
|
117
|
+
end
|
118
|
+
|
119
|
+
# @private
|
120
|
+
def validator_strategies
|
121
|
+
@_validator_strategies ||= restore_validators
|
122
|
+
end
|
123
|
+
|
124
|
+
# @private
|
125
|
+
def restore_validators
|
126
|
+
@_validator_strategies = DEFAULT_VALIDATOR_STRATEGIES.dup
|
127
|
+
end
|
128
|
+
|
129
|
+
# @private
|
130
|
+
def memoized_validators
|
131
|
+
@_memoized_validators ||= clean_memoized_validators
|
132
|
+
end
|
133
|
+
|
134
|
+
# @private
|
135
|
+
def clean_memoized_validators
|
136
|
+
@_memoized_validators = {}
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/script/rubocop
ADDED
@@ -0,0 +1,461 @@
|
|
1
|
+
RSpec.describe "Contracts:" do
|
2
|
+
before :all do
|
3
|
+
@o = GenericExample.new
|
4
|
+
end
|
5
|
+
|
6
|
+
def fails(&some)
|
7
|
+
expect { some.call }.to raise_error(ContractError)
|
8
|
+
end
|
9
|
+
|
10
|
+
def passes(&some)
|
11
|
+
expect { some.call }.to_not raise_error
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "DescendantOf:" do
|
15
|
+
it "should pass for Array" do
|
16
|
+
passes { @o.enumerable_descendant_test(Array) }
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should pass for a hash" do
|
20
|
+
passes { @o.enumerable_descendant_test(Hash) }
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should fail for a number class" do
|
24
|
+
fails { @o.enumerable_descendant_test(Integer) }
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should fail for a non-class" do
|
28
|
+
fails { @o.enumerable_descendant_test(1) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "Num:" do
|
33
|
+
it "should pass for Fixnums" do
|
34
|
+
passes { @o.double(2) }
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should pass for Floats" do
|
38
|
+
passes { @o.double(2.2) }
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should fail for nil and other data types" do
|
42
|
+
fails { @o.double(nil) }
|
43
|
+
fails { @o.double(:x) }
|
44
|
+
fails { @o.double("x") }
|
45
|
+
fails { @o.double(/x/) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "Pos:" do
|
50
|
+
it "should pass for positive numbers" do
|
51
|
+
passes { @o.pos_test(1) }
|
52
|
+
passes { @o.pos_test(1.6) }
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should fail for 0" do
|
56
|
+
fails { @o.pos_test(0) }
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should fail for negative numbers" do
|
60
|
+
fails { @o.pos_test(-1) }
|
61
|
+
fails { @o.pos_test(-1.6) }
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should fail for nil and other data types" do
|
65
|
+
fails { @o.pos_test(nil) }
|
66
|
+
fails { @o.pos_test(:x) }
|
67
|
+
fails { @o.pos_test("x") }
|
68
|
+
fails { @o.pos_test(/x/) }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "Neg:" do
|
73
|
+
it "should pass for negative numbers" do
|
74
|
+
passes { @o.neg_test(-1) }
|
75
|
+
passes { @o.neg_test(-1.6) }
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should fail for 0" do
|
79
|
+
fails { @o.neg_test(0) }
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should fail for positive numbers" do
|
83
|
+
fails { @o.neg_test(1) }
|
84
|
+
fails { @o.neg_test(1.6) }
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should fail for nil and other data types" do
|
88
|
+
fails { @o.neg_test(nil) }
|
89
|
+
fails { @o.neg_test(:x) }
|
90
|
+
fails { @o.neg_test("x") }
|
91
|
+
fails { @o.neg_test(/x/) }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "Nat:" do
|
96
|
+
it "should pass for 0" do
|
97
|
+
passes { @o.nat_test(0) }
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should pass for positive whole numbers" do
|
101
|
+
passes { @o.nat_test(1) }
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should fail for positive non-whole numbers" do
|
105
|
+
fails { @o.nat_test(1.5) }
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should fail for negative numbers" do
|
109
|
+
fails { @o.nat_test(-1) }
|
110
|
+
fails { @o.nat_test(-1.6) }
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should fail for nil and other data types" do
|
114
|
+
fails { @o.nat_test(nil) }
|
115
|
+
fails { @o.nat_test(:x) }
|
116
|
+
fails { @o.nat_test("x") }
|
117
|
+
fails { @o.nat_test(/x/) }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "Any:" do
|
122
|
+
it "should pass for numbers" do
|
123
|
+
passes { @o.show(1) }
|
124
|
+
end
|
125
|
+
it "should pass for strings" do
|
126
|
+
passes { @o.show("bad") }
|
127
|
+
end
|
128
|
+
it "should pass for procs" do
|
129
|
+
passes { @o.show(lambda {}) }
|
130
|
+
end
|
131
|
+
it "should pass for nil" do
|
132
|
+
passes { @o.show(nil) }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "None:" do
|
137
|
+
it "should fail for numbers" do
|
138
|
+
fails { @o.fail_all(1) }
|
139
|
+
end
|
140
|
+
it "should fail for strings" do
|
141
|
+
fails { @o.fail_all("bad") }
|
142
|
+
end
|
143
|
+
it "should fail for procs" do
|
144
|
+
fails { @o.fail_all(lambda {}) }
|
145
|
+
end
|
146
|
+
it "should fail for nil" do
|
147
|
+
fails { @o.fail_all(nil) }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe "Or:" do
|
152
|
+
it "should pass for nums" do
|
153
|
+
passes { @o.num_or_string(1) }
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should pass for strings" do
|
157
|
+
passes { @o.num_or_string("bad") }
|
158
|
+
end
|
159
|
+
|
160
|
+
it "should fail for nil" do
|
161
|
+
fails { @o.num_or_string(nil) }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe "Xor:" do
|
166
|
+
it "should pass for an object with a method :good" do
|
167
|
+
passes { @o.xor_test(A.new) }
|
168
|
+
end
|
169
|
+
|
170
|
+
it "should pass for an object with a method :bad" do
|
171
|
+
passes { @o.xor_test(B.new) }
|
172
|
+
end
|
173
|
+
|
174
|
+
it "should fail for an object with neither method" do
|
175
|
+
fails { @o.xor_test(1) }
|
176
|
+
end
|
177
|
+
|
178
|
+
it "should fail for an object with both methods :good and :bad" do
|
179
|
+
fails { @o.xor_test(F.new) }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe "And:" do
|
184
|
+
it "should pass for an object of class A that has a method :good" do
|
185
|
+
passes { @o.and_test(A.new) }
|
186
|
+
end
|
187
|
+
|
188
|
+
it "should fail for an object that has a method :good but isn't of class A" do
|
189
|
+
fails { @o.and_test(F.new) }
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe "Enum:" do
|
194
|
+
it "should pass for an object that is included" do
|
195
|
+
passes { @o.enum_test(:a) }
|
196
|
+
end
|
197
|
+
|
198
|
+
it "should fail for an object that is not included" do
|
199
|
+
fails { @o.enum_test(:z) }
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
describe "RespondTo:" do
|
204
|
+
it "should pass for an object that responds to :good" do
|
205
|
+
passes { @o.responds_test(A.new) }
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should fail for an object that doesn't respond to :good" do
|
209
|
+
fails { @o.responds_test(B.new) }
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
describe "Send:" do
|
214
|
+
it "should pass for an object that returns true for method :good" do
|
215
|
+
passes { @o.send_test(A.new) }
|
216
|
+
end
|
217
|
+
|
218
|
+
it "should fail for an object that returns false for method :good" do
|
219
|
+
fails { @o.send_test(F.new) }
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
describe "Exactly:" do
|
224
|
+
it "should pass for an object that is exactly a Parent" do
|
225
|
+
passes { @o.exactly_test(Parent.new) }
|
226
|
+
end
|
227
|
+
|
228
|
+
it "should fail for an object that inherits from Parent" do
|
229
|
+
fails { @o.exactly_test(Child.new) }
|
230
|
+
end
|
231
|
+
|
232
|
+
it "should fail for an object that is not related to Parent at all" do
|
233
|
+
fails { @o.exactly_test(A.new) }
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
describe "Eq:" do
|
238
|
+
it "should pass for a class" do
|
239
|
+
passes { @o.eq_class_test(Foo) }
|
240
|
+
end
|
241
|
+
|
242
|
+
it "should pass for a module" do
|
243
|
+
passes { @o.eq_module_test(Bar) }
|
244
|
+
end
|
245
|
+
|
246
|
+
it "should pass for other values" do
|
247
|
+
passes { @o.eq_value_test(Baz) }
|
248
|
+
end
|
249
|
+
|
250
|
+
it "should fail when not equal" do
|
251
|
+
fails { @o.eq_class_test(Bar) }
|
252
|
+
end
|
253
|
+
|
254
|
+
it "should fail when given instance of class" do
|
255
|
+
fails { @o.eq_class_test(Foo.new) }
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
describe "Not:" do
|
260
|
+
it "should pass for an argument that isn't nil" do
|
261
|
+
passes { @o.not_nil(1) }
|
262
|
+
end
|
263
|
+
|
264
|
+
it "should fail for nil" do
|
265
|
+
fails { @o.not_nil(nil) }
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
describe "ArrayOf:" do
|
270
|
+
it "should pass for an array of nums" do
|
271
|
+
passes { @o.product([1, 2, 3]) }
|
272
|
+
end
|
273
|
+
|
274
|
+
it "should fail for an array with one non-num" do
|
275
|
+
fails { @o.product([1, 2, 3, "bad"]) }
|
276
|
+
end
|
277
|
+
|
278
|
+
it "should fail for a non-array" do
|
279
|
+
fails { @o.product(1) }
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
describe "RangeOf:" do
|
284
|
+
require "date"
|
285
|
+
it "should pass for a range of nums" do
|
286
|
+
passes { @o.first_in_range_num(3..10) }
|
287
|
+
end
|
288
|
+
|
289
|
+
it "should pass for a range of dates" do
|
290
|
+
d1 = Date.today
|
291
|
+
d2 = d1 + 18
|
292
|
+
passes { @o.first_in_range_date(d1..d2) }
|
293
|
+
end
|
294
|
+
|
295
|
+
it "should fail for a non-range" do
|
296
|
+
fails { @o.first_in_range_num("foo") }
|
297
|
+
fails { @o.first_in_range_num(:foo) }
|
298
|
+
fails { @o.first_in_range_num(5) }
|
299
|
+
fails { @o.first_in_range_num(nil) }
|
300
|
+
end
|
301
|
+
|
302
|
+
it "should fail for a range with incorrect data type" do
|
303
|
+
fails { @o.first_in_range_num("a".."z") }
|
304
|
+
end
|
305
|
+
|
306
|
+
it "should fail for a badly-defined range" do
|
307
|
+
# For some reason, Ruby 2.0.0 allows (date .. number) as a range.
|
308
|
+
# Perhaps other Ruby versions do too.
|
309
|
+
# Note that (date .. string) gives ArgumentError.
|
310
|
+
# This test guards against ranges with inconsistent data types.
|
311
|
+
begin
|
312
|
+
d1 = Date.today
|
313
|
+
fails { @o.first_in_range_date(d1..10) }
|
314
|
+
fails { @o.first_in_range_num(d1..10) }
|
315
|
+
rescue ArgumentError
|
316
|
+
# If Ruby doesn't like the range, we ignore the test.
|
317
|
+
:nop
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
describe "SetOf:" do
|
323
|
+
it "should pass for a set of nums" do
|
324
|
+
passes { @o.product_from_set(Set.new([1, 2, 3])) }
|
325
|
+
end
|
326
|
+
|
327
|
+
it "should fail for an array with one non-num" do
|
328
|
+
fails { @o.product_from_set(Set.new([1, 2, 3, "bad"])) }
|
329
|
+
end
|
330
|
+
|
331
|
+
it "should fail for a non-array" do
|
332
|
+
fails { @o.product_from_set(1) }
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
describe "Bool:" do
|
337
|
+
it "should pass for an argument that is a boolean" do
|
338
|
+
passes { @o.bool_test(true) }
|
339
|
+
passes { @o.bool_test(false) }
|
340
|
+
end
|
341
|
+
|
342
|
+
it "should fail for nil" do
|
343
|
+
fails { @o.bool_test(nil) }
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
describe "Maybe:" do
|
348
|
+
it "should pass for nums" do
|
349
|
+
expect(@o.maybe_double(1)).to eq(2)
|
350
|
+
end
|
351
|
+
|
352
|
+
it "should pass for nils" do
|
353
|
+
expect(@o.maybe_double(nil)).to eq(nil)
|
354
|
+
end
|
355
|
+
|
356
|
+
it "should fail for strings" do
|
357
|
+
fails { @o.maybe_double("foo") }
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
describe "KeywordArgs:" do
|
362
|
+
it "should pass for exact correct input" do
|
363
|
+
passes { @o.person_keywordargs(:name => "calvin", :age => 10) }
|
364
|
+
end
|
365
|
+
|
366
|
+
it "should fail if some keys don't have contracts" do
|
367
|
+
fails { @o.person_keywordargs(:name => "calvin", :age => 10, :foo => "bar") }
|
368
|
+
end
|
369
|
+
|
370
|
+
it "should fail if a key with a contract on it isn't provided" do
|
371
|
+
fails { @o.person_keywordargs(:name => "calvin") }
|
372
|
+
end
|
373
|
+
|
374
|
+
it "should fail for incorrect input" do
|
375
|
+
fails { @o.person_keywordargs(:name => 50, :age => 10) }
|
376
|
+
fails { @o.hash_keywordargs(:hash => nil) }
|
377
|
+
fails { @o.hash_keywordargs(:hash => 1) }
|
378
|
+
end
|
379
|
+
|
380
|
+
it "should pass if a method is overloaded with non-KeywordArgs" do
|
381
|
+
passes { @o.person_keywordargs("name", 10) }
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
describe "Optional:" do
|
386
|
+
it "can't be used outside of KeywordArgs" do
|
387
|
+
expect do
|
388
|
+
BareOptionalContractUsed.new.something(3, 5)
|
389
|
+
end.to raise_error(ArgumentError, Contracts::Optional::UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
describe "HashOf:" do
|
394
|
+
it "doesn't allow to specify multiple key-value pairs with pretty syntax" do
|
395
|
+
expect do
|
396
|
+
Class.new do
|
397
|
+
include Contracts::Core
|
398
|
+
|
399
|
+
Contract Contracts::HashOf[Symbol => String, Contracts::Num => Contracts::Num] => nil
|
400
|
+
def something(hash)
|
401
|
+
# ...
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end.to raise_error(ArgumentError, "You should provide only one key-value pair to HashOf contract")
|
405
|
+
end
|
406
|
+
|
407
|
+
context "given a fulfilled contract" do
|
408
|
+
it { expect(@o.gives_max_value(:panda => 1, :bamboo => 2)).to eq(2) }
|
409
|
+
it { expect(@o.pretty_gives_max_value(:panda => 1, :bamboo => 2)).to eq(2) }
|
410
|
+
end
|
411
|
+
|
412
|
+
context "given an unfulfilled contract" do
|
413
|
+
it { fails { @o.gives_max_value(:panda => "1", :bamboo => "2") } }
|
414
|
+
it { fails { @o.gives_max_value(nil) } }
|
415
|
+
it { fails { @o.gives_max_value(1) } }
|
416
|
+
it { fails { @o.pretty_gives_max_value(:panda => "1", :bamboo => "2") } }
|
417
|
+
end
|
418
|
+
|
419
|
+
describe "#to_s" do
|
420
|
+
context "given Symbol => String" do
|
421
|
+
it { expect(Contracts::HashOf[Symbol, String].to_s).to eq("Hash<Symbol, String>") }
|
422
|
+
end
|
423
|
+
|
424
|
+
context "given String => Num" do
|
425
|
+
it { expect(Contracts::HashOf[String, Contracts::Num].to_s).to eq("Hash<String, Contracts::Builtin::Num>") }
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
describe "StrictHash:" do
|
431
|
+
context "when given an exact correct input" do
|
432
|
+
it "does not raise an error" do
|
433
|
+
passes { @o.strict_person(:name => "calvin", :age => 10) }
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
context "when given an input with correct keys but wrong types" do
|
438
|
+
it "raises an error" do
|
439
|
+
fails { @o.strict_person(:name => "calvin", :age => "10") }
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
context "when given an input with missing keys" do
|
444
|
+
it "raises an error" do
|
445
|
+
fails { @o.strict_person(:name => "calvin") }
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
context "when given an input with extra keys" do
|
450
|
+
it "raises an error" do
|
451
|
+
fails { @o.strict_person(:name => "calvin", :age => "10", :soft => true) }
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
context "when given not a hash" do
|
456
|
+
it "raises an error" do
|
457
|
+
fails { @o.strict_person(1337) }
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|
461
|
+
end
|