Skip to content

Commit

Permalink
feat(junit): record app_path in JUnit reports
Browse files Browse the repository at this point in the history
  • Loading branch information
alekseiapa committed Jan 23, 2025
1 parent 21a9732 commit 672e83c
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 5 deletions.
67 changes: 67 additions & 0 deletions pytest-embedded-idf/tests/test_idf.py
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,73 @@ def test_python_case(dut):
for testcase in junit_report[1:]:
assert testcase.attrib['is_unity_case'] == '1' # Other test cases


def test_app_path_in_junit_multi_dut_app(testdir):
testdir.makepyfile("""
import pytest
def test_app_path_in_junit_multi_dut_app(app, dut):
assert len(app[0].flash_files) == 3
assert app[0].target == 'esp32'
assert len(app[1].flash_files) == 3
assert app[1].target == 'esp32c3'
assert getattr(dut[0], 'serial')
with pytest.raises(AttributeError):
assert getattr(dut[1], 'serial')
""")

testdir.runpytest(
'-s',
'--count',
2,
'--app-path',
f'{os.path.join(testdir.tmpdir, "hello_world_esp32")}|{os.path.join(testdir.tmpdir, "hello_world_esp32c3")}',
'--embedded-services',
'esp,idf|idf',
'--junitxml',
'report.xml',
)

junit_report = ET.parse('report.xml').getroot()[0]
testcases = junit_report.findall('.//testcase')

assert 'app_path' in testcases[0].attrib
assert 'hello_world_esp32' in testcases[0].attrib['app_path']
assert 'hello_world_esp32c3' in testcases[0].attrib['app_path']


def test_app_path_in_junit_single_dut_app(testdir):
testdir.makepyfile("""
import pytest
def test_app_path_in_junit_single_dut_app(app, dut):
assert len(app.flash_files) == 3
assert app.target == 'esp32c3'
with pytest.raises(AttributeError):
assert getattr(dut, 'serial')
""")

testdir.runpytest(
'-s',
'--embedded-services',
'idf',
'--app-path',
os.path.join(testdir.tmpdir, 'hello_world_esp32c3'),
'--junitxml',
'report.xml',
)

junit_report = ET.parse('report.xml').getroot()[0]
testcases = junit_report.findall('.//testcase')

assert 'app_path' in testcases[0].attrib
# path is relative to pytest root dir (testdir.tmpdir)
assert 'hello_world_esp32c3' == testcases[0].attrib['app_path']


def test_esp_bool_parser_returned_values(testdir, copy_mock_esp_idf, monkeypatch): # noqa: ARG001
monkeypatch.setenv('IDF_PATH', str(testdir))
from esp_bool_parser import SOC_HEADERS, SUPPORTED_TARGETS
Expand Down
58 changes: 53 additions & 5 deletions pytest-embedded/pytest_embedded/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import typing as t
import warnings
import xml.dom.minidom
import xml.etree.ElementTree as ET
from collections import Counter
from operator import itemgetter

Expand Down Expand Up @@ -1167,12 +1168,34 @@ def dut(
serial: t.Optional[t.Union['Serial', 'LinuxSerial']],
qemu: t.Optional['Qemu'],
wokwi: t.Optional['WokwiCLI'],
request: FixtureRequest,
) -> t.Union[Dut, t.List[Dut]]:
"""
A device under test (DUT) object that could gather output from various sources and redirect them to the pexpect
process, and run `expect()` via its pexpect process.
"""
return dut_gn(**locals())
dut_instance = dut_gn(
_fixture_classes_and_options=_fixture_classes_and_options,
openocd=openocd,
gdb=gdb,
app=app,
serial=serial,
qemu=qemu,
wokwi=wokwi,
)

if _testcase_app_paths_map_key not in request.config.stash:
request.config.stash[_testcase_app_paths_map_key] = {}

stash = request.config.stash[_testcase_app_paths_map_key]
testsuite_name = dut_instance.test_case_name

if testsuite_name not in stash:
stash[testsuite_name] = set()

stash[testsuite_name].add(dut_instance.app.app_path)

return dut_instance


