SmokeCrossFit 0.0.0__tar.gz → 1.0.0a1__tar.gz

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.
Files changed (55) hide show
  1. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/.github/workflows/ci.yml +42 -1
  2. {smokecrossfit-0.0.0/SmokeCrossFit.egg-info → smokecrossfit-1.0.0a1}/PKG-INFO +12 -1
  3. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1/SmokeCrossFit.egg-info}/PKG-INFO +12 -1
  4. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/SmokeCrossFit.egg-info/SOURCES.txt +1 -0
  5. smokecrossfit-1.0.0a1/SmokeCrossFit.egg-info/requires.txt +11 -0
  6. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/__init__.py +1 -1
  7. smokecrossfit-1.0.0a1/crossfit/commands/__init__.py +4 -0
  8. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/commands/command.py +6 -6
  9. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/commands/command_builder.py +9 -8
  10. smokecrossfit-1.0.0a1/crossfit/executors/executor_factory.py +16 -0
  11. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/executors/local_executor.py +9 -2
  12. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/models/command_models.py +0 -1
  13. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/tools/dotnet_coverage.py +48 -36
  14. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/tools/jacoco.py +31 -13
  15. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/tools/tool.py +39 -22
  16. smokecrossfit-1.0.0a1/crossfit/tools/tool_factory.py +23 -0
  17. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/pyproject.toml +5 -2
  18. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/conftest.py +7 -1
  19. smokecrossfit-1.0.0a1/testing/sanity/test_dotnet_coverage.py +142 -0
  20. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/sanity/test_jacoco.py +6 -4
  21. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/unit-tests/command_tests/test_command.py +2 -3
  22. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/unit-tests/command_tests/test_command_builder.py +3 -2
  23. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/unit-tests/tools_tests/test_dotnet_coverage.py +0 -6
  24. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/unit-tests/tools_tests/test_jacoco.py +1 -7
  25. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/unit-tests/tools_tests/test_tool.py +7 -7
  26. smokecrossfit-0.0.0/crossfit/commands/__init__.py +0 -3
  27. smokecrossfit-0.0.0/crossfit/executors/executor_factory.py +0 -8
  28. smokecrossfit-0.0.0/crossfit/tools/tool_factory.py +0 -20
  29. smokecrossfit-0.0.0/testing/sanity/test_dotnet_coverage.py +0 -63
  30. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/.gitignore +0 -0
  31. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/README.md +0 -0
  32. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/SmokeCrossFit.egg-info/dependency_links.txt +0 -0
  33. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/SmokeCrossFit.egg-info/top_level.txt +0 -0
  34. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/bin/tools/jacococli.jar +0 -0
  35. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/executors/__init__.py +0 -0
  36. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/executors/executor.py +0 -0
  37. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/models/__init__.py +0 -0
  38. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/models/executor_models.py +0 -0
  39. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/models/tool_models.py +0 -0
  40. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/refs/__init__.py +0 -0
  41. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/crossfit/tools/__init__.py +0 -0
  42. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/requirements.txt +0 -0
  43. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/setup.cfg +0 -0
  44. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/helpers/command/f1.txt +0 -0
  45. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/helpers/tools/dotnetcoverage/classfiles/.gitkeep +0 -0
  46. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/helpers/tools/dotnetcoverage/s1.cobertura.xml +0 -0
  47. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/helpers/tools/dotnetcoverage/s2.cobertura.xml +0 -0
  48. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/helpers/tools/dotnetcoverage/sourcecode/.gitkeep +0 -0
  49. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/helpers/tools/dotnetcoverage/sourcecode/Dummy.cs +0 -0
  50. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/helpers/tools/jacoco/classfiles/.gitkeep +0 -0
  51. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/helpers/tools/jacoco/f1.exec +0 -0
  52. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/helpers/tools/jacoco/f2.exec +0 -0
  53. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/helpers/tools/jacoco/sourcecode/.gitkeep +0 -0
  54. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/unit-tests/executors_tests/test_executor.py +0 -0
  55. {smokecrossfit-0.0.0 → smokecrossfit-1.0.0a1}/testing/unit-tests/executors_tests/test_local_executor.py +0 -0
@@ -1,6 +1,12 @@
1
1
  name: CI/CD
2
2
 
3
3
  on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ tag:
7
+ description: "Tag to build/publish"
8
+ required: true
9
+ type: string
4
10
  push:
5
11
  branches: ["*"]
6
12
  tags:
@@ -37,15 +43,50 @@ jobs:
37
43
  - name: Run unit tests
38
44
  run: pytest testing/unit-tests -v --tb=short
39
45
 
46
+ sanity-tests:
47
+ name: Sanity Tests
48
+ runs-on: ubuntu-latest
49
+ if: startsWith(github.ref, 'refs/tags/v')
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+
53
+ - name: Set up Python
54
+ uses: actions/setup-python@v5
55
+ with:
56
+ python-version: "3.12"
57
+
58
+ - name: Set up .NET SDK
59
+ uses: actions/setup-dotnet@v4
60
+ with:
61
+ dotnet-version: "8.0.x"
62
+
63
+ - name: Cache pip dependencies
64
+ uses: actions/cache@v4
65
+ with:
66
+ path: ~/.cache/pip
67
+ key: ${{ runner.os }}-pip-sanity-${{ hashFiles('requirements.txt') }}
68
+ restore-keys: |
69
+ ${{ runner.os }}-pip-sanity-
70
+
71
+ - name: Install dependencies
72
+ run: |
73
+ python -m pip install --upgrade pip
74
+ pip install -r requirements.txt
75
+ pip install -e .
76
+
77
+ - name: Run sanity tests
78
+ run: pytest testing/sanity -v --tb=short
79
+
40
80
  build:
41
81
  name: Build distributions
42
82
  runs-on: ubuntu-latest
43
- needs: [unit-tests]
83
+ needs: [unit-tests, sanity-tests]
44
84
  if: startsWith(github.ref, 'refs/tags/v')
45
85
  steps:
46
86
  - uses: actions/checkout@v4
47
87
  with:
48
88
  fetch-depth: 0
89
+ fetch-tags: true
49
90
 
50
91
  - name: Set up Python
51
92
  uses: actions/setup-python@v5
