r10k 3.9.1 → 3.11.0

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec_tests.yml +1 -1
  3. data/.travis.yml +0 -10
  4. data/CHANGELOG.mkd +28 -0
  5. data/README.mkd +6 -0
  6. data/doc/dynamic-environments/configuration.mkd +21 -0
  7. data/doc/puppetfile.mkd +15 -1
  8. data/integration/Rakefile +3 -1
  9. data/integration/tests/user_scenario/basic_workflow/negative/neg_specify_deleted_forge_module.rb +3 -9
  10. data/integration/tests/user_scenario/basic_workflow/single_env_purge_unmanaged_modules.rb +21 -25
  11. data/integration/tests/user_scenario/complex_workflow/multi_env_add_change_remove.rb +3 -3
  12. data/integration/tests/user_scenario/complex_workflow/multi_env_remove_re-add.rb +3 -3
  13. data/integration/tests/user_scenario/complex_workflow/multi_env_unamanaged.rb +3 -3
  14. data/lib/r10k/action/base.rb +6 -3
  15. data/lib/r10k/action/deploy/display.rb +6 -3
  16. data/lib/r10k/action/deploy/environment.rb +15 -4
  17. data/lib/r10k/action/deploy/module.rb +37 -8
  18. data/lib/r10k/action/runner.rb +45 -10
  19. data/lib/r10k/cli/deploy.rb +4 -0
  20. data/lib/r10k/git.rb +3 -0
  21. data/lib/r10k/git/cache.rb +1 -1
  22. data/lib/r10k/git/rugged/credentials.rb +77 -0
  23. data/lib/r10k/git/stateful_repository.rb +1 -0
  24. data/lib/r10k/initializers.rb +10 -0
  25. data/lib/r10k/module/base.rb +37 -0
  26. data/lib/r10k/module/forge.rb +7 -2
  27. data/lib/r10k/module/git.rb +2 -1
  28. data/lib/r10k/module/svn.rb +2 -1
  29. data/lib/r10k/module_loader/puppetfile.rb +206 -0
  30. data/lib/r10k/module_loader/puppetfile/dsl.rb +37 -0
  31. data/lib/r10k/puppetfile.rb +83 -160
  32. data/lib/r10k/settings.rb +47 -2
  33. data/lib/r10k/settings/definition.rb +1 -1
  34. data/lib/r10k/source/base.rb +10 -0
  35. data/lib/r10k/source/git.rb +5 -0
  36. data/lib/r10k/source/svn.rb +4 -0
  37. data/lib/r10k/util/purgeable.rb +70 -8
  38. data/lib/r10k/version.rb +1 -1
  39. data/locales/r10k.pot +129 -57
  40. data/r10k.gemspec +2 -0
  41. data/spec/fixtures/unit/action/r10k_forge_auth.yaml +4 -0
  42. data/spec/fixtures/unit/action/r10k_forge_auth_no_url.yaml +3 -0
  43. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/managed_subdir_2/ignored_1 +0 -0
  44. data/spec/fixtures/unit/util/purgeable/managed_two/.hidden/unmanaged_3 +0 -0
  45. data/spec/unit/action/deploy/environment_spec.rb +25 -0
  46. data/spec/unit/action/deploy/module_spec.rb +216 -14
  47. data/spec/unit/action/runner_spec.rb +129 -25
  48. data/spec/unit/git/cache_spec.rb +14 -0
  49. data/spec/unit/git/rugged/credentials_spec.rb +29 -0
  50. data/spec/unit/git/stateful_repository_spec.rb +5 -0
  51. data/spec/unit/module/base_spec.rb +46 -0
  52. data/spec/unit/module/forge_spec.rb +27 -1
  53. data/spec/unit/module/git_spec.rb +17 -8
  54. data/spec/unit/module/svn_spec.rb +18 -0
  55. data/spec/unit/module_loader/puppetfile_spec.rb +343 -0
  56. data/spec/unit/module_spec.rb +28 -0
  57. data/spec/unit/puppetfile_spec.rb +127 -191
  58. data/spec/unit/settings_spec.rb +24 -2
  59. data/spec/unit/util/purgeable_spec.rb +38 -6
  60. metadata +23 -2
