Skip to content

Commit 234d30e

Browse files
author
Martin Belanger
committed
python-libnvme: drop 1.x backward-compatibility shims
Remove the deprecated method wrappers `discovery_ctrl_set()` and `persistent_set()` on `ctrl`, and the module-level aliases `NVME_LOG_LID_DISCOVER` and `root` that existed for compatibility with libnvme 1.x code. Callers should use the property setters (`ctrl.discovery_ctrl`, `ctrl.persistent`) and the canonical names (`NVME_LOG_LID_DISCOVERY`, `global_ctx`) directly. Add a new Python test module (test-objects.py) covering object creation and property access, iterator correctness, boolean flag get/set, and error handling for disconnected controllers, replacing the removed deprecation-warning tests. The new tests allowed finding and fixing a pre-existing bug in nvme.i where a NULL returned by nvme_read_hostnqn() or nvme_read_hostid() was not being handled and would cause a crash. Signed-off-by: Martin Belanger <[email protected]> cac
1 parent 5263254 commit 234d30e

3 files changed

Lines changed: 269 additions & 34 deletions

File tree

libnvme/libnvme/meson.build

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,11 @@ if want_python
7575
# Test section
7676
test('libnvme - python-import-libnvme', python3, args: ['-c', 'from libnvme import nvme'], env: test_env, depends: pynvme_clib)
7777

