Coverage for CIResults/run_import.py: 89%
565 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-23 13:11 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-23 13:11 +0000
1from django.utils.functional import cached_property
2from django.core.exceptions import ValidationError
3from django.core.validators import URLValidator
4from django.db import transaction, connection
5from django.db.models import Q
6from django.utils import timezone
8from CIResults.models import TextStatus, TestResult, TestsuiteRun, RunConfig, RunConfigTag
9from CIResults.models import Machine, Test, TestSuite, Build
10from CIResults.models import UnknownFailure, KnownFailure, Issue
11from CIResults.models import IssueFilterAssociated, RunFilterStatistic
13from collections import defaultdict
14import configparser
15import traceback
16import datetime
17import copy
18import pytz
19import time
20import sys
21import os
23cur_dir = os.path.dirname(os.path.realpath(__file__))
24sys.path.append(os.path.join(cur_dir, 'piglit'))
25from framework import backends # noqa
28def str_to_list(string, separator=' '):
29 if string is None:
30 return []
31 else:
32 return [v.strip() for v in string.split(separator) if len(v) > 0]
35def validated_url(url):
36 if url is None:
37 return None
39 try:
40 URLValidator()(url)
41 return url
42 except ValidationError:
43 return None
46class TestsuiteTestResult:
47 def __init__(self, name, status, start_time, duration, command=None,
48 stdout=None, stderr=None, dmesg=None, url=None):
49 self._name = name
50 self._status = status
51 self._start_time = start_time
52 self._duration = duration
53 self._command = command
54 self._stdout = stdout
55 self._stderr = stderr
56 self._dmesg = dmesg
57 self._url = url
59 @property
60 def name(self):
61 return self._name
63 @property
64 def status(self):
65 return self._status
67 @property
68 def start_time(self):
69 return self._start_time
71 @property
72 def duration(self):
73 return self._duration
75 @property
76 def command(self):
77 return self._command
79 @property
80 def stdout(self):
81 return self._stdout
83 @property
84 def stderr(self):
85 return self._stderr
87 @property
88 def dmesg(self):
89 return self._dmesg
91 @property
92 def url(self):
93 return self._url
96class TestsuiteRunResults:
97 def __init__(self, testsuite, machine_name, run_id, test_results, start_time, duration):
98 self._testsuite = testsuite
99 self._machine_name = machine_name
100 self._run_id = run_id
101 self._test_results = test_results
102 self._start_time = start_time
103 self._duration = duration
105 @classmethod
106 def __result_url__(self, testsuite, run_id, machine_name, test_name):
107 # Generate the testresult's external URL
108 url_pattern = testsuite.result_url_pattern
109 url = url_pattern.format(runconfig=testsuite.runconfig.name,
110 testsuite_build=testsuite.build,
111 run_id=run_id, test=test_name,
112 machine=machine_name)
113 return validated_url(url)
115 @property
116 def testsuite(self):
117 return self._testsuite
119 @property
120 def machine_name(self):
121 return self._machine_name
123 @property
124 def run_id(self):
125 return self._run_id
127 @property
128 def test_results(self):
129 return self._test_results
131 @property
132 def start_time(self):
133 return self._start_time
135 @property
136 def duration(self):
137 return self._duration
139 @cached_property
140 def tests_set(self):
141 s = set()
142 for test_result in self._test_results:
143 s.add(test_result.name)
144 return s
146 @cached_property
147 def statuses_set(self):
148 s = set()
149 for test_result in self._test_results:
150 s.add(test_result.status)
151 return s
154class PiglitResult(TestsuiteRunResults):
155 def __init__(self, testsuite, machine_name, run_id, dir_name):
156 testresults = []
158 try:
159 results = backends.load(dir_name)
160 test_duration_sum = datetime.timedelta()
161 for test_name in results.tests:
162 test = results.tests[test_name]
164 url = self.__result_url__(testsuite, run_id, machine_name, test_name)
165 start_time = datetime.datetime.fromtimestamp(test.time.start, tz=pytz.utc)
166 duration = datetime.timedelta(seconds=test.time.total)
167 test_duration_sum += duration
168 testresults.append(TestsuiteTestResult(name=test_name, status=test.result,
169 start_time=start_time,
170 duration=duration,
171 command=test.command,
172 stdout=str(test.out),
173 stderr=str(test.err),
174 dmesg=test.dmesg,
175 url=url))
177 start = datetime.datetime.fromtimestamp(results.time_elapsed.start, tz=pytz.utc)
178 duration = datetime.timedelta(seconds=results.time_elapsed.total)
180 # Make sure the total duration is at least as long as the sum of all the test executions
181 if duration < test_duration_sum:
182 duration = test_duration_sum
183 except IndexError:
184 start = datetime.datetime.fromtimestamp(0, tz=pytz.utc)
185 duration = datetime.timedelta(seconds=0)
187 super().__init__(testsuite, machine_name, run_id, testresults,
188 start, duration)
191class JsonResult(TestsuiteRunResults):
192 def __init__(self, testsuite, machine_name: str, run_id: int, test_results: list[TestsuiteTestResult]):
193 test_run_duration_sum = datetime.timedelta()
194 for test_result in test_results:
195 test_run_duration_sum += test_result.duration
197 start = datetime.datetime.fromtimestamp(0, tz=pytz.utc)
198 super().__init__(testsuite, machine_name, run_id, test_results, start, test_run_duration_sum)
201class TestsuiteResults:
202 def __init__(self, runconfig, name, build, format, version, result_url_pattern):
203 self._runconfig = runconfig
204 self._name = name
205 self._build = build
206 self._format = format
207 self._version = version
208 self._result_url_pattern = result_url_pattern
210 # Check if the database contains the build, and then fetch the Testsuite
211 # associated with it
212 build = Build.objects.get(name=build)
213 self._db_object = TestSuite.objects.get(name=build.component)
215 # Now check the result format
216 if format == "piglit":
217 if version != 1:
218 msg = "The version {} of the testsuite result format '{}' is unsupported"
219 raise ValueError(msg.format(version, format))
220 self._result_type = PiglitResult
221 elif format == "json":
222 self._result_type = JsonResult
223 else:
224 raise ValueError("The testsuite result format '{}' is unsupported".format(format))
226 @property
227 def runconfig(self):
228 return self._runconfig
230 @property
231 def name(self):
232 return self._name
234 @property
235 def build(self):
236 return self._build
238 @property
239 def format(self):
240 return self._format
242 @property
243 def version(self):
244 return self._version
246 @property
247 def result_url_pattern(self):
248 return self._result_url_pattern
250 @property
251 def db_object(self):
252 return self._db_object
254 def read_results(self, machine, run_id, path):
255 return self._result_type(self, machine, run_id, path)
258class TestSuiteRunDef:
259 def __to_int__(self, field_name, value):
260 try:
261 return int(value)
262 except Exception:
263 raise ValueError("The parameter {} '{}' should be an integer".format(field_name, value))
265 def __init__(self, testsuite_build, results_format, results_format_version,
266 machine, ts_run_id, ts_run_path):
267 self.testsuite_build = testsuite_build
268 self.results_format = results_format
269 self.results_format_version = self.__to_int__("results_format_version", results_format_version)
270 self.machine = machine
271 self.ts_run_id = self.__to_int__("ts_run_id", ts_run_id)
272 self.ts_run_path = ts_run_path
274 for field in ["testsuite_build", "results_format", "machine", "ts_run_id", "ts_run_path"]:
275 if getattr(self, field) is None:
276 raise ValueError("The parameter {} cannot be None".format(field))
279# WARNING: This function is tailored for the purpose of adding results!
280# DO NOT USE AS A LIGHTWEIGHT REPLACEMENT FOR Issue.update_statistics()
281def issue_simple_stats_recomputing(issue, runconfig, stats_changes):
282 # Iterate through all the filters associated to this issue and check if
283 # a filter's statistic has changed from not covered/affected to
284 # covered/affected. If so, and if the issue was not already
285 # covered/affected, update the statistics by directly doing +1 in the
286 # relevant stats field. Since we are only adding results, we cannot be in a
287 # situation where we need to -1 an issue.
288 was_covered = False
289 has_new_filter_covering = False
290 was_affected = False
291 has_new_filter_matched = False
292 for issuefilter in issue.filters.all():
293 prev, new = stats_changes.get(issuefilter, (None, None))
295 # If we have no previous stats for the filter, just fake empty ones
296 if prev is None:
297 prev = RunFilterStatistic(filter=issuefilter, runconfig=runconfig,
298 matched_count=0, covered_count=0)
300 # Check if the issue was covered/affected for this runconfig before
301 if prev.covered_count > 0:
302 was_covered = True
303 if prev.matched_count > 0:
304 was_affected = True
305 # OPTIMIZATION: The issue was already affected, so no changes in
306 # statistics could come by adding more results
307 return
309 if new is not None:
310 if prev.covered_count == 0 and new.covered_count > 0:
311 has_new_filter_covering = True
312 if prev.matched_count == 0 and new.matched_count > 0:
313 has_new_filter_matched = True
315 # Update the covered/affected count if necessary
316 changed = False
317 if not was_covered and has_new_filter_covering:
318 issue.runconfigs_covered_count += 1
319 changed = True
321 if not was_affected and has_new_filter_matched:
322 issue.runconfigs_affected_count += 1
323 issue.last_seen = runconfig.added_on
324 issue.last_seen_runconfig = runconfig
325 changed = True
327 if changed:
328 issue.save()
331class RunConfigResults:
332 def __error__(self, msg):
333 raise ValueError(self._error_prefix + msg)
335 def __init__(self, runconfig_dir=None, name=None, url=None,
336 result_url_pattern=None, environment=None, builds=[], tags=[],
337 results=[], temporary=False, parsed_results=[]):
338 self._run_results = []
340 if runconfig_dir is not None:
341 self.__parse_run_info__(runconfig_dir)
342 if self._testsuites.get("CIRESULTS_TESTSUITE") is not None:
343 # DEPRECATED: Do not use this mode as it will be removed
344 self.__load_results_single_testsuite__(runconfig_dir)
345 else:
346 self.__load_results_multiple_testsuites__(runconfig_dir)
347 else:
348 self._error_prefix = ""
349 self._name = name
350 self._url = url
351 self._result_url_pattern = result_url_pattern
352 self._environment = environment
353 self._builds = builds
354 self._tags = tags
355 self._temporary = temporary
357 if self._name is None:
358 self.__error__("runconfig name unspecified")
360 if results:
361 self.__import_results_from_args__(results)
362 elif parsed_results:
363 self.__import_parsed_results__(parsed_results)
365 def __import_results_from_args__(self, results):
366 self._testsuites = dict()
367 build_to_testsuite = dict()
368 ts_runs = set()
370 for r in results:
371 # Check that the testsuite build is in the list of builds of the runconfig
372 if r.testsuite_build not in self.builds:
373 raise ValueError("The build named '{}' is not part of the list of "
374 "builds of the runconfig".format(r.testsuite_build))
376 # Get the testsuite associated to the build name, or create it
377 tsp = build_to_testsuite.get((r.testsuite_build, r.results_format,
378 r.results_format_version))
379 if tsp is None:
380 try:
381 testsuite_name = Build.objects.get(name=r.testsuite_build).component.name
383 # Create the testsuite results object
384 tsp = TestsuiteResults(self, testsuite_name, r.testsuite_build, r.results_format,
385 r.results_format_version, self._result_url_pattern)
386 self._testsuites[(r.testsuite_build, r.results_format, r.results_format_version)] = tsp
388 build_to_testsuite[r.testsuite_build] = tsp
389 except Build.DoesNotExist:
390 raise ValueError("The build named '{}' does not exist".format(r.testsuite_build))
392 # Check that the testsuite run has not been added already
393 ts_run_key = (testsuite_name, r.ts_run_id, self._name, r.machine)
394 if ts_run_key in ts_runs:
395 msg = "Try to import twice {}'s run ID {} on the runconfig '{}' for the machine '{}'"
396 raise ValueError(msg.format(testsuite_name, r.ts_run_id, self._name, r.machine))
397 else:
398 ts_runs.add(ts_run_key)
400 # Import the results
401 try:
402 self._run_results.append(tsp.read_results(r.machine, r.ts_run_id, r.ts_run_path))
403 except FileNotFoundError:
404 pass
405 except Exception:
406 traceback.print_exc()
408 def __import_parsed_results__(self, results: list[TestsuiteRunResults]):
409 self._run_results = results
411 def __parse_run_info__(self, runconfig_dir):
412 conf_path = os.path.join(runconfig_dir, "runconfig.ini")
413 self._error_prefix = "The RunConfig file {} is invalid: ".format(conf_path)
415 config = configparser.ConfigParser()
416 config.read(conf_path)
418 if not config.has_section("CIRESULTS_RUNCONFIG"):
419 self.__error__("missing the section CIRESULTS_RUNCONFIG")
421 # Parse the main section
422 section = config["CIRESULTS_RUNCONFIG"]
423 self._name = section.get('name')
424 self._url = validated_url(section.get('url'))
425 self._result_url_pattern = section.get('result_url_pattern', '')
426 self._environment = section.get('environment')
427 self._builds = str_to_list(section.get("builds"))
428 self._tags = str_to_list(section.get("tags"))
429 self._temporary = section.getboolean('temporary', False)
431 # Check that if CIRESULTS_TESTSUITE is set, then no other sections are there
432 if "CIRESULTS_TESTSUITE" in config.sections() and len(config.sections()) > 2:
433 msg = "If the section CIRESULTS_TESTSUITE exists, then no additional section/testsuite can be added"
434 self.__error__(msg)
436 # Parse the testsuite sections
437 self._testsuites = dict()
438 for section in config.sections():
439 # Ignore the main section
440 if section == "CIRESULTS_RUNCONFIG":
441 continue
443 build = config[section]['build']
444 if build not in self._builds:
445 msg = "The build '{}' of the testsuite '{}' is not found in the list of builds of the runconfig {}"
446 self.__error__(msg.format(build, section, self._name))
447 format = config[section]['format']
448 version = config[section].getint('version', 1)
449 result_url_pattern = config[section].get('result_url_pattern', '')
450 self._testsuites[section] = TestsuiteResults(self, section, build,
451 format, version,
452 result_url_pattern)
454 if self._name is None:
455 self.__error__("runconfig name unspecified")
457 def __load_testsuite_results__(self, tsp, testsuite_path):
458 # Since we have a valid test suite, we can now continue and look for
459 # all the machines that run any test
460 for machine in [f for f in os.listdir(testsuite_path)]:
461 machine_path = os.path.join(testsuite_path, machine)
462 if os.path.isfile(machine_path):
463 continue
465 for ts_run_id in os.listdir(machine_path):
466 ts_run_path = os.path.join(machine_path, ts_run_id)
467 if os.path.isfile(ts_run_path):
468 continue
470 # Convert the run_id to integer, or ignore the result
471 try:
472 ts_run_id = int(ts_run_id)
473 except Exception:
474 print("RunConfigResults: testsuite run ID '{}' should be an integer".format(ts_run_id))
475 continue
477 try:
478 self._run_results.append(tsp.read_results(machine, ts_run_id, ts_run_path))
479 except FileNotFoundError:
480 pass
481 except Exception:
482 traceback.print_exc()
484 def __load_results_single_testsuite__(self, runconfig_dir):
485 tsp = self._testsuites.get("CIRESULTS_TESTSUITE")
486 self.__load_testsuite_results__(tsp, runconfig_dir)
488 def __load_results_multiple_testsuites__(self, runconfig_dir):
489 for testsuite_name in [f for f in os.listdir(runconfig_dir)]:
490 testsuite_path = os.path.join(runconfig_dir, testsuite_name)
491 if os.path.isfile(testsuite_path):
492 continue
494 tsp = self._testsuites.get(testsuite_name)
495 if tsp is None:
496 print("Ignore the testsuite '{}' because it is not listed in the runconfig file".format(testsuite_name))
497 continue
499 self.__load_testsuite_results__(tsp, testsuite_path)
501 @property
502 def name(self):
503 return self._name
505 @property
506 def url(self):
507 return self._url
509 @property
510 def environment(self):
511 return self._environment
513 @property
514 def builds(self):
515 return self._builds
517 @property
518 def tags(self):
519 return self._tags
521 @property
522 def temporary(self):
523 return self._temporary
525 @property
526 def testsuites(self):
527 return self._testsuites
529 @cached_property
530 def tests(self):
531 tests = dict()
532 for run in self._run_results:
533 db_testsuite = run.testsuite.db_object
534 if db_testsuite not in tests:
535 tests[db_testsuite] = set()
536 tests[db_testsuite] |= run.tests_set
537 return tests
539 @cached_property
540 def machines(self):
541 machines = set()
542 for run in self._run_results:
543 machines.add(run.machine_name)
544 return machines
546 @cached_property
547 def text_statuses(self):
548 statuses = dict()
549 for run in self._run_results:
550 db_testsuite = run.testsuite.db_object
551 if db_testsuite not in statuses:
552 statuses[db_testsuite] = set()
553 statuses[db_testsuite] |= run.statuses_set
554 return statuses
556 def __preload_resources__(self, obj_manager, names):
557 ret = dict()
558 try:
559 if isinstance(names, list):
560 for name in names:
561 ret[name] = obj_manager.get(name=name)
562 elif isinstance(names, str):
563 ret[names] = obj_manager.get(name=names)
564 except Exception as e:
565 raise ValueError("The object {} does not exist in the database".format(name)) from e
566 return ret
568 def __add_missing__(self, obj_type, obj_str, objs, key_field, args, filter={}):
569 # Fetch the current list of objects
570 db_objs = set([getattr(o, key_field) for o in obj_type.objects.filter(**filter)])
572 # Add the missing objects
573 to_add = []
574 for obj in (objs - db_objs):
575 args[key_field] = obj
576 to_add.append(obj_type(**args))
577 if len(to_add) > 0:
578 print("adding {} missing {}".format(len(to_add), obj_str))
579 obj_type.objects.bulk_create(to_add)
581 # Now fetch all the objetcs and index them, making sure we do not add
582 # unecessary objects
583 ret = dict()
584 for entry in obj_type.objects.filter(**filter):
585 key = getattr(entry, key_field)
586 if key in objs:
587 ret[key] = entry
588 return ret
590 def __ts_runs_to_dict__(self, runconfig):
591 ret = dict()
592 for r in TestsuiteRun.objects.filter(runconfig=runconfig):
593 ret[(r.testsuite.name, r.machine.name, r.run_id)] = r
594 return ret
596 @transaction.atomic
597 def commit_to_db(self, new_machines_public=False, new_tests_public=False,
598 new_machines_vetted=False, new_tests_vetted=False,
599 new_statuses_vetted=False, quiet=False):
600 now = timezone.now()
602 # Pre-fetch all the resources
603 db_builds = self.__preload_resources__(Build.objects, self._builds)
604 db_tags = self.__preload_resources__(RunConfigTag.objects, self._tags)
606 # Create the runconfig if it does not exists
607 try:
608 runconfig = RunConfig.objects.get(name=self._name)
609 cur_builds = set(runconfig.builds.all())
610 except Exception:
611 # Create the run with the proper URL
612 runconfig = RunConfig(name=self._name, url=self.url,
613 environment=self.environment,
614 temporary=self._temporary)
615 runconfig.save()
617 # Add it to all the tags
618 for tag in db_tags.values():
619 runconfig.tags.add(tag)
621 # We have no current builds
622 cur_builds = set()
624 # Verify that we do not have two builds for the same component
625 components = dict()
626 for build in set(db_builds.values()) | cur_builds:
627 if build.component not in components:
628 components[build.component] = build
629 else:
630 msg = "ERROR: Two builds ({} and {}) cannot be from the same component ({})"
631 self.__error__(msg.format(build, components[build.component], build.component))
633 # Add all the new builds
634 for new_build in set(db_builds.values()) - cur_builds:
635 runconfig.builds.add(new_build)
637 # Abort early if there is nothing else to do
638 if len(self._run_results) == 0:
639 print("No results to add, exiting...")
640 return
642 # Load all the existing testsuite runs on this runconfig
643 db_ts_runs = dict()
644 tsr_filter = TestsuiteRun.objects.filter(runconfig__name=self._name)
645 for ts_run in tsr_filter.prefetch_related("machine"):
646 if ts_run.machine.name not in db_ts_runs:
647 db_ts_runs[ts_run.machine.name] = set()
648 db_ts_runs[ts_run.machine.name].add((ts_run.testsuite.name, ts_run.run_id))
650 # Add the missing machines, tests and statuses
651 db_machines = self.__add_missing__(Machine, "machine(s)", self.machines,
652 'name', {"public": new_machines_public,
653 "vetted_on": now if new_machines_vetted else None})
654 db_tests = dict()
655 for testsuite in self.tests:
656 db_tests[testsuite.name] = self.__add_missing__(Test,
657 "test(s) ({})".format(testsuite),
658 self.tests[testsuite], 'name',
659 {"public": new_tests_public,
660 "testsuite": testsuite,
661 "vetted_on": now if new_tests_vetted else None},
662 filter={"testsuite": testsuite})
664 # If the runconfig is non-temporary, convert all the tests
665 if not self._temporary:
666 cur_tests_ids = list()
667 for ts in db_tests.values():
668 cur_tests_ids.extend([t.id for t in ts.values()])
669 Test.objects.filter(pk__in=cur_tests_ids, first_runconfig=None).update(first_runconfig=runconfig)
671 db_statuses = dict()
672 for testsuite in self.text_statuses:
673 statuses = self.__add_missing__(TextStatus,
674 "statuse(s) ({})".format(testsuite),
675 self.text_statuses[testsuite],
676 'name', {"testsuite": testsuite,
677 "vetted_on": now if new_statuses_vetted else None},
678 filter={"testsuite": testsuite})
679 db_statuses[testsuite.name] = statuses
681 # Create all the ts_runs
682 new_ts_runs = []
683 to_add = []
684 for run in self._run_results:
685 # Ignore the runs we already have
686 if run.machine_name in db_ts_runs and (run.testsuite.name, run.run_id) in db_ts_runs[run.machine_name]:
687 continue
689 # Create the ts_run object
690 s = TestsuiteRun(testsuite=run.testsuite.db_object,
691 runconfig=runconfig, machine=db_machines[run.machine_name],
692 run_id=run.run_id, start=run.start_time, duration=run.duration)
693 to_add.append(s)
694 new_ts_runs.append(run)
695 if len(to_add) > 0:
696 print("adding {} testsuite runs".format(len(to_add)))
697 TestsuiteRun.objects.bulk_create(to_add)
698 ts_runs_db = self.__ts_runs_to_dict__(runconfig)
700 # Create all the TestResults and find the failures
701 new_results = []
702 failures = []
703 for run in new_ts_runs:
704 db_ts_run = ts_runs_db[(run.testsuite.name, run.machine_name, run.run_id)]
705 for result in run.test_results:
706 ts = run.testsuite.db_object
707 t = TestResult(test=db_tests[ts.name][result.name],
708 ts_run=db_ts_run,
709 status=db_statuses[ts.name][result.status],
710 start=result.start_time, duration=result.duration,
711 command=result.command,
712 stdout=result.stdout,
713 stderr=result.stderr,
714 dmesg=result.dmesg,
715 url=result.url)
716 new_results.append(t)
718 if ts.is_failure(t.status):
719 failures.append(t)
720 if len(new_results) > 0:
721 print("adding {} test results".format(len(new_results)))
723 # We'll need the primary IDs later, so bulk_create on postgresql
724 # and not on the others (since they do not support retrieving it).
725 if connection.vendor == "postgresql":
726 TestResult.objects.bulk_create(new_results, batch_size=5000)
727 else:
728 for result in new_results:
729 result.save()
731 # Fetch all the associated IssueFilters and their related issues
732 db_filters = dict()
733 active_ifas = IssueFilterAssociated.objects_ready_for_matching.filter(Q(deleted_on=None))
734 for e in active_ifas:
735 if e.filter not in db_filters:
736 db_filters[e.filter] = list()
737 db_filters[e.filter].append(e)
739 # Map the failures
740 start = time.time()
741 known_failures = []
742 unknown_failures = []
744 # Get the filters statistics already-existing on this runconfig and lock them for update
745 stats_changes = dict() # stores results as a tuple (current, new)
746 if not self.temporary:
747 for stat in RunFilterStatistic.objects.select_for_update().filter(runconfig=runconfig):
748 stats_changes[stat.filter] = (stat, None)
750 for result in failures:
751 found = False
752 for issuefilter in db_filters.keys():
753 # start matching the result to the current filter
754 if issuefilter.covers(result):
755 # Get or create a statistics object for the current filter and runconfig
756 fs = stats_changes.get(issuefilter, (None, None))
757 if fs[1] is None:
758 if fs[0] is not None:
759 stats_changes[issuefilter] = fs = (fs[0], copy.copy(fs[0]))
760 else:
761 stats_changes[issuefilter] = fs = (None, RunFilterStatistic(filter=issuefilter,
762 runconfig=runconfig,
763 matched_count=0,
764 covered_count=0))
766 fs[1].covered_count += 1
768 if issuefilter.matches(result, skip_cover_test=True):
769 fs[1].matched_count += 1
770 for ifa in db_filters[issuefilter]:
771 known_failures.append(KnownFailure(result=result,
772 matched_ifa=ifa))
773 found = True
774 if not found:
775 unknown_failures.append(UnknownFailure(result=result))
777 msg = "Found {} test failures ({} filters matched, {} failures left unmatched) in {:.2f} ms"
778 print(msg.format(len(failures), len(known_failures), len(unknown_failures), (time.time() - start) * 1000))
779 KnownFailure.objects.bulk_create(known_failures)
780 UnknownFailure.objects.bulk_create(unknown_failures)
782 # Create the statistics objects, but only if the run was not temporary
783 if not self.temporary:
784 # Create a new transaction, in case the creation of stats fail
785 try:
786 with transaction.atomic():
787 # Save all the stats (WARNING: some might already exist in the DB, hence the call to .save())
788 new_stats = [fs[1] for fs in stats_changes.values() if fs[1] is not None]
789 print("Updating the statistics of {} filters".format(len(new_stats)))
790 start = time.time()
791 for stat in new_stats:
792 try:
793 stat.save()
794 except Exception:
795 traceback.print_exc()
796 print("Filter statistics updated in {:.2f} ms".format((time.time() - start) * 1000))
798 # Get the list of issues that need to be updated because the filters'
799 # statistics got updated
800 issues = Issue.objects.select_for_update().filter(filters__in=stats_changes.keys())
801 print("Updating the statistics of {} issues".format(len(issues)))
802 start = time.time()
803 for issue in issues.prefetch_related('filters'):
804 issue_simple_stats_recomputing(issue, runconfig, stats_changes)
805 print("Issue statistics updated in {:.2f} ms".format((time.time() - start) * 1000))
806 except Exception:
807 traceback.print_exc()
809 # Go through all the unknown failures
810 if len(unknown_failures) > 0:
811 # Fetch all archived IFAs that are less than 6 months old
812 db_archived_filters = defaultdict(list)
813 archived_threshold = now - datetime.timedelta(days=180)
814 a_ifas = IssueFilterAssociated.objects.exclude(deleted_on=None).exclude(deleted_on__lt=archived_threshold)
815 for e in a_ifas:
816 db_archived_filters[e.filter].append(e)
818 # Try to match the unknown failures with archived IFAs
819 start = time.time()
820 filters_matching = set()
821 for failure in unknown_failures:
822 for issuefilter in db_archived_filters.keys():
823 if issuefilter.matches(failure.result):
824 failure.matched_archived_ifas.add(*db_archived_filters[issuefilter])
825 filters_matching.add(issuefilter)
827 msg = "Found {}/{} recently-archived filters matching some unknown failures in {:.2f} ms"
828 print(msg.format(len(filters_matching), len(a_ifas), (time.time() - start) * 1000))