@@ -62,4 +62,18 @@ describe R10K::Git::Cache do
62
62
  subject.cached?
63
63
  end
64
64
  end
65
+
66
+ describe "dirname sanitization" do
67
+ it 'sanitizes cache directory name' do
68
+ expect(subject.sanitized_dirname).to eq('git---some-git-remote')
69
+ end
70
+
71
+ context 'with username and password' do
72
+ subject { subclass.new('https://"user:pa$$w0rd:@some/git/remote') }
73
+
74
+ it 'sanitizes cache directory name' do
75
+ expect(subject.sanitized_dirname).to eq('https---some-git-remote')
76
+ end
77
+ end
78
+ end
65
79
  end
@@ -79,6 +79,35 @@ describe R10K::Git::Rugged::Credentials, :unless => R10K::Util::Platform.jruby?
79
79
  end
80
80
  end
81
81
 
82
+ describe "generating github app tokens" do
83
+ it 'errors if app id has invalid characters' do
84
+ expect { subject.github_app_token("123A567890", "fake", "300")
85
+ }.to raise_error(R10K::Git::GitError, /App id contains invalid characters/)
86
+ end
87
+ it 'errors if app ttl has invalid characters' do
88
+ expect { subject.github_app_token("123456", "fake", "abc")
89
+ }.to raise_error(R10K::Git::GitError, /Github App token ttl contains/)
90
+ end
91
+ it 'errors if private file does not exist' do
92
+ R10K::Git.settings[:github_app_key] = "/missing/token/file"
93
+ expect(File).to receive(:readable?).with(R10K::Git.settings[:github_app_key]).and_return false
94
+ expect {
95
+ subject.github_app_token("123456", R10K::Git.settings[:github_app_key], "300")
96
+ }.to raise_error(R10K::Git::GitError, /App key is missing or unreadable/)
97
+ end
98
+ it 'errors if file is not a valid SSL key' do
99
+ token_file = Tempfile.new('token')
100
+ token_file.write('my_token')
101
+ token_file.close
102
+ R10K::Git.settings[:github_app_key] = token_file.path
103
+ expect(File).to receive(:readable?).with(token_file.path).and_return true
104
+ expect {
105
+ subject.github_app_token("123456", R10K::Git.settings[:github_app_key], "300")
106
+ }.to raise_error(R10K::Git::GitError, /App key is not a valid SSL key/)
107
+ token_file.unlink
108
+ end
109
+ end
110
+
82
111
  describe "generating token credentials" do
83
112
  it 'errors if token file does not exist' do
84
113
  R10K::Git.settings[:oauth_token] = "/missing/token/file"
@@ -19,6 +19,11 @@ describe R10K::Git::StatefulRepository do
19
19
  expect(subject.sync_cache?(ref)).to eq true
20
20
  end
21
21
 
22
+ it "is true if the ref is HEAD" do
23
+ expect(cache).to receive(:exist?).and_return true
24
+ expect(subject.sync_cache?('HEAD')).to eq true
25
+ end
26
+
22
27
  it "is true if the ref is unresolvable" do
23
28
  expect(cache).to receive(:exist?).and_return true
24
29
  expect(cache).to receive(:ref_type).with('0.9.x').and_return(:unknown)
@@ -28,6 +28,52 @@ describe R10K::Module::Base do
28
28
  end
29
29
  end
30
30
 
