You are browsing a read-only backup copy of Wikitech. The live site can be found at wikitech.wikimedia.org
Puppet coding/testing: Difference between revisions
imported>Hashar mNo edit summary |
imported>Hashar m (→ruby debugger: how to print the compiled catalogue (p catalogue)) |
||
Line 266: | Line 266: | ||
</source> | </source> | ||
You will then be in a console before the breakage that let you inspect the environment (<code>ls</code>). See https://github.com/pry/pry for details. | You will then be in a console before the breakage that let you inspect the environment (<code>ls</code>) or print the compiled catalogue (<code>p catalogue</code>). See https://github.com/pry/pry for details. | ||
[https://www.mediawiki.org/wiki/Continuous_integration/Entry_points#ruby_debug_tip reference] | [https://www.mediawiki.org/wiki/Continuous_integration/Entry_points#ruby_debug_tip reference] |
Revision as of 07:41, 1 April 2021
We have a set of helpers to lint, check style and even test the Puppet code we write. This part cover how to run the utilities, how to write your own tests and even how to debug!
Running tests
Some puppet modules have test suites using the ruby test runner rspec[1] with rspec-puppet[2] and a set of rake tasks to run linting check (to validate manifests, puppet-lint, hiera yaml files, erb templates). The ruby dependencies are listed in Gemfile
at the root of each git repositories (operations/puppet, operations/puppet/nginx ...). The dependencies are to be installed using bundler (the ruby world package manager).
To install all required dependencies: bundle install
and to run a command in that environment: bundle exec <some command>
.
Assuming a puppet module has a Rakefile
and tests defined in a ./spec
sub directory, one can run syntax checks, style and tests via the three commands:
bundle exec rake syntax
bundle exec rake puppet-lint
bundle exec rake spec
Rake explained
The /Gemfile
asks for the ruby gem puppetlabs_spec_helper (doc) which contains several predefined rake tasks. Hence in a module one just have to create a Rakefile with:
require 'puppetlabs_spec_helper/rake_tasks'
In the module, rake -T
gives the list of all available tasks (and rake -P
list the dependency tree), though most would do nothing:
$ cd modules/mymodule
$ bundle exec rake -T
rake beaker # Run beaker acceptance tests
rake beaker:sets # List available beaker nodesets
rake beaker:ssh[set,node] # Try to use vagrant to login to the Beaker node
rake build # Build puppet module package
rake check:dot_underscore # Fails if any ._ files are present in directory
rake check:git_ignore # Fails if directories contain the files specified in .gitignore
rake check:symlinks # Fails if symlinks are present in directory
rake check:test_file # Fails if .pp files present in tests folder
rake clean # Clean a built module package
rake compute_dev_version # Print development version of module
rake help # Display the list of available rake tasks
rake lint # Run puppet-lint
rake parallel_spec # Parallel spec tests
rake release_checks # Runs all necessary checks on a module in preparation for a release
rake rubocop # Run RuboCop
rake rubocop:auto_correct # Auto-correct RuboCop offenses
rake spec # Run spec tests and clean the fixtures directory if successful
rake spec_clean # Clean up the fixtures directory
rake spec_prep # Create the fixtures directory
rake spec_standalone # Run spec tests on an existing fixtures directory
rake syntax # Syntax check Puppet manifests and templates
rake syntax:hiera # Syntax check Hiera config files
rake syntax:manifests # Syntax check Puppet manifests
rake syntax:templates # Syntax check Puppet templates
rake validate # Check syntax of Ruby files and call :syntax and :metadata_lint
$
The syntax*
tasks come from the rubygem puppet-syntax.
The spec*
tasks are helpers to prepare a puppet environment to run rspec
into:
spec
setup a test environment and run the testsspec_prep
adds fixtures and module dependencies for the test environmentspec_standalone
run tests, assuming the test environment has been previously setup (withspec_pre
orspec
).spec_clean
tears down that environment
Writing tests
To test puppet resources, we rely on rspec-puppet an helper on top of the ruby test runner rspec. rspec-puppet
provides utilities to setup puppet, to compile a catalog and it provides built-in assert methods to run against the generated catalog. The setup recommendation is to point puppet manifest_dir
and module_path
to an empty directory spec/fixtures
that is populated automatically by the puppetlabs_spec_helper rake task spec_prep
(which is conveniently a prerequisite of the task spec
).
A minimal case requires:
- a
Rakefile
- an helper file
spec/spec_helper.rb
which will be loaded by each test - a spec defining the tests to conduct
At first the Rakefile reuses the puppetlabs_spec_helper rake tasks described in the previous section:
Rakefile
:
require 'puppetlabs_spec_helper/rake_tasks'
The tests are placed in sub directories of spec/
based on the type of Puppet resource being tested. That convention lets rspec-puppet properly setup the rspec helpers for the type of puppet resource being tested. rspec finds tests by crawling the hierachy under spec
looking for files with the suffix _spec.rb
. The hierarchy is:
spec/ ├── applications/ ├── classes/ ├──── someclass_spec.rb ├── defines/ ├── functions/ ├──── some_function_spec.rb ├── hosts/ ├── types/ └── types_aliases/
We then need common code to initialize Puppet and point it to the fixture directory. That is where puppetlabs_spec_helper will create a dummy manifests/site.pp
and eventually inject additional modules required for tests.
Create spec/spec_helper.rb
:
require 'rspec-puppet'
# The empty fixture dir will be mymodule/spec/fixtures
fixture_path = File.expand_path(File.join(__FILE__, '..', 'fixtures'))
RSpec.configure do |c|
# Configure puppet
c.module_path = File.join(fixture_path, 'modules')
c.manifest_dir = File.join(fixture_path, 'manifests')
end
The file will be required by each of the specs using require 'spec_helper'
and setup Puppet to point to mymodule/spec/fixtures
directory (for now empty).
Given a puppet module mymodule
consisting of a single class in manifests/init.pp
:
class mymodule {
}
We first have to instruct puppetlabs_spec_helper to inject our module in the fixture directory. To do so create a .fixtures.yml
at the root of the module (eg: give a puppet module mymodule:/modules/mymodule/.fixtures.yml
.
fixtures:
symlinks:
mymodule: "#{source_dir}"
The puppet labs spec_helper task spec_prep
would process that file and symlink our module as spec/fixtures/modules/mymodule
as well as create an empty spec/fixtures/manifests/site.pp
. Late one can symlink other modules (eg: stdlib: "../../../../stdlib"
).
Since we will test a class, we create our test file under spec/classes/
as mymodule_spec.rb
:
# Helper from spec/spec_helper.rb that with the puppet configuration for rspec-puppet
require 'spec_helper'
# We will act on the resource "my module"
# Defined as a class resource since the file is under spec/classes
describe 'mymodule' do
# Check whether puppet can compile the catalog for the 'mymodule' class
it { should.compile }
end
Finally some fancy configuration of rspec via /.rspec
:
--format doc --color
And we can finally get the test environment prepared and run the spec:
$ bundle exec rake spec mymodule should compile into a catalogue without dependency cycles Finished in 0.07349 seconds (files took 0.4312 seconds to load) 1 example, 0 failures $
Had we had an error in the manifest, for example a missing curly brace:
$ bundle exec rake spec mymodule should compile into a catalogue without dependency cycles (FAILED - 1) Failures: 1) mymodule should compile into a catalogue without dependency cycles Failure/Error: it { should compile } error during compilation: Syntax error at end of file; expected '}' at modules/mymodule/spec/fixtures/modules/mymodule/manifests/init.pp:2 on node johndoe # ./spec/classes/mymodule_spec.rb:4:in `block (2 levels) in <top (required)>' Finished in 0.06745 seconds (files took 0.4104 seconds to load) 1 example, 1 failure Failed examples: rspec ./spec/classes/mymodule_spec.rb:4 # mymodule should compile into a catalogue without dependency cycles
Debugging
A collection of tips to debug spec failures.
Dump resources
If an example fail for now obvious reason, it is sometime helpful to dump the catalog resources just before the example. One can just print it before the failing rspec expectation:
it {
pp catalogue.resources
should compile
}
Puppet debug log
Enable puppet debug log to the console. In the spec_helper.rb
add:
RSpec.configure do |conf|
if ENV['PUPPET_DEBUG']
conf.before(:each) do
Puppet::Util::Log.level = :debug
Puppet::Util::Log.newdestination(:console)
end
end
end
Then run tests with PUPPET_DEBUG=1 bundle exec rake spec
Credits: maxlinc@github gist).
Run a single spec / example
At first prepare the fixture environment:
bundle exec rake spec_prep
Then run in the bundle environment, run rspec on a specific spec:
bundle exec rspec spec/classes/someclass_spec.rb
Or you can filter based on the spec name:
bundle exec rspec --example mymodule::someclass
See rspec help for more details.
Pass options to rspec from env
You can pass extra options to rspec via SPEC_OPTS
environment variable. Useful when you want to invoke your tests from rake but want to refine what rspec does:
SPEC_OPTS="--example mymodule::someclass" bundle exec rake
Which would be the equivalent of:
bundle exec rake spec_prep
bundle exec rspec --example my module::someclass
ruby debugger
You can use the gem pry
to break on error and get shown a console in the context of the failure. To your Gemfile add gem 'pry'
and install it with bundle install
then to break inside a spec:
require 'spec_helper'
require 'pry'
describe 'mymodule::someclass' do
it {
# enable debugger
binding.pry
# compilation that fails:
should compile
}
end
You will then be in a console before the breakage that let you inspect the environment (ls
) or print the compiled catalogue (p catalogue
). See https://github.com/pry/pry for details.
Integration with Jenkins
Jenkins job simply runs rake test
(CI entry point) from the root of the operations/puppet.git. The checks we want to run automatically are marked as prerequisites of the test task, for example:
task test: [:rubocop, :puppetlint_head, :syntax_head, :spec]
The tasks suffixed with _head
are optimized to have the utility to only run on files changed in the proposed patch. Typically puppet-lint takes minutes to run against all the puppet manifests, when for CI we only are interested in the manifests that are actually being changed.
The rakefile add a task for each module having a spec
directory. The task is named after the module and put under the namespace spec
. Hence as soon as you create a basic structure for a module mymodule, you can run it from the root of the repository with:
bundle exec rake spec:mymodule
And it is dynamically made a prerequisite of the spec
task which is run by CI. To say it otherwise, once a spec directory is created, Jenkins will try to run the spec.
Resources
- http://rspec-puppet.com/ (might not be up-to-date)
- https://github.com/rodjek/rspec-puppet
- https://github.com/puppetlabs/puppetlabs_spec_helper#puppet-labs-spec-helper (among others: doc about fixtures).
- ↑ https://rspec.info/, Behaviour Driven Development for Ruby
- ↑ http://rspec-puppet.com/, RSpec test framework for your Puppet manifests