puppet-unit-tests 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65355d1254f33d3c4a19d2407ca278b7c3e29e7b1240f9a2e5489e62eb90eeb7
4
- data.tar.gz: 9823aac83e42222e742682ab4f95316f62e2ba4d9651ef3c7bbbeba1564e828a
3
+ metadata.gz: 9ac6abaec554f872d858b6cdbea38ca53c746127f2106d2707556552f5cd4e8d
4
+ data.tar.gz: a2e17acfd871c41c208de48b941132ff585b5912a1e3e51e75f532e9e15c2392
5
5
  SHA512:
6
- metadata.gz: fe22c553caef295c6ce078abd9bd19329671db34cfa525041868dabe3a0dfb6f785f45792f548d6fba24aa67e40add6095a68905b065d7b4e23e15d38a532197
7
- data.tar.gz: 807ed15b9a99e640ad4fe77f8c619806c11d7a27eed56defef1078a3c52517d7dcd88498df9d6d392612a42e4b37b5219fa2f4430a2f76ce79656038209b6f98
6
+ metadata.gz: cff991850a20eef0483e309a21cd146138fc5f78c28ea8629c65f1b999d8bd3321a9888a9971cfca1125fcfd46518efe106aadf53011b02fd821cdeeee5750a0
7
+ data.tar.gz: 25322cbc2df1351fc9b489494492407a4cdb83c9db1108ef21b970ce6036853e8aef4ca95f37cb2e1b5037f1048664a40bad5a034d69c81b36b41ed5eb761504
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
+ # Changelog
2
+
1
3
  ## [Unreleased]
2
4
 
5
+ ## [0.4.0] - 2025-09-11
6
+
7
+ ### Added
8
+
9
+ - Allow empty usecases for class
10
+ - Document minimal class usecase
11
+ - Implement testing of defined types
12
+ - Allow testing of functions
13
+
14
+ ### Fixed
15
+
16
+ - Handle case where use wants to check that an error occurs but none do
17
+
18
+ ## [0.3.1] - 2025-09-11
19
+
20
+ - Use changelog
21
+
22
+ ## [0.3.0] - 2025-09-11
23
+
24
+ - Display errors in red
25
+
26
+ ## [0.2.0] - 2025-09-11
27
+
28
+ - Document rake task in README
29
+ - Fix loading of custom Ruby functions from Puppet environment
30
+ - Run tests in temporary directory
31
+
3
32
  ## [0.1.0] - 2025-09-11
4
33
 
5
34
  - Initial release
data/README.md CHANGED
@@ -18,9 +18,17 @@ Install this gem on your development and CI/CD hosts:
18
18
 
19
19
  Create a "test" directory under your module root. Create subdir "class" for
20
20
  classes, "define" for defined types. Create YAML files inside this directories
21
- with usecases and expected results.
21
+ with usecases and expected results. Files must be named exactly as the Puppet
22
+ object they should test. For example, usecases for a class "foo::bar" should be
23
+ stored in file "tests/class/foo::bar.yaml".
22
24
 
23
- TODO: write a minimal example here and create an "examples" directory.
25
+ A minimal file for a class:
26
+
27
+ ```yaml
28
+ ---
29
+ usecases:
30
+ 'it compiles':
31
+ ```
24
32
 
25
33
  Add these lines to your Rakefile:
26
34
 
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Puppet::Functions.create_function(:'test_data') do
3
+ Puppet::Functions.create_function(:test_data) do
4
4
  dispatch :test_data do
5
- param 'Struct[{uri=>String[1]}]', :options
6
- param 'Puppet::LookupContext', :context
5
+ param "Struct[{uri=>String[1]}]", :options
6
+ param "Puppet::LookupContext", :context
7
7
  end
8
8
 
9
9
  argument_mismatch :missing_uri do
10
- param 'Hash', :options
11
- param 'Puppet::LookupContext', :context
10
+ param "Hash", :options
11
+ param "Puppet::LookupContext", :context
12
12
  end
13
13
 
14
14
  def test_data(options, _context)
