propr 0.2.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.
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"