meangirls 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ module Meangirls
2
+ VERSION = "0.1.0"
3
+ end
data/meangirls.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'meangirls/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "meangirls"
8
+ spec.version = Meangirls::VERSION
9
+ spec.authors = ["Kyle Kingsbury"]
10
+ spec.email = ["aphyr@aphyr.com"]
11
+ spec.description = %q{Serializable data types for eventually consistent systems.}
12
+ spec.summary = spec.description
13
+ spec.homepage = "https://github.com/aphyr/meangirls"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_development_dependency "bundler", "~> 1.3"
20
+ spec.add_development_dependency "rake"
21
+ spec.add_development_dependency "bacon"
22
+ spec.add_development_dependency "mocha-on-bacon"
23
+ spec.add_development_dependency "yajl-ruby"
24
+ end
data/spec/counter.rb ADDED
@@ -0,0 +1,58 @@
1
+ shared :counter do
2
+ should 'create a counter' do
3
+ @class.new.should.be.kind_of? @class
4
+ end
5
+
6
+ should '==' do
7
+ a = @class.new
8
+ b = @class.new
9
+ a.should == b
10
+
11
+ a += 1
12
+ a.should.not == b
13
+ b += 2
14
+ a.should.not == b
15
+ a += 1
16
+ a.should == b
17
+
18
+ a.increment('foo', 2)
19
+ a.should.not == b
20
+ b.increment('foo', 2)
21
+ a.should == b
22
+ end
23
+
24
+ should '===' do
25
+ @s.should === @s
26
+ @s.should === 0
27
+ @s.should === 0.0
28
+ end
29
+
30
+ should '+' do
31
+ a = @s + 1
32
+ a.should.be.kind_of? Meangirls::Counter
33
+ a.should === 1
34
+
35
+ b = @class.new.increment('foo', 2)
36
+ b += 3
37
+ b.should === 5
38
+ end
39
+
40
+ should '-' do
41
+ end
42
+
43
+ should 'float?' do
44
+ @class.new.increment(1)
45
+ end
46
+
47
+ should 'merge' do
48
+ end
49
+
50
+ should 'increment' do
51
+ end
52
+
53
+ should 'to_i' do
54
+ end
55
+
56
+ should 'to_f' do
57
+ end
58
+ end
data/spec/crdt.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'yajl/json_gem'
2
+
3
+ shared :crdt do
4
+ should 'as_json' do
5
+ j = @s.as_json
6
+ j.should.be.kind_of? Hash
7
+ j['type'].should == @s.type
8
+ end
9
+
10
+ should 'round trip' do
11
+ @examples.each do |t|
12
+ Meangirls.parse(t.as_json).should == t
13
+ Meangirls.parse(JSON.parse(t.as_json.to_json)).should == t
14
+ end
15
+ end
16
+ end
data/spec/g_counter.rb ADDED
@@ -0,0 +1,14 @@
1
+ describe 'g-counter' do
2
+ before do
3
+ @class = Meangirls::GCounter
4
+ @s = @class.new
5
+ @examples = [
6
+ @class.new,
7
+ @class.new.increment('a', 1),
8
+ @class.new.increment('a', 1).increment('b', 0).increment('c', 1500.125)
9
+ ]
10
+ end
11
+
12
+ behaves_like :crdt
13
+ behaves_like :counter
14
+ end
data/spec/init.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'bacon'
2
+ require 'mocha-on-bacon'
3
+ require "#{File.expand_path(File.dirname(__FILE__))}/prob"
4
+ require "#{File.expand_path(File.dirname(__FILE__))}/crdt"
5
+ require "#{File.expand_path(File.dirname(__FILE__))}/set"
6
+ require "#{File.expand_path(File.dirname(__FILE__))}/counter"
7
+ require "#{File.expand_path(File.dirname(__FILE__))}/../lib/meangirls"
8
+
9
+ Bacon.summary_on_exit
data/spec/lww_set.rb ADDED
@@ -0,0 +1,65 @@
1
+ describe 'lww-set' do
2
+ before do
3
+ @class = Meangirls::LWWSet
4
+ @idempotent = false
5
+ @s = @class.new
6
+ @examples = [
7
+ @class.new,
8
+ (@class.new << 1),
9
+ (@class.new - [1,2]),
10
+ (@class.new - [1,2] + [2,3]),
11
+ (@class.new + [1,2] - [2,3])
12
+ ]
13
+ end
14
+
15
+ behaves_like :crdt
16
+ behaves_like :set
17
+ behaves_like :prob
18
+
19
+ should '==' do
20
+ a = @class.new
21
+ b = @class.new
22
+ a.add 1, 0
23
+ b.add 1, 0
24
+ a.should == b
25
+
26
+ a.delete 1, 1
27
+ a.should.not == b
28
+ b.delete 1, 1
29
+ a.should == b
30
+ end
31
+
32
+ should 'preserve latest writes' do
33
+ crdt = @class.new
34
+ test_merge crdt do |siblings, merged|
35
+ # Reconstruct transaction log.
36
+ log = []
37
+ siblings.each do |s|
38
+ s.e.each do |e, pair|
39
+ if t = pair.add
40
+ log << [t, :add, e]
41
+ end
42
+ if t = pair.remove
43
+ log << [t, :remove, e]
44
+ end
45
+ end
46
+ end
47
+ log.sort_by!(&:first)
48
+
49
+ # Todo: compact log, removing identical timestamps and choosing operation based on crdt.bias.
50
+ # Not needed currently as local timestamps are guaranteed unique within process.
51
+
52
+ # Replay log on top of original
53
+ model = log.inject(crdt.to_set) do |set, op|
54
+ case op[1]
55
+ when :add
56
+ set.add op.last
57
+ when :remove
58
+ set.delete op.last
59
+ end
60
+ end
61
+
62
+ model == merged.to_set
63
+ end
64
+ end
65
+ end
data/spec/or_set.rb ADDED
@@ -0,0 +1,41 @@
1
+ describe 'or-set' do
2
+ before do
3
+ @class = Meangirls::ORSet
4
+ @idempotent = false
5
+ @s = @class.new
6
+ @examples = [
7
+ @class.new,
8
+ (@class.new << 1),
9
+ (@class.new - [1,2]),
10
+ (@class.new - [1,2] + [2,3]),
11
+ (@class.new + [1,2] - [2,3])
12
+ ]
13
+ end
14
+
15
+ behaves_like :crdt
16
+ behaves_like :set
17
+ behaves_like :prob
18
+
19
+ should '==' do
20
+ a = @class.new
21
+ b = @class.new
22
+ a.add 1, 0
23
+ b.add 1, 0
24
+ a.should == b
25
+
26
+ a.delete 1
27
+ a.should.not == b
28
+ b.delete 1
29
+ a.should == b
30
+ end
31
+
32
+ should 'preserve independent adds' do
33
+ test_merge @class.new do |sibs, merged|
34
+ sibs.inject(Set.new) do |present, sib|
35
+ present | sib.to_set
36
+ end.all? do |e|
37
+ merged.include? e
38
+ end
39
+ end
40
+ end
41
+ end
data/spec/prob.rb ADDED
@@ -0,0 +1,93 @@
1
+ shared :prob do
2
+ COUNT ||= 10000
3
+
4
+ require 'pp'
5
+ def serialize(crdt)
6
+ crdt.to_json
7
+ end
8
+
9
+ def deserialize(text)
10
+ Meangirls.parse JSON.parse text
11
+ end
12
+
13
+ # Serialize then deserialize CRDT.
14
+ def roundtrip(crdt)
15
+ deserialize serialize crdt
16
+ end
17
+
18
+ # Takes a CRDT and returns n copies.
19
+ def split(crdt, n = rand(10))
20
+ n.times.map do
21
+ roundtrip crdt
22
+ end
23
+ end
24
+
25
+ # Merge n CRDTs together.
26
+ def merge(crdts)
27
+ crdts.inject(&:merge)
28
+ end
29
+
30
+ # Generate a random operation on a CRDT.
31
+ def operation
32
+ op = if rand > 0.5
33
+ :add
34
+ else
35
+ :delete
36
+ end
37
+ [op, rand(10)]
38
+ end
39
+
40
+ # A sequence of operations on a CRDT.
41
+ def operations
42
+ rand(5).times.map do
43
+ operation
44
+ end
45
+ end
46
+
47
+ # Apply the given operations to this CRDT.
48
+ def apply(ops, crdt)
49
+ ops.inject(crdt) do |c, op|
50
+ begin
51
+ c.send *op
52
+ rescue Meangirls::DeleteNotAllowed
53
+ rescue Meangirls::ReinsertNotAllowed
54
+ end
55
+ c
56
+ end
57
+ end
58
+
59
+ # Split, apply operations, merge.
60
+ def splurge(input, operations)
61
+ splits = split(input, operations.size).map.with_index do |c, i|
62
+ apply operations[i], c
63
+ end
64
+ merged = merge splits
65
+ [input, splits, merged]
66
+ end
67
+
68
+ # Attempts to generate failing sets of operations given a CRDT and a test
69
+ # block.
70
+ def test_merge(crdt, opts = {}, &block)
71
+ count = opts[:count] || COUNT
72
+ forks = opts[:forks] || rand(5) + 1
73
+
74
+ count.times do
75
+ operations = forks.times.map { operations() }
76
+ input, splits, merged = splurge crdt.clone, operations
77
+ if yield [splits, merged]
78
+ else
79
+ puts "Failed!"
80
+ puts "Input"
81
+ pp input
82
+ puts "Operations"
83
+ pp operations
84
+ puts "Siblings"
85
+ pp splits
86
+ puts "Merged"
87
+ pp merged
88
+ false.should.be.true
89
+ end
90
+ end
91
+ true.should.be.true
92
+ end
93
+ end
data/spec/run ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'open3'
4
+ require 'find'
5
+
6
+ tests = []
7
+
8
+ files, args = ARGV.partition do |arg|
9
+ File.exists? arg
10
+ end
11
+
12
+ dirs = files.empty? ? [File.dirname(__FILE__)] : files
13
+ dirs.each do |dir|
14
+ Find.find(dir) do |path|
15
+ next unless path =~ /\.rb$/
16
+ next if path =~ /\/init\.rb$/
17
+ tests << path
18
+ end
19
+ end
20
+
21
+ system *(["bacon", "-r", File.expand_path(File.dirname(__FILE__) + "/init.rb")] + args + tests.sort)
data/spec/set.rb ADDED
@@ -0,0 +1,186 @@
1
+ shared :set do
2
+ should 'create an empty set' do
3
+ n = @s
4
+ n.should.be.empty
5
+ end
6
+
7
+ if @idempotent
8
+ should '==' do
9
+ @s.should == @s
10
+ a = @class.new
11
+ b = @class.new
12
+ a.should == b
13
+ a << 1
14
+ a.should.not == b
15
+ b << 1
16
+ a.should == b
17
+ a.delete 1
18
+ a.should.not == b
19
+ b.delete 1
20
+ a.should == b
21
+ end
22
+ else
23
+ should '==' do
24
+ @s.should == @s
25
+ a = @class.new
26
+ b = @class.new
27
+ a.should == b
28
+ a << 1
29
+ a.should == a
30
+ a.delete 1
31
+ a.should == a
32
+ end
33
+ end
34
+
35
+ should '===' do
36
+ @s.should === @s
37
+ @s.should === []
38
+ @s.should === Set.new
39
+
40
+ a = @class.new
41
+ a << 1
42
+ a << 2
43
+ a.should === [2,1]
44
+ a.should === [1,2].to_set
45
+ a.should === (1..2)
46
+
47
+ a.delete 1
48
+ a.should === [2]
49
+ a.should === [2].to_set
50
+ a.should === (2..2)
51
+ end
52
+
53
+ should '&' do
54
+ (@s & []).should === []
55
+ (@s & [1,2,3]).should === []
56
+
57
+ a = @class.new
58
+ a << 1
59
+ a << 2
60
+ (a & []).should === []
61
+ (a & [1,3]).should === [1]
62
+ end
63
+
64
+ should '|' do
65
+ (@s | []).should === []
66
+ (@s | [1,2,3]).should === [1,2,3]
67
+
68
+ a = @class.new
69
+ a << 1
70
+ a << 2
71
+ (a | []).should == a
72
+ (a | []).should === [1,2]
73
+ (a | [2,3]).should == (a << 3) if @idempotent
74
+ (a | [2,3]).should === [1,2,3]
75
+ end
76
+
77
+ should '-' do
78
+ (@s - []).should == @s
79
+ (@s - [1,2,3]).should === [] rescue Meangirls::DeleteNotAllowed
80
+
81
+ a = @class.new
82
+ a << 1
83
+ a << 2
84
+ (a - []).should == a
85
+ (a - [2]).should === [1]
86
+ (a - [2,3]).should === [1] rescue Meangirls::DeleteNotAllowed
87
+ end
88
+
89
+ should '<<' do
90
+ (@s << 1).should === [1]
91
+ (@s << 1 << 2).should === [1,2]
92
+ end
93
+
94
+ should 'delete' do
95
+ @s.delete(1).should == nil rescue Meangirls::DeleteNotAllowed
96
+ @s.should === []
97
+
98
+ a = @class.new
99
+ a << 1
100
+ a << 2
101
+ a.delete(1).should == 1
102
+ a.should === [2]
103
+ end
104
+
105
+ should 'empty' do
106
+ @s.should.be.empty
107
+ @s << 1
108
+ @s.should.not.be.empty
109
+ @s.delete 1
110
+ @s.should.be.empty
111
+ end
112
+
113
+ should 'merge empty sets' do
114
+ # Empty set
115
+ @s.merge(@s).should == @s
116
+ end
117
+
118
+ should 'merge with self' do
119
+ a = @class.new
120
+ a << 1
121
+ a << 2
122
+ a.merge(a).should == a
123
+ end
124
+
125
+ should 'merge disjoint sets' do
126
+ a = @class.new
127
+ a << 2
128
+
129
+ b = @class.new
130
+ b << 1
131
+ b << 3
132
+ a.merge(b).should === [1,2,3]
133
+ b.merge(a).should == a.merge(b)
134
+ end
135
+
136
+ should 'merge overlapping sets' do
137
+ a = @class.new
138
+ a << 1
139
+ a << 2
140
+ b = @class.new
141
+ b << 2
142
+ b << 3
143
+ a.merge(b).should === [1,2,3]
144
+ b.merge(a).should == a.merge(b)
145
+ end
146
+
147
+ ([@s.bias] | (@class.biases rescue [])).each do |bias|
148
+ should "merge biased towards #{bias}" do
149
+ Meangirls.expects(:timestamp).times(0..3).returns(0)
150
+ case bias
151
+ when 'a'
152
+ # Should preserve adds
153
+ a = @class.new
154
+ a.bias = 'a' if a.respond_to? :bias=
155
+ a << 1
156
+ a.delete 1
157
+ b = @class.new
158
+ b.bias = 'a' if b.respond_to? :bias=
159
+ b << 1
160
+ a.merge(b).should === [1]
161
+ b.merge(a).should == a.merge(b)
162
+ when 'r'
163
+ # Should preserve deletes
164
+ a = @class.new
165
+ a.bias = 'r' if a.respond_to? :bias=
166
+ a << 1
167
+ a.delete 1
168
+ b = @class.new
169
+ b.bias = 'r' if b.respond_to? :bias=
170
+ b << 1
171
+ a.merge(b).should === []
172
+ b.merge(a).should == a.merge(b)
173
+ end
174
+ end
175
+ end
176
+
177
+ should 'size' do
178
+ @s.size.should == 0
179
+ @s << 1
180
+ @s.size.should == 1
181
+ @s << 1
182
+ @s.size.should == 1
183
+ @s.delete 1
184
+ @s.size.should == 0
185
+ end
186
+ end