foreplay 0.7.6 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +28 -25
- data/.ruby-version +1 -1
- data/foreplay.gemspec +12 -14
- data/lib/foreplay/cli.rb +42 -43
- data/lib/foreplay/engine/remote.rb +121 -0
- data/lib/foreplay/engine/role.rb +23 -0
- data/lib/foreplay/engine/server.rb +123 -0
- data/lib/foreplay/engine/step.rb +52 -0
- data/lib/foreplay/engine/steps.yml +75 -0
- data/lib/foreplay/engine.rb +138 -0
- data/lib/foreplay/foreplay.rb +10 -0
- data/lib/foreplay/launcher.rb +13 -0
- data/lib/foreplay/setup.rb +24 -26
- data/lib/foreplay/version.rb +1 -1
- data/lib/foreplay.rb +7 -3
- data/lib/string.rb +17 -0
- data/spec/lib/foreplay/deploy_spec.rb +79 -24
- data/spec/lib/foreplay/engine_spec.rb +32 -0
- data/spec/lib/foreplay/setup_spec.rb +1 -1
- metadata +47 -60
- data/lib/foreplay/deploy.rb +0 -395
- data/lib/foreplay/utility.rb +0 -30
- data/spec/lib/foreplay/utility_spec.rb +0 -28
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'string'
|
3
|
+
|
4
|
+
class Foreplay::Engine
|
5
|
+
include Foreplay
|
6
|
+
attr_reader :environment, :filters, :mode
|
7
|
+
|
8
|
+
DEFAULTS_KEY = 'defaults'
|
9
|
+
CONFIG_FILE = "#{Dir.getwd}/config/foreplay.yml"
|
10
|
+
|
11
|
+
def initialize(e, f)
|
12
|
+
@environment = e
|
13
|
+
@filters = f
|
14
|
+
end
|
15
|
+
|
16
|
+
def deploy
|
17
|
+
@mode = :deploy
|
18
|
+
execute
|
19
|
+
end
|
20
|
+
|
21
|
+
def check
|
22
|
+
@mode = :check
|
23
|
+
execute
|
24
|
+
end
|
25
|
+
|
26
|
+
def execute
|
27
|
+
# Explain what we're going to do
|
28
|
+
puts "#{mode.capitalize}ing #{environment.dup.yellow} environment, "\
|
29
|
+
"#{explanatory_text(filters, 'role')}, #{explanatory_text(filters, 'server')}"
|
30
|
+
|
31
|
+
threads = []
|
32
|
+
|
33
|
+
roles.each do |role, additional_instructions|
|
34
|
+
next if role == DEFAULTS_KEY # 'defaults' is not a role
|
35
|
+
next if filters.key?('role') && filters['role'] != role
|
36
|
+
|
37
|
+
threads.concat Foreplay::Engine::Role.new(
|
38
|
+
environment,
|
39
|
+
mode,
|
40
|
+
build_instructions(role, additional_instructions)
|
41
|
+
).threads
|
42
|
+
end
|
43
|
+
|
44
|
+
threads.each(&:join)
|
45
|
+
|
46
|
+
puts mode == :deploy ? 'Finished deployment' : 'Deployment configuration check was successful'
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a new hash with +hash+ and +other_hash+ merged recursively, including arrays.
|
50
|
+
#
|
51
|
+
# h1 = { x: { y: [4,5,6] }, z: [7,8,9] }
|
52
|
+
# h2 = { x: { y: [7,8,9] }, z: 'xyz' }
|
53
|
+
# h1.supermerge(h2)
|
54
|
+
# #=> {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]}
|
55
|
+
def supermerge(hash, other_hash)
|
56
|
+
fail 'supermerge only works if you pass two hashes. '\
|
57
|
+
"You passed a #{hash.class} and a #{other_hash.class}." unless hash.is_a?(Hash) && other_hash.is_a?(Hash)
|
58
|
+
|
59
|
+
new_hash = hash.deep_dup.with_indifferent_access
|
60
|
+
|
61
|
+
other_hash.each_pair do |k, v|
|
62
|
+
tv = new_hash[k]
|
63
|
+
|
64
|
+
if tv.is_a?(Hash) && v.is_a?(Hash)
|
65
|
+
new_hash[k] = supermerge(tv, v)
|
66
|
+
elsif tv.is_a?(Array) || v.is_a?(Array)
|
67
|
+
new_hash[k] = Array.wrap(tv) + Array.wrap(v)
|
68
|
+
else
|
69
|
+
new_hash[k] = v
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
new_hash
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def explanatory_text(hsh, key)
|
79
|
+
hsh.key?(key) ? "#{hsh[key].dup.yellow} #{key}" : "all #{key.pluralize}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def build_instructions(role, additional_instructions)
|
83
|
+
instructions = supermerge(defaults, additional_instructions).symbolize_keys
|
84
|
+
instructions[:role] = role
|
85
|
+
required_keys = [:name, :environment, :role, :servers, :path, :repository]
|
86
|
+
|
87
|
+
required_keys.each do |key|
|
88
|
+
next if instructions.key? key
|
89
|
+
terminate("Required key #{key} not found in instructions for #{environment} environment.\nCheck #{CONFIG_FILE}")
|
90
|
+
end
|
91
|
+
|
92
|
+
# Apply server filter
|
93
|
+
instructions[:servers] &= server_filter if server_filter
|
94
|
+
instructions
|
95
|
+
end
|
96
|
+
|
97
|
+
def server_filter
|
98
|
+
@server_filter ||= filters['server'].split(',') if filters.key?('server')
|
99
|
+
end
|
100
|
+
|
101
|
+
def defaults
|
102
|
+
return @defaults if @defaults
|
103
|
+
|
104
|
+
# Establish defaults
|
105
|
+
# First the default defaults
|
106
|
+
@defaults = {
|
107
|
+
name: File.basename(Dir.getwd),
|
108
|
+
environment: environment,
|
109
|
+
env: { 'RAILS_ENV' => environment },
|
110
|
+
port: 50_000
|
111
|
+
}
|
112
|
+
|
113
|
+
@defaults = supermerge(@defaults, roles_all[DEFAULTS_KEY]) if roles_all.key? DEFAULTS_KEY
|
114
|
+
@defaults = supermerge(@defaults, roles[DEFAULTS_KEY]) if roles.key? DEFAULTS_KEY
|
115
|
+
end
|
116
|
+
|
117
|
+
def roles
|
118
|
+
@roles ||= roles_all[environment]
|
119
|
+
end
|
120
|
+
|
121
|
+
def roles_all
|
122
|
+
return @roles_all if @roles_all
|
123
|
+
|
124
|
+
@roles_all = YAML.load(File.read(CONFIG_FILE))
|
125
|
+
|
126
|
+
# This environment
|
127
|
+
unless @roles_all.key? environment
|
128
|
+
terminate("No deployment configuration defined for #{environment} environment.\nCheck #{CONFIG_FILE}")
|
129
|
+
end
|
130
|
+
|
131
|
+
@roles_all
|
132
|
+
rescue Errno::ENOENT
|
133
|
+
terminate "Can't find configuration file #{CONFIG_FILE}.\nPlease run foreplay setup or create the file manually."
|
134
|
+
rescue Psych::SyntaxError
|
135
|
+
terminate "I don't understand the configuration file #{CONFIG_FILE}.\n"\
|
136
|
+
'Please run foreplay setup or edit the file manually.'
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'thor/group'
|
2
|
+
|
3
|
+
class Foreplay::Launcher < Thor::Group
|
4
|
+
include Thor::Actions
|
5
|
+
|
6
|
+
argument :mode, type: :string, required: true
|
7
|
+
argument :environment, type: :string, required: true
|
8
|
+
argument :filters, type: :hash, required: false
|
9
|
+
|
10
|
+
def parse
|
11
|
+
Foreplay::Engine.new(environment, filters).__send__ mode
|
12
|
+
end
|
13
|
+
end
|
data/lib/foreplay/setup.rb
CHANGED
@@ -1,33 +1,31 @@
|
|
1
1
|
require 'thor/group'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
include Thor::Actions
|
3
|
+
class Foreplay::Setup < Thor::Group
|
4
|
+
include Thor::Actions
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
6
|
+
class_option :name, aliases: '-n', default: File.basename(Dir.getwd)
|
7
|
+
class_option :repository, aliases: '-r'
|
8
|
+
class_option :user, aliases: '-u'
|
9
|
+
class_option :password
|
10
|
+
class_option :keyfile
|
11
|
+
class_option :private_key, aliases: '-k'
|
12
|
+
class_option :port, aliases: '-p', default: 50_000
|
13
|
+
class_option :path, aliases: '-f'
|
14
|
+
class_option :servers, aliases: '-s', type: :array
|
15
|
+
class_option :db_adapter, aliases: '-a', default: 'postgresql'
|
16
|
+
class_option :db_encoding, aliases: '-e', default: 'utf8'
|
17
|
+
class_option :db_pool, default: 5
|
18
|
+
class_option :db_name, aliases: '-d'
|
19
|
+
class_option :db_host, aliases: '-h'
|
20
|
+
class_option :db_user
|
21
|
+
class_option :db_password
|
22
|
+
class_option :resque_redis
|
24
23
|
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
def self.source_root
|
25
|
+
File.dirname(__FILE__)
|
26
|
+
end
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
end
|
28
|
+
def config
|
29
|
+
template('setup/foreplay.yml', 'config/foreplay.yml')
|
32
30
|
end
|
33
31
|
end
|
data/lib/foreplay/version.rb
CHANGED
data/lib/foreplay.rb
CHANGED
@@ -1,4 +1,8 @@
|
|
1
1
|
require 'foreplay/version'
|
2
|
-
require 'foreplay/
|
3
|
-
require 'foreplay/
|
4
|
-
require 'foreplay/
|
2
|
+
require 'foreplay/foreplay'
|
3
|
+
require 'foreplay/launcher'
|
4
|
+
require 'foreplay/engine'
|
5
|
+
require 'foreplay/engine/remote'
|
6
|
+
require 'foreplay/engine/role'
|
7
|
+
require 'foreplay/engine/server'
|
8
|
+
require 'foreplay/engine/step'
|
data/lib/string.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Some useful additions to the String class
|
2
|
+
class String
|
3
|
+
colors = %w(black red green yellow blue magenta cyan white)
|
4
|
+
|
5
|
+
colors.each_with_index do |fg_color, i|
|
6
|
+
fg = 30 + i
|
7
|
+
define_method(fg_color) { ansi_attributes(fg) }
|
8
|
+
|
9
|
+
colors.each_with_index do |bg_color, j|
|
10
|
+
define_method("#{fg_color}_on_#{bg_color}") { ansi_attributes(fg, 40 + j) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def ansi_attributes(*args)
|
15
|
+
"\e[#{args.join(';')}m#{self}\e[0m"
|
16
|
+
end
|
17
|
+
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'foreplay'
|
3
2
|
require 'net/ssh/shell'
|
3
|
+
require 'foreplay'
|
4
4
|
|
5
|
-
describe Foreplay::
|
5
|
+
describe Foreplay::Launcher do
|
6
6
|
let(:session) { double(Net::SSH::Connection::Session) }
|
7
7
|
let(:shell) { double(Net::SSH::Shell) }
|
8
8
|
let(:process) { double(Net::SSH::Shell::Process) }
|
@@ -10,7 +10,13 @@ describe Foreplay::Deploy do
|
|
10
10
|
before :each do
|
11
11
|
# Setup foreplay
|
12
12
|
`rm -f config/foreplay.yml`
|
13
|
-
|
13
|
+
command = 'foreplay setup '\
|
14
|
+
'-r git@github.com:Xenapto/foreplay.git '\
|
15
|
+
'-s web1.example.com web2.example.com '\
|
16
|
+
'-f apps/%a '\
|
17
|
+
'-u fred '\
|
18
|
+
'--password trollope'
|
19
|
+
`#{command}`
|
14
20
|
|
15
21
|
# Stub all the things
|
16
22
|
allow(Net::SSH).to receive(:start).and_return(session)
|
@@ -24,37 +30,68 @@ describe Foreplay::Deploy do
|
|
24
30
|
|
25
31
|
it "complains on check if there's no config file" do
|
26
32
|
`rm -f config/foreplay.yml`
|
27
|
-
expect { Foreplay::
|
33
|
+
expect { Foreplay::Launcher.start([:check, 'production', '']) }
|
28
34
|
.to raise_error(RuntimeError, /.*Please run foreplay setup or create the file manually.*/)
|
29
35
|
end
|
30
36
|
|
31
37
|
it "complains on deploy if there's no config file" do
|
32
38
|
`rm -f config/foreplay.yml`
|
33
|
-
expect { Foreplay::
|
39
|
+
expect { Foreplay::Launcher.start([:deploy, 'production', '']) }
|
34
40
|
.to raise_error(RuntimeError, /.*Please run foreplay setup or create the file manually.*/)
|
35
41
|
end
|
36
42
|
|
43
|
+
it 'complains on check if the config file is not valid YAML' do
|
44
|
+
`echo %*:*: > config/foreplay.yml`
|
45
|
+
expect { Foreplay::Launcher.start([:check, 'production', '']) }
|
46
|
+
.to raise_error(RuntimeError, /.*Please run foreplay setup or edit the file manually.*/)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'complains on deploy if the config file is not valid YAML' do
|
50
|
+
`echo %*:*: > config/foreplay.yml`
|
51
|
+
expect { Foreplay::Launcher.start([:deploy, 'production', '']) }
|
52
|
+
.to raise_error(RuntimeError, /.*Please run foreplay setup or edit the file manually.*/)
|
53
|
+
end
|
54
|
+
|
37
55
|
it 'complains if there are no authentication methods defined in the config file' do
|
56
|
+
command = 'foreplay setup '\
|
57
|
+
'-r git@github.com:Xenapto/foreplay.git '\
|
58
|
+
'-s web.example.com '\
|
59
|
+
'-f apps/%a '\
|
60
|
+
'-u fred '\
|
61
|
+
'--password ""'
|
62
|
+
|
63
|
+
match = 'No authentication methods supplied. '\
|
64
|
+
'You must supply a private key, key file or password in the configuration file.'
|
65
|
+
|
38
66
|
`rm -f config/foreplay.yml`
|
39
|
-
`
|
40
|
-
|
67
|
+
`#{command}`
|
68
|
+
|
69
|
+
expect { Foreplay::Launcher.start([:deploy, 'production', '']) }
|
41
70
|
.to raise_error(
|
42
71
|
RuntimeError,
|
43
|
-
|
72
|
+
/.*#{Regexp.quote(match)}*/
|
44
73
|
)
|
45
74
|
end
|
46
75
|
|
47
76
|
it "complains if the private keyfile defined in the config file doesn't exist" do
|
77
|
+
command = 'foreplay setup '\
|
78
|
+
'-r git@github.com:Xenapto/foreplay.git '\
|
79
|
+
'-s web.example.com '\
|
80
|
+
'-f apps/%a '\
|
81
|
+
'-u fred '\
|
82
|
+
'--keyfile "/home/fred/no-such-file"'
|
83
|
+
|
48
84
|
`rm -f config/foreplay.yml`
|
49
|
-
`
|
50
|
-
|
85
|
+
`#{command}`
|
86
|
+
|
87
|
+
expect { Foreplay::Launcher.start([:deploy, 'production', '']) }
|
51
88
|
.to raise_error(Errno::ENOENT, %r{.*No such file or directory @ rb_sysopen - /home/fred/no-such-file.*})
|
52
89
|
end
|
53
90
|
|
54
91
|
it 'complains if a mandatory key is missing from the config file' do
|
55
92
|
`sed -i 's/path:/pxth:/' config/foreplay.yml`
|
56
93
|
|
57
|
-
expect { Foreplay::
|
94
|
+
expect { Foreplay::Launcher.start([:deploy, 'production', '']) }
|
58
95
|
.to raise_error(
|
59
96
|
RuntimeError,
|
60
97
|
/.*Required key path not found in instructions for production environment.*/
|
@@ -62,7 +99,7 @@ describe Foreplay::Deploy do
|
|
62
99
|
end
|
63
100
|
|
64
101
|
it 'complains if we try to deploy an environment that isn\'t defined' do
|
65
|
-
expect { Foreplay::
|
102
|
+
expect { Foreplay::Launcher.start([:deploy, 'unknown', '']) }
|
66
103
|
.to raise_error(
|
67
104
|
RuntimeError,
|
68
105
|
/.*No deployment configuration defined for unknown environment.*/
|
@@ -71,18 +108,18 @@ describe Foreplay::Deploy do
|
|
71
108
|
|
72
109
|
it 'terminates if a remote process exits with a non-zero status' do
|
73
110
|
allow(process).to receive(:exit_status).and_return(1)
|
74
|
-
expect { Foreplay::
|
111
|
+
expect { Foreplay::Launcher.start([:deploy, 'production', '']) }.to raise_error(RuntimeError, /.*output message.*/)
|
75
112
|
end
|
76
113
|
|
77
114
|
it "terminates if a connection can't be established with the remote server" do
|
78
115
|
allow(Net::SSH).to receive(:start).and_call_original
|
79
|
-
expect { Foreplay::
|
116
|
+
expect { Foreplay::Launcher.start([:deploy, 'production', '']) }
|
80
117
|
.to raise_error(RuntimeError, /.*There was a problem starting an ssh session on web1.example.com.*/)
|
81
118
|
end
|
82
119
|
|
83
120
|
it 'checks the config' do
|
84
121
|
expect($stdout).to receive(:puts).at_least(:once)
|
85
|
-
Foreplay::
|
122
|
+
Foreplay::Launcher.start [:check, 'production', '']
|
86
123
|
end
|
87
124
|
|
88
125
|
it 'deploys to the environment' do
|
@@ -93,7 +130,9 @@ describe Foreplay::Deploy do
|
|
93
130
|
.and_return(session)
|
94
131
|
|
95
132
|
[
|
96
|
-
|
133
|
+
"echo Foreplay version #{Foreplay::VERSION}",
|
134
|
+
'mkdir -p apps/foreplay && cd apps/foreplay && rm -rf 50000 && '\
|
135
|
+
'git clone -b master git@github.com:Xenapto/foreplay.git 50000',
|
97
136
|
'rvm rvmrc trust 50000',
|
98
137
|
'rvm rvmrc warning ignore 50000',
|
99
138
|
'cd 50000 && mkdir -p tmp doc log config',
|
@@ -113,11 +152,13 @@ describe Foreplay::Deploy do
|
|
113
152
|
'echo " password: TODO Put here the database user\'s password" >> config/database.yml',
|
114
153
|
'if [ -d ../cache/vendor/bundle/bundle ] ; then rm -rf ../cache/vendor/bundle/bundle'\
|
115
154
|
' ; else echo No evidence of legacy copy bug ; fi',
|
116
|
-
'if [ -d ../cache/vendor/bundle ] ; then
|
155
|
+
'if [ -d ../cache/vendor/bundle ] ; then '\
|
156
|
+
'rsync -aW --no-compress --delete --info=STATS1 ../cache/vendor/bundle/ vendor/bundle'\
|
117
157
|
' ; else echo No bundle to restore ; fi',
|
118
158
|
'sudo ln -f `which bundle` /usr/bin/bundle || echo Using default version of bundle',
|
119
159
|
'bundle install --deployment --clean --jobs 2 --without development test',
|
120
|
-
'mkdir -p ../cache/vendor &&
|
160
|
+
'mkdir -p ../cache/vendor && '\
|
161
|
+
'rsync -aW --no-compress --delete --info=STATS1 vendor/bundle/ ../cache/vendor/bundle',
|
121
162
|
'if [ -f public/assets/manifest.yml ] ; then echo "Not precompiling assets"'\
|
122
163
|
' ; else RAILS_ENV=production bundle exec foreman run rake assets:precompile ; fi',
|
123
164
|
'sudo bundle exec foreman export upstart /etc/init',
|
@@ -135,24 +176,38 @@ describe Foreplay::Deploy do
|
|
135
176
|
expect(shell).to receive(:execute).with(command).and_return(process)
|
136
177
|
end
|
137
178
|
|
138
|
-
Foreplay::
|
179
|
+
Foreplay::Launcher.start [:deploy, 'production', '']
|
139
180
|
end
|
140
181
|
|
141
182
|
it "uses another port if there's already an installed instance" do
|
142
183
|
allow(process).to receive(:on_output).and_yield(process, "50000\n")
|
143
184
|
expect(shell).to receive(:execute).with('echo 51000 > $HOME/.foreplay/foreplay/current_port').and_return(process)
|
144
|
-
Foreplay::
|
185
|
+
Foreplay::Launcher.start [:deploy, 'production', '']
|
145
186
|
end
|
146
187
|
|
147
188
|
it 'uses the private key provided in the config file' do
|
189
|
+
command = 'foreplay setup '\
|
190
|
+
'-r git@github.com:Xenapto/foreplay.git '\
|
191
|
+
'-s web.example.com '\
|
192
|
+
'-f apps/%a '\
|
193
|
+
'-u fred '\
|
194
|
+
'-k "top secret private key"'
|
195
|
+
|
148
196
|
`rm -f config/foreplay.yml`
|
149
|
-
`
|
150
|
-
Foreplay::
|
197
|
+
`#{command}`
|
198
|
+
Foreplay::Launcher.start([:deploy, 'production', ''])
|
151
199
|
end
|
152
200
|
|
153
201
|
it 'adds Redis details for Resque' do
|
202
|
+
command = 'foreplay setup '\
|
203
|
+
'-r git@github.com:Xenapto/foreplay.git '\
|
204
|
+
'-s web.example.com '\
|
205
|
+
'-f apps/%a '\
|
206
|
+
'-u fred '\
|
207
|
+
'--resque-redis "redis://localhost:6379"'
|
208
|
+
|
154
209
|
`rm -f config/foreplay.yml`
|
155
|
-
`
|
156
|
-
Foreplay::
|
210
|
+
`#{command}`
|
211
|
+
Foreplay::Launcher.start([:deploy, 'production', ''])
|
157
212
|
end
|
158
213
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'foreplay'
|
3
|
+
|
4
|
+
describe Foreplay::Engine do
|
5
|
+
context '#supermerge' do
|
6
|
+
let(:engine) { Foreplay::Engine.new('b', 'b') }
|
7
|
+
|
8
|
+
it 'should complain unless two hashes are passed to it' do
|
9
|
+
expect { engine.supermerge('x', 'y') }.to raise_error(RuntimeError)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should merge two simple hashes' do
|
13
|
+
expect(engine.supermerge({ a: 'x' }, b: 'y')).to eq('a' => 'x', 'b' => 'y')
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should merge two hashes both with arrays at the same key' do
|
17
|
+
expect(engine.supermerge({ a: ['x'] }, a: ['y'])).to eq('a' => %w(x y))
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should merge an array and a value at the same key' do
|
21
|
+
expect(engine.supermerge({ a: 'x' }, a: ['y'])).to eq('a' => %w(x y))
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should replace a value at the same key' do
|
25
|
+
expect(engine.supermerge({ a: 'x' }, a: 'y')).to eq('a' => 'y')
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should merge two subhashes at the same key' do
|
29
|
+
expect(engine.supermerge({ a: { b: 'x' } }, a: { c: 'y' })).to eq('a' => { 'b' => 'x', 'c' => 'y' })
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|