mirror of
https://github.com/saltstack/salt-bootstrap.git
synced 2025-04-17 10:10:25 +00:00
Merge pull request #40 from s0undt3ch/features/python-unit-testing
Features/python unit testing
This commit is contained in:
commit
a76d4cdacd
10 changed files with 1486 additions and 2 deletions
|
@ -1,8 +1,11 @@
|
|||
language: python
|
||||
|
||||
before_install:
|
||||
- sudo apt-get update && sudo apt-get install devscripts
|
||||
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then sudo pip install unittest2; fi"
|
||||
- sudo pip install unittest-xml-reporting
|
||||
|
||||
script:
|
||||
- sudo ./.run-boostrap-tests.sh
|
||||
- sudo python tests/runtests.py -vv
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
|
|
159
tests/bootstrap/__init__.py
Normal file
159
tests/bootstrap/__init__.py
Normal file
|
@ -0,0 +1,159 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
bootstrap
|
||||
~~~~~~~~~
|
||||
|
||||
salt-bootstrap script unittesting
|
||||
|
||||
:codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)`
|
||||
:copyright: © 2013 by the SaltStack Team, see AUTHORS for more details.
|
||||
:license: Apache 2.0, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import fcntl
|
||||
import signal
|
||||
import tempfile
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# support python < 2.7 via unittest2
|
||||
if sys.version_info < (2, 7):
|
||||
try:
|
||||
from unittest2 import (
|
||||
TestLoader,
|
||||
TextTestRunner,
|
||||
TestCase,
|
||||
expectedFailure,
|
||||
TestSuite,
|
||||
skipIf,
|
||||
)
|
||||
except ImportError:
|
||||
raise SystemExit('You need to install unittest2 to run the salt tests')
|
||||
else:
|
||||
from unittest import (
|
||||
TestLoader,
|
||||
TextTestRunner,
|
||||
TestCase,
|
||||
expectedFailure,
|
||||
TestSuite,
|
||||
skipIf,
|
||||
)
|
||||
|
||||
from bootstrap.ext.os_data import GRAINS
|
||||
|
||||
TEST_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||
EXT_DIR = os.path.join(TEST_DIR, 'ext')
|
||||
PARENT_DIR = os.path.dirname(TEST_DIR)
|
||||
BOOTSTRAP_SCRIPT_PATH = os.path.join(PARENT_DIR, 'bootstrap-salt-minion.sh')
|
||||
|
||||
|
||||
def non_block_read(output):
|
||||
fd = output.fileno()
|
||||
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
||||
try:
|
||||
return output.read()
|
||||
except:
|
||||
return ''
|
||||
|
||||
|
||||
class BootstrapTestCase(TestCase):
|
||||
def run_script(self,
|
||||
script=BOOTSTRAP_SCRIPT_PATH,
|
||||
args=(),
|
||||
cwd=PARENT_DIR,
|
||||
timeout=None,
|
||||
executable='/bin/sh',
|
||||
stream_stds=False):
|
||||
|
||||
cmd = [script] + list(args)
|
||||
|
||||
out = err = ''
|
||||
|
||||
popen_kwargs = {
|
||||
'cwd': cwd,
|
||||
'shell': True,
|
||||
'stderr': subprocess.PIPE,
|
||||
'stdout': subprocess.PIPE,
|
||||
'close_fds': True,
|
||||
'executable': executable,
|
||||
|
||||
# detach from parent group (no more inherited signals!)
|
||||
'preexec_fn': os.setpgrp
|
||||
}
|
||||
|
||||
cmd = ' '.join(filter(None, [script] + list(args)))
|
||||
|
||||
process = subprocess.Popen(cmd, **popen_kwargs)
|
||||
|
||||
if timeout is not None:
|
||||
stop_at = datetime.now() + timedelta(seconds=timeout)
|
||||
term_sent = False
|
||||
|
||||
while True:
|
||||
process.poll()
|
||||
if process.returncode is not None:
|
||||
break
|
||||
|
||||
rout = non_block_read(process.stdout)
|
||||
if rout:
|
||||
out += rout
|
||||
if stream_stds:
|
||||
sys.stdout.write(rout)
|
||||
|
||||
rerr = non_block_read(process.stderr)
|
||||
if rerr:
|
||||
err += rerr
|
||||
if stream_stds:
|
||||
sys.stderr.write(rerr)
|
||||
|
||||
if timeout is not None:
|
||||
now = datetime.now()
|
||||
|
||||
if now > stop_at:
|
||||
if term_sent is False:
|
||||
# Kill the process group since sending the term signal
|
||||
# would only terminate the shell, not the command
|
||||
# executed in the shell
|
||||
os.killpg(os.getpgid(process.pid), signal.SIGINT)
|
||||
term_sent = True
|
||||
continue
|
||||
|
||||
# As a last resort, kill the process group
|
||||
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
||||
|
||||
return 1, [
|
||||
'Process took more than {0} seconds to complete. '
|
||||
'Process Killed! Current STDOUT: \n{1}'.format(
|
||||
timeout, out
|
||||
)
|
||||
], [
|
||||
'Process took more than {0} seconds to complete. '
|
||||
'Process Killed! Current STDERR: \n{1}'.format(
|
||||
timeout, err
|
||||
)
|
||||
]
|
||||
|
||||
process.communicate()
|
||||
|
||||
try:
|
||||
return process.returncode, out.splitlines(), err.splitlines()
|
||||
finally:
|
||||
try:
|
||||
process.terminate()
|
||||
except OSError:
|
||||
# process already terminated
|
||||
pass
|
||||
|
||||
def assert_script_result(self, fail_msg, expected_rc, process_details):
|
||||
rc, out, err = process_details
|
||||
if rc != expected_rc:
|
||||
err_msg = '{0}:\n'.format(fail_msg)
|
||||
if out:
|
||||
err_msg = '{0}STDOUT:\n{1}\n'.format(err_msg, '\n'.join(out))
|
||||
if err:
|
||||
err_msg = '{0}STDERR:\n{1}\n'.format(err_msg, '\n'.join(err))
|
||||
raise AssertionError(err_msg.rstrip())
|
0
tests/bootstrap/ext/__init__.py
Normal file
0
tests/bootstrap/ext/__init__.py
Normal file
100
tests/bootstrap/ext/console.py
Normal file
100
tests/bootstrap/ext/console.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vim: sw=4 ts=4 fenc=utf-8
|
||||
'''
|
||||
getTerminalSize()
|
||||
- get width and height of console
|
||||
- works on linux,os x,windows,cygwin(windows)
|
||||
- taken from http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python
|
||||
'''
|
||||
|
||||
# Import python libs
|
||||
import os
|
||||
import platform
|
||||
import struct
|
||||
import ctypes
|
||||
import subprocess
|
||||
import fcntl
|
||||
import termios
|
||||
|
||||
__all__ = ['getTerminalSize']
|
||||
|
||||
|
||||
def getTerminalSize():
|
||||
current_os = platform.system()
|
||||
tuple_xy=None
|
||||
if current_os == 'Windows':
|
||||
tuple_xy = _getTerminalSize_windows()
|
||||
if tuple_xy is None:
|
||||
tuple_xy = _getTerminalSize_tput()
|
||||
# needed for window's python in cygwin's xterm!
|
||||
if current_os == 'Linux' or current_os == 'Darwin' or current_os.startswith('CYGWIN'):
|
||||
tuple_xy = _getTerminalSize_linux()
|
||||
if tuple_xy is None:
|
||||
print 'default'
|
||||
tuple_xy = (80, 25) # default value
|
||||
return tuple_xy
|
||||
|
||||
|
||||
def _getTerminalSize_windows():
|
||||
res=None
|
||||
try:
|
||||
# stdin handle is -10
|
||||
# stdout handle is -11
|
||||
# stderr handle is -12
|
||||
|
||||
h = ctypes.windll.kernel32.GetStdHandle(-12)
|
||||
csbi = ctypes.create_string_buffer(22)
|
||||
res = ctypes.windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
|
||||
except Exception:
|
||||
return None
|
||||
if res:
|
||||
(bufx, bufy, curx, cury, wattr,
|
||||
left, top, right, bottom, maxx, maxy) = struct.unpack('hhhhHhhhhhh', csbi.raw)
|
||||
sizex = right - left + 1
|
||||
sizey = bottom - top + 1
|
||||
return sizex, sizey
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _getTerminalSize_tput():
|
||||
# get terminal width
|
||||
# src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window
|
||||
try:
|
||||
proc=subprocess.Popen(['tput', 'cols'],stdin=subprocess.PIPE,stdout=subprocess.PIPE)
|
||||
output=proc.communicate(input=None)
|
||||
cols=int(output[0])
|
||||
proc=subprocess.Popen(['tput', 'lines'],stdin=subprocess.PIPE,stdout=subprocess.PIPE)
|
||||
output=proc.communicate(input=None)
|
||||
rows=int(output[0])
|
||||
return (cols,rows)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _getTerminalSize_linux():
|
||||
def ioctl_GWINSZ(fd):
|
||||
try:
|
||||
cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ,'1234'))
|
||||
except Exception:
|
||||
return None
|
||||
return cr
|
||||
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
|
||||
if not cr:
|
||||
try:
|
||||
fd = os.open(os.ctermid(), os.O_RDONLY)
|
||||
cr = ioctl_GWINSZ(fd)
|
||||
os.close(fd)
|
||||
except Exception:
|
||||
pass
|
||||
if not cr:
|
||||
try:
|
||||
cr = (os.environ['LINES'], os.environ['COLUMNS'])
|
||||
except Exception:
|
||||
return None
|
||||
return int(cr[1]), int(cr[0])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sizex,sizey=getTerminalSize()
|
||||
print 'width =',sizex,'height =',sizey
|
190
tests/bootstrap/ext/os_data.py
Normal file
190
tests/bootstrap/ext/os_data.py
Normal file
|
@ -0,0 +1,190 @@
|
|||
'''
|
||||
This file was copied and adapted from salt's source code
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import platform
|
||||
# Extend the default list of supported distros. This will be used for the
|
||||
# /etc/DISTRO-release checking that is part of platform.linux_distribution()
|
||||
from platform import _supported_dists
|
||||
_supported_dists += ('arch', 'mageia', 'meego', 'vmware', 'bluewhite64',
|
||||
'slamd64', 'ovs', 'system', 'mint', 'oracle')
|
||||
|
||||
_REPLACE_LINUX_RE = re.compile(r'linux', re.IGNORECASE)
|
||||
|
||||
# This maps (at most) the first ten characters (no spaces, lowercased) of
|
||||
# 'osfullname' to the 'os' grain that Salt traditionally uses.
|
||||
# Please see os_data() and _supported_dists.
|
||||
# If your system is not detecting properly it likely needs an entry here.
|
||||
_OS_NAME_MAP = {
|
||||
'redhatente': 'RedHat',
|
||||
'gentoobase': 'Gentoo',
|
||||
'archarm': 'Arch ARM',
|
||||
'arch': 'Arch',
|
||||
'debian': 'Debian',
|
||||
'debiangnu/': 'Debian',
|
||||
'fedoraremi': 'Fedora',
|
||||
'amazonami': 'Amazon',
|
||||
'alt': 'ALT',
|
||||
'oracleserv': 'OEL',
|
||||
}
|
||||
|
||||
# Map the 'os' grain to the 'os_family' grain
|
||||
# These should always be capitalized entries as the lookup comes
|
||||
# post-_OS_NAME_MAP. If your system is having trouble with detection, please
|
||||
# make sure that the 'os' grain is capitalized and working correctly first.
|
||||
_OS_FAMILY_MAP = {
|
||||
'Ubuntu': 'Debian',
|
||||
'Fedora': 'RedHat',
|
||||
'CentOS': 'RedHat',
|
||||
'GoOSe': 'RedHat',
|
||||
'Scientific': 'RedHat',
|
||||
'Amazon': 'RedHat',
|
||||
'CloudLinux': 'RedHat',
|
||||
'OVS': 'RedHat',
|
||||
'OEL': 'RedHat',
|
||||
'Mandrake': 'Mandriva',
|
||||
'ESXi': 'VMWare',
|
||||
'Mint': 'Debian',
|
||||
'VMWareESX': 'VMWare',
|
||||
'Bluewhite64': 'Bluewhite',
|
||||
'Slamd64': 'Slackware',
|
||||
'SLES': 'Suse',
|
||||
'SUSE Enterprise Server': 'Suse',
|
||||
'SUSE Enterprise Server': 'Suse',
|
||||
'SLED': 'Suse',
|
||||
'openSUSE': 'Suse',
|
||||
'SUSE': 'Suse',
|
||||
'Solaris': 'Solaris',
|
||||
'SmartOS': 'Solaris',
|
||||
'Arch ARM': 'Arch',
|
||||
'ALT': 'RedHat',
|
||||
}
|
||||
|
||||
|
||||
def os_data():
|
||||
'''
|
||||
Return grains pertaining to the operating system
|
||||
'''
|
||||
grains = {
|
||||
'num_gpus': 0,
|
||||
'gpus': [],
|
||||
}
|
||||
|
||||
# Windows Server 2008 64-bit
|
||||
# ('Windows', 'MINIONNAME', '2008ServerR2', '6.1.7601', 'AMD64', 'Intel64 Fam ily 6 Model 23 Stepping 6, GenuineIntel')
|
||||
# Ubuntu 10.04
|
||||
# ('Linux', 'MINIONNAME', '2.6.32-38-server', '#83-Ubuntu SMP Wed Jan 4 11:26:59 UTC 2012', 'x86_64', '')
|
||||
(grains['kernel'], grains['nodename'],
|
||||
grains['kernelrelease'], version, grains['cpuarch'], _) = platform.uname()
|
||||
if sys.platform.startswith('win'):
|
||||
grains['osrelease'] = grains['kernelrelease']
|
||||
grains['osversion'] = grains['kernelrelease'] = version
|
||||
grains['os'] = 'Windows'
|
||||
grains['os_family'] = 'Windows'
|
||||
return grains
|
||||
elif not sys.platform.startswith('win'):
|
||||
# Add lsb grains on any distro with lsb-release
|
||||
try:
|
||||
import lsb_release
|
||||
release = lsb_release.get_distro_information()
|
||||
for key, value in release.iteritems():
|
||||
grains['lsb_{0}'.format(key.lower())] = value # override /etc/lsb-release
|
||||
except ImportError:
|
||||
# if the python library isn't available, default to regex
|
||||
if os.path.isfile('/etc/lsb-release'):
|
||||
with open('/etc/lsb-release') as ifile:
|
||||
for line in ifile:
|
||||
# Matches any possible format:
|
||||
# DISTRIB_ID="Ubuntu"
|
||||
# DISTRIB_ID='Mageia'
|
||||
# DISTRIB_ID=Fedora
|
||||
# DISTRIB_RELEASE='10.10'
|
||||
# DISTRIB_CODENAME='squeeze'
|
||||
# DISTRIB_DESCRIPTION='Ubuntu 10.10'
|
||||
regex = re.compile('^(DISTRIB_(?:ID|RELEASE|CODENAME|DESCRIPTION))=(?:\'|")?([\w\s\.-_]+)(?:\'|")?')
|
||||
match = regex.match(line.rstrip('\n'))
|
||||
if match:
|
||||
# Adds: lsb_distrib_{id,release,codename,description}
|
||||
grains['lsb_{0}'.format(match.groups()[0].lower())] = match.groups()[1].rstrip()
|
||||
elif os.path.isfile('/etc/os-release'):
|
||||
# Arch ARM linux
|
||||
with open('/etc/os-release') as ifile:
|
||||
# Imitate lsb-release
|
||||
for line in ifile:
|
||||
# NAME="Arch Linux ARM"
|
||||
# ID=archarm
|
||||
# ID_LIKE=arch
|
||||
# PRETTY_NAME="Arch Linux ARM"
|
||||
# ANSI_COLOR="0;36"
|
||||
# HOME_URL="http://archlinuxarm.org/"
|
||||
# SUPPORT_URL="https://archlinuxarm.org/forum"
|
||||
# BUG_REPORT_URL="https://github.com/archlinuxarm/PKGBUILDs/issues"
|
||||
regex = re.compile('^([\w]+)=(?:\'|")?([\w\s\.-_]+)(?:\'|")?')
|
||||
match = regex.match(line.rstrip('\n'))
|
||||
if match:
|
||||
name, value = match.groups()
|
||||
if name.lower() == 'name':
|
||||
grains['lsb_distrib_id'] = value.strip()
|
||||
elif os.path.isfile('/etc/altlinux-release'):
|
||||
# ALT Linux
|
||||
grains['lsb_distrib_id'] = 'altlinux'
|
||||
with open('/etc/altlinux-release') as ifile:
|
||||
# This file is symlinked to from:
|
||||
# /etc/fedora-release
|
||||
# /etc/redhat-release
|
||||
# /etc/system-release
|
||||
for line in ifile:
|
||||
# ALT Linux Sisyphus (unstable)
|
||||
comps = line.split()
|
||||
if comps[0] == 'ALT':
|
||||
grains['lsb_distrib_release'] = comps[2]
|
||||
grains['lsb_distrib_codename'] = \
|
||||
comps[3].replace('(', '').replace(')', '')
|
||||
# Use the already intelligent platform module to get distro info
|
||||
(osname, osrelease, oscodename) = platform.linux_distribution(
|
||||
supported_dists=_supported_dists)
|
||||
# Try to assign these three names based on the lsb info, they tend to
|
||||
# be more accurate than what python gets from /etc/DISTRO-release.
|
||||
# It's worth noting that Ubuntu has patched their Python distribution
|
||||
# so that platform.linux_distribution() does the /etc/lsb-release
|
||||
# parsing, but we do it anyway here for the sake for full portability.
|
||||
grains['osfullname'] = grains.get('lsb_distrib_id', osname).strip()
|
||||
grains['osrelease'] = grains.get('lsb_distrib_release', osrelease).strip()
|
||||
grains['oscodename'] = grains.get('lsb_distrib_codename', oscodename).strip()
|
||||
distroname = _REPLACE_LINUX_RE.sub('', grains['osfullname']).strip()
|
||||
# return the first ten characters with no spaces, lowercased
|
||||
shortname = distroname.replace(' ', '').lower()[:10]
|
||||
# this maps the long names from the /etc/DISTRO-release files to the
|
||||
# traditional short names that Salt has used.
|
||||
grains['os'] = _OS_NAME_MAP.get(shortname, distroname)
|
||||
elif grains['kernel'] == 'SunOS':
|
||||
grains['os'] = 'Solaris'
|
||||
if os.path.isfile('/etc/release'):
|
||||
with open('/etc/release', 'r') as fp_:
|
||||
rel_data = fp_.read()
|
||||
if 'SmartOS' in rel_data:
|
||||
grains['os'] = 'SmartOS'
|
||||
#grains.update(_sunos_cpudata(grains))
|
||||
elif grains['kernel'] == 'VMkernel':
|
||||
grains['os'] = 'ESXi'
|
||||
elif grains['kernel'] == 'Darwin':
|
||||
grains['os'] = 'MacOS'
|
||||
# grains.update(_bsd_cpudata(grains))
|
||||
else:
|
||||
grains['os'] = grains['kernel']
|
||||
#if grains['kernel'] in ('FreeBSD', 'OpenBSD'):
|
||||
# grains.update(_bsd_cpudata(grains))
|
||||
if not grains['os']:
|
||||
grains['os'] = 'Unknown {0}'.format(grains['kernel'])
|
||||
grains['os_family'] = 'Unknown'
|
||||
else:
|
||||
# this assigns family names based on the os name
|
||||
# family defaults to the os name if not found
|
||||
grains['os_family'] = _OS_FAMILY_MAP.get(grains['os'], grains['os'])
|
||||
|
||||
return grains
|
||||
|
||||
GRAINS = os_data()
|
149
tests/bootstrap/test_install.py
Normal file
149
tests/bootstrap/test_install.py
Normal file
|
@ -0,0 +1,149 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
bootstrap.test_install
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Run installation tests.
|
||||
|
||||
:codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)`
|
||||
:copyright: © 2013 by the SaltStack Team, see AUTHORS for more details.
|
||||
:license: Apache 2.0, see LICENSE for more details.
|
||||
'''
|
||||
|
||||
import shutil
|
||||
from bootstrap import *
|
||||
|
||||
|
||||
class InstallationTestCase(BootstrapTestCase):
|
||||
|
||||
def setUp(self):
|
||||
if os.geteuid() is not 0:
|
||||
self.skipTest('you must be root to run this test')
|
||||
|
||||
def tearDown(self):
|
||||
cleanup_commands = []
|
||||
if GRAINS['os_family'] == 'Debian':
|
||||
cleanup_commands.append(
|
||||
'apt-get remove -y -o DPkg::Options::=--force-confold '
|
||||
'--purge salt-master salt-minion salt-syndic'
|
||||
)
|
||||
cleanup_commands.append(
|
||||
'apt-get autoremove -y -o DPkg::Options::=--force-confold '
|
||||
'--purge'
|
||||
)
|
||||
elif GRAINS['os_family'] == 'RedHat':
|
||||
cleanup_commands.append(
|
||||
'yum -y remove salt-minion salt-master'
|
||||
)
|
||||
|
||||
for cleanup in cleanup_commands:
|
||||
print 'Running cleanup command {0!r}'.format(cleanup)
|
||||
self.assert_script_result(
|
||||
'Failed to execute cleanup command {0!r}'.format(cleanup),
|
||||
0,
|
||||
self.run_script(
|
||||
script=None,
|
||||
args=cleanup.split(),
|
||||
timeout=15 * 60,
|
||||
stream_stds=True
|
||||
)
|
||||
)
|
||||
|
||||
if os.path.isdir('/tmp/git'):
|
||||
print 'Cleaning salt git checkout'
|
||||
shutil.rmtree('/tmp/git')
|
||||
|
||||
def test_install_using_bash(self):
|
||||
if not os.path.exists('/bin/bash'):
|
||||
self.skipTest('\'/bin/bash\' was not found on this system')
|
||||
|
||||
self.assert_script_result(
|
||||
'Failed to install using bash',
|
||||
0,
|
||||
self.run_script(
|
||||
executable='/bin/bash',
|
||||
timeout=15 * 60,
|
||||
stream_stds=True
|
||||
)
|
||||
)
|
||||
|
||||
def test_install_using_sh(self):
|
||||
self.assert_script_result(
|
||||
'Failed to install using sh',
|
||||
0,
|
||||
self.run_script(
|
||||
timeout=15 * 60,
|
||||
stream_stds=True
|
||||
)
|
||||
)
|
||||
|
||||
def test_install_explicit_stable(self):
|
||||
self.assert_script_result(
|
||||
'Failed to install explicit stable using sh',
|
||||
0,
|
||||
self.run_script(
|
||||
args=('stable',),
|
||||
timeout=15 * 60,
|
||||
stream_stds=True
|
||||
)
|
||||
)
|
||||
|
||||
def test_install_daily(self):
|
||||
rc, out, err = self.run_script(
|
||||
args=('daily',), timeout=15 * 60, stream_stds=True
|
||||
)
|
||||
if GRAINS['os'] == 'Ubuntu':
|
||||
self.assert_script_result(
|
||||
'Failed to install daily',
|
||||
0, (rc, out, err)
|
||||
)
|
||||
else:
|
||||
self.assert_script_result(
|
||||
'Although system is not Ubuntu, we managed to install',
|
||||
1, (rc, out, err)
|
||||
)
|
||||
|
||||
def test_install_stable_piped_through_sh(self):
|
||||
self.assert_script_result(
|
||||
'Failed to install stable piped through sh',
|
||||
0,
|
||||
self.run_script(
|
||||
script=None,
|
||||
args='cat {0} | sh '.format(BOOTSTRAP_SCRIPT_PATH).split(),
|
||||
timeout=15 * 60,
|
||||
stream_stds=True
|
||||
)
|
||||
)
|
||||
|
||||
def test_install_latest_from_git_develop(self):
|
||||
self.assert_script_result(
|
||||
'Failed to install using latest git develop',
|
||||
0,
|
||||
self.run_script(
|
||||
args=('git', 'develop'),
|
||||
timeout=15 * 60,
|
||||
stream_stds=True
|
||||
)
|
||||
)
|
||||
|
||||
def test_install_specific_git_tag(self):
|
||||
self.assert_script_result(
|
||||
'Failed to install using specific git tag',
|
||||
0,
|
||||
self.run_script(
|
||||
args=('git', 'v0.12.1'),
|
||||
timeout=15 * 60,
|
||||
stream_stds=True
|
||||
)
|
||||
)
|
||||
|
||||
def test_install_specific_git_sha(self):
|
||||
self.assert_script_result(
|
||||
'Failed to install using specific git sha',
|
||||
0,
|
||||
self.run_script(
|
||||
args=('git', 'v0.12.1'),
|
||||
timeout=15 * 60,
|
||||
stream_stds=True
|
||||
)
|
||||
)
|
27
tests/bootstrap/test_lint.py
Normal file
27
tests/bootstrap/test_lint.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
bootstrap.test_lint
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)`
|
||||
:copyright: © 2013 by the UfSoft.org Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
'''
|
||||
from bootstrap import *
|
||||
|
||||
|
||||
class LintTestCase(BootstrapTestCase):
|
||||
def test_bashisms(self):
|
||||
'''
|
||||
Lint check the bootstrap script for any possible bash'isms.
|
||||
'''
|
||||
if not os.path.exists('/usr/bin/perl'):
|
||||
self.skipTest('\'/usr/bin/perl\' was not found on this system')
|
||||
self.assert_script_result(
|
||||
'Some bashisms were found',
|
||||
0,
|
||||
self.run_script(
|
||||
script=os.path.join(EXT_DIR, 'checkbashisms'),
|
||||
args=('-pxfn', BOOTSTRAP_SCRIPT_PATH)
|
||||
)
|
||||
)
|
23
tests/bootstrap/test_usage.py
Normal file
23
tests/bootstrap/test_usage.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
bootstrap.test_usage
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)`
|
||||
:copyright: © 2013 by the UfSoft.org Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from bootstrap import *
|
||||
|
||||
|
||||
class UsageTestCase(BootstrapTestCase):
|
||||
def test_no_daemon_install_fails(self):
|
||||
'''
|
||||
Passing '-N'(no minion) without passing '-M'(install master) or
|
||||
'-S'(install syndic) fails.
|
||||
'''
|
||||
self.assert_script_result(
|
||||
'Not installing any daemons did not throw any error',
|
||||
1,
|
||||
self.run_script(args=('-N',))
|
||||
)
|
640
tests/ext/checkbashisms
Executable file
640
tests/ext/checkbashisms
Executable file
|
@ -0,0 +1,640 @@
|
|||
#! /usr/bin/perl -w
|
||||
|
||||
# This script is essentially copied from /usr/share/lintian/checks/scripts,
|
||||
# which is:
|
||||
# Copyright (C) 1998 Richard Braakman
|
||||
# Copyright (C) 2002 Josip Rodin
|
||||
# This version is
|
||||
# Copyright (C) 2003 Julian Gilbey
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use strict;
|
||||
use Getopt::Long qw(:config gnu_getopt);
|
||||
use File::Temp qw/tempfile/;
|
||||
|
||||
sub init_hashes;
|
||||
|
||||
(my $progname = $0) =~ s|.*/||;
|
||||
|
||||
my $usage = <<"EOF";
|
||||
Usage: $progname [-n] [-f] [-x] script ...
|
||||
or: $progname --help
|
||||
or: $progname --version
|
||||
This script performs basic checks for the presence of bashisms
|
||||
in /bin/sh scripts.
|
||||
EOF
|
||||
|
||||
my $version = <<"EOF";
|
||||
This is $progname, from the Debian devscripts package, version 2.11.6ubuntu1.4
|
||||
This code is copyright 2003 by Julian Gilbey <jdg\@debian.org>,
|
||||
based on original code which is copyright 1998 by Richard Braakman
|
||||
and copyright 2002 by Josip Rodin.
|
||||
This program comes with ABSOLUTELY NO WARRANTY.
|
||||
You are free to redistribute this code under the terms of the
|
||||
GNU General Public License, version 2, or (at your option) any later version.
|
||||
EOF
|
||||
|
||||
my ($opt_echo, $opt_force, $opt_extra, $opt_posix);
|
||||
my ($opt_help, $opt_version);
|
||||
my @filenames;
|
||||
|
||||
# Detect if STDIN is a pipe
|
||||
if (-p STDIN or -f STDIN) {
|
||||
my ($tmp_fh, $tmp_filename) = tempfile("chkbashisms_tmp.XXXX", TMPDIR => 1, UNLINK => 1);
|
||||
while (my $line = <STDIN>) {
|
||||
print $tmp_fh $line;
|
||||
}
|
||||
close($tmp_fh);
|
||||
push(@ARGV, $tmp_filename);
|
||||
}
|
||||
|
||||
##
|
||||
## handle command-line options
|
||||
##
|
||||
$opt_help = 1 if int(@ARGV) == 0;
|
||||
|
||||
GetOptions("help|h" => \$opt_help,
|
||||
"version|v" => \$opt_version,
|
||||
"newline|n" => \$opt_echo,
|
||||
"force|f" => \$opt_force,
|
||||
"extra|x" => \$opt_extra,
|
||||
"posix|p" => \$opt_posix,
|
||||
)
|
||||
or die "Usage: $progname [options] filelist\nRun $progname --help for more details\n";
|
||||
|
||||
if ($opt_help) { print $usage; exit 0; }
|
||||
if ($opt_version) { print $version; exit 0; }
|
||||
|
||||
$opt_echo = 1 if $opt_posix;
|
||||
|
||||
my $status = 0;
|
||||
my $makefile = 0;
|
||||
my (%bashisms, %string_bashisms, %singlequote_bashisms);
|
||||
|
||||
my $LEADIN = qr'(?:(?:^|[`&;(|{])\s*|(?:if|then|do|while|shell)\s+)';
|
||||
init_hashes;
|
||||
|
||||
foreach my $filename (@ARGV) {
|
||||
my $check_lines_count = -1;
|
||||
|
||||
my $display_filename = $filename;
|
||||
if ($filename =~ /chkbashisms_tmp\.....$/) {
|
||||
$display_filename = "(stdin)";
|
||||
}
|
||||
|
||||
if (!$opt_force) {
|
||||
$check_lines_count = script_is_evil_and_wrong($filename);
|
||||
}
|
||||
|
||||
if ($check_lines_count == 0 or $check_lines_count == 1) {
|
||||
warn "script $display_filename does not appear to be a /bin/sh script; skipping\n";
|
||||
next;
|
||||
}
|
||||
|
||||
if ($check_lines_count != -1) {
|
||||
warn "script $display_filename appears to be a shell wrapper; only checking the first "
|
||||
. "$check_lines_count lines\n";
|
||||
}
|
||||
|
||||
unless (open C, '<', $filename) {
|
||||
warn "cannot open script $display_filename for reading: $!\n";
|
||||
$status |= 2;
|
||||
next;
|
||||
}
|
||||
|
||||
my $cat_string = "";
|
||||
my $cat_indented = 0;
|
||||
my $quote_string = "";
|
||||
my $last_continued = 0;
|
||||
my $continued = 0;
|
||||
my $found_rules = 0;
|
||||
my $buffered_orig_line = "";
|
||||
my $buffered_line = "";
|
||||
|
||||
while (<C>) {
|
||||
next unless ($check_lines_count == -1 or $. <= $check_lines_count);
|
||||
|
||||
if ($. == 1) { # This should be an interpreter line
|
||||
if (m,^\#!\s*(\S+),) {
|
||||
my $interpreter = $1;
|
||||
|
||||
if ($interpreter =~ m,/make$,) {
|
||||
init_hashes if !$makefile++;
|
||||
$makefile = 1;
|
||||
} else {
|
||||
init_hashes if $makefile--;
|
||||
$makefile = 0;
|
||||
}
|
||||
next if $opt_force;
|
||||
|
||||
if ($interpreter =~ m,/bash$,) {
|
||||
warn "script $display_filename is already a bash script; skipping\n";
|
||||
$status |= 2;
|
||||
last; # end this file
|
||||
}
|
||||
elsif ($interpreter !~ m,/(sh|posh)$,) {
|
||||
### ksh/zsh?
|
||||
warn "script $display_filename does not appear to be a /bin/sh script; skipping\n";
|
||||
$status |= 2;
|
||||
last;
|
||||
}
|
||||
} else {
|
||||
warn "script $display_filename does not appear to have a \#! interpreter line;\nyou may get strange results\n";
|
||||
}
|
||||
}
|
||||
|
||||
chomp;
|
||||
my $orig_line = $_;
|
||||
|
||||
# We want to remove end-of-line comments, so need to skip
|
||||
# comments that appear inside balanced pairs
|
||||
# of single or double quotes
|
||||
|
||||
# Remove comments in the "quoted" part of a line that starts
|
||||
# in a quoted block? The problem is that we have no idea
|
||||
# whether the program interpreting the block treats the
|
||||
# quote character as part of the comment or as a quote
|
||||
# terminator. We err on the side of caution and assume it
|
||||
# will be treated as part of the comment.
|
||||
# s/^(?:.*?[^\\])?$quote_string(.*)$/$1/ if $quote_string ne "";
|
||||
|
||||
# skip comment lines
|
||||
if (m,^\s*\#, && $quote_string eq '' && $buffered_line eq '' && $cat_string eq '') {
|
||||
next;
|
||||
}
|
||||
|
||||
# Remove quoted strings so we can more easily ignore comments
|
||||
# inside them
|
||||
s/(^|[^\\](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
|
||||
s/(^|[^\\](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
|
||||
|
||||
# If the remaining string contains what looks like a comment,
|
||||
# eat it. In either case, swap the unmodified script line
|
||||
# back in for processing.
|
||||
if (m/(?:^|[^[\\])[\s\&;\(\)](\#.*$)/) {
|
||||
$_ = $orig_line;
|
||||
s/\Q$1\E//; # eat comments
|
||||
} else {
|
||||
$_ = $orig_line;
|
||||
}
|
||||
|
||||
# Handle line continuation
|
||||
if (!$makefile && $cat_string eq '' && m/\\$/) {
|
||||
chop;
|
||||
$buffered_line .= $_;
|
||||
$buffered_orig_line .= $orig_line . "\n";
|
||||
next;
|
||||
}
|
||||
|
||||
if ($buffered_line ne '') {
|
||||
$_ = $buffered_line . $_;
|
||||
$orig_line = $buffered_orig_line . $orig_line;
|
||||
$buffered_line ='';
|
||||
$buffered_orig_line ='';
|
||||
}
|
||||
|
||||
if ($makefile) {
|
||||
$last_continued = $continued;
|
||||
if (/[^\\]\\$/) {
|
||||
$continued = 1;
|
||||
} else {
|
||||
$continued = 0;
|
||||
}
|
||||
|
||||
# Don't match lines that look like a rule if we're in a
|
||||
# continuation line before the start of the rules
|
||||
if (/^[\w%-]+:+\s.*?;?(.*)$/ and !($last_continued and !$found_rules)) {
|
||||
$found_rules = 1;
|
||||
$_ = $1 if $1;
|
||||
}
|
||||
|
||||
last if m%^\s*(override\s|export\s)?\s*SHELL\s*:?=\s*(/bin/)?bash\s*%;
|
||||
|
||||
# Remove "simple" target names
|
||||
s/^[\w%.-]+(?:\s+[\w%.-]+)*::?//;
|
||||
s/^\t//;
|
||||
s/(?<!\$)\$\((\w+)\)/\${$1}/g;
|
||||
s/(\$){2}/$1/g;
|
||||
s/^[\s\t]*[@-]{1,2}//;
|
||||
}
|
||||
|
||||
if ($cat_string ne "" && (m/^\Q$cat_string\E$/ || ($cat_indented && m/^\t*\Q$cat_string\E$/))) {
|
||||
$cat_string = "";
|
||||
next;
|
||||
}
|
||||
my $within_another_shell = 0;
|
||||
if (m,(^|\s+)((/usr)?/bin/)?((b|d)?a|k|z|t?c)sh\s+-c\s*.+,) {
|
||||
$within_another_shell = 1;
|
||||
}
|
||||
# if cat_string is set, we are in a HERE document and need not
|
||||
# check for things
|
||||
if ($cat_string eq "" and !$within_another_shell) {
|
||||
my $found = 0;
|
||||
my $match = '';
|
||||
my $explanation = '';
|
||||
my $line = $_;
|
||||
|
||||
# Remove "" / '' as they clearly aren't quoted strings
|
||||
# and not considering them makes the matching easier
|
||||
$line =~ s/(^|[^\\])(\'\')+/$1/g;
|
||||
$line =~ s/(^|[^\\])(\"\")+/$1/g;
|
||||
|
||||
if ($quote_string ne "") {
|
||||
my $otherquote = ($quote_string eq "\"" ? "\'" : "\"");
|
||||
# Inside a quoted block
|
||||
if ($line =~ /(?:^|^.*?[^\\])$quote_string(.*)$/) {
|
||||
my $rest = $1;
|
||||
my $templine = $line;
|
||||
|
||||
# Remove quoted strings delimited with $otherquote
|
||||
$templine =~ s/(^|[^\\])$otherquote[^$quote_string]*?[^\\]$otherquote/$1/g;
|
||||
# Remove quotes that are themselves quoted
|
||||
# "a'b"
|
||||
$templine =~ s/(^|[^\\])$otherquote.*?$quote_string.*?[^\\]$otherquote/$1/g;
|
||||
# "\""
|
||||
$templine =~ s/(^|[^\\])$quote_string\\$quote_string$quote_string/$1/g;
|
||||
|
||||
# After all that, were there still any quotes left?
|
||||
my $count = () = $templine =~ /(^|[^\\])$quote_string/g;
|
||||
next if $count == 0;
|
||||
|
||||
$count = () = $rest =~ /(^|[^\\])$quote_string/g;
|
||||
if ($count % 2 == 0) {
|
||||
# Quoted block ends on this line
|
||||
# Ignore everything before the closing quote
|
||||
$line = $rest || '';
|
||||
$quote_string = "";
|
||||
} else {
|
||||
next;
|
||||
}
|
||||
} else {
|
||||
# Still inside the quoted block, skip this line
|
||||
next;
|
||||
}
|
||||
}
|
||||
|
||||
# Check even if we removed the end of a quoted block
|
||||
# in the previous check, as a single line can end one
|
||||
# block and begin another
|
||||
if ($quote_string eq "") {
|
||||
# Possible start of a quoted block
|
||||
for my $quote ("\"", "\'") {
|
||||
my $templine = $line;
|
||||
my $otherquote = ($quote eq "\"" ? "\'" : "\"");
|
||||
|
||||
# Remove balanced quotes and their content
|
||||
$templine =~ s/(^|[^\\\"](?:\\\\)*)\'[^\']*\'/$1/g;
|
||||
$templine =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/g;
|
||||
|
||||
# Don't flag quotes that are themselves quoted
|
||||
# "a'b"
|
||||
$templine =~ s/$otherquote.*?$quote.*?$otherquote//g;
|
||||
# "\""
|
||||
$templine =~ s/(^|[^\\])$quote\\$quote$quote/$1/g;
|
||||
# \' or \"
|
||||
$templine =~ s/\\[\'\"]//g;
|
||||
my $count = () = $templine =~ /(^|(?!\\))$quote/g;
|
||||
|
||||
# If there's an odd number of non-escaped
|
||||
# quotes in the line it's almost certainly the
|
||||
# start of a quoted block.
|
||||
if ($count % 2 == 1) {
|
||||
$quote_string = $quote;
|
||||
$line =~ s/^(.*)$quote.*$/$1/;
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# since this test is ugly, I have to do it by itself
|
||||
# detect source (.) trying to pass args to the command it runs
|
||||
# The first expression weeds out '. "foo bar"'
|
||||
if (not $found and
|
||||
not m/$LEADIN\.\s+(\"[^\"]+\"|\'[^\']+\'|\$\([^)]+\)+(?:\/[^\s;]+)?)\s*(\&|\||\d?>|<|;|\Z)/
|
||||
and m/$LEADIN(\.\s+[^\s;\`:]+\s+([^\s;]+))/) {
|
||||
if ($2 =~ /^(\&|\||\d?>|<)/) {
|
||||
# everything is ok
|
||||
;
|
||||
} else {
|
||||
$found = 1;
|
||||
$match = $1;
|
||||
$explanation = "sourced script with arguments";
|
||||
output_explanation($display_filename, $orig_line, $explanation);
|
||||
}
|
||||
}
|
||||
|
||||
# Remove "quoted quotes". They're likely to be inside
|
||||
# another pair of quotes; we're not interested in
|
||||
# them for their own sake and removing them makes finding
|
||||
# the limits of the outer pair far easier.
|
||||
$line =~ s/(^|[^\\\'\"])\"\'\"/$1/g;
|
||||
$line =~ s/(^|[^\\\'\"])\'\"\'/$1/g;
|
||||
|
||||
while (my ($re,$expl) = each %singlequote_bashisms) {
|
||||
if ($line =~ m/($re)/) {
|
||||
$found = 1;
|
||||
$match = $1;
|
||||
$explanation = $expl;
|
||||
output_explanation($display_filename, $orig_line, $explanation);
|
||||
}
|
||||
}
|
||||
|
||||
my $re='(?<![\$\\\])\$\'[^\']+\'';
|
||||
if ($line =~ m/(.*)($re)/){
|
||||
my $count = () = $1 =~ /(^|[^\\])\'/g;
|
||||
if( $count % 2 == 0 ) {
|
||||
output_explanation($display_filename, $orig_line, q<$'...' should be "$(printf '...')">);
|
||||
}
|
||||
}
|
||||
|
||||
# $cat_line contains the version of the line we'll check
|
||||
# for heredoc delimiters later. Initially, remove any
|
||||
# spaces between << and the delimiter to make the following
|
||||
# updates to $cat_line easier.
|
||||
my $cat_line = $line;
|
||||
$cat_line =~ s/(<\<-?)\s+/$1/g;
|
||||
|
||||
# Ignore anything inside single quotes; it could be an
|
||||
# argument to grep or the like.
|
||||
$line =~ s/(^|[^\\\"](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
|
||||
|
||||
# As above, with the exception that we don't remove the string
|
||||
# if the quote is immediately preceeded by a < or a -, so we
|
||||
# can match "foo <<-?'xyz'" as a heredoc later
|
||||
# The check is a little more greedy than we'd like, but the
|
||||
# heredoc test itself will weed out any false positives
|
||||
$cat_line =~ s/(^|[^<\\\"-](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
|
||||
|
||||
$re='(?<![\$\\\])\$\"[^\"]+\"';
|
||||
if ($line =~ m/(.*)($re)/){
|
||||
my $count = () = $1 =~ /(^|[^\\])\"/g;
|
||||
if( $count % 2 == 0 ) {
|
||||
output_explanation($display_filename, $orig_line, q<$"foo" should be eval_gettext "foo">);
|
||||
}
|
||||
}
|
||||
|
||||
while (my ($re,$expl) = each %string_bashisms) {
|
||||
if ($line =~ m/($re)/) {
|
||||
$found = 1;
|
||||
$match = $1;
|
||||
$explanation = $expl;
|
||||
output_explanation($display_filename, $orig_line, $explanation);
|
||||
}
|
||||
}
|
||||
|
||||
# We've checked for all the things we still want to notice in
|
||||
# double-quoted strings, so now remove those strings as well.
|
||||
$line =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
|
||||
$cat_line =~ s/(^|[^<\\\'-](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
|
||||
while (my ($re,$expl) = each %bashisms) {
|
||||
if ($line =~ m/($re)/) {
|
||||
$found = 1;
|
||||
$match = $1;
|
||||
$explanation = $expl;
|
||||
output_explanation($display_filename, $orig_line, $explanation);
|
||||
}
|
||||
}
|
||||
|
||||
# Only look for the beginning of a heredoc here, after we've
|
||||
# stripped out quoted material, to avoid false positives.
|
||||
if ($cat_line =~ m/(?:^|[^<])\<\<(\-?)\s*(?:[\\]?(\w+)|[\'\"](.*?)[\'\"])/) {
|
||||
$cat_indented = ($1 && $1 eq '-')? 1 : 0;
|
||||
$cat_string = $2;
|
||||
$cat_string = $3 if not defined $cat_string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
warn "error: $filename: Unterminated heredoc found, EOF reached. Wanted: <$cat_string>\n"
|
||||
if ($cat_string ne '');
|
||||
warn "error: $filename: Unterminated quoted string found, EOF reached. Wanted: <$quote_string>\n"
|
||||
if ($quote_string ne '');
|
||||
warn "error: $filename: EOF reached while on line continuation.\n"
|
||||
if ($buffered_line ne '');
|
||||
|
||||
close C;
|
||||
}
|
||||
|
||||
exit $status;
|
||||
|
||||
sub output_explanation {
|
||||
my ($filename, $line, $explanation) = @_;
|
||||
|
||||
warn "possible bashism in $filename line $. ($explanation):\n$line\n";
|
||||
$status |= 1;
|
||||
}
|
||||
|
||||
# Returns non-zero if the given file is not actually a shell script,
|
||||
# just looks like one.
|
||||
sub script_is_evil_and_wrong {
|
||||
my ($filename) = @_;
|
||||
my $ret = -1;
|
||||
# lintian's version of this function aborts if the file
|
||||
# can't be opened, but we simply return as the next
|
||||
# test in the calling code handles reporting the error
|
||||
# itself
|
||||
open (IN, '<', $filename) or return $ret;
|
||||
my $i = 0;
|
||||
my $var = "0";
|
||||
my $backgrounded = 0;
|
||||
local $_;
|
||||
while (<IN>) {
|
||||
chomp;
|
||||
next if /^#/o;
|
||||
next if /^$/o;
|
||||
last if (++$i > 55);
|
||||
if (m~
|
||||
# the exec should either be "eval"ed or a new statement
|
||||
(^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*)
|
||||
|
||||
# eat anything between the exec and $0
|
||||
exec\s*.+\s*
|
||||
|
||||
# optionally quoted executable name (via $0)
|
||||
.?\$$var.?\s*
|
||||
|
||||
# optional "end of options" indicator
|
||||
(--\s*)?
|
||||
|
||||
# Match expressions of the form '${1+$@}', '${1:+"$@"',
|
||||
# '"${1+$@', "$@", etc where the quotes (before the dollar
|
||||
# sign(s)) are optional and the second (or only if the $1
|
||||
# clause is omitted) parameter may be $@ or $*.
|
||||
#
|
||||
# Finally the whole subexpression may be omitted for scripts
|
||||
# which do not pass on their parameters (i.e. after re-execing
|
||||
# they take their parameters (and potentially data) from stdin
|
||||
.?(\${1:?\+.?)?(\$(\@|\*))?~x) {
|
||||
$ret = $. - 1;
|
||||
last;
|
||||
} elsif (/^\s*(\w+)=\$0;/) {
|
||||
$var = $1;
|
||||
} elsif (m~
|
||||
# Match scripts which use "foo $0 $@ &\nexec true\n"
|
||||
# Program name
|
||||
\S+\s+
|
||||
|
||||
# As above
|
||||
.?\$$var.?\s*
|
||||
(--\s*)?
|
||||
.?(\${1:?\+.?)?(\$(\@|\*))?.?\s*\&~x) {
|
||||
|
||||
$backgrounded = 1;
|
||||
} elsif ($backgrounded and m~
|
||||
# the exec should either be "eval"ed or a new statement
|
||||
(^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*)
|
||||
exec\s+true(\s|\Z)~x) {
|
||||
|
||||
$ret = $. - 1;
|
||||
last;
|
||||
} elsif (m~\@DPATCH\@~) {
|
||||
$ret = $. - 1;
|
||||
last;
|
||||
}
|
||||
|
||||
}
|
||||
close IN;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
sub init_hashes {
|
||||
|
||||
%bashisms = (
|
||||
qr'(?:^|\s+)function \w+(\s|\(|\Z)' => q<'function' is useless>,
|
||||
$LEADIN . qr'select\s+\w+' => q<'select' is not POSIX>,
|
||||
qr'(test|-o|-a)\s*[^\s]+\s+==\s' => q<should be 'b = a'>,
|
||||
qr'\[\s+[^\]]+\s+==\s' => q<should be 'b = a'>,
|
||||
qr'\s\|\&' => q<pipelining is not POSIX>,
|
||||
qr'[^\\\$]\{([^\s\\\}]*?,)+[^\\\}\s]*\}' => q<brace expansion>,
|
||||
qr'\{\d+\.\.\d+\}' => q<brace expansion, should be $(seq a b)>,
|
||||
qr'(?:^|\s+)\w+\[\d+\]=' => q<bash arrays, H[0]>,
|
||||
$LEADIN . qr'read\s+(?:-[a-qs-zA-Z\d-]+)' => q<read with option other than -r>,
|
||||
$LEADIN . qr'read\s*(?:-\w+\s*)*(?:\".*?\"|[\'].*?[\'])?\s*(?:;|$)'
|
||||
=> q<read without variable>,
|
||||
$LEADIN . qr'echo\s+(-n\s+)?-n?en?\s' => q<echo -e>,
|
||||
$LEADIN . qr'exec\s+-[acl]' => q<exec -c/-l/-a name>,
|
||||
$LEADIN . qr'let\s' => q<let ...>,
|
||||
qr'(?<![\$\(])\(\(.*\)\)' => q<'((' should be '$(('>,
|
||||
qr'(?:^|\s+)(\[|test)\s+-a' => q<test with unary -a (should be -e)>,
|
||||
qr'\&>' => q<should be \>word 2\>&1>,
|
||||
qr'(<\&|>\&)\s*((-|\d+)[^\s;|)}`&\\\\]|[^-\d\s]+(?<!\$)(?!\d))' =>
|
||||
q<should be \>word 2\>&1>,
|
||||
qr'\[\[(?!:)' => q<alternative test command ([[ foo ]] should be [ foo ])>,
|
||||
qr'/dev/(tcp|udp)' => q</dev/(tcp|udp)>,
|
||||
$LEADIN . qr'builtin\s' => q<builtin>,
|
||||
$LEADIN . qr'caller\s' => q<caller>,
|
||||
$LEADIN . qr'compgen\s' => q<compgen>,
|
||||
$LEADIN . qr'complete\s' => q<complete>,
|
||||
$LEADIN . qr'declare\s' => q<declare>,
|
||||
$LEADIN . qr'dirs(\s|\Z)' => q<dirs>,
|
||||
$LEADIN . qr'disown\s' => q<disown>,
|
||||
$LEADIN . qr'enable\s' => q<enable>,
|
||||
$LEADIN . qr'mapfile\s' => q<mapfile>,
|
||||
$LEADIN . qr'readarray\s' => q<readarray>,
|
||||
$LEADIN . qr'shopt(\s|\Z)' => q<shopt>,
|
||||
$LEADIN . qr'suspend\s' => q<suspend>,
|
||||
$LEADIN . qr'time\s' => q<time>,
|
||||
$LEADIN . qr'type\s' => q<type>,
|
||||
$LEADIN . qr'typeset\s' => q<typeset>,
|
||||
$LEADIN . qr'ulimit(\s|\Z)' => q<ulimit>,
|
||||
$LEADIN . qr'set\s+-[BHT]+' => q<set -[BHT]>,
|
||||
$LEADIN . qr'alias\s+-p' => q<alias -p>,
|
||||
$LEADIN . qr'unalias\s+-a' => q<unalias -a>,
|
||||
$LEADIN . qr'local\s+-[a-zA-Z]+' => q<local -opt>,
|
||||
qr'(?:^|\s+)\s*\(?\w*[^\(\w\s]+\S*?\s*\(\)\s*([\{|\(]|\Z)'
|
||||
=> q<function names should only contain [a-z0-9_]>,
|
||||
$LEADIN . qr'(push|pop)d(\s|\Z)' => q<(push|pop)d>,
|
||||
$LEADIN . qr'export\s+-[^p]' => q<export only takes -p as an option>,
|
||||
qr'(?:^|\s+)[<>]\(.*?\)' => q<\<() process substituion>,
|
||||
$LEADIN . qr'readonly\s+-[af]' => q<readonly -[af]>,
|
||||
$LEADIN . qr'(sh|\$\{?SHELL\}?) -[rD]' => q<sh -[rD]>,
|
||||
$LEADIN . qr'(sh|\$\{?SHELL\}?) --\w+' => q<sh --long-option>,
|
||||
$LEADIN . qr'(sh|\$\{?SHELL\}?) [-+]O' => q<sh [-+]O>,
|
||||
qr'\[\^[^]]+\]' => q<[^] should be [!]>,
|
||||
$LEADIN . qr'printf\s+-v' => q<'printf -v var ...' should be var='$(printf ...)'>,
|
||||
$LEADIN . qr'coproc\s' => q<coproc>,
|
||||
qr';;?&' => q<;;& and ;& special case operators>,
|
||||
$LEADIN . qr'jobs\s' => q<jobs>,
|
||||
# $LEADIN . qr'jobs\s+-[^lp]\s' => q<'jobs' with option other than -l or -p>,
|
||||
$LEADIN . qr'command\s+-[^p]\s' => q<'command' with option other than -p>,
|
||||
);
|
||||
|
||||
%string_bashisms = (
|
||||
qr'\$\[[^][]+\]' => q<'$[' should be '$(('>,
|
||||
qr'\$\{\w+\:\d+(?::\d+)?\}' => q<${foo:3[:1]}>,
|
||||
qr'\$\{!\w+[\@*]\}' => q<${!prefix[*|@]>,
|
||||
qr'\$\{!\w+\}' => q<${!name}>,
|
||||
qr'\$\{\w+(/.+?){1,2}\}' => q<${parm/?/pat[/str]}>,
|
||||
qr'\$\{\#?\w+\[[0-9\*\@]+\]\}' => q<bash arrays, ${name[0|*|@]}>,
|
||||
qr'\$\{?RANDOM\}?\b' => q<$RANDOM>,
|
||||
qr'\$\{?(OS|MACH)TYPE\}?\b' => q<$(OS|MACH)TYPE>,
|
||||
qr'\$\{?HOST(TYPE|NAME)\}?\b' => q<$HOST(TYPE|NAME)>,
|
||||
qr'\$\{?DIRSTACK\}?\b' => q<$DIRSTACK>,
|
||||
qr'\$\{?EUID\}?\b' => q<$EUID should be "$(id -u)">,
|
||||
qr'\$\{?UID\}?\b' => q<$UID should be "$(id -ru)">,
|
||||
qr'\$\{?SECONDS\}?\b' => q<$SECONDS>,
|
||||
qr'\$\{?BASH_[A-Z]+\}?\b' => q<$BASH_SOMETHING>,
|
||||
qr'\$\{?SHELLOPTS\}?\b' => q<$SHELLOPTS>,
|
||||
qr'\$\{?PIPESTATUS\}?\b' => q<$PIPESTATUS>,
|
||||
qr'\$\{?SHLVL\}?\b' => q<$SHLVL>,
|
||||
qr'<<<' => q<\<\<\< here string>,
|
||||
$LEADIN . qr'echo\s+(?:-[^e\s]+\s+)?\"[^\"]*(\\[abcEfnrtv0])+.*?[\"]' => q<unsafe echo with backslash>,
|
||||
qr'\$\(\([\s\w$*/+-]*\w\+\+.*?\)\)' => q<'$((n++))' should be '$n; $((n=n+1))'>,
|
||||
qr'\$\(\([\s\w$*/+-]*\+\+\w.*?\)\)' => q<'$((++n))' should be '$((n=n+1))'>,
|
||||
qr'\$\(\([\s\w$*/+-]*\w\-\-.*?\)\)' => q<'$((n--))' should be '$n; $((n=n-1))'>,
|
||||
qr'\$\(\([\s\w$*/+-]*\-\-\w.*?\)\)' => q<'$((--n))' should be '$((n=n-1))'>,
|
||||
qr'\$\(\([\s\w$*/+-]*\*\*.*?\)\)' => q<exponentiation is not POSIX>,
|
||||
$LEADIN . qr'printf\s["\'][^"\']+?%[qb].+?["\']' => q<printf %q|%b>,
|
||||
);
|
||||
|
||||
%singlequote_bashisms = (
|
||||
$LEADIN . qr'echo\s+(?:-[^e\s]+\s+)?\'[^\']*(\\[abcEfnrtv0])+.*?[\']' => q<unsafe echo with backslash>,
|
||||
$LEADIN . qr'source\s+[\"\']?(?:\.\/|\/|\$|[\w~.-])\S*' =>
|
||||
q<should be '.', not 'source'>,
|
||||
);
|
||||
|
||||
if ($opt_echo) {
|
||||
$bashisms{$LEADIN . qr'echo\s+-[A-Za-z]*n'} = q<echo -n>;
|
||||
}
|
||||
if ($opt_posix) {
|
||||
$bashisms{$LEADIN . qr'local\s+\w+(\s+\W|\s*[;&|)]|$)'} = q<local foo>;
|
||||
$bashisms{$LEADIN . qr'local\s+\w+='} = q<local foo=bar>;
|
||||
$bashisms{$LEADIN . qr'local\s+\w+\s+\w+'} = q<local x y>;
|
||||
$bashisms{$LEADIN . qr'((?:test|\[)\s+.+\s-[ao])\s'} = q<test -a/-o>;
|
||||
$bashisms{$LEADIN . qr'kill\s+-[^sl]\w*'} = q<kill -[0-9] or -[A-Z]>;
|
||||
$bashisms{$LEADIN . qr'trap\s+["\']?.*["\']?\s+.*[1-9]'} = q<trap with signal numbers>;
|
||||
}
|
||||
|
||||
if ($makefile) {
|
||||
$string_bashisms{qr'(\$\(|\`)\s*\<\s*([^\s\)]{2,}|[^DF])\s*(\)|\`)'} =
|
||||
q<'$(\< foo)' should be '$(cat foo)'>;
|
||||
} else {
|
||||
$bashisms{$LEADIN . qr'\w+\+='} = q<should be VAR="${VAR}foo">;
|
||||
$string_bashisms{qr'(\$\(|\`)\s*\<\s*\S+\s*(\)|\`)'} = q<'$(\< foo)' should be '$(cat foo)'>;
|
||||
}
|
||||
|
||||
if ($opt_extra) {
|
||||
$string_bashisms{qr'\$\{?BASH\}?\b'} = q<$BASH>;
|
||||
$string_bashisms{qr'(?:^|\s+)RANDOM='} = q<RANDOM=>;
|
||||
$string_bashisms{qr'(?:^|\s+)(OS|MACH)TYPE='} = q<(OS|MACH)TYPE=>;
|
||||
$string_bashisms{qr'(?:^|\s+)HOST(TYPE|NAME)='} = q<HOST(TYPE|NAME)=>;
|
||||
$string_bashisms{qr'(?:^|\s+)DIRSTACK='} = q<DIRSTACK=>;
|
||||
$string_bashisms{qr'(?:^|\s+)EUID='} = q<EUID=>;
|
||||
$string_bashisms{qr'(?:^|\s+)UID='} = q<UID=>;
|
||||
$string_bashisms{qr'(?:^|\s+)BASH(_[A-Z]+)?='} = q<BASH(_SOMETHING)=>;
|
||||
$string_bashisms{qr'(?:^|\s+)SHELLOPTS='} = q<SHELLOPTS=>;
|
||||
$string_bashisms{qr'\$\{?POSIXLY_CORRECT\}?\b'} = q<$POSIXLY_CORRECT>;
|
||||
}
|
||||
}
|
193
tests/runtests.py
Executable file
193
tests/runtests.py
Executable file
|
@ -0,0 +1,193 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
test-bootstrap.py
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
salt-bootstrap script unit-testing
|
||||
|
||||
:codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)`
|
||||
:copyright: © 2013 by the SaltStack Team, see AUTHORS for more details.
|
||||
:license: Apache 2.0, see LICENSE for more details.
|
||||
'''
|
||||
|
||||
import os
|
||||
import pprint
|
||||
import shutil
|
||||
import optparse
|
||||
|
||||
from bootstrap import GRAINS, TestLoader, TextTestRunner
|
||||
try:
|
||||
from bootstrap.ext import console
|
||||
width, height = console.getTerminalSize()
|
||||
PNUM = width
|
||||
except:
|
||||
PNUM = 70
|
||||
|
||||
try:
|
||||
import xmlrunner
|
||||
except ImportError:
|
||||
xmlrunner = None
|
||||
|
||||
|
||||
TEST_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
XML_OUTPUT_DIR = os.environ.get(
|
||||
'XML_TEST_REPORTS', os.path.join(TEST_DIR, 'xml-test-reports')
|
||||
)
|
||||
|
||||
|
||||
def print_header(header, sep='~', top=True, bottom=True, inline=False,
|
||||
centered=False):
|
||||
'''
|
||||
Allows some pretty printing of headers on the console, either with a
|
||||
"ruler" on bottom and/or top, inline, centered, etc.
|
||||
'''
|
||||
if top and not inline:
|
||||
print(sep * PNUM)
|
||||
|
||||
if centered and not inline:
|
||||
fmt = u'{0:^{width}}'
|
||||
elif inline and not centered:
|
||||
fmt = u'{0:{sep}<{width}}'
|
||||
elif inline and centered:
|
||||
fmt = u'{0:{sep}^{width}}'
|
||||
else:
|
||||
fmt = u'{0}'
|
||||
print(fmt.format(header, sep=sep, width=PNUM))
|
||||
|
||||
if bottom and not inline:
|
||||
print(sep * PNUM)
|
||||
|
||||
|
||||
def run_suite(opts, path, display_name, suffix='[!_]*.py'):
|
||||
'''
|
||||
Execute a unit test suite
|
||||
'''
|
||||
loader = TestLoader()
|
||||
tests = loader.discover(path, suffix, TEST_DIR)
|
||||
|
||||
header = '{0} Tests'.format(display_name)
|
||||
print_header('Starting {0}'.format(header))
|
||||
|
||||
if opts.xmlout:
|
||||
if not os.path.isdir(XML_OUTPUT_DIR):
|
||||
os.makedirs(XML_OUTPUT_DIR)
|
||||
runner = xmlrunner.XMLTestRunner(
|
||||
output=XML_OUTPUT_DIR,
|
||||
verbosity=opts.verbosity
|
||||
).run(tests)
|
||||
else:
|
||||
runner = TextTestRunner(
|
||||
verbosity=opts.verbosity
|
||||
).run(tests)
|
||||
return runner.wasSuccessful()
|
||||
|
||||
|
||||
def run_integration_suite(opts, display_name, suffix='[!_]*.py'):
|
||||
'''
|
||||
Run an integration test suite
|
||||
'''
|
||||
path = os.path.join(TEST_DIR, 'bootstrap')
|
||||
return run_suite(opts, path, display_name, suffix)
|
||||
|
||||
|
||||
def main():
|
||||
parser = optparse.OptionParser()
|
||||
|
||||
test_selection_group = optparse.OptionGroup(
|
||||
parser,
|
||||
"Tests Selection",
|
||||
"In case of no selection, all tests will be executed."
|
||||
)
|
||||
test_selection_group.add_option(
|
||||
'-L', '--lint',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Run Lint tests'
|
||||
)
|
||||
test_selection_group.add_option(
|
||||
'-U', '--usage',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Run Usage tests'
|
||||
)
|
||||
test_selection_group.add_option(
|
||||
'-I', '--install',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Run Installation tests'
|
||||
)
|
||||
parser.add_option_group(test_selection_group)
|
||||
|
||||
output_options_group = optparse.OptionGroup(parser, "Output Options")
|
||||
output_options_group.add_option(
|
||||
'-v',
|
||||
'--verbose',
|
||||
dest='verbosity',
|
||||
default=1,
|
||||
action='count',
|
||||
help='Verbose test runner output'
|
||||
)
|
||||
output_options_group.add_option(
|
||||
'-x',
|
||||
'--xml',
|
||||
dest='xmlout',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='XML test runner output(Output directory: {0})'.format(
|
||||
XML_OUTPUT_DIR
|
||||
)
|
||||
)
|
||||
output_options_group.add_option(
|
||||
'--no-clean',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Do not clean the XML output files before running.'
|
||||
)
|
||||
parser.add_option_group(output_options_group)
|
||||
|
||||
options, _ = parser.parse_args()
|
||||
|
||||
if options.xmlout and xmlrunner is None:
|
||||
parser.error(
|
||||
'\'--xml\' is not available. The xmlrunner library is not '
|
||||
'installed.'
|
||||
)
|
||||
elif options.xmlout:
|
||||
print(
|
||||
'Generated XML reports will be stored on {0!r}'.format(
|
||||
XML_OUTPUT_DIR
|
||||
)
|
||||
)
|
||||
|
||||
if not any((options.lint, options.usage, options.install)):
|
||||
options.lint = True
|
||||
options.usage = True
|
||||
options.install = True
|
||||
|
||||
if not options.no_clean and os.path.isdir(XML_OUTPUT_DIR):
|
||||
shutil.rmtree(XML_OUTPUT_DIR)
|
||||
|
||||
print 'Detected system grains:'
|
||||
pprint.pprint(GRAINS)
|
||||
|
||||
overall_status = []
|
||||
|
||||
if options.lint:
|
||||
status = run_integration_suite(options, 'Lint', "*lint.py")
|
||||
overall_status.append(status)
|
||||
if options.usage:
|
||||
run_integration_suite(options, 'Usage', "*usage.py")
|
||||
overall_status.append(status)
|
||||
if options.install:
|
||||
run_integration_suite(options, 'Installation', "*install.py")
|
||||
overall_status.append(status)
|
||||
|
||||
if overall_status.count(False) > 0:
|
||||
# We have some false results, the test-suite failed
|
||||
parser.exit(1)
|
||||
|
||||
parser.exit(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Add table
Reference in a new issue