From 398b822d42e979182157dac9b1a982df4beb05fe Mon Sep 17 00:00:00 2001 From: Luca Greco Date: Wed, 17 Jan 2024 14:07:53 +0000 Subject: [PATCH] Bug 1868144 - Introduce a new condprof-addons linter and condprof-addons-verify taskcluster task. r=sparky,jmaher,linter-reviewers,ahal Differential Revision: https://phabricator.services.mozilla.com/D195434 --- .../lint/linters/condprof-addons.rst | 85 ++++++ taskcluster/ci/fetch/browsertime.yml | 1 + taskcluster/ci/source-test/mozlint.yml | 16 + testing/condprofile/condprof/util.py | 22 +- tools/lint/condprof-addons.yml | 10 + tools/lint/condprof-addons/__init__.py | 217 +++++++++++++ .../files/condprof-addons/browsertime.yml | 10 + .../condprof-addons/fake-condprof-config.json | 1 + .../fake-config-01.json | 1 + .../fake-config-02.json | 1 + .../fake-config-03.json | 1 + .../firefox-addons/fake-ext-01.xpi | 0 .../firefox-addons/fake-ext-02.xpi | 0 .../condprof-addons/firefox-addons-fake.tar | Bin 0 -> 10240 bytes .../condprof-addons/with-missing-xpi.json | 5 + tools/lint/test/python.toml | 2 + tools/lint/test/test_condprof_addons.py | 285 ++++++++++++++++++ 17 files changed, 649 insertions(+), 8 deletions(-) create mode 100644 docs/code-quality/lint/linters/condprof-addons.rst create mode 100644 tools/lint/condprof-addons.yml create mode 100644 tools/lint/condprof-addons/__init__.py create mode 100644 tools/lint/test/files/condprof-addons/browsertime.yml create mode 100644 tools/lint/test/files/condprof-addons/fake-condprof-config.json create mode 100644 tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-01.json create mode 100644 tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-02.json create mode 100644 tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-03.json create mode 100644 tools/lint/test/files/condprof-addons/fake-fetches-dir/firefox-addons/fake-ext-01.xpi create mode 100644 tools/lint/test/files/condprof-addons/fake-fetches-dir/firefox-addons/fake-ext-02.xpi create mode 100644 tools/lint/test/files/condprof-addons/firefox-addons-fake.tar create mode 100644 tools/lint/test/files/condprof-addons/with-missing-xpi.json create mode 100644 tools/lint/test/test_condprof_addons.py diff --git a/docs/code-quality/lint/linters/condprof-addons.rst b/docs/code-quality/lint/linters/condprof-addons.rst new file mode 100644 index 000000000000..c302eef5bed2 --- /dev/null +++ b/docs/code-quality/lint/linters/condprof-addons.rst @@ -0,0 +1,85 @@ +CondProf Addons +=============== + +CondProf Addons is a linter for condprof customization JSON files (see :searchfox:`testing/condprofile/condprof/customization`), +it reports linting errors if: + +- any of the addons required by the customization files (e.g. see :searchfox:`testing/condprofile/condprof/customization/webext.json`) + is not found in the tar file fetched through the `firefox-addons` fetch task (see :searchfox:`taskcluster/ci/fetch/browsertime.yml`) +- or the expected `firefox-addons` fetch task has not been found + +Run Locally +----------- + +The mozlint integration of condprof-addons can be run using mach: + +.. parsed-literal:: + + $ mach lint --linter condprof-addons + +Alternatively, if the ``--linter condprof-addons`` is omitted, the ``condprof-addons`` will still be selected automatically if +any of the files paths passed explicitly is detected to be part of the condprof customization directory. + +The ``condprof-addons`` will also be running automatically on ``mach lint --outgoing`` if there are customization files changes +detected in the outgoing patches. + +Run on Taskcluster +------------------ + +The condprof-addons job shows up as ``misc(condprof-addons)`` in the linting job. It should run automatically if changes are made +to condprof customization JSON files. + +Fix reported errors +------------------- + +XPI file is missing from the firefox-addons.tar archive +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This linting errors is expected to be reported if the linter detected that a confprof customization file +requires an addon but the related xpi filename is not included in the firefox-addons.tar file fetched +through the `firefox-addons` fetch task (see :searchfox:`taskcluster/ci/fetch/browsertime.yml`). + +If the patch or phabricator revision is not meant to be landed, but only used as a temporary patch +pushed on try or only applied locally (e.g. to run the tp6/tp6m webextensions perftests with a given +third party extension installed to gather some metrics and/or GeckoProfiler data), then it can be +safely ignored. + +On the contrary, if the patch or phabricator revision is meant to be landed on mozilla-central, +the linting error have to be fixed before or along landing the change, either by: + +- removing the addition to the customization file if it wasn't intended to include that addon to all runs + of the tp6/tp6m webextensions perftests + +- updating the `firefox-addons` fetch task as defined in :searchfox:`taskcluster/ci/fetch/browsertime.yml` + by creating a pull request in the github repository where the asset is stored, and ask a review from + a peer of the `#webextensions-reviewer` and `#perftests-reviewers` review groups. + +firefox-addons taskcluster config 'add-prefix' attribute should be set to 'firefox-addons/' +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If this linting error is hit, then the `firefox-addons` task defined in :searchfox:`taskcluster/ci/fetch/browsertime.yml` +is missing the `add-prefix` attribute or its value is not set to the expected 'firefox-addons/' subdir name. + +This is enforced as a linting rule because when the condprof utility is going to build a conditioned profile +for which some add-ons xpi files are expected to be sideloaded (see :searchfox:`testing/condprofile/condprof/customization.webext.json`), +to avoid re-downloading the same xpi from a remote urls every time the conditioned profile is built on the build infrastructure +(which for tp6/tp6m perftests will happen once per job being executed) condprof is going to look first if the expected xpi file +names are already available in `$MOZ_FETCHES_DIR/firefox-addons`. + +firefox-addons taskcluser fetch config section not found +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This linting error is hit if the linter does not find the expected `firefox-addons` task defined in :searchfox:`taskcluster/ci/fetch/browsertime.yml` +or it is missing the expected `fetch` attribute. + +Configuration +------------- + +ConfProf Addons does not currently provide any configuration files. + +Sources +------- + +* :searchfox:`Configuration (YAML) ` +* :searchfox:`Source ` +* :searchfox:`Test ` diff --git a/taskcluster/ci/fetch/browsertime.yml b/taskcluster/ci/fetch/browsertime.yml index c69223d8af5a..7f7abaf3ac46 100644 --- a/taskcluster/ci/fetch/browsertime.yml +++ b/taskcluster/ci/fetch/browsertime.yml @@ -174,6 +174,7 @@ firefox-addons: fetch: type: static-url artifact-name: firefox-addons.tar.zst + add-prefix: firefox-addons/ url: https://github.com/mozilla/perf-automation/raw/c9e497eaa86f45538e3509120947215c6f97c95b/test_files/firefox-addons.tar sha256: 20372ff1d58fc33d1568f8922fe66e2e2e01c77663820344d2a364a8ddd68281 size: 3584000 diff --git a/taskcluster/ci/source-test/mozlint.yml b/taskcluster/ci/source-test/mozlint.yml index 1c91debf2166..28f238182719 100644 --- a/taskcluster/ci/source-test/mozlint.yml +++ b/taskcluster/ci/source-test/mozlint.yml @@ -561,6 +561,22 @@ perfdocs-verify: - 'testing/perfdocs/**' - 'tools/lint/perfdocs.yml' +condprof-addons-verify: + description: | + Verifies all xpi files needed by condprof customization sideloading addons are + included in the firefox-addons fetch task. + treeherder: + symbol: misc(condprof-addons) + run: + mach: | + lint -v -l condprof-addons -f treeherder -f json:/builds/worker/mozlint.json . + fetches: + fetch: + - firefox-addons + when: + files-changed: + - 'testing/condprofile/condprof/customization/*.json' + - 'taskcluster/ci/fetch/browsertime.yml' fluent-lint: description: Check for problems with Fluent files. diff --git a/testing/condprofile/condprof/util.py b/testing/condprofile/condprof/util.py index 53235c3548eb..444398b9f486 100644 --- a/testing/condprofile/condprof/util.py +++ b/testing/condprofile/condprof/util.py @@ -21,7 +21,7 @@ from requests.packages.urllib3.util.retry import Retry from condprof import progress -TASK_CLUSTER = "TASK_ID" in os.environ.keys() +TASK_CLUSTER = "MOZ_AUTOMATION" in os.environ.keys() DOWNLOAD_TIMEOUT = 30 @@ -122,7 +122,12 @@ def fresh_profile(profile, customization_data): extensions = [] for name, url in customization_data["addons"].items(): logger.info("Downloading addon %s" % name) - extension = download_file(url, check_mozfetches=True) + # When running on the CI, we expect the xpi files to have been + # fetched by the firefox-addons fetch task dependency (see + # taskcluster/ci/fetch/browsertime.yml) and the condprof-addons + # linter enforces the content of the archive to be unpacked into + # a subdirectory named "firefox-addons". + extension = download_file(url, mozfetches_subdir="firefox-addons") extensions.append(extension) logger.info("Installing addons") new_profile.addons.install(extensions) @@ -184,25 +189,26 @@ def check_exists(archive, server=None, all_types=False): return exists, resp.headers -def check_mozfetches_dir(target): +def check_mozfetches_dir(target, mozfetches_subdir): logger.info("Checking for existence of: %s in MOZ_FETCHES_DIR" % target) fetches = os.environ.get("MOZ_FETCHES_DIR") if fetches is None: return None - fetches_target = os.path.join(fetches, target) + fetches_target = os.path.join(fetches, mozfetches_subdir, target) if not os.path.exists(fetches_target): return None logger.info("Already fetched and available in MOZ_FETCHES_DIR: %s" % fetches_target) return fetches_target -def download_file(url, target=None, check_mozfetches=False): +def download_file(url, target=None, mozfetches_subdir=None): if target is None: target = url.split("/")[-1] - # check if the assets has been fetched through a taskgraph fetch task dependency. - if check_mozfetches: - filepath = check_mozfetches_dir(target) + # check if the assets has been fetched through a taskgraph fetch task dependency + # and already available in the MOZ_FETCHES_DIR passed as an additional parameter. + if mozfetches_subdir is not None: + filepath = check_mozfetches_dir(target, mozfetches_subdir) if filepath is not None: return filepath diff --git a/tools/lint/condprof-addons.yml b/tools/lint/condprof-addons.yml new file mode 100644 index 000000000000..a62c1fb6b9ef --- /dev/null +++ b/tools/lint/condprof-addons.yml @@ -0,0 +1,10 @@ +--- +condprof-addons: + description: Lint condprof customizations json files sideloading addons + include: + - 'testing/condprofile/condprof/customization' + exclude: [] + extensions: ['json'] + support-files: ['taskcluster/ci/fetch/browsertime.yml'] + type: structured_log + payload: condprof-addons:lint diff --git a/tools/lint/condprof-addons/__init__.py b/tools/lint/condprof-addons/__init__.py new file mode 100644 index 000000000000..f17ab26f3f35 --- /dev/null +++ b/tools/lint/condprof-addons/__init__.py @@ -0,0 +1,217 @@ +# 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 hashlib +import json +import os +import tarfile +import tempfile +from pathlib import Path + +import requests +import yaml +from mozlint.pathutils import expand_exclusions + +BROWSERTIME_FETCHES_PATH = Path("taskcluster/ci/fetch/browsertime.yml") +CUSTOMIZATIONS_PATH = Path("testing/condprofile/condprof/customization/") +DOWNLOAD_TIMEOUT = 30 +ERR_FETCH_TASK_MISSING = "firefox-addons taskcluster fetch config section not found" +ERR_FETCH_TASK_ADDPREFIX = "firefox-addons taskcluster config 'add-prefix' attribute should be set to 'firefox-addons/'" +ERR_FETCH_TASK_ARCHIVE = ( + "Error downloading or opening archive from firefox-addons taskcluster fetch url" +) +LINTER_NAME = "condprof-addons" +MOZ_FETCHES_DIR = os.environ.get("MOZ_FETCHES_DIR") +RULE_DESC = "condprof addons all listed in firefox-addons.tar fetched archive" +MOZ_AUTOMATION = "MOZ_AUTOMATION" in os.environ + +tempdir = tempfile.gettempdir() + + +def lint(paths, config, logger, fix=None, **lintargs): + filepaths = [Path(p) for p in expand_exclusions(paths, config, lintargs["root"])] + + if len(filepaths) == 0: + return + + linter = CondprofAddonsLinter(topsrcdir=lintargs["root"], logger=logger) + + for filepath in filepaths: + linter.lint(filepath) + + +class CondprofAddonsLinter: + def __init__(self, topsrcdir, logger): + self.topsrcdir = topsrcdir + self.logger = logger + self.BROWSERTIME_FETCHES_FULLPATH = Path( + self.topsrcdir, BROWSERTIME_FETCHES_PATH + ) + self.CUSTOMIZATIONS_FULLPATH = Path(self.topsrcdir, CUSTOMIZATIONS_PATH) + self.tar_xpi_filenames = self.get_firefox_addons_tar_names() + + def lint(self, filepath): + data = self.read_json(filepath) + + if "addons" not in data: + return + + for addon_key in data["addons"]: + xpi_url = data["addons"][addon_key] + xpi_filename = xpi_url.split("/")[-1] + self.logger.info(f"Found addon {xpi_filename}") + if xpi_filename not in self.tar_xpi_filenames: + self.logger.lint_error( + self.get_missing_xpi_msg(xpi_filename), + lineno=0, + column=None, + path=str(filepath), + linter=LINTER_NAME, + rule=RULE_DESC, + ) + + def get_missing_xpi_msg(self, xpi_filename): + return f"{xpi_filename} is missing from the firefox-addons.tar archive" + + def read_json(self, filepath): + with filepath.open("r") as f: + return json.load(f) + + def read_yaml(self, filepath): + with filepath.open("r") as f: + return yaml.safe_load(f) + + def download_firefox_addons_tar(self, firefox_addons_tar_url, tar_tmp_path): + self.logger.info(f"Downloading {firefox_addons_tar_url} to {tar_tmp_path}") + res = requests.get( + firefox_addons_tar_url, stream=True, timeout=DOWNLOAD_TIMEOUT + ) + res.raise_for_status() + with tar_tmp_path.open("wb") as f: + for chunk in res.iter_content(chunk_size=1024): + if chunk is not None: + f.write(chunk) + f.flush() + + def get_firefox_addons_tar_names(self): + # Get firefox-addons fetch task config. + browsertime_fetches = self.read_yaml(self.BROWSERTIME_FETCHES_FULLPATH) + + if not ( + "firefox-addons" in browsertime_fetches + and "fetch" in browsertime_fetches["firefox-addons"] + ): + self.logger.lint_error( + ERR_FETCH_TASK_MISSING, + lineno=0, + column=None, + path=BROWSERTIME_FETCHES_PATH, + linter=LINTER_NAME, + rule=RULE_DESC, + ) + return [] + + fetch_config = browsertime_fetches["firefox-addons"]["fetch"] + + if not ( + "add-prefix" in fetch_config + and fetch_config["add-prefix"] == "firefox-addons/" + ): + self.logger.lint_error( + ERR_FETCH_TASK_ADDPREFIX, + lineno=0, + column=None, + path=BROWSERTIME_FETCHES_PATH, + linter=LINTER_NAME, + rule=RULE_DESC, + ) + return [] + + firefox_addons_tar_url = fetch_config["url"] + firefox_addons_tar_sha256 = fetch_config["sha256"] + + tar_xpi_files = list() + + # When running on the CI, try to retrieve the list of xpi files from the target MOZ_FETCHES_DIR + # subdirectory instead of downloading the archive from the fetch url. + if MOZ_AUTOMATION: + fetches_path = ( + Path(MOZ_FETCHES_DIR) if MOZ_FETCHES_DIR is not None else None + ) + if fetches_path is not None and fetches_path.exists(): + self.logger.info( + "Detected MOZ_FETCHES_DIR, look for pre-downloaded firefox-addons fetch results" + ) + # add-prefix presence and value has been enforced at the start of this method. + fetches_addons = Path(fetches_path, "firefox-addons/") + if fetches_addons.exists(): + self.logger.info( + f"Retrieve list of xpi files from firefox-addons fetch result at {str(fetches_addons)}" + ) + for xpi_path in fetches_addons.iterdir(): + if xpi_path.suffix == ".xpi": + tar_xpi_files.append(xpi_path.name) + return tar_xpi_files + else: + self.logger.warning( + "No 'firefox-addons/' subdir found in MOZ_FETCHES_DIR" + ) + + # Fallback to download the tar archive and retrieve the list of xpi file from it + # (e.g. when linting the local changes on the developers environment). + tar_tmp_path = Path(tempdir, "firefox-addons.tar") + tar_tmp_ready = False + + # If the firefox-addons.tar file is found in the tempdir, check if the + # file hash matches, if it does then don't download it again. + if tar_tmp_path.exists(): + tar_tmp_hash = hashlib.sha256() + with tar_tmp_path.open("rb") as f: + while chunk := f.read(1024): + tar_tmp_hash.update(chunk) + if tar_tmp_hash.hexdigest() == firefox_addons_tar_sha256: + self.logger.info( + f"Pre-downloaded file for {tar_tmp_path} found and sha256 matching" + ) + tar_tmp_ready = True + else: + self.logger.info( + f"{tar_tmp_path} sha256 does not match the fetch config" + ) + + # If the file is not found or the hash doesn't match, download it from the fetch task url. + if not tar_tmp_ready: + try: + self.download_firefox_addons_tar(firefox_addons_tar_url, tar_tmp_path) + except requests.exceptions.HTTPError as http_err: + self.logger.lint_error( + f"{ERR_FETCH_TASK_ARCHIVE}, {str(http_err)}", + lineno=0, + column=None, + path=BROWSERTIME_FETCHES_PATH, + linter=LINTER_NAME, + rule=RULE_DESC, + ) + return [] + + # Retrieve and return the list of xpi file names. + try: + with tarfile.open(tar_tmp_path, "r") as tf: + names = tf.getnames() + for name in names: + file_path = Path(name) + if file_path.suffix == ".xpi": + tar_xpi_files.append(file_path.name) + except tarfile.ReadError as read_err: + self.logger.lint_error( + f"{ERR_FETCH_TASK_ARCHIVE}, {str(read_err)}", + lineno=0, + column=None, + path=BROWSERTIME_FETCHES_PATH, + linter=LINTER_NAME, + rule=RULE_DESC, + ) + return [] + + return tar_xpi_files diff --git a/tools/lint/test/files/condprof-addons/browsertime.yml b/tools/lint/test/files/condprof-addons/browsertime.yml new file mode 100644 index 000000000000..7f065809d960 --- /dev/null +++ b/tools/lint/test/files/condprof-addons/browsertime.yml @@ -0,0 +1,10 @@ +--- +firefox-addons: + description: "fixture for the expected firefox-addons.tar ci fetch config" + fetch: + type: static-url + artifact-name: firefox-addons.tar.zst + add-prefix: firefox-addons/ + url: https://localhost/fake-firefox-addons.tar + sha256: 20372ff1d58fc33d1568f8922fe66e2e2e01c77663820344d2a364a8ddd68281 + size: 3584000 diff --git a/tools/lint/test/files/condprof-addons/fake-condprof-config.json b/tools/lint/test/files/condprof-addons/fake-condprof-config.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/tools/lint/test/files/condprof-addons/fake-condprof-config.json @@ -0,0 +1 @@ +{} diff --git a/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-01.json b/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-01.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-01.json @@ -0,0 +1 @@ +{} diff --git a/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-02.json b/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-02.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-02.json @@ -0,0 +1 @@ +{} diff --git a/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-03.json b/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-03.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/tools/lint/test/files/condprof-addons/fake-customizations-dir/fake-config-03.json @@ -0,0 +1 @@ +{} diff --git a/tools/lint/test/files/condprof-addons/fake-fetches-dir/firefox-addons/fake-ext-01.xpi b/tools/lint/test/files/condprof-addons/fake-fetches-dir/firefox-addons/fake-ext-01.xpi new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/lint/test/files/condprof-addons/fake-fetches-dir/firefox-addons/fake-ext-02.xpi b/tools/lint/test/files/condprof-addons/fake-fetches-dir/firefox-addons/fake-ext-02.xpi new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/lint/test/files/condprof-addons/firefox-addons-fake.tar b/tools/lint/test/files/condprof-addons/firefox-addons-fake.tar new file mode 100644 index 0000000000000000000000000000000000000000..2b7e13b82cc1c179eb582a10117264a49179aef2 GIT binary patch literal 10240 zcmeIyO$viB5QgC##S2s?iQ}Bdg@w{C#9Hw9O$u!V!L9;Q-q{2k^275b){QG(lFCe7 z8oa5j$K1;0e5hDfIZnJLA{dG7E%X|`F zFXN@aQFFju*RFGa--F!B_02zcCq76>`AdPbY?dVdPyXQ$`FF4UqmuvTt`_*Pe*f_Z z`RD(CdjBz!e}TAJ5&;AdKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R;X~ F;0b5TOospf literal 0 HcmV?d00001 diff --git a/tools/lint/test/files/condprof-addons/with-missing-xpi.json b/tools/lint/test/files/condprof-addons/with-missing-xpi.json new file mode 100644 index 000000000000..ae44833a709d --- /dev/null +++ b/tools/lint/test/files/condprof-addons/with-missing-xpi.json @@ -0,0 +1,5 @@ +{ + "addons": { + "non-existing": "http://localhost/non-existing.xpi" + } +} diff --git a/tools/lint/test/python.toml b/tools/lint/test/python.toml index f1cc80f8444e..7ad524c41f8a 100644 --- a/tools/lint/test/python.toml +++ b/tools/lint/test/python.toml @@ -10,6 +10,8 @@ requirements = "tools/lint/python/black_requirements.txt" ["test_codespell.py"] +["test_condprof_addons.py"] + ["test_eslint.py"] skip-if = ["os == 'win'"] # busts the tree for subsequent tasks on the same worker (bug 1708591) # Setup conflicts with stylelint setup so this should run sequentially. diff --git a/tools/lint/test/test_condprof_addons.py b/tools/lint/test/test_condprof_addons.py new file mode 100644 index 000000000000..e1401a7119ff --- /dev/null +++ b/tools/lint/test/test_condprof_addons.py @@ -0,0 +1,285 @@ +import importlib +import tempfile +from pathlib import Path +from unittest import mock + +import mozunit +import requests + +LINTER = "condprof-addons" + + +def linter_module_mocks( + customizations_path=".", browsertime_fetches_path="browsertime.yml", **othermocks +): + return mock.patch.multiple( + LINTER, + CUSTOMIZATIONS_PATH=Path(customizations_path), + BROWSERTIME_FETCHES_PATH=Path(browsertime_fetches_path), + **othermocks, + ) + + +def linter_class_mocks(**mocks): + return mock.patch.multiple( + f"{LINTER}.CondprofAddonsLinter", + **mocks, + ) + + +# Sanity check (make sure linter message includes the xpi filename). +def test_get_missing_xpi_msg(lint, paths): + condprof_addons = importlib.import_module("condprof-addons") + with linter_class_mocks( + get_firefox_addons_tar_names=mock.Mock(return_value=list()), + ): + instance = condprof_addons.CondprofAddonsLinter( + topsrcdir=paths()[0], logger=mock.Mock() + ) + assert instance.get_missing_xpi_msg("test.xpi").startswith( + "test.xpi is missing" + ) + + +def test_xpi_missing_from_firefox_addons_tar(lint, paths): + fixture_customizations = paths("with-missing-xpi.json") + with linter_module_mocks(), linter_class_mocks( + get_firefox_addons_tar_names=mock.Mock(return_value=list()), + ): + logger_mock = mock.Mock() + lint(fixture_customizations, logger=logger_mock) + assert logger_mock.lint_error.call_count == 1 + assert Path(fixture_customizations[0]).samefile( + logger_mock.lint_error.call_args.kwargs["path"] + ) + importlib.import_module("condprof-addons") + assert "non-existing.xpi" in logger_mock.lint_error.call_args.args[0] + + +def test_xpi_all_found_in_firefox_addons_tar(lint, paths): + get_tarnames_mock = mock.Mock( + return_value=["an-extension.xpi", "another-extension.xpi"] + ) + read_json_mock = mock.Mock( + return_value={ + "addons": { + "an-extension": "http://localhost/ext/an-extension.xpi", + "another-extension": "http://localhost/ext/another-extension.xpi", + } + } + ) + + with linter_module_mocks(), linter_class_mocks( + get_firefox_addons_tar_names=get_tarnames_mock, read_json=read_json_mock + ): + logger_mock = mock.Mock() + # Compute a fake condprof customization path, the content is + # going to be the read_json_mock.return_value and so the + # fixture file does not actually exists. + fixture_customizations = paths("fake-condprof-config.json") + lint( + fixture_customizations, + logger=logger_mock, + config={"include": paths(), "extensions": ["json", "yml"]}, + ) + assert read_json_mock.call_count == 1 + assert get_tarnames_mock.call_count == 1 + assert logger_mock.lint_error.call_count == 0 + + +def test_lint_error_on_missing_or_invalid_firefoxaddons_fetch_task( + lint, + paths, +): + read_json_mock = mock.Mock(return_value=dict()) + read_yaml_mock = mock.Mock(return_value=dict()) + # Verify that an explicit linter error is reported if the fetch task is not found. + with linter_module_mocks(), linter_class_mocks( + read_json=read_json_mock, read_yaml=read_yaml_mock + ): + logger_mock = mock.Mock() + fixture_customizations = paths("fake-condprof-config.json") + condprof_addons = importlib.import_module("condprof-addons") + + def assert_linter_error(yaml_mock_value, expected_msg): + logger_mock.reset_mock() + read_yaml_mock.return_value = yaml_mock_value + lint(fixture_customizations, logger=logger_mock) + assert logger_mock.lint_error.call_count == 1 + expected_path = condprof_addons.BROWSERTIME_FETCHES_PATH + assert logger_mock.lint_error.call_args.kwargs["path"] == expected_path + assert logger_mock.lint_error.call_args.args[0] == expected_msg + + # Mock a yaml file that is not including the expected firefox-addons fetch task. + assert_linter_error( + yaml_mock_value=dict(), expected_msg=condprof_addons.ERR_FETCH_TASK_MISSING + ) + # Mock a yaml file where firefox-addons is missing the fetch attribute. + assert_linter_error( + yaml_mock_value={"firefox-addons": {}}, + expected_msg=condprof_addons.ERR_FETCH_TASK_MISSING, + ) + # Mock a yaml file where firefox-addons add-prefix is missing. + assert_linter_error( + yaml_mock_value={"firefox-addons": {"fetch": {}}}, + expected_msg=condprof_addons.ERR_FETCH_TASK_ADDPREFIX, + ) + # Mock a yaml file where firefox-addons add-prefix is invalid. + assert_linter_error( + yaml_mock_value={ + "firefox-addons": {"fetch": {"add-prefix": "invalid-subdir-name/"}} + }, + expected_msg=condprof_addons.ERR_FETCH_TASK_ADDPREFIX, + ) + + +def test_get_xpi_list_from_fetch_dir(lint, paths): + # Verify that when executed on the CI, the helper method looks for the xpi files + # in the MOZ_FETCHES_DIR subdir where they are expected to be unpacked by the + # fetch task. + with linter_module_mocks( + MOZ_AUTOMATION=1, MOZ_FETCHES_DIR=paths("fake-fetches-dir")[0] + ): + condprof_addons = importlib.import_module("condprof-addons") + logger_mock = mock.Mock() + Path(paths("browsertime.yml")[0]) + + linter = condprof_addons.CondprofAddonsLinter( + topsrcdir=paths()[0], logger=logger_mock + ) + results = linter.tar_xpi_filenames + + results.sort() + assert results == ["fake-ext-01.xpi", "fake-ext-02.xpi"] + + +def test_get_xpi_list_from_downloaded_tar(lint, paths): + def mocked_download_tar(firefox_addons_tar_url, tar_tmp_path): + tar_tmp_path.write_bytes(Path(paths("firefox-addons-fake.tar")[0]).read_bytes()) + + download_firefox_addons_tar_mock = mock.Mock() + download_firefox_addons_tar_mock.side_effect = mocked_download_tar + + # Verify that when executed locally on a developer machine, the tar archive is downloaded + # and the list of xpi files included in it returned by the helper method. + with tempfile.TemporaryDirectory() as tempdir, linter_module_mocks( + MOZ_AUTOMATION=0, + tempdir=tempdir, + ), linter_class_mocks( + download_firefox_addons_tar=download_firefox_addons_tar_mock, + ): + condprof_addons = importlib.import_module("condprof-addons") + logger_mock = mock.Mock() + Path(paths("browsertime.yml")[0]) + + linter = condprof_addons.CondprofAddonsLinter( + topsrcdir=paths()[0], logger=logger_mock + ) + results = linter.tar_xpi_filenames + assert len(results) > 0 + print("List of addons found in the downloaded file archive:", results) + assert all(filename.endswith(".xpi") for filename in results) + assert download_firefox_addons_tar_mock.call_count == 1 + + +@mock.patch("requests.get") +def test_error_on_downloading_tar(requests_get_mock, lint, paths): + # Verify that when executed locally and the tar archive fails to download + # the linter does report an explicit linting error with the http error included. + with tempfile.TemporaryDirectory() as tempdir, linter_module_mocks( + MOZ_AUTOMATION=0, tempdir=tempdir + ): + condprof_addons = importlib.import_module("condprof-addons") + logger_mock = mock.Mock() + response_mock = mock.Mock() + response_mock.raise_for_status.side_effect = requests.exceptions.HTTPError( + "MOCK_ERROR" + ) + requests_get_mock.return_value = response_mock + Path(paths("browsertime.yml")[0]) + + linter = condprof_addons.CondprofAddonsLinter( + topsrcdir=paths()[0], logger=logger_mock + ) + + assert ( + logger_mock.lint_error.call_args.kwargs["path"] + == condprof_addons.BROWSERTIME_FETCHES_PATH + ) + assert ( + logger_mock.lint_error.call_args.args[0] + == f"{condprof_addons.ERR_FETCH_TASK_ARCHIVE}, MOCK_ERROR" + ) + assert requests_get_mock.call_count == 1 + assert len(linter.tar_xpi_filenames) == 0 + + +@mock.patch("requests.get") +def test_error_on_opening_tar(requests_get_mock, lint, paths): + # Verify that when executed locally and the tar archive fails to open + # the linter does report an explicit linting error with the tarfile error included. + with tempfile.TemporaryDirectory() as tempdir, linter_module_mocks( + MOZ_AUTOMATION=0, tempdir=tempdir + ): + condprof_addons = importlib.import_module("condprof-addons") + logger_mock = mock.Mock() + response_mock = mock.Mock() + response_mock.raise_for_status.return_value = None + + def mock_iter_content(chunk_size): + yield b"fake tar content" + yield b"expected to trigger tarfile.ReadError" + + response_mock.iter_content.side_effect = mock_iter_content + requests_get_mock.return_value = response_mock + Path(paths("browsertime.yml")[0]) + + linter = condprof_addons.CondprofAddonsLinter( + topsrcdir=paths()[0], logger=logger_mock + ) + + assert ( + logger_mock.lint_error.call_args.kwargs["path"] + == condprof_addons.BROWSERTIME_FETCHES_PATH + ) + actual_msg = logger_mock.lint_error.call_args.args[0] + print("Got linter error message:", actual_msg) + assert actual_msg.startswith( + f"{condprof_addons.ERR_FETCH_TASK_ARCHIVE}, file could not be opened successfully" + ) + assert requests_get_mock.call_count == 1 + assert len(linter.tar_xpi_filenames) == 0 + + +def test_lint_all_customization_files_when_linting_browsertime_yml( + lint, + paths, +): + get_tarnames_mock = mock.Mock(return_value=["an-extension.xpi"]) + read_json_mock = mock.Mock( + return_value={ + "addons": {"an-extension": "http://localhost/ext/an-extension.xpi"} + } + ) + with linter_module_mocks( + customizations_path="fake-customizations-dir", + ), linter_class_mocks( + get_firefox_addons_tar_names=get_tarnames_mock, + read_json=read_json_mock, + ): + logger_mock = mock.Mock() + importlib.import_module("condprof-addons") + # When mozlint detects a change to the ci fetch browser.yml support file, + # condprof-addons linter is called for the entire customizations dir path + # and we expect that to be expanded to the list of the json customizations + # files from that directory path. + lint(paths("fake-customizations-dir"), logger=logger_mock) + # Expect read_json_mock to be called once per each of the json files + # found in the fixture dir. + assert read_json_mock.call_count == 3 + assert get_tarnames_mock.call_count == 1 + assert logger_mock.lint_error.call_count == 0 + + +if __name__ == "__main__": + mozunit.main()