orionis 0.443.0__py3-none-any.whl → 0.444.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,343 @@
1
+ import argparse
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Optional, List, Type, Union, Dict
4
+ from orionis.console.args.enums.actions import ArgumentAction
5
+ from orionis.console.exceptions.cli_orionis_value_error import CLIOrionisValueError
6
+
7
+ @dataclass(kw_only=True, frozen=True, slots=True)
8
+ class CLIArgument:
9
+ """
10
+ Represents a command-line argument for argparse.
11
+
12
+ This class encapsulates all the properties and validation logic needed to create
13
+ a command-line argument that can be added to an argparse ArgumentParser. It provides
14
+ automatic validation, type checking, and smart defaults for common argument patterns.
15
+
16
+ Attributes
17
+ ----------
18
+ flags : List[str]
19
+ List of flags for the argument (e.g., ['--export', '-e']). Must contain at least one flag.
20
+ type : Type
21
+ Data type of the argument. Can be any Python type or custom type.
22
+ help : str
23
+ Description of the argument. If not provided, will be auto-generated from the primary flag.
24
+ default : Any, optional
25
+ Default value for the argument.
26
+ choices : List[Any], optional
27
+ List of valid values for the argument. All choices must match the specified type.
28
+ required : bool, default False
29
+ Whether the argument is required. Only applies to optional arguments.
30
+ metavar : str, optional
31
+ Metavar for displaying in help messages. Auto-generated from primary flag if not provided.
32
+ dest : str, optional
33
+ Destination name for the argument in the namespace. Auto-generated from primary flag if not provided.
34
+ action : Union[str, ArgumentAction], default ArgumentAction.STORE
35
+ Action to perform with the argument when it's encountered.
36
+ nargs : Union[int, str], optional
37
+ Number of arguments expected (e.g., 1, 2, '+', '*').
38
+ const : Any, optional
39
+ Constant value for store_const or append_const actions.
40
+
41
+ Raises
42
+ ------
43
+ CLIOrionisValueError
44
+ If any validation fails during initialization.
45
+ """
46
+
47
+ # Required fields
48
+ flags: List[str] = None
49
+ type: Type = None
50
+ help: str = None
51
+
52
+ default: Any = field(
53
+ default_factory = None,
54
+ metadata = {
55
+ "description": "Default value for the argument.",
56
+ "default": None
57
+ }
58
+ )
59
+
60
+ choices: Optional[List[Any]] = field(
61
+ default_factory = None,
62
+ metadata = {
63
+ "description": "List of valid choices for the argument.",
64
+ "default": None
65
+ }
66
+ )
67
+
68
+ required: bool = field(
69
+ default_factory = False,
70
+ metadata = {
71
+ "description": "Indicates if the argument is required.",
72
+ "default": False
73
+ }
74
+ )
75
+
76
+ metavar: Optional[str] = field(
77
+ default_factory = None,
78
+ metadata = {
79
+ "description": "Metavar for displaying in help messages.",
80
+ "default": None
81
+ }
82
+ )
83
+
84
+ dest: Optional[str] = field(
85
+ default_factory = None,
86
+ metadata = {
87
+ "description": "Destination name for the argument in the namespace.",
88
+ "default": None
89
+ }
90
+ )
91
+
92
+ action: Union[str, ArgumentAction] = field(
93
+ default_factory = ArgumentAction.STORE,
94
+ metadata = {
95
+ "description": "Action to perform with the argument.",
96
+ "default": ArgumentAction.STORE.value
97
+ }
98
+ )
99
+
100
+ nargs: Optional[Union[int, str]] = field(
101
+ default_factory = None,
102
+ metadata = {
103
+ "description": "Number of arguments expected (e.g., 1, 2, '+', '*').",
104
+ "default": None
105
+ }
106
+ )
107
+
108
+ const: Any = field(
109
+ default_factory = None,
110
+ metadata = {
111
+ "description": "Constant value for store_const or append_const actions.",
112
+ "default": None
113
+ }
114
+ )
115
+
116
+ def __post_init__(self):
117
+ """
118
+ Validate and normalize all argument attributes after initialization.
119
+
120
+ This method performs comprehensive validation of all argument attributes
121
+ and applies smart defaults where appropriate. It ensures the argument
122
+ configuration is valid for use with argparse.
123
+
124
+ Raises
125
+ ------
126
+ CLIOrionisValueError
127
+ If any validation fails or invalid values are provided.
128
+ """
129
+
130
+ # Validate flags - must be provided and non-empty
131
+ if not self.flags:
132
+ raise CLIOrionisValueError(
133
+ "Flags list cannot be empty. Please provide at least one flag (e.g., ['--export', '-e'])"
134
+ )
135
+
136
+ # Convert single string flag to list for consistency
137
+ if isinstance(self.flags, str):
138
+ object.__setattr__(self, 'flags', [self.flags])
139
+
140
+ # Ensure flags is a list
141
+ if not isinstance(self.flags, list):
142
+ raise CLIOrionisValueError("Flags must be a string or a list of strings")
143
+
144
+ # Validate each flag format and ensure they're strings
145
+ for flag in self.flags:
146
+ if not isinstance(flag, str):
147
+ raise CLIOrionisValueError("All flags must be strings")
148
+
149
+ # Check for duplicate flags
150
+ if len(set(self.flags)) != len(self.flags):
151
+ raise CLIOrionisValueError("Duplicate flags are not allowed in the flags list")
152
+
153
+ # Determine primary flag (longest one, or first if only one)
154
+ primary_flag = max(self.flags, key=len) if len(self.flags) > 1 else self.flags[0]
155
+
156
+ # Validate type is actually a type
157
+ if not isinstance(self.type, type):
158
+ raise CLIOrionisValueError("Type must be a valid Python type or custom type class")
159
+
160
+ # Auto-generate help if not provided
161
+ if self.help is None:
162
+ object.__setattr__(self, 'help', f"Argument for {primary_flag}")
163
+
164
+ # Ensure help is a string
165
+ if not isinstance(self.help, str):
166
+ raise CLIOrionisValueError("Help text must be a string")
167
+
168
+ # Validate choices if provided
169
+ if self.choices is not None:
170
+ # Ensure choices is a list
171
+ if not isinstance(self.choices, list):
172
+ raise CLIOrionisValueError("Choices must be provided as a list")
173
+
174
+ # Ensure all choices match the specified type
175
+ if self.type and not all(isinstance(choice, self.type) for choice in self.choices):
176
+ raise CLIOrionisValueError(
177
+ f"All choices must be of type {self.type.__name__}"
178
+ )
179
+
180
+ # Validate required is boolean
181
+ if not isinstance(self.required, bool):
182
+ raise CLIOrionisValueError("Required field must be a boolean value (True or False)")
183
+
184
+ # Auto-generate metavar if not provided
185
+ if self.metavar is None:
186
+ metavar = primary_flag.lstrip('-').upper().replace('-', '_')
187
+ object.__setattr__(self, 'metavar', metavar)
188
+
189
+ # Ensure metavar is a string
190
+ if not isinstance(self.metavar, str):
191
+ raise CLIOrionisValueError("Metavar must be a string")
192
+
193
+ # Auto-generate dest if not provided
194
+ if self.dest is None:
195
+ dest = primary_flag.lstrip('-').replace('-', '_')
196
+ object.__setattr__(self, 'dest', dest)
197
+
198
+ # Ensure dest is a string
199
+ if not isinstance(self.dest, str):
200
+ raise CLIOrionisValueError("Destination (dest) must be a string")
201
+
202
+ # Ensure dest is a valid Python identifier
203
+ if not self.dest.isidentifier():
204
+ raise CLIOrionisValueError(f"Destination '{self.dest}' is not a valid Python identifier")
205
+
206
+ # Normalize action value
207
+ if self.action is None:
208
+ object.__setattr__(self, 'action', ArgumentAction.STORE.value)
209
+ elif isinstance(self.action, str):
210
+ try:
211
+ action_enum = ArgumentAction(self.action)
212
+ object.__setattr__(self, 'action', action_enum.value)
213
+ except ValueError:
214
+ raise CLIOrionisValueError(f"Invalid action '{self.action}'. Please use a valid ArgumentAction value")
215
+ elif isinstance(self.action, ArgumentAction):
216
+ object.__setattr__(self, 'action', self.action.value)
217
+ else:
218
+ raise CLIOrionisValueError("Action must be a string or an ArgumentAction enum value")
219
+
220
+ # Special handling for boolean types
221
+ if self.type is bool:
222
+
223
+ # Auto-configure action based on default value
224
+ action = ArgumentAction.STORE_TRUE.value if not self.default else ArgumentAction.STORE_FALSE.value
225
+ object.__setattr__(self, 'action', action)
226
+
227
+ # argparse ignores type with store_true/false actions
228
+ object.__setattr__(self, 'type', None)
229
+
230
+ # Special handling for list types
231
+ if self.type is list and self.nargs is None:
232
+
233
+ # Auto-configure for accepting multiple values
234
+ object.__setattr__(self, 'nargs', '+')
235
+ object.__setattr__(self, 'type', str)
236
+
237
+ def addToParser(self, parser: argparse.ArgumentParser) -> None:
238
+ """
239
+ Add this argument to an argparse ArgumentParser instance.
240
+
241
+ This method integrates the CLIArgument configuration with an argparse
242
+ ArgumentParser by building the appropriate keyword arguments and adding
243
+ the argument with all its flags and options. The method handles all
244
+ necessary conversions and validations to ensure compatibility with
245
+ argparse's expected format.
246
+
247
+ Parameters
248
+ ----------
249
+ parser : argparse.ArgumentParser
250
+ The ArgumentParser instance to which this argument will be added.
251
+ The parser must be a valid argparse.ArgumentParser object.
252
+
253
+ Returns
254
+ -------
255
+ None
256
+ This method does not return any value. It modifies the provided
257
+ parser by adding the argument configuration to it.
258
+
259
+ Raises
260
+ ------
261
+ CLIOrionisValueError
262
+ If there's an error adding the argument to the parser, such as
263
+ conflicting argument names, invalid configurations, or argparse
264
+ internal errors during argument registration.
265
+ """
266
+
267
+ # Build the keyword arguments dictionary for argparse compatibility
268
+ # This filters out None values and handles special argument types
269
+ kwargs = self._buildParserKwargs()
270
+
271
+ # Attempt to add the argument to the parser with all flags and options
272
+ try:
273
+ # Use unpacking to pass all flags as positional arguments
274
+ # and all configuration options as keyword arguments
275
+ parser.add_argument(*self.flags, **kwargs)
276
+
277
+ # Catch any exception that occurs during argument addition
278
+ # and wrap it in our custom exception for consistent error handling
279
+ except Exception as e:
280
+ raise CLIOrionisValueError(f"Error adding argument {self.flags}: {e}")
281
+
282
+ def _buildParserKwargs(self) -> Dict[str, Any]:
283
+ """
284
+ Build the keyword arguments dictionary for argparse compatibility.
285
+
286
+ This private method constructs a dictionary of keyword arguments that will be
287
+ passed to argparse's add_argument method. It handles the conversion from
288
+ CLIArgument attributes to argparse-compatible parameters, filtering out None
289
+ values and applying special handling for different argument types (optional
290
+ vs positional arguments).
291
+
292
+ The method ensures that the resulting kwargs dictionary contains only valid
293
+ argparse parameters with appropriate values, preventing errors during argument
294
+ registration with the ArgumentParser.
295
+
296
+ Returns
297
+ -------
298
+ Dict[str, Any]
299
+ A dictionary containing keyword arguments ready to be unpacked and passed
300
+ to argparse.ArgumentParser.add_argument(). The dictionary includes only
301
+ non-None values and excludes parameters that are invalid for the specific
302
+ argument type (e.g., 'required' parameter for positional arguments).
303
+
304
+ Notes
305
+ -----
306
+ This method distinguishes between optional arguments (those starting with '-')
307
+ and positional arguments, applying different validation rules for each type.
308
+ Positional arguments cannot use the 'required' parameter, so it's automatically
309
+ removed from the kwargs if present.
310
+ """
311
+
312
+ # Determine argument type by checking if any flag starts with a dash
313
+ # Optional arguments have flags like '--export' or '-e'
314
+ # Positional arguments have flags without dashes like 'filename'
315
+ is_optional = any(flag.startswith('-') for flag in self.flags)
316
+ is_positional = not is_optional
317
+
318
+ # Build the base kwargs dictionary with all possible argparse parameters
319
+ # Each key corresponds to a parameter accepted by argparse.add_argument()
320
+ kwargs = {
321
+ "help": self.help, # Help text displayed in usage messages
322
+ "default": self.default, # Default value when argument not provided
323
+ "required": self.required and is_optional, # Whether argument is mandatory
324
+ "metavar": self.metavar, # Name displayed in help messages
325
+ "dest": self.dest, # Attribute name in the parsed namespace
326
+ "choices": self.choices, # List of valid values for the argument
327
+ "action": self.action, # Action to take when argument is encountered
328
+ "nargs": self.nargs, # Number of command-line arguments expected
329
+ "type": self.type, # Type to convert the argument to
330
+ "const": self.const # Constant value for certain actions
331
+ }
332
+
333
+ # Filter out None values to prevent passing invalid parameters to argparse
334
+ # argparse will raise errors if None values are explicitly passed for certain parameters
335
+ kwargs = {k: v for k, v in kwargs.items() if v is not None}
336
+
337
+ # Remove 'required' parameter for positional arguments since it's not supported
338
+ # Positional arguments are inherently required by argparse's design
339
+ if is_positional and 'required' in kwargs:
340
+ del kwargs['required']
341
+
342
+ # Return the cleaned and validated kwargs dictionary
343
+ return kwargs
File without changes
@@ -0,0 +1,43 @@
1
+ from enum import Enum
2
+
3
+ class ArgumentAction(Enum):
4
+ """
5
+ Enumeration for valid argparse action types.
6
+
7
+ This enum provides a comprehensive list of all standard action types
8
+ that can be used with Python's argparse module when defining command
9
+ line arguments. Each enum member corresponds to a specific behavior
10
+ for how argument values should be processed and stored.
11
+
12
+ Returns
13
+ -------
14
+ str
15
+ The string value representing the argparse action type.
16
+ """
17
+
18
+ # Store the argument value directly
19
+ STORE = "store"
20
+
21
+ # Store a constant value when the argument is specified
22
+ STORE_CONST = "store_const"
23
+
24
+ # Store True when the argument is specified
25
+ STORE_TRUE = "store_true"
26
+
27
+ # Store False when the argument is specified
28
+ STORE_FALSE = "store_false"
29
+
30
+ # Append each argument value to a list
31
+ APPEND = "append"
32
+
33
+ # Append a constant value to a list when the argument is specified
34
+ APPEND_CONST = "append_const"
35
+
36
+ # Count the number of times the argument is specified
37
+ COUNT = "count"
38
+
39
+ # Display help message and exit
40
+ HELP = "help"
41
+
42
+ # Display version information and exit
43
+ VERSION = "version"
@@ -15,17 +15,32 @@ class VersionCommand(BaseCommand):
15
15
 
