255 lines
6 KiB
Python
255 lines
6 KiB
Python
#
|
|
# Test for the loop.py
|
|
#
|
|
|
|
import contextlib
|
|
import fcntl
|
|
import os
|
|
import threading
|
|
import time
|
|
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()
|