+++ /dev/null
-#!/usr/bin/env python3
-# SPDX-License-Identifier: LGPL-2.1-or-later
-#
-# Copyright © 2017 Michal Sekletar <msekleta@redhat.com>
-
-# ATTENTION: This uses the *installed* systemd, not the one from the built
-# source tree.
-
-import os
-import subprocess
-import sys
-import time
-import unittest
-import uuid
-from enum import Enum
-
-class InstallChange(Enum):
- NO_CHANGE = 0
- LINES_SWAPPED = 1
- COMMAND_ADDED_BEFORE = 2
- COMMAND_ADDED_AFTER = 3
- COMMAND_INTERLEAVED = 4
- REMOVAL = 5
-
-class ExecutionResumeTest(unittest.TestCase):
- def setUp(self):
- self.unit = 'test-issue-518.service'
- self.unitfile_path = f'/run/systemd/system/{self.unit}'
- self.output_file = f"/tmp/test-issue-518-{uuid.uuid4()}"
- self.unit_files = {}
-
- unit_file_content = f'''
- [Service]
- Type=oneshot
- ExecStart=/bin/sleep 3
- ExecStart=/bin/bash -c "echo foo >>{self.output_file}"
- '''
- self.unit_files[InstallChange.NO_CHANGE] = unit_file_content
-
- unit_file_content = f'''
- [Service]
- Type=oneshot
- ExecStart=/bin/bash -c "echo foo >>{self.output_file}"
- ExecStart=/bin/sleep 3
- '''
- self.unit_files[InstallChange.LINES_SWAPPED] = unit_file_content
-
- unit_file_content = f'''
- [Service]
- Type=oneshot
- ExecStart=/bin/bash -c "echo bar >>{self.output_file}"
- ExecStart=/bin/sleep 3
- ExecStart=/bin/bash -c "echo foo >>{self.output_file}"
- '''
- self.unit_files[InstallChange.COMMAND_ADDED_BEFORE] = unit_file_content
-
- unit_file_content = f'''
- [Service]
- Type=oneshot
- ExecStart=/bin/sleep 3
- ExecStart=/bin/bash -c "echo foo >>{self.output_file}"
- ExecStart=/bin/bash -c "echo bar >>{self.output_file}"
- '''
- self.unit_files[InstallChange.COMMAND_ADDED_AFTER] = unit_file_content
-
- unit_file_content = f'''
- [Service]
- Type=oneshot
- ExecStart=/bin/bash -c "echo baz >>{self.output_file}"
- ExecStart=/bin/sleep 3
- ExecStart=/bin/bash -c "echo foo >>{self.output_file}"
- ExecStart=/bin/bash -c "echo bar >>{self.output_file}"
- '''
- self.unit_files[InstallChange.COMMAND_INTERLEAVED] = unit_file_content
-
- unit_file_content = f'''
- [Service]
- Type=oneshot
- ExecStart=/bin/bash -c "echo bar >>{self.output_file}"
- ExecStart=/bin/bash -c "echo baz >>{self.output_file}"
- '''
- self.unit_files[InstallChange.REMOVAL] = unit_file_content
-
- def reload(self):
- subprocess.check_call(['systemctl', 'daemon-reload'])
-
- def write_unit_file(self, unit_file_change):
- if not isinstance(unit_file_change, InstallChange):
- raise ValueError('Unknown unit file change')
-
- content = self.unit_files[unit_file_change]
-
- with open(self.unitfile_path, 'w', encoding='utf-8') as f:
- f.write(content)
-
- self.reload()
-
- def check_output(self, expected_output):
- for _ in range(15):
- # Wait until the unit finishes so we don't check an incomplete log
- if subprocess.call(['systemctl', '-q', 'is-active', self.unit]) == 0:
- continue
-
- os.sync()
-
- try:
- with open(self.output_file, 'r', encoding='utf-8') as log:
- output = log.read()
- self.assertEqual(output, expected_output)
- return
- except IOError:
- pass
-
- time.sleep(1)
-
- self.fail(f'Timed out while waiting for the output file {self.output_file} to appear')
-
- def setup_unit(self):
- self.write_unit_file(InstallChange.NO_CHANGE)
- subprocess.check_call(['systemctl', '--job-mode=replace', '--no-block', 'start', self.unit])
- time.sleep(1)
-
- def test_no_change(self):
- expected_output = 'foo\n'
-
- self.setup_unit()
- self.reload()
-
- self.check_output(expected_output)
-
- def test_swapped(self):
- self.setup_unit()
- self.write_unit_file(InstallChange.LINES_SWAPPED)
- self.reload()
-
- self.assertTrue(not os.path.exists(self.output_file))
-
- def test_added_before(self):
- expected_output = 'foo\n'
-
- self.setup_unit()
- self.write_unit_file(InstallChange.COMMAND_ADDED_BEFORE)
- self.reload()
-
- self.check_output(expected_output)
-
- def test_added_after(self):
- expected_output = 'foo\nbar\n'
-
- self.setup_unit()
- self.write_unit_file(InstallChange.COMMAND_ADDED_AFTER)
- self.reload()
-
- self.check_output(expected_output)
-
- def test_interleaved(self):
- expected_output = 'foo\nbar\n'
-
- self.setup_unit()
- self.write_unit_file(InstallChange.COMMAND_INTERLEAVED)
- self.reload()
-
- self.check_output(expected_output)
-
- def test_removal(self):
- self.setup_unit()
- self.write_unit_file(InstallChange.REMOVAL)
- self.reload()
-
- self.assertTrue(not os.path.exists(self.output_file))
-
- def test_issue_6533(self):
- unit = "test-issue-6533.service"
- unitfile_path = f"/run/systemd/system/{unit}"
-
- content = '''
- [Service]
- ExecStart=/bin/sleep 5
- '''
-
- with open(unitfile_path, 'w', encoding='utf-8') as f:
- f.write(content)
-
- self.reload()
-
- subprocess.check_call(['systemctl', '--job-mode=replace', '--no-block', 'start', unit])
- time.sleep(2)
-
- content = '''
- [Service]
- ExecStart=/bin/sleep 5
- ExecStart=/bin/true
- '''
-
- with open(unitfile_path, 'w', encoding='utf-8') as f:
- f.write(content)
-
- self.reload()
- time.sleep(5)
-
- self.assertNotEqual(subprocess.call("journalctl -b _PID=1 | grep -q 'Freezing execution'", shell=True), 0)
-
- def tearDown(self):
- for f in [self.output_file, self.unitfile_path]:
- try:
- os.remove(f)
- except OSError:
- # ignore error if log file doesn't exist
- pass
-
- self.reload()
-
-if __name__ == '__main__':
- unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=3))
--- /dev/null
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# shellcheck disable=SC2016
+set -eux
+set -o pipefail
+
+# shellcheck source=test/units/test-control.sh
+. "$(dirname "$0")"/test-control.sh
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+setup_base_unit() {
+ local unit_path="${1:?}"
+ local log_file="${2:?}"
+ local unit_name="${unit_path##*/}"
+
+ cat >"$unit_path" <<EOF
+[Service]
+Type=oneshot
+ExecStart=sleep 3
+ExecStart=bash -c "echo foo >>$log_file"
+EOF
+ systemctl daemon-reload
+
+ systemctl --job-mode=replace --no-block start "$unit_name"
+ # Wait until the unit leaves the "inactive" state
+ timeout 5s bash -xec "while [[ \"\$(systemctl show -P ActiveState $unit_name)\" == inactive ]]; do sleep .1; done"
+ # Sleep for 1 second from the unit start to get well "into" the first (or second) ExecStart= directive
+ sleep 1
+}
+
+check_output() {
+ local unit_name="${1:?}"
+ local log_file="${2:?}"
+ local expected="${3?}"
+ local unit_name="${unit_path##*/}"
+
+ # Wait until the unit becomes inactive before checking the log
+ timeout 10s bash -xec "while [[ \"\$(systemctl show -P ActiveState $unit_name)\" != inactive ]]; do sleep .5; done"
+
+ diff "$log_file" <(echo -ne "$expected")
+}
+
+testcase_no_change() {
+ local unit_path log_file
+
+ unit_path="$(mktemp /run/systemd/system/test-deserialization-no-change-XXX.service)"
+ log_file="$(mktemp)"
+
+ setup_base_unit "$unit_path" "$log_file"
+
+ # Simple sanity test without any reordering shenanignans, to check if the base unit works as expected.
+ check_output "$unit_path" "$log_file" "foo\n"
+
+ rm -f "$unit_path" "$log_file"
+}
+
+testcase_swapped() {
+ local unit_path log_file
+
+ unit_path="$(mktemp /run/systemd/system/test-deserialization-swapped-XXX.service)"
+ log_file="$(mktemp)"
+
+ setup_base_unit "$unit_path" "$log_file"
+
+ # Swap the two ExecStart= lines.
+ #
+ # Since we should be in the first "sleep" of the base unit, after replacing the unit with the following
+ # one we should continue running from the respective "ExecStart=sleep 3" line, which is now the last
+ # one, resulting no output in the final log file.
+ cat >"$unit_path" <<EOF
+[Service]
+Type=oneshot
+ExecStart=bash -c "echo foo >>$log_file"
+ExecStart=sleep 3
+EOF
+ systemctl daemon-reload
+
+ check_output "$unit_path" "$log_file" ""
+
+ rm -f "$unit_path" "$log_file"
+}
+
+testcase_added_before() {
+ local unit_path log_file
+
+ unit_path="$(mktemp /run/systemd/system/test-deserialization-added-before-XXX.service)"
+ log_file="$(mktemp)"
+
+ setup_base_unit "$unit_path" "$log_file"
+
+ # Add one new ExecStart= before the existing ones.
+ #
+ # Since, after reload, we should continue running from the "sleep 3" statement, the newly added "echo
+ # bar" one will have no efect and we should end up with the same output as in the previous case.
+ cat >"$unit_path" <<EOF
+[Service]
+Type=oneshot
+ExecStart=bash -c "echo bar >>$log_file"
+ExecStart=sleep 3
+ExecStart=bash -c "echo foo >>$log_file"
+EOF
+ systemctl daemon-reload
+
+ check_output "$unit_path" "$log_file" "foo\n"
+
+ rm -f "$unit_path" "$log_file"
+}
+
+testcase_added_after() {
+ local unit_path log_file
+
+ unit_path="$(mktemp /run/systemd/system/test-deserialization-added-after-XXX.service)"
+ log_file="$(mktemp)"
+
+ setup_base_unit "$unit_path" "$log_file"
+
+ # Add an ExecStart= line after the existing ones.
+ #
+ # Same case as above, except the newly added ExecStart= should get executed, as it was added after the
+ # "sleep 3" statement.
+ cat >"$unit_path" <<EOF
+[Service]
+Type=oneshot
+ExecStart=sleep 3
+ExecStart=bash -c "echo foo >>$log_file"
+ExecStart=bash -c "echo bar >>$log_file"
+EOF
+ systemctl daemon-reload
+
+ check_output "$unit_path" "$log_file" "foo\nbar\n"
+
+ rm -f "$unit_path" "$log_file"
+}
+
+testcase_interleaved() {
+ local unit_path log_file
+
+ unit_path="$(mktemp /run/systemd/system/test-deserialization-interleaved-XXX.service)"
+ log_file="$(mktemp)"
+
+ setup_base_unit "$unit_path" "$log_file"
+
+ # Combination of the two previous cases.
+ cat >"$unit_path" <<EOF
+[Service]
+Type=oneshot
+ExecStart=bash -c "echo baz >>$log_file"
+ExecStart=sleep 3
+ExecStart=bash -c "echo foo >>$log_file"
+ExecStart=bash -c "echo bar >>$log_file"
+EOF
+ systemctl daemon-reload
+
+ check_output "$unit_path" "$log_file" "foo\nbar\n"
+
+ rm -f "$unit_path" "$log_file"
+}
+
+testcase_removal() {
+ local unit_path log_file
+
+ unit_path="$(mktemp /run/systemd/system/test-deserialization-removal-XXX.service)"
+ log_file="$(mktemp)"
+
+ setup_base_unit "$unit_path" "$log_file"
+
+ # Remove the currently executed ExecStart= line.
+ #
+ # In this case we completely drop the currently excuted "sleep 3" statement, so after reload systemd
+ # should complain that the currently executed command vanished and simply finish executing the unit,
+ # resulting in an empty log.
+ cat >"$unit_path" <<EOF
+[Service]
+Type=oneshot
+ExecStart=bash -c "echo bar >>$log_file"
+ExecStart=bash -c "echo baz >>$log_file"
+EOF
+ systemctl daemon-reload
+
+ check_output "$unit_path" "$log_file" ""
+
+ rm -f "$unit_path" "$log_file"
+}
+
+testcase_issue_6533() {
+ local unit_path unit_name log_file
+
+ unit_path="$(mktemp /run/systemd/system/test-deserialization-issue-6533-XXX.service)"
+ unit_name="${unit_path##*/}"
+ log_file="$(mktemp)"
+
+ cat >"$unit_path" <<EOF
+[Service]
+Type=simple
+ExecStart=/bin/sleep 5
+EOF
+ systemctl daemon-reload
+
+ systemctl --job-mode=replace --no-block start "$unit_name"
+ sleep 2
+
+ # Make sure we try to execute the next command only for oneshot services, as for other types we allow
+ # only one ExecStart= directive.
+ #
+ # See: https://github.com/systemd/systemd/issues/6533
+ cat >"$unit_path" <<EOF
+[Service]
+Type=simple
+ExecStart=/bin/sleep 5
+ExecStart=bash -c "echo foo >>$log_file"
+EOF
+ systemctl daemon-reload
+
+ check_output "$unit_path" "$log_file" ""
+ (! journalctl -b --grep "Freezing execution" _PID=1)
+}
+
+mkdir -p /run/systemd/system/
+run_testcases
+systemctl daemon-reload