puppetlabs-onceover 5.0.3 → 5.0.4

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.rubocop_todo.yml +35 -15
  4. data/CHANGELOG.md +387 -3
  5. data/Gemfile +1 -0
  6. data/lib/puppetlabs-onceover/cli/plugins.rb +3 -3
  7. data/lib/puppetlabs-onceover/controlrepo.rb +8 -7
  8. data/lib/puppetlabs-onceover/version.rb +1 -1
  9. data/spec/fixtures/controlrepos/deploy_test/Puppetfile +3 -0
  10. data/spec/fixtures/controlrepos/deploy_test/a_dir/nested_file.txt +1 -0
  11. data/spec/fixtures/controlrepos/deploy_test/a_symlink +1 -0
  12. data/spec/fixtures/controlrepos/deploy_test/environment.conf +1 -0
  13. data/spec/fixtures/controlrepos/deploy_test/regular_file.txt +1 -0
  14. data/spec/puppetlabs-onceover/beaker_spec.rb +293 -0
  15. data/spec/puppetlabs-onceover/class_spec.rb +89 -0
  16. data/spec/puppetlabs-onceover/cli/run_spec.rb +80 -0
  17. data/spec/puppetlabs-onceover/cli/show_spec.rb +67 -0
  18. data/spec/puppetlabs-onceover/cli_spec.rb +79 -0
  19. data/spec/puppetlabs-onceover/controlrepo_spec.rb +787 -5
  20. data/spec/puppetlabs-onceover/deploy_spec.rb +246 -0
  21. data/spec/puppetlabs-onceover/group_spec.rb +121 -0
  22. data/spec/puppetlabs-onceover/logger_spec.rb +30 -0
  23. data/spec/puppetlabs-onceover/node_spec.rb +124 -0
  24. data/spec/puppetlabs-onceover/rake_tasks_spec.rb +173 -0
  25. data/spec/puppetlabs-onceover/rspec/formatters_spec.rb +361 -0
  26. data/spec/puppetlabs-onceover/runner_spec.rb +307 -0
  27. data/spec/puppetlabs-onceover/test_spec.rb +177 -0
  28. data/spec/puppetlabs-onceover/testconfig_spec.rb +451 -0
  29. data/spec/puppetlabs-onceover/vendored_modules_spec.rb +302 -0
  30. data/spec/puppetlabs-onceover/version_spec.rb +10 -0
  31. data/spec/spec_helper.rb +8 -0
  32. metadata +22 -1
