Skip to content

Commit 33221d7

Browse files
Add catch2 support
- Also drops support for Python 2.7 and 3.5. - Adapt boost tests. PR #60
1 parent 7062934 commit 33221d7

26 files changed

+36543
-38
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,8 @@ jobs:
5353
strategy:
5454
fail-fast: false
5555
matrix:
56-
python: ["2.7", "3.5", "3.6", "3.7", "3.8"]
56+
python: ["3.6", "3.7", "3.8"]
5757
include:
58-
- python: "2.7"
59-
tox_env: "py27-pytestlatest"
60-
- python: "3.5"
61-
tox_env: "py35-pytestlatest"
6258
- python: "3.6"
6359
tox_env: "py36-pytestlatest"
6460
- python: "3.7"

README.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ Use `pytest <https://pypi.python.org/pypi/pytest>`_ runner to discover and execu
66

77
|python| |version| |anaconda| |ci| |coverage| |black|
88

9-
Supports both `Google Test <https://code.google.com/p/googletest>`_ and
10-
`Boost::Test <http://www.boost.org/doc/libs/release/libs/test>`_:
9+
Supports `Google Test <https://code.google.com/p/googletest>`_,
10+
`Boost::Test <http://www.boost.org/doc/libs/release/libs/test>`_,
11+
and `Catch2 <https://github.com/catchorg/Catch2>`_:
1112

1213
.. image:: https://raw.githubusercontent.com/pytest-dev/pytest-cpp/master/images/screenshot.png
1314

@@ -46,7 +47,7 @@ Usage
4647

4748
Once installed, when py.test runs it will search and run tests
4849
found in executable files, detecting if the suites are
49-
Google or Boost tests automatically.
50+
Google, Boost, or Catch2 tests automatically.
5051

5152
Configuration Options
5253
~~~~~~~~~~~~~~~~~~~~~

setup.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package_dir={"": "src"},
1010
entry_points={"pytest11": ["cpp = pytest_cpp.plugin"],},
1111
install_requires=["pytest !=5.4.0, !=5.4.1", "colorama",],
12+
python_requires=">=3.6",
1213
# metadata for upload to PyPI
1314
author="Bruno Oliveira",
1415
author_email="[email protected]",
@@ -23,10 +24,7 @@
2324
"Intended Audience :: Developers",
2425
"License :: OSI Approved :: MIT License",
2526
"Operating System :: OS Independent",
26-
"Programming Language :: Python :: 2",
27-
"Programming Language :: Python :: 2.7",
2827
"Programming Language :: Python :: 3",
29-
"Programming Language :: Python :: 3.5",
3028
"Programming Language :: Python :: 3.6",
3129
"Programming Language :: Python :: 3.7",
3230
"Programming Language :: Python :: 3.8",

src/pytest_cpp/boost.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,6 @@ def read_file(name):
7979
)
8080
return [failure], stdout
8181

82-
if report is not None and (
83-
report.startswith("Boost.Test framework internal error: ")
84-
or report.startswith("Test setup error: ")
85-
):
86-
# boost.test doesn't do XML output on fatal-enough errors.
87-
failure = BoostTestFailure("unknown location", 0, report)
88-
return [failure], stdout
89-
9082
results = self._parse_log(log=log)
9183
shutil.rmtree(temp_dir)
9284