16
16
  def handle(self) -> None:
17
17
  """
18
- Execute the version command.
18
+ Executes the version command to display the current Orionis framework version.
19
19
 
20
- This method retrieves and prints the version of the Orionis framework.
20
+ This method retrieves the version number from the framework metadata and prints it
21
+ in a formatted, bold, and successful style to the console. If an unexpected error occurs
22
+ during execution, it raises a CLIOrionisRuntimeError with the original exception message.
23
+
24
+ Parameters
25
+ ----------
26
+ None
27
+
28
+ Returns
29
+ -------
30
+ None
31
+ This method does not return any value. It outputs the version information to the console.
21
32
 
22
33
  Raises
23
34
  ------
24
- ValueError
25
- If an unexpected error occurs during execution, a ValueError is raised
35
+ CLIOrionisRuntimeError
36
+ If an unexpected error occurs during execution, a CLIOrionisRuntimeError is raised
26
37
  with the original exception message.
27
38
  """
39
+
40
+ # Print the Orionis framework version in a bold, success style
28
41
  try:
29
42
  self.textSuccessBold(f"Orionis Framework v{VERSION}")
43
+
44
+ # Raise a custom runtime error if any exception occurs
30
45
  except Exception as e:
31
46
  raise CLIOrionisRuntimeError(f"An unexpected error occurred: {e}") from e
