Skip to content

Commit acd3c87

Browse files
authored
Merge pull request #14 from voxpupuli/gh-13-curl-404
(gh-13) Fail for curl 404 and add specs for common.sh lib
2 parents 77694b8 + 6cb6645 commit acd3c87

File tree

6 files changed

+862
-36
lines changed

6 files changed

+862
-36
lines changed

files/common.sh

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ download() {
143143
if exists 'wget'; then
144144
exec_and_capture wget -O "${_file}" "${_url}"
145145
elif exists 'curl'; then
146-
exec_and_capture curl -sSL -o "${_file}" "${_url}"
146+
exec_and_capture curl --fail-with-body -sSL -o "${_file}" "${_url}"
147147
else
148148
fail "Unable to download ${_url}. Neither wget nor curl are installed."
149149
fi
@@ -315,13 +315,14 @@ install_package() {
315315

316316
info "Installing ${_package} ${_version}"
317317

318+
if [[ -z "${_os_family}" ]] || [[ -z "${_os_full_version}" ]]; then
319+
set_platform_globals
320+
_os_family="${os_family}"
321+
_os_full_version="${os_full_version}"
322+
fi
323+
318324
local _package_and_version
319325
if [[ -n "${_version}" ]] && [[ "${_version}" != 'latest' ]]; then
320-
if [[ -z "${_os_family}" ]] || [[ -z "${_os_full_version}" ]]; then
321-
set_platform_globals
322-
_os_family="${os_family}"
323-
_os_full_version="${os_full_version}"
324-
fi
325326
case $_os_family in
326327
debian|ubuntu)
327328
local _deb_package_version
@@ -336,27 +337,36 @@ install_package() {
336337
_package_and_version="${_package}"
337338
fi
338339

339-
if exists 'dnf'; then
340-
exec_and_capture dnf install -y "${_package_and_version}"
341-
elif exists 'yum'; then
342-
exec_and_capture yum install -y "${_package_and_version}"
343-
elif exists 'zypper'; then
344-
exec_and_capture zypper install -y "${_package_and_version}"
345-
elif exists 'apt'; then
346-
exec_and_capture apt install -y "${_package_and_version}"
347-
elif exists 'apt-get'; then
348-
exec_and_capture apt-get install -y "${_package_and_version}"
349-
else
350-
fail "Unable to install ${_package}. Neither dnf, yum, zypper, apt nor apt-get are installed."
351-
fi
340+
case ${_os_family} in
341+
debian|ubuntu)
342+
if exists 'apt-get'; then
343+
exec_and_capture apt-get install -y "${_package_and_version}"
344+
elif exists 'apt'; then
345+
exec_and_capture apt install -y "${_package_and_version}"
346+
else
347+
fail "Unable to install ${_package}. Neither apt nor apt-get are installed."
348+
fi
349+
;;
350+
*)
351+
if exists 'dnf'; then
352+
exec_and_capture dnf install -y "${_package_and_version}"
353+
elif exists 'yum'; then
354+
exec_and_capture yum install -y "${_package_and_version}"
355+
elif exists 'zypper'; then
356+
exec_and_capture zypper install -y "${_package_and_version}"
357+
else
358+
fail "Unable to install ${_package}. Neither dnf, yum nor zypper are installed."
359+
fi
360+
;;
361+
esac
352362
}
353363

