You’ve got a great idea for a new puppet-lint check? Brilliant! Let’s go and turn it into code.
First, get a skeleton project set up. In this tutorial, you will
write a new check that ensures that manifest files end with a newline. The
first thing you need to do is create a folder for our project. For convention’s
sake, you should use puppet-lint-<something descriptive>-check
.
You should be using some sort of version control to manage this project, This tutorial will use git as it’s version control. If you’re making the check public, you should consider publishing the repository on GitHub. If you don’t have an account, go and create one now (it’s free for open source projects).
As puppet-lint plugins are just Ruby gems, the rest of this setup might be familiar to you.
Every project needs a README file.
If you’re not familiar with the various licenses commonly used on open source
projects, visit Choose A License to have a look
at some options. When you find one you’re happy with (the MIT license is highly recommended), drop it in a file called LICENSE
in the root of your project.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Gem::Specification.new do |spec|
spec.name = 'puppet-lint-trailing_newline-check'
spec.version = '1.0.0'
spec.homepage = 'https://github.com/rodjek/puppet-lint-trailing_newline-check'
spec.license = 'MIT'
spec.author = 'Tim Sharpe'
spec.email = 'tim@sharpe.id.au'
spec.files = Dir[
'README.md',
'LICENSE',
'lib/**/*',
'spec/**/*',
]
spec.test_files = Dir['spec/**/*']
spec.summary = 'A puppet-lint plugin to check file endings.'
spec.description = <<-EOF
A puppet-lint plugin to check that manifest files end with a newline.
EOF
spec.add_dependency 'puppet-lint', '~> 1.0'
spec.add_development_dependency 'rspec', '~> 3.0'
spec.add_development_dependency 'rspec-its', '~> 1.0'
spec.add_development_dependency 'rspec-collection_matchers', '~> 1.0'
spec.add_development_dependency 'rake'
end
As puppet-lint plugins are distributed as Ruby Gems, you need to have
a gemspec
file which holds all the metadata about your Gem and is used when
packaging it up. The contents of this file are pretty self-explanitory however
if there is anything above that doesn’t make sense, check the
RubyGems Specification
Reference.
A few interesting lines:
~> 1.0
.
The pessimistic version operator (~>
) here means that it will match any
version number between 1.0.0 and 2.0.0. The reason we put the upper bound there
is that under the rules of Semantic Versioning a bump in
the major version number means a backward incompatible API change and there’s
a good chance the plugin won’t work.add_dependency
, these gems will not be installed when you run
gem install
.1
2
3
4
5
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
task :default => :spec
rake
is an ersatz make
written in Ruby and is the standard method of
automating tasks in Ruby projects. In this case, you are going to use it to
easily run the test suite.
rake spec
).rake
without
any arguments.1
2
3
require 'puppet-lint'
PuppetLint::Plugins.load_spec_helper
spec_helper.rb
by convention is where you configure RSpec and prepare any
requirements your tests may have.
spec_helper.rb
includes a number of helpful matchers that
make it very easy to test plugins, so you should make them available to our
plugin too.1
2
3
source 'https://rubygems.org'
gemspec
Bundler is a dependency manager tool for Ruby projects and it reads its
configuration from Gemfile
.
gemspec
file, saving us from having to define them all twice.Now that our Gemfile
is in place, tell bundler to install everything
needed to write our plugin.
Author’s Note: I manually specify a path for bundler to install the gems into rather than
using the default behaviour which is to install them into $BUNDLE_PATH
or
$GEM_HOME
. This way everything is contained nicely in my project directory.
1
2
3
/.bundle/
/vendor/gems/
/Gemfile.lock
At this point you should have a lot of files in your project directory, but you don’t want to commit all these into the repository.
--path
if you specified one).At this point, your code should look like this.
Now that all the setup work is done, you can start actually writing some code. You’re going to develop this module in a test driven manner, meaning you write tests to describe what the check should find before diving into the fun stuff.
First, create the folder where the tests will live
This directory structure is important as the magic that you imported from puppet-lint’s spec_helper.rb into the spec_helper.rb is only activated for files under this path.
Now, for the first tests. The check name is going to be trailing_newline
, so
tests will go in spec/puppet-lint/plugins/check_trailing_newline_spec.rb
.
The first thing to do in any test file is require our spec_helper.rb
file.
1
2
3
4
5
require 'spec_helper'
describe 'trailing_newline' do
# tests will go here
end
On line 3, you’ll note that we’re telling rspec which check to test. It’s important that this string matches the name of check or rspec will have no idea which check it should be running.
Fortunately, this check only needs two test cases: what should happen when the code ends with a newline and what happens when it doesn’t.
1
2
3
4
5
6
7
8
9
10
11
12
13
describe 'trailing_newline' do
let(:msg) { 'expected newline at the end of the file' }
context 'with fix disabled' do
context 'code ending with a newline' do
let(:code) { "'test'\n" }
it 'should not detect any problems' do
expect(problems).to have(0).problems
end
end
end
end
--fix
mode is disabled.it
block, rspec takes theproblems
.
This all happens automatically for you so that all you have to do is use the various matchers
to check the results are what you want. In this case, you are using the have
matcher
to check that the test didn’t return any results.Testing that valid code doesn’t passes without errors is all well and good, but you really want to test that the check can also detect problems.
1
2
3
4
5
6
7
8
9
10
11
context 'code not ending with a newline' do
let(:code) { "'test'" }
it 'should detect a single problem' do
expect(problems).to have(1).problem
end
it 'should create a warning' do
expect(problems).to contain_warning(msg).on_line(1).in_column(6)
end
end
At this point, the only unfamiliar thing in the above block should be the
contain_warning
matcher on line 9. This is a bit of syntactic sugar that
puppet-lint’s spec_helper
gives you. contain_warning
and contain_error
take a single argument which is the expected message returned by your check (we
defined this in the “Getting Started” section above. In addition, it also has
two methods that you can chain on the end to test the line (on_line
) and
column (in_column
) that your check thinks the problem is on.
At this point, the test file should look like this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
require 'spec_helper'
describe 'trailing_newline' do
let(:msg) { 'expected newline at the end of the file' }
context 'with fix disabled' do
context 'code not ending with a newline' do
let(:code) { "'test'" }
it 'should detect a single problem' do
expect(problems).to have(1).problem
end
it 'should create a warning' do
expect(problems).to contain_warning(msg).on_line(1).in_column(6)
end
end
context 'code ending with a newline' do
let(:code) { "'test'\n" }
it 'should not detect any problems' do
expect(problems).to have(0).problems
end
end
end
end
If you run your tests right now, you should see a nasty block of errors because the check doesn’t actually exist yet.
At this point, your code should look like this.
Now for the fun bit, actually writing the check code!
First, create the directory where our check will live (you’ll note it’s
exactly the same as where we put our tests, but under lib/
instead of
spec/
).
Next, define our new check (in
lib/puppet-lint/plugins/check_trailing_newline.rb
)
1
2
3
4
PuppetLint.new_check(:trailing_newline) do
def check
end
end
If you save and run the tests again now, you’ll see that we only have 2 failures this time. Now that the check object has been defined, our check that code ending with a newline causes no alerts passes.
The first thing the check needs to do is grab the last token in the file and
check if it is a newline. You can do this by accessing the tokens
array,
which is an array of PuppetLint::Lexer::Token objects representing the
tokenised contents of the manifest.
1
2
3
4
5
6
def check
last_token = tokens.last
unless last_token.type == :NEWLINE
end
end
Now create a warning if the last token is not a newline.
1
2
3
4
5
6
7
8
9
10
11
def check
last_token = tokens.last
unless last_token.type == :NEWLINE
notify :warning, {
:message => 'expected newline at the end of the file',
:line => last_token.line,
:column => manifest_lines.last.length,
}
end
end
Here, we introduce manifest_lines
which is exactly what it sounds like - the
manifest being parsed, split into an array of lines. It’s not normally used in
checks but in this case it’s a quick way of finding the column number of the
end of the last line.
Run your tests again and everything should now pass.
At this point, your code should look like this.
As with the check logic, you should start by writing tests.
Add a new context to the describe
block in your spec file and some before
and after
hooks to enable and disable the fix functionality.
1
2
3
4
5
6
7
8
9
context 'with fix enabled' do
before do
PuppetLint.configuration.fix = true
end
after do
PuppetLint.configuration.fix = false
end
end
Next, add some specs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
context 'code not ending in a newline' do
let(:code) { "'test'" }
it 'should only detect a single problem' do
expect(problems).to have(1).problem
end
it 'should fix the problem' do
expect(problems).to contain_fixed(msg).on_line(1).in_column(6)
end
it 'should add a newline to the end of the manifest' do
expect(manifest).to eq("'test'\n")
end
end
context 'code ending in a newline' do
let(:code) { "'test'\n" }
it 'should not detect any problems' do
expect(problems).to have(0).problems
end
it 'should not modify the manifest' do
expect(manifest).to eq(code)
end
end
These specs should look pretty familiar to you. The only new thing introduced
here is the manifest
helper which contains the rendered puppet manifest after
it has gone through the fixing process.
If you run the tests now, you should have a few new failures.
At this point, your code should look like this.
The first thing you need to do is define a fix
method in
lib/puppet-lint/plugins/check_trailing_newlines.rb
which will be passed the
problem hash generated by notify
in your check
method.
1
2
def fix(problem)
end
Inside this method, you can modify the tokens
array to fix problems
as necessary. In this case, all you need to do is append a newline token to the
array.
1
2
3
def fix(problem)
tokens << PuppetLint::Lexer::Token.new(:NEWLINE, "\n", 0, 0)
end
Run your tests again and everything should be working.
At this point, your code should look like this.
gem build puppet-lint-<your check>-check.gemspec
) and push
it up to RubyGems (gem push
).For more information, check out the API reference and Token reference.