@@ -1,6 +1,8 @@
1
1
  from orionis.container.providers.service_provider import ServiceProvider
2
2
  from orionis.test.contracts.unit_test import IUnitTest
3
3
  from orionis.test.core.unit_test import UnitTest
4
+ from orionis.foundation.config.testing.entities.testing import Testing
5
+ import os
4
6
 
5
7
  class TestingProvider(ServiceProvider):
6
8
  """
@@ -28,7 +30,39 @@ class TestingProvider(ServiceProvider):
28
30
  None
29
31
  """
30
32
 
31
- self.app.singleton(IUnitTest, UnitTest, alias="core.orionis.testing")
33
+ # Create a Testing configuration instance from the application config
34
+ config = Testing(**self.app.config('testing'))
35
+
36
+ # Create a UnitTest instance
37
+ unit_test = UnitTest(
38
+ app=self.app,
39
+ storage=self.app.path('storage_testing')
40
+ )
41
+
42
+ # Configure the UnitTest instance with settings from the Testing configuration
43
+ unit_test.configure(
44
+ verbosity=config.verbosity,
45
+ execution_mode=config.execution_mode,
46
+ max_workers=config.max_workers,
47
+ fail_fast=config.fail_fast,
48
+ print_result=config.print_result,
49
+ throw_exception=config.throw_exception,
50
+ persistent=config.persistent,
51
+ persistent_driver=config.persistent_driver,
52
+ web_report=config.web_report
53
+ )
54
+
55
+ # Discover tests based on the configuration
56
+ unit_test.discoverTests(
57
+ base_path=config.base_path,
58
+ folder_path=config.folder_path,
59
+ pattern=config.pattern,
60
+ test_name_pattern=config.test_name_pattern,
61
+ tags=config.tags
62
+ )
63
+
64
+ # Register the UnitTest instance in the application container
65
+ self.app.instance(IUnitTest, unit_test, alias="core.orionis.testing")
32
66
 
