justinf-unification_assertion 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in unification_assertion.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # UnificationAssertion
2
+
3
+ UnificationAssertion provides powerful and simple way to compare two
4
+ structures which may be different partialy. The comparison is based on
5
+ unification algorithm, which is used in Prolog implementations and
6
+ type inference algorithms.
7
+
8
+ The assertion will be like the following:
9
+
10
+ assert_unifiable({ "timestamp" => :_,
11
+ "person" => {
12
+ "id" => :_,
13
+ "name" => "John",
14
+ "email" => "john@example.com",
15
+ "created_at" => :_a,
16
+ "updated_at" => :_a
17
+ }
18
+ }, JSON.parse(@response.body))
19
+
20
+ It compares two hash objects, but it does not care the exact value of
21
+ `"timestamp"`, `"id"`, `"created_at"`, and `"updated_at"`. The meta
22
+ variable `:_a` is not a black hole but test the equality between
23
+ `"created_at"` and `"updated_at"`.
24
+
25
+ ## Introduction
26
+
27
+ I have been writing some tests like the following Rails functional
28
+ tests.
29
+
30
+ # Testing a web api which create a person data and send the created person object as JSON
31
+ post(:create_person, { :person => { :name => "John", :email => "john@example.com" } })
32
+
33
+ # Compare expected hash and actual result parsed by JSON parser
34
+ assert_equal({ "timestamp" => Time.now,
35
+ "person" => {
36
+ "id" => 13,
37
+ "name" => "John",
38
+ "email" => "john@example.com",
39
+ "created_at" => Time.now,
40
+ "updated_at" => Time.now
41
+ }
42
+ }, JSON.parse(@response.body))
43
+
44
+ You may point out some problems on the test.
45
+
46
+ * The ID of the result may be different from 13
47
+ * It is not sure to assume `result["timestamp"]` is equal to `Time.now`
48
+ * It is not sure to assume `result["person"]["created_at"]` is equal to `Time.now`
49
+
50
+ The root of the problems is that the comparison is too strict. The
51
+ properties I would like to test is only its `name` and `email`
52
+ fields. So I should test like the following:
53
+
54
+ assert_equal "John", result["person"]["name"]
55
+ assert_equal "john@example.com", result["person"]["email"]
56
+
57
+ It looks too complicated, and we need some way to compare structures.
58
+ This library, UnificationAssertion, provides the primitive for the
59
+ comparison called `assert_unifiable`.
60
+
61
+ assert_unifiable({ "timestamp" => :_,
62
+ "person" => {
63
+ "id" => :_,
64
+ "name" => "John",
65
+ "email" => "john@example.com",
66
+ "created_at" => :_a,
67
+ "updated_at" => :_a
68
+ }
69
+ }, JSON.parse(@response.body))
70
+
71
+ Symbols `:_a` for example, where its name starts with `_` is
72
+ interpreted as a meta variable. `assert_unifiable` does not care their
73
+ exact value is, but only the existence (can be `nil`) and equalities
74
+ for each occurance will be tested. The special symbol `:_` is a
75
+ wildcard. It can appear many times, but it will not be bound with any
76
+ value.
77
+
78
+ ## Examples
79
+
80
+ assert_unifiable(:_a, 1) # pass, :_a will be 1
81
+ assert_unifiable([:_a, 1], [1, 1]) # pass, :_a will be 1
82
+ assert_unifiable([:_, :_], [1, 2]) # pass, :_ can not be bound with any value
83
+ assert_unifiable([:_a, :_a], [1, 2]) # fail, :_a can not be either 1 and 2
84
+ assert_unifiable([:_a], [1,2,3]) # fail, :_a can be a value but can not be a sequence
85
+
86
+ assert_unifiable({ :x => :_a }, { :x => 1 }) # pass, :_a will be 1
87
+ assert_unifiable({ :y => :_a }, { }) # fail, a key :y should be present
88
+ assert_unifiable({ :y => :_a }, { :y => nil }) # pass, :_a will be nil
89
+ assert_unifiable({ :_a => 1 }, { :x => 1 }) # fail, meta variable can not appear as a key
90
+
91
+ # assert_unifiable can receive a block, which will be yielded with the result of unification.
92
+ assert_unifiable([:_a, :_b], [1, 2]) do |unifier|
93
+ assert unifier[:_a] < unifier[:_b]
94
+ end
95
+
96
+ ## Installation
97
+
98
+ Update your `Gemfile`.
99
+
100
+ gem "unification_assertion", :git => "git://github.com/soutaro/unification_assertion.git"
101
+
102
+ Write your test case.
103
+
104
+ require "minitest/autorun"
105
+ require "unification_assertion"
106
+
107
+ class GreatTest < MiniTest::Unit::TestCase
108
+ include UnificationAssertion
109
+
110
+ def test_something
111
+ assert_unifiable([:_a, :_b], [1, 2])
112
+ end
113
+ end
114
+
115
+ ## Known Issues
116
+
117
+ ### It skips occur check
118
+
119
+ The recursive pattern can not be processed well.
120
+
121
+ assert_unifiable(:_a, { :x => :_a })
122
+
123
+ Usual unification algorithm rejects such input by *occur check*. This
124
+ library is expected to be used for testing, so that I omit the
125
+ checking. (Who in the world will write such comparison?)
126
+
127
+ ## Author
128
+
129
+ Written by Soutaro Matsumoto. (matsumoto at soutaro dot com)
130
+
131
+ Released under the MIT License: www.opensource.org/licenses/mit-license.php
132
+
133
+ github.com/soutaro/unification_assertion
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ desc "Test the library"
4
+ task :test do
5
+ ENV['JSON'] = 'pure'
6
+ ENV['RUBYOPT'] = "-Ilib #{ENV['RUBY_OPT']}"
7
+ exec "ruby", *Dir['./test/*.rb']
8
+ end
@@ -0,0 +1,145 @@
1
+ require "unification_assertion/version"
2
+
3
+ module UnificationAssertion
4
+ module_function
5
+
6
+ @@comparators = {}
7
+
8
+ @@comparators[Array] = lambda {|a, b, message, eqs, unifier, &block|
9
+ block.call(a.length, b.length, message + " (Array length mismatch)")
10
+ eqs.concat(a.zip(b))
11
+ }
12
+
13
+ @@comparators[Hash] = lambda {|a, b, message, eqs, unifier, &block|
14
+ block.call(a.keys.sort, b.keys.sort, message + " (Hash keys mismatch)")
15
+ eqs.concat(a.keys.map {|key| [a[key], b[key]] })
16
+ }
17
+
18
+ # Comparators are hash from a class to its comparator.
19
+ # It is used to check unifiability of two object of the given hash.
20
+ #
21
+ # There are two comparators defined by default; for |Array| and |Hash|.
22
+ #
23
+ # == Example ==
24
+ # UnificationAssertion.comparators[Array] = lambda do |a, b, message, eqs, unifier, &block|
25
+ # block.call(a.length, bl.ength, message + " (Array length mismatch)")
26
+ # eqs.concat(a.zip(b))
27
+ # end
28
+ #
29
+ # This is our comparator for |Array|. It first tests if the length of the two arrays are equal.
30
+ # And then, it pushes the equations of each components of the arrays.
31
+ # The components of the arrays will be tested unifiability later.
32
+ #
33
+ # == Comparator ==
34
+ #
35
+ # Comparator is a lambda which takes 5 arguments and block and checks the unifiability
36
+ # of the first two arguments.
37
+ #
38
+ # * |a|, |b| Objects they are compared.
39
+ # * |message| Message given to |unify|.
40
+ # * |eqs| Equations array. This is for output.
41
+ # * |unifier| Current unifier. This is just for reference.
42
+ # * |&block| Block given to |unify|, which can be used to check the equality of two values.
43
+ #
44
+ def comparators
45
+ @@comparators
46
+ end
47
+
48
+ # Run unification algorithm for given equations.
49
+ # If all equations in |eqs| are unifiable, |unify| returns (the most-general) unifier.
50
+ #
51
+ # Which identifies an symbol as a meta variable if the name matches with |options[:meta_pattern]|.
52
+ # The default of the meta variable pattern is |/^_/|.
53
+ # For example, |:_a| and |:_xyz| are meta variable, but |:a| and |:hello_world| are not.
54
+ #
55
+ # It also accepts wildcard variable. Which matches with any value, but does not introduce new equational constraints.
56
+ # The default wildcard is |:_|.
57
+ #
58
+ # |unify| takes a block to test equation of two values.
59
+ # The simplest form should be using |assert_equal|, however it can be customized as you like.
60
+ #
61
+ # == Example ==
62
+ # unify([[:_a, 1], [{ :x => :_b, :y => 1 }, { :x => 3, :y => 1 }]], "Example!!") do |x, y, message|
63
+ # assert_equal(x,y,message)
64
+ # end
65
+ #
66
+ # The |unify| call will return an hash |{ :_a => 1, :_b => 3 }|.
67
+ # The block will be used to test equality between 1 and 1 (it will pass.)
68
+ #
69
+ def unify(eqs, message = "", unifier = {}, options = {}, &block)
70
+ options = { :meta_pattern => /^_/, :wildcard => :_ }.merge!(options)
71
+
72
+ pattern = options[:meta_pattern]
73
+ wildcard = options[:wildcard]
74
+
75
+ while eq = eqs.shift
76
+ a,b = eq
77
+ case
78
+ when (Symbol === a and a.to_s =~ pattern)
79
+ unless a == wildcard
80
+ eqs = substitute({ a => b }, eqs)
81
+ unifier = substitute({ a => b }, unifier).merge!(a => b)
82
+ end
83
+ when (Symbol === b and b.to_s =~ pattern)
84
+ unless b == wildcard
85
+ eqs = substitute({ b => a }, eqs)
86
+ unifier = substitute({ b => a }, unifier).merge!(b => a)
87
+ end
88
+ when (a.class == b.class and @@comparators[a.class])
89
+ @@comparators[a.class].call(a, b, message, eqs, unifier, &block)
90
+ else
91
+ yield(a, b, message)
92
+ end
93
+ end
94
+
95
+ unifier.inject({}) {|acc, (key, value)|
96
+ if key == value
97
+ acc
98
+ else
99
+ acc.merge!(key => value)
100
+ end
101
+ }
102
+ end
103
+
104
+ @@substituters = {}
105
+ @@substituters[Hash] = lambda {|unifier, hash|
106
+ hash.inject({}) {|acc, (key, val)|
107
+ if unifier[val]
108
+ acc.merge!(key => unifier[val])
109
+ else
110
+ acc.merge!(key => substitute(unifier, val))
111
+ end
112
+ }
113
+ }
114
+ @@substituters[Symbol] = lambda {|unifier, symbol| unifier[symbol] or symbol }
115
+ @@substituters[Array] = lambda {|unifier, array|
116
+ array.map {|x| substitute(unifier, x) }
117
+ }
118
+
119
+ def substitutions
120
+ @@substituters
121
+ end
122
+
123
+ def substitute(unifier, a)
124
+ subst = @@substituters[a.class]
125
+ if subst
126
+ subst.call(unifier, a)
127
+ else
128
+ a
129
+ end
130
+ end
131
+
132
+ # Run unification between |a| and |b|, and fails if they are not unifiable.
133
+ # |assert_unifiable| can have block, which yields the unifier for |a| and |b| if exists.
134
+ #
135
+ def assert_unifiable(a, b, message = "", options = {}, &block)
136
+ unifier = unify([[a, b]], message, {}, options) do |a, b, message|
137
+ assert_equal(a, b, message)
138
+ end
139
+
140
+ if block
141
+ yield(unifier)
142
+ end
143
+ end
144
+ end
145
+
@@ -0,0 +1,3 @@
1
+ module UnificationAssertion
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,127 @@
1
+ require "minitest/autorun"
2
+ require "unification_assertion"
3
+
4
+ class UnificationAssertionTest < MiniTest::Unit::TestCase
5
+ include UnificationAssertion
6
+
7
+ def call_unify(a, b)
8
+ begin
9
+ unifier = unify([[a,b]]) do |a, b|
10
+ unless a == b
11
+ raise "Failure"
12
+ end
13
+ end
14
+ return unifier
15
+ rescue
16
+ nil
17
+ end
18
+ end
19
+
20
+ def test_substitution
21
+ subst = { :a => 1 }
22
+
23
+ assert_equal 1, substitute(subst, :a)
24
+ assert_equal :b, substitute(subst, :b)
25
+
26
+ assert_equal "a", substitute(subst, "a")
27
+
28
+ assert_equal [], substitute(subst, [])
29
+ assert_equal [1], substitute(subst, [:a])
30
+ assert_equal [:b], substitute(subst, [:b])
31
+
32
+ assert_equal [[1]], substitute(subst, [[:a]])
33
+ assert_equal [[:b]], substitute(subst, [[:b]])
34
+
35
+ assert_equal({}, substitute(subst, {}))
36
+ assert_equal({ x: 1 }, substitute(subst, { x: :a }))
37
+ assert_equal({ x: :b}, substitute(subst, { x: :b }))
38
+
39
+ assert_equal({ x: { y: 1 } }, substitute(subst, { x: { y: :a } }))
40
+ assert_equal({ x: { y: :b } }, substitute(subst, { x: { y: :b } }))
41
+ end
42
+
43
+ def test_unify
44
+ assert_nil call_unify(:a, :b)
45
+
46
+ assert_equal({}, call_unify(:a, :a))
47
+
48
+ assert_equal({ :_a => 1 }, call_unify(:_a, 1))
49
+ assert_equal({ :_b => :a }, call_unify(:a, :_b))
50
+
51
+ assert_equal({ :_a => [1,:_b,3] }, call_unify([1,:_b,3], :_a))
52
+ assert_equal({}, call_unify([1,2,3], [1,2,3]))
53
+ assert_nil call_unify([1,2,3], [1,3])
54
+ assert_equal({ :_b => 2 }, call_unify([1,2,3], [1,:_b, 3]))
55
+ assert_nil call_unify([1,2,3], [:_a, 2, :_a])
56
+
57
+ assert_equal({}, call_unify({ a:1, b:2 }, { b:2, a:1 }))
58
+ assert_equal({ :_a => 2 }, call_unify({ a:1, b: :_a }, { b:2, a:1 }))
59
+ assert_equal({ :_b => :_a }, call_unify({ a: :_b, b: :_a }, { b: :_b, a: :_a }))
60
+ assert_equal({ :_a => 1 }, call_unify([1, { :x => :_a }], [:_a, { :x => 1 }]))
61
+ assert_nil call_unify([1, { :x => :_a }], [:_a, { :x => 2 }])
62
+ assert_equal({ :_a => 1, :_b => 1}, call_unify([1, :_b], [:_a, :_a]))
63
+
64
+ assert_equal({}, call_unify([:_, :_], [1,2]))
65
+ end
66
+
67
+ def test_assertion
68
+ assert_unifiable(:_a, [1,2,3])
69
+
70
+ assert_unifiable(:_a, 1) do |unifier|
71
+ assert_equal 1, unifier[:_a]
72
+ end
73
+
74
+ # :_ is wildcard
75
+ assert_unifiable([:_, :_, :_], [1,2,3])
76
+
77
+ assert_unifiable({ :created_at => :_a,
78
+ :updated_at => :_b },
79
+ { :created_at => Time.now,
80
+ :updated_at => Time.now + 1 }) do |unifier|
81
+ assert unifier[:_a] <= unifier[:_b]
82
+ end
83
+
84
+ # Meta variable pattern can be changed (ML type variable style)
85
+ assert_unifiable([:"'a", :"'b", :"'_"],
86
+ [1, 2, 3],
87
+ "Test message",
88
+ :meta_pattern => /^'/,
89
+ :wildcard => :"'_")
90
+ end
91
+
92
+ def test_assertion_failure
93
+ # 1 and 3 is incompatible
94
+ assert_raises(MiniTest::Assertion) do
95
+ assert_unifiable([:_a, :_a], [1, 3])
96
+ end
97
+
98
+ # Time.now and Time.now+1 is incompatible
99
+ assert_raises(MiniTest::Assertion) do
100
+ assert_unifiable({ :created_at => :_a,
101
+ :updated_at => :_a },
102
+ { :created_at => Time.now,
103
+ :updated_at => Time.now + 1 })
104
+ end
105
+
106
+ # There is no ``row'' variable
107
+ assert_raises(MiniTest::Assertion) do
108
+ assert_unifiable([:_a, :_b], [1,2,3])
109
+ end
110
+
111
+ # There is no ``row'' variable
112
+ assert_raises(MiniTest::Assertion) do
113
+ assert_unifiable({ :_a => 3 }, { :x => :_b })
114
+ end
115
+ end
116
+
117
+ def test_occur_check_skipped
118
+ # This should be nil because of (absent) occur check will fail.
119
+ # :_a \in FV( { :x => :_a } ) => cyclic!!
120
+ #
121
+ # However, I have not implement it.
122
+ # This unification implementation is only for testing.
123
+ # Who on the earth write this kind of test? (meaningless and it will result different from what the programmer expects)
124
+ #
125
+ assert_equal({ :_a => { :x => :_a }}, call_unify(:_a, { :x => :_a }))
126
+ end
127
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "unification_assertion/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "justinf-unification_assertion"
7
+ s.version = UnificationAssertion::VERSION
8
+ s.authors = ["Soutaro Matsumoto"]
9
+ s.email = ["matsumoto@soutaro.com"]
10
+ s.homepage = "https://github.com/soutaro/unification_assertion"
11
+ s.summary = "Assertion to test unifiability of two structures"
12
+ s.description = "UnificationAssertion defines +assert_unifiable+ assertion to test if given two values are unifiable.
13
+ I only made this gem because Soutaro hadn't pushed it to rubygems yet.
14
+ "
15
+
16
+ s.rubyforge_project = "unification_assertion"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ # specify any dependencies here; for example:
24
+ # s.add_development_dependency "rspec"
25
+ # s.add_runtime_dependency "rest-client"
26
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: justinf-unification_assertion
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Soutaro Matsumoto
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-02-29 00:00:00 Z
19
+ dependencies: []
20
+
21
+ description: "UnificationAssertion defines +assert_unifiable+ assertion to test if given two values are unifiable.\n\
22
+ I only made this gem because Soutaro hadn't pushed it to rubygems yet.\n "
23
+ email:
24
+ - matsumoto@soutaro.com
25
+ executables: []
26
+
27
+ extensions: []
28
+
29
+ extra_rdoc_files: []
30
+
31
+ files:
32
+ - .gitignore
33
+ - Gemfile
34
+ - README.md
35
+ - Rakefile
36
+ - lib/unification_assertion.rb
37
+ - lib/unification_assertion/version.rb
38
+ - test/unification_assertion_test.rb
39
+ - unification_assertion.gemspec
40
+ homepage: https://github.com/soutaro/unification_assertion
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options: []
45
+
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ hash: 3
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ hash: 3
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ requirements: []
67
+
68
+ rubyforge_project: unification_assertion
69
+ rubygems_version: 1.8.15
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Assertion to test unifiability of two structures
73
+ test_files: []
74
+