forked from mirrors/gecko-dev
		
	Bug 1873265 - mach skip-fails improvements r=jmaher
Differential Revision: https://phabricator.services.mozilla.com/D198329
This commit is contained in:
		
							parent
							
								
									23e246b6b0
								
							
						
					
					
						commit
						d9e526f1a8
					
				
					 5 changed files with 299 additions and 73 deletions
				
			
		|  | @ -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. | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| ```` | ```` | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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. | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 Tom Marble
						Tom Marble