propr 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/NOTES.md +62 -0
  2. data/README.md +553 -0
  3. data/Rakefile +83 -0
  4. data/TODO.md +64 -0
  5. data/lib/propr.rb +123 -0
  6. data/lib/propr/dsl.rb +6 -0
  7. data/lib/propr/dsl/check.rb +49 -0
  8. data/lib/propr/dsl/property.rb +62 -0
  9. data/lib/propr/property.rb +23 -0
  10. data/lib/propr/random.rb +143 -0
  11. data/lib/propr/random/array.rb +19 -0
  12. data/lib/propr/random/bigdecimal.rb +43 -0
  13. data/lib/propr/random/boolean.rb +7 -0
  14. data/lib/propr/random/complex.rb +0 -0
  15. data/lib/propr/random/date.rb +17 -0
  16. data/lib/propr/random/float.rb +60 -0
  17. data/lib/propr/random/hash.rb +55 -0
  18. data/lib/propr/random/integer.rb +38 -0
  19. data/lib/propr/random/maybe.rb +0 -0
  20. data/lib/propr/random/nil.rb +8 -0
  21. data/lib/propr/random/range.rb +32 -0
  22. data/lib/propr/random/rational.rb +0 -0
  23. data/lib/propr/random/set.rb +22 -0
  24. data/lib/propr/random/string.rb +41 -0
  25. data/lib/propr/random/symbol.rb +13 -0
  26. data/lib/propr/random/time.rb +14 -0
  27. data/lib/propr/rspec.rb +97 -0
  28. data/lib/propr/runner.rb +53 -0
  29. data/lib/propr/shrink/array.rb +16 -0
  30. data/lib/propr/shrink/bigdecimal.rb +17 -0
  31. data/lib/propr/shrink/boolean.rb +11 -0
  32. data/lib/propr/shrink/complex.rb +0 -0
  33. data/lib/propr/shrink/date.rb +12 -0
  34. data/lib/propr/shrink/float.rb +17 -0
  35. data/lib/propr/shrink/hash.rb +18 -0
  36. data/lib/propr/shrink/integer.rb +10 -0
  37. data/lib/propr/shrink/maybe.rb +11 -0
  38. data/lib/propr/shrink/nil.rb +5 -0
  39. data/lib/propr/shrink/object.rb +5 -0
  40. data/lib/propr/shrink/range.rb +4 -0
  41. data/lib/propr/shrink/rational.rb +4 -0
  42. data/lib/propr/shrink/set.rb +18 -0
  43. data/lib/propr/shrink/string.rb +19 -0
  44. data/lib/propr/shrink/symbol.rb +5 -0
  45. data/lib/propr/shrink/time.rb +9 -0
  46. data/spec/examples/choose/array.example +12 -0
  47. data/spec/examples/choose/hash.example +12 -0
  48. data/spec/examples/choose/range.example +13 -0
  49. data/spec/examples/choose/set.example +12 -0
  50. data/spec/examples/guard.example +38 -0
  51. data/spec/examples/random/array.example +38 -0
  52. data/spec/examples/random/hash.example +18 -0
  53. data/spec/examples/random/integer.example +23 -0
  54. data/spec/examples/random/range.example +43 -0
  55. data/spec/examples/scale.example +17 -0
  56. data/spec/examples/shrink/array.example +20 -0
  57. data/spec/examples/shrink/bigdecimal.example +20 -0
  58. data/spec/examples/shrink/float.example +20 -0
  59. data/spec/examples/shrink/hash.example +20 -0
  60. data/spec/examples/shrink/integer.example +21 -0
  61. data/spec/examples/shrink/maybe.example +24 -0
  62. data/spec/examples/shrink/set.example +21 -0
  63. data/spec/examples/shrink/string.example +17 -0
  64. data/spec/issues/003.example +9 -0
  65. data/spec/spec_helper.rb +24 -0
  66. metadata +143 -0