@@ -1,9 +1,20 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SmokeCrossFit
3
- Version: 0.0.0
3
+ Version: 1.0.0a1
4
4
  Summary: A unified interface for various code coverage tools (JaCoCo, dotnet-coverage)
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
7
+ Requires-Dist: annotated-types~=0.7.0
8
+ Requires-Dist: colorama~=0.4.6
9
+ Requires-Dist: pydantic~=2.11.7
10
+ Requires-Dist: pydantic_core~=2.33.2
11
+ Requires-Dist: Pygments~=2.19.2
12
+ Requires-Dist: typeguard~=4.4.4
13
+ Requires-Dist: typing-inspection~=0.4.1
14
+ Requires-Dist: typing_extensions~=4.14.1
15
+ Requires-Dist: pytest>=8.4.3
16
+ Requires-Dist: build>=1.2.0
17
+ Requires-Dist: twine>=5.0.0
7
18
 
8
19
  # CrossFit Coverage Tools
9
20
 
@@ -1,9 +1,20 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SmokeCrossFit
3
- Version: 0.0.0
3
+ Version: 1.0.0a1
4
4
  Summary: A unified interface for various code coverage tools (JaCoCo, dotnet-coverage)
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
7
+ Requires-Dist: annotated-types~=0.7.0
8
+ Requires-Dist: colorama~=0.4.6
9
+ Requires-Dist: pydantic~=2.11.7
10
+ Requires-Dist: pydantic_core~=2.33.2
11
+ Requires-Dist: Pygments~=2.19.2
12
+ Requires-Dist: typeguard~=4.4.4
13
+ Requires-Dist: typing-inspection~=0.4.1
14
+ Requires-Dist: typing_extensions~=4.14.1
15
+ Requires-Dist: pytest>=8.4.3
16
+ Requires-Dist: build>=1.2.0
17
+ Requires-Dist: twine>=5.0.0
7
18
 
8
19
  # CrossFit Coverage Tools
9
20
 
@@ -6,6 +6,7 @@ requirements.txt
6
6
  SmokeCrossFit.egg-info/PKG-INFO
7
7
  SmokeCrossFit.egg-info/SOURCES.txt
8
8
  SmokeCrossFit.egg-info/dependency_links.txt
9
+ SmokeCrossFit.egg-info/requires.txt
9
10
  SmokeCrossFit.egg-info/top_level.txt
10
11
  crossfit/__init__.py
11
12
  crossfit/bin/tools/jacococli.jar
@@ -0,0 +1,11 @@
1
+ annotated-types~=0.7.0
2
+ colorama~=0.4.6
3
+ pydantic~=2.11.7
4
+ pydantic_core~=2.33.2
5
+ Pygments~=2.19.2
6
+ typeguard~=4.4.4
7
+ typing-inspection~=0.4.1
8
+ typing_extensions~=4.14.1
9
+ pytest>=8.4.3
10
+ build>=1.2.0
11
+ twine>=5.0.0
@@ -1,5 +1,5 @@
1
1
  from crossfit import refs
2
- from crossfit.commands.command import Command
2
+ from crossfit.commands import Command
3
3
  from crossfit.tools import Tool, Jacoco, DotnetCoverage, create_tool
4
4
  from crossfit.executors import Executor, LocalExecutor, create_executor
5
5
 
@@ -0,0 +1,4 @@
1
+ from .command import Command
2
+ from .command_builder import CommandBuilder
3
+
4
+ __all__ = ['Command', 'CommandBuilder']
@@ -1,7 +1,7 @@
1
1
  import copy
2
2
  import glob
3
3
  from pathlib import Path
4
- from typing import Optional, List, Tuple, Self
4
+ from typing import Optional, Self
5
5
  from typeguard import typechecked
6
6
 
7
7
  COMMAND_DELIMITER = " "
@@ -11,9 +11,9 @@ class Command:
11
11
  next_command: Optional[Self]
12
12
  execution_call: Optional[str]
13
13
  command_to_execute: Optional[str]
14
- command_body: List[str]
15
- options: List[Tuple[str, Optional[str]]]
16
- arguments: List[str]
14
+ command_body: list[str]
15
+ options: list[tuple[str, Optional[str]]]
16
+ arguments: list[str]
17
17
  values_delimiter: Optional[str]
18
18
 
19
19
  @typechecked()
@@ -30,14 +30,14 @@ class Command:
30
30
  self.values_delimiter = None
31
31
 
32
32
  @property
33
- def command(self) -> List[str]:
33
+ def command(self) -> list[str]:
34
34
  """
35
35
  :returns: The command itself as a list of strings
36
36
  """
37
37
  return list(filter(lambda s: s is not None, [self.execution_call, self.command_to_execute, *self.command_body]))
38
38
 
39
39
  @command.setter
40
- def command(self, value: List[str]):
40
+ def command(self, value: list[str]):
41
41
  """
42
42
  Sets the command body from a list of strings
43
43
  :param value: The command body as list of strings
@@ -1,8 +1,9 @@
1
1
  import copy
2
2
  import glob
3
3
  import os.path
4
+
4
5
  from pathlib import Path
5
- from typing import List, Optional, Self, Tuple
6
+ from typing import Optional, Self
6
7
  from typeguard import typechecked
7
8
  from crossfit.commands.command import Command
8
9
 
@@ -26,7 +27,7 @@ class CommandBuilder:
26
27
  self._command.next_command = command
27
28
 
28
29
  @typechecked()
29
- def with_command(self, command: List[str]) -> Self:
30
+ def with_command(self, command: list[str]) -> Self:
30
31
  """
31
32
  Initializes the command from a list of strings.
32
33
  :param command: A list containing at least two strings - execution call and command to execute
@@ -49,7 +50,7 @@ class CommandBuilder:
49
50
  :param path: Optional path to prepend to the execution call
50
51
  :returns: Self for method chaining
51
52
  """
52
- self._command.execution_call = execution_call if path is None else os.path.relpath(path / execution_call)
53
+ self._command.execution_call = execution_call if path is None else str(os.path.relpath(path / execution_call))
53
54
  return self
54
55
 
55
56
  @typechecked()
@@ -63,7 +64,7 @@ class CommandBuilder:
63
64
  return self
64
65
 
65
66
  @typechecked()
66
- def set_command_body(self, command_body: List[str]) -> Self:
67
+ def set_command_body(self, command_body: list[str]) -> Self:
67
68
  """
