backup 3.0.20 → 3.0.21

Sign up to get free protection for your applications and to get access to all the features.
Files changed (178) hide show
  1. data/Gemfile +1 -5
  2. data/Gemfile.lock +46 -50
  3. data/README.md +54 -27
  4. data/lib/backup.rb +16 -39
  5. data/lib/backup/archive.rb +42 -18
  6. data/lib/backup/cleaner.rb +110 -25
  7. data/lib/backup/cli/helpers.rb +17 -32
  8. data/lib/backup/cli/utility.rb +46 -107
  9. data/lib/backup/compressor/base.rb +14 -2
  10. data/lib/backup/compressor/bzip2.rb +10 -24
  11. data/lib/backup/compressor/gzip.rb +10 -24
  12. data/lib/backup/compressor/lzma.rb +10 -23
  13. data/lib/backup/compressor/pbzip2.rb +12 -32
  14. data/lib/backup/config.rb +171 -0
  15. data/lib/backup/configuration/compressor/base.rb +1 -2
  16. data/lib/backup/configuration/compressor/pbzip2.rb +4 -4
  17. data/lib/backup/configuration/database/base.rb +2 -1
  18. data/lib/backup/configuration/database/mongodb.rb +8 -0
  19. data/lib/backup/configuration/database/mysql.rb +4 -0
  20. data/lib/backup/configuration/database/postgresql.rb +4 -0
  21. data/lib/backup/configuration/database/redis.rb +4 -0
  22. data/lib/backup/configuration/database/riak.rb +5 -1
  23. data/lib/backup/configuration/encryptor/base.rb +1 -2
  24. data/lib/backup/configuration/encryptor/open_ssl.rb +1 -1
  25. data/lib/backup/configuration/helpers.rb +7 -2
  26. data/lib/backup/configuration/notifier/base.rb +4 -28
  27. data/lib/backup/configuration/storage/base.rb +1 -1
  28. data/lib/backup/configuration/storage/dropbox.rb +14 -4
  29. data/lib/backup/configuration/syncer/base.rb +10 -0
  30. data/lib/backup/configuration/syncer/rsync/base.rb +28 -0
  31. data/lib/backup/configuration/syncer/rsync/local.rb +11 -0
  32. data/lib/backup/configuration/syncer/rsync/pull.rb +11 -0
  33. data/lib/backup/configuration/syncer/rsync/push.rb +31 -0
  34. data/lib/backup/configuration/syncer/s3.rb +0 -4
  35. data/lib/backup/database/base.rb +25 -7
  36. data/lib/backup/database/mongodb.rb +112 -75
  37. data/lib/backup/database/mysql.rb +54 -29
  38. data/lib/backup/database/postgresql.rb +60 -42
  39. data/lib/backup/database/redis.rb +61 -39
  40. data/lib/backup/database/riak.rb +35 -11
  41. data/lib/backup/dependency.rb +4 -5
  42. data/lib/backup/encryptor/base.rb +13 -1
  43. data/lib/backup/encryptor/gpg.rb +39 -39
  44. data/lib/backup/encryptor/open_ssl.rb +28 -38
  45. data/lib/backup/logger.rb +20 -11
  46. data/lib/backup/model.rb +206 -163
  47. data/lib/backup/notifier/base.rb +27 -25
  48. data/lib/backup/notifier/campfire.rb +7 -13
  49. data/lib/backup/notifier/hipchat.rb +28 -28
  50. data/lib/backup/notifier/mail.rb +24 -26
  51. data/lib/backup/notifier/presently.rb +10 -18
  52. data/lib/backup/notifier/prowl.rb +9 -17
  53. data/lib/backup/notifier/twitter.rb +11 -18
  54. data/lib/backup/package.rb +47 -0
  55. data/lib/backup/packager.rb +81 -16
  56. data/lib/backup/splitter.rb +48 -35
  57. data/lib/backup/storage/base.rb +44 -172
  58. data/lib/backup/storage/cloudfiles.rb +31 -46
  59. data/lib/backup/storage/cycler.rb +117 -0
  60. data/lib/backup/storage/dropbox.rb +92 -76
  61. data/lib/backup/storage/ftp.rb +30 -40
  62. data/lib/backup/storage/local.rb +44 -45
  63. data/lib/backup/storage/ninefold.rb +55 -49
  64. data/lib/backup/storage/rsync.rb +49 -56
  65. data/lib/backup/storage/s3.rb +33 -44
  66. data/lib/backup/storage/scp.rb +21 -48
  67. data/lib/backup/storage/sftp.rb +26 -40
  68. data/lib/backup/syncer/base.rb +7 -0
  69. data/lib/backup/syncer/rsync/base.rb +78 -0
  70. data/lib/backup/syncer/rsync/local.rb +53 -0
  71. data/lib/backup/syncer/rsync/pull.rb +38 -0
  72. data/lib/backup/syncer/rsync/push.rb +113 -0
  73. data/lib/backup/syncer/s3.rb +42 -32
  74. data/lib/backup/version.rb +1 -1
  75. data/spec/archive_spec.rb +235 -69
  76. data/spec/cleaner_spec.rb +304 -0
  77. data/spec/cli/helpers_spec.rb +142 -1
  78. data/spec/cli/utility_spec.rb +338 -13
  79. data/spec/compressor/base_spec.rb +31 -0
  80. data/spec/compressor/bzip2_spec.rb +60 -35
  81. data/spec/compressor/gzip_spec.rb +60 -35
  82. data/spec/compressor/lzma_spec.rb +60 -35
  83. data/spec/compressor/pbzip2_spec.rb +98 -37
  84. data/spec/config_spec.rb +321 -0
  85. data/spec/configuration/base_spec.rb +4 -4
  86. data/spec/configuration/compressor/bzip2_spec.rb +1 -0
  87. data/spec/configuration/compressor/gzip_spec.rb +1 -0
  88. data/spec/configuration/compressor/lzma_spec.rb +1 -0
  89. data/spec/configuration/compressor/pbzip2_spec.rb +32 -0
  90. data/spec/configuration/database/base_spec.rb +2 -1
  91. data/spec/configuration/database/mongodb_spec.rb +26 -16
  92. data/spec/configuration/database/mysql_spec.rb +4 -0
  93. data/spec/configuration/database/postgresql_spec.rb +4 -0
  94. data/spec/configuration/database/redis_spec.rb +4 -0
  95. data/spec/configuration/database/riak_spec.rb +4 -0
  96. data/spec/configuration/encryptor/gpg_spec.rb +1 -0
  97. data/spec/configuration/encryptor/open_ssl_spec.rb +1 -0
  98. data/spec/configuration/notifier/base_spec.rb +32 -0
  99. data/spec/configuration/notifier/campfire_spec.rb +1 -0
  100. data/spec/configuration/notifier/hipchat_spec.rb +1 -0
  101. data/spec/configuration/notifier/mail_spec.rb +1 -0
  102. data/spec/configuration/notifier/presently_spec.rb +1 -0
  103. data/spec/configuration/notifier/prowl_spec.rb +1 -0
  104. data/spec/configuration/notifier/twitter_spec.rb +1 -0
  105. data/spec/configuration/storage/cloudfiles_spec.rb +1 -0
  106. data/spec/configuration/storage/dropbox_spec.rb +4 -3
  107. data/spec/configuration/storage/ftp_spec.rb +1 -0
  108. data/spec/configuration/storage/local_spec.rb +1 -0
  109. data/spec/configuration/storage/ninefold_spec.rb +1 -0
  110. data/spec/configuration/storage/rsync_spec.rb +3 -1
  111. data/spec/configuration/storage/s3_spec.rb +1 -0
  112. data/spec/configuration/storage/scp_spec.rb +1 -0
  113. data/spec/configuration/storage/sftp_spec.rb +1 -0
  114. data/spec/configuration/syncer/rsync/base_spec.rb +33 -0
  115. data/spec/configuration/syncer/rsync/local_spec.rb +10 -0
  116. data/spec/configuration/syncer/rsync/pull_spec.rb +10 -0
  117. data/spec/configuration/syncer/{rsync_spec.rb → rsync/push_spec.rb} +12 -15
  118. data/spec/configuration/syncer/s3_spec.rb +2 -3
  119. data/spec/database/base_spec.rb +35 -20
  120. data/spec/database/mongodb_spec.rb +298 -119
  121. data/spec/database/mysql_spec.rb +147 -72
  122. data/spec/database/postgresql_spec.rb +155 -100
  123. data/spec/database/redis_spec.rb +200 -97
  124. data/spec/database/riak_spec.rb +82 -24
  125. data/spec/dependency_spec.rb +49 -0
  126. data/spec/encryptor/base_spec.rb +30 -0
  127. data/spec/encryptor/gpg_spec.rb +105 -28
  128. data/spec/encryptor/open_ssl_spec.rb +85 -114
  129. data/spec/logger_spec.rb +74 -8
  130. data/spec/model_spec.rb +528 -220
  131. data/spec/notifier/base_spec.rb +89 -0
  132. data/spec/notifier/campfire_spec.rb +147 -119
  133. data/spec/notifier/hipchat_spec.rb +140 -145
  134. data/spec/notifier/mail_spec.rb +190 -248
  135. data/spec/notifier/presently_spec.rb +147 -282
  136. data/spec/notifier/prowl_spec.rb +79 -111
  137. data/spec/notifier/twitter_spec.rb +87 -106
  138. data/spec/package_spec.rb +61 -0
  139. data/spec/packager_spec.rb +154 -0
  140. data/spec/spec_helper.rb +36 -13
  141. data/spec/splitter_spec.rb +90 -41
  142. data/spec/storage/base_spec.rb +95 -239
  143. data/spec/storage/cloudfiles_spec.rb +185 -75
  144. data/spec/storage/cycler_spec.rb +239 -0
  145. data/spec/storage/dropbox_spec.rb +318 -87
  146. data/spec/storage/ftp_spec.rb +165 -152
  147. data/spec/storage/local_spec.rb +206 -54
  148. data/spec/storage/ninefold_spec.rb +264 -128
  149. data/spec/storage/rsync_spec.rb +244 -163
  150. data/spec/storage/s3_spec.rb +175 -64
  151. data/spec/storage/scp_spec.rb +156 -150
  152. data/spec/storage/sftp_spec.rb +153 -135
  153. data/spec/syncer/base_spec.rb +22 -0
  154. data/spec/syncer/rsync/base_spec.rb +118 -0
  155. data/spec/syncer/rsync/local_spec.rb +121 -0
  156. data/spec/syncer/rsync/pull_spec.rb +90 -0
  157. data/spec/syncer/rsync/push_spec.rb +327 -0
  158. data/spec/syncer/s3_spec.rb +180 -91
  159. data/templates/cli/utility/config +1 -1
  160. data/templates/cli/utility/database/mongodb +4 -0
  161. data/templates/cli/utility/database/mysql +3 -0
  162. data/templates/cli/utility/database/postgresql +3 -0
  163. data/templates/cli/utility/database/redis +3 -0
  164. data/templates/cli/utility/database/riak +3 -0
  165. data/templates/cli/utility/storage/dropbox +4 -1
  166. data/templates/cli/utility/syncer/rsync_local +12 -0
  167. data/templates/cli/utility/syncer/{rsync → rsync_pull} +2 -2
  168. data/templates/cli/utility/syncer/rsync_push +17 -0
  169. data/templates/storage/dropbox/authorization_url.erb +1 -1
  170. metadata +42 -17
  171. data/lib/backup/configuration/syncer/rsync.rb +0 -45
  172. data/lib/backup/finder.rb +0 -87
  173. data/lib/backup/storage/object.rb +0 -47
  174. data/lib/backup/syncer/rsync.rb +0 -152
  175. data/spec/backup_spec.rb +0 -11
  176. data/spec/finder_spec.rb +0 -91
  177. data/spec/storage/object_spec.rb +0 -74
  178. data/spec/syncer/rsync_spec.rb +0 -195
