class PDQTest::Puppet

Constants

CLASS_RE
CONTAINER_PATHS

platform paths

DEFAULT_FACTS_C
EXAMPLES_DIR
FIXTURES
HOST_PATHS

path for common things on the host computer running pdqtest (vm, laptop, etc)

MANIFESTS_DIR
METADATA
SETTINGS
TMP_PUPPETFILE
XATS_TESTS

statics

Public Class Methods

class2filename(c) click to toggle source
# File lib/pdqtest/puppet.rb, line 204
def self.class2filename(c)
  if c == module_name
    f = "#{MANIFESTS_DIR}#{File::ALT_SEPARATOR||File::SEPARATOR}init.pp"
  else
    f = c.gsub(module_name, MANIFESTS_DIR).gsub('::', (File::ALT_SEPARATOR||File::SEPARATOR)) + '.pp'
  end

  f
end
cp(key) click to toggle source
# File lib/pdqtest/puppet.rb, line 91
def self.cp(key)
  CONTAINER_PATHS[Util.host_platform][key] ||
      raise("missing variable CONTAINER_PATHS[#{Util.host_platform}][#{key}]")
end
debug_arg() click to toggle source
# File lib/pdqtest/puppet.rb, line 409
def self.debug_arg
  @@debug ? "--debug --trace --evaltrace" : ""