78-
py_tests = [
78+
py_tests = [
7979
[ 'create-ctrl-object', [ files('tests/create-ctrl-obj.py'), ] ],
8080
[ 'sigsegv-during-gc', [ files('tests/gc.py'), ] ],
8181
[ 'read-nbft-file', [ files('tests/test-nbft.py'), '--filename', join_paths(meson.current_source_dir(), 'tests', 'NBFT') ] ],
82+
[ 'object-properties-and-errors', [ files('tests/test-objects.py'), ] ],
8283
]
8384
foreach test: py_tests
8485
description = test[0]

libnvme/libnvme/nvme.i

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919

2020
%allowexception;
2121

22-
%rename(global_ctx) nvme_global_ctx;
23-
%rename(host) nvme_host;
24-
%rename(ctrl) nvme_ctrl;
25-
%rename(subsystem) nvme_subsystem;
26-
%rename(ns) nvme_ns;
22+
%rename(global_ctx) nvme_global_ctx;
23+
%rename(host) nvme_host;
24+
%rename(ctrl) nvme_ctrl;
25+
%rename(subsystem) nvme_subsystem;
26+
%rename(ns) nvme_ns;
2727

2828
%{
2929
#include <ccan/list/list.h>
@@ -40,13 +40,13 @@
4040
}
4141
PyObject *read_hostnqn() {
4242
char * val = nvme_read_hostnqn();
43-
PyObject * obj = PyUnicode_FromString(val);
43+
PyObject * obj = val ? PyUnicode_FromString(val) : Py_NewRef(Py_None);
4444
free(val);
4545
return obj;
4646
}
4747
PyObject *read_hostid() {
4848
char * val = nvme_read_hostid();
49-
PyObject * obj = PyUnicode_FromString(val);
49+
PyObject * obj = val ? PyUnicode_FromString(val) : Py_NewRef(Py_None);
5050
free(val);
5151
return obj;
5252
}
@@ -461,8 +461,9 @@ struct nvme_ctrl {
461461
char *dhchap_ctrl_key;
462462

463463
/**
464-
* We are remapping the following members of the C code's
465-
* nvme_ctrl_t to different names in Python. Here's the mapping:
464+
* We are remapping the following member(s) of the C code's
465+
* nvme_ctrl_t to different name(s) in Python. Here's the
466+
* mapping:
466467
*
467468
* C code Python (SWIG)
468469
* ===================== =====================
@@ -703,14 +704,6 @@ struct nvme_ns {
703704
return $self;
704705
}
705706

706-
%pythoncode %{
707-
def discovery_ctrl_set(self, discovery: bool):
708-
r"""DEPRECATED METHOD: Use property setter instead (e.g. ctrl.discovery_ctrl = True)"""
709-
import warnings
710-
warnings.warn("Use property setter instead (e.g. ctrl_obj.discovery_ctrl = True)", DeprecationWarning, stacklevel=2)
711-
return _nvme.ctrl_discovery_ctrl_set(self, discovery)
712-
%}
713-
714707
bool init(struct nvme_host *h, int instance) {
715708
return nvme_init_ctrl(h, $self, instance) == 0;
716709
}
@@ -738,13 +731,6 @@ struct nvme_ns {
738731
bool connected() {
739732
return nvme_ctrl_get_name($self) != NULL;
740733
}
741-
%pythoncode %{
742-
def persistent_set(self, persistent: bool):
743-
r"""DEPRECATED METHOD: Use property setter instead (e.g. ctrl.persistent = True)"""
744-
import warnings
745-
warnings.warn("Use property setter instead (e.g. ctrl_obj.persistent = True)", DeprecationWarning, stacklevel=2)
746-
return _nvme.ctrl_persistent_set(self, persistent)
747-
%}
748734
void rescan() {
749735
nvme_rescan_ctrl($self);
750736
}
@@ -1268,12 +1254,3 @@ PyObject *nbft_get(struct nvme_global_ctx *ctx, const char * filename);
12681254
%rename($ignore, %$isvariable) ""; // ignore all variables
12691255

12701256
%include "../src/nvme/types.h"
1271-
1272-
1273-
%pythoncode %{
1274-
# Definitions for backward compatibility between libnvme 1.x and 3.x (there is no 2.x)
1275-
# Some terms (class/variable names) were changed and this allows running older python
1276-
# code written for libnvme 1.x with libnvme 3.x (and possibly later).
1277-
NVME_LOG_LID_DISCOVER = _nvme.NVME_LOG_LID_DISCOVERY
1278-
root = global_ctx
1279-
%}
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: LGPL-2.1-or-later
3+
"""Unit tests for the libnvme Python bindings.
4+
5+
These tests cover object creation, property access, and error handling.
6+
They do not require real NVMe hardware to run.
7+
"""
8+
import gc
9+
import unittest
10+
from libnvme import nvme
11+
12+
13+
class TestConstants(unittest.TestCase):
14+
"""Verify that well-known constants are accessible and have correct values."""
15+
16+
def test_disc_subsys_name_is_string(self):
17+
self.assertIsInstance(nvme.NVME_DISC_SUBSYS_NAME, str)
18+
19+
def test_disc_subsys_name_value(self):
20+
self.assertEqual(nvme.NVME_DISC_SUBSYS_NAME, 'nqn.2014-08.org.nvmexpress.discovery')
21+
22+
def test_log_lid_discovery_is_int(self):
23+
self.assertIsInstance(nvme.NVME_LOG_LID_DISCOVERY, int)
24+
25+
26+
27+
class TestGlobalCtx(unittest.TestCase):
28+
29+
def test_creation_no_args(self):
30+
ctx = nvme.global_ctx()
31+
self.assertIsNotNone(ctx)
32+
33+
def test_context_manager(self):
34+
with nvme.global_ctx() as ctx:
35+
self.assertIsNotNone(ctx)
36+
37+
def test_hosts_iterator_returns_list(self):
38+
ctx = nvme.global_ctx()
39+
hosts = list(ctx.hosts())
40+
self.assertIsInstance(hosts, list)
41+
42+
def test_refresh_topology_does_not_raise(self):
43+
ctx = nvme.global_ctx()
44+
ctx.refresh_topology()
45+
46+
def test_log_level_all_valid_levels(self):
47+
ctx = nvme.global_ctx()
48+
for level in ('debug', 'info', 'notice', 'warning', 'err', 'crit', 'alert', 'emerg'):
49+
with self.subTest(level=level):
50+
ctx.log_level(level)
51+
52+
53+
class TestHost(unittest.TestCase):
54+
55+
def setUp(self):
56+
self.ctx = nvme.global_ctx()
57+
58+
def tearDown(self):
59+
self.ctx = None
60+
gc.collect()
61+
62+
def test_creation_default(self):
63+
host = nvme.host(self.ctx)
64+
self.assertIsNotNone(host)
65+
66+
def test_creation_with_explicit_hostnqn(self):
67+
hostnqn = 'nqn.2014-08.com.example:test-host-creation'
68+
host = nvme.host(self.ctx, hostnqn=hostnqn)
69+
self.assertIsNotNone(host)
70+
self.assertEqual(host.hostnqn, hostnqn)
71+
72+
def test_creation_with_hostnqn_and_hostid(self):
73+
hostnqn = 'nqn.2014-08.com.example:test-host-props'
74+
hostid = '11111111-2222-3333-4444-555555555555'
75+
host = nvme.host(self.ctx, hostnqn=hostnqn, hostid=hostid)
76+
self.assertEqual(host.hostnqn, hostnqn)
77+
self.assertEqual(host.hostid, hostid)
78+
79+
def test_creation_with_hostsymname(self):
80+
hostnqn = 'nqn.2014-08.com.example:test-host-symname'
81+
symname = 'my-storage-host'
82+
host = nvme.host(self.ctx, hostnqn=hostnqn, hostsymname=symname)
83+
self.assertEqual(host.hostsymname, symname)
84+
85+
def test_set_symname(self):
86+
hostnqn = 'nqn.2014-08.com.example:test-host-set-symname'
87+
host = nvme.host(self.ctx, hostnqn=hostnqn)
88+
host.set_symname('updated-symname')
89+
self.assertEqual(host.hostsymname, 'updated-symname')
90+
91+
def test_dhchap_host_key_is_none_by_default(self):
92+
hostnqn = 'nqn.2014-08.com.example:test-host-dhchap'
93+
host = nvme.host(self.ctx, hostnqn=hostnqn)
94+
self.assertIsNone(host.dhchap_host_key)
95+
96+
def test_subsystems_iterator_returns_list(self):
97+
host = nvme.host(self.ctx)
98+
subsystems = list(host.subsystems())
99+
self.assertIsInstance(subsystems, list)
100+
101+
def test_str_contains_class_name(self):
102+
host = nvme.host(self.ctx)
103+
self.assertIn('nvme.host', str(host))
104+
105+
def test_context_manager(self):
106+
with nvme.host(self.ctx) as h:
107+
self.assertIsNotNone(h)
108+
109+
110+
class TestCtrl(unittest.TestCase):
111+
112+
def setUp(self):
113+
self.ctx = nvme.global_ctx()
114+
self.subsysnqn = nvme.NVME_DISC_SUBSYS_NAME
115+
116+
def tearDown(self):
117+
self.ctx = None
118+
gc.collect()
119+
120+
def _make_loop_ctrl(self):
121+
return nvme.ctrl(self.ctx, subsysnqn=self.subsysnqn, transport='loop')
122+
123+
def test_creation_loop_transport(self):
124+
ctrl = self._make_loop_ctrl()
125+
self.assertIsNotNone(ctrl)
126+
127+
def test_creation_tcp_transport_with_traddr(self):
128+
ctrl = nvme.ctrl(
129+
self.ctx,
130+
subsysnqn=self.subsysnqn,
131+
transport='tcp',
132+
traddr='192.168.1.1',
133+
trsvcid='4420',
134+
)
135+
self.assertIsNotNone(ctrl)
136+
137+
def test_transport_property(self):
138+
ctrl = self._make_loop_ctrl()
139+
self.assertEqual(ctrl.transport, 'loop')
140+
141+
def test_subsysnqn_property(self):
142+
ctrl = self._make_loop_ctrl()
143+
self.assertEqual(ctrl.subsysnqn, self.subsysnqn)
144+
145+
def test_traddr_property(self):
146+
ctrl = nvme.ctrl(
147+
self.ctx,
148+
subsysnqn=self.subsysnqn,
149+
transport='tcp',
150+
traddr='10.0.0.1',
151+
)
152+
self.assertEqual(ctrl.traddr, '10.0.0.1')
153+
154+
def test_trsvcid_property(self):
155+
ctrl = nvme.ctrl(
156+
self.ctx,
157+
subsysnqn=self.subsysnqn,
158+
transport='tcp',
159+
traddr='10.0.0.1',
160+
trsvcid='8009',
161+
)
162+
self.assertEqual(ctrl.trsvcid, '8009')
163+
164+
def test_connected_returns_false_before_connect(self):
165+
ctrl = self._make_loop_ctrl()
166+
self.assertFalse(ctrl.connected())
167+
168+
def test_name_is_none_before_connect(self):
169+
ctrl = self._make_loop_ctrl()
170+
self.assertIsNone(ctrl.name)
171+
172+
def test_str_contains_transport(self):
173+
ctrl = self._make_loop_ctrl()
174+
s = str(ctrl)
175+
self.assertIn('loop', s)
176+
177+
def test_context_manager(self):
178+
with nvme.ctrl(self.ctx, subsysnqn=self.subsysnqn, transport='loop') as c:
179+
self.assertIsNotNone(c)
180+
181+
def test_namespaces_iterator_returns_list(self):
182+
ctrl = self._make_loop_ctrl()
183+
nss = list(ctrl.namespaces())
184+
self.assertIsInstance(nss, list)
185+
186+
def test_discovery_ctrl_flag_default_false(self):
187+
ctrl = self._make_loop_ctrl()
188+
self.assertFalse(ctrl.discovery_ctrl)
189+
190+
def test_discovery_ctrl_flag_set_and_clear(self):
191+
ctrl = self._make_loop_ctrl()
192+
ctrl.discovery_ctrl = True
193+
self.assertTrue(ctrl.discovery_ctrl)
194+
ctrl.discovery_ctrl = False
195+
self.assertFalse(ctrl.discovery_ctrl)
196+
197+
def test_persistent_flag_default_false(self):
198+
ctrl = self._make_loop_ctrl()
199+
self.assertFalse(ctrl.persistent)
200+
201+
def test_persistent_flag_set(self):
202+
ctrl = self._make_loop_ctrl()
203+
ctrl.persistent = True
204+
self.assertTrue(ctrl.persistent)
205+
206+
def test_unique_discovery_ctrl_flag(self):
207+
ctrl = self._make_loop_ctrl()
208+
ctrl.unique_discovery_ctrl = True
209+
self.assertTrue(ctrl.unique_discovery_ctrl)
210+
211+
def test_multiple_ctrls_same_ctx(self):
212+
"""Multiple controllers can be created under the same context."""
213+
ctrls = [self._make_loop_ctrl() for _ in range(5)]
214+
self.assertEqual(len(ctrls), 5)
215+
for c in ctrls:
216+
self.assertFalse(c.connected())
217+
218+
219+
class TestCtrlErrorHandling(unittest.TestCase):
220+
"""Error paths that can be exercised without real hardware."""
221+
222+
def setUp(self):
223+
self.ctx = nvme.global_ctx()
224+
self.ctrl = nvme.ctrl(
225+
self.ctx,
226+
subsysnqn=nvme.NVME_DISC_SUBSYS_NAME,
227+
transport='loop',
228+
)
229+
230+
def tearDown(self):
231+
self.ctrl = None
232+
self.ctx = None
233+
gc.collect()
234+
235+
def test_disconnect_unconnected_raises_attribute_error(self):
236+
with self.assertRaises(AttributeError):
237+
self.ctrl.disconnect()
238+
239+
def test_discover_unconnected_raises_attribute_error(self):
240+
with self.assertRaises(AttributeError):
241+
self.ctrl.discover()
242+
243+
244+
class TestHelperFunctions(unittest.TestCase):
245+
"""Module-level helper functions exposed by the bindings."""
246+
247+
def test_read_hostnqn_returns_string_or_none(self):
248+
hostnqn = nvme.read_hostnqn()
249+
self.assertIsInstance(hostnqn, (str, type(None)))
250+
251+
def test_read_hostid_returns_string_or_none(self):
252+
hostid = nvme.read_hostid()
253+
self.assertIsInstance(hostid, (str, type(None)))
254+
255+
256+
if __name__ == '__main__':
257+
unittest.main()

0 commit comments

Comments
 (0)