@@ -1,11 +1,10 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require File.expand_path('../spec_helper.rb', __FILE__)
4
- require 'timecop'
5
4
 
6
5
  describe Backup::Logger do
7
6
  let(:logger_time) { Time.now.strftime("%Y/%m/%d %H:%M:%S") }
8
- let(:logfile_path) { File.join(Backup::LOG_PATH, 'backup.log') }
7
+ let(:logfile_path) { File.join(Backup::Config.log_path, 'backup.log') }
9
8
  let(:logfile_mock) { mock }
10
9
  let(:s) { sequence '' }
11
10
 
@@ -17,7 +16,7 @@ describe Backup::Logger do
17
16
  subject.unstub(message_type)
18
17
  end
19
18
 
20
- subject.send(:remove_const, :QUIET) rescue nil
19
+ subject.quiet = nil
21
20
  subject.send(:remove_instance_variable, :@messages) rescue nil
22
21
  subject.send(:remove_instance_variable, :@has_warnings) rescue nil
23
22
  end
@@ -145,6 +144,66 @@ describe Backup::Logger do
145
144
  end
146
145
  end # describe '#clear!'
147
146
 
147
+ describe '#truncate!' do
148
+ context 'when log file is <= max_bytes' do
149
+ it 'should do nothing' do
150
+ stat = mock
151
+ [1, 2].each do |size|
152
+ File.expects(:stat).with(
153
+ File.join(Backup::Config.log_path, 'backup.log')
154
+ ).returns(stat)
155
+ stat.expects(:size).returns(size)
156
+
157
+ FileUtils.expects(:mv).never
158
+ File.expects(:open).never
159
+ FileUtils.expects(:rm_f).never
160
+
161
+ subject.truncate!(2)
162
+ end
163
+ end
164
+ end
165
+
166
+ context 'when log file is > max_bytes' do
167
+ before do
168
+ FileUtils.unstub(:mkdir_p)
169
+ FileUtils.unstub(:mv)
170
+ FileUtils.unstub(:rm)
171
+ FileUtils.unstub(:rm_f)
172
+ end
173
+
174
+ it 'should truncate the file, removing older lines' do
175
+ max_bytes = 5_000
176
+ line_len = 120
177
+ Dir.mktmpdir do |dir|
178
+ Backup::Config.update(:root_path => dir)
179
+ FileUtils.mkdir_p(Backup::Config.log_path)
180
+ log_file = File.join(Backup::Config.log_path, 'backup.log')
181
+
182
+ lineno = 0
183
+ over_max = (max_bytes * 1.25).to_i
184
+ File.open(log_file, 'w') do |f|
185
+ until f.pos > over_max
186
+ f.puts "#{ lineno += 1 }: ".ljust(line_len, 'x')
187
+ end
188
+ end
189
+ File.stat(log_file).size.
190
+ should be_within(line_len + 2).of(over_max)
191
+
192
+ subject.truncate!(max_bytes)
193
+
194
+ File.stat(log_file).size.
195
+ should be_within(line_len + 2).of(max_bytes)
196
+
197
+ File.readlines(log_file).last.chomp.
198
+ should == "#{ lineno }: ".ljust(line_len, 'x')
199
+
200
+ File.exist?(log_file + '~').should be_false
201
+ end
202
+ end
203
+ end
204
+
205
+ end # describe '#truncate!'
206
+
148
207
  describe '#loggify' do
