diff options
Diffstat (limited to 'python/mozperftest/mozperftest/test')
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 |