teddecor.UnitTest.Testing

Testing

This module contains the base class and decorator for running tests. In a sense, this module is the brains of teddecor's unit testing.

  1"""Testing
  2
  3This module contains the base class and decorator for running tests.
  4In a sense, this module is the brains of teddecor's unit testing.
  5"""
  6from __future__ import annotations
  7
  8from typing import Callable, Pattern
  9
 10from .Results import TestResult, ClassResult, ResultType
 11from ..Util import slash
 12from ..TED.markup import TED
 13
 14__all__ = ["test", "Test", "run", "TestResult", "wrap"]
 15
 16
 17def run(test: Callable, display: bool = True) -> TestResult:
 18    """Runs a single test case, function decorated with `@test` and constructs it's results.
 19
 20    Args:
 21        test (Callable): @test function to run
 22
 23    Returns:
 24        dict: Formated results from running the test
 25
 26    Raises:
 27        TypeError: When the callable test is not decorated with `@test`
 28    """
 29
 30    if test.__name__ == "test_wrapper":
 31        _result = TestResult(test())
 32        if display:
 33            _result.write()
 34        return _result
 35    else:
 36        raise TypeError("Test function must have @test decorator")
 37
 38
 39def wrap(func: Callable, *args, **kwargs) -> Callable:
 40    """Used to return a lambda that runs the function with the given args.
 41    This is so that the function can be run later with provided parameters
 42
 43    Args:
 44        func (Callable): Function to run
 45
 46    Returns:
 47        Callable: Lambda of the funciton to be run later
 48    """
 49    return lambda: func(*args, **kwargs)
 50
 51
 52def __getTracback(error: Exception) -> list:
 53    """Generate a fromatted traceback from an error.
 54
 55    Args:
 56        error (Exception): Raised exception to extract the traceback from
 57
 58    Returns:
 59        list: The formatted traceback
 60    """
 61    import traceback
 62
 63    stack = []
 64    for frame in traceback.extract_tb(error.__traceback__):
 65        if "test_wrapper" not in frame.name:
 66            stack.append(
 67                f"\[[@F magenta ~{frame.filename}]{TED.encode(frame.filename.split(slash())[-1])}[~ @F]:[@F yellow]{frame.lineno}[@F]] {TED.encode(frame.name)}"
 68            )
 69
 70    if str(error) == "":
 71        if isinstance(error, AssertionError):
 72            message = "Assertion Failed"
 73        else:
 74            message = f"Unkown exception <{error.__class__.__name__}>"
 75    else:
 76        message = str(error)
 77
 78    stack.append(f"\[[@F red]Error Message[@F]] {message}")
 79    return stack
 80
 81
 82def test(func):
 83    """Decorator for test case (function)."""
 84
 85    def test_wrapper(*args, **kwargs):
 86        """Executes the function this decorator is on and collect the run results.
 87
 88        Returns:
 89            tuple: The test run results. Formatted in the order of function name, type of result, and addition info.
 90
 91        Note:
 92            In the case of a skip and failed result the info portion is filled it with the type of skip and the traceback respectivily.
 93        """
 94        try:
 95            func(*args, **kwargs)
 96        except AssertionError as error:
 97            return (func.__name__, ResultType.FAILED, __getTracback(error))
 98        except NotImplementedError:
 99            return (func.__name__, ResultType.SKIPPED, "")
