diff --git a/test/test-exec-deserialization.py b/test/test-exec-deserialization.py deleted file mode 100755 index f8f3a6d272..0000000000 --- a/test/test-exec-deserialization.py +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: LGPL-2.1-or-later -# -# Copyright © 2017 Michal Sekletar - -# 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)) diff --git a/test/units/testsuite-07.exec-deserialization.sh b/test/units/testsuite-07.exec-deserialization.sh new file mode 100755 index 0000000000..a0aa5809fc --- /dev/null +++ b/test/units/testsuite-07.exec-deserialization.sh @@ -0,0 +1,221 @@ +#!/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" <>$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" <>$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" <>$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" <>$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" <>$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" <>$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" <"$unit_path" <>$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