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 collections
import defaultdict
18 from datetime
import date
, datetime
, timedelta
19 from io
import StringIO
20 from pathlib
import Path
23 from redo
import retry
24 from requests
.packages
.urllib3
.util
.retry
import Retry
27 API_ROOT
= "https://firefox-ci-tc.services.mozilla.com/api/index/v1"
28 MULTI_REVISION_ROOT
= f
"{API_ROOT}/namespaces"
29 MULTI_TASK_ROOT
= f
"{API_ROOT}/tasks"
30 ON_TRY
= "MOZ_AUTOMATION" in os
.environ
32 METRICS_MATCHER
= re
.compile(r
"(perfMetrics\s.*)")
34 "org.mozilla.fenix": "fenix",
35 "org.mozilla.firefox": "fenix",
36 "org.mozilla.geckoview_example": "geckoview",
40 class NoPerfMetricsError(Exception):
41 """Raised when perfMetrics were not found, or were not output
44 def __init__(self
, flavor
):
46 f
"No perftest results were found in the {flavor} test. Results must be "
47 'reported using:\n info("perfMetrics", { metricName: metricValue });'
52 def __init__(self
, matcher
):
54 self
.stdout
= sys
.__stdout
__
55 self
.matcher
= matcher
65 newline_index
= buf
.index("\n")
67 # No newline, wait for next call
71 # Get data up to next newline and combine with previously buffered data
72 data
= self
.buf
+ buf
[: newline_index
+ 1]
73 buf
= buf
[newline_index
+ 1 :]
75 # Reset buffer then output line
77 if data
.strip() == "":
79 self
.stdout
.write(data
.strip("\n") + "\n")
81 # Check if a temporary commit wa created
82 match
= self
.matcher
.match(data
)
84 # Last line found is the revision we want
85 self
._match
.append(match
.group(1))
91 @contextlib.contextmanager
92 def silence(layer
=None):
94 to_patch
= (MachLogger
,)
96 to_patch
= (MachLogger
, layer
)
98 meths
= ("info", "debug", "warning", "error", "log")
99 patched
= defaultdict(dict)
101 oldout
, olderr
= sys
.stdout
, sys
.stderr
102 sys
.stdout
, sys
.stderr
= StringIO(), StringIO()
104 def _vacuum(*args
, **kw
):
105 sys
.stdout
.write(str(args
))
109 if not hasattr(obj
, meth
):
111 patched
[obj
][meth
] = getattr(obj
, meth
)
112 setattr(obj
, meth
, _vacuum
)
114 stdout
= stderr
= None
116 sys
.stdout
.buffer = sys
.stdout
117 sys
.stderr
.buffer = sys
.stderr
118 sys
.stdout
.fileno
= sys
.stderr
.fileno
= lambda: -1
120 yield sys
.stdout
, sys
.stderr
123 stdout
= sys
.stdout
.read()
125 stderr
= sys
.stderr
.read()
128 sys
.stdout
, sys
.stderr
= oldout
, olderr
129 for obj
, meths
in patched
.items():
130 for name
, old_func
in meths
.items():
132 setattr(obj
, name
, old_func
)
135 if stdout
is not None:
137 if stderr
is not None:
141 def simple_platform():
142 plat
= host_platform()
144 if plat
.startswith("win"):
146 elif plat
.startswith("linux"):
153 is_64bits
= sys
.maxsize
> 2**32
155 if sys
.platform
.startswith("win"):
158 elif sys
.platform
.startswith("linux"):
161 elif sys
.platform
.startswith("darwin"):
164 raise ValueError(f
"platform not yet supported: {sys.platform}")
168 """Wrapper around the mach logger to make logging simpler."""
170 def __init__(self
, mach_cmd
):
171 self
._logger
= mach_cmd
.log
177 def info(self
, msg
, name
="mozperftest", **kwargs
):
178 self
._logger
(logging
.INFO
, name
, kwargs
, msg
)
180 def debug(self
, msg
, name
="mozperftest", **kwargs
):
181 self
._logger
(logging
.DEBUG
, name
, kwargs
, msg
)
183 def warning(self
, msg
, name
="mozperftest", **kwargs
):
184 self
._logger
(logging
.WARNING
, name
, kwargs
, msg
)
186 def error(self
, msg
, name
="mozperftest", **kwargs
):
187 self
._logger
(logging
.ERROR
, name
, kwargs
, msg
)
190 def install_package(virtualenv_manager
, package
, ignore_failure
=False):
191 """Installs a package using the virtualenv manager.
193 Makes sure the package is really installed when the user already has it
194 in their local installation.
196 Returns True on success, or re-raise the error. If ignore_failure
197 is set to True, ignore the error and return False
199 from pip
._internal
.req
.constructors
import install_req_from_line
201 # Ensure that we are looking in the right places for packages. This
202 # is required in CI because pip installs in an area that is not in
204 venv_site_lib
= str(Path(virtualenv_manager
.bin_path
, "..", "lib").resolve())
205 venv_site_packages
= str(
208 f
"python{sys.version_info.major}.{sys.version_info.minor}",
212 if venv_site_packages
not in sys
.path
and ON_TRY
:
213 sys
.path
.insert(0, venv_site_packages
)
215 req
= install_req_from_line(package
)
216 req
.check_if_exists(use_user_site
=False)
217 # already installed, check if it's in our venv
218 if req
.satisfied_by
is not None:
219 site_packages
= os
.path
.abspath(req
.satisfied_by
.location
)
220 if site_packages
.startswith(venv_site_lib
):
221 # already installed in this venv, we can skip
225 subprocess
.check_call(
226 [virtualenv_manager
.python_path
, "-m", "pip", "install", package
]
230 if not ignore_failure
:
235 def install_requirements_file(
236 virtualenv_manager
, requirements_file
, ignore_failure
=False
238 """Installs a package using the virtualenv manager.
240 Makes sure the package is really installed when the user already has it
241 in their local installation.
243 Returns True on success, or re-raise the error. If ignore_failure
244 is set to True, ignore the error and return False
247 # Ensure that we are looking in the right places for packages. This
248 # is required in CI because pip installs in an area that is not in
250 venv_site_lib
= str(Path(virtualenv_manager
.bin_path
, "..", "lib").resolve())
251 venv_site_packages
= Path(
253 f
"python{sys.version_info.major}.{sys.version_info.minor}",
256 if not venv_site_packages
.exists():
257 venv_site_packages
= Path(
262 venv_site_packages
= str(venv_site_packages
)
263 if venv_site_packages
not in sys
.path
and ON_TRY
:
264 sys
.path
.insert(0, venv_site_packages
)
269 os
.chdir(Path(requirements_file
).parent
)
270 subprocess
.check_call(
272 virtualenv_manager
.python_path
,
281 "https://pypi.pub.build.mozilla.org/pub/",
286 if not ignore_failure
:
293 # on try, we create tests packages where tests, like
294 # xpcshell tests, don't have the same path.
295 # see - python/mozbuild/mozbuild/action/test_archive.py
296 # this mapping will map paths when running there.
297 # The key is the source path, and the value the ci path
299 Path("netwerk"): Path("xpcshell", "tests", "netwerk"),
300 Path("dom"): Path("mochitest", "tests", "dom"),
304 def build_test_list(tests
):
305 """Collects tests given a list of directories, files and URLs.
307 Returns a tuple containing the list of tests found and a temp dir for tests
308 that were downloaded from an URL.
312 if isinstance(tests
, str):
316 if test
.startswith("http"):
318 temp_dir
= tempfile
.mkdtemp()
319 target
= Path(temp_dir
, test
.split("/")[-1])
320 download_file(test
, target
)
321 res
.append(str(target
))
325 if ON_TRY
and not p_test
.resolve().exists():
326 # until we have pathlib.Path.is_relative_to() (3.9)
327 for src_path
, ci_path
in _TRY_MAPPING
.items():
328 src_path
, ci_path
= str(src_path
), str(ci_path
) # noqa
329 if test
.startswith(src_path
):
330 p_test
= Path(test
.replace(src_path
, ci_path
))
333 resolved_test
= p_test
.resolve()
335 if resolved_test
.is_file():
336 res
.append(str(resolved_test
))
337 elif resolved_test
.is_dir():
338 for file in resolved_test
.rglob("perftest_*.js"):
339 res
.append(str(file))
341 raise FileNotFoundError(str(resolved_test
))
346 def download_file(url
, target
, retry_sleep
=RETRY_SLEEP
, attempts
=3):
347 """Downloads a file, given an URL in the target path.
349 The function will attempt several times on failures.
352 def _download_file(url
, target
):
353 req
= requests
.get(url
, stream
=True, timeout
=30)
354 target_dir
= target
.parent
.resolve()
355 if str(target_dir
) != "":
356 target_dir
.mkdir(exist_ok
=True)
358 with target
.open("wb") as f
:
359 for chunk
in req
.iter_content(chunk_size
=1024):
370 sleeptime
=retry_sleep
,
375 @contextlib.contextmanager
376 def temporary_env(**env
):
378 for key
, value
in env
.items():
379 old
[key
] = os
.environ
.get(key
)
380 if value
is None and key
in os
.environ
:
382 elif value
is not None:
383 os
.environ
[key
] = value
387 for key
, value
in old
.items():
388 if value
is None and key
in os
.environ
:
390 elif value
is not None:
391 os
.environ
[key
] = value
394 def convert_day(day
):
395 if day
in ("yesterday", "today"):
397 if day
== "yesterday":
398 curr
= curr
- timedelta(1)
399 day
= curr
.strftime("%Y.%m.%d")
401 # verify that the user provided string is in the expected format
402 # if it can't parse it, it'll raise a value error
403 datetime
.strptime(day
, "%Y.%m.%d")
408 def get_revision_namespace_url(route
, day
="yesterday"):
409 """Builds a URL to obtain all the namespaces of a given build route for a single day."""
410 day
= convert_day(day
)
411 return f
"""{MULTI_REVISION_ROOT}/{route}.{day}.revision"""
414 def get_multi_tasks_url(route
, revision
, day
="yesterday"):
415 """Builds a URL to obtain all the tasks of a given build route for a single day.
417 If previous is true, then we get builds from the previous day,
418 otherwise, we look at the current day.
420 day
= convert_day(day
)
421 return f
"""{MULTI_TASK_ROOT}/{route}.{day}.revision.{revision}"""
425 if isinstance(val
, (bool, int)):
427 if not isinstance(bool, str):
428 raise ValueError(val
)
430 if val
in ("y", "yes", "t", "true", "on", "1"):
432 elif val
in ("n", "no", "f", "false", "off", "0"):
435 raise ValueError("invalid truth value %r" % (val
,))
438 @contextlib.contextmanager
440 tempdir
= tempfile
.mkdtemp()
444 shutil
.rmtree(tempdir
)
447 def load_class(path
):
448 """Loads a class given its path and returns it.
450 The path is a string of the form `package.module:class` that points
451 to the class to be imported.
453 If if can't find it, or if the path is malformed,
454 an ImportError is raised.
457 raise ImportError(f
"Malformed path '{path}'")
458 elmts
= path
.split(":")
460 raise ImportError(f
"Malformed path '{path}'")
461 mod_name
, klass_name
= elmts
463 mod
= importlib
.import_module(mod_name
)
464 except ModuleNotFoundError
:
465 raise ImportError(f
"Can't find '{mod_name}'")
467 klass
= getattr(mod
, klass_name
)
468 except AttributeError:
469 raise ImportError(f
"Can't find '{klass_name}' in '{mod_name}'")
473 def load_class_from_path(klass_name
, path
):
474 """This function returns a Transformer class with the given path.
476 :param str path: The path points to the custom transformer.
477 :param bool ret_members: If true then return inspect.getmembers().
478 :return Transformer if not ret_members else inspect.getmembers().
480 file = pathlib
.Path(path
)
482 if not file.exists():
483 raise ImportError(f
"The class path {path} does not exist.")
485 # Importing a source file directly
486 spec
= importlib
.util
.spec_from_file_location(name
=file.name
, location
=path
)
487 module
= importlib
.util
.module_from_spec(spec
)
488 spec
.loader
.exec_module(module
)
490 members
= inspect
.getmembers(
492 lambda c
: inspect
.isclass(c
) and c
.__name
__ == klass_name
,
496 raise ImportError(f
"The path {path} was found but it was not a valid class.")
498 return members
[0][-1]
501 def run_script(cmd
, cmd_args
=None, verbose
=False, display
=False, label
=None):
502 """Used to run a command in a subprocess."""
503 if isinstance(cmd
, str):
504 cmd
= shlex
.split(cmd
)
507 except AttributeError:
509 joiner
= subprocess
.list2cmdline
513 sys
.stdout
.write(f
"=> {label} ")
517 args
= cmd
+ list(cmd_args
)
521 sys
.stdout
.write(f
"\nRunning {' '.join(args)}\n")
523 output
= subprocess
.check_output(args
)
525 sys
.stdout
.write("\n")
526 for line
in output
.split(b
"\n"):
527 sys
.stdout
.write(line
.decode("utf8") + "\n")
528 sys
.stdout
.write("[OK]\n")
531 except subprocess
.CalledProcessError
as e
:
532 for line
in e
.output
.split(b
"\n"):
533 sys
.stdout
.write(line
.decode("utf8") + "\n")
534 sys
.stdout
.write("[FAILED]\n")
539 def run_python_script(
547 """Used to run a Python script in isolation."""
550 cmd
= [virtualenv_manager
.python_path
, "-m", module
]
551 return run_script(cmd
, module_args
, verbose
=verbose
, display
=display
, label
=label
)
554 def checkout_script(cmd
, cmd_args
=None, verbose
=False, display
=False, label
=None):
555 return run_script(cmd
, cmd_args
, verbose
, display
, label
)[0]
558 def checkout_python_script(
566 return run_python_script(
567 virtualenv_manager
, module
, module_args
, verbose
, display
, label
572 "{0}/secrets/v1/secret/project"
573 "{1}releng{1}gecko{1}build{1}level-{2}{1}conditioned-profiles"
575 _WPT_URL
= "{0}/secrets/v1/secret/project/perftest/gecko/level-{1}/perftest-login"
576 _DEFAULT_SERVER
= "https://firefox-ci-tc.services.mozilla.com"
579 @functools.lru_cache()
580 def get_tc_secret(wpt
=False):
581 """Returns the Taskcluster secret.
583 Raises an OSError when not running on try
586 raise OSError("Not running in Taskcluster")
587 session
= requests
.Session()
588 retry
= Retry(total
=5, backoff_factor
=0.1, status_forcelist
=[500, 502, 503, 504])
589 http_adapter
= requests
.adapters
.HTTPAdapter(max_retries
=retry
)
590 session
.mount("https://", http_adapter
)
591 session
.mount("http://", http_adapter
)
592 secrets_url
= _URL
.format(
593 os
.environ
.get("TASKCLUSTER_PROXY_URL", _DEFAULT_SERVER
),
595 os
.environ
.get("MOZ_SCM_LEVEL", "1"),
598 secrets_url
= _WPT_URL
.format(
599 os
.environ
.get("TASKCLUSTER_PROXY_URL", _DEFAULT_SERVER
),
600 os
.environ
.get("MOZ_SCM_LEVEL", "1"),
602 res
= session
.get(secrets_url
, timeout
=DOWNLOAD_TIMEOUT
)
603 res
.raise_for_status()
604 return res
.json()["secret"]
607 def get_output_dir(output
, folder
=None):
609 raise Exception("Output path was not provided.")
611 result_dir
= Path(output
)
612 if folder
is not None:
613 result_dir
= Path(result_dir
, folder
)
615 result_dir
.mkdir(parents
=True, exist_ok
=True)
616 result_dir
= result_dir
.resolve()
621 def create_path(path
):
625 create_path(path
.parent
)
626 path
.mkdir(exist_ok
=True)
630 def get_pretty_app_name(app
):
631 # XXX See bug 1712337, we need a singluar point of entry
632 # for the binary to allow us to get the version/app info
633 # so that we can get a pretty name on desktop.
634 return PRETTY_APP_NAMES
[app
]