Merge ~fginther/+git/ksc-test-results:fginther/hints into ~canonical-kernel/+git/ksc-test-results:main
- Git
- lp:~fginther/+git/ksc-test-results
- fginther/hints
- Merge into 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) |
Related bugs: |
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
1 | diff --git a/check-hint-age b/check-hint-age |
2 | new file mode 100755 |
3 | index 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: |
54 | diff --git a/combine-results b/combine-results |
55 | index 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'] |
83 | diff --git a/compile-individual-job-results b/compile-individual-job-results |
84 | index 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']) |
326 | diff --git a/do-it-all b/do-it-all |
327 | index 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 |
360 | diff --git a/find-imported-job-results b/find-imported-job-results |
361 | index 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) |
396 | diff --git a/summarize-results b/summarize-results |
397 | index 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'))) |