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

Reply via email to