forked from trezor/trezor-firmware
-
Notifications
You must be signed in to change notification settings - Fork 0
/
gitlab.py
174 lines (141 loc) · 5.01 KB
/
gitlab.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
"""
Helper functions for communication with Gitlab.
Allowing for interaction with the test results, e.g. with UI tests.
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable, Iterator
import requests
AnyDict = dict[Any, Any]
HERE = Path(__file__).parent
BRANCHES_API_TEMPLATE = "https://gitlab.com/satoshilabs/trezor/trezor-firmware/-/pipelines.json?scope=branches&page={}"
GRAPHQL_API = "https://gitlab.com/api/graphql"
RAW_REPORT_URL_TEMPLATE = (
"https://gitlab.com/satoshilabs/trezor/trezor-firmware/-/jobs/{}/raw"
)
UI_JOB_NAMES = (
"core click R test",
"core device R test",
"core click test",
"core device test",
"core persistence test",
"legacy device test",
)
SAVE_GRAPHQL_RESULTS = False
@dataclass
class TestResult:
failed: int = 0
passed: int = 0
error: int = 0
@classmethod
def from_line(cls, line: str) -> TestResult:
self = TestResult()
for key in self.__annotations__:
match = re.search(rf"(\d+) {key}", line)
if match:
setattr(self, key, int(match.group(1)))
return self
@classmethod
def from_job_id(cls, job_id: str) -> TestResult:
report_link = RAW_REPORT_URL_TEMPLATE.format(job_id)
raw_content = requests.get(report_link).text
result_pattern = r"= .* passed.*s \(\d.*\) ="
result_line_match = re.search(result_pattern, raw_content)
if not result_line_match:
print("No results yet.")
return TestResult()
return cls.from_line(result_line_match.group(0))
def _get_gitlab_branches(page: int) -> list[AnyDict]:
return requests.get(BRANCHES_API_TEMPLATE.format(page)).json()["pipelines"]
def _get_branch_obj(branch_name: str) -> AnyDict:
# Trying first 10 pages of branches
for page in range(1, 11):
branches = _get_gitlab_branches(page)
for branch_obj in branches:
if branch_obj["ref"]["name"] == branch_name:
return branch_obj
raise ValueError(f"Branch {branch_name} not found")
def _get_pipeline_jobs_info(pipeline_iid: int) -> AnyDict:
# Getting just the stuff we need - the job names and IDs
graphql_query = """
query getJobsFromPipeline($projectPath: ID!, $iid: ID!) {
project(fullPath: $projectPath) {
pipeline(iid: $iid) {
stages {
nodes {
groups {
nodes {
jobs {
nodes {
id
name
}
}
}
}
}
}
}
}
}
"""
query = {
"query": graphql_query,
"variables": {
"projectPath": "satoshilabs/trezor/trezor-firmware",
"iid": pipeline_iid,
},
}
return requests.post(GRAPHQL_API, json=query).json()
def _yield_pipeline_jobs(pipeline_iid: int) -> Iterator[AnyDict]:
jobs_info = _get_pipeline_jobs_info(pipeline_iid)
if SAVE_GRAPHQL_RESULTS: # for development purposes
with open("jobs_info.json", "w") as f:
json.dump(jobs_info, f, indent=2)
stages = jobs_info["data"]["project"]["pipeline"]["stages"]["nodes"]
for stage in stages:
nodes = stage["groups"]["nodes"]
for node in nodes:
jobs = node["jobs"]["nodes"]
for job in jobs:
yield job
def _get_job_ui_fixtures_results(job: AnyDict) -> AnyDict:
print(f"Checking job {job['name']}")
job_id = job["id"].split("/")[-1]
job_results = TestResult.from_job_id(job_id)
if job_results.failed:
print(f"ERROR: Job {job['name']} failed - {job_results}")
return {}
url = f"https://satoshilabs.gitlab.io/-/trezor/trezor-firmware/-/jobs/{job_id}/artifacts/tests/ui_tests/fixtures.results.json"
response = requests.get(url)
if response.status_code != 200:
print("No UI results found")
return {}
return response.json()
def get_jobs_of_interest(
only_jobs: Iterable[str] | None, exclude_jobs: Iterable[str] | None
) -> Iterable[str]:
if only_jobs and exclude_jobs:
raise ValueError("Cannot specify both only_jobs and exclude_jobs")
if only_jobs:
return [job for job in UI_JOB_NAMES if job in only_jobs]
if exclude_jobs:
return [job for job in UI_JOB_NAMES if job not in exclude_jobs]
return UI_JOB_NAMES
def get_branch_ui_fixtures_results(
branch_name: str, jobs_of_interest: Iterable[str] | None = None
) -> dict[str, AnyDict]:
print(f"Checking branch {branch_name}")
if jobs_of_interest is None:
jobs_of_interest = UI_JOB_NAMES
branch_obj = _get_branch_obj(branch_name)
pipeline_iid = branch_obj["iid"]
def yield_key_value() -> Iterator[tuple[str, AnyDict]]:
for job in _yield_pipeline_jobs(pipeline_iid):
for ui_job_name in jobs_of_interest:
if job["name"] == ui_job_name:
yield job["name"], _get_job_ui_fixtures_results(job)
return dict(yield_key_value())