Files
scst/scripts/kunit/test_clean.py
T
Gleb Chesnokov 60ff16ef4f scst/include/backport.h: Allow resurrection with active references
Found by manual code inspection, not by an analyzer or a runtime test.
When scst_suspend_activity() times out with commands still active,
__scst_resume_activity() revives the command and management counters.
The old wrapper calls percpu_ref_reinit(), whose zero-count requirement
is enforced by this kernel assertion:

    WARN_ON_ONCE(!percpu_ref_is_zero(ref));

Upstream 3.18-4.19 and the retained EL7/UEK4/UEK5 implementations preserve
the per-CPU allocation and use long counters, but reinit still rejects
this legitimate nonzero resurrection. Restore the initial reference
additively, retain outstanding references, and restore per-CPU operation
with the kernel's bias, counter clearing and release-store ordering.
Keep explicitly forced atomic mode unchanged.

SCST serializes kill and resurrection with scst_suspend_mutex and keeps
both counters alive even when their release callbacks run. Require the
same external serialization for mode changes and exit. Concurrent gets
and puts remain allowed.

Kill confirmation can wake the caller before confirm_switch is cleared.
Drain a pending switch callback with rcu_barrier_sched() before reusing
the counters. The kernel switch waitqueue is private, and older kernels
do not export percpu_ref_switch_to_percpu(). This conditional barrier
can also wait for unrelated RCU callbacks; it is skipped once the switch
has completed. Native resurrection on 4.20+ and EL8+ remains unchanged.

Add scst-LIB-percpu-ref to scst.ko with private, real kernel counters.
Keep the backport helper available to KUnit on modern kernels, using
their counter layout and unified RCU API. Cover immediate resurrection
with active refs, resurrection after draining, forced atomic mode, and
repeated suspend cancellations with commands completing while dead.

Also fix the fake Kbuild cleaner used by the Python tooling tests.
CI reported test_cleanup_without_packaged_tooling failing with
"find: cannot delete .../isert-scst.ko: No such file or directory".
iSCSI cleans kernel/ and kernel/isert-scst/ concurrently, so one fixture
find can remove an artifact after the other has enumerated it. Use
-ignore_readdir_race to tolerate only vanished files, matching Kbuild's
idempotent rm -f behavior. Keep other find errors and the injected
FAIL_CLEAN failure fatal; retain parallel cleanup and artifact checks.

Tests, builds and VM validation were not run, as requested.
2026-10-21 19:48:40 +03:00

128 lines
6.9 KiB
Python

