[RFC 3/9] selftests: block_seek_hole: add loop block driver tests

Stefan Hajnoczi
2024-03-28T20:40:03Z

Run the tests with:

$ make TARGETS=block_seek_hole -C tools/selftests run_tests

Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
---
tools/testing/selftests/Makefile | 1 +
.../selftests/block_seek_hole/Makefile | 17 +++
.../testing/selftests/block_seek_hole/config | 1 +
.../selftests/block_seek_hole/map_holes.py | 37 +++++++
.../testing/selftests/block_seek_hole/test.py | 103 ++++++++++++++++++
5 files changed, 159 insertions(+)
create mode 100644 tools/testing/selftests/block_seek_hole/Makefile
create mode 100644 tools/testing/selftests/block_seek_hole/config
create mode 100755 tools/testing/selftests/block_seek_hole/map_holes.py
create mode 100755 tools/testing/selftests/block_seek_hole/test.py

diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index e1504833654db..8a21d6031b940 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -2,6 +2,7 @@
TARGETS += alsa
TARGETS += amd-pstate
TARGETS += arm64
+TARGETS += block_seek_hole
TARGETS += bpf
TARGETS += breakpoints
TARGETS += cachestat
diff --git a/tools/testing/selftests/block_seek_hole/Makefile b/tools/testing/selftests/block_seek_hole/Makefile
new file mode 100644
index 0000000000000..3f4bbd52db29f
--- /dev/null
+++ b/tools/testing/selftests/block_seek_hole/Makefile
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: GPL-2.0-only
+PY3 = $(shell which python3 2>/dev/null)
+
+ifneq ($(PY3),)
+
+TEST_PROGS := test.py
+
+include ../lib.mk
+
+else
+
+all: no_py3_warning
+
+no_py3_warning:
+ @echo "Missing python3. This test will be skipped."
+
+endif
diff --git a/tools/testing/selftests/block_seek_hole/config b/tools/testing/selftests/block_seek_hole/config
new file mode 100644
index 0000000000000..72437e0c0fc1c
--- /dev/null
+++ b/tools/testing/selftests/block_seek_hole/config
@@ -0,0 +1 @@
+CONFIG_BLK_DEV_LOOP=m
diff --git a/tools/testing/selftests/block_seek_hole/map_holes.py b/tools/testing/selftests/block_seek_hole/map_holes.py
new file mode 100755
index 0000000000000..9477ec5d69d3a
--- /dev/null
+++ b/tools/testing/selftests/block_seek_hole/map_holes.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# map_holes.py <filename>
+#
+# Print the holes and data ranges in a file.
+
+import errno
+import os
+import sys
+
+def map_holes(fd):
+ end = os.lseek(fd, 0, os.SEEK_END)
+ offset = 0
+
+ print('TYPE START END SIZE')
+
+ while offset < end:
+ contents = 'DATA'
+ new_offset = os.lseek(fd, offset, os.SEEK_HOLE)
+ if new_offset == offset:
+ contents = 'HOLE'
+ try:
+ new_offset = os.lseek(fd, offset, os.SEEK_DATA)
+ except OSError as err:
+ if err.errno == errno.ENXIO:
+ new_offset = end
+ else:
+ raise err
+ assert new_offset != offset
+ print(f'{contents} {offset} {new_offset} {new_offset - offset}')
+ offset = new_offset
+
+if __name__ == '__main__':
+ with open(sys.argv[1], 'rb') as f:
+ fd = f.fileno()
+ map_holes(fd)
diff --git a/tools/testing/selftests/block_seek_hole/test.py b/tools/testing/selftests/block_seek_hole/test.py
new file mode 100755
index 0000000000000..4f7c2d01ab3d3
--- /dev/null
+++ b/tools/testing/selftests/block_seek_hole/test.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# test.py
+#
+# Test SEEK_HOLE/SEEK_DATA support in block drivers
+
+import os
+import subprocess
+import sys
+from contextlib import contextmanager
+
+KB = 1024
+MB = 1024 * KB
+
+def run(args):
+ try:
+ cmd = subprocess.run(args, check=True, capture_output=True)
+ except subprocess.CalledProcessError as e:
+ print(e)
+ print(e.stderr.decode('utf-8').strip())
+ sys.exit(1)
+ return cmd
+
+@contextmanager
+def test_file(layout_fn, prefix='test'):
+ '''A context manager that creates a test file and produces its path'''
+ path = f'{prefix}-{os.getpid()}'
+ with open(path, 'w+b') as f:
+ layout_fn(f)
+
+ try:
+ yield path
+ finally:
+ os.unlink(path)
+
+@contextmanager
+def loop_device(file_path):
+ '''A context manager that attaches a loop device for a given file and produces the path of the loop device'''
+ cmd = run(['losetup', '--show', '-f', file_path])
+ loop_path = os.fsdecode(cmd.stdout.strip())
+
+ try:
+ yield loop_path
+ finally:
+ run(['losetup', '-d', loop_path])
+
+def test(layout, dev_context_manager):
+ with test_file(layout) as file_path, dev_context_manager(file_path) as dev_path:
+ cmd = run(['./map_holes.py', file_path])
+ file_output = cmd.stdout.decode('utf-8').strip()
+
+ cmd = run(['./map_holes.py', dev_path])
+ dev_output = cmd.stdout.decode('utf-8').strip()
+
+ if file_output != dev_output:
+ print(f'FAIL {dev_context_manager.__name__} {layout.__name__}')
+ print('File output:')
+ print(file_output)
+ print('Does not match device output:')
+ print(dev_output)
+ sys.exit(1)
+
+def test_all(layouts, dev_context_managers):
+ for dev_context_manager in dev_context_managers:
+ for layout in layouts:
+ test(layout, dev_context_manager)
+
+# Different data layouts to test
+
+def data_at_beginning_and_end(f):
+ f.write(b'A' * 4 * KB)
+ f.seek(256 * MB)
+
+ f.write(b'B' * 64 * KB)
+
+ f.seek(1024 * MB - KB)
+ f.write(b'C' * KB)
+
+def holes_at_beginning_and_end(f):
+ f.seek(128 * MB)
+ f.write(b'A' * 4 * KB)
+
+ f.seek(512 * MB)
+ f.write(b'B' * 64 * KB)
+
+ f.truncate(1024 * MB)
+
+def no_holes(f):
+ # Just 1 MB so test file generation is quick
+ mb = b'A' * MB
+ f.write(mb)
+
+def empty_file(f):
+ f.truncate(1024 * MB)
+
+if __name__ == '__main__':
+ layouts = [data_at_beginning_and_end,
+ holes_at_beginning_and_end,
+ no_holes,
+ empty_file]
+ dev_context_managers = [loop_device]
+ test_all(layouts, dev_context_managers)
--
2.44.0