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
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).
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.
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
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 @test
self.
Returns:
list: Function names decorated with @test
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
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
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
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
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
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
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
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
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
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
Inherited Members
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