This is an automated email from the ASF dual-hosted git repository. gurwls223 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push: new 5cc8a7935769 [SPARK-52739][PYTHON][TEST] Apply a patch to unittest.main to report errors in setUpClass properly 5cc8a7935769 is described below commit 5cc8a7935769cc00156448a00ef4ee73f043ba4f Author: Takuya Ueshin <ues...@databricks.com> AuthorDate: Thu Jul 10 11:07:10 2025 +0900 [SPARK-52739][PYTHON][TEST] Apply a patch to unittest.main to report errors in setUpClass properly ### What changes were proposed in this pull request? Applies a patch to `unittest.main` to report errors in `setUpClass` properly. The patch will be applied when `pyspark.testing` module is imported, so most of the tests will successfully fail when `setUpClass` fails. ### Why are the changes needed? In Python 3.12+, `unittest` returns exit code `5` when no tests are run and none are skipped. However, if a test class’s `setUpClass()` method raises an exception, no tests are executed, and unittest may incorrectly return exit code `5`. ```py import unittest class BugTest(unittest.TestCase): classmethod def setUpClass(cls): raise ValueError("Simulated setup failure") def test_example(self): self.assertTrue(True) ``` ```sh python -m unittest bug_test.py echo $? ``` This will return the exit code `5`, instead of `1` that means failures. As a result, if `setUpClass` fails for some reason, e.g., fails to launch Spark in `ReusedPySparkTestCase`, etc., the test will be considered as 'OK'. The issue was already reported to the Python community: https://github.com/python/cpython/issues/136442 In the meantime, we should apply the patch to avoid the misjudgment. ### Does this PR introduce _any_ user-facing change? The unit test will expectedly fail when `setUpClass` fails. ### How was this patch tested? Added the related tests. ### Was this patch authored or co-authored using generative AI tooling? No. Closes #51429 from ueshin/issues/SPARK-52739/set_up_class. Authored-by: Takuya Ueshin <ues...@databricks.com> Signed-off-by: Hyukjin Kwon <gurwls...@apache.org> --- dev/sparktestsupport/modules.py | 8 +++++ dev/tox.ini | 3 +- python/pyspark/testing/__init__.py | 26 ++++++++++++++ python/pyspark/testing/tests/__init__.py | 16 +++++++++ python/pyspark/testing/tests/test_fail.py | 37 +++++++++++++++++++ .../testing/tests/test_fail_in_set_up_class.py | 42 ++++++++++++++++++++++ python/pyspark/testing/tests/test_no_tests.py | 36 +++++++++++++++++++ python/pyspark/testing/tests/test_pass_all.py | 37 +++++++++++++++++++ python/pyspark/testing/tests/test_skip_all.py | 38 ++++++++++++++++++++ python/pyspark/testing/tests/test_skip_class.py | 42 ++++++++++++++++++++++ .../testing/tests/test_skip_set_up_class.py | 42 ++++++++++++++++++++++ 11 files changed, 326 insertions(+), 1 deletion(-) diff --git a/dev/sparktestsupport/modules.py b/dev/sparktestsupport/modules.py index 4c748aec6427..4b2fb56b5f98 100644 --- a/dev/sparktestsupport/modules.py +++ b/dev/sparktestsupport/modules.py @@ -587,6 +587,14 @@ pyspark_testing = Module( # doctests "pyspark.testing.utils", "pyspark.testing.pandasutils", + # unittests + "pyspark.testing.tests.test_fail", + "pyspark.testing.tests.test_fail_in_set_up_class", + "pyspark.testing.tests.test_no_tests", + "pyspark.testing.tests.test_pass_all", + "pyspark.testing.tests.test_skip_all", + "pyspark.testing.tests.test_skip_class", + "pyspark.testing.tests.test_skip_set_up_class", ], ) diff --git a/dev/tox.ini b/dev/tox.ini index 05a6b16a03bd..8a8e03ec9be3 100644 --- a/dev/tox.ini +++ b/dev/tox.ini @@ -43,7 +43,8 @@ per-file-ignores = python/pyspark/sql/tests/*.py: F403, python/pyspark/streaming/tests/*.py: F403, python/pyspark/tests/*.py: F403, - python/pyspark/testing/*: F401 + python/pyspark/testing/*.py: F401, + python/pyspark/testing/tests/*.py: F403 exclude = */target/*, docs/.local_ruby_bundle/, diff --git a/python/pyspark/testing/__init__.py b/python/pyspark/testing/__init__.py index 63bea27a32db..27bcd2a1a07b 100644 --- a/python/pyspark/testing/__init__.py +++ b/python/pyspark/testing/__init__.py @@ -14,7 +14,33 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import sys import typing +import unittest + + +_unittest_main = None + +if sys.version_info >= (3, 12) and _unittest_main is None: + _unittest_main = unittest.main + + def unittest_main(*args, **kwargs): + exit = kwargs.pop("exit", True) + kwargs["exit"] = False + res = _unittest_main(*args, **kwargs) + + if exit: + if not res.result.wasSuccessful(): + sys.exit(1) + elif res.result.testsRun == 0 and len(res.result.skipped) == 0: + sys.exit(5) + else: + sys.exit(0) + + return res + + unittest.main = unittest_main + from pyspark.testing.utils import assertDataFrameEqual, assertSchemaEqual diff --git a/python/pyspark/testing/tests/__init__.py b/python/pyspark/testing/tests/__init__.py new file mode 100644 index 000000000000..12bdf0d0175b --- /dev/null +++ b/python/pyspark/testing/tests/__init__.py @@ -0,0 +1,16 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# diff --git a/python/pyspark/testing/tests/test_fail.py b/python/pyspark/testing/tests/test_fail.py new file mode 100644 index 000000000000..d525cae8288f --- /dev/null +++ b/python/pyspark/testing/tests/test_fail.py @@ -0,0 +1,37 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import unittest + + +class FailTests(unittest.TestCase): + def test_something(self): + self.assertEqual(True, False) + + +if __name__ == "__main__": + from pyspark.testing.tests.test_fail import * # noqa: F401 + + try: + import xmlrunner + + testRunner = xmlrunner.XMLTestRunner(output="target/test-reports", verbosity=2) + except ImportError: + testRunner = None + try: + unittest.main(testRunner=testRunner, verbosity=2) + except SystemExit as e: + assert e.code == 1, f"status code: {e.code}" diff --git a/python/pyspark/testing/tests/test_fail_in_set_up_class.py b/python/pyspark/testing/tests/test_fail_in_set_up_class.py new file mode 100644 index 000000000000..e61f6db28825 --- /dev/null +++ b/python/pyspark/testing/tests/test_fail_in_set_up_class.py @@ -0,0 +1,42 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import unittest + + +class FailInSetUpClassTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + raise Exception("error") + + def test_something(self): + self.assertEqual(True, True) + + +if __name__ == "__main__": + from pyspark.testing.tests.test_fail_in_set_up_class import * # noqa: F401 + + try: + import xmlrunner + + testRunner = xmlrunner.XMLTestRunner(output="target/test-reports", verbosity=2) + except ImportError: + testRunner = None + try: + unittest.main(testRunner=testRunner, verbosity=2) + except SystemExit as e: + assert e.code == 1, f"status code: {e.code}" diff --git a/python/pyspark/testing/tests/test_no_tests.py b/python/pyspark/testing/tests/test_no_tests.py new file mode 100644 index 000000000000..ed16fb16f019 --- /dev/null +++ b/python/pyspark/testing/tests/test_no_tests.py @@ -0,0 +1,36 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import sys +import unittest + + +if __name__ == "__main__": + from pyspark.testing.tests.test_no_tests import * # noqa: F401 + + try: + import xmlrunner + + testRunner = xmlrunner.XMLTestRunner(output="target/test-reports", verbosity=2) + except ImportError: + testRunner = None + try: + unittest.main(testRunner=testRunner, verbosity=2) + except SystemExit as e: + if sys.version_info >= (3, 12): + assert e.code == 5, f"status code: {e.code}" + else: + assert e.code == 0, f"status code: {e.code}" diff --git a/python/pyspark/testing/tests/test_pass_all.py b/python/pyspark/testing/tests/test_pass_all.py new file mode 100644 index 000000000000..5a1b090def36 --- /dev/null +++ b/python/pyspark/testing/tests/test_pass_all.py @@ -0,0 +1,37 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import unittest + + +class PassAllTests(unittest.TestCase): + def test_something(self): + self.assertEqual(True, True) + + +if __name__ == "__main__": + from pyspark.testing.tests.test_pass_all import * # noqa: F401 + + try: + import xmlrunner + + testRunner = xmlrunner.XMLTestRunner(output="target/test-reports", verbosity=2) + except ImportError: + testRunner = None + try: + unittest.main(testRunner=testRunner, verbosity=2) + except SystemExit as e: + assert e.code == 0, f"status code: {e.code}" diff --git a/python/pyspark/testing/tests/test_skip_all.py b/python/pyspark/testing/tests/test_skip_all.py new file mode 100644 index 000000000000..ae229f4d7c4a --- /dev/null +++ b/python/pyspark/testing/tests/test_skip_all.py @@ -0,0 +1,38 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import unittest + + +class SkipAllTests(unittest.TestCase): + @unittest.skip + def test_something(self): + self.assertEqual(True, False) + + +if __name__ == "__main__": + from pyspark.testing.tests.test_skip_all import * # noqa: F401 + + try: + import xmlrunner + + testRunner = xmlrunner.XMLTestRunner(output="target/test-reports", verbosity=2) + except ImportError: + testRunner = None + try: + unittest.main(testRunner=testRunner, verbosity=2) + except SystemExit as e: + assert e.code == 0, f"status code: {e.code}" diff --git a/python/pyspark/testing/tests/test_skip_class.py b/python/pyspark/testing/tests/test_skip_class.py new file mode 100644 index 000000000000..1d7febb46d15 --- /dev/null +++ b/python/pyspark/testing/tests/test_skip_class.py @@ -0,0 +1,42 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import sys +import unittest + + +@unittest.skip +class SkipClassTests(unittest.TestCase): + def test_something(self): + self.assertEqual(True, False) + + +if __name__ == "__main__": + from pyspark.testing.tests.test_skip_class import * # noqa: F401 + + try: + import xmlrunner + + testRunner = xmlrunner.XMLTestRunner(output="target/test-reports", verbosity=2) + except ImportError: + testRunner = None + try: + unittest.main(testRunner=testRunner, verbosity=2) + except SystemExit as e: + if sys.version_info >= (3, 12): + assert e.code == 5, f"status code: {e.code}" + else: + assert e.code == 0, f"status code: {e.code}" diff --git a/python/pyspark/testing/tests/test_skip_set_up_class.py b/python/pyspark/testing/tests/test_skip_set_up_class.py new file mode 100644 index 000000000000..7c7398565650 --- /dev/null +++ b/python/pyspark/testing/tests/test_skip_set_up_class.py @@ -0,0 +1,42 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import unittest + + +class SkipSetUpClassTests(unittest.TestCase): + @classmethod + @unittest.skip + def setUpClass(cls): + super().setUpClass() + + def test_something(self): + self.assertEqual(True, False) + + +if __name__ == "__main__": + from pyspark.testing.tests.test_skip_set_up_class import * # noqa: F401 + + try: + import xmlrunner + + testRunner = xmlrunner.XMLTestRunner(output="target/test-reports", verbosity=2) + except ImportError: + testRunner = None + try: + unittest.main(testRunner=testRunner, verbosity=2) + except SystemExit as e: + assert e.code == 0, f"status code: {e.code}" --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@spark.apache.org For additional commands, e-mail: commits-h...@spark.apache.org