beaker_puppet_helpers 1.0.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.
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'beaker_puppet_helpers'
5
+ s.version = '1.0.0'
6
+ s.authors = ['Vox Pupuli']
7
+ s.email = ['voxpupuli@groups.io']
8
+ s.homepage = 'https://github.com/voxpupuli/beaker_puppet_helpers'
9
+ s.summary = "Beaker's Puppet DSL Extension Helpers"
10
+ s.description = 'For use for the Beaker acceptance testing tool'
11
+ s.license = 'Apache-2.0'
12
+
13
+ s.required_ruby_version = '>= 2.7', '< 4'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.require_paths = ['lib']
17
+
18
+ # Run time dependencies
19
+ s.add_runtime_dependency 'beaker', '>= 4', '< 6'
20
+ s.add_runtime_dependency 'puppet-modulebuilder', '~> 0.3', '< 2'
21
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BeakerPuppetHelpers
4
+ # The DSL methods for beaker. These are included in Beaker.
5
+ module DSL
6
+ # @!macro [new] common_opts
7
+ # @param [Hash{Symbol=>String}] opts Options to alter execution.
8
+ # @option opts [Boolean] :silent (false) Do not produce log output
9
+ # @option opts [Array<Fixnum>] :acceptable_exit_codes ([0]) An array
10
+ # (or range) of integer exit codes that should be considered
11
+ # acceptable. An error will be thrown if the exit code does not
12
+ # match one of the values in this list.
13
+ # @option opts [Boolean] :accept_all_exit_codes (false) Consider all
14
+ # exit codes as passing.
15
+ # @option opts [Boolean] :dry_run (false) Do not actually execute any
16
+ # commands on the SUT
17
+ # @option opts [String] :stdin (nil) Input to be provided during command
18
+ # execution on the SUT.
19
+ # @option opts [Boolean] :pty (false) Execute this command in a pseudoterminal.
20
+ # @option opts [Boolean] :expect_connection_failure (false) Expect this command
21
+ # to result in a connection failure, reconnect and continue execution.
22
+ # @option opts [Hash{String=>String}] :environment ({}) These will be
23
+ # treated as extra environment variables that should be set before
24
+ # running the command.
25
+ #
26
+
27
+ # Runs 'puppet apply' on a remote host, piping manifest through stdin
28
+ #
29
+ # @param [Beaker::Host] hosts
30
+ # The host that this command should be run on
31
+ #
32
+ # @param [String] manifest The puppet manifest to apply
33
+ #
34
+ # @!macro common_opts
35
+ # @option opts [Boolean] :parseonly (false) If this key is true, the
36
+ # "--parseonly" command line parameter will
37
+ # be passed to the 'puppet apply' command.
38
+ #
39
+ # @option opts [Boolean] :trace (false) If this key exists in the Hash,
40
+ # the "--trace" command line parameter will be
41
+ # passed to the 'puppet apply' command.
42
+ #
43
+ # @option opts [Array<Integer>] :acceptable_exit_codes ([0]) The list of exit
44
+ # codes that will NOT raise an error when found upon
45
+ # command completion. If provided, these values will
46
+ # be combined with those used in :catch_failures and
47
+ # :expect_failures to create the full list of
48
+ # passing exit codes.
49
+ #
50
+ # @option opts [Hash] :environment Additional environment variables to be
51
+ # passed to the 'puppet apply' command
52
+ #
53
+ # @option opts [Boolean] :catch_failures (false) By default `puppet
54
+ # --apply` will exit with 0, which does not count
55
+ # as a test failure, even if there were errors or
56
+ # changes when applying the manifest. This option
57
+ # enables detailed exit codes and causes a test
58
+ # failure if `puppet --apply` indicates there was
59
+ # a failure during its execution.
60
+ #
61
+ # @option opts [Boolean] :catch_changes (false) This option enables
62
+ # detailed exit codes and causes a test failure
63
+ # if `puppet --apply` indicates that there were
64
+ # changes or failures during its execution.
65
+ #
66
+ # @option opts [Boolean] :expect_changes (false) This option enables
67
+ # detailed exit codes and causes a test failure
68
+ # if `puppet --apply` indicates that there were
69
+ # no resource changes during its execution.
70
+ #
71
+ # @option opts [Boolean] :expect_failures (false) This option enables
72
+ # detailed exit codes and causes a test failure
73
+ # if `puppet --apply` indicates there were no
74
+ # failure during its execution.
75
+ #
76
+ # @option opts [Boolean] :future_parser (false) This option enables
77
+ # the future parser option that is available
78
+ # from Puppet verion 3.2
79
+ # By default it will use the 'current' parser.
80
+ #
81
+ # @option opts [Boolean] :noop (false) If this option exists, the
82
+ # the "--noop" command line parameter will be
83
+ # passed to the 'puppet apply' command.
84
+ #
85
+ # @option opts [String] :modulepath The search path for modules, as
86
+ # a list of directories separated by the system
87
+ # path separator character. (The POSIX path separator
88
+ # is ‘:’, and the Windows path separator is ‘;’.)
89
+ #
90
+ # @option opts [String] :hiera_config The path of the hiera.yaml configuration.
91
+ #
92
+ # @option opts [String] :debug (false) If this option exists,
93
+ # the "--debug" command line parameter
94
+ # will be passed to the 'puppet apply' command.
95
+ # @option opts [Boolean] :run_in_parallel Whether to run on each host in parallel.
96
+ #
97
+ # @param [Block] block This method will yield to a block of code passed
98
+ # by the caller; this can be used for additional
99
+ # validation, etc.
100
+ #
101
+ # @return [Array<Result>, Result, nil] An array of results, a result
102
+ # object, or nil. Check {Beaker::Shared::HostManager#run_block_on} for
103
+ # more details on this.
104
+ def apply_manifest_on(hosts, manifest, opts = {}, &block)
105
+ block_on hosts, opts do |host|
106
+ on_options = {}
107
+ on_options[:acceptable_exit_codes] = Array(opts[:acceptable_exit_codes])
108
+
109
+ puppet_apply_opts = {}
110
+ if opts[:debug]
111
+ puppet_apply_opts[:debug] = nil
112
+ else
113
+ puppet_apply_opts[:verbose] = nil
114
+ end
115
+ puppet_apply_opts[:parseonly] = nil if opts[:parseonly]
116
+ puppet_apply_opts[:trace] = nil if opts[:trace]
117
+ puppet_apply_opts[:parser] = 'future' if opts[:future_parser]
118
+ puppet_apply_opts[:modulepath] = opts[:modulepath] if opts[:modulepath]
119
+ puppet_apply_opts[:hiera_config] = opts[:hiera_config] if opts[:hiera_config]
120
+ puppet_apply_opts[:noop] = nil if opts[:noop]
121
+
122
+ # From puppet help:
123
+ # "... an exit code of '2' means there were changes, an exit code of
124
+ # '4' means there were failures during the transaction, and an exit
125
+ # code of '6' means there were both changes and failures."
126
+ if [opts[:catch_changes], opts[:catch_failures], opts[:expect_failures], opts[:expect_changes]].compact.length > 1
127
+ raise(ArgumentError,
128
+ 'Cannot specify more than one of `catch_failures`, ' \
129
+ '`catch_changes`, `expect_failures`, or `expect_changes` ' \
130
+ 'for a single manifest')
131
+ end
132
+
133
+ if opts[:catch_changes]
134
+ puppet_apply_opts['detailed-exitcodes'] = nil
135
+
136
+ # We're after idempotency so allow exit code 0 only.
137
+ on_options[:acceptable_exit_codes] |= [0]
138
+ elsif opts[:catch_failures]
139
+ puppet_apply_opts['detailed-exitcodes'] = nil
140
+
141
+ # We're after only complete success so allow exit codes 0 and 2 only.
142
+ on_options[:acceptable_exit_codes] |= [0, 2]
143
+ elsif opts[:expect_failures]
144
+ puppet_apply_opts['detailed-exitcodes'] = nil
145
+
146
+ # We're after failures specifically so allow exit codes 1, 4, and 6 only.
147
+ on_options[:acceptable_exit_codes] |= [1, 4, 6]
148
+ elsif opts[:expect_changes]
149
+ puppet_apply_opts['detailed-exitcodes'] = nil
150
+
151
+ # We're after changes specifically so allow exit code 2 only.
152
+ on_options[:acceptable_exit_codes] |= [2]
153
+ else
154
+ # Either use the provided acceptable_exit_codes or default to [0]
155
+ on_options[:acceptable_exit_codes] |= [0]
156
+ end
157
+
158
+ # Not really thrilled with this implementation, might want to improve it
159
+ # later. Basically, there is a magic trick in the constructor of
160
+ # PuppetCommand which allows you to pass in a Hash for the last value in
161
+ # the *args Array; if you do so, it will be treated specially. So, here
162
+ # we check to see if our caller passed us a hash of environment variables
163
+ # that they want to set for the puppet command. If so, we set the final
164
+ # value of *args to a new hash with just one entry (the value of which
165
+ # is our environment variables hash)
166
+ puppet_apply_opts['ENV'] = opts[:environment] if opts.key?(:environment)
167
+
168
+ puppet_apply_opts = host[:default_apply_opts].merge(puppet_apply_opts) if host[:default_apply_opts].respond_to? :merge
169
+
170
+ file_path = host.tmpfile(%(apply_manifest_#{Time.now.strftime('%H%M%S%L')}.pp))
171
+ begin
172
+ create_remote_file(host, file_path, "#{manifest}\n")
173
+
174
+ on(host, Beaker::PuppetCommand.new('apply', file_path, puppet_apply_opts), **on_options, &block)
175
+ ensure
176
+ host.rm_rf(file_path)
177
+ end
178
+ end
179
+ end
180
+
181
+ # Runs 'puppet apply' on default host
182
+ # @see #apply_manifest_on
183
+ # @return [Array<Result>, Result, nil] An array of results, a result
184
+ # object, or nil. Check {Beaker::Shared::HostManager#run_block_on} for
185
+ # more details on this.
186
+ def apply_manifest(manifest, opts = {}, &block)
187
+ apply_manifest_on(default, manifest, opts, &block)
188
+ end
189
+
190
+ # Get a facter fact from a provided host
191
+ #
192
+ # @param [Beaker::Host, Array<Beaker::Host>, String, Symbol] host
193
+ # One or more hosts to act upon, or a role (String or Symbol) that
194
+ # identifies one or more hosts.
195
+ # @param [String] name The name of the fact to query for
196
+ # @!macro common_opts
197
+ # @return String The value of the fact 'name' on the provided host
198
+ # @raise [FailTest] Raises an exception if call to facter fails
199
+ def fact_on(host, name, opts = {})
200
+ raise(ArgumentError, "fact_on's `name` option must be a String. You provided a #{name.class}: '#{name}'") unless name.is_a?(String)
201
+
202
+ if opts.is_a?(Hash)
203
+ opts['json'] = nil
204
+ else
205
+ opts << ' --json'
206
+ end
207
+
208
+ result = on host, Beaker::Command.new('facter', [name], opts)
209
+ if result.is_a?(Array)
210
+ result.map { |res| JSON.parse(res.stdout)[name] }
211
+ else
212
+ JSON.parse(result.stdout)[name]
213
+ end
214
+ end
215
+
216
+ # Get a facter fact from the default host
217
+ # @see #fact_on
218
+ def fact(name, opts = {})
219
+ fact_on(default, name, opts)
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'beaker/command'
4
+
5
+ module BeakerPuppetHelpers
6
+ # Methods to install Puppet
7
+ class InstallUtils
8
+ # @api private
9
+ REPOS = {
10
+ release: {
11
+ apt: 'https://apt.puppet.com',
12
+ yum: 'https://yum.puppet.com',
13
+ },
14
+ nightly: {
15
+ apt: 'https://nightlies.puppet.com/apt',
16
+ yum: 'https://nightlies.puppet.com/yum',
17
+ },
18
+ }.freeze
19
+
20
+ # Install official Puppet release repository configuration on host(s).
21
+ #
22
+ # @example Install Puppet 7
23
+ # install_puppet_release_repo_on(hosts, 'puppet7')
24
+ #
25
+ # @param [Beaker::Host] host
26
+ # A host to act upon.
27
+ # @param [String] collection
28
+ # The collection to install. The default (puppet) is the latest
29
+ # available version.
30
+ # @param [Boolean] nightly
31
+ # Whether to install nightly or release packages
32
+ #
33
+ # @note This method only works on redhat-like and debian-like hosts. There
34
+ # are no official Puppet releases for other platforms.
35
+ #
36
+ def self.install_puppet_release_repo_on(host, collection = 'puppet', nightly: false)
37
+ repos = REPOS[nightly ? :nightly : :release]
38
+
39
+ variant, version, _arch = host['packaging_platform'].split('-', 3)
40
+
41
+ case variant
42
+ when 'el', 'fedora', 'sles', 'cisco-wrlinux'
43
+ # sles 11 and later do not handle gpg keys well. We can't
44
+ # automatically import the keys because of sad things, so we
45
+ # have to manually import it once we install the release
46
+ # package. We'll have to remember to update this block when
47
+ # we update the signing keys
48
+ if variant == 'sles' && version >= '11'
49
+ %w[puppet puppet-20250406].each do |gpg_key|
50
+ wget_on(host, "https://yum.puppet.com/RPM-GPG-KEY-#{gpg_key}") do |filename|
51
+ host.exec(Beaker::Command.new("rpm --import '#{filename}'"))
52
+ end
53
+ end
54
+ end
55
+
56
+ url = "#{repos[:yum]}/#{collection}-release-#{variant}-#{version}.noarch.rpm"
57
+ host.install_package(url)
58
+ when 'debian', 'ubuntu'
59
+ url = "#{repos[:apt]}/#{collection}-release-#{host['platform'].codename}.deb"
60
+ wget_on(host, url) do |filename|
61
+ host.install_package(filename)
62
+ end
63
+ host.exec(Beaker::Command.new('apt-get update'))
64
+
65
+ # On Debian we can't count on /etc/profile.d
66
+ host.add_env_var('PATH', '/opt/puppetlabs/bin')
67
+ else
68
+ raise "No repository installation step for #{variant} yet..."
69
+ end
70
+ end
71
+
72
+ # Determine the Puppet package name
73
+ #
74
+ # @param [Beaker::Host] host
75
+ # The host to act on
76
+ # @param [Boolean] prefer_aio
77
+ # Whether to prefer AIO packages or OS packages
78
+ # @return [String] The Puppet package name
79
+ def self.puppet_package_name(host, prefer_aio: true)
80
+ case host['packaging_platform'].split('-', 3).first
81
+ when /el-|fedora|sles|cisco_|debian|ubuntu/
82
+ prefer_aio ? 'puppet-agent' : 'puppet'
83
+ when /freebsd/
84
+ 'sysutils/puppet'
85
+ else
86
+ 'puppet'
87
+ end
88
+ end
89
+
90
+ # @param [Beaker::Host] host
91
+ # The host to act on
92
+ # @api private
93
+ def self.wget_on(host, url)
94
+ extension = File.extname(url)
95
+ name = File.basename(url, extension)
96
+ # Can't use host.tmpfile since we need to set an extension
97
+ target = host.exec(Beaker::Command.new("mktemp -t '#{name}-XXXXXX#{extension}'")).stdout.strip
98
+ begin
99
+ host.exec(Beaker::Command.new("wget -O '#{target}' '#{url}'"))
100
+ yield target
101
+ ensure
102
+ host.rm_rf(target)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'beaker/command'
4
+ require 'puppet/modulebuilder'
5
+
6
+ module BeakerPuppetHelpers
7
+ # Methods to help install puppet modules
8
+ module ModuleUtils
9
+ # Install the desired module with the PMT on a given host
10
+ #
11
+ # @param [Beaker::Host, Array<Beaker::Host>, String, Symbol] hosts
12
+ # One or more hosts to act upon, or a role (String or Symbol) that
13
+ # identifies one or more hosts.
14
+ # @param [String] module_name
15
+ # The short name of the module to be installed
16
+ # @param [String] version
17
+ # The version of the module to be installed
18
+ # @param [String] module_repository
19
+ # An optional module repository to install from
20
+ def install_puppet_module_via_pmt_on(hosts, module_name, version = nil, module_repository = nil)
21
+ block_on hosts do |host|
22
+ puppet_opts = {}
23
+ puppet_opts.merge!(host[:default_module_install_opts]) if host[:default_module_install_opts]
24
+ puppet_opts[:version] = version if version
25
+ puppet_opts[:module_repository] = module_repository if module_repository
26
+
27
+ on host, Beaker::PuppetCommand.new('module', ['install', module_name], puppet_opts)
28
+ end
29
+ end
30
+
31
+ # Install local module for acceptance testing
32
+ #
33
+ # This uses puppet-modulebuilder to build the module located at source
34
+ # and then copies it to the hosts. There it runs puppet module install.
35
+ #
36
+ # @param [Beaker::Host, Array<Beaker::Host>, String, Symbol] hosts
37
+ # One or more hosts to act upon, or a role (String or Symbol) that
38
+ # identifies one or more hosts.
39
+ # @param [String] source
40
+ # The directory where the module sits
41
+ def install_local_module_on(hosts, source = '.')
42
+ builder = Puppet::Modulebuilder::Builder.new(File.realpath(source))
43
+ source_path = builder.build
44
+ begin
45
+ block_on hosts do |host|
46
+ target_file = host.tmpfile('puppet_module')
47
+ begin
48
+ host.do_scp_to(source_path, target_file, {})
49
+ install_puppet_module_via_pmt_on(host, target_file)
50
+ ensure
51
+ host.rm_rf(target_file)
52
+ end
53
+ end
54
+ ensure
55
+ File.unlink(source_path) if source_path
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A collection of helpers to make Puppet usage easier with Beaker
4
+ module BeakerPuppetHelpers
5
+ autoload :DSL, File.join(__dir__, 'beaker_puppet_helpers', 'dsl.rb')
6
+ autoload :InstallUtils, File.join(__dir__, 'beaker_puppet_helpers', 'install_utils.rb')
7
+ autoload :ModuleUtils, File.join(__dir__, 'beaker_puppet_helpers', 'module_utils.rb')
8
+ end
9
+
10
+ require 'beaker'
11
+ Beaker::DSL.register(BeakerPuppetHelpers::DSL)
12
+ Beaker::DSL.register(BeakerPuppetHelpers::ModuleUtils)
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ class ClassMixedWithDSLHelpers
6
+ include Beaker::DSL::Patterns
7
+ include BeakerPuppetHelpers::DSL
8
+
9
+ def logger
10
+ @logger ||= RSpec::Mocks::Double.new('Beaker::Logger').as_null_object
11
+ end
12
+ end
13
+
14
+ describe BeakerPuppetHelpers::DSL do
15
+ subject(:dsl) { ClassMixedWithDSLHelpers.new }
16
+
17
+ let(:master) { instance_double(Beaker::Host) }
18
+ let(:agent) { instance_double(Beaker::Host) }
19
+ let(:hosts) { [master, agent] }
20
+
21
+ describe '#apply_manifest_on' do
22
+ before do
23
+ hosts.each do |host|
24
+ allow(host).to receive(:tmpfile).and_return('temp')
25
+ allow(host).to receive(:rm_rf).with('temp')
26
+ allow(host).to receive(:[]).with(:default_apply_opts)
27
+ end
28
+ end
29
+
30
+ it 'calls puppet' do
31
+ expect(dsl).to receive(:create_remote_file).and_return(true)
32
+ expect(Beaker::PuppetCommand).to receive(:new).and_return('puppet_command')
33
+ expect(dsl).to receive(:on).with(agent, 'puppet_command', acceptable_exit_codes: [0])
34
+
35
+ dsl.apply_manifest_on(agent, 'class { "boo": }')
36
+ end
37
+
38
+ it 'operates on an array of hosts' do
39
+ the_hosts = [master, agent]
40
+
41
+ expect(dsl).to receive(:create_remote_file).twice.and_return(true)
42
+ the_hosts.each do |host|
43
+ expect(Beaker::PuppetCommand).to receive(:new).and_return('puppet_command')
44
+ expect(dsl).to receive(:on).with(host, 'puppet_command', acceptable_exit_codes: [0])
45
+ end
46
+
47
+ result = dsl.apply_manifest_on(the_hosts, 'include foobar')
48
+ expect(result).to be_an(Array)
49
+ end
50
+
51
+ it 'operates on an array of hosts in parallel' do
52
+ InParallel::InParallelExecutor.logger = dsl.logger
53
+ # This will only get hit if forking processes is supported and at least 2 items are being submitted to run in parallel
54
+ # expect( InParallel::InParallelExecutor ).to receive(:_execute_in_parallel).with(any_args).and_call_original.exactly(2).times
55
+ the_hosts = [master, agent]
56
+
57
+ allow(dsl).to receive(:create_remote_file).and_return(true)
58
+ allow(Beaker::PuppetCommand).to receive(:new).and_return('puppet_command')
59
+ the_hosts.each do |host|
60
+ allow(dsl).to receive(:on).with(host, 'puppet_command', acceptable_exit_codes: [0])
61
+ end
62
+
63
+ result = dsl.apply_manifest_on(the_hosts, 'include foobar')
64
+ expect(result).to be_an(Array)
65
+ end
66
+
67
+ it 'runs block_on in parallel if set' do
68
+ InParallel::InParallelExecutor.logger = dsl.logger
69
+ the_hosts = [master, agent]
70
+
71
+ allow(dsl).to receive(:create_remote_file).and_return(true)
72
+ allow(Beaker::PuppetCommand).to receive(:new).and_return('puppet_command')
73
+ the_hosts.each do |host|
74
+ allow(dsl).to receive(:on).with(host, 'puppet_command', acceptable_exit_codes: [0])
75
+ end
76
+ expect(dsl).to receive(:block_on).with(anything, { run_in_parallel: true })
77
+
78
+ dsl.apply_manifest_on(the_hosts, 'include foobar', run_in_parallel: true)
79
+ end
80
+
81
+ it 'adds acceptable exit codes with :catch_failures' do
82
+ expect(dsl).to receive(:create_remote_file).and_return(true)
83
+ expect(Beaker::PuppetCommand).to receive(:new).and_return('puppet_command')
84
+ expect(dsl).to receive(:on).with(agent, 'puppet_command', acceptable_exit_codes: [0, 2])
85
+
86
+ dsl.apply_manifest_on(agent, 'class { "boo": }', catch_failures: true)
87
+ end
88
+
89
+ it 'allows acceptable exit codes through :catch_failures' do
90
+ expect(dsl).to receive(:create_remote_file).and_return(true)
91
+ expect(Beaker::PuppetCommand).to receive(:new).and_return('puppet_command')
92
+ expect(dsl).to receive(:on).with(agent, 'puppet_command', acceptable_exit_codes: [4, 0, 2])
93
+
94
+ dsl.apply_manifest_on(agent, 'class { "boo": }', acceptable_exit_codes: [4], catch_failures: true)
95
+ end
96
+
97
+ it 'enforces a 0 exit code through :catch_changes' do
98
+ expect(dsl).to receive(:create_remote_file).and_return(true)
99
+ expect(Beaker::PuppetCommand).to receive(:new).and_return('puppet_command')
100
+ expect(dsl).to receive(:on).with(agent, 'puppet_command', acceptable_exit_codes: [0])
101
+
102
+ dsl.apply_manifest_on(agent, 'class { "boo": }', catch_changes: true)
103
+ end
104
+
105
+ it 'enforces a 2 exit code through :expect_changes' do
106
+ expect(dsl).to receive(:create_remote_file).and_return(true)
107
+ expect(Beaker::PuppetCommand).to receive(:new).and_return('puppet_command')
108
+ expect(dsl).to receive(:on).with(agent, 'puppet_command', acceptable_exit_codes: [2])
109
+
110
+ dsl.apply_manifest_on(
111
+ agent,
112
+ 'class { "boo": }',
113
+ expect_changes: true,
114
+ )
115
+ end
116
+
117
+ it 'enforces exit codes through :expect_failures' do
118
+ expect(dsl).to receive(:create_remote_file).and_return(true)
119
+ expect(Beaker::PuppetCommand).to receive(:new).and_return('puppet_command')
120
+ expect(dsl).to receive(:on).with(agent, 'puppet_command', acceptable_exit_codes: [1, 4, 6])
121
+
122
+ dsl.apply_manifest_on(agent, 'class { "boo": }', expect_failures: true)
123
+ end
124
+
125
+ it 'enforces exit codes through :expect_failures and catch_failures' do
126
+ expect do
127
+ dsl.apply_manifest_on(agent, 'class { "boo": }', expect_failures: true, catch_failures: true)
128
+ end.to raise_error(ArgumentError, /catch_failures.+expect_failures/)
129
+ end
130
+
131
+ it 'enforces merges exit codes from :expect_failures and acceptable_exit_codes' do
132
+ expect(dsl).to receive(:create_remote_file).and_return(true)
133
+ expect(Beaker::PuppetCommand).to receive(:new).and_return('puppet_command')
134
+
135
+ expect(dsl).to receive(:on).with(agent, 'puppet_command', acceptable_exit_codes: [1, 2, 3, 4, 5, 6])
136
+
137
+ dsl.apply_manifest_on(agent, 'class { "boo": }', acceptable_exit_codes: (1..5), expect_failures: true)
138
+ end
139
+
140
+ it 'can set the --parser future flag' do
141
+ expect(dsl).to receive(:create_remote_file).and_return(true)
142
+
143
+ expect(Beaker::PuppetCommand).to receive(:new).with('apply', anything, include(parser: 'future')).and_return('puppet_command')
144
+
145
+ expect(dsl).to receive(:on).with(agent, 'puppet_command', acceptable_exit_codes: [0])
146
+
147
+ dsl.apply_manifest_on(agent, 'class { "boo": }', future_parser: true)
148
+ end
149
+
150
+ it 'can set the --noops flag' do
151
+ expect(dsl).to receive(:create_remote_file).and_return(true)
152
+ expect(Beaker::PuppetCommand).to receive(:new).with('apply', anything, include(noop: nil)).and_return('puppet_command')
153
+ expect(dsl).to receive(:on).with(agent, 'puppet_command', acceptable_exit_codes: [0])
154
+
155
+ dsl.apply_manifest_on(agent, 'class { "boo": }', noop: true)
156
+ end
157
+
158
+ it 'can set the --debug flag' do
159
+ allow(dsl).to receive(:hosts).and_return(hosts)
160
+ allow(dsl).to receive(:create_remote_file).and_return(true)
161
+ allow(dsl).to receive(:on).with(agent, 'puppet_command', acceptable_exit_codes: [0])
162
+
163
+ expect(Beaker::PuppetCommand).to receive(:new).with(
164
+ 'apply', anything, include(debug: nil)
165
+ ).and_return('puppet_command')
166
+
167
+ dsl.apply_manifest_on(agent, 'class { "boo": }', debug: true)
168
+ end
169
+ end
170
+
171
+ describe '#apply_manifest' do
172
+ it 'delegates to #apply_manifest_on with the default host' do
173
+ expect(dsl).to receive(:default).and_return(agent)
174
+ expect(dsl).to receive(:apply_manifest_on).with(agent, 'manifest', { opt: 'value' }).once
175
+
176
+ dsl.apply_manifest('manifest', { opt: 'value' })
177
+ end
178
+ end
179
+
180
+ describe '#fact_on' do
181
+ it 'retrieves a fact on a single host' do
182
+ result = instance_double(Beaker::Result, stdout: '{"osfamily": "family"}')
183
+ expect(dsl).to receive(:on).and_return(result)
184
+
185
+ expect(dsl.fact_on('host', 'osfamily')).to eq('family')
186
+ end
187
+
188
+ it 'converts each element to a structured fact when it receives an array of results from #on' do
189
+ times = hosts.length
190
+
191
+ result = instance_double(Beaker::Result, stdout: '{"os": {"name":"name", "family": "family"}}')
192
+ allow(dsl).to receive(:on).and_return([result] * times)
193
+
194
+ expect(dsl.fact_on(hosts, 'os')).to eq([{ 'name' => 'name', 'family' => 'family' }] * times)
195
+ end
196
+
197
+ it 'returns a single result for single host' do
198
+ result = instance_double(Beaker::Result, stdout: '{"osfamily": "family"}')
199
+ allow(dsl).to receive(:on).and_return(result)
200
+
201
+ expect(dsl.fact_on('host', 'osfamily')).to eq('family')
202
+ end
203
+
204
+ it 'preserves data types' do
205
+ result = instance_double(Beaker::Result, stdout: '{"identity": { "uid": 0, "user": "root", "privileged": true }}')
206
+ allow(dsl).to receive(:on).and_return(result)
207
+
208
+ structured_fact = dsl.fact_on('host', 'identity')
209
+
210
+ expect(structured_fact['uid'].class).to be(Integer)
211
+ expect(structured_fact['user'].class).to be(String)
212
+ expect(structured_fact['privileged'].class).to be(TrueClass)
213
+ end
214
+
215
+ it 'raises an error when it receives a symbol for a fact' do
216
+ expect { dsl.fact_on('host', :osfamily) }
217
+ .to raise_error(ArgumentError, /fact_on's `name` option must be a String. You provided a Symbol: 'osfamily'/)
218
+ end
219
+ end
220
+
221
+ describe '#fact' do
222
+ it 'delegates to #fact_on with the default host' do
223
+ expect(dsl).to receive(:fact_on).with(anything, 'osfamily', {}).once
224
+ expect(dsl).to receive(:default)
225
+
226
+ dsl.fact('osfamily')
227
+ end
228
+ end
229
+ end