149
208
 
150
209
  it 'returns an array of strings split on newline separators' do
@@ -224,7 +283,7 @@ describe Backup::Logger do
224
283
  end
225
284
 
226
285
  it 'returns nil if quiet? is true' do
227
- subject.send(:const_set, :QUIET, true)
286
+ subject.quiet = true
228
287
  subject.expects(:puts).never
229
288
  subject.send(:to_console, 'a string')
230
289
  end
@@ -277,10 +336,17 @@ describe Backup::Logger do
277
336
 
278
337
  end # color methods
279
338
 
280
- describe '#quiet?' do
281
- it 'reports if the QUIET constant has been set' do
282
- expect { subject.send(:const_set, :QUIET, true) }.to
283
- change{ subject.send(:quiet?) }.from(false).to(true)
339
+ describe '#quiet' do
340
+ it 'should be nil by default' do
341
+ # of course, 'before' is setting it to nil ;)
342
+ subject.quiet.should be_nil
343
+ end
344
+
345
+ it 'can be set true/false' do
346
+ subject.quiet = true
347
+ subject.quiet.should be_true
348
+ subject.quiet = false
349
+ subject.quiet.should be_false
284
350
  end
285
351
  end
286
352
 
@@ -2,308 +2,432 @@
2
2
 
3
3
  require File.expand_path('../spec_helper.rb', __FILE__)
4
4
 
5
- describe Backup::Model do
5
+ describe 'Backup::Model' do
6
+ let(:model) { Backup::Model.new(:test_trigger, 'test label') }
7
+ let(:s) { sequence '' }
6
8
 
7
9
  before do
8
- # stub out the creation of an archive, for this spec's purpose
9
- Backup::Archive.stubs(:new).returns(true)
10
+ Backup::Model.all.clear
11
+ end
10
12
 
11
- # create mockup classes for testing the behavior of Backup::Model
12
- class Backup::Database::TestDatabase
13
- def initialize(&block); end
14
- end
15
- class Backup::Storage::TestStorage
16
- def initialize(storage_id = nil, &block); end
17
- end
18
- class Backup::Compressor::TestGzip
19
- def initialize(&block); end
20
- end
21
- class Backup::Compressor::TestSevenZip
22
- def initialize(&block); end
23
- end
24
- class Backup::Encryptor::TestOpenSSL
25
- def initialize(&block); end
26
- end
27
- class Backup::Encryptor::TestGPG
28
- def initialize(&block); end
13
+ describe '.all' do
14
+ it 'should be an empty array by default' do
15
+ Backup::Model.all.should == []
29
16
  end
30
- class Backup::Notifier::TestMail
31
- def initialize(&block); end
32
- end
33
-
34
17
  end
35
18
 
