codecov-cli 11.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.
Files changed (83) hide show
  1. codecov_cli/__init__.py +3 -0
  2. codecov_cli/commands/__init__.py +0 -0
  3. codecov_cli/commands/base_picking.py +75 -0
  4. codecov_cli/commands/commit.py +72 -0
  5. codecov_cli/commands/create_report_result.py +41 -0
  6. codecov_cli/commands/empty_upload.py +80 -0
  7. codecov_cli/commands/get_report_results.py +50 -0
  8. codecov_cli/commands/labelanalysis.py +269 -0
  9. codecov_cli/commands/process_test_results.py +273 -0
  10. codecov_cli/commands/report.py +65 -0
  11. codecov_cli/commands/send_notifications.py +46 -0
  12. codecov_cli/commands/staticanalysis.py +62 -0
  13. codecov_cli/commands/upload.py +316 -0
  14. codecov_cli/commands/upload_coverage.py +186 -0
  15. codecov_cli/commands/upload_process.py +133 -0
  16. codecov_cli/fallbacks.py +41 -0
  17. codecov_cli/helpers/__init__.py +0 -0
  18. codecov_cli/helpers/args.py +31 -0
  19. codecov_cli/helpers/ci_adapters/__init__.py +63 -0
  20. codecov_cli/helpers/ci_adapters/appveyor_ci.py +54 -0
  21. codecov_cli/helpers/ci_adapters/azure_pipelines.py +44 -0
  22. codecov_cli/helpers/ci_adapters/base.py +102 -0
  23. codecov_cli/helpers/ci_adapters/bitbucket_ci.py +42 -0
  24. codecov_cli/helpers/ci_adapters/bitrise_ci.py +37 -0
  25. codecov_cli/helpers/ci_adapters/buildkite.py +45 -0
  26. codecov_cli/helpers/ci_adapters/circleci.py +47 -0
  27. codecov_cli/helpers/ci_adapters/cirrus_ci.py +36 -0
  28. codecov_cli/helpers/ci_adapters/cloudbuild.py +70 -0
  29. codecov_cli/helpers/ci_adapters/codebuild.py +49 -0
  30. codecov_cli/helpers/ci_adapters/droneci.py +36 -0
  31. codecov_cli/helpers/ci_adapters/github_actions.py +90 -0
  32. codecov_cli/helpers/ci_adapters/gitlab_ci.py +56 -0
  33. codecov_cli/helpers/ci_adapters/heroku.py +36 -0
  34. codecov_cli/helpers/ci_adapters/jenkins.py +38 -0
  35. codecov_cli/helpers/ci_adapters/local.py +39 -0
  36. codecov_cli/helpers/ci_adapters/teamcity.py +37 -0
  37. codecov_cli/helpers/ci_adapters/travis_ci.py +44 -0
  38. codecov_cli/helpers/ci_adapters/woodpeckerci.py +36 -0
  39. codecov_cli/helpers/config.py +66 -0
  40. codecov_cli/helpers/encoder.py +49 -0
  41. codecov_cli/helpers/folder_searcher.py +114 -0
  42. codecov_cli/helpers/git.py +97 -0
  43. codecov_cli/helpers/git_services/__init__.py +14 -0
  44. codecov_cli/helpers/git_services/github.py +40 -0
  45. codecov_cli/helpers/glob.py +146 -0
  46. codecov_cli/helpers/logging_utils.py +77 -0
  47. codecov_cli/helpers/options.py +51 -0
  48. codecov_cli/helpers/request.py +198 -0
  49. codecov_cli/helpers/upload_type.py +15 -0
  50. codecov_cli/helpers/validators.py +13 -0
  51. codecov_cli/helpers/versioning_systems.py +201 -0
  52. codecov_cli/main.py +99 -0
  53. codecov_cli/opentelemetry.py +26 -0
  54. codecov_cli/plugins/__init__.py +92 -0
  55. codecov_cli/plugins/compress_pycoverage_contexts.py +141 -0
  56. codecov_cli/plugins/gcov.py +69 -0
  57. codecov_cli/plugins/pycoverage.py +134 -0
  58. codecov_cli/plugins/types.py +8 -0
  59. codecov_cli/plugins/xcode.py +117 -0
  60. codecov_cli/runners/__init__.py +80 -0
  61. codecov_cli/runners/dan_runner.py +64 -0
  62. codecov_cli/runners/pytest_standard_runner.py +184 -0
  63. codecov_cli/runners/types.py +33 -0
  64. codecov_cli/services/__init__.py +0 -0
  65. codecov_cli/services/commit/__init__.py +86 -0
  66. codecov_cli/services/commit/base_picking.py +24 -0
  67. codecov_cli/services/empty_upload/__init__.py +42 -0
  68. codecov_cli/services/report/__init__.py +169 -0
  69. codecov_cli/services/upload/__init__.py +169 -0
  70. codecov_cli/services/upload/file_finder.py +320 -0
  71. codecov_cli/services/upload/legacy_upload_sender.py +132 -0
  72. codecov_cli/services/upload/network_finder.py +49 -0
  73. codecov_cli/services/upload/upload_collector.py +198 -0
  74. codecov_cli/services/upload/upload_sender.py +232 -0
  75. codecov_cli/services/upload_completion/__init__.py +38 -0
  76. codecov_cli/services/upload_coverage/__init__.py +93 -0
  77. codecov_cli/types.py +88 -0
  78. codecov_cli-11.0.0.dist-info/METADATA +298 -0
  79. codecov_cli-11.0.0.dist-info/RECORD +83 -0
  80. codecov_cli-11.0.0.dist-info/WHEEL +5 -0
  81. codecov_cli-11.0.0.dist-info/entry_points.txt +3 -0
  82. codecov_cli-11.0.0.dist-info/licenses/LICENSE +201 -0
  83. codecov_cli-11.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,201 @@
