Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

667 lines (567 sloc) 21.411 kb
# encoding: utf-8
require File.expand_path('../spec_helper.rb', __FILE__)
describe 'Backup::Model' do
let(:model) { Backup::Model.new(:test_trigger, 'test label') }
let(:s) { sequence '' }
before do
Backup::Model.all.clear
end
describe '.all' do
it 'should be an empty array by default' do
Backup::Model.all.should == []
end
end
describe 'finder methods' do
before do
[:a, :b, :c, :b, :d].each_with_index do |sym, i|
Backup::Model.new("trigger_#{sym}", "label#{i}")
end
end
describe '.find' do
it 'should return the first matching model' do
Backup::Model.find('trigger_b').label.should == 'label1'
end
it 'should accept symbols' do
Backup::Model.find(:trigger_b).label.should == 'label1'
end
it 'should raise an error if trigger is not found' do
expect do
Backup::Model.find(:f)
end.to raise_error(
Backup::Errors::Model::MissingTriggerError,
"Model::MissingTriggerError: Could not find trigger 'f'."
)
end
end
describe '.find_matching' do
it 'should find all triggers matching a wildcard' do
Backup::Model.find_matching('tri*_b').count.should be(2)
Backup::Model.find_matching('trigg*').count.should be(5)
end
it 'should return an empty array if no matches are found' do
Backup::Model.find_matching('foo*').should == []
end
end
end # describe 'finder methods'
describe '#initialize' do
it 'should convert trigger to a string' do
Backup::Model.new(:foo, :bar).trigger.should == 'foo'
end
it 'should convert label to a string' do
Backup::Model.new(:foo, :bar).label.should == 'bar'
end
it 'should set all procedure variables to an empty array' do
model.send(:procedure_instance_variables).each do |var|
model.instance_variable_get(var).should == []
end
end
it 'should accept and instance_eval a block' do
block = lambda {|model| throw(:instance, model) }
caught = catch(:instance) do
Backup::Model.new('gotcha', '', &block)
end
caught.trigger.should == 'gotcha'
end
it 'should add itself to Model.all' do
Backup::Model.all.should == [model]
end
end # describe '#initialize'
describe 'DSL Methods' do
module Fake
module NoArg
class Base
attr_accessor :block_arg
def initialize(&block)
instance_eval(&block) if block_given?
end
end
end
module OneArg
class Base
attr_accessor :arg1, :block_arg
def initialize(arg1, &block)
@arg1 = arg1
instance_eval(&block) if block_given?
end
end
end
module TwoArgs
class Base
attr_accessor :arg1, :arg2, :block_arg
def initialize(arg1, arg2, &block)
@arg1 = arg1
@arg2 = arg2
instance_eval(&block) if block_given?
end
end
end
end
# Set +const+ to +replacement+ for the calling block
def using_fake(const, replacement)
orig = Backup.const_get(const)
Backup.send(:remove_const, const)
Backup.const_set(const, replacement)
yield
Backup.send(:remove_const, const)
Backup.const_set(const, orig)
end
describe '#archive' do
it 'should add archives' do
using_fake('Archive', Fake::TwoArgs::Base) do
model.archive('foo') {|a| a.block_arg = :foo }
model.archive('bar') {|a| a.block_arg = :bar }
model.archives.count.should == 2
a1, a2 = model.archives
a1.arg1.should be(model)
a1.arg2.should == 'foo'
a1.block_arg.should == :foo
a2.arg1.should be(model)
a2.arg2.should == 'bar'
a2.block_arg.should == :bar
end
end
end
describe '#database' do
it 'should add databases' do
using_fake('Database', Fake::OneArg) do
model.database('Base') {|a| a.block_arg = :foo }
model.database('Base') {|a| a.block_arg = :bar }
model.databases.count.should be(2)
d1, d2 = model.databases
d1.arg1.should be(model)
d1.block_arg.should == :foo
d2.arg1.should be(model)
d2.block_arg.should == :bar
end
end
it 'should accept a nested class name' do
using_fake('Database', Fake) do
model.database('OneArg::Base')
model.databases.first.should be_an_instance_of Fake::OneArg::Base
end
end
end
describe '#store_with' do
it 'should add storages' do
using_fake('Storage', Fake::TwoArgs) do
model.store_with('Base', 'foo') {|a| a.block_arg = :foo }
model.store_with('Base', 'bar') {|a| a.block_arg = :bar }
model.storages.count.should be(2)
s1, s2 = model.storages
s1.arg1.should be(model)
s1.arg2.should == 'foo'
s1.block_arg.should == :foo
s2.arg1.should be(model)
s2.arg2.should == 'bar'
s2.block_arg.should == :bar
end
end
it 'should accept a nested class name' do
using_fake('Storage', Fake) do
model.store_with('TwoArgs::Base')
model.storages.first.should be_an_instance_of Fake::TwoArgs::Base
end
end
end
describe '#sync_with' do
it 'should add syncers' do
using_fake('Syncer', Fake::NoArg) do
model.sync_with('Base') {|a| a.block_arg = :foo }
model.sync_with('Base') {|a| a.block_arg = :bar }
model.syncers.count.should be(2)
s1, s2 = model.syncers
s1.block_arg.should == :foo
s2.block_arg.should == :bar
end
end
it 'should accept a nested class name' do
using_fake('Syncer', Fake) do
model.sync_with('NoArg::Base')
model.syncers.first.should be_an_instance_of Fake::NoArg::Base
end
end
it 'should warn user of change from RSync to RSync::Push' do
Backup::Logger.expects(:warn)
model.sync_with('Backup::Config::RSync')
model.syncers.first.should
be_an_instance_of Backup::Syncer::RSync::Push
end
it 'should warn user of change from S3 to Cloud::S3' do
Backup::Logger.expects(:warn)
model.sync_with('Backup::Config::S3')
model.syncers.first.should
be_an_instance_of Backup::Syncer::Cloud::S3
end
it 'should warn user of change from CloudFiles to Cloud::CloudFiles' do
Backup::Logger.expects(:warn)
model.sync_with('Backup::Config::CloudFiles')
model.syncers.first.should
be_an_instance_of Backup::Syncer::Cloud::CloudFiles
end
end
describe '#notify_by' do
it 'should add notifiers' do
using_fake('Notifier', Fake::OneArg) do
model.notify_by('Base') {|a| a.block_arg = :foo }
model.notify_by('Base') {|a| a.block_arg = :bar }
model.notifiers.count.should be(2)
n1, n2 = model.notifiers
n1.arg1.should be(model)
n1.block_arg.should == :foo
n2.arg1.should be(model)
n2.block_arg.should == :bar
end
end
it 'should accept a nested class name' do
using_fake('Notifier', Fake) do
model.notify_by('OneArg::Base')
model.notifiers.first.should be_an_instance_of Fake::OneArg::Base
end
end
end
describe '#encrypt_with' do
it 'should add an encryptor' do
using_fake('Encryptor', Fake::NoArg) do
model.encrypt_with('Base') {|a| a.block_arg = :foo }
model.encryptor.should be_an_instance_of Fake::NoArg::Base
model.encryptor.block_arg.should == :foo
end
end
it 'should accept a nested class name' do
using_fake('Encryptor', Fake) do
model.encrypt_with('NoArg::Base')
model.encryptor.should be_an_instance_of Fake::NoArg::Base
end
end
end
describe '#compress_with' do
it 'should add a compressor' do
using_fake('Compressor', Fake::NoArg) do
model.compress_with('Base') {|a| a.block_arg = :foo }
model.compressor.should be_an_instance_of Fake::NoArg::Base
model.compressor.block_arg.should == :foo
end
end
it 'should accept a nested class name' do
using_fake('Compressor', Fake) do
model.compress_with('NoArg::Base')
model.compressor.should be_an_instance_of Fake::NoArg::Base
end
end
end
describe '#split_into_chunks_of' do
it 'should add a splitter' do
using_fake('Splitter', Fake::TwoArgs::Base) do
model.split_into_chunks_of(123)
model.splitter.should be_an_instance_of Fake::TwoArgs::Base
model.splitter.arg1.should be(model)
model.splitter.arg2.should == 123
end
end
it 'should raise an error if chunk_size is not an Integer' do
expect do
model.split_into_chunks_of('345')
end.to raise_error {|err|
err.should be_an_instance_of Backup::Errors::Model::ConfigurationError
err.message.should match(/must be an Integer/)
}
end
end
end # describe 'DSL Methods'
describe '#prepare!' do
it 'should prepare for the backup' do
FileUtils.expects(:mkdir_p).with(
File.join(Backup::Config.data_path, 'test_trigger')
)
Backup::Cleaner.expects(:prepare).with(model)
model.prepare!
end
end
describe '#perform!' do
let(:procedure_a) { lambda {} }
let(:procedure_b) { mock }
let(:procedure_c) { mock }
let(:procedure_d) { lambda {} }
let(:procedure_e) { lambda {} }
let(:procedure_f) { mock }
let(:procedures) do
[ procedure_a, [procedure_b, procedure_c],
procedure_d, procedure_e, [procedure_f] ]
end
let(:syncer_a) { mock }
let(:syncer_b) { mock }
let(:syncers) { [syncer_a, syncer_b] }
let(:notifier_a) { mock }
let(:notifier_b) { mock }
let(:notifiers) { [notifier_a, notifier_b] }
it 'should set the @time and @started_at variables' do
Timecop.freeze(Time.now)
started_at = Time.now
time = started_at.strftime("%Y.%m.%d.%H.%M.%S")
model.expects(:log!).with(:started)
model.expects(:log!).with(:finished)
model.perform!
model.time.should == time
model.instance_variable_get(:@started_at).should == started_at
end
context 'when no errors occur' do
before do
model.expects(:procedures).returns(procedures)
model.expects(:syncers).returns(syncers)
model.expects(:notifiers).returns(notifiers)
end
context 'when databases are configured' do
before do
model.instance_variable_set(:@databases, [true])
end
it 'should perform all procedures' do
model.expects(:log!).in_sequence(s).with(:started)
procedure_a.expects(:call).in_sequence(s)
procedure_b.expects(:perform!).in_sequence(s)
procedure_c.expects(:perform!).in_sequence(s)
procedure_d.expects(:call).in_sequence(s)
procedure_e.expects(:call).in_sequence(s)
procedure_f.expects(:perform!).in_sequence(s)
syncer_a.expects(:perform!).in_sequence(s)
syncer_b.expects(:perform!).in_sequence(s)
notifier_a.expects(:perform!).in_sequence(s)
notifier_b.expects(:perform!).in_sequence(s)
model.expects(:log!).in_sequence(s).with(:finished)
model.perform!
end
end
context 'when archives are configured' do
before do
model.instance_variable_set(:@archives, [true])
end
it 'should perform all procedures' do
model.expects(:log!).in_sequence(s).with(:started)
procedure_a.expects(:call).in_sequence(s)
procedure_b.expects(:perform!).in_sequence(s)
procedure_c.expects(:perform!).in_sequence(s)
procedure_d.expects(:call).in_sequence(s)
procedure_e.expects(:call).in_sequence(s)
procedure_f.expects(:perform!).in_sequence(s)
syncer_a.expects(:perform!).in_sequence(s)
syncer_b.expects(:perform!).in_sequence(s)
notifier_a.expects(:perform!).in_sequence(s)
notifier_b.expects(:perform!).in_sequence(s)
model.expects(:log!).in_sequence(s).with(:finished)
model.perform!
end
end
end # context 'when no errors occur'
# for the purposes of testing the error handling, we're just going to
# stub the first thing this method calls and raise an error
context 'when errors occur' do
let(:error_a) { mock }
let(:error_b) { mock }
let(:notifier) { mock }
before do
error_a.stubs(:backtrace).returns(['many', 'backtrace', 'lines'])
end
it 'logs, notifies and continues if a StandardError is rescued' do
Time.stubs(:now).raises(StandardError, 'non-fatal error')
Backup::Errors::ModelError.expects(:wrap).in_sequence(s).with do |err, msg|
err.message.should == 'non-fatal error'
msg.should match(/Backup for test label \(test_trigger\) Failed!/)
end.returns(error_a)
Backup::Logger.expects(:error).in_sequence(s).with(error_a)
Backup::Logger.expects(:error).in_sequence(s).with(
"\nBacktrace:\n\s\smany\n\s\sbacktrace\n\s\slines\n\n"
)
Backup::Cleaner.expects(:warnings).in_sequence(s).with(model)
Backup::Errors::ModelError.expects(:new).in_sequence(s).with do |msg|
msg.should match(/Backup will now attempt to continue/)
end.returns(error_b)
Backup::Logger.expects(:message).in_sequence(s).with(error_b)
# notifiers called, but any Exception is ignored
notifier.expects(:perform!).with(true).raises(Exception)
model.expects(:notifiers).returns([notifier])
# returns to allow next trigger to run
expect { model.perform! }.not_to raise_error
end
it 'logs, notifies and exits if an Exception is rescued' do
Time.stubs(:now).raises(Exception, 'fatal error')
Backup::Errors::ModelError.expects(:wrap).in_sequence(s).with do |err, msg|
err.message.should == 'fatal error'
msg.should match(/Backup for test label \(test_trigger\) Failed!/)
end.returns(error_a)
Backup::Logger.expects(:error).in_sequence(s).with(error_a)
Backup::Logger.expects(:error).in_sequence(s).with(
"\nBacktrace:\n\s\smany\n\s\sbacktrace\n\s\slines\n\n"
)
Backup::Cleaner.expects(:warnings).in_sequence(s).with(model)
Backup::Errors::ModelError.expects(:new).in_sequence(s).with do |msg|
msg.should match(/Backup will now exit/)
end.returns(error_b)
Backup::Logger.expects(:error).in_sequence(s).with(error_b)
expect do
# notifiers called, but any Exception is ignored
notifier = mock
notifier.expects(:perform!).with(true).raises(Exception)
model.expects(:notifiers).returns([notifier])
end.not_to raise_error
expect do
model.perform!
end.to raise_error(SystemExit) {|exit| exit.status.should be(1) }
end
end # context 'when errors occur'
end # describe '#perform!'
describe '#package!' do
it 'should package the backup' do
Backup::Packager.expects(:package!).in_sequence(s).with(model)
Backup::Cleaner.expects(:remove_packaging).in_sequence(s).with(model)
model.send(:package!)
model.package.should be_an_instance_of Backup::Package
end
end
describe '#clean' do
it 'should remove the final packaged files' do
package = mock
model.instance_variable_set(:@package, package)
Backup::Cleaner.expects(:remove_package).with(package)
model.send(:clean!)
end
end
describe '#procedures' do
it 'should return an array of specific, ordered procedures' do
model.stubs(:databases).returns(:databases)
model.stubs(:archives).returns(:archives)
model.stubs(:package!).returns(:package)
model.stubs(:storages).returns(:storages)
model.stubs(:clean!).returns(:clean)
one, two, three, four, five = model.send(:procedures)
one.should == :databases
two.should == :archives
three.call.should == :package
four.should == :storages
five.call.should == :clean
end
end
describe '#procedure_instance_variables' do
# these are all set to an empty Array in #initialize
it 'should return an array of Array holding instance variables' do
model.send(:procedure_instance_variables).should ==
[:@databases, :@archives, :@storages, :@notifiers, :@syncers]
end
end
describe '#get_class_from_scope' do
module Fake
module TestScope
class TestKlass; end
end
end
module TestScope
module TestKlass; end
end
context 'when name is given as a string' do
it 'should return the constant for the given scope and name' do
model.send(
:get_class_from_scope,
Fake,
'TestScope'
).should == Fake::TestScope
end
it 'should accept a nested class name' do
model.send(
:get_class_from_scope,
Fake,
'TestScope::TestKlass'
).should == Fake::TestScope::TestKlass
end
end
context 'when name is given as a module' do
it 'should return the constant for the given scope and name' do
model.send(
:get_class_from_scope,
Fake,
TestScope
).should == Fake::TestScope
end
it 'should accept a nested class name' do
model.send(
:get_class_from_scope,
Fake,
TestScope::TestKlass
).should == Fake::TestScope::TestKlass
end
end
context 'when name is given as a module defined under Backup::Config' do
# this is necessary since the specs in spec/config_spec.rb
# remove all the constants from Backup::Config as part of those tests.
before(:all) do
module Backup::Config
module TestScope
module TestKlass; end
end
end
end
it 'should return the constant for the given scope and name' do
model.send(
:get_class_from_scope,
Fake,
Backup::Config::TestScope
).should == Fake::TestScope
end
it 'should accept a nested class name' do
model.send(
:get_class_from_scope,
Fake,
Backup::Config::TestScope::TestKlass
).should == Fake::TestScope::TestKlass
end
end
end # describe '#get_class_from_scope'
describe '#log!' do
context 'when action is :started' do
it 'should log that the backup has started with the version' do
Backup::Logger.expects(:message).with(
"Performing Backup for 'test label (test_trigger)'!\n" +
"[ backup #{ Backup::Version.current } : #{ RUBY_DESCRIPTION } ]"
)
model.send(:log!, :started)
end
end
context 'when action is :finished' do
before { model.expects(:elapsed_time).returns('01:02:03') }
context 'when warnings were issued' do
before { Backup::Logger.expects(:has_warnings?).returns(true) }
it 'should log a warning that the backup has finished with warnings' do
Backup::Logger.expects(:warn).with(
"Backup for 'test label (test_trigger)' " +
"Completed Successfully (with Warnings) in 01:02:03"
)
model.send(:log!, :finished)
end
end
context 'when no warnings were issued' do
it 'should log that the backup has finished with the elapsed time' do
Backup::Logger.expects(:message).with(
"Backup for 'test label (test_trigger)' " +
"Completed Successfully in 01:02:03"
)
model.send(:log!, :finished)
end
end
end
end # describe '#log!'
describe '#elapsed_time' do
it 'should return a string representing the elapsed time' do
Timecop.freeze(Time.now)
{ 0 => '00:00:00', 1 => '00:00:01', 59 => '00:00:59',
60 => '00:01:00', 61 => '00:01:01', 119 => '00:01:59',
3540 => '00:59:00', 3541 => '00:59:01', 3599 => '00:59:59',
3600 => '01:00:00', 3601 => '01:00:01', 3659 => '01:00:59',
3660 => '01:01:00', 3661 => '01:01:01', 3719 => '01:01:59',
7140 => '01:59:00', 7141 => '01:59:01', 7199 => '01:59:59',
212400 => '59:00:00', 212401 => '59:00:01', 212459 => '59:00:59',
212460 => '59:01:00', 212461 => '59:01:01', 212519 => '59:01:59',
215940 => '59:59:00', 215941 => '59:59:01', 215999 => '59:59:59'
}.each do |duration, expected|
model.instance_variable_set(:@started_at, Time.now - duration)
model.send(:elapsed_time).should == expected
end
end
end
end
Jump to Line
Something went wrong with that request. Please try again.