@samitouri / QOSamiQemu / commits / 7d06bbb2d9

iotests: test O_TRUNC behavior for fuse exports

The test cases for the blockdev-based export and for the file-based export with growable=on work before commit a94a1d7699 ("fuse: Manually process requests (without libfuse)"), then are broken until commit "block/export/fuse: fix regression with O_TRUNC when export is growable". The test case for the blockdev-based export requires passwordless sudo for losetup and chmod similar to test 108. Signed-off-by: Fiona Ebner <f.ebner@proxmox.com> Message-ID: <20260702132256.661429-3-f.ebner@proxmox.com> [kwolf: Catch OSError when probing sudo support] Reviewed-by: Kevin Wolf <kwolf@redhat.com> Signed-off-by: Kevin Wolf <kwolf@redhat.com>

Fiona Ebner committed Jul 2, 2026 at 15:22 UTC 7d06bbb2d9ba77e45b5a3131c9e7f934df99d5bc
2 files changed +176
tests/qemu-iotests/tests/fuse-truncate new
+171
@@ -0,0 +1,171 @@
1 +#!/usr/bin/env python3
2 +# group: rw
3 +#
4 +# Test how fuse exports behave with regard to O_TRUNC.
5 +#
6 +# Copyright (C) 2026 Proxmox Server Solutions GmbH
7 +#
8 +# SPDX-License-Identifier: GPL-2.0-or-later
9 +
10 +import os
11 +import subprocess
12 +from pathlib import Path
13 +
14 +import iotests
15 +from iotests import qemu_img, QemuStorageDaemon
16 +
17 +fuse_mount_point = os.path.join(iotests.test_dir, 'export.fuse')
18 +image_size = 1 * 1024 * 1024
19 +image = os.path.join(iotests.test_dir, 'image.' + iotests.imgfmt)
20 +
21 +def check_fuse_support():
22 + Path(fuse_mount_point).touch()
23 + test_qsd = QemuStorageDaemon('--blockdev', 'null-co,node-name=node0',
24 + qmp=True)
25 + res = test_qsd.qmp('block-export-add', {
26 + 'id': 'exp0',
27 + 'type': 'fuse',
28 + 'node-name': 'node0',
29 + 'mountpoint': fuse_mount_point,
30 + 'allow-other': 'off'
31 + })
32 + test_qsd.stop()
33 + os.remove(fuse_mount_point)
34 + if 'error' in res:
35 + assert (res['error']['desc'] ==
36 + "Parameter 'type' does not accept value 'fuse'")
37 + iotests.notrun('No FUSE support')
38 +
39 +def check_sudo_support():
40 + try:
41 + subprocess.run(['sudo', '-n', 'losetup', '--version'],
42 + capture_output=True, check=True)
43 + except (OSError, subprocess.CalledProcessError):
44 + return False
45 + try:
46 + subprocess.run(['sudo', '-n', 'chmod', '--version'],
47 + capture_output=True, check=True)
48 + except (OSError, subprocess.CalledProcessError):
49 + return False
50 + return True
51 +
52 +check_fuse_support()
53 +
54 +class TestTruncateBase(iotests.QMPTestCase):
55 + growable = False
56 + supports_preconditions = True
57 +
58 + def evaluate_preconditions(self):
59 + return
60 +
61 + def add_blockdev(self):
62 + qemu_img('create', '-f', iotests.imgfmt, image, str(image_size))
63 + self.qsd.cmd('blockdev-add', {
64 + 'node-name': 'node0',
65 + 'driver': iotests.imgfmt,
66 + 'file': {
67 + 'driver': 'file',
68 + 'filename': image
69 + }
70 + })
71 +
72 + def cleanup_blockdev(self):
73 + os.remove(image)
74 +
75 + def add_export(self):
76 + self.qsd.cmd('block-export-add', {
77 + 'id': 'exp0',
78 + 'type': 'fuse',
79 + 'node-name': 'node0',
80 + 'mountpoint': fuse_mount_point,
81 + 'growable': self.growable,
82 + 'writable': True,
83 + 'allow-other': 'off'
84 + })
85 +
86 + def stop_qsd(self):
87 + if self.qsd:
88 + self.qsd.cmd('block-export-del', {'id': 'exp0'})
89 + self.qsd.stop()
90 + self.qsd = None
91 +
92 + def setUp(self):
93 + self.evaluate_preconditions()
94 + if not self.supports_preconditions:
95 + return
96 + Path(fuse_mount_point).touch()
97 + self.qsd = QemuStorageDaemon(qmp=True)
98 + self.add_blockdev()
99 + self.add_export()
100 +
101 + def tearDown(self):
102 + if not self.supports_preconditions:
103 + return
104 + self.stop_qsd()
105 + self.cleanup_blockdev()
106 + os.remove(fuse_mount_point)
107 +
108 +class TestTruncateFileGrowable(TestTruncateBase):
109 + growable = True
110 +
111 + def test_o_trunc(self):
112 + with open(fuse_mount_point, 'w+b') as file:
113 + file.seek(0, os.SEEK_END)
114 + self.assertEqual(file.tell(), 0)
115 + file.write(b"test")
116 + self.stop_qsd()
117 +
118 +class TestTruncateFileNotGrowable(TestTruncateBase):
119 + growable = False
120 +
121 + def test_o_trunc(self):
122 + with open(fuse_mount_point, 'w+b') as file:
123 + file.seek(0, os.SEEK_END)
124 + self.assertEqual(file.tell(), image_size)
125 + file.seek(0, os.SEEK_SET)
126 + file.write(b"test")
127 + self.stop_qsd()
128 +
129 +class TestTruncateBlockdev(TestTruncateBase):
130 + growable = False
131 +
132 + def evaluate_preconditions(self):
133 + self.supports_preconditions = check_sudo_support()
134 +
135 + def add_blockdev(self):
136 + qemu_img('create', '-f', iotests.imgfmt, image, str(image_size))
137 + res = subprocess.run(['sudo', '-n', 'losetup', '--show', '-f', image],
138 + check=True, capture_output=True, text=True)
139 + self.loopdev = res.stdout.strip()
140 + subprocess.run(['sudo', '-n', 'chmod', 'go+rw', self.loopdev],
141 + check=True, capture_output=True)
142 + self.qsd.cmd('blockdev-add', {
143 + 'node-name': 'node0',
144 + 'driver': iotests.imgfmt,
145 + 'file': {
146 + 'driver': 'host_device',
147 + 'filename': self.loopdev
148 + }
149 + })
150 +
151 + def cleanup_blockdev(self):
152 + subprocess.run(['sudo', '-n', 'losetup', '--detach', self.loopdev],
153 + check=True, capture_output=True)
154 + os.remove(image)
155 +
156 + def test_o_trunc(self):
157 + if not self.supports_preconditions:
158 + iotests.case_notrun('No passwordless sudo for losetup and chmod')
159 + return
160 +
161 + with open(fuse_mount_point, 'w+b') as file:
162 + file.seek(0, os.SEEK_END)
163 + self.assertEqual(file.tell(), image_size)
164 + file.seek(0, os.SEEK_SET)
165 + file.write(b"test")
166 + self.stop_qsd()
167 +
168 +if __name__ == '__main__':
169 + iotests.main(supported_fmts=['raw'],
170 + supported_protocols=['file'],
171 + supported_platforms=['linux'])
tests/qemu-iotests/tests/fuse-truncate.out new
+5
@@ -0,0 +1,5 @@
1 +...
2 +----------------------------------------------------------------------
3 +Ran 3 tests
4 +
5 +OK