Skip to content

Commit 6b64188

Browse files
committed
Adds ability to download forge modules in parallel
* previously only the git repositories were downloaded in parallel. This saved a ton of time but forge modules were still downloaded in serial. * This adds the same parallel download feature of the repositories to the forge modules. * Refactors a few methods so they can be run in parallel. * Extracts repository download method into more generic method so we can reuse it with forge downloads. * Typical speed improvments will be at least 400% with default of 10 threads. The more modules defined the better it gets. * Moves all methods to the fixtues helper module * remove condition logic to or assignments * move fixture references to cached value methods * moves download logic to individual methods * Forces rubocop to version 0.49 * previously anybody with rubocop gem younger than 0.49 would have issues with rubocop tests. This was due to needing features in 0.49 and not enforcing that version.
1 parent 0797846 commit 6b64188

File tree

4 files changed

+158
-90
lines changed

4 files changed

+158
-90
lines changed

Gemfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ gemspec
77
group :development do
88
gem 'codecov'
99
gem 'github_changelog_generator' if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.2.0')
10+
gem 'pry'
1011
gem 'puppet', ENV['PUPPET_GEM_VERSION'] || ENV['PUPPET_VERSION'] || '~> 4.0'
1112
gem 'simplecov', '~> 0'
1213
gem 'simplecov-console'
1314
if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.1.0')
14-
gem 'rubocop', '< 0.50'
15+
gem 'rubocop', '= 0.49'
1516
gem 'rubocop-rspec', '~> 1'
1617
end
1718
end

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Host github.com
5959

