replicate 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.
- data/COPYING +18 -0
- data/README.md +99 -0
- data/Rakefile +6 -0
- data/bin/replicate +71 -0
- data/lib/replicate.rb +10 -0
- data/lib/replicate/active_record.rb +217 -0
- data/lib/replicate/dumper.rb +109 -0
- data/lib/replicate/emitter.rb +54 -0
- data/lib/replicate/loader.rb +133 -0
- data/lib/replicate/object.rb +57 -0
- data/lib/replicate/status.rb +54 -0
- data/test/active_record_test.rb +253 -0
- data/test/dumper_test.rb +71 -0
- data/test/loader_test.rb +93 -0
- data/test/replicate_test.rb +10 -0
- metadata +95 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
module Replicate
|
2
|
+
# Base class for Dumper / Loader classes. Manages a list of callback listeners
|
3
|
+
# and dispatches to each when #emit is called.
|
4
|
+
class Emitter
|
5
|
+
# Yields self to the block and calls #complete when block is finished.
|
6
|
+
def initialize
|
7
|
+
@listeners = []
|
8
|
+
if block_given?
|
9
|
+
yield self
|
10
|
+
complete
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Register a listener to be called for each loaded object with the
|
15
|
+
# type, id, attributes, object structure. Listeners are executed in the
|
16
|
+
# reverse order of which they were registered. Listeners registered later
|
17
|
+
# modify the view of listeners registered earlier.
|
18
|
+
#
|
19
|
+
# p - An optional Proc object. Must respond to call.
|
20
|
+
# block - An optional block.
|
21
|
+
#
|
22
|
+
# Returns nothing.
|
23
|
+
def listen(p=nil, &block)
|
24
|
+
@listeners.unshift p if p
|
25
|
+
@listeners.unshift block if block
|
26
|
+
end
|
27
|
+
|
28
|
+
# Sugar for creating a listener with an object instance. Instances of the
|
29
|
+
# class must respond to call(type, id, attrs, object).
|
30
|
+
#
|
31
|
+
# klass - The class to create. Must respond to new.
|
32
|
+
# args - Arguments to pass to new in addition to self.
|
33
|
+
#
|
34
|
+
# Returns the object created.
|
35
|
+
def use(klass, *args, &block)
|
36
|
+
instance = klass.new(self, *args, &block)
|
37
|
+
listen instance
|
38
|
+
instance
|
39
|
+
end
|
40
|
+
|
41
|
+
# Emit an object event to each listener.
|
42
|
+
#
|
43
|
+
# Returns the object.
|
44
|
+
def emit(type, id, attributes, object)
|
45
|
+
@listeners.each { |p| p.call(type, id, attributes, object) }
|
46
|
+
object
|
47
|
+
end
|
48
|
+
|
49
|
+
# Notify all listeners that processing is complete.
|
50
|
+
def complete
|
51
|
+
@listeners.each { |p| p.complete if p.respond_to?(:complete) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module Replicate
|
2
|
+
# Load replicants in a streaming fashion.
|
3
|
+
#
|
4
|
+
# The Loader reads [type, id, attributes] replicant tuples and creates
|
5
|
+
# objects in the current environment.
|
6
|
+
#
|
7
|
+
# Objects are expected to arrive in order such that a record referenced via
|
8
|
+
# foreign key always precedes the referencing record. The Loader maintains a
|
9
|
+
# mapping of primary keys from the dump system to the current environment.
|
10
|
+
# This mapping is used to properly establish new foreign key values on all
|
11
|
+
# records inserted.
|
12
|
+
class Loader < Emitter
|
13
|
+
|
14
|
+
# Stats hash.
|
15
|
+
attr_reader :stats
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@keymap = Hash.new { |hash,k| hash[k] = {} }
|
19
|
+
@stats = Hash.new { |hash,k| hash[k] = 0 }
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
# Register a filter to write status information to the given stream. By
|
24
|
+
# default, a single line is used to report object counts while the dump is
|
25
|
+
# in progress; dump counts for each class are written when complete. The
|
26
|
+
# verbose and quiet options can be used to increase or decrease
|
27
|
+
# verbository.
|
28
|
+
#
|
29
|
+
# out - An IO object to write to, like stderr.
|
30
|
+
# verbose - Whether verbose output should be enabled.
|
31
|
+
# quiet - Whether quiet output should be enabled.
|
32
|
+
#
|
33
|
+
# Returns the Replicate::Status object.
|
34
|
+
def log_to(out=$stderr, verbose=false, quiet=false)
|
35
|
+
use Replicate::Status, 'load', out, verbose, quiet
|
36
|
+
end
|
37
|
+
|
38
|
+
# Feed a single replicant tuple into the loader.
|
39
|
+
#
|
40
|
+
# type - The class to create. Must respond to load_replicant.
|
41
|
+
# id - The remote system's id for this object.
|
42
|
+
# attrs - Hash of primitively typed objects.
|
43
|
+
#
|
44
|
+
# Returns the need object resulting from the load operation.
|
45
|
+
def feed(type, id, attrs)
|
46
|
+
type = type.to_s
|
47
|
+
object = load(type, id, attrs)
|
48
|
+
@stats[type] += 1
|
49
|
+
emit type, id, attrs, object
|
50
|
+
end
|
51
|
+
|
52
|
+
# Read multiple [type, id, attrs] replicant tuples from io and call the
|
53
|
+
# feed method to load and filter the object.
|
54
|
+
def read(io)
|
55
|
+
begin
|
56
|
+
while object = Marshal.load(io)
|
57
|
+
type, id, attrs = object
|
58
|
+
feed type, id, attrs
|
59
|
+
end
|
60
|
+
rescue EOFError
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Load an individual replicant into the underlying datastore.
|
65
|
+
#
|
66
|
+
# type - Model class name as a String.
|
67
|
+
# id - Primary key id of the record on the dump system. This must be
|
68
|
+
# translated to the local system and stored in the keymap.
|
69
|
+
# attrs - Hash of attributes to set on the new record.
|
70
|
+
#
|
71
|
+
# Returns the new object instance.
|
72
|
+
def load(type, id, attributes)
|
73
|
+
model_class = constantize(type)
|
74
|
+
translate_ids attributes
|
75
|
+
begin
|
76
|
+
new_id, instance = model_class.load_replicant(type, id, attributes)
|
77
|
+
rescue => boom
|
78
|
+
warn "error: loading #{type} #{id} #{boom.class} #{boom}"
|
79
|
+
raise
|
80
|
+
end
|
81
|
+
register_id instance, type, id, new_id
|
82
|
+
instance
|
83
|
+
end
|
84
|
+
|
85
|
+
# Translate remote system id references in the attributes hash to their
|
86
|
+
# local system id values. The attributes hash may include special id
|
87
|
+
# values like this:
|
88
|
+
# { 'title' => 'hello there',
|
89
|
+
# 'repository_id' => [:id, 'Repository', 1234],
|
90
|
+
# 'label_ids' => [:id, 'Label', [333, 444, 555, 666, ...]]
|
91
|
+
# ... }
|
92
|
+
# These values are translated to local system ids. All object
|
93
|
+
# references must be loaded prior to the referencing object.
|
94
|
+
def translate_ids(attributes)
|
95
|
+
attributes.each do |key, value|
|
96
|
+
next unless value.is_a?(Array) && value[0] == :id
|
97
|
+
type, value = value[1].to_s, value[2]
|
98
|
+
local_ids =
|
99
|
+
Array(value).map do |remote_id|
|
100
|
+
if local_id = @keymap[type][remote_id]
|
101
|
+
local_id
|
102
|
+
else
|
103
|
+
warn "error: #{type} #{remote_id} missing from keymap"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
if value.is_a?(Array)
|
107
|
+
attributes[key] = local_ids
|
108
|
+
else
|
109
|
+
attributes[key] = local_ids[0]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Register an id in the keymap. Every object loaded must be stored here so
|
115
|
+
# that key references can be resolved.
|
116
|
+
def register_id(object, type, remote_id, local_id)
|
117
|
+
@keymap[type.to_s][remote_id] = local_id
|
118
|
+
c = object.class
|
119
|
+
while !['Object', 'ActiveRecord::Base'].include?(c.name)
|
120
|
+
@keymap[c.name][remote_id] = local_id
|
121
|
+
c = c.superclass
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
# Turn a string into an object by traversing constants.
|
127
|
+
def constantize(string)
|
128
|
+
namespace = Object
|
129
|
+
string.split('::').each { |name| namespace = namespace.const_get(name) }
|
130
|
+
namespace
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Replicate
|
2
|
+
# Simple OpenStruct style object that supports the dump and load protocols.
|
3
|
+
# Useful in tests and also when you want to dump an object that doesn't
|
4
|
+
# implement the dump and load methods.
|
5
|
+
#
|
6
|
+
# >> object = Replicate::Object.new :name => 'Joe', :age => 24
|
7
|
+
# >> object.age
|
8
|
+
# >> 24
|
9
|
+
# >> object.attributes
|
10
|
+
# { 'name' => 'Joe', 'age' => 24 }
|
11
|
+
#
|
12
|
+
class Object
|
13
|
+
attr_accessor :id
|
14
|
+
attr_accessor :attributes
|
15
|
+
|
16
|
+
def initialize(id=nil, attributes={})
|
17
|
+
attributes, id = id, nil if id.is_a?(Hash)
|
18
|
+
@id = id || self.class.generate_id
|
19
|
+
self.attributes = attributes
|
20
|
+
end
|
21
|
+
|
22
|
+
def attributes=(hash)
|
23
|
+
@attributes = {}
|
24
|
+
hash.each { |key, value| write_attribute key, value }
|
25
|
+
end
|
26
|
+
|
27
|
+
def [](key)
|
28
|
+
@attributes[key.to_s]
|
29
|
+
end
|
30
|
+
|
31
|
+
def []=(key, value)
|
32
|
+
@attributes[key.to_s] = value
|
33
|
+
end
|
34
|
+
|
35
|
+
def write_attribute(key, value)
|
36
|
+
meta = (class<<self;self;end)
|
37
|
+
meta.send(:define_method, key) { value }
|
38
|
+
meta.send(:define_method, "#{key}=") { |val| write_attribute(key, val) }
|
39
|
+
@attributes[key.to_s] = value
|
40
|
+
value
|
41
|
+
end
|
42
|
+
|
43
|
+
def dump_replicant(dumper)
|
44
|
+
dumper.write self.class, @id, @attributes, self
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.load_replicant(type, id, attrs)
|
48
|
+
object = new(generate_id, attrs)
|
49
|
+
[object.id, object]
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.generate_id
|
53
|
+
@last_id ||= 0
|
54
|
+
@last_id += 1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Replicate
|
2
|
+
# Replicate filter that writes info to the console. Used by the Dumper and
|
3
|
+
# Loader to get basic console output.
|
4
|
+
class Status
|
5
|
+
def initialize(dumper, prefix, out, verbose=false, quiet=false)
|
6
|
+
@dumper = dumper
|
7
|
+
@prefix = prefix
|
8
|
+
@out = out
|
9
|
+
@verbose = verbose
|
10
|
+
@quiet = quiet
|
11
|
+
@count = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(type, id, attrs, object)
|
15
|
+
@count += 1
|
16
|
+
if @verbose
|
17
|
+
verbose_log type, id, attrs, object
|
18
|
+
elsif !@quiet
|
19
|
+
normal_log type, id, attrs, object
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def verbose_log(type, id, attrs, object)
|
24
|
+
desc_attr = %w[name login email number title].find { |k| attrs.key?(k) }
|
25
|
+
desc = desc_attr ? attrs[desc_attr] : id
|
26
|
+
@out.puts "#{@prefix}: %-30s %s" % [type.sub('Replicate::', ''), desc]
|
27
|
+
end
|
28
|
+
|
29
|
+
def normal_log(type, id, attrs, object)
|
30
|
+
@out.write "==> #{@prefix}ing: #{@count} objects \r"
|
31
|
+
end
|
32
|
+
|
33
|
+
def complete
|
34
|
+
dump_stats if !@quiet
|
35
|
+
end
|
36
|
+
|
37
|
+
def dump_stats(stats=@dumper.stats.dup)
|
38
|
+
@out.puts "==> #{@prefix}ed #{@count} total objects: "
|
39
|
+
width = 0
|
40
|
+
stats.keys.each do |key|
|
41
|
+
class_name = format_class_name(key)
|
42
|
+
stats[class_name] = stats.delete(key)
|
43
|
+
width = class_name.size if class_name.size > width
|
44
|
+
end
|
45
|
+
stats.to_a.sort_by { |k,n| k }.each do |class_name, count|
|
46
|
+
@out.write "%-#{width + 1}s %5d\n" % [class_name, count]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def format_class_name(class_name)
|
51
|
+
class_name.sub(/Replicate::/, '')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,253 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'stringio'
|
3
|
+
require 'active_record'
|
4
|
+
require 'replicate'
|
5
|
+
|
6
|
+
dbfile = File.expand_path('../db', __FILE__)
|
7
|
+
File.unlink dbfile if File.exist?(dbfile)
|
8
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => dbfile)
|
9
|
+
|
10
|
+
ActiveRecord::Migration.verbose = false
|
11
|
+
ActiveRecord::Schema.define do
|
12
|
+
create_table "users", :force => true do |t|
|
13
|
+
t.string "login"
|
14
|
+
t.datetime "created_at"
|
15
|
+
t.datetime "updated_at"
|
16
|
+
end
|
17
|
+
|
18
|
+
create_table "profiles", :force => true do |t|
|
19
|
+
t.integer "user_id"
|
20
|
+
t.string "name"
|
21
|
+
t.string "homepage"
|
22
|
+
end
|
23
|
+
|
24
|
+
create_table "emails", :force => true do |t|
|
25
|
+
t.integer "user_id"
|
26
|
+
t.string "email"
|
27
|
+
t.datetime "created_at"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class User < ActiveRecord::Base
|
32
|
+
has_one :profile, :dependent => :destroy
|
33
|
+
has_many :emails, :dependent => :destroy, :order => 'id'
|
34
|
+
|
35
|
+
replicate_natural_key :login
|
36
|
+
end
|
37
|
+
|
38
|
+
class Profile < ActiveRecord::Base
|
39
|
+
belongs_to :user
|
40
|
+
|
41
|
+
replicate_natural_key :user_id
|
42
|
+
end
|
43
|
+
|
44
|
+
class Email < ActiveRecord::Base
|
45
|
+
belongs_to :user
|
46
|
+
|
47
|
+
replicate_natural_key :user_id, :email
|
48
|
+
end
|
49
|
+
|
50
|
+
class ActiveRecordTest < Test::Unit::TestCase
|
51
|
+
def setup
|
52
|
+
self.class.fixtures
|
53
|
+
ActiveRecord::Base.connection.increment_open_transactions
|
54
|
+
ActiveRecord::Base.connection.begin_db_transaction
|
55
|
+
|
56
|
+
@rtomayko = User.find_by_login('rtomayko')
|
57
|
+
@kneath = User.find_by_login('kneath')
|
58
|
+
@tmm1 = User.find_by_login('tmm1')
|
59
|
+
|
60
|
+
User.replicate_associations = []
|
61
|
+
|
62
|
+
@dumper = Replicate::Dumper.new
|
63
|
+
@loader = Replicate::Loader.new
|
64
|
+
end
|
65
|
+
|
66
|
+
def teardown
|
67
|
+
ActiveRecord::Base.connection.rollback_db_transaction
|
68
|
+
ActiveRecord::Base.connection.decrement_open_transactions
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.fixtures
|
72
|
+
return if @fixtures
|
73
|
+
@fixtures = true
|
74
|
+
user = User.create! :login => 'rtomayko'
|
75
|
+
user.create_profile :name => 'Ryan Tomayko', :homepage => 'http://tomayko.com'
|
76
|
+
user.emails.create! :email => 'ryan@github.com'
|
77
|
+
user.emails.create! :email => 'rtomayko@gmail.com'
|
78
|
+
|
79
|
+
user = User.create! :login => 'kneath'
|
80
|
+
user.create_profile :name => 'Kyle Neath', :homepage => 'http://warpspire.com'
|
81
|
+
user.emails.create! :email => 'kyle@github.com'
|
82
|
+
|
83
|
+
user = User.create! :login => 'tmm1'
|
84
|
+
user.create_profile :name => 'tmm1', :homepage => 'https://github.com/tmm1'
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_extension_modules_loaded
|
88
|
+
assert User.respond_to?(:load_replicant)
|
89
|
+
assert User.new.respond_to?(:dump_replicant)
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_auto_dumping_belongs_to_associations
|
93
|
+
objects = []
|
94
|
+
@dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
|
95
|
+
|
96
|
+
rtomayko = User.find_by_login('rtomayko')
|
97
|
+
@dumper.dump rtomayko.profile
|
98
|
+
|
99
|
+
assert_equal 2, objects.size
|
100
|
+
|
101
|
+
type, id, attrs, obj = objects.shift
|
102
|
+
assert_equal 'User', type
|
103
|
+
assert_equal rtomayko.id, id
|
104
|
+
assert_equal 'rtomayko', attrs['login']
|
105
|
+
assert_equal rtomayko.created_at, attrs['created_at']
|
106
|
+
assert_equal rtomayko, obj
|
107
|
+
|
108
|
+
type, id, attrs, obj = objects.shift
|
109
|
+
assert_equal 'Profile', type
|
110
|
+
assert_equal rtomayko.profile.id, id
|
111
|
+
assert_equal 'Ryan Tomayko', attrs['name']
|
112
|
+
assert_equal rtomayko.profile, obj
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_auto_dumping_has_one_associations
|
116
|
+
objects = []
|
117
|
+
@dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
|
118
|
+
|
119
|
+
rtomayko = User.find_by_login('rtomayko')
|
120
|
+
@dumper.dump rtomayko
|
121
|
+
|
122
|
+
assert_equal 2, objects.size
|
123
|
+
|
124
|
+
type, id, attrs, obj = objects.shift
|
125
|
+
assert_equal 'User', type
|
126
|
+
assert_equal rtomayko.id, id
|
127
|
+
assert_equal 'rtomayko', attrs['login']
|
128
|
+
assert_equal rtomayko.created_at, attrs['created_at']
|
129
|
+
assert_equal rtomayko, obj
|
130
|
+
|
131
|
+
type, id, attrs, obj = objects.shift
|
132
|
+
assert_equal 'Profile', type
|
133
|
+
assert_equal rtomayko.profile.id, id
|
134
|
+
assert_equal 'Ryan Tomayko', attrs['name']
|
135
|
+
assert_equal [:id, 'User', rtomayko.id], attrs['user_id']
|
136
|
+
assert_equal rtomayko.profile, obj
|
137
|
+
end
|
138
|
+
|
139
|
+
def test_dumping_has_many_associations
|
140
|
+
objects = []
|
141
|
+
@dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
|
142
|
+
|
143
|
+
User.replicate_associations :emails
|
144
|
+
rtomayko = User.find_by_login('rtomayko')
|
145
|
+
@dumper.dump rtomayko
|
146
|
+
|
147
|
+
assert_equal 4, objects.size
|
148
|
+
|
149
|
+
type, id, attrs, obj = objects.shift
|
150
|
+
assert_equal 'User', type
|
151
|
+
assert_equal rtomayko.id, id
|
152
|
+
assert_equal 'rtomayko', attrs['login']
|
153
|
+
assert_equal rtomayko.created_at, attrs['created_at']
|
154
|
+
assert_equal rtomayko, obj
|
155
|
+
|
156
|
+
type, id, attrs, obj = objects.shift
|
157
|
+
assert_equal 'Profile', type
|
158
|
+
assert_equal rtomayko.profile.id, id
|
159
|
+
assert_equal 'Ryan Tomayko', attrs['name']
|
160
|
+
assert_equal rtomayko.profile, obj
|
161
|
+
|
162
|
+
type, id, attrs, obj = objects.shift
|
163
|
+
assert_equal 'Email', type
|
164
|
+
assert_equal 'ryan@github.com', attrs['email']
|
165
|
+
assert_equal [:id, 'User', rtomayko.id], attrs['user_id']
|
166
|
+
assert_equal rtomayko.emails.first, obj
|
167
|
+
|
168
|
+
type, id, attrs, obj = objects.shift
|
169
|
+
assert_equal 'Email', type
|
170
|
+
assert_equal 'rtomayko@gmail.com', attrs['email']
|
171
|
+
assert_equal [:id, 'User', rtomayko.id], attrs['user_id']
|
172
|
+
assert_equal rtomayko.emails.last, obj
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_loading_everything
|
176
|
+
objects = []
|
177
|
+
@dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
|
178
|
+
|
179
|
+
# dump all users and associated objects and destroy
|
180
|
+
User.replicate_associations :emails
|
181
|
+
dumped_users = {}
|
182
|
+
%w[rtomayko kneath tmm1].each do |login|
|
183
|
+
user = User.find_by_login(login)
|
184
|
+
@dumper.dump user
|
185
|
+
user.destroy
|
186
|
+
dumped_users[login] = user
|
187
|
+
end
|
188
|
+
assert_equal 9, objects.size
|
189
|
+
|
190
|
+
# insert another record to ensure id changes for loaded records
|
191
|
+
sr = User.create!(:login => 'sr')
|
192
|
+
sr.create_profile :name => 'Simon Rozet'
|
193
|
+
sr.emails.create :email => 'sr@github.com'
|
194
|
+
|
195
|
+
# load everything back up
|
196
|
+
objects.each { |type, id, attrs, obj| @loader.feed type, id, attrs }
|
197
|
+
|
198
|
+
# verify attributes are set perfectly again
|
199
|
+
user = User.find_by_login('rtomayko')
|
200
|
+
assert_equal 'rtomayko', user.login
|
201
|
+
assert_equal dumped_users['rtomayko'].created_at, user.created_at
|
202
|
+
assert_equal dumped_users['rtomayko'].updated_at, user.updated_at
|
203
|
+
assert_equal 'Ryan Tomayko', user.profile.name
|
204
|
+
assert_equal 2, user.emails.size
|
205
|
+
|
206
|
+
# make sure everything was recreated
|
207
|
+
%w[rtomayko kneath tmm1].each do |login|
|
208
|
+
user = User.find_by_login(login)
|
209
|
+
assert_not_nil user
|
210
|
+
assert_not_nil user.profile
|
211
|
+
assert !user.emails.empty?, "#{login} has no emails" if login != 'tmm1'
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def test_loading_with_existing_records
|
216
|
+
objects = []
|
217
|
+
@dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
|
218
|
+
|
219
|
+
# dump all users and associated objects and destroy
|
220
|
+
User.replicate_associations :emails
|
221
|
+
dumped_users = {}
|
222
|
+
%w[rtomayko kneath tmm1].each do |login|
|
223
|
+
user = User.find_by_login(login)
|
224
|
+
user.profile.update_attribute :name, 'CHANGED'
|
225
|
+
@dumper.dump user
|
226
|
+
dumped_users[login] = user
|
227
|
+
end
|
228
|
+
assert_equal 9, objects.size
|
229
|
+
|
230
|
+
# load everything back up
|
231
|
+
objects.each { |type, id, attrs, obj| @loader.feed type, id, attrs }
|
232
|
+
|
233
|
+
# ensure additional objects were not created
|
234
|
+
assert_equal 3, User.count
|
235
|
+
|
236
|
+
# verify attributes are set perfectly again
|
237
|
+
user = User.find_by_login('rtomayko')
|
238
|
+
assert_equal 'rtomayko', user.login
|
239
|
+
assert_equal dumped_users['rtomayko'].created_at, user.created_at
|
240
|
+
assert_equal dumped_users['rtomayko'].updated_at, user.updated_at
|
241
|
+
assert_equal 'CHANGED', user.profile.name
|
242
|
+
assert_equal 2, user.emails.size
|
243
|
+
|
244
|
+
# make sure everything was recreated
|
245
|
+
%w[rtomayko kneath tmm1].each do |login|
|
246
|
+
user = User.find_by_login(login)
|
247
|
+
assert_not_nil user
|
248
|
+
assert_not_nil user.profile
|
249
|
+
assert_equal 'CHANGED', user.profile.name
|
250
|
+
assert !user.emails.empty?, "#{login} has no emails" if login != 'tmm1'
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|