unification_assertion 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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