68
69
  Sets the command body containing arguments and options.
69
70
  :param command_body: A list of strings representing the command body
@@ -83,7 +84,7 @@ class CommandBuilder:
83
84
  if self._command.values_delimiter != delimiter:
84
85
  self._command.values_delimiter = delimiter
85
86
  if update_current_values:
86
- self.__update_all_options()
87
+ self._update_all_options()
87
88
  return self
88
89
 
89
90
  @typechecked()
@@ -99,7 +100,7 @@ class CommandBuilder:
99
100
  return self
100
101
 
101
102
  @typechecked()
102
- def add_options(self, *args: Tuple[str, Optional[str]]) -> Self:
103
+ def add_options(self, *args: tuple[str, Optional[str]]) -> Self:
103
104
  """
104
105
  Adds multiple options to the command.
105
106
  :param args: Variable number of tuples, each containing (option, value) pairs
@@ -144,12 +145,12 @@ class CommandBuilder:
144
145
  """
145
146
  return copy.copy(self._command)
146
147
 
147
- def __update_all_options(self) -> Self:
148
+ def _update_all_options(self) -> Self:
148
149
  """
149
150
  Updates all existing options with the current delimiter.
150
151
  :returns: Self for method chaining
151
152
  """
152
- self._command.command_body = self._command.command_body[:-sum(len(option) for option in self._command.options)]
153
+ self._command.command_body = self._command.arguments
153
154
  options = self._command.options.copy()
154
155
  self._command.options = []
155
156
  self.add_options(*options)
@@ -0,0 +1,16 @@
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
+ """
6
+ Factory method to create an executor based on the specified type.
7
+ :param executor_type: The type of executor to create.
8
+ :param logger: Optional logger instance for logging execution details.
9
+ :param catch: Whether to catch exceptions during execution.
10
+ :param kwargs: Additional keyword arguments for executor initialization.
11
+ :return: An instance of the specified executor type.
12
+ """
13
+ if executor_type == ExecutorType.Local:
14
+ return LocalExecutor(logger, catch, **kwargs)
15
+ else:
16
+ raise ValueError(f"Unknown executor type: {executor_type}")
@@ -1,6 +1,8 @@
1
1
  import subprocess
2
2
  from logging import Logger
3
3
  import shlex
4
+ from pathlib import Path
5
+
4
6
  from crossfit.commands.command import Command
5
7
  from crossfit.executors.executor import Executor
6
8
  from crossfit.models.command_models import CommandResult
@@ -9,20 +11,25 @@ from crossfit.models.command_models import CommandResult
9
11
  class LocalExecutor(Executor):
10
12
  """Executor that runs commands locally via subprocess."""
11
13
 
12
- def __init__(self, logger: Logger, catch: bool = True, **execution_kwargs):
14
+ def __init__(self, logger: Logger, catch: bool = True, workdir: Path = None, **execution_kwargs):
13
15
  """
14
16
  :param logger: Logger instance for logging execution details (required)
15
17
  :param catch: If True, catches exceptions and returns error in CommandResult.
16
18
  If False, re-raises exceptions.
17
19
  :param execution_kwargs: Additional arguments passed to subprocess.run
18
20
  """
21
+ self._workdir = workdir
22
+ if workdir is not None:
23
+ execution_kwargs["cwd"] = str(workdir)
24
+
19
25
  super().__init__(logger, catch)
26
+
20
27
  self._exec_kwargs = {
21
28
  "capture_output": True,
22
29
  "check": True,
23
30
  "text": True,
31
+ **execution_kwargs,
24
32
  }
25
- self._exec_kwargs.update(execution_kwargs)
26
33
 
27
34
  def _execute_single(self, command: Command) -> CommandResult:
28
35
  """
@@ -1,6 +1,5 @@
1
1
  from enum import Enum
2
2
  from typing import Optional
3
-
4
3
  from pydantic import BaseModel
5
4
 
6
5
 
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import List, Tuple, Optional
2
+ from typing import Optional
3
3
 
4
4
  from crossfit import Command
5
5
  from crossfit.models import ReportFormat, ToolType
@@ -9,16 +9,54 @@ from crossfit.tools.tool import Tool
9
9
  class DotnetCoverage(Tool):
10
10
  _tool_type = ToolType.DotnetCoverage
11
11
 
12
- def snapshot_coverage(self, session, target_dir, target_file, *extras: Tuple[str, Optional[str]]) -> Command:
12
+ def save_report(self,
13
+ coverage_files,
14
+ target_dir,
15
+ sourcecode_dir=None,
16
+ report_format: ReportFormat = None,
17
+ report_formats: list[ReportFormat] = None,
18
+ *extras: tuple[str, Optional[str]]) -> Command:
19
+ """
20
+ Creates a dotnet-coverage report from coverage files to the given path.
21
+ :param coverage_files: File paths (can handle wildcards) to create dotnet-coverage report from.
22
+ :param target_dir: Targeted directory to save the dotnet-coverage report to.
23
+ :param sourcecode_dir: Directory containing the covered source code files.
24
+ :param report_format: Primary format of the dotnet-coverage report.
25
+ :param report_formats: Additional formats of dotnet-coverage reports to create.
26
+ :param extras: Extra options to pass to the dotnet CLI's report command.
27
+ :return: A Command object configured to generate the coverage report.
28
+ """
29
+ multiple_values_delimiter = ';'
30
+ if sourcecode_dir:
31
+ extras += ("-sourcedirs", str(sourcecode_dir)),
32
+ extras += ("-targetdir", str(target_dir)),
33
+ command_builder = (
34
+ self._create_command_builder("", ToolType.DotnetReportGenerator, None, *extras)
35
+ .set_values_delimiter(":", True)
36
+ .add_option("-reports", f"\"{multiple_values_delimiter.join(map(str, coverage_files))}\""))
37
+ combined_formats = set((report_formats or []) + [report_format])
38
+ command_builder = command_builder.add_option(
39
+ "-reporttypes",f"\"{multiple_values_delimiter.join([rf.value for rf in combined_formats if rf])}\"")
40
+
41
+ command = command_builder.build_command()
42
+ command.command = [kw.replace("--", "-") for kw in command.command[2:]]
43
+
44
+ return command
45
+
46
+ def snapshot_coverage(self,
47
+ session,
48
+ target_dir,
49
+ target_file,
50
+ *extras: tuple[str, Optional[str]]) -> Command:
13
51
  """
