Skip to content

Commit 6bdd72b

Browse files
pci.PCIDevices: Use Popen(lspci -mn, universal_newlines=True)
Fixes one part of #21 by calling Popen(lspci -nm, universal_newlines=True) This makes Popen().stdout it return an __inter__: string, which the code expects (otherwise, it opens the stdout pipe as binary stream): From: https://docs.python.org/3.6/library/subprocess.html#popen-constructor: "If encoding or errors are specified, the file objects stdin, stdout and stderr are opened in text mode with the specified encoding and errors, as described above in Frequently Used Arguments. If universal_newlines is True, they are opened in text mode with default encoding. >>>__Otherwise, they are opened as binary streams.__<<<" The new test tests xcp.pci.PCIDevices() by setting PATH to a shell script which simluates "lspci -mn" with "cat tests/data/lspci-mn", so calls the real Popen. Final thoughts on using mock: Mocking xcp.pci.subprocess.Popen.stdout using open("tests/data/lspci-mn") does not model the actual behavior of subprocess.Popen().std* in Python3: By it's nature, mocking Popen() does not validate it's correct use in xcp.pci and therfore, it failed to detect the Python3 problem in `xcp/pci.py` Correctly modeling subprocess.Popen() without really calling it is nontrivial, and would require something like the testfixtures library and even that would still be a mock and use the actual subprocess class. Mocking is better suited when the test knows how an API must be called, which isn't the case here. Signed-off-by: Bernhard Kaindl <[email protected]>
1 parent d51a816 commit 6bdd72b

File tree

5 files changed

+38
-5
lines changed

5 files changed

+38
-5
lines changed

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 & 0 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'",

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: 24 additions & 3 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,6 +69,22 @@ def tests_nodb(self):
6669
PCIIds.read()
6770
exists_mock.assert_called_once_with("/usr/share/hwdata/pci.ids")
6871

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+
6988
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, \
@@ -80,13 +99,15 @@ def test_videoclass_by_mock_calls(self):
8099
@classmethod
81100
def mock_lspci_using_open_testfile(cls):
82101
"""Mock xcp.pci.PCIDevices.Popen() using open(tests/data/lspci-mn)"""
83-
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
84104
with patch("xcp.pci.subprocess.Popen") as popen_mock, \
85105
open("tests/data/lspci-mn") as fake_data:
86106
popen_mock.return_value.stdout.__iter__ = Mock(return_value=iter(fake_data))
87107
devs = PCIDevices()
88-
popen_mock.assert_called_once_with(['lspci', '-mn'], bufsize = 1,
89-
stdout = subprocess.PIPE)
108+
popen_mock.assert_called_once_with(
109+
["lspci", "-mn"], bufsize=1, stdout=subprocess.PIPE, universal_newlines=True
110+
)
90111
return devs
91112

92113
def assert_videoclass_devices(self, ids, devs): # type: (PCIIds, PCIDevices) -> None

xcp/pci.py

Lines changed: 6 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('-')]

0 commit comments

Comments
 (0)