@pytest.fixture
Expand All @@ -1197,6 +1220,7 @@ def unity_tester(dut: t.Union['IdfDut', t.Tuple['IdfDut']]) -> t.Optional['CaseT
_pytest_embedded_key = pytest.StashKey['PytestEmbedded']()
_session_tempdir_key = pytest.StashKey['session_tempdir']()
_junit_report_path_key = pytest.StashKey[str]()
_testcase_app_paths_map_key = pytest.StashKey[dict]()


def pytest_configure(config: Config) -> None:
Expand Down Expand Up @@ -1273,6 +1297,29 @@ def get_param(item: Function, key: str, default: t.Any = None) -> t.Any:

return item.callspec.params.get(key, default) or default

@staticmethod
def add_app_paths_to_testcases(report_path: str, testcase_app_paths_map: dict, rootdir: str) -> None:
with open(report_path, 'r+') as file:
tree = ET.parse(file)
root = tree.getroot()

for testcase in root.findall('.//testcase'):
pytest_testcase_name = testcase.get('pytest_case_name')
if pytest_testcase_name in testcase_app_paths_map:
# Map the app paths as relative paths and add to the 'app_path' attribute
relative_paths = {
os.path.relpath(path, rootdir) for path in testcase_app_paths_map[pytest_testcase_name]
}
testcase.set('app_path', ':'.join(relative_paths))

# Remove the 'pytest_case_name' attribute used for app_path mapping purposes
if 'pytest_case_name' in testcase.attrib:
testcase.attrib.pop('pytest_case_name')

file.seek(0)
file.truncate()
file.write(escape_illegal_xml_chars(ET.tostring(root, encoding='unicode')))

@pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_collection_modifyitems(self, config: Config, items: t.List[Function]):
# ------ add marker based on target ------
Expand Down Expand Up @@ -1382,10 +1429,11 @@ def pytest_sessionfinish(self, session: Session, exitstatus: int) -> None:
if _stash_junit_report_path:
# before we only modified the junit report generated by the unity test cases
# now we do it again to check the python test cases
with open(_stash_junit_report_path) as fr:
file_str = fr.read()
with open(_stash_junit_report_path, 'w') as fw:
fw.write(escape_illegal_xml_chars(file_str))
self.add_app_paths_to_testcases(
_stash_junit_report_path,
testcase_app_paths_map=session.config.stash.get(_testcase_app_paths_map_key, {}),
rootdir=session.config.rootdir,
)

if self.prettify_junit_report:
_prettify_xml(_stash_junit_report_path)
Expand Down
8 changes: 8 additions & 0 deletions pytest-embedded/pytest_embedded/unity.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,13 @@ def merge(self, junit_files: List[str]):
junit_case_is_fail = junit_case.find('failure') is not None

junit_case.attrib['is_unity_case'] = '0'
junit_case.attrib['pytest_case_name'] = junit_case.attrib.get('name')
if self.unity_test_report_mode == UnityTestReportMode.REPLACE.value:
junit_parent.remove(junit_case)

for case in merging_cases:
case.attrib['is_unity_case'] = '1'
case.attrib['pytest_case_name'] = junit_case.attrib.get('name')
junit_parent.append(case)

junit_parent.attrib['errors'] = self._int_add(
Expand All @@ -324,5 +326,11 @@ def merge(self, junit_files: List[str]):
if int(junit_parent.attrib['failures']) > 0:
self.failed = True

for testcase in self.junit.findall('.//testcase'):
# Add the 'pytest_case_name' attribute for each test case to enable later extraction
# of the app_path from the pytest_case_name to app_path mapping
if 'pytest_case_name' not in testcase.attrib:
testcase.attrib['pytest_case_name'] = testcase.attrib.get('name')

self.junit.write(self.junit_path)
logging.debug(f'Merged junit report dumped to {os.path.realpath(self.junit_path)}')
43 changes: 43 additions & 0 deletions pytest-embedded/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,49 @@ def test_unclosed_file_handler(test_input, dut):
result.assert_outcomes(passed=1024)


def test_app_paths_in_junit_report(testdir):
testdir.makepyfile(r"""
import pytest
import inspect
output = inspect.cleandoc(
'''
TEST(group, test_case)foo.c:100::FAIL:Expected 2 was 1
TEST(group, test_case_2)foo.c:101::FAIL:Expected 1 was 2
TEST(group, test case 3)foo bar.c:102::PASS
TEST(group, test case 4)foo bar.c:103::FAIL:Expected 3 was 4
-------------------
4 Tests 3 Failures 0 Ignored
FAIL
''')
@pytest.mark.parametrize('count', [2], indirect=True)
def test_expect_unity_test_output_multi_dut(dut):
dut_0 = dut[0]
dut_1 = dut[1]
dut_0.write(output)
dut_1.write(output)
dut_0.expect_unity_test_output()
dut_1.expect_unity_test_output()
@pytest.mark.parametrize('count', [2], indirect=True)
def test_expect_unity_test_output_multi_dut_record_1(dut):
dut_1 = dut[1]
dut_1.write(output)
dut_1.expect_unity_test_output()
""")

testdir.runpytest('--junitxml', 'report.xml')

root = ET.parse('report.xml').getroot()
testcases = root.findall('.//testcase')
app_paths = [testcase.get('app_path') for testcase in testcases]

# should be relative to the pytest root folder
assert app_paths == ['.'] * len(testcases)


class TestTargetMarkers:
def test_add_target_as_marker_simple(self, pytester):
pytester.makepyfile("""
Expand Down

0 comments on commit 672e83c

Please sign in to comment.