From b720009f3e2f5dc1f8fd88afc57e671ee1c37cde Mon Sep 17 00:00:00 2001 From: Salomon Popp Date: Fri, 3 Apr 2026 09:20:30 +0200 Subject: [PATCH] fix: propagate exit code (#108) * fix(pytest): propagate exit code * feat: derive exit code for unittest --- neotest.py | 2 +- neotest_python/__init__.py | 12 ++++++------ neotest_python/base.py | 4 ++-- neotest_python/django_unittest.py | 7 ++++--- neotest_python/pytest_.py | 8 ++++---- neotest_python/unittest.py | 8 ++++---- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/neotest.py b/neotest.py index 6aa3cb0..911c6ce 100644 --- a/neotest.py +++ b/neotest.py @@ -17,4 +17,4 @@ with add_to_path(): from neotest_python import main if __name__ == "__main__": - main(sys.argv[1:]) + sys.exit(main(sys.argv[1:])) diff --git a/neotest_python/__init__.py b/neotest_python/__init__.py index 2719f93..9555326 100644 --- a/neotest_python/__init__.py +++ b/neotest_python/__init__.py @@ -50,20 +50,18 @@ parser.add_argument( parser.add_argument("args", nargs="*") -def main(argv: List[str]): +def main(argv: List[str]) -> int: if "--pytest-collect" in argv: argv.remove("--pytest-collect") from .pytest_ import collect - collect(argv) - return + return collect(argv) if "--pytest-extract-test-name-template" in argv: argv.remove("--pytest-extract-test-name-template") from .pytest_ import extract_test_name_template - extract_test_name_template(argv) - return + return extract_test_name_template(argv) args = parser.parse_args(argv) adapter = get_adapter(TestRunner(args.runner), args.emit_parameterized_ids) @@ -74,7 +72,9 @@ def main(argv: List[str]): stream_file.write(json.dumps({"id": pos_id, "result": result}) + "\n") stream_file.flush() - results = adapter.run(args.args, stream) + results, exit_code = adapter.run(args.args, stream) with open(args.results_file, "w") as results_file: json.dump(results, results_file) + + return exit_code diff --git a/neotest_python/base.py b/neotest_python/base.py index b0b7df3..dc719d1 100644 --- a/neotest_python/base.py +++ b/neotest_python/base.py @@ -1,6 +1,6 @@ import abc from enum import Enum -from typing import TYPE_CHECKING, Callable, Dict, List, Optional +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple class NeotestResultStatus(str, Enum): @@ -43,6 +43,6 @@ class NeotestAdapter(abc.ABC): } @abc.abstractmethod - def run(self, args: List[str], stream: Callable): + def run(self, args: List[str], stream: Callable) -> Tuple[Dict, int]: del args, stream raise NotImplementedError diff --git a/neotest_python/django_unittest.py b/neotest_python/django_unittest.py index 63847c4..714364b 100644 --- a/neotest_python/django_unittest.py +++ b/neotest_python/django_unittest.py @@ -55,7 +55,7 @@ class DjangoNeotestAdapter(CaseUtilsMixin, NeotestAdapter): relative_dotted = relative_stem.replace(os.sep, ".") return [*args, ".".join([relative_dotted, *child_ids])] - def run(self, args: List[str], _) -> Dict: + def run(self, args: List[str], _) -> Tuple[Dict, int]: errs: Dict[str, Tuple[Exception, Any, TracebackType]] = {} results = {} @@ -146,5 +146,6 @@ class DjangoNeotestAdapter(CaseUtilsMixin, NeotestAdapter): runner = DjangoUnittestRunner( **vars(parser.parse_args(argv[1:-1])) # parse plugin config args ) - runner.run_tests(test_labels=[argv[-1]]) # pass test label - return results + failures = runner.run_tests(test_labels=[argv[-1]]) # pass test label + exit_code = 0 if failures == 0 else 1 + return results, exit_code diff --git a/neotest_python/pytest_.py b/neotest_python/pytest_.py index ad62f5f..a010b6e 100644 --- a/neotest_python/pytest_.py +++ b/neotest_python/pytest_.py @@ -4,7 +4,7 @@ from typing import Callable, Dict, List, Optional, Union from . import params_getter from io import StringIO from pathlib import Path -from typing import Callable, Dict, Generator, List, Optional, Union +from typing import Callable, Dict, Generator, List, Optional, Tuple, Union import pytest from _pytest._code.code import ExceptionRepr @@ -24,18 +24,18 @@ class PytestNeotestAdapter(NeotestAdapter): self, args: List[str], stream: Callable[[str, NeotestResult], None], - ) -> Dict[str, NeotestResult]: + ) -> Tuple[Dict[str, NeotestResult], int]: result_collector = NeotestResultCollector( self, stream=stream, emit_parameterized_ids=self.emit_parameterized_ids ) - pytest.main( + exit_code = pytest.main( args=args, plugins=[ result_collector, NeotestDebugpyPlugin(), ], ) - return result_collector.results + return result_collector.results, int(exit_code) class NeotestResultCollector: diff --git a/neotest_python/unittest.py b/neotest_python/unittest.py index 0f41a26..6d23d48 100644 --- a/neotest_python/unittest.py +++ b/neotest_python/unittest.py @@ -45,7 +45,7 @@ class UnittestNeotestAdapter(NeotestAdapter): return [*args, ".".join([relative_dotted, *child_ids])] # TODO: Stream results - def run(self, args: List[str], _) -> Dict: + def run(self, args: List[str], _) -> Tuple[Dict, int]: results = {} errs: Dict[str, Tuple[Exception, Any, TracebackType]] = {} @@ -96,11 +96,11 @@ class UnittestNeotestAdapter(NeotestAdapter): # Prepend an executable name which is just used in output argv = ["neotest-python"] + self.convert_args(args[-1], args[:-1]) - unittest.main( + program = unittest.main( module=None, argv=argv, testRunner=NeotestUnittestRunner(resultclass=NeotestTextTestResult), exit=False, ) - - return results + exit_code = 0 if program.result.wasSuccessful() else 1 + return results, exit_code