@@ -102,22 +94,23 @@ def _parse_log(self, log):
10294
This is always a XML file, and from this we produce most of the
10395
failures possible when running BoostTest.
10496
"""
105-
# Fatal errors apparently generate invalid xml in the form:
106-
# <FatalError>...</FatalError><TestLog>...</TestLog>
107-
# so we have to manually split it into two xmls if that's the case.
97+
# Boosttest will sometimes generate unparseable XML
98+
# so we surround it with xml tags.
10899
parsed_elements = []
109-
if log.startswith("<FatalError"):
110-
fatal, log = log.split("</FatalError>")
111-
fatal += "</FatalError>" # put it back, removed by split()
112-
fatal_root = ElementTree.fromstring(fatal)
113-
fatal_root.text = "Fatal Error: %s" % fatal_root.text
114-
parsed_elements.append(fatal_root)
100+
log = "<xml>{}</xml>".format(log)
115101

116102
log_root = ElementTree.fromstring(log)
103+
testlog = log_root.find("TestLog")
104+
117105
parsed_elements.extend(log_root.findall("Exception"))
118106
parsed_elements.extend(log_root.findall("Error"))
119107
parsed_elements.extend(log_root.findall("FatalError"))
120108

109+
if testlog is not None:
110+
parsed_elements.extend(testlog.findall("Exception"))
111+
parsed_elements.extend(testlog.findall("Error"))
112+
parsed_elements.extend(testlog.findall("FatalError"))
113+
121114
result = []
122115
for elem in parsed_elements:
123116
filename = elem.attrib["file"]

src/pytest_cpp/catch2.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import os
2+
import subprocess
3+
import tempfile
4+
from xml.etree import ElementTree
5+
6+
import pytest
7+
8+
from pytest_cpp.error import CppTestFailure
9+
10+
11+
class Catch2Facade(object):
12+
"""
13+
Facade for Catch2.
14+
"""
15+
16+
@classmethod
17+
def is_test_suite(cls, executable):
18+
try:
19+
output = subprocess.check_output(
20+
[executable, "--help"],
21+
stderr=subprocess.STDOUT,
22+
universal_newlines=True,
23+
)
24+
except (subprocess.CalledProcessError, OSError):
25+
return False
26+
else:
27+
return "--list-test-names-only" in output
28+
29+
def list_tests(self, executable):
30+
"""
31+
Executes test with "--list-test-names-only" and gets list of tests
32+
parsing output like this:
33+
34+
1: All test cases reside in other .cpp files (empty)
35+
2: Factorial of 0 is 1 (fail)
36+
2: Factorials of 1 and higher are computed (pass)
37+
"""
38+
# This will return an exit code with the number of tests available
39+
try:
40+
output = subprocess.check_output(
41+
[executable, "--list-test-names-only"],
42+
stderr=subprocess.STDOUT,
43+
universal_newlines=True,
44+
)
45+
except subprocess.CalledProcessError as e:
46+
output = e.output
47+
48+
result = output.strip().split("\n")
49+
50+
return result
51+
52+
def run_test(self, executable, test_id="", test_args=(), harness=None):
53+
harness = harness or []
54+
xml_filename = self._get_temp_xml_filename()
55+
args = harness + [
56+
executable,
57+
test_id,
58+
"--success",
59+
"--reporter=xml",
60+
"--out %s" % xml_filename,
61+
]
62+
args.extend(test_args)
63+
64+
try:
65+
output = subprocess.check_output(
66+
args, stderr=subprocess.STDOUT, universal_newlines=True
67+
)
68+
except subprocess.CalledProcessError as e:
69+
output = e.output
70+
if e.returncode != 1:
71+
msg = (
72+
"Internal Error: calling {executable} "
73+
"for test {test_id} failed (returncode={returncode}):\n"
74+
"{output}"
75+
)
76+
failure = Catch2Failure(
77+
executable,
78+
0,
79+
msg.format(
80+
executable=executable,
81+
test_id=test_id,
82+
output=e.output,
83+
returncode=e.returncode,
84+
),
85+
)
86+
87+
return [failure], output
88+
89+
results = self._parse_xml(xml_filename)
90+
os.remove(xml_filename)
91+
for (executed_test_id, failures, skipped) in results:
92+
if executed_test_id == test_id:
93+
if failures:
94+
return (
95+
[
96+
Catch2Failure(filename, linenum, lines)
97+
for (filename, linenum, lines) in failures
98+
],
99+
output,
100+
)
101+
elif skipped:
102+
pytest.skip()
103+
else:
104+
return None, output
105+
106+
msg = "Internal Error: could not find test " "{test_id} in results:\n{results}"
107+
108+
results_list = "\n".join("\n".join(x) for (n, x, f) in results)
109+
failure = Catch2Failure(msg.format(test_id=test_id, results=results_list))
110+
return [failure], output
111+
112+
def _get_temp_xml_filename(self):
113+
return tempfile.mktemp()
114+
115+
def _parse_xml(self, xml_filename):
116+
root = ElementTree.parse(xml_filename)
117+
result = []
118+
for test_suite in root.findall("Group"):
119+
test_suite_name = test_suite.attrib["name"]
120+
for test_case in test_suite.findall("TestCase"):
121+
test_name = test_case.attrib["name"]
122+
test_result = test_case.find("OverallResult")
123+
failures = []
124+
if test_result.attrib["success"] == "false":
125+
test_checks = test_case.findall("Expression")
126+
for check in test_checks:
127+
file_name = check.attrib["filename"]
128+
line_num = check.attrib["line"]
129+
if check.attrib["success"] == "false":
130+
expected = check.find("Original").text
131+
actual = check.find("Expanded").text
132+
fail_msg = "Expected: {expected}\nActual: {actual}".format(
133+
expected=expected, actual=actual
134+
)
135+
failures.append((file_name, line_num, fail_msg,))
136+
skipped = False # TODO: skipped tests don't appear in the results
137+
result.append((test_name, failures, skipped))
138+
139+
return result
140+
141+
142+
class Catch2Failure(CppTestFailure):
143+
def __init__(self, filename, linenum, lines):
144+
self.lines = lines.splitlines()
145+
self.filename = filename
146+
self.linenum = int(linenum)
147+
148+
def get_lines(self):
149+
m = ("red", "bold")
150+
return [(x, m) for x in self.lines]
151+
152+
def get_file_reference(self):
153+
return self.filename, self.linenum

src/pytest_cpp/plugin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
from pytest_cpp.boost import BoostTestFacade
88
from pytest_cpp.error import CppFailureRepr, CppFailureError
99
from pytest_cpp.google import GoogleTestFacade
10+
from pytest_cpp.catch2 import Catch2Facade
1011

11-
FACADES = [GoogleTestFacade, BoostTestFacade]
12+
FACADES = [GoogleTestFacade, BoostTestFacade, Catch2Facade]
1213
DEFAULT_MASKS = ("test_*", "*_test")
1314

1415
_ARGUMENTS = "cpp_arguments"

tests/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
*
22

3+
!acceptance/
34
!.gitignore
45
!*.h
56
!*.cpp
@@ -8,3 +9,4 @@
89
!*.xml
910
!README*
1011
!SCons*
12+
!catch.hpp

tests/SConstruct

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ if 'CXX' in os.environ:
1919

2020
env = Environment(**kwargs)
2121
genv = env.Clone(LIBS=['gtest'] + LIBS)
22+
c2env = env.Clone(CPPPATH=['.'])
2223

23-
Export('env genv')
24+
Export('env genv c2env')
2425

2526
genv.Program('gtest.cpp')
2627
genv.Program('gtest_args.cpp')
@@ -38,5 +39,9 @@ boost_files = [
3839
for filename in boost_files:
3940
env.Program(filename)
4041

42+
c2env.Program('catch2_success.cpp')
43+
c2env.Program('catch2_failure.cpp')
44+
4145
SConscript('acceptance/googletest-samples/SConscript')
4246
SConscript('acceptance/boosttest-samples/SConscript')
47+
SConscript('acceptance/catch2-samples/SConscript')
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// 000-CatchMain.cpp
2+
3+
// It is generally recommended to have a single file provide the main
4+
// of a testing binary, and other test files to link against it.
5+
6+
// Let Catch provide main():
7+
#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file
8+
#include "catch.hpp"
9+
10+
// That's it
11+
12+
// Compile implementation of Catch for use with files that do contain tests:
13+
// - g++ -std=c++11 -Wall -I$(CATCH_SINGLE_INCLUDE) -c 000-CatchMain.cpp
14+
// - cl -EHsc -I%CATCH_SINGLE_INCLUDE% -c 000-CatchMain.cpp
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// 010-TestCase.cpp
2+
3+
#include "catch.hpp"
4+
5+
int Factorial( int number ) {
6+
return number <= 1 ? number : Factorial( number - 1 ) * number; // fail
7+
// return number <= 1 ? 1 : Factorial( number - 1 ) * number; // pass
8+
}
9+
10+
TEST_CASE( "Factorial of 0 is 1 (fail)", "[single-file]" ) {
11+
REQUIRE( Factorial(0) == 1 );
12+
}
13+
14+
TEST_CASE( "Factorials of 1 and higher are computed (pass)", "[single-file]" ) {
15+
REQUIRE( Factorial(1) == 1 );
16+
REQUIRE( Factorial(2) == 2 );
17+
REQUIRE( Factorial(3) == 6 );
18+
REQUIRE( Factorial(10) == 3628800 );
19+
}
20+
21+
// Compile & run:
22+
// - g++ -std=c++11 -Wall -I$(CATCH_SINGLE_INCLUDE) -o 010-TestCase 010-TestCase.cpp && 010-TestCase --success
23+
// - cl -EHsc -I%CATCH_SINGLE_INCLUDE% 010-TestCase.cpp && 010-TestCase --success
24+
25+
// Expected compact output (all assertions):
26+
//
27+
// prompt> 010-TestCase --reporter compact --success
28+
// 010-TestCase.cpp:14: failed: Factorial(0) == 1 for: 0 == 1
29+
// 010-TestCase.cpp:18: passed: Factorial(1) == 1 for: 1 == 1
30+
// 010-TestCase.cpp:19: passed: Factorial(2) == 2 for: 2 == 2
31+
// 010-TestCase.cpp:20: passed: Factorial(3) == 6 for: 6 == 6
32+
// 010-TestCase.cpp:21: passed: Factorial(10) == 3628800 for: 3628800 (0x375f00) == 3628800 (0x375f00)
33+
// Failed 1 test case, failed 1 assertion.

0 commit comments

Comments
 (0)