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,134 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import typing
|
|
7
|
+
from glob import iglob
|
|
8
|
+
|
|
9
|
+
import sentry_sdk
|
|
10
|
+
|
|
11
|
+
from codecov_cli.helpers.folder_searcher import globs_to_regex, search_files
|
|
12
|
+
from codecov_cli.plugins.types import PreparationPluginReturn
|
|
13
|
+
|
|
14
|
+
coverage_files_regex = globs_to_regex([".coverage", ".coverage.*"])
|
|
15
|
+
logger = logging.getLogger("codecovcli")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PycoverageConfig(dict):
|
|
19
|
+
@property
|
|
20
|
+
def project_root(self) -> typing.Optional[pathlib.Path]:
|
|
21
|
+
"""
|
|
22
|
+
The project root to search for coverage files.
|
|
23
|
+
project_root: pathlib.Path [default os.getcwd()]
|
|
24
|
+
"""
|
|
25
|
+
return self.get("project_root", pathlib.Path(os.getcwd()))
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def report_type(self) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Report type to generate.
|
|
31
|
+
Overridden if include_contexts == True
|
|
32
|
+
report_type: str [values xml|json; default xml]
|
|
33
|
+
"""
|
|
34
|
+
return self.get("report_type", "xml")
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def path_to_coverage_file(self) -> str:
|
|
38
|
+
"""
|
|
39
|
+
The coverage dir with .coverage file
|
|
40
|
+
If set, will not look search for coverage files
|
|
41
|
+
"""
|
|
42
|
+
return self.get("path_to_coverage_file", None)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def include_contexts(self) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Includes test context in JSON report. Flag.
|
|
48
|
+
(test contexts are the test labels used in ATS)
|
|
49
|
+
include_contexts: bool [default True]
|
|
50
|
+
"""
|
|
51
|
+
return self.get("include_contexts", True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Pycoverage(object):
|
|
55
|
+
def __init__(self, config: dict):
|
|
56
|
+
self.config = PycoverageConfig(config)
|
|
57
|
+
|
|
58
|
+
def run_preparation(self, collector) -> PreparationPluginReturn:
|
|
59
|
+
with sentry_sdk.start_span(name="pycoverage"):
|
|
60
|
+
if shutil.which("coverage") is None:
|
|
61
|
+
logger.warning("coverage.py is not installed or can't be found.")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
path_to_coverage_data = self._get_path_to_coverage()
|
|
65
|
+
if path_to_coverage_data is None:
|
|
66
|
+
logger.warning("No coverage data found to transform")
|
|
67
|
+
return
|
|
68
|
+
coverage_dir = pathlib.Path(path_to_coverage_data).parent
|
|
69
|
+
if self.config.report_type == "xml":
|
|
70
|
+
return self._generate_XML_report(coverage_dir)
|
|
71
|
+
if self.config.report_type == "json":
|
|
72
|
+
return self._generate_JSON_report(coverage_dir)
|
|
73
|
+
return PreparationPluginReturn(
|
|
74
|
+
success=False,
|
|
75
|
+
messages=[f"report type {self.config.report_type} unknown"],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def _get_path_to_coverage(self) -> pathlib.Path:
|
|
79
|
+
if self.config.path_to_coverage_file:
|
|
80
|
+
path = pathlib.Path(self.config.path_to_coverage_file)
|
|
81
|
+
if path.exists():
|
|
82
|
+
return pathlib.Path(self.config.path_to_coverage_file)
|
|
83
|
+
logger.warning(
|
|
84
|
+
f"Dir {self.config.path_to_coverage_file} doesn't exist or doesn't have .coverage file. Falling back to search"
|
|
85
|
+
)
|
|
86
|
+
return next(
|
|
87
|
+
search_files(
|
|
88
|
+
self.config.project_root,
|
|
89
|
+
[],
|
|
90
|
+
filename_include_regex=coverage_files_regex,
|
|
91
|
+
filename_exclude_regex=None,
|
|
92
|
+
),
|
|
93
|
+
None,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _generate_XML_report(self, dir: pathlib.Path) -> PreparationPluginReturn:
|
|
97
|
+
"""Generates up-to-date XML report in the given directory"""
|
|
98
|
+
# the following if conditions avoid creating dummy .coverage file
|
|
99
|
+
if next(iglob(str(dir / ".coverage.*")), None) is not None:
|
|
100
|
+
logger.info(f"Running coverage combine -a in {dir}")
|
|
101
|
+
subprocess.run(["coverage", "combine", "-a"], cwd=dir)
|
|
102
|
+
|
|
103
|
+
if (dir / ".coverage").exists():
|
|
104
|
+
logger.info(f"Generating coverage.xml report in {dir}")
|
|
105
|
+
completed_process = subprocess.run(
|
|
106
|
+
["coverage", "xml", "-i"], cwd=dir, capture_output=True
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
output = completed_process.stdout.decode().strip()
|
|
110
|
+
logger.info(output)
|
|
111
|
+
return PreparationPluginReturn(success=True, messages=[])
|
|
112
|
+
|
|
113
|
+
def _generate_JSON_report(self, dir: pathlib.Path):
|
|
114
|
+
if (dir / ".coverage").exists():
|
|
115
|
+
logger.info(
|
|
116
|
+
f"Generating JSON report in {dir}",
|
|
117
|
+
extra=dict(
|
|
118
|
+
extra_log_attributes=dict(
|
|
119
|
+
include_contexts=self.config.include_contexts
|
|
120
|
+
)
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
command = ["coverage", "json"]
|
|
124
|
+
if self.config.include_contexts:
|
|
125
|
+
command.append("--show-contexts")
|
|
126
|
+
completed_process = subprocess.run(command, cwd=dir, capture_output=True)
|
|
127
|
+
|
|
128
|
+
output = completed_process.stdout.decode().strip()
|
|
129
|
+
logger.info(output)
|
|
130
|
+
return PreparationPluginReturn(success=True, messages=[])
|
|
131
|
+
logger.warning(f".coverage file not found at {dir}. Parsing failed")
|
|
132
|
+
return PreparationPluginReturn(
|
|
133
|
+
success=False, messages=[f".coverage file not found at {dir}."]
|
|
134
|
+
)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import typing
|
|
8
|
+
from fnmatch import translate
|
|
9
|
+
|
|
10
|
+
import sentry_sdk
|
|
11
|
+
|
|
12
|
+
from codecov_cli.helpers.folder_searcher import globs_to_regex, search_files
|
|
13
|
+
from codecov_cli.plugins.types import PreparationPluginReturn
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("codecovcli")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class XcodePlugin(object):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
app_name: typing.Optional[str] = None,
|
|
22
|
+
derived_data_folder: typing.Optional[pathlib.Path] = None,
|
|
23
|
+
):
|
|
24
|
+
self.derived_data_folder = (
|
|
25
|
+
derived_data_folder
|
|
26
|
+
or pathlib.Path("~/Library/Developer/Xcode/DerivedData").expanduser()
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# this is to speed up processing and to build reports for the project being tested,
|
|
30
|
+
# if empty the plugin will build reports for every xcode project it finds
|
|
31
|
+
self.app_name = app_name or ""
|
|
32
|
+
|
|
33
|
+
def run_preparation(self, collector) -> PreparationPluginReturn:
|
|
34
|
+
with sentry_sdk.start_span(name="xcode"):
|
|
35
|
+
logger.debug("Running xcode plugin...")
|
|
36
|
+
|
|
37
|
+
if shutil.which("xcrun") is None:
|
|
38
|
+
logger.warning("xcrun is not installed or can't be found.")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
logger.debug(f"DerivedData folder: {self.derived_data_folder}")
|
|
42
|
+
|
|
43
|
+
filename_include_regex = globs_to_regex(["*.profdata"])
|
|
44
|
+
|
|
45
|
+
matched_paths = list(
|
|
46
|
+
search_files(
|
|
47
|
+
folder_to_search=self.derived_data_folder,
|
|
48
|
+
folders_to_ignore=[],
|
|
49
|
+
filename_include_regex=filename_include_regex,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if not matched_paths:
|
|
54
|
+
logger.warning("No swift data found.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
logger.info(
|
|
58
|
+
"Running swift coverage on the following list of files:",
|
|
59
|
+
extra=dict(
|
|
60
|
+
extra_log_attributes=dict(
|
|
61
|
+
matched_paths=[p.as_posix() for p in matched_paths]
|
|
62
|
+
)
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
for path in matched_paths:
|
|
67
|
+
self.swiftcov(path, self.app_name)
|
|
68
|
+
|
|
69
|
+
return PreparationPluginReturn(success=True, messages="")
|
|
70
|
+
|
|
71
|
+
def swiftcov(self, path: pathlib.Path, app_name: str) -> None:
|
|
72
|
+
directory = os.path.dirname(path)
|
|
73
|
+
build_dir = pathlib.Path(re.sub("(Build).*", "Build", directory))
|
|
74
|
+
|
|
75
|
+
for type in ["app", "framework", "xctest"]:
|
|
76
|
+
filename_include_regex = re.compile(translate(f"*.{type}"))
|
|
77
|
+
matched_dir_paths = search_files(
|
|
78
|
+
folder_to_search=build_dir,
|
|
79
|
+
folders_to_ignore=[],
|
|
80
|
+
filename_include_regex=filename_include_regex,
|
|
81
|
+
search_for_directories=True,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
for dir_path in matched_dir_paths:
|
|
85
|
+
# proj name without extension
|
|
86
|
+
proj = dir_path.stem
|
|
87
|
+
if app_name == "" or (app_name.lower() in proj.lower()):
|
|
88
|
+
logger.info(f"+ Building reports for {proj} {type}")
|
|
89
|
+
proj_path = dir_path / proj
|
|
90
|
+
dest = (
|
|
91
|
+
proj_path
|
|
92
|
+
if proj_path.is_file()
|
|
93
|
+
else dir_path / "Contents/MacOS/{proj}"
|
|
94
|
+
)
|
|
95
|
+
output_file_name = f"{proj}.{type}.coverage.txt".replace(" ", "")
|
|
96
|
+
self.run_llvm_cov(output_file_name, path, dest)
|
|
97
|
+
|
|
98
|
+
def run_llvm_cov(
|
|
99
|
+
self, output_file_name: str, path: pathlib.Path, dest: pathlib.Path
|
|
100
|
+
) -> None:
|
|
101
|
+
with open(output_file_name, "w") as output_file:
|
|
102
|
+
s = subprocess.run(
|
|
103
|
+
[
|
|
104
|
+
"xcrun",
|
|
105
|
+
"llvm-cov",
|
|
106
|
+
"show",
|
|
107
|
+
"-instr-profile",
|
|
108
|
+
str(path),
|
|
109
|
+
str(dest),
|
|
110
|
+
],
|
|
111
|
+
stdout=output_file,
|
|
112
|
+
)
|
|
113
|
+
# 0 = success
|
|
114
|
+
if s.returncode != 0:
|
|
115
|
+
logger.warning(f"llvm-cov failed to produce results for {dest}")
|
|
116
|
+
else:
|
|
117
|
+
logger.info(f"Generated {output_file_name} file successfully")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from codecov_cli.runners.dan_runner import DoAnythingNowRunner
|
|
8
|
+
from codecov_cli.runners.pytest_standard_runner import PytestStandardRunner
|
|
9
|
+
from codecov_cli.runners.types import LabelAnalysisRunnerInterface
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("codecovcli")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UnableToFindRunner(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_runner_from_yaml(
|
|
19
|
+
plugin_dict: typing.Dict, dynamic_params: typing.Dict
|
|
20
|
+
) -> LabelAnalysisRunnerInterface:
|
|
21
|
+
try:
|
|
22
|
+
module_obj = import_module(plugin_dict["module"])
|
|
23
|
+
class_obj = getattr(module_obj, plugin_dict["class"])
|
|
24
|
+
except ModuleNotFoundError:
|
|
25
|
+
click.secho(
|
|
26
|
+
f"Unable to dynamically load module {plugin_dict['module']}",
|
|
27
|
+
err=True,
|
|
28
|
+
)
|
|
29
|
+
raise
|
|
30
|
+
except AttributeError:
|
|
31
|
+
click.secho(
|
|
32
|
+
f"Unable to dynamically load class {plugin_dict['class']} from module {plugin_dict['module']}",
|
|
33
|
+
err=True,
|
|
34
|
+
)
|
|
35
|
+
raise
|
|
36
|
+
try:
|
|
37
|
+
final_params = {**plugin_dict["params"], **dynamic_params}
|
|
38
|
+
return class_obj(**final_params)
|
|
39
|
+
except TypeError:
|
|
40
|
+
click.secho(
|
|
41
|
+
f"Unable to instantiate {class_obj} with parameters {final_params}",
|
|
42
|
+
err=True,
|
|
43
|
+
)
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_runner(
|
|
48
|
+
cli_config, runner_name: str, dynamic_params: typing.Dict = None
|
|
49
|
+
) -> LabelAnalysisRunnerInterface:
|
|
50
|
+
if dynamic_params is None:
|
|
51
|
+
dynamic_params = {}
|
|
52
|
+
if runner_name == "pytest":
|
|
53
|
+
config_params = cli_config.get("runners", {}).get("pytest", {})
|
|
54
|
+
# This is for backwards compatibility with versions <= 0.3.4
|
|
55
|
+
# In which the key for this config was 'python', not 'pytest'
|
|
56
|
+
if config_params == {}:
|
|
57
|
+
config_params = cli_config.get("runners", {}).get("python", {})
|
|
58
|
+
if config_params:
|
|
59
|
+
logger.warning(
|
|
60
|
+
"Using 'python' to configure the PytestStandardRunner is deprecated. Please change to 'pytest'"
|
|
61
|
+
)
|
|
62
|
+
final_params = {**config_params, **dynamic_params}
|
|
63
|
+
return PytestStandardRunner(final_params)
|
|
64
|
+
elif runner_name == "dan":
|
|
65
|
+
config_params = cli_config.get("runners", {}).get("dan", {})
|
|
66
|
+
final_params = {**config_params, **dynamic_params}
|
|
67
|
+
return DoAnythingNowRunner(final_params)
|
|
68
|
+
logger.debug(
|
|
69
|
+
f"Trying to load runner {runner_name}",
|
|
70
|
+
extra=dict(
|
|
71
|
+
extra_log_attributes=dict(
|
|
72
|
+
available_runners=cli_config.get("runners", {}).keys()
|
|
73
|
+
)
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
if cli_config and runner_name in cli_config.get("runners", {}):
|
|
77
|
+
return _load_runner_from_yaml(
|
|
78
|
+
cli_config["runners"][runner_name], dynamic_params=dynamic_params
|
|
79
|
+
)
|
|
80
|
+
raise UnableToFindRunner(f"Can't find runner {runner_name}")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from codecov_cli.runners.types import (
|
|
6
|
+
LabelAnalysisRequestResult,
|
|
7
|
+
LabelAnalysisRunnerInterface,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DoAnythingNowConfigParams(dict):
|
|
12
|
+
@property
|
|
13
|
+
def collect_tests_command(self) -> Union[List[str], str]:
|
|
14
|
+
"""
|
|
15
|
+
Command to run when collecting tests.
|
|
16
|
+
The output of this command needs to be a list of test labels,
|
|
17
|
+
one test label per line.
|
|
18
|
+
"""
|
|
19
|
+
return self.get("collect_tests_command", None)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def process_labelanalysis_result_command(self) -> Union[List[str], str]:
|
|
23
|
+
"""
|
|
24
|
+
Command to run that handles the label analysis result.
|
|
25
|
+
The result will be passed as an argument to the command in JSON format.
|
|
26
|
+
"""
|
|
27
|
+
return self.get("process_labelanalysis_result_command", None)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DoAnythingNowRunner(LabelAnalysisRunnerInterface):
|
|
31
|
+
def __init__(self, config_params: Optional[dict] = None) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
if config_params is None:
|
|
34
|
+
config_params = {}
|
|
35
|
+
self.params = DoAnythingNowConfigParams(config_params)
|
|
36
|
+
|
|
37
|
+
def collect_tests(self) -> List[str]:
|
|
38
|
+
command = self.params.collect_tests_command
|
|
39
|
+
if command is None:
|
|
40
|
+
raise Exception(
|
|
41
|
+
"DAN runner missing 'collect_tests_command' configuration value"
|
|
42
|
+
)
|
|
43
|
+
return list(
|
|
44
|
+
subprocess.run(command, check=True, capture_output=True)
|
|
45
|
+
.stdout.decode()
|
|
46
|
+
.splitlines()
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def process_labelanalysis_result(self, result: LabelAnalysisRequestResult):
|
|
50
|
+
json_result = json.dumps(result)
|
|
51
|
+
command = self.params.process_labelanalysis_result_command
|
|
52
|
+
if command is None:
|
|
53
|
+
raise Exception(
|
|
54
|
+
"DAN runner missing 'process_labelanalysis_result_command' configuration value"
|
|
55
|
+
)
|
|
56
|
+
command_list = []
|
|
57
|
+
if isinstance(command, list):
|
|
58
|
+
command_list.extend(command)
|
|
59
|
+
else:
|
|
60
|
+
command_list.append(command)
|
|
61
|
+
command_list.append(json_result)
|
|
62
|
+
return subprocess.run(
|
|
63
|
+
command_list, check=True, capture_output=True
|
|
64
|
+
).stdout.decode()
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
import random
|
|
4
|
+
import subprocess
|
|
5
|
+
from subprocess import CalledProcessError
|
|
6
|
+
from sys import stdout
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from codecov_cli.runners.types import (
|
|
12
|
+
LabelAnalysisRequestResult,
|
|
13
|
+
LabelAnalysisRunnerInterface,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("codecovcli")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PytestStandardRunnerConfigParams(dict):
|
|
20
|
+
@property
|
|
21
|
+
def python_path(self) -> str:
|
|
22
|
+
python_path = self.get("python_path")
|
|
23
|
+
return python_path or "python"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def collect_tests_options(self) -> List[str]:
|
|
27
|
+
return self.get("collect_tests_options", [])
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def execute_tests_options(self) -> List[str]:
|
|
31
|
+
options = self.get("execute_tests_options", [])
|
|
32
|
+
if any(map(lambda option: option.startswith("--cov"), options)):
|
|
33
|
+
logger.warning(
|
|
34
|
+
"--cov option detected when running tests. Please use coverage_root config option instead"
|
|
35
|
+
)
|
|
36
|
+
return options
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def coverage_root(self) -> str:
|
|
40
|
+
"""
|
|
41
|
+
The coverage root. This will be passed to --cov=<coverage_root_dir>
|
|
42
|
+
Default: ./
|
|
43
|
+
"""
|
|
44
|
+
return self.get("coverage_root", "./")
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def get_available_params(cls) -> List[str]:
|
|
48
|
+
"""Lists all the @property attribute names of this class.
|
|
49
|
+
These attributes are considered the 'valid config options'
|
|
50
|
+
"""
|
|
51
|
+
klass_methods = [
|
|
52
|
+
x
|
|
53
|
+
for x in dir(cls)
|
|
54
|
+
if (inspect.isdatadescriptor(getattr(cls, x)) and not x.startswith("__"))
|
|
55
|
+
]
|
|
56
|
+
return klass_methods
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PytestStandardRunner(LabelAnalysisRunnerInterface):
|
|
60
|
+
dry_run_runner_options = ["--cov-context=test"]
|
|
61
|
+
params: PytestStandardRunnerConfigParams
|
|
62
|
+
|
|
63
|
+
def __init__(self, config_params: Optional[dict] = None) -> None:
|
|
64
|
+
super().__init__()
|
|
65
|
+
if config_params is None:
|
|
66
|
+
config_params = {}
|
|
67
|
+
# Before we create the config params we emit warnings if any param is unknown
|
|
68
|
+
# So the user knows something is wrong with their config
|
|
69
|
+
self._possibly_warn_bad_config(config_params)
|
|
70
|
+
self.params = PytestStandardRunnerConfigParams(config_params)
|
|
71
|
+
|
|
72
|
+
def _possibly_warn_bad_config(self, config_params: dict):
|
|
73
|
+
available_config_params = (
|
|
74
|
+
PytestStandardRunnerConfigParams.get_available_params()
|
|
75
|
+
)
|
|
76
|
+
provided_config_params = config_params.keys()
|
|
77
|
+
for provided_param in provided_config_params:
|
|
78
|
+
if provided_param not in available_config_params:
|
|
79
|
+
logger.warning(f"Config parameter '{provided_param}' is unknown.")
|
|
80
|
+
|
|
81
|
+
def parse_captured_output_error(self, exp: CalledProcessError) -> str:
|
|
82
|
+
result = ""
|
|
83
|
+
for out_stream in [exp.stdout, exp.stderr]:
|
|
84
|
+
if out_stream:
|
|
85
|
+
if type(out_stream) is bytes:
|
|
86
|
+
out_stream = out_stream.decode()
|
|
87
|
+
result += "\n" + out_stream
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
def _execute_pytest(self, pytest_args: List[str], capture_output: bool = True):
|
|
91
|
+
"""Handles calling pytest using subprocess.run.
|
|
92
|
+
Raises Exception if pytest fails
|
|
93
|
+
Returns the complete pytest output
|
|
94
|
+
"""
|
|
95
|
+
command = [self.params.python_path, "-m", "pytest"] + pytest_args
|
|
96
|
+
try:
|
|
97
|
+
result = subprocess.run(
|
|
98
|
+
command,
|
|
99
|
+
capture_output=capture_output,
|
|
100
|
+
check=True,
|
|
101
|
+
stdout=(stdout if not capture_output else None),
|
|
102
|
+
)
|
|
103
|
+
except CalledProcessError as exp:
|
|
104
|
+
message = f"Pytest exited with non-zero code {exp.returncode}."
|
|
105
|
+
message += "\nThis is likely not a problem with label-analysis. Check pytest's output and options."
|
|
106
|
+
|
|
107
|
+
if capture_output:
|
|
108
|
+
# If pytest failed but we captured its output the user won't know what's wrong
|
|
109
|
+
# So we need to include that in the error message
|
|
110
|
+
message += "\nPYTEST OUTPUT:"
|
|
111
|
+
message += self.parse_captured_output_error(exp)
|
|
112
|
+
else:
|
|
113
|
+
message += "\n(you can check pytest options on the logs before the test session start)"
|
|
114
|
+
raise click.ClickException(message)
|
|
115
|
+
if capture_output:
|
|
116
|
+
return result.stdout.decode()
|
|
117
|
+
|
|
118
|
+
def collect_tests(self):
|
|
119
|
+
default_options = ["-q", "--collect-only"]
|
|
120
|
+
extra_args = self.params.collect_tests_options
|
|
121
|
+
options_to_use = default_options + extra_args
|
|
122
|
+
logger.info(
|
|
123
|
+
"Collecting tests",
|
|
124
|
+
extra=dict(
|
|
125
|
+
extra_log_attributes=dict(
|
|
126
|
+
pytest_command=[self.params.python_path, "-m", "pytest"],
|
|
127
|
+
pytest_options=options_to_use,
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
output = self._execute_pytest(options_to_use)
|
|
133
|
+
lines = output.split(sep="\n")
|
|
134
|
+
test_names = list(line for line in lines if ("::" in line and "test" in line))
|
|
135
|
+
return test_names
|
|
136
|
+
|
|
137
|
+
def process_labelanalysis_result(self, result: LabelAnalysisRequestResult):
|
|
138
|
+
default_options = [
|
|
139
|
+
f"--cov={self.params.coverage_root}",
|
|
140
|
+
"--cov-context=test",
|
|
141
|
+
] + self.params.execute_tests_options
|
|
142
|
+
all_labels = set(
|
|
143
|
+
result.absent_labels
|
|
144
|
+
+ result.present_diff_labels
|
|
145
|
+
+ result.global_level_labels
|
|
146
|
+
)
|
|
147
|
+
skipped_tests = set(result.present_report_labels) - all_labels
|
|
148
|
+
if skipped_tests:
|
|
149
|
+
logger.info(
|
|
150
|
+
"Some tests are being skipped. (run in verbose mode to get list of tests skipped)",
|
|
151
|
+
extra=dict(
|
|
152
|
+
extra_log_attributes=dict(skipped_tests_count=len(skipped_tests))
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
logger.debug(
|
|
156
|
+
"List of skipped tests",
|
|
157
|
+
extra=dict(
|
|
158
|
+
extra_log_attributes=dict(skipped_tests=sorted(skipped_tests))
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if len(all_labels) == 0:
|
|
163
|
+
all_labels = [random.choice(result.present_report_labels)]
|
|
164
|
+
logger.info(
|
|
165
|
+
"All tests are being skipped. Selected random label to run",
|
|
166
|
+
extra=dict(extra_log_attributes=dict(selected_label=all_labels[0])),
|
|
167
|
+
)
|
|
168
|
+
tests_to_run = [
|
|
169
|
+
label.split("[")[0] if "[" in label else label for label in all_labels
|
|
170
|
+
]
|
|
171
|
+
command_array = default_options + tests_to_run
|
|
172
|
+
logger.info(
|
|
173
|
+
"Running tests. (run in verbose mode to get list of tests executed)"
|
|
174
|
+
)
|
|
175
|
+
logger.info(f' pytest options: "{" ".join(default_options)}"')
|
|
176
|
+
logger.info(f" executed tests: {len(tests_to_run)}")
|
|
177
|
+
logger.debug(
|
|
178
|
+
"List of tests executed",
|
|
179
|
+
extra=dict(extra_log_attributes=dict(executed_tests=tests_to_run)),
|
|
180
|
+
)
|
|
181
|
+
output = self._execute_pytest(command_array, capture_output=False)
|
|
182
|
+
logger.info(f"Finished running {len(tests_to_run)} tests successfully")
|
|
183
|
+
logger.info(f' pytest options: "{" ".join(default_options)}"')
|
|
184
|
+
logger.debug(output)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# This is supposed to be a TypedDict,
|
|
5
|
+
# But that is Python >= 3.7 only
|
|
6
|
+
# So we are not using those
|
|
7
|
+
class LabelAnalysisRequestResult(dict):
|
|
8
|
+
@property
|
|
9
|
+
def present_report_labels(self) -> List[str]:
|
|
10
|
+
return self.get("present_report_labels", [])
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def absent_labels(self) -> List[str]:
|
|
14
|
+
return self.get("absent_labels", [])
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def present_diff_labels(self) -> List[str]:
|
|
18
|
+
return self.get("present_diff_labels", [])
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def global_level_labels(self) -> List[str]:
|
|
22
|
+
return self.get("global_level_labels", [])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LabelAnalysisRunnerInterface(object):
|
|
26
|
+
params: Dict = None
|
|
27
|
+
dry_run_runner_options: List[str] = []
|
|
28
|
+
|
|
29
|
+
def collect_tests(self) -> List[str]:
|
|
30
|
+
raise NotImplementedError()
|
|
31
|
+
|
|
32
|
+
def process_labelanalysis_result(self, result: LabelAnalysisRequestResult):
|
|
33
|
+
raise NotImplementedError()
|
|
File without changes
|