diff --git a/auto/stylize_as_junit.py b/auto/stylize_as_junit.py index 3a574c13..261a57ae 100644 --- a/auto/stylize_as_junit.py +++ b/auto/stylize_as_junit.py @@ -40,9 +40,7 @@ def run(self): raise Exception("Empty test result file: %s" % result_file) # define an expression for your file reference - entry_one = Combine( - oneOf(list(alphas)) + ':/' + - Word(alphanums + '_-./')) + entry_one = Regex(r'[A-Za-z]:[\\/][^:\r\n]+') entry_two = Word(printables + ' ', excludeChars=':') entry = entry_one | entry_two diff --git a/docs/UnityChangeLog.md b/docs/UnityChangeLog.md index 6e73e27c..071b9378 100644 --- a/docs/UnityChangeLog.md +++ b/docs/UnityChangeLog.md @@ -17,6 +17,7 @@ Prior to 2008, the project was an internal project and not released to the publi Significant Bugfixes: + - Correct Windows drive paths in the Python JUnit converter (#628) @nailongzhanshen666 - Default `UNITY_INCLUDE_EXEC_TIME` macros compile as ISO C99, are statement-safe, and accept `-Wsign-conversion` (#838) - `unity_test_summary.rb` honors its own default result directory and root path again. @youdie006 diff --git a/test/test_stylize_as_junit.py b/test/test_stylize_as_junit.py new file mode 100644 index 00000000..02cdd2f4 --- /dev/null +++ b/test/test_stylize_as_junit.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Unity Project +# SPDX-License-Identifier: MIT + +"""Regression tests for the Unity-to-JUnit converter.""" + +import importlib.util +from pathlib import Path +import tempfile +import unittest +import xml.etree.ElementTree as ET + + +SCRIPT = Path(__file__).resolve().parents[1] / 'auto' / 'stylize_as_junit.py' +SPEC = importlib.util.spec_from_file_location('stylize_as_junit', SCRIPT) +CONVERTER = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(CONVERTER) + + +class WindowsPathsTest(unittest.TestCase): + def test_paths_preserve_failure_details(self): + paths = [ + r'C:\Path\To\My File\test_example.c', + 'C:/Path/To/My File/test_example.c', + 'D:/path/test_example.c', + '/tmp/test_example.c', + 'test_example.c', + ] + for path in paths: + with self.subTest(path=path), tempfile.TemporaryDirectory() as tmp: + result = Path(tmp) / 'example.testfail' + output = Path(tmp) / 'result.xml' + result.write_text(f'{path}:24:test_example:FAIL:expected 2 was 1\n') + summary = CONVERTER.UnityTestSummary() + summary.set_targets([str(result)]) + summary.set_output(str(output)) + summary.run() + suite = ET.parse(output).getroot() + case = suite.find('.//testcase') + self.assertEqual(case.attrib['name'], 'test_example') + self.assertEqual(case.attrib['classname'], 'test_example') + failure = case.find('failure') + self.assertIsNotNone(failure) + self.assertEqual(failure.attrib['message'], 'expected 2 was 1') + self.assertIn('[File]=' + path + ', [Line]=24', failure.text) + + def test_windows_pass_and_ignore(self): + with tempfile.TemporaryDirectory() as tmp: + result = Path(tmp) / 'example.testpass' + output = Path(tmp) / 'result.xml' + result.write_text( + 'C:/My Tests/test_example.c:12:test_pass:PASS\n' + 'C:\\My Tests\\test_example.c:13:test_ignore:IGNORE:not ready\n' + ) + summary = CONVERTER.UnityTestSummary() + summary.set_targets([str(result)]) + summary.set_output(str(output)) + summary.run() + cases = ET.parse(output).getroot().findall('.//testcase') + self.assertEqual([case.attrib['name'] for case in cases], + ['test_pass', 'test_ignore']) + self.assertTrue(all(case.attrib['classname'] == 'test_example' + for case in cases)) + self.assertIsNone(cases[0].find('failure')) + self.assertEqual(cases[1].find('skipped').attrib['message'], + 'not ready') + + +if __name__ == '__main__': + unittest.main()