-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjeeves.py
246 lines (186 loc) · 6.34 KB
/
jeeves.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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import json
import os
from enum import Enum, StrEnum, auto
from io import StringIO
from pathlib import Path
from typing import TextIO
from xml.etree import ElementTree # noqa: S405
import funcy
import rich
import sh
import typer
from pyld import jsonld
from yarl import URL
import yaml_ld
COMMENTING_NOT_ALLOWED = (
'GraphQL: Resource not accessible by integration (addComment)'
)
gh = sh.gh.bake(_env={**os.environ, 'NO_COLOR': '1'})
artifacts = Path(__file__).parent / 'tests/artifacts'
pytest_xml = artifacts / 'pytest.xml'
COMMENT_TEMPLATE = '''
## Test Diff Report
{body}
'''
def update_submodule():
"""Update the `specification` submodule from GitHub."""
sh.git.submodule.update('--remote', '--init', '--recursive')
class TestStatus(StrEnum):
"""Status of a unit test."""
PASSED = 'passed'
FAILED = 'failure'
SKIPPED = 'skipped'
ERROR = 'error'
def _parse_pytest_xml(xml_data: TextIO):
tree = ElementTree.parse(xml_data)
root = tree.getroot()
# Iterate over each testcase element
for testcase in root.iter('testcase'):
# Basic test information
class_name = testcase.get('classname')
test_name = testcase.get('name')
full_test_name = f'{class_name}.{test_name}'
for choice in list(TestStatus):
if testcase.find(choice.value) is not None:
yield full_test_name, choice
break
else:
yield full_test_name, TestStatus.PASSED
def test_with_artifacts():
"""Run pytest and save the results to artifacts directory."""
try:
sh.pytest.bake(
color='no',
junitxml=pytest_xml,
cov_report='term-missing:skip-covered',
cov='yaml_ld',
).tests(
_out=artifacts / 'coverage.txt',
)
except sh.ErrorReturnCode as err:
typer.echo(err)
typer.echo(err.stdout)
typer.echo(err.stderr)
class PostComment(Enum):
"""Status of a comment post attempt."""
POSTED = auto()
NO_EXISTING_COMMENTS = auto()
NOT_ALLOWED = auto()
def _post(command):
try:
command()
except sh.ErrorReturnCode as err:
error_text = err.stderr.decode()
if 'no comments found for current user' in error_text:
return PostComment.NO_EXISTING_COMMENTS
elif COMMENTING_NOT_ALLOWED in error_text:
rich.print(f'Cannot post a comment: {error_text}')
return PostComment.NOT_ALLOWED
raise
def ci(): # noqa: C901, WPS210, WPS213, WPS231
"""Run CI."""
# Download artifact from a previous run
previous_run = funcy.first(
json.loads(
gh.api('repos/iolanta-tech/python-yaml-ld/actions/artifacts'),
)['artifacts'],
)
download_path = URL(previous_run['archive_download_url']).path.lstrip('/')
previous_run_test_report = dict(
_parse_pytest_xml(
StringIO(
sh.zcat(
_in=gh.api(download_path, _piped=True),
),
),
),
)
test_with_artifacts()
with pytest_xml.open() as current_xml_data:
current_test_report = dict(_parse_pytest_xml(current_xml_data))
newly_passed = [
test_name
for test_name, current_result in current_test_report.items()
if (
current_result == TestStatus.PASSED
and previous_run_test_report.get(test_name) != TestStatus.PASSED
)
]
newly_failed = [
test_name
for test_name, current_result in current_test_report.items()
if (
current_result in {TestStatus.FAILED, TestStatus.ERROR}
and previous_run_test_report.get(test_name) == TestStatus.PASSED
)
]
comment_body_parts = []
if newly_passed:
comment_body_parts.append('## 🎉 Newly passed!')
for test_name in newly_passed:
comment_body_parts.append(f'* 🟢 `{test_name}`')
if newly_failed:
comment_body_parts.append('## 💥 Newly failed')
for test_name in newly_failed:
comment_body_parts.append(f'* 🔴 `{test_name}`')
if not comment_body_parts:
comment_body_parts.append('Nothing had changed in tests.')
comment = COMMENT_TEMPLATE.format(
body='\n\n'.join(comment_body_parts),
)
post_comment = gh.pr.comment.bake(_in=comment)
if pr_number := os.environ.get('PR_NUMBER'):
post_comment = post_comment.bake(pr_number)
post_comment = post_comment.bake(body_file='-')
recent_comment_edit = _post(post_comment.bake('--edit-last'))
if recent_comment_edit == PostComment.NO_EXISTING_COMMENTS:
_post(post_comment)
if newly_failed:
raise typer.Exit(1)
def serve():
"""
Serve the iolanta.tech site.
The site will be available at http://localhost:9841
"""
sh.mkdocs.serve(
'-a', 'localhost:6453',
_fg=True,
)
def install_mkdocs_insiders():
"""Install Insiders version of `mkdocs-material` theme."""
name = 'mkdocs-material-insiders'
if not (Path.cwd() / name).is_dir():
sh.gh.repo.clone(f'iolanta-tech/{name}')
sh.pip.install('-e', name)
def deploy_to_github_pages():
"""Build the docs & deploy → gh-pages branch."""
sh.mkdocs('gh-deploy', '--force', '--clean', '--verbose')
def demonstrate_need_to_reset_cache():
"""Fail if cache is not reset in expand()."""
yaml_ld.expand(
'file:///home/anatoly/projects/iolanta/iolanta/data/'
'textual-browser.yaml',
)
expanded = jsonld.expand(
{
'@context': {
'@import': (
'https://json-ld.org/contexts/dollar-convenience.jsonld'
),
'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
'iolanta': 'https://iolanta.tech/',
'@base': 'https://iolanta.tech/visualizations/',
'x': {'@id': 'iolanta:visualized-with', '@type': '@id'},
},
'$id': 'https://iolanta.tech/visualizations/index.yaml',
'rdfs:label': 'Iolanta visualizations index 1.0',
'$included': [
{'$id': 'rdfs:', 'x': 'rdfs.yaml'},
{
'$id': 'http://xmlns.com/foaf/0.1/',
'x': 'foaf.yaml',
},
],
},
)
raise ValueError(expanded[0]['@included'])