summaryrefslogtreecommitdiffstats
path: root/python/mozperftest/mozperftest/test
diff options
context:
space:
mode:
Diffstat (limited to 'python/mozperftest/mozperftest/test')
-rw-r--r--python/mozperftest/mozperftest/test/__init__.py25
-rw-r--r--python/mozperftest/mozperftest/test/androidlog.py62
-rw-r--r--python/mozperftest/mozperftest/test/browsertime/__init__.py19
-rw-r--r--python/mozperftest/mozperftest/test/browsertime/package-lock.json1874
-rw-r--r--python/mozperftest/mozperftest/test/browsertime/package.json12
-rw-r--r--python/mozperftest/mozperftest/test/browsertime/runner.py473
-rw-r--r--python/mozperftest/mozperftest/test/browsertime/visualtools.py196
-rw-r--r--python/mozperftest/mozperftest/test/noderunner.py75
-rw-r--r--python/mozperftest/mozperftest/test/webpagetest.py413
-rw-r--r--python/mozperftest/mozperftest/test/xpcshell.py189
10 files changed, 3338 insertions, 0 deletions
diff --git a/python/mozperftest/mozperftest/test/__init__.py b/python/mozperftest/mozperftest/test/__init__.py
new file mode 100644
index 0000000000..c7d7d6e049
--- /dev/null
+++ b/python/mozperftest/mozperftest/test/__init__.py
@@ -0,0 +1,25 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+from mozperftest.layers import Layers
+from mozperftest.test.androidlog import AndroidLog
+from mozperftest.test.browsertime import BrowsertimeRunner
+from mozperftest.test.webpagetest import WebPageTest
+from mozperftest.test.xpcshell import XPCShell
+
+
+def get_layers():
+ return BrowsertimeRunner, AndroidLog, XPCShell, WebPageTest
+
+
+def pick_test(env, flavor, mach_cmd):
+ if flavor == "xpcshell":
+ return Layers(env, mach_cmd, (XPCShell,))
+ if flavor == "desktop-browser":
+ return Layers(env, mach_cmd, (BrowsertimeRunner,))
+ if flavor == "mobile-browser":
+ return Layers(env, mach_cmd, (BrowsertimeRunner, AndroidLog))
+ if flavor == "webpagetest":
+ return Layers(env, mach_cmd, (WebPageTest,))
+
+ raise NotImplementedError(flavor)
diff --git a/python/mozperftest/mozperftest/test/androidlog.py b/python/mozperftest/mozperftest/test/androidlog.py
new file mode 100644
index 0000000000..88bf01f2fe
--- /dev/null
+++ b/python/mozperftest/mozperftest/test/androidlog.py
@@ -0,0 +1,62 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+from pathlib import Path
+
+from mozperftest.layers import Layer
+
+
+class AndroidLog(Layer):
+ """Runs an android log test."""
+
+ name = "androidlog"
+ activated = False
+ arguments = {
+ "first-timestamp": {
+ "type": str,
+ "default": None,
+ "help": "First timestamp regexp",
+ },
+ "second-timestamp": {
+ "type": str,
+ "default": None,
+ "help": "Second timestamp regexp",
+ },
+ "subtest-name": {
+ "type": str,
+ "default": "TimeToDisplayed",
+ "help": "Name of the metric that is produced",
+ },
+ }
+
+ def _get_logcat(self):
+ logcat = self.get_arg("android-capture-logcat")
+ if logcat is None:
+ raise NotImplementedError()
+ # check if the path is absolute or relative to output
+ path = Path(logcat)
+ if not path.is_absolute():
+ return Path(self.get_arg("output"), path).resolve()
+ return path.resolve()
+
+ def __call__(self, metadata):
+ app_name = self.get_arg("android-app-name")
+ first_ts = r".*Start proc.*" + app_name.replace(".", r"\.") + ".*"
+ second_ts = r".*Fully drawn.*" + app_name.replace(".", r"\.") + ".*"
+ options = {
+ "first-timestamp": self.get_arg("first-timestamp", first_ts),
+ "second-timestamp": self.get_arg("second-timestamp", second_ts),
+ "processor": self.env.hooks.get("logcat_processor"),
+ "transform-subtest-name": self.get_arg("subtest-name"),
+ }
+
+ metadata.add_result(
+ {
+ "results": str(self._get_logcat()),
+ "transformer": "LogCatTimeTransformer",
+ "transformer-options": options,
+ "name": "LogCat",
+ }
+ )
+
+ return metadata
diff --git a/python/mozperftest/mozperftest/test/browsertime/__init__.py b/python/mozperftest/mozperftest/test/browsertime/__init__.py
new file mode 100644
index 0000000000..f5e32101cc
--- /dev/null
+++ b/python/mozperftest/mozperftest/test/browsertime/__init__.py
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+
+from mozperftest.test.browsertime.runner import BrowsertimeRunner # noqa
+
+
+def add_option(env, name, value, overwrite=False):
+ if not overwrite:
+ options = env.get_arg("browsertime-extra-options", "")
+ options += f",{name}={value}"
+ else:
+ options = f"{name}={value}"
+ env.set_arg("browsertime-extra-options", options)
+
+
+def add_options(env, options, overwrite=False):
+ for i, (name, value) in enumerate(options):
+ add_option(env, name, value, overwrite=overwrite and i == 0)
diff --git a/python/mozperftest/mozperftest/test/browsertime/package-lock.json b/python/mozperftest/mozperftest/test/browsertime/package-lock.json
new file mode 100644
index 0000000000..af88126fcc
--- /dev/null
+++ b/python/mozperftest/mozperftest/test/browsertime/package-lock.json
@@ -0,0 +1,1874 @@
+{
+ "name": "mozilla-central-tools-browsertime",
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.17.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz",
+ "integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "@cypress/xvfb": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
+ "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==",
+ "dev": true,
+ "requires": {
+ "debug": "^3.1.0",
+ "lodash.once": "^4.1.1"
+ }
+ },
+ "@devicefarmer/adbkit": {
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit/-/adbkit-2.11.3.tgz",
+ "integrity": "sha512-rsgWREAvSRQjdP9/3GoAV6Tq+o97haywgbTfCgt5yUqiDpaaq3hlH9FTo9XsdG8x+Jd0VQ9nTC2IXsDu8JGRSA==",
+ "dev": true,
+ "requires": {
+ "@devicefarmer/adbkit-logcat": "^1.1.0",
+ "@devicefarmer/adbkit-monkey": "~1.0.1",
+ "bluebird": "~2.9.24",
+ "commander": "^2.3.0",
+ "debug": "~2.6.3",
+ "node-forge": "^0.10.0",
+ "split": "~0.3.3"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ }
+ }
+ },
+ "@devicefarmer/adbkit-logcat": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-logcat/-/adbkit-logcat-1.1.0.tgz",
+ "integrity": "sha512-K90P5gUXM/w+yzLvJIRQ+tJooNU6ipUPPQkljtPJ0laR66TGtpt4Gqsjm0n9dPHK1W5KGgU1R5wnCd6RTSlPNA==",
+ "dev": true
+ },
+ "@devicefarmer/adbkit-monkey": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit-monkey/-/adbkit-monkey-1.0.1.tgz",
+ "integrity": "sha512-HilPrVrCosYWqSyjfpDtaaN1kJwdlBpS+IAflP3z+e7nsEgk3JGJf1Vg0NgHJooTf5HDfXSyZqMVg+5jvXCK0g==",
+ "dev": true,
+ "requires": {
+ "async": "~0.2.9"
+ }
+ },
+ "@jimp/bmp": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.16.1.tgz",
+ "integrity": "sha512-iwyNYQeBawrdg/f24x3pQ5rEx+/GwjZcCXd3Kgc+ZUd+Ivia7sIqBsOnDaMZdKCBPlfW364ekexnlOqyVa0NWg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1",
+ "bmp-js": "^0.1.0"
+ }
+ },
+ "@jimp/core": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.16.1.tgz",
+ "integrity": "sha512-la7kQia31V6kQ4q1kI/uLimu8FXx7imWVajDGtwUG8fzePLWDFJyZl0fdIXVCL1JW2nBcRHidUot6jvlRDi2+g==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1",
+ "any-base": "^1.1.0",
+ "buffer": "^5.2.0",
+ "exif-parser": "^0.1.12",
+ "file-type": "^9.0.0",
+ "load-bmfont": "^1.3.1",
+ "mkdirp": "^0.5.1",
+ "phin": "^2.9.1",
+ "pixelmatch": "^4.0.2",
+ "tinycolor2": "^1.4.1"
+ },
+ "dependencies": {
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ }
+ }
+ },
+ "@jimp/custom": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.16.1.tgz",
+ "integrity": "sha512-DNUAHNSiUI/j9hmbatD6WN/EBIyeq4AO0frl5ETtt51VN1SvE4t4v83ZA/V6ikxEf3hxLju4tQ5Pc3zmZkN/3A==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/core": "^0.16.1"
+ }
+ },
+ "@jimp/gif": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.16.1.tgz",
+ "integrity": "sha512-r/1+GzIW1D5zrP4tNrfW+3y4vqD935WBXSc8X/wm23QTY9aJO9Lw6PEdzpYCEY+SOklIFKaJYUAq/Nvgm/9ryw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1",
+ "gifwrap": "^0.9.2",
+ "omggif": "^1.0.9"
+ }
+ },
+ "@jimp/jpeg": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.16.1.tgz",
+ "integrity": "sha512-8352zrdlCCLFdZ/J+JjBslDvml+fS3Z8gttdml0We759PnnZGqrnPRhkOEOJbNUlE+dD4ckLeIe6NPxlS/7U+w==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1",
+ "jpeg-js": "0.4.2"
+ }
+ },
+ "@jimp/plugin-blit": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.16.1.tgz",
+ "integrity": "sha512-fKFNARm32RoLSokJ8WZXHHH2CGzz6ire2n1Jh6u+XQLhk9TweT1DcLHIXwQMh8oR12KgjbgsMGvrMVlVknmOAg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-blur": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.16.1.tgz",
+ "integrity": "sha512-1WhuLGGj9MypFKRcPvmW45ht7nXkOKu+lg3n2VBzIB7r4kKNVchuI59bXaCYQumOLEqVK7JdB4glaDAbCQCLyw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-circle": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.16.1.tgz",
+ "integrity": "sha512-JK7yi1CIU7/XL8hdahjcbGA3V7c+F+Iw+mhMQhLEi7Q0tCnZ69YJBTamMiNg3fWPVfMuvWJJKOBRVpwNTuaZRg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-color": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.16.1.tgz",
+ "integrity": "sha512-9yQttBAO5SEFj7S6nJK54f+1BnuBG4c28q+iyzm1JjtnehjqMg6Ljw4gCSDCvoCQ3jBSYHN66pmwTV74SU1B7A==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1",
+ "tinycolor2": "^1.4.1"
+ }
+ },
+ "@jimp/plugin-contain": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.16.1.tgz",
+ "integrity": "sha512-44F3dUIjBDHN+Ym/vEfg+jtjMjAqd2uw9nssN67/n4FdpuZUVs7E7wadKY1RRNuJO+WgcD5aDQcsvurXMETQTg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-cover": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.16.1.tgz",
+ "integrity": "sha512-YztWCIldBAVo0zxcQXR+a/uk3/TtYnpKU2CanOPJ7baIuDlWPsG+YE4xTsswZZc12H9Kl7CiziEbDtvF9kwA/Q==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-crop": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.16.1.tgz",
+ "integrity": "sha512-UQdva9oQzCVadkyo3T5Tv2CUZbf0klm2cD4cWMlASuTOYgaGaFHhT9st+kmfvXjKL8q3STkBu/zUPV6PbuV3ew==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-displace": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.16.1.tgz",
+ "integrity": "sha512-iVAWuz2+G6Heu8gVZksUz+4hQYpR4R0R/RtBzpWEl8ItBe7O6QjORAkhxzg+WdYLL2A/Yd4ekTpvK0/qW8hTVw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-dither": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.16.1.tgz",
+ "integrity": "sha512-tADKVd+HDC9EhJRUDwMvzBXPz4GLoU6s5P7xkVq46tskExYSptgj5713J5Thj3NMgH9Rsqu22jNg1H/7tr3V9Q==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-fisheye": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.16.1.tgz",
+ "integrity": "sha512-BWHnc5hVobviTyIRHhIy9VxI1ACf4CeSuCfURB6JZm87YuyvgQh5aX5UDKtOz/3haMHXBLP61ZBxlNpMD8CG4A==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-flip": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.16.1.tgz",
+ "integrity": "sha512-KdxTf0zErfZ8DyHkImDTnQBuHby+a5YFdoKI/G3GpBl3qxLBvC+PWkS2F/iN3H7wszP7/TKxTEvWL927pypT0w==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-gaussian": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.16.1.tgz",
+ "integrity": "sha512-u9n4wjskh3N1mSqketbL6tVcLU2S5TEaFPR40K6TDv4phPLZALi1Of7reUmYpVm8mBDHt1I6kGhuCJiWvzfGyg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-invert": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.16.1.tgz",
+ "integrity": "sha512-2DKuyVXANH8WDpW9NG+PYFbehzJfweZszFYyxcaewaPLN0GxvxVLOGOPP1NuUTcHkOdMFbE0nHDuB7f+sYF/2w==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-mask": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.16.1.tgz",
+ "integrity": "sha512-snfiqHlVuj4bSFS0v96vo2PpqCDMe4JB+O++sMo5jF5mvGcGL6AIeLo8cYqPNpdO6BZpBJ8MY5El0Veckhr39Q==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-normalize": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.16.1.tgz",
+ "integrity": "sha512-dOQfIOvGLKDKXPU8xXWzaUeB0nvkosHw6Xg1WhS1Z5Q0PazByhaxOQkSKgUryNN/H+X7UdbDvlyh/yHf3ITRaw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-print": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.16.1.tgz",
+ "integrity": "sha512-ceWgYN40jbN4cWRxixym+csyVymvrryuKBQ+zoIvN5iE6OyS+2d7Mn4zlNgumSczb9GGyZZESIgVcBDA1ezq0Q==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1",
+ "load-bmfont": "^1.4.0"
+ }
+ },
+ "@jimp/plugin-resize": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.16.1.tgz",
+ "integrity": "sha512-u4JBLdRI7dargC04p2Ha24kofQBk3vhaf0q8FwSYgnCRwxfvh2RxvhJZk9H7Q91JZp6wgjz/SjvEAYjGCEgAwQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-rotate": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.16.1.tgz",
+ "integrity": "sha512-ZUU415gDQ0VjYutmVgAYYxC9Og9ixu2jAGMCU54mSMfuIlmohYfwARQmI7h4QB84M76c9hVLdONWjuo+rip/zg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-scale": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.16.1.tgz",
+ "integrity": "sha512-jM2QlgThIDIc4rcyughD5O7sOYezxdafg/2Xtd1csfK3z6fba3asxDwthqPZAgitrLgiKBDp6XfzC07Y/CefUw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-shadow": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.16.1.tgz",
+ "integrity": "sha512-MeD2Is17oKzXLnsphAa1sDstTu6nxscugxAEk3ji0GV1FohCvpHBcec0nAq6/czg4WzqfDts+fcPfC79qWmqrA==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugin-threshold": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.16.1.tgz",
+ "integrity": "sha512-iGW8U/wiCSR0+6syrPioVGoSzQFt4Z91SsCRbgNKTAk7D+XQv6OI78jvvYg4o0c2FOlwGhqz147HZV5utoSLxA==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1"
+ }
+ },
+ "@jimp/plugins": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.16.1.tgz",
+ "integrity": "sha512-c+lCqa25b+4q6mJZSetlxhMoYuiltyS+ValLzdwK/47+aYsq+kcJNl+TuxIEKf59yr9+5rkbpsPkZHLF/V7FFA==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/plugin-blit": "^0.16.1",
+ "@jimp/plugin-blur": "^0.16.1",
+ "@jimp/plugin-circle": "^0.16.1",
+ "@jimp/plugin-color": "^0.16.1",
+ "@jimp/plugin-contain": "^0.16.1",
+ "@jimp/plugin-cover": "^0.16.1",
+ "@jimp/plugin-crop": "^0.16.1",
+ "@jimp/plugin-displace": "^0.16.1",
+ "@jimp/plugin-dither": "^0.16.1",
+ "@jimp/plugin-fisheye": "^0.16.1",
+ "@jimp/plugin-flip": "^0.16.1",
+ "@jimp/plugin-gaussian": "^0.16.1",
+ "@jimp/plugin-invert": "^0.16.1",
+ "@jimp/plugin-mask": "^0.16.1",
+ "@jimp/plugin-normalize": "^0.16.1",
+ "@jimp/plugin-print": "^0.16.1",
+ "@jimp/plugin-resize": "^0.16.1",
+ "@jimp/plugin-rotate": "^0.16.1",
+ "@jimp/plugin-scale": "^0.16.1",
+ "@jimp/plugin-shadow": "^0.16.1",
+ "@jimp/plugin-threshold": "^0.16.1",
+ "timm": "^1.6.1"
+ }
+ },
+ "@jimp/png": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.16.1.tgz",
+ "integrity": "sha512-iyWoCxEBTW0OUWWn6SveD4LePW89kO7ZOy5sCfYeDM/oTPLpR8iMIGvZpZUz1b8kvzFr27vPst4E5rJhGjwsdw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/utils": "^0.16.1",
+ "pngjs": "^3.3.3"
+ }
+ },
+ "@jimp/tiff": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.16.1.tgz",
+ "integrity": "sha512-3K3+xpJS79RmSkAvFMgqY5dhSB+/sxhwTFA9f4AVHUK0oKW+u6r52Z1L0tMXHnpbAdR9EJ+xaAl2D4x19XShkQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "utif": "^2.0.1"
+ }
+ },
+ "@jimp/types": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.16.1.tgz",
+ "integrity": "sha512-g1w/+NfWqiVW4CaXSJyD28JQqZtm2eyKMWPhBBDCJN9nLCN12/Az0WFF3JUAktzdsEC2KRN2AqB1a2oMZBNgSQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/bmp": "^0.16.1",
+ "@jimp/gif": "^0.16.1",
+ "@jimp/jpeg": "^0.16.1",
+ "@jimp/png": "^0.16.1",
+ "@jimp/tiff": "^0.16.1",
+ "timm": "^1.6.1"
+ }
+ },
+ "@jimp/utils": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.16.1.tgz",
+ "integrity": "sha512-8fULQjB0x4LzUSiSYG6ZtQl355sZjxbv8r9PPAuYHzS9sGiSHJQavNqK/nKnpDsVkU88/vRGcE7t3nMU0dEnVw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "regenerator-runtime": "^0.13.3"
+ }
+ },
+ "@sitespeed.io/chromedriver": {
+ "version": "98.0.4758-48",
+ "resolved": "https://registry.npmjs.org/@sitespeed.io/chromedriver/-/chromedriver-98.0.4758-48.tgz",
+ "integrity": "sha512-kTFFaJD0K2j59+XG4o6olv28I1gaZ19qPlIRQLP7dfhaVZQDvxtzKyVIUHlU0q4m69XnCliOcO14008ZlxSW+g==",
+ "dev": true,
+ "requires": {
+ "node-downloader-helper": "1.0.19",
+ "node-stream-zip": "1.15.0"
+ }
+ },
+ "@sitespeed.io/edgedriver": {
+ "version": "95.0.1020-30",
+ "resolved": "https://registry.npmjs.org/@sitespeed.io/edgedriver/-/edgedriver-95.0.1020-30.tgz",
+ "integrity": "sha512-5hXxNCtbX/SeG6nsyXg4QWIEKacxBJTO5T43rUXlTrUlecFfvHNhTVY5PE2bwpKcdPQ168Vp0S/+g55QJi9s/Q==",
+ "dev": true,
+ "requires": {
+ "node-downloader-helper": "1.0.18",
+ "node-stream-zip": "1.15.0"
+ },
+ "dependencies": {
+ "node-downloader-helper": {
+ "version": "1.0.18",
+ "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-1.0.18.tgz",
+ "integrity": "sha512-C7hxYz/yg4d8DFVC6c4fMIOI7jywbpQHOznkax/74F8NcC8wSOLO+UxNMcwds/5wEL8W+RPXT9C389w3bDOMxw==",
+ "dev": true
+ }
+ }
+ },
+ "@sitespeed.io/geckodriver": {
+ "version": "0.29.1-3",
+ "resolved": "https://registry.npmjs.org/@sitespeed.io/geckodriver/-/geckodriver-0.29.1-3.tgz",
+ "integrity": "sha512-qHYtvH/81lPcgzFQB2qObp9M8bMIrc7O8TWm05SVfiGUKKy4Kku0huoa/IB9e0ksrrRFYtm9GQT6JF+bANZPKA==",
+ "dev": true,
+ "requires": {
+ "node-downloader-helper": "1.0.18",
+ "node-stream-zip": "1.14.0",
+ "tar": "6.1.11"
+ },
+ "dependencies": {
+ "node-downloader-helper": {
+ "version": "1.0.18",
+ "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-1.0.18.tgz",
+ "integrity": "sha512-C7hxYz/yg4d8DFVC6c4fMIOI7jywbpQHOznkax/74F8NcC8wSOLO+UxNMcwds/5wEL8W+RPXT9C389w3bDOMxw==",
+ "dev": true
+ },
+ "node-stream-zip": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.14.0.tgz",
+ "integrity": "sha512-SKXyiBy9DBemsPHf/piHT00Y+iPK+zwru1G6+8UdOBzITnmmPMHYBMV6M1znyzyhDhUFQW0HEmbGiPqtp51M6Q==",
+ "dev": true
+ }
+ }
+ },
+ "@sitespeed.io/throttle": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@sitespeed.io/throttle/-/throttle-3.0.0.tgz",
+ "integrity": "sha512-tTAnBaoMwtdECY6SYno/OSRnzZsazg63zesRNBxQXkpDG+1FU1FTXLJQx6/2SkKJo6WvrELp8XhoUIV9SQvlCg==",
+ "dev": true,
+ "requires": {
+ "minimist": "1.2.5"
+ }
+ },
+ "@sitespeed.io/tracium": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@sitespeed.io/tracium/-/tracium-0.3.3.tgz",
+ "integrity": "sha512-dNZafjM93Y+F+sfwTO5gTpsGXlnc/0Q+c2+62ViqP3gkMWvHEMSKkaEHgVJLcLg3i/g19GSIPziiKpgyne07Bw==",
+ "dev": true,
+ "requires": {
+ "debug": "^4.1.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
+ "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ }
+ }
+ },
+ "@types/node": {
+ "version": "17.0.14",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.14.tgz",
+ "integrity": "sha512-SbjLmERksKOGzWzPNuW7fJM7fk3YXVTFiZWB/Hs99gwhk+/dnrQRPBQjPW9aO+fi1tAffi9PrwFvsmOKmDTyng==",
+ "dev": true
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true
+ },
+ "any-base": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
+ "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==",
+ "dev": true,
+ "optional": true
+ },
+ "async": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
+ "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "optional": true
+ },
+ "bluebird": {
+ "version": "2.9.34",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.34.tgz",
+ "integrity": "sha1-L3tOyAIWMoqf3evfacjUlC/v99g=",
+ "dev": true
+ },
+ "bmp-js": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
+ "integrity": "sha1-4Fpj95amwf8l9Hcex62twUjAcjM=",
+ "dev": true,
+ "optional": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "browsertime": {
+ "version": "https://github.com/sitespeedio/browsertime/tarball/eae18165d9d82b9a5ad38b0bd1507a2d86a70988",
+ "integrity": "sha512-UiQ2xHLHN9ISnVRfFXmWCncLn5+Huca3ykTBYPOmnLcOyx7U9+cfMwEYMioyTbgh1IdByZd2KZ1dKLs2CtoU/Q==",
+ "dev": true,
+ "requires": {
+ "@cypress/xvfb": "1.2.4",
+ "@devicefarmer/adbkit": "2.11.3",
+ "@sitespeed.io/chromedriver": "98.0.4758-48",
+ "@sitespeed.io/edgedriver": "95.0.1020-30",
+ "@sitespeed.io/geckodriver": "0.29.1-3",
+ "@sitespeed.io/throttle": "3.0.0",
+ "@sitespeed.io/tracium": "0.3.3",
+ "btoa": "1.2.1",
+ "chrome-har": "0.12.0",
+ "chrome-remote-interface": "0.31.0",
+ "dayjs": "1.10.7",
+ "execa": "5.1.1",
+ "fast-stats": "0.0.6",
+ "find-up": "5.0.0",
+ "get-port": "5.1.1",
+ "hasbin": "1.2.3",
+ "intel": "1.2.0",
+ "jimp": "0.16.1",
+ "lodash.get": "4.4.2",
+ "lodash.groupby": "4.6.0",
+ "lodash.isempty": "4.4.0",
+ "lodash.merge": "4.6.2",
+ "lodash.pick": "4.4.0",
+ "lodash.set": "4.3.2",
+ "selenium-webdriver": "4.1.0",
+ "speedline-core": "1.4.3",
+ "yargs": "17.2.1"
+ }
+ },
+ "btoa": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
+ "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
+ "dev": true
+ },
+ "buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "buffer-equal": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz",
+ "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=",
+ "dev": true,
+ "optional": true
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ }
+ },
+ "chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true
+ },
+ "chrome-har": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/chrome-har/-/chrome-har-0.12.0.tgz",
+ "integrity": "sha512-VRQOsN9omU6q5/8h6eU9tkHPV2VvOCAh1JL4Hpk8ZIyrTLFWdK0A7UOsKNplvr+9Ls/8Wr71G20cuX2OsRPbwA==",
+ "dev": true,
+ "requires": {
+ "dayjs": "1.8.31",
+ "debug": "4.1.1",
+ "tough-cookie": "4.0.0",
+ "uuid": "8.0.0"
+ },
+ "dependencies": {
+ "dayjs": {
+ "version": "1.8.31",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.31.tgz",
+ "integrity": "sha512-mPh1mslned+5PuIuiUfbw4CikHk6AEAf2Baxih+wP5fssv+wmlVhvgZ7mq+BhLt7Sr/Hc8leWDiwe6YnrpNt3g==",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
+ "chrome-remote-interface": {
+ "version": "0.31.0",
+ "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.31.0.tgz",
+ "integrity": "sha512-DrD4ZACKAFT3lVldKVDRlYrI9bmZSk7kYcf+OKwFpBM9fZyCPvVKb+yGnmXBkHv7/BEkW8ouu+EHRugAOJ3pPg==",
+ "dev": true,
+ "requires": {
+ "commander": "2.11.x",
+ "ws": "^7.2.0"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
+ "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
+ "dev": true
+ }
+ }
+ },
+ "cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "dayjs": {
+ "version": "1.10.7",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
+ "integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==",
+ "dev": true
+ },
+ "dbug": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/dbug/-/dbug-0.4.2.tgz",
+ "integrity": "sha1-MrSzEF6IYQQ6b5rHVdgOVC02WzE=",
+ "dev": true
+ },
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "dom-walk": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
+ "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==",
+ "dev": true,
+ "optional": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ }
+ },
+ "exif-parser": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz",
+ "integrity": "sha1-WKnS1ywCwfbwKg70qRZicrd2CSI=",
+ "dev": true,
+ "optional": true
+ },
+ "fast-stats": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/fast-stats/-/fast-stats-0.0.6.tgz",
+ "integrity": "sha512-m0zkwa7Z07Wc4xm1YtcrCHmhzNxiYRrrfUyhkdhSZPzaAH/Ewbocdaq7EPVBFz19GWfIyyPcLfRHjHJYe83jlg==",
+ "dev": true
+ },
+ "file-type": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz",
+ "integrity": "sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw==",
+ "dev": true,
+ "optional": true
+ },
+ "find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "requires": {
+ "minipass": "^3.0.0"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true
+ },
+ "get-port": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
+ "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
+ "dev": true
+ },
+ "get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true
+ },
+ "gifwrap": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.2.tgz",
+ "integrity": "sha512-fcIswrPaiCDAyO8xnWvHSZdWChjKXUanKKpAiWWJ/UTkEi/aYKn5+90e7DE820zbEaVR9CE2y4z9bzhQijZ0BA==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "image-q": "^1.1.1",
+ "omggif": "^1.0.10"
+ }
+ },
+ "glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "global": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
+ "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "min-document": "^2.19.0",
+ "process": "^0.11.10"
+ }
+ },
+ "has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "hasbin": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/hasbin/-/hasbin-1.2.3.tgz",
+ "integrity": "sha1-eMWSaJPIAhXCtWiuH9P8q3omlrA=",
+ "dev": true,
+ "requires": {
+ "async": "~1.5"
+ },
+ "dependencies": {
+ "async": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
+ "dev": true
+ }
+ }
+ },
+ "human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true
+ },
+ "ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "optional": true
+ },
+ "image-q": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/image-q/-/image-q-1.1.1.tgz",
+ "integrity": "sha1-/IQJlmRGC5DKhi2TALa/u7+/gFY=",
+ "dev": true,
+ "optional": true
+ },
+ "image-ssim": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz",
+ "integrity": "sha1-g7Qsei5uS4VQVHf+aRf128VkIOU=",
+ "dev": true
+ },
+ "immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "intel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/intel/-/intel-1.2.0.tgz",
+ "integrity": "sha1-EdEUfraz9Fgr31M3s31UFYTp5B4=",
+ "dev": true,
+ "requires": {
+ "chalk": "^1.1.0",
+ "dbug": "~0.4.2",
+ "stack-trace": "~0.0.9",
+ "strftime": "~0.10.0",
+ "symbol": "~0.3.1",
+ "utcstring": "~0.1.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "is-function": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
+ "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
+ "dev": true,
+ "optional": true
+ },
+ "is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "jimp": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.16.1.tgz",
+ "integrity": "sha512-+EKVxbR36Td7Hfd23wKGIeEyHbxShZDX6L8uJkgVW3ESA9GiTEPK08tG1XI2r/0w5Ch0HyJF5kPqF9K7EmGjaw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "@jimp/custom": "^0.16.1",
+ "@jimp/plugins": "^0.16.1",
+ "@jimp/types": "^0.16.1",
+ "regenerator-runtime": "^0.13.3"
+ }
+ },
+ "jpeg-js": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.2.tgz",
+ "integrity": "sha512-+az2gi/hvex7eLTMTlbRLOhH6P6WFdk2ITI8HJsaH2VqYO0I594zXSYEP+tf4FW+8Cy68ScDXoAsQdyQanv3sw==",
+ "dev": true
+ },
+ "jszip": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
+ "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==",
+ "dev": true,
+ "requires": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "set-immediate-shim": "~1.0.1"
+ }
+ },
+ "lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "dev": true,
+ "requires": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "load-bmfont": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.1.tgz",
+ "integrity": "sha512-8UyQoYmdRDy81Brz6aLAUhfZLwr5zV0L3taTQ4hju7m6biuwiWiJXjPhBJxbUQJA8PrkvJ/7Enqmwk2sM14soA==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "buffer-equal": "0.0.1",
+ "mime": "^1.3.4",
+ "parse-bmfont-ascii": "^1.0.3",
+ "parse-bmfont-binary": "^1.0.5",
+ "parse-bmfont-xml": "^1.1.4",
+ "phin": "^2.9.1",
+ "xhr": "^2.0.1",
+ "xtend": "^4.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^5.0.0"
+ }
+ },
+ "lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
+ "dev": true
+ },
+ "lodash.groupby": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz",
+ "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=",
+ "dev": true
+ },
+ "lodash.isempty": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
+ "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=",
+ "dev": true
+ },
+ "lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=",
+ "dev": true
+ },
+ "lodash.pick": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
+ "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=",
+ "dev": true
+ },
+ "lodash.set": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
+ "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
+ "dev": true
+ },
+ "merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "optional": true
+ },
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true
+ },
+ "min-document": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
+ "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "dom-walk": "^0.1.0"
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "dev": true
+ },
+ "minipass": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz",
+ "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dev": true,
+ "requires": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ }
+ },
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node-downloader-helper": {
+ "version": "1.0.19",
+ "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-1.0.19.tgz",
+ "integrity": "sha512-Bwp8WWDDP5ftg+FmAKU08a9+oiUTPoYzMvXgUqZZPQ7VMo1qKBzW3XdTXHeYnqjGLfkTZ2GPibgAWpApfpeS2g==",
+ "dev": true
+ },
+ "node-forge": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
+ "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
+ "dev": true
+ },
+ "node-stream-zip": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz",
+ "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==",
+ "dev": true
+ },
+ "npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.0.0"
+ }
+ },
+ "omggif": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
+ "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
+ "dev": true,
+ "optional": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^2.1.0"
+ }
+ },
+ "p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "requires": {
+ "yocto-queue": "^0.1.0"
+ }
+ },
+ "p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^3.0.2"
+ }
+ },
+ "pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "dev": true
+ },
+ "parse-bmfont-ascii": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz",
+ "integrity": "sha1-Eaw8P/WPfCAgqyJ2kHkQjU36AoU=",
+ "dev": true,
+ "optional": true
+ },
+ "parse-bmfont-binary": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz",
+ "integrity": "sha1-0Di0dtPp3Z2x4RoLDlOiJ5K2kAY=",
+ "dev": true,
+ "optional": true
+ },
+ "parse-bmfont-xml": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.4.tgz",
+ "integrity": "sha512-bjnliEOmGv3y1aMEfREMBJ9tfL3WR0i0CKPj61DnSLaoxWR3nLrsQrEbCId/8rF4NyRF0cCqisSVXyQYWM+mCQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "xml-parse-from-string": "^1.0.0",
+ "xml2js": "^0.4.5"
+ }
+ },
+ "parse-headers": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.4.tgz",
+ "integrity": "sha512-psZ9iZoCNFLrgRjZ1d8mn0h9WRqJwFxM9q3x7iUjN/YT2OksthDJ5TiPCu2F38kS4zutqfW+YdVVkBZZx3/1aw==",
+ "dev": true,
+ "optional": true
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true
+ },
+ "phin": {
+ "version": "2.9.3",
+ "resolved": "https://registry.npmjs.org/phin/-/phin-2.9.3.tgz",
+ "integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==",
+ "dev": true,
+ "optional": true
+ },
+ "pixelmatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz",
+ "integrity": "sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "pngjs": "^3.0.0"
+ }
+ },
+ "pngjs": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
+ "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==",
+ "dev": true,
+ "optional": true
+ },
+ "process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+ "dev": true,
+ "optional": true
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
+ "psl": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
+ "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==",
+ "dev": true
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "dev": true
+ },
+ "readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "regenerator-runtime": {
+ "version": "0.13.9",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
+ "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
+ "dev": true,
+ "optional": true
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+ "dev": true,
+ "optional": true
+ },
+ "selenium-webdriver": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.1.0.tgz",
+ "integrity": "sha512-kUDH4N8WruYprTzvug4Pl73Th+WKb5YiLz8z/anOpHyUNUdM3UzrdTOxmSNaf9AczzBeY+qXihzku8D1lMaKOg==",
+ "dev": true,
+ "requires": {
+ "jszip": "^3.6.0",
+ "tmp": "^0.2.1",
+ "ws": ">=7.4.6"
+ }
+ },
+ "set-immediate-shim": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+ "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
+ "dev": true
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true
+ },
+ "signal-exit": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz",
+ "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==",
+ "dev": true
+ },
+ "speedline-core": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz",
+ "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "image-ssim": "^0.2.0",
+ "jpeg-js": "^0.4.1"
+ }
+ },
+ "split": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
+ "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=",
+ "dev": true,
+ "requires": {
+ "through": "2"
+ }
+ },
+ "stack-trace": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+ "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=",
+ "dev": true
+ },
+ "strftime": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/strftime/-/strftime-0.10.1.tgz",
+ "integrity": "sha512-nVvH6JG8KlXFPC0f8lojLgEsPA18lRpLZ+RrJh/NkQV2tqOgZfbas8gcU8SFgnnqR3rWzZPYu6N2A3xzs/8rQg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
+ },
+ "symbol": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/symbol/-/symbol-0.3.1.tgz",
+ "integrity": "sha1-tvmpANSWpX8CQI8iGYwQndoGMEE=",
+ "dev": true
+ },
+ "tar": {
+ "version": "6.1.11",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
+ "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
+ "dev": true,
+ "requires": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^3.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ }
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+ "dev": true
+ },
+ "timm": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz",
+ "integrity": "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==",
+ "dev": true,
+ "optional": true
+ },
+ "tinycolor2": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
+ "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==",
+ "dev": true,
+ "optional": true
+ },
+ "tmp": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+ "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
+ "dev": true,
+ "requires": {
+ "rimraf": "^3.0.0"
+ }
+ },
+ "tough-cookie": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
+ "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
+ "dev": true,
+ "requires": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.1.2"
+ }
+ },
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true
+ },
+ "utcstring": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/utcstring/-/utcstring-0.1.0.tgz",
+ "integrity": "sha1-Qw/VEKt/yVtdWRDJAteYgMIIQ2s=",
+ "dev": true
+ },
+ "utif": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/utif/-/utif-2.0.1.tgz",
+ "integrity": "sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "pako": "^1.0.5"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+ "dev": true
+ },
+ "uuid": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz",
+ "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==",
+ "dev": true
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "ws": {
+ "version": "7.5.6",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz",
+ "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==",
+ "dev": true
+ },
+ "xhr": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz",
+ "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "global": "~4.4.0",
+ "is-function": "^1.0.1",
+ "parse-headers": "^2.0.0",
+ "xtend": "^4.0.0"
+ }
+ },
+ "xml-parse-from-string": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz",
+ "integrity": "sha1-qQKekp09vN7RafPG4oI42VpdWig=",
+ "dev": true,
+ "optional": true
+ },
+ "xml2js": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
+ "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ }
+ },
+ "xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "dev": true,
+ "optional": true
+ },
+ "xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "dev": true,
+ "optional": true
+ },
+ "y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "yargs": {
+ "version": "17.2.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz",
+ "integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==",
+ "dev": true,
+ "requires": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ }
+ },
+ "yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true
+ },
+ "yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true
+ }
+ }
+}
diff --git a/python/mozperftest/mozperftest/test/browsertime/package.json b/python/mozperftest/mozperftest/test/browsertime/package.json
new file mode 100644
index 0000000000..493651df61
--- /dev/null
+++ b/python/mozperftest/mozperftest/test/browsertime/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "mozilla-central-tools-browsertime",
+ "description": "This package file is for node modules used in mozilla-central/tools/browsertime",
+ "repository": {},
+ "license": "MPL-2.0",
+ "dependencies": {},
+ "devDependencies": {
+ "browsertime": "https://github.com/sitespeedio/browsertime/tarball/eae18165d9d82b9a5ad38b0bd1507a2d86a70988"
+ },
+ "notes(private)": "We don't want to publish to npm, so this is marked as private",
+ "private": true
+}
diff --git a/python/mozperftest/mozperftest/test/browsertime/runner.py b/python/mozperftest/mozperftest/test/browsertime/runner.py
new file mode 100644
index 0000000000..54a9ace44a
--- /dev/null
+++ b/python/mozperftest/mozperftest/test/browsertime/runner.py
@@ -0,0 +1,473 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+import collections
+import json
+import os
+import pathlib
+import re
+import shutil
+import sys
+from pathlib import Path
+
+from mozperftest.test.browsertime.visualtools import get_dependencies, xvfb
+from mozperftest.test.noderunner import NodeRunner
+from mozperftest.utils import ON_TRY, get_output_dir, install_package
+
+BROWSERTIME_SRC_ROOT = Path(__file__).parent
+
+
+def matches(args, *flags):
+ """Returns True if any argument matches any of the given flags
+
+ Maybe with an argument.
+ """
+
+ for flag in flags:
+ if flag in args or any(arg.startswith(flag + "=") for arg in args):
+ return True
+ return False
+
+
+def extract_browser_name(args):
+ "Extracts the browser name if any"
+ # These are BT arguments, it's BT job to check them
+ # here we just want to extract the browser name
+ res = re.findall(r"(--browser|-b)[= ]([\w]+)", " ".join(args))
+ if res == []:
+ return None
+ return res[0][-1]
+
+
+class NodeException(Exception):
+ pass
+
+
+class BrowsertimeRunner(NodeRunner):
+ """Runs a browsertime test."""
+
+ name = "browsertime"
+ activated = True
+ user_exception = True
+
+ arguments = {
+ "cycles": {"type": int, "default": 1, "help": "Number of full cycles"},
+ "iterations": {"type": int, "default": 1, "help": "Number of iterations"},
+ "node": {"type": str, "default": None, "help": "Path to Node.js"},
+ "geckodriver": {"type": str, "default": None, "help": "Path to geckodriver"},
+ "binary": {
+ "type": str,
+ "default": None,
+ "help": "Path to the desktop browser, or Android app name.",
+ },
+ "clobber": {
+ "action": "store_true",
+ "default": False,
+ "help": "Force-update the installation.",
+ },
+ "install-url": {
+ "type": str,
+ "default": None,
+ "help": "Use this URL as the install url.",
+ },
+ "extra-options": {
+ "type": str,
+ "default": "",
+ "help": "Extra options passed to browsertime.js",
+ },
+ "xvfb": {"action": "store_true", "default": False, "help": "Use xvfb"},
+ "no-window-recorder": {
+ "action": "store_true",
+ "default": False,
+ "help": "Use the window recorder",
+ },
+ "viewport-size": {"type": str, "default": "1280x1024", "help": "Viewport size"},
+ "existing-results": {
+ "type": str,
+ "default": None,
+ "help": "Directory containing existing results to load.",
+ },
+ }
+
+ def __init__(self, env, mach_cmd):
+ super(BrowsertimeRunner, self).__init__(env, mach_cmd)
+ self.topsrcdir = mach_cmd.topsrcdir
+ self._mach_context = mach_cmd._mach_context
+ self.virtualenv_manager = mach_cmd.virtualenv_manager
+ self._created_dirs = []
+ self._test_script = None
+ self._setup_helper = None
+ self.get_binary_path = mach_cmd.get_binary_path
+
+ @property
+ def setup_helper(self):
+ if self._setup_helper is not None:
+ return self._setup_helper
+ sys.path.append(str(Path(self.topsrcdir, "tools", "lint", "eslint")))
+ import setup_helper
+
+ self._setup_helper = setup_helper
+ return self._setup_helper
+
+ @property
+ def artifact_cache_path(self):
+ """Downloaded artifacts will be kept here."""
+ # The convention is $MOZBUILD_STATE_PATH/cache/$FEATURE.
+ return Path(self._mach_context.state_dir, "cache", "browsertime")
+
+ @property
+ def state_path(self):
+ """Unpacked artifacts will be kept here."""
+ # The convention is $MOZBUILD_STATE_PATH/$FEATURE.
+ res = Path(self._mach_context.state_dir, "browsertime")
+ os.makedirs(str(res), exist_ok=True)
+ return res
+
+ @property
+ def browsertime_js(self):
+ root = os.environ.get("BROWSERTIME", self.state_path)
+ path = Path(root, "node_modules", "browsertime", "bin", "browsertime.js")
+ if path.exists():
+ os.environ["BROWSERTIME_JS"] = str(path)
+ return path
+
+ @property
+ def visualmetrics_py(self):
+ root = os.environ.get("BROWSERTIME", self.state_path)
+ path = Path(
+ root, "node_modules", "browsertime", "browsertime", "visualmetrics.py"
+ )
+ if path.exists():
+ os.environ["VISUALMETRICS_PY"] = str(path)
+ return path
+
+ def _get_browsertime_package(self):
+ with Path(
+ os.environ.get("BROWSERTIME", self.state_path),
+ "node_modules",
+ "browsertime",
+ "package.json",
+ ).open() as package:
+
+ return json.load(package)
+
+ def _get_browsertime_resolved(self):
+ try:
+ with Path(
+ os.environ.get("BROWSERTIME", self.state_path),
+ "node_modules",
+ ".package-lock.json",
+ ).open() as package_lock:
+ return json.load(package_lock)["packages"]["node_modules/browsertime"][
+ "resolved"
+ ]
+
+ except FileNotFoundError:
+ # Older versions of node/npm add this metadata to package.json
+ return self._get_browsertime_package().get("_from")
+
+ def _should_install(self):
+ # If browsertime doesn't exist, install it
+ if not self.visualmetrics_py.exists() or not self.browsertime_js.exists():
+ return True
+
+ # Browsertime exists, check if it's outdated
+ with Path(BROWSERTIME_SRC_ROOT, "package.json").open() as new:
+ new_pkg = json.load(new)
+
+ return not self._get_browsertime_resolved().endswith(
+ new_pkg["devDependencies"]["browsertime"]
+ )
+
+ def setup(self):
+ """Install browsertime and visualmetrics.py prerequisites and the Node.js package."""
+
+ node = self.get_arg("node")
+ if node is not None:
+ os.environ["NODEJS"] = node
+
+ super(BrowsertimeRunner, self).setup()
+ install_url = self.get_arg("install-url")
+
+ # installing Python deps on the fly
+ visualmetrics = self.get_arg("visualmetrics", False)
+
+ if visualmetrics:
+ # installing Python deps on the fly
+ for dep in get_dependencies():
+ install_package(self.virtualenv_manager, dep, ignore_failure=True)
+
+ # check if the browsertime package has been deployed correctly
+ # for this we just check for the browsertime directory presence
+ # we also make sure the visual metrics module is there *if*
+ # we need it
+ if not self._should_install() and not self.get_arg("clobber"):
+ return
+
+ # preparing ~/.mozbuild/browsertime
+ for file in ("package.json", "package-lock.json"):
+ src = BROWSERTIME_SRC_ROOT / file
+ target = self.state_path / file
+ # Overwrite the existing files
+ shutil.copyfile(str(src), str(target))
+
+ package_json_path = self.state_path / "package.json"
+
+ if install_url is not None:
+ self.info(
+ "Updating browsertime node module version in {package_json_path} "
+ "to {install_url}",
+ install_url=install_url,
+ package_json_path=str(package_json_path),
+ )
+
+ expr = r"/tarball/[a-f0-9]{40}$"
+ if not re.search(expr, install_url):
+ raise ValueError(
+ "New upstream URL does not end with {}: '{}'".format(
+ expr[:-1], install_url
+ )
+ )
+
+ with package_json_path.open() as f:
+ existing_body = json.loads(
+ f.read(), object_pairs_hook=collections.OrderedDict
+ )
+
+ existing_body["devDependencies"]["browsertime"] = install_url
+ updated_body = json.dumps(existing_body)
+ with package_json_path.open("w") as f:
+ f.write(updated_body)
+
+ self._setup_node_packages(package_json_path)
+
+ def _setup_node_packages(self, package_json_path):
+ # Install the browsertime Node.js requirements.
+ if not self.setup_helper.check_node_executables_valid():
+ return
+
+ should_clobber = self.get_arg("clobber")
+ # To use a custom `geckodriver`, set
+ # os.environ[b"GECKODRIVER_BASE_URL"] = bytes(url)
+ # to an endpoint with binaries named like
+ # https://github.com/sitespeedio/geckodriver/blob/master/install.js#L31.
+
+ if ON_TRY:
+ os.environ["CHROMEDRIVER_SKIP_DOWNLOAD"] = "true"
+ os.environ["GECKODRIVER_SKIP_DOWNLOAD"] = "true"
+
+ self.info(
+ "Installing browsertime node module from {package_json}",
+ package_json=str(package_json_path),
+ )
+ install_url = self.get_arg("install-url")
+
+ self.setup_helper.package_setup(
+ str(self.state_path),
+ "browsertime",
+ should_update=install_url is not None,
+ should_clobber=should_clobber,
+ no_optional=install_url or ON_TRY,
+ )
+
+ def extra_default_args(self, args=[]):
+ # Add Mozilla-specific default arguments. This is tricky because browsertime is quite
+ # loose about arguments; repeat arguments are generally accepted but then produce
+ # difficult to interpret type errors.
+ extra_args = []
+
+ # Default to Firefox. Override with `-b ...` or `--browser=...`.
+ if not matches(args, "-b", "--browser"):
+ extra_args.extend(("-b", "firefox"))
+
+ # Default to not collect HAR. Override with `--skipHar=false`.
+ if not matches(args, "--har", "--skipHar", "--gzipHar"):
+ extra_args.append("--skipHar")
+
+ extra_args.extend(["--viewPort", self.get_arg("viewport-size")])
+
+ if not matches(args, "--android"):
+ binary = self.get_arg("binary")
+ if binary is not None:
+ extra_args.extend(("--firefox.binaryPath", binary))
+ else:
+ # If --firefox.binaryPath is not specified, default to the objdir binary
+ # Note: --firefox.release is not a real browsertime option, but it will
+ # silently ignore it instead and default to a release installation.
+ if (
+ not matches(
+ args,
+ "--firefox.binaryPath",
+ "--firefox.release",
+ "--firefox.nightly",
+ "--firefox.beta",
+ "--firefox.developer",
+ )
+ and extract_browser_name(args) != "chrome"
+ ):
+ extra_args.extend(("--firefox.binaryPath", self.get_binary_path()))
+
+ geckodriver = self.get_arg("geckodriver")
+ if geckodriver is not None:
+ extra_args.extend(("--firefox.geckodriverPath", geckodriver))
+
+ if extra_args:
+ self.debug(
+ "Running browsertime with extra default arguments: {extra_args}",
+ extra_args=extra_args,
+ )
+
+ return extra_args
+
+ def _android_args(self, metadata):
+ app_name = self.get_arg("android-app-name")
+
+ args_list = [
+ "--android",
+ "--firefox.android.package",
+ app_name,
+ ]
+ activity = self.get_arg("android-activity")
+ if activity is not None:
+ args_list += ["--firefox.android.activity", activity]
+
+ return args_list
+
+ def _line_handler(self, line):
+ line_matcher = re.compile(r"(\[\d{4}-\d{2}-\d{2}.*\])\s+([a-zA-Z]+):\s+(.*)")
+ match = line_matcher.match(line)
+ if not match:
+ return
+
+ date, level, msg = match.groups()
+ msg = msg.replace("{", "{{").replace("}", "}}")
+ level = level.lower()
+ if "error" in level:
+ self.error("Mozperftest failed to run: {}".format(msg), msg)
+ elif "warning" in level:
+ self.warning(msg)
+ else:
+ self.info(msg)
+
+ def run(self, metadata):
+ self._test_script = metadata.script
+ self.setup()
+
+ existing = self.get_arg("browsertime-existing-results")
+ if existing:
+ metadata.add_result(
+ {"results": existing, "name": self._test_script["name"]}
+ )
+ return metadata
+
+ cycles = self.get_arg("cycles", 1)
+ for cycle in range(1, cycles + 1):
+
+ # Build an output directory
+ output = self.get_arg("output")
+ if output is None:
+ output = pathlib.Path(self.topsrcdir, "artifacts")
+ result_dir = get_output_dir(output, f"browsertime-results-{cycle}")
+
+ # Run the test cycle
+ metadata.run_hook(
+ "before_cycle", metadata, self.env, cycle, self._test_script
+ )
+ try:
+ metadata = self._one_cycle(metadata, result_dir)
+ finally:
+ metadata.run_hook(
+ "after_cycle", metadata, self.env, cycle, self._test_script
+ )
+ return metadata
+
+ def _one_cycle(self, metadata, result_dir):
+ profile = self.get_arg("profile-directory")
+ is_login_site = False
+
+ args = [
+ "--resultDir",
+ str(result_dir),
+ "--firefox.profileTemplate",
+ profile,
+ "--iterations",
+ str(self.get_arg("iterations")),
+ self._test_script["filename"],
+ ]
+
+ # Set *all* prefs found in browser_prefs because
+ # browsertime will override the ones found in firefox.profileTemplate
+ # with its own defaults at `firefoxPreferences.js`
+ # Using `--firefox.preference` ensures we override them.
+ # see https://github.com/sitespeedio/browsertime/issues/1427
+ browser_prefs = metadata.get_options("browser_prefs")
+ for key, value in browser_prefs.items():
+ args += ["--firefox.preference", f"{key}:{value}"]
+
+ if self.get_arg("verbose"):
+ args += ["-vvv"]
+
+ # if the visualmetrics layer is activated, we want to feed it
+ visualmetrics = self.get_arg("visualmetrics", False)
+ if visualmetrics:
+ args += ["--video", "true"]
+ if not self.get_arg("no-window-recorder"):
+ args += ["--firefox.windowRecorder", "true"]
+
+ extra_options = self.get_arg("extra-options")
+ if extra_options:
+ for option in extra_options.split(","):
+ option = option.strip()
+ if not option:
+ continue
+ option = option.split("=", 1)
+ if len(option) != 2:
+ self.warning(
+ f"Skipping browsertime option {option} as it "
+ "is missing a name/value pairing. We expect options "
+ "to be formatted as: --browsertime-extra-options "
+ "'browserRestartTries=1,timeouts.browserStart=10'"
+ )
+ continue
+ name, value = option
+
+ # Check if we have a login site
+ if name == "browsertime.login" and value:
+ is_login_site = True
+
+ self.info(f"Adding extra browsertime argument: --{name} {value}")
+ args += ["--" + name, value]
+
+ if self.get_arg("android"):
+ args.extend(self._android_args(metadata))
+
+ # Remove any possible verbose option if we are on Try and using logins
+ if is_login_site and ON_TRY:
+ self.info("Turning off verbose mode for login-logic")
+ self.info(
+ "Please contact the perftest team if you need verbose mode enabled."
+ )
+ for verbose_level in ("-v", "-vv", "-vvv", "-vvvv"):
+ try:
+ args.remove(verbose_level)
+ except ValueError:
+ pass
+
+ extra = self.extra_default_args(args=args)
+ command = [str(self.browsertime_js)] + extra + args
+ self.info("Running browsertime with this command %s" % " ".join(command))
+
+ if visualmetrics and self.get_arg("xvfb"):
+ with xvfb():
+ exit_code = self.node(command, self._line_handler)
+ else:
+ exit_code = self.node(command, self._line_handler)
+
+ if exit_code != 0:
+ raise NodeException(exit_code)
+
+ metadata.add_result(
+ {"results": str(result_dir), "name": self._test_script["name"]}
+ )
+
+ return metadata
diff --git a/python/mozperftest/mozperftest/test/browsertime/visualtools.py b/python/mozperftest/mozperftest/test/browsertime/visualtools.py
new file mode 100644
index 0000000000..d25cb131d3
--- /dev/null
+++ b/python/mozperftest/mozperftest/test/browsertime/visualtools.py
@@ -0,0 +1,196 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+""" Collects visualmetrics dependencies.
+"""
+import contextlib
+import os
+import subprocess
+import sys
+import time
+from distutils.spawn import find_executable
+
+from mozperftest.utils import host_platform
+
+_PILLOW_VERSION = "7.2.0"
+_PYSSIM_VERSION = "0.4"
+
+
+def _start_xvfb():
+ old_display = os.environ.get("DISPLAY")
+ xvfb = find_executable("Xvfb")
+ if xvfb is None:
+ raise FileNotFoundError("Xvfb")
+ cmd = [xvfb, ":99"]
+ proc = subprocess.Popen(
+ cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, close_fds=True
+ )
+ os.environ["DISPLAY"] = ":99"
+ time.sleep(0.2)
+ return proc, old_display
+
+
+def _stop_xvfb(proc, old_display):
+ proc, old_display
+ if old_display is None:
+ del os.environ["DISPLAY"]
+ else:
+ os.environ["DISPLAY"] = old_display
+ if proc is not None:
+ try:
+ proc.terminate()
+ proc.wait()
+ except OSError:
+ pass
+
+
+@contextlib.contextmanager
+def xvfb():
+ proc, old_display = _start_xvfb()
+ try:
+ yield
+ finally:
+ _stop_xvfb(proc, old_display)
+
+
+def get_plat():
+ return host_platform(), f"{sys.version_info.major}.{sys.version_info.minor}"
+
+
+NUMPY = {
+ ("linux64", "3.10",): (
+ "88/cc/92815174c345015a326e3fff8beddcb951b3ef0f7c8296fcc22c622add7c"
+ "/numpy-1.23.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+ ),
+ ("linux64", "3.9",): (
+ "8d/d6/cc2330e512936a904a4db1629b71d697fb309115f6d2ede94d183cdfe185"
+ "/numpy-1.23.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+ ),
+ ("linux64", "3.8",): (
+ "86/c9/9f9d6812fa8a031a568c2c1c49f207a0a4030ead438644c887410fc49c8a"
+ "/numpy-1.23.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+ ),
+ ("linux64", "3.7",): (
+ "d6/2e/a2dbcff6f46bb65645d18538d67183a1cf56b006ba96a12575c282a976bc/"
+ "numpy-1.19.2-cp37-cp37m-manylinux1_x86_64.whl"
+ ),
+ ("linux64", "3.6",): (
+ "b8/e5/a64ef44a85397ba3c377f6be9c02f3cb3e18023f8c89850dd319e7945521/"
+ "numpy-1.19.2-cp36-cp36m-manylinux1_x86_64.whl"
+ ),
+ ("darwin", "3.10",): (
+ "c0/c2/8d58f3ccd1aa3b1eaa5c333a6748e225b45cf8748b13f052cbb3c811c996"
+ "/numpy-1.23.1-cp310-cp310-macosx_10_9_x86_64.whl"
+ ),
+ ("darwin", "3.9",): (
+ "e5/43/b1b80cbcea9f2d0e6adadd27a8da2c71b751d5670a846b444087fab408a1"
+ "/numpy-1.23.1-cp39-cp39-macosx_10_9_x86_64.whl"
+ ),
+ ("darwin", "3.8",): (
+ "71/08/bc1e4fb7392aa0721f299c444e8c99fa97c8cb41fe33791eca8e26364639"
+ "/numpy-1.23.1-cp38-cp38-macosx_10_9_x86_64.whl"
+ ),
+ ("darwin", "3.7",): (
+ "c1/a9/f04a5b7db30cc30b41fe516b8914c5049264490a34a49d977937606fbb23/"
+ "numpy-1.19.2-cp37-cp37m-macosx_10_9_x86_64.whl"
+ ),
+ ("darwin", "3.6",): (
+ "be/8e/800113bd3a0c9195b24574b8922ad92be96278028833c389b69a8b14f657/"
+ "numpy-1.19.2-cp36-cp36m-macosx_10_9_x86_64.whl"
+ ),
+ ("win64", "3.10",): (
+ "8b/11/75a93826457f94a4c857a38ea3f178915f27ff38ffee1753e36994be7810"
+ "/numpy-1.23.1-cp310-cp310-win_amd64.whl"
+ ),
+ ("win64", "3.9",): (
+ "bd/dd/0610fb49c433fe5987ae312fe672119080fd77be484b5698d6fa7554148b"
+ "/numpy-1.23.1-cp39-cp39-win_amd64.whl"
+ ),
+ ("win64", "3.8",): (
+ "d0/19/6e81ed6fe30271ebcf25e5e2a0bdf1fa06ddee03a8cb82625503826970db"
+ "/numpy-1.23.1-cp38-cp38-win_amd64.whl"
+ ),
+ ("win64", "3.7",): (
+ "82/4e/61764556b7ec13f5bd441b04530e2f9f11bb164308ef0e6951919bb846cb/"
+ "numpy-1.19.2-cp37-cp37m-win_amd64.whl"
+ ),
+ ("win64", "3.6",): (
+ "dc/8e/a78d4e4a28adadbf693a9c056a0d5955a906889fa0dc3768b88deb236e22/"
+ "numpy-1.19.2-cp36-cp36m-win_amd64.whl"
+ ),
+}
+
+
+SCIPY = {
+ ("linux64", "3.10",): (
+ "bc/fe/72b611ba221c3367b06163992af4807515d6e0e09b3b9beee8ec22162d6f"
+ "/scipy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+ ),
+ ("linux64", "3.9",): (
+ "25/82/da07cc3bb40554f1f82d7e24bfa7ffbfb05b50c16eb8d738ebb74b68af8f"
+ "/scipy-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+ ),
+ ("linux64", "3.8",): (
+ "cf/28/5ac0afe5fb473a934ef6bc7953a98a3d2eacf9a8f456524f035f3a844ca4"
+ "/scipy-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+ ),
+ ("linux64", "3.7",): (
+ "65/f9/f7a7e5009711579c72da2725174825e5056741bf4001815d097eef1b2e17"
+ "/scipy-1.5.2-cp37-cp37m-manylinux1_x86_64.whl"
+ ),
+ ("linux64", "3.6",): (
+ "2b/a8/f4c66eb529bb252d50e83dbf2909c6502e2f857550f22571ed8556f62d95"
+ "/scipy-1.5.2-cp36-cp36m-manylinux1_x86_64.whl"
+ ),
+ ("darwin", "3.10",): (
+ "7c/f3/47b882f8b7a4dbc38e8bc5d7befe3ad2da582ae2229745e1eac77217f3e4"
+ "/scipy-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl"
+ ),
+ ("darwin", "3.9",): (
+ "b0/de/e8d273063e1b21ec82e4a09a9654c4dcbc3215abbd59b7038c4ff4272e9e"
+ "/scipy-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl"
+ ),
+ ("darwin", "3.8",): (
+ "dd/cc/bb5a9705dd30e7f558358168c793084f80de7cca88b06c82dca9d765b225"
+ "/scipy-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl"
+ ),
+ ("darwin", "3.7",): (
+ "bc/47/e71e7f198a0b547fe861520a0240e3171256822dae81fcc97a36b772303e"
+ "/scipy-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl"
+ ),
+ ("darwin", "3.6",): (
+ "00/c0/ddf03baa7ee2a3540d8fbab0fecff7cdd0595dffd91cda746caa95cb686d"
+ "/scipy-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl"
+ ),
+ ("win64", "3.10"): (
+ "31/c2/0b8758ebaeb43e089eb56168390824a830f9f419ae07d755d99a46e5a937"
+ "/scipy-1.8.1-cp310-cp310-win_amd64.whl"
+ ),
+ ("win64", "3.9"): (
+ "ba/a1/a8fa291b8ae6523866dd099af377bc508c280c8ca43a42483c76775ce3cd"
+ "/scipy-1.8.1-cp39-cp39-win_amd64.whl"
+ ),
+ ("win64", "3.8"): (
+ "8d/3e/e6f6fa6458e03ecd456ae6178529d4bd610a7c4999189f34d0668e4e69a6"
+ "/scipy-1.8.1-cp38-cp38-win_amd64.whl"
+ ),
+ ("win64", "3.7",): (
+ "66/80/d8a5050df5b4d8229e018f3222fe603ce7f92c026b78f4e05d69c3a6c43b"
+ "/scipy-1.5.2-cp37-cp37m-win_amd64.whl"
+ ),
+ ("win64", "3.6",): (
+ "fc/f6/3d455f8b376a0faf1081dbba38bbd594c074292bdec08feaac589f53bc06"
+ "/scipy-1.5.2-cp36-cp36m-win_amd64.whl"
+ ),
+}
+
+
+def get_dependencies():
+ return (
+ "https://files.pythonhosted.org/packages/" + NUMPY[get_plat()],
+ "https://files.pythonhosted.org/packages/" + SCIPY[get_plat()],
+ "Pillow==%s" % _PILLOW_VERSION,
+ "pyssim==%s" % _PYSSIM_VERSION,
+ "influxdb==5.3.0",
+ "grafana_api==1.0.3",
+ )
diff --git a/python/mozperftest/mozperftest/test/noderunner.py b/python/mozperftest/mozperftest/test/noderunner.py
new file mode 100644
index 0000000000..4304609bff
--- /dev/null
+++ b/python/mozperftest/mozperftest/test/noderunner.py
@@ -0,0 +1,75 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+import os
+import sys
+
+import mozpack.path as mozpath
+
+from mozperftest.layers import Layer
+from mozperftest.utils import silence
+
+
+class NodeRunner(Layer):
+ name = "node"
+
+ def __init__(self, env, mach_cmd):
+ super(NodeRunner, self).__init__(env, mach_cmd)
+ self.topsrcdir = mach_cmd.topsrcdir
+ self._mach_context = mach_cmd._mach_context
+ self.python_path = mach_cmd.virtualenv_manager.python_path
+
+ from mozbuild.nodeutil import find_node_executable
+
+ self.node_path = os.path.abspath(find_node_executable()[0])
+
+ def setup(self):
+ """Install the Node.js package."""
+ self.verify_node_install()
+
+ def node(self, args, line_handler=None):
+ """Invoke node (interactively) with the given arguments."""
+ return self.run_process(
+ [self.node_path] + args,
+ append_env=self.append_env(),
+ pass_thru=False, # Allow user to run Node interactively.
+ ensure_exit_code=False, # Don't throw on non-zero exit code.
+ cwd=mozpath.join(self.topsrcdir),
+ line_handler=line_handler,
+ )
+
+ def append_env(self, append_path=True):
+ # Ensure that bare `node` and `npm` in scripts, including post-install
+ # scripts, finds the binary we're invoking with. Without this, it's
+ # easy for compiled extensions to get mismatched versions of the Node.js
+ # extension API.
+ path = os.environ.get("PATH", "").split(os.pathsep) if append_path else []
+ node_dir = os.path.dirname(self.node_path)
+ path = [node_dir] + path
+
+ return {
+ "PATH": os.pathsep.join(path),
+ # Bug 1560193: The JS library browsertime uses to execute commands
+ # (execa) will muck up the PATH variable and put the directory that
+ # node is in first in path. If this is globally-installed node,
+ # that means `/usr/bin` will be inserted first which means that we
+ # will get `/usr/bin/python` for `python`.
+ #
+ # Our fork of browsertime supports a `PYTHON` environment variable
+ # that points to the exact python executable to use.
+ "PYTHON": self.python_path,
+ }
+
+ def verify_node_install(self):
+ # check if Node is installed
+ sys.path.append(mozpath.join(self.topsrcdir, "tools", "lint", "eslint"))
+ import setup_helper
+
+ with silence():
+ node_valid = setup_helper.check_node_executables_valid()
+ if not node_valid:
+ # running again to get details printed out
+ setup_helper.check_node_executables_valid()
+ raise ValueError("Can't find Node. did you run ./mach bootstrap ?")
+
+ return True
diff --git a/python/mozperftest/mozperftest/test/webpagetest.py b/python/mozperftest/mozperftest/test/webpagetest.py
new file mode 100644
index 0000000000..2dc6698b8d
--- /dev/null
+++ b/python/mozperftest/mozperftest/test/webpagetest.py
@@ -0,0 +1,413 @@
+import json
+import pathlib
+import re
+import time
+import traceback
+from threading import Thread
+
+import requests
+
+import mozperftest.utils as utils
+from mozperftest.layers import Layer
+from mozperftest.runner import HERE
+
+ACCEPTED_BROWSERS = ["Chrome", "Firefox"]
+
+ACCEPTED_CONNECTIONS = [
+ "DSL",
+ "Cable",
+ "FIOS",
+ "Dial",
+ "Edge",
+ "2G",
+ "3GSlow",
+ "3GFast",
+ "3G",
+ "4G",
+ "LTE",
+ "Native",
+ "custom",
+]
+
+ACCEPTED_STATISTICS = ["average", "median", "standardDeviation"]
+WPT_KEY_FILE = "WPT_key.txt"
+WPT_API_EXPIRED_MESSAGE = "API key expired"
+
+
+class WPTTimeOutError(Exception):
+ """
+ This error is raised if a request that you have made has not returned results within a
+ specified time, for this code that timeout is ~6 hours.
+ """
+
+ pass
+
+
+class WPTBrowserSelectionError(Exception):
+ """
+ This error is raised if you provide an invalid browser option when requesting a test
+ The only browsers allowed are specified the ACCEPTED_BROWSERS list at the top of the code
+ browser must be a case-sensitive match in the list.
+ """
+
+ pass
+
+
+class WPTLocationSelectionError(Exception):
+ """
+ This error is raised if you provide an invalid testing location option when requesting a test
+ Acceptable locations are specified here: https://www.webpagetest.org/getTesters.php?f=html
+ Connection type must be a case-sensitive match
+ For example to test in Virginia, USA you would put ec2-us-east1 as your location.
+ """
+
+ pass
+
+
+class WPTInvalidConnectionSelection(Exception):
+ """
+ This error is raised if you provide an invalid connection option when requesting a test
+ The only connection allowed are specified the ACCEPTED_CONNECTIONS list at the top of the code
+ Connection type must be a case-sensitive match in the list.
+ """
+
+ pass
+
+
+class WPTDataProcessingError(Exception):
+ """
+ This error is raised when a value you were expecting in your webpagetest result is not there.
+ """
+
+ pass
+
+
+class WPTInvalidURLError(Exception):
+ """
+ This error is raised if you provide an invalid website url when requesting a test
+ A website must be in the format {domain_name}.{top_level_domain}
+ for example "google.ca" and "youtube.com" both work and are valid website urls, but
+ "google" and "youtube" are not.
+ """
+
+ pass
+
+
+class WPTErrorWithWebsite(Exception):
+ """
+ This error is raised if the first and repeat view results of the test you requested
+ is not in-line with what is returned. For instance if you request 3 runs with first
+ and repeat view and results show 3 first view and 2 repeat view tests this exception
+ is raised.
+ """
+
+ pass
+
+
+class WPTInvalidStatisticsError(Exception):
+ """
+ This error is raised if the first and repeat view results of the test you requested
+ is not in-line with what is returned. For instance if you request 3 runs with first
+ and repeat view and results show 3 first view and 2 repeat view tests this exception
+ is raised.
+ """
+
+ pass
+
+
+class WPTExpiredAPIKeyError(Exception):
+ """
+ This error is raised if we get a notification from WPT that our API key has expired
+ """
+
+ pass
+
+
+class PropagatingErrorThread(Thread):
+ def run(self):
+ self.exc = None
+ try:
+ self._target(*self._args, **self._kwargs)
+ except Exception as e:
+ self.exc = e
+
+ def join(self, timeout=None):
+ super(PropagatingErrorThread, self).join()
+ if self.exc:
+ raise self.exc
+
+
+class WebPageTestData:
+ def open_data(self, data):
+ return {
+ "name": "webpagetest",
+ "subtest": data["name"],
+ "data": [
+ {"file": "webpagetest", "value": value, "xaxis": xaxis}
+ for xaxis, value in enumerate(data["values"])
+ ],
+ "shouldAlert": True,
+ }
+
+ def transform(self, data):
+ return data
+
+ merge = transform
+
+
+class WebPageTest(Layer):
+ """
+ This is the webpagetest layer, it is responsible for sending requests to run a webpagetest
+ pageload test, receiving the results as well processing them into a useful data format.
+ """
+
+ name = "webpagetest"
+ activated = False
+ arguments = {
+ "no-video": {
+ "action": "store_true",
+ "default": False,
+ "help": "Disable video, required for calculating Speed Index and filmstrip view",
+ },
+ "no-images": {
+ "action": "store_true",
+ "default": False,
+ "help": "Set to True to disable screenshot capturing, False by default",
+ },
+ }
+
+ def __init__(self, env, mach_cmd):
+ super(WebPageTest, self).__init__(env, mach_cmd)
+ if utils.ON_TRY:
+ self.WPT_key = utils.get_tc_secret(wpt=True)["wpt_key"]
+ else:
+ self.WPT_key = pathlib.Path(HERE, WPT_KEY_FILE).open().read()
+ self.statistic_types = ["average", "median", "standardDeviation"]
+ self.timeout_limit = 21600
+ self.wait_between_requests = 180
+
+ def run(self, metadata):
+ options = metadata.script["options"]
+ test_list = options["test_list"]
+ self.statistic_types = options["test_parameters"]["statistics"]
+ self.wpt_browser_metrics = options["browser_metrics"]
+ self.pre_run_error_checks(options["test_parameters"], test_list)
+ self.create_and_run_wpt_threaded_tests(test_list, metadata)
+ try:
+ self.test_runs_left_this_month()
+ except Exception:
+ self.warning("testBalance check had an issue, please investigate")
+ return metadata
+
+ def pre_run_error_checks(self, options, test_list):
+ if options["browser"] not in ACCEPTED_BROWSERS:
+ raise WPTBrowserSelectionError(
+ "Invalid Browser Option Selected, please choose one of the following: "
+ f"{ACCEPTED_BROWSERS}"
+ )
+ if options["connection"] not in ACCEPTED_CONNECTIONS:
+ raise WPTInvalidConnectionSelection(
+ "Invalid Connection Option Selected, please choose one of the following: "
+ f"{ACCEPTED_CONNECTIONS}"
+ )
+ if not len(self.statistic_types):
+ raise WPTInvalidStatisticsError(
+ "No statistics provided please provide some"
+ )
+ for stat in self.statistic_types:
+ if stat not in ACCEPTED_STATISTICS:
+ raise WPTInvalidStatisticsError(
+ f"This is an invalid statistic, statistics can only be from "
+ f"the following list: {ACCEPTED_STATISTICS}"
+ )
+
+ if "timeout_limit" in options.keys():
+ self.timeout_limit = options["timeout_limit"]
+ if "wait_between_requests" in options.keys():
+ self.wait_between_requests = options["wait_between_requests"]
+ if "statistics" in options.keys():
+ self.statistic_types = options["statistics"]
+
+ options["capture_video"] = 0 if self.get_arg("no-video") else options["video"]
+ options["noimages"] = 1 if self.get_arg("no-images") else options["noimages"]
+ self.location_queue(options["location"])
+ self.check_urls_are_valid(test_list)
+
+ def location_queue(self, location):
+ location_list = {}
+ try:
+ location_list = self.request_with_timeout(
+ "https://www.webpagetest.org/getLocations.php?f=json"
+ )["data"]
+ except Exception:
+ self.error(
+ "Error with getting location queue data, see below for more details"
+ )
+ self.info(traceback.format_exc())
+ if location and location not in location_list.keys():
+ raise WPTLocationSelectionError(
+ "Invalid location selected please choose one of the locations here: "
+ f"{location_list.keys()}"
+ )
+ self.info(
+ f"Test queue at {location}({location_list[location]['Label']}) is "
+ f"{location_list[location]['PendingTests']['Queued']}"
+ )
+
+ def request_with_timeout(self, url):
+ requested_results = requests.get(url)
+ results_of_request = json.loads(requested_results.text)
+ start = time.monotonic()
+ if (
+ "statusText" in results_of_request.keys()
+ and results_of_request["statusText"] == WPT_API_EXPIRED_MESSAGE
+ ):
+ raise WPTExpiredAPIKeyError("The API key has expired")
+ while (
+ requested_results.status_code == 200
+ and time.monotonic() - start < self.timeout_limit
+ and (
+ "statusCode" in results_of_request.keys()
+ and results_of_request["statusCode"] != 200
+ )
+ ):
+ requested_results = requests.get(url)
+ results_of_request = json.loads(requested_results.text)
+ time.sleep(self.wait_between_requests)
+ if time.monotonic() - start > self.timeout_limit:
+ raise WPTTimeOutError(
+ f"{url} test timed out after {self.timeout_limit} seconds"
+ )
+ return results_of_request
+
+ def check_urls_are_valid(self, test_list):
+ for url in test_list:
+ if "." not in url:
+ raise WPTInvalidURLError(f"{url} is an invalid url")
+
+ def create_and_run_wpt_threaded_tests(self, test_list, metadata):
+ threads = []
+ for website in test_list:
+ t = PropagatingErrorThread(
+ target=self.create_and_run_wpt_tests, args=(website, metadata)
+ )
+ t.start()
+ threads.append(t)
+ for thread in threads:
+ thread.join()
+
+ def create_and_run_wpt_tests(self, website_to_be_tested, metadata):
+ wpt_run = self.get_WPT_results(
+ website_to_be_tested, metadata.script["options"]["test_parameters"]
+ )
+ self.post_run_error_checks(
+ wpt_run, metadata.script["options"], website_to_be_tested
+ )
+ self.add_wpt_run_to_metadata(wpt_run, metadata, website_to_be_tested)
+
+ def get_WPT_results(self, website, options):
+ self.info(f"Testing: {website}")
+ wpt_test_request_link = self.create_wpt_request_link(options, website)
+ send_wpt_test_request = self.request_with_timeout(wpt_test_request_link)[
+ "data"
+ ]["jsonUrl"]
+ results_of_test = self.request_with_timeout(send_wpt_test_request)
+ return results_of_test
+
+ def create_wpt_request_link(self, options, website_to_be_tested):
+ test_parameters = ""
+ for key_value_pair in list(options.items())[6:]:
+ test_parameters += "&{}={}".format(*key_value_pair)
+ return (
+ f"https://www.webpagetest.org/runtest.php?url={website_to_be_tested}&k={self.WPT_key}&"
+ f"location={options['location']}:{options['browser']}.{options['connection']}&"
+ f"f=json{test_parameters}"
+ )
+
+ def post_run_error_checks(self, results_of_test, options, url):
+ self.info(f"{url} test can be found here: {results_of_test['data']['summary']}")
+
+ if results_of_test["data"]["testRuns"] != results_of_test["data"][
+ "successfulFVRuns"
+ ] or (
+ not results_of_test["data"]["fvonly"]
+ and results_of_test["data"]["testRuns"]
+ != results_of_test["data"]["successfulRVRuns"]
+ ):
+ """
+ This error is raised in 2 conditions:
+ 1) If the testRuns requested does not equal the successfulFVRuns(Firstview runs)
+ 2) If repeat view is enabled and if testRuns requested does not equal successfulFVRuns
+ and successfulRVRuns
+ """
+ # TODO: establish a threshold for failures, and consider failing see bug 1762470
+ self.warning(
+ f"Something went wrong with firstview/repeat view runs for: {url}"
+ )
+ self.confirm_correct_browser_and_location(
+ results_of_test["data"], options["test_parameters"]
+ )
+
+ def confirm_correct_browser_and_location(self, data, options):
+ if data["location"] != f"{options['location']}:{options['browser']}":
+ raise WPTBrowserSelectionError(
+ "Resulting browser & location are not aligned with submitted browser & location"
+ )
+
+ def add_wpt_run_to_metadata(self, wbt_run, metadata, website):
+ requested_values = self.extract_desired_values_from_wpt_run(wbt_run)
+ if requested_values is not None:
+ metadata.add_result(
+ {
+ "name": ("WebPageTest:" + re.match(r"(^.\w+)", website)[0]),
+ "framework": {"name": "mozperftest"},
+ "transformer": "mozperftest.test.webpagetest:WebPageTestData",
+ "shouldAlert": True,
+ "results": [
+ {
+ "values": [metric_value],
+ "name": metric_name,
+ "shouldAlert": True,
+ }
+ for metric_name, metric_value in requested_values.items()
+ ],
+ }
+ )
+
+ def extract_desired_values_from_wpt_run(self, wpt_run):
+ view_types = ["firstView"]
+ if not wpt_run["data"]["fvonly"]:
+ view_types.append("repeatView")
+ desired_values = {}
+ for statistic in self.statistic_types:
+ for view in view_types:
+ for value in self.wpt_browser_metrics:
+ if isinstance(wpt_run["data"][statistic][view], list):
+ self.error(f"Fail {wpt_run['data']['url']}")
+ return
+ if value not in wpt_run["data"][statistic][view].keys():
+ raise WPTDataProcessingError(
+ f"{value} not found {wpt_run['data']['url']}"
+ )
+ desired_values[f"{value}.{view}.{statistic}"] = int(
+ wpt_run["data"][statistic][view][value]
+ )
+ try:
+ desired_values["browserVersion"] = float(
+ re.match(
+ r"\d+.\d+",
+ wpt_run["data"]["runs"]["1"]["firstView"]["browserVersion"],
+ )[0]
+ )
+ desired_values["webPagetestVersion"] = float(wpt_run["webPagetestVersion"])
+ except Exception:
+ self.error("Issue found with processing browser/WPT version")
+ return desired_values
+
+ def test_runs_left_this_month(self):
+ tests_left_this_month = self.request_with_timeout(
+ f"https://www.webpagetest.org/testBalance.php?k={self.WPT_key}&f=json"
+ )
+ self.info(
+ f"There are {tests_left_this_month['data']['remaining']} tests remaining"
+ )
diff --git a/python/mozperftest/mozperftest/test/xpcshell.py b/python/mozperftest/mozperftest/test/xpcshell.py
new file mode 100644
index 0000000000..0f4d4ea490
--- /dev/null
+++ b/python/mozperftest/mozperftest/test/xpcshell.py
@@ -0,0 +1,189 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# 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/.
+import os
+from collections import defaultdict
+from distutils.dir_util import copy_tree
+from pathlib import Path
+
+from mozperftest.layers import Layer
+from mozperftest.utils import temp_dir
+
+
+class XPCShellTestError(Exception):
+ pass
+
+
+class NoPerfMetricsError(Exception):
+ pass
+
+
+class XPCShellData:
+ def open_data(self, data):
+ return {
+ "name": "xpcshell",
+ "subtest": data["name"],
+ "data": [
+ {"file": "xpcshell", "value": value, "xaxis": xaxis}
+ for xaxis, value in enumerate(data["values"])
+ ],
+ }
+
+ def transform(self, data):
+ return data
+
+ merge = transform
+
+
+class XPCShell(Layer):
+ """Runs an xpcshell test."""
+
+ name = "xpcshell"
+ activated = True
+
+ arguments = {
+ "cycles": {"type": int, "default": 13, "help": "Number of full cycles"},
+ "binary": {
+ "type": str,
+ "default": None,
+ "help": (
+ "xpcshell binary path. If not provided, "
+ "looks for it in the source tree."
+ ),
+ },
+ "mozinfo": {
+ "type": str,
+ "default": None,
+ "help": (
+ "mozinfo binary path. If not provided, looks for it in the obj tree."
+ ),
+ },
+ "xre-path": {"type": str, "default": None, "help": "XRE path."},
+ "nodejs": {"type": str, "default": None, "help": "nodejs binary path."},
+ }
+
+ def __init__(self, env, mach_cmd):
+ super(XPCShell, self).__init__(env, mach_cmd)
+ self.topsrcdir = mach_cmd.topsrcdir
+ self._mach_context = mach_cmd._mach_context
+ self.python_path = mach_cmd.virtualenv_manager.python_path
+ self.topobjdir = mach_cmd.topobjdir
+ self.distdir = mach_cmd.distdir
+ self.bindir = mach_cmd.bindir
+ self.statedir = mach_cmd.statedir
+ self.metrics = []
+ self.topsrcdir = mach_cmd.topsrcdir
+
+ def setup(self):
+ pass
+
+ def run(self, metadata):
+ test = Path(metadata.script["filename"])
+
+ # let's grab the manifest
+ manifest = Path(test.parent, "xpcshell.ini")
+ if not manifest.exists():
+ raise FileNotFoundError(str(manifest))
+
+ nodejs = self.get_arg("nodejs")
+ if nodejs is not None:
+ os.environ["MOZ_NODE_PATH"] = nodejs
+
+ import runxpcshelltests
+
+ verbose = self.get_arg("verbose")
+ xpcshell = runxpcshelltests.XPCShellTests(log=self)
+ kwargs = {}
+ kwargs["testPaths"] = test.name
+ kwargs["verbose"] = verbose
+ binary = self.get_arg("binary")
+ if binary is None:
+ binary = self.mach_cmd.get_binary_path("xpcshell")
+ kwargs["xpcshell"] = binary
+ binary = Path(binary)
+ mozinfo = self.get_arg("mozinfo")
+ if mozinfo is None:
+ mozinfo = binary.parent / ".." / "mozinfo.json"
+ if not mozinfo.exists():
+ mozinfo = Path(self.topobjdir, "mozinfo.json")
+ else:
+ mozinfo = Path(mozinfo)
+
+ kwargs["mozInfo"] = str(mozinfo)
+ kwargs["symbolsPath"] = str(Path(self.distdir, "crashreporter-symbols"))
+ kwargs["logfiles"] = True
+ kwargs["profileName"] = "firefox"
+ plugins = binary.parent / "plugins"
+ if not plugins.exists():
+ plugins = Path(self.distdir, "plugins")
+ kwargs["pluginsPath"] = str(plugins)
+
+ modules = Path(self.topobjdir, "_tests", "modules")
+ if not modules.exists():
+ modules = binary.parent / "modules"
+
+ kwargs["testingModulesDir"] = str(modules)
+ kwargs["utility_path"] = self.bindir
+ kwargs["manifest"] = str(manifest)
+ kwargs["totalChunks"] = 1
+ xre_path = self.get_arg("xre-path")
+ if xre_path is not None:
+ self.info(f"Copying {xre_path} elements to {binary.parent}")
+ copy_tree(xre_path, str(binary.parent), update=True)
+
+ http3server = binary.parent / "http3server"
+ if http3server.exists():
+ kwargs["http3server"] = str(http3server)
+
+ cycles = self.get_arg("cycles", 1)
+ self.info("Running %d cycles" % cycles)
+
+ for cycle in range(cycles):
+ self.info("Cycle %d" % (cycle + 1))
+ with temp_dir() as tmp:
+ kwargs["tempDir"] = tmp
+ if not xpcshell.runTests(kwargs):
+ raise XPCShellTestError()
+
+ self.info("tests done.")
+
+ results = defaultdict(list)
+ for m in self.metrics:
+ for key, val in m.items():
+ results[key].append(val)
+
+ if len(results.items()) == 0:
+ raise NoPerfMetricsError(
+ "No perftest results were found in the xpcshell test. Results must be "
+ 'reported using:\n info("perfMetrics", { metricName: metricValue });'
+ )
+
+ metadata.add_result(
+ {
+ "name": test.name,
+ "framework": {"name": "mozperftest"},
+ "transformer": "mozperftest.test.xpcshell:XPCShellData",
+ "results": [
+ {"values": measures, "name": subtest}
+ for subtest, measures in results.items()
+ ],
+ }
+ )
+
+ return metadata
+
+ def log_raw(self, data, **kw):
+ if data["action"] != "log":
+ return
+ if data["message"].strip('"') != "perfMetrics":
+ self.info(data["message"])
+ return
+ self.metrics.append(data["extra"])
+
+ def process_output(self, procid, line, command):
+ self.info(line)
+
+ def dummy(self, *args, **kw):
+ pass
+
+ test_end = suite_start = suite_end = test_start = dummy