forked from fedora-modularity/check_modulemd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
check_modulemd.py
executable file
·405 lines (352 loc) · 15 KB
/
check_modulemd.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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
#!/usr/bin/env python
import os
import modulemd
import requests
import re
from enchant.checker import SpellChecker
from enchant import DictWithPWL
from pdc_client import PDCClient
from avocado import main
from avocado import Test
import yaml
import tempfile
from moduleframework import module_framework
from moduleframework import common
class ModulemdTest(Test):
"""
Validate modulemd
params:
:param modulemd: Path to the modulemd file.
"""
def setUp(self):
"""
Verify required modulemd file parameter has been specified, exists,
and can be loaded. The file name and loaded metadata are saved.
"""
mmd = modulemd.ModuleMetadata()
mdfile = self.params.get("modulemd")
self.tmdfile = None
if not mdfile:
# try to use module testing farmework if possible
# https://pagure.io/modularity-testing-framework
try:
mtf_backend = module_framework.CommonFunctions()
self.tmdfile = tempfile.mkstemp(suffix=".yaml")[1]
with open(self.tmdfile, "w+b") as yamlfile:
yaml.dump(mtf_backend.getModulemdYamlconfig(), yamlfile, default_flow_style=False)
mdfile = self.tmdfile
except common.ConfigExc:
pass
if mdfile is None:
self.error("modulemd parameter must be supplied")
mdfile = str(mdfile)
if not os.path.isfile(mdfile):
self.error("modulemd file %s must exist" % mdfile)
try:
mmd.load(mdfile)
except Exception as ex:
self.error("There was an error while processing modulemd file %s: %s" % (mdfile, ex))
# Infer the module name from the mdfile name and check that it is sane
mdfileModuleName, mdfileExtension = os.path.basename(mdfile).split('.', 1)
if (mdfileExtension != "yaml") and (mdfileExtension != "yml"):
self.error("modulemd file %s must have a .y[a]ml extension" % mdfile)
if mmd.name == '':
# The name can be missing from the metadata because the builder
# knows how to infer it
mmd.name = mdfileModuleName
elif mmd.name != mdfileModuleName:
self.error("modulemd file name %s and module name %s do not match" % (
mdfileModuleName, mmd.name))
self.mdfile = mdfile
self.mmd = mmd
def _init_spell_checker(self):
"""
Initialize spell checker dictionary
"""
default_dict = "en_US"
spell_dict = None
jargonfile = self.params.get('jargonfile')
if not jargonfile:
jargonfile = os.environ.get('JARGONFILE')
if jargonfile is not None:
try:
jargonfile = str(jargonfile)
spell_dict = DictWithPWL(default_dict, jargonfile)
except:
self.error(
"Could not initialize dictionary using %s file" % jargonfile)
if not spell_dict:
try:
spell_dict = DictWithPWL(default_dict)
except:
self.error(
"Could not initialize spell checker with dictionary %s" % default_dict)
#Check if there is jargonfile on module repo
url = ("https://src.fedoraproject.org/cgit/modules/%s.git/plain/jargon.txt" %
self.mmd.name)
resp = requests.get(url)
if resp.status_code >= 200 and resp.status_code < 300:
for w in resp.content.split("\n"):
if w != '':
spell_dict.add_to_session(w)
#add words from module name as jargon
for w in self.mmd.name.split('-'):
spell_dict.add_to_session(w)
try:
chkr = SpellChecker(spell_dict)
except:
self.error("Could not initialize spell checker")
return chkr
def test_debugdump(self):
"""
Make sure we can dump a copy of the metadata to the logs for debugging.
"""
data = self.mmd.dumps()
if not data:
self.error("Could not dump metadata for modulemd file %s" %
self.mdfile)
self.log.debug("Metadata dump for modulemd file %s:\n%s" %
(self.mdfile, data))
def test_api(self):
"""
Do we provide an API?
"""
self.log.info("Checking for presence of proper API definition")
#API is not mandatory, but most modules should have it
if not self.mmd.api:
self.log.warn("API is not defined for module file: %s" %
self.mdfile)
return
self.assertIsInstance(self.mmd.api.rpms, set)
self.assertGreater(len(self.mmd.api.rpms), 0)
def test_components(self):
"""
Do we include any components?
"""
self.log.info("Checking for presence of components")
self.assertTrue(self.mmd.components)
self.assertIsInstance(self.mmd.components.rpms, dict)
self.assertIsInstance(self.mmd.components.modules, dict)
self.assertGreater(len(self.mmd.components.rpms) +
len(self.mmd.components.modules), 0)
def _query_pdc(self, module_name, stream):
"""
Check if module and stream are built successfully on PDC server
"""
pdc_server = "https://pdc.fedoraproject.org/rest_api/v1/unreleasedvariants"
#Using develop=True to not authenticate to the server
pdc_session = PDCClient(pdc_server, ssl_verify=True, develop=True)
pdc_query = dict(
variant_id = module_name,
variant_version = stream,
#active=True returns only succesful builds
active = True
)
try:
mod_info = pdc_session(**pdc_query)
except Exception as ex:
self.error("Could not query PDC server for %s (stream: %s) - %s" % (
module_name, stream, ex))
if not mod_info or "results" not in mod_info.keys() or not mod_info["results"]:
self.error("%s (stream: %s) is not available on PDC" % (
module_name, stream))
def test_dependencies(self):
"""
Do our module-level dependencies look sane?
"""
# check that all the references modules and stream are registered in the PDC (i.e. they exist)
self.log.info("Checking sanity of module level dependencies")
if self.mmd.requires:
for p in self.mmd.requires.keys():
self._query_pdc(p, self.mmd.requires[p])
else:
self.log.info("No dependencies to sanity check")
self.log.info("Checking sanity of module level build dependencies")
if self.mmd.buildrequires:
for p in self.mmd.buildrequires.keys():
self._query_pdc(p, self.mmd.buildrequires[p])
else:
self.log.info("No build dependencies to sanity check")
def test_description(self):
"""
Does the description end with a period?
"""
self.log.info(
"Checking for presence of description that is properly punctuated")
if self.mmd.description and len(self.mmd.description) > 0:
if len(self.mmd.description) != len(self.mmd.description.rstrip()):
self.log.warn("Description should not end with newline/whitespace '%s'" %
self.mmd.description)
if not self.mmd.description.rstrip().endswith('.'):
self.log.warn("Description should end with a period: '%s'" %
self.mmd.description)
else:
self.error("No description")
def test_description_spelling(self):
"""
Any spellcheck failures in description?
"""
self.log.info("Checking for spelling errors in description")
chkr = self._init_spell_checker()
chkr.set_text(self.mmd.description)
for err in chkr:
self.log.warn(
"Potential spelling problem in description: %s" % err.word)
def _read_license_file(self, filename):
"""
Read all licenses from a file and return them as a list
"""
try:
with open(filename) as f:
data = f.readlines()
except Exception as ex:
self.error("Could not open file %s (%s)" % (filename, ex))
licenses = []
for line in data:
#remove all comments from the file
license = re.sub("#.*","", line).strip()
if not license:
continue
licenses.append(license)
return licenses
def _split_license(self, license):
license_regex = re.compile(r'\(([^)]+)\)|\s(?:and|or|AND|OR)\s')
return (x.strip() for x in
(l for l in license_regex.split(license) if l))
def test_license(self):
"""
Does the module provide license?
Based on code from: https://github.com/rpm-software-management/rpmlint/blob/master/TagsCheck.py
"""
self.log.info(
"Checking for presence of license")
if not self.mmd.module_licenses:
self.error("No module license")
self.assertIsInstance(self.mmd.module_licenses, set)
self.assertGreater(len(self.mmd.module_licenses), 0)
valid_licenses = []
license_files = [
"valid_sw_licenses.txt",
"valid_doc_licenses.txt",
"valid_content_licenses.txt"
]
for license_file in license_files:
licenses = self._read_license_file(license_file)
if not licenses:
continue
valid_licenses.extend(licenses)
#remove duplicated entries
valid_licenses = list(set(valid_licenses))
for license in self.mmd.module_licenses:
if license in valid_licenses:
continue
for l1 in self._split_license(license):
if l1 in valid_licenses:
continue
for l2 in self._split_license(l1):
if l2 not in valid_licenses:
self.error("License '%s' is not valid" % l2)
def test_summary(self):
"""
Does the summary not end with a period?
"""
self.log.info(
"Checking for presence of summary that is properly punctuated")
if self.mmd.summary and len(self.mmd.summary) > 0:
if len(self.mmd.summary) != len(self.mmd.summary.rstrip()):
self.log.warn("Summary should not end with newline/whitespace: '%s'" %
self.mmd.summary)
if self.mmd.summary.rstrip().endswith('.'):
self.log.warn("Summary should not end with a period: '%s'" %
self.mmd.summary)
else:
self.error("No summary")
def test_summary_spelling(self):
"""
Any spellcheck failures in summary?
"""
self.log.info("Checking for spelling errors in summary")
chkr = self._init_spell_checker()
chkr.set_text(self.mmd.summary)
for err in chkr:
self.log.warn(
"Potential spelling problem in summary: %s" % err.word)
def test_rationales(self):
"""
Do all the rationales end with a period?
"""
self.log.info(
"Checking for presence of component rationales that are properly punctuated")
for p in self.mmd.components.rpms.values():
if p.rationale and len(p.rationale) > 0:
if len(p.rationale) != len(p.rationale.rstrip()):
self.log.warn("Rationale for component RPM %s should" % p.name +
" not end with newline/whitespace: '%s'" % p.rationale)
if not p.rationale.rstrip().endswith('.'):
self.log.warn("Rationale for component RPM %s should end with a period: %s" % (
p.name, p.rationale))
else:
self.error("No rationale for component RPM %s" % p.name)
for p in self.mmd.components.modules.values():
if p.rationale and len(p.rationale) > 0:
if len(p.rationale) != len(p.rationale.rstrip()):
self.log.warn("Rationale for component module %s should" % p.name +
" not end with newline/whitespace: '%s'" % p.rationale)
if not p.rationale.rstrip().endswith('.'):
self.log.warn("Rationale for component module %s should end with a period: '%s'" % (
p.name, p.rationale))
else:
self.error("No rationale for component module %s" % p.name)
def test_rationales_spelling(self):
"""
Any spellcheck failures in rationales?
"""
self.log.info("Checking for spelling errors in component rationales")
chkr = self._init_spell_checker()
for p in self.mmd.components.rpms.values():
chkr.set_text(p.rationale)
for err in chkr:
self.log.warn("Potential spelling problem in component RPM %s rationale: %s" % (
p.name, err.word))
for p in self.mmd.components.modules.values():
chkr.set_text(p.rationale)
for err in chkr:
self.log.warn("Potential spelling problem in component module %s rationale: %s" % (
p.name, err.word))
def _is_commit_ref_available(self, package, namespace):
"""
Check if commit ref is available on git repository
"""
if not package or not namespace:
self.error("Missing parameter to check if commit is available")
repository = "https://src.fedoraproject.org/cgit/%s/%s.git" % (namespace, package.name)
if package.repository:
repository = package.repository
ref = "HEAD"
if package.ref:
ref = package.ref
patch_path = "/patch/?id=%s" % ref
url = repository + patch_path
resp = requests.head(url)
if resp.status_code < 200 or resp.status_code >= 300:
self.error("Could not find ref '%s' on '%s'. returned exit status %d; output:\n%s" %
(ref, package.name, resp.status_code, resp.text))
self.log.info("Found ref: %s for %s" % (ref, package.name))
def test_component_availability(self):
"""
Are all the components we reference in the packages section available?
"""
# verify that the specified ref (if none, defaults to master HEAD) is available in the
# specified repository (if none, defaults to Fedora dist-git).
for p in self.mmd.components.rpms.values():
self._is_commit_ref_available(p, "rpms")
for p in self.mmd.components.modules.values():
self._is_commit_ref_available(p, "modules")
def tearDown(self):
"""
Do any required teardown here
"""
if self.tmdfile:
os.remove(self.tmdfile)
if __name__ == "__main__":
main()