33
67
  def boot(self) -> None:
34
68
  """
@@ -42,4 +76,7 @@ class TestingProvider(ServiceProvider):
42
76
  None
43
77
  """
44
78
 
45
- pass
79
+ # Ensure directory for testing storage exists
80
+ storage_path = self.app.path('storage_testing')
81
+ if not os.path.exists(storage_path):
82
+ os.makedirs(storage_path, exist_ok=True)
@@ -5,7 +5,7 @@
5
5
  NAME = "orionis"
6
6
 
7
7
  # Current version of the framework
8
- VERSION = "0.443.0"
8
+ VERSION = "0.444.0"
9
9
 
10
10
  # Full name of the author or maintainer of the project
11
11
  AUTHOR = "Raul Mauricio Uñate Castro"
@@ -1,5 +1,6 @@
1
1
  import io
2
2
  import json
3
+ from os import walk
3
4
  import re
4
5
  import time
5
6
  import traceback
@@ -9,6 +10,7 @@ from contextlib import redirect_stdout, redirect_stderr
9
10
  from datetime import datetime
10
11
  from pathlib import Path
11
12
  from typing import Any, Dict, List, Optional, Tuple
13
+ from orionis.app import Orionis
12
14
  from orionis.container.resolver.resolver import Resolver
13
15
  from orionis.foundation.config.testing.enums.drivers import PersistentDrivers
14
16
  from orionis.foundation.config.testing.enums.mode import ExecutionMode
@@ -102,7 +104,9 @@ class UnitTest(IUnitTest):
102
104
  """
103
105
 
104
106
  def __init__(
105
- self
107
+ self,
108
+ app: Optional[IApplication] = None,
109
+ storage: Optional[str] = None
106
110
  ) -> None:
107
111
  """
108
112
  Initialize a UnitTest instance with default configuration and internal state.
@@ -113,11 +117,11 @@ class UnitTest(IUnitTest):
113
117
  -------
114
118
  None