14
52
  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.
53
+ :param session: Session id of the dotnet-coverage agent collected-coverage for snapshot.
16
54
  :param target_dir: Targeted directory to save the dotnet-coverage collection to.
17
55
  :param target_file: Specified snapshot file name - when not given, uses default with .xml suffix.
18
56
  :param extras: Extra options to pass to the dotnet-coverage CLI's snapshot command.
19
57
  :return: A Command object configured to snapshot coverage data.
20
58
  """
21
- target_path = Path(target_dir) / (
59
+ target_path = target_dir / (
22
60
  target_file if target_file is not None else self._get_default_target_filename())
23
61
  if not target_path.suffix:
24
62
  target_path = target_path.with_suffix(".xml")
@@ -28,7 +66,11 @@ class DotnetCoverage(Tool):
28
66
 
29
67
  return command_builder.build_command()
30
68
 
31
- def merge_coverage(self, coverage_files, target_dir, target_file, *extras: Tuple[str, Optional[str]]) -> Command:
69
+ def merge_coverage(self,
70
+ coverage_files,
71
+ target_dir,
72
+ target_file,
73
+ *extras: tuple[str, Optional[str]]) -> Command:
32
74
  """
33
75
  Merges multiple coverage files into a single unified coverage file.
34
76
  :param coverage_files: File paths to coverage files to merge.
@@ -37,7 +79,7 @@ class DotnetCoverage(Tool):
37
79
  :param extras: Extra options to pass to the dotnet-coverage CLI's merge command.
38
80
  :return: A Command object configured to merge coverage files.
39
81
  """
40
- extras += ("--output", str(Path(target_dir) / (
82
+ extras += ("--output", str(target_dir / (
41
83
  target_file if target_file is not None else Path(self._get_default_target_filename()).with_suffix(
42
84
  ".xml")))),
43
85
  command_builder = self._create_command_builder("merge", None, coverage_files, *extras)
@@ -45,33 +87,3 @@ class DotnetCoverage(Tool):
45
87
  command_builder = command_builder.add_option("--output-format", ReportFormat.Cobertura.value.lower())
46
88
 
47
89
  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
@@ -1,6 +1,6 @@
1
1
  import os.path
2
- from pathlib import Path
3
- from typing import Optional, Tuple
2
+
3
+ from typing import Optional
4
4
  from crossfit import Command
5
5
  from crossfit.commands.command_builder import CommandBuilder
6
6
  from crossfit.models.tool_models import ReportFormat, ToolType
@@ -11,8 +11,12 @@ class Jacoco(Tool):
11
11
  """JaCoCo coverage tool implementation for Java projects."""
12
12
  _tool_type = ToolType.Jacoco
13
13
 
14
- def _create_command_builder(self, command, tool_type = None, path_arguments = None, required_flags = None,
15
- *extras: Tuple[str, Optional[str]]) -> CommandBuilder:
14
+ def _create_command_builder(self,
15
+ command,
16
+ tool_type = None,
17
+ path_arguments = None,
18
+ required_flags = None,
19
+ *extras: tuple[str, Optional[str]]) -> CommandBuilder:
16
20
  """
17
21
  Creates a CommandBuilder for JaCoCo CLI commands.
18
22
  :param command: The JaCoCo command to execute (e.g., 'report', 'dump', 'merge').
@@ -25,11 +29,11 @@ class Jacoco(Tool):
25
29
  """
26
30
  tool_type = tool_type or self._tool_type
27
31
  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))}"))
32
+ .set_execution_call(f"java -jar {os.path.relpath(self._path / str(tool_type.value))}"))
29
33
 
30
34
  required_flags = required_flags or []
31
35
  for required_flag in required_flags:
32
- if required_flag not in [extra[0] for extra in list(extras)]:
36
+ if required_flag not in [extra[0] for extra in extras]:
33
37
  msg = (f"Encountered error while building {tool_type.name} command. "
34
38
  f"JaCoCo flag option {required_flag} is required for command '{command}'.")
35
39
  self._logger.error(msg)
@@ -39,8 +43,14 @@ class Jacoco(Tool):
39
43
 
40
44
  return command_builder
41
45
 
42
- def save_report(self, coverage_files, target_dir, report_format, report_formats, sourcecode_dir, build_dir, *extras)\
43
- -> Command:
46
+ def save_report(self,
47
+ coverage_files,
48
+ target_dir,
49
+ sourcecode_dir=None,
50
+ report_format = None,
51
+ report_formats = None,
52
+ build_dir = None,
53
+ *extras) -> Command:
44
54
  """
45
55
  Creates a JaCoCo coverage report from coverage files to the given path.
46
56
  :param coverage_files: File paths to JaCoCo .exec coverage files to create the report from.
@@ -64,11 +74,15 @@ class Jacoco(Tool):
64
74
  command = command.add_option(f"--{rf.name.lower()}", str(target_dir))
65
75
  elif rf is not None:
