Merge ~fginther/+git/ksc-test-results:fginther/hints into ~canonical-kernel/+git/ksc-test-results:main

Proposed by Francis Ginther
Status: Merged
Merged at revision: f879f6c111169c022449cec732faaef34ad72a8e
Proposed branch: ~fginther/+git/ksc-test-results:fginther/hints
Merge into: ~canonical-kernel/+git/ksc-test-results:main
Diff against target: 433 lines (+220/-18)
6 files modified
check-hint-age (+47/-0)
combine-results (+9/-2)
compile-individual-job-results (+134/-7)
do-it-all (+13/-3)
find-imported-job-results (+4/-4)
summarize-results (+13/-2)
Reviewer Review Type Date Requested Status
Canonical Kernel Pending
Review via email: mp+404699@code.launchpad.net

Commit message

Add support for applying hints and identifying provisioning failures

Add support for applying hints to test failures from a separate hints repo. Includes the ability to refresh the hints and the full reports when the hints have been updated.

Also identifies failures due to known provisioning issues.

Signed-off-by: Francis Ginther <email address hidden>

Description of the change

These will require the hint repo to be in place which will contain the 'compile-hints' script.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/check-hint-age b/check-hint-age
2new file mode 100755
3index 0000000..e1937cf
4--- /dev/null
5+++ b/check-hint-age
6@@ -0,0 +1,47 @@
7+#!/usr/bin/env python3
8+#
9+
10+import datetime
11+import os
12+
13+from argparse import ArgumentParser, RawDescriptionHelpFormatter
14+
15+
16+def main(raw_hints_dir, age):
17+ min_age = int(datetime.datetime.now().timestamp()) - (age * 60)
18+ print(min_age)
19+ for raw_hints in os.listdir(raw_hints_dir):
20+ if not os.path.isfile(os.path.join(raw_hints_dir, raw_hints)):
21+ continue
22+ mtime = int(os.path.getmtime(os.path.join(raw_hints_dir, raw_hints)))
23+ if mtime > min_age:
24+ return 1
25+ return 0
26+
27+
28+if __name__ == '__main__':
29+ retval = -1
30+
31+ # Command line argument setup and initial processing
32+ #
33+ app_description = '''
34+ Returns non-zero if any files in the requested dir are newer than the specified age.
35+ '''
36+ app_epilog = '''
37+ '''
38+ parser = ArgumentParser(description=app_description, epilog=app_epilog, formatter_class=RawDescriptionHelpFormatter)
39+ parser.add_argument('raw_hints_dir', help='Directory containing the raw hint files')
40+ parser.add_argument('age', help='Age to check (in minutes)')
41+
42+ args = parser.parse_args()
43+
44+ retval = 0
45+ try:
46+ retval = main(args.raw_hints_dir, int(args.age))
47+
48+ except KeyboardInterrupt:
49+ pass
50+
51+ exit(retval)
52+
53+# VI:SEt ts=4 sw=4 expandtab syntax=python:
54diff --git a/combine-results b/combine-results
55index 8fafdbc..1852917 100755
56--- a/combine-results
57+++ b/combine-results
58@@ -122,6 +122,12 @@ class Collector():
59 if rsuite['failed'] > 0:
60 status = 'failed'
61 break
62+ if rsuite.get('hinted', 0) > 0:
63+ status = 'hinted'
64+ break
65+ if rsuite.get('noprov', 0) > 0:
66+ status = 'noprov'
67+ break
68 if status == 'passed':
69 o += CS(f'{status:20s} ', CF('yellow'))
70 else:
71@@ -130,8 +136,9 @@ class Collector():
72 isd = sd['tests']['ops'][op]['flavours'][flavour]['clouds'][cloud]['suites'][suite]['instance-types'][instance_type]
73 if 'status' in isd:
74 if isd['status'] != 'failed':
75- isd['status'] = status
76- isd['job-name'] = rmeta['job-name']
77+ if isd['status'] != 'hinted':
78+ isd['status'] = status
79+ isd['job-name'] = rmeta['job-name']
80 else:
81 isd['status'] = status
82 isd['job-name'] = rmeta['job-name']
83diff --git a/compile-individual-job-results b/compile-individual-job-results
84index a7a8dee..612bdbe 100755
85--- a/compile-individual-job-results
86+++ b/compile-individual-job-results
87@@ -1,6 +1,7 @@
88 #!/usr/bin/env python3
89 #
90
91+import re
92 import sys
93 import os
94 import xmltodict
95@@ -31,6 +32,26 @@ class NoJunitResultError(Exception):
96 class NoConfigXMLError(Exception):
97 pass
98
99+class HintDB():
100+ '''
101+ Represents the set of hints to check and apply.
102+ '''
103+ def __init__(self, codename, kernel_package, hints_root):
104+ # Load the nearest hint file which matches this specific series and kernel.
105+ # If one doesn't exist, look for a generic fall back.
106+ # If none is found, use an empty dictionary so that no hints are applied.
107+ self._data = {}
108+ for filename in [f'{codename}-{kernel_package}-hints.json', 'hints.json']:
109+ hint_path = os.path.join(hints_root, filename)
110+ if os.path.exists(hint_path):
111+ with open(hint_path, 'r') as hint_file:
112+ self._data = json.loads(hint_file.read())
113+ break
114+
115+ def get_hints(self):
116+ return self._data
117+
118+
119 class JobResults():
120 '''
121 Take the junitResult.xml and job description from a jenkins job and combine them
122@@ -38,14 +59,16 @@ class JobResults():
123
124 This is a collection which can be iterated over like a dicionary.
125 '''
126- def __init__(self, job_root):
127+ def __init__(self, job_root, hint_root):
128 self._data = {}
129 self._description = None
130+ self.provisioning_issues = ['Provisioning', 'kernel-results.xml']
131
132 self._data = {
133- 'results' : self.condensed_results(self.junit_results(job_root)),
134- 'meta' : self.description(job_root),
135+ 'meta' : self.description(job_root)
136 }
137+ self._hints = HintDB(self.series_codename, self.kernel_package, hint_root).get_hints()
138+ self._data['results'] = self.condensed_results(self.junit_results(job_root))
139
140 def __iter__(self):
141 return iter(self._data)
142@@ -116,45 +139,136 @@ class JobResults():
143
144 return retval
145
146+ @property
147+ def series_codename(self):
148+ return self._data['meta']['series-codename']
149+
150+ @property
151+ def kernel_package(self):
152+ return self._data['meta']['kernel-package']
153+
154+ @property
155+ def kernel_version(self):
156+ return self._data['meta']['kernel-version']
157+
158+ @property
159+ def kernel_flavour(self):
160+ return self._data['meta']['kernel-flavour']
161+
162+ @property
163+ def kernel_arch(self):
164+ return self._data['meta']['arch']
165+
166+ @property
167+ def cloud(self):
168+ return self._data['meta']['cloud']
169+
170+ @property
171+ def instance_type(self):
172+ return self._data['meta']['instance-type']
173+
174+ def provisioning_failed(self, suite, case):
175+ if suite['name'] in self.provisioning_issues:
176+ return True
177+ return False
178+
179+ def hinted(self, suite, case):
180+ # Select the hints that match this suite's name. This needs to be an exact match.
181+ suite_hints = self._hints.get(suite['name'], [])
182+ for hint in suite_hints:
183+ if not re.search(hint['series'], self.series_codename):
184+ continue
185+ if not re.search(hint['source'], self.kernel_package):
186+ continue
187+ if not re.search(hint['version'], self.kernel_version):
188+ continue
189+ if not re.search(hint['flavour'], self.kernel_flavour):
190+ continue
191+ if not re.search(hint['arch'], self.kernel_arch):
192+ continue
193+ if not re.search(hint['cloud'], self.cloud):
194+ continue
195+ if not re.search(hint['instance-type'], self.instance_type):
196+ continue
197+ if not re.search(hint['test-case'], case['testName']):
198+ continue
199+ return True
200+ return False
201+
202 def _suite_results(self, _suite):
203 suite = {
204 'cases' : []
205 }
206 suite['name'] = _suite['name'].replace('Autotest - ', '')
207+ debug = False
208 suite['duration'] = _suite['duration']
209 total_failed = 0
210 total_skipped = 0
211+ total_hinted = 0
212+ total_noprov = 0
213 cl = _suite['cases']['case']
214 if type(cl) is list:
215 for case in _suite['cases']['case']:
216 failed = True if case['failedSince'] != '0' else False
217 skipped = True if case['skipped'] == 'true' else False
218+ hinted = False
219+ noprov = False
220+ if failed and self.provisioning_failed(suite, case):
221+ # Only check for failed provisioning if the test is failed
222+ # If it did fail to provision, there is no need to check the hints.
223+ noprov = True
224+ failed = False
225+ if failed and self.hinted(suite, case):
226+ # Only hint if the test case is otherwise failed
227+ hinted = True
228+ failed = False
229 case = {
230 'test-name' : case['testName'],
231 'failed' : failed,
232+ 'noprov' : noprov,
233 'skipped' : skipped,
234+ 'hinted' : hinted,
235 'duration' : case['duration'],
236 }
237 if failed: total_failed += 1
238+ if noprov: total_noprov += 1
239 if skipped: total_skipped += 1
240+ if hinted: total_hinted += 1
241 suite['cases'].append(case)
242
243 else:
244 case = _suite['cases']['case']
245 failed = True if case['failedSince'] != '0' else False
246 skipped = True if case['skipped'] == 'true' else False
247+ hinted = False
248+ noprov = False
249+ if failed and self.provisioning_failed(suite, case):
250+ # Only check for failed provisioning if the test is failed
251+ # If it did fail to provision, there is no need to check for hints
252+ noprov = True
253+ failed = False
254+ if failed and self.hinted(suite, case):
255+ # Only hint if the test case is otherwise failed
256+ hinted = True
257+ failed = False
258 case = {
259 'test-name' : case['testName'],
260 'failed' : failed,
261+ 'noprov' : noprov,
262 'skipped' : skipped,
263+ 'hinted' : hinted,
264 'duration' : case['duration'],
265 }
266 if failed: total_failed += 1
267+ if noprov: total_noprov += 1
268 if skipped: total_skipped += 1
269+ if hinted: total_hinted += 1
270 suite['cases'].append(case)
271 suite['failed'] = total_failed
272+ suite['noprov'] = total_noprov
273 suite['skipped'] = total_skipped
274- suite['passed'] = len(suite['cases']) - total_failed
275+ suite['hinted'] = total_hinted
276+ suite['passed'] = len(suite['cases']) - (total_failed + total_hinted + total_noprov)
277 return suite
278
279 def condensed_results(self, results):
280@@ -201,9 +315,9 @@ class JobResultsSummary():
281 # Go through the individual tests results and if any of them have
282 # failures then the summary for this kernel is a fail.
283 #
284- summary_status = 'failed' if self._failures(results) else 'passed'
285+ #summary_status = 'failed' if self._failures(results) else 'passed'
286
287- self._data['summary-status'] = summary_status
288+ self._data['summary-status'] = self._status(results)
289
290 for key in keepers:
291 self._data[key] = results['meta'][key]
292@@ -224,6 +338,18 @@ class JobResultsSummary():
293 return True
294 return False
295
296+ def _status(self, results):
297+ status = 'passed'
298+ for suite in results['results']['suites']:
299+ for case in suite['cases']:
300+ if case['failed']:
301+ return 'failed'
302+ if case.get('hinted', 0):
303+ status = 'hinted'
304+ if case.get('noprov', 0):
305+ status = 'noprov'
306+ return status
307+
308 def get(self, *args, **kwargs):
309 return self._data.get(*args, **kwargs)
310
311@@ -246,12 +372,13 @@ if __name__ == '__main__':
312 '''
313 parser = ArgumentParser(description=app_description, epilog=app_epilog, formatter_class=RawDescriptionHelpFormatter)
314 parser.add_argument('job', help='')
315+ parser.add_argument('hintroot', help='')
316 parser.add_argument('destroot', help='')
317
318 args = parser.parse_args()
319
320 try:
321- job_results = JobResults(args.job)
322+ job_results = JobResults(args.job, args.hintroot)
323 # pre_json(job_results._data)
324
325 dest = dest_path(args.destroot, job_results['meta'])
326diff --git a/do-it-all b/do-it-all
327index 7374080..c25e9a2 100755
328--- a/do-it-all
329+++ b/do-it-all
330@@ -14,8 +14,18 @@ $KT/import-raw-initiator-job-results initiator-raw imported
331 find web -name expectations.json | xargs rm
332 $KT/compile-initiator-job-results imported web
333
334-for job in `$KT/find-imported-job-results imported`; do
335- $KT/compile-individual-job-results $job web
336+if [ -d "hints/raw/.git" ]; then
337+ git -C hints/raw/ pull --ff-only
338+fi
339+ALL=""
340+if ! $KT/check-hint-age hints/raw/ 30; then
341+ # Hints have updated since last report cycle, refresh all
342+ ALL="--all"
343+ /testresults/hints/raw/compile-hints hints/raw hints/compiled
344+fi
345+
346+for job in `$KT/find-imported-job-results $ALL imported`; do
347+ $KT/compile-individual-job-results $job hints/compiled web
348 done
349
350 PKGS=`find web -maxdepth 4 -mindepth 4 -type d`
351@@ -44,7 +54,7 @@ for pkg in $PKGS; do
352 $KSC/rtr-lvl2 $pkg
353 done
354
355-$KSC/rtr-log .
356+$KSC/rtr-log $ALL .
357
358 timeout 15m $KSC/adt-cache
359 timeout 15m $KSC/refresh-cache
360diff --git a/find-imported-job-results b/find-imported-job-results
361index 9c4795d..327bb2e 100755
362--- a/find-imported-job-results
363+++ b/find-imported-job-results
364@@ -7,13 +7,13 @@ import sys
365 def pre(*args, **kwargs):
366 print(*args, file=sys.stderr, **kwargs)
367
368-def find_jenkins_job_results(root):
369+def find_jenkins_job_results(root, process_all):
370 '''
371 Recursively descend a directory tree looking for jenkins job results. These are
372 marked by having a 'junitResult.xml' file in their root.
373 '''
374 dlist = os.listdir(root)
375- if 'junitResult.xml' in dlist and '.processed' not in dlist:
376+ if 'junitResult.xml' in dlist and ('.processed' not in dlist or process_all):
377 # this looks like a winner
378 #
379 yield root
380@@ -21,7 +21,7 @@ def find_jenkins_job_results(root):
381 for fid in dlist:
382 p = f'{root}/{fid}'
383 if os.path.isdir(p):
384- yield from find_jenkins_job_results(p)
385+ yield from find_jenkins_job_results(p, process_all)
386
387
388 if __name__ == '__main__':
389@@ -35,5 +35,5 @@ if __name__ == '__main__':
390
391 args = parser.parse_args()
392
393- for jpath in find_jenkins_job_results(args.srcroot):
394+ for jpath in find_jenkins_job_results(args.srcroot, args.all):
395 print(jpath)
396diff --git a/summarize-results b/summarize-results
397index 4d6235e..943551f 100755
398--- a/summarize-results
399+++ b/summarize-results
400@@ -37,6 +37,8 @@ class Summ():
401 self.__sum = None
402
403 def cloud_summary_status(self, cs):
404+ hinted = False
405+ noprov = False
406 incomplete = False
407 for suite in cs['suites']:
408 for itype in cs['suites'][suite]['instance-types']:
409@@ -44,13 +46,22 @@ class Summ():
410 if status == 'failed':
411 return 'failed'
412
413+ if status == 'hinted':
414+ hinted = True
415+
416+ if status == 'noprov':
417+ noprov = True
418+
419 if status == 'incomplete':
420 incomplete = True
421
422+ if hinted:
423+ return 'hinted'
424+ if noprov:
425+ return 'noprov'
426 if incomplete:
427 return 'incomplete'
428- else:
429- return 'passed'
430+ return 'passed'
431
432 def summarize(self):
433 pre(CS(f'Summarizing Test Results ({self.root})', CF('green')))

Subscribers

People subscribed via source and target branches