Skip to content

Commit 65618d0

Browse files
grass.gunittest: Inherit more from unittest classes (OSGeo#6256)
Python is one of the few languages that allows multiple inheritance. That means that even we override two classes, one of which is is the base class of another, and we want the second class to use both our overridden class and overridden base class: it is possible. That is the case in our gunittest code. Python uses a MRO (method resolution order) to know from which class call the function in this case. This order ignores duplicate classes if it happens. Python will handle at runtime an error when instantiating the class if there is a cycle in the base classes that can’t exist (this isn’t our case here). The `super()` call is where it makes the sense the most in Python, in multiple inheritance situations. This PR is a follow up of some of my previous PRs (like OSGeo#6200) that synced some gunittest code from unittest code so that the contents of the functions are exactly the same or similar. Starting from there, classes, like the TextTestResult, were made subclasses of our overridden base class, AND unittest’s class. Then, replace all function implementations that are exactly the same as the unittest classe with a super() call. If there’s nothing else that remains apart from a super() call, it is useless to keep it, so remove the function. With these removed, assignments to attributes that are used only by the unittest class can be simplified out, as done by the base class. I simplified the GrassTestRunner class, in particular the only method run(), to use a call to the base classes instead of merging our code into the upstream code. That means our timing call is done before and after the base class call (that already does some timing calls, especially since 3.12 added durations natively. This means that our whole test call might be a little less precise (as it includes some time taken by the unittest call), but we don’t have to keep the implementation updated for each Python version. Finished off with some typing annotations to make type checkers understand that our overridden classes are accepted as inputs. I also made a base implementation of the setTimes method, and called it with super() where appropriate in our classes (instead of defining it multiple times). When developing, I stepped through function calls in the debugger to make sure the correct method resolution order was used, and to see which base implementations of functions were called and in which order. * gunittest: Copy over _SubTest to allow creating derived classes * gunittest: Apply ruff fixes to _SubTest * gunittest: Prepare TextTestResult for multiple inheritance by syncing its implementation with unittest.TextTestResult * gunittest: Make TextTestResult derive from grass.gunittest.result.TestResult and unittest.TextTestResult using multiple inheritance * gunittest: Remove TextTestResult methods that have no changes with unittest's implementation * gunittest: Remove setting TextTestResult attributes already handled with unittest's implementation * grass.gunittest: Remove addSubTest() and _SubTest() class, as not needed to override * grass.gunittest: Remove setting unused self._newline attribute * grass.gunittest: Reorder MultiTestResult arguments passed to super call * grass.gunittest: Add type annotations to TestResult, TextTestResult, KeyValueTestResult, and MultiTestResult classes * grass.gunittest: Remove unnecessary pass since docstring is present * grass.gunittest: Make GrassTestRunner a subclass of unittest.TextTestRunner * grass.gunittest: Adjust typing annotation for TextTestResult's stream argument * grass.gunittest: Fix ruff issue RUF021 * grass.gunittest: Simplify GrassTestRunner to count test duration after calling the base run() implementation * grass.gunittest: Implement setTimes in TestResult class * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 2f08636 commit 65618d0

File tree

2 files changed

+113
-176
lines changed

2 files changed

+113
-176
lines changed

python/grass/gunittest/result.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
"""Test result object"""
22

3+
from __future__ import annotations
4+
35
import unittest
6+
from typing import TYPE_CHECKING, TextIO
7+
8+
if TYPE_CHECKING:
9+
import grass.gunittest.case
410

511

612
class TestResult(unittest.TestResult):
@@ -15,22 +21,47 @@ class TestResult(unittest.TestResult):
1521
formatted traceback of the error that occurred.
1622
"""
1723

24+
start_time: float | None = None
25+
end_time: float | None = None
26+
time_taken: float | None = None
27+
1828
# descriptions and verbosity unused
1929
# included for compatibility with unittest's TestResult
2030
# where are also unused, so perhaps we can remove them
2131
# stream set to None and not included in interface, it would not make sense
22-
def __init__(self, stream=None, descriptions=None, verbosity=None):
32+
def __init__(
33+
self,
34+
stream: TextIO | None = None,
35+
descriptions: bool | None = None,
36+
verbosity: int | None = None,
37+
) -> None:
2338
super().__init__(stream=stream, descriptions=descriptions, verbosity=verbosity)
24-
self.successes = []
39+
self.successes: list[
40+
grass.gunittest.case.TestCase | unittest.case.TestCase
41+
] = []
42+
self.start_time = None
43+
self.end_time = None
44+
self.time_taken = None
2545

26-
def addSuccess(self, test):
46+
def addSuccess(
47+
self, test: grass.gunittest.case.TestCase | unittest.case.TestCase
48+
) -> None:
2749
super().addSuccess(test)
2850
self.successes.append(test)
2951

3052
# TODO: better would be to pass start at the beginning
3153
# alternative is to leave counting time on class
32-
# TODO: document: we expect all grass classes to have setTimes
3354
# TODO: alternatively, be more forgiving for non-unittest methods
34-
def setTimes(self, start_time, end_time, time_taken):
35-
pass
36-
# TODO: implement this
55+
def setTimes(self, start_time: float, end_time: float, time_taken: float) -> None:
56+
"""
57+
Store the start time, end time and time taken for a test.
58+
59+
We expect all grass classes to have setTimes.
60+
61+
:param start_time: The start time of the test, as returned by time.time()
62+
:param end_time: The end time of the test, as returned by time.time()
63+
:param time_taken: The time taken for the test, usually end_time-start_time
64+
"""
65+
self.start_time = start_time
66+
self.end_time = end_time
67+
self.time_taken = time_taken

0 commit comments

Comments
 (0)