1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
16 from copy
import deepcopy
17 from pathlib
import Path
18 from statistics
import median
19 from xmlrpc
.client
import Fault
24 from yaml
import CLoader
as Loader
26 from yaml
import Loader
31 from manifestparser
import ManifestParser
32 from manifestparser
.toml
import add_skip_if
, alphabetize_toml_str
, sort_paths
33 from mozci
.task
import TestTask
34 from mozci
.util
.taskcluster
import get_task
36 from taskcluster
.exceptions
import TaskclusterRestFailure
38 TASK_LOG
= "live_backing.log"
39 TASK_ARTIFACT
= "public/logs/" + TASK_LOG
40 ATTACHMENT_DESCRIPTION
= "Compressed " + TASK_ARTIFACT
+ " for task "
42 r
".*Created attachment ([0-9]+)\n.*"
43 + ATTACHMENT_DESCRIPTION
44 + "([A-Za-z0-9_-]+)\n.*"
47 BUGZILLA_AUTHENTICATION_HELP
= "Must create a Bugzilla API key per https://github.com/mozilla/mozci-tools/blob/main/citools/test_triage_bug_filer.py"
49 MS_PER_MINUTE
= 60 * 1000 # ms per minute
50 DEBUG_THRESHOLD
= 40 * MS_PER_MINUTE
# 40 minutes in ms
51 OPT_THRESHOLD
= 20 * MS_PER_MINUTE
# 20 minutes in ms
56 DIFFERENCE
= "difference"
57 DURATIONS
= "durations"
61 FAILED_RUNS
= "runs_failed"
62 FAILURE_RATIO
= 0.4 # more than this fraction of failures will disable
63 INTERMITTENT_RATIO_REFTEST
= 0.4 # reftest low frequency intermittent
64 FAILURE_RATIO_REFTEST
= 0.8 # disable ratio for reftest (high freq intermittent)
69 MEDIAN_DURATION
= "duration_median"
70 MINIMUM_RUNS
= 3 # mininum number of runs to consider success/failure
71 MOCK_BUG_DEFAULTS
= {"blocks": [], "comments": []}
72 MOCK_TASK_DEFAULTS
= {"extra": {}, "failure_types": {}, "results": []}
73 MOCK_TASK_INITS
= ["results"]
74 MODIFIERS
= "modifiers"
86 r
"image comparison, max difference: ([0-9]+), number of differing pixels: ([0-9]+)"
88 SUM_BY_LABEL
= "sum_by_label"
90 TEST_TYPES
= [EQEQ
, NOTEQ
]
91 TOTAL_DURATION
= "duration_total"
92 TOTAL_RUNS
= "runs_total"
93 WP
= "testing/web-platform/"
94 WPT0
= WP
+ "tests/infrastructure"
95 WPT_META0
= WP
+ "tests/infrastructure/metadata"
96 WPT_META0_CLASSIC
= WP
+ "meta/infrastructure"
98 WPT_META1
= WPT1
.replace("tests", "meta")
99 WPT2
= WP
+ "mozilla/tests"
100 WPT_META2
= WPT2
.replace("tests", "meta")
101 WPT_MOZILLA
= "/_mozilla"
105 def __init__(self
, data
, defaults
={}, inits
=[]):
107 self
._defaults
= defaults
109 values
= self
._data
.get(name
, []) # assume type is an array
110 values
= [Mock(value
, defaults
, inits
) for value
in values
]
111 self
._data
[name
] = values
113 def __getattr__(self
, name
):
114 if name
in self
._data
:
115 return self
._data
[name
]
116 if name
in self
._defaults
:
117 return self
._defaults
[name
]
121 class Classification(object):
122 "Classification of the failure (not the task result)"
124 DISABLE_INTERMITTENT
= "disable_intermittent" # reftest [40%, 80%)
125 DISABLE_FAILURE
= "disable_failure" # reftest (80%,100%] failure
126 DISABLE_MANIFEST
= "disable_manifest" # crash found
127 DISABLE_RECOMMENDED
= "disable_recommended" # disable first failing path
128 DISABLE_TOO_LONG
= "disable_too_long" # runtime threshold exceeded
129 INTERMITTENT
= "intermittent"
130 SECONDARY
= "secondary" # secondary failing path
131 SUCCESS
= "success" # path always succeeds
144 class Skipfails(object):
145 "mach manifest skip-fails implementation: Update manifests to skip failing tests"
148 REVISION
= "revision"
149 TREEHERDER
= "treeherder.mozilla.org"
150 BUGZILLA_SERVER_DEFAULT
= "bugzilla.allizom.org"
154 command_context
=None,
162 self
.command_context
= command_context
163 if self
.command_context
is not None:
164 self
.topsrcdir
= self
.command_context
.topsrcdir
166 self
.topsrcdir
= Path(__file__
).parent
.parent
167 self
.topsrcdir
= os
.path
.normpath(self
.topsrcdir
)
168 if isinstance(try_url
, list) and len(try_url
) == 1:
169 self
.try_url
= try_url
[0]
171 self
.try_url
= try_url
172 self
.dry_run
= dry_run
173 self
.implicit_vars
= implicit_vars
174 self
.verbose
= verbose
176 if bugzilla
is not None:
177 self
.bugzilla
= bugzilla
178 elif "BUGZILLA" in os
.environ
:
179 self
.bugzilla
= os
.environ
["BUGZILLA"]
181 self
.bugzilla
= Skipfails
.BUGZILLA_SERVER_DEFAULT
182 if self
.bugzilla
== "disable":
183 self
.bugzilla
= None # Bug filing disabled
184 self
.component
= "skip-fails"
186 self
._attach
_rx
= None
190 self
.headers
= {} # for Treeherder requests
191 self
.headers
["Accept"] = "application/json"
192 self
.headers
["User-Agent"] = "treeherder-pyclient"
193 self
.jobs_url
= "https://treeherder.mozilla.org/api/jobs/"
197 self
.bugs
= [] # preloaded bugs, currently not an updated cache
198 self
.error_summary
= {}
199 self
._subtest
_rx
= None
201 self
.failure_types
= None
203 def _initialize_bzapi(self
):
204 """Lazily initializes the Bugzilla API (returns True on success)"""
205 if self
._bzapi
is None and self
.bugzilla
is not None:
206 self
._bzapi
= bugzilla
.Bugzilla(self
.bugzilla
)
207 self
._attach
_rx
= re
.compile(ATTACHMENT_REGEX
, flags
=re
.M
)
208 return self
._bzapi
is not None
210 def pprint(self
, obj
):
212 self
.pp
= pprint
.PrettyPrinter(indent
=4, stream
=sys
.stderr
)
217 if self
.command_context
is not None:
218 self
.command_context
.log(
219 logging
.ERROR
, self
.component
, {ERROR
: str(e
)}, "ERROR: {error}"
222 print(f
"ERROR: {e}", file=sys
.stderr
, flush
=True)
224 def warning(self
, e
):
225 if self
.command_context
is not None:
226 self
.command_context
.log(
227 logging
.WARNING
, self
.component
, {ERROR
: str(e
)}, "WARNING: {error}"
230 print(f
"WARNING: {e}", file=sys
.stderr
, flush
=True)
233 if self
.command_context
is not None:
234 self
.command_context
.log(
235 logging
.INFO
, self
.component
, {ERROR
: str(e
)}, "INFO: {error}"
238 print(f
"INFO: {e}", file=sys
.stderr
, flush
=True)
244 def full_path(self
, filename
):
245 """Returns full path for the relative filename"""
247 return os
.path
.join(self
.topsrcdir
, os
.path
.normpath(filename
))
249 def isdir(self
, filename
):
250 """Returns True if filename is a directory"""
252 return os
.path
.isdir(self
.full_path(filename
))
254 def exists(self
, filename
):
255 """Returns True if filename exists"""
257 return os
.path
.exists(self
.full_path(filename
))
268 "Run skip-fails on try_url, return True on success"
270 try_url
= self
.try_url
271 revision
, repo
= self
.get_revision(try_url
)
272 if use_tasks
is not None:
273 tasks
= self
.read_tasks(use_tasks
)
274 self
.vinfo(f
"use tasks: {use_tasks}")
275 self
.failure_types
= None # do NOT cache failure_types
277 tasks
= self
.get_tasks(revision
, repo
)
278 self
.failure_types
= {} # cache failure_types
279 if use_failures
is not None:
280 failures
= self
.read_failures(use_failures
)
281 self
.vinfo(f
"use failures: {use_failures}")
283 failures
= self
.get_failures(tasks
)
284 if save_failures
is not None:
285 self
.write_json(save_failures
, failures
)
286 self
.vinfo(f
"save failures: {save_failures}")
287 if save_tasks
is not None:
288 self
.write_tasks(save_tasks
, tasks
)
289 self
.vinfo(f
"save tasks: {save_tasks}")
292 f
"skip-fails assumes implicit-vars for reftest: {self.implicit_vars}"
294 for manifest
in failures
:
295 kind
= failures
[manifest
][KIND
]
296 for label
in failures
[manifest
][LL
]:
297 for path
in failures
[manifest
][LL
][label
][PP
]:
298 classification
= failures
[manifest
][LL
][label
][PP
][path
][CC
]
299 if classification
.startswith("disable_") or (
300 self
.turbo
and classification
== Classification
.SECONDARY
302 anyjs
= {} # anyjs alternate basename = False
306 lineno
= failures
[manifest
][LL
][label
][PP
][path
].get(LINENO
, 0)
307 for task_id
in failures
[manifest
][LL
][label
][PP
][path
][RUNS
]:
308 if kind
== Kind
.TOML
:
309 break # just use the first task_id
310 elif kind
== Kind
.LIST
:
311 difference
= failures
[manifest
][LL
][label
][PP
][path
][
313 ][task_id
].get(DIFFERENCE
, 0)
315 differences
.append(difference
)
316 pixel
= failures
[manifest
][LL
][label
][PP
][path
][RUNS
][
321 status
= failures
[manifest
][LL
][label
][PP
][path
][RUNS
][
324 elif kind
== Kind
.WPT
:
325 filename
= os
.path
.basename(path
)
326 anyjs
[filename
] = False
329 in failures
[manifest
][LL
][label
][PP
][path
][RUNS
][
333 query
= failures
[manifest
][LL
][label
][PP
][path
][
336 anyjs
[filename
+ query
] = False
341 in failures
[manifest
][LL
][label
][PP
][path
][RUNS
][
345 any_filename
= os
.path
.basename(
346 failures
[manifest
][LL
][label
][PP
][path
][RUNS
][
350 anyjs
[any_filename
] = False
351 if query
is not None:
352 anyjs
[any_filename
+ query
] = False
371 if max_failures
>= 0 and num_failures
>= max_failures
:
373 f
"max_failures={max_failures} threshold reached: stopping."
378 def get_revision(self
, url
):
379 parsed
= urllib
.parse
.urlparse(url
)
380 if parsed
.scheme
!= "https":
381 raise ValueError("try_url scheme not https")
382 if parsed
.netloc
!= Skipfails
.TREEHERDER
:
383 raise ValueError(f
"try_url server not {Skipfails.TREEHERDER}")
384 if len(parsed
.query
) == 0:
385 raise ValueError("try_url query missing")
386 query
= urllib
.parse
.parse_qs(parsed
.query
)
387 if Skipfails
.REVISION
not in query
:
388 raise ValueError("try_url query missing revision")
389 revision
= query
[Skipfails
.REVISION
][0]
390 if Skipfails
.REPO
in query
:
391 repo
= query
[Skipfails
.REPO
][0]
394 self
.vinfo(f
"considering {repo} revision={revision}")
395 return revision
, repo
397 def get_tasks(self
, revision
, repo
):
398 push
= mozci
.push
.Push(revision
, repo
)
401 def get_failures(self
, tasks
):
403 find failures and create structure comprised of runs by path:
407 classification: Classification
408 * unknown (default) < 3 runs
409 * intermittent (not enough failures)
410 * disable_recommended (enough repeated failures) >3 runs >= 4
411 * disable_manifest (disable DEFAULT if no other failures)
412 * secondary (not first failure in group)
427 SUM_BY_LABEL
: {}, # All sums implicitly zero
431 CC
: Classification
.UNKNOWN
,
440 for task
in tasks
: # add explicit failures
442 if len(task
.results
) == 0:
443 continue # ignore aborted tasks
444 failure_types
= task
.failure_types
# call magic property once
445 if self
.failure_types
is not None:
446 self
.failure_types
[task
.id] = failure_types
447 self
.vinfo(f
"Getting failure_types from task: {task.id}")
448 for manifest
in failure_types
:
452 if mm
.endswith(".ini"):
454 f
"cannot analyze skip-fails on INI manifests: {mm}"
457 elif mm
.endswith(".list"):
459 elif mm
.endswith(".toml"):
463 path
, mm
, _query
, _anyjs
= self
.wpt_paths(mm
)
464 if path
is None: # not WPT
466 f
"cannot analyze skip-fails on unknown manifest type: {manifest}"
470 if mm
not in manifest_paths
:
471 manifest_paths
[mm
] = []
473 ff
[mm
] = deepcopy(manifest_
)
475 if ll
not in ff
[mm
][LL
]:
476 ff
[mm
][LL
][ll
] = deepcopy(label_
)
477 for path_type
in failure_types
[manifest
]:
478 path
, _type
= path_type
483 path
, mmpath
, query
, anyjs
= self
.wpt_paths(path
)
486 f
"non existant failure path: {path_type[0]}"
490 mm
= os
.path
.dirname(mmpath
)
491 if mm
not in manifest_paths
:
492 manifest_paths
[mm
] = []
494 ff
[mm
] = deepcopy(manifest_
)
496 if ll
not in ff
[mm
][LL
]:
497 ff
[mm
][LL
][ll
] = deepcopy(label_
)
498 elif kind
== Kind
.LIST
:
500 if len(words
) != 3 or words
[1] not in TEST_TYPES
:
501 self
.warning(f
"reftest type not supported: {path}")
503 allpaths
= self
.get_allpaths(task
.id, mm
, path
)
504 elif kind
== Kind
.TOML
:
506 path
= DEF
# refers to the manifest itself
508 for path
in allpaths
:
509 if path
in manifest_paths
[mm
]:
510 continue # duplicate path for this task
511 manifest_paths
[mm
].append(path
)
513 f
"Getting failure info in manifest: {mm}, path: {path}"
515 if path
not in ff
[mm
][LL
][ll
][PP
]:
516 ff
[mm
][LL
][ll
][PP
][path
] = deepcopy(path_
)
517 if task
.id not in ff
[mm
][LL
][ll
][PP
][path
][RUNS
]:
518 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id] = deepcopy(run_
)
519 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][RR
] = False
520 if query
is not None:
521 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][QUERY
] = query
522 if anyjs
is not None:
523 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][ANYJS
] = anyjs
524 ff
[mm
][LL
][ll
][PP
][path
][TOTAL_RUNS
] += 1
525 ff
[mm
][LL
][ll
][PP
][path
][FAILED_RUNS
] += 1
526 if kind
== Kind
.LIST
:
532 ) = self
.get_lineno_difference_pixels_status(
536 ff
[mm
][LL
][ll
][PP
][path
][LINENO
] = lineno
538 self
.vinfo(f
"ERROR no lineno for {path}")
540 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][
543 if status
== FAIL
and difference
== 0 and pixels
== 0:
544 # intermittent, not error
545 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][RR
] = True
546 ff
[mm
][LL
][ll
][PP
][path
][FAILED_RUNS
] -= 1
548 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][
552 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][
555 except AttributeError:
556 pass # self.warning(f"unknown attribute in task (#1): {ae}")
558 for task
in tasks
: # add results
560 if len(task
.results
) == 0:
561 continue # ignore aborted tasks
562 self
.vinfo(f
"Getting results from task: {task.id}")
563 for result
in task
.results
:
567 if mm
.endswith(".ini"):
569 f
"cannot analyze skip-fails on INI manifests: {mm}"
572 elif mm
.endswith(".list"):
574 elif mm
.endswith(".toml"):
578 path
, mm
, _query
, _anyjs
= self
.wpt_paths(mm
)
579 if path
is None: # not WPT
581 f
"cannot analyze skip-fails on unknown manifest type: {result.group}"
584 if mm
not in manifest_paths
:
587 ff
[mm
] = deepcopy(manifest_
)
588 if ll
not in ff
[mm
][LL
]:
589 ff
[mm
][LL
][ll
] = deepcopy(label_
)
590 if task
.id not in ff
[mm
][LL
][ll
][DURATIONS
]:
591 # duration may be None !!!
592 ff
[mm
][LL
][ll
][DURATIONS
][task
.id] = result
.duration
or 0
593 if ff
[mm
][LL
][ll
][OPT
] is None:
594 ff
[mm
][LL
][ll
][OPT
] = self
.get_opt_for_task(task
.id)
595 for path
in manifest_paths
[mm
]: # all known paths
596 self
.vinfo(f
"Getting result for manifest: {mm}, path: {path}")
597 if path
not in ff
[mm
][LL
][ll
][PP
]:
598 ff
[mm
][LL
][ll
][PP
][path
] = deepcopy(path_
)
599 if task
.id not in ff
[mm
][LL
][ll
][PP
][path
][RUNS
]:
600 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id] = deepcopy(run_
)
601 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][RR
] = result
.ok
602 ff
[mm
][LL
][ll
][PP
][path
][TOTAL_RUNS
] += 1
604 ff
[mm
][LL
][ll
][PP
][path
][FAILED_RUNS
] += 1
605 if kind
== Kind
.LIST
:
611 ) = self
.get_lineno_difference_pixels_status(
615 ff
[mm
][LL
][ll
][PP
][path
][LINENO
] = lineno
617 self
.vinfo(f
"ERROR no lineno for {path}")
619 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][
628 # intermittent, not error
629 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][RR
] = True
630 ff
[mm
][LL
][ll
][PP
][path
][FAILED_RUNS
] -= 1
632 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][
636 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][
639 except AttributeError:
640 pass # self.warning(f"unknown attribute in task (#2): {ae}")
642 for mm
in ff
: # determine classifications
644 for label
in ff
[mm
][LL
]:
646 opt
= ff
[mm
][LL
][ll
][OPT
]
647 durations
= [] # summarize durations
648 for task_id
in ff
[mm
][LL
][ll
][DURATIONS
]:
649 duration
= ff
[mm
][LL
][ll
][DURATIONS
][task_id
]
650 durations
.append(duration
)
651 if len(durations
) > 0:
652 total_duration
= sum(durations
)
653 median_duration
= median(durations
)
654 ff
[mm
][LL
][ll
][TOTAL_DURATION
] = total_duration
655 ff
[mm
][LL
][ll
][MEDIAN_DURATION
] = median_duration
656 if (opt
and median_duration
> OPT_THRESHOLD
) or (
657 (not opt
) and median_duration
> DEBUG_THRESHOLD
659 if kind
== Kind
.TOML
:
662 paths
= ff
[mm
][LL
][ll
][PP
].keys()
664 if path
not in ff
[mm
][LL
][ll
][PP
]:
665 ff
[mm
][LL
][ll
][PP
][path
] = deepcopy(path_
)
666 if task_id
not in ff
[mm
][LL
][ll
][PP
][path
][RUNS
]:
667 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id] = deepcopy(run_
)
668 ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task
.id][RR
] = False
669 ff
[mm
][LL
][ll
][PP
][path
][TOTAL_RUNS
] += 1
670 ff
[mm
][LL
][ll
][PP
][path
][FAILED_RUNS
] += 1
671 ff
[mm
][LL
][ll
][PP
][path
][
673 ] = Classification
.DISABLE_TOO_LONG
674 primary
= True # we have not seen the first failure
675 for path
in sort_paths(ff
[mm
][LL
][ll
][PP
]):
676 classification
= ff
[mm
][LL
][ll
][PP
][path
][CC
]
677 if classification
== Classification
.UNKNOWN
:
678 failed_runs
= ff
[mm
][LL
][ll
][PP
][path
][FAILED_RUNS
]
679 total_runs
= ff
[mm
][LL
][ll
][PP
][path
][TOTAL_RUNS
]
680 status
= FAIL
# default status, only one run could be PASS
681 for task_id
in ff
[mm
][LL
][ll
][PP
][path
][RUNS
]:
682 status
= ff
[mm
][LL
][ll
][PP
][path
][RUNS
][task_id
].get(
685 if kind
== Kind
.LIST
:
686 failure_ratio
= INTERMITTENT_RATIO_REFTEST
688 failure_ratio
= FAILURE_RATIO
689 if total_runs
>= MINIMUM_RUNS
:
690 if failed_runs
/ total_runs
< failure_ratio
:
692 classification
= Classification
.SUCCESS
694 classification
= Classification
.INTERMITTENT
695 elif kind
== Kind
.LIST
:
696 if failed_runs
/ total_runs
< FAILURE_RATIO_REFTEST
:
697 classification
= Classification
.DISABLE_INTERMITTENT
699 classification
= Classification
.DISABLE_FAILURE
702 classification
= Classification
.DISABLE_MANIFEST
704 classification
= Classification
.DISABLE_RECOMMENDED
707 classification
= Classification
.SECONDARY
708 ff
[mm
][LL
][ll
][PP
][path
][CC
] = classification
709 if classification
not in ff
[mm
][LL
][ll
][SUM_BY_LABEL
]:
710 ff
[mm
][LL
][ll
][SUM_BY_LABEL
][classification
] = 0
711 ff
[mm
][LL
][ll
][SUM_BY_LABEL
][classification
] += 1
714 def _get_os_version(self
, os
, platform
):
715 """Return the os_version given the label platform string"""
716 i
= platform
.find(os
)
718 yy
= platform
[j
: j
+ 2]
719 mm
= platform
[j
+ 2 : j
+ 4]
722 def get_bug_by_id(self
, id):
723 """Get bug by bug id"""
730 if bug
is None and self
._initialize
_bzapi
():
731 bug
= self
._bzapi
.getbug(id)
734 def get_bugs_by_summary(self
, summary
):
735 """Get bug by bug summary"""
739 if b
.summary
== summary
:
743 if self
._initialize
_bzapi
():
744 query
= self
._bzapi
.build_query(short_desc
=summary
)
745 query
["include_fields"] = [
754 bugs
= self
._bzapi
.query(query
)
759 summary
="Bug short description",
760 description
="Bug description",
763 version
="unspecified",
769 if self
._initialize
_bzapi
():
770 if not self
._bzapi
.logged_in
:
772 "Must create a Bugzilla API key per https://github.com/mozilla/mozci-tools/blob/main/citools/test_triage_bug_filer.py"
774 raise PermissionError(f
"Not authenticated for Bugzilla {self.bugzilla}")
775 createinfo
= self
._bzapi
.build_createbug(
780 description
=description
,
782 createinfo
["type"] = bugtype
783 bug
= self
._bzapi
.createbug(createinfo
)
786 def add_bug_comment(self
, id, comment
, meta_bug_id
=None):
787 """Add a comment to an existing bug"""
789 if self
._initialize
_bzapi
():
790 if not self
._bzapi
.logged_in
:
791 self
.error(BUGZILLA_AUTHENTICATION_HELP
)
792 raise PermissionError("Not authenticated for Bugzilla")
793 if meta_bug_id
is not None:
794 blocks_add
= [meta_bug_id
]
797 updateinfo
= self
._bzapi
.build_update(
798 comment
=comment
, blocks_add
=blocks_add
800 self
._bzapi
.update_bugs([id], updateinfo
)
821 Skip a failure (for TOML, WPT and REFTEST manifests)
822 For wpt anyjs is a dictionary mapping from alternate basename to
823 a boolean (indicating if the basename has been handled in the manifest)
826 self
.vinfo(f
"\n\n===== Skip failure in manifest: {manifest} =====")
827 self
.vinfo(f
" path: {path}")
831 skip_if
= self
.task_to_skip_if(task_id
, kind
)
834 f
"Unable to calculate skip-if condition from manifest={manifest} from failure label={label}"
837 if kind
== Kind
.TOML
:
839 elif kind
== Kind
.WPT
:
840 _path
, manifest
, _query
, _anyjs
= self
.wpt_paths(path
)
841 filename
= os
.path
.basename(path
)
842 elif kind
== Kind
.LIST
:
845 self
.info(f
"Unexpected status: {status}")
846 if status
== PASS
or classification
== Classification
.DISABLE_INTERMITTENT
:
847 zero
= True # refest lower ranges should include zero
851 if classification
== Classification
.DISABLE_MANIFEST
:
852 comment
= "Disabled entire manifest due to crash result"
853 elif classification
== Classification
.DISABLE_TOO_LONG
:
854 comment
= "Disabled entire manifest due to excessive run time"
856 if kind
== Kind
.TOML
:
857 filename
= self
.get_filename_in_manifest(manifest
, path
)
858 comment
= f
'Disabled test due to failures in test file: "{filename}"'
859 if classification
== Classification
.SECONDARY
:
860 comment
+= " (secondary)"
862 bug_reference
= " (secondary)"
863 if kind
!= Kind
.LIST
:
864 self
.vinfo(f
"filename: {filename}")
865 if kind
== Kind
.WPT
and len(anyjs
) > 1:
866 comment
+= "\nAdditional WPT wildcard paths:"
867 for p
in sorted(anyjs
.keys()):
869 comment
+= f
'\n "{p}"'
870 platform
, testname
= self
.label_to_platform_testname(label
)
871 if platform
is not None:
872 comment
+= "\nCommand line to reproduce (experimental):\n"
873 comment
+= f
" \"mach try fuzzy -q '{platform}' {testname}\""
874 comment
+= f
"\nTry URL = {try_url}"
875 comment
+= f
"\nrevision = {revision}"
876 comment
+= f
"\nrepo = {repo}"
877 comment
+= f
"\nlabel = {label}"
878 if task_id
is not None:
879 comment
+= f
"\ntask_id = {task_id}"
880 if kind
!= Kind
.LIST
:
881 push_id
= self
.get_push_id(revision
, repo
)
882 if push_id
is not None:
883 comment
+= f
"\npush_id = {push_id}"
884 job_id
= self
.get_job_id(push_id
, task_id
)
885 if job_id
is not None:
886 comment
+= f
"\njob_id = {job_id}"
892 ) = self
.get_bug_suggestions(repo
, job_id
, path
, anyjs
)
893 if log_url
is not None:
894 comment
+= f
"\nBug suggestions: {suggestions_url}"
895 comment
+= f
"\nSpecifically see at line {line_number} in the attached log: {log_url}"
896 comment
+= f
'\n\n "{line}"\n'
897 bug_summary
= f
"MANIFEST {manifest}"
900 if self
.bugzilla
is None:
901 self
.vinfo("Bugzilla has been disabled: no bugs created or updated")
903 bugs
= self
.get_bugs_by_summary(bug_summary
)
906 f
"This bug covers excluded failing tests in the MANIFEST {manifest}"
908 description
+= "\n(generated by `mach manifest skip-fails`)"
909 product
, component
= self
.get_file_info(path
)
912 f
'Dry-run NOT creating bug: {product}::{component} "{bug_summary}"'
915 bug
= self
.create_bug(bug_summary
, description
, product
, component
)
918 f
'Created Bug {bugid} {product}::{component} : "{bug_summary}"'
922 product
= bugs
[0].product
923 component
= bugs
[0].component
924 self
.vinfo(f
'Found Bug {bugid} {product}::{component} "{bug_summary}"')
925 if meta_bug_id
is not None:
926 if meta_bug_id
in bugs
[0].blocks
:
928 f
" Bug {bugid} already blocks meta bug {meta_bug_id}"
930 meta_bug_id
= None # no need to add again
931 comments
= bugs
[0].getcomments()
932 for i
in range(len(comments
)):
933 text
= comments
[i
]["text"]
934 m
= self
._attach
_rx
.findall(text
)
937 attachments
[a_task_id
] = m
[0][0]
938 if a_task_id
== task_id
:
940 f
" Bug {bugid} already has the compressed log attached for this task"
943 self
.error(f
'More than one bug found for summary: "{bug_summary}"')
945 bug_reference
= f
"Bug {bugid}" + bug_reference
946 extra
= self
.get_extra(task_id
)
948 if kind
== Kind
.LIST
:
950 f
"\nfuzzy-if condition on line {lineno}: {skip_if} # {bug_reference}"
953 comment
+= f
"\nskip-if condition: {skip_if} # {bug_reference}"
954 manifest_path
= self
.full_path(manifest
)
956 additional_comment
= ""
958 if os
.path
.exists(manifest_path
):
959 manifest_str
= io
.open(manifest_path
, "r", encoding
="utf-8").read()
961 # ensure parent directories exist
962 os
.makedirs(os
.path
.dirname(manifest_path
), exist_ok
=True)
963 manifest_str
, additional_comment
= self
.wpt_add_skip_if(
964 manifest_str
, anyjs
, skip_if
, bug_reference
966 elif kind
== Kind
.TOML
:
967 mp
= ManifestParser(use_toml
=True, document
=True)
968 mp
.read(manifest_path
)
969 document
= mp
.source_documents
[manifest_path
]
970 additional_comment
= add_skip_if(document
, filename
, skip_if
, bug_reference
)
971 manifest_str
= alphabetize_toml_str(document
)
972 elif kind
== Kind
.LIST
:
975 f
"cannot determine line to edit in manifest: {manifest_path}"
977 # elif skip_if.find("winWidget") >= 0 and skip_if.find("!is64Bit") >= 0:
979 # "Skipping failures for Windows 32-bit are temporarily disabled"
981 elif not os
.path
.exists(manifest_path
):
982 self
.error(f
"manifest does not exist: {manifest_path}")
984 manifest_str
= io
.open(manifest_path
, "r", encoding
="utf-8").read()
985 manifest_str
, additional_comment
= self
.reftest_add_fuzzy_if(
995 if not manifest_str
and additional_comment
:
996 self
.warning(additional_comment
)
997 if additional_comment
:
998 comment
+= "\n" + additional_comment
999 if len(manifest_str
) > 0:
1000 fp
= io
.open(manifest_path
, "w", encoding
="utf-8", newline
="\n")
1001 fp
.write(manifest_str
)
1003 self
.info(f
'Edited ["{filename}"] in manifest: "{manifest}"')
1004 if kind
!= Kind
.LIST
:
1005 self
.info(f
'added skip-if condition: "{skip_if}" # {bug_reference}')
1007 self
.info(f
"Dry-run NOT adding comment to Bug {bugid}:\n{comment}")
1009 f
'Dry-run NOT editing ["{filename}"] in manifest: "{manifest}"'
1011 self
.info(f
'would add skip-if condition: "{skip_if}" # {bug_reference}')
1012 if task_id
is not None and task_id
not in attachments
:
1013 self
.info("would add compressed log for this task")
1015 elif self
.bugzilla
is None:
1016 self
.warning(f
"NOT adding comment to Bug {bugid}:\n{comment}")
1018 self
.add_bug_comment(bugid
, comment
, meta_bug_id
)
1019 self
.info(f
"Added comment to Bug {bugid}:\n{comment}")
1020 if meta_bug_id
is not None:
1021 self
.info(f
" Bug {bugid} blocks meta Bug: {meta_bug_id}")
1022 if task_id
is not None and task_id
not in attachments
:
1023 self
.add_attachment_log_for_task(bugid
, task_id
)
1024 self
.info("Added compressed log for this task")
1026 self
.error(f
'Error editing ["{filename}"] in manifest: "{manifest}"')
1028 def get_variants(self
):
1029 """Get mozinfo for each test variants"""
1031 if len(self
.variants
) == 0:
1032 variants_file
= "taskcluster/kinds/test/variants.yml"
1033 variants_path
= self
.full_path(variants_file
)
1034 fp
= io
.open(variants_path
, "r", encoding
="utf-8")
1035 raw_variants
= load(fp
, Loader
=Loader
)
1037 for k
, v
in raw_variants
.items():
1040 mozinfo
= v
["mozinfo"]
1041 self
.variants
[k
] = mozinfo
1042 return self
.variants
1044 def get_task_details(self
, task_id
):
1045 """Download details for task task_id"""
1047 if task_id
in self
.tasks
: # if cached
1048 task
= self
.tasks
[task_id
]
1050 self
.vinfo(f
"get_task_details for task: {task_id}")
1052 task
= get_task(task_id
)
1053 except TaskclusterRestFailure
:
1054 self
.warning(f
"Task {task_id} no longer exists.")
1056 self
.tasks
[task_id
] = task
1059 def get_extra(self
, task_id
):
1060 """Calculate extra for task task_id"""
1062 if task_id
in self
.extras
: # if cached
1063 extra
= self
.extras
[task_id
]
1066 task
= self
.get_task_details(task_id
) or {}
1075 test_setting
= task
.get("extra", {}).get("test-setting", {})
1076 platform
= test_setting
.get("platform", {})
1077 platform_os
= platform
.get("os", {})
1080 if "name" in platform_os
:
1081 os
= platform_os
["name"]
1086 if "version" in platform_os
:
1087 os_version
= platform_os
["version"]
1088 if len(os_version
) == 4:
1089 os_version
= os_version
[0:2] + "." + os_version
[2:4]
1090 if "build" in platform_os
:
1091 build
= platform_os
["build"]
1092 if "arch" in platform
:
1093 arch
= platform
["arch"]
1094 if arch
== "x86" or arch
.find("32") >= 0:
1099 if arch
!= "aarch64" and arch
!= "ppc":
1101 if "display" in platform
:
1102 display
= platform
["display"]
1103 if "runtime" in test_setting
:
1104 for k
in test_setting
["runtime"]:
1105 if k
== "no-fission" and test_setting
["runtime"][k
]:
1106 runtimes
.append("no-fission")
1107 elif k
in self
.variants
: # draw-snapshot -> snapshot
1108 runtimes
.append(self
.variants
[k
]) # adds mozinfo
1109 if "build" in test_setting
:
1110 tbuild
= test_setting
["build"]
1113 if tbuild
[k
] == "opt":
1115 elif tbuild
[k
] == "debug":
1117 build_types
.append(tbuild
[k
])
1119 build_types
.append(k
)
1122 "arch": arch
or unknown
,
1123 "bits": bits
or unknown
,
1124 "build": build
or unknown
,
1125 "build_types": build_types
,
1127 "display": display
or unknown
,
1129 "os": os
or unknown
,
1130 "os_version": os_version
or unknown
,
1131 "runtimes": runtimes
,
1133 self
.extras
[task_id
] = extra
1136 def get_opt_for_task(self
, task_id
):
1137 extra
= self
.get_extra(task_id
)
1140 def task_to_skip_if(self
, task_id
, kind
):
1141 """Calculate the skip-if condition for failing task task_id"""
1143 if kind
== Kind
.WPT
:
1147 elif kind
== Kind
.LIST
:
1157 version
= "os_version"
1158 extra
= self
.get_extra(task_id
)
1160 os
= extra
.get("os")
1162 if kind
== Kind
.LIST
:
1164 skip_if
= "gtkWidget"
1166 skip_if
= "winWidget"
1168 skip_if
= "cocoaWidget"
1169 elif os
== "android":
1172 self
.error(f
"cannot calculate skip-if for unknown OS: '{os}'")
1174 elif extra
.get("os_version") is not None:
1176 extra
.get("build") is not None
1178 and extra
["os_version"] == "11"
1179 and extra
["build"] == "2009"
1181 skip_if
= "win11_2009" # mozinfo.py:137
1183 skip_if
= "os" + eq
+ qq
+ os
+ qq
1184 skip_if
+= aa
+ version
+ eq
+ qq
+ extra
["os_version"] + qq
1185 if kind
!= Kind
.LIST
and extra
.get("arch") is not None:
1186 skip_if
+= aa
+ arch
+ eq
+ qq
+ extra
["arch"] + qq
1187 # since we always give arch/processor, bits are not required
1188 # if extra["bits"] is not None:
1189 # skip_if += aa + "bits" + eq + extra["bits"]
1190 debug
= extra
.get("debug", False)
1191 runtimes
= extra
.get("runtimes", [])
1192 fission
= "no-fission" not in runtimes
1193 snapshot
= "snapshot" in runtimes
1194 swgl
= "swgl" in runtimes
1195 build_types
= extra
.get("build_types", [])
1196 asan
= "asan" in build_types
1197 ccov
= "ccov" in build_types
1198 tsan
= "tsan" in build_types
1199 optimized
= (not debug
) and (not ccov
) and (not asan
) and (not tsan
)
1201 if kind
== Kind
.LIST
:
1203 skip_if
+= "optimized"
1205 skip_if
+= "isDebugBuild"
1207 skip_if
+= "isCoverageBuild"
1209 skip_if
+= "AddressSanitizer"
1211 skip_if
+= "ThreadSanitizer"
1212 # See implicit VARIANT_DEFAULTS in
1213 # https://searchfox.org/mozilla-central/source/layout/tools/reftest/manifest.sys.mjs#30
1214 if not self
.implicit_vars
and fission
:
1215 skip_if
+= aa
+ "fission"
1216 elif not fission
: # implicit default: fission
1217 skip_if
+= aa
+ nn
+ "fission"
1218 if extra
.get("bits") is not None:
1219 if extra
["bits"] == "32":
1220 skip_if
+= aa
+ nn
+ "is64Bit" # override implicit is64Bit
1221 elif not self
.implicit_vars
and os
== "winWidget":
1222 skip_if
+= aa
+ "is64Bit"
1223 if not self
.implicit_vars
and not swgl
:
1224 skip_if
+= aa
+ nn
+ "swgl"
1225 elif swgl
: # implicit default: !swgl
1226 skip_if
+= aa
+ "swgl"
1227 if os
== "gtkWidget":
1228 if not self
.implicit_vars
and not snapshot
:
1229 skip_if
+= aa
+ nn
+ "useDrawSnapshot"
1230 elif snapshot
: # implicit default: !useDrawSnapshot
1231 skip_if
+= aa
+ "useDrawSnapshot"
1236 if extra
.get("display") is not None:
1237 skip_if
+= aa
+ "display" + eq
+ qq
+ extra
["display"] + qq
1238 for runtime
in extra
.get("runtimes", []):
1239 skip_if
+= aa
+ runtime
1240 for build_type
in extra
.get("build_types", []):
1241 # note: lite will not evaluate on non-android platforms
1242 if build_type
not in ["debug", "lite", "opt", "shippable"]:
1243 skip_if
+= aa
+ build_type
1246 def get_file_info(self
, path
, product
="Testing", component
="General"):
1248 Get bugzilla product and component for the path.
1249 Provide defaults (in case command_context is not defined
1250 or there isn't file info available).
1252 if path
!= DEF
and self
.command_context
is not None:
1253 reader
= self
.command_context
.mozbuild_reader(config_mode
="empty")
1254 info
= reader
.files_info([path
])
1255 cp
= info
[path
]["BUG_COMPONENT"]
1256 product
= cp
.product
1257 component
= cp
.component
1258 return product
, component
1260 def get_filename_in_manifest(self
, manifest
, path
):
1261 """return relative filename for path in manifest"""
1263 filename
= os
.path
.basename(path
)
1266 manifest_dir
= os
.path
.dirname(manifest
)
1268 j
= min(len(manifest_dir
), len(path
))
1269 while i
< j
and manifest_dir
[i
] == path
[i
]:
1271 if i
< len(manifest_dir
):
1272 for _
in range(manifest_dir
.count("/", i
) + 1):
1273 filename
= "../" + filename
1275 filename
= path
[i
+ 1 :]
1278 def get_push_id(self
, revision
, repo
):
1279 """Return the push_id for revision and repo (or None)"""
1281 self
.vinfo(f
"Retrieving push_id for {repo} revision: {revision} ...")
1282 if revision
in self
.push_ids
: # if cached
1283 push_id
= self
.push_ids
[revision
]
1286 push_url
= f
"https://treeherder.mozilla.org/api/project/{repo}/push/"
1288 params
["full"] = "true"
1289 params
["count"] = 10
1290 params
["revision"] = revision
1291 r
= requests
.get(push_url
, headers
=self
.headers
, params
=params
)
1292 if r
.status_code
!= 200:
1293 self
.warning(f
"FAILED to query Treeherder = {r} for {r.url}")
1296 if "results" in response
:
1297 results
= response
["results"]
1298 if len(results
) > 0:
1302 self
.push_ids
[revision
] = push_id
1305 def get_job_id(self
, push_id
, task_id
):
1306 """Return the job_id for push_id, task_id (or None)"""
1308 self
.vinfo(f
"Retrieving job_id for push_id: {push_id}, task_id: {task_id} ...")
1309 k
= f
"{push_id}:{task_id}"
1310 if k
in self
.job_ids
: # if cached
1311 job_id
= self
.job_ids
[k
]
1315 params
["push_id"] = push_id
1316 r
= requests
.get(self
.jobs_url
, headers
=self
.headers
, params
=params
)
1317 if r
.status_code
!= 200:
1318 self
.warning(f
"FAILED to query Treeherder = {r} for {r.url}")
1321 if "results" in response
:
1322 results
= response
["results"]
1323 if len(results
) > 0:
1324 for result
in results
:
1325 if len(result
) > 14:
1326 if result
[14] == task_id
:
1329 self
.job_ids
[k
] = job_id
1332 def get_bug_suggestions(self
, repo
, job_id
, path
, anyjs
=None):
1334 Return the (suggestions_url, line_number, line, log_url)
1335 for the given repo and job_id
1338 f
"Retrieving bug_suggestions for {repo} job_id: {job_id}, path: {path} ..."
1340 suggestions_url
= f
"https://treeherder.mozilla.org/api/project/{repo}/jobs/{job_id}/bug_suggestions/"
1344 r
= requests
.get(suggestions_url
, headers
=self
.headers
)
1345 if r
.status_code
!= 200:
1346 self
.warning(f
"FAILED to query Treeherder = {r} for {r.url}")
1348 if anyjs
is not None:
1349 pathdir
= os
.path
.dirname(path
) + "/"
1350 paths
= [pathdir
+ f
for f
in anyjs
.keys()]
1354 if len(response
) > 0:
1355 for sugg
in response
:
1357 path_end
= sugg
.get("path_end", None)
1358 # handles WPT short paths
1359 if path_end
is not None and p
.endswith(path_end
):
1360 line_number
= sugg
["line_number"] + 1
1361 line
= sugg
["search"]
1362 log_url
= f
"https://treeherder.mozilla.org/logviewer?repo={repo}&job_id={job_id}&lineNumber={line_number}"
1364 rv
= (suggestions_url
, line_number
, line
, log_url
)
1367 def read_json(self
, filename
):
1368 """read data as JSON from filename"""
1370 fp
= io
.open(filename
, "r", encoding
="utf-8")
1371 data
= json
.load(fp
)
1375 def read_tasks(self
, filename
):
1376 """read tasks as JSON from filename"""
1378 if not os
.path
.exists(filename
):
1379 msg
= f
"use-tasks JSON file does not exist: {filename}"
1380 raise OSError(2, msg
, filename
)
1381 tasks
= self
.read_json(filename
)
1382 tasks
= [Mock(task
, MOCK_TASK_DEFAULTS
, MOCK_TASK_INITS
) for task
in tasks
]
1384 if len(task
.extra
) > 0: # pre-warm cache for extra information
1385 self
.extras
[task
.id] = task
.extra
1388 def read_failures(self
, filename
):
1389 """read failures as JSON from filename"""
1391 if not os
.path
.exists(filename
):
1392 msg
= f
"use-failures JSON file does not exist: {filename}"
1393 raise OSError(2, msg
, filename
)
1394 failures
= self
.read_json(filename
)
1397 def read_bugs(self
, filename
):
1398 """read bugs as JSON from filename"""
1400 if not os
.path
.exists(filename
):
1401 msg
= f
"bugs JSON file does not exist: {filename}"
1402 raise OSError(2, msg
, filename
)
1403 bugs
= self
.read_json(filename
)
1404 bugs
= [Mock(bug
, MOCK_BUG_DEFAULTS
) for bug
in bugs
]
1407 def write_json(self
, filename
, data
):
1408 """saves data as JSON to filename"""
1409 fp
= io
.open(filename
, "w", encoding
="utf-8")
1410 json
.dump(data
, fp
, indent
=2, sort_keys
=True)
1413 def write_tasks(self
, save_tasks
, tasks
):
1414 """saves tasks as JSON to save_tasks"""
1417 if not isinstance(task
, TestTask
):
1420 jtask
["id"] = task
.id
1421 jtask
["label"] = task
.label
1422 jtask
["duration"] = task
.duration
1423 jtask
["result"] = task
.result
1424 jtask
["state"] = task
.state
1425 jtask
["extra"] = self
.get_extra(task
.id)
1427 for k
, v
in task
.tags
.items():
1428 if k
== "createdForUser":
1429 jtags
[k
] = "ci@mozilla.com"
1432 jtask
["tags"] = jtags
1433 jtask
["tier"] = task
.tier
1434 jtask
["results"] = [
1435 {"group": r
.group
, "ok": r
.ok
, "duration": r
.duration
}
1436 for r
in task
.results
1438 jtask
["errors"] = None # Bug with task.errors property??
1440 if self
.failure_types
is not None and task
.id in self
.failure_types
:
1441 failure_types
= self
.failure_types
[task
.id] # use cache
1443 failure_types
= task
.failure_types
1444 for k
in failure_types
:
1445 jft
[k
] = [[f
[0], f
[1].value
] for f
in task
.failure_types
[k
]]
1446 jtask
["failure_types"] = jft
1447 jtasks
.append(jtask
)
1448 self
.write_json(save_tasks
, jtasks
)
1450 def label_to_platform_testname(self
, label
):
1451 """convert from label to platform, testname for mach command line"""
1454 platform_details
= label
.split("/")
1455 if len(platform_details
) == 2:
1456 platform
, details
= platform_details
1457 words
= details
.split("-")
1459 platform
+= "/" + words
.pop(0) # opt or debug
1461 _chunk
= int(words
[-1])
1465 words
.pop() # remove test suffix
1466 testname
= "-".join(words
)
1469 return platform
, testname
1471 def add_attachment_log_for_task(self
, bugid
, task_id
):
1472 """Adds compressed log for this task to bugid"""
1474 log_url
= f
"https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/{task_id}/artifacts/public/logs/live_backing.log"
1475 r
= requests
.get(log_url
, headers
=self
.headers
)
1476 if r
.status_code
!= 200:
1477 self
.error(f
"Unable to get log for task: {task_id}")
1479 attach_fp
= tempfile
.NamedTemporaryFile()
1480 fp
= gzip
.open(attach_fp
, "wb")
1481 fp
.write(r
.text
.encode("utf-8"))
1483 if self
._initialize
_bzapi
():
1484 description
= ATTACHMENT_DESCRIPTION
+ task_id
1485 file_name
= TASK_LOG
+ ".gz"
1486 comment
= "Added compressed log"
1487 content_type
= "application/gzip"
1489 self
._bzapi
.attachfile(
1493 file_name
=file_name
,
1495 content_type
=content_type
,
1499 pass # Fault expected: Failed to fetch key 9372091 from network storage: The specified key does not exist.
1501 def get_wpt_path_meta(self
, shortpath
):
1502 if shortpath
.startswith(WPT0
):
1504 meta
= shortpath
.replace(WPT0
, WPT_META0
, 1)
1505 elif shortpath
.startswith(WPT1
):
1507 meta
= shortpath
.replace(WPT1
, WPT_META1
, 1)
1508 elif shortpath
.startswith(WPT2
):
1510 meta
= shortpath
.replace(WPT2
, WPT_META2
, 1)
1511 elif shortpath
.startswith(WPT_MOZILLA
):
1512 shortpath
= shortpath
[len(WPT_MOZILLA
) :]
1513 path
= WPT2
+ shortpath
1514 meta
= WPT_META2
+ shortpath
1516 path
= WPT1
+ shortpath
1517 meta
= WPT_META1
+ shortpath
1520 def wpt_paths(self
, shortpath
):
1522 Analyzes the WPT short path for a test and returns
1523 (path, manifest, query, anyjs) where
1524 path is the relative path to the test file
1525 manifest is the relative path to the file metadata
1526 query is the test file query paramters (or None)
1527 anyjs is the html test file as reported by mozci (or None)
1531 i
= shortpath
.find("?")
1533 query
= shortpath
[i
:]
1534 shortpath
= shortpath
[0:i
]
1535 path
, manifest
= self
.get_wpt_path_meta(shortpath
)
1536 failure_type
= not self
.isdir(path
)
1538 i
= path
.find(".any.")
1540 anyjs
= path
# orig path
1541 manifest
= manifest
.replace(path
[i
:], ".any.js")
1542 path
= path
[0:i
] + ".any.js"
1544 i
= path
.find(".window.")
1546 anyjs
= path
# orig path
1547 manifest
= manifest
.replace(path
[i
:], ".window.js")
1548 path
= path
[0:i
] + ".window.js"
1550 i
= path
.find(".worker.")
1552 anyjs
= path
# orig path
1553 manifest
= manifest
.replace(path
[i
:], ".worker.js")
1554 path
= path
[0:i
] + ".worker.js"
1556 manifest_classic
= ""
1557 if manifest
.startswith(WPT_META0
):
1558 manifest_classic
= manifest
.replace(WPT_META0
, WPT_META0_CLASSIC
, 1)
1559 if self
.exists(manifest_classic
):
1560 if self
.exists(manifest
):
1562 f
"Both classic {manifest_classic} and metadata {manifest} manifests exist"
1566 f
"Using the classic {manifest_classic} manifest as the metadata manifest {manifest} does not exist"
1568 manifest
= manifest_classic
1569 if not self
.exists(path
):
1570 return (None, None, None, None)
1571 return (path
, manifest
, query
, anyjs
)
1573 def wpt_add_skip_if(self
, manifest_str
, anyjs
, skip_if
, bug_reference
):
1575 Edits a WPT manifest string to add disabled condition
1576 anyjs is a dictionary mapping from filename and any alternate basenames to
1577 a boolean (indicating if the file has been handled in the manifest).
1578 Returns additional_comment (if any)
1581 additional_comment
= ""
1582 disabled_key
= False
1583 disabled
= " disabled:"
1584 condition_start
= " if "
1585 condition
= condition_start
+ skip_if
+ ": " + bug_reference
1586 lines
= manifest_str
.splitlines()
1587 section
= None # name of the section
1592 if line
.startswith("["):
1593 if section
is not None and not anyjs
[section
]: # not yet handled
1594 if not disabled_key
:
1595 lines
.insert(i
, disabled
)
1597 lines
.insert(i
, condition
)
1598 lines
.insert(i
+ 1, "") # blank line after condition
1601 anyjs
[section
] = True
1602 section
= line
[1:-1]
1603 if section
in anyjs
and not anyjs
[section
]:
1604 disabled_key
= False
1606 section
= None # ignore section we are not interested in
1607 elif section
is not None:
1608 if line
== disabled
:
1610 elif line
.startswith(" ["):
1611 if i
> 0 and i
- 1 < n
and lines
[i
- 1] == "":
1615 if not disabled_key
:
1616 lines
.insert(i
, disabled
)
1619 lines
.insert(i
, condition
)
1620 lines
.insert(i
+ 1, "") # blank line after condition
1623 anyjs
[section
] = True
1625 elif line
.startswith(" ") and not line
.startswith(" "):
1626 if disabled_key
: # insert condition above new key
1627 lines
.insert(i
, condition
)
1630 anyjs
[section
] = True
1632 disabled_key
= False
1633 elif line
.startswith(" "):
1634 if disabled_key
and line
== condition
:
1635 anyjs
[section
] = True # condition already present
1638 if section
is not None and not anyjs
[section
]: # not yet handled
1639 if i
> 0 and i
- 1 < n
and lines
[i
- 1] == "":
1641 if not disabled_key
:
1642 lines
.append(disabled
)
1645 lines
.append(condition
)
1646 lines
.append("") # blank line after condition
1649 anyjs
[section
] = True
1650 for section
in anyjs
:
1651 if not anyjs
[section
]:
1652 if i
> 0 and i
- 1 < n
and lines
[i
- 1] != "":
1653 lines
.append("") # blank line before condition
1656 lines
.append("[" + section
+ "]")
1657 lines
.append(disabled
)
1658 lines
.append(condition
)
1659 lines
.append("") # blank line after condition
1662 manifest_str
= "\n".join(lines
) + "\n"
1663 return manifest_str
, additional_comment
1665 def reftest_add_fuzzy_if(
1677 Edits a reftest manifest string to add disabled condition
1680 if self
.lmp
is None:
1681 from parse_reftest
import ListManifestParser
1683 self
.lmp
= ListManifestParser(
1684 self
.implicit_vars
, self
.verbose
, self
.error
, self
.warning
, self
.info
1686 manifest_str
, additional_comment
= self
.lmp
.reftest_add_fuzzy_if(
1696 return manifest_str
, additional_comment
1698 def get_lineno_difference_pixels_status(self
, task_id
, manifest
, allmods
):
1701 - lineno in manifest
1702 - image comparison, max *difference*
1703 - number of differing *pixels*
1704 - status (PASS or FAIL)
1705 as cached from reftest_errorsummary.log for a task
1708 manifest_obj
= self
.error_summary
.get(manifest
, {})
1709 allmods_obj
= manifest_obj
.get(allmods
, {})
1710 lineno
= allmods_obj
.get(LINENO
, 0)
1711 runs_obj
= allmods_obj
.get(RUNS
, {})
1712 task_obj
= runs_obj
.get(task_id
, {})
1713 difference
= task_obj
.get(DIFFERENCE
, 0)
1714 pixels
= task_obj
.get(PIXELS
, 0)
1715 status
= task_obj
.get(STATUS
, FAIL
)
1716 return lineno
, difference
, pixels
, status
1718 def reftest_find_lineno(self
, manifest
, modifiers
, allmods
):
1720 Return the line number with modifiers in manifest (else 0)
1726 for i
in range(len(modifiers
)):
1727 if modifiers
[i
].find("pref(") >= 0 or modifiers
[i
].find("skip-if(") >= 0:
1728 prefs
.append(modifiers
[i
])
1730 mods
.append(modifiers
[i
])
1732 manifest_str
= io
.open(manifest
, "r", encoding
="utf-8").read()
1733 lines
= manifest_str
.splitlines()
1737 for linenum
in range(len(lines
)):
1738 line
= lines
[linenum
]
1739 if len(line
) > 0 and line
[0] == "#":
1741 comment_start
= line
.find(" #") # MUST NOT match anchors!
1742 if comment_start
> 0:
1743 line
= line
[0:comment_start
].strip()
1744 words
= line
.split()
1746 if n
> 1 and words
[0] == "defaults":
1747 defaults
= words
[1:].copy()
1749 line_defaults
= defaults
.copy()
1752 if words
[i
].find("pref(") >= 0 or words
[i
].find("skip-if(") >= 0:
1753 line_defaults
.append(words
[i
])
1758 if (len(prefs
) == 0 or prefs
== line_defaults
) and words
== mods
:
1760 lineno
= linenum
+ 1
1762 elif m
> 2 and n
> 2:
1763 if words
[-3:] == mods
[-3:]:
1764 alt_lineno
= linenum
+ 1
1766 bwords
= [os
.path
.basename(f
) for f
in words
[-2:]]
1767 bmods
= [os
.path
.basename(f
) for f
in mods
[-2:]]
1769 alt_lineno
= linenum
+ 1
1774 f
"manifest '{manifest}' found lineno: {lineno}, but it does not contain all the prefs from modifiers,\nSEARCH: {allmods}\nFOUND : {lines[alt_lineno - 1]}"
1779 f
"manifest '{manifest}' does not contain line with modifiers: {allmods}"
1783 def get_allpaths(self
, task_id
, manifest
, path
):
1785 Looks up the reftest_errorsummary.log for a task
1786 and caches the details in self.error_summary by
1787 task_id, manifest, allmods
1788 where allmods is the concatenation of all modifiers
1789 and the details include
1790 - image comparison, max *difference*
1791 - number of differing *pixels*
1792 - status: unexpected PASS or FAIL
1794 The list iof unique modifiers (allmods) for the given path are returned
1798 words
= path
.split()
1799 if len(words
) != 3 or words
[1] not in TEST_TYPES
:
1801 f
"reftest_errorsummary.log for task: {task_id} has unsupported test type '{path}'"
1804 if manifest
in self
.error_summary
:
1805 for allmods
in self
.error_summary
[manifest
]:
1806 if self
.error_summary
[manifest
][allmods
][
1808 ] == path
and task_id
in self
.error_summary
[manifest
][allmods
].get(
1811 allpaths
.append(allmods
)
1812 if len(allpaths
) > 0:
1813 return allpaths
# cached (including self tests)
1814 error_url
= f
"https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/{task_id}/artifacts/public/test_info/reftest_errorsummary.log"
1815 self
.vinfo(f
"Requesting reftest_errorsummary.log for task: {task_id}")
1816 r
= requests
.get(error_url
, headers
=self
.headers
)
1817 if r
.status_code
!= 200:
1818 self
.error(f
"Unable to get reftest_errorsummary.log for task: {task_id}")
1820 for line
in r
.text
.encode("utf-8").splitlines():
1821 summary
= json
.loads(line
)
1822 group
= summary
.get(GROUP
, "")
1823 if not group
or not os
.path
.exists(group
): # not error line
1825 test
= summary
.get(TEST
, None)
1828 if not MODIFIERS
in summary
:
1830 f
"reftest_errorsummary.log for task: {task_id} does not have modifiers for '{test}'"
1833 words
= test
.split()
1834 if len(words
) != 3 or words
[1] not in TEST_TYPES
:
1836 f
"reftest_errorsummary.log for task: {task_id} has unsupported test '{test}'"
1839 status
= summary
.get(STATUS
, "")
1840 if status
not in [FAIL
, PASS
]:
1842 f
"reftest_errorsummary.log for task: {task_id} has unknown status: {status} for '{test}'"
1845 error
= summary
.get(SUBTEST
, "")
1846 mods
= summary
[MODIFIERS
]
1847 allmods
= " ".join(mods
)
1848 if group
not in self
.error_summary
:
1849 self
.error_summary
[group
] = {}
1850 if allmods
not in self
.error_summary
[group
]:
1851 self
.error_summary
[group
][allmods
] = {}
1852 self
.error_summary
[group
][allmods
][TEST
] = test
1853 lineno
= self
.error_summary
[group
][allmods
].get(LINENO
, 0)
1855 lineno
= self
.reftest_find_lineno(group
, mods
, allmods
)
1857 self
.error_summary
[group
][allmods
][LINENO
] = lineno
1858 if RUNS
not in self
.error_summary
[group
][allmods
]:
1859 self
.error_summary
[group
][allmods
][RUNS
] = {}
1860 if task_id
not in self
.error_summary
[group
][allmods
][RUNS
]:
1861 self
.error_summary
[group
][allmods
][RUNS
][task_id
] = {}
1862 self
.error_summary
[group
][allmods
][RUNS
][task_id
][ERROR
] = error
1863 if self
._subtest
_rx
is None:
1864 self
._subtest
_rx
= re
.compile(SUBTEST_REGEX
)
1865 m
= self
._subtest
_rx
.findall(error
)
1867 difference
= int(m
[0][0])
1868 pixels
= int(m
[0][1])
1873 self
.error_summary
[group
][allmods
][RUNS
][task_id
][
1877 self
.error_summary
[group
][allmods
][RUNS
][task_id
][PIXELS
] = pixels
1879 self
.error_summary
[group
][allmods
][RUNS
][task_id
][STATUS
] = status
1881 allpaths
.append(allmods
)