Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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.