15
- uri = options['uri']
15
+ uri = options["uri"]
16
16
  hiera_data = Thread.current[:hiera_data]
17
17
  return {} unless hiera_data
18
18
 
@@ -21,6 +21,6 @@ Puppet::Functions.create_function(:'test_data') do
21
21
 
22
22
  def missing_uri(_options, _context)
23
23
  "one of 'uri' or 'uris' must be declared in hiera.yaml " \
24
- 'when using this data_hash function.'
24
+ "when using this data_hash function."
25
25
  end
26
26
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetUnitTests
4
+ class Error < StandardError; end
5
+
6
+ # Error found in user data files before starting to run tests.
7
+ class UserDataError < Error
8
+ attr_accessor :path
9
+ end
10
+ end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rake'
4
- require 'rake/tasklib'
3
+ require "rake"
4
+ require "rake/tasklib"
5
5
 
6
- require_relative 'runner'
6
+ require_relative "runner"
7
7
 
8
8
  module PuppetUnitTests
9
9
  # Simple rake task to execute tests.
@@ -12,14 +12,20 @@ module PuppetUnitTests
12
12
  super()
13
13
  @name = name
14
14
 
15
- desc 'Run Puppet unit tests' unless ::Rake.application.last_description
15
+ desc "Run Puppet unit tests" unless ::Rake.application.last_description
16
16
  task(name, *args) do |_, task_args|
17
17
  yield(*[self, task_args].slice(0, task_block.arity)) if task_block
18
18
  runner = Runner.new
19
- errors = runner.run
20
- unless errors.empty?
21
- runner.format_errors(errors)
22
- abort("Puppet unit tests failed!")
19
+ begin
20
+ errors = runner.run
21
+ unless errors.empty?
22
+ runner.format_errors(errors)
23
+ abort("Puppet unit tests failed!")
24
+ end
25
+ rescue UserDataError => e
26
+ warn("In file #{e.path}:")
27
+ warn(" #{e.message}")
28
+ abort("Error in YAML test files")
23
29
  end
24
30
  end
25
31
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rainbow'
3
+ require "rainbow"
4
4
 
5
5
  require_relative "test_environment"
6
6
  require_relative "test_data"
@@ -11,7 +11,7 @@ module PuppetUnitTests
11
11
  def initialize(assets_dir = nil)
12
12
  @env = nil
13
13
  @assets_dir = assets_dir || File.realpath(
14
- File.join(__dir__, '..', '..', 'assets')
14
+ File.join(__dir__, "..", "..", "assets")
15
15
  )
16
16
  @env_dir = nil
17
17
  end
@@ -20,7 +20,7 @@ module PuppetUnitTests
20
20
  setup
21
21
  all_errors = {}
22
22
 
23
- Dir.glob("tests/class/*.yaml") do |yaml_test_file|
23
+ Dir.glob("tests/{class,define,function}/*.yaml") do |yaml_test_file|
24
24
  test_data = TestData.parse(yaml_test_file)
25
25
  test_data.trusted_facts["certname"] ||= @env.nodename
26
26
  errors = test_data.check(@env)
@@ -28,7 +28,6 @@ module PuppetUnitTests
28
28
 
29
29
  all_errors[test_data.name] = errors
30
30
  end
31
- # TODO: test defines
32
31
  # TODO: test functions
33
32
  # TODO: test plans
34
33
 
@@ -52,13 +51,13 @@ module PuppetUnitTests
52
51
  private
53
52
 
54
53
  def setup
55
- @env_dir = Dir.mktmpdir('puppet-test-env')
56
- FileUtils.mkdir(File.join(@env_dir, 'modules'))
54
+ @env_dir = Dir.mktmpdir("puppet-test-env")
55
+ FileUtils.mkdir(File.join(@env_dir, "modules"))
57
56
  FileUtils.copy_entry(
58
- File.join(@assets_dir, 'test-env'),
57
+ File.join(@assets_dir, "test-env"),
59
58
  @env_dir
60
59
  )
