Bug 1873265 - mach skip-fails improvements r=jmaher

Differential Revision: https://phabricator.services.mozilla.com/D198329
This commit is contained in:
Tom Marble 2024-01-12 15:56:31 +00:00
parent 23e246b6b0
commit d9e526f1a8
5 changed files with 299 additions and 73 deletions

View file

@ -16,19 +16,19 @@ Naming Convention
The build system does not enforce file naming for test manifest files. The build system does not enforce file naming for test manifest files.
However, the following convention is used. However, the following convention is used.
mochitest.ini mochitest.toml
For the *plain* flavor of mochitests. For the *plain* flavor of mochitests.
chrome.ini chrome.toml
For the *chrome* flavor of mochitests. For the *chrome* flavor of mochitests.
browser.ini browser.toml
For the *browser chrome* flavor of mochitests. For the *browser chrome* flavor of mochitests.
a11y.ini a11y.toml
For the *a11y* flavor of mochitests. For the *a11y* flavor of mochitests.
xpcshell.ini xpcshell.toml
For *xpcshell* tests. For *xpcshell* tests.
.. _manifestparser_manifests: .. _manifestparser_manifests:
@ -36,25 +36,28 @@ xpcshell.ini
ManifestParser Manifests ManifestParser Manifests
========================== ==========================
ManifestParser manifests are essentially ini files that conform to a basic ManifestParser manifests are essentially toml files that conform to a basic
set of assumptions. set of assumptions.
The :doc:`reference documentation </mozbase/manifestparser>` The :doc:`reference documentation </mozbase/manifestparser>`
for manifestparser manifests describes the basic format of test manifests. for manifestparser manifests describes the basic format of test manifests.
In summary, manifests are ini files with section names describing test files:: In summary, manifests are toml files with section names describing test files::
[test_foo.js] ["test_foo.js"]
[test_bar.js] ["test_bar.js"]
Keys under sections can hold metadata about each test:: Keys under sections can hold metadata about each test::
[test_foo.js] ["test_foo.js"]
skip-if = os == "win" skip-if = ["os == 'win'"]
[test_foo.js] ["test_foo.js"]
skip-if = os == "linux" && debug skip-if = ["os == 'linux' && debug"]
[test_baz.js] ["test_baz.js"]
fail-if = os == "mac" || os == "android" fail-if = [
"os == 'mac'",
"os == 'android'",
]
There is a special **DEFAULT** section whose keys/metadata apply to all There is a special **DEFAULT** section whose keys/metadata apply to all
sections/tests:: sections/tests::
@ -62,7 +65,7 @@ sections/tests::
[DEFAULT] [DEFAULT]
property = value property = value
[test_foo.js] ["test_foo.js"]
In the above example, **test_foo.js** inherits the metadata **property = value** In the above example, **test_foo.js** inherits the metadata **property = value**
from the **DEFAULT** section. from the **DEFAULT** section.
@ -105,10 +108,10 @@ support-files
in its own **support-files** entry. These use a syntax where paths in its own **support-files** entry. These use a syntax where paths
starting with ``!/`` will indicate the beginning of the path to a starting with ``!/`` will indicate the beginning of the path to a
shared support file starting from the root of the srcdir. For example, shared support file starting from the root of the srcdir. For example,
if a manifest at ``dom/base/test/mochitest.ini`` has a support file, if a manifest at ``dom/base/test/mochitest.toml`` has a support file,
``dom/base/test/server-script.sjs``, and a mochitest in ``dom/base/test/server-script.sjs``, and a mochitest in
``dom/workers/test`` depends on that support file, the test manifest ``dom/workers/test`` depends on that support file, the test manifest
at ``dom/workers/test/mochitest.ini`` must include at ``dom/workers/test/mochitest.toml`` must include
``!/dom/base/test/server-script.sjs`` in its **support-files** entry. ``!/dom/base/test/server-script.sjs`` in its **support-files** entry.
generated-files generated-files
@ -131,7 +134,7 @@ dupe-manifest
Record that this manifest duplicates another manifest. Record that this manifest duplicates another manifest.
The common scenario is two manifest files will include a shared The common scenario is two manifest files will include a shared
manifest file via the ``[include:file]`` special section. The build manifest file via the ``["include:file"]`` special section. The build
system enforces that each test file is only provided by a single system enforces that each test file is only provided by a single
manifest. Having this key present bypasses that check. manifest. Having this key present bypasses that check.
@ -147,10 +150,11 @@ skip-if
.. parsed-literal:: .. parsed-literal::
[test_foo.js] ["test_foo.js"]
skip-if = skip-if = [
os == "mac" && fission # bug 123 - fails on fission "os == 'mac' && fission", # bug 123 - fails on fission
os == "windows" && debug # bug 456 - hits an assertion "os == 'windows' && debug", # bug 456 - hits an assertion
]
fail-if fail-if
Expect test failure if the specified condition is true. Expect test failure if the specified condition is true.