36
- let(:model) { Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') {} }
19
+ describe 'finder methods' do
20
+ before do
21
+ [:a, :b, :c, :b, :d].each_with_index do |sym, i|
22
+ Backup::Model.new("trigger_#{sym}", "label#{i}")
23
+ end
24
+ end
37
25
 
38
- it do
39
- Backup::Model.extension.should == 'tar'
40
- end
26
+ describe '.find' do
27
+ it 'should return the first matching model' do
28
+ Backup::Model.find('trigger_b').label.should == 'label1'
29
+ end
41
30
 
42
- it do
43
- Backup::Model.new('foo', 'bar') {}
44
- Backup::Model.extension.should == 'tar'
45
- end
31
+ it 'should accept symbols' do
32
+ Backup::Model.find(:trigger_b).label.should == 'label1'
33
+ end
46
34
 
47
- before do
48
- Backup::Model.extension = 'tar'
49
- end
35
+ it 'should raise an error if trigger is not found' do
36
+ expect do
37
+ Backup::Model.find(:f)
38
+ end.to raise_error(
39
+ Backup::Errors::Model::MissingTriggerError,
40
+ "Model::MissingTriggerError: Could not find trigger 'f'."
41
+ )
42
+ end
43
+ end
50
44
 
51
- it do
52
- Backup::Model.new('blah', 'blah') {}
53
- Backup::Model.extension.should == 'tar'
54
- end
45
+ describe '.find_matching' do
46
+ it 'should find all triggers matching a wildcard' do
47
+ Backup::Model.find_matching('tri*_b').count.should be(2)
48
+ Backup::Model.find_matching('trigg*').count.should be(5)
49
+ end
55
50
 
56
- it do
57
- Backup::Model.new('blah', 'blah') {}
58
- Backup::Model.file.should == "#{ File.join(Backup::TMP_PATH, "#{ Backup::TIME }.#{ Backup::TRIGGER }.tar") }"
59
- end
51
+ it 'should return an empty array if no matches are found' do
52
+ Backup::Model.find_matching('foo*').should == []
53
+ end
54
+ end
60
55
 
61
- it do
62
- Backup::Model.new('blah', 'blah') {}
63
- File.basename(Backup::Model.file).should == "#{ Backup::TIME }.#{ Backup::TRIGGER }.tar"
64
- end
56
+ end # describe 'finder methods'
65
57
 
66
- it do
67
- Backup::Model.new('blah', 'blah') {}
68
- Backup::Model.tmp_path.should == File.join(Backup::TMP_PATH, Backup::TRIGGER)
69
- end
58
+ describe '#initialize' do
70
59
 
71
- it 'should create a new model with a trigger and label' do
72
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') {}
73
- model.trigger.should == 'mysql-s3'
74
- model.label.should == 'MySQL S3 Backup for MyApp'
75
- end
60
+ it 'should convert trigger to a string' do
61
+ Backup::Model.new(:foo, :bar).trigger.should == 'foo'
62
+ end
76
63
 
77
- it 'should have the time logged in the object' do
78
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') {}
79
- model.time.should == Backup::TIME
80
- end
64
+ it 'should convert label to a string' do
65
+ Backup::Model.new(:foo, :bar).label.should == 'bar'
66
+ end
81
67
 
82
- describe '#extension' do
83
- it 'should start out with just .tar before compression occurs' do
84
- Backup::Model.extension.should == 'tar'
68
+ it 'should set all procedure variables to an empty array' do
69
+ model.send(:procedure_instance_variables).each do |var|
70
+ model.instance_variable_get(var).should == []
71
+ end
85
72
  end
86
- end
87
73
 
88
- describe 'databases' do
89
- it 'should add the mysql adapter to the array of databases to invoke' do
90
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
91
- database('TestDatabase')
74
+ it 'should accept and instance_eval a block' do
75
+ block = lambda {|model| throw(:instance, model) }
76
+ caught = catch(:instance) do
77
+ Backup::Model.new('gotcha', '', &block)
92
78
  end
79
+ caught.trigger.should == 'gotcha'
80
+ end
93
81
 
94
- model.databases.count.should == 1
82
+ it 'should add itself to Model.all' do
83
+ Backup::Model.all.should == [model]
95
84
  end
96
85
 
97
- it 'should add 2 mysql adapters to the array of adapters to invoke' do
98
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
99
- database('TestDatabase')
100
- database('TestDatabase')
86
+ end # describe '#initialize'
87
+
88
+ describe 'DSL Methods' do
89
+
90
+ module Fake
91
+ module NoArg
92
+ class Base
93
+ attr_accessor :block_arg
94
+ def initialize(&block)
95
+ instance_eval(&block) if block_given?
96
+ end
97
+ end
101
98
  end
99
+ module OneArg
100
+ class Base
101
+ attr_accessor :arg1, :block_arg
102
+ def initialize(arg1, &block)
103
+ @arg1 = arg1
104
+ instance_eval(&block) if block_given?
105
+ end
106
+ end
107
+ end
108
+ module TwoArgs
109
+ class Base
110
+ attr_accessor :arg1, :arg2, :block_arg
111
+ def initialize(arg1, arg2, &block)
112
+ @arg1 = arg1
113
+ @arg2 = arg2
114
+ instance_eval(&block) if block_given?
115
+ end
116
+ end
117
+ end
118
+ end
102
119
 
103
- model.databases.count.should == 2
120
+ # Set +const+ to +replacement+ for the calling block
121
+ def using_fake(const, replacement)
122
+ orig = Backup.const_get(const)
123
+ Backup.send(:remove_const, const)
124
+ Backup.const_set(const, replacement)
125
+ yield
126
+ Backup.send(:remove_const, const)
127
+ Backup.const_set(const, orig)
104
128
  end
105
- end
106
129
 
107
- describe 'storages' do
108
- it 'should add a storage to the array of storages to use' do
109
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
110
- store_with('TestStorage')
130
+ describe '#archive' do
131
+ it 'should add archives' do
132
+ using_fake('Archive', Fake::TwoArgs::Base) do
133
+ model.archive('foo') {|a| a.block_arg = :foo }
134
+ model.archive('bar') {|a| a.block_arg = :bar }
135
+ model.archives.count.should == 2
136
+ a1, a2 = model.archives
137
+ a1.arg1.should be(model)
138
+ a1.arg2.should == 'foo'
139
+ a1.block_arg.should == :foo
140
+ a2.arg1.should be(model)
141
+ a2.arg2.should == 'bar'
142
+ a2.block_arg.should == :bar
143
+ end
111
144
  end
112
-
113
- model.storages.count.should == 1
114
145
  end
115
146
 
116
- it 'should add a storage to the array of storages to use' do
117
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
118
- store_with('TestStorage')
119
- store_with('TestStorage')
147
+ describe '#database' do
148
+ it 'should add databases' do
149
+ using_fake('Database', Fake::OneArg) do
150
+ model.database('Base') {|a| a.block_arg = :foo }
151
+ model.database('Base') {|a| a.block_arg = :bar }
152
+ model.databases.count.should be(2)
153
+ d1, d2 = model.databases
154
+ d1.arg1.should be(model)
155
+ d1.block_arg.should == :foo
156
+ d2.arg1.should be(model)
157
+ d2.block_arg.should == :bar
158
+ end
120
159
  end
121
160
 
122
- model.storages.count.should == 2
161
+ it 'should accept a nested class name' do
162
+ using_fake('Database', Fake) do
163
+ model.database('OneArg::Base')
164
+ model.databases.first.should be_an_instance_of Fake::OneArg::Base
165
+ end
166
+ end
123
167
  end
124
168
 
125
- it 'should accept an optional storage_id parameter' do
126
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
127
- store_with('TestStorage', 'test storage_id')
169
+ describe '#store_with' do
170
+ it 'should add storages' do
171
+ using_fake('Storage', Fake::TwoArgs) do
172
+ model.store_with('Base', 'foo') {|a| a.block_arg = :foo }
173
+ model.store_with('Base', 'bar') {|a| a.block_arg = :bar }
174
+ model.storages.count.should be(2)
175
+ s1, s2 = model.storages
176
+ s1.arg1.should be(model)
177
+ s1.arg2.should == 'foo'
178
+ s1.block_arg.should == :foo
179
+ s2.arg1.should be(model)
180
+ s2.arg2.should == 'bar'
181
+ s2.block_arg.should == :bar
182
+ end
128
183
  end
129
184
 
130
- model.storages.count.should == 1
185
+ it 'should accept a nested class name' do
186
+ using_fake('Storage', Fake) do
187
+ model.store_with('TwoArgs::Base')
188
+ model.storages.first.should be_an_instance_of Fake::TwoArgs::Base
189
+ end
190
+ end
131
191
  end
132
192
 
133
- end
193
+ describe '#sync_with' do
194
+ it 'should add syncers' do
195
+ using_fake('Syncer', Fake::NoArg) do
196
+ model.sync_with('Base') {|a| a.block_arg = :foo }
197
+ model.sync_with('Base') {|a| a.block_arg = :bar }
198
+ model.syncers.count.should be(2)
199
+ s1, s2 = model.syncers
200
+ s1.block_arg.should == :foo
201
+ s2.block_arg.should == :bar
202
+ end
203
+ end
134
204
 
135
- describe 'archives' do
136
- it 'should add an archive to the array of archives to use' do
137
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
138
- archive('my_archive')
205
+ it 'should accept a nested class name' do
206
+ using_fake('Syncer', Fake) do
207
+ model.sync_with('NoArg::Base')
208
+ model.syncers.first.should be_an_instance_of Fake::NoArg::Base
209
+ end
139
210
  end
140
211
 
141
- model.archives.count.should == 1
212
+ it 'should warn user of change from RSync to RSync::Local' do
213
+ Backup::Logger.expects(:warn)
214
+ model.sync_with('Backup::Config::RSync')
215
+ model.syncers.first.should be_an_instance_of Backup::Syncer::RSync::Local
216
+ end
142
217
  end
143
218
 
144
- it 'should add a storage to the array of storages to use' do
145
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
146
- archive('TestStorage')
147
- archive('TestStorage')
219
+ describe '#notify_by' do
220
+ it 'should add notifiers' do
221
+ using_fake('Notifier', Fake::OneArg) do
222
+ model.notify_by('Base') {|a| a.block_arg = :foo }
223
+ model.notify_by('Base') {|a| a.block_arg = :bar }
224
+ model.notifiers.count.should be(2)
225
+ n1, n2 = model.notifiers
226
+ n1.arg1.should be(model)
227
+ n1.block_arg.should == :foo
228
+ n2.arg1.should be(model)
229
+ n2.block_arg.should == :bar
230
+ end
148
231
  end
149
232
 
150
- model.archives.count.should == 2
233
+ it 'should accept a nested class name' do
234
+ using_fake('Notifier', Fake) do
235
+ model.notify_by('OneArg::Base')
236
+ model.notifiers.first.should be_an_instance_of Fake::OneArg::Base
237
+ end
238
+ end
151
239
  end
152
- end
153
240
 
154
- describe '#compress_with' do
155
- it 'should add a compressor to the array of compressors to use' do
156
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
157
- compress_with('TestGzip')
241
+ describe '#encrypt_with' do
242
+ it 'should add an encryptor' do
243
+ using_fake('Encryptor', Fake::NoArg) do
244
+ model.encrypt_with('Base') {|a| a.block_arg = :foo }
245
+ model.encryptor.should be_an_instance_of Fake::NoArg::Base
246
+ model.encryptor.block_arg.should == :foo
247
+ end
158
248
  end
159
249
 
160
- model.compressors.count.should == 1
250
+ it 'should accept a nested class name' do
251
+ using_fake('Encryptor', Fake) do
252
+ model.encrypt_with('NoArg::Base')
253
+ model.encryptor.should be_an_instance_of Fake::NoArg::Base
254
+ end
255
+ end
161
256
  end
162
257
 
163
- it 'should add a compressor to the array of compressors to use' do
164
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
165
- compress_with('TestGzip')
166
- compress_with('TestSevenZip')
258
+ describe '#compress_with' do
259
+ it 'should add a compressor' do
260
+ using_fake('Compressor', Fake::NoArg) do
261
+ model.compress_with('Base') {|a| a.block_arg = :foo }
262
+ model.compressor.should be_an_instance_of Fake::NoArg::Base
263
+ model.compressor.block_arg.should == :foo
264
+ end
167
265
  end
168
266
 
169
- model.compressors.count.should == 2
267
+ it 'should accept a nested class name' do
268
+ using_fake('Compressor', Fake) do
269
+ model.compress_with('NoArg::Base')
270
+ model.compressor.should be_an_instance_of Fake::NoArg::Base
271
+ end
272
+ end
170
273
  end
171
- end
172
274
 
173
- describe '#encrypt_with' do
174
- it 'should add a encryptor to the array of encryptors to use' do
175
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
176
- encrypt_with('TestOpenSSL')
275
+ describe '#split_into_chunks_of' do
276
+ it 'should add a splitter' do
277
+ using_fake('Splitter', Fake::TwoArgs::Base) do
278
+ model.split_into_chunks_of(123)
279
+ model.splitter.should be_an_instance_of Fake::TwoArgs::Base
280
+ model.splitter.arg1.should be(model)
281
+ model.splitter.arg2.should == 123
282
+ end
177
283
  end
178
284
 
179
- model.encryptors.count.should == 1
285
+ it 'should raise an error if chunk_size is not an Integer' do
286
+ expect do
287
+ model.split_into_chunks_of('345')
288
+ end.to raise_error {|err|
289
+ err.should be_an_instance_of Backup::Errors::Model::ConfigurationError
290
+ err.message.should match(/must be an Integer/)
291
+ }
292
+ end
180
293
  end
181
294
 
182
- it 'should add a encryptor to the array of encryptors to use' do
183
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
184
- encrypt_with('TestOpenSSL')
185
- encrypt_with('TestGPG')
186
- end
295
+ end # describe 'DSL Methods'
296
+
297
+ describe '#prepare!' do
298
+ it 'should prepare for the backup' do
299
+ FileUtils.expects(:mkdir_p).with(
300
+ File.join(Backup::Config.data_path, 'test_trigger')
301
+ )
302
+ Backup::Cleaner.expects(:prepare).with(model)
187
303
 
188
- model.encryptors.count.should == 2
304
+ model.prepare!
189
305
  end
190
306
  end
191
307
 
192
- describe '#notify_by' do
193
- it 'should add a notifier to the array of notifiers to use' do
194
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
195
- notify_by('TestMail')
196
- end
197
-
198
- model.notifiers.count.should == 1
308
+ describe '#perform!' do
309
+ let(:procedure_a) { lambda {} }
310
+ let(:procedure_b) { mock }
311
+ let(:procedure_c) { mock }
312
+ let(:procedure_d) { lambda {} }
313
+ let(:procedure_e) { lambda {} }
314
+ let(:procedure_f) { mock }
315
+ let(:procedures) do
316
+ [ procedure_a, [procedure_b, procedure_c],
317
+ procedure_d, procedure_e, [procedure_f] ]
318
+ end
319
+ let(:syncer_a) { mock }
320
+ let(:syncer_b) { mock }
321
+ let(:syncers) { [syncer_a, syncer_b] }
322
+ let(:notifier_a) { mock }
323
+ let(:notifier_b) { mock }
324
+ let(:notifiers) { [notifier_a, notifier_b] }
325
+
326
+ it 'should set the @time and @started_at variables' do
327
+ Timecop.freeze(Time.now)
328
+ started_at = Time.now
329
+ time = started_at.strftime("%Y.%m.%d.%H.%M.%S")
330
+
331
+ model.perform!
332
+ model.time.should == time
333
+ model.instance_variable_get(:@started_at).should == started_at
199
334
  end
200
335
 
201
- it 'should add a notifier to the array of notifiers to use' do
202
- model = Backup::Model.new('mysql-s3', 'MySQL S3 Backup for MyApp') do
203
- notify_by('TestMail')
204
- notify_by('TestMail')
336
+ context 'when no errors occur' do
337
+ before do
338
+ model.expects(:procedures).returns(procedures)
339
+ model.expects(:syncers).returns(syncers)
340
+ model.expects(:notifiers).returns(notifiers)
205
341
  end
206
342
 
207
- model.notifiers.count.should == 2
208
- end
209
- end
343
+ context 'when databases are configured' do
344
+ before do
345
+ model.instance_variable_set(:@databases, [true])
346
+ end
210
347
 
211
- describe '#package!' do
212
- let(:packager) { Backup::Packager.new(model) }
348
+ it 'should perform all procedures' do
349
+ model.expects(:log!).in_sequence(s).with(:started)
213
350
 
214
- before do
215
- [:utility, :run].each { |method| model.stubs(method) }
216
- end
351
+ procedure_a.expects(:call).in_sequence(s)
352
+ procedure_b.expects(:perform!).in_sequence(s)
353
+ procedure_c.expects(:perform!).in_sequence(s)
354
+ procedure_d.expects(:call).in_sequence(s)
355
+ procedure_e.expects(:call).in_sequence(s)
356
+ procedure_f.expects(:perform!).in_sequence(s)
217
357
 
218
- it 'should package the folder' do
219
- packager.expects(:utility).with(:tar).returns(:tar)
220
- packager.expects(:run).with(%|tar -c -f '#{ File.join( Backup::TMP_PATH, "#{ Backup::TIME }.#{ Backup::TRIGGER }.tar" ) }' -C '#{ Backup::TMP_PATH }' '#{ Backup::TRIGGER }'|)
221
- Backup::Logger.expects(:message).with("Backup::Packager started packaging the backup files.")
222
- packager.package!
223
- end
224
- end
358
+ syncer_a.expects(:perform!).in_sequence(s)
359
+ syncer_b.expects(:perform!).in_sequence(s)
225
360
 
226
- describe '#clean!' do
227
- let(:cleaner) { Backup::Cleaner.new(model) }
361
+ notifier_a.expects(:perform!).in_sequence(s)
362
+ notifier_b.expects(:perform!).in_sequence(s)
228
363
 
229
- before do
230
- [:utility, :run, :rm].each { |method| model.stubs(method) }
231
- end
364
+ model.expects(:log!).in_sequence(s).with(:finished)
232
365
 
233
- context 'when the backup archive is not chunked' do
234
- it 'should remove the temporary files and folders that were created' do
235
- cleaner.expects(:utility).with(:rm).returns(:rm)
236
- Backup::Model.chunk_suffixes = []
237
- cleaner.expects(:run).with "rm -rf '#{ File.join(Backup::TMP_PATH, Backup::TRIGGER) }' " +
238
- "'#{ File.join(Backup::TMP_PATH, "#{ Backup::TIME }.#{ Backup::TRIGGER }.tar") }'"
239
- cleaner.clean!
366
+ model.perform!
367
+ end
240
368
  end
241
- end
242
369
 
243
- context 'when the backup archive is chunked' do
244
- it 'should remove the temporary files and folders that were created' do
245
- cleaner.expects(:utility).with(:rm).returns(:rm)
246
- Backup::Model.chunk_suffixes = ["aa", "ab", "ac"]
247
- cleaner.expects(:run).with "rm -rf '#{ File.join(Backup::TMP_PATH, Backup::TRIGGER) }' " +
248
- "'#{ File.join(Backup::TMP_PATH, "#{ Backup::TIME }.#{ Backup::TRIGGER }.tar") }' " +
249
- "'#{ File.join(Backup::TMP_PATH, "#{ Backup::TIME }.#{ Backup::TRIGGER }.tar-aa") }' " +
250
- "'#{ File.join(Backup::TMP_PATH, "#{ Backup::TIME }.#{ Backup::TRIGGER }.tar-ab") }' " +
251
- "'#{ File.join(Backup::TMP_PATH, "#{ Backup::TIME }.#{ Backup::TRIGGER }.tar-ac") }'"
252
- cleaner.clean!
253
- end
254
- end
370
+ context 'when archives are configured' do
371
+ before do
372
+ model.instance_variable_set(:@archives, [true])
373
+ end
255
374
 
256
- it do
257
- Backup::Logger.expects(:message).with("Backup::Cleaner started cleaning up the temporary files.")
258
- model.send(:clean!)
259
- end
260
- end
375
+ it 'should perform all procedures' do
376
+ model.expects(:log!).in_sequence(s).with(:started)
261
377
 
262
- describe '#split_into_chunks_of' do
263
- it do
264
- model.should respond_to(:split_into_chunks_of)
265
- end
378
+ procedure_a.expects(:call).in_sequence(s)
379
+ procedure_b.expects(:perform!).in_sequence(s)
380
+ procedure_c.expects(:perform!).in_sequence(s)
381
+ procedure_d.expects(:call).in_sequence(s)
382
+ procedure_e.expects(:call).in_sequence(s)
383
+ procedure_f.expects(:perform!).in_sequence(s)
266
384
 
267
- it do
268
- model.split_into_chunks_of(500)
269
- model.chunk_size.should == 500
270
- end
385
+ syncer_a.expects(:perform!).in_sequence(s)
386
+ syncer_b.expects(:perform!).in_sequence(s)
271
387
 
272
- it do
273
- model.chunk_size.should == nil
274
- end
275
- end
388
+ notifier_a.expects(:perform!).in_sequence(s)
389
+ notifier_b.expects(:perform!).in_sequence(s)
276
390
 
277
- describe '#perform!' do
391
+ model.expects(:log!).in_sequence(s).with(:finished)
392
+
393
+ model.perform!
394
+ end
395
+ end
396
+
397
+ end # context 'when no errors occur'
278
398
 
279
399
  # for the purposes of testing the error handling, we're just going to
280
400
  # stub the first thing this method calls and raise an error
281
- describe 'when errors occur' do
282
- let(:model) { Backup::Model.new('foo', 'foo') {} }
401
+ context 'when errors occur' do
402
+ let(:error_a) { mock }
403
+ let(:error_b) { mock }
404
+ let(:notifier) { mock }
283
405
 
284
406
  before do
285
- # method ensures that #clean! is always run before exiting
286
- model.expects(:clean!)
407
+ error_a.stubs(:backtrace).returns(['many', 'backtrace', 'lines'])
287
408
  end
288
409
 
289
410
  it 'logs, notifies and continues if a StandardError is rescued' do
290
- model.stubs(:databases).raises(StandardError, 'non-fatal error')
291
-
292
- Backup::Logger.expects(:error).twice
293
- Backup::Logger.expects(:message).once
411
+ Time.stubs(:now).raises(StandardError, 'non-fatal error')
294
412
 
295
- Backup::Errors::ModelError.expects(:wrap).with do |err, msg|
413
+ Backup::Errors::ModelError.expects(:wrap).in_sequence(s).with do |err, msg|
296
414
  err.message.should == 'non-fatal error'
297
- msg.should match(/Backup for foo \(foo\) Failed!/)
298
- end
415
+ msg.should match(/Backup for test label \(test_trigger\) Failed!/)
416
+ end.returns(error_a)
417
+ Backup::Logger.expects(:error).in_sequence(s).with(error_a)
418
+ Backup::Logger.expects(:error).in_sequence(s).with(
419
+ "\nBacktrace:\n\s\smany\n\s\sbacktrace\n\s\slines\n\n"
420
+ )
421
+
422
+ Backup::Cleaner.expects(:warnings).in_sequence(s).with(model)
299
423
 
300
- Backup::Errors::ModelError.expects(:new).with do |msg|
424
+ Backup::Errors::ModelError.expects(:new).in_sequence(s).with do |msg|
301
425
  msg.should match(/Backup will now attempt to continue/)
302
- end
426
+ end.returns(error_b)
427
+ Backup::Logger.expects(:message).in_sequence(s).with(error_b)
303
428
 
304
429
  # notifiers called, but any Exception is ignored
305
- notifier = mock
306
- notifier.expects(:perform!).raises(Exception)
430
+ notifier.expects(:perform!).with(true).raises(Exception)
307
431
  model.expects(:notifiers).returns([notifier])
308
432
 
309
433
  # returns to allow next trigger to run
@@ -311,31 +435,215 @@ describe Backup::Model do
311
435
  end
312
436
 
313
437
  it 'logs, notifies and exits if an Exception is rescued' do
314
- model.stubs(:databases).raises(Exception, 'fatal error')
438
+ Time.stubs(:now).raises(Exception, 'fatal error')
315
439
 
316
- Backup::Logger.expects(:error).times(3)
317
- Backup::Logger.expects(:message).never
318
-
319
- Backup::Errors::ModelError.expects(:wrap).with do |err, msg|
440
+ Backup::Errors::ModelError.expects(:wrap).in_sequence(s).with do |err, msg|
320
441
  err.message.should == 'fatal error'
321
- msg.should match(/Backup for foo \(foo\) Failed!/)
322
- end
442
+ msg.should match(/Backup for test label \(test_trigger\) Failed!/)
443
+ end.returns(error_a)
444
+ Backup::Logger.expects(:error).in_sequence(s).with(error_a)
445
+ Backup::Logger.expects(:error).in_sequence(s).with(
446
+ "\nBacktrace:\n\s\smany\n\s\sbacktrace\n\s\slines\n\n"
447
+ )
448
+
449
+ Backup::Cleaner.expects(:warnings).in_sequence(s).with(model)
323
450
 
324
- Backup::Errors::ModelError.expects(:new).with do |msg|
451
+ Backup::Errors::ModelError.expects(:new).in_sequence(s).with do |msg|
325
452
  msg.should match(/Backup will now exit/)
326
- end
453
+ end.returns(error_b)
454
+ Backup::Logger.expects(:error).in_sequence(s).with(error_b)
327
455
 
328
456
  expect do
329
457
  # notifiers called, but any Exception is ignored
330
458
  notifier = mock
331
- notifier.expects(:perform!).raises(Exception)
459
+ notifier.expects(:perform!).with(true).raises(Exception)
332
460
  model.expects(:notifiers).returns([notifier])
333
461
  end.not_to raise_error
334
462
 
335
- expect { model.perform! }.to raise_error(SystemExit)
463
+ expect do
464
+ model.perform!
465
+ end.to raise_error(SystemExit) {|exit| exit.status.should be(1) }
336
466
  end
337
467
 
338
468
  end # context 'when errors occur'
339
469
 
340
470
  end # describe '#perform!'
471
+
472
+ describe '#package!' do
473
+ it 'should package the backup' do
474
+ Backup::Packager.expects(:package!).in_sequence(s).with(model)
475
+ Backup::Cleaner.expects(:remove_packaging).in_sequence(s).with(model)
476
+
477
+ model.send(:package!)
478
+ model.package.should be_an_instance_of Backup::Package
479
+ end
480
+ end
481
+
482
+ describe '#clean' do
483
+ it 'should remove the final packaged files' do
484
+ package = mock
485
+ model.instance_variable_set(:@package, package)
486
+ Backup::Cleaner.expects(:remove_package).with(package)
487
+
488
+ model.send(:clean!)
489
+ end
490
+ end
491
+
492
+ describe '#procedures' do
493
+ it 'should return an array of specific, ordered procedures' do
494
+ model.stubs(:databases).returns(:databases)
495
+ model.stubs(:archives).returns(:archives)
496
+ model.stubs(:package!).returns(:package)
497
+ model.stubs(:storages).returns(:storages)
498
+ model.stubs(:clean!).returns(:clean)
499
+
500
+ one, two, three, four, five = model.send(:procedures)
501
+ one.should == :databases
502
+ two.should == :archives
503
+ three.call.should == :package
504
+ four.should == :storages
505
+ five.call.should == :clean
506
+ end
507
+ end
508
+
509
+ describe '#procedure_instance_variables' do
510
+ # these are all set to an empty Array in #initialize
511
+ it 'should return an array of Array holding instance variables' do
512
+ model.send(:procedure_instance_variables).should ==
513
+ [:@databases, :@archives, :@storages, :@notifiers, :@syncers]
514
+ end
515
+ end
516
+
517
+ describe '#get_class_from_scope' do
518
+
519
+ module Fake
520
+ module TestScope
521
+ class TestKlass; end
522
+ end
523
+ end
524
+ module TestScope
525
+ module TestKlass; end
526
+ end
527
+
528
+ context 'when name is given as a string' do
529
+ it 'should return the constant for the given scope and name' do
530
+ model.send(
531
+ :get_class_from_scope,
532
+ Fake,
533
+ 'TestScope'
534
+ ).should == Fake::TestScope
535
+ end
536
+
537
+ it 'should accept a nested class name' do
538
+ model.send(
539
+ :get_class_from_scope,
540
+ Fake,
541
+ 'TestScope::TestKlass'
542
+ ).should == Fake::TestScope::TestKlass
543
+ end
544
+ end
545
+
546
+ context 'when name is given as a module' do
547
+ it 'should return the constant for the given scope and name' do
548
+ model.send(
549
+ :get_class_from_scope,
550
+ Fake,
551
+ TestScope
552
+ ).should == Fake::TestScope
553
+ end
554
+
555
+ it 'should accept a nested class name' do
556
+ model.send(
557
+ :get_class_from_scope,
558
+ Fake,
559
+ TestScope::TestKlass
560
+ ).should == Fake::TestScope::TestKlass
561
+ end
562
+ end
563
+
564
+ context 'when name is given as a module defined under Backup::Config' do
565
+ # this is necessary since the specs in spec/config_spec.rb
566
+ # remove all the constants from Backup::Config as part of those tests.
567
+ before(:all) do
568
+ module Backup::Config
569
+ module TestScope
570
+ module TestKlass; end
571
+ end
572
+ end
573
+ end
574
+
575
+ it 'should return the constant for the given scope and name' do
576
+ model.send(
577
+ :get_class_from_scope,
578
+ Fake,
579
+ Backup::Config::TestScope
580
+ ).should == Fake::TestScope
581
+ end
582
+
583
+ it 'should accept a nested class name' do
584
+ model.send(
585
+ :get_class_from_scope,
586
+ Fake,
587
+ Backup::Config::TestScope::TestKlass
588
+ ).should == Fake::TestScope::TestKlass
589
+ end
590
+ end
591
+
592
+ end # describe '#get_class_from_scope'
593
+
594
+ describe '#log!' do
595
+ context 'when action is :started' do
596
+ it 'should log that the backup has started with the version' do
597
+ Backup::Logger.expects(:message).with(
598
+ "Performing Backup for 'test label (test_trigger)'!\n" +
599
+ "[ backup #{ Backup::Version.current } : #{ RUBY_DESCRIPTION } ]"
600
+ )
601
+ model.send(:log!, :started)
602
+ end
603
+ end
604
+
605
+ context 'when action is :finished' do
606
+ before { model.expects(:elapsed_time).returns('01:02:03') }
607
+ context 'when warnings were issued' do
608
+ before { Backup::Logger.expects(:has_warnings?).returns(true) }
609
+ it 'should log a warning that the backup has finished with warnings' do
610
+ Backup::Logger.expects(:warn).with(
611
+ "Backup for 'test label (test_trigger)' " +
612
+ "Completed Successfully (with Warnings) in 01:02:03"
613
+ )
614
+ model.send(:log!, :finished)
615
+ end
616
+ end
617
+
618
+ context 'when no warnings were issued' do
619
+ it 'should log that the backup has finished with the elapsed time' do
620
+ Backup::Logger.expects(:message).with(
621
+ "Backup for 'test label (test_trigger)' " +
622
+ "Completed Successfully in 01:02:03"
623
+ )
624
+ model.send(:log!, :finished)
625
+ end
626
+ end
627
+ end
628
+ end # describe '#log!'
629
+
630
+ describe '#elapsed_time' do
631
+ it 'should return a string representing the elapsed time' do
632
+ Timecop.freeze(Time.now)
633
+ { 0 => '00:00:00', 1 => '00:00:01', 59 => '00:00:59',
634
+ 60 => '00:01:00', 61 => '00:01:01', 119 => '00:01:59',
635
+ 3540 => '00:59:00', 3541 => '00:59:01', 3599 => '00:59:59',
636
+ 3600 => '01:00:00', 3601 => '01:00:01', 3659 => '01:00:59',
637
+ 3660 => '01:01:00', 3661 => '01:01:01', 3719 => '01:01:59',
638
+ 7140 => '01:59:00', 7141 => '01:59:01', 7199 => '01:59:59',
639
+ 212400 => '59:00:00', 212401 => '59:00:01', 212459 => '59:00:59',
640
+ 212460 => '59:01:00', 212461 => '59:01:01', 212519 => '59:01:59',
641
+ 215940 => '59:59:00', 215941 => '59:59:01', 215999 => '59:59:59'
642
+ }.each do |duration, expected|
643
+ model.instance_variable_set(:@started_at, Time.now - duration)
644
+ model.send(:elapsed_time).should == expected
645
+ end
646
+ end
647
+ end
648
+
341
649
  end