beaker_puppet_helpers 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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