View file

@ -1279,6 +1279,13 @@ def manifest(_command_context):
dest="use_failures", dest="use_failures",
help="Use failures from file", help="Use failures from file",
) )
@CommandArgument(
"-M",
"--max-failures",
default=-1,
dest="max_failures",
help="Maximum number of failures to skip (-1 == no limit)",
)
@CommandArgument("-v", "--verbose", action="store_true", help="Verbose mode") @CommandArgument("-v", "--verbose", action="store_true", help="Verbose mode")
@CommandArgument( @CommandArgument(
"-d", "-d",
@ -1296,6 +1303,7 @@ def skipfails(
use_tasks=None, use_tasks=None,
save_failures=None, save_failures=None,
use_failures=None, use_failures=None,
max_failures=-1,
verbose=False, verbose=False,
dry_run=False, dry_run=False,
): ):
@ -1307,10 +1315,19 @@ def skipfails(
except ValueError: except ValueError:
meta_bug_id = None meta_bug_id = None
if max_failures is not None:
try:
max_failures = int(max_failures)
except ValueError:
max_failures = -1
else:
max_failures = -1
Skipfails(command_context, try_url, verbose, bugzilla, dry_run, turbo).run( Skipfails(command_context, try_url, verbose, bugzilla, dry_run, turbo).run(
meta_bug_id, meta_bug_id,
save_tasks, save_tasks,
use_tasks, use_tasks,
save_failures, save_failures,
use_failures, use_failures,
max_failures,
) )

View file

@ -75,8 +75,8 @@ advantages:
.. code-block:: text .. code-block:: text
['test_broken.js'] ["test_broken.js"]
disabled = 'https://bugzilla.mozilla.org/show_bug.cgi?id=123456' disabled = "https://bugzilla.mozilla.org/show_bug.cgi?id=123456"
* ability to run different (subsets of) tests on different * ability to run different (subsets of) tests on different
platforms. Traditionally, we've done a bit of magic or had the test platforms. Traditionally, we've done a bit of magic or had the test
@ -86,10 +86,8 @@ advantages:
.. code-block:: text .. code-block:: text
['test_works_on_windows_only.js'] ["test_works_on_windows_only.js"]
skip-if = [ skip-if = ["os != 'win'"]
"os != 'win'",
]
* ability to markup tests with metadata. We have a large, complicated, * ability to markup tests with metadata. We have a large, complicated,
and always changing infrastructure. key, value metadata may be used and always changing infrastructure. key, value metadata may be used
@ -98,7 +96,7 @@ advantages:
number, if it were desirable. number, if it were desirable.
* ability to have sane and well-defined test-runs. You can keep * ability to have sane and well-defined test-runs. You can keep
different manifests for different test runs and ``['include:FILENAME.toml']`` different manifests for different test runs and ``["include:FILENAME.toml"]``
(sub)manifests as appropriate to your needs. (sub)manifests as appropriate to your needs.
Manifest Format Manifest Format
@ -109,9 +107,9 @@ relative to the manifest:
.. code-block:: text .. code-block:: text
['foo.js'] ["foo.js"]
['bar.js'] ["bar.js"]
['fleem.js'] ["fleem.js"]
The sections are read in order. In addition, tests may include The sections are read in order. In addition, tests may include
arbitrary key, value metadata to be used by the harness. You may also arbitrary key, value metadata to be used by the harness. You may also
@ -121,31 +119,31 @@ be inherited by each test unless overridden:
.. code-block:: text .. code-block:: text
[DEFAULT] [DEFAULT]
type = 'restart' type = "restart"
['lilies.js'] ["lilies.js"]
color = 'white' color = "white"
['daffodils.js'] ["daffodils.js"]
color = 'yellow' color = "yellow"
type = 'other' type = "other"
# override type from DEFAULT # override type from DEFAULT
['roses.js'] ["roses.js"]
color = 'red' color = "red"
You can also include other manifests: You can also include other manifests:
.. code-block:: text .. code-block:: text
['include:subdir/anothermanifest.toml'] ["include:subdir/anothermanifest.toml"]
And reference parent manifests to inherit keys and values from the DEFAULT And reference parent manifests to inherit keys and values from the DEFAULT
section, without adding possible included tests. section, without adding possible included tests.
.. code-block:: text .. code-block:: text
['parent:../manifest.toml'] ["parent:../manifest.toml"]
Manifests are included relative to the directory of the manifest with Manifests are included relative to the directory of the manifest with
the `[include:]` directive unless they are absolute paths. the `[include:]` directive unless they are absolute paths.
@ -155,17 +153,17 @@ new line, or be inline.
.. code-block:: text .. code-block:: text
['roses.js'] ["roses.js"]
# a valid comment # a valid comment
color = 'red' # another valid comment color = "red" # another valid comment
Because in TOML all values must be quoted there is no risk of an anchor in Because in TOML all values must be quoted there is no risk of an anchor in
an URL being interpreted as a comment. an URL being interpreted as a comment.
.. code-block:: text .. code-block:: text
['test1.js'] ["test1.js"]
url = 'https://foo.com/bar#baz' # Bug 1234 url = "https://foo.com/bar#baz" # Bug 1234
Manifest Conditional Expressions Manifest Conditional Expressions
@ -198,7 +196,7 @@ This data corresponds to a one-line manifest:
.. code-block:: text .. code-block:: text
['testToolbar/testBackForwardButtons.js'] ["testToolbar/testBackForwardButtons.js"]
If additional key, values were specified, they would be in this dict If additional key, values were specified, they would be in this dict
as well. as well.
@ -241,7 +239,7 @@ Additional manifest files may be included with an `[include:]` directive:
.. code-block:: text .. code-block:: text
['include:path-to-additional-file-manifest.toml'] ["include:path-to-additional-file-manifest.toml"]
The path to included files is relative to the current manifest. The path to included files is relative to the current manifest.
@ -317,10 +315,8 @@ files will look like this:
.. code-block:: text .. code-block:: text
['test_foo.py'] ["test_foo.py"]
timeout-if = [ timeout-if = ["300, os == 'win'"]
"300, os == 'win'",
]
The value is <timeout>, <condition> where condition is the same format as the one in The value is <timeout>, <condition> where condition is the same format as the one in
`skip-if`. In the above case, if os == 'win', a timeout of 300 seconds will be `skip-if`. In the above case, if os == 'win', a timeout of 300 seconds will be
@ -499,6 +495,108 @@ manifestparser includes a suite of tests.
`test_manifest.txt` is a doctest that may be helpful in figuring out `test_manifest.txt` is a doctest that may be helpful in figuring out
how to use the API. Tests are run via `mach python-test testing/mozbase/manifestparser`. how to use the API. Tests are run via `mach python-test testing/mozbase/manifestparser`.
Using mach manifest skip-fails
``````````````````````````````
The first of the ``mach manifest`` subcommands is ``skip-fails``. This command
can be used to *automatically* edit manifests to skip tests that are failing
as well as file the corresponding bugs for the failures. This is particularly
useful when "greening up" a new platform.
You may verify the proposed changes from ``skip-fails`` output and examine
any local manifest changes with ``hg status``.
Here is the usage:
.. code-block:: text
$ ./mach manifest skip-fails --help
usage: mach [global arguments] manifest skip-fails [command arguments]
Sub Command Arguments:
try_url Treeherder URL for try (please use quotes)
-b BUGZILLA, --bugzilla BUGZILLA
Bugzilla instance
-m META_BUG_ID, --meta-bug-id META_BUG_ID
Meta Bug id
-s, --turbo Skip all secondary failures
-t SAVE_TASKS, --save-tasks SAVE_TASKS
Save tasks to file
-T USE_TASKS, --use-tasks USE_TASKS
Use tasks from file
-f SAVE_FAILURES, --save-failures SAVE_FAILURES
Save failures to file
-F USE_FAILURES, --use-failures USE_FAILURES
Use failures from file
-M MAX_FAILURES, --max-failures MAX_FAILURES
Maximum number of failures to skip (-1 == no limit)
-v, --verbose Verbose mode
-d, --dry-run Determine manifest changes, but do not write them
$
``try_url`` --- Treeherder URL
------------------------------
This is the url (usually in single quotes) from running tests in try, for example:
'https://treeherder.mozilla.org/jobs?repo=try&revision=babc28f495ee8af2e4f059e9cbd23e84efab7d0d'
``--bugzilla BUGZILLA`` --- Bugzilla instance
---------------------------------------------
By default the Bugzilla instance is ``bugzilla.allizom.org``, but you may set it on the command
line to another value such as ``bugzilla.mozilla.org`` (or by setting the environment variable
``BUGZILLA``).
``--meta-bug-id META_BUG_ID`` --- Meta Bug id
---------------------------------------------
Any new bugs that are filed will block (be dependents of) this "meta" bug (optional).
``--turbo`` --- Skip all secondary failures
-------------------------------------------
The default ``skip-fails`` behavior is to skip only the first failure (for a given label) for each test.
In `turbo` mode, all failures for this manifest + label will skipped.
``--save-tasks SAVE_TASKS`` --- Save tasks to file
--------------------------------------------------
This feature is primarily for ``skip-fails`` development and debugging.
It will save the tasks (downloaded via mozci) to the specified JSON file
(which may be used in a future ``--use-tasks`` option)
``--use-tasks USE_TASKS`` --- Use tasks from file
-------------------------------------------------
This feature is primarily for ``skip-fails`` development and debugging.
It will uses the tasks from the specified JSON file (instead of downloading them via mozci).
See also ``--save-tasks``.
``--save-failures SAVE_FAILURES`` --- Save failures to file
-----------------------------------------------------------
This feature is primarily for ``skip-fails`` development and debugging.
It will save the failures (calculated from the tasks) to the specified JSON file
(which may be used in a future ``--use-failures`` option)
``--use-failures USE_FAILURES`` --- Use failures from file
----------------------------------------------------------
This feature is primarily for ``skip-fails`` development and debugging.
It will uses the failures from the specified JSON file (instead of downloading them via mozci).
See also ``--save-failures``.
``--max-failures MAX_FAILURES`` --- Maximum number of failures to skip
----------------------------------------------------------------------
This feature is primarily for ``skip-fails`` development and debugging.
It will limit the number of failures that are skipped (default is -1 == no limit).
``--verbose`` --- Verbose mode
------------------------------
Increase verbosity of output.
``--dry-run`` --- Dry run
-------------------------
In dry run mode, the manifest changes (and bugs top be filed) are determined, but not written.
Bugs Bugs
```` ````

View file

@ -2,16 +2,20 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this # License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
import gzip
import io import io
import json import json
import logging import logging
import os import os
import os.path import os.path
import pprint import pprint
import re
import sys import sys
import tempfile
import urllib.parse import urllib.parse
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from xmlrpc.client import Fault
from yaml import load from yaml import load
@ -29,6 +33,14 @@ from mozci.task import TestTask
from mozci.util.taskcluster import get_task from mozci.util.taskcluster import get_task
BUGZILLA_AUTHENTICATION_HELP = "Must create a Bugzilla API key per https://github.com/mozilla/mozci-tools/blob/main/citools/test_triage_bug_filer.py" BUGZILLA_AUTHENTICATION_HELP = "Must create a Bugzilla API key per https://github.com/mozilla/mozci-tools/blob/main/citools/test_triage_bug_filer.py"
TASK_LOG = "live_backing.log"
TASK_ARTIFACT = "public/logs/" + TASK_LOG
ATTACHMENT_DESCRIPTION = "Compressed " + TASK_ARTIFACT + " for task "
ATTACHMENT_REGEX = (
r".*Created attachment ([0-9]+)\n.*"
+ ATTACHMENT_DESCRIPTION
+ "([A-Za-z0-9_-]+)\n.*"
)
class MockResult(object): class MockResult(object):
@ -137,6 +149,7 @@ class Skipfails(object):
self.bugzilla = Skipfails.BUGZILLA_SERVER_DEFAULT self.bugzilla = Skipfails.BUGZILLA_SERVER_DEFAULT
self.component = "skip-fails" self.component = "skip-fails"
self._bzapi = None self._bzapi = None
self._attach_rx = None
self.variants = {} self.variants = {}
self.tasks = {} self.tasks = {}
self.pp = None self.pp = None
@ -151,6 +164,7 @@ class Skipfails(object):
"""Lazily initializes the Bugzilla API""" """Lazily initializes the Bugzilla API"""
if self._bzapi is None: if self._bzapi is None:
self._bzapi = bugzilla.Bugzilla(self.bugzilla) self._bzapi = bugzilla.Bugzilla(self.bugzilla)
self._attach_rx = re.compile(ATTACHMENT_REGEX, flags=re.M)
def pprint(self, obj): def pprint(self, obj):
if self.pp is None: if self.pp is None:
@ -182,6 +196,10 @@ class Skipfails(object):
else: else:
print(f"INFO: {e}", file=sys.stderr, flush=True) print(f"INFO: {e}", file=sys.stderr, flush=True)
def vinfo(self, e):
if self.verbose:
self.info(e)
def run( def run(
self, self,
meta_bug_id=None, meta_bug_id=None,
@ -189,6 +207,7 @@ class Skipfails(object):
use_tasks=None, use_tasks=None,
save_failures=None, save_failures=None,
use_failures=None, use_failures=None,
max_failures=-1,
): ):
"Run skip-fails on try_url, return True on success" "Run skip-fails on try_url, return True on success"
@ -197,7 +216,7 @@ class Skipfails(object):
if use_tasks is not None: if use_tasks is not None:
if os.path.exists(use_tasks): if os.path.exists(use_tasks):
self.info(f"use tasks: {use_tasks}") self.vinfo(f"use tasks: {use_tasks}")
tasks = self.read_json(use_tasks) tasks = self.read_json(use_tasks)
tasks = [MockTask(task) for task in tasks] tasks = [MockTask(task) for task in tasks]
else: else:
@ -208,7 +227,7 @@ class Skipfails(object):
if use_failures is not None: if use_failures is not None:
if os.path.exists(use_failures): if os.path.exists(use_failures):
self.info(f"use failures: {use_failures}") self.vinfo(f"use failures: {use_failures}")
failures = self.read_json(use_failures) failures = self.read_json(use_failures)
else: else:
self.error(f"use failures JSON file does not exist: {use_failures}") self.error(f"use failures JSON file does not exist: {use_failures}")
@ -216,13 +235,14 @@ class Skipfails(object):
else: else:
failures = self.get_failures(tasks) failures = self.get_failures(tasks)
if save_failures is not None: if save_failures is not None:
self.info(f"save failures: {save_failures}") self.vinfo(f"save failures: {save_failures}")
self.write_json(save_failures, failures) self.write_json(save_failures, failures)
if save_tasks is not None: if save_tasks is not None:
self.info(f"save tasks: {save_tasks}") self.vinfo(f"save tasks: {save_tasks}")
self.write_tasks(save_tasks, tasks) self.write_tasks(save_tasks, tasks)
num_failures = 0
for manifest in failures: for manifest in failures:
if not manifest.endswith(".toml"): if not manifest.endswith(".toml"):
self.warning(f"cannot process skip-fails on INI manifests: {manifest}") self.warning(f"cannot process skip-fails on INI manifests: {manifest}")
@ -249,6 +269,12 @@ class Skipfails(object):
repo, repo,
meta_bug_id, meta_bug_id,
) )
num_failures += 1
if max_failures >= 0 and num_failures >= max_failures:
self.warning(
f"max_failures={max_failures} threshold reached. stopping."
)
return True
break # just use the first task_id break # just use the first task_id
return True return True
@ -268,8 +294,7 @@ class Skipfails(object):
repo = query[Skipfails.REPO][0] repo = query[Skipfails.REPO][0]
else: else:
repo = "try" repo = "try"
if self.verbose: self.vinfo(f"considering {repo} revision={revision}")
self.info(f"considering {repo} revision={revision}")
return revision, repo return revision, repo
def get_tasks(self, revision, repo): def get_tasks(self, revision, repo):
@ -284,8 +309,8 @@ class Skipfails(object):
* True (passed) * True (passed)
classification: Classification classification: Classification
* unknown (default) < 3 runs * unknown (default) < 3 runs
* intermittent (not enough failures) >3 runs < 0.5 failure rate * intermittent (not enough failures) >3 runs < 0.4 failure rate
* disable_recommended (enough repeated failures) >3 runs >= 0.5 * disable_recommended (enough repeated failures) >3 runs >= 0.4
* disable_manifest (disable DEFAULT if no other failures) * disable_manifest (disable DEFAULT if no other failures)
* secondary (not first failure in group) * secondary (not first failure in group)
* success * success
@ -397,7 +422,7 @@ class Skipfails(object):
"classification" "classification"
] ]
if total_runs >= 3: if total_runs >= 3:
if failed_runs / total_runs < 0.5: if failed_runs / total_runs < 0.4:
if failed_runs == 0: if failed_runs == 0:
classification = Classification.SUCCESS classification = Classification.SUCCESS
else: else:
@ -551,6 +576,7 @@ class Skipfails(object):
): ):
"""Skip a failure""" """Skip a failure"""
self.vinfo(f"===== Skip failure in manifest: {manifest} =====")
skip_if = self.task_to_skip_if(task_id) skip_if = self.task_to_skip_if(task_id)
if skip_if is None: if skip_if is None:
self.warning( self.warning(
@ -583,10 +609,14 @@ class Skipfails(object):
) )
if log_url is not None: if log_url is not None:
comment += f"\n\nBug suggestions: {suggestions_url}" comment += f"\n\nBug suggestions: {suggestions_url}"
comment += f"\nSpecifically see at line {line_number}:\n" comment += f"\nSpecifically see at line {line_number} in the attached log: {log_url}"
comment += f'\n "{line}"' comment += f'\n\n "{line}"\n'
comment += f"\n\nIn the log: {log_url}" platform, testname = self.label_to_platform_testname(label)
if platform is not None:
comment += "\n\nCommand line to reproduce:\n\n"
comment += f" \"mach try fuzzy -q '{platform}' {testname}\""
bug_summary = f"MANIFEST {manifest}" bug_summary = f"MANIFEST {manifest}"
attachments = {}
bugs = self.get_bugs_by_summary(bug_summary) bugs = self.get_bugs_by_summary(bug_summary)
if len(bugs) == 0: if len(bugs) == 0:
description = ( description = (
@ -602,7 +632,7 @@ class Skipfails(object):
else: else:
bug = self.create_bug(bug_summary, description, product, component) bug = self.create_bug(bug_summary, description, product, component)
bugid = bug.id bugid = bug.id
self.info( self.vinfo(
f'Created Bug {bugid} {product}::{component} : "{bug_summary}"' f'Created Bug {bugid} {product}::{component} : "{bug_summary}"'
) )
bug_reference = f"Bug {bugid}" + bug_reference bug_reference = f"Bug {bugid}" + bug_reference
@ -611,11 +641,22 @@ class Skipfails(object):
bug_reference = f"Bug {bugid}" + bug_reference bug_reference = f"Bug {bugid}" + bug_reference
product = bugs[0].product product = bugs[0].product
component = bugs[0].component component = bugs[0].component
self.info(f'Found Bug {bugid} {product}::{component} "{bug_summary}"') self.vinfo(f'Found Bug {bugid} {product}::{component} "{bug_summary}"')
if meta_bug_id is not None: if meta_bug_id is not None:
if meta_bug_id in bugs[0].blocks: if meta_bug_id in bugs[0].blocks:
self.info(f" Bug {bugid} already blocks meta bug {meta_bug_id}") self.vinfo(f" Bug {bugid} already blocks meta bug {meta_bug_id}")
meta_bug_id = None # no need to add again meta_bug_id = None # no need to add again
comments = bugs[0].getcomments()
for i in range(len(comments)):
text = comments[i]["text"]
m = self._attach_rx.findall(text)
if len(m) == 1:
a_task_id = m[0][1]
attachments[a_task_id] = m[0][0]
if a_task_id == task_id:
self.vinfo(
f" Bug {bugid} already has the compressed log attached for this task"
)
else: else:
self.error(f'More than one bug found for summary: "{bug_summary}"') self.error(f'More than one bug found for summary: "{bug_summary}"')
return return
@ -623,11 +664,16 @@ class Skipfails(object):
self.warning(f"Dry-run NOT adding comment to Bug {bugid}: {comment}") self.warning(f"Dry-run NOT adding comment to Bug {bugid}: {comment}")
self.info(f'Dry-run NOT editing ["{filename}"] manifest: "{manifest}"') self.info(f'Dry-run NOT editing ["{filename}"] manifest: "{manifest}"')
self.info(f'would add skip-if condition: "{skip_if}" # {bug_reference}') self.info(f'would add skip-if condition: "{skip_if}" # {bug_reference}')
if task_id not in attachments:
self.info("would add compressed log for this task")
return return
self.add_bug_comment(bugid, comment, meta_bug_id) self.add_bug_comment(bugid, comment, meta_bug_id)
self.info(f"Added comment to Bug {bugid}: {comment}") self.info(f"Added comment to Bug {bugid}: {comment}")
if meta_bug_id is not None: if meta_bug_id is not None:
self.info(f" Bug {bugid} blocks meta Bug: {meta_bug_id}") self.info(f" Bug {bugid} blocks meta Bug: {meta_bug_id}")
if task_id not in attachments:
self.add_attachment_log_for_task(bugid, task_id)
self.info("Added compressed log for this task")
mp = ManifestParser(use_toml=True, document=True) mp = ManifestParser(use_toml=True, document=True)
manifest_path = os.path.join(self.topsrcdir, os.path.normpath(manifest)) manifest_path = os.path.join(self.topsrcdir, os.path.normpath(manifest))
mp.read(manifest_path) mp.read(manifest_path)
@ -745,7 +791,7 @@ class Skipfails(object):
Provide defaults (in case command_context is not defined Provide defaults (in case command_context is not defined
or there isn't file info available). or there isn't file info available).
""" """
if self.command_context is not None: if path != "DEFAULT" and self.command_context is not None:
reader = self.command_context.mozbuild_reader(config_mode="empty") reader = self.command_context.mozbuild_reader(config_mode="empty")
info = reader.files_info([path]) info = reader.files_info([path])
cp = info[path]["BUG_COMPONENT"] cp = info[path]["BUG_COMPONENT"]
@ -774,7 +820,7 @@ class Skipfails(object):
def get_push_id(self, revision, repo): def get_push_id(self, revision, repo):
"""Return the push_id for revision and repo (or None)""" """Return the push_id for revision and repo (or None)"""
self.info(f"Retrieving push_id for {repo} revision: {revision} ...") self.vinfo(f"Retrieving push_id for {repo} revision: {revision} ...")
if revision in self.push_ids: # if cached if revision in self.push_ids: # if cached
push_id = self.push_ids[revision] push_id = self.push_ids[revision]
else: else:
@ -801,7 +847,7 @@ class Skipfails(object):
def get_job_id(self, push_id, task_id): def get_job_id(self, push_id, task_id):
"""Return the job_id for push_id, task_id (or None)""" """Return the job_id for push_id, task_id (or None)"""
self.info(f"Retrieving job_id for push_id: {push_id}, task_id: {task_id} ...") self.vinfo(f"Retrieving job_id for push_id: {push_id}, task_id: {task_id} ...")
if push_id in self.job_ids: # if cached if push_id in self.job_ids: # if cached
job_id = self.job_ids[push_id] job_id = self.job_ids[push_id]
else: else:
@ -829,7 +875,7 @@ class Skipfails(object):
Return the (suggestions_url, line_number, line, log_url) Return the (suggestions_url, line_number, line, log_url)
for the given repo and job_id for the given repo and job_id
""" """
self.info( self.vinfo(
f"Retrieving bug_suggestions for {repo} job_id: {job_id}, path: {path} ..." f"Retrieving bug_suggestions for {repo} job_id: {job_id}, path: {path} ..."
) )
suggestions_url = f"https://treeherder.mozilla.org/api/project/{repo}/jobs/{job_id}/bug_suggestions/" suggestions_url = f"https://treeherder.mozilla.org/api/project/{repo}/jobs/{job_id}/bug_suggestions/"
@ -844,7 +890,7 @@ class Skipfails(object):
if len(response) > 0: if len(response) > 0:
for sugg in response: for sugg in response:
if sugg["path_end"] == path: if sugg["path_end"] == path:
line_number = sugg["line_number"] line_number = sugg["line_number"] + 1
line = sugg["search"] line = sugg["search"]
log_url = f"https://treeherder.mozilla.org/logviewer?repo={repo}&job_id={job_id}&lineNumber={line_number}" log_url = f"https://treeherder.mozilla.org/logviewer?repo={repo}&job_id={job_id}&lineNumber={line_number}"
break break
@ -895,3 +941,54 @@ class Skipfails(object):
jtask["failure_types"] = jft jtask["failure_types"] = jft
jtasks.append(jtask) jtasks.append(jtask)
self.write_json(save_tasks, jtasks) self.write_json(save_tasks, jtasks)
def label_to_platform_testname(self, label):
"""convert from label to platform, testname for mach command line"""
platform = None
testname = None
platform_details = label.split("/")
if len(platform_details) == 2:
platform, details = platform_details
words = details.split("-")
if len(words) > 2:
platform += "/" + words.pop(0) # opt or debug
try:
_chunk = int(words[-1])
words.pop()
except ValueError:
pass
words.pop() # remove test suffix
testname = "-".join(words)
else:
platform = None
return platform, testname
def add_attachment_log_for_task(self, bugid, task_id):
"""Adds compressed log for this task to bugid"""
log_url = f"https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/{task_id}/artifacts/public/logs/live_backing.log"
r = requests.get(log_url, headers=self.headers)
if r.status_code != 200:
self.error(f"Unable get log for task: {task_id}")
return
attach_fp = tempfile.NamedTemporaryFile()
fp = gzip.open(attach_fp, "wb")
fp.write(r.text.encode("utf-8"))
fp.close()
self._initialize_bzapi()
description = ATTACHMENT_DESCRIPTION + task_id
file_name = TASK_LOG + ".gz"
comment = "Added compressed log"
content_type = "application/gzip"
try:
self._bzapi.attachfile(
[bugid],
attach_fp.name,
description,
file_name=file_name,
comment=comment,
content_type=content_type,
is_private=False,
)
except Fault:
pass # Fault expected: Failed to fetch key 9372091 from network storage: The specified key does not exist.

View file

@ -166,5 +166,15 @@ def test_get_filename_in_manifest():
) )
def test_label_to_platform_testname():
"""Test label_to_platform_testname"""
sf = Skipfails()
label = "test-linux2204-64-wayland/opt-mochitest-browser-chrome-swr-13"
platform, testname = sf.label_to_platform_testname(label)
assert platform == "test-linux2204-64-wayland/opt"
assert testname == "mochitest-browser-chrome"
if __name__ == "__main__": if __name__ == "__main__":
main() main()