@@ -0,0 +1,173 @@
1
+ require 'spec_helper'
2
+ require 'rake'
3
+ require 'ostruct'
4
+
5
+ describe 'rake_tasks.rb' do
6
+ # SimpleCov/Coverage tracks a given file path's hit-lines against the ISeq
7
+ # produced by the FIRST time it's compiled in this process. Re-`load`ing
8
+ # the file fresh in a before(:each) (once per example) makes every example
9
+ # after the first exercise a *new*, uninstrumented compilation, so real
10
+ # task-body coverage silently reads back as 0. Instead we `load` the file
11
+ # exactly once for the whole group, and use `Rake::Task#reenable` before
12
+ # each invoke so a task can be invoked again in a later example.
13
+ rake_tasks_path = File.expand_path('../../lib/puppetlabs-onceover/rake_tasks.rb', __dir__)
14
+ Rake.application = Rake::Application.new
15
+ load rake_tasks_path
16
+
17
+ before(:each) do
18
+ Rake::Task.tasks.each(&:reenable)
19
+ end
20
+
21
+ describe 'generate_fixtures' do
22
+ it 'writes fixtures.yml when it does not already exist' do
23
+ repo = double('repo', root: '/tmp/nonexistent_root_for_test', fixtures: "fixture_content")
24
+ allow(PuppetlabsOnceover::Controlrepo).to receive(:new).and_return(repo)
25
+ allow(File).to receive(:exist?).and_call_original
26
+ allow(File).to receive(:exist?).with(File.expand_path('./.fixtures.yml', repo.root)).and_return(false)
27
+ allow(File).to receive(:write)
28
+
29
+ Rake::Task['generate_fixtures'].invoke
30
+
31
+ expect(File).to have_received(:write).with(File.expand_path('./.fixtures.yml', repo.root), 'fixture_content')
32
+ end
33
+
34
+ it 'raises when fixtures.yml already exists' do
35
+ repo = double('repo', root: '/tmp/nonexistent_root_for_test')
36
+ allow(PuppetlabsOnceover::Controlrepo).to receive(:new).and_return(repo)
37
+ allow(File).to receive(:exist?).and_call_original
38
+ allow(File).to receive(:exist?).with(File.expand_path('./.fixtures.yml', repo.root)).and_return(true)
39
+
40
+ expect { Rake::Task['generate_fixtures'].invoke }.to raise_error(/already exits/)
41
+ end
42
+ end
43
+
44
+ describe 'hiera_setup' do
45
+ it 'updates the datadir for hash values containing datadir and writes the new config' do
46
+ hiera_config = { defaults: { datadir: 'orig/data' } }
47
+ repo = double('repo')
48
+ allow(repo).to receive(:hiera_config).and_return(hiera_config)
49
+ # Use File.expand_path so these are real OS-native absolute paths on any
50
+ # platform (Windows Pathname#relative_path_from raises "different prefix"
51
+ # when comparing a bare '/some/repo'-style path against one that's been
52
+ # expanded to a drive-letter path).
53
+ allow(repo).to receive(:hiera_config_file).and_return(File.expand_path('/some/repo/hiera.yaml'))
54
+ allow(repo).to receive(:hiera_data).and_return(File.expand_path('/some/repo/data'))
55
+ allow(repo).to receive(:hiera_config=)
56
+ allow(PuppetlabsOnceover::Controlrepo).to receive(:new).and_return(repo)
57
+
58
+ Rake::Task['hiera_setup'].invoke
59
+
60
+ expect(repo).to have_received(:hiera_config=) do |new_config|
61
+ expect(new_config[:defaults][:datadir]).to eq('data')
62
+ end
63
+ end
64
+
65
+ it 'leaves non-hash values alone' do
66
+ hiera_config = { version: 5 }
67
+ repo = double('repo')
68
+ allow(repo).to receive(:hiera_config).and_return(hiera_config)
69
+ allow(repo).to receive(:hiera_config_file).and_return('/some/repo/hiera.yaml')
70
+ allow(repo).to receive(:hiera_data).and_return('/some/repo/data')
71
+ allow(repo).to receive(:hiera_config=)
72
+ allow(PuppetlabsOnceover::Controlrepo).to receive(:new).and_return(repo)
73
+
74
+ Rake::Task['hiera_setup'].invoke
75
+
76
+ expect(repo).to have_received(:hiera_config=).with(hiera_config)
77
+ end
78
+ end
79
+
80
+ describe 'controlrepo_details' do
81
+ it 'prints the controlrepo to_s' do
82
+ repo = double('repo', to_s: 'REPO DETAILS')
83
+ allow(PuppetlabsOnceover::Controlrepo).to receive(:new).and_return(repo)
84
+
85
+ expect { Rake::Task['controlrepo_details'].invoke }.to output(/REPO DETAILS/).to_stdout
86
+ end
87
+ end
88
+
89
+ describe 'generate_onceover_yaml' do
90
+ it 'reads the controlrepo and attempts to render the controlrepo.yaml.erb template' do
91
+ repo = double('repo')
92
+ allow(PuppetlabsOnceover::Controlrepo).to receive(:new).and_return(repo)
93
+
94
+ # NOTE: the bundled erb gem (6.0.7, from Ruby 3.3.12) no longer accepts
95
+ # ERB.new's legacy positional (safe_level, trim_mode) arguments, which
96
+ # rake_tasks.rb still passes. This is a pre-existing incompatibility in
97
+ # production code, not something introduced by this test. We assert the
98
+ # task reaches and exercises that ERB.new call (line coverage), rather
99
+ # than papering over the ArgumentError it currently raises in this Ruby.
100
+ expect { Rake::Task['generate_onceover_yaml'].invoke }.to raise_error(ArgumentError)
101
+ end
102
+ end
103
+
104
+ describe 'generate_nodesets' do
105
+ it 'writes a nodeset entry for each factset, handling a 404 box lookup' do
106
+ repo = double('repo')
107
+ allow(repo).to receive(:facts).and_return([{ 'os' => { 'family' => 'RedHat' } }])
108
+ allow(repo).to receive(:facts_files).and_return(['/some/repo/facts/centos7.json'])
109
+ allow(PuppetlabsOnceover::Controlrepo).to receive(:new).and_return(repo)
110
+
111
+ require 'puppetlabs-onceover/beaker'
112
+ allow(PuppetlabsOnceover::Beaker).to receive(:facts_to_vagrant_box).and_return('centos/7')
113
+ allow(PuppetlabsOnceover::Beaker).to receive(:facts_to_platform).and_return('el-7-x86_64')
114
+
115
+ http_double = double('http', use_ssl: true, 'use_ssl=': true)
116
+ response = double('response', code: '404', body: '{}')
117
+ allow(Net::HTTP).to receive(:new).and_return(http_double)
118
+ allow(http_double).to receive(:get).and_return(response)
119
+
120
+ # See the note in the generate_onceover_yaml spec above: the bundled
121
+ # erb 6.0.7 no longer accepts ERB.new's legacy positional arguments,
122
+ # so this task raises ArgumentError once it reaches its own ERB.new
123
+ # call further down. We assert the "HOSTS:" header and 404 branch
124
+ # (comment_out = true) execute first, covering those lines.
125
+ expect { Rake::Task['generate_nodesets'].invoke }.to raise_error(ArgumentError).and output(/HOSTS:/).to_stdout
126
+ end
127
+
128
+ it 'writes a nodeset entry using the virtualbox provider url when the box lookup succeeds' do
129
+ repo = double('repo')
130
+ allow(repo).to receive(:facts).and_return([{ 'os' => { 'family' => 'RedHat' } }])
131
+ allow(repo).to receive(:facts_files).and_return(['/some/repo/facts/centos7.json'])
132
+ allow(PuppetlabsOnceover::Controlrepo).to receive(:new).and_return(repo)
133
+
134
+ require 'puppetlabs-onceover/beaker'
135
+ allow(PuppetlabsOnceover::Beaker).to receive(:facts_to_vagrant_box).and_return('centos/7')
136
+ allow(PuppetlabsOnceover::Beaker).to receive(:facts_to_platform).and_return('el-7-x86_64')
137
+
138
+ box_body = {
139
+ 'current_version' => {
140
+ 'providers' => [
141
+ { 'name' => 'virtualbox', 'original_url' => 'https://example.com/box.box' }
142
+ ]
143
+ }
144
+ }.to_json
145
+
146
+ http_double = double('http', use_ssl: true, 'use_ssl=': true)
147
+ response = double('response', code: '200', body: box_body)
148
+ allow(Net::HTTP).to receive(:new).and_return(http_double)
149
+ allow(http_double).to receive(:get).and_return(response)
150
+
151
+ # As above: covers the successful (non-404) lookup branch, including
152
+ # the virtualbox provider url extraction, before hitting the same
153
+ # pre-existing ERB.new incompatibility.
154
+ expect { Rake::Task['generate_nodesets'].invoke }.to raise_error(ArgumentError).and output(/HOSTS:/).to_stdout
155
+ end
156
+ end
157
+
158
+ describe 'generate_vendor_cache' do
159
+ it 'creates a VendoredModules cache for the controlrepo' do
160
+ repo = double('repo', spec_dir: '/some/repo/spec')
161
+ allow(PuppetlabsOnceover::Controlrepo).to receive(:new).with(debug: true).and_return(repo)
162
+
163
+ require 'puppetlabs-onceover/vendored_modules'
164
+ allow(PuppetlabsOnceover::VendoredModules).to receive(:new)
165
+
166
+ Rake::Task['generate_vendor_cache'].invoke
167
+
168
+ expect(PuppetlabsOnceover::VendoredModules).to have_received(:new).with(
169
+ hash_including(repo: repo, force_update: true)
170
+ )
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,361 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+ require 'yaml'
6
+ require 'puppetlabs-onceover/controlrepo'
7
+ require 'puppetlabs-onceover/rspec/formatters'
8
+
9
+ describe PuppetlabsOnceoverFormatter do
10
+ let(:output) { StringIO.new }
11
+ subject(:formatter) { described_class.new(output) }
12
+
13
+ def group_double(description)
14
+ group = double('group', description: description)
15
+ allow(group).to receive(:parent_groups).and_return([group])
16
+ group
17
+ end
18
+
19
+ def stub_world_groups(descriptions)
20
+ groups = descriptions.map { |d| double('group', description: d) }
21
+ allow(RSpec.configuration.world).to receive(:example_groups).and_return(groups)
22
+ end
23
+
24
+ def failure_double(role:, factset:, raw_error:, description: 'a test', file_path: '/spec/foo_spec.rb', line_number: 5)
25
+ metadata = {
26
+ example_group: {
27
+ description: factset,
28
+ parent_example_group: { description: role }
29
+ },
30
+ execution_result: double('execution_result', exception: RuntimeError.new(raw_error)),
31
+ description: description,
32
+ file_path: file_path,
33
+ line_number: line_number
34
+ }
35
+ double('example', metadata: metadata)
36
+ end
37
+
38
+ describe '#example_group_started' do
39
+ context 'when the group is a top-level role' do
40
+ it 'prints the role name and sets previous_role' do
41
+ stub_world_groups(['role::foo', 'role::barbaz'])
42
+ group = group_double('role::foo')
43
+ notification = double('notification', group: group)
44
+
45
+ formatter.example_group_started(notification)
46
+
47
+ expect(output.string).to include('role::foo:')
48
+ end
49
+
50
+ it 'does not reprint the same role twice in a row' do
51
+ stub_world_groups(['role::foo'])
52
+ group = group_double('role::foo')
53
+ notification = double('notification', group: group)
54
+
55
+ formatter.example_group_started(notification)
56
+ first_output = output.string
57
+ formatter.example_group_started(notification)
58
+
59
+ expect(output.string).to eq(first_output)
60
+ end
61
+ end
62
+
63
+ context 'when the group is nested (not the top-level role)' do
64
+ it "prints '? ' as a placeholder" do
65
+ top = double('top_group')
66
+ group = double('group', description: 'nested context')
67
+ allow(group).to receive(:parent_groups).and_return([top, group])
68
+ notification = double('notification', group: group)
69
+
70
+ formatter.example_group_started(notification)
71
+
72
+ expect(output.string).to eq('? ')
73
+ end
74
+ end
75
+ end
76
+
77
+ describe '#example_passed' do
78
+ it 'writes a green P' do
79
+ formatter.example_passed(double('notification'))
80
+ expect(output.string).to include('P')
81
+ end
82
+ end
83
+
84
+ describe '#example_failed' do
85
+ it 'writes a red F' do
86
+ formatter.example_failed(double('notification'))
87
+ expect(output.string).to include('F')
88
+ end
89
+ end
90
+
91
+ describe '#example_pending' do
92
+ it 'writes a yellow ?' do
93
+ formatter.example_pending(double('notification'))
94
+ expect(output.string).to include('?')
95
+ end
96
+ end
97
+
98
+ describe '#parse_errors' do
99
+ it 'parses a compilation error with a file/line/column location' do
100
+ allow(RSpec.configuration).to receive(:onceover_tempdir).and_return('/tmp/xyz/.onceover')
101
+ allow(RSpec.configuration).to receive(:onceover_root).and_return('/tmp/xyz')
102
+ allow(RSpec.configuration).to receive(:onceover_environmentpath).and_return('etc/puppetlabs/code/environments')
103
+
104
+ raw = 'Some function failed error during compilation: bad value at (file: /tmp/xyz/.onceover/etc/puppetlabs/code/environments/production/site/role/manifests/foo.pp, line: 4, column: 2); more'
105
+ results = formatter.parse_errors(raw)
106
+ expect(results.first[:line]).to eq('4')
107
+ expect(results.first[:column]).to eq('2')
108
+ expect(results.first[:file]).to eq('site/role/manifests/foo.pp')
109
+ end
110
+
111
+ it 'parses a compilation error whose location omits the file' do
112
+ raw = 'error during compilation: bad value at (line: 9, column: 1); trailer'
113
+ results = formatter.parse_errors(raw)
114
+ expect(results.first[:file]).to be_nil
115
+ expect(results.first[:line]).to eq('9')
116
+ end
117
+
118
+ it 'parses a compilation error with no location at all, matching "on node"' do
119
+ raw = 'error during compilation: something bad happened on node foo.example.com'
120
+ results = formatter.parse_errors(raw)
121
+ expect(results.first[:text]).to eq('something bad happened')
122
+ expect(results.first[:file]).to be_nil
123
+ end
124
+
125
+ it 'falls back to the raw string when a compilation error matches neither location pattern' do
126
+ raw = 'error during compilation: nothing structured here whatsoever'
127
+ results = formatter.parse_errors(raw)
128
+ expect(results).to eq([{ text: raw }])
129
+ end
130
+
131
+ it 'falls back to the raw string for a non-compilation error' do
132
+ raw = "NoMethodError: undefined method 'foo' for nil"
133
+ results = formatter.parse_errors(raw)
134
+ expect(results).to eq([{ text: raw }])
135
+ end
136
+ end
137
+
138
+ describe '#calculate_relative_source' do
139
+ before do
140
+ allow(RSpec.configuration).to receive(:onceover_tempdir).and_return('/tmp/xyz/.onceover')
141
+ allow(RSpec.configuration).to receive(:onceover_root).and_return('/tmp/xyz')
142
+ allow(RSpec.configuration).to receive(:onceover_environmentpath).and_return('etc/puppetlabs/code/environments')
143
+ end
144
+
145
+ it 'returns nil when given a nil file' do
146
+ expect(formatter.calculate_relative_source(nil)).to be_nil
147
+ end
148
+
149
+ it 'calculates the path relative to the production environment root' do
150
+ file = '/tmp/xyz/.onceover/etc/puppetlabs/code/environments/production/site/role/manifests/foo.pp'
151
+ expect(formatter.calculate_relative_source(file)).to eq('site/role/manifests/foo.pp')
152
+ end
153
+ end
154
+
155
+ describe '#extract_failure_data' do
156
+ it 'attaches the list of failing factsets to each parsed error' do
157
+ fails = [
158
+ failure_double(role: 'role::foo', factset: 'using fact set centos-7-x86_64', raw_error: 'boom'),
159
+ failure_double(role: 'role::foo', factset: 'using fact set windows-2019-x64', raw_error: 'boom')
160
+ ]
161
+ results = formatter.extract_failure_data(fails)
162
+ expect(results.first[:factsets]).to eq(['centos-7-x86_64', 'windows-2019-x64'])
163
+ end
164
+ end
165
+
166
+ describe '#extract_failures' do
167
+ it 'groups failures by role and then by distinct error message' do
168
+ notification = double(
169
+ 'notification',
170
+ failed_examples: [
171
+ failure_double(role: 'role::foo', factset: 'using fact set centos-7-x86_64', raw_error: 'boom one'),
172
+ failure_double(role: 'role::foo', factset: 'using fact set windows-2019-x64', raw_error: 'boom two'),
173
+ failure_double(role: 'role::bar', factset: 'using fact set centos-7-x86_64', raw_error: 'boom three')
174
+ ]
175
+ )
176
+
177
+ grouped = formatter.extract_failures(notification)
178
+
179
+ expect(grouped.keys).to contain_exactly('role::foo', 'role::bar')
180
+ expect(grouped['role::foo'].size).to eq(2)
181
+ expect(grouped['role::bar'].size).to eq(1)
182
+ end
183
+ end
184
+
185
+ describe '#dump_failures' do
186
+ before do
187
+ allow(RSpec.configuration).to receive(:onceover_tempdir).and_return('/tmp/xyz/.onceover')
188
+ allow(RSpec.configuration).to receive(:onceover_root).and_return('/tmp/xyz')
189
+ allow(RSpec.configuration).to receive(:onceover_environmentpath).and_return('etc/puppetlabs/code/environments')
190
+ end
191
+
192
+ it 'renders the error summary template for each failing role' do
193
+ notification = double(
194
+ 'notification',
195
+ failed_examples: [
196
+ failure_double(role: 'role::foo', factset: 'using fact set centos-7-x86_64', raw_error: 'boom')
197
+ ]
198
+ )
199
+
200
+ formatter.dump_failures(notification)
201
+
202
+ expect(output.string).to include('role::foo')
203
+ expect(output.string).to include('failed')
204
+ end
205
+ end
206
+
207
+ describe '#longest_group' do
208
+ it 'returns the length of the longest example group description' do
209
+ stub_world_groups(['role::a', 'role::much_longer_name'])
210
+ expect(formatter.longest_group).to eq('role::much_longer_name'.length)
211
+ end
212
+ end
213
+
214
+ describe 'color helper methods' do
215
+ [:class_name, :black, :red, :green, :yellow, :blue, :magenta, :cyan, :white, :bold].each do |method|
216
+ it "##{method} wraps the given text" do
217
+ expect(formatter.public_send(method, 'hello')).to be_a(String)
218
+ expect(formatter.public_send(method, 'hello')).to include('hello')
219
+ end
220
+ end
221
+ end
222
+ end
223
+
224
+ describe PuppetlabsOnceoverFormatterParallel do
225
+ let(:output) { StringIO.new }
226
+ subject(:formatter) { described_class.new(output) }
227
+
228
+ def failure_double(role:, factset:, raw_error:)
229
+ metadata = {
230
+ example_group: {
231
+ description: factset,
232
+ parent_example_group: { description: role }
233
+ },
234
+ execution_result: double('execution_result', exception: RuntimeError.new(raw_error))
235
+ }
236
+ double('example', metadata: metadata)
237
+ end
238
+
239
+ describe '#example_group_started' do
240
+ it 'does nothing' do
241
+ expect { formatter.example_group_started(double('notification')) }.not_to raise_error
242
+ expect(output.string).to eq('')
243
+ end
244
+ end
245
+
246
+ describe '#example_passed' do
247
+ it 'writes and flushes a green P' do
248
+ formatter.example_passed(double('notification'))
249
+ expect(output.string).to include('P')
250
+ end
251
+ end
252
+
253
+ describe '#example_failed' do
254
+ it 'writes and flushes a red F' do
255
+ formatter.example_failed(double('notification'))
256
+ expect(output.string).to include('F')
257
+ end
258
+ end
259
+
260
+ describe '#example_pending' do
261
+ it 'writes and flushes a yellow ?' do
262
+ formatter.example_pending(double('notification'))
263
+ expect(output.string).to include('?')
264
+ end
265
+ end
266
+
267
+ describe '#dump_failures' do
268
+ it 'dumps a yaml file of the failures into the parallel results dir' do
269
+ tempdir = Dir.mktmpdir
270
+ allow(RSpec.configuration).to receive(:onceover_tempdir).and_return(tempdir)
271
+
272
+ notification = double(
273
+ 'notification',
274
+ failed_examples: [failure_double(role: 'role::foo', factset: 'using fact set centos-7-x86_64', raw_error: 'boom')]
275
+ )
276
+
277
+ formatter.dump_failures(notification)
278
+
279
+ files = Dir["#{tempdir}/parallel/*.yaml"]
280
+ expect(files.size).to eq(1)
281
+ expect(YAML.load_file(files.first)).to have_key('role::foo')
282
+
283
+ FileUtils.remove_entry(tempdir)
284
+ end
285
+ end
286
+
287
+ describe '#output_results' do
288
+ before do
289
+ allow(RSpec.configuration).to receive(:onceover_tempdir).and_return('/tmp/xyz/.onceover')
290
+ allow(RSpec.configuration).to receive(:onceover_root).and_return('/tmp/xyz')
291
+ allow(RSpec.configuration).to receive(:onceover_environmentpath).and_return('etc/puppetlabs/code/environments')
292
+ end
293
+
294
+ it 'merges all yaml result files, renders them, and deletes the source files' do
295
+ directory = Dir.mktmpdir
296
+ File.write(File.join(directory, 'results-1.yaml'), { 'role::foo' => [{ text: 'boom one' }] }.to_yaml)
297
+ File.write(File.join(directory, 'results-2.yaml'), { 'role::bar' => [{ text: 'boom two' }] }.to_yaml)
298
+
299
+ formatter.output_results(directory)
300
+
301
+ expect(output.string).to include('role::foo')
302
+ expect(output.string).to include('role::bar')
303
+ expect(Dir["#{directory}/*.yaml"]).to be_empty
304
+
305
+ FileUtils.remove_entry(directory)
306
+ end
307
+
308
+ it 'merges values when the same role appears in multiple files' do
309
+ directory = Dir.mktmpdir
310
+ File.write(File.join(directory, 'results-1.yaml'), { 'role::foo' => [{ text: 'boom one' }] }.to_yaml)
311
+ File.write(File.join(directory, 'results-2.yaml'), { 'role::foo' => [{ text: 'boom two' }] }.to_yaml)
312
+
313
+ formatter.output_results(directory)
314
+
315
+ expect(output.string).to include('role::foo')
316
+
317
+ FileUtils.remove_entry(directory)
318
+ end
319
+ end
320
+ end
321
+
322
+ describe FailureCollector do
323
+ let(:tempdir) { Dir.mktmpdir }
324
+
325
+ before do
326
+ allow(RSpec.configuration).to receive(:onceover_tempdir).and_return(tempdir)
327
+ end
328
+
329
+ after { FileUtils.rm_rf(tempdir) }
330
+
331
+ describe '#initialize' do
332
+ it 'touches a failures.out file in the onceover tempdir' do
333
+ described_class.new(StringIO.new)
334
+ expect(File.exist?(File.join(tempdir, 'failures.out'))).to be true
335
+ end
336
+ end
337
+
338
+ describe '#dump_failures' do
339
+ it 'appends details of each failed example to failures.out' do
340
+ collector = described_class.new(StringIO.new)
341
+
342
+ fe = double(
343
+ 'failed_example',
344
+ metadata: {
345
+ description: 'does a thing',
346
+ execution_result: double('execution_result', exception: RuntimeError.new('kaboom')),
347
+ file_path: '/spec/foo_spec.rb',
348
+ line_number: 12
349
+ }
350
+ )
351
+ failures = double('failures', failed_examples: [fe])
352
+
353
+ collector.dump_failures(failures)
354
+
355
+ contents = File.read(File.join(tempdir, 'failures.out'))
356
+ expect(contents).to include('does a thing')
357
+ expect(contents).to include('kaboom')
358
+ expect(contents).to include('/spec/foo_spec.rb:12')
359
+ end
360
+ end
361
+ end