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

Reply via email to