end
filename2class(f) click to toggle source
# File lib/pdqtest/puppet.rb, line 214
def self.filename2class(f)
  # strip any leading `./`
  f.sub!(/^\.\//,'')

  if f == "#{MANIFESTS_DIR}#{File::ALT_SEPARATOR||File::SEPARATOR}init.pp"
    c = module_name
  else
    c = f.gsub(MANIFESTS_DIR, "#{module_name}").gsub(File::ALT_SEPARATOR||File::SEPARATOR, '::').gsub('.pp','')
  end

  c
end
find_classes() click to toggle source

find the available classes in this module

# File lib/pdqtest/puppet.rb, line 241
def self.find_classes()
  mod_name = module_name
  classes = []
  if Dir.exists?(MANIFESTS_DIR)
    Find.find(MANIFESTS_DIR) do |m|
      if m =~ /\.pp$/
        # check the file contains a valid class
        if ! File.readlines(m).grep(CLASS_RE).empty?
          # Class detected, work out class name and add to list of found classes
          classes << filename2class(m)
        else
          $logger.info "no puppet class found in #{m}"
        end
      end
    end
  end

  classes
end
find_examples() click to toggle source
# File lib/pdqtest/puppet.rb, line 227
def self.find_examples()
  examples = []
  if Dir.exists?(EXAMPLES_DIR)
    Find.find(EXAMPLES_DIR) do |e|
      if ! File.directory?(e) && ! File.readlines(e).grep(setting(:magic_marker_re)).empty?
        examples << e
      end
    end
  end
  $logger.info "examples to run" + examples.to_s
  examples
end
fixtures_yml() click to toggle source

Regenerate .fixtures.yml from metadata github.com/puppetlabs/puppetlabs_spec_helper#using-fixtures The format looks like this:

“`

fixtures:
  forge_modules:
    stdlib:
      repo: "puppetlabs/stdlib"
      ref: "2.6.0"

Note that ref doesn't accept a range like metadata.json does, but then r10k doesn't follow dependencies anyway so may as well just code a static known good working version in it and not worry —> use a known good version for testing, not a range `metadata.json`

# File lib/pdqtest/puppet.rb, line 170
def self.fixtures_yml
  fixtures = {
      "fixtures" => {
          "forge_modules" => {}
      }
  }
  module_metadata["dependencies"].each do |dep|
      forge_name  = dep["name"]
      puppet_name = dep["name"].split("-")[1]
      ref         = dep["version_requirement"]

      fixtures["fixtures"]["forge_modules"][puppet_name] = {
        "repo" => forge_name,
        "ref"  => ref,
      }
  end

  # now we have our list of fixtures from `metadata.json`, merge it with any
  # exiting .fixtures.yml content to preserve any git fixtures that may have
  # been added manually. Clobber/update any existing content that is NOT
  # from `metadata.json` while preserving things that are from git. If we
  # have ended up declaring different versions of the same module then its
  # up to user's to resolve this by removing the dependency from either
  # `metadata.json` or `.fixtures.yml`. You shouldn't be depending on git
  # resources in `metadata.json` anyway so this shoudln't be an issue
  if File.exist?(FIXTURES)
    existing_fixtures = YAML.load_file(FIXTURES)
    existing_fixtures.deep_merge(fixtures)
    fixtures = existing_fixtures
  end

  File.open(FIXTURES, 'w') { |f| YAML.dump(fixtures, f) }
end
get_bats_executed() click to toggle source
# File lib/pdqtest/puppet.rb, line 118
def self.get_bats_executed
  @@bats_executed
end
get_setup_executed() click to toggle source
# File lib/pdqtest/puppet.rb, line 122
def self.get_setup_executed
  @@setup_executed
end
hp(key) click to toggle source
# File lib/pdqtest/puppet.rb, line 96
def self.hp(key)
  HOST_PATHS[Util.host_platform][key] ||
      raise("missing variable HOST_PATHS[#{Util.host_platform}][#{key}]")
end
info() click to toggle source
# File lib/pdqtest/puppet.rb, line 417
def self.info
  $logger.info "Parsed module name: #{module_name}"
end
install_modules() click to toggle source

extract a Puppetfile from metadata.json and install modules using r10k

# File lib/pdqtest/puppet.rb, line 422
def self.install_modules()
  json = JSON.parse(File.read(METADATA))
  puppetfile = []
  if json.has_key?("dependencies")
    json["dependencies"].each { |dependency|
      line = "mod '#{dependency['name']}'"
      if dependency.has_key?("version_requirement")
        # R10K supports specifc named version or 'latest', not the rich versions defined in metadata. To make this
        # work we will drop any version that specifies a range and just install the latest
        if dependency['version_requirement'].match?(/^\d/)
          line += ", '#{dependency['version_requirement']}'"
        end
      end
      puppetfile << line
    }
  end

  File.open(TMP_PUPPETFILE, "w") do |f|
    f.puts(puppetfile)
  end

  PDQTest::Emoji.emoji_message(:slow, "I'm downloading The Internet, please hold...")

  cmd = "bundle exec r10k puppetfile install --verbose --moduledir ./spec/fixtures/modules --puppetfile #{TMP_PUPPETFILE}"
  status = system(cmd)

  if ! status
    $logger.error "Failed to run the R10K command: #{cmd}"
  end

  status
end
module_metadata() click to toggle source
# File lib/pdqtest/puppet.rb, line 130
def self.module_metadata
  if File.exist? METADATA
    file = File.read(METADATA)
    JSON.parse(file)
  else
    raise("Puppet metadata not found at #{METADATA} - not a valid puppet module")
  end
end
module_name() click to toggle source
# File lib/pdqtest/puppet.rb, line 145
def self.module_name
  module_metadata['name'].split(/[\/-]/)[1]
end
os_support() click to toggle source
# File lib/pdqtest/puppet.rb, line 149
def self.os_support
  module_metadata['operatingsystem_support'] || []
end
puppet_apply(example) click to toggle source
# File lib/pdqtest/puppet.rb, line 413
def self.puppet_apply(example)
  "cd #{Docker.test_dir} ; #{setting(:puppet)} apply #{debug_arg} --detailed-exitcodes #{example}"
end
puppet_strings() click to toggle source
# File lib/pdqtest/puppet.rb, line 508
def self.puppet_strings
  # Runs in the context of _our_ PDQTest bundle so doesn't need clean env
  system("bundle exec puppet strings generate --format markdown")
end
reset_bats_executed() click to toggle source
# File lib/pdqtest/puppet.rb, line 110
def self.reset_bats_executed
  @@bats_executed = []
end
reset_setup_executed() click to toggle source
# File lib/pdqtest/puppet.rb, line 114
def self.reset_setup_executed
  @@setup_executed = []
end
run(cc, container, example=nil) click to toggle source
# File lib/pdqtest/puppet.rb, line 353
def self.run(cc, container, example=nil)
  # we must always have ./spec/fixtures/modules because we need to create a
  # symlink back to the main module inside here...
  # (spec/fixtures/modules/foo -> /testcase)
  if ! Dir.exists?('spec/fixtures/modules')
    $logger.info
      "creating empty spec/fixtures/modules, if you module fails to run due "
      "to missing dependencies run `make` or `pdqtest all` to retrieve them"
    FileUtils.mkdir_p('spec/fixtures/modules')
  end

  status = true
  $logger.info "...running container setup"
  setup_start = Time.now
  res = PDQTest::Execution.exec(cc, container, setup)
  setup_end = Time.now
  status &= PDQTest::Execution.exec_status(res)
  if Util.is_windows
    # write a script to allow user to update modules
    $logger.info "wasted #{((setup_end - setup_start))} seconds of your life on windows tax"
    File.open("refresh.ps1", 'w') do |file|
      res[:REAL_CMD].each do |c|
        file.puts("#{c[0]} #{c[1]} \"#{c[2]}\"")
      end
    end
    Emoji.emoji_message(
        :shame,
        "run refresh.ps1 to update container after changing files on host!",
        ::Logger::WARN)
  end
  if status
      $logger.info "...run tests"
      if example
        status &= run_example(cc, container, example)
        if ! status
          $logger.error "Example #{example} failed!"
        end
      else
        find_examples.each { |e|
          if status
            status &= run_example(cc, container, e)
            if ! status
              $logger.error "Example #{e} failed! - skipping rest of tests"
            end
          end
        }
      end
  else
    PDQTest::Execution.log_all(res)
    $logger.error "Error running puppet setup, see previous error, command was: #{res[:REAL_CMD]}"
  end

  PDQTest::Emoji.partial_status(status, 'Puppet')
  status
end
run_example(cc, container, example) click to toggle source
# File lib/pdqtest/puppet.rb, line 309
def self.run_example(cc, container, example)
  $logger.info "testing #{example}"
  status = false

  if setup_test(cc, container, example)

    # see if we should run a bats test before running puppet
    if xats_test(cc, container, example, setting(:before_suffix))

      # run puppet apply - 1st run
      res = PDQTest::Execution.exec(cc, container, puppet_apply(example))
      PDQTest::Execution.log_out(res)
      if PDQTest::Execution.exec_status(res, true) # allow 2 as exit status

        if @@skip_second_run
          $logger.info "Skipping idempotency check as you requested..."

          # check the system right now since puppet ran OK once
          status = xats_test(cc, container, example, setting(:after_suffix))
        else
          # run puppet apply - 2nd run (check for idempotencey/no more changes)
          res = PDQTest::Execution.exec(cc, container, puppet_apply(example))
          PDQTest::Execution.log_out(res)

          # run the bats test if nothing failed yet
          if PDQTest::Execution.exec_status(res) # only allow 0 as exit status
            status = xats_test(cc, container, example, setting(:after_suffix))
          else
            $logger.error "Not idempotent: #{example}"
          end
        end
      else
        $logger.error "First puppet run of #{example} failed (status: #{res[:STATUS]})"
      end
    else
      $logger.error "#{setting(:name)} tests to run before #{example} failed"
    end
  else
    $logger.error "Setup script for #{example} failed (see previous error)"
  end

  status
end
save_module_metadata(metadata) click to toggle source
# File lib/pdqtest/puppet.rb, line 139
def self.save_module_metadata(metadata)
  File.open(METADATA,"w") do |f|
    f.write(JSON.pretty_generate(metadata))
  end
end
set_debug(debug) click to toggle source
# File lib/pdqtest/puppet.rb, line 126
def self.set_debug(debug)
  @@debug = debug
end
setting(key) click to toggle source
# File lib/pdqtest/puppet.rb, line 101
def self.setting(key)
  SETTINGS[Util.host_platform][key] ||
      raise("missing variable SETTINGS[#{Util.host_platform}][#{key}]")
end
setup() click to toggle source
# File lib/pdqtest/puppet.rb, line 455
def self.setup
  commands = []

  # link testcase module
  commands << Util.mk_link(
      Util.joinp(cp(:module_dir), module_name),
      PDQTest::Docker.test_dir
  )


  # link dependency modules
  sfm = Util.joinp("spec", "fixtures", "modules")
  if ! File.exist? sfm
    raise("Modules directory does not exist - please run unit tests first to create it")
  end
  Dir.entries(sfm).select { |entry|
    File.directory?(Util.joinp(sfm, entry)) && !(entry =='.' || entry == '..')
  }.reject { |entry|
    # do not copy the symlink of ourself (pdk creates it)
    entry == module_name
  }.each { |entry|
    commands << Util.mk_link(
        Util.joinp(cp(:module_dir), entry),
        Util.joinp(PDQTest::Docker.test_dir, sfm, entry)
    )
  }


  # link hieradata
  if Dir.exist? hp(:hiera_dir)
    commands << Util.mk_link(
        cp(:hiera_dir),
        Util.joinp(Docker.test_dir, hp(:hiera_dir))
    )
  end

  # link hiera.yaml
  if File.exist? hp(:hiera_yaml)
    commands << Util.mk_link(
        cp(:hiera_yaml),
        Util.joinp(Docker.test_dir, hp(:hiera_yaml))
    )
  end

  # link external facts
  commands << Util.mk_link(
      Util.joinp(cp(:facts_dir), DEFAULT_FACTS_C),
      Util.joinp(Docker.test_dir, hp(:default_facts))
  )
  commands

end
setup_test(cc, container, example) click to toggle source
# File lib/pdqtest/puppet.rb, line 285
def self.setup_test(cc, container, example)
  setup_script = Util.joinp(XATS_TESTS, test_basename(example)) + setting(:setup_suffix)
  if File.exists?(setup_script)
    script = File.read(setup_script)
    $logger.debug "setup script: \n #{script}"
    if script.strip.empty?
      $logger.info "skipping empty setup script at #{setup_script}"
      status = true
    else
      $logger.info "Setting up test for #{example}"

      res = PDQTest::Execution.exec(cc, container, script)
      status = PDQTest::Execution.exec_status(res)
      PDQTest::Execution.log_all(res)
    end
    @@setup_executed << setup_script
  else
    $logger.info "no setup file for #{example} (should be in #{setup_script})"
    status = true
  end

  status
end
skip_second_run(skip_second_run) click to toggle source
# File lib/pdqtest/puppet.rb, line 106
def self.skip_second_run(skip_second_run)
  @@skip_second_run = skip_second_run
end
test_basename(t) click to toggle source
# File lib/pdqtest/puppet.rb, line 261
def self.test_basename(t)
  # remove any leading `./`
  t.sub!(/^\.\//, '')
  # remove examples/ and .pp
  # eg ./examples/apache/mod/mod_php.pp --> apache/mod/mod_php
  t.gsub(EXAMPLES_DIR + '/','').gsub('.pp','')
end
xats_test(cc, container, example, suffix) click to toggle source
# File lib/pdqtest/puppet.rb, line 269
def self.xats_test(cc, container, example, suffix)
  testcase = Util.joinp(XATS_TESTS, test_basename(example) + suffix)
  if File.exists?(testcase)
    $logger.info "*** #{setting(:name)} test **** #{setting(:test_cmd)} #{testcase}"
    res = PDQTest::Execution.exec(cc, container, "cd #{Docker.test_dir} ; #{setting(:test_cmd)} #{testcase}")
    status = PDQTest::Execution.exec_status(res)
    PDQTest::Execution.log_all(res)
    @@bats_executed << testcase
  else
    $logger.info "no #{suffix} tests for #{example} (should be at #{testcase})"
    status = true
  end

  status
end