SmokeCrossFit 0.0.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.
- crossfit/__init__.py +17 -0
- crossfit/bin/tools/jacococli.jar +0 -0
- crossfit/commands/__init__.py +3 -0
- crossfit/commands/command.py +86 -0
- crossfit/commands/command_builder.py +176 -0
- crossfit/executors/__init__.py +5 -0
- crossfit/executors/executor.py +46 -0
- crossfit/executors/executor_factory.py +8 -0
- crossfit/executors/local_executor.py +95 -0
- crossfit/models/__init__.py +5 -0
- crossfit/models/command_models.py +31 -0
- crossfit/models/executor_models.py +6 -0
- crossfit/models/tool_models.py +14 -0
- crossfit/refs/__init__.py +9 -0
- crossfit/tools/__init__.py +7 -0
- crossfit/tools/dotnet_coverage.py +77 -0
- crossfit/tools/jacoco.py +105 -0
- crossfit/tools/tool.py +90 -0
- crossfit/tools/tool_factory.py +20 -0
- smokecrossfit-0.0.0.dist-info/METADATA +197 -0
- smokecrossfit-0.0.0.dist-info/RECORD +23 -0
- smokecrossfit-0.0.0.dist-info/WHEEL +5 -0
- smokecrossfit-0.0.0.dist-info/top_level.txt +1 -0
crossfit/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from crossfit import refs
|
|
2
|
+
from crossfit.commands.command import Command
|
|
3
|
+
from crossfit.tools import Tool, Jacoco, DotnetCoverage, create_tool
|
|
4
|
+
from crossfit.executors import Executor, LocalExecutor, create_executor
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
'refs',
|
|
8
|
+
'Command',
|
|
9
|
+
'Tool',
|
|
10
|
+
'Jacoco',
|
|
11
|
+
'DotnetCoverage',
|
|
12
|
+
'create_tool',
|
|
13
|
+
'Executor',
|
|
14
|
+
'LocalExecutor',
|
|
15
|
+
'create_executor'
|
|
16
|
+
]
|
|
17
|
+
|
|
Binary file
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import glob
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, List, Tuple, Self
|
|
5
|
+
from typeguard import typechecked
|
|
6
|
+
|
|
7
|
+
COMMAND_DELIMITER = " "
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Command:
|
|
11
|
+
next_command: Optional[Self]
|
|
12
|
+
execution_call: Optional[str]
|
|
13
|
+
command_to_execute: Optional[str]
|
|
14
|
+
command_body: List[str]
|
|
15
|
+
options: List[Tuple[str, Optional[str]]]
|
|
16
|
+
arguments: List[str]
|
|
17
|
+
values_delimiter: Optional[str]
|
|
18
|
+
|
|
19
|
+
@typechecked()
|
|
20
|
+
def __init__(self):
|
|
21
|
+
"""
|
|
22
|
+
Initializes the command structure
|
|
23
|
+
"""
|
|
24
|
+
self.next_command = None
|
|
25
|
+
self.execution_call = None
|
|
26
|
+
self.command_to_execute = None
|
|
27
|
+
self.command_body = []
|
|
28
|
+
self.options = []
|
|
29
|
+
self.arguments = []
|
|
30
|
+
self.values_delimiter = None
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def command(self) -> List[str]:
|
|
34
|
+
"""
|
|
35
|
+
:returns: The command itself as a list of strings
|
|
36
|
+
"""
|
|
37
|
+
return list(filter(lambda s: s is not None, [self.execution_call, self.command_to_execute, *self.command_body]))
|
|
38
|
+
|
|
39
|
+
@command.setter
|
|
40
|
+
def command(self, value: List[str]):
|
|
41
|
+
"""
|
|
42
|
+
Sets the command body from a list of strings
|
|
43
|
+
:param value: The command body as list of strings
|
|
44
|
+
"""
|
|
45
|
+
self.options = []
|
|
46
|
+
self.arguments = []
|
|
47
|
+
self.command_body = value
|
|
48
|
+
|
|
49
|
+
def __str__(self) -> str:
|
|
50
|
+
"""
|
|
51
|
+
:returns: The command itself as a single string
|
|
52
|
+
"""
|
|
53
|
+
return COMMAND_DELIMITER.join(self.command)
|
|
54
|
+
|
|
55
|
+
def validate(self):
|
|
56
|
+
"""
|
|
57
|
+
Validates that the command has an execution call set
|
|
58
|
+
:raises AttributeError: If execution call is not set
|
|
59
|
+
"""
|
|
60
|
+
if not self.execution_call:
|
|
61
|
+
raise AttributeError(f"Ensure tool's execution call is set. Valued as: '{self.execution_call}'")
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def validate_path(path: Path):
|
|
65
|
+
"""
|
|
66
|
+
Validates that the given path exists, supports wildcards
|
|
67
|
+
:param path: The path to validate
|
|
68
|
+
:raises FileNotFoundError: If the path does not exist
|
|
69
|
+
"""
|
|
70
|
+
paths = glob.glob(str(path), recursive=True)
|
|
71
|
+
if not paths:
|
|
72
|
+
raise FileNotFoundError(f"Could not recognize given path - '{path}' - does not exist")
|
|
73
|
+
|
|
74
|
+
def __copy__(self) -> Self:
|
|
75
|
+
"""
|
|
76
|
+
:returns: A shallow copy of the command
|
|
77
|
+
"""
|
|
78
|
+
command_copy = Command()
|
|
79
|
+
command_copy.next_command = copy.copy(self.next_command)
|
|
80
|
+
command_copy.execution_call = self.execution_call
|
|
81
|
+
command_copy.command_to_execute = self.command_to_execute
|
|
82
|
+
command_copy.command_body = self.command_body
|
|
83
|
+
command_copy.options = self.options
|
|
84
|
+
command_copy.arguments = self.arguments
|
|
85
|
+
command_copy.values_delimiter = self.values_delimiter
|
|
86
|
+
return command_copy
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import glob
|
|
3
|
+
import os.path
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional, Self, Tuple
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
from crossfit.commands.command import Command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CommandBuilder:
|
|
11
|
+
"""Builder class for constructing Command objects with a fluent interface."""
|
|
12
|
+
_command: Command
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
"""
|
|
16
|
+
Initializes a new CommandBuilder instance with an empty Command.
|
|
17
|
+
"""
|
|
18
|
+
self._command: Command = Command()
|
|
19
|
+
|
|
20
|
+
@typechecked()
|
|
21
|
+
def with_next_command(self, command: Command):
|
|
22
|
+
"""
|
|
23
|
+
Sets the next command to be executed after the current command.
|
|
24
|
+
:param command: The Command object to be executed next
|
|
25
|
+
"""
|
|
26
|
+
self._command.next_command = command
|
|
27
|
+
|
|
28
|
+
@typechecked()
|
|
29
|
+
def with_command(self, command: List[str]) -> Self:
|
|
30
|
+
"""
|
|
31
|
+
Initializes the command from a list of strings.
|
|
32
|
+
:param command: A list containing at least two strings - execution call and command to execute
|
|
33
|
+
:returns: Self for method chaining
|
|
34
|
+
:raises ValueError: If the command list has fewer than two elements
|
|
35
|
+
"""
|
|
36
|
+
if len(command) < 2:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
'Command construct parameter must have at least two str arguments - execution call and any arguments')
|
|
39
|
+
self._command.execution_call = command[0]
|
|
40
|
+
self._command.command_to_execute = command[1]
|
|
41
|
+
self._command.command_body = command[2:]
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
@typechecked()
|
|
45
|
+
def set_execution_call(self, execution_call: str, path: Optional[Path] = None) -> Self:
|
|
46
|
+
"""
|
|
47
|
+
Sets the execution call for the command.
|
|
48
|
+
:param execution_call: The executable or interpreter to run
|
|
49
|
+
:param path: Optional path to prepend to the execution call
|
|
50
|
+
:returns: Self for method chaining
|
|
51
|
+
"""
|
|
52
|
+
self._command.execution_call = execution_call if path is None else os.path.relpath(path / execution_call)
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
@typechecked()
|
|
56
|
+
def set_command_to_execute(self, command_to_execute: str) -> Self:
|
|
57
|
+
"""
|
|
58
|
+
Sets the main command or script to execute.
|
|
59
|
+
:param command_to_execute: The command or script name to execute
|
|
60
|
+
:returns: Self for method chaining
|
|
61
|
+
"""
|
|
62
|
+
self._command.command_to_execute = command_to_execute
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
@typechecked()
|
|
66
|
+
def set_command_body(self, command_body: List[str]) -> Self:
|
|
67
|
+
"""
|
|
68
|
+
Sets the command body containing arguments and options.
|
|
69
|
+
:param command_body: A list of strings representing the command body
|
|
70
|
+
:returns: Self for method chaining
|
|
71
|
+
"""
|
|
72
|
+
self._command.command_body = command_body
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
@typechecked()
|
|
76
|
+
def set_values_delimiter(self, delimiter: Optional[str], update_current_values: bool = False) -> Self:
|
|
77
|
+
"""
|
|
78
|
+
Sets the delimiter used between options and their values.
|
|
79
|
+
:param delimiter: The delimiter string (e.g., '=' or ':'), or None for space separation
|
|
80
|
+
:param update_current_values: If True, updates all existing options with the new delimiter
|
|
81
|
+
:returns: Self for method chaining
|
|
82
|
+
"""
|
|
83
|
+
if self._command.values_delimiter != delimiter:
|
|
84
|
+
self._command.values_delimiter = delimiter
|
|
85
|
+
if update_current_values:
|
|
86
|
+
self.__update_all_options()
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
@typechecked()
|
|
90
|
+
def add_option(self, option: str, value: Optional[str] = None, delimiter: Optional[str] = None) -> Self:
|
|
91
|
+
"""
|
|
92
|
+
Adds a single option to the command.
|
|
93
|
+
:param option: The option flag or name (e.g., '--verbose' or '-v')
|
|
94
|
+
:param value: Optional value for the option
|
|
95
|
+
:param delimiter: Optional delimiter to use for this specific option
|
|
96
|
+
:returns: Self for method chaining
|
|
97
|
+
"""
|
|
98
|
+
self.__add_option(option, value, delimiter)
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
@typechecked()
|
|
102
|
+
def add_options(self, *args: Tuple[str, Optional[str]]) -> Self:
|
|
103
|
+
"""
|
|
104
|
+
Adds multiple options to the command.
|
|
105
|
+
:param args: Variable number of tuples, each containing (option, value) pairs
|
|
106
|
+
:returns: Self for method chaining
|
|
107
|
+
"""
|
|
108
|
+
for option in args:
|
|
109
|
+
self.__add_option(option[0], option[1])
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
@typechecked()
|
|
113
|
+
def add_arguments(self, *args) -> Self:
|
|
114
|
+
"""
|
|
115
|
+
Adds positional arguments to the command.
|
|
116
|
+
:param args: Variable number of arguments to add
|
|
117
|
+
:returns: Self for method chaining
|
|
118
|
+
"""
|
|
119
|
+
self._command.command_body = self._command.command_body[len(self._command.arguments):]
|
|
120
|
+
self._command.arguments.extend(list(args).copy())
|
|
121
|
+
self._command.command_body = self._command.arguments + self._command.command_body
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
@typechecked()
|
|
125
|
+
def add_path_arguments(self, *paths: Path) -> Self:
|
|
126
|
+
"""
|
|
127
|
+
Adds path arguments to the command, resolving glob patterns.
|
|
128
|
+
:param paths: Variable number of Path objects, which may contain glob patterns
|
|
129
|
+
:returns: Self for method chaining
|
|
130
|
+
:raises FileNotFoundError: If a path does not exist and is not a valid glob pattern
|
|
131
|
+
"""
|
|
132
|
+
[self._command.validate_path(path) for path in paths]
|
|
133
|
+
resolved_glob_paths = []
|
|
134
|
+
for path in paths:
|
|
135
|
+
resolved_glob_paths.extend(
|
|
136
|
+
os.path.relpath(resolved_path) for resolved_path in glob.glob(str(path), recursive=True))
|
|
137
|
+
self.add_arguments(*[os.path.relpath(path) for path in resolved_glob_paths])
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
def build_command(self) -> Command:
|
|
141
|
+
"""
|
|
142
|
+
Builds and returns a copy of the configured Command object.
|
|
143
|
+
:returns: A copy of the Command object with all configured settings
|
|
144
|
+
"""
|
|
145
|
+
return copy.copy(self._command)
|
|
146
|
+
|
|
147
|
+
def __update_all_options(self) -> Self:
|
|
148
|
+
"""
|
|
149
|
+
Updates all existing options with the current delimiter.
|
|
150
|
+
:returns: Self for method chaining
|
|
151
|
+
"""
|
|
152
|
+
self._command.command_body = self._command.command_body[:-sum(len(option) for option in self._command.options)]
|
|
153
|
+
options = self._command.options.copy()
|
|
154
|
+
self._command.options = []
|
|
155
|
+
self.add_options(*options)
|
|
156
|
+
return self
|
|
157
|
+
|
|
158
|
+
def __add_option(self, option: str, value: Optional[str] = None, delimiter: Optional[str] = None) -> Self:
|
|
159
|
+
"""
|
|
160
|
+
Internal method to add an option to the command body.
|
|
161
|
+
:param option: The option flag or name
|
|
162
|
+
:param value: Optional value for the option
|
|
163
|
+
:param delimiter: Optional delimiter to override the default
|
|
164
|
+
:returns: Self for method chaining
|
|
165
|
+
"""
|
|
166
|
+
if delimiter is not None:
|
|
167
|
+
self._command.values_delimiter = delimiter
|
|
168
|
+
|
|
169
|
+
if value is not None:
|
|
170
|
+
if self._command.values_delimiter is not None:
|
|
171
|
+
self._command.command_body.append(f"{option}{self._command.values_delimiter}{value}")
|
|
172
|
+
else:
|
|
173
|
+
self._command.command_body.extend([option, value])
|
|
174
|
+
else:
|
|
175
|
+
self._command.command_body.append(option)
|
|
176
|
+
self._command.options.append((option, value))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from logging import Logger
|
|
3
|
+
|
|
4
|
+
from crossfit.commands.command import Command
|
|
5
|
+
from crossfit.models.command_models import CommandResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Executor(ABC):
|
|
9
|
+
"""Abstract base class for command executors."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, logger: Logger, catch: bool = True):
|
|
12
|
+
"""
|
|
13
|
+
:param logger: Logger instance for logging execution details (required)
|
|
14
|
+
:param catch: If True, catches exceptions and returns error in CommandResult.
|
|
15
|
+
If False, re-raises exceptions.
|
|
16
|
+
"""
|
|
17
|
+
self._logger = logger
|
|
18
|
+
self._catch = catch
|
|
19
|
+
|
|
20
|
+
def execute(self, command: Command) -> CommandResult:
|
|
21
|
+
"""
|
|
22
|
+
Executes the given command and any chained commands via next_command.
|
|
23
|
+
:param command: The Command object to execute
|
|
24
|
+
:returns: Aggregated CommandResult from all executed commands
|
|
25
|
+
"""
|
|
26
|
+
result = self._execute_single(command)
|
|
27
|
+
|
|
28
|
+
current = command.next_command
|
|
29
|
+
while current is not None:
|
|
30
|
+
if result.code != 0:
|
|
31
|
+
self._logger.warning(f"Stopping command chain due to failure. Code: {result.code}")
|
|
32
|
+
break
|
|
33
|
+
next_result = self._execute_single(current)
|
|
34
|
+
result = result.add_result(next_result)
|
|
35
|
+
current = current.next_command
|
|
36
|
+
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def _execute_single(self, command: Command) -> CommandResult:
|
|
41
|
+
"""
|
|
42
|
+
Executes a single command without handling chained commands.
|
|
43
|
+
:param command: The Command object to execute
|
|
44
|
+
:returns: CommandResult with execution details
|
|
45
|
+
"""
|
|
46
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from crossfit.models.executor_models import ExecutorType
|
|
2
|
+
from crossfit.executors.local_executor import LocalExecutor
|
|
3
|
+
|
|
4
|
+
def create_executor(executor_type: ExecutorType, logger = None, catch: bool = True, **kwargs):
|
|
5
|
+
if executor_type == ExecutorType.Local:
|
|
6
|
+
return LocalExecutor(logger, catch, **kwargs)
|
|
7
|
+
else:
|
|
8
|
+
raise ValueError(f"Unknown executor type: {executor_type}")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from logging import Logger
|
|
3
|
+
import shlex
|
|
4
|
+
from crossfit.commands.command import Command
|
|
5
|
+
from crossfit.executors.executor import Executor
|
|
6
|
+
from crossfit.models.command_models import CommandResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LocalExecutor(Executor):
|
|
10
|
+
"""Executor that runs commands locally via subprocess."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, logger: Logger, catch: bool = True, **execution_kwargs):
|
|
13
|
+
"""
|
|
14
|
+
:param logger: Logger instance for logging execution details (required)
|
|
15
|
+
:param catch: If True, catches exceptions and returns error in CommandResult.
|
|
16
|
+
If False, re-raises exceptions.
|
|
17
|
+
:param execution_kwargs: Additional arguments passed to subprocess.run
|
|
18
|
+
"""
|
|
19
|
+
super().__init__(logger, catch)
|
|
20
|
+
self._exec_kwargs = {
|
|
21
|
+
"capture_output": True,
|
|
22
|
+
"check": True,
|
|
23
|
+
"text": True,
|
|
24
|
+
}
|
|
25
|
+
self._exec_kwargs.update(execution_kwargs)
|
|
26
|
+
|
|
27
|
+
def _execute_single(self, command: Command) -> CommandResult:
|
|
28
|
+
"""
|
|
29
|
+
Executes a single command without handling chained commands.
|
|
30
|
+
:param command: The Command object to execute
|
|
31
|
+
:returns: CommandResult with execution details
|
|
32
|
+
"""
|
|
33
|
+
command_str = str(command)
|
|
34
|
+
try:
|
|
35
|
+
command.validate()
|
|
36
|
+
res = subprocess.run(shlex.split(command_str), **self._exec_kwargs)
|
|
37
|
+
|
|
38
|
+
if res.returncode != 0 or (res.stderr and len(res.stderr)):
|
|
39
|
+
raise subprocess.CalledProcessError(
|
|
40
|
+
res.returncode, command_str, output=res.stdout, stderr=res.stderr
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self._logger.info(f"Command '{command_str}' finished with exit code {res.returncode}. {res.stdout}")
|
|
44
|
+
return CommandResult(
|
|
45
|
+
code=res.returncode,
|
|
46
|
+
command=command_str,
|
|
47
|
+
output=res.stdout,
|
|
48
|
+
error=res.stderr,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
except subprocess.CalledProcessError as cpe:
|
|
52
|
+
self._logger.error(
|
|
53
|
+
f"Execution of command '{command_str}' failed with error: {cpe.stderr}. Return code {cpe.returncode}."
|
|
54
|
+
)
|
|
55
|
+
if not self._catch:
|
|
56
|
+
raise
|
|
57
|
+
return CommandResult(
|
|
58
|
+
code=cpe.returncode,
|
|
59
|
+
command=command_str,
|
|
60
|
+
output=cpe.stdout or "",
|
|
61
|
+
error=cpe.stderr or "",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
except FileNotFoundError as not_found_e:
|
|
65
|
+
self._logger.error(f"Command '{command_str}' not found. {not_found_e.strerror}")
|
|
66
|
+
if not self._catch:
|
|
67
|
+
raise
|
|
68
|
+
return CommandResult(
|
|
69
|
+
code=124,
|
|
70
|
+
command=command_str,
|
|
71
|
+
output="",
|
|
72
|
+
error=not_found_e.strerror,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
except AttributeError as attr_e:
|
|
76
|
+
self._logger.error(f"Command validation failed: {attr_e}")
|
|
77
|
+
if not self._catch:
|
|
78
|
+
raise
|
|
79
|
+
return CommandResult(
|
|
80
|
+
code=1,
|
|
81
|
+
command=command_str,
|
|
82
|
+
output="",
|
|
83
|
+
error=str(attr_e),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
self._logger.error(f"An error occurred while executing command '{command_str}': {e}")
|
|
88
|
+
if not self._catch:
|
|
89
|
+
raise
|
|
90
|
+
return CommandResult(
|
|
91
|
+
code=1,
|
|
92
|
+
command=command_str,
|
|
93
|
+
output="",
|
|
94
|
+
error=str(e),
|
|
95
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CommandType(Enum):
|
|
8
|
+
SaveReport = "report"
|
|
9
|
+
SnapshotCoverage = "snapshot"
|
|
10
|
+
MergeCoverage = "merge"
|
|
11
|
+
ResetCoverage = "reset"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CommandResult(BaseModel):
|
|
15
|
+
code: int
|
|
16
|
+
command: str
|
|
17
|
+
output: Optional[str] = ""
|
|
18
|
+
target: Optional[str] = ""
|
|
19
|
+
error: Optional[str] = ""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def __add__(self, other):
|
|
23
|
+
self.code &= other.code
|
|
24
|
+
self.command += " && " + other.command
|
|
25
|
+
self.output = "\n".join(filter(lambda val: val is not None, (self.output, other.output)))
|
|
26
|
+
self.target = other.target or self.target
|
|
27
|
+
self.error = "\n".join(filter(lambda val: val is not None, (self.error, other.error)))
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def add_result(self, other):
|
|
31
|
+
return self + other
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ToolType(Enum):
|
|
5
|
+
Jacoco = "jacococli.jar"
|
|
6
|
+
DotnetCoverage = "dotnet-coverage"
|
|
7
|
+
DotnetReportGenerator = "reportgenerator"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ReportFormat(Enum):
|
|
11
|
+
Csv = "Csv"
|
|
12
|
+
Html = "Html"
|
|
13
|
+
Xml = "Xml"
|
|
14
|
+
Cobertura = "Cobertura"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
__filepath = Path(__file__)
|
|
4
|
+
bin_dir = (__filepath.parent.parent / r'bin')
|
|
5
|
+
tools_dir = (bin_dir / r'tools')
|
|
6
|
+
executors_dir = (bin_dir / r'executors')
|
|
7
|
+
deps_dir = (bin_dir / r'deps')
|
|
8
|
+
|
|
9
|
+
__all__ = ['bin_dir', 'tools_dir', 'deps_dir', 'executors_dir']
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Tuple, Optional
|
|
3
|
+
|
|
4
|
+
from crossfit import Command
|
|
5
|
+
from crossfit.models import ReportFormat, ToolType
|
|
6
|
+
from crossfit.tools.tool import Tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DotnetCoverage(Tool):
|
|
10
|
+
_tool_type = ToolType.DotnetCoverage
|
|
11
|
+
|
|
12
|
+
def snapshot_coverage(self, session, target_dir, target_file, *extras: Tuple[str, Optional[str]]) -> Command:
|
|
13
|
+
"""
|
|
14
|
+
Triggers dotnet-coverage agent to save cobertura formatted coverage files to the given path.
|
|
15
|
+
:param session: Session id of the dotnet-coverage agent collected-coverage to snapshot.
|
|
16
|
+
:param target_dir: Targeted directory to save the dotnet-coverage collection to.
|
|
17
|
+
:param target_file: Specified snapshot file name - when not given, uses default with .xml suffix.
|
|
18
|
+
:param extras: Extra options to pass to the dotnet-coverage CLI's snapshot command.
|
|
19
|
+
:return: A Command object configured to snapshot coverage data.
|
|
20
|
+
"""
|
|
21
|
+
target_path = Path(target_dir) / (
|
|
22
|
+
target_file if target_file is not None else self._get_default_target_filename())
|
|
23
|
+
if not target_path.suffix:
|
|
24
|
+
target_path = target_path.with_suffix(".xml")
|
|
25
|
+
extras += ("--output", str(target_path)),
|
|
26
|
+
command_builder = self._create_command_builder(
|
|
27
|
+
"snapshot", None, None, *extras).add_arguments(session)
|
|
28
|
+
|
|
29
|
+
return command_builder.build_command()
|
|
30
|
+
|
|
31
|
+
def merge_coverage(self, coverage_files, target_dir, target_file, *extras: Tuple[str, Optional[str]]) -> Command:
|
|
32
|
+
"""
|
|
33
|
+
Merges multiple coverage files into a single unified coverage file.
|
|
34
|
+
:param coverage_files: File paths to coverage files to merge.
|
|
35
|
+
:param target_dir: Targeted directory to save the merged coverage file to.
|
|
36
|
+
:param target_file: Specified merged file name - when not given, uses default with .xml suffix.
|
|
37
|
+
:param extras: Extra options to pass to the dotnet-coverage CLI's merge command.
|
|
38
|
+
:return: A Command object configured to merge coverage files.
|
|
39
|
+
"""
|
|
40
|
+
extras += ("--output", str(Path(target_dir) / (
|
|
41
|
+
target_file if target_file is not None else Path(self._get_default_target_filename()).with_suffix(
|
|
42
|
+
".xml")))),
|
|
43
|
+
command_builder = self._create_command_builder("merge", None, coverage_files, *extras)
|
|
44
|
+
if {"--output-format", "-f"}.intersection(command_builder.build_command().command):
|
|
45
|
+
command_builder = command_builder.add_option("--output-format", ReportFormat.Cobertura.value.lower())
|
|
46
|
+
|
|
47
|
+
return command_builder.build_command()
|
|
48
|
+
|
|
49
|
+
def save_report(self, coverage_files, target_dir, sourcecode_dir,
|
|
50
|
+
report_format: ReportFormat = None, report_formats: List[ReportFormat] = None,
|
|
51
|
+
*extras: Tuple[str, Optional[str]]) -> Command:
|
|
52
|
+
"""
|
|
53
|
+
Creates a dotnet-coverage report from coverage files to the given path.
|
|
54
|
+
:param coverage_files: File paths (can handle wildcards) to create dotnet-coverage report from.
|
|
55
|
+
:param target_dir: Targeted directory to save the dotnet-coverage report to.
|
|
56
|
+
:param sourcecode_dir: Directory containing the covered source code files.
|
|
57
|
+
:param report_format: Primary format of the dotnet-coverage report.
|
|
58
|
+
:param report_formats: Additional formats of dotnet-coverage reports to create.
|
|
59
|
+
:param extras: Extra options to pass to the dotnet CLI's report command.
|
|
60
|
+
:return: A Command object configured to generate the coverage report.
|
|
61
|
+
"""
|
|
62
|
+
multiple_values_delimiter = ';'
|
|
63
|
+
if sourcecode_dir:
|
|
64
|
+
extras += ("-sourcedirs", str(sourcecode_dir)),
|
|
65
|
+
extras += ("-targetdir", str(target_dir)),
|
|
66
|
+
command_builder = (
|
|
67
|
+
self._create_command_builder("", ToolType.DotnetReportGenerator, None, *extras)
|
|
68
|
+
.set_values_delimiter(":", True)
|
|
69
|
+
.add_option("-reports", f"\"{multiple_values_delimiter.join(map(str, coverage_files))}\""))
|
|
70
|
+
combined_formats = set((report_formats or []) + [report_format])
|
|
71
|
+
command_builder = command_builder.add_option(
|
|
72
|
+
"-reporttypes",f"\"{multiple_values_delimiter.join([rf.value for rf in combined_formats if rf])}\"")
|
|
73
|
+
|
|
74
|
+
command = command_builder.build_command()
|
|
75
|
+
command.command = [kw.replace("--", "-") for kw in command.command[2:]]
|
|
76
|
+
|
|
77
|
+
return command
|
crossfit/tools/jacoco.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
from crossfit import Command
|
|
5
|
+
from crossfit.commands.command_builder import CommandBuilder
|
|
6
|
+
from crossfit.models.tool_models import ReportFormat, ToolType
|
|
7
|
+
from crossfit.tools.tool import Tool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Jacoco(Tool):
|
|
11
|
+
"""JaCoCo coverage tool implementation for Java projects."""
|
|
12
|
+
_tool_type = ToolType.Jacoco
|
|
13
|
+
|
|
14
|
+
def _create_command_builder(self, command, tool_type = None, path_arguments = None, required_flags = None,
|
|
15
|
+
*extras: Tuple[str, Optional[str]]) -> CommandBuilder:
|
|
16
|
+
"""
|
|
17
|
+
Creates a CommandBuilder for JaCoCo CLI commands.
|
|
18
|
+
:param command: The JaCoCo command to execute (e.g., 'report', 'dump', 'merge').
|
|
19
|
+
:param tool_type: The tool type used to build the command on (defaults to self._tool_type).
|
|
20
|
+
:param path_arguments: Path arguments to add to the command (e.g., coverage files).
|
|
21
|
+
:param required_flags: List of required flag options that must be present in extras.
|
|
22
|
+
:param extras: Extra options to pass to the JaCoCo CLI as tuples of (option, value).
|
|
23
|
+
:return: A CommandBuilder configured for the JaCoCo command.
|
|
24
|
+
:raises ValueError: If required flags are missing and catch is False.
|
|
25
|
+
"""
|
|
26
|
+
tool_type = tool_type or self._tool_type
|
|
27
|
+
command_builder = (super()._create_command_builder(command, tool_type, path_arguments, *extras)
|
|
28
|
+
.set_execution_call(f"java -jar {os.path.relpath(Path(self._path) / str(tool_type.value))}"))
|
|
29
|
+
|
|
30
|
+
required_flags = required_flags or []
|
|
31
|
+
for required_flag in required_flags:
|
|
32
|
+
if required_flag not in [extra[0] for extra in list(extras)]:
|
|
33
|
+
msg = (f"Encountered error while building {tool_type.name} command. "
|
|
34
|
+
f"JaCoCo flag option {required_flag} is required for command '{command}'.")
|
|
35
|
+
self._logger.error(msg)
|
|
36
|
+
if not self._catch:
|
|
37
|
+
raise ValueError(msg)
|
|
38
|
+
command_builder.set_command_body(["--help"])
|
|
39
|
+
|
|
40
|
+
return command_builder
|
|
41
|
+
|
|
42
|
+
def save_report(self, coverage_files, target_dir, report_format, report_formats, sourcecode_dir, build_dir, *extras)\
|
|
43
|
+
-> Command:
|
|
44
|
+
"""
|
|
45
|
+
Creates a JaCoCo coverage report from coverage files to the given path.
|
|
46
|
+
:param coverage_files: File paths to JaCoCo .exec coverage files to create the report from.
|
|
47
|
+
:param target_dir: Targeted directory to save the JaCoCo report to.
|
|
48
|
+
:param report_format: Primary format of the JaCoCo report (e.g., HTML, XML, CSV).
|
|
49
|
+
:param report_formats: Additional formats of JaCoCo reports to create.
|
|
50
|
+
:param sourcecode_dir: Directory containing the covered source code files.
|
|
51
|
+
:param build_dir: Directory containing the compiled class files (required for JaCoCo).
|
|
52
|
+
:param extras: Extra options to pass to the JaCoCo CLI's report command.
|
|
53
|
+
:return: A Command object configured to generate the coverage report.
|
|
54
|
+
"""
|
|
55
|
+
if sourcecode_dir:
|
|
56
|
+
extras += ("--sourcefiles", str(sourcecode_dir)),
|
|
57
|
+
if build_dir:
|
|
58
|
+
extras += ("--classfiles", str(build_dir)),
|
|
59
|
+
command = self._create_command_builder(
|
|
60
|
+
"report", None, coverage_files, ["--classfiles"], *extras)
|
|
61
|
+
combined_formats = set((report_formats or []) + [report_format])
|
|
62
|
+
for rf in combined_formats:
|
|
63
|
+
if rf == ReportFormat.Html:
|
|
64
|
+
command = command.add_option(f"--{rf.name.lower()}", str(target_dir))
|
|
65
|
+
elif rf is not None:
|
|
66
|
+
command = command.add_option(f"--{rf.name.lower()}",
|
|
67
|
+
str((Path(target_dir) / self._get_default_target_filename())
|
|
68
|
+
.with_suffix(f".{rf.value.lower()}")))
|
|
69
|
+
return command.build_command()
|
|
70
|
+
|
|
71
|
+
def snapshot_coverage(self, session, target_dir, target_file, *extras) -> Command:
|
|
72
|
+
"""
|
|
73
|
+
Triggers JaCoCo agent to dump coverage data to the given path.
|
|
74
|
+
:param session: Session identifier (not used by JaCoCo dump but kept for interface consistency).
|
|
75
|
+
:param target_dir: Targeted directory to save the JaCoCo coverage dump to.
|
|
76
|
+
:param target_file: Specified snapshot file name - when not given, uses default with .exec suffix.
|
|
77
|
+
:param extras: Extra options to pass to the JaCoCo CLI's dump command.
|
|
78
|
+
:return: A Command object configured to dump coverage data.
|
|
79
|
+
"""
|
|
80
|
+
target_path = Path(target_dir) / (
|
|
81
|
+
target_file if target_file is not None else self._get_default_target_filename())
|
|
82
|
+
if not target_path.suffix:
|
|
83
|
+
target_path = target_path.with_suffix(".exec")
|
|
84
|
+
extras += ("--destfile", str(target_path)),
|
|
85
|
+
command = self._create_command_builder(
|
|
86
|
+
"dump", None, None, None, *extras)
|
|
87
|
+
return command.build_command()
|
|
88
|
+
|
|
89
|
+
def merge_coverage(self, coverage_files, target_dir, target_file, *extras) -> Command:
|
|
90
|
+
"""
|
|
91
|
+
Merges multiple JaCoCo coverage files into a single unified coverage file.
|
|
92
|
+
:param coverage_files: File paths to JaCoCo .exec coverage files to merge.
|
|
93
|
+
:param target_dir: Targeted directory to save the merged coverage file to.
|
|
94
|
+
:param target_file: Specified merged file name - when not given, uses default with .exec suffix.
|
|
95
|
+
:param extras: Extra options to pass to the JaCoCo CLI's merge command.
|
|
96
|
+
:return: A Command object configured to merge coverage files.
|
|
97
|
+
"""
|
|
98
|
+
target_path = Path(target_dir) / (
|
|
99
|
+
target_file if target_file is not None else self._get_default_target_filename())
|
|
100
|
+
if not target_path.suffix:
|
|
101
|
+
target_path = target_path.with_suffix(".exec")
|
|
102
|
+
extras += ("--destfile", str(target_path)),
|
|
103
|
+
command = self._create_command_builder(
|
|
104
|
+
"merge", None, coverage_files, None,*extras)
|
|
105
|
+
return command.build_command()
|
crossfit/tools/tool.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import logging
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Collection, Optional, Tuple, List, Union
|
|
6
|
+
from crossfit.commands.command import Command
|
|
7
|
+
from crossfit.commands.command_builder import CommandBuilder
|
|
8
|
+
from crossfit.models.tool_models import ToolType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Tool(ABC):
|
|
12
|
+
"""Abstract base class for coverage tools. Tools build and return Commands."""
|
|
13
|
+
_tool_type: ToolType
|
|
14
|
+
_path: Optional[Path]
|
|
15
|
+
_logger: logging.Logger
|
|
16
|
+
_catch: bool
|
|
17
|
+
|
|
18
|
+
def __init__(self, logger: logging.Logger, path: Optional[Path] = None, catch: bool = True):
|
|
19
|
+
"""
|
|
20
|
+
:param logger: Logger instance for logging (required).
|
|
21
|
+
:param path: The path to the tool executable/jar.
|
|
22
|
+
:param catch: If True, catches exceptions and returns fallback. If False, re-raises.
|
|
23
|
+
"""
|
|
24
|
+
self._logger: logging.Logger = logger
|
|
25
|
+
self._path: Optional[Path] = path
|
|
26
|
+
self._catch: bool = catch
|
|
27
|
+
|
|
28
|
+
def _get_default_target_filename(self) -> str:
|
|
29
|
+
"""Returns the default target filename for this tool."""
|
|
30
|
+
return f"cross-{self._tool_type.name}".lower()
|
|
31
|
+
|
|
32
|
+
def _create_command_builder(self, command: str, tool_type: Optional[ToolType] = None,
|
|
33
|
+
path_arguments: Optional[Collection[Path]] = None,
|
|
34
|
+
*extras: Tuple[str, Optional[str]]) -> CommandBuilder:
|
|
35
|
+
"""
|
|
36
|
+
Creates a CommandBuilder for the tool's wanted functionality.
|
|
37
|
+
:param command: The command of the tool to build on.
|
|
38
|
+
:param tool_type: The tool type used to build the command on (defaults to self._tool_type).
|
|
39
|
+
:param path_arguments: Path arguments to add to the command.
|
|
40
|
+
:param extras: Extra options to pass to the CLI's command as tuples of (option, value).
|
|
41
|
+
:returns: A CommandBuilder configured for this tool command.
|
|
42
|
+
"""
|
|
43
|
+
tool_type = tool_type or self._tool_type
|
|
44
|
+
path_arguments = path_arguments or []
|
|
45
|
+
|
|
46
|
+
command_builder = CommandBuilder().set_execution_call(str(tool_type.value), self._path)
|
|
47
|
+
try:
|
|
48
|
+
return command_builder.set_command_to_execute(command).add_path_arguments(*path_arguments).add_options(
|
|
49
|
+
*extras)
|
|
50
|
+
except FileNotFoundError as e:
|
|
51
|
+
self._logger.error(f"Encountered exception while building {tool_type.name} command. Error - {e}")
|
|
52
|
+
if not self._catch:
|
|
53
|
+
raise
|
|
54
|
+
return command_builder.set_command_body(["--help"])
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def save_report(self, coverage_files: List[Union[Path, str]], target_dir: Path, report_format, report_formats,
|
|
58
|
+
sourcecode_dir: Optional[Path], build_dir: Optional[Path],
|
|
59
|
+
*extras: Tuple[str, Optional[str]]) -> Command:
|
|
60
|
+
"""Builds a command to create a coverage report."""
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def snapshot_coverage(self, session: str, target_dir: Path, target_file: Optional[Path],
|
|
65
|
+
*extras: Tuple[str, Optional[str]]) -> Command:
|
|
66
|
+
"""Builds a command to snapshot coverage data."""
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def merge_coverage(self, coverage_files: List[Union[Path, str]], target_dir: Path, target_file: Optional[Path],
|
|
71
|
+
*extras: Tuple[str, Optional[str]]) -> Command:
|
|
72
|
+
"""Builds a command to merge coverage files."""
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
|
|
75
|
+
def reset_coverage(self, session: str, *extras: Tuple[str, Optional[str]]) -> Command:
|
|
76
|
+
"""
|
|
77
|
+
Builds a command to reset coverage data.
|
|
78
|
+
Uses next_command chaining to snapshot with --reset flag and then clean up temp file.
|
|
79
|
+
:param session: Session id of the coverage agent.
|
|
80
|
+
:param extras: Extra options to pass to the CLI's command.
|
|
81
|
+
:returns: A Command with chained cleanup command via next_command.
|
|
82
|
+
"""
|
|
83
|
+
target_dir = Path(tempfile.gettempdir()) / r"crossfit"
|
|
84
|
+
extras = extras + (("--reset", None),)
|
|
85
|
+
|
|
86
|
+
snapshot_command = self.snapshot_coverage(session, target_dir, None, *extras)
|
|
87
|
+
cleanup_command = CommandBuilder().with_command(["rm", "-f", str(target_dir)]).build_command()
|
|
88
|
+
snapshot_command.next_command = cleanup_command
|
|
89
|
+
|
|
90
|
+
return snapshot_command
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from logging import Logger
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
import crossfit.refs
|
|
7
|
+
from crossfit.tools import Jacoco, DotnetCoverage
|
|
8
|
+
from crossfit.models import ToolType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_tool(tool_type: ToolType, tool_path: str = None, cwd: Union[str, os.PathLike] = None, logger: Logger = None,
|
|
12
|
+
catch: bool = True, **kwargs):
|
|
13
|
+
if cwd:
|
|
14
|
+
kwargs["cwd"] = cwd
|
|
15
|
+
if tool_type == ToolType.Jacoco:
|
|
16
|
+
return Jacoco(logger, Path(tool_path) or Path(crossfit.refs.tools_dir), catch)
|
|
17
|
+
elif tool_type == ToolType.DotnetCoverage:
|
|
18
|
+
return DotnetCoverage(logger, Path(tool_path) or Path(crossfit.refs.tools_dir), catch)
|
|
19
|
+
else:
|
|
20
|
+
raise ValueError(f"Unknown tool type: {tool_type}")
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: SmokeCrossFit
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: A unified interface for various code coverage tools (JaCoCo, dotnet-coverage)
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# CrossFit Coverage Tools
|
|
9
|
+
|
|
10
|
+
CrossFit is a Python package designed to provide a unified interface for various code coverage tools. It wraps the functionality of different coverage CLI tools, allowing developers to use a consistent API regardless of which underlying tool they prefer.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
To install CrossFit, run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install crossfit
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Prerequisites
|
|
21
|
+
|
|
22
|
+
Before using CrossFit, ensure you have the following installed:
|
|
23
|
+
|
|
24
|
+
1. Python 3.7 or higher
|
|
25
|
+
2. Java Runtime Environment (JRE) - Required for JaCoCo integration
|
|
26
|
+
3. The specific coverage tools you intend to use (e.g., JaCoCo, dotnet-coverage)
|
|
27
|
+
|
|
28
|
+
## Supported Tools
|
|
29
|
+
|
|
30
|
+
CrossFit currently supports the following coverage tools:
|
|
31
|
+
|
|
32
|
+
1. **JaCoCo** - Java-based code coverage tool
|
|
33
|
+
2. **dotnet-coverage** - .NET coverage tool
|
|
34
|
+
3. **dotnet-reportgenerator** - .NET report generation tool
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
CrossFit provides a unified interface for managing code coverage across different tools. Here's how to use it:
|
|
39
|
+
|
|
40
|
+
### Basic Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from crossfit.tools.jacoco import Jacoco
|
|
44
|
+
|
|
45
|
+
# Initialize the JaCoCo tool
|
|
46
|
+
jacoco = Jacoco(tool_path="/path/to/jacoco")
|
|
47
|
+
|
|
48
|
+
# Save coverage report
|
|
49
|
+
result = jacoco.save_report(
|
|
50
|
+
coverage_files=["coverage.exec"],
|
|
51
|
+
target_dir="/output/directory",
|
|
52
|
+
sourcecode_dir="/source/code"
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Key Features
|
|
57
|
+
|
|
58
|
+
- **Unified Interface**: Consistent API across different coverage tools
|
|
59
|
+
- **Command Building**: Automatic validation of required flags
|
|
60
|
+
- **Coverage Management**: Merge, snapshot, and save coverage data
|
|
61
|
+
- **Error Handling**: Comprehensive logging and error reporting
|
|
62
|
+
- **Multiple Report Formats**: Support for CSV, HTML, XML, and Cobertura formats
|
|
63
|
+
|
|
64
|
+
### Example Workflow
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from crossfit.tools.jacoco import Jacoco
|
|
68
|
+
from crossfit.models.tool_models import ReportFormat
|
|
69
|
+
|
|
70
|
+
# Initialize JaCoCo tool
|
|
71
|
+
jacoco = Jacoco(tool_path="/path/to/jacoco")
|
|
72
|
+
|
|
73
|
+
# Merge coverage files
|
|
74
|
+
merge_result = jacoco.merge_coverage(
|
|
75
|
+
coverage_files=["coverage1.exec", "coverage2.exec"],
|
|
76
|
+
target_dir="/output",
|
|
77
|
+
target_file="merged.exec"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Generate HTML report
|
|
81
|
+
report_result = jacoco.save_report(
|
|
82
|
+
coverage_files=["merged.exec"],
|
|
83
|
+
target_dir="/reports",
|
|
84
|
+
sourcecode_dir="/src",
|
|
85
|
+
report_format=ReportFormat.Html
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Package Structure
|
|
90
|
+
|
|
91
|
+
The package follows a modular structure:
|
|
92
|
+
|
|
93
|
+
- `crossfit/tools/jacoco.py`: Implementation of JaCoCo tool wrapper
|
|
94
|
+
- `crossfit/tools/dotnet_coverage.py`: Implementation of .NET coverage tool wrapper
|
|
95
|
+
- `crossfit/tools/tool.py`: Base tool class and abstract methods
|
|
96
|
+
- `crossfit/models/tool_models.py`: Enum definitions for tool types and report formats
|
|
97
|
+
- `crossfit/models/command_models.py`: Command result models and command types
|
|
98
|
+
- `crossfit/commands/command.py`: Command building and execution utilities
|
|
99
|
+
|
|
100
|
+
## Core Components
|
|
101
|
+
|
|
102
|
+
### Tool Models (`crossfit/models/tool_models.py`)
|
|
103
|
+
|
|
104
|
+
Defines the core enumerations used throughout the package:
|
|
105
|
+
|
|
106
|
+
- **ToolType**: Enum representing supported tools
|
|
107
|
+
- `Jacoco`
|
|
108
|
+
- `DotnetCoverage`
|
|
109
|
+
- `DotnetReportGenerator`
|
|
110
|
+
|
|
111
|
+
- **ReportFormat**: Enum representing supported report formats
|
|
112
|
+
- `Csv`
|
|
113
|
+
- `Html`
|
|
114
|
+
- `Xml`
|
|
115
|
+
- `Cobertura`
|
|
116
|
+
|
|
117
|
+
### Command Models (`crossfit/models/command_models.py`)
|
|
118
|
+
|
|
119
|
+
Provides structured representations of command results:
|
|
120
|
+
|
|
121
|
+
- **CommandType**: Enum defining command types
|
|
122
|
+
- `SaveReport`
|
|
123
|
+
- `SnapshotCoverage`
|
|
124
|
+
- `MergeCoverage`
|
|
125
|
+
- `ResetCoverage`
|
|
126
|
+
|
|
127
|
+
- **CommandResult**: Pydantic model for command execution results with fields:
|
|
128
|
+
- `code`: Exit code from command execution
|
|
129
|
+
- `command`: The executed command string
|
|
130
|
+
- `output`: Standard output from command
|
|
131
|
+
- `target`: Target path/file of operation
|
|
132
|
+
- `error`: Error output from command
|
|
133
|
+
|
|
134
|
+
### Command Builder (`crossfit/commands/command.py`)
|
|
135
|
+
|
|
136
|
+
Provides utilities for building and executing shell commands:
|
|
137
|
+
|
|
138
|
+
- **Command**: Class for constructing shell commands with:
|
|
139
|
+
- Execution call (e.g., `java -jar`)
|
|
140
|
+
- Command keywords (e.g., `dump`, `merge`, `report`)
|
|
141
|
+
- Options and arguments
|
|
142
|
+
- Path validation
|
|
143
|
+
- Delimiter handling for option-value pairs
|
|
144
|
+
|
|
145
|
+
## Tool Implementations
|
|
146
|
+
|
|
147
|
+
### JaCoCo Tool (`crossfit/tools/jacoco.py`)
|
|
148
|
+
|
|
149
|
+
Implements the JaCoCo coverage tool with methods:
|
|
150
|
+
|
|
151
|
+
- `save_report()`: Generate coverage reports in various formats
|
|
152
|
+
- `snapshot_coverage()`: Take coverage snapshots
|
|
153
|
+
- `merge_coverage()`: Merge multiple coverage files
|
|
154
|
+
- `_get_command()`: Build validated JaCoCo commands with required flag checking
|
|
155
|
+
|
|
156
|
+
### DotNet Coverage Tool (`crossfit/tools/dotnet_coverage.py`)
|
|
157
|
+
|
|
158
|
+
Implements the .NET coverage tool with methods:
|
|
159
|
+
|
|
160
|
+
- `save_report()`: Generate reports from coverage files
|
|
161
|
+
- `merge_coverage()`: Merge coverage data
|
|
162
|
+
- `snapshot_coverage()`: Capture coverage snapshots
|
|
163
|
+
- `reset_coverage()`: Reset coverage data
|
|
164
|
+
|
|
165
|
+
## Advanced Usage
|
|
166
|
+
|
|
167
|
+
### Working with Multiple Report Formats
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from crossfit.tools.jacoco import Jacoco
|
|
171
|
+
from crossfit.models.tool_models import ReportFormat
|
|
172
|
+
|
|
173
|
+
jacoco = Jacoco(tool_path="/path/to/jacoco")
|
|
174
|
+
|
|
175
|
+
# Generate multiple report formats
|
|
176
|
+
result = jacoco.save_report(
|
|
177
|
+
coverage_files=["coverage.exec"],
|
|
178
|
+
target_dir="/reports",
|
|
179
|
+
sourcecode_dir="/src",
|
|
180
|
+
report_formats=[ReportFormat.Html, ReportFormat.Xml, ReportFormat.Cobertura]
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Custom Command Arguments
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from crossfit.tools.jacoco import Jacoco
|
|
188
|
+
|
|
189
|
+
jacoco = Jacoco(tool_path="/path/to/jacoco")
|
|
190
|
+
|
|
191
|
+
# Pass custom arguments to commands
|
|
192
|
+
result = jacoco.merge_coverage(
|
|
193
|
+
["coverage1.exec", "coverage2.exec"],
|
|
194
|
+
"/output", "merged.exec",
|
|
195
|
+
*["--format", "xml"] # Custom arguments
|
|
196
|
+
)
|
|
197
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
crossfit/__init__.py,sha256=Bdtz57-tapFnCYiIa9V5k_edCRkXWKqbp13xODBEv4c,382
|
|
2
|
+
crossfit/bin/tools/jacococli.jar,sha256=gRx_jGs1jF1oqJc8-oZ_aJK-emcbaXpLE8S0R-ba91w,607951
|
|
3
|
+
crossfit/commands/__init__.py,sha256=Hiu6s_GoE0cxhtbgnc8rUXPUxRejnHOQAAfhpSHheLI,51
|
|
4
|
+
crossfit/commands/command.py,sha256=0ujP5wvzfA0P9WJfzJrucgGWBEuiZjbxs9oMtdTD8p0,2752
|
|
5
|
+
crossfit/commands/command_builder.py,sha256=GLfvt0zXDQYEirQd8VGOjAUQvnpeslk2KLQp4KMYCAw,7128
|
|
6
|
+
crossfit/executors/__init__.py,sha256=7lUt1jO6RQwY_6xorlXaDBsOLlBradx9bKxzS-VSms0,179
|
|
7
|
+
crossfit/executors/executor.py,sha256=xPDTuP8wb9OQNC44Hy6yy9w9tDTT3NuH6oS_fXs_gp0,1665
|
|
8
|
+
crossfit/executors/executor_factory.py,sha256=xSsTU3f8AgeMiFCwyGM4ZOm3p9Lp--KQRIqNY5ueaZI,389
|
|
9
|
+
crossfit/executors/local_executor.py,sha256=dNkHoY5bQxRQbniec1ZwGKt56QKbZP_qwXSW3avTfno,3435
|
|
10
|
+
crossfit/models/__init__.py,sha256=awbAmN1HTp01PWgTB4JNwlg1EF_9w9ecSxqOaUVme4E,205
|
|
11
|
+
crossfit/models/command_models.py,sha256=0bK7deXh16SRMPtHXTyYmqScAVoRzyycO3uNRgImBHo,818
|
|
12
|
+
crossfit/models/executor_models.py,sha256=9d4UYXKvzIMwf_COBHxg23bvBxJp21A97gJP9oAkvb0,92
|
|
13
|
+
crossfit/models/tool_models.py,sha256=CM-JKXzM563BGverMP4jQFWcqcplA5RXcNJwhoWjHHg,266
|
|
14
|
+
crossfit/refs/__init__.py,sha256=OEUVIOc-CDNp7KmmcDzKfwkz9LA7SlRtrD3Y5rjQIIU,270
|
|
15
|
+
crossfit/tools/__init__.py,sha256=YtIhLrhdEtDOOH_TMGr9_tgt59vxN-kM97I5ajeN9LY,196
|
|
16
|
+
crossfit/tools/dotnet_coverage.py,sha256=1NBkj6Inb-qf3LITciu1248oboMhQTShFqveu-tkW0U,4359
|
|
17
|
+
crossfit/tools/jacoco.py,sha256=oLw895WvaVk_2mSeI3vAh7vWswg-daMzTEnl26tx1cY,5992
|
|
18
|
+
crossfit/tools/tool.py,sha256=OL3xVMf56UdCnahwX1rz-U7qZcXWFo7ep5B0CCu6RFs,4296
|
|
19
|
+
crossfit/tools/tool_factory.py,sha256=Fstp2NlIOsfYY6UX8vefJ7_ClrojnsbOMA7J63dDl58,741
|
|
20
|
+
smokecrossfit-0.0.0.dist-info/METADATA,sha256=aCChRaK7lsbSmxFrrKw_6pREAgJLZlX7J7Chd8G-sEc,5648
|
|
21
|
+
smokecrossfit-0.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
22
|
+
smokecrossfit-0.0.0.dist-info/top_level.txt,sha256=rDrcypRByvkrDsESoQmYOSkOKMO7eBPRToxHNQflYOI,9
|
|
23
|
+
smokecrossfit-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
crossfit
|