[systemd-devel] [PATCH] initial sysv-generator test suite

Zbigniew Jędrzejewski-Szmek zbyszek at in.waw.pl
Tue Jan 20 07:48:01 PST 2015


On Tue, Jan 20, 2015 at 04:19:24PM +0100, Martin Pitt wrote:
> 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.pitt at 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.pitt at 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')
> +
> +
> + at 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')
Python part looks nice.

> +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.pitt at 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

Maybe we could do this check in configure.ac/Makefile.am (add the test
to the list conditinally)? We already test for python presence and
extract the version, so we shouldn't duplicate the tests here and
have an extra wrapper.

Zbyszek


More information about the systemd-devel mailing list