1
0
mirror of https://github.com/systemd/systemd.git synced 2024-12-22 17:35:35 +03:00

test: rewrite test-exec-deserialization.py

Rewrite the test in bash and make it part of our integration test suite,
so it's actually executed in all our upstream CI environments.

The original test is flaky in environments where daemon-reload might
occur during the test runtime (e.g. when running the test in parallel
with the systemd-networkd test suite). Also, it was run only in CentOS
CI in limited way (i.e. without sanitizers), since it tests the host's
systemd, instead of the just built one.

Resolves: #29943
This commit is contained in:
Frantisek Sumsal 2024-01-02 18:27:45 +01:00 committed by Yu Watanabe
parent 0e3638380d
commit 995bf013a1
2 changed files with 221 additions and 214 deletions

View File

@ -1,214 +0,0 @@
#!/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))

View File

@ -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" <<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