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,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,8 @@
1
+ import typing
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class PreparationPluginReturn(object):
7
+ success: bool
8
+ messages: typing.List[str]
@@ -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