meangirls 0.1.0

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,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