66
76
  command = command.add_option(f"--{rf.name.lower()}",
67
- str((Path(target_dir) / self._get_default_target_filename())
77
+ str((target_dir / self._get_default_target_filename())
68
78
  .with_suffix(f".{rf.value.lower()}")))
69
79
  return command.build_command()
70
80
 
71
- def snapshot_coverage(self, session, target_dir, target_file, *extras) -> Command:
81
+ def snapshot_coverage(self,
82
+ session,
83
+ target_dir,
84
+ target_file,
85
+ *extras) -> Command:
72
86
  """
73
87
  Triggers JaCoCo agent to dump coverage data to the given path.
74
88
  :param session: Session identifier (not used by JaCoCo dump but kept for interface consistency).
@@ -77,7 +91,7 @@ class Jacoco(Tool):
77
91
  :param extras: Extra options to pass to the JaCoCo CLI's dump command.
78
92
  :return: A Command object configured to dump coverage data.
79
93
  """
80
- target_path = Path(target_dir) / (
94
+ target_path = target_dir / (
81
95
  target_file if target_file is not None else self._get_default_target_filename())
82
96
  if not target_path.suffix:
83
97
  target_path = target_path.with_suffix(".exec")
@@ -86,7 +100,11 @@ class Jacoco(Tool):
86
100
  "dump", None, None, None, *extras)
87
101
  return command.build_command()
88
102
 
89
- def merge_coverage(self, coverage_files, target_dir, target_file, *extras) -> Command:
103
+ def merge_coverage(self,
104
+ coverage_files,
105
+ target_dir,
106
+ target_file,
107
+ *extras) -> Command:
90
108
  """
91
109
  Merges multiple JaCoCo coverage files into a single unified coverage file.
92
110
  :param coverage_files: File paths to JaCoCo .exec coverage files to merge.
@@ -95,7 +113,7 @@ class Jacoco(Tool):
95
113
  :param extras: Extra options to pass to the JaCoCo CLI's merge command.
96
114
  :return: A Command object configured to merge coverage files.
97
115
  """
98
- target_path = Path(target_dir) / (
116
+ target_path = target_dir / (
99
117
  target_file if target_file is not None else self._get_default_target_filename())
100
118
  if not target_path.suffix:
101
119
  target_path = target_path.with_suffix(".exec")
@@ -1,27 +1,28 @@
1
1
  import tempfile
2
- import logging
2
+
3
3
  from abc import ABC, abstractmethod
4
+ from logging import Logger
4
5
  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
6
+ from typing import Optional
7
+ from crossfit import Command
8
+ from crossfit.commands import CommandBuilder
9
+ from crossfit.models import ToolType, ReportFormat
9
10
 
10
11
 
11
12
  class Tool(ABC):
12
13
  """Abstract base class for coverage tools. Tools build and return Commands."""
13
14
  _tool_type: ToolType
14
15
  _path: Optional[Path]
15
- _logger: logging.Logger
16
+ _logger: Logger
16
17
  _catch: bool
17
18
 
18
- def __init__(self, logger: logging.Logger, path: Optional[Path] = None, catch: bool = True):
19
+ def __init__(self, logger: Logger, path: Optional[Path] = None, catch: bool = True):
19
20
  """
20
21
  :param logger: Logger instance for logging (required).
21
22
  :param path: The path to the tool executable/jar.
22
23
  :param catch: If True, catches exceptions and returns fallback. If False, re-raises.
23
24
  """
24
- self._logger: logging.Logger = logger
25
+ self._logger: Logger = logger
25
26
  self._path: Optional[Path] = path
26
27
  self._catch: bool = catch
27
28
 
@@ -29,9 +30,11 @@ class Tool(ABC):
29
30
  """Returns the default target filename for this tool."""
30
31
  return f"cross-{self._tool_type.name}".lower()
31
32
 
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:
33
+ def _create_command_builder(self,
34
+ command: str,
35
+ tool_type: Optional[ToolType] = None,
36
+ path_arguments: Optional[list[Path]] = None,
37
+ *extras: tuple[str, Optional[str]]) -> CommandBuilder:
35
38
  """
36
39
  Creates a CommandBuilder for the tool's wanted functionality.
37
40
  :param command: The command of the tool to build on.
@@ -45,8 +48,10 @@ class Tool(ABC):
45
48
 
46
49
  command_builder = CommandBuilder().set_execution_call(str(tool_type.value), self._path)
47
50
  try:
48
- return command_builder.set_command_to_execute(command).add_path_arguments(*path_arguments).add_options(
49
- *extras)
51
+ return (command_builder
52
+ .set_command_to_execute(command)
53
+ .add_path_arguments(*path_arguments)
54
+ .add_options(*extras))
50
55
  except FileNotFoundError as e:
51
56
  self._logger.error(f"Encountered exception while building {tool_type.name} command. Error - {e}")
52
57
  if not self._catch:
@@ -54,25 +59,37 @@ class Tool(ABC):
54
59
  return command_builder.set_command_body(["--help"])
55
60
 
56
61
  @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:
62
+ def save_report(self,
63
+ coverage_files: list[Path],
64
+ target_dir: Path,
65
+ sourcecode_dir: Optional[Path] = None,
66
+ report_format: ReportFormat = None,
67
+ report_formats: list[ReportFormat] = None,
68
+ *extras: tuple[str, Optional[str]]) -> Command:
60
69
  """Builds a command to create a coverage report."""
61
70
  raise NotImplementedError
62
71
 
63
72
  @abstractmethod
64
- def snapshot_coverage(self, session: str, target_dir: Path, target_file: Optional[Path],
65
- *extras: Tuple[str, Optional[str]]) -> Command:
73
+ def snapshot_coverage(self,
74
+ session: str,
75
+ target_dir: Path,
76
+ target_file: Optional[Path],
77
+ *extras: tuple[str, Optional[str]]) -> Command:
66
78
  """Builds a command to snapshot coverage data."""
67
79
  raise NotImplementedError
68
80
 
69
81
  @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:
82
+ def merge_coverage(self,
83
+ coverage_files: list[Path],
84
+ target_dir: Path,
85
+ target_file: Optional[Path],
86
+ *extras: tuple[str, Optional[str]]) -> Command:
72
87
  """Builds a command to merge coverage files."""
73
88
  raise NotImplementedError
74
89
 
75
- def reset_coverage(self, session: str, *extras: Tuple[str, Optional[str]]) -> Command:
90
+ def reset_coverage(self,
91
+ session: str,
92
+ *extras: tuple[str, Optional[str]]) -> Command:
76
93
  """
77
94
  Builds a command to reset coverage data.
78
95
  Uses next_command chaining to snapshot with --reset flag and then clean up temp file.
@@ -81,7 +98,7 @@ class Tool(ABC):
81
98
  :returns: A Command with chained cleanup command via next_command.
82
99
  """
83
100
  target_dir = Path(tempfile.gettempdir()) / r"crossfit"
84
- extras = extras + (("--reset", None),)
101
+ extras += ("--reset", None),
85
102
 
86
103
  snapshot_command = self.snapshot_coverage(session, target_dir, None, *extras)
87
104
  cleanup_command = CommandBuilder().with_command(["rm", "-f", str(target_dir)]).build_command()
@@ -0,0 +1,23 @@
1
+ from logging import Logger
2
+ from pathlib import Path
3
+
4
+ import crossfit.refs
5
+ from crossfit.tools import Jacoco, DotnetCoverage
6
+ from crossfit.models import ToolType
7
+
8
+
9
+ def create_tool(tool_type: ToolType, tool_path: Path = None, logger: Logger = None, catch: bool = True):
10
+ """
11
+ Factory method to create a tool based on the specified type.
12
+ :param tool_type: The type of tool to create.
13
+ :param tool_path: Optional path to the tool executable.
14
+ :param logger: Optional logger instance for logging tool operations.
15
+ :param catch: Whether to catch exceptions during tool operations.
16
+ :return: An instance of the specified tool type.
17
+ """
18
+ if tool_type == ToolType.Jacoco:
19
+ return Jacoco(logger, tool_path or crossfit.refs.tools_dir, catch)
20
+ elif tool_type == ToolType.DotnetCoverage:
21
+ return DotnetCoverage(logger, tool_path or crossfit.refs.tools_dir, catch)
22
+ else:
23
+ raise ValueError(f"Unknown tool type: {tool_type}")
@@ -4,12 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "SmokeCrossFit"
7
- dynamic = ["version"]
7
+ dynamic = ["version", "dependencies"]
8
8
  description = "A unified interface for various code coverage tools (JaCoCo, dotnet-coverage)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
11
11
 
12
- [tool.setuptools-scm]
12
+ [tool.setuptools.dynamic]
13
+ dependencies = { file = ["requirements.txt"] }
14
+
15
+ [tool.setuptools_scm]
13
16
 
14
17
  [tool.setuptools.packages.find]
15
18
  include = ["crossfit*"]
@@ -1,8 +1,10 @@
1
1
  import logging
2
2
  import sys
3
+ import pytest
4
+
3
5
  from pathlib import Path
6
+ from crossfit import LocalExecutor
4
7
 
5
- import pytest
6
8
 
7
9
  @pytest.fixture(scope="session")
8
10
  def tests_dir_path() -> Path:
@@ -17,3 +19,7 @@ def logger() -> logging.Logger:
17
19
  lg.addHandler(handler)
18
20
  lg.setLevel(logging.DEBUG)
19
21
  return lg
22
+
23
+ @pytest.fixture
24
+ def local_executor(logger):
25
+ return LocalExecutor(logger, False, **{"check": True})
@@ -0,0 +1,142 @@
1
+ import pytest
2
+ import subprocess
3
+ import shutil
4
+ import os
5
+ from pathlib import Path
6
+ from crossfit import DotnetCoverage, Command
7
+ from crossfit.executors import LocalExecutor
8
+ from crossfit.models import CommandResult, ReportFormat
9
+
10
+ # Path where global dotnet tools are installed
11
+ DOTNET_TOOLS_PATH = os.path.expanduser("~/.dotnet/tools")
12
+
13
+
14
+ def _get_dotnet_path():
15
+ """Find dotnet executable in PATH or common installation locations."""
16
+ # First try to find dotnet in PATH
17
+ dotnet_in_path = shutil.which("dotnet")
18
+ if dotnet_in_path:
19
+ return dotnet_in_path
20
+
21
+ # Fallback to common installation paths
22
+ common_paths = [
23
+ "/usr/local/share/dotnet/dotnet", # macOS
24
+ "/usr/share/dotnet/dotnet", # Linux
25
+ "/usr/bin/dotnet", # Linux alternative
26
+ ]
27
+ for path in common_paths:
28
+ if os.path.isfile(path) and os.access(path, os.X_OK):
29
+ return path
30
+
31
+ raise RuntimeError("dotnet executable not found. Please install .NET SDK.")
32
+
33
+
34
+ def _get_dotnet_major_version(dotnet_path):
35
+ """Get the major version of the .NET SDK."""
36
+ try:
37
+ result = subprocess.run([dotnet_path, "--version"], capture_output=True, text=True, check=True)
38
+ version_str = result.stdout.strip()
39
+ major_version = int(version_str.split('.')[0])
40
+ return major_version
41
+ except Exception:
42
+ return None
43
+
44
+
45
+ def _get_tool_versions(dotnet_path):
46
+ """Get compatible tool versions based on .NET SDK version."""
47
+ major_version = _get_dotnet_major_version(dotnet_path)
48
+
49
+ # For .NET 7 or older, use older compatible versions
50
+ if major_version is not None and major_version < 8:
51
+ return {
52
+ "dotnet-coverage": "17.13.0",
53
+ "dotnet-reportgenerator-globaltool": "5.2.5"
54
+ }
55
+ # For .NET 8+, let dotnet choose the latest compatible versions
56
+ return None
57
+
58
+
59
+ @pytest.fixture(scope="module")
60
+ def install_dotnet_tools():
61
+ """Install required dotnet tools before tests and uninstall after."""
62
+ dotnet_path = _get_dotnet_path()
63
+ tool_versions = _get_tool_versions(dotnet_path)
64
+
65
+ # Install the tools
66
+ if tool_versions:
67
+ subprocess.run([dotnet_path, "tool", "install", "--global", "dotnet-coverage",
68
+ "--version", tool_versions["dotnet-coverage"]], check=False)
69
+ subprocess.run([dotnet_path, "tool", "install", "--global", "dotnet-reportgenerator-globaltool",
70
+ "--version", tool_versions["dotnet-reportgenerator-globaltool"]], check=False)
71
+ else:
72
+ subprocess.run([dotnet_path, "tool", "install", "--global", "dotnet-coverage"], check=False)
73
+ subprocess.run([dotnet_path, "tool", "install", "--global", "dotnet-reportgenerator-globaltool"], check=False)
74
+
75
+ yield
76
+
77
+ # Cleanup: uninstall the tools after tests
78
+ subprocess.run([dotnet_path, "tool", "uninstall", "--global", "dotnet-coverage"], check=False)
79
+ subprocess.run([dotnet_path, "tool", "uninstall", "--global", "dotnet-reportgenerator-globaltool"], check=False)
80
+
81
+
82
+ @pytest.fixture
83
+ def dotnetcoverage_tool(logger, install_dotnet_tools):
84
+ return DotnetCoverage(logger, catch=True)
85
+
86
+
87
+ @pytest.fixture
88
+ def local_executor(logger, install_dotnet_tools):
89
+ # Add dotnet tools directory to PATH for subprocess execution
90
+ env = os.environ.copy()
91
+ env["PATH"] = f"{DOTNET_TOOLS_PATH}:{env.get('PATH', '')}"
92
+ return LocalExecutor(logger, False, **{"check": True, "env": env})
93
+
94
+
95
+ @pytest.fixture
96
+ def coverage_files(tests_dir_path):
97
+ return [Path(tests_dir_path / r"helpers/tools/dotnetcoverage/s1.cobertura.xml"),
98
+ Path(tests_dir_path / r"helpers/tools/dotnetcoverage/s2.cobertura.xml")]
99
+
100
+
101
+ @pytest.fixture
102
+ def target_dir(tests_dir_path):
103
+ target = Path(tests_dir_path / r"helpers/tools/dotnetcoverage/output/")
104
+ target.mkdir(parents=True, exist_ok=True)
105
+ return target
106
+
107
+
108
+ @pytest.fixture
109
+ def sourcecode_dir(tests_dir_path):
110
+ return Path(tests_dir_path / r"helpers/tools/dotnetcoverage/sourcecode/")
111
+
112
+
113
+ @pytest.mark.parametrize("report_format, report_formats, expected_return_code", [
114
+ (None, [ReportFormat.Html, ReportFormat.Xml], 0),
115
+ (ReportFormat.Html, None, 0),
116
+ (ReportFormat.Xml, [], 0),
117
+ ], ids=[
118
+ "Csv_Report_With_Html_And_Xml",
119
+ "Html_Report",
120
+ "Xml_Report_Only",
121
+ ])
122
+ def test_dotnetcoverage_execute_report(dotnetcoverage_tool, local_executor, coverage_files, target_dir, sourcecode_dir,
123
+ report_format, report_formats, expected_return_code):
124
+ command = dotnetcoverage_tool.save_report(
125
+ coverage_files, target_dir, sourcecode_dir, report_format, report_formats)
126
+
127
+ assert isinstance(command, Command)
128
+
129
+ result = local_executor.execute(command)
130
+ assert isinstance(result, CommandResult)
131
+ assert result.code == expected_return_code
132
+
133
+
134
+ def test_dotnetcoverage_merge_coverage(dotnetcoverage_tool, local_executor, coverage_files, target_dir):
135
+ target_file = Path("merged")
136
+
137
+ command = dotnetcoverage_tool.merge_coverage(coverage_files, target_dir, target_file)
138
+ assert isinstance(command, Command)
139
+
140
+ result = local_executor.execute(command)
141
+ assert isinstance(result, CommandResult)
142
+ assert result.code == 0
@@ -25,7 +25,9 @@ def coverage_files(tests_dir_path):
25
25
 
26
26
  @pytest.fixture
27
27
  def target_dir(tests_dir_path):
28
- return Path(str(os.path.relpath(tests_dir_path / r"helpers/tools/jacoco/output/")))
28
+ target = Path(str(os.path.relpath(tests_dir_path / r"helpers/tools/jacoco/output/")))
29
+ target.mkdir(parents=True, exist_ok=True)
30
+ return target
29
31
 
30
32
 
31
33
  @pytest.fixture
@@ -54,9 +56,9 @@ def test_jacoco_execute_report(jacoco_tool, local_executor, coverage_files, targ
54
56
  command = jacoco_tool.save_report(
55
57
  coverage_files,
56
58
  target_dir,
59
+ sourcecode_dir,
57
60
  report_format,
58
61
  report_formats,
59
- sourcecode_dir,
60
62
  classfiles_dir,
61
63
  *extras
62
64
  )
@@ -68,7 +70,7 @@ def test_jacoco_execute_report(jacoco_tool, local_executor, coverage_files, targ
68
70
  assert result.code == expected_return_code
69
71
 
70
72
 
71
- def test_jacoco_execute_report_files_wildcard(jacoco_tool, local_executor, target_dir, sourcecode_dir, classfiles_dir):
73
+ def test_jacoco_execute_report_files_wildcard(jacoco_tool, local_executor, target_dir, sourcecode_dir, classfiles_dir, tests_dir_path):
72
74
  report_format = ReportFormat.Csv
73
75
  coverage_files = [Path(tests_dir_path / r"helpers/tools/jacoco/*.exec")]
74
76
  report_formats = [ReportFormat.Html, ReportFormat.Xml]
@@ -77,9 +79,9 @@ def test_jacoco_execute_report_files_wildcard(jacoco_tool, local_executor, targe
77
79
  command = jacoco_tool.save_report(
78
80
  coverage_files,
79
81
  target_dir,
82
+ sourcecode_dir,
80
83
  report_format,
81
84
  report_formats,
82
- sourcecode_dir,
83
85
  classfiles_dir,
84
86
  *extras
85
87
  )
@@ -1,9 +1,8 @@
1
1
  import copy
2
- from pathlib import Path
3
-
4
2
  import pytest
5
3
 
6
- from crossfit import Command
4
+ from pathlib import Path
5
+ from crossfit.commands.command import Command
7
6
 
8
7
 
9
8
  class TestCommand:
@@ -1,8 +1,9 @@
1
1
  import os.path
2
- from pathlib import Path
3
2
  import pytest
3
+
4
+ from pathlib import Path
4
5
  from typeguard import TypeCheckError
5
- from crossfit import Command
6
+ from crossfit.commands.command import Command
6
7
  from crossfit.commands.command_builder import CommandBuilder
7
8
 
8
9
 
@@ -2,7 +2,6 @@ from pathlib import Path
2
2
  import pytest
3
3
  import crossfit
4
4
  from crossfit import DotnetCoverage, Command
5
- from crossfit.executors import LocalExecutor
6
5
  from crossfit.models import CommandResult, ReportFormat
7
6
 
8
7
 
@@ -11,11 +10,6 @@ def dotnetcoverage_tool(logger):
11
10
  return DotnetCoverage(logger, crossfit.refs.tools_dir, True)
12
11
 
13
12
 
14
- @pytest.fixture
15
- def local_executor(logger):
16
- return LocalExecutor(logger, False, **{"check": True})
17
-
18
-
19
13
  @pytest.fixture
20
14
  def coverage_files(tests_dir_path):
21
15
  return [Path(tests_dir_path / r"helpers/tools/dotnetcoverage/s1.cobertura.xml"),
@@ -3,7 +3,6 @@ from pathlib import Path
3
3
  import pytest
4
4
  import crossfit
5
5
  from crossfit import Jacoco, Command
6
- from crossfit.executors.local_executor import LocalExecutor
7
6
  from crossfit.models import CommandResult, ReportFormat
8
7
 
9
8
 
@@ -12,11 +11,6 @@ def jacoco_tool(logger):
12
11
  return Jacoco(logger, crossfit.refs.tools_dir, True)
13
12
 
14
13
 
15
- @pytest.fixture
16
- def local_executor(logger):
17
- return LocalExecutor(logger, False, **{"check": True})
18
-
19
-
20
14
  @pytest.fixture
21
15
  def coverage_files(tests_dir_path):
22
16
  return [
@@ -61,9 +55,9 @@ def test_jacoco_execute_report(
61
55
  command = jacoco_tool.save_report(
62
56
  coverage_files,
63
57
  target_dir,
58
+ sourcecode_dir,
64
59
  report_format,
65
60
  report_formats,
66
- sourcecode_dir,
67
61
  classfiles_dir,
68
62
  )
69
63
  assert isinstance(command, Command)
@@ -1,13 +1,13 @@
1
1
  # test_tool.py
2
2
  import tempfile
3
+ import pytest
4
+
3
5
  from pathlib import Path
4
6
  from typing import List, Optional, Tuple
5
7
 
6
- import pytest
7
-
8
8
  from crossfit.commands.command import Command
9
9
  from crossfit.commands.command_builder import CommandBuilder
10
- from crossfit.models.tool_models import ToolType
10
+ from crossfit.models.tool_models import ToolType, ReportFormat
11
11
  from crossfit.tools.tool import Tool
12
12
 
13
13
 
@@ -18,9 +18,9 @@ class ConcreteTool(Tool):
18
18
  def __init__(self, logger, path: Optional[Path] = None, catch: bool = True):
19
19
  super().__init__(logger, path, catch)
20
20
 
21
- def save_report(self, coverage_files: List[Path], target_dir: Path, report_format, report_formats,
22
- sourcecode_dir: Optional[Path], build_dir: Optional[Path],
23
- *extras: Tuple[str, Optional[str]]) -> Command:
21
+ def save_report(self, coverage_files: list[Path], target_dir: Path, sourcecode_dir: Optional[Path] = None,
22
+ report_format: ReportFormat = None, report_formats: list[ReportFormat] = None,
23
+ *extras: tuple[str, Optional[str]]) -> Command:
24
24
  return self._create_command_builder("report", None, coverage_files, *extras).build_command()
25
25
 
26
26
  def snapshot_coverage(self, session: str, target_dir: Path, target_file: Optional[str],
@@ -189,7 +189,7 @@ class TestToolAbstractMethods:
189
189
 
190
190
  def test_save_report_returns_command(self, tool):
191
191
  """Test that save_report returns a Command."""
192
- command = tool.save_report([], Path("/output"), None, None, None, None)
192
+ command = tool.save_report([], Path("/output"), None, None, None)
193
193
  assert isinstance(command, Command)
194
194
 
195
195
  def test_snapshot_coverage_returns_command(self, tool):
@@ -1,3 +0,0 @@
1
- from .command import Command
2
-
3
- __all__ = ['Command']
@@ -1,8 +0,0 @@
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}")
@@ -1,20 +0,0 @@
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}")
@@ -1,63 +0,0 @@
1
- import pytest
2
- from pathlib import Path
3
- from crossfit import DotnetCoverage, Command
4
- from crossfit.executors import LocalExecutor
5
- from crossfit.models import CommandResult, ReportFormat
6
-
7
-
8
- @pytest.fixture
9
- def dotnetcoverage_tool(logger):
10
- return DotnetCoverage(logger, catch=True)
11
-
12
-
13
- @pytest.fixture
14
- def local_executor(logger):
15
- return LocalExecutor(logger, False, **{"check": True})
16
-
17
-
18
- @pytest.fixture
19
- def coverage_files(tests_dir_path):
20
- return [Path(tests_dir_path / r"helpers/tools/dotnetcoverage/s1.cobertura.xml"),
21
- Path(tests_dir_path / r"helpers/tools/dotnetcoverage/s2.cobertura.xml")]
22
-
23
-
24
- @pytest.fixture
25
- def target_dir(tests_dir_path):
26
- return Path(tests_dir_path / r"helpers/tools/dotnetcoverage/output/")
27
-
28
-
29
- @pytest.fixture
30
- def sourcecode_dir(tests_dir_path):
31
- return Path(tests_dir_path / r"helpers/tools/dotnetcoverage/sourcecode/")
32
-
33
-
34
- @pytest.mark.parametrize("report_format, report_formats, expected_return_code", [
35
- (None, [ReportFormat.Html, ReportFormat.Xml], 0),
36
- (ReportFormat.Html, None, 0),
37
- (ReportFormat.Xml, [], 0),
38
- ], ids=[
39
- "Csv_Report_With_Html_And_Xml",
40
- "Html_Report",
41
- "Xml_Report_Only",
42
- ])
43
- def test_dotnetcoverage_execute_report(dotnetcoverage_tool, local_executor, coverage_files, target_dir, sourcecode_dir,
44
- report_format, report_formats, expected_return_code):
45
- command = dotnetcoverage_tool.save_report(
46
- coverage_files, target_dir, sourcecode_dir, report_format, report_formats)
47
-
48
- assert isinstance(command, Command)
49
-
50
- result = local_executor.execute(command)
51
- assert isinstance(result, CommandResult)
52
- assert result.code == expected_return_code
53
-
54
-
55
- def test_dotnetcoverage_merge_coverage(dotnetcoverage_tool, local_executor, coverage_files, target_dir):
56
- target_file = Path("merged")
57
-
58
- command = dotnetcoverage_tool.merge_coverage(coverage_files, target_dir, target_file)
59
- assert isinstance(command, Command)
60
-
61
- result = local_executor.execute(command)
62
- assert isinstance(result, CommandResult)
63
- assert result.code == 0
File without changes
File without changes