100
101        return (func.__name__, ResultType.SUCCESS, "")
102
103    return test_wrapper
104
105
106class Test:
107    """Class used to indentify and run tests. It will also print the results to the screen."""
108
109    def getNodeValue(self, node) -> bool:
110        """Gets the decorator value from node
111
112        Args:
113            node (Any): Any ast node type
114
115        Returns:
116            str: id of ast.Name node
117        """
118        import ast
119
120        if isinstance(node, ast.Attribute):  # and node.attr in valid_paths:
121            if "test" in node.attr:
122                return True
123
124        elif isinstance(node, ast.Name):
125            if "test" in node.id:
126                return True
127
128        return False
129
130    def getTests(self, regex: Pattern) -> list:
131        """Gets all function names in the current class decorated with `@test`self.
132
133        Returns:
134            list: Function names decorated with `@test`
135        """
136        import ast
137        import inspect
138
139        result = []
140
141        def visit_FunctionDef(node):
142            """Checks given ast.FunctionDef node for a decorator `test` and adds it to the result."""
143            import re
144
145            for decorator in node.decorator_list:
146                if self.getNodeValue(decorator):
147                    if regex is not None and re.match(regex, node.name):
148                        result.append(node.name)
149                    elif regex is None:
150                        result.append(node.name)
151                else:
152                    continue
153
154        visitor = ast.NodeVisitor()
155        visitor.visit_FunctionDef = visit_FunctionDef
156        visitor.visit(
157            compile(inspect.getsource(self.__class__), "?", "exec", ast.PyCF_ONLY_AST)
158        )
159
160        return result
161
162    def executeTests(self, regex: Pattern) -> ClassResult:
163        """Will execute all functions decorated with `@test`"""
164
165        fnames: list = self.getTests(regex)
166        """Function names decorated with `@test`"""
167
168        results = ClassResult(name=self.__class__.__name__)
169
170        for name in fnames:
171            results.append(run(getattr(self, name), display=False))
172
173        return results
174
175    def run(self, display: bool = True, regex: Pattern = None) -> ClassResult:
176
177        """Will find and execute all tests in class. Prints results when done.
178
179        Args:
180            display (bool, optional): Whether to display the results
181            regex (Pattern, optional): Pattern of which tests should be run
182
183        Returns:
184            ClassResult: Results object that can save and print the results
185        """
186
187        results = self.executeTests(regex=regex)
188
189        if display:
190            results.write()
191
192        return results
def test(func)
 83def test(func):
 84    """Decorator for test case (function)."""
 85
 86    def test_wrapper(*args, **kwargs):
 87        """Executes the function this decorator is on and collect the run results.
 88
 89        Returns:
 90            tuple: The test run results. Formatted in the order of function name, type of result, and addition info.
 91
 92        Note:
 93            In the case of a skip and failed result the info portion is filled it with the type of skip and the traceback respectivily.
 94        """
 95        try:
 96            func(*args, **kwargs)
 97        except AssertionError as error:
 98            return (func.__name__, ResultType.FAILED, __getTracback(error))
 99        except NotImplementedError:
100            return (func.__name__, ResultType.SKIPPED, "")
101
102        return (func.__name__, ResultType.SUCCESS, "")
103
104    return test_wrapper

Decorator for test case (function).

class Test:
107class Test:
108    """Class used to indentify and run tests. It will also print the results to the screen."""
109
110    def getNodeValue(self, node) -> bool:
111        """Gets the decorator value from node
112
113        Args:
114            node (Any): Any ast node type
115
116        Returns:
117            str: id of ast.Name node
118        """
119        import ast
120
121        if isinstance(node, ast.Attribute):  # and node.attr in valid_paths:
122            if "test" in node.attr:
123                return True
124
125        elif isinstance(node, ast.Name):
126            if "test" in node.id:
127                return True
128
129        return False
130
131    def getTests(self, regex: Pattern) -> list:
132        """Gets all function names in the current class decorated with `@test`self.
133
134        Returns:
135            list: Function names decorated with `@test`
136        """
137        import ast
138        import inspect
139
140        result = []
141
142        def visit_FunctionDef(node):
143            """Checks given ast.FunctionDef node for a decorator `test` and adds it to the result."""
144            import re
145
146            for decorator in node.decorator_list:
147                if self.getNodeValue(decorator):
148                    if regex is not None and re.match(regex, node.name):
149                        result.append(node.name)
150                    elif regex is None:
151                        result.append(node.name)
152                else:
153                    continue
154
155        visitor = ast.NodeVisitor()
156        visitor.visit_FunctionDef = visit_FunctionDef
157        visitor.visit(
158            compile(inspect.getsource(self.__class__), "?", "exec", ast.PyCF_ONLY_AST)
159        )
160
161        return result
162
163    def executeTests(self, regex: Pattern) -> ClassResult:
164        """Will execute all functions decorated with `@test`"""
165
166        fnames: list = self.getTests(regex)
167        """Function names decorated with `@test`"""
168
169        results = ClassResult(name=self.__class__.__name__)
170
171        for name in fnames:
172            results.append(run(getattr(self, name), display=False))
173
174        return results
175
176    def run(self, display: bool = True, regex: Pattern = None) -> ClassResult:
177
178        """Will find and execute all tests in class. Prints results when done.
179
180        Args:
181            display (bool, optional): Whether to display the results
182            regex (Pattern, optional): Pattern of which tests should be run
183
184        Returns:
185            ClassResult: Results object that can save and print the results
186        """
187
188        results = self.executeTests(regex=regex)
189
190        if display:
191            results.write()
192
193        return results