61
- FileUtils.ln_s(Dir.pwd, File.join(@env_dir, 'modules'))
60
+ FileUtils.ln_s(Dir.pwd, File.join(@env_dir, "modules"))
62
61
  @env = TestEnvironment.new do |e|
63
62
  e.environmentpath = @env_dir
64
63
  e.hiera_config = File.join(@env_dir, "hiera.yaml")
@@ -32,7 +32,9 @@ module PuppetUnitTests
32
32
  type = File.basename(dir)
33
33
  name = File.basename(path, ".yaml")
34
34
  data = YAML.load_file(path)
35
- usecases = data["usecases"].map { |dn, du| TestUsecase.parse(dn, du) }
35
+ usecases = data["usecases"].map do |dn, du|
36
+ parse_usecase(dn, du, type)
37
+ end
36
38
  td = new(type, name, usecases)
37
39
  td.facts = data["facts"] || {}
38
40
  td.server_facts = data["server_facts"] || {}
@@ -42,6 +44,21 @@ module PuppetUnitTests
42
44
  td.stubs = data["stubs"] || []
43
45
 
44
46
  td
47
+ rescue UserDataError => e
48
+ e.path = path
49
+ raise e
50
+ end
51
+
52
+ private
53
+
54
+ def parse_usecase(name, data, type)
55
+ case type
56
+ when "class" then ClassUsecase.parse(name, data)
57
+ when "define" then DefineUsecase.parse(name, data)
58
+ when "function" then FunctionUsecase.parse(name, data)
59
+ else
60
+ raise Error, "usecase type #{type} not implemented"
61
+ end
45
62
  end
46
63
  end
47
64
 
@@ -65,20 +82,36 @@ module PuppetUnitTests
65
82
  private
66
83
 
67
84
  def check_usecase(env, uc)
85
+ errors = []
68
86
  if uc.error
69
- catalog_error(env, uc, Regexp.new(uc.error))
87
+ errors += with_catalog_error(env, uc, Regexp.new(uc.error))
70
88
  else
71
- catalog = catalog(env, uc)
72
- errors = []
73
- errors += uc.present.map { |rs| check_present(catalog, rs) }
74
- errors += uc.absent.map { |rs| check_absent(catalog, rs) }
75
-
76
- errors.flatten.compact
89
+ case uc
90
+ when ClassUsecase, DefineUsecase
91
+ with_compiler(env, uc) do |comp|
92
+ errors += uc.present.map { |rs| check_present(comp.catalog, rs) }
93
+ errors += uc.absent.map { |rs| check_absent(comp.catalog, rs) }
94
+ end
95
+ when FunctionUsecase
96
+ with_compiler(env, uc) do |comp|
97
+ errors += check_function_result(comp, uc.parameters, uc.result)
98
+ end
99
+ end
77
100
  end
101
+
102
+ errors.flatten.compact
78
103
  rescue Puppet::PreformattedError => e
79
104
  [e.arguments[:detail]]
80
105
  end
81
106
 
107
+ def check_function_result(compiler, parameters, expected)
108
+ scope = compiler.context_overrides[:global_scope]
109
+ actual = scope.call_function(@name, parameters)
110
+ return [] if actual == expected
111
+
112
+ ["expected result #{expected.inspect}, got #{actual.inspect}"]
113
+ end
114
+
82
115
  def check_present(catalog, rs)
83
116
  res_disp = "#{rs.type}[#{rs.title}]"
84
117
  res = catalog.resource(rs.type, rs.title)
@@ -98,28 +131,37 @@ module PuppetUnitTests
98
131
  def check_absent(catalog, rs)
99
132
  res_disp = "#{rs.type}[#{rs.title}]"
100
133
  res = catalog.resource(rs.type, rs.title)
101
- return [nil] unless res
102
- return [nil] if rs.parameters.all? { |k, v| res[k] == v }
134
+ return [] unless res
135
+ return [] unless rs.parameters.all? { |k, v| res[k] == v }
103
136
 
104
137
  ["#{res_disp} should be absent"]