31
+ describe 'deleting the spec dir' do
32
+ let(:module_org) { "coolorg" }
33
+ let(:module_name) { "coolmod" }
34
+ let(:title) { "#{module_org}-#{module_name}" }
35
+ let(:dirname) { Pathname.new(Dir.mktmpdir) }
36
+ let(:spec_path) { dirname + module_name + 'spec' }
37
+
38
+ before(:each) do
39
+ logger = double("logger")
40
+ allow_any_instance_of(described_class).to receive(:logger).and_return(logger)
41
+ allow(logger).to receive(:debug2).with(any_args)
42
+ allow(logger).to receive(:info).with(any_args)
43
+ end
44
+
45
+ it 'does not remove the spec directory by default' do
46
+ FileUtils.mkdir_p(spec_path)
47
+ m = described_class.new(title, dirname, {})
48
+ m.maybe_delete_spec_dir
49
+ expect(Dir.exist?(spec_path)).to eq true
50
+ end
51
+
52
+ it 'detects a symlink and deletes the target' do
53
+ Dir.mkdir(dirname + module_name)
54
+ target_dir = Dir.mktmpdir
55
+ FileUtils.ln_s(target_dir, spec_path)
56
+ m = described_class.new(title, dirname, {exclude_spec: true})
57
+ m.maybe_delete_spec_dir
58
+ expect(Dir.exist?(target_dir)).to eq false
59
+ end
60
+
61
+ it 'removes the spec directory if exclude_spec is set' do
62
+ FileUtils.mkdir_p(spec_path)
63
+ m = described_class.new(title, dirname, {exclude_spec: true})
64
+ m.maybe_delete_spec_dir
65
+ expect(Dir.exist?(spec_path)).to eq false
66
+ end
67
+
68
+ it 'does not remove the spec directory if spec_deletable is false' do
69
+ FileUtils.mkdir_p(spec_path)
70
+ m = described_class.new(title, dirname, {})
71
+ m.spec_deletable = false
72
+ m.maybe_delete_spec_dir
73
+ expect(Dir.exist?(spec_path)).to eq true
74
+ end
75
+ end
76
+
31
77
  describe "path variables" do
32
78
  it "uses the module name as the name" do
33
79
  m = described_class.new('eight_hundred', '/moduledir', [])
@@ -78,6 +78,7 @@ describe R10K::Module::Forge do
78
78
  allow_any_instance_of(described_class).to receive(:logger).and_return(logger_dbl)
79
79
 
80
80
  allow(logger_dbl).to receive(:info).with(/Deploying module to.*/)
81
+ allow(logger_dbl).to receive(:debug2).with(/No spec dir detected/)
81
82
  expect(logger_dbl).to receive(:warn).with(/puppet forge module.*puppetlabs-corosync.*has been deprecated/i)
82
83
 
83
84
  subject.sync
@@ -90,6 +91,7 @@ describe R10K::Module::Forge do
90
91
  allow_any_instance_of(described_class).to receive(:logger).and_return(logger_dbl)
91
92
 
92
93
  allow(logger_dbl).to receive(:info).with(/Deploying module to.*/)
94
+ allow(logger_dbl).to receive(:debug2).with(/No spec dir detected/)
93
95
  expect(logger_dbl).to_not receive(:warn).with(/puppet forge module.*puppetlabs-corosync.*has been deprecated/i)
94
96
 
95
97
  subject.sync
@@ -104,9 +106,16 @@ describe R10K::Module::Forge do
104
106
 
105
107
  it "uses the latest version from the forge when the version is :latest" do
106
108
  subject = described_class.new('branan/eight_hundred', fixture_modulepath, { version: :latest })
107
- expect(subject.v3_module).to receive_message_chain(:current_release, :version).and_return('8.8.8')
109
+ release = double("Module Release", version: '8.8.8')
110
+ expect(subject.v3_module).to receive(:current_release).and_return(release).twice
108
111
  expect(subject.expected_version).to eq '8.8.8'
109
112
  end
113
+
114
+ it "throws when there are no available versions" do
115
+ subject = described_class.new('branan/eight_hundred', fixture_modulepath, { version: :latest })
116
+ expect(subject.v3_module).to receive(:current_release).and_return(nil)
117
+ expect { subject.expected_version }.to raise_error(PuppetForge::ReleaseNotFound)
118
+ end
110
119
  end
