contracts 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,67 @@
1
+ require './lib/contracts'
2
+ require 'benchmark'
3
+ require 'rubygems'
4
+ require 'method_profiler'
5
+ require 'ruby-prof'
6
+
7
+ include Contracts
8
+
9
+ def add a, b
10
+ a + b
11
+ end
12
+
13
+ Contract Num, Num => Num
14
+ def contracts_add a, b
15
+ a + b
16
+ end
17
+
18
+ def explicit_add a, b
19
+ raise unless a.is_a?(Numeric)
20
+ raise unless b.is_a?(Numeric)
21
+ c = a + b
22
+ raise unless c.is_a?(Numeric)
23
+ c
24
+ end
25
+
26
+ def benchmark
27
+ Benchmark.bm 30 do |x|
28
+ x.report 'testing add' do
29
+ 1000000.times do |_|
30
+ add(rand(1000), rand(1000))
31
+ end
32
+ end
33
+ x.report 'testing contracts add' do
34
+ 1000000.times do |_|
35
+ contracts_add(rand(1000), rand(1000))
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def profile
42
+ profilers = []
43
+ profilers << MethodProfiler.observe(Contract)
44
+ profilers << MethodProfiler.observe(Object)
45
+ profilers << MethodProfiler.observe(Contracts::MethodDecorators)
46
+ profilers << MethodProfiler.observe(Contracts::Decorator)
47
+ profilers << MethodProfiler.observe(Contracts::Support)
48
+ profilers << MethodProfiler.observe(UnboundMethod)
49
+ 10000.times do |_|
50
+ contracts_add(rand(1000), rand(1000))
51
+ end
52
+ profilers.each { |p| puts p.report }
53
+ end
54
+
55
+ def ruby_prof
56
+ RubyProf.start
57
+ 100000.times do |_|
58
+ contracts_add(rand(1000), rand(1000))
59
+ end
60
+ result = RubyProf.stop
61
+ printer = RubyProf::FlatPrinter.new(result)
62
+ printer.print(STDOUT)
63
+ end
64
+
65
+ benchmark
66
+ profile
67
+ ruby_prof if ENV["FULL_BENCH"] # takes some time
@@ -0,0 +1,81 @@
1
+ require './lib/contracts'
2
+ require 'benchmark'
3
+ require 'rubygems'
4
+ require 'method_profiler'
5
+ require 'ruby-prof'
6
+
7
+ class Obj < Struct.new(:value)
8
+ include Contracts
9
+
10
+ Contract Num, Num => Num
11
+ def contracts_add a, b
12
+ a + b
13
+ end
14
+ end
15
+
16
+ class ObjWithInvariants < Struct.new(:value)
17
+ include Contracts
18
+ include Contracts::Invariants
19
+
20
+ Invariant(:value_not_nil) { value != nil }
21
+ Invariant(:value_not_string) { !value.is_a?(String) }
22
+
23
+ Contract Num, Num => Num
24
+ def contracts_add a, b
25
+ a + b
26
+ end
27
+ end
28
+
29
+ def benchmark
30
+ obj = Obj.new(3)
31
+ obj_with_invariants = ObjWithInvariants.new(3)
32
+
33
+ Benchmark.bm 30 do |x|
34
+ x.report 'testing contracts add' do
35
+ 1000000.times do |_|
36
+ obj.contracts_add(rand(1000), rand(1000))
37
+ end
38
+ end
39
+ x.report 'testing contracts add with invariants' do
40
+ 1000000.times do |_|
41
+ obj_with_invariants.contracts_add(rand(1000), rand(1000))
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def profile
48
+ obj_with_invariants = ObjWithInvariants.new(3)
49
+
50
+ profilers = []
51
+ profilers << MethodProfiler.observe(Contract)
52
+ profilers << MethodProfiler.observe(Object)
53
+ profilers << MethodProfiler.observe(Contracts::Support)
54
+ profilers << MethodProfiler.observe(Contracts::Invariants)
55
+ profilers << MethodProfiler.observe(Contracts::Invariants::InvariantExtension)
56
+ profilers << MethodProfiler.observe(UnboundMethod)
57
+
58
+ 10000.times do |_|
59
+ obj_with_invariants.contracts_add(rand(1000), rand(1000))
60
+ end
61
+
62
+ profilers.each { |p| puts p.report }
63
+ end
64
+
65
+ def ruby_prof
66
+ RubyProf.start
67
+
68
+ obj_with_invariants = ObjWithInvariants.new(3)
69
+
70
+ 100000.times do |_|
71
+ obj_with_invariants.contracts_add(rand(1000), rand(1000))
72
+ end
73
+
74
+ result = RubyProf.stop
75
+ printer = RubyProf::FlatPrinter.new(result)
76
+ printer.print(STDOUT)
77
+ end
78
+
79
+ benchmark
80
+ profile
81
+ ruby_prof if ENV["FULL_BENCH"] # takes some time
@@ -0,0 +1,59 @@
1
+ require 'benchmark'
2
+
3
+ module Wrapper
4
+ def self.extended(klass)
5
+ klass.class_eval do
6
+ @@methods = {}
7
+ def self.methods
8
+ @@methods
9
+ end
10
+ def self.set_method k, v
11
+ @@methods[k] = v
12
+ end
13
+ end
14
+ end
15
+
16
+ def method_added name
17
+ return if methods.include?(name)
18
+ puts "#{name} added"
19
+ set_method(name, instance_method(name))
20
+ class_eval %{
21
+ def #{name}(*args)
22
+ self.class.methods[#{name.inspect}].bind(self).call(*args)
23
+ end
24
+ }, __FILE__, __LINE__ + 1
25
+ end
26
+ end
27
+
28
+ class NotWrapped
29
+ def add a, b
30
+ a + b
31
+ end
32
+ end
33
+
34
+ class Wrapped
35
+ extend ::Wrapper
36
+ def add a, b
37
+ a + b
38
+ end
39
+ end
40
+
41
+
42
+ w = Wrapped.new
43
+ nw = NotWrapped.new
44
+ #p w.add(1, 4)
45
+ #exit
46
+ # 30 is the width of the output column
47
+ Benchmark.bm 30 do |x|
48
+ x.report 'wrapped' do
49
+ 100000.times do |_|
50
+ w.add(rand(1000), rand(1000))
51
+ end
52
+ end
53
+ x.report 'not wrapped' do
54
+ 100000.times do |_|
55
+ nw.add(rand(1000), rand(1000))
56
+ end
57
+ end
58
+ end
59
+
@@ -0,0 +1,13 @@
1
+ require File.expand_path(File.join(__FILE__, '../lib/contracts/version'))
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "contracts"
5
+ s.version = Contracts::VERSION
6
+ s.date = "2014-05-08"
7
+ s.summary = "Contracts for Ruby."
8
+ s.description = "This library provides contracts for Ruby. Contracts let you clearly express how your code behaves, and free you from writing tons of boilerplate, defensive code."
9
+ s.author = "Aditya Bhargava"
10
+ s.email = "bluemangroupie@gmail.com"
11
+ s.files = `git ls-files`.split("\n")
12
+ s.homepage = "http://github.com/egonSchiele/contracts.ruby"
13
+ end
@@ -1,7 +1,39 @@
1
- require 'decorators'
2
- require 'builtin_contracts'
1
+ require 'contracts/support'
2
+ require 'contracts/decorators'
3
+ require 'contracts/builtin_contracts'
4
+ require 'contracts/invariants'
3
5
 
4
- class ContractError < ArgumentError
6
+ # @private
7
+ # Base class for Contract errors
8
+ #
9
+ # If default failure callback is used it stores failure data
10
+ class ContractBaseError < ArgumentError
11
+ attr_reader :data
12
+
13
+ def initialize(message, data)
14
+ super(message)
15
+ @data = data
16
+ end
17
+
18
+ # Used to convert to simple ContractError from other contract errors
19
+ def to_contract_error
20
+ self
21
+ end
22
+ end
23
+
24
+ # Default contract error
25
+ #
26
+ # If default failure callback is used, users normally see only these contract errors
27
+ class ContractError < ContractBaseError
28
+ end
29
+
30
+ # @private
31
+ # Special contract error used internally to detect pattern failure during pattern matching
32
+ class PatternMatchingError < ContractBaseError
33
+ # Used to convert to ContractError from PatternMatchingError
34
+ def to_contract_error
35
+ ContractError.new(to_s, data)
36
+ end
5
37
  end
6
38
 
7
39
  module Contracts
@@ -14,6 +46,8 @@ module Contracts
14
46
  end
15
47
 
16
48
  def self.common base
49
+ return if base.respond_to?(:Contract)
50
+
17
51
  base.extend MethodDecorators
18
52
  base.instance_eval do
19
53
  def functype(funcname)
@@ -27,6 +61,7 @@ module Contracts
27
61
  end
28
62
  base.class_eval do
29
63
  def Contract(*args)
64
+ return if ENV["NO_CONTRACTS"]
30
65
  self.class.Contract(*args)
31
66
  end
32
67
 
@@ -39,7 +74,7 @@ module Contracts
39
74
  end
40
75
  end
41
76
  end
42
- end
77
+ end
43
78
  end
44
79
 
45
80
  # This is the main Contract class. When you write a new contract, you'll
@@ -48,7 +83,14 @@ end
48
83
  # Contract [contract names] => return_value
49
84
  #
50
85
  # This class also provides useful callbacks and a validation method.
51
- class Contract < Decorator
86
+ class Contract < Contracts::Decorator
87
+ # Default implementation of failure_callback. Provided as a block to be able
88
+ # to monkey patch #failure_callback only temporary and then switch it back.
89
+ # First important usage - for specs.
90
+ DEFAULT_FAILURE_CALLBACK = Proc.new do |data|
91
+ raise data[:contracts].failure_exception.new(failure_msg(data), data)
92
+ end
93
+
52
94
  attr_reader :args_contracts, :ret_contract, :klass, :method
53
95
  # decorator_name :contract
54
96
  def initialize(klass, method, *contracts)
@@ -89,17 +131,8 @@ class Contract < Decorator
89
131
  data[:contract].to_s
90
132
  end
91
133
 
92
- if RUBY_VERSION =~ /^1\.8/
93
- if data[:method].respond_to?(:__file__)
94
- position = data[:method].__file__ + ":" + data[:method].__line__.to_s
95
- else
96
- position = data[:method].inspect
97
- end
98
- else
99
- file, line = data[:method].source_location
100
- position = file + ":" + line.to_s
101
- end
102
- method_name = data[:method].is_a?(Proc) ? "Proc" : data[:method].name
134
+ position = Support.method_position(data[:method])
135
+ method_name = Support.method_name(data[:method])
103
136
 
104
137
  header = if data[:return_value]
105
138
  "Contract violation for return value:"
@@ -123,13 +156,41 @@ class Contract < Decorator
123
156
  #
124
157
  # Example of monkeypatching:
125
158
  #
126
- # Contract.failure_callback(data)
159
+ # def Contract.failure_callback(data)
127
160
  # puts "You had an error!"
128
161
  # puts failure_msg(data)
129
162
  # exit
130
163
  # end
131
- def self.failure_callback(data)
132
- raise ContractError, failure_msg(data)
164
+ def self.failure_callback(data, use_pattern_matching=true)
165
+ if data[:contracts].pattern_match? && use_pattern_matching
166
+ return DEFAULT_FAILURE_CALLBACK.call(data)
167
+ end
168
+
169
+ fetch_failure_callback.call(data)
170
+ end
171
+
172
+ # Used to override failure_callback without monkeypatching.
173
+ #
174
+ # Takes: block parameter, that should accept one argument - data.
175
+ #
176
+ # Example usage:
177
+ #
178
+ # Contract.override_failure_callback do |data|
179
+ # puts "You had an error"
180
+ # puts failure_msg(data)
181
+ # exit
182
+ # end
183
+ def self.override_failure_callback(&blk)
184
+ @failure_callback = blk
185
+ end
186
+
187
+ # Used to restore default failure callback
188
+ def self.restore_failure_callback
189
+ @failure_callback = DEFAULT_FAILURE_CALLBACK
190
+ end
191
+
192
+ def self.fetch_failure_callback
193
+ @failure_callback ||= DEFAULT_FAILURE_CALLBACK
133
194
  end
134
195
 
135
196
  # Used to verify if an argument satisfies a contract.
@@ -157,7 +218,7 @@ class Contract < Decorator
157
218
  # e.g. [Num, String]
158
219
  # TODO account for these errors too
159
220
  lambda { |arg|
160
- return false unless arg.is_a?(Array)
221
+ return false unless arg.is_a?(Array) && arg.length == contract.length
161
222
  arg.zip(contract).all? do |_arg, _contract|
162
223
  Contract.valid?(_arg, _contract)
163
224
  end
@@ -186,10 +247,10 @@ class Contract < Decorator
186
247
  elsif klass == Class
187
248
  lambda { |arg| arg.is_a?(contract) }
188
249
  else
189
- lambda { |arg| contract == arg }
250
+ lambda { |arg| contract == arg }
190
251
  end
191
252
  end
192
- end
253
+ end
193
254
 
194
255
  def [](*args, &blk)
195
256
  call(*args, &blk)
@@ -200,6 +261,7 @@ class Contract < Decorator
200
261
  end
201
262
 
202
263
  def call_with(this, *args, &blk)
264
+
203
265
  _args = blk ? args + [blk] : args
204
266
 
205
267
  # check contracts on arguments
@@ -234,7 +296,30 @@ class Contract < Decorator
234
296
  end
235
297
  unless @ret_validator[result]
236
298
  Contract.failure_callback({:arg => result, :contract => @ret_contract, :class => @klass, :method => @method, :contracts => self, :return_value => true})
237
- end
299
+ end
300
+
301
+ this.verify_invariants!(@method) if this.respond_to?(:verify_invariants!)
302
+
238
303
  result
239
304
  end
305
+
306
+ # Used to determine type of failure exception this contract should raise in case of failure
307
+ def failure_exception
308
+ if @pattern_match
309
+ PatternMatchingError
310
+ else
311
+ ContractError
312
+ end
313
+ end
314
+
315
+ # @private
316
+ # Used internally to mark contract as pattern matching contract
317
+ def pattern_match!
318
+ @pattern_match = true
319
+ end
320
+
321
+ # Used to determine if contract is a pattern matching contract
322
+ def pattern_match?
323
+ @pattern_match
324
+ end
240
325
  end
@@ -1,4 +1,4 @@
1
- require 'testable'
1
+ require 'contracts/testable'
2
2
 
3
3
  =begin rdoc
4
4
  This module contains all the builtin contracts.
@@ -0,0 +1,179 @@
1
+ module Contracts
2
+ module MethodDecorators
3
+ def self.extended(klass)
4
+ return if klass.respond_to?(:decorated_methods=)
5
+
6
+ class << klass
7
+ attr_accessor :decorated_methods
8
+ end
9
+ end
10
+
11
+ # first, when you write a contract, the decorate method gets called which
12
+ # sets the @decorators variable. Then when the next method after the contract
13
+ # is defined, method_added is called and we look at the @decorators variable
14
+ # to find the decorator for that method. This is how we associate decorators
15
+ # with methods.
16
+ def method_added(name)
17
+ common_method_added name, false
18
+ super
19
+ end
20
+
21
+ def singleton_method_added name
22
+ common_method_added name, true
23
+ super
24
+ end
25
+
26
+ def common_method_added name, is_class_method
27
+ return unless @decorators
28
+
29
+ decorators = @decorators.dup
30
+ @decorators = nil
31
+ @decorated_methods ||= {:class_methods => {}, :instance_methods => {}}
32
+
33
+ if is_class_method
34
+ method_reference = method(name)
35
+ method_type = :class_methods
36
+ # private_methods is an array of strings on 1.8 and an array of symbols on 1.9
37
+ is_private = self.private_methods.include?(name) || self.private_methods.include?(name.to_s)
38
+ else
39
+ method_reference = instance_method(name)
40
+ method_type = :instance_methods
41
+ # private_instance_methods is an array of strings on 1.8 and an array of symbols on 1.9
42
+ is_private = self.private_instance_methods.include?(name) || self.private_instance_methods.include?(name.to_s)
43
+ end
44
+
45
+ @decorated_methods[method_type][name] ||= []
46
+
47
+ decorators.each do |klass, args|
48
+ # a reference to the method gets passed into the contract here. This is good because
49
+ # we are going to redefine this method with a new name below...so this reference is
50
+ # now the *only* reference to the old method that exists.
51
+ # We assume here that the decorator (klass) responds to .new
52
+ decorator = klass.new(self, method_reference, *args)
53
+ @decorated_methods[method_type][name] << decorator
54
+ end
55
+
56
+ if @decorated_methods[method_type][name].any? { |x| x.method != method_reference }
57
+ @decorated_methods[method_type][name].each do |decorator|
58
+ decorator.pattern_match!
59
+ end
60
+ end
61
+
62
+ # in place of this method, we are going to define our own method. This method
63
+ # just calls the decorator passing in all args that were to be passed into the method.
64
+ # The decorator in turn has a reference to the actual method, so it can call it
65
+ # on its own, after doing it's decorating of course.
66
+
67
+ =begin
68
+ Very important: THe line `current = #{self}` in the start is crucial.
69
+ Not having it means that any method that used contracts could NOT use `super`
70
+ (see this issue for example: https://github.com/egonSchiele/contracts.ruby/issues/27).
71
+ Here's why: Suppose you have this code:
72
+
73
+ class Foo
74
+ Contract nil => String
75
+ def to_s
76
+ "Foo"
77
+ end
78
+ end
79
+
80
+ class Bar < Foo
81
+ Contract nil => String
82
+ def to_s
83
+ super + "Bar"
84
+ end
85
+ end
86
+
87
+ b = Bar.new
88
+ p b.to_s
89
+
90
+ `to_s` in Bar calls `super`. So you expect this to call `Foo`'s to_s. However,
91
+ we have overwritten the function (that's what this next defn is). So it gets a
92
+ reference to the function to call by looking at `decorated_methods`.
93
+
94
+ Now, this line used to read something like:
95
+
96
+ current = self#{is_class_method ? "" : ".class"}
97
+
98
+ In that case, `self` would always be `Bar`, regardless of whether you were calling
99
+ Foo's to_s or Bar's to_s. So you would keep getting Bar's decorated_methods, which
100
+ means you would always call Bar's to_s...infinite recursion! Instead, you want to
101
+ call Foo's version of decorated_methods. So the line needs to be `current = #{self}`.
102
+ =end
103
+ method_def = %{
104
+ def #{is_class_method ? "self." : ""}#{name}(*args, &blk)
105
+ current = #{self}
106
+ ancestors = current.ancestors
107
+ ancestors.shift # first one is just the class itself
108
+ while current && !current.respond_to?(:decorated_methods) || current.decorated_methods.nil?
109
+ current = ancestors.shift
110
+ end
111
+ if !current.respond_to?(:decorated_methods) || current.decorated_methods.nil?
112
+ raise "Couldn't find decorator for method " + self.class.name + ":#{name}.\nDoes this method look correct to you? If you are using contracts from rspec, rspec wraps classes in it's own class.\nLook at the specs for contracts.ruby as an example of how to write contracts in this case."
113
+ end
114
+ methods = current.decorated_methods[#{is_class_method ? ":class_methods" : ":instance_methods"}][#{name.inspect}]
115
+
116
+ # this adds support for overloading methods. Here we go through each method and call it with the arguments.
117
+ # If we get a ContractError, we move to the next function. Otherwise we return the result.
118
+ # If we run out of functions, we raise the last ContractError.
119
+ success = false
120
+ i = 0
121
+ result = nil
122
+ expected_error = methods[0].failure_exception
123
+ while !success
124
+ method = methods[i]
125
+ i += 1
126
+ begin
127
+ success = true
128
+ result = method.call_with(self, *args, &blk)
129
+ rescue expected_error => error
130
+ success = false
131
+ unless methods[i]
132
+ begin
133
+ ::Contract.failure_callback(error.data, false)
134
+ rescue expected_error => final_error
135
+ raise final_error.to_contract_error
136
+ end
137
+ end
138
+ end
139
+ end
140
+ result
141
+ end
142
+ #{is_private ? "private #{name.inspect}" : ""}
143
+ }
144
+
145
+ class_eval method_def, __FILE__, __LINE__ + 1
146
+ end
147
+
148
+ def decorate(klass, *args)
149
+ @decorators ||= []
150
+ @decorators << [klass, args]
151
+ end
152
+ end
153
+
154
+ class Decorator
155
+ # an attr_accessor for a class variable:
156
+ class << self; attr_accessor :decorators; end
157
+
158
+ def self.inherited(klass)
159
+ name = klass.name.gsub(/^./) {|m| m.downcase}
160
+
161
+ return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
162
+
163
+ # the file and line parameters set the text for error messages
164
+ # make a new method that is the name of your decorator.
165
+ # that method accepts random args and a block.
166
+ # inside, `decorate` is called with those params.
167
+ MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
168
+ def #{klass}(*args, &blk)
169
+ return if ENV["NO_CONTRACTS"]
170
+ decorate(#{klass}, *args, &blk)
171
+ end
172
+ ruby_eval
173
+ end
174
+
175
+ def initialize(klass, method)
176
+ @method = method
177
+ end
178
+ end
179
+ end