105
138
  end
106
139
 
107
- def catalog(env, usecase)
108
- env.compile(
140
+ def with_compiler(env, usecase, &)
141
+ env.with_compiler(
109
142
  code(usecase),
110
143
  facts: @facts.merge(usecase.facts),
111
144
  server_facts: @server_facts.merge(usecase.server_facts),
112
- trusted_facts: @trusted_facts.merge(usecase.trusted_facts)
145
+ trusted_facts: @trusted_facts.merge(usecase.trusted_facts),
146
+ &
113
147
  )
114
148
  end
115
149
 
116
- def catalog_error(env, usecase, regexp)
117
- catalog(env, usecase)
150
+ def with_catalog_error(env, usecase, regexp)
151
+ with_compiler(env, usecase)
152
+
153
+ [
154
+ "expected to see error matching #{regexp.source.inspect} " \
155
+ "but got no error"
156
+ ]
118
157
  rescue Puppet::ParseError => e
119
158
  if regexp.match?(e.message)
120
159
  []
121
160
  else
122
- [e.message]
161
+ [
162
+ "expected to see error matching #{regexp.source.inspect} " \
163
+ "but got: #{e.message}"
164
+ ]
123
165
  end
124
166
  end
125
167
 
@@ -133,6 +175,11 @@ module PuppetUnitTests
133
175
  #{parameters.join(",\n ")}
134
176
  }
135
177
  EOPP
178
+ when "function"
179
+ parameters = uc.parameters.map(&:inspect)
180
+ <<~EOPP
181
+ $_test_result = #{@name}(#{parameters.join(", ")})
182
+ EOPP
136
183
  else raise "#{@type} not implemented"
137
184
  end
138
185
  end
@@ -53,15 +53,17 @@ module PuppetUnitTests
53
53
  @settings[:environmentpath] = Pathname.new(path).realpath
54
54
  end
55
55
 
56
- def compile(code, facts:, server_facts:, trusted_facts:, stubs: [])
56
+ def clear_compiler
57
+ Puppet.runtime[:facter].clear
58
+ Puppet.pop_context
59
+ Puppet.pop_context
60
+ end
61
+
62
+ def with_compiler(code, facts:, server_facts:, trusted_facts:, stubs: [])
57
63
  # TODO: Puppet.features.add(:posix) { true } if facts match UNIX
58
64
 
59
- code = stubs.map do |st|
60
- stub_path = @stubsdir.join("#{st}.pp")
61
- File.read(stub_path)
62
- end.join($RS) + code
65
+ code = format_stubs(stubs) + $RS + code
63
66
 
64
- Puppet.runtime[:facter].clear
65
67
  name = trusted_facts["certname"]
66
68
  node_facts = Puppet::Node::Facts.new(name, facts)
67
69
  node = Puppet::Node.new(name, parameters: {}, facts: node_facts)
@@ -76,19 +78,35 @@ module PuppetUnitTests
76
78
  node.add_server_facts(server_facts)
77
79
 
78
80
  Puppet[:code] = code