6060
```
6161

62-
Note: parallel downloads is only available for repositories and not forge modules.
62+
Note: parallel downloads are available for repositories and forge modules.
6363

6464
### Parallel tests
6565
It is also possible to use the `parallel_tests` Gem via the `:parallel_spec` Rake task to run rspec commands in parallel on groups of spec files.

lib/puppetlabs_spec_helper/tasks/fixtures.rb

Lines changed: 126 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
module PuppetlabsSpecHelper; end
66
module PuppetlabsSpecHelper::Tasks; end
7+
78
module PuppetlabsSpecHelper::Tasks::FixtureHelpers
89
# This is a helper for the self-symlink entry of fixtures.yml
910
def source_dir
1011
Dir.pwd
1112
end
1213

14+
# @return [String] - the name of current module
1315
def module_name
1416
raise ArgumentError unless File.file?('metadata.json') && File.readable?('metadata.json')
1517

@@ -23,18 +25,39 @@ def module_name
2325
File.basename(Dir.pwd).split('-').last
2426
end
2527

26-
# cache the repositories and return a hash object
28+
# @return [Hash] - returns a hash of all the fixture repositories
29+
# @example
30+
# {"puppetlabs-stdlib"=>{"target"=>"https://gitlab.com/puppetlabs/puppet-stdlib.git",
31+
# "ref"=>nil, "branch"=>"master", "scm"=>nil,
32+
# }}
2733
def repositories
28-
unless @repositories
29-
@repositories = fixtures('repositories')
30-
end
31-
@repositories
34+
@repositories ||= fixtures('repositories') || {}
3235
end
3336

37+
# @return [Hash] - returns a hash of all the fixture forge modules
38+
# @example
39+
# {"puppetlabs-stdlib"=>{"target"=>"spec/fixtures/modules/stdlib",
40+
# "ref"=>nil, "branch"=>nil, "scm"=>nil,
41+
# "flags"=>"--module_repository=https://myforge.example.com/", "subdir"=>nil}}
42+
def forge_modules
43+
@forge_modules ||= fixtures('forge_modules') || {}
44+
end
45+
46+
# @return [Hash] - a hash of symlinks specified in the fixtures file
47+
def symlinks
48+
@symlinks ||= fixtures('symlinks') || {}
49+
end
50+
51+
# @return [Hash] - returns a hash with the module name and the source directory
3452
def auto_symlink
3553
{ module_name => '#{source_dir}' }
3654
end
3755

56+
# @return [Boolean] - true if the os is a windows system
57+
def windows?
58+
!!File::ALT_SEPARATOR
59+
end
60+
3861
def fixtures(category)
3962
fixtures_yaml = if ENV['FIXTURES_YML']
4063
ENV['FIXTURES_YML']
@@ -77,7 +100,6 @@ def fixtures(category)
77100

78101
result = {}
79102
if fixtures.include?(category) && !fixtures[category].nil?
80-
81103
defaults = { 'target' => 'spec/fixtures/modules' }
82104

83105
# load defaults from the `.fixtures.yml` `defaults` section
@@ -235,6 +257,7 @@ def module_working_directory
235257
# returns the current thread count that is currently active
236258
# a status of false or nil means the thread completed
237259
# so when anything else we count that as a active thread
260+
# @return [Integer] - current thread count
238261
def current_thread_count(items)
239262
active_threads = items.find_all do |_item, opts|
240263
if opts[:thread]
@@ -247,79 +270,46 @@ def current_thread_count(items)
247270
active_threads.count
248271
end
249272

250-
# returns the max_thread_count
251-
# because we may want to limit ssh or https connections
273+
# @summary Set a limit on the amount threads used, defaults to 10
274+
# MAX_FIXTURE_THREAD_COUNT can be used to set this limit
275+
# @return [Integer] - returns the max_thread_count
252276
def max_thread_limit
253-
unless @max_thread_limit
254-
# the default thread count is 10 but can be
255-
# raised by using environment variable MAX_FIXTURE_THREAD_COUNT
256-
@max_thread_limit = if ENV['MAX_FIXTURE_THREAD_COUNT'].to_i > 0
257-
ENV['MAX_FIXTURE_THREAD_COUNT'].to_i
258-
else
259-
10 # the default
260-
end
261-
end
262-
@max_thread_limit
277+
@max_thread_limit ||= (ENV['MAX_FIXTURE_THREAD_COUNT'] || 10).to_i
263278
end
264-
end
265-
include PuppetlabsSpecHelper::Tasks::FixtureHelpers
266279

267-
desc 'Create the fixtures directory'
268-
task :spec_prep do
269-
# Ruby only sets File::ALT_SEPARATOR on Windows and Rubys standard library
270-
# uses this to check for Windows
271-
is_windows = !!File::ALT_SEPARATOR
272-
if is_windows
273-
begin
274-
require 'win32/dir'
275-
rescue LoadError
276-
$stderr.puts 'win32-dir gem not installed, falling back to executing mklink directly'
277-
end
278-
end
279-
280-
# git has a race condition creating that directory, that would lead to aborted clone operations
281-
FileUtils.mkdir_p('spec/fixtures/modules')
282-
283-
repositories.each do |remote, opts|
284-
scm = 'git'
285-
target = opts['target']
286-
subdir = opts['subdir']
287-
ref = opts['ref']
288-
scm = opts['scm'] if opts['scm']
289-
branch = opts['branch'] if opts['branch']
290-
flags = opts['flags']
291-
# get the current active threads that are alive
292-
count = current_thread_count(repositories)
293-
if count < max_thread_limit
294-
logger.debug "New Thread started for #{remote}"
295-
# start up a new thread and store it in the opts hash
296-
opts[:thread] = Thread.new do
297-
if valid_repo?(scm, target, remote)
298-
update_repo(scm, target)
299-
else
300-
clone_repo(scm, remote, target, subdir, ref, branch, flags)
280+
# @param items [Hash] - a hash of either repositories or forge modules
281+
# @param [Block] - the method you wish to use to download the item
282+
def download_items(items)
283+
items.each do |remote, opts|
284+
# get the current active threads that are alive
285+
count = current_thread_count(items)
286+
if count < max_thread_limit
287+
logger.debug "New Thread started for #{remote}"
288+
# start up a new thread and store it in the opts hash
289+
opts[:thread] = Thread.new do
290+
yield(remote, opts)
301291
end
302-
revision(scm, target, ref) if ref
303-
remove_subdirectory(target, subdir) if subdir
292+
else
293+
# the last thread started should be the longest wait
294+
item, item_opts = items.find_all { |_i, o| o.key?(:thread) }.last
295+
logger.debug "Waiting on #{item}"
296+
item_opts[:thread].join # wait for the thread to finish
297+
# now that we waited lets try again
298+
redo
304299
end
305-
else
306-
# the last thread started should be the longest wait
307-
item, item_opts = repositories.find_all { |_i, o| o.key?(:thread) }.last
308-
logger.debug "Waiting on #{item}"
309-
item_opts[:thread].join # wait for the thread to finish
310-
# now that we waited lets try again
311-
redo
312300
end
301+
# wait for all the threads to finish
302+
items.each { |_remote, opts| opts[:thread].join }
313303
end
314304

315-
# wait for all the threads to finish
316-
repositories.each { |_remote, opts| opts[:thread].join }
317-
318-
fixtures('symlinks').each do |target, link|
305+
# @param target [String] - the target directory
306+
# @param link [String] - the name of the link you wish to create
307+
# works on windows and linux
308+
def setup_symlink(target, link)
319309
link = link['target']
320-
next if File.symlink?(link)
310+
return if File.symlink?(link)
321311
logger.info("Creating symlink from #{link} to #{target}")
322-
if is_windows
312+
if windows?
323313
target = File.join(File.dirname(link), target) unless Pathname.new(target).absolute?
324314
if Dir.respond_to?(:create_junction)
325315
Dir.create_junction(link, target)
@@ -331,7 +321,35 @@ def max_thread_limit
331321
end
332322
end
333323

334-
fixtures('forge_modules').each do |remote, opts|
324+
# @return [Boolean] - returns true if the module was downloaded successfully, false otherwise
325+
# @param [String] - the remote url or namespace/name of the module to download
326+
# @param [Hash] - list of options such as version, branch, ref
327+
def download_repository(remote, opts)
328+
scm = 'git'
329+
target = opts['target']
330+
subdir = opts['subdir']
331+
ref = opts['ref']
332+
scm = opts['scm'] if opts['scm']
333+
branch = opts['branch'] if opts['branch']
334+
flags = opts['flags']
335+
if valid_repo?(scm, target, remote)
336+
update_repo(scm, target)
337+
else
338+
clone_repo(scm, remote, target, subdir, ref, branch, flags)
339+
end
340+
revision(scm, target, ref) if ref
341+
remove_subdirectory(target, subdir) if subdir
342+
end
343+
344+
# @return [String] - the spec/fixtures/modules directory in the module root folder
345+
def module_target_dir
346+
@module_target_dir ||= File.expand_path('spec/fixtures/modules')
347+
end
348+
349+
# @return [Boolean] - returns true if the module was downloaded successfully, false otherwise
350+
# @param [String] - the remote url or namespace/name of the module to download
351+
# @param [Hash] - list of options such as version
352+
def download_module(remote, opts)
335353
ref = ''
336354
flags = ''
337355
if opts.instance_of?(String)
@@ -342,34 +360,60 @@ def max_thread_limit
342360
flags = " #{opts['flags']}" if opts['flags']
343361
end
344362

345-
next if File.directory?(target)
363+
return false if File.directory?(target)
364+
365+
# The PMT cannot handle multi threaded runs due to cache directory collisons
366+
# so we randomize the directory instead.
367+
# Does working_dir even need to be passed?
368+
Dir.mktmpdir do |working_dir|
369+
command = 'puppet module install' + ref + flags + ' --ignore-dependencies' \
370+
' --force' \
371+
" --module_working_dir \"#{working_dir}\"" \
372+
" --target-dir \"#{module_target_dir}\" \"#{remote}\""
346373

347-
working_dir = module_working_directory
348-
target_dir = File.expand_path('spec/fixtures/modules')
374+
unless system(command)
375+
raise "Failed to install module #{remote} to #{module_target_dir}"
376+
end
377+
end
378+
$CHILD_STATUS.success?
379+
end
380+
end
349381

350-
command = 'puppet module install' + ref + flags + \
351-
' --ignore-dependencies' \
352-
' --force' \
353-
" --module_working_dir \"#{working_dir}\"" \
354-
" --target-dir \"#{target_dir}\" \"#{remote}\""
382+
include PuppetlabsSpecHelper::Tasks::FixtureHelpers
355383

356-
unless system(command)
357-
raise "Failed to install module #{remote} to #{target_dir}"
384+
desc 'Create the fixtures directory'
385+
task :spec_prep do
386+
# Ruby only sets File::ALT_SEPARATOR on Windows and Rubys standard library
387+
# uses this to check for Windows
388+
if windows?
389+
begin
390+
require 'win32/dir'
391+
rescue LoadError
392+
$stderr.puts 'win32-dir gem not installed, falling back to executing mklink directly'
358393
end
359394
end
360395

396+
# git has a race condition creating that directory, that would lead to aborted clone operations
397+
FileUtils.mkdir_p('spec/fixtures/modules')
398+
399+
symlinks.each { |target, link| setup_symlink(target, link) }
400+
401+
download_items(repositories) { |remote, opts| download_repository(remote, opts) }
402+
403+
download_items(forge_modules) { |remote, opts| download_module(remote, opts) }
404+
361405
FileUtils.mkdir_p('spec/fixtures/manifests')
362406
FileUtils.touch('spec/fixtures/manifests/site.pp')
363407
end
364408

365409
desc 'Clean up the fixtures directory'
366410
task :spec_clean do
367-
fixtures('repositories').each do |_remote, opts|
411+
repositories.each do |_remote, opts|
368412
target = opts['target']
369413
FileUtils.rm_rf(target)
370414
end
371415

372-
fixtures('forge_modules').each do |_remote, opts|
416+
forge_modules.each do |_remote, opts|
373417
target = opts['target']
374418
FileUtils.rm_rf(target)
375419
end

spec/unit/puppetlabs_spec_helper/tasks/fixtures_spec.rb

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@
128128
allow(YAML).to receive(:load_file).with('.fixtures.yml').and_return('fixtures' => { 'forge_modules' => { 'stdlib' => 'puppetlabs-stdlib' } })
129129
expect(subject.fixtures('forge_modules')).to eq('puppetlabs-stdlib' => {
130130
'target' => 'spec/fixtures/modules/stdlib',
131-
'ref' => nil,
131+
'ref' => nil,
132132
'branch' => nil,
133-
'scm' => nil,
134-
'flags' => nil,
133+
'scm' => nil,
134+
'flags' => nil,
135135
'subdir' => nil,
136136
})
137137
end
@@ -143,14 +143,37 @@
143143
'fixtures' => { 'forge_modules' => { 'stdlib' => 'puppetlabs-stdlib' } })
144144
expect(subject.fixtures('forge_modules')).to eq('puppetlabs-stdlib' => {
145145
'target' => 'spec/fixtures/modules/stdlib',
146-
'ref' => nil,
146+
'ref' => nil,
147147
'branch' => nil,
148-
'scm' => nil,
149-
'flags' => '--module_repository=https://myforge.example.com/',
148+
'scm' => nil,
149+
'flags' => '--module_repository=https://myforge.example.com/',
150150
'subdir' => nil,
151151
})
152152
end
153153
end
154+
155+
context 'when file specifies repository fixtures' do
156+
before(:each) do
157+
allow(File).to receive(:exist?).with('.fixtures.yml').and_return true
158+
allow(YAML).to receive(:load_file).with('.fixtures.yml').and_return(
159+
'fixtures' => {
160+
'repositories' => { 'stdlib' => 'https://github.com/puppetlabs/puppetlabs-stdlib.git' },
161+
},
162+
)
163+
end
164+
165+
it 'returns the hash' do
166+
expect(subject.repositories).to eq('https://github.com/puppetlabs/puppetlabs-stdlib.git' => {
167+
'target' => 'spec/fixtures/modules/stdlib',
168+
'ref' => nil,
169+
'branch' => nil,
170+
'scm' => nil,
171+
'flags' => nil,
172+
'subdir' => nil,
173+
})
174+
end
175+
end
176+
154177
context 'when file specifies puppet version' do
155178
def stub_fixtures(data)
156179
allow(File).to receive(:exist?).with('.fixtures.yml').and_return true

0 commit comments

Comments
 (0)