Hello all, We've had numerous problems with the SysV generator in the past, and we just recently introduced another regression: init.d scripts which end in ".sh" are now totally broken.
Thus I think it's high time to write some integration tests for that. The attached patch provides the necessary framework and an initial set of tests; e. g. test_multiple_provides() covers Michael's recent commit b7e71846. I can reproduce the ".sh" bug from above with a simple | def test_sh_suffix(self): | '''init.d script with .sh suffix''' | | self.add_sysv('foo.sh', {}, enable=True) | err, results = self.run_generator() | [... actual checks here, not written yet ...] which currently fails with | ====================================================================== | FAIL: test_sh_suffix (__main__.SysvGeneratorTest) | init.d script with .sh suffix | ---------------------------------------------------------------------- | Traceback (most recent call last): | File "test/../test/sysv-generator-test.py", line 179, in test_sh_suffix | err, results = self.run_generator() | File "test/../test/sysv-generator-test.py", line 58, in run_generator | self.assertFalse('Fail' in err, err) | AssertionError: True is not false : Looking for unit files in (higher priority first): | /etc/systemd/system | /run/systemd/system | /usr/local/lib/systemd/system | /lib/systemd/system | /usr/lib/systemd/system | Looking for SysV init scripts in: | /tmp/sysv-gen-test.7qlq6kg2/init.d | Looking for SysV rcN.d links in: | /tmp/sysv-gen-test.7qlq6kg2 | Failed to create unit file /tmp/sysv-gen-test.7qlq6kg2/output/foo.service: File exists Indeed it just creates a symlink pointing to itself and nothing else. I will look into that actual bug in a bit, and write a complete test along with it. But before I spend more work on the tests, I'd appreciate a quick review of it whether the general structure is ok for you. As this deals with temp dirs, cleaning them up, running external programs, parsing their output etc., I chose Python for this, as this stuff is just soooo much faster and convenient to write. We already have test/rule-syntax-check.py, so there's precedent :-) As automake's tests are rather limited and require a single command without arguments, but I want to make this obey configure's $(PYTHON) and skip the test properly if python 3 is not available, I created a simple shell wrapper around it. Obviously this is still lacking a lot of important cases; I'm happy to add them later on, I just wanted to get some initial generic feedback. Thanks, Martin -- Martin Pitt | http://www.piware.de Ubuntu Developer (www.ubuntu.com) | Debian Developer (www.debian.org)
>From 7d4f85e42ff5a7a05477e712dcb58ab99d02a87a Mon Sep 17 00:00:00 2001 From: Martin Pitt <martin.p...@ubuntu.com> Date: Tue, 20 Jan 2015 16:08:05 +0100 Subject: [PATCH] test: add initial integration test for systemd-sysv-generator This is still missing a lot of important scenarios and corner cases, but provides the groundwork and covers a recent bug (commit b7e718) --- Makefile.am | 9 ++- test/sysv-generator-test.py | 177 ++++++++++++++++++++++++++++++++++++++++++++ test/sysv-generator-test.sh | 33 +++++++++ 3 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 test/sysv-generator-test.py create mode 100755 test/sysv-generator-test.sh diff --git a/Makefile.am b/Makefile.am index 788e634..f7ae578 100644 --- a/Makefile.am +++ b/Makefile.am @@ -3767,7 +3767,9 @@ endif # ------------------------------------------------------------------------------ TESTS += \ test/udev-test.pl \ - test/rules-test.sh + test/rules-test.sh \ + test/sysv-generator-test.sh \ + $(NULL) manual_tests += \ test-libudev \ @@ -3812,7 +3814,10 @@ EXTRA_DIST += \ test/sys.tar.xz \ test/udev-test.pl \ test/rules-test.sh \ - test/rule-syntax-check.py + test/rule-syntax-check.py \ + test/sysv-generator-test.sh \ + test/sysv-generator-test.py \ + $(NULL) # ------------------------------------------------------------------------------ ata_id_SOURCES = \ diff --git a/test/sysv-generator-test.py b/test/sysv-generator-test.py new file mode 100644 index 0000000..a3f80ca --- /dev/null +++ b/test/sysv-generator-test.py @@ -0,0 +1,177 @@ +# systemd-sysv-generator integration test +# +# (C) 2015 Canonical Ltd. +# Author: Martin Pitt <martin.p...@ubuntu.com> +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +# systemd 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with systemd; If not, see <http://www.gnu.org/licenses/>. + +import unittest +import sys +import os +import subprocess +import tempfile +import configparser +from glob import glob + +sysv_generator = os.path.join(os.environ.get('builddir', '.'), 'systemd-sysv-generator') + + +@unittest.skipUnless(os.path.exists(sysv_generator), + '%s does not exist' % sysv_generator) +class SysvGeneratorTest(unittest.TestCase): + def setUp(self): + self.workdir = tempfile.TemporaryDirectory(prefix='sysv-gen-test.') + self.init_d_dir = os.path.join(self.workdir.name, 'init.d') + os.mkdir(self.init_d_dir) + self.rcnd_dir = self.workdir.name + self.out_dir = os.path.join(self.workdir.name, 'output') + os.mkdir(self.out_dir) + + def run_generator(self, expect_error=False): + '''Run sysv-generator. + + Fail if stderr contains any "Fail", unless expect_error is True. + Return (stderr, filename -> ConfigParser) pair with ouput to stderr and + parsed generated units. + ''' + env = os.environ.copy() + env['SYSTEMD_LOG_LEVEL'] = 'debug' + env['SYSTEMD_SYSVINIT_PATH'] = self.init_d_dir + env['SYSTEMD_SYSVRCND_PATH'] = self.rcnd_dir + gen = subprocess.Popen( + [sysv_generator, 'ignored', 'ignored', self.out_dir], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True, env=env) + (out, err) = gen.communicate() + if not expect_error: + self.assertFalse('Fail' in err, err) + self.assertEqual(gen.returncode, 0) + + results = {} + for service in glob(self.out_dir + '/*.service'): + cp = configparser.RawConfigParser() + cp.optionxform = lambda o: o # don't lower-case option names + with open(service) as f: + cp.read_file(f) + results[os.path.basename(service)] = cp + + return (err, results) + + def add_sysv(self, fname, keys, enable=False): + '''Create a SysV init script with the given keys in the LSB header + + There are sensible default values for all fields. + + If enable is True, links will be created in the rcN.d dirs. + ''' + name_without_sh = fname.endswith('.sh') and fname[:-3] or fname + keys.setdefault('Provides', name_without_sh) + keys.setdefault('Required-Start', '$local_fs') + keys.setdefault('Required-Stop', keys['Required-Start']) + keys.setdefault('Default-Start', '2 3 4 5') + keys.setdefault('Default-Stop', '0 1 6') + keys.setdefault('Short-Description', 'test %s service' % + name_without_sh) + keys.setdefault('Description', 'long description for test %s service' % + name_without_sh) + with open(os.path.join(self.init_d_dir, fname), 'w') as f: + f.write('#!/bin/init-d-interpreter\n### BEGIN INIT INFO\n') + for k, v in keys.items(): + if v is not None: + f.write('#%20s %s\n' % (k + ':', v)) + f.write('### END INIT INFO\ncode --goes here\n') + os.fchmod(f.fileno(), 0o755) + + if enable: + def make_link(prefix, runlevel): + d = os.path.join(self.rcnd_dir, 'rc%s.d' % runlevel) + os.makedirs(d, exist_ok=True) + os.symlink('../init.d/' + fname, os.path.join(d, prefix + fname)) + + for rl in keys['Default-Start'].split(): + make_link('S01', rl) + for rl in keys['Default-Stop'].split(): + make_link('K01', rl) + + def test_nothing(self): + '''no input files''' + + results = self.run_generator()[1] + self.assertEqual(results, {}) + self.assertEqual(os.listdir(self.out_dir), []) + + def test_simple_disabled(self): + '''simple service without dependencies, disabled''' + + self.add_sysv('foo', {}, enable=False) + err, results = self.run_generator() + self.assertEqual(len(results), 1) + + # no enablement links or other stuff + self.assertEqual(os.listdir(self.out_dir), ['foo.service']) + + s = results['foo.service'] + self.assertEqual(s.sections(), ['Unit', 'Service']) + self.assertEqual(s.get('Unit', 'Description'), 'LSB: test foo service') + # $local_fs does not need translation, don't expect any dependency + # fields here + self.assertEqual(set(s.options('Unit')), + set(['Documentation', 'SourcePath', 'Description'])) + + self.assertEqual(s.get('Service', 'Type'), 'forking') + init_script = os.path.join(self.init_d_dir, 'foo') + self.assertEqual(s.get('Service', 'ExecStart'), + '%s start' % init_script) + self.assertEqual(s.get('Service', 'ExecStop'), + '%s stop' % init_script) + + def test_simple_enabled(self): + '''simple service without dependencies, enabled''' + + self.add_sysv('foo', {}, enable=True) + err, results = self.run_generator() + self.assertEqual(list(results.keys()), ['foo.service']) + + # should be enabled + for runlevel in [2, 3, 4, 5]: + target = os.readlink(os.path.join( + self.out_dir, 'runlevel%i.target.wants' % runlevel, 'foo.service')) + self.assertTrue(os.path.exists(target)) + self.assertEqual(os.path.basename(target), 'foo.service') + + def test_lsb_dep_network(self): + '''LSB dependency: $network''' + + self.add_sysv('foo', {'Required-Start': '$network'}) + s = self.run_generator()[1]['foo.service'] + self.assertEqual(set(s.options('Unit')), + set(['Documentation', 'SourcePath', 'Description', 'After', 'Wants'])) + self.assertEqual(s.get('Unit', 'After'), 'network-online.target') + self.assertEqual(s.get('Unit', 'Wants'), 'network-online.target') + + def test_multiple_provides(self): + '''multiple Provides: names''' + + self.add_sysv('foo', {'Provides': 'foo bar baz'}) + s = self.run_generator()[1]['foo.service'] + self.assertEqual(set(s.options('Unit')), + set(['Documentation', 'SourcePath', 'Description'])) + # should create symlinks for the alternative names + for f in ['bar.service', 'baz.service']: + self.assertEqual(os.readlink(os.path.join(self.out_dir, f)), + 'foo.service') + + +if __name__ == '__main__': + unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/test/sysv-generator-test.sh b/test/sysv-generator-test.sh new file mode 100755 index 0000000..dc26824 --- /dev/null +++ b/test/sysv-generator-test.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# Call the sysv-generator test, if python is available. +# +# (C) 2015 Canonical Ltd. +# Author: Martin Pitt <martin.p...@ubuntu.com> +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +# systemd 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with systemd; If not, see <http://www.gnu.org/licenses/>. + +[ -n "$srcdir" ] || srcdir=`dirname $0`/.. + +# skip if we don't have python3 +type ${PYTHON:=python3} >/dev/null 2>&1 || { + echo "$0: No $PYTHON installed, skipping udev rule syntax check" + exit 0 +} + +$PYTHON --version 2>&1 | grep -q ' 3.' || { + echo "$0: This check requires Python 3, skipping udev rule syntax check" + exit 0 +} + +$PYTHON $srcdir/test/sysv-generator-test.py -- 2.1.4
_______________________________________________ systemd-devel mailing list systemd-devel@lists.freedesktop.org http://lists.freedesktop.org/mailman/listinfo/systemd-devel