debian-forge/test/mod/test_loop.py
Christian Kellner 568a4ad97a loop: add new on_close callback to Loop
Add a new signal like callback to the `Loop` class which will be
invoked before the actual loop device is closed, i.e. the loop
device has an open file descriptor to the device node and it is
being closed. Can be used to perform custom cleanup tasks.
2021-12-09 00:44:21 +00:00

255 lines
6 KiB
Python

#
# Test for the loop.py
#
import contextlib
import fcntl
import os
import time
import threading
from tempfile import TemporaryDirectory, TemporaryFile
import pytest
from osbuild import loop
from ..test import TestBase
@pytest.fixture(name="tempdir")
def tempdir_fixture():
with TemporaryDirectory(prefix="loop-") as tmp:
yield tmp
@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_basic(tempdir):
test_data = b"osbuild"
path = os.path.join(tempdir, "test.img")
ctl = loop.LoopControl()
assert ctl
with pytest.raises(ValueError):
ctl.loop_for_fd(-1)
lo, f = None, None
try:
f = open(path, "wb+")
f.truncate(1024)
f.flush()
lo = ctl.loop_for_fd(f.fileno(), autoclear=True)
assert lo.is_bound_to(f.fileno())
sb = os.fstat(f.fileno())
assert lo
assert lo.devname
info = lo.get_status()
assert info.lo_inode == sb.st_ino
assert info.lo_number == lo.minor
# check for `LoopInfo.is_bound_to` helper
assert info.is_bound_to(sb)
with TemporaryFile(dir=tempdir) as t:
t.write(b"")
t.flush()
st = os.fstat(t.fileno())
assert not info.is_bound_to(st)
# check for autoclear flags setting and helpers
assert info.autoclear
lo.set_status(autoclear=False)
info = lo.get_status()
assert not info.autoclear
with open(os.path.join("/dev", lo.devname), "wb") as f:
f.write(test_data)
# the `flush_buf` seems to be necessary when calling
# `LoopInfo.clear_fd`, otherwise the data integrity
# check later will fail
lo.flush_buf()
lo.clear_fd()
finally:
if lo:
with contextlib.suppress(OSError):
lo.clear_fd()
lo.close()
if f:
f.close()
ctl.close()
# check for data integrity, i.e. that what we wrote via the
# loop device was actually written to the underlying file
with open(path, "rb") as f:
assert f.read(len(test_data)) == test_data
# closing must be a no-op on a closed LoopControl
ctl.close()
# check we raise exceptions on methods that require
# an open LoopControl
for fn in (ctl.add, ctl.remove, ctl.get_unbound):
with pytest.raises(RuntimeError):
fn()
with pytest.raises(RuntimeError):
ctl.loop_for_fd(0)
@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_clear_fd_wait(tempdir):
path = os.path.join(tempdir, "test.img")
ctl = loop.LoopControl()
assert ctl
delay_time = 0.25
def close_loop(lo, barrier):
barrier.wait()
time.sleep(delay_time)
print("closing loop")
lo.close()
lo, lo2, f = None, None, None
try:
f = open(path, "wb+")
f.truncate(1024)
f.flush()
lo = ctl.loop_for_fd(f.fileno(), autoclear=False)
assert lo
# Increase reference count of the loop to > 1 thus
# preventing the kernel from immediately closing the
# device. Instead the kernel will set the autoclear
# attribute and return
lo2 = loop.Loop(lo.minor)
assert lo2
# as long as the second loop is alive, the kernel can
# not clear the fd and thus we will get a timeout
with pytest.raises(TimeoutError):
lo.clear_fd_wait(f.fileno(), 0.1, 0.01)
# start a thread and sync with a barrier, then close
# the loop device in the background thread while the
# main thread is waiting in `clear_fd_wait`. We wait
# four times the delay time of the thread to ensure
# we don't get a timeout.
barrier = threading.Barrier(2)
thread = threading.Thread(
target=close_loop,
args=(lo2, barrier)
)
barrier.reset()
thread.start()
barrier.wait()
lo.clear_fd_wait(f.fileno(), 4*delay_time, delay_time/10)
# no timeout exception has occurred and thus the device
# must not be be bound to the original file anymore
assert not lo.is_bound_to(f.fileno())
finally:
if lo2:
lo2.close()
if lo:
with contextlib.suppress(OSError):
lo.clear_fd()
lo.close()
if f:
f.close()
ctl.close()
@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_lock(tempdir):
path = os.path.join(tempdir, "test.img")
ctl = loop.LoopControl()
assert ctl
lo, lo2, f = None, None, None
try:
f = open(path, "wb+")
f.truncate(1024)
f.flush()
lo = ctl.loop_for_fd(f.fileno(), autoclear=True, lock=True)
assert lo
lo2 = loop.Loop(lo.minor)
assert lo2
with pytest.raises(BlockingIOError):
lo2.flock(fcntl.LOCK_EX | fcntl.LOCK_NB)
lo.close()
lo = None
# after lo is closed, the lock should be release and
# we should be able to obtain the lock
lo2.flock(fcntl.LOCK_EX | fcntl.LOCK_NB)
lo2.clear_fd()
finally:
if lo2:
lo2.close()
if lo:
lo.close()
if f:
f.close()
ctl.close()
@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_on_close(tempdir):
path = os.path.join(tempdir, "test.img")
ctl = loop.LoopControl()
assert ctl
lo, f = None, None
invoked = False
def on_close(l):
nonlocal invoked
invoked = True
# check that this is a no-op
l.close()
try:
f = open(path, "wb+")
f.truncate(1024)
f.flush()
lo = ctl.loop_for_fd(f.fileno(), autoclear=True, lock=True)
assert lo
lo.on_close = on_close
lo.close()
assert invoked
finally:
if lo:
lo.close()
ctl.close()