@@ -0,0 +1,83 @@
1
+ require "pathname"
2
+ abspath = Pathname.new(File.dirname(__FILE__)).expand_path
3
+ relpath = abspath.relative_path_from(Pathname.pwd)
4
+
5
+ begin
6
+ require "rubygems"
7
+ require "bundler/setup"
8
+ rescue LoadError
9
+ warn "couldn't load bundler:"
10
+ warn " #{$!}"
11
+ end
12
+
13
+ task :console do
14
+ exec *%w(irb -I lib -r propr)
15
+ end
16
+
17
+ begin
18
+ require "rspec/core/rake_task"
19
+
20
+ RSpec::Core::RakeTask.new do |t|
21
+ t.verbose = false
22
+ t.pattern = "#{relpath}/spec/examples/**/*.example"
23
+
24
+ t.rspec_opts = %w(--color)
25
+ t.rspec_opts << "--profile"
26
+ t.rspec_opts << "-I#{abspath}/spec"
27
+ end
28
+ rescue LoadError
29
+ task :spec do
30
+ warn "couldn't load rspec"
31
+ warn " #{$!}"
32
+ exit 1
33
+ end
34
+ end
35
+
36
+ begin
37
+ require "rcov"
38
+ begin
39
+ require "rspec/core/rake_task"
40
+ RSpec::Core::RakeTask.new(:rcov) do |t|
41
+ t.rcov = true
42
+ t.rcov_opts = "--exclude spec/,gems/,00401"
43
+
44
+ t.verbose = false
45
+ t.pattern = "#{relpath}/spec/examples/**/*.example"
46
+
47
+ t.rspec_opts = %w(--color --format p)
48
+ t.rspec_opts << "-I#{abspath}/spec"
49
+ end
50
+ rescue LoadError
51
+ task :rcov do
52
+ warn "couldn't load rspec"
53
+ warn " #{$!}"
54
+ exit 1
55
+ end
56
+ end
57
+ rescue LoadError
58
+ task :rcov do
59
+ warn "couldn't load rcov:"
60
+ warn " #{$!}"
61
+ exit 1
62
+ end
63
+ end
64
+
65
+ begin
66
+ require "yard"
67
+
68
+ # Note options are loaded from .yardopts
69
+ YARD::Rake::YardocTask.new(:yard => :clobber_yard)
70
+
71
+ task :clobber_yard do
72
+ rm_rf "#{relpath}/doc/generated"
73
+ mkdir_p "#{relpath}/doc/generated/images"
74
+ end
75
+ rescue LoadError
76
+ task :yard do
77
+ warn "couldn't load yard:"
78
+ warn " #{$!}"
79
+ exit 1
80
+ end
81
+ end
82
+
83
+ task :default => :spec
data/TODO.md ADDED
@@ -0,0 +1,64 @@
1
+ Priorities
2
+
3
+ 1. Write unit tests
4
+ 2. Write property tests
5
+ 3. Write example properties and subclasses (markup)
6
+ 4. API documentation
7
+ 5. User documentation
8
+ 6. Optimization (drop Ruby 1.8)
9
+ 7. Publish as rubygem
10
+ 8. Update Stupidedi
11
+
12
+ Specifics
13
+
14
+ * Organization of common properties
15
+
16
+ ```ruby
17
+ module ShrinkSpecs
18
+ def self
19
+ # must hold for all implementations
20
+ property("no value is smaller than itself"){|x| not x.shrink.member?(x) }
21
+ end
22
+ end
23
+
24
+ describe String, "#shrink" do
25
+ # property holds for all implementations
26
+ ShrinkSpecs.self
27
+ .check { String.random ... }
28
+
29
+ # property of the String#shrink implementation
30
+ property("empty"){|s| s.shrink.member?("") }
31
+ .check { String.random ... }
32
+
33
+ # property of the String#shrink implementation
34
+ property("shorter"){|s| s.shrink.all?{|x| x.length < s.length }}
35
+ .check { String.random ... }
36
+ end
37
+
38
+ describe Integer, "#shrink" do
39
+ # property holds for all implementations
40
+ ShrinkSpecs.self
41
+ .check { Integer.random ... }
42
+
43
+ # property of the Integer#shrink implementation
44
+ property("smaller"){|n| n.shrink.all?{|m| m.abs < n.abs }}
45
+ .check { Integer.random ... }
46
+ end
47
+ ```
48
+
49
+ * Stateful random generation
50
+ * <del>Re-consider sized values (magntitude, center) or range?</del>
51
+ * <del>Re-implement sized values</del>
52
+ * Steal `collect` and `classify` from QuickCheck
53
+
54
+ ```ruby
55
+ property("foo") { ... }
56
+ .check{|rand| rand.integer.tap{|n| classify(n < 0, "negative") }
57
+ .tap{|n| classify(n > 0, "positive") }}
58
+
59
+ property("bar") { ... }
60
+ check{|rand| rand.array.tap{|xs| collect xs.length }}
61
+ ```
62
+
63
+ * <del>Shrink input with breadth first</del>
64
+ * See also: smallcheck, deepcheck
@@ -0,0 +1,123 @@
1
+ module Propr
2
+ autoload :Property, "propr/property"
3
+ autoload :Dsl, "propr/dsl"
4
+ autoload :Runner, "propr/runner"
5
+ autoload :RSpec, "propr/rspec"
6
+ autoload :RSpecAdapter, "propr/rspec"
7
+
8
+ require "fr"
9
+ require "propr/random"
10
+
11
+ def self.wrap(object)
12
+ if object.nil?
13
+ []
14
+ elsif object.respond_to?(:to_ary)
15
+ object.to_ary || [object]
16
+ else
17
+ [object]
18
+ end
19
+ end
20
+
21
+ class GuardFailure < StandardError
22
+ end
23
+
24
+ class Falsifiable < StandardError
25
+ attr_reader :counterex, :shrunken, :passed, :skipped
26
+
27
+ def initialize(counterex, shrunken, passed, skipped)
28
+ @counterex, @shrunken, @passed, @skipped =
29
+ counterex, shrunken, passed, skipped
30
+ end
31
+
32
+ def counterex
33
+ Propr.wrap(@counterex)
34
+ end
35
+
36
+ def shrunken
37
+ Propr.wrap(@shrunken)
38
+ end
39
+
40
+ def to_s
41
+ if @shrunken.nil?
42
+ ["input: #{counterex.map(&:inspect).join(", ")}",
43
+ "after: #{@passed} passed, #{@skipped} skipped"].join("\n")
44
+ else
45
+ ["input: #{counterex.map(&:inspect).join(", ")}",
46
+ "shrunken: #{shrunken.map(&:inspect).join(", ")}",
47
+ "after: #{@passed} passed, #{@skipped} skipped"].join("\n")
48
+ end
49
+ end
50
+ end
51
+
52
+ class Failure < StandardError
53
+ attr_reader :counterex, :shrunken, :passed, :skipped
54
+
55
+ def initialize(exception, counterex, shrunken, passed, skipped)
56
+ @exception, @counterex, @shrunken, @passed, @skipped =
57
+ exception, counterex, shrunken, passed, skipped
58
+ end
59
+
60
+ def counterex
61
+ Propr.wrap(@counterex)
62
+ end
63
+
64
+ def shrunken
65
+ Propr.wrap(@shrunken)
66
+ end
67
+
68
+ def class
69
+ @exception.class
70
+ end
71
+
72
+ def backtrace
73
+ @exception.backtrace
74
+ end
75
+
76
+ def to_s
77
+ if @shrunken.nil?
78
+ [@exception.message,
79
+ "input: #{counterex.map(&:inspect).join(", ")}",
80
+ "after: #{@passed} passed, #{@skipped} skipped"].join("\n")
81
+ else
82
+ [@exception.message,
83
+ "input: #{counterex.map(&:inspect).join(", ")}",
84
+ "shrunken: #{shrunken.map(&:inspect).join(", ")}",
85
+ "after: #{@passed} passed, #{@skipped} skipped"].join("\n")
86
+ end
87
+ end
88
+
89
+ alias_method :message, :to_s
90
+ end
91
+
92
+ class NoMoreTries < StandardError
93
+ # @return [Integer]
94
+ attr_reader :tries
95
+
96
+ def initialize(tries)
97
+ @tries = tries
98
+ end
99
+
100
+ # @return [String]
101
+ def to_s
102
+ "Exceeded #{@tries} failed guards"
103
+ end
104
+ end
105
+
106
+ def self.RSpec(checkdsl, propdsl)
107
+ Module.new.tap do |m|
108
+ m.send(:define_method, :property) { raise }
109
+ m.send(:define_singleton_method, :rand) { rand }
110
+ m.send(:define_singleton_method, :included) do |scope|
111
+
112
+ # @todo: raise an error if body isn't given
113
+ scope.send(:define_singleton_method, :property) do |name, options = {}, &body|
114
+ q = Dsl::Property.wrap(body)
115
+ p = Property.new(name, q)
116
+ RSpecAdapter.new(self, options, p)
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ RSpec = RSpec(nil, nil)
123
+ end
@@ -0,0 +1,6 @@
1
+ module Propr
2
+ module Dsl
3
+ autoload :Check, "propr/dsl/check"
4
+ autoload :Property, "propr/dsl/property"
5
+ end
6
+ end
@@ -0,0 +1,49 @@
1
+ module Propr
2
+ module Dsl
3
+
4
+ class Check
5
+
6
+ # Generates a monadic action, to be run with Random.eval
7
+ def self.wrap(block, m = Propr::Random)
8
+ new(block, m).instance_exec(&block)
9
+ end
10
+
11
+ def initialize(block, m)
12
+ @context, @m =
13
+ Kernel.eval("self", block.binding), m
14
+ end
15
+
16
+ def bind(f, &g)
17
+ @m.bind(f, &g)
18
+ end
19
+
20
+ def unit(value)
21
+ @m.unit(value)
22
+ end
23
+
24
+ def guard(*conditions)
25
+ @m.guard(*conditions)
26
+ end
27
+
28
+ def join(value)
29
+ @m.join(value)
30
+ end
31
+
32
+ def sequence(actions)
33
+ @m.sequence(actions)
34
+ end
35
+
36
+ def scale(*args)
37
+ @m.scale(*args)
38
+ end
39
+
40
+ private
41
+
42
+ def method_missing(name, *args, &block)
43
+ @context.__send__(name, *args, &block)
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,62 @@
1
+ module Propr
2
+ module Dsl
3
+
4
+ class Property
5
+
6
+ # Properties shouldn't be monadic, as all random data is generated
7
+ # elsewhere and passed as arguments to the property. However, this
8
+ # provides a workaround: m.eval, m.unit, m.bind, etc where `m` is
9
+ # given as an argument to `wrap`.
10
+ attr_reader :m
11
+
12
+ # Generates a new function, which should return a Boolean
13
+ def self.wrap(block, m = Propr::Random)
14
+ case block.arity
15
+ when 0; lambda{|| new(block, m).instance_exec(&block) }
16
+ when 1; lambda{|a| new(block, m).instance_exec(a,&block) }
17
+ when 2; lambda{|a,b| new(block, m).instance_exec(a,b,&block) }
18
+ when 3; lambda{|a,b,c| new(block, m).instance_exec(a,b,c,&block) }
19
+ when 4; lambda{|a,b,c,d| new(block, m).instance_exec(a,b,c,d &block) }
20
+ when 5; lambda{|a,b,c,d,e| new(block, m).instance_exec(a,b,c,d,e,&block) }
21
+ when 6; lambda{|a,b,c,d,e,f| new(block, m).instance_exec(a,b,c,d,e,f,&block) }
22
+ when 7; lambda{|a,b,c,d,e,f,g| new(block, m).instance_exec(a,b,c,d,e,f,g,&block) }
23
+ when 8; lambda{|a,b,c,d,e,f,g,h| new(block, m).instance_exec(a,b,c,d,e,f,g,h,&block) }
24
+ else lambda{|*args| new(block, m).instance_exec(*args,&block) }
25
+ end
26
+ end
27
+
28
+ def initialize(block, m)
29
+ @context, @m =
30
+ Kernel.eval("self", block.binding), m
31
+ end
32
+
33
+ def error?(type = Exception)
34
+ begin
35
+ yield
36
+ false
37
+ rescue => e
38
+ e.is_a?(type)
39
+ end
40
+ end
41
+
42
+ def guard(*conditions)
43
+ if index = conditions.index{|x| not x }
44
+ raise GuardFailure,
45
+ "guard condition #{index} was false"
46
+ end
47
+ end
48
+
49
+ def label(value)
50
+ # @todo
51
+ end
52
+
53
+ private
54
+
55
+ def method_missing(name, *args, &block)
56
+ @context.__send__(name, *args, &block)
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,23 @@
1
+ module Propr
2
+ class Property
3
+ # @return [Proc]
4
+ def self.new(name, body)
5
+ body.instance_variable_set(:@name, name)
6
+
7
+ # @return [String]
8
+ body.define_singleton_method(:name) { @name }
9
+
10
+ # @return [Boolean]
11
+ body.define_singleton_method(:check) do |*args, &block|
12
+ if block.nil?
13
+ true == call(*args)
14
+ else
15
+ count = args.first || 100
16
+ count.times.all? { true == call(*block.call) }
17
+ end
18
+ end
19
+
20
+ body
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,143 @@
1
+ module Propr
2
+
3
+ class Random
4
+ end
5
+
6
+ class << Random
7
+ include Fr::Monad
8
+
9
+ # Evaluators
10
+ #############################################
11
+
12
+ def run(computation, scale = BigDecimal(1))
13
+ computation.call(bound scale)
14
+ end
15
+
16
+ def eval(computation, scale = BigDecimal(1), retries = 0)
17
+ skipped = 0
18
+ scale = bound(scale)
19
+
20
+ while true
21
+ value, _, success = computation.call(scale)
22
+
23
+ if success
24
+ return value
25
+ elsif (skipped += 1) > retries
26
+ raise NoMoreTries, retries
27
+ end
28
+ end
29
+ end
30
+
31
+ # Combinators
32
+ #############################################
33
+
34
+ def unit(value)
35
+ lambda do |scale|
36
+ [value, scale, true]
37
+ end
38
+ end
39
+
40
+ def bind(f, &g)
41
+ lambda do |scale|
42
+ value, scale, success = f.call(scale)
43
+
44
+ success ?
45
+ g.call(value).call(scale) :
46
+ [value, scale, success]
47
+ end
48
+ end
49
+
50
+ # Actions
51
+ #############################################
52
+
53
+ # TODO: Make Random an instance of Functor and use Functor.guard?
54
+ def guard(*conditions)
55
+ lambda do |scale|
56
+ [nil, scale, conditions.all?]
57
+ end
58
+ end
59
+
60
+ # When given two arguments, scales a numeric value around a
61
+ # given origin `zero`, using the current scale factor (0..1).
62
+ #
63
+ def scale(number, range, zero)
64
+ if range.zero?
65
+ # No scaling
66
+ lambda do |scale|
67
+ [number, scale, true]
68
+ end
69
+ else
70
+ # Shrink range exponentially, and -1 + scale reduces the
71
+ # rng_ to 0 when scale = 0, but rng_ = range when scale = 1.
72
+ lambda do |scale|
73
+ rng_ = (range ** scale) - 1 + scale
74
+ pct = (number - zero) / range
75
+ [zero + rng_ * pct, scale, true]
76
+ end
77
+ end
78
+ end
79
+
80
+ # Generate psuedo-random number normally distributed between
81
+ # 0 <= x < 1. This distribution is not weighted using `scale`.
82
+ #
83
+ def rand(limit = nil)
84
+ if not limit.nil? and limit <= 0
85
+ raise InvalidArgument, "limit <= 0"
86
+ end
87
+
88
+ lambda do |scale|
89
+ [Kernel.rand(limit), scale, true]
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def bound(scale)
96
+ if scale > 1
97
+ scale.coerce(1)[0]
98
+ elsif scale < 0
99
+ scale.coerce(0)[0]
100
+ else
101
+ scale
102
+ end
103
+ end
104
+
105
+ end
106
+ end
107
+
108
+ # Instances of Monad Random
109
+ require "propr/random/array"
110
+ require "propr/random/bigdecimal"
111
+ require "propr/random/boolean"
112
+ require "propr/random/complex"
113
+ require "propr/random/date"
114
+ require "propr/random/float"
115
+ require "propr/random/hash"
116
+ require "propr/random/integer"
117
+ require "propr/random/maybe"
118
+ require "propr/random/nil"
119
+ require "propr/random/range"
120
+ require "propr/random/rational"
121
+ require "propr/random/set"
122
+ require "propr/random/string"
123
+ require "propr/random/symbol"
124
+ require "propr/random/time"
125
+
126
+ # Instances of Shrink "typeclass"
127
+ require "propr/shrink/object"
128
+ require "propr/shrink/array"
129
+ require "propr/shrink/bigdecimal"
130
+ require "propr/shrink/boolean"
131
+ require "propr/shrink/complex"
132
+ require "propr/shrink/date"
133
+ require "propr/shrink/float"
134
+ require "propr/shrink/hash"
135
+ require "propr/shrink/integer"
136
+ require "propr/shrink/maybe"
137
+ require "propr/shrink/nil"
138
+ require "propr/shrink/range"
139
+ require "propr/shrink/rational"
140
+ require "propr/shrink/set"
141
+ require "propr/shrink/string"
142
+ require "propr/shrink/symbol"
143
+ require "propr/shrink/time"