111
120
 
112
121
  describe "determining the status" do
@@ -158,6 +167,23 @@ describe R10K::Module::Forge do
158
167
  describe "#sync" do
159
168
  subject { described_class.new('branan/eight_hundred', fixture_modulepath, { version: '8.0.0' }) }
160
169
 
170
+ context "syncing the repo" do
171
+ let(:module_org) { "coolorg" }
172
+ let(:module_name) { "coolmod" }
173
+ let(:title) { "#{module_org}-#{module_name}" }
174
+ let(:dirname) { Pathname.new(Dir.mktmpdir) }
175
+ let(:spec_path) { dirname + module_name + 'spec' }
176
+ subject { described_class.new(title, dirname, {}) }
177
+
178
+ it 'defaults to keeping the spec dir' do
179
+ FileUtils.mkdir_p(spec_path)
180
+ expect(subject).to receive(:status).and_return(:absent)
181
+ expect(subject).to receive(:install)
182
+ subject.sync
183
+ expect(Dir.exist?(spec_path)).to eq true
184
+ end
185
+ end
186
+
161
187
  it 'does nothing when the module is in sync' do
162
188
  allow(subject).to receive(:status).and_return :insync
163
189
 
@@ -89,6 +89,23 @@ describe R10K::Module::Git do
89
89
  end
90
90
  end
91
91
 
92
+ describe 'syncing the repo' do
93
+ let(:module_org) { "coolorg" }
94
+ let(:module_name) { "coolmod" }
95
+ let(:title) { "#{module_org}-#{module_name}" }
96
+ let(:dirname) { Pathname.new(Dir.mktmpdir) }
97
+ let(:spec_path) { dirname + module_name + 'spec' }
98
+ subject { described_class.new(title, dirname, {}) }
99
+
100
+ it 'defaults to keeping the spec dir' do
101
+ FileUtils.mkdir_p(spec_path)
102
+ allow(mock_repo).to receive(:resolve).with('master').and_return('abc123')
103
+ allow(mock_repo).to receive(:sync)
104
+ subject.sync
105
+ expect(Dir.exist?(spec_path)).to eq true
106
+ end
107
+ end
108
+
92
109
  describe "determining the status" do
93
110
  subject do
