justinf-unification_assertion 0.0.1

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.
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
+