Class used to indentify and run tests. It will also print the results to the screen.

Test()
def getNodeValue(self, node) -> bool:
110    def getNodeValue(self, node) -> bool:
111        """Gets the decorator value from node
112
113        Args:
114            node (Any): Any ast node type
115
116        Returns:
117            str: id of ast.Name node
118        """
119        import ast
120
121        if isinstance(node, ast.Attribute):  # and node.attr in valid_paths:
122            if "test" in node.attr:
123                return True
124
125        elif isinstance(node, ast.Name):
126            if "test" in node.id:
127                return True
128
129        return False

Gets the decorator value from node

Args: node (Any): Any ast node type

Returns: str: id of ast.Name node

def getTests(self, regex: Pattern) -> list:
131    def getTests(self, regex: Pattern) -> list:
132        """Gets all function names in the current class decorated with `@test`self.
133
134        Returns:
135            list: Function names decorated with `@test`
136        """
137        import ast
138        import inspect
139
140        result = []
141
142        def visit_FunctionDef(node):
143            """Checks given ast.FunctionDef node for a decorator `test` and adds it to the result."""
144            import re
145
146            for decorator in node.decorator_list:
147                if self.getNodeValue(decorator):
148                    if regex is not None and re.match(regex, node.name):
149                        result.append(node.name)
150                    elif regex is None:
151                        result.append(node.name)
152                else:
153                    continue
154
155        visitor = ast.NodeVisitor()
156        visitor.visit_FunctionDef = visit_FunctionDef
157        visitor.visit(
158            compile(inspect.getsource(self.__class__), "?", "exec", ast.PyCF_ONLY_AST)
159        )
160
161        return result

Gets all function names in the current class decorated with @testself.

Returns: list: Function names decorated with @test

def executeTests(self, regex: Pattern) -> teddecor.UnitTest.Results.ClassResult:
163    def executeTests(self, regex: Pattern) -> ClassResult:
164        """Will execute all functions decorated with `@test`"""
165
166        fnames: list = self.getTests(regex)
167        """Function names decorated with `@test`"""
168
169        results = ClassResult(name=self.__class__.__name__)
170
171        for name in fnames:
172            results.append(run(getattr(self, name), display=False))
173
174        return results

Will execute all functions decorated with @test

def run( self, display: bool = True, regex: Pattern = None) -> teddecor.UnitTest.Results.ClassResult:
176    def run(self, display: bool = True, regex: Pattern = None) -> ClassResult:
177
178        """Will find and execute all tests in class. Prints results when done.
179
180        Args:
181            display (bool, optional): Whether to display the results
182            regex (Pattern, optional): Pattern of which tests should be run
183
184        Returns:
185            ClassResult: Results object that can save and print the results
186        """
187
188        results = self.executeTests(regex=regex)
189
190        if display:
191            results.write()
192
193        return results

Will find and execute all tests in class. Prints results when done.

Args: display (bool, optional): Whether to display the results regex (Pattern, optional): Pattern of which tests should be run

Returns: ClassResult: Results object that can save and print the results

def run( test: Callable, display: bool = True) -> teddecor.UnitTest.Testing.TestResult:
18def run(test: Callable, display: bool = True) -> TestResult:
19    """Runs a single test case, function decorated with `@test` and constructs it's results.
20
21    Args:
22        test (Callable): @test function to run
23
24    Returns:
25        dict: Formated results from running the test
26
27    Raises:
28        TypeError: When the callable test is not decorated with `@test`
29    """
30
31    if test.__name__ == "test_wrapper":
32        _result = TestResult(test())
33        if display:
34            _result.write()
35        return _result
36    else:
37        raise TypeError("Test function must have @test decorator")

Runs a single test case, function decorated with @test and constructs it's results.

Args: test (Callable): @test function to run

Returns: dict: Formated results from running the test

Raises: TypeError: When the callable test is not decorated with @test