115
119
  """
116
- # Application instance for dependency injection (set via __setApp)
117
- self.__app: Optional[IApplication] = None
120
+ # Application instance for dependency injection
121
+ self.__app: Optional[IApplication] = app or Orionis()
118
122
 
119
- # Storage path for test results (set via __setApp)
120
- self.__storage: Optional[str] = None
123
+ # Storage path for test results
124
+ self.__storage: Optional[str] = storage or self.__app.path('storage_testing')
121
125
 
122
126
  # Configuration values (set via configure)
123
127
  self.__verbosity: Optional[int] = None
@@ -220,6 +224,100 @@ class UnitTest(IUnitTest):
220
224
  # Return the instance to allow method chaining
221
225
  return self
222
226
 
227
+ def discoverTests(
228
+ self,
229
+ base_path: str | Path,
230
+ folder_path: str | List[str],
231
+ pattern: str,
232
+ test_name_pattern: Optional[str] = None,
233
+ tags: Optional[List[str]] = None
234
+ ) -> 'UnitTest':
235
+ """
236
+ Discover test cases from specified folders using flexible path discovery.
237
+
238
+ This method provides a convenient way to discover and load test cases from multiple folders
239
+ based on various path specifications. It supports wildcard discovery, single folder loading,
240
+ and multiple folder loading. The method automatically resolves paths relative to the base
241
+ directory and discovers all folders containing files matching the specified pattern.
242
+
243
+ Parameters
244
+ ----------
245
+ base_path : str or Path
246
+ Base directory path for resolving relative folder paths. This serves as the root
247
+ directory from which all folder searches are conducted.
248
+ folder_path : str or list of str
249
+ Specification of folders to search for test cases. Can be:
250
+ - '*' : Discover all folders containing matching files within base_path
251
+ - str : Single folder path relative to base_path
252
+ - list of str : Multiple folder paths relative to base_path
253
+ pattern : str
254
+ File name pattern to match test files, supporting wildcards (* and ?).
255
+ Examples: 'test_*.py', '*_test.py', 'test*.py'
256
+ test_name_pattern : str, optional
257
+ Regular expression pattern to filter test method names. Only tests whose
258
+ names match this pattern will be included. Default is None (no filtering).
259
+ tags : list of str, optional
260
+ List of tags to filter tests. Only tests decorated with matching tags
261
+ will be included. Default is None (no tag filtering).
262
+
263
+ Returns
264
+ -------
265
+ UnitTest
266
+ The current UnitTest instance with discovered tests added to the suite,
267
+ enabling method chaining.
268
+
269
+ Notes
270
+ -----
271
+ - All paths are resolved as absolute paths relative to the base_path
272
+ - When folder_path is '*', the method searches recursively through all subdirectories
273
+ - The method uses the existing discoverTestsInFolder method for actual test discovery
274
+ - Duplicate folders are automatically eliminated using a set data structure
275
+ - The method does not validate the existence of specified folders; validation
276
+ occurs during the actual test discovery process
277
+ """
278
+ # Resolve the base path as an absolute path from the current working directory
279
+ base_path = (Path.cwd() / base_path).resolve()
280
+
281
+ # Use a set to store discovered folders and automatically eliminate duplicates
282
+ discovered_folders = set()
283
+
284
+ # Handle wildcard discovery: search all folders containing matching files
285
+ if folder_path == '*':
286
+
287
+ # Search recursively through the entire base path for folders with matching files
288
+ discovered_folders.update(self.__listMatchingFolders(base_path, base_path, pattern))
289
+
290
+ # Handle multiple folder paths: process each folder in the provided list
291
+ elif isinstance(folder_path, list):
292
+ for custom in folder_path:
293
+ # Resolve each custom folder path relative to the base path
294
+ custom_path = (base_path / custom).resolve()
295
+ # Add all matching folders found within this custom path
296
+ discovered_folders.update(self.__listMatchingFolders(base_path, custom_path, pattern))
297
+
298
+ # Handle single folder path: process the single specified folder
299
+ else:
300
+
301
+ # Resolve the single folder path relative to the base path
302
+ custom_path = (base_path / folder_path).resolve()
303
+ # Add all matching folders found within this single path
304
+ discovered_folders.update(self.__listMatchingFolders(base_path, custom_path, pattern))
305
+
306
+ # Iterate through all discovered folders and perform test discovery
307
+ for folder in discovered_folders:
308
+
309
+ # Use the existing discoverTestsInFolder method to actually discover and load tests
310
+ self.discoverTestsInFolder(
311
+ base_path=base_path,
312
+ folder_path=folder,
313
+ pattern=pattern,
314
+ test_name_pattern=test_name_pattern or None,
315
+ tags=tags or None
316
+ )
317
+
318
+ # Return the current instance to enable method chaining
319
+ return self
320
+
223
321
  def discoverTestsInFolder(
224
322
  self,
225
323
  *,
@@ -1342,6 +1440,37 @@ class UnitTest(IUnitTest):
1342
1440
  # Return the suite containing only the filtered tests
1343
1441
  return filtered_suite
1344
1442
 
1443
+ def __listMatchingFolders(
1444
+ self,
1445
+ base_path: Path,
1446
+ custom_path: Path,
1447
+ pattern: str
1448
+ ) -> List[str]:
1449
+ """
1450
+ List folders within a given path containing files matching a pattern.
1451
+
1452
+ Parameters
1453
+ ----------
1454
+ base_path : Path
1455
+ The base directory path for calculating relative paths.
1456
+ custom_path : Path
1457
+ The directory path to search for matching files.
1458
+ pattern : str
1459
+ The filename pattern to match, supporting '*' and '?' wildcards.
1460
+
1461
+ Returns
1462
+ -------
1463
+ List[str]
1464
+ List of relative folder paths containing files matching the pattern.
1465
+ """
1466
+ regex = re.compile('^' + pattern.replace('*', '.*').replace('?', '.') + '$')
1467
+ matched_folders = set()
1468
+ for root, _, files in walk(str(custom_path)):
1469
+ if any(regex.fullmatch(file) for file in files):
1470
+ rel_path = Path(root).relative_to(base_path).as_posix()
1471
+ matched_folders.add(rel_path)
1472
+ return list(matched_folders)
1473
+
1345
1474
  def getTestNames(
1346
1475
  self
1347
1476
  ) -> List[str]:
orionis/test/kernel.py CHANGED
@@ -1,9 +1,4 @@
1
- from pathlib import Path
2
- import re
3
- from typing import List
4
- from os import walk
5
1
  from orionis.console.output.contracts.console import IConsole
6
- from orionis.foundation.config.testing.entities.testing import Testing
7
2
  from orionis.foundation.contracts.application import IApplication
8
3
  from orionis.test.contracts.kernel import ITestKernel
9
4
  from orionis.test.contracts.unit_test import IUnitTest
@@ -18,115 +13,67 @@ class TestKernel(ITestKernel):
18
13
  """
19
14
  Initialize the TestKernel with the provided application instance.
