#!/usr/bin/env perl ############################################################################# ## ## Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies). ## Contact: http://www.qt-project.org/legal ## ## This file is part of the Qt Toolkit. ## ## $QT_BEGIN_LICENSE:LGPL$ ## Commercial License Usage ## Licensees holding valid commercial Qt licenses may use this file in ## accordance with the commercial license agreement provided with the ## Software or, alternatively, in accordance with the terms contained in ## a written agreement between you and Digia. For licensing terms and ## conditions see http://qt.digia.com/licensing. For further information ## use the contact form at http://qt.digia.com/contact-us. ## ## GNU Lesser General Public License Usage ## Alternatively, this file may be used under the terms of the GNU Lesser ## General Public License version 2.1 as published by the Free Software ## Foundation and appearing in the file LICENSE.LGPL included in the ## packaging of this file. Please review the following information to ## ensure the GNU Lesser General Public License version 2.1 requirements ## will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ## ## In addition, as a special exception, Digia gives you certain additional ## rights. These rights are described in the Digia Qt LGPL Exception ## version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ## ## GNU General Public License Usage ## Alternatively, this file may be used under the terms of the GNU ## General Public License version 3.0 as published by the Free Software ## Foundation and appearing in the file LICENSE.GPL included in the ## packaging of this file. Please review the following information to ## ensure the GNU General Public License version 3.0 requirements will be ## met: http://www.gnu.org/copyleft/gpl.html. ## ## ## $QT_END_LICENSE$ ## ############################################################################# =head1 NAME 20-puppet-tests.t - run puppet system tests =head1 SYNOPSIS perl t/20-puppet-tests.t [test1 [test2 ...]] Runs "puppet --noop" over some tests. A test is a .pp file located under a "tests" subdirectory of a puppet module. By default, all tests in the current repository are run. If additional arguments are passed on the command-line, they are interpreted as regular expressions; only the tests whose filenames match the given tests will be run. This makes it easy to run all the tests for a specific module (e.g. C to run all tests for 'baselayout' module). =head1 CREATING A TEST The most basic test simply instantiates one or more resources of a given type. For example, modules/quux/tests/config.pp might instantiate a quux::config item: # basic verification that quux::config doesn't crash quux::config { "test config": path => "/etc/quux/quux.conf", key => "Name", value => "Quux number 1", } This tests at least that puppet is able to instantiate resources of the given type without any fatal error. This is superior to a syntax check only, because it will detect error conditions such as unsatisfiable or circular dependencies. Using the selftest::expect type defined in this repository, it is also possible to perform some verification on the output generated by puppet. For example: quux::config { "test config": path => "/etc/quux/quux.conf", key => "Name", value => "Quux number 1", } selftest::expect { "creates /etc/quux and /etc/quux/quux.conf": output => [ # regular expressions; output must match all of these, in this order 'File\[/etc/quux\]', 'File\[/etc/quux/quux\.conf\]', ] } The above test will succeed only if 'puppet apply --noop' exits with a zero exit code _and_ does something with File[/etc/quux] and File[/etc/quux/quux.conf] (in that order). This is a superior test than not verifying any output at all, but it is difficult to be more precise than "does something" when verifying the output because the output of puppet is not designed to be parsed in this way. The selftest::expect_no_warnings type may be used to test that no warnings occur during the processing of a manifest: quux::config { "test config": } selftest::expect_no_warnings { "empty quux::config has no warnings": } The selftest::skip_all type may be used to skip a test. This should be placed as early as possible in the test file. For example: if $::operatingsystem == 'windows' { selftest::skip_all { "not supported on Windows": } } # ... rest of test goes here Note that the top-level test .pp file must be entirely parseable - puppet must survive at least until the skip_all type is activated. =head1 PERMISSION PROBLEMS Typically, puppet is run as root, but clearly that is not desirable for running these tests. Most puppet code will work fine in --noop mode regardless of whether or not puppet is running as root; however, a few things (e.g. an 'exec' resource with a 'user' parameter other than root) will abort if puppet is not running as root, even in --noop mode. In this case, this script will attempt to run puppet with the 'fakeroot' utility, which pretends to be root. If fakeroot is not available, any test which appears to need root permission will be skipped. =cut use strict; use warnings; use v5.10; use Capture::Tiny qw( capture_merged ); use English qw( -no_match_vars ); use File::Basename; use File::Find::Rule; use File::Spec::Functions; use File::Temp qw( tempdir ); use File::chdir; use FindBin; use List::MoreUtils qw( natatime ); use Test::More; BEGIN { do( catfile( $FindBin::Bin, '..', 'sync_and_run.pl' ) ) || die "load sync_and_run.pl: $! $@"; QtQA::Puppet::SyncAndRun->import(); # $DIR should point to sync_and_run.pl's directory, not this directory $QtQA::Puppet::SyncAndRun::DIR = catfile( $FindBin::Bin, '..' ); } # puppet wants a writable var dir to store its state, even in --noop mode my $PUPPET_VAR_DIR = tempdir( 'qtqa-puppet-test.XXXXXX', CLEANUP => 1, TMPDIR => 1 ); # pattern matching error messages which indicate root permission is required my $NEED_ROOT_ERRORS = qr{ \QOnly root can execute commands as other users\E # add more here as discovered }xms; # 1 if we have a working fakeroot command sub have_fakeroot { if ($OSNAME =~ m{win32}i) { return 0; } qx(fakeroot -v 2>&1); return ($? == 0); } sub find_all_test_pp_files { local $CWD = $QtQA::Puppet::SyncAndRun::DIR; my @files = qx(git ls-files -- "*/tests/*.pp"); if ($? != 0) { die "'git ls-files' exited with status $?"; } chomp @files; if (@files < 2) { local $LIST_SEPARATOR = "\n"; die "found too few files, something must be wrong.\nfiles: @files"; } return sort @files; } sub test_one_pp_file { my ($filename) = @_; my @cmd = ( find_puppet(), 'apply', '--verbose', '--color', 'false', '--logdest', 'console', '--noop', '--confdir', '.', ($OSNAME =~ m{win32}i ? ('--config', 'puppet-win32.conf') : ()), '--vardir', $PUPPET_VAR_DIR, # make sure we don't load any of the node's usual modules; just start as an empty node '--node_terminus', 'plain', '--modulepath', 'modules', $filename, ); my $status; my $output = capture_merged { $status = system( @cmd ); }; my $skip; if ($status != 0 && $output =~ $NEED_ROOT_ERRORS) { if (have_fakeroot()) { # On Linux and Mac, fakeroot resolves some issues with --noop tests such as # "Only root can execute commands as other users"... unshift( @cmd, 'fakeroot', '--' ); $output = capture_merged { $status = system( @cmd ); }; } else { $skip = "$filename seems to require root permissions and you don't have fakeroot"; } } my $raw_output = $output; subtest "$filename OK" => sub { if ($skip) { plan skip_all => $skip; } if ($output =~ m{\btest-skip-all: ([^\n]+)\n}ms) { plan skip_all => $1; } my @expected_output; while ($output =~ m{\btest-expect: ([^:\n]*): ([^\n]+)\n}msp) { my $expected = [$1, $2]; push @expected_output, $expected; is( $output =~ s{\Q${^MATCH}\E}{}, 1 ) || return; } my $no_warnings = 0; while ($output =~ m{\btest-expect-no-warnings: ([^\n]+)\n}msp) { $no_warnings = $1; is( $output =~ s{\Q${^MATCH}\E}{}, 1 ) || return; } my @warnings; while ($output =~ m{\bwarning: ([^\n]+)\n}msg) { push @warnings, $1; } if ($no_warnings) { local $LIST_SEPARATOR = "\n "; ok( !@warnings, $no_warnings ) || diag( "output:\n $raw_output" ); } foreach my $expected (@expected_output) { my ($name, $pattern) = @{ $expected }; $name ||= "content matches $pattern"; like( $output, qr{$pattern}, $name ); } if (!is( $status, 0, "$filename OK" )) { diag( $output ); return; } }; return; } sub test_all_pp_files { local $CWD = $QtQA::Puppet::SyncAndRun::DIR; # 'state' dir must exist even in --noop mode (at least for puppet 2.6) mkdir( catfile( $PUPPET_VAR_DIR, 'state' ) ) || die "mkdir: $!"; my @pp = find_all_test_pp_files(); # filter tests by the patterns passed on cmdline while (my $pattern = shift @ARGV) { my $count1 = @pp; @pp = grep { $_ =~ m{$pattern} } @pp; my $count2 = @pp; diag( "$pattern - selected $count2 tests (of $count1)\n" ); } plan tests => scalar(@pp); foreach my $pp (@pp) { test_one_pp_file( $pp ); } return; } test_all_pp_files(); done_testing();