class TestResult(teddecor.UnitTest.Results.Result):
136class TestResult(Result):
137    def __init__(self, result: tuple[str, ResultType, Union[str, list]] = None):
138        super().__init__()
139
140        if result is not None:
141            self._name = result[0]
142            self._result = result[1]
143            self._info = result[2]
144        else:
145            self._name = "Unknown"
146            self._result = ResultType.SKIPPED
147            self._info = "Unkown test"
148
149        if result[1] == ResultType.SUCCESS:
150            self._counts[0] += 1
151        elif result[1] == ResultType.FAILED:
152            self._counts[1] += 1
153        elif result[1] == ResultType.SKIPPED:
154            self._counts[2] += 1
155
156    @property
157    def result(self) -> str:
158        return self._result[0]
159
160    @property
161    def color(self) -> str:
162        return self._result[1]
163
164    @property
165    def icon(self) -> str:
166        return self._result[2]
167
168    @property
169    def info(self) -> Union[str, list]:
170        return self._info
171
172    def pretty(self, indent: int = 0) -> list:
173        """Used to convert results into a list of strings. Allows for additional indentation to be added.
174
175        Args:
176            indent (int, optional): Amount of indent to add in spaces. Defaults to 0.
177
178        Returns:
179            list: The formatted results as a list of string with indents
180        """
181        out = []
182        out.append(
183            "".ljust(indent, " ")
184            + f"\[{self.color}{self.icon}[@F]] {TED.encode(self.name)}"
185        )
186        if isinstance(self.info, list):
187            for trace in self.info:
188                out.append("".ljust(indent + 4, " ") + trace)
189        else:
190            if self.info != "":
191                out.append("".ljust(indent + 4, " ") + self.info)
192
193        return out
194
195    def str(self, indent: int = 0) -> list:
196        """Results formatted into lines without colors
197
198        Args:
199            indent (int, optional): The amount to indent the lines. Defaults to 0.
200
201        Returns:
202            list: The results as lines
203        """
204        out = []
205        out.append(" " * indent + f"[{self.icon}] {self.name}")
206        if isinstance(self.info, list):
207            for trace in self.info:
208                out.append(" " * (indent + 4) + TED.strip(trace))
209        else:
210            if self.info != "":
211                out.append(" " * (indent + 4) + self.info)
212
213        return out
214
215    def dict(self) -> dict:
216        """Convert the test result into a dictionary
217
218        Returns:
219            dict: Dictionary format of the test result
220        """
221        return {self.name: {"result": self._result[0], "info": self.info}}
222
223    def csv(self) -> str:
224        """The results formatted as CSV
225
226        Returns:
227            str: CSV format of the result
228        """
229        info = "\n".join(self.info) if isinstance(self.info, list) else self.info
230        return f'{self.name},{self.result},"{info}"'
231
232    def save(
233        self, location: str = "", ext: Union[str, list[str]] = SaveType.CSV
234    ) -> bool:
235        """Takes a file location and creates a json file with the test data
236
237        Args:
238            location (str, optional): The location where the json file will be created. Defaults to "".
239
240        Returns:
241            bool: True if the file was successfully created
242        """
243
244        location = self.isdir(location)
245
246        if isinstance(ext, str):
247            self.__save_to_file(location, ext)
248        elif isinstance(ext, list):
249            for ext in ext:
250                self.__save_to_file(location, ext)
251        return True
252
253    def __save_to_file(self, location: str, type: str) -> bool:
254        """Saves the data to a given file type in a given location
255
256        Args:
257            location (str): Where to save the file
258            type (str): Type of file to save, CSV, JSON, TXT
259
260        Returns:
261            bool: True if file was saved
262        """
263        if location is not None:
264            with open(location + self.name + type, "+w", encoding="utf-8") as file:
265                if type == SaveType.CSV:
266                    file.write("Test Case,result,info\n")
267                    file.write(self.csv())
268                    return True
269                elif type == SaveType.JSON:
270                    from json import dumps
271
272                    file.write(dumps(self.dict(), indent=2))
273                    return True
274                elif type == SaveType.TXT:
275                    file.write(repr(self))
276                    return True
277        else:
278            return False

Base class for result types