1
+ from itertools import chain
2
+ import logging
3
+ import re
4
+ import subprocess
5
+ import typing as t
6
+ from pathlib import Path
7
+ from shutil import which
8
+
9
+ from codecov_cli.fallbacks import FallbackFieldEnum
10
+ from codecov_cli.helpers.folder_searcher import search_files
11
+ from codecov_cli.helpers.git import parse_git_service, parse_slug
12
+ from abc import ABC, abstractmethod
13
+
14
+ logger = logging.getLogger("codecovcli")
15
+
16
+ IGNORE_DIRS = [
17
+ "*.egg-info",
18
+ ".DS_Store",
19
+ ".circleci",
20
+ ".env",
21
+ ".envs",
22
+ ".git",
23
+ ".gitignore",
24
+ ".mypy_cache",
25
+ ".nvmrc",
26
+ ".nyc_output",
27
+ ".ruff_cache",
28
+ ".venv",
29
+ ".venvns",
30
+ ".virtualenv",
31
+ ".virtualenvs",
32
+ "__pycache__",
33
+ "bower_components",
34
+ "build/lib/",
35
+ "jspm_packages",
36
+ "node_modules",
37
+ "vendor",
38
+ "virtualenv",
39
+ "virtualenvs",
40
+ ]
41
+
42
+ IGNORE_PATHS = [
43
+ "*.gif",
44
+ "*.jpeg",
45
+ "*.jpg",
46
+ "*.md",
47
+ "*.png",
48
+ "shunit2*",
49
+ ]
50
+
51
+
52
+ class VersioningSystemInterface(ABC):
53
+ def __repr__(self) -> str:
54
+ return str(type(self))
55
+
56
+ @abstractmethod
57
+ def get_fallback_value(self, fallback_field: FallbackFieldEnum) -> t.Optional[str]:
58
+ pass
59
+
60
+ @abstractmethod
61
+ def get_network_root(self) -> t.Optional[Path]:
62
+ pass
63
+
64
+ @abstractmethod
65
+ def list_relevant_files(
66
+ self, directory: t.Optional[Path] = None, recurse_submodules: bool = False
67
+ ) -> t.Optional[t.List[str]]:
68
+ pass
69
+
70
+
71
+ def get_versioning_system() -> t.Optional[VersioningSystemInterface]:
72
+ for klass in [GitVersioningSystem, NoVersioningSystem]:
73
+ if klass.is_available():
74
+ logger.debug(f"versioning system found: {klass}")
75
+ return klass()
76
+
77
+
78
+ class GitVersioningSystem(VersioningSystemInterface):
79
+ @classmethod
80
+ def is_available(cls):
81
+ if which("git") is not None:
82
+ p = subprocess.run(
83
+ ["git", "rev-parse", "--show-toplevel"], capture_output=True
84
+ )
85
+ if p.stdout:
86
+ return True
87
+ return False
88
+
89
+ def get_fallback_value(self, fallback_field: FallbackFieldEnum):
90
+ if fallback_field == FallbackFieldEnum.commit_sha:
91
+ # here we will get the commit SHA of the latest commit
92
+ # that is NOT a merge commit
93
+ p = subprocess.run(
94
+ # List current commit parent's SHA
95
+ ["git", "rev-parse", "HEAD^@"],
96
+ capture_output=True,
97
+ )
98
+ parents_hash = p.stdout.decode().strip().splitlines()
99
+ if len(parents_hash) == 2:
100
+ # IFF the current commit is a merge commit it will have 2 parents
101
+ # We return the 2nd one - The commit that came from the branch merged into ours
102
+ return parents_hash[1]
103
+ # At this point we know the current commit is not a merge commit
104
+ # so we get it's SHA and return that
105
+ p = subprocess.run(["git", "log", "-1", "--format=%H"], capture_output=True)
106
+ if p.stdout:
107
+ return p.stdout.decode().strip()
108
+
109
+ if fallback_field == FallbackFieldEnum.branch:
110
+ p = subprocess.run(
111
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True
112
+ )
113
+ if p.stdout:
114
+ branch_name = p.stdout.decode().strip()
115
+ # branch_name will be 'HEAD' if we are in 'detached HEAD' state
116
+ return branch_name if branch_name != "HEAD" else None
117
+
118
+ if fallback_field == FallbackFieldEnum.slug:
119
+ # if there are multiple remotes, we will prioritize using the one called 'origin' if it exists, else we will use the first one in 'git remote' list
120
+
121
+ p = subprocess.run(["git", "remote"], capture_output=True)
122
+
123
+ if not p.stdout:
124
+ return None
125
+
126
+ remotes = p.stdout.decode().strip().splitlines()
127
+
128
+ remote_name = "origin" if "origin" in remotes else remotes[0]
129
+
130
+ p = subprocess.run(
131
+ ["git", "ls-remote", "--get-url", remote_name], capture_output=True
132
+ )
133
+ if not p.stdout:
134
+ return None
135
+
136
+ remote_url = p.stdout.decode().strip()
137
+
138
+ return parse_slug(remote_url)
139
+
140
+ if fallback_field == FallbackFieldEnum.git_service:
141
+ # if there are multiple remotes, we will prioritize using the one called 'origin' if it exists, else we will use the first one in 'git remote' list
142
+
143
+ p = subprocess.run(["git", "remote"], capture_output=True)
144
+ if not p.stdout:
145
+ return None
146
+
147
+ remotes = p.stdout.decode().strip().splitlines()
148
+ remote_name = "origin" if "origin" in remotes else remotes[0]
149
+ p = subprocess.run(
150
+ ["git", "ls-remote", "--get-url", remote_name], capture_output=True
151
+ )
152
+ if not p.stdout:
153
+ return None
154
+
155
+ remote_url = p.stdout.decode().strip()
156
+ return parse_git_service(remote_url)
157
+
158
+ return None
159
+
160
+ def get_network_root(self):
161
+ p = subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True)
162
+ if p.stdout:
163
+ return Path(p.stdout.decode().rstrip())
164
+ return None
165
+
166
+ def list_relevant_files(
167
+ self, directory: t.Optional[Path] = None, recurse_submodules: bool = False
168
+ ) -> t.List[str]:
169
+ dir_to_use = directory or self.get_network_root()
170
+ if dir_to_use is None:
171
+ raise ValueError("Can't determine root folder")
172
+
173
+ cmd = ["git", "-C", str(dir_to_use), "ls-files", "-z"]
174
+ if recurse_submodules:
175
+ cmd.append("--recurse-submodules")
176
+ res = subprocess.run(cmd, capture_output=True)
177
+ return res.stdout.decode().split("\0")
178
+
179
+
180
+ class NoVersioningSystem(VersioningSystemInterface):
181
+ @classmethod
182
+ def is_available(cls):
183
+ return True
184
+
185
+ def get_network_root(self):
186
+ return Path.cwd()
187
+
188
+ def get_fallback_value(self, fallback_field: FallbackFieldEnum):
189
+ return None
190
+
191
+ def list_relevant_files(
192
+ self, directory: t.Optional[Path] = None, recurse_submodules: bool = False
193
+ ) -> t.List[str]:
194
+ dir_to_use = directory or self.get_network_root()
195
+ if dir_to_use is None:
196
+ raise ValueError("Can't determine root folder")
197
+
198
+ files = search_files(
199
+ dir_to_use, folders_to_ignore=[], filename_include_regex=re.compile("")
200
+ )
201
+ return [f.relative_to(dir_to_use).as_posix() for f in files]
codecov_cli/main.py ADDED
@@ -0,0 +1,99 @@
1
+ import logging
2
+ import pathlib
3
+ import typing
4
+
5
+ import click
6
+
7
+ from codecov_cli import __version__
8
+ from codecov_cli.opentelemetry import init_telem
9
+ from codecov_cli.commands.base_picking import pr_base_picking
10
+ from codecov_cli.commands.commit import create_commit
11
+ from codecov_cli.commands.create_report_result import create_report_results
12
+ from codecov_cli.commands.empty_upload import empty_upload
13
+ from codecov_cli.commands.get_report_results import get_report_results
14
+ from codecov_cli.commands.labelanalysis import label_analysis
15
+ from codecov_cli.commands.process_test_results import process_test_results
16
+ from codecov_cli.commands.report import create_report
17
+ from codecov_cli.commands.send_notifications import send_notifications
18
+ from codecov_cli.commands.staticanalysis import static_analysis
19
+ from codecov_cli.commands.upload import do_upload
20
+ from codecov_cli.commands.upload_coverage import upload_coverage
21
+ from codecov_cli.commands.upload_process import upload_process
22
+ from codecov_cli.helpers.ci_adapters import get_ci_adapter, get_ci_providers_list
23
+ from codecov_cli.helpers.config import load_cli_config
24
+ from codecov_cli.helpers.logging_utils import configure_logger
25
+ from codecov_cli.helpers.versioning_systems import get_versioning_system
26
+
27
+ logger = logging.getLogger("codecovcli")
28
+
29
+
30
+ @click.group()
31
+ @click.option(
32
+ "--auto-load-params-from",
33
+ type=click.Choice(
34
+ [provider.get_service_name() for provider in get_ci_providers_list()],
35
+ case_sensitive=False,
36
+ ),
37
+ )
38
+ @click.option(
39
+ "--codecov-yml-path",
40
+ type=click.Path(path_type=pathlib.Path),
41
+ default=None,
42
+ )
43
+ @click.option(
44
+ "--enterprise-url", "--url", "-u", help="Change the upload host (Enterprise use)"
45
+ )
46
+ @click.option("-v", "--verbose", "verbose", help="Use verbose logging", is_flag=True)
47
+ @click.option(
48
+ "--disable-telem", help="Disable sending telemetry data to Codecov", is_flag=True
49
+ )
50
+ @click.pass_context
51
+ @click.version_option(__version__, prog_name="codecovcli")
52
+ def cli(
53
+ ctx: click.Context,
54
+ auto_load_params_from: typing.Optional[str],
55
+ codecov_yml_path: pathlib.Path,
56
+ enterprise_url: str,
57
+ verbose: bool = False,
58
+ disable_telem: bool = False,
59
+ ):
60
+ ctx.obj["cli_args"] = ctx.params
61
+ ctx.obj["cli_args"]["version"] = f"cli-{__version__}"
62
+ configure_logger(logger, log_level=(logging.DEBUG if verbose else logging.INFO))
63
+ ctx.help_option_names = ["-h", "--help"]
64
+ ctx.obj["ci_adapter"] = get_ci_adapter(auto_load_params_from)
65
+ ctx.obj["versioning_system"] = get_versioning_system()
66
+ ctx.obj["codecov_yaml"] = load_cli_config(codecov_yml_path)
67
+ if ctx.obj["codecov_yaml"] is None:
68
+ logger.debug("No codecov_yaml found")
69
+ elif (token := ctx.obj["codecov_yaml"].get("codecov", {}).get("token")) is not None:
70
+ ctx.default_map = {ctx.invoked_subcommand: {"token": token}}
71
+ ctx.obj["enterprise_url"] = enterprise_url
72
+ ctx.obj["disable_telem"] = disable_telem
73
+
74
+ init_telem(ctx.obj)
75
+
76
+
77
+ cli.add_command(do_upload)
78
+ cli.add_command(create_commit)
79
+ cli.add_command(create_report)
80
+ cli.add_command(pr_base_picking)
81
+ cli.add_command(empty_upload)
82
+ cli.add_command(upload_coverage)
83
+ cli.add_command(upload_process)
84
+ cli.add_command(send_notifications)
85
+ cli.add_command(process_test_results)
86
+
87
+ # deprecated commands:
88
+ cli.add_command(create_report_results)
89
+ cli.add_command(get_report_results)
90
+ cli.add_command(label_analysis)
91
+ cli.add_command(static_analysis)
92
+
93
+
94
+ def run():
95
+ cli(obj={})
96
+
97
+
98
+ if __name__ == "__main__":
99
+ run()
@@ -0,0 +1,26 @@
1
+ import os
2
+ import time
3
+
4
+ import sentry_sdk
5
+
6
+ from codecov_cli import __version__
7
+
8
+
9
+ def init_telem(ctx):
10
+ if ctx["disable_telem"]:
11
+ return
12
+ if ctx["enterprise_url"]: # dont run on dedicated cloud
13
+ return
14
+ if os.getenv("CODECOV_ENV", "production") == "test":
15
+ return
16
+
17
+ sentry_sdk.init(
18
+ dsn="https://0bea75c61745c221a6ef1ac1709b1f4d@o26192.ingest.us.sentry.io/4508615876083713",
19
+ enable_tracing=True,
20
+ environment=os.getenv("CODECOV_ENV", "production"),
21
+ release=f"cli@{__version__}",
22
+ )
23
+
24
+
25
+ def close_telem():
26
+ sentry_sdk.flush()
@@ -0,0 +1,92 @@
1
+ import logging
2
+ import typing
3
+ from importlib import import_module
4
+
5
+ import click
6
+
7
+ from codecov_cli.plugins.compress_pycoverage_contexts import CompressPycoverageContexts
8
+ from codecov_cli.plugins.gcov import GcovPlugin
9
+ from codecov_cli.plugins.pycoverage import Pycoverage
10
+ from codecov_cli.plugins.xcode import XcodePlugin
11
+
12
+ logger = logging.getLogger("codecovcli")
13
+
14
+
15
+ class NoopPlugin(object):
16
+ def run_preparation(self, collector):
17
+ pass
18
+
19
+
20
+ def select_preparation_plugins(
21
+ cli_config: typing.Dict, plugin_names: typing.List[str], plugin_config: typing.Dict
22
+ ):
23
+ plugins = [_get_plugin(cli_config, p, plugin_config) for p in plugin_names]
24
+ logger.debug(
25
+ "Selected preparation plugins",
26
+ extra=dict(
27
+ extra_log_attributes=dict(
28
+ selected_plugins=list(map(type, plugins)),
29
+ cli_config=cli_config,
30
+ )
31
+ ),
32
+ )
33
+ return plugins
34
+
35
+
36
+ def _load_plugin_from_yaml(plugin_dict: typing.Dict):
37
+ try:
38
+ module_obj = import_module(plugin_dict["module"])
39
+ class_obj = getattr(module_obj, plugin_dict["class"])
40
+ except ModuleNotFoundError:
41
+ click.secho(
42
+ f"Unable to dynamically load module {plugin_dict['module']}",
43
+ err=True,
44
+ )
45
+ return NoopPlugin()
46
+ except AttributeError:
47
+ click.secho(
48
+ f"Unable to dynamically load class {plugin_dict['class']} from module {plugin_dict['module']}",
49
+ err=True,
50
+ )
51
+ return NoopPlugin()
52
+ try:
53
+ params = plugin_dict.get("params", None)
54
+ if params:
55
+ return class_obj(**plugin_dict["params"])
56
+ else:
57
+ return class_obj()
58
+
59
+ except TypeError:
60
+ click.secho(
61
+ f"Unable to instantiate {class_obj} with provided parameters {plugin_dict.get('params', '')}",
62
+ err=True,
63
+ )
64
+ return NoopPlugin()
65
+
66
+
67
+ def _get_plugin(cli_config, plugin_name, plugin_config):
68
+ if plugin_name == "noop":
69
+ return NoopPlugin()
70
+ if plugin_name == "gcov":
71
+ return GcovPlugin(
72
+ plugin_config.get("project_root", None),
73
+ plugin_config.get("folders_to_ignore", None),
74
+ plugin_config.get("gcov_executable", "gcov"),
75
+ plugin_config.get("gcov_include", None),
76
+ plugin_config.get("gcov_ignore", None),
77
+ plugin_config.get("gcov_args", None),
78
+ )
79
+ if plugin_name == "pycoverage":
80
+ config = cli_config.get("plugins", {}).get("pycoverage", {})
81
+ return Pycoverage(config)
82
+ if plugin_name == "xcode":
83
+ return XcodePlugin(
84
+ plugin_config.get("swift_project", None),
85
+ )
86
+ if plugin_name == "compress-pycoverage":
87
+ config = cli_config.get("plugins", {}).get("compress-pycoverage", {})
88
+ return CompressPycoverageContexts(config)
89
+ if cli_config and plugin_name in cli_config.get("plugins", {}):
90
+ return _load_plugin_from_yaml(cli_config["plugins"][plugin_name])
91
+ click.secho(f"Unable to find plugin {plugin_name}", fg="magenta", err=True)
92
+ return NoopPlugin()
@@ -0,0 +1,141 @@
1
+ import json
2
+ import logging
3
+ import pathlib
4
+ from decimal import Decimal
5
+ from typing import Any, List
6
+
7
+ import ijson
8
+ import sentry_sdk
9
+
10
+ from codecov_cli.plugins.types import PreparationPluginReturn
11
+
12
+ logger = logging.getLogger("codecovcli")
13
+
14
+
15
+ class Encoder(json.JSONEncoder):
16
+ def default(self, o: Any) -> Any:
17
+ if isinstance(o, Decimal):
18
+ return str(o)
19
+ return super().default(o)
20
+
21
+
22
+ class CompressPycoverageContextsConfig(dict):
23
+ @property
24
+ def file_to_compress(self) -> pathlib.Path:
25
+ """
26
+ The report file to compress.
27
+ file_to_compress: Union[str, pathlib.Path] [default coverage.json]
28
+ """
29
+ return pathlib.Path(self.get("file_to_compress", "coverage.json"))
30
+
31
+ @property
32
+ def delete_uncompressed(self) -> bool:
33
+ """
34
+ Flag indicating to delete the original file after compressing.
35
+ Recommended to avoid uploading the uncompressed file.
36
+ delete_uncompressed: bool [default True]
37
+ """
38
+ return self.get("delete_uncompressed", True)
39
+
40
+
41
+ class CompressPycoverageContexts(object):
42
+ def __init__(self, config: dict = None) -> None:
43
+ if config is None:
44
+ config = {}
45
+ self.config = CompressPycoverageContextsConfig(config)
46
+ self.file_to_compress = self.config.file_to_compress
47
+ self.file_to_write = pathlib.Path(
48
+ str(self.file_to_compress).replace(".json", "") + ".codecov.json"
49
+ )
50
+
51
+ def run_preparation(self, collector) -> PreparationPluginReturn:
52
+ with sentry_sdk.start_span(name="compress_pycoverage"):
53
+ if not self.file_to_compress.exists():
54
+ logger.warning(
55
+ f"File to compress {self.file_to_compress} not found. Aborting"
56
+ )
57
+ return PreparationPluginReturn(
58
+ success=False,
59
+ messages=[f"File to compress {self.file_to_compress} not found."],
60
+ )
61
+ if not self.file_to_compress.is_file():
62
+ logger.warning(
63
+ f"File to compress {self.file_to_compress} is not a file. Aborting"
64
+ )
65
+ return PreparationPluginReturn(
66
+ success=False,
67
+ messages=[f"File to compress {self.file_to_compress} is not a file."],
68
+ )
69
+ # Create in and out streams
70
+ fd_in = open(self.file_to_compress, "rb")
71
+ fd_out = open(self.file_to_write, "w")
72
+ # Compress the file
73
+ fd_out.write("{")
74
+ self._copy_meta(fd_in, fd_out)
75
+ files_in_report = ijson.kvitems(fd_in, "files")
76
+ self._compress_files(files_in_report, fd_out)
77
+ fd_out.write("}")
78
+ # Close streams
79
+ fd_in.close()
80
+ fd_out.close()
81
+ logger.info(f"Compressed report written to {self.file_to_write}")
82
+ # Delete original file if needed
83
+ if self.config.delete_uncompressed:
84
+ logger.info(f"Deleting file {self.file_to_compress}")
85
+ self.file_to_compress.unlink()
86
+ return PreparationPluginReturn(success=True, messages=[])
87
+
88
+ def _compress_files(self, files_in_report, fd_out) -> None:
89
+ """
90
+ Compress the 'files' entry in the coverage data.
91
+ This is done by creating a labels table [str -> int] mapping labels to an index.
92
+ This index then substitutes the label itself in the contexts
93
+ """
94
+ labels_table = {}
95
+ nxt_idx = 0
96
+
97
+ fd_out.write('"files":{')
98
+ for file_name, file_coverage_details in files_in_report:
99
+ self._copy_file_details(file_name, file_coverage_details, fd_out)
100
+ fd_out.write('"contexts": {')
101
+ contexts = file_coverage_details["contexts"]
102
+ for line_number, labels in contexts.items():
103
+ fd_out.write(f'"{line_number}":')
104
+ new_labels = []
105
+ for label in labels:
106
+ stripped_label = label.split("|")[0] # removes '|run' from label
107
+ if stripped_label not in labels_table:
108
+ labels_table[stripped_label] = nxt_idx
109
+ nxt_idx += 1
110
+ new_labels.append(labels_table[stripped_label])
111
+ fd_out.write(json.dumps(new_labels))
112
+ # fd_out.write(self._bitmask_label_indexes(new_labels))
113
+ fd_out.write(",")
114
+ if len(contexts): # Avoid removing '{' if contexts == {}
115
+ # Because there will be an extra ',' after the last line
116
+ fd_out.seek(fd_out.tell() - 1)
117
+ # One curly brace for the 'contexts', one for the file_name
118
+ fd_out.write("}},")
119
+ # Because there will be an extra ',' after the last file_name
120
+ fd_out.seek(fd_out.tell() - 1)
121
+ fd_out.write("},")
122
+ # Save the inverted index of labels table in the report
123
+ # So when we are processing the result we have int -> label
124
+ fd_out.write(
125
+ f'"labels_table": {json.dumps({value: key for key, value in labels_table.items()})}'
126
+ )
127
+
128
+ def _copy_file_details(self, file_name, file_details, fd_out) -> None:
129
+ fd_out.write(f'"{file_name}":{{')
130
+ fd_out.write(f'"executed_lines": {file_details["executed_lines"]},')
131
+ fd_out.write(f'"summary": {json.dumps(file_details["summary"], cls=Encoder)},')
132
+ fd_out.write(f'"missing_lines": {file_details["missing_lines"]},')
133
+ fd_out.write(f'"excluded_lines": {file_details["excluded_lines"]},')
134
+
135
+ def _copy_meta(self, fd_in, fd_out) -> None:
136
+ meta = ijson.kvitems(fd_in, "")
137
+ for key, value in meta:
138
+ if key == "files":
139
+ continue
140
+ fd_out.write(f'"{key}": {json.dumps(value, cls=Encoder)},')
141
+ fd_in.seek(0)
@@ -0,0 +1,69 @@
1
+ import logging
2
+ import os
3
+ import pathlib
4
+ import shutil
5
+ import subprocess
6
+ import typing
7
+
8
+ import sentry_sdk
9
+
10
+ from codecov_cli.helpers.folder_searcher import globs_to_regex, search_files
11
+ from codecov_cli.plugins.types import PreparationPluginReturn
12
+
13
+ logger = logging.getLogger("codecovcli")
14
+
15
+
16
+ class GcovPlugin(object):
17
+ def __init__(
18
+ self,
19
+ project_root: typing.Optional[pathlib.Path] = None,
20
+ folders_to_ignore: typing.Optional[typing.List[str]] = None,
21
+ executable: typing.Optional[str] = "gcov",
22
+ patterns_to_include: typing.Optional[typing.List[str]] = None,
23
+ patterns_to_ignore: typing.Optional[typing.List[str]] = None,
24
+ extra_arguments: typing.Optional[typing.List[str]] = None,
25
+ ):
26
+ self.executable = executable or "gcov"
27
+ self.extra_arguments = extra_arguments or []
28
+ self.folders_to_ignore = folders_to_ignore or []
29
+ self.patterns_to_ignore = patterns_to_ignore or []
30
+ self.patterns_to_include = patterns_to_include or []
31
+ self.project_root = project_root or pathlib.Path(os.getcwd())
32
+
33
+ def run_preparation(self, collector) -> PreparationPluginReturn:
34
+ with sentry_sdk.start_span(name="gcov"):
35
+ logger.debug(
36
+ f"Running {self.executable} plugin...",
37
+ )
38
+
39
+ if shutil.which(self.executable) is None:
40
+ logger.warning(f"{self.executable} is not installed or can't be found.")
41
+ return
42
+
43
+ filename_include_regex = globs_to_regex(["*.gcno", *self.patterns_to_include])
44
+ filename_exclude_regex = globs_to_regex(self.patterns_to_ignore)
45
+
46
+ matched_paths = [
47
+ str(path)
48
+ for path in search_files(
49
+ self.project_root,
50
+ self.folders_to_ignore,
51
+ filename_include_regex=filename_include_regex,
52
+ filename_exclude_regex=filename_exclude_regex,
53
+ )
54
+ ]
55
+
56
+ if not matched_paths:
57
+ logger.warning(f"No {self.executable} data found.")
58
+ return
59
+
60
+ logger.warning(f"Running {self.executable} on the following list of files:")
61
+ for path in matched_paths:
62
+ logger.warning(path)
63
+
64
+ s = subprocess.run(
65
+ [self.executable, "-pb", *self.extra_arguments, *matched_paths],
66
+ cwd=self.project_root,
67
+ capture_output=True,
68
+ )
69
+ return PreparationPluginReturn(success=True, messages=[s.stdout])