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.
- codecov_cli/__init__.py +3 -0
- codecov_cli/commands/__init__.py +0 -0
- codecov_cli/commands/base_picking.py +75 -0
- codecov_cli/commands/commit.py +72 -0
- codecov_cli/commands/create_report_result.py +41 -0
- codecov_cli/commands/empty_upload.py +80 -0
- codecov_cli/commands/get_report_results.py +50 -0
- codecov_cli/commands/labelanalysis.py +269 -0
- codecov_cli/commands/process_test_results.py +273 -0
- codecov_cli/commands/report.py +65 -0
- codecov_cli/commands/send_notifications.py +46 -0
- codecov_cli/commands/staticanalysis.py +62 -0
- codecov_cli/commands/upload.py +316 -0
- codecov_cli/commands/upload_coverage.py +186 -0
- codecov_cli/commands/upload_process.py +133 -0
- codecov_cli/fallbacks.py +41 -0
- codecov_cli/helpers/__init__.py +0 -0
- codecov_cli/helpers/args.py +31 -0
- codecov_cli/helpers/ci_adapters/__init__.py +63 -0
- codecov_cli/helpers/ci_adapters/appveyor_ci.py +54 -0
- codecov_cli/helpers/ci_adapters/azure_pipelines.py +44 -0
- codecov_cli/helpers/ci_adapters/base.py +102 -0
- codecov_cli/helpers/ci_adapters/bitbucket_ci.py +42 -0
- codecov_cli/helpers/ci_adapters/bitrise_ci.py +37 -0
- codecov_cli/helpers/ci_adapters/buildkite.py +45 -0
- codecov_cli/helpers/ci_adapters/circleci.py +47 -0
- codecov_cli/helpers/ci_adapters/cirrus_ci.py +36 -0
- codecov_cli/helpers/ci_adapters/cloudbuild.py +70 -0
- codecov_cli/helpers/ci_adapters/codebuild.py +49 -0
- codecov_cli/helpers/ci_adapters/droneci.py +36 -0
- codecov_cli/helpers/ci_adapters/github_actions.py +90 -0
- codecov_cli/helpers/ci_adapters/gitlab_ci.py +56 -0
- codecov_cli/helpers/ci_adapters/heroku.py +36 -0
- codecov_cli/helpers/ci_adapters/jenkins.py +38 -0
- codecov_cli/helpers/ci_adapters/local.py +39 -0
- codecov_cli/helpers/ci_adapters/teamcity.py +37 -0
- codecov_cli/helpers/ci_adapters/travis_ci.py +44 -0
- codecov_cli/helpers/ci_adapters/woodpeckerci.py +36 -0
- codecov_cli/helpers/config.py +66 -0
- codecov_cli/helpers/encoder.py +49 -0
- codecov_cli/helpers/folder_searcher.py +114 -0
- codecov_cli/helpers/git.py +97 -0
- codecov_cli/helpers/git_services/__init__.py +14 -0
- codecov_cli/helpers/git_services/github.py +40 -0
- codecov_cli/helpers/glob.py +146 -0
- codecov_cli/helpers/logging_utils.py +77 -0
- codecov_cli/helpers/options.py +51 -0
- codecov_cli/helpers/request.py +198 -0
- codecov_cli/helpers/upload_type.py +15 -0
- codecov_cli/helpers/validators.py +13 -0
- codecov_cli/helpers/versioning_systems.py +201 -0
- codecov_cli/main.py +99 -0
- codecov_cli/opentelemetry.py +26 -0
- codecov_cli/plugins/__init__.py +92 -0
- codecov_cli/plugins/compress_pycoverage_contexts.py +141 -0
- codecov_cli/plugins/gcov.py +69 -0
- codecov_cli/plugins/pycoverage.py +134 -0
- codecov_cli/plugins/types.py +8 -0
- codecov_cli/plugins/xcode.py +117 -0
- codecov_cli/runners/__init__.py +80 -0
- codecov_cli/runners/dan_runner.py +64 -0
- codecov_cli/runners/pytest_standard_runner.py +184 -0
- codecov_cli/runners/types.py +33 -0
- codecov_cli/services/__init__.py +0 -0
- codecov_cli/services/commit/__init__.py +86 -0
- codecov_cli/services/commit/base_picking.py +24 -0
- codecov_cli/services/empty_upload/__init__.py +42 -0
- codecov_cli/services/report/__init__.py +169 -0
- codecov_cli/services/upload/__init__.py +169 -0
- codecov_cli/services/upload/file_finder.py +320 -0
- codecov_cli/services/upload/legacy_upload_sender.py +132 -0
- codecov_cli/services/upload/network_finder.py +49 -0
- codecov_cli/services/upload/upload_collector.py +198 -0
- codecov_cli/services/upload/upload_sender.py +232 -0
- codecov_cli/services/upload_completion/__init__.py +38 -0
- codecov_cli/services/upload_coverage/__init__.py +93 -0
- codecov_cli/types.py +88 -0
- codecov_cli-11.0.0.dist-info/METADATA +298 -0
- codecov_cli-11.0.0.dist-info/RECORD +83 -0
- codecov_cli-11.0.0.dist-info/WHEEL +5 -0
- codecov_cli-11.0.0.dist-info/entry_points.txt +3 -0
- codecov_cli-11.0.0.dist-info/licenses/LICENSE +201 -0
- 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])
|