# SPDX-License-Identifier: GPL-2.0-only
"""Exercise cleanup recipes in temporary trees without touching real modules."""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
COMPONENTS = next(line.split(':=', 1)[1].split()
for line in (ROOT / 'scst/src/kunit/modules.mk').read_text().splitlines()
if line.startswith('SCST_KUNIT_COMPONENTS := '))
class CleanTests(unittest.TestCase):
def check_clean(self, directory, goal, fail=False, tooling=True):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
for path in ('Makefile', 'scst/Makefile', 'scst/src/Makefile',
'scst/src/dev_handlers/Makefile', 'scst/src/kunit/select.mk',
'scst/src/kunit/modules.mk', 'scst/include/scst_const.h',
'iscsi-scst/Makefile', 'iscsi-scst/usr/Makefile',
'qla2x00t-32gbit/Makefile',
'qla2x00t-32gbit/qla2x00-target/Makefile'):
target = root / path
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(ROOT / path, target)
# Other components are deliberately inert; kunit-clean must not visit them.
for component in ('srpt', 'scst_local', 'fcst', 'usr', 'scstadmin'):
target = root / component
target.mkdir(parents=True, exist_ok=True)
(target / 'Makefile').write_text('clean extraclean:;@rm -f visited.o\n')
(target / 'visited.o').touch()
kernel = root / 'kernel'
kernel.mkdir()
# iSCSI cleans kernel/ and kernel/isert-scst/ concurrently. Like
# Kbuild's rm -f, tolerate artifacts already removed by a peer.
(kernel / 'Makefile').write_text(
'clean:\n'
'\t@test "$(FAIL_CLEAN)" != y\n'
'\t@find "$(M)" -ignore_readdir_race -type f '
'\\( -name "*.o" -o -name "*.ko" \\) -delete\n')
artifacts = ['scst/src/scst.ko', 'scst/src/kunit/lib/suites/test.o',
'scst/src/dev_handlers/scst_vdisk.ko',
'scst/src/dev_handlers/kunit/vdisk/suites/test.o',
'iscsi-scst/kernel/iscsi-scst.ko',
'iscsi-scst/kernel/kunit/suites/test.o',
'iscsi-scst/kernel/isert-scst/isert-scst.ko',
'iscsi-scst/kernel/isert-scst/kunit/suites/test.o',
'qla2x00t-32gbit/qla2xxx_scst.ko',
'qla2x00t-32gbit/qla_os.o',
'qla2x00t-32gbit/qla2x00-target/qla2x00tgt.ko',
'qla2x00t-32gbit/qla2x00-target/kunit/suites/test.o']
metadata = ['.kunit-modules', '.kunit-modules.tmp']
if tooling:
metadata += ['scripts/kunit/__pycache__/run.pyc',
'scripts/kunit/upstream/__pycache__/kunit_parser.pyc']
generated = ['scst/include/build_mode.h', 'scst/include/build_mode.h.tmp',
'scst/include/scst_itf_ver.h',
'iscsi-scst/include/iscsi_scst_itf_ver.h',
'iscsi-scst/conftest/sock_recvmsg/result-test.txt']
preserved = ['.kunit-run.toml', 'kunit-results/run/report.txt',
'scst/build_mode', 'scst/src/kunit/lib/suites/test.c']
for path in artifacts + metadata + generated + preserved:
target = root / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text('preserve me\n' if path in preserved else '')
env = {key: value for key, value in os.environ.items()
if key not in ('MAKEFLAGS', 'MFLAGS', 'KDIR', 'KVER')
and not key.startswith(('KUNIT_', 'CONFIG_SCST_KUNIT_'))}
result = subprocess.run(['make', '-s', '-j4', '-C', str(root / directory), goal,
'KDIR=' + str(kernel), 'KVER=test',
'FAIL_CLEAN=' + ('y' if fail else 'n'),
'OFED_FLAVOR=in-tree',
'OFED_KERNEL_IB_DEVEL_RPM_INSTALLED=false',
'OFED_COMPAT_RDMA_DEVEL_RPM_INSTALLED=false'],
env=env, capture_output=True, text=True)
self.assertEqual(result.returncode == 0, not fail, result.stdout + result.stderr)
for path in metadata:
self.assertFalse((root / path).exists(), path)
if not tooling:
self.assertFalse((root / 'scripts').exists())
for path in preserved:
self.assertEqual((root / path).read_text(), 'preserve me\n', path)
if not fail and directory == '' and goal in ('clean', 'extraclean', 'kunit-clean'):
for path in artifacts + generated:
self.assertFalse((root / path).exists(), path)
if goal == 'kunit-clean':
self.assertTrue((root / 'srpt/visited.o').exists())
def test_top_level_cleanup_removes_build_state_and_preserves_reports(self):
for goal in ('kunit-clean', 'clean', 'extraclean'):
with self.subTest(goal=goal):
self.check_clean('', goal)
def test_component_cleanup_invalidates_module_list(self):
for directory in ('scst', 'scst/src', 'scst/src/dev_handlers', 'iscsi-scst',
'qla2x00t-32gbit/qla2x00-target'):
if directory != 'scst' and 'kunit/select.mk' not in (ROOT / directory / 'Makefile').read_text():
continue
for goal in ('clean', 'extraclean'):
with self.subTest(directory=directory, goal=goal):
self.check_clean(directory, goal)
for goal in ('scst_clean', 'scst_extraclean', 'iscsi_clean', 'iscsi_extraclean'):
if goal.startswith('iscsi_') and 'iscsi-scst' not in COMPONENTS:
continue
with self.subTest(goal=goal):
self.check_clean('', goal)
def test_failed_cleanup_still_invalidates_module_list(self):
self.check_clean('', 'kunit-clean', fail=True)
def test_cleanup_without_packaged_tooling(self):
# Debian DKMS ships module sources without the scripts/ directory.
for directory in ('', 'scst', 'scst/src', 'iscsi-scst'):
if directory and 'kunit/select.mk' not in (ROOT / directory / 'Makefile').read_text():
continue
for goal in ('clean', 'extraclean'):
with self.subTest(directory=directory, goal=goal):
self.check_clean(directory, goal, tooling=False)
self.check_clean('', 'kunit-clean', tooling=False)
if __name__ == '__main__':
unittest.main()