354364
# Update the package manager cache.
355365
refresh_package_cache() {
356-
if exists 'apt'; then
357-
exec_and_capture apt update
358-
elif exists 'apt-get'; then
366+
if exists 'apt-get'; then
359367
exec_and_capture apt-get update
368+
elif exists 'apt'; then
369+
exec_and_capture apt update
360370
elif exists 'dnf'; then
361371
exec_and_capture dnf clean all
362372
elif exists 'yum'; then

puppet-stub/puppet.gemspec

Lines changed: 0 additions & 14 deletions
This file was deleted.

spec/bash_spec_helper.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
require 'tmpdir'
4+
require 'rspec'
5+
require 'lib/bash_rspec'
6+
require 'lib/contexts'
7+
8+
RSpec.configure do |c|
9+
c.include BashRspec
10+
end

spec/lib/bash_rspec.rb

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
# frozen_string_literal: true
2+
3+
require 'mkmf'
4+
require 'open3'
5+
6+
# Simple harness for testing libraries of BASH shell functions within
7+
# the RSpec framework.
8+
#
9+
# This module provides a simple DSL for mocking system commands
10+
# and functions in a given script. It also provides a way to
11+
# generate a test script that sources the given script and
12+
# then executes a command for evaluation.
13+
#
14+
# The module assumes that the the file under test is located within
15+
# the repository, may be sourced as a library of bash functions
16+
# without side-effects, and that the file under test has been set via:
17+
#
18+
# subject { 'path/to/file.sh' }
19+
#
20+
# # Usage
21+
#
22+
# ## Testing a function
23+
#
24+
# The test() function returns Open3.capture2e() output and status:
25+
#
26+
# it 'tests execution of something()' do
27+
# output, status = test("something")
28+
# expect(status.success?).to be(true)
29+
# expect(output).to include('something exciting')
30+
# end
31+
#
32+
# ## Mocking a command
33+
#
34+
# The receive_command() method may be used to mock a command:
35+
#
36+
# it 'tests execution of something() but without boom' do
37+
# allow_script.to receive_command(:boom).and_exec(<<~"EOF")
38+
# echo "pretend we did something destructive instead"
39+
# EOF
40+
# output, status = test("something_that_goes_boom_inside")
41+
# expect(status.success?).to be(true)
42+
# expect(output).to include('something exciting without as much boom')
43+
# end
44+
#
45+
# ## Redeclaring a BASH function
46+
#
47+
# The redeclare() method may be used to replace a BASH function while
48+
# allowing you to call the original function:
49+
#
50+
# it 'tests execution of something() but with modified other()' do
51+
# allow_script.to redeclare(:other).and_exec(<<~"EOF")
52+
# echo "do something first"
53+
# original_other $@
54+
# EOF
55+
# output, status = test("something")
56+
# expect(status.success?).to be(true)
57+
# expect(output).to include('something exciting')
58+
# end
59+
#
60+
# ## Setting environment variables
61+
#
62+
# The set_env() method may be used to set environment variables that
63+
# the script may need for execution:
64+
#
65+
# it 'tests execution of something() with a custom env var' do
66+
# allow_script.to set_env('SOME_VAR', 'some_value')
67+
# output, status = test("echo $SOME_VAR")
68+
# expect(status.success?).to be(true)
69+
# expect(output).to eq('some_value')
70+
# end
71+
#
72+
# All of the condition behavior defined above are keyed by the name of
73+
# the command, function or variable. Multiple calls with the same key
74+
# only redefine the behavior to the last call.
75+
module BashRspec
76+
# Test whether a command is available on the system.
77+
def self.found(command)
78+
# from stdlib mkmf gem
79+
find_executable(command.to_s)
80+
end
81+
82+
# Encapsulates replaced behavior for 'receive_command'.
83+
class CommandMock
84+
# The command to be mocked.
85+
attr_reader :command
86+
# The behavior to be executed in place of the command.
87+
attr_reader :behavior
88+
89+
def initialize(command)
90+
@command = command
91+
end
92+
93+
def and_exec(behavior)
94+
@behavior = behavior
95+
self
96+
end
97+
98+
def indented_behavior
99+
behavior.split("\n").map do |line|
100+
line.sub(%r{^}, ' ')
101+
end.join("\n")
102+
end
103+
104+
def generate
105+
<<~"EOF"
106+
function #{command}() {
107+
#{indented_behavior}
108+
}
109+
EOF
110+
end
111+
end
112+
113+
# Encapsulates replaced behavior for 'redeclare' of a BASH function.
114+
class FunctionMock < CommandMock
115+
alias as and_exec
116+
117+
def generate
118+
<<~"EOF"
119+
eval "original_$(declare -f #{command})"
120+
121+
#{super}
122+
EOF
123+
end
124+
end
125+
126+
# Simple class to encapsulate environment variables that should
127+
# be set prior to sourcing the script under test.
128+
class EnvVar
129+
attr_reader :name, :value
130+
131+
def initialize(name, value)
132+
@name = name
133+
@value = value
134+
end
135+
136+
def generate
137+
%(#{name}="#{value}")
138+
end
139+
end
140+
141+
# Keeps track of the set of mocks for a given script.
142+
class ScriptMocker
143+
# The script file to be mocked.
144+
attr_reader :script
145+
# The set of CommandMock instances for the script.
146+
attr_reader :mocks
147+
# The set of EnvVar instances to set for the script.
148+
attr_reader :env_vars
149+
150+
def initialize(script)
151+
@script = script
152+
@mocks = {}
153+
@env_vars = {}
154+
end
155+
156+
def to(condition)
157+
case condition
158+
when CommandMock
159+
mocks[condition.command.to_sym] = condition
160+
when EnvVar
161+
env_vars[condition.name.to_sym] = condition
162+
else
163+
raise(ArgumentError, "BashRspec doesn't know what to do with condition: #{condition}")
164+
end
165+
self
166+
end
167+
168+
# Concatenates the set of CommandMock#behavior for the given script.
169+
def generate(type)
170+
header = case type
171+
when :mocks
172+
'mock commands'
173+
when :env_vars
174+
'environment variables'
175+
else
176+
raise(ArgumentError, "BashRspec doesn't know how to generate #{type}")
177+
end
178+
conditions = send(type).values
179+
code = conditions.map(&:generate).join("\n")
180+
<<~EOS
181+
# #{header}
182+
#{code}
183+
EOS
184+
end
185+
end
186+
187+
# Returns (and creates and caches) a ScriptMocker instance
188+
# for the given script within the current Rspec test context.
189+
def lookup(script)
190+
@scripts ||= {}
191+
@scripts[script] ||= ScriptMocker.new(script)
192+
end
193+
194+
def module_root
195+
File.expand_path(File.join(__dir__, '..', '..'))
196+
end
197+
198+
# Mock a system command.
199+
def receive_command(command)
200+
CommandMock.new(command)
201+
end
202+
203+
# Redeclare a Bash function as original_${function}()
204+
# in order to replace it with a mock that can access
205+
# it's original behavior.
206+
def redeclare(function)
207+
FunctionMock.new(function)
208+
end
209+
210+
# Set an environment variable for the script being tested.
211+
def set_env(name, value)
212+
EnvVar.new(name, value)
213+
end
214+
215+
# Begins an rspec-mocks style interface for mocking commands
216+
# in a given script file.
217+
def allow_script(script = subject)
218+
lookup(script)
219+
end
220+
221+
# Assembly of everything needed to run a test.
222+
# (Also useful for debugging.)
223+
def script_harness(script = subject)
224+
sm = lookup(script)
225+
<<~SCRIPT
226+
#{sm.generate(:env_vars)}
227+
228+
# source under test
229+
source "${MODULE_ROOT}/#{script}"
230+
231+
#{sm.generate(:mocks)}
232+
SCRIPT
233+
end
234+
235+
# Run a command scriptlet in the context of the script under test.
236+
def test(command, script: subject)
237+
test_script = <<~SCRIPT
238+
#{script_harness(script)}
239+
240+
# test code
241+
#{command}
242+
SCRIPT
243+
Open3.capture2e({ 'MODULE_ROOT' => module_root }, 'bash', '-c', test_script)
244+
end
245+
end

0 commit comments

Comments
 (0)