Skip to content

Commit c583723

Browse files
Merge pull request #38 from xenserver-next/Py3-fix-PCIDevices-Popen-universal_newlines
pci.PCIDevices(): Fix Popen("lspci -mn") for Py3 using universal_newlines=True
2 parents 5c36c6e + 6bdd72b commit c583723

5 files changed

Lines changed: 50 additions & 11 deletions

File tree

pylintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ disable=W0142,W0703,C0111,R0201,W0603,W0613,W0212,W0141,
5353
unrecognized-option, # Skip complaining on pylintrc options only in pylint2/pylint3
5454
unknown-option-value, # Skip complaining about checkers only in pylint2/pylint3
5555
useless-object-inheritance, # "object" is not obsolete for supporting Python2
56+
super-with-arguments, # super() with arguments is a Python3-only feature
5657
consider-using-f-string, # Python3-only feature, need to migrate everything first
5758
consider-using-with, # Only for new code, move to Python3 is more important
5859
logging-not-lazy # Debug-Logging is not used in "hot" code paths here

pyproject.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ dependencies = [
4646
[project.optional-dependencies]
4747
test = [
4848
"mock",
49+
"pyfakefs",
4950
"pytest",
5051
"pytest-cov",
5152
"pytest_httpserver; python_version >= '3.7'",
@@ -122,10 +123,6 @@ disable_error_code = ["var-annotated", "unreachable"]
122123

123124
# Most of these should be easily fixable by adding type annotations as comments(PEP484):
124125

125-
[[tool.mypy.overrides]]
126-
module = ["tests.test_pci"]
127-
disable_error_code = ["no-any-return"]
128-
129126
[[tool.mypy.overrides]]
130127
module = ["tests.test_mac"]
131128
disable_error_code = ["var-annotated"]

tests/data/lspci

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/sh
2+
# Simulate lspci -nm for tests.test_pci.test_videoclass_without_mock():
3+
if [ "$1" = "-mn" ]; then
4+
PATH=/usr/bin:/bin
5+
cat tests/data/lspci-mn
6+
fi

tests/test_pci.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import subprocess
22
import unittest
3+
from os import environ
4+
5+
import pyfakefs.fake_filesystem_unittest # type: ignore[import]
36
from mock import patch, Mock
47

58
from xcp.pci import PCI, PCIIds, PCIDevices
@@ -66,7 +69,23 @@ def tests_nodb(self):
6669
PCIIds.read()
6770
exists_mock.assert_called_once_with("/usr/share/hwdata/pci.ids")
6871

69-
def tests_videoclass(self):
72+
def test_videoclass_without_mock(self):
73+
"""
74+
Verifies that xcp.pci uses the open() and Popen() correctly across versions.
75+
Tests PCIIds.read() and PCIDevices() without mock for verifying compatibility
76+
with all Python versions.
77+
(The old test using moc could not detect a missing step in the Py3 migration)
78+
"""
79+
with pyfakefs.fake_filesystem_unittest.Patcher() as p:
80+
assert p.fs
81+
p.fs.add_real_file("tests/data/pci.ids", target_path="/usr/share/hwdata/pci.ids")
82+
ids = PCIIds.read()
83+
saved_PATH = environ["PATH"]
84+
environ["PATH"] = "tests/data" # Let PCIDevices() call Popen("tests/data/lspci")
85+
self.assert_videoclass_devices(ids, PCIDevices())
86+
environ["PATH"] = saved_PATH
87+
88+
def test_videoclass_by_mock_calls(self):
7089
with patch("xcp.pci.os.path.exists") as exists_mock, \
7190
patch("xcp.pci.open") as open_mock, \
7291
open("tests/data/pci.ids") as fake_data:
@@ -75,15 +94,26 @@ def tests_videoclass(self):
7594
ids = PCIIds.read()
7695
exists_mock.assert_called_once_with("/usr/share/hwdata/pci.ids")
7796
open_mock.assert_called_once_with("/usr/share/hwdata/pci.ids")
78-
video_class = ids.lookupClass('Display controller')
79-
self.assertEqual(video_class, ['03'])
97+
self.assert_videoclass_devices(ids, self.mock_lspci_using_open_testfile())
8098

99+
@classmethod
100+
def mock_lspci_using_open_testfile(cls):
101+
"""Mock xcp.pci.PCIDevices.Popen() using open(tests/data/lspci-mn)"""
102+
# Note: Mocks Popen using open, which is wrong, but mocking using Popen is
103+
# not supported by mock, so the utility of this test is limited - may be removed
81104
with patch("xcp.pci.subprocess.Popen") as popen_mock, \
82105
open("tests/data/lspci-mn") as fake_data:
83106
popen_mock.return_value.stdout.__iter__ = Mock(return_value=iter(fake_data))
84107
devs = PCIDevices()
85-
popen_mock.assert_called_once_with(['lspci', '-mn'], bufsize = 1,
86-
stdout = subprocess.PIPE)
108+
popen_mock.assert_called_once_with(
109+
["lspci", "-mn"], bufsize=1, stdout=subprocess.PIPE, universal_newlines=True
110+
)
111+
return devs
112+
113+
def assert_videoclass_devices(self, ids, devs): # type: (PCIIds, PCIDevices) -> None
114+
"""Verification function for checking the otuput of PCIDevices.findByClass()"""
115+
video_class = ids.lookupClass('Display controller')
116+
self.assertEqual(video_class, ["03"])
87117
sorted_devices = sorted(devs.findByClass(video_class),
88118
key=lambda x: x['id'])
89119
self.assertEqual(len(sorted_devices), 2)

xcp/pci.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,12 @@ class PCIDevices(object):
255255
def __init__(self):
256256
self.devs = {}
257257

258-
cmd = subprocess.Popen(['lspci', '-mn'], bufsize = 1,
259-
stdout = subprocess.PIPE)
258+
cmd = subprocess.Popen(
259+
["lspci", "-mn"],
260+
bufsize=1,
261+
stdout=subprocess.PIPE,
262+
universal_newlines=True,
263+
)
260264
for l in cmd.stdout:
261265
line = l.rstrip()
262266
el = [x for x in line.replace('"', '').split() if not x.startswith('-')]
@@ -271,6 +275,7 @@ def __init__(self):
271275
cmd.wait()
272276

273277
def findByClass(self, cls, subcls = None):
278+
# type: (str|list[str], str|None) -> list[dict[str, str]]
274279
""" return all devices that match either of:
275280
276281
class, subclass

0 commit comments

Comments
 (0)