kiwi-el8/test/unit/storage/loop_device_test.py
Marcus Schäfer 925f960445
Fix potential race condition in loop detach
The call to 'losetup -d' is in fact an async operation. Once
the command returns the loop can still be associated with the
former file because it gets lazy unbound and releases later.
Prior re-use of the same loop device it is therefore required
to wait until the kernel event queue is processed.
2024-06-20 18:08:35 +02:00

95 lines
3.0 KiB
Python

import logging
from unittest.mock import (
patch, call
)
from pytest import (
raises, fixture
)
from kiwi.storage.loop_device import LoopDevice
from kiwi.exceptions import (
KiwiLoopSetupError,
KiwiCommandError
)
class TestLoopDevice:
@fixture(autouse=True)
def inject_fixtures(self, caplog):
self._caplog = caplog
@patch('os.path.exists')
def setup(self, mock_exists):
mock_exists.return_value = False
self.loop = LoopDevice('loop-file', 20, 4096)
@patch('os.path.exists')
def setup_method(self, cls, mock_exists):
self.setup()
def test_loop_setup_invalid(self):
with raises(KiwiLoopSetupError):
LoopDevice('loop-file-does-not-exist-and-no-size-given')
def test_get_device(self):
assert self.loop.get_device() == ''
def test_is_loop(self):
assert self.loop.is_loop() is True
@patch('os.path.exists')
@patch('kiwi.storage.loop_device.Command.run')
@patch('kiwi.storage.loop_device.CommandCapabilities.has_option_in_help')
def test_create(
self, mock_has_option_in_help, mock_command, mock_exists
):
mock_has_option_in_help.return_value = True
mock_exists.return_value = False
self.loop.create()
call = mock_command.call_args_list[0]
assert mock_command.call_args_list[0] == \
call([
'qemu-img', 'create', 'loop-file', '20M'
])
call = mock_command.call_args_list[1]
assert mock_command.call_args_list[1] == \
call([
'losetup', '--sector-size', '4096',
'-f', '--show', 'loop-file'
])
mock_has_option_in_help.return_value = False
mock_command.reset_mock()
self.loop.create()
assert mock_command.call_args_list[1] == \
call([
'losetup', '--logical-blocksize', '4096',
'-f', '--show', 'loop-file'
])
self.loop.node_name = None
@patch('kiwi.storage.loop_device.Command.run')
@patch('os.path.exists')
def test_context_manager_exit(self, mock_os_path_exists, mock_command_run):
mock_os_path_exists.return_value = True
def side_effect(params):
# raise on first command which is 'losetup -f ...'
# and raise on last command which is 'udevadm settle' to trigger
# the logging error
if params[1] == '-f' or params[1] == 'settle':
raise KiwiCommandError('issue')
mock_command_run.side_effect = side_effect
with self._caplog.at_level(logging.ERROR):
with LoopDevice('loop-file', 20) as loop_provider:
loop_provider.node_name = '/dev/loop0'
with raises(KiwiCommandError):
loop_provider.create(overwrite=False)
assert mock_command_run.call_args_list == [
call(['losetup', '-f', '--show', 'loop-file']),
call(['losetup', '-d', '/dev/loop0']),
call(['udevadm', 'settle'])
]