TestResult( result: tuple[str, teddecor.UnitTest.Results.ResultType, typing.Union[str, list]] = None)
137    def __init__(self, result: tuple[str, ResultType, Union[str, list]] = None):
138        super().__init__()
139
140        if result is not None:
141            self._name = result[0]
142            self._result = result[1]
143            self._info = result[2]
144        else:
145            self._name = "Unknown"
146            self._result = ResultType.SKIPPED
147            self._info = "Unkown test"
148
149        if result[1] == ResultType.SUCCESS:
150            self._counts[0] += 1
151        elif result[1] == ResultType.FAILED:
152            self._counts[1] += 1
153        elif result[1] == ResultType.SKIPPED:
154            self._counts[2] += 1
result: str
color: str
icon: str
info: Union[str, list]
def pretty(self, indent: int = 0) -> list:
172    def pretty(self, indent: int = 0) -> list:
173        """Used to convert results into a list of strings. Allows for additional indentation to be added.
174
175        Args:
176            indent (int, optional): Amount of indent to add in spaces. Defaults to 0.
177
178        Returns:
179            list: The formatted results as a list of string with indents
180        """
181        out = []
182        out.append(
183            "".ljust(indent, " ")
184            + f"\[{self.color}{self.icon}[@F]] {TED.encode(self.name)}"
185        )
186        if isinstance(self.info, list):
187            for trace in self.info:
188                out.append("".ljust(indent + 4, " ") + trace)
189        else:
190            if self.info != "":
191                out.append("".ljust(indent + 4, " ") + self.info)
192
193        return out

Used to convert results into a list of strings. Allows for additional indentation to be added.

Args: indent (int, optional): Amount of indent to add in spaces. Defaults to 0.

Returns: list: The formatted results as a list of string with indents

def str(self, indent: int = 0) -> list:
195    def str(self, indent: int = 0) -> list:
196        """Results formatted into lines without colors
197
198        Args:
199            indent (int, optional): The amount to indent the lines. Defaults to 0.
200
201        Returns:
202            list: The results as lines
203        """
204        out = []
205        out.append(" " * indent + f"[{self.icon}] {self.name}")
206        if isinstance(self.info, list):
207            for trace in self.info:
208                out.append(" " * (indent + 4) + TED.strip(trace))
209        else:
210            if self.info != "":
211                out.append(" " * (indent + 4) + self.info)
212
213        return out

Results formatted into lines without colors

Args: indent (int, optional): The amount to indent the lines. Defaults to 0.

Returns: list: The results as lines

def dict(self) -> dict:
215    def dict(self) -> dict:
216        """Convert the test result into a dictionary
217
218        Returns:
219            dict: Dictionary format of the test result
220        """
221        return {self.name: {"result": self._result[0], "info": self.info}}

Convert the test result into a dictionary

Returns: dict: Dictionary format of the test result

def csv(self) -> str:
223    def csv(self) -> str:
224        """The results formatted as CSV
225
226        Returns:
227            str: CSV format of the result
228        """
229        info = "\n".join(self.info) if isinstance(self.info, list) else self.info
230        return f'{self.name},{self.result},"{info}"'

The results formatted as CSV

Returns: str: CSV format of the result

def save(self, location: str = '', ext: Union[str, list[str]] = '.csv') -> bool:
232    def save(
233        self, location: str = "", ext: Union[str, list[str]] = SaveType.CSV
234    ) -> bool:
235        """Takes a file location and creates a json file with the test data
236
237        Args:
238            location (str, optional): The location where the json file will be created. Defaults to "".
239
240        Returns:
241            bool: True if the file was successfully created
242        """
243
244        location = self.isdir(location)
245
246        if isinstance(ext, str):
247            self.__save_to_file(location, ext)
248        elif isinstance(ext, list):
249            for ext in ext:
250                self.__save_to_file(location, ext)
251        return True

Takes a file location and creates a json file with the test data

Args: location (str, optional): The location where the json file will be created. Defaults to "".

Returns: bool: True if the file was successfully created

def wrap(func: Callable, *args, **kwargs) -> Callable:
40def wrap(func: Callable, *args, **kwargs) -> Callable:
41    """Used to return a lambda that runs the function with the given args.
42    This is so that the function can be run later with provided parameters
43
44    Args:
45        func (Callable): Function to run
46
47    Returns:
48        Callable: Lambda of the funciton to be run later
49    """
50    return lambda: func(*args, **kwargs)

Used to return a lambda that runs the function with the given args. This is so that the function can be run later with provided parameters

Args: func (Callable): Function to run

Returns: Callable: Lambda of the funciton to be run later