20
15
 
16
+ This constructor sets up the test kernel by validating the application
17
+ instance and resolving required dependencies for testing operations.
18
+
21
19
  Parameters
22
20
  ----------
23
21
  app : IApplication
24
- Application instance implementing the IApplication contract.
22
+ The application instance that provides dependency injection
23
+ and service resolution capabilities.
25
24
 
26
25
  Raises
27
26
  ------
28
27
  OrionisTestConfigException
29
- If the provided app is not an instance of IApplication.
28
+ If the provided app parameter is not an instance of IApplication.
29
+
30
+ Returns
31
+ -------
32
+ None
33
+ This is a constructor method and does not return a value.
30
34
  """
35
+ # Validate that the provided app parameter is an IApplication instance
31
36
  if not isinstance(app, IApplication):
32
37
  raise OrionisTestConfigException(
33
38
  f"Failed to initialize TestKernel: expected IApplication, got {type(app).__module__}.{type(app).__name__}."
34
39
  )
35
40
 
36
- self.__config = Testing(**app.config('testing'))
41
+ # Resolve the unit test service from the application container
37
42
  self.__unit_test: IUnitTest = app.make('core.orionis.testing')
38
- self.__unit_test._UnitTest__app = app
39
- self.__unit_test._UnitTest__storage = app.path('storage_testing')
40
- self.__console: IConsole = app.make('core.orionis.console')
41
-
42
- def __listMatchingFolders(
43
- self,
44
- base_path: Path,
45
- custom_path: Path,
46
- pattern: str
47
- ) -> List[str]:
48
- """
49
- List folders within a given path containing files matching a pattern.
50
-
51
- Parameters
52
- ----------
53
- base_path : Path
54
- The base directory path for calculating relative paths.
55
- custom_path : Path
56
- The directory path to search for matching files.
57
- pattern : str
58
- The filename pattern to match, supporting '*' and '?' wildcards.
59
43
 
60
- Returns
61
- -------
62
- List[str]
63
- List of relative folder paths containing files matching the pattern.
64
- """
65
- regex = re.compile('^' + pattern.replace('*', '.*').replace('?', '.') + '$')
66
- matched_folders = set()
67
- for root, _, files in walk(str(custom_path)):
68
- if any(regex.fullmatch(file) for file in files):
69
- rel_path = Path(root).relative_to(base_path).as_posix()
70
- matched_folders.add(rel_path)
71
- return list(matched_folders)
44
+ # Resolve the console service from the application container
45
+ self.__console: IConsole = app.make('core.orionis.console')
72
46
 
73
47
  def handle(self) -> IUnitTest:
74
48
  """
75
- Configure, discover, and execute unit tests based on the current configuration.
49
+ Execute the unit test suite and handle any exceptions that occur during testing.
50
+
51
+ This method serves as the main entry point for running tests through the test kernel.
52
+ It executes the unit test suite via the injected unit test service and provides
53
+ comprehensive error handling for both expected test failures and unexpected errors.
54
+ The method ensures graceful termination of the application in case of any failures.
76
55
 
77
56
  Returns
78
57
  -------
79
58
  IUnitTest
80
- The configured and executed unit test instance.
59
+ The unit test service instance after successful test execution. This allows
60
+ for potential chaining of operations or access to test results.
81
61
 
82
62
  Raises
83
63
  ------
84
- OrionisTestFailureException
85
- If test execution fails.
86
- Exception
87
- If an unexpected error occurs during test execution.
64
+ SystemExit
65
+ Indirectly raised through console.exitError() when test failures or
66
+ unexpected errors occur during test execution.
88
67
  """
89
- try:
90
- self.__unit_test.configure(
91
- verbosity=self.__config.verbosity,
92
- execution_mode=self.__config.execution_mode,
93
- max_workers=self.__config.max_workers,
94
- fail_fast=self.__config.fail_fast,
95
- print_result=self.__config.print_result,
96
- throw_exception=self.__config.throw_exception,
97
- persistent=self.__config.persistent,
98
- persistent_driver=self.__config.persistent_driver,
99
- web_report=self.__config.web_report
100
- )
101
-
102
- base_path = (Path.cwd() / self.__config.base_path).resolve()
103
- folder_path = self.__config.folder_path
104
- pattern = self.__config.pattern
105
- discovered_folders = set()
106
-
107
- if folder_path == '*':
108
- discovered_folders.update(self.__listMatchingFolders(base_path, base_path, pattern))
109
- elif isinstance(folder_path, list):
110
- for custom in folder_path:
111
- custom_path = (base_path / custom).resolve()
112
- discovered_folders.update(self.__listMatchingFolders(base_path, custom_path, pattern))
113
- else:
114
- custom_path = (base_path / folder_path).resolve()
115
- discovered_folders.update(self.__listMatchingFolders(base_path, custom_path, pattern))
116
-
117
- for folder in discovered_folders:
118
- self.__unit_test.discoverTestsInFolder(
119
- folder_path=folder,
120
- base_path=self.__config.base_path,
121
- pattern=pattern,
122
- test_name_pattern=self.__config.test_name_pattern or None,
123
- tags=self.__config.tags or None
124
- )
125
68
 
69
+ # Execute the unit test suite through the injected unit test service
70
+ try:
126
71
  return self.__unit_test.run()
127
72
 
73
+ # Handle expected test failures with a descriptive error message
128
74
  except OrionisTestFailureException as e:
129
75
  self.__console.exitError(f"Test execution failed: {e}")
130
76
 
77
+ # Handle any unexpected errors that occur during test execution
131
78
  except Exception as e:
132
79
  self.__console.exitError(f"An unexpected error occurred: {e}")
@@ -1,4 +1,4 @@
1
- from orionis.support.facades.workers import Workers
1
+ from orionis.services.system.workers import Workers
2
2
  from orionis.test.exceptions import OrionisTestValueError
3
3
 
4
4
  class __ValidWorkers:
@@ -25,7 +25,7 @@ class __ValidWorkers:
25
25
  OrionisTestValueError
26
26
  If `max_workers` is not a positive integer within the allowed range.
27
27
  """
