duple 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+
3
+ describe Duple::CLI::Structure do
4
+ include Duple::CLISpecHelpers
5
+
6
+ def invoke_structure(options = nil)
7
+ invoke_cli(:structure, options)
8
+ end
9
+
10
+ context 'from heroku to local' do
11
+ before {
12
+ stub_fetch_config
13
+ stub_dump_structure
14
+ stub_reset_local
15
+ stub_restore_structure
16
+ }
17
+
18
+ let(:source) { 'stage' }
19
+ let(:target) { 'development' }
20
+
21
+ it 'fetches the source credentials' do
22
+ runner.should_receive(:capture).with("heroku config -a duple-stage")
23
+ .and_return(heroku_config_response)
24
+
25
+ invoke_structure
26
+ end
27
+
28
+ it 'dumps the structure from the source' do
29
+ runner.should_receive(:run)
30
+ .with(%{PGPASSWORD="pg-pass" pg_dump -Fc --no-acl -O -s -h pg-host -U pg-user -p 6022 pg-db > tmp/duple/stage-structure.dump})
31
+
32
+ invoke_structure
33
+ end
34
+
35
+ it 'resets the local database' do
36
+ runner.should_receive(:run).with(%{bundle exec rake db:drop db:create})
37
+
38
+ invoke_structure
39
+ end
40
+
41
+ it 'loads the structure into the local database' do
42
+ runner.should_receive(:run)
43
+ .with(%{PGPASSWORD="" pg_restore -v --no-acl -O -s -h localhost -U postgres -p 5432 -d duple_development < tmp/duple/stage-structure.dump})
44
+
45
+ invoke_structure
46
+ end
47
+ end
48
+
49
+ context 'from heroku to heroku' do
50
+ before {
51
+ stub_fetch_config
52
+ stub_dump_structure
53
+ stub_reset_heroku
54
+ stub_restore_structure
55
+ }
56
+
57
+ let(:source) { 'production' }
58
+ let(:target) { 'stage' }
59
+
60
+ it 'fetches the source credentials' do
61
+ runner.should_receive(:capture).with("heroku config -a duple-production")
62
+ .and_return(heroku_config_response)
63
+
64
+ invoke_structure
65
+ end
66
+
67
+ it 'fetches the target credentials' do
68
+ runner.should_receive(:capture).with("heroku config -a duple-stage")
69
+ .and_return(heroku_config_response)
70
+
71
+ invoke_structure
72
+ end
73
+
74
+ it 'dumps the structure from the source' do
75
+ runner.should_receive(:run)
76
+ .with(%{PGPASSWORD="pg-pass" pg_dump -Fc --no-acl -O -s -h pg-host -U pg-user -p 6022 pg-db > tmp/duple/production-structure.dump})
77
+
78
+ invoke_structure
79
+ end
80
+
81
+ it 'resets the target database' do
82
+ runner.should_receive(:run).with(%{heroku pg:reset -a duple-stage})
83
+
84
+ invoke_structure
85
+ end
86
+
87
+ it 'loads the structure into the target database' do
88
+ runner.should_receive(:run)
89
+ .with(%{PGPASSWORD="pg-pass" pg_restore -v --no-acl -O -s -h pg-host -U pg-user -p 6022 -d pg-db < tmp/duple/production-structure.dump})
90
+
91
+ invoke_structure
92
+ end
93
+ end
94
+
95
+ context 'from local to heroku' do
96
+ before {
97
+ stub_fetch_config
98
+ stub_dump_structure
99
+ stub_reset_heroku
100
+ stub_restore_structure
101
+ }
102
+
103
+ let(:source) { 'development' }
104
+ let(:target) { 'stage' }
105
+
106
+ it 'fetches the target credentials' do
107
+ runner.should_receive(:capture).with("heroku config -a duple-stage")
108
+ .and_return(heroku_config_response)
109
+
110
+ invoke_structure
111
+ end
112
+
113
+ it 'dumps the structure from the local database' do
114
+ runner.should_receive(:run)
115
+ .with(%{PGPASSWORD="" pg_dump -Fc --no-acl -O -s -h localhost -U postgres -p 5432 duple_development > tmp/duple/development-structure.dump})
116
+
117
+ invoke_structure
118
+ end
119
+
120
+ it 'resets the target database' do
121
+ runner.should_receive(:run).with(%{heroku pg:reset -a duple-stage})
122
+
123
+ invoke_structure
124
+ end
125
+
126
+ it 'loads the structure into the target database' do
127
+ runner.should_receive(:run)
128
+ .with(%{PGPASSWORD="pg-pass" pg_restore -v --no-acl -O -s -h pg-host -U pg-user -p 6022 -d pg-db < tmp/duple/development-structure.dump})
129
+
130
+ invoke_structure
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,166 @@
1
+ require 'spec_helper'
2
+
3
+ describe Duple::Configuration do
4
+
5
+ describe '#excluded_tables' do
6
+ let(:config_hash) { YAML.load(File.read('spec/config/groups.yml'))}
7
+
8
+ context 'with neither tables nor group options' do
9
+ it 'returns an empty array' do
10
+ config = Duple::Configuration.new(config_hash, {})
11
+ config.excluded_tables.should == []
12
+ end
13
+ end
14
+
15
+ context 'with the group option' do
16
+ it 'returns the tables specified by the group' do
17
+ config = Duple::Configuration.new(config_hash, {group: 'no_comments'})
18
+ config.excluded_tables.should == ['comments']
19
+ end
20
+
21
+ context 'with an include_all group' do
22
+ it 'returns the tables specified by the group' do
23
+ config = Duple::Configuration.new(config_hash, {group: 'all_but_comments'})
24
+ config.excluded_tables.should == ['comments']
25
+ end
26
+ end
27
+ end
28
+
29
+ context 'with both tables and group options' do
30
+ it 'does not exclude tables in the tables option' do
31
+ config = Duple::Configuration.new(config_hash, {
32
+ group: 'no_comments',
33
+ tables: ['comments']
34
+ })
35
+ config.included_tables.should == ['comments']
36
+ config.excluded_tables.should == []
37
+ end
38
+
39
+ context 'with an include_all group' do
40
+ it 'does not exclude tables in the tables option' do
41
+ config = Duple::Configuration.new(config_hash, {
42
+ group: 'all_but_comments',
43
+ tables: ['comments']
44
+ })
45
+ config.included_tables.should == []
46
+ config.excluded_tables.should == []
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ describe '#included_tables' do
53
+ let(:config_hash) { YAML.load(File.read('spec/config/groups.yml'))}
54
+
55
+ context 'with neither tables nor group options' do
56
+ it 'returns an empty array' do
57
+ config = Duple::Configuration.new(config_hash, {})
58
+ config.included_tables.should == []
59
+ end
60
+ end
61
+
62
+ context 'with the tables option' do
63
+ it 'returns the tables specified in the option value' do
64
+ config = Duple::Configuration.new(config_hash, {tables: ['categories']})
65
+ config.included_tables.should == ['categories']
66
+ end
67
+ end
68
+
69
+ context 'with the group option' do
70
+ it 'returns the tables specified by the group' do
71
+ config = Duple::Configuration.new(config_hash, {group: 'minimal'})
72
+ config.included_tables.should == ['categories', 'links']
73
+ end
74
+
75
+ context 'with an include_all group' do
76
+ it 'returns an empty array' do
77
+ config = Duple::Configuration.new(config_hash, {group: 'all'})
78
+ config.included_tables.should == []
79
+ end
80
+ end
81
+ end
82
+
83
+ context 'with both tables and group options' do
84
+ it 'returns all tables in the group and the option value' do
85
+ config = Duple::Configuration.new(config_hash, {
86
+ group: 'minimal',
87
+ tables: ['posts', 'comments']
88
+ })
89
+ config.included_tables.should == ['categories', 'comments', 'links', 'posts']
90
+ end
91
+
92
+ context 'with an include_all group' do
93
+ it 'returns an empty array' do
94
+ config = Duple::Configuration.new(config_hash, {
95
+ group: 'all',
96
+ tables: ['posts', 'comments']
97
+ })
98
+ config.included_tables.should == []
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ describe '#source_environment' do
105
+ let(:config_hash) { YAML.load(File.read('spec/config/simple.yml'))}
106
+
107
+ it 'gets the default source environment' do
108
+ config = Duple::Configuration.new(config_hash, {})
109
+ config.source_environment.should_not be_nil
110
+ config.source_environment['appname'].should == 'duple-stage'
111
+ end
112
+
113
+ it 'gets the source environment' do
114
+ config = Duple::Configuration.new(config_hash, { source: 'production' })
115
+ config.source_environment.should_not be_nil
116
+ config.source_environment['appname'].should == 'duple-production'
117
+ end
118
+
119
+ it 'does not allow multiple default sources' do
120
+ config_hash['environments']['backstage'] = {'default_source' => true}
121
+ config = Duple::Configuration.new(config_hash, {})
122
+ expect {
123
+ config.source_environment
124
+ }.to raise_error(ArgumentError, 'Only a single environment can be default_source.')
125
+ end
126
+ end
127
+
128
+ describe '#target_environment' do
129
+ let(:config_hash) { YAML.load(File.read('spec/config/simple.yml'))}
130
+
131
+ it 'gets the default target environment' do
132
+ config = Duple::Configuration.new(config_hash, {})
133
+ config.target_environment.should_not be_nil
134
+ config.target_environment['type'].should == 'local'
135
+ end
136
+
137
+ it 'gets the target environment' do
138
+ config = Duple::Configuration.new(config_hash, { target: 'stage' })
139
+ config.target_environment.should_not be_nil
140
+ config.target_environment['appname'].should == 'duple-stage'
141
+ end
142
+
143
+ it 'fails if the target is not allowed' do
144
+ config = Duple::Configuration.new(config_hash, { target: 'production' })
145
+ expect {
146
+ config.target_environment
147
+ }.to raise_error(ArgumentError, 'Invalid target: production is not allowed to be a target.')
148
+ end
149
+
150
+ it 'does not allow multiple default targets' do
151
+ config_hash['environments']['backstage'] = {'default_target' => true}
152
+ config = Duple::Configuration.new(config_hash, {})
153
+ expect {
154
+ config.target_environment
155
+ }.to raise_error(ArgumentError, 'Only a single environment can be default_target.')
156
+ end
157
+
158
+ it 'allows multiple disallowed targets' do
159
+ config_hash['environments']['reporting'] = {'allow_target' => false}
160
+ config = Duple::Configuration.new(config_hash, {})
161
+ expect {
162
+ config.target_environment
163
+ }.to_not raise_error(ArgumentError, 'Only a single environment can be allow_target.')
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+ require 'fileutils'
3
+
4
+ describe Duple::Runner do
5
+
6
+ let(:test_dir) { 'tmp' }
7
+ let(:test_file) { "#{test_dir}/spawnbag.txt" }
8
+ let(:log) { StringIO.new }
9
+ after { log.close }
10
+
11
+ before { FileUtils.mkdir_p(test_dir) }
12
+ after { FileUtils.rm(test_file) if File.exists?(test_file) }
13
+
14
+ context 'in live mode' do
15
+ let(:runner) { Duple::Runner.new(log: log) }
16
+
17
+ it 'executes a command' do
18
+ runner.run("touch #{test_file}")
19
+
20
+ Pathname.new(test_file).should exist
21
+ end
22
+
23
+ it 'captures the output of the command' do
24
+ result = runner.capture("cat #{__FILE__}")
25
+ result.should =~ /captures the output of the command/
26
+ end
27
+
28
+ it 'writes the commands to the log' do
29
+ runner.run("touch #{test_file}")
30
+ log.string.should == " * Running: touch tmp/spawnbag.txt\n"
31
+ end
32
+
33
+ it 'raises an error if the command fails' do
34
+ expect {
35
+ runner.capture("cat NOT_A_REAL_FILE > /dev/null 2>&1")
36
+ }.to raise_error(RuntimeError, /Command failed: pid \d+ exit 1/)
37
+ end
38
+ end
39
+
40
+ context 'in dry run mode' do
41
+ let(:runner) { Duple::Runner.new(dry_run: true, log: log) }
42
+
43
+ it 'does not actually run the commands' do
44
+ runner.run("touch #{test_file}")
45
+
46
+ Pathname.new(test_file).should_not exist
47
+ end
48
+
49
+ it 'writes the commands to the log' do
50
+ runner.run("touch #{test_file}")
51
+ log.string.should == " * Running: touch tmp/spawnbag.txt\n"
52
+ end
53
+ end
54
+
55
+ context 'with a recorder' do
56
+ let(:recorder) { StringIO.new }
57
+ let(:runner) { Duple::Runner.new(recorder: recorder, log: log) }
58
+
59
+ it 'logs commands to a file' do
60
+ runner.capture('ls tmp')
61
+ runner.capture('date')
62
+ recorder.string.split("\n").should == ['ls tmp', 'date']
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,18 @@
1
+ lib_path = File.expand_path('../../lib', __FILE__)
2
+ $:.unshift(lib_path)
3
+
4
+ require 'simplecov'
5
+ require 'duple'
6
+ Dir['spec/support/**/*.rb'].each { |f| require File.expand_path(f) }
7
+
8
+ RSpec.configure do |config|
9
+ config.treat_symbols_as_metadata_keys_with_true_values = true
10
+ config.run_all_when_everything_filtered = true
11
+ config.filter_run :focus
12
+
13
+ # Run specs in random order to surface order dependencies. If you find an
14
+ # order dependency and want to debug it, you can fix the order by providing
15
+ # the seed, which is printed after each run.
16
+ # --seed 1234
17
+ config.order = 'random'
18
+ end
@@ -0,0 +1,85 @@
1
+ module Duple
2
+ module CLISpecHelpers
3
+ def self.included(base)
4
+ base.send(:let, :runner) { double_runner }
5
+ base.send(:let, :source) { 'stage' }
6
+ base.send(:let, :target) { 'development' }
7
+ base.send(:let, :snapshot_path) { 'tmp/duple/stage-2012-10-19-03-09-30.dump' }
8
+ end
9
+
10
+ def double_runner
11
+ runner = fire_double('Duple::Runner')
12
+ Duple::Runner.stub(:new).and_return(runner)
13
+ runner
14
+ end
15
+
16
+ def heroku_pgbackups_url_response
17
+ File.read('spec/config/heroku_pgbackups_url.txt')
18
+ end
19
+
20
+ def heroku_pgbackups_response
21
+ File.read('spec/config/heroku_pgbackups.txt')
22
+ end
23
+
24
+ def heroku_config_response
25
+ File.read('spec/config/heroku_config.txt')
26
+ end
27
+
28
+ def stub_fetch_url
29
+ runner.stub(:capture).with(/heroku pgbackups:url/)
30
+ .and_return(heroku_pgbackups_url_response)
31
+ end
32
+
33
+ def stub_fetch_config
34
+ runner.stub(:capture).with(/heroku config/)
35
+ .and_return(heroku_config_response)
36
+ end
37
+
38
+ def stub_fetch_backups
39
+ runner.stub(:capture).with(/heroku pgbackups /)
40
+ .and_return(heroku_pgbackups_response)
41
+ end
42
+
43
+ def stub_download_snapshot
44
+ runner.stub(:run).with(/curl/)
45
+ end
46
+
47
+ def stub_dump_structure
48
+ runner.stub(:run).with(/pg_dump .* -s /)
49
+ end
50
+
51
+ def stub_restore_structure
52
+ runner.stub(:run).with(/pg_restore .* -s /)
53
+ end
54
+
55
+ def stub_dump_data
56
+ runner.stub(:run).with(/pg_dump .* -a /)
57
+ end
58
+
59
+ def stub_restore_url
60
+ runner.stub(:run).with(/heroku pgbackups:restore .* -a /)
61
+ end
62
+
63
+ def stub_restore_data
64
+ runner.stub(:run).with(/pg_restore .* -a /)
65
+ end
66
+
67
+ def stub_reset_heroku
68
+ runner.stub(:run).with(/heroku pg:reset/)
69
+ end
70
+
71
+ def stub_reset_local
72
+ runner.stub(:run).with(/bundle exec rake db:drop db:create/)
73
+ end
74
+
75
+ def invoke_cli(command, options = nil)
76
+ options ||= {}
77
+ script = Duple::CLI::Root.new
78
+ script.invoke(command, [], {
79
+ config: 'spec/config/simple.yml',
80
+ source: source,
81
+ target: target
82
+ }.merge(options))
83
+ end
84
+ end
85
+ end