Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

Commit 25da9d9

Browse files
Send labels to Codecov only after collecting them (#191)
These changes alter the way label analysis command works by making 2 requests to Codecov instead of 1. Why would we want to do this, if it's more time to make a second request? Because it takes (usually) longer to collect the labels than it takes to make a second request. Specially for bigger repos, collecting the labels can take dozens of seconds. By making the label collection parallel with the labels processing we can speed up the total ATS time. We still PATCH the labels in case the worker hasn't finished the calculations, and it will correctly refresh the request labels from the DB (based on [here](codecov/worker#1210)). However even if it misses the requested labels we recalculate the `absent_labels`, if needed (`_potentially_calculate_absent_labels`). So we still guarantee the exact same response everytime. Potentially faster. PS.: The file `test_instantiation.py` changes because of the linter
1 parent 7dd911e commit 25da9d9

File tree

2 files changed

+234
-46
lines changed

2 files changed

+234
-46
lines changed

codecov_cli/commands/labelanalysis.py

Lines changed: 92 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -90,73 +90,68 @@ def label_analysis(
9090
raise click.ClickException(
9191
click.style("Unable to run label analysis", fg="red")
9292
)
93-
upload_url = enterprise_url or CODECOV_API_URL
94-
url = f"{upload_url}/labels/labels-analysis"
95-
token_header = f"Repotoken {token}"
9693

9794
codecov_yaml = ctx.obj["codecov_yaml"] or {}
9895
cli_config = codecov_yaml.get("cli", {})
96+
# Raises error if no runner is found
9997
runner = get_runner(cli_config, runner_name)
10098
logger.debug(
10199
f"Selected runner: {runner}",
102100
extra=dict(extra_log_attributes=dict(config=runner.params)),
103101
)
102+
103+
upload_url = enterprise_url or CODECOV_API_URL
104+
url = f"{upload_url}/labels/labels-analysis"
105+
token_header = f"Repotoken {token}"
106+
payload = {
107+
"base_commit": base_commit_sha,
108+
"head_commit": head_commit_sha,
109+
"requested_labels": None,
110+
}
111+
# Send the initial label analysis request without labels
112+
# Because labels might take a long time to collect
113+
eid = _send_labelanalysis_request(payload, url, token_header)
114+
104115
logger.info("Collecting labels...")
105116
requested_labels = runner.collect_tests()
106117
logger.info(f"Collected {len(requested_labels)} tests")
107118
logger.debug(
108119
"Labels collected.",
109120
extra=dict(extra_log_attributes=dict(labels_collected=requested_labels)),
110121
)
111-
payload = {
112-
"base_commit": base_commit_sha,
113-
"head_commit": head_commit_sha,
114-
"requested_labels": requested_labels,
115-
}
116-
logger.info("Requesting set of labels to run...")
117-
try:
118-
response = requests.post(
119-
url, json=payload, headers={"Authorization": token_header}
120-
)
121-
if response.status_code >= 500:
122-
logger.warning(
123-
"Sorry. Codecov is having problems",
124-
extra=dict(extra_log_attributes=dict(status_code=response.status_code)),
125-
)
122+
payload["requested_labels"] = requested_labels
123+
124+
if eid:
125+
# Initial request with no labels was successful
126+
# Now we PATCH the labels in
127+
patch_url = f"{upload_url}/labels/labels-analysis/{eid}"
128+
_patch_labels(payload, patch_url, token_header)
129+
else:
130+
# Initial request with no labels failed
131+
# Retry it
132+
eid = _send_labelanalysis_request(payload, url, token_header)
133+
if eid is None:
126134
_fallback_to_collected_labels(requested_labels, runner, dry_run=dry_run)
127135
return
128-
if response.status_code >= 400:
129-
logger.warning(
130-
"Got a 4XX status code back from Codecov",
131-
extra=dict(
132-
extra_log_attributes=dict(
133-
status_code=response.status_code, response_json=response.json()
134-
)
135-
),
136-
)
137-
raise click.ClickException(
138-
"There is some problem with the submitted information"
139-
)
140-
except requests.RequestException:
141-
raise click.ClickException(click.style("Unable to reach Codecov", fg="red"))
142-
eid = response.json()["external_id"]
136+
143137
has_result = False
144138
logger.info("Label request sent. Waiting for result.")
145139
start_wait = time.monotonic()
146-
time.sleep(2)
140+
time.sleep(1)
147141
while not has_result:
148142
resp_data = requests.get(
149143
f"{upload_url}/labels/labels-analysis/{eid}",
150144
headers={"Authorization": token_header},
151145
)
152146
resp_json = resp_data.json()
153147
if resp_json["state"] == "finished":
148+
request_result = _potentially_calculate_absent_labels(
149+
resp_data.json()["result"], requested_labels
150+
)
154151
if not dry_run:
155-
runner.process_labelanalysis_result(
156-
LabelAnalysisRequestResult(resp_data.json()["result"])
157-
)
152+
runner.process_labelanalysis_result(request_result)
158153
else:
159-
_dry_run_output(LabelAnalysisRequestResult(resp_data.json()["result"]))
154+
_dry_run_output(LabelAnalysisRequestResult(request_result))
160155
return
161156
if resp_json["state"] == "error":
162157
logger.error(
@@ -179,6 +174,65 @@ def label_analysis(
179174
time.sleep(5)
180175

181176

177+
def _potentially_calculate_absent_labels(
178+
request_result, requested_labels
179+
) -> LabelAnalysisRequestResult:
180+
if request_result["absent_labels"]:
181+
return LabelAnalysisRequestResult(request_result)
182+
requested_labels_set = set(requested_labels)
183+
present_labels_set = set(request_result["present_report_labels"])
184+
request_result["absent_labels"] = list(requested_labels_set - present_labels_set)
185+
return LabelAnalysisRequestResult(request_result)
186+
187+
188+
def _patch_labels(payload, url, token_header):
189+
logger.info("Sending collected labels to Codecov...")
190+
try:
191+
response = requests.patch(
192+
url, json=payload, headers={"Authorization": token_header}
193+
)
194+
if response.status_code < 300:
195+
logger.info("Labels successfully sent to Codecov")
196+
except requests.RequestException:
197+
raise click.ClickException(click.style("Unable to reach Codecov", fg="red"))
198+
199+
200+
def _send_labelanalysis_request(payload, url, token_header):
201+
logger.info(
202+
"Requesting set of labels to run...",
203+
extra=dict(
204+
extra_log_attributes=dict(
205+
with_labels=(payload["requested_labels"] is not None)
206+
)
207+
),
208+
)
209+
try:
210+
response = requests.post(
211+
url, json=payload, headers={"Authorization": token_header}
212+
)
213+
if response.status_code >= 500:
214+
logger.warning(
215+
"Sorry. Codecov is having problems",
216+
extra=dict(extra_log_attributes=dict(status_code=response.status_code)),
217+
)
218+
return None
219+
if response.status_code >= 400:
220+
logger.warning(
221+
"Got a 4XX status code back from Codecov",
222+
extra=dict(
223+
extra_log_attributes=dict(
224+
status_code=response.status_code, response_json=response.json()
225+
)
226+
),
227+
)
228+
raise click.ClickException(
229+
"There is some problem with the submitted information"
230+
)
231+
except requests.RequestException:
232+
raise click.ClickException(click.style("Unable to reach Codecov", fg="red"))
233+
return response.json()["external_id"]
234+
235+
182236
def _dry_run_output(result: LabelAnalysisRequestResult):
183237
logger.info(
184238
"Not executing tests because '--dry-run' is on. List of labels selected for running below."

tests/commands/test_invoke_labelanalysis.py

Lines changed: 142 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import json
22

3+
import click
34
import pytest
45
import responses
56
from click.testing import CliRunner
67
from responses import matchers
78

8-
from codecov_cli.commands.labelanalysis import _fallback_to_collected_labels
9+
from codecov_cli.commands.labelanalysis import (
10+
_fallback_to_collected_labels,
11+
_potentially_calculate_absent_labels,
12+
_send_labelanalysis_request,
13+
)
914
from codecov_cli.commands.labelanalysis import time as labelanalysis_time
1015
from codecov_cli.main import cli
1116
from codecov_cli.runners.types import LabelAnalysisRequestResult
@@ -46,6 +51,49 @@ def get_labelanalysis_deps(mocker):
4651
}
4752

4853

54+
class TestLabelAnalysisNotInvoke(object):
55+
def test_potentially_calculate_labels(self):
56+
request_result = {
57+
"present_report_labels": ["label_1", "label_2", "label_3"],
58+
"absent_labels": [],
59+
"present_diff_labels": ["label_2", "label_3"],
60+
"global_level_labels": ["label_1"],
61+
}
62+
collected_labels = ["label_1", "label_2", "label_3", "label_4"]
63+
expected = {**request_result, "absent_labels": ["label_4"]}
64+
assert (
65+
_potentially_calculate_absent_labels(request_result, collected_labels)
66+
== expected
67+
)
68+
request_result["absent_labels"] = ["label_4", "label_5"]
69+
expected["absent_labels"].append("label_5")
70+
assert (
71+
_potentially_calculate_absent_labels(request_result, collected_labels)
72+
== expected
73+
)
74+
75+
def test_send_label_analysis_bad_payload(self):
76+
payload = {
77+
"base_commit": "base_commit",
78+
"head_commit": "head_commit",
79+
"requested_labels": [],
80+
}
81+
url = "https://api.codecov.io/labels/labels-analysis"
82+
header = "Repotoken STATIC_TOKEN"
83+
with responses.RequestsMock() as rsps:
84+
rsps.add(
85+
responses.POST,
86+
"https://api.codecov.io/labels/labels-analysis",
87+
json={"error": "list field cannot be empty list"},
88+
status=400,
89+
match=[
90+
matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"})
91+
],
92+
)
93+
with pytest.raises(click.ClickException):
94+
_send_labelanalysis_request(payload, url, header)
95+
96+
4997
class TestLabelAnalysisCommand(object):
5098
def test_labelanalysis_help(self, mocker, fake_ci_provider):
5199
mocker.patch("codecov_cli.main.get_ci_adapter", return_value=fake_ci_provider)
@@ -133,6 +181,15 @@ def test_invoke_label_analysis(self, get_labelanalysis_deps, mocker):
133181
matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"})
134182
],
135183
)
184+
rsps.add(
185+
responses.PATCH,
186+
"https://api.codecov.io/labels/labels-analysis/label-analysis-request-id",
187+
json={"external_id": "label-analysis-request-id"},
188+
status=201,
189+
match=[
190+
matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"})
191+
],
192+
)
136193
rsps.add(
137194
responses.GET,
138195
"https://api.codecov.io/labels/labels-analysis/label-analysis-request-id",
@@ -144,12 +201,12 @@ def test_invoke_label_analysis(self, get_labelanalysis_deps, mocker):
144201
["-v", "label-analysis", "--token=STATIC_TOKEN", "--base-sha=BASE_SHA"],
145202
obj={},
146203
)
147-
mock_get_runner.assert_called()
148-
fake_runner.process_labelanalysis_result.assert_called_with(
149-
label_analysis_result
150-
)
151-
print(result.output)
152-
assert result.exit_code == 0
204+
assert result.exit_code == 0
205+
mock_get_runner.assert_called()
206+
fake_runner.process_labelanalysis_result.assert_called_with(
207+
label_analysis_result
208+
)
209+
print(result.output)
153210

154211
def test_invoke_label_analysis_dry_run(self, get_labelanalysis_deps, mocker):
155212
mock_get_runner = get_labelanalysis_deps["mock_get_runner"]
@@ -172,6 +229,15 @@ def test_invoke_label_analysis_dry_run(self, get_labelanalysis_deps, mocker):
172229
matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"})
173230
],
174231
)
232+
rsps.add(
233+
responses.PATCH,
234+
"https://api.codecov.io/labels/labels-analysis/label-analysis-request-id",
235+
json={"external_id": "label-analysis-request-id"},
236+
status=201,
237+
match=[
238+
matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"})
239+
],
240+
)
175241
rsps.add(
176242
responses.GET,
177243
"https://api.codecov.io/labels/labels-analysis/label-analysis-request-id",
@@ -190,8 +256,8 @@ def test_invoke_label_analysis_dry_run(self, get_labelanalysis_deps, mocker):
190256
)
191257
mock_get_runner.assert_called()
192258
fake_runner.process_labelanalysis_result.assert_not_called()
193-
assert result.exit_code == 0
194259
print(result.output)
260+
assert result.exit_code == 0
195261
assert json.dumps(label_analysis_result) in result.output
196262

197263
def test_fallback_to_collected_labels(self, mocker):
@@ -305,6 +371,15 @@ def test_fallback_collected_labels_codecov_error_processing_label_analysis(
305371
matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"})
306372
],
307373
)
374+
rsps.add(
375+
responses.PATCH,
376+
"https://api.codecov.io/labels/labels-analysis/label-analysis-request-id",
377+
json={"external_id": "label-analysis-request-id"},
378+
status=201,
379+
match=[
380+
matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"})
381+
],
382+
)
308383
rsps.add(
309384
responses.GET,
310385
"https://api.codecov.io/labels/labels-analysis/label-analysis-request-id",
@@ -346,6 +421,15 @@ def test_fallback_collected_labels_codecov_max_wait_time_exceeded(
346421
matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"})
347422
],
348423
)
424+
rsps.add(
425+
responses.PATCH,
426+
"https://api.codecov.io/labels/labels-analysis/label-analysis-request-id",
427+
json={"external_id": "label-analysis-request-id"},
428+
status=201,
429+
match=[
430+
matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"})
431+
],
432+
)
349433
rsps.add(
350434
responses.GET,
351435
"https://api.codecov.io/labels/labels-analysis/label-analysis-request-id",
@@ -374,3 +458,53 @@ def test_fallback_collected_labels_codecov_max_wait_time_exceeded(
374458
"global_level_labels": [],
375459
}
376460
)
461+
462+
def test_first_labelanalysis_request_fails_but_second_works(
463+
self, get_labelanalysis_deps, mocker
464+
):
465+
mock_get_runner = get_labelanalysis_deps["mock_get_runner"]
466+
fake_runner = get_labelanalysis_deps["fake_runner"]
467+
collected_labels = get_labelanalysis_deps["collected_labels"]
468+
469+
label_analysis_result = {
470+
"present_report_labels": ["test_present"],
471+
"absent_labels": ["test_absent"],
472+
"present_diff_labels": ["test_in_diff"],
473+
"global_level_labels": ["test_global"],
474+
}
475+
476+
with responses.RequestsMock() as rsps:
477+
rsps.add(
478+
responses.POST,
479+
"https://api.codecov.io/labels/labels-analysis",
480+
status=502,
481+
match=[
482+
matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"})
483+
],
484+
)
485+
rsps.add(
486+
responses.POST,
487+
"https://api.codecov.io/labels/labels-analysis",
488+
json={"external_id": "label-analysis-request-id"},
489+
status=201,
490+
match=[
491+
matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"})
492+
],
493+
)
494+
rsps.add(
495+
responses.GET,
496+
"https://api.codecov.io/labels/labels-analysis/label-analysis-request-id",
497+
json={"state": "finished", "result": label_analysis_result},
498+
)
499+
cli_runner = CliRunner()
500+
result = cli_runner.invoke(
501+
cli,
502+
["-v", "label-analysis", "--token=STATIC_TOKEN", "--base-sha=BASE_SHA"],
503+
obj={},
504+
)
505+
assert result.exit_code == 0
506+
mock_get_runner.assert_called()
507+
fake_runner.process_labelanalysis_result.assert_called_with(
508+
label_analysis_result
509+
)
510+
print(result.output)

0 commit comments

Comments
 (0)