puppet_fixtures 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0b6246620641ff6d3570c494428f46def8a857befdda5bcca94ada409f363e76
4
+ data.tar.gz: bafb926914441b2118e2a72cfd3ce2108073576c76f6e7e16dc6edacd1585414
5
+ SHA512:
6
+ metadata.gz: a8fff928c60d024ffff15ef613e2eafbea0c00c51d64057c820b3b2500793c0b24bd72da3ca6e169e4c45868db34b2cc60db4cca47eec8dc77edc3d0cfc2e46b
7
+ data.tar.gz: e8a69aee563e805b5f17f24b567b40492cb4760b80b7ee988e466c87fb0d013cba74b167f3319f124b33798414ed13717cc96364188412a59a896f9e89499c80
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ require 'optparse'
5
+
6
+ require 'puppet_fixtures'
7
+
8
+ COMMANDS = ['clean', 'install', 'show']
9
+
10
+ command = ARGV[0]
11
+ unless COMMANDS.include?(command)
12
+ $stderr.puts "Usage: #{$0} #{COMMANDS.join('|')}"
13
+ exit 1
14
+ end
15
+
16
+ fixtures = PuppetFixtures::Fixtures.new
17
+
18
+ case command
19
+ when 'clean'
20
+ fixtures.clean
21
+ when 'install'
22
+ fixtures.download
23
+ when 'show'
24
+ if (path = fixtures.fixture_path)
25
+ puts "Parsing #{fixtures.fixture_path}"
26
+
27
+ if fixtures.symlinks.any?
28
+ puts
29
+ puts "Symlinks"
30
+ fixtures.symlinks.each do |_target, symlink|
31
+ puts " #{symlink}"
32
+ end
33
+ end
34
+
35
+ if fixtures.forge_modules.any?
36
+ puts
37
+ puts "Forge modules"
38
+ fixtures.forge_modules.each do |mod, opts|
39
+ dir = Pathname.new(opts[:target]).relative_path_from(fixtures.module_target_dir)
40
+ description = mod
41
+ description += " #{opts[:ref]}"
42
+ puts " #{dir} => #{description}"
43
+ end
44
+ end
45
+
46
+ if fixtures.repositories.any?
47
+ puts
48
+ puts "Repositories"
49
+ fixtures.repositories.each do |repository, opts|
50
+ dir = Pathname.new(opts[:target]).relative_path_from(fixtures.module_target_dir)
51
+ description = ["#{opts[:scm]}+#{repository}", opts[:ref]]
52
+ description << "branch #{opts[:branch]}" if opts[:branch]
53
+ puts " #{dir} => #{description.compact.join(' ')}"
54
+ end
55
+ end
56
+ else
57
+ $stderr.puts "No fixture file found"
58
+ end
59
+ end
@@ -0,0 +1,15 @@
1
+ require_relative '../puppet_fixtures'
2
+
3
+ require 'rake'
4
+
5
+ namespace :fixtures do
6
+ desc 'Create the fixtures directory'
7
+ task :prep do
8
+ PuppetFixtures.new.download(max_thread_limit: ENV.fetch('MAX_FIXTURE_THREAD_COUNT', 10).to_i)
9
+ end
10
+
11
+ desc 'Clean up the fixtures directory'
12
+ task :clean do
13
+ PuppetFixtures.new.clean
14
+ end
15
+ end
@@ -0,0 +1,576 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+ require 'yaml'
6
+
7
+ # PuppetFixtures is a mechanism to download Puppet fixtures.
8
+ #
9
+ # These fixtures can be symlinks, repositories (git or Mercurial) or forge
10
+ # modules.
11
+ module PuppetFixtures
12
+ # @return [Boolean]
13
+ # true if the os is a windows system
14
+ def self.windows?
15
+ # Ruby only sets File::ALT_SEPARATOR on Windows and Rubys standard library
16
+ # uses this to check for Windows
17
+ !!File::ALT_SEPARATOR
18
+ end
19
+
20
+ class Fixtures
21
+ attr_reader :source_dir
22
+
23
+ def initialize(source_dir: Dir.pwd, max_thread_limit: 10)
24
+ @source_dir = source_dir
25
+ @max_thread_limit = max_thread_limit
26
+ end
27
+
28
+ # @return [Hash]
29
+ # A hash of all the fixture repositories
30
+ # @example
31
+ # {
32
+ # "puppetlabs-stdlib"=>{
33
+ # "target"=>"https://gitlab.com/puppetlabs/puppet-stdlib.git",
34
+ # "ref"=>nil,
35
+ # "branch"=>"main",
36
+ # "scm"=>nil,
37
+ # }
38
+ # }
39
+ def repositories
40
+ @repositories ||= fixtures['repositories']
41
+ end
42
+
43
+ # @return [Hash]
44
+ # A hash of all the fixture forge modules
45
+ # @example
46
+ # {
47
+ # "puppetlabs-stdlib"=>{
48
+ # "target"=>"spec/fixtures/modules/stdlib",
49
+ # "ref"=>nil,
50
+ # "branch"=>nil,
51
+ # "scm"=>nil,
52
+ # "flags"=>"--module_repository=https://myforge.example.com/",
53
+ # "subdir"=>nil,
54
+ # }
55
+ # }
56
+ def forge_modules
57
+ @forge_modules ||= fixtures['forge_modules']
58
+ end
59
+
60
+ # @return [Hash[String, Symlink]]
61
+ # A hash of symlinks specified in the fixtures file
62
+ def symlinks
63
+ @symlinks ||= fixtures['symlinks']
64
+ end
65
+
66
+ def fixtures
67
+ @fixtures ||= begin
68
+ categories = read_fixtures_file['fixtures']
69
+
70
+ categories['symlinks'] ||= begin
71
+ metadata = PuppetFixtures::Metadata.new(File.join(source_dir, 'metadata.json'))
72
+ { metadata.name.split('-').last => source_dir }
73
+ rescue ArgumentError
74
+ {}
75
+ end
76
+ categories['forge_modules'] ||= {}
77
+ categories['repositories'] ||= {}
78
+
79
+ defaults = { 'target' => module_target_dir }
80
+
81
+ ['symlinks', 'forge_modules', 'repositories'].to_h do |category|
82
+ # load defaults from the `.fixtures.yml` `defaults` section
83
+ # for the requested category and merge them into my defaults
84
+ if (category_defaults = categories.dig('defaults', category))
85
+ category_defaults = defaults.merge(category_defaults)
86
+ else
87
+ category_defaults = defaults
88
+ end
89
+
90
+ entries = categories[category].to_h do |fixture, opts|
91
+ # convert a simple string fixture to a hash, by
92
+ # using the string fixture as the `repo` option of the hash.
93
+ if opts.instance_of?(String)
94
+ opts = { 'repo' => opts }
95
+ end
96
+ # there should be a warning or something if it's not a hash...
97
+ next unless opts.instance_of?(Hash)
98
+
99
+ # merge our options into the defaults to get the
100
+ # final option list
101
+ opts = category_defaults.merge(opts)
102
+
103
+ next unless include_repo?(opts['puppet_version'])
104
+
105
+ entry = validate_fixture_hash!(
106
+ target: File.join(opts['target'], fixture),
107
+ ref: opts['ref'] || opts['tag'],
108
+ branch: opts['branch'],
109
+ scm: opts.fetch('scm', 'git'),
110
+ flags: opts['flags'],
111
+ subdir: opts['subdir'],
112
+ )
113
+
114
+ case category
115
+ when 'forge_modules'
116
+ entry.delete(:scm)
117
+ entry.delete(:branch)
118
+ entry.delete(:subdir)
119
+ when 'symlinks'
120
+ entry = PuppetFixtures::Symlink.new(link: entry[:target], target: opts['repo'])
121
+ end
122
+
123
+ [opts['repo'], entry]
124
+ end
125
+
126
+ [category, entries]
127
+ end
128
+ end
129
+ end
130
+
131
+ # @param [String] remote
132
+ # The remote url or namespace/name of the module to download
133
+ # @param [String] scm
134
+ # The SCM to use
135
+ # @return [Boolean]
136
+ # Returns true if the module was downloaded successfully, false otherwise
137
+ def download_repository(remote, target:, scm:, subdir:, ref:, branch:, flags:)
138
+ repository = PuppetFixtures::Repository.factory(scm: scm, remote: remote, target: target, branch: branch, ref: ref)
139
+ repository.download(flags, subdir)
140
+ end
141
+
142
+ # @return [String]
143
+ # the spec/fixtures/modules directory in the module root folder
144
+ def module_target_dir
145
+ # TODO: relative to source_dir?
146
+ @module_target_dir ||= File.expand_path(File.join('spec', 'fixtures', 'modules'))
147
+ end
148
+
149
+ # @param [String] remote
150
+ # the remote url or namespace/name of the module to download
151
+ # @return [Boolean]
152
+ # returns true if the module was downloaded, false otherwise
153
+ def download_module(remote, ref:, target:, flags:)
154
+ if File.directory?(target)
155
+ if !ref || ref.empty?
156
+ logger.debug("Module #{target} already up to date")
157
+ return false
158
+ end
159
+
160
+ begin
161
+ version = PuppetFixtures::Metadata.new(File.join(target, 'metadata.json')).version
162
+ rescue ArgumentError
163
+ logger.warn "Unable to detect module version for #{target}; updating"
164
+ else
165
+ if ref == version
166
+ logger.debug("Module #{target} already up to date (#{ref})")
167
+ return false
168
+ else
169
+ logger.debug("Module #{target} version #{version} != #{ref}; updating")
170
+ end
171
+ end
172
+ end
173
+
174
+ command = ['puppet', 'module', 'install']
175
+ command << '--version' << ref if ref
176
+ command += flags if flags
177
+ command += ['--ignore-dependencies', '--force', '--target-dir', module_target_dir, remote]
178
+
179
+ unless run_command(command)
180
+ raise "Failed to install module #{remote} to #{module_target_dir}"
181
+ end
182
+
183
+ true
184
+ end
185
+
186
+ def download
187
+ logger.debug("Downloading to #{module_target_dir}")
188
+ FileUtils.mkdir_p(module_target_dir)
189
+
190
+ if symlinks.empty?
191
+ logger.debug('No symlinks to create')
192
+ else
193
+ symlinks.each_value do |symlink|
194
+ logger.info("Creating symlink #{symlink}")
195
+ symlink.create
196
+ end
197
+ end
198
+
199
+ queue = Queue.new
200
+
201
+ repositories.each do |remote, opts|
202
+ queue << [:repository, remote, opts]
203
+ end
204
+ forge_modules.each do |remote, opts|
205
+ queue << [:forge, remote, opts]
206
+ end
207
+
208
+ if queue.empty?
209
+ logger.debug('Nothing to download')
210
+ return
211
+ end
212
+
213
+ instance = self
214
+
215
+ thread_count = [@max_thread_limit, queue.size].min
216
+ logger.debug("Download queue size: #{queue.size}; using #{thread_count} threads")
217
+
218
+ threads = thread_count.times.map do |i|
219
+ Thread.new do
220
+ type, remote, opts = queue.pop(true)
221
+ case type
222
+ when :repository
223
+ instance.download_repository(remote, **opts)
224
+ when :forge
225
+ instance.download_module(remote, **opts)
226
+ end
227
+ end
228
+ end
229
+
230
+ begin
231
+ threads.map(&:join)
232
+ rescue Interrupt
233
+ # pass
234
+ end
235
+ end
236
+
237
+ def clean
238
+ repositories.each_value do |opts|
239
+ target = opts[:target]
240
+ logger.debug("Removing repository #{target}")
241
+ FileUtils.rm_rf(target)
242
+ end
243
+
244
+ forge_modules.each_value do |opts|
245
+ target = opts[:target]
246
+ logger.debug("Removing forge module #{target}")
247
+ FileUtils.rm_rf(target)
248
+ end
249
+
250
+ symlinks.each_value do |symlink|
251
+ logger.debug("Removing symlink #{symlink}")
252
+ symlink.remove
253
+ end
254
+ end
255
+
256
+ def fixture_path
257
+ if ENV['FIXTURES_YML']
258
+ ENV['FIXTURES_YML']
259
+ elsif File.exist?('.fixtures.yml')
260
+ '.fixtures.yml'
261
+ elsif File.exist?('.fixtures.yaml')
262
+ '.fixtures.yaml'
263
+ else
264
+ nil
265
+ end
266
+ end
267
+
268
+ private
269
+
270
+ def include_repo?(version_range)
271
+ return true unless version_range
272
+
273
+ require 'semantic_puppet'
274
+
275
+ puppet_spec = Gem::Specification.find_by_name('puppet')
276
+ puppet_version = SemanticPuppet::Version.parse(puppet_spec.version.to_s)
277
+
278
+ constraint = SemanticPuppet::VersionRange.parse(version_range)
279
+ constraint.include?(puppet_version)
280
+ end
281
+
282
+ def git_remote_url(target)
283
+ output, status = Open3.capture2e('git', '--git-dir', File.join(target, '.git'), 'ls-remote', '--get-url', 'origin')
284
+ status.success? ? output.strip : nil
285
+ end
286
+
287
+ # creates a logger so we can log events with certain levels
288
+ def logger
289
+ @logger ||= begin
290
+ require 'logger'
291
+ Logger.new($stderr, level: ENV['ENABLE_LOGGER'] ? Logger::DEBUG : Logger::INFO)
292
+ end
293
+ end
294
+
295
+ def read_fixtures_file
296
+ fixtures_yaml = fixture_path
297
+
298
+ fixtures = nil
299
+ if fixtures_yaml
300
+ begin
301
+ fixtures = YAML.load_file(fixtures_yaml)
302
+ rescue Errno::ENOENT
303
+ raise "Fixtures file not found: '#{fixtures_yaml}'"
304
+ rescue Psych::SyntaxError => e
305
+ raise "Found malformed YAML in '#{fixtures_yaml}' on line #{e.line} column #{e.column}: #{e.problem}"
306
+ end
307
+ end
308
+ fixtures ||= { 'fixtures' => {} }
309
+
310
+ unless fixtures.include?('fixtures')
311
+ # File is non-empty, but does not specify fixtures
312
+ raise("No 'fixtures' entries found in '#{fixtures_yaml}'; required")
313
+ end
314
+
315
+ fixtures
316
+ end
317
+
318
+ def validate_fixture_hash!(**hash)
319
+ if hash[:flags].is_a?(String)
320
+ require 'shellwords'
321
+ hash[:flags] = Shellwords.split(hash[:flags])
322
+ end
323
+
324
+ if hash['scm'] == 'git' && hash['ref'].include?('/')
325
+ # Forward slashes in the ref aren't allowed. And is probably a branch name.
326
+ raise ArgumentError, "The ref for #{hash['target']} is invalid (Contains a forward slash). If this is a branch name, please use the 'branch' setting instead."
327
+ end
328
+
329
+ hash
330
+ end
331
+
332
+ # @param [Array[String]] command
333
+ def run_command(command, chdir: nil)
334
+ logger.debug do
335
+ require 'shellwords'
336
+ if chdir
337
+ "Calling command #{Shellwords.join(command)} in #{chdir}"
338
+ else
339
+ "Calling command #{Shellwords.join(command)}"
340
+ end
341
+ end
342
+ if chdir
343
+ system(*command, chdir: chdir)
344
+ else
345
+ system(*command)
346
+ end
347
+ end
348
+ end
349
+
350
+ class Metadata
351
+ def initialize(path)
352
+ raise ArgumentError unless File.file?(path) && File.readable?(path)
353
+
354
+ @metadata = JSON.parse(File.read(path))
355
+ rescue JSON::ParserError => e
356
+ raise ArgumentError, "Failed to read module metadata at #{path}: #{e}"
357
+ end
358
+
359
+ # @return [String[1]] The module name
360
+ def name
361
+ n = @metadata['name']
362
+ raise ArgumentError "No module name found" if !n || n.empty?
363
+
364
+ n
365
+ end
366
+
367
+ # @return [String[1]] The module version
368
+ def version
369
+ v = @metadata['version']
370
+ raise ArgumentError "No module name found" if !v || v.empty?
371
+
372
+ v
373
+ end
374
+ end
375
+
376
+ class Symlink
377
+ # @param target [String]
378
+ # the target directory
379
+ # @param link [String]
380
+ # the name of the link you wish to create
381
+ def initialize(target:, link:)
382
+ @target = target
383
+ @link = link
384
+ end
385
+
386
+ # Create a junction on Windows or otherwise a symlink
387
+ # works on windows and linux
388
+ def create
389
+ return if File.symlink?(@link)
390
+
391
+ if PuppetFixtures.windows?
392
+ begin
393
+ require 'win32/dir'
394
+ rescue LoadError
395
+ end
396
+ target = File.join(File.dirname(@link), @target) unless Pathname.new(@target).absolute?
397
+ if Dir.respond_to?(:create_junction)
398
+ Dir.create_junction(@link, target)
399
+ else
400
+ warn 'win32-dir gem not installed, falling back to executing mklink directly'
401
+ # TODO: use run_command
402
+ system("call mklink /J \"#{@link.tr('/', '\\')}\" \"#{target.tr('/', '\\')}\"")
403
+ end
404
+ else
405
+ FileUtils.ln_sf(@target, @link)
406
+ end
407
+ end
408
+
409
+ def remove
410
+ FileUtils.rm_f(@link)
411
+ end
412
+
413
+ def to_s
414
+ # TODO: relative?
415
+ "#{@link} => #{@target}"
416
+ end
417
+ end
418
+
419
+ module Repository
420
+ def self.factory(scm:, remote:, target:, branch:, ref:)
421
+ cls = case scm
422
+ when 'git'
423
+ Repository::Git
424
+ when 'hg'
425
+ Repository::Mercurial
426
+ else
427
+ raise ArgumentError, "Unfortunately #{scm} is not supported yet"
428
+ end
429
+ cls.new(remote: remote, target: target, branch: branch, ref: ref)
430
+ end
431
+
432
+ class Base
433
+ def initialize(remote:, target:, branch:, ref:)
434
+ @remote = remote
435
+ @target = target
436
+ @ref = ref
437
+ @branch = branch
438
+ end
439
+
440
+ def download(flags = nil, subdir = nil)
441
+ can_update = false
442
+ if File.directory?(@target)
443
+ if remote_url_changed?
444
+ warn "Remote for #{@target} has changed, recloning repository"
445
+ FileUtils.rm_rf(@target)
446
+ else
447
+ can_update = true
448
+ end
449
+ end
450
+
451
+ if can_update
452
+ update
453
+ else
454
+ clone(flags)
455
+ unless File.exist?(@target)
456
+ raise "Failed to clone repository #{@remote} into #{@target}"
457
+ end
458
+ end
459
+
460
+ revision
461
+ remove_subdirectory(subdir) if subdir
462
+ end
463
+
464
+ protected
465
+
466
+ def remove_subdirectory(subdir)
467
+ Dir.mktmpdir do |tmpdir|
468
+ FileUtils.mv(Dir.glob(File.join(@target, subdir, "{.[^\.]*,*}")), tmpdir)
469
+ FileUtils.rm_rf(File.join(@target, subdir))
470
+ FileUtils.mv(Dir.glob(File.join(tmpdir, "{.[^\.]*,*}")), @target.to_s)
471
+ end
472
+ end
473
+
474
+ def run_command(command, chdir: nil)
475
+ # TODO: duplicated
476
+ logger.debug do
477
+ require 'shellwords'
478
+ if chdir
479
+ "Calling command #{Shellwords.join(command)} in #{chdir}"
480
+ else
481
+ "Calling command #{Shellwords.join(command)}"
482
+ end
483
+ end
484
+ if chdir
485
+ system(*command, chdir: chdir)
486
+ else
487
+ system(*command)
488
+ end
489
+ end
490
+
491
+ def logger
492
+ # TODO: duplicated
493
+ @logger ||= begin
494
+ require 'logger'
495
+ # TODO: progname?
496
+ Logger.new($stderr, level: ENV['ENABLE_LOGGER'] ? Logger::DEBUG : Logger::INFO)
497
+ end
498
+ end
499
+ end
500
+
501
+ class Git < Base
502
+ def clone(flags = nil)
503
+ command = ['git', 'clone']
504
+ command.push('--depth', '1') unless @ref
505
+ command.push('-b', @branch) if @branch
506
+ command.push(flags) if flags
507
+ command.push(@remote, @target)
508
+
509
+ run_command(command)
510
+ end
511
+
512
+ def update
513
+ # TODO: should this pull?
514
+ command = ['git', 'fetch']
515
+ command.push('--unshallow') if shallow_git_repo?
516
+
517
+ run_command(command, chdir: @target)
518
+ end
519
+
520
+ def revision
521
+ return true unless @ref
522
+
523
+ command = ['git', 'reset', '--hard', @ref]
524
+ result = run_command(command, chdir: @target)
525
+ raise "Invalid ref #{ref} for #{@target}" unless result
526
+
527
+ result
528
+ end
529
+
530
+ def remote_url_changed?(remote = 'origin')
531
+ remote_url(remote) != @remote
532
+ end
533
+
534
+ private
535
+
536
+ def remote_url(remote = 'origin')
537
+ output, status = Open3.capture2e('git', '--git-dir', File.join(@target, '.git'), 'ls-remote', '--get-url', remote)
538
+ status.success? ? output.strip : nil
539
+ end
540
+
541
+ def shallow_git_repo?
542
+ File.file?(File.join(@target, '.git', 'shallow'))
543
+ end
544
+ end
545
+
546
+ class Mercurial < Base
547
+ def clone(flags = nil)
548
+ command = ['hg', 'clone']
549
+ command.push('-b', @branch) if @branch
550
+ command.push(flags) if flags
551
+ command.push(@remote, @target)
552
+
553
+ run_command(command)
554
+ end
555
+
556
+ def update
557
+ run_command(['hg', 'pull'])
558
+ end
559
+
560
+ def revision
561
+ return true unless @ref
562
+
563
+ command = ['hg', 'update', '--clean', '-r', @ref]
564
+ result = run_command(command, chdir: @target)
565
+ raise "Invalid ref #{ref} for #{@target}" unless result
566
+
567
+ result
568
+ end
569
+
570
+ def remote_url_changed?
571
+ # Not implemented
572
+ false
573
+ end
574
+ end
575
+ end
576
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: puppet_fixtures
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ewoud Kohl van Wijngaarden
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ description: |
27
+ Originally part of puppetlabs_spec_helper, but with a significant
28
+ refactoring to make it available standalone.
29
+ executables:
30
+ - puppet-fixtures
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - bin/puppet-fixtures
35
+ - lib/puppet_fixtures.rb
36
+ - lib/puppet_fixtures/tasks.rb
37
+ homepage: https://github.com/voxpupuli/puppet_fixtures
38
+ licenses:
39
+ - GPL-2.0-only
40
+ metadata:
41
+ source_code_uri: https://github.com/voxpupuli/puppet_fixtures
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '2.7'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '4'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.6.7
60
+ specification_version: 4
61
+ summary: Set up fixtures for Puppet testing
62
+ test_files: []