Skip to content

Commit bfe587c

Browse files
authored
Add cpp_harness_collect feature (#90)
Close #89
1 parent ddf754a commit bfe587c

File tree

9 files changed

+147
-41
lines changed

9 files changed

+147
-41
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
# 2.2.2 (UNRELEASED)
2+
3+
- New `cpp_harness_collect` configuration option allows users to add prefix arguments to run C++ test executables, allowing to use additional tools like `wine` of `qemu` for a test runners in cross-platform development.
4+
15
# 2.2.1 (2022-09-23)
26

37
- Fixed a problem while handling errors using``--gtest_filter``. The recommendation is to use pytest's own
48
filtering facilities (like `-k`) instead of passing filtering arguments to the underlying framework
59
([#84](https://github.com/pytest-dev/pytest-cpp/issues/84)).
610

7-
811
# 2.2.0 (2022-08-22)
912

1013
- Dropped support for Python 3.6.

README.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,28 @@ wrapping the test binary, like valgrind and memcheck:
129129
cpp_harness = valgrind --tool=memcheck
130130
131131
132+
cpp_harness_collect
133+
^^^^^^^^^^^^^^^^^^^
134+
135+
This option allows the usage of tools or emulators (like wine or qemu) that are used by invoking them
136+
on the console wrapping the test binary during a test collection.
137+
138+
Might be used in the combination with ``cpp_harness`` to run a binary in emulators, like wine or qemu
139+
in cross-compilation targets.
140+
141+
.. code-block:: ini
142+
143+
[pytest]
144+
cpp_harness_collect = qemu-x86_64 -L libs/
145+
146+
or
147+
148+
.. code-block:: ini
149+
150+
[pytest]
151+
cpp_harness_collect = qemu-x86_64 -L libs/
152+
cpp_harness = qemu-x86_64 -L libs/
153+
132154
Changelog
133155
=========
134156

src/pytest_cpp/boost.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pytest_cpp.error import CppTestFailure
1111
from pytest_cpp.error import Markup
1212
from pytest_cpp.facade_abc import AbstractFacade
13+
from pytest_cpp.helpers import make_cmdline
1314

1415

1516
class BoostTestFacade(AbstractFacade):
@@ -18,10 +19,15 @@ class BoostTestFacade(AbstractFacade):
1819
"""
1920

2021
@classmethod
21-
def is_test_suite(cls, executable: str) -> bool:
22+
def is_test_suite(
23+
cls,
24+
executable: str,
25+
harness_collect: Sequence[str] = (),
26+
) -> bool:
27+
args = make_cmdline(harness_collect, executable, ["--help"])
2228
try:
2329
output = subprocess.check_output(
24-
[executable, "--help"],
30+
args,
2531
stderr=subprocess.STDOUT,
2632
universal_newlines=True,
2733
)
@@ -30,7 +36,12 @@ def is_test_suite(cls, executable: str) -> bool:
3036
else:
3137
return "--output_format" in output and "log_format" in output
3238

33-
def list_tests(self, executable: str) -> list[str]:
39+
def list_tests(
40+
self,
41+
executable: str,
42+
harness_collect: Sequence[str] = (),
43+
) -> list[str]:
44+
del harness_collect
3445
# unfortunately boost doesn't provide us with a way to list the tests
3546
# inside the executable, so the test_id is a dummy placeholder :(
3647
return [os.path.basename(os.path.splitext(executable)[0])]
@@ -50,15 +61,17 @@ def read_file(name: str) -> str:
5061
return ""
5162

5263
with tempfile.TemporaryDirectory(prefix="pytest-cpp") as temp_dir:
53-
log_xml = os.path.join(temp_dir, "log.xml")
54-
report_xml = os.path.join(temp_dir, "report.xml")
55-
args = list(harness) + [
56-
executable,
57-
"--output_format=XML",
58-
"--log_sink=%s" % log_xml,
59-
"--report_sink=%s" % report_xml,
60-
]
64+
# On Windows, ValueError is raised when path and start are on different drives.
65+
# In this case failing back to the absolute path.
66+
try:
67+
log_xml = os.path.join(os.path.relpath(temp_dir), "log.xml")
68+
report_xml = os.path.join(os.path.relpath(temp_dir), "report.xml")
69+
except ValueError:
70+
log_xml = os.path.join(temp_dir, "log.xml")
71+
report_xml = os.path.join(temp_dir, "report.xml")
72+
args = make_cmdline(harness, executable, ["--output_format=XML", f"--log_sink={log_xml}", f"--report_sink={report_xml}"])
6173
args.extend(test_args)
74+
6275
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
6376
raw_stdout, _ = p.communicate()
6477
stdout = raw_stdout.decode("utf-8") if raw_stdout else ""

src/pytest_cpp/catch2.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pytest_cpp.error import CppTestFailure
1212
from pytest_cpp.error import Markup
1313
from pytest_cpp.facade_abc import AbstractFacade
14+
from pytest_cpp.helpers import make_cmdline
1415

1516

1617
class Catch2Facade(AbstractFacade):
@@ -19,10 +20,15 @@ class Catch2Facade(AbstractFacade):
1920
"""
2021

2122
@classmethod
22-
def is_test_suite(cls, executable: str) -> bool:
23+
def is_test_suite(
24+
cls,
25+
executable: str,
26+
harness_collect: Sequence[str] = (),
27+
) -> bool:
28+
args = make_cmdline(harness_collect, executable, ["--help"])
2329
try:
2430
output = subprocess.check_output(
25-
[executable, "--help"],
31+
args,
2632
stderr=subprocess.STDOUT,
2733
universal_newlines=True,
2834
)
@@ -31,7 +37,11 @@ def is_test_suite(cls, executable: str) -> bool:
3137
else:
3238
return "--list-test-names-only" in output
3339

34-
def list_tests(self, executable: str) -> list[str]:
40+
def list_tests(
41+
self,
42+
executable: str,
43+
harness_collect: Sequence[str] = (),
44+
) -> list[str]:
3545
"""
3646
Executes test with "--list-test-names-only" and gets list of tests
3747
parsing output like this:
@@ -41,9 +51,10 @@ def list_tests(self, executable: str) -> list[str]:
4151
2: Factorials of 1 and higher are computed (pass)
4252
"""
4353
# This will return an exit code with the number of tests available
54+
args = make_cmdline(harness_collect, executable, ["--list-test-names-only"])
4455
try:
4556
output = subprocess.check_output(
46-
[executable, "--list-test-names-only"],
57+
args,
4758
stderr=subprocess.STDOUT,
4859
universal_newlines=True,
4960
)
@@ -61,15 +72,16 @@ def run_test(
6172
test_args: Sequence[str] = (),
6273
harness: Sequence[str] = (),
6374
) -> tuple[Sequence[Catch2Failure] | None, str]:
64-
with tempfile.TemporaryDirectory(prefix="pytest-cpp") as tmp_dir:
65-
xml_filename = os.path.join(tmp_dir, "cpp-report.xml")
66-
args = list(harness) + [
67-
executable,
68-
test_id,
69-
"--success",
70-
"--reporter=xml",
71-
"--out %s" % xml_filename,
72-
]
75+
with tempfile.TemporaryDirectory(prefix="pytest-cpp") as temp_dir:
76+
"""
77+
On Windows, ValueError is raised when path and start are on different drives.
78+
In this case failing back to the absolute path.
79+
"""
80+
try:
81+
xml_filename = os.path.join(os.path.relpath(temp_dir), "cpp-report.xml")
82+
except ValueError:
83+
xml_filename = os.path.join(temp_dir, "cpp-report.xml")
84+
args = make_cmdline(harness, executable, [test_id, "--success", "--reporter=xml", f"--out {xml_filename}"])
7385
args.extend(test_args)
7486

7587
try:

src/pytest_cpp/facade_abc.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,19 @@
1010
class AbstractFacade(ABC):
1111
@classmethod
1212
@abstractmethod
13-
def is_test_suite(cls, executable: str) -> bool:
13+
def is_test_suite(
14+
cls,
15+
executable: str,
16+
harness_collect: Sequence[str] = (),
17+
) -> bool:
1418
"""Return True if the given path to an executable contains tests for this framework."""
1519

1620
@abstractmethod
17-
def list_tests(self, executable: str) -> list[str]:
21+
def list_tests(
22+
self,
23+
executable: str,
24+
harness_collect: Sequence[str] = (),
25+
) -> list[str]:
1826
"""Return a list of test ids found in the given executable."""
1927

2028
@abstractmethod

src/pytest_cpp/google.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pytest_cpp.error import CppTestFailure
1212
from pytest_cpp.error import Markup
1313
from pytest_cpp.facade_abc import AbstractFacade
14+
from pytest_cpp.helpers import make_cmdline
1415

1516

1617
class GoogleTestFacade(AbstractFacade):
@@ -19,10 +20,15 @@ class GoogleTestFacade(AbstractFacade):
1920
"""
2021

2122
@classmethod
22-
def is_test_suite(cls, executable: str) -> bool:
23+
def is_test_suite(
24+
cls,
25+
executable: str,
26+
harness_collect: Sequence[str] = (),
27+
) -> bool:
28+
args = make_cmdline(harness_collect, executable, ["--help"])
2329
try:
2430
output = subprocess.check_output(
25-
[executable, "--help"],
31+
args,
2632
stderr=subprocess.STDOUT,
2733
universal_newlines=True,
2834
)
@@ -31,7 +37,11 @@ def is_test_suite(cls, executable: str) -> bool:
3137
else:
3238
return "--gtest_list_tests" in output
3339

34-
def list_tests(self, executable: str) -> list[str]:
40+
def list_tests(
41+
self,
42+
executable: str,
43+
harness_collect: Sequence[str] = (),
44+
) -> list[str]:
3545
"""
3646
Executes google-test with "--gtest_list_tests" and gets list of tests
3747
parsing output like this:
@@ -41,8 +51,9 @@ def list_tests(self, executable: str) -> list[str]:
4151
ReturnsTrueForPrimes
4252
CanGetNextPrime
4353
"""
54+
args = make_cmdline(harness_collect, executable, ["--gtest_list_tests"])
4455
output = subprocess.check_output(
45-
[executable, "--gtest_list_tests"],
56+
args,
4657
stderr=subprocess.STDOUT,
4758
universal_newlines=True,
4859
)
@@ -71,13 +82,14 @@ def run_test(
7182
test_args: Sequence[str] = (),
7283
harness: Sequence[str] = (),
7384
) -> tuple[list[GoogleTestFailure] | None, str]:
74-
with tempfile.TemporaryDirectory(prefix="pytest-cpp") as tmp_dir:
75-
xml_filename = os.path.join(tmp_dir, "cpp-report.xml")
76-
args = list(harness) + [
77-
executable,
78-
"--gtest_filter=" + test_id,
79-
"--gtest_output=xml:%s" % xml_filename,
80-
]
85+
with tempfile.TemporaryDirectory(prefix="pytest-cpp") as temp_dir:
86+
# On Windows, ValueError is raised when path and start are on different drives.
87+
# In this case failing back to the absolute path.
88+
try:
89+
xml_filename = os.path.join(os.path.relpath(temp_dir), "cpp-report.xml")
90+
except ValueError:
91+
xml_filename = os.path.join(temp_dir, "cpp-report.xml")
92+
args = make_cmdline(harness, executable, [f"--gtest_filter={test_id}", f"--gtest_output=xml:{xml_filename}"])
8193
args.extend(test_args)
8294

8395
try:

src/pytest_cpp/helpers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
from typing import Sequence
3+
4+
5+
def make_cmdline(harness: Sequence[str], executable: str, arg: Sequence[str] = ()) ->Sequence[str]:
6+
return [*harness, executable, *arg]

src/pytest_cpp/plugin.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,12 @@ def pytest_collect_file(
6767
):
6868
return None
6969

70+
harness_collect = parent.config.getini("cpp_harness_collect")
7071
for facade_class in FACADES:
71-
if facade_class.is_test_suite(str(file_path)):
72+
if facade_class.is_test_suite(
73+
str(file_path),
74+
harness_collect=harness_collect
75+
):
7276
return CppFile.from_parent(
7377
path=file_path,
7478
parent=parent,
@@ -102,7 +106,13 @@ def pytest_addoption(parser: pytest.Parser) -> None:
102106
"cpp_harness",
103107
type="args",
104108
default=(),
105-
help="command that wraps the cpp binary",
109+
help="command that wraps the cpp binary when running tests",
110+
)
111+
parser.addini(
112+
"cpp_harness_collect",
113+
type="args",
114+
default=(),
115+
help="command that wraps the cpp binary when collecting tests",
106116
)
107117
parser.addini(
108118
"cpp_verbose",
@@ -144,7 +154,11 @@ def from_parent( # type:ignore[override]
144154
)
145155

146156
def collect(self) -> Iterator[CppItem]:
147-
for test_id in self.facade.list_tests(str(self.fspath)):
157+
harness_collect=self.config.getini("cpp_harness_collect")
158+
for test_id in self.facade.list_tests(
159+
str(self.fspath),
160+
harness_collect=harness_collect,
161+
):
148162
yield CppItem.from_parent(
149163
parent=self,
150164
name=test_id,

tests/test_pytest_cpp.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pytest_cpp.error import CppFailureRepr
1212
from pytest_cpp.error import CppTestFailure
1313
from pytest_cpp.google import GoogleTestFacade
14+
from pytest_cpp.helpers import make_cmdline
1415

1516

1617
def assert_outcomes(result, expected_outcomes):
@@ -91,6 +92,21 @@ def test_success(facade, name, test_id, exes):
9192
assert facade.run_test(exes.get(name), test_id)[0] is None
9293

9394

95+
def test_cmdline_builder_happy_flow():
96+
arg_string = make_cmdline( ["wine"], "gtest", ["--help"] )
97+
assert arg_string == ["wine", "gtest", "--help"]
98+
99+
100+
def test_cmdline_builder_with_empty_harness():
101+
arg_string = make_cmdline(list(), "boost_test", ["--output_format=XML", "--log_sink=dummy.log"])
102+
assert arg_string == ["boost_test", "--output_format=XML", "--log_sink=dummy.log"]
103+
104+
105+
def test_cmdline_builder_with_empty_args():
106+
arg_string = make_cmdline( ["wine"], "gtest")
107+
assert arg_string == ["wine", "gtest"]
108+
109+
94110
def test_google_failure(exes):
95111
facade = GoogleTestFacade()
96112
failures, _ = facade.run_test(exes.get("gtest"), "FooTest.test_failure")

0 commit comments

Comments
 (0)