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/.
17 from pathlib
import Path
18 from xmlrpc
.client
import Fault
23 from yaml
import CLoader
as Loader
25 from yaml
import Loader
30 from manifestparser
import ManifestParser
31 from manifestparser
.toml
import add_skip_if
, alphabetize_toml_str
, sort_paths
32 from mozci
.task
import TestTask
33 from mozci
.util
.taskcluster
import get_task
35 BUGZILLA_AUTHENTICATION_HELP
= "Must create a Bugzilla API key per https://github.com/mozilla/mozci-tools/blob/main/citools/test_triage_bug_filer.py"
36 TASK_LOG
= "live_backing.log"
37 TASK_ARTIFACT
= "public/logs/" + TASK_LOG
38 ATTACHMENT_DESCRIPTION
= "Compressed " + TASK_ARTIFACT
+ " for task "
40 r
".*Created attachment ([0-9]+)\n.*"
41 + ATTACHMENT_DESCRIPTION
42 + "([A-Za-z0-9_-]+)\n.*"
46 class MockResult(object):
47 def __init__(self
, result
):
52 return self
.result
["group"]
56 _ok
= self
.result
["ok"]
60 class MockTask(object):
61 def __init__(self
, task
):
63 if "results" in self
.task
:
64 self
.task
["results"] = [
65 MockResult(result
) for result
in self
.task
["results"]
68 self
.task
["results"] = []
71 def failure_types(self
):
72 if "failure_types" in self
.task
:
73 return self
.task
["failure_types"]
74 else: # note no failure_types in Task object
79 return self
.task
["id"]
83 return self
.task
["label"]
87 return self
.task
["results"]
90 class Classification(object):
91 "Classification of the failure (not the task result)"
93 DISABLE_MANIFEST
= "disable_manifest" # crash found
94 DISABLE_RECOMMENDED
= "disable_recommended" # disable first failing path
95 INTERMITTENT
= "intermittent"
96 SECONDARY
= "secondary" # secondary failing path
97 SUCCESS
= "success" # path always succeeds
103 constant indexes for attributes of a run
113 class Skipfails(object):
114 "mach manifest skip-fails implementation: Update manifests to skip failing tests"
117 REVISION
= "revision"
118 TREEHERDER
= "treeherder.mozilla.org"
119 BUGZILLA_SERVER_DEFAULT
= "bugzilla.allizom.org"
123 command_context
=None,
130 self
.command_context
= command_context
131 if self
.command_context
is not None:
132 self
.topsrcdir
= self
.command_context
.topsrcdir
134 self
.topsrcdir
= Path(__file__
).parent
.parent
135 self
.topsrcdir
= os
.path
.normpath(self
.topsrcdir
)
136 if isinstance(try_url
, list) and len(try_url
) == 1:
137 self
.try_url
= try_url
[0]
139 self
.try_url
= try_url
140 self
.dry_run
= dry_run
141 self
.verbose
= verbose
143 if bugzilla
is not None:
144 self
.bugzilla
= bugzilla
146 if "BUGZILLA" in os
.environ
:
147 self
.bugzilla
= os
.environ
["BUGZILLA"]
149 self
.bugzilla
= Skipfails
.BUGZILLA_SERVER_DEFAULT
150 self
.component
= "skip-fails"
152 self
._attach
_rx
= None
156 self
.headers
= {} # for Treeherder requests
157 self
.headers
["Accept"] = "application/json"
158 self
.headers
["User-Agent"] = "treeherder-pyclient"
159 self
.jobs_url
= "https://treeherder.mozilla.org/api/jobs/"
163 def _initialize_bzapi(self
):
164 """Lazily initializes the Bugzilla API"""
165 if self
._bzapi
is None:
166 self
._bzapi
= bugzilla
.Bugzilla(self
.bugzilla
)
167 self
._attach
_rx
= re
.compile(ATTACHMENT_REGEX
, flags
=re
.M
)
169 def pprint(self
, obj
):
171 self
.pp
= pprint
.PrettyPrinter(indent
=4, stream
=sys
.stderr
)
176 if self
.command_context
is not None:
177 self
.command_context
.log(
178 logging
.ERROR
, self
.component
, {"error": str(e
)}, "ERROR: {error}"
181 print(f
"ERROR: {e}", file=sys
.stderr
, flush
=True)
183 def warning(self
, e
):
184 if self
.command_context
is not None:
185 self
.command_context
.log(
186 logging
.WARNING
, self
.component
, {"error": str(e
)}, "WARNING: {error}"
189 print(f
"WARNING: {e}", file=sys
.stderr
, flush
=True)
192 if self
.command_context
is not None:
193 self
.command_context
.log(
194 logging
.INFO
, self
.component
, {"error": str(e
)}, "INFO: {error}"
197 print(f
"INFO: {e}", file=sys
.stderr
, flush
=True)
212 "Run skip-fails on try_url, return True on success"
214 try_url
= self
.try_url
215 revision
, repo
= self
.get_revision(try_url
)
217 if use_tasks
is not None:
218 if os
.path
.exists(use_tasks
):
219 self
.vinfo(f
"use tasks: {use_tasks}")
220 tasks
= self
.read_json(use_tasks
)
221 tasks
= [MockTask(task
) for task
in tasks
]
223 self
.error(f
"uses tasks JSON file does not exist: {use_tasks}")
226 tasks
= self
.get_tasks(revision
, repo
)
228 if use_failures
is not None:
229 if os
.path
.exists(use_failures
):
230 self
.vinfo(f
"use failures: {use_failures}")
231 failures
= self
.read_json(use_failures
)
233 self
.error(f
"use failures JSON file does not exist: {use_failures}")
236 failures
= self
.get_failures(tasks
)
237 if save_failures
is not None:
238 self
.vinfo(f
"save failures: {save_failures}")
239 self
.write_json(save_failures
, failures
)
241 if save_tasks
is not None:
242 self
.vinfo(f
"save tasks: {save_tasks}")
243 self
.write_tasks(save_tasks
, tasks
)
246 for manifest
in failures
:
247 if not manifest
.endswith(".toml"):
248 self
.warning(f
"cannot process skip-fails on INI manifests: {manifest}")
250 for path
in failures
[manifest
]["path"]:
251 for label
in failures
[manifest
]["path"][path
]:
252 classification
= failures
[manifest
]["path"][path
][label
][
255 if classification
.startswith("disable_") or (
256 self
.turbo
and classification
== Classification
.SECONDARY
258 for task_id
in failures
[manifest
]["path"][path
][label
][
273 if max_failures
>= 0 and num_failures
>= max_failures
:
275 f
"max_failures={max_failures} threshold reached. stopping."
278 break # just use the first task_id
281 def get_revision(self
, url
):
282 parsed
= urllib
.parse
.urlparse(url
)
283 if parsed
.scheme
!= "https":
284 raise ValueError("try_url scheme not https")
285 if parsed
.netloc
!= Skipfails
.TREEHERDER
:
286 raise ValueError(f
"try_url server not {Skipfails.TREEHERDER}")
287 if len(parsed
.query
) == 0:
288 raise ValueError("try_url query missing")
289 query
= urllib
.parse
.parse_qs(parsed
.query
)
290 if Skipfails
.REVISION
not in query
:
291 raise ValueError("try_url query missing revision")
292 revision
= query
[Skipfails
.REVISION
][0]
293 if Skipfails
.REPO
in query
:
294 repo
= query
[Skipfails
.REPO
][0]
297 self
.vinfo(f
"considering {repo} revision={revision}")
298 return revision
, repo
300 def get_tasks(self
, revision
, repo
):
301 push
= mozci
.push
.Push(revision
, repo
)
304 def get_failures(self
, tasks
):
306 find failures and create structure comprised of runs by path:
310 classification: Classification
311 * unknown (default) < 3 runs
312 * intermittent (not enough failures) >3 runs < 0.4 failure rate
313 * disable_recommended (enough repeated failures) >3 runs >= 0.4
314 * disable_manifest (disable DEFAULT if no other failures)
315 * secondary (not first failure in group)
323 if len(task
.results
) == 0:
324 continue # ignore aborted tasks
325 for manifest
in task
.failure_types
:
326 if manifest
not in failures
:
327 failures
[manifest
] = {"sum_by_label": {}, "path": {}}
328 if manifest
not in manifest_paths
:
329 manifest_paths
[manifest
] = []
330 for path_type
in task
.failure_types
[manifest
]:
331 path
, _type
= path_type
334 if path
not in failures
[manifest
]["path"]:
335 failures
[manifest
]["path"][path
] = {}
336 if path
not in manifest_paths
[manifest
]:
337 manifest_paths
[manifest
].append(path
)
338 if task
.label
not in failures
[manifest
]["sum_by_label"]:
339 failures
[manifest
]["sum_by_label"][task
.label
] = {
340 Classification
.UNKNOWN
: 0,
341 Classification
.SECONDARY
: 0,
342 Classification
.INTERMITTENT
: 0,
343 Classification
.DISABLE_RECOMMENDED
: 0,
344 Classification
.DISABLE_MANIFEST
: 0,
345 Classification
.SUCCESS
: 0,
347 if task
.label
not in failures
[manifest
]["path"][path
]:
348 failures
[manifest
]["path"][path
][task
.label
] = {
351 "classification": Classification
.UNKNOWN
,
352 "runs": {task
.id: False},
355 failures
[manifest
]["path"][path
][task
.label
]["runs"][
358 except AttributeError as ae
:
359 self
.warning(f
"unknown attribute in task: {ae}")
361 # calculate success/failure for each known path
362 for manifest
in manifest_paths
:
363 manifest_paths
[manifest
] = sort_paths(manifest_paths
[manifest
])
366 if len(task
.results
) == 0:
367 continue # ignore aborted tasks
368 for result
in task
.results
:
369 manifest
= result
.group
370 if manifest
not in failures
:
372 f
"result for {manifest} not in any failures, ignored"
375 for path
in manifest_paths
[manifest
]:
376 if task
.label
not in failures
[manifest
]["sum_by_label"]:
377 failures
[manifest
]["sum_by_label"][task
.label
] = {
378 Classification
.UNKNOWN
: 0,
379 Classification
.SECONDARY
: 0,
380 Classification
.INTERMITTENT
: 0,
381 Classification
.DISABLE_RECOMMENDED
: 0,
382 Classification
.DISABLE_MANIFEST
: 0,
383 Classification
.SUCCESS
: 0,
385 if task
.label
not in failures
[manifest
]["path"][path
]:
386 failures
[manifest
]["path"][path
][task
.label
] = {
389 "classification": Classification
.UNKNOWN
,
394 not in failures
[manifest
]["path"][path
][task
.label
]["runs"]
397 failures
[manifest
]["path"][path
][task
.label
]["runs"][
403 or failures
[manifest
]["path"][path
][task
.label
]["runs"][
407 failures
[manifest
]["path"][path
][task
.label
]["total_runs"] += 1
409 failures
[manifest
]["path"][path
][task
.label
][
412 except AttributeError as ae
:
413 self
.warning(f
"unknown attribute in task: {ae}")
415 # classify failures and roll up summary statistics
416 for manifest
in failures
:
417 for path
in failures
[manifest
]["path"]:
418 for label
in failures
[manifest
]["path"][path
]:
419 failed_runs
= failures
[manifest
]["path"][path
][label
]["failed_runs"]
420 total_runs
= failures
[manifest
]["path"][path
][label
]["total_runs"]
421 classification
= failures
[manifest
]["path"][path
][label
][
425 if failed_runs
/ total_runs
< 0.4:
427 classification
= Classification
.SUCCESS
429 classification
= Classification
.INTERMITTENT
431 classification
= Classification
.SECONDARY
432 failures
[manifest
]["path"][path
][label
][
435 failures
[manifest
]["sum_by_label"][label
][classification
] += 1
437 # Identify the first failure (for each test, in a manifest, by label)
438 for manifest
in failures
:
439 alpha_paths
= sort_paths(failures
[manifest
]["path"].keys())
440 for path
in alpha_paths
:
441 for label
in failures
[manifest
]["path"][path
]:
443 failures
[manifest
]["sum_by_label"][label
][
444 Classification
.DISABLE_RECOMMENDED
448 if path
== "DEFAULT":
449 classification
= failures
[manifest
]["path"][path
][label
][
453 classification
== Classification
.SECONDARY
454 and failures
[manifest
]["sum_by_label"][label
][
459 # ONLY failure in the manifest for this label => DISABLE
460 failures
[manifest
]["path"][path
][label
][
462 ] = Classification
.DISABLE_MANIFEST
463 failures
[manifest
]["sum_by_label"][label
][
466 failures
[manifest
]["sum_by_label"][label
][
467 Classification
.DISABLE_MANIFEST
473 and failures
[manifest
]["path"][path
][label
][
476 == Classification
.SECONDARY
478 # FIRST failure in the manifest for this label => DISABLE
479 failures
[manifest
]["path"][path
][label
][
481 ] = Classification
.DISABLE_RECOMMENDED
482 failures
[manifest
]["sum_by_label"][label
][
483 Classification
.SECONDARY
485 failures
[manifest
]["sum_by_label"][label
][
486 Classification
.DISABLE_RECOMMENDED
491 def _get_os_version(self
, os
, platform
):
492 """Return the os_version given the label platform string"""
493 i
= platform
.find(os
)
495 yy
= platform
[j
: j
+ 2]
496 mm
= platform
[j
+ 2 : j
+ 4]
499 def get_bug_by_id(self
, id):
500 """Get bug by bug id"""
502 self
._initialize
_bzapi
()
503 bug
= self
._bzapi
.getbug(id)
506 def get_bugs_by_summary(self
, summary
):
507 """Get bug by bug summary"""
509 self
._initialize
_bzapi
()
510 query
= self
._bzapi
.build_query(short_desc
=summary
)
511 query
["include_fields"] = [
520 bugs
= self
._bzapi
.query(query
)
525 summary
="Bug short description",
526 description
="Bug description",
529 version
="unspecified",
534 self
._initialize
_bzapi
()
535 if not self
._bzapi
.logged_in
:
537 "Must create a Bugzilla API key per https://github.com/mozilla/mozci-tools/blob/main/citools/test_triage_bug_filer.py"
539 raise PermissionError(f
"Not authenticated for Bugzilla {self.bugzilla}")
540 createinfo
= self
._bzapi
.build_createbug(
545 description
=description
,
547 createinfo
["type"] = bugtype
548 bug
= self
._bzapi
.createbug(createinfo
)
551 def add_bug_comment(self
, id, comment
, meta_bug_id
=None):
552 """Add a comment to an existing bug"""
554 self
._initialize
_bzapi
()
555 if not self
._bzapi
.logged_in
:
556 self
.error(BUGZILLA_AUTHENTICATION_HELP
)
557 raise PermissionError("Not authenticated for Bugzilla")
558 if meta_bug_id
is not None:
559 blocks_add
= [meta_bug_id
]
562 updateinfo
= self
._bzapi
.build_update(comment
=comment
, blocks_add
=blocks_add
)
563 self
._bzapi
.update_bugs([id], updateinfo
)
579 self
.vinfo(f
"===== Skip failure in manifest: {manifest} =====")
580 skip_if
= self
.task_to_skip_if(task_id
)
583 f
"Unable to calculate skip-if condition from manifest={manifest} from failure label={label}"
587 if classification
== Classification
.DISABLE_MANIFEST
:
589 comment
= "Disabled entire manifest due to crash result"
591 filename
= self
.get_filename_in_manifest(manifest
, path
)
592 comment
= f
'Disabled test due to failures: "{filename}"'
593 if classification
== Classification
.SECONDARY
:
594 comment
+= " (secondary)"
595 bug_reference
= " (secondary)"
596 comment
+= f
"\nTry URL = {try_url}"
597 comment
+= f
"\nrevision = {revision}"
598 comment
+= f
"\nrepo = {repo}"
599 comment
+= f
"\nlabel = {label}"
600 comment
+= f
"\ntask_id = {task_id}"
601 push_id
= self
.get_push_id(revision
, repo
)
602 if push_id
is not None:
603 comment
+= f
"\npush_id = {push_id}"
604 job_id
= self
.get_job_id(push_id
, task_id
)
605 if job_id
is not None:
606 comment
+= f
"\njob_id = {job_id}"
607 suggestions_url
, line_number
, line
, log_url
= self
.get_bug_suggestions(
610 if log_url
is not None:
611 comment
+= f
"\n\nBug suggestions: {suggestions_url}"
612 comment
+= f
"\nSpecifically see at line {line_number} in the attached log: {log_url}"
613 comment
+= f
'\n\n "{line}"\n'
614 platform
, testname
= self
.label_to_platform_testname(label
)
615 if platform
is not None:
616 comment
+= "\n\nCommand line to reproduce:\n\n"
617 comment
+= f
" \"mach try fuzzy -q '{platform}' {testname}\""
618 bug_summary
= f
"MANIFEST {manifest}"
620 bugs
= self
.get_bugs_by_summary(bug_summary
)
623 f
"This bug covers excluded failing tests in the MANIFEST {manifest}"
625 description
+= "\n(generated by mach manifest skip-fails)"
626 product
, component
= self
.get_file_info(path
)
629 f
'Dry-run NOT creating bug: {product}::{component} "{bug_summary}"'
633 bug
= self
.create_bug(bug_summary
, description
, product
, component
)
636 f
'Created Bug {bugid} {product}::{component} : "{bug_summary}"'
638 bug_reference
= f
"Bug {bugid}" + bug_reference
641 bug_reference
= f
"Bug {bugid}" + bug_reference
642 product
= bugs
[0].product
643 component
= bugs
[0].component
644 self
.vinfo(f
'Found Bug {bugid} {product}::{component} "{bug_summary}"')
645 if meta_bug_id
is not None:
646 if meta_bug_id
in bugs
[0].blocks
:
647 self
.vinfo(f
" Bug {bugid} already blocks meta bug {meta_bug_id}")
648 meta_bug_id
= None # no need to add again
649 comments
= bugs
[0].getcomments()
650 for i
in range(len(comments
)):
651 text
= comments
[i
]["text"]
652 m
= self
._attach
_rx
.findall(text
)
655 attachments
[a_task_id
] = m
[0][0]
656 if a_task_id
== task_id
:
658 f
" Bug {bugid} already has the compressed log attached for this task"
661 self
.error(f
'More than one bug found for summary: "{bug_summary}"')
664 self
.warning(f
"Dry-run NOT adding comment to Bug {bugid}: {comment}")
665 self
.info(f
'Dry-run NOT editing ["{filename}"] manifest: "{manifest}"')
666 self
.info(f
'would add skip-if condition: "{skip_if}" # {bug_reference}')
667 if task_id
not in attachments
:
668 self
.info("would add compressed log for this task")
670 self
.add_bug_comment(bugid
, comment
, meta_bug_id
)
671 self
.info(f
"Added comment to Bug {bugid}: {comment}")
672 if meta_bug_id
is not None:
673 self
.info(f
" Bug {bugid} blocks meta Bug: {meta_bug_id}")
674 if task_id
not in attachments
:
675 self
.add_attachment_log_for_task(bugid
, task_id
)
676 self
.info("Added compressed log for this task")
677 mp
= ManifestParser(use_toml
=True, document
=True)
678 manifest_path
= os
.path
.join(self
.topsrcdir
, os
.path
.normpath(manifest
))
679 mp
.read(manifest_path
)
680 document
= mp
.source_documents
[manifest_path
]
681 add_skip_if(document
, filename
, skip_if
, bug_reference
)
682 manifest_str
= alphabetize_toml_str(document
)
683 fp
= io
.open(manifest_path
, "w", encoding
="utf-8", newline
="\n")
684 fp
.write(manifest_str
)
686 self
.info(f
'Edited ["{filename}"] in manifest: "{manifest}"')
687 self
.info(f
'added skip-if condition: "{skip_if}" # {bug_reference}')
689 def get_variants(self
):
690 """Get mozinfo for each test variants"""
692 if len(self
.variants
) == 0:
693 variants_file
= "taskcluster/ci/test/variants.yml"
694 variants_path
= os
.path
.join(
695 self
.topsrcdir
, os
.path
.normpath(variants_file
)
697 fp
= io
.open(variants_path
, "r", encoding
="utf-8")
698 raw_variants
= load(fp
, Loader
=Loader
)
700 for k
, v
in raw_variants
.items():
703 mozinfo
= v
["mozinfo"]
704 self
.variants
[k
] = mozinfo
707 def get_task(self
, task_id
):
708 """Download details for task task_id"""
710 if task_id
in self
.tasks
: # if cached
711 task
= self
.tasks
[task_id
]
713 task
= get_task(task_id
)
714 self
.tasks
[task_id
] = task
717 def task_to_skip_if(self
, task_id
):
718 """Calculate the skip-if condition for failing task task_id"""
721 task
= self
.get_task(task_id
)
728 test_setting
= task
.get("extra", {}).get("test-setting", {})
729 platform
= test_setting
.get("platform", {})
730 platform_os
= platform
.get("os", {})
731 if "name" in platform_os
:
732 os
= platform_os
["name"]
737 if "version" in platform_os
:
738 os_version
= platform_os
["version"]
739 if len(os_version
) == 4:
740 os_version
= os_version
[0:2] + "." + os_version
[2:4]
741 if "arch" in platform
:
742 arch
= platform
["arch"]
743 if arch
== "x86" or arch
.find("32") >= 0:
745 if "display" in platform
:
746 display
= platform
["display"]
747 if "runtime" in test_setting
:
748 for k
in test_setting
["runtime"]:
749 if k
in self
.variants
:
750 runtimes
.append(self
.variants
[k
]) # adds mozinfo
751 if "build" in test_setting
:
752 tbuild
= test_setting
["build"]
757 if tbuild
[k
] == "opt":
759 elif tbuild
[k
] == "debug":
762 build_types
.append(k
)
763 if len(build_types
) == 0:
765 build_types
.append("!debug")
767 build_types
.append("debug")
770 skip_if
= "os == '" + os
+ "'"
771 if os_version
is not None:
773 skip_if
+= "os_version == '" + os_version
+ "'"
776 skip_if
+= "bits == '" + bits
+ "'"
777 if display
is not None:
779 skip_if
+= "display == '" + display
+ "'"
780 for runtime
in runtimes
:
783 for build_type
in build_types
:
785 skip_if
+= build_type
788 def get_file_info(self
, path
, product
="Testing", component
="General"):
790 Get bugzilla product and component for the path.
791 Provide defaults (in case command_context is not defined
792 or there isn't file info available).
794 if path
!= "DEFAULT" and self
.command_context
is not None:
795 reader
= self
.command_context
.mozbuild_reader(config_mode
="empty")
796 info
= reader
.files_info([path
])
797 cp
= info
[path
]["BUG_COMPONENT"]
799 component
= cp
.component
800 return product
, component
802 def get_filename_in_manifest(self
, manifest
, path
):
803 """return relative filename for path in manifest"""
805 filename
= os
.path
.basename(path
)
806 if filename
== "DEFAULT":
808 manifest_dir
= os
.path
.dirname(manifest
)
810 j
= min(len(manifest_dir
), len(path
))
811 while i
< j
and manifest_dir
[i
] == path
[i
]:
813 if i
< len(manifest_dir
):
814 for _
in range(manifest_dir
.count("/", i
) + 1):
815 filename
= "../" + filename
817 filename
= path
[i
+ 1 :]
820 def get_push_id(self
, revision
, repo
):
821 """Return the push_id for revision and repo (or None)"""
823 self
.vinfo(f
"Retrieving push_id for {repo} revision: {revision} ...")
824 if revision
in self
.push_ids
: # if cached
825 push_id
= self
.push_ids
[revision
]
828 push_url
= f
"https://treeherder.mozilla.org/api/project/{repo}/push/"
830 params
["full"] = "true"
832 params
["revision"] = revision
833 r
= requests
.get(push_url
, headers
=self
.headers
, params
=params
)
834 if r
.status_code
!= 200:
835 self
.warning(f
"FAILED to query Treeherder = {r} for {r.url}")
838 if "results" in response
:
839 results
= response
["results"]
844 self
.push_ids
[revision
] = push_id
847 def get_job_id(self
, push_id
, task_id
):
848 """Return the job_id for push_id, task_id (or None)"""
850 self
.vinfo(f
"Retrieving job_id for push_id: {push_id}, task_id: {task_id} ...")
851 if push_id
in self
.job_ids
: # if cached
852 job_id
= self
.job_ids
[push_id
]
856 params
["push_id"] = push_id
857 r
= requests
.get(self
.jobs_url
, headers
=self
.headers
, params
=params
)
858 if r
.status_code
!= 200:
859 self
.warning(f
"FAILED to query Treeherder = {r} for {r.url}")
862 if "results" in response
:
863 results
= response
["results"]
865 for result
in results
:
867 if result
[14] == task_id
:
870 self
.job_ids
[push_id
] = job_id
873 def get_bug_suggestions(self
, repo
, job_id
, path
):
875 Return the (suggestions_url, line_number, line, log_url)
876 for the given repo and job_id
879 f
"Retrieving bug_suggestions for {repo} job_id: {job_id}, path: {path} ..."
881 suggestions_url
= f
"https://treeherder.mozilla.org/api/project/{repo}/jobs/{job_id}/bug_suggestions/"
885 r
= requests
.get(suggestions_url
, headers
=self
.headers
)
886 if r
.status_code
!= 200:
887 self
.warning(f
"FAILED to query Treeherder = {r} for {r.url}")
890 if len(response
) > 0:
891 for sugg
in response
:
892 if sugg
["path_end"] == path
:
893 line_number
= sugg
["line_number"] + 1
894 line
= sugg
["search"]
895 log_url
= f
"https://treeherder.mozilla.org/logviewer?repo={repo}&job_id={job_id}&lineNumber={line_number}"
897 rv
= (suggestions_url
, line_number
, line
, log_url
)
900 def read_json(self
, filename
):
901 """read data as JSON from filename"""
902 fp
= io
.open(filename
, "r", encoding
="utf-8")
907 def write_json(self
, filename
, data
):
908 """saves data as JSON to filename"""
909 fp
= io
.open(filename
, "w", encoding
="utf-8")
910 json
.dump(data
, fp
, indent
=2, sort_keys
=True)
913 def write_tasks(self
, save_tasks
, tasks
):
914 """saves tasks as JSON to save_tasks"""
917 if not isinstance(task
, TestTask
):
920 jtask
["id"] = task
.id
921 jtask
["label"] = task
.label
922 jtask
["duration"] = task
.duration
923 jtask
["result"] = task
.result
924 jtask
["state"] = task
.state
926 for k
, v
in task
.tags
.items():
927 if k
== "createdForUser":
928 jtags
[k
] = "ci@mozilla.com"
931 jtask
["tags"] = jtags
932 jtask
["tier"] = task
.tier
934 {"group": r
.group
, "ok": r
.ok
, "duration": r
.duration
}
935 for r
in task
.results
937 jtask
["errors"] = None # Bug with task.errors property??
939 for k
in task
.failure_types
:
940 jft
[k
] = [[f
[0], f
[1].value
] for f
in task
.failure_types
[k
]]
941 jtask
["failure_types"] = jft
943 self
.write_json(save_tasks
, jtasks
)
945 def label_to_platform_testname(self
, label
):
946 """convert from label to platform, testname for mach command line"""
949 platform_details
= label
.split("/")
950 if len(platform_details
) == 2:
951 platform
, details
= platform_details
952 words
= details
.split("-")
954 platform
+= "/" + words
.pop(0) # opt or debug
956 _chunk
= int(words
[-1])
960 words
.pop() # remove test suffix
961 testname
= "-".join(words
)
964 return platform
, testname
966 def add_attachment_log_for_task(self
, bugid
, task_id
):
967 """Adds compressed log for this task to bugid"""
969 log_url
= f
"https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/{task_id}/artifacts/public/logs/live_backing.log"
970 r
= requests
.get(log_url
, headers
=self
.headers
)
971 if r
.status_code
!= 200:
972 self
.error(f
"Unable get log for task: {task_id}")
974 attach_fp
= tempfile
.NamedTemporaryFile()
975 fp
= gzip
.open(attach_fp
, "wb")
976 fp
.write(r
.text
.encode("utf-8"))
978 self
._initialize
_bzapi
()
979 description
= ATTACHMENT_DESCRIPTION
+ task_id
980 file_name
= TASK_LOG
+ ".gz"
981 comment
= "Added compressed log"
982 content_type
= "application/gzip"
984 self
._bzapi
.attachfile(
990 content_type
=content_type
,
994 pass # Fault expected: Failed to fetch key 9372091 from network storage: The specified key does not exist.