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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: abb844553faec0df87b8ea925f00f879c5a3edbf
4
+ data.tar.gz: 978765d0faff2c84ad1396f793a6cf301227104a
5
+ SHA512:
6
+ metadata.gz: c7d1f786d3f0e877266551e1adf80fe6ad2f9311d2d05c621635a9f150b37306afb7afd23721442099d213ad9dc13213edaba82ebf07fce7b50ff6b8e1fecf69
7
+ data.tar.gz: f28f1453b2ac77bda020c183c4f082c285eda0c0a1b1e555c3cd54c24aa5bd47de2b49f90f593c0c40cd0060f48be286072eff55b70557cf91348c9bba32939b
@@ -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
@@ -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
@@ -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,170 @@
1
+ require "unification_assertion/version"
2
+
3
+ require "minitest/unit"
4
+
5
+ module UnificationAssertion
6
+ include MiniTest::Assertions
7
+
8
+ module_function
9
+
10
+ @@comparators = {}
11
+
12
+ @@comparators[Array] = lambda {|a, b, eqs, unifier, path, block|
13
+ block.call(a.length, b.length, path + ".length")
14
+ a.zip(b).each.with_index do |pair, index|
15
+ pair << path + "[#{index}]"
16
+ eqs << pair
17
+ end
18
+ eqs
19
+ }
20
+
21
+ @@comparators[Hash] = lambda {|a, b, eqs, unifier, path, block|
22
+ block.call(a.keys.sort, b.keys.sort, path + ".keys.sort")
23
+ eqs.concat(a.keys.map {|key| [a[key], b[key], path+"[#{key.inspect}]"] })
24
+ }
25
+
26
+ # Comparators are hash from a class to its comparator.
27
+ # It is used to check unifiability of two object of the given hash.
28
+ #
29
+ # There are two comparators defined by default; for |Array| and |Hash|.
30
+ #
31
+ # == Example ==
32
+ # UnificationAssertion.comparators[Array] = lambda do |a, b, message, eqs, unifier, &block|
33
+ # block.call(a.length, bl.ength, message + " (Array length mismatch)")
34
+ # eqs.concat(a.zip(b))
35
+ # end
36
+ #
37
+ # This is our comparator for |Array|. It first tests if the length of the two arrays are equal.
38
+ # And then, it pushes the equations of each components of the arrays.
39
+ # The components of the arrays will be tested unifiability later.
40
+ #
41
+ # == Comparator ==
42
+ #
43
+ # Comparator is a lambda which takes 5 arguments and block and checks the unifiability
44
+ # of the first two arguments.
45
+ #
46
+ # * |a|, |b| Objects they are compared.
47
+ # * |message| Message given to |unify|.
48
+ # * |eqs| Equations array. This is for output.
49
+ # * |unifier| Current unifier. This is just for reference.
50
+ # * |&block| Block given to |unify|, which can be used to check the equality of two values.
51
+ #
52
+ def comparators
53
+ @@comparators
54
+ end
55
+
56
+ # Run unification algorithm for given equations.
57
+ # If all equations in |eqs| are unifiable, |unify| returns (the most-general) unifier.
58
+ #
59
+ # Which identifies an symbol as a meta variable if the name matches with |options[:meta_pattern]|.
60
+ # The default of the meta variable pattern is |/^_/|.
61
+ # For example, |:_a| and |:_xyz| are meta variable, but |:a| and |:hello_world| are not.
62
+ #
63
+ # It also accepts wildcard variable. Which matches with any value, but does not introduce new equational constraints.
64
+ # The default wildcard is |:_|.
65
+ #
66
+ # |unify| takes a block to test equation of two values.
67
+ # The simplest form should be using |assert_equal|, however it can be customized as you like.
68
+ #
69
+ # == Example ==
70
+ # unify([[:_a, 1], [{ :x => :_b, :y => 1 }, { :x => 3, :y => 1 }]], "Example!!") do |x, y, message|
71
+ # assert_equal(x,y,message)
72
+ # end
73
+ #
74
+ # The |unify| call will return an hash |{ :_a => 1, :_b => 3 }|.
75
+ # The block will be used to test equality between 1 and 1 (it will pass.)
76
+ #
77
+ def unify(eqs, unifier = {}, options = {}, &block)
78
+ options = { :meta_pattern => /^_/, :wildcard => :_ }.merge!(options)
79
+
80
+ pattern = options[:meta_pattern]
81
+ wildcard = options[:wildcard]
82
+
83
+ while eq = eqs.shift
84
+ a,b,path = eq
85
+ case
86
+ when (Symbol === a and a.to_s =~ pattern)
87
+ unless a == wildcard
88
+ eqs = substitute({ a => b }, eqs)
89
+ unifier = substitute({ a => b }, unifier).merge!(a => b)
90
+ end
91
+ when (Symbol === b and b.to_s =~ pattern)
92
+ unless b == wildcard
93
+ eqs = substitute({ b => a }, eqs)
94
+ unifier = substitute({ b => a }, unifier).merge!(b => a)
95
+ end
96
+ when (a.class == b.class and @@comparators[a.class])
97
+ @@comparators[a.class].call(a, b, eqs, unifier, path, block)
98
+ else
99
+ yield(a, b, path)
100
+ end
101
+ end
102
+
103
+ unifier.inject({}) {|acc, (key, value)|
104
+ if key == value
105
+ acc
106
+ else
107
+ acc.merge!(key => value)
108
+ end
109
+ }
110
+ end
111
+
112
+ @@substituters = {}
113
+ @@substituters[Hash] = lambda {|unifier, hash|
114
+ hash.inject({}) {|acc, (key, val)|
115
+ if unifier[val]
116
+ acc.merge!(key => unifier[val])
117
+ else
118
+ acc.merge!(key => substitute(unifier, val))
119
+ end
120
+ }
121
+ }
122
+ @@substituters[Symbol] = lambda {|unifier, symbol| unifier[symbol] or symbol }
123
+ @@substituters[Array] = lambda {|unifier, array|
124
+ array.map {|x| substitute(unifier, x) }
125
+ }
126
+
127
+ def substitutions
128
+ @@substituters
129
+ end
130
+
131
+ def substitute(unifier, a)
132
+ subst = @@substituters[a.class]
133
+ if subst
134
+ subst.call(unifier, a)
135
+ else
136
+ a
137
+ end
138
+ end
139
+
140
+ # Run unification between |a| and |b|, and fails if they are not unifiable.
141
+ # |assert_unifiable| can have block, which yields the unifier for |a| and |b| if exists.
142
+ #
143
+ def assert_unifiable(a, b, original_message = "", options = {}, &block)
144
+ msg = proc {|eq, path|
145
+ header = if original_message == nil or original_message.length == 0
146
+ original_message
147
+ else
148
+ "No unification"
149
+ end
150
+
151
+ footer = "\nCould not find a solution of equation at it#{path}.\n=> #{mu_pp(eq[0])} == #{mu_pp(eq[1])}"
152
+
153
+ message(header, footer) {
154
+ a_pp = mu_pp(a)
155
+ b_pp = mu_pp(b)
156
+
157
+ "=> #{a_pp}\n=> #{b_pp}"
158
+ }
159
+ }
160
+
161
+ unifier = unify([[a, b, ""]], {}, options) do |x, y, path|
162
+ assert(x==y, msg.call([x, y], path))
163
+ end
164
+
165
+ if block
166
+ yield(unifier)
167
+ end
168
+ end
169
+ end
170
+
@@ -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::Test
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,25 @@
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 = "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
+ s.licenses = ["MIT"]
14
+
15
+ s.rubyforge_project = "unification_assertion"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ # specify any dependencies here; for example:
23
+ # s.add_development_dependency "rspec"
24
+ s.add_runtime_dependency "minitest", '~> 5'
25
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unification_assertion
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Soutaro Matsumoto
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5'
27
+ description: UnificationAssertion defines +assert_unifiable+ assertion to test if
28
+ given two values are unifiable.
29
+ email:
30
+ - matsumoto@soutaro.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".gitignore"
36
+ - Gemfile
37
+ - README.md
38
+ - Rakefile
39
+ - lib/unification_assertion.rb
40
+ - lib/unification_assertion/version.rb
41
+ - test/unification_assertion_test.rb
42
+ - unification_assertion.gemspec
43
+ homepage: https://github.com/soutaro/unification_assertion
44
+ licenses:
45
+ - MIT
46
+ metadata: {}
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubyforge_project: unification_assertion
63
+ rubygems_version: 2.2.0
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: Assertion to test unifiability of two structures
67
+ test_files:
68
+ - test/unification_assertion_test.rb