79
- catalog = Puppet::Resource::Catalog.indirection.find(
80
- node.name, use_node: node
81
+ compiler = Puppet::Parser::Compiler.new(node)
82
+ compiler.compile
83
+ loaders = Puppet::Pops::Loaders.new(@puppet_env)
84
+ Puppet.push_context(
85
+ {
86
+ loaders: loaders,
87
+ global_scope: compiler.context_overrides[:global_scope]
88
+ },
89
+ "set globals"
81
90
  )
91
+
82
92
  Puppet::Pops::Evaluator::DeferredResolver.resolve_and_replace(
83
- node.facts, catalog
93
+ node.facts, compiler.catalog
84
94
  )
85
- Puppet.pop_context
86
95
 
87
- catalog
96
+ yield compiler
97
+ ensure
98
+ Puppet.runtime[:facter].clear
88
99
  end
89
100
 
90
101
  private
91
102
 
103
+ def format_stubs(stubs)
104
+ stubs.map do |st|
105
+ stub_path = @stubsdir.join("#{st}.pp")
106
+ File.read(stub_path)
107
+ end.join($RS)
108
+ end
109
+
92
110
  def global_setup(settings)
93
111
  Puppet.runtime[:facter] = FakeFacter.new
94
112
  Puppet.settings.initialize_app_defaults(settings)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "error"
3
4
  require_relative "resource_spec"
4
5
 
5
6
  module PuppetUnitTests
@@ -19,6 +20,13 @@ module PuppetUnitTests
19
20
 
20
21
  def initialize(name)
21
22
  @name = name
23
+
24
+ @facts = {}
25
+ @server_facts = {}
26
+ @trusted_facts = {}
27
+ @hiera = {}
28
+ @settings = {}
29
+
22
30
  @present = []
23
31
  @absent = []
24
32
  @error = nil
@@ -27,6 +35,8 @@ module PuppetUnitTests
27
35
  class << self
28
36
  def parse(name, data_usecase)
29
37
  uc = new(name)
38
+ return uc unless data_usecase
39
+
30
40
  uc.facts = data_usecase["facts"] || {}
31
41
  uc.server_facts = data_usecase["server_facts"] || {}
32
42
  uc.trusted_facts = data_usecase["trusted_facts"] || {}
@@ -40,7 +50,7 @@ module PuppetUnitTests
40
50
  end
41
51
  end
42
52
  if data_usecase.key?("absent")
43
- uc.present = data_usecase["absent"].map do |rs|
53
+ uc.absent = data_usecase["absent"].map do |rs|
44
54
  ResourceSpec.parse(rs)
45
55
  end
46
56
  end
@@ -49,4 +59,60 @@ module PuppetUnitTests
49
59
  end
50
60
  end
51
61
  end
62
+
63
+ # A test usecase for a Puppet class.
64
+ class ClassUsecase < TestUsecase
65
+ end
66
+
67
+ # A test usecase for a Puppet defined type.
68
+ class DefineUsecase < TestUsecase
69
+ attr_accessor :title
70
+ attr_accessor :parameters
71
+
72
+ def initialize(name)
73
+ super
74
+
75
+ @title = nil
76
+ @parameters = {}
77
+ end
78
+
79
+ class << self
80
+ def parse(name, data)
81
+ uc = super
82
+
83
+ if data.nil? || !data.key?("title")
84
+ raise UserDataError, "Missing title for defined type #{@name}"
85
+ end
86
+
87
+ uc.title = data["title"]
88
+ uc.parameters = data["parameters"] || {}
89
+
90
+ uc
91
+ end
92
+ end
93
+ end
94
+
95
+ # A test usecase for a Puppet function.
96
+ class FunctionUsecase < TestUsecase
97
+ attr_accessor :parameters
98
+ attr_accessor :result
99
+
100
+ def initialize(name)
101
+ super
102
+ @parameters = []
103
+ @result = nil
104
+ end
105
+
106
+ class << self
107
+ def parse(name, data)
108
+ uc = super
109
+ return uc if data.nil?
110
+
111
+ uc.parameters = data["parameters"] || []
112
+ uc.result = data["result"]
113
+
114
+ uc
115
+ end
116
+ end
117
+ end
52
118
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PuppetUnitTests
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -1,8 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "puppet_unit_tests/version"
4
-
5
- module PuppetUnitTests
6
- class Error < StandardError; end
7
- # Your code goes here...
8
- end
4
+ require_relative "puppet_unit_tests/runner"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puppet-unit-tests
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arlo le Rouge
@@ -54,6 +54,7 @@ files:
54
54
  - assets/test-env/hiera.yaml
55
55
  - assets/test-env/lib/puppet/functions/test_data.rb
56
56
  - lib/puppet_unit_tests.rb
57
+ - lib/puppet_unit_tests/error.rb
57
58
  - lib/puppet_unit_tests/fake_facter.rb
58
59
  - lib/puppet_unit_tests/rake_task.rb
59
60
  - lib/puppet_unit_tests/resource_spec.rb