94
111
  described_class.new(
@@ -119,14 +136,6 @@ describe R10K::Module::Git do
119
136
  allow(mock_repo).to receive(:head).and_return('abc123')
120
137
  end
121
138
 
122
- context "when option is unrecognized" do
123
- let(:opts) { { unrecognized: true } }
124
-
125
- it "raises an error" do
126
- expect { test_module(opts) }.to raise_error(ArgumentError, /cannot handle option 'unrecognized'/)
127
- end
128
- end
129
-
130
139
  describe "desired ref" do
131
140
  context "when no desired ref is given" do
132
141
  it "defaults to master" do
@@ -119,6 +119,24 @@ describe R10K::Module::SVN do
119
119
  end
120
120
  end
121
121
 
122
+ describe 'the default spec dir' do
123
+ let(:module_org) { "coolorg" }
124
+ let(:module_name) { "coolmod" }
125
+ let(:title) { "#{module_org}-#{module_name}" }
126
+ let(:dirname) { Pathname.new(Dir.mktmpdir) }
127
+ let(:spec_path) { dirname + module_name + 'spec' }
128
+ subject { described_class.new(title, dirname, {}) }
129
+
130
+ it 'is kept by default' do
131
+
132
+ FileUtils.mkdir_p(spec_path)
133
+ expect(subject).to receive(:status).and_return(:absent)
134
+ expect(subject).to receive(:install).and_return(nil)
135
+ subject.sync
136
+ expect(Dir.exist?(spec_path)).to eq true
137
+ end
138
+ end
139
+
122
140
  describe "synchronizing" do
123
141
 
124
142
  subject { described_class.new('foo', '/moduledir', :svn => 'https://github.com/adrienthebo/r10k-fixture-repo', :rev => 123) }
@@ -0,0 +1,343 @@
1
+ require 'spec_helper'
2
+ require 'r10k/module_loader/puppetfile'
3
+
4
+ describe R10K::ModuleLoader::Puppetfile do
5
+ describe 'initial parameters' do
6
+ describe 'honor' do
7
+ let(:options) do
8
+ {
9
+ basedir: '/test/basedir/env',
10
+ forge: 'localforge.internal.corp',
11
+ overrides: { modules: { deploy_modules: true } },
12
+ environment: R10K::Environment::Git.new('env',
13
+ '/test/basedir/',
14
+ 'env',
15
+ { remote: 'git://foo/remote',
16
+ ref: 'env' })
17
+ }
18
+ end
19
+
20
+ subject { R10K::ModuleLoader::Puppetfile.new(**options) }
21
+
22
+ describe 'the moduledir' do
23
+ it 'respects absolute paths' do
24
+ absolute_options = options.merge({moduledir: '/opt/puppetlabs/special/modules'})
25
+ puppetfile = R10K::ModuleLoader::Puppetfile.new(**absolute_options)
26
+ expect(puppetfile.instance_variable_get(:@moduledir)).to eq('/opt/puppetlabs/special/modules')
27
+ end
28
+
29
+ it 'roots the moduledir in the basepath if a relative path is specified' do
30
+ relative_options = options.merge({moduledir: 'my/special/modules'})
31
+ puppetfile = R10K::ModuleLoader::Puppetfile.new(**relative_options)
32
+ expect(puppetfile.instance_variable_get(:@moduledir)).to eq('/test/basedir/env/my/special/modules')
33
+ end
34
+ end
35
+
36
+ describe 'the Puppetfile' do
37
+ it 'respects absolute paths' do
38
+ absolute_options = options.merge({puppetfile: '/opt/puppetlabs/special/Puppetfile'})
39
+ puppetfile = R10K::ModuleLoader::Puppetfile.new(**absolute_options)
40
+ expect(puppetfile.instance_variable_get(:@puppetfile_path)).to eq('/opt/puppetlabs/special/Puppetfile')
41
+ end
42
+
43
+ it 'roots the Puppetfile in the basepath if a relative path is specified' do
44
+ relative_options = options.merge({puppetfile: 'Puppetfile.global'})
45
+ puppetfile = R10K::ModuleLoader::Puppetfile.new(**relative_options)
46
+ expect(puppetfile.instance_variable_get(:@puppetfile_path)).to eq('/test/basedir/env/Puppetfile.global')
47
+ end
48
+ end
49
+
50
+ it 'the forge' do
51
+ expect(subject.instance_variable_get(:@forge)).to eq('localforge.internal.corp')
52
+ end
53
+
54
+ it 'the overrides' do
55
+ expect(subject.instance_variable_get(:@overrides)).to eq({ modules: { deploy_modules: true }})
56
+ end
57
+
58
+ it 'the environment' do
59
+ expect(subject.instance_variable_get(:@environment).name).to eq('env')
60
+ end
61
+ end
62
+
63
+ describe 'sane defaults' do
64
+ subject { R10K::ModuleLoader::Puppetfile.new(basedir: '/test/basedir') }
65
+
66
+ it 'has a moduledir rooted in the basedir' do
67
+ expect(subject.instance_variable_get(:@moduledir)).to eq('/test/basedir/modules')
68
+ end
69
+
70
+ it 'has a Puppetfile rooted in the basedir' do
71
+ expect(subject.instance_variable_get(:@puppetfile_path)).to eq('/test/basedir/Puppetfile')
72
+ end
73
+
74
+ it 'uses the public forge' do
75
+ expect(subject.instance_variable_get(:@forge)).to eq('forgeapi.puppetlabs.com')
76
+ end
77
+
78
+ it 'creates an empty overrides' do
79
+ expect(subject.instance_variable_get(:@overrides)).to eq({})
80
+ end
81
+
82
+ it 'does not require an environment' do
83
+ expect(subject.instance_variable_get(:@environment)).to eq(nil)
84
+ end
85
+ end
86
+ end
87
+
88
+ describe 'adding modules' do
89
+ let(:basedir) { '/test/basedir' }
90
+
91
+ subject { R10K::ModuleLoader::Puppetfile.new(basedir: basedir) }
92
+
93
+ it 'should transform Forge modules with a string arg to have a version key' do
94
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', subject.moduledir, hash_including(version: '1.2.3'), anything).and_call_original
95
+
96
+ expect { subject.add_module('puppet/test_module', '1.2.3') }.to change { subject.modules }
97
+ expect(subject.modules.collect(&:name)).to include('test_module')
98
+ end
99
+
100
+ it 'should not accept Forge modules with a version comparison' do
101
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', subject.moduledir, hash_including(version: '< 1.2.0'), anything).and_call_original
102
+
103
+ expect {
104
+ subject.add_module('puppet/test_module', '< 1.2.0')
105
+ }.to raise_error(RuntimeError, /module puppet\/test_module.*doesn't have an implementation/i)
106
+
107
+ expect(subject.modules.collect(&:name)).not_to include('test_module')
108
+ end
109
+
110
+ it 'should set :spec_deletable to true for modules in the basedir' do
111
+ module_opts = { git: 'git@example.com:puppet/test_module.git' }
112
+ subject.add_module('puppet/test_module', module_opts)
113
+ expect(subject.modules[0].spec_deletable).to be true
114
+ end
115
+
116
+ it 'should set :spec_deletable to false for modules outside the basedir' do
117
+ module_opts = { git: 'git@example.com:puppet/test_module.git', install_path: 'some/path' }
118
+ subject.add_module('puppet/test_module', module_opts)
119
+ expect(subject.modules[0].spec_deletable).to be false
120
+ end
121
+
122
+ it 'should accept non-Forge modules with a hash arg' do
123
+ module_opts = { git: 'git@example.com:puppet/test_module.git' }
124
+
125
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', subject.moduledir, module_opts, anything).and_call_original
126
+
127
+ expect { subject.add_module('puppet/test_module', module_opts) }.to change { subject.modules }
128
+ expect(subject.modules.collect(&:name)).to include('test_module')
129
+ end
130
+
131
+ it 'should accept non-Forge modules with a valid relative :install_path option' do
132
+ module_opts = {
133
+ install_path: 'vendor',
134
+ git: 'git@example.com:puppet/test_module.git',
135
+ }
136
+
137
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', File.join(basedir, 'vendor'), module_opts, anything).and_call_original
138
+
139
+ expect { subject.add_module('puppet/test_module', module_opts) }.to change { subject.modules }
140
+ expect(subject.modules.collect(&:name)).to include('test_module')
141
+ end
142
+
143
+ it 'should accept non-Forge modules with a valid absolute :install_path option' do
144
+ install_path = File.join(basedir, 'vendor')
145
+
146
+ module_opts = {
147
+ install_path: install_path,
148
+ git: 'git@example.com:puppet/test_module.git',
149
+ }
150
+
151
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', install_path, module_opts, anything).and_call_original
152
+
153
+ expect { subject.add_module('puppet/test_module', module_opts) }.to change { subject.modules }
154
+ expect(subject.modules.collect(&:name)).to include('test_module')
155
+ end
156
+
157
+ it 'should reject non-Forge modules with an invalid relative :install_path option' do
158
+ module_opts = {
159
+ install_path: '../../vendor',
160
+ git: 'git@example.com:puppet/test_module.git',
161
+ }
162
+
163
+ expect { subject.add_module('puppet/test_module', module_opts) }.to raise_error(R10K::Error, /cannot manage content.*is not within/i).and not_change { subject.modules }
164
+ end
165
+
166
+ it 'should reject non-Forge modules with an invalid absolute :install_path option' do
167
+ module_opts = {
168
+ install_path: '/tmp/mydata/vendor',
169
+ git: 'git@example.com:puppet/test_module.git',
170
+ }
171
+
172
+ expect { subject.add_module('puppet/test_module', module_opts) }.to raise_error(R10K::Error, /cannot manage content.*is not within/i).and not_change { subject.modules }
173
+ end
174
+
175
+ it 'should disable and not add modules that conflict with the environment' do
176
+ env = instance_double('R10K::Environment::Base')
177
+ mod = instance_double('R10K::Module::Base', name: 'conflict', origin: :puppetfile, 'origin=': nil)
178
+ loader = R10K::ModuleLoader::Puppetfile.new(basedir: basedir, environment: env)
179
+ allow(env).to receive(:'module_conflicts?').with(mod).and_return(true)
180
+ allow(mod).to receive(:spec_deletable=)
181
+
182
+ expect(R10K::Module).to receive(:new).with('conflict', anything, anything, anything).and_return(mod)
183
+ expect { loader.add_module('conflict', {}) }.not_to change { loader.modules }
184
+ end
185
+ end
186
+
187
+ describe '#purge_exclusions' do
188
+ let(:managed_dirs) { ['dir1', 'dir2'] }
189
+ subject { R10K::ModuleLoader::Puppetfile.new(basedir: '/test/basedir') }
190
+
191
+ it 'includes managed_directories' do
192
+ expect(subject.send(:determine_purge_exclusions, managed_dirs)).to match_array(managed_dirs)
193
+ end
194
+
195
+ context 'when belonging to an environment' do
196
+ let(:env_contents) { ['env1', 'env2' ] }
197
+ let(:env) { double(:environment, desired_contents: env_contents) }
198
+
199
+ subject { R10K::ModuleLoader::Puppetfile.new(basedir: '/test/basedir', environment: env) }
200
+
201
+ it "includes environment's desired_contents" do
202
+ expect(subject.send(:determine_purge_exclusions, managed_dirs)).to match_array(managed_dirs + env_contents)
203
+ end
204
+ end
205
+ end
206
+
207
+ describe '#managed_directories' do
208
+
209
+ let(:basedir) { '/test/basedir' }
210
+ subject { R10K::ModuleLoader::Puppetfile.new(basedir: basedir) }
211
+
212
+ before do
213
+ allow(subject).to receive(:puppetfile_content).and_return('')
214
+ end
215
+
216
+ it 'returns an array of paths that #purge! will operate within' do
217
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', subject.moduledir, hash_including(version: '1.2.3'), anything).and_call_original
218
+ subject.add_module('puppet/test_module', '1.2.3')
219
+ subject.load
220
+
221
+ expect(subject.modules.length).to be 1
222
+ expect(subject.managed_directories).to match_array([subject.moduledir])
223
+ end
224
+
225
+ context "with a module with install_path == ''" do
226
+ it "basedir isn't in the list of paths to purge" do
227
+ module_opts = { install_path: '', git: 'git@example.com:puppet/test_module.git' }
228
+
229
+ expect(R10K::Module).to receive(:new).with('puppet/test_module', basedir, module_opts, anything).and_call_original
230
+ subject.add_module('puppet/test_module', module_opts)
231
+ subject.load
232
+
233
+ expect(subject.modules.length).to be 1
234
+ expect(subject.managed_directories).to be_empty
235
+ end
236
+ end
237
+ end
238
+
239
+ describe 'evaluating a Puppetfile' do
240
+ def expect_wrapped_error(error, pf_path, error_type)
241
+ expect(error).to be_a_kind_of(R10K::Error)
242
+ expect(error.message).to eq("Failed to evaluate #{pf_path}")
243
+ expect(error.original).to be_a_kind_of(error_type)
244
+ end
245
+
246
+ subject { described_class.new(basedir: @path) }
247
+
248
+ it 'wraps and re-raises syntax errors' do
249
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'invalid-syntax')
250
+ pf_path = File.join(@path, 'Puppetfile')
251
+ expect {
252
+ subject.load
253
+ }.to raise_error do |e|
254
+ expect_wrapped_error(e, pf_path, SyntaxError)
255
+ end
256
+ end
257
+
258
+ it 'wraps and re-raises load errors' do
259
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'load-error')
260
+ pf_path = File.join(@path, 'Puppetfile')
261
+ expect {
262
+ subject.load
263
+ }.to raise_error do |e|
264
+ expect_wrapped_error(e, pf_path, LoadError)
265
+ end
266
+ end
267
+
268
+ it 'wraps and re-raises argument errors' do
269
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'argument-error')
270
+ pf_path = File.join(@path, 'Puppetfile')
271
+ expect {
272
+ subject.load
273
+ }.to raise_error do |e|
274
+ expect_wrapped_error(e, pf_path, ArgumentError)
275
+ end
276
+ end
277
+
278
+ it 'rejects Puppetfiles with duplicate module names' do
279
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'duplicate-module-error')
280
+ pf_path = File.join(@path, 'Puppetfile')
281
+ expect {
282
+ subject.load
283
+ }.to raise_error(R10K::Error, /Puppetfiles cannot contain duplicate module names/i)
284
+ end
285
+
286
+ it 'wraps and re-raises name errors' do
287
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'name-error')
288
+ pf_path = File.join(@path, 'Puppetfile')
289
+ expect {
290
+ subject.load
291
+ }.to raise_error do |e|
292
+ expect_wrapped_error(e, pf_path, NameError)
293
+ end
294
+ end
295
+
296
+ it 'accepts a forge module with a version' do
297
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'valid-forge-with-version')
298
+ pf_path = File.join(@path, 'Puppetfile')
299
+ expect { subject.load }.not_to raise_error
300
+ end
301
+
302
+ describe 'setting a custom moduledir' do
303
+ it 'allows setting an absolute moduledir' do
304
+ @path = '/fake/basedir'
305
+ allow(subject).to receive(:puppetfile_content).and_return('moduledir "/fake/moduledir"')
306
+ subject.load
307
+ expect(subject.instance_variable_get(:@moduledir)).to eq('/fake/moduledir')
308
+ end
309
+
310
+ it 'roots relative moduledirs in the basedir' do
311
+ @path = '/fake/basedir'
312
+ allow(subject).to receive(:puppetfile_content).and_return('moduledir "my/moduledir"')
313
+ subject.load
314
+ expect(subject.instance_variable_get(:@moduledir)).to eq(File.join(@path, 'my/moduledir'))
315
+ end
316
+ end
317
+
318
+ it 'accepts a forge module without a version' do
319
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'valid-forge-without-version')
320
+ pf_path = File.join(@path, 'Puppetfile')
321
+ expect { subject.load }.not_to raise_error
322
+ end
323
+
324
+ it 'creates a git module and applies the default branch specified in the Puppetfile' do
325
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'default-branch-override')
326
+ pf_path = File.join(@path, 'Puppetfile')
327
+ expect { subject.load }.not_to raise_error
328
+ git_module = subject.modules[0]
329
+ expect(git_module.default_ref).to eq 'here_lies_the_default_branch'
330
+ end
331
+
332
+ it 'creates a git module and applies the provided default_branch_override' do
333
+ @path = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'unit', 'puppetfile', 'default-branch-override')
334
+ pf_path = File.join(@path, 'Puppetfile')
335
+ default_branch_override = 'default_branch_override_name'
336
+ subject.default_branch_override = default_branch_override
337
+ expect { subject.load }.not_to raise_error
338
+ git_module = subject.modules[0]
339
+ expect(git_module.default_override_ref).to eq default_branch_override
340
+ expect(git_module.default_ref).to eq 'here_lies_the_default_branch'
341
+ end
342
+ end
343
+ end