28
- max_allowed = Workers.calculate()
28
+ max_allowed = Workers().calculate()
29
29
  if not isinstance(max_workers, int) or max_workers < 1 or max_workers > max_allowed:
30
30
  raise OrionisTestValueError(
31
31
  f"Invalid max_workers: Expected a positive integer between 1 and {max_allowed}, got '{max_workers}' ({type(max_workers).__name__})."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orionis
3
- Version: 0.443.0
3
+ Version: 0.444.0
4
4
  Summary: Orionis Framework – Elegant, Fast, and Powerful.
5
5
  Home-page: https://github.com/orionis-framework/framework
6
6
  Author: Raul Mauricio Uñate Castro
@@ -2,13 +2,16 @@ orionis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  orionis/app.py,sha256=b69fOzj2J8Aw5g0IldWZXixUDeeTO9vcHc_Njses9HU,603
3
3
  orionis/console/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  orionis/console/kernel.py,sha256=1CuBCLR6KItRt0_m50YQXirJUMX6lJf4Z4vvOjBqaUU,856
5
- orionis/console/arguments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- orionis/console/arguments/parser.py,sha256=WRaeyRjqnwXKBLn56sK2jubS_DAPbfVQ2rtfUGluA8A,101
5
+ orionis/console/args/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ orionis/console/args/argument.py,sha256=ZbY8Gbxbk6pvORRL1evzXB9qGesepD0zdbMsbqfcFjw,14688
7
+ orionis/console/args/parser.py,sha256=WRaeyRjqnwXKBLn56sK2jubS_DAPbfVQ2rtfUGluA8A,101
8
+ orionis/console/args/enums/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ orionis/console/args/enums/actions.py,sha256=S3T-vWS6DJSGtANrq3od3-90iYAjPvJwaOZ2V02y34c,1222
7
10
  orionis/console/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
11
  orionis/console/base/command.py,sha256=2kKyTaEzI16Up-XCUeNeJmDWPLN-CweQm3EgrN9U8NQ,3027
9
12
  orionis/console/base/contracts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
13
  orionis/console/base/contracts/command.py,sha256=s9yjma-s1URkVm0EbVvSkETAm-N8xX7OnZS43P8pvk8,1957
11
- orionis/console/commands/version.py,sha256=TfiuMCcESdlNuhnbl_h9qbOb8aYDXcc5X1J5LfD1v7M,1041
14
+ orionis/console/commands/version.py,sha256=kR8xzyc-Wisk7AXqg3Do7M9xTg_CxJgAtESPGrbRtpI,1673
12
15
  orionis/console/contracts/kernel.py,sha256=mh4LlhEYHh3FuGZZQ0GBhD6ZLa5YQvaNj2r01IIHI5Y,826
13
16
  orionis/console/core/reactor.py,sha256=lNfj-L4MKZhBn07l4H5L5dVW2xBRiq6-kyIuqnUNawQ,73
14
17
  orionis/console/dumper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -173,10 +176,10 @@ orionis/foundation/providers/inspirational_provider.py,sha256=ZIsuEq2Sif7C1b1hYC
173
176
  orionis/foundation/providers/logger_provider.py,sha256=PvwMxP5TKmn9DP8H8nJfyr16XgiJaGHyxPSMOpFgv84,1448
174
177
  orionis/foundation/providers/path_resolver_provider.py,sha256=s44Mg68RsUNPlilQlXMBE7onVexa7kyDmVQmci1JL4g,1342
175
178
  orionis/foundation/providers/progress_bar_provider.py,sha256=P__zpCyC29WCwErYGbh5dgcMRxw3XYmHzaUkzms9vPM,1345
176
- orionis/foundation/providers/testing_provider.py,sha256=o47qiK8Xoz4hfsxw4jMnMxEbSseJFIdJT-WqxGurqGc,1426
179
+ orionis/foundation/providers/testing_provider.py,sha256=fSZfwKnScTxGlGrcEPReGIiOPs8XkoEaNNARN1wC6LU,2939
177
180
  orionis/foundation/providers/workers_provider.py,sha256=YMRLdq_YQnR1unnoYvDpYQZbLli04f0CckuR6Q--wKg,1379
178
181
  orionis/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
179
- orionis/metadata/framework.py,sha256=0H0j_EmQM8BHWnt2ncNF6rkR_4bAOwUryKoXQsKlZ_M,4088
182
+ orionis/metadata/framework.py,sha256=Que9Q_spu-gld9CNZt90yTGnUPDQBWf87yrjBiveqPQ,4088
180
183
  orionis/metadata/package.py,sha256=k7Yriyp5aUcR-iR8SK2ec_lf0_Cyc-C7JczgXa-I67w,16039
181
184
  orionis/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
182
185
  orionis/services/asynchrony/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -302,7 +305,7 @@ orionis/support/standard/exceptions/value.py,sha256=rsyWFQweImaJGTJa7Id7RhPlwWJ4
302
305
  orionis/support/wrapper/__init__.py,sha256=jGoWoIGYuRYqMYQKlrX7Dpcbg-AGkHoB_aM2xhu73yc,62
303
306
  orionis/support/wrapper/dot_dict.py,sha256=T8xWwwOhBZHNeXRwE_CxvOwG9UFxsLqNmOJjV2CNIrc,7284
304
307
  orionis/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
305
- orionis/test/kernel.py,sha256=OXsrfm0wLdm8eg1dvmE6kqK_0xXI6RcZJ5HUaGteGas,5185
308
+ orionis/test/kernel.py,sha256=nJJDN2xusp9VZzezfocIvoenT2BheverjSovYCbRECg,3229
306
309
  orionis/test/cases/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
307
310
  orionis/test/cases/asynchronous.py,sha256=3e1Y3qzIxVU7i7lbLFEVyJ89IA74JsB7famx71W-p2E,1974
308
311
  orionis/test/cases/synchronous.py,sha256=S5jhuDEZ5I9wosrTFaCtowkD5r5HzJH6mKPOdEJcDJE,1734
@@ -315,7 +318,7 @@ orionis/test/contracts/render.py,sha256=wpDQzUtT0r8KFZ7zPcxWHXQ1EVNKxzA_rZ6ZKUcZ
315
318
  orionis/test/contracts/test_result.py,sha256=SNXJ2UerkweYn7uCT0i0HmMGP0XBrL_9KJs-0ZvIYU4,4002
316
319
  orionis/test/contracts/unit_test.py,sha256=PSnjEyM-QGQ3Pm0ZOqaa8QdPOtilGBVO4R87JYdVa-8,5386
317
320
  orionis/test/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
318
- orionis/test/core/unit_test.py,sha256=NiFk1u_a69JjQXBkXIzvlswoOniNm4YV4_dDGajRqQk,57400
321
+ orionis/test/core/unit_test.py,sha256=IIpPLM4pXZKCpKAZ-0PPatGuWjBXxgjKQxB8IJLB1zY,63310
319
322
  orionis/test/entities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
320
323
  orionis/test/entities/result.py,sha256=IMAd1AiwOf2z8krTDBFMpQe_1PG4YJ5Z0qpbr9xZwjg,4507
321
324
  orionis/test/enums/__init__.py,sha256=M3imAgMvKFTKg55FbtVoY3zxj7QRY9AfaUWxiSZVvn4,66
@@ -346,10 +349,10 @@ orionis/test/validators/tags.py,sha256=Qv-p8XFyAjY7OI861s52eADGf3LqzOWYfKt4L1cpo
346
349
  orionis/test/validators/throw_exception.py,sha256=PLtM94BArQf11VJhxfBHJSHARZSia-Q8ePixctU2JwQ,893
347
350
  orionis/test/validators/verbosity.py,sha256=rADzM82cPcJ2_6crszpobJuwb5WihWNQf6i4M_yrCpw,1785
348
351
  orionis/test/validators/web_report.py,sha256=n9BfzOZz6aEiNTypXcwuWbFRG0OdHNSmCNusHqc02R8,853
349
- orionis/test/validators/workers.py,sha256=HcZ3cnrk6u7cvM1xZpn_lsglHAq69_jx9RcTSvLrdb0,1204
352
+ orionis/test/validators/workers.py,sha256=rWcdRexINNEmGaO7mnc1MKUxkHKxrTsVuHgbnIfJYgc,1206
350
353
  orionis/test/view/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
351
354
  orionis/test/view/render.py,sha256=f-zNhtKSg9R5Njqujbg2l2amAs2-mRVESneLIkWOZjU,4082
352
- orionis-0.443.0.dist-info/licenses/LICENCE,sha256=JhC-z_9mbpUrCfPjcl3DhDA8trNDMzb57cvRSam1avc,1463
355
+ orionis-0.444.0.dist-info/licenses/LICENCE,sha256=JhC-z_9mbpUrCfPjcl3DhDA8trNDMzb57cvRSam1avc,1463
353
356
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
354
357
  tests/container/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
355
358
  tests/container/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -495,8 +498,8 @@ tests/testing/validators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
495
498
  tests/testing/validators/test_testing_validators.py,sha256=WPo5GxTP6xE-Dw3X1vZoqOMpb6HhokjNSbgDsDRDvy4,16588
496
499
  tests/testing/view/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
497
500
  tests/testing/view/test_render.py,sha256=tnnMBwS0iKUIbogLvu-7Rii50G6Koddp3XT4wgdFEYM,1050
498
- orionis-0.443.0.dist-info/METADATA,sha256=O2L-DtPlApeU58Vi-0VNQlTpec1YZ6R7QCzdNjFBm0M,4772
499
- orionis-0.443.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
500
- orionis-0.443.0.dist-info/top_level.txt,sha256=2bdoHgyGZhOtLAXS6Om8OCTmL24dUMC_L1quMe_ETbk,14
501
- orionis-0.443.0.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
502
- orionis-0.443.0.dist-info/RECORD,,
501
+ orionis-0.444.0.dist-info/METADATA,sha256=YgxccCDNizNHssu-14sVeL6dVMvkTMfvdfd3UcB1SeY,4772
502
+ orionis-0.444.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
503
+ orionis-0.444.0.dist-info/top_level.txt,sha256=2bdoHgyGZhOtLAXS6Om8OCTmL24dUMC_L1quMe_ETbk,14
504
+ orionis-0.444.0.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
505
+ orionis-0.444.0.dist-info/RECORD,,
File without changes
File without changes