summaryrefslogtreecommitdiffstats
path: root/tools/lint/test
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tools/lint/test-manifest-alpha.yml19
-rw-r--r--tools/lint/test-manifest-alpha/__init__.py77
-rw-r--r--tools/lint/test-manifest-alpha/error-level-manifests.yml7
-rw-r--r--tools/lint/test-manifest-disable.yml16
-rw-r--r--tools/lint/test-manifest-skip-if.yml18
-rw-r--r--tools/lint/test/conftest.py305
-rw-r--r--tools/lint/test/files/android-format/Bad.java8
-rw-r--r--tools/lint/test/files/android-format/Main.kt7
-rw-r--r--tools/lint/test/files/android-format/build.gradle1
-rw-r--r--tools/lint/test/files/black/bad.py6
-rw-r--r--tools/lint/test/files/black/invalid.py4
-rw-r--r--tools/lint/test/files/clang-format/bad/bad.cpp6
-rw-r--r--tools/lint/test/files/clang-format/bad/bad2.c8
-rw-r--r--tools/lint/test/files/clang-format/bad/bad2.h1
-rw-r--r--tools/lint/test/files/clang-format/bad/good.cpp1
-rw-r--r--tools/lint/test/files/clang-format/good/foo.cpp1
-rw-r--r--tools/lint/test/files/clippy/test1/Cargo.toml17
-rw-r--r--tools/lint/test/files/clippy/test1/bad.rs14
-rw-r--r--tools/lint/test/files/clippy/test1/bad2.rs14
-rw-r--r--tools/lint/test/files/clippy/test1/good.rs6
-rw-r--r--tools/lint/test/files/clippy/test2/Cargo.lock5
-rw-r--r--tools/lint/test/files/clippy/test2/Cargo.toml8
-rw-r--r--tools/lint/test/files/clippy/test2/src/bad_1.rs15
-rw-r--r--tools/lint/test/files/clippy/test2/src/bad_2.rs17
-rw-r--r--tools/lint/test/files/codespell/ignore.rst5
-rw-r--r--tools/lint/test/files/eslint/good.js0
-rw-r--r--tools/lint/test/files/eslint/import/bad_import.js1
-rw-r--r--tools/lint/test/files/eslint/nolint/foo.txt0
-rw-r--r--tools/lint/test/files/eslint/subdir/bad.js2
-rw-r--r--tools/lint/test/files/eslint/testprettierignore1
-rwxr-xr-xtools/lint/test/files/file-perm/maybe-shebang/bad.js2
-rwxr-xr-xtools/lint/test/files/file-perm/maybe-shebang/good.js5
-rwxr-xr-xtools/lint/test/files/file-perm/no-shebang/bad-shebang.c2
-rwxr-xr-xtools/lint/test/files/file-perm/no-shebang/bad.c1
-rwxr-xr-xtools/lint/test/files/file-perm/no-shebang/bad.pngbin0 -> 49 bytes
-rw-r--r--tools/lint/test/files/file-perm/no-shebang/good.c1
-rw-r--r--tools/lint/test/files/file-whitespace/bad-newline.c3
-rw-r--r--tools/lint/test/files/file-whitespace/bad-windows.c3
-rw-r--r--tools/lint/test/files/file-whitespace/bad.c3
-rw-r--r--tools/lint/test/files/file-whitespace/bad.js3
-rw-r--r--tools/lint/test/files/file-whitespace/good.c1
-rw-r--r--tools/lint/test/files/file-whitespace/good.js5
-rw-r--r--tools/lint/test/files/fluent-lint/bad.ftl44
-rw-r--r--tools/lint/test/files/fluent-lint/brand-names-excluded.ftl2
-rw-r--r--tools/lint/test/files/fluent-lint/brand-names.ftl30
-rw-r--r--tools/lint/test/files/fluent-lint/comment-group1.ftl35
-rw-r--r--tools/lint/test/files/fluent-lint/comment-group2.ftl15
-rw-r--r--tools/lint/test/files/fluent-lint/comment-resource1.ftl11
-rw-r--r--tools/lint/test/files/fluent-lint/comment-resource2.ftl6
-rw-r--r--tools/lint/test/files/fluent-lint/comment-resource3.ftl6
-rw-r--r--tools/lint/test/files/fluent-lint/comment-resource4.ftl8
-rw-r--r--tools/lint/test/files/fluent-lint/comment-resource5.ftl8
-rw-r--r--tools/lint/test/files/fluent-lint/comment-resource6.ftl4
-rw-r--r--tools/lint/test/files/fluent-lint/comment-variables1.ftl60
-rw-r--r--tools/lint/test/files/fluent-lint/comment-variables2.ftl27
-rw-r--r--tools/lint/test/files/fluent-lint/excluded.ftl6
-rw-r--r--tools/lint/test/files/fluent-lint/test-brands.ftl5
-rw-r--r--tools/lint/test/files/fluent-lint/tools/lint/fluent-lint/exclusions.yml17
-rw-r--r--tools/lint/test/files/license/.eslintrc.js5
-rw-r--r--tools/lint/test/files/license/bad.c1
-rw-r--r--tools/lint/test/files/license/bad.js6
-rw-r--r--tools/lint/test/files/license/good-other.h9
-rw-r--r--tools/lint/test/files/license/good.c8
-rw-r--r--tools/lint/test/files/license/good.js7
-rw-r--r--tools/lint/test/files/lintpref/bad.js2
-rw-r--r--tools/lint/test/files/lintpref/good.js6
-rw-r--r--tools/lint/test/files/rst/.dotfile.rst11
-rw-r--r--tools/lint/test/files/rst/bad.rst20
-rw-r--r--tools/lint/test/files/rst/bad2.rst4
-rw-r--r--tools/lint/test/files/rst/bad3.rst6
-rw-r--r--tools/lint/test/files/rst/good.rst11
-rw-r--r--tools/lint/test/files/ruff/bad.py4
-rw-r--r--tools/lint/test/files/ruff/ruff.toml1
-rw-r--r--tools/lint/test/files/rustfmt/subdir/bad.rs16
-rw-r--r--tools/lint/test/files/rustfmt/subdir/bad2.rs17
-rw-r--r--tools/lint/test/files/rustfmt/subdir/good.rs6
-rw-r--r--tools/lint/test/files/shellcheck/bad.sh3
-rw-r--r--tools/lint/test/files/shellcheck/good.sh2
-rw-r--r--tools/lint/test/files/stylelint/nolint/foo.txt0
-rw-r--r--tools/lint/test/files/stylelint/subdir/bad.css5
-rw-r--r--tools/lint/test/files/test-manifest-alpha/mochitest-in-order.ini29
-rw-r--r--tools/lint/test/files/test-manifest-alpha/mochitest-mostly-in-order.ini30
-rw-r--r--tools/lint/test/files/test-manifest-alpha/mochitest-very-out-of-order.ini29
-rw-r--r--tools/lint/test/files/test-manifest-alpha/other-ini-very-out-of-order.ini29
-rw-r--r--tools/lint/test/files/trojan-source/README5
-rw-r--r--tools/lint/test/files/trojan-source/commenting-out.cpp9
-rw-r--r--tools/lint/test/files/trojan-source/early-return.py9
-rw-r--r--tools/lint/test/files/trojan-source/invisible-function.rs15
-rw-r--r--tools/lint/test/files/updatebot/.yamllint6
-rw-r--r--tools/lint/test/files/updatebot/cargo-mismatch.yaml44
-rw-r--r--tools/lint/test/files/updatebot/good1.yaml44
-rw-r--r--tools/lint/test/files/updatebot/good2.yaml74
-rw-r--r--tools/lint/test/files/updatebot/no-revision.yaml43
-rw-r--r--tools/lint/test/files/yaml/.yamllint6
-rw-r--r--tools/lint/test/files/yaml/bad.yml8
-rw-r--r--tools/lint/test/files/yaml/good.yml6
-rw-r--r--tools/lint/test/python.ini35
-rw-r--r--tools/lint/test/test_android_format.py38
-rw-r--r--tools/lint/test/test_black.py53
-rw-r--r--tools/lint/test/test_clang_format.py139
-rw-r--r--tools/lint/test/test_codespell.py37
-rw-r--r--tools/lint/test/test_eslint.py99
-rw-r--r--tools/lint/test/test_file_license.py23
-rw-r--r--tools/lint/test/test_file_perm.py35
-rw-r--r--tools/lint/test/test_file_whitespace.py51
-rw-r--r--tools/lint/test/test_fluent_lint.py156
-rw-r--r--tools/lint/test/test_lintpref.py16
-rw-r--r--tools/lint/test/test_manifest_alpha.py33
-rw-r--r--tools/lint/test/test_perfdocs.py858
-rw-r--r--tools/lint/test/test_perfdocs_generation.py297
-rw-r--r--tools/lint/test/test_perfdocs_helpers.py206
-rw-r--r--tools/lint/test/test_rst.py25
-rw-r--r--tools/lint/test/test_ruff.py39
-rw-r--r--tools/lint/test/test_rustfmt.py70
-rw-r--r--tools/lint/test/test_shellcheck.py26
-rw-r--r--tools/lint/test/test_stylelint.py65
-rw-r--r--tools/lint/test/test_trojan_source.py25
-rw-r--r--tools/lint/test/test_updatebot.py44
-rw-r--r--tools/lint/test/test_yaml.py28
119 files changed, 3837 insertions, 0 deletions
diff --git a/tools/lint/test-manifest-alpha.yml b/tools/lint/test-manifest-alpha.yml
new file mode 100644
index 0000000000..cd4761e4b6
--- /dev/null
+++ b/tools/lint/test-manifest-alpha.yml
@@ -0,0 +1,19 @@
+---
+test-manifest-alpha:
+ description: Mochitest manifest tests should be in alphabetical order.
+ exclude:
+ - "**/application.ini"
+ - "**/l10n.ini"
+ - "**/xpcshell.ini"
+ - "**/python.ini"
+ - "**/manifest.ini"
+ - dom/canvas/test/webgl-conf/mochitest-errata.ini
+ - python/mozbuild/mozbuild/test/backend/data
+ - testing/mozbase/manifestparser/tests
+ - testing/web-platform
+ - xpcom/tests/unit/data
+ extensions: ['ini']
+ type: external
+ payload: test-manifest-alpha:lint
+ support-files:
+ - 'tools/lint/test-manifest-alpha/error-level-manifests.yml'
diff --git a/tools/lint/test-manifest-alpha/__init__.py b/tools/lint/test-manifest-alpha/__init__.py
new file mode 100644
index 0000000000..87d6ce5c5d
--- /dev/null
+++ b/tools/lint/test-manifest-alpha/__init__.py
@@ -0,0 +1,77 @@
+# 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 difflib
+import os
+import sys
+
+import yaml
+from manifestparser import TestManifest
+from mozlint import result
+from mozlint.pathutils import expand_exclusions
+from mozpack import path as mozpath
+
+# Since this linter matches on all files with .ini extensions, we can omit
+# those extensions from this allowlist, which makes it easier to match
+# against variants like "mochitest-serviceworker.ini".
+FILENAME_ALLOWLIST = ["mochitest", "browser", "chrome", "a11y"]
+
+here = os.path.abspath(os.path.dirname(__file__))
+ERROR_LEVEL_MANIFESTS_PATH = os.path.join(here, "error-level-manifests.yml")
+
+
+def lint(paths, config, fix=None, **lintargs):
+ try:
+ with open(ERROR_LEVEL_MANIFESTS_PATH) as f:
+ error_level_manifests = yaml.safe_load(f)
+ except (IOError, ValueError) as e:
+ print("{}: error:\n {}".format(ERROR_LEVEL_MANIFESTS_PATH, e), file=sys.stderr)
+ sys.exit(1)
+
+ topsrcdir = lintargs["root"]
+
+ results = []
+ file_names = list(expand_exclusions(paths, config, lintargs["root"]))
+
+ for file_name in file_names:
+ name = os.path.basename(file_name)
+ if not any(name.startswith(allowed) for allowed in FILENAME_ALLOWLIST):
+ continue
+
+ manifest = TestManifest(manifests=(file_name,), strict=False)
+
+ test_names = [test["name"] for test in manifest.tests]
+ sorted_test_names = sorted(test_names)
+
+ if test_names != sorted_test_names:
+ rel_file_path = mozpath.relpath(file_name, topsrcdir)
+ level = "warning"
+
+ if (mozpath.normsep(rel_file_path) in error_level_manifests) or (
+ any(
+ mozpath.match(rel_file_path, e)
+ for e in error_level_manifests
+ if "*" in e
+ )
+ ):
+ level = "error"
+
+ diff_instance = difflib.Differ()
+ diff_result = diff_instance.compare(test_names, sorted_test_names)
+ diff_list = list(diff_result)
+
+ res = {
+ "path": rel_file_path,
+ "lineno": 0,
+ "column": 0,
+ "message": (
+ "The mochitest test manifest is not in alphabetical order. "
+ "Expected ordering: \n\n%s\n\n" % "\n".join(sorted_test_names)
+ ),
+ "level": level,
+ "diff": "\n".join(diff_list),
+ }
+ results.append(result.from_config(config, **res))
+
+ return {"results": results, "fixed": 0}
diff --git a/tools/lint/test-manifest-alpha/error-level-manifests.yml b/tools/lint/test-manifest-alpha/error-level-manifests.yml
new file mode 100644
index 0000000000..90c091a3b8
--- /dev/null
+++ b/tools/lint/test-manifest-alpha/error-level-manifests.yml
@@ -0,0 +1,7 @@
+---
+# This file contains a list of manifest files or directory patterns that have
+# have been put into alphabetical order already. Items in this list will
+# cause the test-manifest-alpha linter to use the Error level rather than
+# the Warning level.
+
+- browser/**
diff --git a/tools/lint/test-manifest-disable.yml b/tools/lint/test-manifest-disable.yml
new file mode 100644
index 0000000000..7d93889704
--- /dev/null
+++ b/tools/lint/test-manifest-disable.yml
@@ -0,0 +1,16 @@
+---
+no-comment-disable:
+ description: >
+ "Use 'disabled=<reason>' to disable a test instead of a
+ comment"
+ include: ['.']
+ exclude:
+ - "**/application.ini"
+ - "**/l10n.ini"
+ - dom/canvas/test/webgl-conf/mochitest-errata.ini
+ - testing/mozbase/manifestparser/tests
+ - testing/web-platform
+ - xpcom/tests/unit/data
+ extensions: ['ini']
+ type: regex
+ payload: ^[ \t]*(#|;)[ \t]*\[
diff --git a/tools/lint/test-manifest-skip-if.yml b/tools/lint/test-manifest-skip-if.yml
new file mode 100644
index 0000000000..7ad1bd717c
--- /dev/null
+++ b/tools/lint/test-manifest-skip-if.yml
@@ -0,0 +1,18 @@
+---
+multiline-skip-if:
+ description: Conditions joined by || should be on separate lines
+ hint: |
+ skip-if =
+ <condition one> # reason
+ <condition two> # reason
+ exclude:
+ - "**/application.ini"
+ - "**/l10n.ini"
+ - dom/canvas/test/webgl-conf/mochitest-errata.ini
+ - testing/mozbase/manifestparser/tests
+ - testing/web-platform
+ - xpcom/tests/unit/data
+ extensions: ['ini']
+ level: warning
+ type: regex
+ payload: '^\s*(skip|fail)-if\s*=[^(]*\|\|'
diff --git a/tools/lint/test/conftest.py b/tools/lint/test/conftest.py
new file mode 100644
index 0000000000..f8cf8a30b6
--- /dev/null
+++ b/tools/lint/test/conftest.py
@@ -0,0 +1,305 @@
+import logging
+import os
+import pathlib
+import sys
+from collections import defaultdict
+
+import pytest
+from mozbuild.base import MozbuildObject
+from mozlint.parser import Parser
+from mozlint.pathutils import findobject
+from mozlint.result import ResultSummary
+from mozlog.structuredlog import StructuredLogger
+from mozpack import path
+
+here = path.abspath(path.dirname(__file__))
+build = MozbuildObject.from_environment(cwd=here, virtualenv_name="python-test")
+
+lintdir = path.dirname(here)
+sys.path.insert(0, lintdir)
+logger = logging.getLogger("mozlint")
+
+
+def pytest_generate_tests(metafunc):
+ """Finds, loads and returns the config for the linter name specified by the
+ LINTER global variable in the calling module.
+
+ This implies that each test file (that uses this fixture) should only be
+ used to test a single linter. If no LINTER variable is defined, the test
+ will fail.
+ """
+ if "config" in metafunc.fixturenames:
+ if not hasattr(metafunc.module, "LINTER"):
+ pytest.fail(
+ "'config' fixture used from a module that didn't set the LINTER variable"
+ )
+
+ name = metafunc.module.LINTER
+ config_path = path.join(lintdir, "{}.yml".format(name))
+ parser = Parser(build.topsrcdir)
+ configs = parser.parse(config_path)
+ config_names = {config["name"] for config in configs}
+
+ marker = metafunc.definition.get_closest_marker("lint_config")
+ if marker:
+ config_name = marker.kwargs["name"]
+ if config_name not in config_names:
+ pytest.fail(f"lint config {config_name} not present in {name}.yml")
+ configs = [
+ config for config in configs if config["name"] == marker.kwargs["name"]
+ ]
+
+ ids = [config["name"] for config in configs]
+ metafunc.parametrize("config", configs, ids=ids)
+
+
+@pytest.fixture(scope="module")
+def root(request):
+ """Return the root directory for the files of the linter under test.
+
+ For example, with LINTER=flake8 this would be tools/lint/test/files/flake8.
+ """
+ if not hasattr(request.module, "LINTER"):
+ pytest.fail(
+ "'root' fixture used from a module that didn't set the LINTER variable"
+ )
+ return path.join(here, "files", request.module.LINTER)
+
+
+@pytest.fixture(scope="module")
+def paths(root):
+ """Return a function that can resolve file paths relative to the linter
+ under test.
+
+ Can be used like `paths('foo.py', 'bar/baz')`. This will return a list of
+ absolute paths under the `root` files directory.
+ """
+
+ def _inner(*paths):
+ if not paths:
+ return [root]
+ return [path.normpath(path.join(root, p)) for p in paths]
+
+ return _inner
+
+
+@pytest.fixture(autouse=True)
+def run_setup(config):
+ """Make sure that if the linter named in the LINTER global variable has a
+ setup function, it gets called before running the tests.
+ """
+ if "setup" not in config:
+ return
+
+ if config["name"] == "clang-format":
+ # Skip the setup for the clang-format linter, as it requires a Mach context
+ # (which we may not have if pytest is invoked directly).
+ return
+
+ log = logging.LoggerAdapter(
+ logger, {"lintname": config.get("name"), "pid": os.getpid()}
+ )
+
+ func = findobject(config["setup"])
+ func(
+ build.topsrcdir,
+ virtualenv_manager=build.virtualenv_manager,
+ virtualenv_bin_path=build.virtualenv_manager.bin_path,
+ log=log,
+ )
+
+
+@pytest.fixture
+def lint(config, root, request):
+ """Find and return the 'lint' function for the external linter named in the
+ LINTER global variable.
+
+ This will automatically pass in the 'config' and 'root' arguments if not
+ specified.
+ """
+
+ if hasattr(request.module, "fixed"):
+ request.module.fixed = 0
+
+ try:
+ func = findobject(config["payload"])
+ except (ImportError, ValueError):
+ pytest.fail(
+ "could not resolve a lint function from '{}'".format(config["payload"])
+ )
+
+ ResultSummary.root = root
+
+ def wrapper(paths, config=config, root=root, collapse_results=False, **lintargs):
+ logger.setLevel(logging.DEBUG)
+ lintargs["log"] = logging.LoggerAdapter(
+ logger, {"lintname": config.get("name"), "pid": os.getpid()}
+ )
+
+ results = func(paths, config, root=root, **lintargs)
+ if hasattr(request.module, "fixed") and isinstance(results, dict):
+ request.module.fixed += results["fixed"]
+
+ if isinstance(results, dict):
+ results = results["results"]
+
+ if isinstance(results, (list, tuple)):
+ results = sorted(results)
+
+ if not collapse_results:
+ return results
+
+ ret = defaultdict(list)
+ for r in results:
+ ret[r.relpath].append(r)
+ return ret
+
+ return wrapper
+
+
+@pytest.fixture
+def structuredlog_lint(config, root, logger=None):
+ """Find and return the 'lint' function for the external linter named in the
+ LINTER global variable. This variant of the lint function is for linters that
+ use the 'structuredlog' type.
+
+ This will automatically pass in the 'config' and 'root' arguments if not
+ specified.
+ """
+ try:
+ func = findobject(config["payload"])
+ except (ImportError, ValueError):
+ pytest.fail(
+ "could not resolve a lint function from '{}'".format(config["payload"])
+ )
+
+ ResultSummary.root = root
+
+ if not logger:
+ logger = structured_logger()
+
+ def wrapper(
+ paths,
+ config=config,
+ root=root,
+ logger=logger,
+ collapse_results=False,
+ **lintargs,
+ ):
+ lintargs["log"] = logging.LoggerAdapter(
+ logger, {"lintname": config.get("name"), "pid": os.getpid()}
+ )
+ results = func(paths, config, root=root, logger=logger, **lintargs)
+ if not collapse_results:
+ return results
+
+ ret = defaultdict(list)
+ for r in results:
+ ret[r.path].append(r)
+ return ret
+
+ return wrapper
+
+
+@pytest.fixture
+def global_lint(config, root, request):
+ try:
+ func = findobject(config["payload"])
+ except (ImportError, ValueError):
+ pytest.fail(
+ "could not resolve a lint function from '{}'".format(config["payload"])
+ )
+
+ ResultSummary.root = root
+
+ def wrapper(config=config, root=root, collapse_results=False, **lintargs):
+ logger.setLevel(logging.DEBUG)
+ lintargs["log"] = logging.LoggerAdapter(
+ logger, {"lintname": config.get("name"), "pid": os.getpid()}
+ )
+ results = func(config, root=root, **lintargs)
+ if hasattr(request.module, "fixed") and isinstance(results, dict):
+ request.module.fixed += results["fixed"]
+
+ if isinstance(results, dict):
+ results = results["results"]
+
+ if isinstance(results, (list, tuple)):
+ results = sorted(results)
+
+ if not collapse_results:
+ return results
+
+ ret = defaultdict(list)
+ for r in results:
+ ret[r.relpath].append(r)
+ return ret
+
+ return wrapper
+
+
+@pytest.fixture
+def create_temp_file(tmpdir):
+ def inner(contents, name=None):
+ name = name or "temp.py"
+ path = tmpdir.join(name)
+ path.write(contents)
+ return path.strpath
+
+ return inner
+
+
+@pytest.fixture
+def structured_logger():
+ return StructuredLogger("logger")
+
+
+@pytest.fixture
+def perfdocs_sample():
+ from test_perfdocs import (
+ DYNAMIC_SAMPLE_CONFIG,
+ SAMPLE_CONFIG,
+ SAMPLE_INI,
+ SAMPLE_TEST,
+ temp_dir,
+ temp_file,
+ )
+
+ with temp_dir() as tmpdir:
+ suite_dir = pathlib.Path(tmpdir, "suite")
+ raptor_dir = pathlib.Path(tmpdir, "raptor")
+ raptor_suitedir = pathlib.Path(tmpdir, "raptor", "suite")
+ raptor_another_suitedir = pathlib.Path(tmpdir, "raptor", "another_suite")
+ perfdocs_dir = pathlib.Path(tmpdir, "perfdocs")
+
+ perfdocs_dir.mkdir(parents=True, exist_ok=True)
+ suite_dir.mkdir(parents=True, exist_ok=True)
+ raptor_dir.mkdir(parents=True, exist_ok=True)
+ raptor_suitedir.mkdir(parents=True, exist_ok=True)
+ raptor_another_suitedir.mkdir(parents=True, exist_ok=True)
+
+ with temp_file(
+ "perftest.ini", tempdir=suite_dir, content="[perftest_sample.js]"
+ ) as tmpmanifest, temp_file(
+ "raptor_example1.ini", tempdir=raptor_suitedir, content=SAMPLE_INI
+ ) as tmpexample1manifest, temp_file(
+ "raptor_example2.ini", tempdir=raptor_another_suitedir, content=SAMPLE_INI
+ ) as tmpexample2manifest, temp_file(
+ "perftest_sample.js", tempdir=suite_dir, content=SAMPLE_TEST
+ ) as tmptest, temp_file(
+ "config.yml", tempdir=perfdocs_dir, content=SAMPLE_CONFIG
+ ) as tmpconfig, temp_file(
+ "config_2.yml", tempdir=perfdocs_dir, content=DYNAMIC_SAMPLE_CONFIG
+ ) as tmpconfig_2, temp_file(
+ "index.rst", tempdir=perfdocs_dir, content="{documentation}"
+ ) as tmpindex:
+ yield {
+ "top_dir": tmpdir,
+ "manifest": {"path": tmpmanifest},
+ "example1_manifest": tmpexample1manifest,
+ "example2_manifest": tmpexample2manifest,
+ "test": tmptest,
+ "config": tmpconfig,
+ "config_2": tmpconfig_2,
+ "index": tmpindex,
+ }
diff --git a/tools/lint/test/files/android-format/Bad.java b/tools/lint/test/files/android-format/Bad.java
new file mode 100644
index 0000000000..13aa5d49d5
--- /dev/null
+++ b/tools/lint/test/files/android-format/Bad.java
@@ -0,0 +1,8 @@
+package org.mozilla.geckoview;
+
+import java.util.Arrays;
+
+public class Bad {
+ public static void main() {
+ }
+}
diff --git a/tools/lint/test/files/android-format/Main.kt b/tools/lint/test/files/android-format/Main.kt
new file mode 100644
index 0000000000..a172cf71ee
--- /dev/null
+++ b/tools/lint/test/files/android-format/Main.kt
@@ -0,0 +1,7 @@
+package org.mozilla.geckoview
+
+import java.util.Arrays;
+
+fun main() {
+println("Hello")
+}
diff --git a/tools/lint/test/files/android-format/build.gradle b/tools/lint/test/files/android-format/build.gradle
new file mode 100644
index 0000000000..6d2aff6d60
--- /dev/null
+++ b/tools/lint/test/files/android-format/build.gradle
@@ -0,0 +1 @@
+buildDir "${topobjdir}/gradle/build/tools/lint/test/files/android-format"
diff --git a/tools/lint/test/files/black/bad.py b/tools/lint/test/files/black/bad.py
new file mode 100644
index 0000000000..0a50df4dd9
--- /dev/null
+++ b/tools/lint/test/files/black/bad.py
@@ -0,0 +1,6 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+print (
+ "test"
+ )
diff --git a/tools/lint/test/files/black/invalid.py b/tools/lint/test/files/black/invalid.py
new file mode 100644
index 0000000000..079ecbad30
--- /dev/null
+++ b/tools/lint/test/files/black/invalid.py
@@ -0,0 +1,4 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+print(
diff --git a/tools/lint/test/files/clang-format/bad/bad.cpp b/tools/lint/test/files/clang-format/bad/bad.cpp
new file mode 100644
index 0000000000..f08a83f795
--- /dev/null
+++ b/tools/lint/test/files/clang-format/bad/bad.cpp
@@ -0,0 +1,6 @@
+int main ( ) {
+
+return 0;
+
+
+}
diff --git a/tools/lint/test/files/clang-format/bad/bad2.c b/tools/lint/test/files/clang-format/bad/bad2.c
new file mode 100644
index 0000000000..9792e85071
--- /dev/null
+++ b/tools/lint/test/files/clang-format/bad/bad2.c
@@ -0,0 +1,8 @@
+#include "bad2.h"
+
+
+int bad2() {
+ int a =2;
+ return a;
+
+}
diff --git a/tools/lint/test/files/clang-format/bad/bad2.h b/tools/lint/test/files/clang-format/bad/bad2.h
new file mode 100644
index 0000000000..a35d49d7e7
--- /dev/null
+++ b/tools/lint/test/files/clang-format/bad/bad2.h
@@ -0,0 +1 @@
+int bad2(void );
diff --git a/tools/lint/test/files/clang-format/bad/good.cpp b/tools/lint/test/files/clang-format/bad/good.cpp
new file mode 100644
index 0000000000..76e8197013
--- /dev/null
+++ b/tools/lint/test/files/clang-format/bad/good.cpp
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/tools/lint/test/files/clang-format/good/foo.cpp b/tools/lint/test/files/clang-format/good/foo.cpp
new file mode 100644
index 0000000000..76e8197013
--- /dev/null
+++ b/tools/lint/test/files/clang-format/good/foo.cpp
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/tools/lint/test/files/clippy/test1/Cargo.toml b/tools/lint/test/files/clippy/test1/Cargo.toml
new file mode 100644
index 0000000000..92d5072eca
--- /dev/null
+++ b/tools/lint/test/files/clippy/test1/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "hello_world" # the name of the package
+version = "0.1.0" # the current version, obeying semver
+authors = ["Alice <a@example.com>", "Bob <b@example.com>"]
+
+[[bin]]
+name ="good"
+path = "good.rs"
+
+[[bin]]
+name ="bad"
+path = "bad.rs"
+
+[[bin]]
+name ="bad2"
+path = "bad2.rs"
+
diff --git a/tools/lint/test/files/clippy/test1/bad.rs b/tools/lint/test/files/clippy/test1/bad.rs
new file mode 100644
index 0000000000..c403fba603
--- /dev/null
+++ b/tools/lint/test/files/clippy/test1/bad.rs
@@ -0,0 +1,14 @@
+fn main() {
+ // Statements here are executed when the compiled binary is called
+
+ // Print text to the console
+ println!("Hello World!");
+ // Clippy detects this as a swap and considers this as an error
+ let mut a=1;
+ let mut b=1;
+
+ a = b;
+ b = a;
+
+
+}
diff --git a/tools/lint/test/files/clippy/test1/bad2.rs b/tools/lint/test/files/clippy/test1/bad2.rs
new file mode 100644
index 0000000000..bf488bbe72
--- /dev/null
+++ b/tools/lint/test/files/clippy/test1/bad2.rs
@@ -0,0 +1,14 @@
+fn main() {
+ // Statements here are executed when the compiled binary is called
+
+ // Print text to the console
+ println!("Hello World!");
+ let mut a;
+ let mut b=1;
+ let mut vec = vec![1, 2];
+
+ for x in 5..10 - 5 {
+ a = x;
+ }
+
+ }
diff --git a/tools/lint/test/files/clippy/test1/good.rs b/tools/lint/test/files/clippy/test1/good.rs
new file mode 100644
index 0000000000..9bcaee67b7
--- /dev/null
+++ b/tools/lint/test/files/clippy/test1/good.rs
@@ -0,0 +1,6 @@
+fn main() {
+ // Statements here are executed when the compiled binary is called
+
+ // Print text to the console
+ println!("Hello World!");
+}
diff --git a/tools/lint/test/files/clippy/test2/Cargo.lock b/tools/lint/test/files/clippy/test2/Cargo.lock
new file mode 100644
index 0000000000..6b2bc69eeb
--- /dev/null
+++ b/tools/lint/test/files/clippy/test2/Cargo.lock
@@ -0,0 +1,5 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "hello_world_2"
+version = "0.2.0"
diff --git a/tools/lint/test/files/clippy/test2/Cargo.toml b/tools/lint/test/files/clippy/test2/Cargo.toml
new file mode 100644
index 0000000000..b0ac992088
--- /dev/null
+++ b/tools/lint/test/files/clippy/test2/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "hello_world_2" # the name of the package
+version = "0.2.0" # the current version, obeying semver
+authors = ["Alice <a@example.com>", "Bob <b@example.com>"]
+
+[[bin]]
+name = "fake_lib1"
+path = "src/bad_1.rs"
diff --git a/tools/lint/test/files/clippy/test2/src/bad_1.rs b/tools/lint/test/files/clippy/test2/src/bad_1.rs
new file mode 100644
index 0000000000..2fe0630202
--- /dev/null
+++ b/tools/lint/test/files/clippy/test2/src/bad_1.rs
@@ -0,0 +1,15 @@
+mod bad_2;
+
+fn main() {
+ // Statements here are executed when the compiled binary is called
+
+ // Print text to the console
+ println!("Hello World!");
+ // Clippy detects this as a swap and considers this as an error
+ let mut a=1;
+ let mut b=1;
+
+ a = b;
+ b = a;
+
+}
diff --git a/tools/lint/test/files/clippy/test2/src/bad_2.rs b/tools/lint/test/files/clippy/test2/src/bad_2.rs
new file mode 100644
index 0000000000..f77de330b4
--- /dev/null
+++ b/tools/lint/test/files/clippy/test2/src/bad_2.rs
@@ -0,0 +1,17 @@
+fn foo() {
+ // Statements here are executed when the compiled binary is called
+
+ // Print text to the console
+ println!("Hello World!");
+ let mut a;
+ let mut b=1;
+ let mut vec = Vec::new();
+ vec.push(1);
+ vec.push(2);
+
+
+ for x in 5..10 - 5 {
+ a = x;
+ }
+
+ }
diff --git a/tools/lint/test/files/codespell/ignore.rst b/tools/lint/test/files/codespell/ignore.rst
new file mode 100644
index 0000000000..1371d07054
--- /dev/null
+++ b/tools/lint/test/files/codespell/ignore.rst
@@ -0,0 +1,5 @@
+This is a file with some typos and informations.
+But also testing false positive like optin (because this isn't always option)
+or stuff related to our coding style like:
+aparent (aParent).
+but detects mistakes like mozila
diff --git a/tools/lint/test/files/eslint/good.js b/tools/lint/test/files/eslint/good.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tools/lint/test/files/eslint/good.js
diff --git a/tools/lint/test/files/eslint/import/bad_import.js b/tools/lint/test/files/eslint/import/bad_import.js
new file mode 100644
index 0000000000..e2a8ec8de1
--- /dev/null
+++ b/tools/lint/test/files/eslint/import/bad_import.js
@@ -0,0 +1 @@
+/* import-globals-from notpresent/notpresent.js */
diff --git a/tools/lint/test/files/eslint/nolint/foo.txt b/tools/lint/test/files/eslint/nolint/foo.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tools/lint/test/files/eslint/nolint/foo.txt
diff --git a/tools/lint/test/files/eslint/subdir/bad.js b/tools/lint/test/files/eslint/subdir/bad.js
new file mode 100644
index 0000000000..9d2dd18f39
--- /dev/null
+++ b/tools/lint/test/files/eslint/subdir/bad.js
@@ -0,0 +1,2 @@
+// Missing semicolon
+let foo = "bar"
diff --git a/tools/lint/test/files/eslint/testprettierignore b/tools/lint/test/files/eslint/testprettierignore
new file mode 100644
index 0000000000..c2df665174
--- /dev/null
+++ b/tools/lint/test/files/eslint/testprettierignore
@@ -0,0 +1 @@
+# Intentionally empty file.
diff --git a/tools/lint/test/files/file-perm/maybe-shebang/bad.js b/tools/lint/test/files/file-perm/maybe-shebang/bad.js
new file mode 100755
index 0000000000..1a0b4c5fd6
--- /dev/null
+++ b/tools/lint/test/files/file-perm/maybe-shebang/bad.js
@@ -0,0 +1,2 @@
+# Nothing too
+
diff --git a/tools/lint/test/files/file-perm/maybe-shebang/good.js b/tools/lint/test/files/file-perm/maybe-shebang/good.js
new file mode 100755
index 0000000000..8149c0d4f3
--- /dev/null
+++ b/tools/lint/test/files/file-perm/maybe-shebang/good.js
@@ -0,0 +1,5 @@
+#!/usr/bin/env node
+
+
+# Nothing
+
diff --git a/tools/lint/test/files/file-perm/no-shebang/bad-shebang.c b/tools/lint/test/files/file-perm/no-shebang/bad-shebang.c
new file mode 100755
index 0000000000..7151678efa
--- /dev/null
+++ b/tools/lint/test/files/file-perm/no-shebang/bad-shebang.c
@@ -0,0 +1,2 @@
+#!/bin/bash
+int main() { return 0; }
diff --git a/tools/lint/test/files/file-perm/no-shebang/bad.c b/tools/lint/test/files/file-perm/no-shebang/bad.c
new file mode 100755
index 0000000000..76e8197013
--- /dev/null
+++ b/tools/lint/test/files/file-perm/no-shebang/bad.c
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/tools/lint/test/files/file-perm/no-shebang/bad.png b/tools/lint/test/files/file-perm/no-shebang/bad.png
new file mode 100755
index 0000000000..db3a5fda7e
--- /dev/null
+++ b/tools/lint/test/files/file-perm/no-shebang/bad.png
Binary files differ
diff --git a/tools/lint/test/files/file-perm/no-shebang/good.c b/tools/lint/test/files/file-perm/no-shebang/good.c
new file mode 100644
index 0000000000..76e8197013
--- /dev/null
+++ b/tools/lint/test/files/file-perm/no-shebang/good.c
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/tools/lint/test/files/file-whitespace/bad-newline.c b/tools/lint/test/files/file-whitespace/bad-newline.c
new file mode 100644
index 0000000000..3746a0add3
--- /dev/null
+++ b/tools/lint/test/files/file-whitespace/bad-newline.c
@@ -0,0 +1,3 @@
+int main() { return 0; }
+
+ \ No newline at end of file
diff --git a/tools/lint/test/files/file-whitespace/bad-windows.c b/tools/lint/test/files/file-whitespace/bad-windows.c
new file mode 100644
index 0000000000..70d4c697b9
--- /dev/null
+++ b/tools/lint/test/files/file-whitespace/bad-windows.c
@@ -0,0 +1,3 @@
+int main(){
+ return 42;
+}
diff --git a/tools/lint/test/files/file-whitespace/bad.c b/tools/lint/test/files/file-whitespace/bad.c
new file mode 100644
index 0000000000..4309b1f55d
--- /dev/null
+++ b/tools/lint/test/files/file-whitespace/bad.c
@@ -0,0 +1,3 @@
+int main() {
+return 0;
+}
diff --git a/tools/lint/test/files/file-whitespace/bad.js b/tools/lint/test/files/file-whitespace/bad.js
new file mode 100644
index 0000000000..3441696ef1
--- /dev/null
+++ b/tools/lint/test/files/file-whitespace/bad.js
@@ -0,0 +1,3 @@
+# Nothing too
+
+
diff --git a/tools/lint/test/files/file-whitespace/good.c b/tools/lint/test/files/file-whitespace/good.c
new file mode 100644
index 0000000000..76e8197013
--- /dev/null
+++ b/tools/lint/test/files/file-whitespace/good.c
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/tools/lint/test/files/file-whitespace/good.js b/tools/lint/test/files/file-whitespace/good.js
new file mode 100644
index 0000000000..8149c0d4f3
--- /dev/null
+++ b/tools/lint/test/files/file-whitespace/good.js
@@ -0,0 +1,5 @@
+#!/usr/bin/env node
+
+
+# Nothing
+
diff --git a/tools/lint/test/files/fluent-lint/bad.ftl b/tools/lint/test/files/fluent-lint/bad.ftl
new file mode 100644
index 0000000000..ebc0e1a602
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/bad.ftl
@@ -0,0 +1,44 @@
+Blah-blah = Uppercase letters in identifiers are not permitted.
+blah-blah = This is a legal identifier.
+blah_blah = Underscores in identifiers are not permitted.
+
+bad-apostrophe-1 = The bee's knees
+bad-apostrophe-end-1 = The bees' knees
+bad-apostrophe-2 = The bee‘s knees
+bad-single-quote = 'The bee’s knees'
+ok-apostrophe = The bee’s knees
+ok-single-quote = ‘The bee’s knees’
+bad-double-quote = "The bee’s knees"
+good-double-quote = “The bee’s knees”
+bad-ellipsis = The bee’s knees...
+good-ellipsis = The bee’s knees…
+
+embedded-tag = Read more about <a data-l10n-name="privacy-policy"> our privacy policy </a>.
+bad-embedded-tag = Read more about <a data-l10n-name="privacy-policy"> our 'privacy' policy </a>.
+
+Invalid_Id = This identifier is in the exclusions file and will not cause an error.
+
+good-has-attributes =
+ .accessKey = Attribute identifiers are not checked.
+
+bad-has-attributes =
+ .accessKey = Attribute 'values' are checked.
+
+good-function-call = Last modified: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") }
+
+# $engineName (String) - The engine name that will currently be used for the private window.
+good-variable-identifier = { $engineName } is your default search engine in Private Windows
+
+short-id = I am too short
+
+identifiers-in-selectors-should-be-ignored =
+ .label = { $tabCount ->
+ [1] Send Tab to Device
+ [UPPERCASE] Send Tab to Device
+ *[other] Send { $tabCount } Tabs to Device
+ }
+
+this-message-reference-is-ignored =
+ .label = { menu-quit.label }
+
+ok-message-with-html-and-var = This is a <a href="{ $url }">link</a>
diff --git a/tools/lint/test/files/fluent-lint/brand-names-excluded.ftl b/tools/lint/test/files/fluent-lint/brand-names-excluded.ftl
new file mode 100644
index 0000000000..9f3afa28b8
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/brand-names-excluded.ftl
@@ -0,0 +1,2 @@
+# Comment
+bad-firefox1 = Welcome to Firefox
diff --git a/tools/lint/test/files/fluent-lint/brand-names.ftl b/tools/lint/test/files/fluent-lint/brand-names.ftl
new file mode 100644
index 0000000000..c338d920ca
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/brand-names.ftl
@@ -0,0 +1,30 @@
+bad-firefox1 = Welcome to Firefox
+
+# Comment should be ignored when displaying the offset of the error
+bad-firefox2 = Welcome to Firefox again
+bad-firefox2b = <span>Welcome to Firefox<span> again
+bad-firefox3 = <b>Firefox</b>
+bad-firefox-excluded = <b>Firefox</b>
+
+bad-mozilla1 = Welcome to Mozilla
+bad-mozilla2 = Welcome to Mozilla again
+bad-mozilla2b = <span>Welcome to Mozilla</span> again
+bad-mozilla3 = <b>Mozilla</b>
+
+bad-thunderbird1 = Welcome to Thunderbird
+bad-thunderbird2 = <span>Welcome to Thunderbird</span> again
+bad-thunderbird3 = <b>Thunderbird</b>
+
+good-firefox1 = Welcome to { -brand-firefox }
+good-firefox2 = Welcome to { firefox-message }
+
+good-mozilla1 = Welcome to { -brand-mozilla }
+good-mozilla2 = Welcome to { mozilla-message }
+
+good-thunderbird1 = Welcome to { -brand-thunderbird }
+good-thunderbird2 = Welcome to { thunderbird-message }
+
+# There are no brand checks on terms
+-brand-firefox = Firefox
+
+bland-message = No brands here.
diff --git a/tools/lint/test/files/fluent-lint/comment-group1.ftl b/tools/lint/test/files/fluent-lint/comment-group1.ftl
new file mode 100644
index 0000000000..32c19dc441
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/comment-group1.ftl
@@ -0,0 +1,35 @@
+# 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/.
+
+### Test group comments.
+
+fake-identifier-1 = Fake text
+
+## Pass: This group comment has proper spacing.
+
+fake-identifier-2 = Fake text
+## Fail: (GC03) Group comments should have an empty line before them.
+
+fake-identifier-3 = Fake text
+
+## Fail: (GC02) Group comments should have an empty line after them.
+fake-identifier-4 = Fake text
+
+## Pass: A single comment is fine.
+
+## Fail: (GC04) A group comment must be followed by at least one message.
+
+fake-identifier-5 = Fake text
+
+
+## Fail: (GC03) Only allow 1 line above.
+
+fake-identifier-6 = Fake text
+
+## Fail: (GC02) Only allow 1 line below.
+
+
+fake-identifier-6 = Fake text
+
+## Fail: (GC01) Group comments should not be at the end of a file.
diff --git a/tools/lint/test/files/fluent-lint/comment-group2.ftl b/tools/lint/test/files/fluent-lint/comment-group2.ftl
new file mode 100644
index 0000000000..47d29fa211
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/comment-group2.ftl
@@ -0,0 +1,15 @@
+# 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/.
+
+### Test group comments.
+
+## Pass: This group comment is followed by a term
+
+-fake-term = Fake text
+
+## Pass: The last group comment is allowed to be an empty ##
+
+fake-identifier-1 = Fake text
+
+##
diff --git a/tools/lint/test/files/fluent-lint/comment-resource1.ftl b/tools/lint/test/files/fluent-lint/comment-resource1.ftl
new file mode 100644
index 0000000000..f5d5e53d59
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/comment-resource1.ftl
@@ -0,0 +1,11 @@
+# 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/.
+
+### Pass: This is a resource comment with proper spacing.
+
+fake-identifier-1 = Fake text
+
+### Fail: (RC01) There should not be more than one resource comment
+
+fake-identifier-2 = Fake text
diff --git a/tools/lint/test/files/fluent-lint/comment-resource2.ftl b/tools/lint/test/files/fluent-lint/comment-resource2.ftl
new file mode 100644
index 0000000000..44a77f4e73
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/comment-resource2.ftl
@@ -0,0 +1,6 @@
+# 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/.
+### Fail: (RC03) There should be an empty line preceeding.
+
+fake-identifier-1 = Fake text
diff --git a/tools/lint/test/files/fluent-lint/comment-resource3.ftl b/tools/lint/test/files/fluent-lint/comment-resource3.ftl
new file mode 100644
index 0000000000..b261404380
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/comment-resource3.ftl
@@ -0,0 +1,6 @@
+# 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/.
+
+### Fail: (RC02) There should be an empty line following.
+fake-identifier-1 = Fake text
diff --git a/tools/lint/test/files/fluent-lint/comment-resource4.ftl b/tools/lint/test/files/fluent-lint/comment-resource4.ftl
new file mode 100644
index 0000000000..c24e8887f8
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/comment-resource4.ftl
@@ -0,0 +1,8 @@
+# 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/.
+
+
+### Fail: (RC03) There should be only one space above.
+
+fake-identifier-1 = Fake text
diff --git a/tools/lint/test/files/fluent-lint/comment-resource5.ftl b/tools/lint/test/files/fluent-lint/comment-resource5.ftl
new file mode 100644
index 0000000000..60d8e8c264
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/comment-resource5.ftl
@@ -0,0 +1,8 @@
+# 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/.
+
+### Fail: (RC02) There should be only one space below.
+
+
+fake-identifier-1 = Fake text
diff --git a/tools/lint/test/files/fluent-lint/comment-resource6.ftl b/tools/lint/test/files/fluent-lint/comment-resource6.ftl
new file mode 100644
index 0000000000..a2ca9abfe7
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/comment-resource6.ftl
@@ -0,0 +1,4 @@
+### Pass: Check two conditions.
+### 1. This is an edge case, but we shouldn't error if there is only a resource comment.
+### 2. Make sure this linter does not error if there is no license header. The license is
+### checked with `mach lint license`.
diff --git a/tools/lint/test/files/fluent-lint/comment-variables1.ftl b/tools/lint/test/files/fluent-lint/comment-variables1.ftl
new file mode 100644
index 0000000000..10de9de195
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/comment-variables1.ftl
@@ -0,0 +1,60 @@
+# 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/.
+
+### Test group comments.
+### $var doesn't count towards commented placeables
+
+message-without-comment = This string has a { $var }
+
+## Variables:
+## $foo-group (String): group level comment
+
+# Variables:
+# $foo1 (String): just text
+message-with-comment = This string has a { $foo1 }
+
+message-with-group-comment = This string has a { $foo-group }
+
+select-without-comment1 = {
+ $select1 ->
+ [t] Foo
+ *[s] Bar
+}
+
+select-without-comment2 = {
+ $select2 ->
+ [t] Foo { $select2 }
+ *[s] Bar
+}
+
+## Variables:
+## $select4 (Integer): a number
+
+# Variables:
+# $select3 (Integer): a number
+select-with-comment1 = {
+ $select3 ->
+ [t] Foo
+ *[s] Bar
+}
+
+select-with-group-comment1 = {
+ $select4 ->
+ [t] Foo { $select4 }
+ *[s] Bar
+}
+
+message-attribute-without-comment =
+ .label = This string as { $attr }
+
+# Variables:
+# $attr2 (String): just text
+message-attribute-with-comment =
+ .label = This string as { $attr2 }
+
+message-selection-function =
+ { PLATFORM() ->
+ [macos] foo
+ *[other] bar
+ }
diff --git a/tools/lint/test/files/fluent-lint/comment-variables2.ftl b/tools/lint/test/files/fluent-lint/comment-variables2.ftl
new file mode 100644
index 0000000000..2e9ae8e684
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/comment-variables2.ftl
@@ -0,0 +1,27 @@
+# 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/.
+
+## Terms are not checked for variables, no need for comments.
+
+-term-without-comment1 = { $term1 }
+-term-without-comment2 = {
+ $select2 ->
+ [t] Foo { $term2 }
+ *[s] Bar
+}
+
+# Testing that variable references from terms are not kept around when analyzing
+# standard messages (see bug 1812568).
+#
+# Variables:
+# $message1 (String) - Just text
+message-with-comment = This string has a { $message1 }
+
+# This comment is not necessary, just making sure it doesn't get carried over to
+# the following message which uses the same variable.
+#
+# Variables:
+# $term-message (string) - Text
+-term-with-variable = { $term-message }
+message-without-comment = This string has a { $term-message }
diff --git a/tools/lint/test/files/fluent-lint/excluded.ftl b/tools/lint/test/files/fluent-lint/excluded.ftl
new file mode 100644
index 0000000000..79fe509ad6
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/excluded.ftl
@@ -0,0 +1,6 @@
+# This file is used to test excluding paths from tests.
+Blah-blah = Uppercase letters in identifiers are not permitted.
+blah-blah = This is a legal identifier.
+blah_blah = Underscores in identifiers are not permitted.
+
+bad-apostrophe-1 = The bee's knees
diff --git a/tools/lint/test/files/fluent-lint/test-brands.ftl b/tools/lint/test/files/fluent-lint/test-brands.ftl
new file mode 100644
index 0000000000..1aa6e9d0e8
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/test-brands.ftl
@@ -0,0 +1,5 @@
+# These are brands used in the fluent-lint test
+
+-brand-first = Firefox
+-brand-second = Thunderbird
+-brand-third = Mozilla
diff --git a/tools/lint/test/files/fluent-lint/tools/lint/fluent-lint/exclusions.yml b/tools/lint/test/files/fluent-lint/tools/lint/fluent-lint/exclusions.yml
new file mode 100644
index 0000000000..1aecf8cedd
--- /dev/null
+++ b/tools/lint/test/files/fluent-lint/tools/lint/fluent-lint/exclusions.yml
@@ -0,0 +1,17 @@
+# 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/.
+---
+ID01:
+ messages:
+ - Invalid_Id
+ files:
+ - excluded.ftl
+ID02:
+ messages: []
+ files: []
+CO01:
+ messages:
+ - bad-firefox-excluded
+ files:
+ - brand-names-excluded.ftl
diff --git a/tools/lint/test/files/license/.eslintrc.js b/tools/lint/test/files/license/.eslintrc.js
new file mode 100644
index 0000000000..0449fdfa33
--- /dev/null
+++ b/tools/lint/test/files/license/.eslintrc.js
@@ -0,0 +1,5 @@
+
+// Dot file to verify that it works
+// without license
+
+"use strict";
diff --git a/tools/lint/test/files/license/bad.c b/tools/lint/test/files/license/bad.c
new file mode 100644
index 0000000000..76e8197013
--- /dev/null
+++ b/tools/lint/test/files/license/bad.c
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/tools/lint/test/files/license/bad.js b/tools/lint/test/files/license/bad.js
new file mode 100644
index 0000000000..5de1a72f1f
--- /dev/null
+++ b/tools/lint/test/files/license/bad.js
@@ -0,0 +1,6 @@
+/*
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Pulic Unknown License as published by
+ * the Free Software Foundation, version 3.
+ *
+ */
diff --git a/tools/lint/test/files/license/good-other.h b/tools/lint/test/files/license/good-other.h
new file mode 100644
index 0000000000..fb915e9b26
--- /dev/null
+++ b/tools/lint/test/files/license/good-other.h
@@ -0,0 +1,9 @@
+/*
+Permission to use, copy, modify, distribute and sell this software
+and its documentation for any purpose is hereby granted without fee,
+provided that the above copyright notice appear in all copies and
+that both that copyright notice and this permission notice appear
+in supporting documentation. Samphan Raruenrom makes no
+representations about the suitability of this software for any
+purpose. It is provided "as is" without express or implied warranty.
+*/
diff --git a/tools/lint/test/files/license/good.c b/tools/lint/test/files/license/good.c
new file mode 100644
index 0000000000..d1a6827fb1
--- /dev/null
+++ b/tools/lint/test/files/license/good.c
@@ -0,0 +1,8 @@
+
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+int main() { return 0; }
diff --git a/tools/lint/test/files/license/good.js b/tools/lint/test/files/license/good.js
new file mode 100644
index 0000000000..d10ae3a8d5
--- /dev/null
+++ b/tools/lint/test/files/license/good.js
@@ -0,0 +1,7 @@
+#!/usr/bin/env node
+/* 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/. */
+
+# Nothing
+
diff --git a/tools/lint/test/files/lintpref/bad.js b/tools/lint/test/files/lintpref/bad.js
new file mode 100644
index 0000000000..ab55ba5dad
--- /dev/null
+++ b/tools/lint/test/files/lintpref/bad.js
@@ -0,0 +1,2 @@
+// Real test pref, matching value.
+pref("dom.webidl.test1", true);
diff --git a/tools/lint/test/files/lintpref/good.js b/tools/lint/test/files/lintpref/good.js
new file mode 100644
index 0000000000..0bf81c8f58
--- /dev/null
+++ b/tools/lint/test/files/lintpref/good.js
@@ -0,0 +1,6 @@
+// Fake prefs.
+pref("foo.bar", 1);
+pref("foo.baz", "1.234");
+
+// Real test pref, different value.
+pref("dom.webidl.test1", false);
diff --git a/tools/lint/test/files/rst/.dotfile.rst b/tools/lint/test/files/rst/.dotfile.rst
new file mode 100644
index 0000000000..be24e1d161
--- /dev/null
+++ b/tools/lint/test/files/rst/.dotfile.rst
@@ -0,0 +1,11 @@
+============
+Coding style
+==========
+
+foo bar
+~~~~~
+
+
+This file has error but should not be there
+as we don't analyze dot files
+
diff --git a/tools/lint/test/files/rst/bad.rst b/tools/lint/test/files/rst/bad.rst
new file mode 100644
index 0000000000..c9b60f613e
--- /dev/null
+++ b/tools/lint/test/files/rst/bad.rst
@@ -0,0 +1,20 @@
+============
+Coding style
+============
+
+
+This document attempts to explain the basic styles and patterns used in
+the Mozilla codebase. New code should try to conform to these standards,
+so it is as easy to maintain as existing code. There are exceptions, but
+it's still important to know the rules!
+
+
+Whitespace
+~~~~~~~~
+
+Line length
+~~~~~~~~~~~
+
+Line length
+~~~~~~~~~~~
+
diff --git a/tools/lint/test/files/rst/bad2.rst b/tools/lint/test/files/rst/bad2.rst
new file mode 100644
index 0000000000..81c35dde06
--- /dev/null
+++ b/tools/lint/test/files/rst/bad2.rst
@@ -0,0 +1,4 @@
+====
+Test
+===
+
diff --git a/tools/lint/test/files/rst/bad3.rst b/tools/lint/test/files/rst/bad3.rst
new file mode 100644
index 0000000000..b7e66e5c92
--- /dev/null
+++ b/tools/lint/test/files/rst/bad3.rst
@@ -0,0 +1,6 @@
+
+.. _When_Should_I_Use_a_Hashtable.3F:
+
+When Should I Use a Hashtable?
+------------------------------
+
diff --git a/tools/lint/test/files/rst/good.rst b/tools/lint/test/files/rst/good.rst
new file mode 100644
index 0000000000..fd12da85d3
--- /dev/null
+++ b/tools/lint/test/files/rst/good.rst
@@ -0,0 +1,11 @@
+============
+Coding style
+============
+
+
+This document attempts to explain the basic styles and patterns used in
+the Mozilla codebase. New code should try to conform to these standards,
+so it is as easy to maintain as existing code. There are exceptions, but
+it's still important to know the rules!
+
+
diff --git a/tools/lint/test/files/ruff/bad.py b/tools/lint/test/files/ruff/bad.py
new file mode 100644
index 0000000000..0015d7e7f9
--- /dev/null
+++ b/tools/lint/test/files/ruff/bad.py
@@ -0,0 +1,4 @@
+import distutils
+
+if not "foo" in "foobar":
+ print("oh no!")
diff --git a/tools/lint/test/files/ruff/ruff.toml b/tools/lint/test/files/ruff/ruff.toml
new file mode 100644
index 0000000000..34f5ca74a4
--- /dev/null
+++ b/tools/lint/test/files/ruff/ruff.toml
@@ -0,0 +1 @@
+# Empty config to force ruff to ignore the global one.
diff --git a/tools/lint/test/files/rustfmt/subdir/bad.rs b/tools/lint/test/files/rustfmt/subdir/bad.rs
new file mode 100644
index 0000000000..fb1746fafd
--- /dev/null
+++ b/tools/lint/test/files/rustfmt/subdir/bad.rs
@@ -0,0 +1,16 @@
+fn main() {
+ // Statements here are executed when the compiled binary is called
+
+ // Print text to the console
+ println!("Hello World!");
+ // Clippy detects this as a swap and considers this as an error
+ let mut a =
+ 1;
+ let mut b=1;
+
+ a =
+ b;
+ b = a;
+
+
+}
diff --git a/tools/lint/test/files/rustfmt/subdir/bad2.rs b/tools/lint/test/files/rustfmt/subdir/bad2.rs
new file mode 100644
index 0000000000..a4236a2de7
--- /dev/null
+++ b/tools/lint/test/files/rustfmt/subdir/bad2.rs
@@ -0,0 +1,17 @@
+fn main() {
+ // Statements here are executed when the compiled binary is called
+
+ // Print text to the console
+ println!("Hello World!");
+ let mut a;
+ let mut b=1;
+ let mut vec = Vec::new();
+ vec.push(1);
+ vec.push(2);
+
+
+ for x in 5..10 - 5 {
+ a = x;
+ }
+
+ }
diff --git a/tools/lint/test/files/rustfmt/subdir/good.rs b/tools/lint/test/files/rustfmt/subdir/good.rs
new file mode 100644
index 0000000000..9bcaee67b7
--- /dev/null
+++ b/tools/lint/test/files/rustfmt/subdir/good.rs
@@ -0,0 +1,6 @@
+fn main() {
+ // Statements here are executed when the compiled binary is called
+
+ // Print text to the console
+ println!("Hello World!");
+}
diff --git a/tools/lint/test/files/shellcheck/bad.sh b/tools/lint/test/files/shellcheck/bad.sh
new file mode 100644
index 0000000000..b2eb195558
--- /dev/null
+++ b/tools/lint/test/files/shellcheck/bad.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+hello="Hello world"
+echo $1
diff --git a/tools/lint/test/files/shellcheck/good.sh b/tools/lint/test/files/shellcheck/good.sh
new file mode 100644
index 0000000000..e61d501955
--- /dev/null
+++ b/tools/lint/test/files/shellcheck/good.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+echo "Hello world"
diff --git a/tools/lint/test/files/stylelint/nolint/foo.txt b/tools/lint/test/files/stylelint/nolint/foo.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tools/lint/test/files/stylelint/nolint/foo.txt
diff --git a/tools/lint/test/files/stylelint/subdir/bad.css b/tools/lint/test/files/stylelint/subdir/bad.css
new file mode 100644
index 0000000000..70004c1fb2
--- /dev/null
+++ b/tools/lint/test/files/stylelint/subdir/bad.css
@@ -0,0 +1,5 @@
+#foo {
+ /* Duplicate property: */
+ font-size: 12px;
+ font-size: 12px;
+}
diff --git a/tools/lint/test/files/test-manifest-alpha/mochitest-in-order.ini b/tools/lint/test/files/test-manifest-alpha/mochitest-in-order.ini
new file mode 100644
index 0000000000..9de566702a
--- /dev/null
+++ b/tools/lint/test/files/test-manifest-alpha/mochitest-in-order.ini
@@ -0,0 +1,29 @@
+[DEFAULT]
+support-files =
+ click-event-helper.js
+ head.js
+ test-button-overlay.html
+ ../../../../dom/media/test/gizmo.mp4
+ ../../../../dom/media/test/owl.mp3
+
+prefs =
+ media.videocontrols.picture-in-picture.display-text-tracks.enabled=false
+
+[browser_AAA_run_first_firstTimePiPToggleEvents.js]
+[browser_aaa_run_first_firstTimePiPToggleEvents.js]
+[browser_backgroundTab.js]
+[browser_cannotTriggerFromContent.js]
+[browser_closePipPause.js]
+skip-if = os == "linux" && bits == 64 && os_version == "18.04" # Bug 1569205
+[browser_closePip_pageNavigationChanges.js]
+[browser_closePlayer.js]
+[browser_closeTab.js]
+[browser_close_unpip_focus.js]
+[browser_contextMenu.js]
+[browser_cornerSnapping.js]
+run-if = os == "mac"
+[browser_dblclickFullscreen.js]
+[browser_flipIconWithRTL.js]
+skip-if =
+ os == "linux" && ccov # Bug 1678091
+ tsan # Bug 1678091
diff --git a/tools/lint/test/files/test-manifest-alpha/mochitest-mostly-in-order.ini b/tools/lint/test/files/test-manifest-alpha/mochitest-mostly-in-order.ini
new file mode 100644
index 0000000000..01f2551bd4
--- /dev/null
+++ b/tools/lint/test/files/test-manifest-alpha/mochitest-mostly-in-order.ini
@@ -0,0 +1,30 @@
+[DEFAULT]
+support-files =
+ click-event-helper.js
+ head.js
+ test-button-overlay.html
+ ../../../../dom/media/test/gizmo.mp4
+ ../../../../dom/media/test/owl.mp3
+
+prefs =
+ media.videocontrols.picture-in-picture.display-text-tracks.enabled=false
+
+[browser_AAA_run_first_firstTimePiPToggleEvents.js]
+[browser_aaa_run_first_firstTimePiPToggleEvents.js]
+[browser_backgroundTab.js]
+[browser_cannotTriggerFromContent.js]
+[browser_closePipPause.js]
+skip-if = os == "linux" && bits == 64 && os_version == "18.04" # Bug 1569205
+[browser_closePip_pageNavigationChanges.js]
+[browser_closePlayer.js]
+[browser_closeTab.js]
+[browser_close_unpip_focus.js]
+[browser_contextMenu.js]
+[browser_cornerSnapping.js]
+run-if = os == "mac"
+[browser_dblclickFullscreen.js]
+[browser_flipIconWithRTL.js]
+skip-if =
+ os == "linux" && ccov # Bug 1678091
+ tsan # Bug 1678091
+[browser_a_new_test.js]
diff --git a/tools/lint/test/files/test-manifest-alpha/mochitest-very-out-of-order.ini b/tools/lint/test/files/test-manifest-alpha/mochitest-very-out-of-order.ini
new file mode 100644
index 0000000000..45bfdd776b
--- /dev/null
+++ b/tools/lint/test/files/test-manifest-alpha/mochitest-very-out-of-order.ini
@@ -0,0 +1,29 @@
+[DEFAULT]
+support-files =
+ click-event-helper.js
+ head.js
+ test-button-overlay.html
+ ../../../../dom/media/test/gizmo.mp4
+ ../../../../dom/media/test/owl.mp3
+
+prefs =
+ media.videocontrols.picture-in-picture.display-text-tracks.enabled=false
+
+[browser_contextMenu.js]
+[browser_aaa_run_first_firstTimePiPToggleEvents.js]
+[browser_AAA_run_first_firstTimePiPToggleEvents.js]
+[browser_backgroundTab.js]
+[browser_cannotTriggerFromContent.js]
+[browser_close_unpip_focus.js]
+[browser_closePip_pageNavigationChanges.js]
+[browser_closePipPause.js]
+skip-if = os == "linux" && bits == 64 && os_version == "18.04" # Bug 1569205
+[browser_cornerSnapping.js]
+run-if = os == "mac"
+[browser_closePlayer.js]
+[browser_closeTab.js]
+[browser_dblclickFullscreen.js]
+[browser_flipIconWithRTL.js]
+skip-if =
+ os == "linux" && ccov # Bug 1678091
+ tsan # Bug 1678091
diff --git a/tools/lint/test/files/test-manifest-alpha/other-ini-very-out-of-order.ini b/tools/lint/test/files/test-manifest-alpha/other-ini-very-out-of-order.ini
new file mode 100644
index 0000000000..45bfdd776b
--- /dev/null
+++ b/tools/lint/test/files/test-manifest-alpha/other-ini-very-out-of-order.ini
@@ -0,0 +1,29 @@
+[DEFAULT]
+support-files =
+ click-event-helper.js
+ head.js
+ test-button-overlay.html
+ ../../../../dom/media/test/gizmo.mp4
+ ../../../../dom/media/test/owl.mp3
+
+prefs =
+ media.videocontrols.picture-in-picture.display-text-tracks.enabled=false
+
+[browser_contextMenu.js]
+[browser_aaa_run_first_firstTimePiPToggleEvents.js]
+[browser_AAA_run_first_firstTimePiPToggleEvents.js]
+[browser_backgroundTab.js]
+[browser_cannotTriggerFromContent.js]
+[browser_close_unpip_focus.js]
+[browser_closePip_pageNavigationChanges.js]
+[browser_closePipPause.js]
+skip-if = os == "linux" && bits == 64 && os_version == "18.04" # Bug 1569205
+[browser_cornerSnapping.js]
+run-if = os == "mac"
+[browser_closePlayer.js]
+[browser_closeTab.js]
+[browser_dblclickFullscreen.js]
+[browser_flipIconWithRTL.js]
+skip-if =
+ os == "linux" && ccov # Bug 1678091
+ tsan # Bug 1678091
diff --git a/tools/lint/test/files/trojan-source/README b/tools/lint/test/files/trojan-source/README
new file mode 100644
index 0000000000..343a9d0c3c
--- /dev/null
+++ b/tools/lint/test/files/trojan-source/README
@@ -0,0 +1,5 @@
+These examples are taken from trojan source:
+https://github.com/nickboucher/trojan-source
+
+The examples are published under the MIT license.
+
diff --git a/tools/lint/test/files/trojan-source/commenting-out.cpp b/tools/lint/test/files/trojan-source/commenting-out.cpp
new file mode 100644
index 0000000000..d67df70ce1
--- /dev/null
+++ b/tools/lint/test/files/trojan-source/commenting-out.cpp
@@ -0,0 +1,9 @@
+#include <iostream>
+
+int main() {
+ bool isAdmin = false;
+ /*‮ } ⁦if (isAdmin)⁩ ⁦ begin admins only */
+ std::cout << "You are an admin.\n";
+ /* end admins only ‮ { ⁦*/
+ return 0;
+} \ No newline at end of file
diff --git a/tools/lint/test/files/trojan-source/early-return.py b/tools/lint/test/files/trojan-source/early-return.py
new file mode 100644
index 0000000000..2797d8ae9f
--- /dev/null
+++ b/tools/lint/test/files/trojan-source/early-return.py
@@ -0,0 +1,9 @@
+#!/usr/bin/env python3
+bank = { 'alice': 100 }
+
+def subtract_funds(account: str, amount: int):
+ ''' Subtract funds from bank account then ⁧''' ;return
+ bank[account] -= amount
+ return
+
+subtract_funds('alice', 50) \ No newline at end of file
diff --git a/tools/lint/test/files/trojan-source/invisible-function.rs b/tools/lint/test/files/trojan-source/invisible-function.rs
new file mode 100644
index 0000000000..b32efb0372
--- /dev/null
+++ b/tools/lint/test/files/trojan-source/invisible-function.rs
@@ -0,0 +1,15 @@
+fn isAdmin() {
+ return false;
+}
+
+fn is​Admin() {
+ return true;
+}
+
+fn main() {
+ if is​Admin() {
+ printf("You are an admin\n");
+ } else {
+ printf("You are NOT an admin.\n");
+ }
+} \ No newline at end of file
diff --git a/tools/lint/test/files/updatebot/.yamllint b/tools/lint/test/files/updatebot/.yamllint
new file mode 100644
index 0000000000..4f11bbd6c5
--- /dev/null
+++ b/tools/lint/test/files/updatebot/.yamllint
@@ -0,0 +1,6 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+# Explicity default .yamllint to isolate tests from tree-wide yamlint config.
+---
+extends: default
diff --git a/tools/lint/test/files/updatebot/cargo-mismatch.yaml b/tools/lint/test/files/updatebot/cargo-mismatch.yaml
new file mode 100644
index 0000000000..ac18d2b87c
--- /dev/null
+++ b/tools/lint/test/files/updatebot/cargo-mismatch.yaml
@@ -0,0 +1,44 @@
+---
+# Version of this schema
+schema: 1
+
+bugzilla:
+ # Bugzilla product and component for this directory and subdirectories
+ product: Core
+ component: "Graphics: WebGPU"
+
+# Document the source of externally hosted code
+origin:
+
+ # Short name of the package/library
+ name: wgpu
+
+ description: A cross-platform pure-Rust graphics API
+
+ # Full URL for the package's homepage/etc
+ # Usually different from repository url
+ url: https://github.com/gfx-rs/wgpu
+
+ # Human-readable identifier for this version/release
+ # Generally "version NNN", "tag SSS", "bookmark SSS"
+ release: commit 32af4f56
+
+ # Revision to pull in
+ # Must be a long or short commit SHA (long preferred)
+ revision: idontmatchanything
+
+ license: ['MIT', 'Apache-2.0']
+
+updatebot:
+ maintainer-phab: jimb
+ maintainer-bz: jimb@mozilla.com
+ tasks:
+ - type: vendoring
+ enabled: true
+ frequency: 1 week
+
+vendoring:
+ url: https://github.com/gfx-rs/wgpu
+ source-hosting: github
+ vendor-directory: gfx/wgpu_bindings/
+ flavor: rust
diff --git a/tools/lint/test/files/updatebot/good1.yaml b/tools/lint/test/files/updatebot/good1.yaml
new file mode 100644
index 0000000000..f57d2c5b4c
--- /dev/null
+++ b/tools/lint/test/files/updatebot/good1.yaml
@@ -0,0 +1,44 @@
+---
+schema: 1
+
+bugzilla:
+ product: Core
+ component: Graphics
+
+origin:
+ name: angle
+
+ description: ANGLE - Almost Native Graphics Layer Engine
+
+ url: https://chromium.googlesource.com/angle/angle
+
+ # Note that while the vendoring information here, including revision,
+ # release, and upstream repo locations refer to the third party upstream,
+ # Angle is vendored from a mozilla git repository that pulls from
+ # upstream and mainntains local patches there.
+ release: commit 018f85dea11fd5e41725750c6958695a6b8e8409
+ revision: 018f85dea11fd5e41725750c6958695a6b8e8409
+
+ license: BSD-3-Clause
+
+updatebot:
+ maintainer-phab: jgilbert
+ maintainer-bz: jgilbert@mozilla.com
+ tasks:
+ - type: commit-alert
+ enabled: true
+ branch: chromium/4515
+ needinfo: ["jgilbert@mozilla.com"]
+
+vendoring:
+ url: https://chromium.googlesource.com/angle/angle
+ tracking: tag
+ source-hosting: angle
+ vendor-directory: gfx/angle/checkout
+ skip-vendoring-steps: ["fetch", "update-moz-build"]
+
+ update-actions:
+ - action: run-script
+ script: '{yaml_dir}/auto-update-angle.sh'
+ args: ['{revision}']
+ cwd: '{cwd}'
diff --git a/tools/lint/test/files/updatebot/good2.yaml b/tools/lint/test/files/updatebot/good2.yaml
new file mode 100644
index 0000000000..0161d28b11
--- /dev/null
+++ b/tools/lint/test/files/updatebot/good2.yaml
@@ -0,0 +1,74 @@
+---
+# Version of this schema
+schema: 1
+
+bugzilla:
+ # Bugzilla product and component for this directory and subdirectories
+ product: Core
+ component: "Audio/Video: Playback"
+
+# Document the source of externally hosted code
+origin:
+
+ # Short name of the package/library
+ name: dav1d
+
+ description: dav1d, a fast AV1 decoder
+
+ # Full URL for the package's homepage/etc
+ # Usually different from repository url
+ url: https://code.videolan.org/videolan/dav1d
+
+ # Human-readable identifier for this version/release
+ # Generally "version NNN", "tag SSS", "bookmark SSS"
+ release: ffb59680356fd210816cf9e46d9d023ade1f4d5a
+
+ # Revision to pull in
+ # Must be a long or short commit SHA (long preferred)
+ revision: ffb59680356fd210816cf9e46d9d023ade1f4d5a
+
+ # The package's license, where possible using the mnemonic from
+ # https://spdx.org/licenses/
+ # Multiple licenses can be specified (as a YAML list)
+ # A "LICENSE" file must exist containing the full license text
+ license: BSD-2-Clause
+
+ license-file: COPYING
+
+updatebot:
+ maintainer-phab: chunmin
+ maintainer-bz: cchang@mozilla.com
+ tasks:
+ - type: vendoring
+ enabled: true
+ frequency: release
+
+vendoring:
+ url: https://code.videolan.org/videolan/dav1d
+ source-hosting: gitlab
+ vendor-directory: third_party/dav1d
+
+ exclude:
+ - build/.gitattributes
+ - build/.gitignore
+ - doc
+ - examples
+ - package
+ - tools
+
+ generated:
+ - '{yaml_dir}/vcs_version.h'
+ - '{yaml_dir}/version.h'
+
+ update-actions:
+ - action: copy-file
+ from: include/vcs_version.h.in
+ to: '{yaml_dir}/vcs_version.h'
+ - action: replace-in-file
+ pattern: '@VCS_TAG@'
+ with: '{revision}'
+ file: '{yaml_dir}/vcs_version.h'
+ - action: run-script
+ script: '{yaml_dir}/update-version.sh'
+ cwd: '{vendor_dir}'
+ args: ['{yaml_dir}/version.h']
diff --git a/tools/lint/test/files/updatebot/no-revision.yaml b/tools/lint/test/files/updatebot/no-revision.yaml
new file mode 100644
index 0000000000..4d581508d8
--- /dev/null
+++ b/tools/lint/test/files/updatebot/no-revision.yaml
@@ -0,0 +1,43 @@
+---
+schema: 1
+
+bugzilla:
+ product: Core
+ component: Graphics
+
+origin:
+ name: angle
+
+ description: ANGLE - Almost Native Graphics Layer Engine
+
+ url: https://chromium.googlesource.com/angle/angle
+
+ # Note that while the vendoring information here, including revision,
+ # release, and upstream repo locations refer to the third party upstream,
+ # Angle is vendored from a mozilla git repository that pulls from
+ # upstream and mainntains local patches there.
+ release: commit 018f85dea11fd5e41725750c6958695a6b8e8409
+
+ license: BSD-3-Clause
+
+updatebot:
+ maintainer-phab: jgilbert
+ maintainer-bz: jgilbert@mozilla.com
+ tasks:
+ - type: commit-alert
+ enabled: true
+ branch: chromium/4515
+ needinfo: ["jgilbert@mozilla.com"]
+
+vendoring:
+ url: https://chromium.googlesource.com/angle/angle
+ tracking: tag
+ source-hosting: angle
+ vendor-directory: gfx/angle/checkout
+ skip-vendoring-steps: ["fetch", "update-moz-build"]
+
+ update-actions:
+ - action: run-script
+ script: '{yaml_dir}/auto-update-angle.sh'
+ args: ['{revision}']
+ cwd: '{cwd}'
diff --git a/tools/lint/test/files/yaml/.yamllint b/tools/lint/test/files/yaml/.yamllint
new file mode 100644
index 0000000000..4f11bbd6c5
--- /dev/null
+++ b/tools/lint/test/files/yaml/.yamllint
@@ -0,0 +1,6 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+# Explicity default .yamllint to isolate tests from tree-wide yamlint config.
+---
+extends: default
diff --git a/tools/lint/test/files/yaml/bad.yml b/tools/lint/test/files/yaml/bad.yml
new file mode 100644
index 0000000000..195ac7b030
--- /dev/null
+++ b/tools/lint/test/files/yaml/bad.yml
@@ -0,0 +1,8 @@
+---
+yamllint:
+ description: YAML linteraaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaax
+ include:
+ - .cron.yml
+ - browser/config/
+ - wrong
+ application:bar
diff --git a/tools/lint/test/files/yaml/good.yml b/tools/lint/test/files/yaml/good.yml
new file mode 100644
index 0000000000..b30941b797
--- /dev/null
+++ b/tools/lint/test/files/yaml/good.yml
@@ -0,0 +1,6 @@
+---
+yamllint:
+ description: YAML linter
+ include:
+ - .cron.yml
+ - browser/config/
diff --git a/tools/lint/test/python.ini b/tools/lint/test/python.ini
new file mode 100644
index 0000000000..acbd032423
--- /dev/null
+++ b/tools/lint/test/python.ini
@@ -0,0 +1,35 @@
+[DEFAULT]
+subsuite = mozlint
+
+[test_android_format.py]
+[test_black.py]
+requirements = tools/lint/python/black_requirements.txt
+[test_clang_format.py]
+[test_codespell.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.
+sequential = true
+[test_file_license.py]
+[test_file_perm.py]
+skip-if = os == "win"
+[test_file_whitespace.py]
+[test_fluent_lint.py]
+[test_lintpref.py]
+[test_manifest_alpha.py]
+[test_perfdocs.py]
+[test_perfdocs_generation.py]
+[test_updatebot.py]
+[test_perfdocs_helpers.py]
+[test_rst.py]
+requirements = tools/lint/rst/requirements.txt
+[test_ruff.py]
+requirements = tools/lint/python/ruff_requirements.txt
+[test_rustfmt.py]
+[test_shellcheck.py]
+[test_stylelint.py]
+skip-if = os == "win" # busts the tree for subsequent tasks on the same worker (bug 1708591)
+# Setup conflicts with eslint setup so this should run sequentially.
+sequential = true
+[test_trojan_source.py]
+[test_yaml.py]
diff --git a/tools/lint/test/test_android_format.py b/tools/lint/test/test_android_format.py
new file mode 100644
index 0000000000..70cd1ea02e
--- /dev/null
+++ b/tools/lint/test/test_android_format.py
@@ -0,0 +1,38 @@
+import mozunit
+from conftest import build
+
+LINTER = "android-format"
+
+
+def test_basic(global_lint, config):
+ substs = {
+ "GRADLE_ANDROID_FORMAT_LINT_CHECK_TASKS": [
+ "spotlessJavaCheck",
+ "spotlessKotlinCheck",
+ ],
+ "GRADLE_ANDROID_FORMAT_LINT_FIX_TASKS": [
+ "spotlessJavaApply",
+ "spotlessKotlinApply",
+ ],
+ "GRADLE_ANDROID_FORMAT_LINT_FOLDERS": ["tools/lint/test/files/android-format"],
+ }
+ results = global_lint(
+ config=config,
+ topobjdir=build.topobjdir,
+ root=build.topsrcdir,
+ substs=substs,
+ extra_args=["-PandroidFormatLintTest"],
+ )
+ print(results)
+
+ # When first task (spotlessJavaCheck) hits error, we won't check next Kotlin error.
+ # So results length will be 1.
+ assert len(results) == 1
+ assert results[0].level == "error"
+
+ # Since android-format is global lint, fix=True overrides repository files directly.
+ # No way to add this test.
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_black.py b/tools/lint/test/test_black.py
new file mode 100644
index 0000000000..9027670665
--- /dev/null
+++ b/tools/lint/test/test_black.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+
+# 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 mozunit
+
+LINTER = "black"
+fixed = 0
+
+
+def test_lint_fix(lint, create_temp_file):
+
+ contents = """def is_unique(
+ s
+ ):
+ s = list(s
+ )
+ s.sort()
+
+
+ for i in range(len(s) - 1):
+ if s[i] == s[i + 1]:
+ return 0
+ else:
+ return 1
+
+
+if __name__ == "__main__":
+ print(
+ is_unique(input())
+ ) """
+
+ path = create_temp_file(contents, "bad.py")
+ lint([path], fix=True)
+ assert fixed == 1
+
+
+def test_lint_black(lint, paths):
+ results = lint(paths())
+ assert len(results) == 2
+
+ assert results[0].level == "error"
+ assert results[0].relpath == "bad.py"
+
+ assert "EOF" in results[1].message
+ assert results[1].level == "error"
+ assert results[1].relpath == "invalid.py"
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_clang_format.py b/tools/lint/test/test_clang_format.py
new file mode 100644
index 0000000000..809280601d
--- /dev/null
+++ b/tools/lint/test/test_clang_format.py
@@ -0,0 +1,139 @@
+import mozunit
+from conftest import build
+
+LINTER = "clang-format"
+fixed = 0
+
+
+def test_good(lint, config, paths):
+ results = lint(paths("good/"), root=build.topsrcdir, use_filters=False)
+ print(results)
+ assert len(results) == 0
+
+ results = lint(paths("good/"), root=build.topsrcdir, use_filters=False, fix=True)
+ assert fixed == len(results)
+
+
+def test_basic(lint, config, paths):
+ results = lint(paths("bad/bad.cpp"), root=build.topsrcdir, use_filters=False)
+ print(results)
+ assert len(results) == 1
+
+ assert "Reformat C/C++" in results[0].message
+ assert results[0].level == "warning"
+ assert results[0].lineno == 1
+ assert results[0].column == 0
+ assert "bad.cpp" in results[0].path
+ assert (
+ results[0].diff
+ == """\
+-int main ( ) {
+-
+-return 0;
+-
+-
+-}
++int main() { return 0; }
+""" # noqa
+ )
+
+
+def test_dir(lint, config, paths):
+ results = lint(paths("bad/"), root=build.topsrcdir, use_filters=False)
+ print(results)
+ assert len(results) == 5
+
+ assert "Reformat C/C++" in results[0].message
+ assert results[0].level == "warning"
+ assert results[0].lineno == 1
+ assert results[0].column == 0
+ assert "bad.cpp" in results[0].path
+ assert (
+ results[0].diff
+ == """\
+-int main ( ) {
+-
+-return 0;
+-
+-
+-}
++int main() { return 0; }
+""" # noqa
+ )
+
+ assert "Reformat C/C++" in results[1].message
+ assert results[1].level == "warning"
+ assert results[1].lineno == 1
+ assert results[1].column == 0
+ assert "bad2.c" in results[1].path
+ assert (
+ results[1].diff
+ == """\
+-#include "bad2.h"
+-
+-
+-int bad2() {
++#include "bad2.h"
++
++int bad2() {
+"""
+ )
+
+ assert "Reformat C/C++" in results[2].message
+ assert results[2].level == "warning"
+ assert results[2].lineno == 5
+ assert results[2].column == 0
+ assert "bad2.c" in results[2].path
+ assert (
+ results[2].diff
+ == """\
+- int a =2;
++ int a = 2;
+"""
+ )
+
+ assert "Reformat C/C++" in results[3].message
+ assert results[3].level == "warning"
+ assert results[3].lineno == 6
+ assert results[3].column == 0
+ assert "bad2.c" in results[3].path
+ assert (
+ results[3].diff
+ == """\
+- return a;
+-
+-}
++ return a;
++}
+"""
+ )
+
+ assert "Reformat C/C++" in results[4].message
+ assert results[4].level == "warning"
+ assert results[4].lineno == 1
+ assert results[4].column == 0
+ assert "bad2.h" in results[4].path
+ assert (
+ results[4].diff
+ == """\
+-int bad2(void );
++int bad2(void);
+"""
+ )
+
+
+def test_fixed(lint, create_temp_file):
+
+ contents = """int main ( ) { \n
+return 0; \n
+
+}"""
+
+ path = create_temp_file(contents, "ignore.cpp")
+ lint([path], use_filters=False, fix=True)
+
+ assert fixed == 1
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_codespell.py b/tools/lint/test/test_codespell.py
new file mode 100644
index 0000000000..8baae66b41
--- /dev/null
+++ b/tools/lint/test/test_codespell.py
@@ -0,0 +1,37 @@
+import mozunit
+
+LINTER = "codespell"
+fixed = 0
+
+
+def test_lint_codespell_fix(lint, create_temp_file):
+ contents = """This is a file with some typos and informations.
+But also testing false positive like optin (because this isn't always option)
+or stuff related to our coding style like:
+aparent (aParent).
+but detects mistakes like mozila
+""".lstrip()
+
+ path = create_temp_file(contents, "ignore.rst")
+ lint([path], fix=True)
+
+ assert fixed == 2
+
+
+def test_lint_codespell(lint, paths):
+ results = lint(paths())
+ assert len(results) == 2
+
+ assert results[0].message == "informations ==> information"
+ assert results[0].level == "error"
+ assert results[0].lineno == 1
+ assert results[0].relpath == "ignore.rst"
+
+ assert results[1].message == "mozila ==> mozilla"
+ assert results[1].level == "error"
+ assert results[1].lineno == 5
+ assert results[1].relpath == "ignore.rst"
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_eslint.py b/tools/lint/test/test_eslint.py
new file mode 100644
index 0000000000..b4fda2fb35
--- /dev/null
+++ b/tools/lint/test/test_eslint.py
@@ -0,0 +1,99 @@
+import mozunit
+import pytest
+from conftest import build
+
+LINTER = "eslint"
+fixed = 0
+
+
+@pytest.fixture
+def eslint(lint):
+ def inner(*args, **kwargs):
+ # --no-ignore is for ESLint to avoid the .eslintignore file.
+ # --ignore-path is because Prettier doesn't have the --no-ignore option
+ # and therefore needs to be given an empty file for the tests to work.
+ kwargs["extra_args"] = [
+ "--no-ignore",
+ "--ignore-path=tools/lint/test/files/eslint/testprettierignore",
+ ]
+ return lint(*args, **kwargs)
+
+ return inner
+
+
+def test_lint_with_global_exclude(lint, config, paths):
+ config["exclude"] = ["subdir", "import"]
+ # This uses lint directly as we need to not ignore the excludes.
+ results = lint(paths(), config=config, root=build.topsrcdir)
+ assert len(results) == 0
+
+
+def test_no_files_to_lint(eslint, config, paths):
+ # A directory with no files to lint.
+ results = eslint(paths("nolint"), root=build.topsrcdir)
+ assert results == []
+
+ # Errors still show up even when a directory with no files is passed in.
+ results = eslint(paths("nolint", "subdir/bad.js"), root=build.topsrcdir)
+ assert len(results) == 1
+
+
+def test_bad_import(eslint, config, paths):
+ results = eslint(paths("import"), config=config, root=build.topsrcdir)
+ assert results == 1
+
+
+def test_eslint_rule(eslint, config, create_temp_file):
+ contents = """var re = /foo bar/;
+var re = new RegExp("foo bar");
+"""
+ path = create_temp_file(contents, "bad.js")
+ results = eslint(
+ [path], config=config, root=build.topsrcdir, rules=["no-regex-spaces: error"]
+ )
+
+ assert len(results) == 2
+
+
+def test_eslint_fix(eslint, config, create_temp_file):
+ contents = """/*eslint no-regex-spaces: "error"*/
+
+var re = /foo bar/;
+var re = new RegExp("foo bar");
+
+var re = /foo bar/;
+var re = new RegExp("foo bar");
+
+var re = /foo bar/;
+var re = new RegExp("foo bar");
+"""
+ path = create_temp_file(contents, "bad.js")
+ eslint([path], config=config, root=build.topsrcdir, fix=True)
+
+ # ESLint returns counts of files fixed, not errors fixed.
+ assert fixed == 1
+
+
+def test_prettier_rule(eslint, config, create_temp_file):
+ contents = """var re = /foobar/;
+ var re = "foo";
+"""
+ path = create_temp_file(contents, "bad.js")
+ results = eslint([path], config=config, root=build.topsrcdir)
+
+ assert len(results) == 1
+
+
+def test_prettier_fix(eslint, config, create_temp_file):
+ contents = """var re = /foobar/;
+ var re = "foo";
+"""
+ path = create_temp_file(contents, "bad.js")
+ eslint([path], config=config, root=build.topsrcdir, fix=True)
+
+ # Prettier returns counts of files fixed, not errors fixed.
+ assert fixed == 1
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_file_license.py b/tools/lint/test/test_file_license.py
new file mode 100644
index 0000000000..b00b023bae
--- /dev/null
+++ b/tools/lint/test/test_file_license.py
@@ -0,0 +1,23 @@
+import mozunit
+
+LINTER = "license"
+
+
+def test_lint_license(lint, paths):
+ results = lint(paths())
+ print(results)
+ assert len(results) == 3
+
+ assert ".eslintrc.js" in results[0].relpath
+
+ assert "No matching license strings" in results[1].message
+ assert results[1].level == "error"
+ assert "bad.c" in results[1].relpath
+
+ assert "No matching license strings" in results[2].message
+ assert results[2].level == "error"
+ assert "bad.js" in results[2].relpath
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_file_perm.py b/tools/lint/test/test_file_perm.py
new file mode 100644
index 0000000000..08d6a20eef
--- /dev/null
+++ b/tools/lint/test/test_file_perm.py
@@ -0,0 +1,35 @@
+import mozunit
+import pytest
+
+LINTER = "file-perm"
+
+
+@pytest.mark.lint_config(name="file-perm")
+def test_lint_file_perm(lint, paths):
+ results = lint(paths("no-shebang"), collapse_results=True)
+
+ assert results.keys() == {
+ "no-shebang/bad.c",
+ "no-shebang/bad-shebang.c",
+ "no-shebang/bad.png",
+ }
+
+ for path, issues in results.items():
+ for issue in issues:
+ assert "permissions on a source" in issue.message
+ assert issue.level == "error"
+
+
+@pytest.mark.lint_config(name="maybe-shebang-file-perm")
+def test_lint_shebang_file_perm(config, lint, paths):
+ results = lint(paths("maybe-shebang"))
+
+ assert len(results) == 1
+
+ assert "permissions on a source" in results[0].message
+ assert results[0].level == "error"
+ assert results[0].relpath == "maybe-shebang/bad.js"
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_file_whitespace.py b/tools/lint/test/test_file_whitespace.py
new file mode 100644
index 0000000000..51b6fc4795
--- /dev/null
+++ b/tools/lint/test/test_file_whitespace.py
@@ -0,0 +1,51 @@
+import mozunit
+
+LINTER = "file-whitespace"
+fixed = 0
+
+
+def test_lint_file_whitespace(lint, paths):
+ results = lint(paths())
+ print(results)
+ assert len(results) == 5
+
+ assert "File does not end with newline character" in results[1].message
+ assert results[1].level == "error"
+ assert "bad-newline.c" in results[1].relpath
+
+ assert "Empty Lines at end of file" in results[0].message
+ assert results[0].level == "error"
+ assert "bad-newline.c" in results[0].relpath
+
+ assert "Windows line return" in results[2].message
+ assert results[2].level == "error"
+ assert "bad-windows.c" in results[2].relpath
+
+ assert "Trailing whitespace" in results[3].message
+ assert results[3].level == "error"
+ assert "bad.c" in results[3].relpath
+ assert results[3].lineno == 1
+
+ assert "Trailing whitespace" in results[4].message
+ assert results[4].level == "error"
+ assert "bad.c" in results[4].relpath
+ assert results[4].lineno == 2
+
+
+def test_lint_file_whitespace_fix(lint, paths, create_temp_file):
+
+ contents = """int main() { \n
+ return 0; \n
+}
+
+
+"""
+
+ path = create_temp_file(contents, "bad.cpp")
+ lint([path], fix=True)
+ # Gives a different answer on Windows. Probably because of Windows CR
+ assert fixed == 3 or fixed == 2
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_fluent_lint.py b/tools/lint/test/test_fluent_lint.py
new file mode 100644
index 0000000000..d7867a8269
--- /dev/null
+++ b/tools/lint/test/test_fluent_lint.py
@@ -0,0 +1,156 @@
+# 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 mozunit
+
+LINTER = "fluent-lint"
+
+
+def test_lint_exclusions(lint, paths):
+ results = lint(paths("excluded.ftl"))
+ assert len(results) == 1
+ assert results[0].rule == "TE01"
+ assert results[0].lineno == 6
+ assert results[0].column == 20
+
+
+def test_lint_single_file(lint, paths):
+ results = lint(paths("bad.ftl"))
+ assert len(results) == 13
+ assert results[0].rule == "ID01"
+ assert results[0].lineno == 1
+ assert results[0].column == 1
+ assert results[1].rule == "ID01"
+ assert results[1].lineno == 3
+ assert results[1].column == 1
+ assert results[2].rule == "TE01"
+ assert results[2].lineno == 5
+ assert results[2].column == 20
+ assert results[3].rule == "TE01"
+ assert results[3].lineno == 6
+ assert results[3].column == 24
+ assert results[4].rule == "TE02"
+ assert results[4].lineno == 7
+ assert results[4].column == 20
+ assert results[5].rule == "TE03"
+ assert results[5].lineno == 8
+ assert results[5].column == 20
+ assert results[6].rule == "TE04"
+ assert results[6].lineno == 11
+ assert results[6].column == 20
+ assert results[7].rule == "TE05"
+ assert results[7].lineno == 13
+ assert results[7].column == 16
+ assert results[8].rule == "TE03"
+ assert results[8].lineno == 17
+ assert results[8].column == 20
+ assert results[9].rule == "TE03"
+ assert results[9].lineno == 25
+ assert results[9].column == 18
+ assert results[10].rule == "ID02"
+ assert results[10].lineno == 32
+ assert results[10].column == 1
+ assert results[11].rule == "VC01"
+ assert "$tabCount" in results[11].message
+ assert results[12].rule == "VC01"
+ assert "$url" in results[12].message
+
+
+def test_comment_group(lint, paths):
+ results = lint(paths("comment-group1.ftl"))
+ assert len(results) == 6
+ assert results[0].rule == "GC03"
+ assert results[0].lineno == 12
+ assert results[0].column == 1
+ assert results[1].rule == "GC02"
+ assert results[1].lineno == 16
+ assert results[1].column == 1
+ assert results[2].rule == "GC04"
+ assert results[2].lineno == 21
+ assert results[2].column == 1
+ assert results[3].rule == "GC03"
+ assert results[3].lineno == 26
+ assert results[3].column == 1
+ assert results[4].rule == "GC02"
+ assert results[4].lineno == 30
+ assert results[4].column == 1
+ assert results[5].rule == "GC01"
+ assert results[5].lineno == 35
+ assert results[5].column == 1
+
+ results = lint(paths("comment-group2.ftl"))
+ assert (len(results)) == 0
+
+
+def test_comment_resource(lint, paths):
+ results = lint(paths("comment-resource1.ftl"))
+ assert len(results) == 1
+ assert results[0].rule == "RC01"
+ assert results[0].lineno == 9
+ assert results[0].column == 1
+
+ results = lint(paths("comment-resource2.ftl"))
+ assert len(results) == 1
+ assert results[0].rule == "RC03"
+ assert results[0].lineno == 4
+ assert results[0].column == 1
+
+ results = lint(paths("comment-resource3.ftl"))
+ assert len(results) == 1
+ assert results[0].rule == "RC02"
+ assert results[0].lineno == 5
+ assert results[0].column == 1
+
+ results = lint(paths("comment-resource4.ftl"))
+ assert len(results) == 1
+ assert results[0].rule == "RC03"
+ assert results[0].lineno == 6
+ assert results[0].column == 1
+
+ results = lint(paths("comment-resource5.ftl"))
+ assert len(results) == 1
+ assert results[0].rule == "RC02"
+ assert results[0].lineno == 5
+ assert results[0].column == 1
+
+ results = lint(paths("comment-resource6.ftl"))
+ assert len(results) == 0
+
+
+def test_brand_names(lint, paths):
+ results = lint(paths("brand-names.ftl"), {"brand-files": ["test-brands.ftl"]})
+ assert len(results) == 11
+ assert results[0].rule == "CO01"
+ assert results[0].lineno == 1
+ assert results[0].column == 16
+ assert "Firefox" in results[0].message
+ assert "Mozilla" not in results[0].message
+ assert "Thunderbird" not in results[0].message
+ assert results[1].rule == "CO01"
+ assert results[1].lineno == 4
+ assert results[1].column == 16
+
+ results = lint(paths("brand-names-excluded.ftl"))
+ assert len(results) == 0
+
+
+def test_comment_variables(lint, paths):
+ results = lint(paths("comment-variables1.ftl"))
+ assert len(results) == 4
+ assert results[0].rule == "VC01"
+ assert "$var" in results[0].message
+ assert results[1].rule == "VC01"
+ assert "$select1" in results[1].message
+ assert results[2].rule == "VC01"
+ assert "$select2" in results[2].message
+ assert results[3].rule == "VC01"
+ assert "$attr" in results[3].message
+
+ results = lint(paths("comment-variables2.ftl"))
+ assert len(results) == 1
+ assert results[0].rule == "VC01"
+ assert "$term-message" in results[0].message
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_lintpref.py b/tools/lint/test/test_lintpref.py
new file mode 100644
index 0000000000..3e75b1675e
--- /dev/null
+++ b/tools/lint/test/test_lintpref.py
@@ -0,0 +1,16 @@
+import mozunit
+
+LINTER = "lintpref"
+
+
+def test_lintpref(lint, paths):
+ results = lint(paths())
+ assert len(results) == 1
+ assert results[0].level == "error"
+ assert 'pref("dom.webidl.test1", true);' in results[0].message
+ assert "bad.js" in results[0].relpath
+ assert results[0].lineno == 2
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_manifest_alpha.py b/tools/lint/test/test_manifest_alpha.py
new file mode 100644
index 0000000000..2e8e1a6c77
--- /dev/null
+++ b/tools/lint/test/test_manifest_alpha.py
@@ -0,0 +1,33 @@
+# 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 mozunit
+
+LINTER = "test-manifest-alpha"
+
+
+def test_very_out_of_order(lint, paths):
+ results = lint(paths("mochitest-very-out-of-order.ini"))
+ assert len(results) == 1
+ assert results[0].diff
+
+
+def test_in_order(lint, paths):
+ results = lint(paths("mochitest-in-order.ini"))
+ assert len(results) == 0
+
+
+def test_mostly_in_order(lint, paths):
+ results = lint(paths("mochitest-mostly-in-order.ini"))
+ assert len(results) == 1
+ assert results[0].diff
+
+
+def test_other_ini_very_out_of_order(lint, paths):
+ """Test that an .ini file outside of the allowlist is ignored."""
+ results = lint(paths("other-ini-very-out-of-order.ini"))
+ assert len(results) == 0
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_perfdocs.py b/tools/lint/test/test_perfdocs.py
new file mode 100644
index 0000000000..3e52e86271
--- /dev/null
+++ b/tools/lint/test/test_perfdocs.py
@@ -0,0 +1,858 @@
+import contextlib
+import os
+import pathlib
+import shutil
+import tempfile
+from unittest import mock
+
+import mozunit
+import pytest
+
+LINTER = "perfdocs"
+
+
+class PerfDocsLoggerMock:
+ LOGGER = None
+ PATHS = []
+ FAILED = True
+
+
+"""
+This is a sample mozperftest test that we use for testing
+the verification process.
+"""
+SAMPLE_TEST = """
+"use strict";
+
+async function setUp(context) {
+ context.log.info("setUp example!");
+}
+
+async function test(context, commands) {
+ context.log.info("Test with setUp/tearDown example!");
+ await commands.measure.start("https://www.sitespeed.io/");
+ await commands.measure.start("https://www.mozilla.org/en-US/");
+}
+
+async function tearDown(context) {
+ context.log.info("tearDown example!");
+}
+
+module.noexport = {};
+
+module.exports = {
+ setUp,
+ tearDown,
+ test,
+ owner: "Performance Testing Team",
+ name: "Example",
+ description: "The description of the example test.",
+ longDescription: `
+ This is a longer description of the test perhaps including information
+ about how it should be run locally or links to relevant information.
+ `
+};
+"""
+
+
+SAMPLE_CONFIG = """
+name: mozperftest
+manifest: None
+static-only: False
+suites:
+ suite:
+ description: "Performance tests from the 'suite' folder."
+ tests:
+ Example: ""
+"""
+
+
+DYNAMIC_SAMPLE_CONFIG = """
+name: {}
+manifest: None
+static-only: False
+suites:
+ suite:
+ description: "Performance tests from the 'suite' folder."
+ tests:
+ Example: "Performance test Example from suite."
+ another_suite:
+ description: "Performance tests from the 'another_suite' folder."
+ tests:
+ Example: "Performance test Example from another_suite."
+"""
+
+
+SAMPLE_METRICS_CONFIG = """
+name: raptor
+manifest: "None"{}
+static-only: False
+suites:
+ suite:
+ description: "Performance tests from the 'suite' folder."{}
+ tests:
+ Example: "Performance test Example from another_suite."
+ another_suite:
+ description: "Performance tests from the 'another_suite' folder."
+ tests:
+ Example: "Performance test Example from another_suite."
+"""
+
+
+SAMPLE_INI = """
+[Example]
+test_url = Example_url
+alert_on = fcp
+"""
+
+SAMPLE_METRICS_INI = """
+[Example]
+test_url = Example_url
+alert_on = fcp,SpeedIndex
+"""
+
+
+@contextlib.contextmanager
+def temp_file(name="temp", tempdir=None, content=None):
+ if tempdir is None:
+ tempdir = tempfile.mkdtemp()
+ path = pathlib.Path(tempdir, name)
+ if content is not None:
+ with path.open("w", newline="\n") as f:
+ f.write(content)
+ try:
+ yield path
+ finally:
+ try:
+ shutil.rmtree(str(tempdir))
+ except FileNotFoundError:
+ pass
+
+
+@contextlib.contextmanager
+def temp_dir():
+ tempdir = pathlib.Path(tempfile.mkdtemp())
+ try:
+ yield tempdir
+ finally:
+ try:
+ shutil.rmtree(str(tempdir))
+ except FileNotFoundError:
+ pass
+
+
+def setup_sample_logger(logger, structured_logger, top_dir):
+ from perfdocs.logger import PerfDocLogger
+
+ PerfDocLogger.LOGGER = structured_logger
+ PerfDocLogger.PATHS = ["perfdocs"]
+ PerfDocLogger.TOP_DIR = top_dir
+
+ import perfdocs.gatherer as gt
+ import perfdocs.generator as gn
+ import perfdocs.verifier as vf
+
+ gt.logger = logger
+ vf.logger = logger
+ gn.logger = logger
+
+
+@mock.patch("taskgraph.util.taskcluster.get_artifact")
+@mock.patch("tryselect.tasks.generate_tasks")
+@mock.patch("perfdocs.generator.Generator")
+@mock.patch("perfdocs.verifier.Verifier")
+@mock.patch("perfdocs.logger.PerfDocLogger", new=PerfDocsLoggerMock)
+def test_perfdocs_start_and_fail(
+ verifier,
+ generator,
+ get_artifact_mock,
+ gen_tasks_mock,
+ structured_logger,
+ config,
+ paths,
+):
+ from perfdocs.perfdocs import run_perfdocs
+
+ with temp_file("bad", content="foo") as temp:
+ run_perfdocs(
+ config, logger=structured_logger, paths=[str(temp)], generate=False
+ )
+ assert PerfDocsLoggerMock.LOGGER == structured_logger
+ assert PerfDocsLoggerMock.PATHS == [temp]
+ assert PerfDocsLoggerMock.FAILED
+
+ assert verifier.call_count == 1
+ assert mock.call().validate_tree() in verifier.mock_calls
+ assert generator.call_count == 0
+
+
+@mock.patch("taskgraph.util.taskcluster.get_artifact")
+@mock.patch("tryselect.tasks.generate_tasks")
+@mock.patch("perfdocs.generator.Generator")
+@mock.patch("perfdocs.verifier.Verifier")
+@mock.patch("perfdocs.logger.PerfDocLogger", new=PerfDocsLoggerMock)
+def test_perfdocs_start_and_pass(verifier, generator, structured_logger, config, paths):
+ from perfdocs.perfdocs import run_perfdocs
+
+ PerfDocsLoggerMock.FAILED = False
+ with temp_file("bad", content="foo") as temp:
+ run_perfdocs(
+ config, logger=structured_logger, paths=[str(temp)], generate=False
+ )
+ assert PerfDocsLoggerMock.LOGGER == structured_logger
+ assert PerfDocsLoggerMock.PATHS == [temp]
+ assert not PerfDocsLoggerMock.FAILED
+
+ assert verifier.call_count == 1
+ assert mock.call().validate_tree() in verifier.mock_calls
+ assert generator.call_count == 1
+ assert mock.call().generate_perfdocs() in generator.mock_calls
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger", new=PerfDocsLoggerMock)
+def test_perfdocs_bad_paths(structured_logger, config, paths):
+ from perfdocs.perfdocs import run_perfdocs
+
+ with pytest.raises(Exception):
+ run_perfdocs(config, logger=structured_logger, paths=["bad"], generate=False)
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_gatherer_fetch_perfdocs_tree(
+ logger, structured_logger, perfdocs_sample
+):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.gatherer import Gatherer
+
+ gatherer = Gatherer(top_dir)
+ assert not gatherer._perfdocs_tree
+
+ gatherer.fetch_perfdocs_tree()
+
+ expected = "Found 1 perfdocs directories"
+ args, _ = logger.log.call_args
+
+ assert expected in args[0]
+ assert logger.log.call_count == 1
+ assert gatherer._perfdocs_tree
+
+ expected = ["path", "yml", "rst", "static"]
+ for i, key in enumerate(gatherer._perfdocs_tree[0].keys()):
+ assert key == expected[i]
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_gatherer_get_test_list(logger, structured_logger, perfdocs_sample):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.gatherer import Gatherer
+
+ gatherer = Gatherer(top_dir)
+ gatherer.fetch_perfdocs_tree()
+ framework = gatherer.get_test_list(gatherer._perfdocs_tree[0])
+
+ expected = ["name", "test_list", "yml_content", "yml_path"]
+ for i, key in enumerate(sorted(framework.keys())):
+ assert key == expected[i]
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verification(logger, structured_logger, perfdocs_sample):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ # Make sure that we had no warnings
+ assert logger.warning.call_count == 0
+ assert logger.log.call_count == 1
+ assert len(logger.mock_calls) == 1
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_validate_yaml_pass(
+ logger, structured_logger, perfdocs_sample
+):
+ top_dir = perfdocs_sample["top_dir"]
+ yaml_path = perfdocs_sample["config"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.verifier import Verifier
+
+ valid = Verifier(top_dir).validate_yaml(pathlib.Path(yaml_path))
+
+ assert valid
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_invalid_yaml(logger, structured_logger, perfdocs_sample):
+ top_dir = perfdocs_sample["top_dir"]
+ yaml_path = perfdocs_sample["config"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier("top_dir")
+ with open(yaml_path, "r", newline="\n") as f:
+ lines = f.readlines()
+ print(lines)
+ with open(yaml_path, "w", newline="\n") as f:
+ f.write("\n".join(lines[2:]))
+ valid = verifier.validate_yaml(yaml_path)
+
+ expected = ("YAML ValidationError: 'name' is a required property\n", yaml_path)
+ args, _ = logger.warning.call_args
+
+ assert logger.warning.call_count == 1
+ assert expected[0] in args[0]
+ assert not valid
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_validate_rst_pass(
+ logger, structured_logger, perfdocs_sample
+):
+ top_dir = perfdocs_sample["top_dir"]
+ rst_path = perfdocs_sample["index"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.verifier import Verifier
+
+ valid = Verifier(top_dir).validate_rst_content(pathlib.Path(rst_path))
+
+ assert valid
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_invalid_rst(logger, structured_logger, perfdocs_sample):
+ top_dir = perfdocs_sample["top_dir"]
+ rst_path = perfdocs_sample["index"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ # Replace the target string to invalid Keyword for test
+ with open(rst_path, "r") as file:
+ filedata = file.read()
+
+ filedata = filedata.replace("documentation", "Invalid Keyword")
+
+ with open(rst_path, "w", newline="\n") as file:
+ file.write(filedata)
+
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier("top_dir")
+ valid = verifier.validate_rst_content(rst_path)
+
+ expected = (
+ "Cannot find a '{documentation}' entry in the given index file",
+ rst_path,
+ )
+ args, _ = logger.warning.call_args
+
+ assert logger.warning.call_count == 1
+ assert args == expected
+ assert not valid
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_validate_descriptions_pass(
+ logger, structured_logger, perfdocs_sample
+):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ verifier._check_framework_descriptions(verifier._gatherer.perfdocs_tree[0])
+
+ assert logger.warning.call_count == 0
+ assert logger.log.call_count == 1
+ assert len(logger.mock_calls) == 1
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_not_existing_suite_in_test_list(
+ logger, structured_logger, perfdocs_sample
+):
+ top_dir = perfdocs_sample["top_dir"]
+ manifest_path = perfdocs_sample["manifest"]["path"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ os.remove(manifest_path)
+ verifier._check_framework_descriptions(verifier._gatherer.perfdocs_tree[0])
+
+ expected = (
+ "Could not find an existing suite for suite - bad suite name?",
+ perfdocs_sample["config"],
+ )
+ args, _ = logger.warning.call_args
+
+ assert logger.warning.call_count == 1
+ assert args == expected
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_not_existing_tests_in_suites(
+ logger, structured_logger, perfdocs_sample
+):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ with open(perfdocs_sample["config"], "r") as file:
+ filedata = file.read()
+ filedata = filedata.replace("Example", "DifferentName")
+ with open(perfdocs_sample["config"], "w", newline="\n") as file:
+ file.write(filedata)
+
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ verifier._check_framework_descriptions(verifier._gatherer.perfdocs_tree[0])
+
+ expected = [
+ "Could not find an existing test for DifferentName - bad test name?",
+ "Could not find a test description for Example",
+ ]
+
+ assert logger.warning.call_count == 2
+ for i, call in enumerate(logger.warning.call_args_list):
+ args, _ = call
+ assert args[0] == expected[i]
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_missing_contents_in_suite(
+ logger, structured_logger, perfdocs_sample
+):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ with open(perfdocs_sample["config"], "r") as file:
+ filedata = file.read()
+ filedata = filedata.replace("suite:", "InvalidSuite:")
+ with open(perfdocs_sample["config"], "w", newline="\n") as file:
+ file.write(filedata)
+
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ verifier._check_framework_descriptions(verifier._gatherer.perfdocs_tree[0])
+
+ expected = (
+ "Could not find an existing suite for InvalidSuite - bad suite name?",
+ "Missing suite description for suite",
+ )
+
+ assert logger.warning.call_count == 2
+ for i, call in enumerate(logger.warning.call_args_list):
+ args, _ = call
+ assert args[0] == expected[i]
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_invalid_dir(logger, structured_logger, perfdocs_sample):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier("invalid_path")
+ with pytest.raises(Exception) as exceinfo:
+ verifier.validate_tree()
+
+ assert str(exceinfo.value) == "No valid perfdocs directories found"
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_file_invalidation(
+ logger, structured_logger, perfdocs_sample
+):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.verifier import Verifier
+
+ with mock.patch("perfdocs.verifier.Verifier.validate_yaml", return_value=False):
+ verifier = Verifier(top_dir)
+ with pytest.raises(Exception):
+ verifier.validate_tree()
+
+ # Check if "File validation error" log is called
+ # and Called with a log inside perfdocs_tree().
+ assert logger.log.call_count == 2
+ assert len(logger.mock_calls) == 2
+
+
+@pytest.mark.parametrize(
+ "manifest, metric_definitions, expected",
+ [
+ [
+ SAMPLE_INI,
+ """
+metrics:
+ "FirstPaint":
+ aliases:
+ - fcp
+ description: "Example" """,
+ 1,
+ ],
+ [
+ SAMPLE_METRICS_INI,
+ """
+metrics:
+ FirstPaint:
+ aliases:
+ - fcp
+ description: Example
+ SpeedIndex:
+ aliases:
+ - speedindex
+ - si
+ description: Example
+ """,
+ 2,
+ ],
+ ],
+)
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_nonexistent_documented_metrics(
+ logger, structured_logger, perfdocs_sample, manifest, metric_definitions, expected
+):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ with open(perfdocs_sample["config"], "w", newline="\n") as f:
+ f.write(SAMPLE_METRICS_CONFIG.format(metric_definitions, ""))
+ with open(perfdocs_sample["manifest"]["path"], "w", newline="\n") as f:
+ f.write(manifest)
+
+ sample_gatherer_result = {
+ "suite": {"Example": {}},
+ "another_suite": {"Example": {}},
+ }
+
+ from perfdocs.verifier import Verifier
+
+ with mock.patch("perfdocs.framework_gatherers.RaptorGatherer.get_test_list") as m:
+ m.return_value = sample_gatherer_result
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ assert len(logger.warning.call_args_list) == expected
+ for (args, _) in logger.warning.call_args_list:
+ assert "Cannot find documented metric" in args[0]
+ assert "being used" in args[0]
+
+
+@pytest.mark.parametrize(
+ "manifest, metric_definitions",
+ [
+ [
+ SAMPLE_INI,
+ """
+metrics:
+ "FirstPaint":
+ aliases:
+ - fcp
+ description: "Example" """,
+ ],
+ [
+ SAMPLE_METRICS_INI,
+ """
+metrics:
+ SpeedIndex:
+ aliases:
+ - speedindex
+ - si
+ description: Example
+ """,
+ ],
+ ],
+)
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_undocumented_metrics(
+ logger, structured_logger, perfdocs_sample, manifest, metric_definitions
+):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ with open(perfdocs_sample["config"], "w", newline="\n") as f:
+ f.write(SAMPLE_METRICS_CONFIG.format(metric_definitions, ""))
+ with open(perfdocs_sample["manifest"]["path"], "w", newline="\n") as f:
+ f.write(manifest)
+
+ sample_gatherer_result = {
+ "suite": {"Example": {"metrics": ["fcp", "SpeedIndex"]}},
+ "another_suite": {"Example": {}},
+ }
+
+ from perfdocs.verifier import Verifier
+
+ with mock.patch("perfdocs.framework_gatherers.RaptorGatherer.get_test_list") as m:
+ m.return_value = sample_gatherer_result
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ assert len(logger.warning.call_args_list) == 1
+ for (args, _) in logger.warning.call_args_list:
+ assert "Missing description for the metric" in args[0]
+
+
+@pytest.mark.parametrize(
+ "manifest, metric_definitions, expected",
+ [
+ [
+ SAMPLE_INI,
+ """
+metrics:
+ "FirstPaint":
+ aliases:
+ - fcp
+ - SpeedIndex
+ description: "Example" """,
+ 3,
+ ],
+ [
+ SAMPLE_METRICS_INI,
+ """
+metrics:
+ FirstPaint:
+ aliases:
+ - fcp
+ description: Example
+ SpeedIndex:
+ aliases:
+ - speedindex
+ - si
+ description: Example
+ """,
+ 5,
+ ],
+ ],
+)
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_duplicate_metrics(
+ logger, structured_logger, perfdocs_sample, manifest, metric_definitions, expected
+):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ with open(perfdocs_sample["config"], "w", newline="\n") as f:
+ indented_defs = "\n".join(
+ [(" " * 8) + metric_line for metric_line in metric_definitions.split("\n")]
+ )
+ f.write(SAMPLE_METRICS_CONFIG.format(metric_definitions, indented_defs))
+ with open(perfdocs_sample["manifest"]["path"], "w", newline="\n") as f:
+ f.write(manifest)
+
+ sample_gatherer_result = {
+ "suite": {"Example": {"metrics": ["fcp", "SpeedIndex"]}},
+ "another_suite": {"Example": {}},
+ }
+
+ from perfdocs.verifier import Verifier
+
+ with mock.patch("perfdocs.framework_gatherers.RaptorGatherer.get_test_list") as m:
+ m.return_value = sample_gatherer_result
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ assert len(logger.warning.call_args_list) == expected
+ for (args, _) in logger.warning.call_args_list:
+ assert "Duplicate definitions found for " in args[0]
+
+
+@pytest.mark.parametrize(
+ "manifest, metric_definitions",
+ [
+ [
+ SAMPLE_INI,
+ """
+metrics:
+ "FirstPaint":
+ aliases:
+ - fcp
+ - SpeedIndex
+ description: "Example" """,
+ ],
+ [
+ SAMPLE_METRICS_INI,
+ """
+metrics:
+ FirstPaint:
+ aliases:
+ - fcp
+ description: Example
+ SpeedIndex:
+ aliases:
+ - speedindex
+ - si
+ description: Example
+ """,
+ ],
+ ],
+)
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_verifier_valid_metrics(
+ logger, structured_logger, perfdocs_sample, manifest, metric_definitions
+):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ with open(perfdocs_sample["config"], "w", newline="\n") as f:
+ f.write(SAMPLE_METRICS_CONFIG.format(metric_definitions, ""))
+ with open(perfdocs_sample["manifest"]["path"], "w", newline="\n") as f:
+ f.write(manifest)
+
+ sample_gatherer_result = {
+ "suite": {"Example": {"metrics": ["fcp", "SpeedIndex"]}},
+ "another_suite": {"Example": {}},
+ }
+
+ from perfdocs.verifier import Verifier
+
+ with mock.patch("perfdocs.framework_gatherers.RaptorGatherer.get_test_list") as m:
+ m.return_value = sample_gatherer_result
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ assert len(logger.warning.call_args_list) == 0
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_framework_gatherers(logger, structured_logger, perfdocs_sample):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ # Check to make sure that every single framework
+ # gatherer that has been implemented produces a test list
+ # in every suite that contains a test with an associated
+ # manifest.
+ from perfdocs.gatherer import frameworks
+
+ for framework, gatherer in frameworks.items():
+ with open(perfdocs_sample["config"], "w", newline="\n") as f:
+ f.write(DYNAMIC_SAMPLE_CONFIG.format(framework))
+
+ fg = gatherer(perfdocs_sample["config"], top_dir)
+ if getattr(fg, "get_test_list", None) is None:
+ # Skip framework gatherers that have not
+ # implemented a method to build a test list.
+ continue
+
+ # Setup some framework-specific things here if needed
+ if framework == "raptor":
+ fg._manifest_path = perfdocs_sample["manifest"]["path"]
+ fg._get_subtests_from_ini = mock.Mock()
+ fg._get_subtests_from_ini.return_value = {
+ "Example": perfdocs_sample["manifest"],
+ }
+
+ if framework == "talos":
+ fg._get_ci_tasks = mock.Mock()
+ for suite, suitetests in fg.get_test_list().items():
+ assert suite == "Talos Tests"
+ assert suitetests
+ continue
+
+ if framework == "awsy":
+ for suite, suitetests in fg.get_test_list().items():
+ assert suite == "Awsy tests"
+ assert suitetests
+ continue
+
+ for suite, suitetests in fg.get_test_list().items():
+ assert suite == "suite"
+ for test, manifest in suitetests.items():
+ assert test == "Example"
+ assert (
+ pathlib.Path(manifest["path"])
+ == perfdocs_sample["manifest"]["path"]
+ )
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_framework_gatherers_urls(logger, structured_logger, perfdocs_sample):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.gatherer import frameworks
+ from perfdocs.generator import Generator
+ from perfdocs.utils import read_yaml
+ from perfdocs.verifier import Verifier
+
+ # This test is only for raptor
+ gatherer = frameworks["raptor"]
+ with open(perfdocs_sample["config"], "w", newline="\n") as f:
+ f.write(DYNAMIC_SAMPLE_CONFIG.format("raptor"))
+
+ fg = gatherer(perfdocs_sample["config_2"], top_dir)
+ fg.get_suite_list = mock.Mock()
+ fg.get_suite_list.return_value = {
+ "suite": [perfdocs_sample["example1_manifest"]],
+ "another_suite": [perfdocs_sample["example2_manifest"]],
+ }
+
+ v = Verifier(top_dir)
+ gn = Generator(v, generate=True, workspace=top_dir)
+
+ # Check to make sure that if a test is present under multiple
+ # suties the urls are generated correctly for the test under
+ # every suite
+ for suite, suitetests in fg.get_test_list().items():
+ url = fg._descriptions.get(suite)
+ assert url is not None
+ assert url[0]["name"] == "Example"
+ assert url[0]["test_url"] == "Example_url"
+
+ perfdocs_tree = gn._perfdocs_tree[0]
+ yaml_content = read_yaml(
+ pathlib.Path(
+ os.path.join(os.path.join(perfdocs_tree["path"], perfdocs_tree["yml"]))
+ )
+ )
+ suites = yaml_content["suites"]
+
+ # Check that the sections for each suite are generated correctly
+ for suite_name, suite_details in suites.items():
+ gn._verifier._gatherer = mock.Mock(framework_gatherers={"raptor": gatherer})
+ section = gn._verifier._gatherer.framework_gatherers[
+ "raptor"
+ ].build_suite_section(fg, suite_name, suites.get(suite_name)["description"])
+ assert suite_name.capitalize() == section[0]
+ assert suite_name in section[2]
+
+ tests = suites.get(suite_name).get("tests", {})
+ for test_name in tests.keys():
+ desc = gn._verifier._gatherer.framework_gatherers[
+ "raptor"
+ ].build_test_description(fg, test_name, tests[test_name], suite_name)
+ assert f"**test url**: `<{url[0]['test_url']}>`__" in desc[0]
+ assert f"**expected**: {url[0]['expected']}" in desc[0]
+ assert test_name in desc[0]
+
+
+def test_perfdocs_logger_failure(config, paths):
+ from perfdocs.logger import PerfDocLogger
+
+ PerfDocLogger.LOGGER = None
+ with pytest.raises(Exception):
+ PerfDocLogger()
+
+ PerfDocLogger.PATHS = []
+ with pytest.raises(Exception):
+ PerfDocLogger()
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_perfdocs_generation.py b/tools/lint/test/test_perfdocs_generation.py
new file mode 100644
index 0000000000..29b1555467
--- /dev/null
+++ b/tools/lint/test/test_perfdocs_generation.py
@@ -0,0 +1,297 @@
+import os
+import pathlib
+from unittest import mock
+
+import mozunit
+
+LINTER = "perfdocs"
+
+
+def setup_sample_logger(logger, structured_logger, top_dir):
+ from perfdocs.logger import PerfDocLogger
+
+ PerfDocLogger.LOGGER = structured_logger
+ PerfDocLogger.PATHS = ["perfdocs"]
+ PerfDocLogger.TOP_DIR = top_dir
+
+ import perfdocs.gatherer as gt
+ import perfdocs.generator as gn
+ import perfdocs.utils as utils
+ import perfdocs.verifier as vf
+
+ gt.logger = logger
+ vf.logger = logger
+ gn.logger = logger
+ utils.logger = logger
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_generator_generate_perfdocs_pass(
+ logger, structured_logger, perfdocs_sample
+):
+ from test_perfdocs import temp_file
+
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ templates_dir = pathlib.Path(top_dir, "tools", "lint", "perfdocs", "templates")
+ templates_dir.mkdir(parents=True, exist_ok=True)
+
+ from perfdocs.generator import Generator
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ generator = Generator(verifier, generate=True, workspace=top_dir)
+ with temp_file("index.rst", tempdir=templates_dir, content="{test_documentation}"):
+ generator.generate_perfdocs()
+
+ assert logger.warning.call_count == 0
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_generator_needed_regeneration(
+ logger, structured_logger, perfdocs_sample
+):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.generator import Generator
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ generator = Generator(verifier, generate=False, workspace=top_dir)
+ generator.generate_perfdocs()
+
+ expected = "PerfDocs need to be regenerated."
+ args, _ = logger.warning.call_args
+
+ assert logger.warning.call_count == 1
+ assert args[0] == expected
+
+
+@mock.patch("perfdocs.generator.get_changed_files", new=lambda x: [])
+@mock.patch("perfdocs.generator.ON_TRY", new=True)
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_generator_needed_update(logger, structured_logger, perfdocs_sample):
+ from test_perfdocs import temp_file
+
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ templates_dir = pathlib.Path(top_dir, "tools", "lint", "perfdocs", "templates")
+ templates_dir.mkdir(parents=True, exist_ok=True)
+
+ from perfdocs.generator import Generator
+ from perfdocs.verifier import Verifier
+
+ # Initializing perfdocs
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ generator = Generator(verifier, generate=True, workspace=top_dir)
+ with temp_file("index.rst", tempdir=templates_dir, content="{test_documentation}"):
+ generator.generate_perfdocs()
+
+ # Removed file for testing and run again
+ generator._generate = False
+ files = [f for f in os.listdir(generator.perfdocs_path)]
+ for f in files:
+ os.remove(str(pathlib.Path(generator.perfdocs_path, f)))
+
+ generator.generate_perfdocs()
+
+ expected = (
+ "PerfDocs are outdated, run ./mach lint -l perfdocs --fix .` to update them. "
+ "You can also apply the perfdocs.diff patch file produced from this "
+ "reviewbot test to fix the issue."
+ )
+ args, _ = logger.warning.call_args
+
+ assert logger.warning.call_count == 1
+ assert args[0] == expected
+
+ # Check to ensure a diff was produced
+ assert logger.log.call_count == 6
+
+ logs = [v[0][0] for v in logger.log.call_args_list]
+ for failure_log in (
+ "Some files are missing or are funny.",
+ "Missing in existing docs: index.rst",
+ "Missing in existing docs: mozperftest.rst",
+ ):
+ assert failure_log in logs
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_generator_created_perfdocs(
+ logger, structured_logger, perfdocs_sample
+):
+ from test_perfdocs import temp_file
+
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ templates_dir = pathlib.Path(top_dir, "tools", "lint", "perfdocs", "templates")
+ templates_dir.mkdir(parents=True, exist_ok=True)
+
+ from perfdocs.generator import Generator
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ generator = Generator(verifier, generate=True, workspace=top_dir)
+ with temp_file("index.rst", tempdir=templates_dir, content="{test_documentation}"):
+ perfdocs_tmpdir = generator._create_perfdocs()
+
+ files = [f for f in os.listdir(perfdocs_tmpdir)]
+ files.sort()
+ expected_files = ["index.rst", "mozperftest.rst"]
+
+ for i, file in enumerate(files):
+ assert file == expected_files[i]
+
+ with pathlib.Path(perfdocs_tmpdir, expected_files[0]).open() as f:
+ filedata = f.readlines()
+ assert "".join(filedata) == " * :doc:`mozperftest`"
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_generator_build_perfdocs(logger, structured_logger, perfdocs_sample):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.generator import Generator
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ generator = Generator(verifier, generate=True, workspace=top_dir)
+ frameworks_info = generator.build_perfdocs_from_tree()
+
+ expected = ["dynamic", "static"]
+
+ for framework in sorted(frameworks_info.keys()):
+ for i, framework_info in enumerate(frameworks_info[framework]):
+ assert framework_info == expected[i]
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_generator_create_temp_dir(logger, structured_logger, perfdocs_sample):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.generator import Generator
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ generator = Generator(verifier, generate=True, workspace=top_dir)
+ tmpdir = generator._create_temp_dir()
+
+ assert pathlib.Path(tmpdir).is_dir()
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_generator_create_temp_dir_fail(
+ logger, structured_logger, perfdocs_sample
+):
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ from perfdocs.generator import Generator
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ generator = Generator(verifier, generate=True, workspace=top_dir)
+ with mock.patch("perfdocs.generator.pathlib") as path_mock:
+ path_mock.Path().mkdir.side_effect = OSError()
+ path_mock.Path().is_dir.return_value = False
+ tmpdir = generator._create_temp_dir()
+
+ expected = "Error creating temp file: "
+ args, _ = logger.critical.call_args
+
+ assert not tmpdir
+ assert logger.critical.call_count == 1
+ assert args[0] == expected
+
+
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_generator_save_perfdocs_pass(
+ logger, structured_logger, perfdocs_sample
+):
+ from test_perfdocs import temp_file
+
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ templates_dir = pathlib.Path(top_dir, "tools", "lint", "perfdocs", "templates")
+ templates_dir.mkdir(parents=True, exist_ok=True)
+
+ from perfdocs.generator import Generator
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ generator = Generator(verifier, generate=True, workspace=top_dir)
+
+ assert not generator.perfdocs_path.is_dir()
+
+ with temp_file("index.rst", tempdir=templates_dir, content="{test_documentation}"):
+ perfdocs_tmpdir = generator._create_perfdocs()
+
+ generator._save_perfdocs(perfdocs_tmpdir)
+
+ expected = ["index.rst", "mozperftest.rst"]
+ files = [f for f in os.listdir(generator.perfdocs_path)]
+ files.sort()
+
+ for i, file in enumerate(files):
+ assert file == expected[i]
+
+
+@mock.patch("perfdocs.generator.shutil")
+@mock.patch("perfdocs.logger.PerfDocLogger")
+def test_perfdocs_generator_save_perfdocs_fail(
+ logger, shutil, structured_logger, perfdocs_sample
+):
+ from test_perfdocs import temp_file
+
+ top_dir = perfdocs_sample["top_dir"]
+ setup_sample_logger(logger, structured_logger, top_dir)
+
+ templates_dir = pathlib.Path(top_dir, "tools", "lint", "perfdocs", "templates")
+ templates_dir.mkdir(parents=True, exist_ok=True)
+
+ from perfdocs.generator import Generator
+ from perfdocs.verifier import Verifier
+
+ verifier = Verifier(top_dir)
+ verifier.validate_tree()
+
+ generator = Generator(verifier, generate=True, workspace=top_dir)
+ with temp_file("index.rst", tempdir=templates_dir, content="{test_documentation}"):
+ perfdocs_tmpdir = generator._create_perfdocs()
+
+ shutil.copytree = mock.Mock(side_effect=Exception())
+ generator._save_perfdocs(perfdocs_tmpdir)
+
+ expected = "There was an error while saving the documentation: "
+ args, _ = logger.critical.call_args
+
+ assert logger.critical.call_count == 1
+ assert args[0] == expected
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_perfdocs_helpers.py b/tools/lint/test/test_perfdocs_helpers.py
new file mode 100644
index 0000000000..02c1abfecc
--- /dev/null
+++ b/tools/lint/test/test_perfdocs_helpers.py
@@ -0,0 +1,206 @@
+import mozunit
+import pytest
+
+LINTER = "perfdocs"
+
+testdata = [
+ {
+ "table_specifications": {
+ "title": ["not a string"],
+ "widths": [10, 10, 10, 10],
+ "header_rows": 1,
+ "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]],
+ "indent": 2,
+ },
+ "error_msg": "TableBuilder attribute title must be a string.",
+ },
+ {
+ "table_specifications": {
+ "title": "I've got a lovely bunch of coconuts",
+ "widths": ("not", "a", "list"),
+ "header_rows": 1,
+ "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]],
+ "indent": 2,
+ },
+ "error_msg": "TableBuilder attribute widths must be a list of integers.",
+ },
+ {
+ "table_specifications": {
+ "title": "There they are all standing in a row",
+ "widths": ["not an integer"],
+ "header_rows": 1,
+ "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]],
+ "indent": 2,
+ },
+ "error_msg": "TableBuilder attribute widths must be a list of integers.",
+ },
+ {
+ "table_specifications": {
+ "title": "Big ones, small ones",
+ "widths": [10, 10, 10, 10],
+ "header_rows": "not an integer",
+ "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]],
+ "indent": 2,
+ },
+ "error_msg": "TableBuilder attribute header_rows must be an integer.",
+ },
+ {
+ "table_specifications": {
+ "title": "Some as big as your head!",
+ "widths": [10, 10, 10, 10],
+ "header_rows": 1,
+ "headers": ("not", "a", "list"),
+ "indent": 2,
+ },
+ "error_msg": "TableBuilder attribute headers must be a two-dimensional list of strings.",
+ },
+ {
+ "table_specifications": {
+ "title": "(And bigger)",
+ "widths": [10, 10, 10, 10],
+ "header_rows": 1,
+ "headers": ["not", "two", "dimensional"],
+ "indent": 2,
+ },
+ "error_msg": "TableBuilder attribute headers must be a two-dimensional list of strings.",
+ },
+ {
+ "table_specifications": {
+ "title": "Give 'em a twist, a flick of the wrist'",
+ "widths": [10, 10, 10, 10],
+ "header_rows": 1,
+ "headers": [[1, 2, 3]],
+ "indent": 2,
+ },
+ "error_msg": "TableBuilder attribute headers must be a two-dimensional list of strings.",
+ },
+ {
+ "table_specifications": {
+ "title": "That's what the showman said!",
+ "widths": [10, 10, 10, 10],
+ "header_rows": 1,
+ "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]],
+ "indent": "not an integer",
+ },
+ "error_msg": "TableBuilder attribute indent must be an integer.",
+ },
+]
+
+table_specifications = {
+ "title": "I've got a lovely bunch of coconuts",
+ "widths": [10, 10, 10],
+ "header_rows": 1,
+ "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]],
+ "indent": 2,
+}
+
+
+@pytest.mark.parametrize("testdata", testdata)
+def test_table_builder_invalid_attributes(testdata):
+ from perfdocs.doc_helpers import TableBuilder
+
+ table_specifications = testdata["table_specifications"]
+ error_msg = testdata["error_msg"]
+
+ with pytest.raises(TypeError) as error:
+ TableBuilder(
+ table_specifications["title"],
+ table_specifications["widths"],
+ table_specifications["header_rows"],
+ table_specifications["headers"],
+ table_specifications["indent"],
+ )
+
+ assert str(error.value) == error_msg
+
+
+def test_table_builder_mismatched_columns():
+ from perfdocs.doc_helpers import MismatchedRowLengthsException, TableBuilder
+
+ table_specifications = {
+ "title": "I've got a lovely bunch of coconuts",
+ "widths": [10, 10, 10, 42],
+ "header_rows": 1,
+ "headers": [["Coconut 1", "Coconut 2", "Coconut 3"]],
+ "indent": 2,
+ }
+
+ with pytest.raises(MismatchedRowLengthsException) as error:
+ TableBuilder(
+ table_specifications["title"],
+ table_specifications["widths"],
+ table_specifications["header_rows"],
+ table_specifications["headers"],
+ table_specifications["indent"],
+ )
+ assert (
+ str(error.value)
+ == "Number of table headers must match number of column widths."
+ )
+
+
+def test_table_builder_add_row_too_long():
+ from perfdocs.doc_helpers import MismatchedRowLengthsException, TableBuilder
+
+ table = TableBuilder(
+ table_specifications["title"],
+ table_specifications["widths"],
+ table_specifications["header_rows"],
+ table_specifications["headers"],
+ table_specifications["indent"],
+ )
+ with pytest.raises(MismatchedRowLengthsException) as error:
+ table.add_row(
+ ["big ones", "small ones", "some as big as your head!", "(and bigger)"]
+ )
+ assert (
+ str(error.value)
+ == "Number of items in a row must must number of columns defined."
+ )
+
+
+def test_table_builder_add_rows_type_error():
+ from perfdocs.doc_helpers import TableBuilder
+
+ table = TableBuilder(
+ table_specifications["title"],
+ table_specifications["widths"],
+ table_specifications["header_rows"],
+ table_specifications["headers"],
+ table_specifications["indent"],
+ )
+ with pytest.raises(TypeError) as error:
+ table.add_rows(
+ ["big ones", "small ones", "some as big as your head!", "(and bigger)"]
+ )
+ assert str(error.value) == "add_rows() requires a two-dimensional list of strings."
+
+
+def test_table_builder_validate():
+ from perfdocs.doc_helpers import TableBuilder
+
+ table = TableBuilder(
+ table_specifications["title"],
+ table_specifications["widths"],
+ table_specifications["header_rows"],
+ table_specifications["headers"],
+ table_specifications["indent"],
+ )
+ table.add_row(["big ones", "small ones", "some as big as your head!"])
+ table.add_row(
+ ["Give 'em a twist", "A flick of the wrist", "That's what the showman said!"]
+ )
+ table = table.finish_table()
+ print(table)
+ assert (
+ table == " .. list-table:: **I've got a lovely bunch of coconuts**\n"
+ " :widths: 10 10 10\n :header-rows: 1\n\n"
+ " * - **Coconut 1**\n - Coconut 2\n - Coconut 3\n"
+ " * - **big ones**\n - small ones\n - some as big as your head!\n"
+ " * - **Give 'em a twist**\n - A flick of the wrist\n"
+ " - That's what the showman said!\n\n"
+ )
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_rst.py b/tools/lint/test/test_rst.py
new file mode 100644
index 0000000000..e540081a94
--- /dev/null
+++ b/tools/lint/test/test_rst.py
@@ -0,0 +1,25 @@
+import mozunit
+import pytest
+from mozfile import which
+
+LINTER = "rst"
+pytestmark = pytest.mark.skipif(
+ not which("rstcheck"), reason="rstcheck is not installed"
+)
+
+
+def test_basic(lint, paths):
+ results = lint(paths())
+ assert len(results) == 2
+
+ assert "Title underline too short" in results[0].message
+ assert results[0].level == "error"
+ assert results[0].relpath == "bad.rst"
+
+ assert "Title overline & underline mismatch" in results[1].message
+ assert results[1].level == "error"
+ assert results[1].relpath == "bad2.rst"
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_ruff.py b/tools/lint/test/test_ruff.py
new file mode 100644
index 0000000000..fbb483780e
--- /dev/null
+++ b/tools/lint/test/test_ruff.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+
+# 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 pprint import pprint
+from textwrap import dedent
+
+import mozunit
+
+LINTER = "ruff"
+fixed = 0
+
+
+def test_lint_fix(lint, create_temp_file):
+ contents = dedent(
+ """
+ import distutils
+ print("hello!")
+ """
+ )
+
+ path = create_temp_file(contents, "bad.py")
+ lint([path], fix=True)
+ assert fixed == 1
+
+
+def test_lint_ruff(lint, paths):
+ results = lint(paths())
+ pprint(results, indent=2)
+ assert len(results) == 2
+ assert results[0].level == "error"
+ assert results[0].relpath == "bad.py"
+ assert "`distutils` imported but unused" in results[0].message
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_rustfmt.py b/tools/lint/test/test_rustfmt.py
new file mode 100644
index 0000000000..c176a2695d
--- /dev/null
+++ b/tools/lint/test/test_rustfmt.py
@@ -0,0 +1,70 @@
+import mozunit
+
+LINTER = "rustfmt"
+fixed = 0
+
+
+def test_good(lint, config, paths):
+ results = lint(paths("subdir/good.rs"))
+ print(results)
+ assert len(results) == 0
+
+
+def test_basic(lint, config, paths):
+ results = lint(paths("subdir/bad.rs"))
+ print(results)
+ assert len(results) >= 1
+
+ assert "Reformat rust" in results[0].message
+ assert results[0].level == "warning"
+ assert results[0].lineno == 4
+ assert "bad.rs" in results[0].path
+ assert "Print text to the console" in results[0].diff
+
+
+def test_dir(lint, config, paths):
+ results = lint(paths("subdir/"))
+ print(results)
+ assert len(results) >= 4
+
+ assert "Reformat rust" in results[0].message
+ assert results[0].level == "warning"
+ assert results[0].lineno == 4
+ assert "bad.rs" in results[0].path
+ assert "Print text to the console" in results[0].diff
+
+ assert "Reformat rust" in results[1].message
+ assert results[1].level == "warning"
+ assert results[1].lineno == 4
+ assert "bad2.rs" in results[1].path
+ assert "Print text to the console" in results[1].diff
+
+
+def test_fix(lint, create_temp_file):
+
+ contents = """fn main() {
+ // Statements here are executed when the compiled binary is called
+
+ // Print text to the console
+ println!("Hello World!");
+ let mut a;
+ let mut b=1;
+ let mut vec = Vec::new();
+ vec.push(1);
+ vec.push(2);
+
+
+ for x in 5..10 - 5 {
+ a = x;
+ }
+
+ }
+"""
+
+ path = create_temp_file(contents, "bad.rs")
+ lint([path], fix=True)
+ assert fixed == 3
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_shellcheck.py b/tools/lint/test/test_shellcheck.py
new file mode 100644
index 0000000000..1b41e298bd
--- /dev/null
+++ b/tools/lint/test/test_shellcheck.py
@@ -0,0 +1,26 @@
+import mozunit
+import pytest
+from mozfile import which
+
+LINTER = "shellcheck"
+pytestmark = pytest.mark.skipif(
+ not which("shellcheck"), reason="shellcheck is not installed"
+)
+
+
+def test_basic(lint, paths):
+ results = lint(paths())
+ print(results)
+ assert len(results) == 2
+
+ assert "hello appears unused" in results[0].message
+ assert results[0].level == "error"
+ assert results[0].relpath == "bad.sh"
+
+ assert "Double quote to prevent" in results[1].message
+ assert results[1].level == "error"
+ assert results[1].relpath == "bad.sh"
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_stylelint.py b/tools/lint/test/test_stylelint.py
new file mode 100644
index 0000000000..5d758ad318
--- /dev/null
+++ b/tools/lint/test/test_stylelint.py
@@ -0,0 +1,65 @@
+import mozunit
+import pytest
+from conftest import build
+
+LINTER = "stylelint"
+fixed = 0
+
+
+@pytest.fixture
+def stylelint(lint):
+ def inner(*args, **kwargs):
+ # --ignore-path is because stylelint doesn't have the --no-ignore option
+ # and therefore needs to be given an empty file for the tests to work.
+ kwargs["extra_args"] = [
+ "--ignore-path=tools/lint/test/files/eslint/testprettierignore",
+ ]
+ return lint(*args, **kwargs)
+
+ return inner
+
+
+def test_lint_with_global_exclude(lint, config, paths):
+ config["exclude"] = ["subdir", "import"]
+ # This uses lint directly as we need to not ignore the excludes.
+ results = lint(paths(), config=config, root=build.topsrcdir)
+ assert len(results) == 0
+
+
+def test_no_files_to_lint(stylelint, config, paths):
+ # A directory with no files to lint.
+ results = stylelint(paths("nolint"), root=build.topsrcdir)
+ assert results == []
+
+ # Errors still show up even when a directory with no files is passed in.
+ results = stylelint(paths("nolint", "subdir/bad.css"), root=build.topsrcdir)
+ assert len(results) == 1
+
+
+def test_stylelint(stylelint, config, create_temp_file):
+ contents = """#foo {
+ font-size: 12px;
+ font-size: 12px;
+}
+"""
+ path = create_temp_file(contents, "bad.css")
+ results = stylelint([path], config=config, root=build.topsrcdir)
+
+ assert len(results) == 1
+
+
+def test_stylelint_fix(stylelint, config, create_temp_file):
+ contents = """#foo {
+ font-size: 12px;
+ font-size: 12px;
+}
+"""
+ path = create_temp_file(contents, "bad.css")
+ stylelint([path], config=config, root=build.topsrcdir, fix=True)
+
+ # stylelint returns counts of files fixed, not errors fixed.
+ assert fixed == 1
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_trojan_source.py b/tools/lint/test/test_trojan_source.py
new file mode 100644
index 0000000000..64a3789c37
--- /dev/null
+++ b/tools/lint/test/test_trojan_source.py
@@ -0,0 +1,25 @@
+import mozunit
+
+LINTER = "trojan-source"
+
+
+def test_lint_trojan_source(lint, paths):
+ results = lint(paths())
+ print(results)
+ assert len(results) == 3
+
+ assert "disallowed characters" in results[0].message
+ assert results[0].level == "error"
+ assert "commenting-out.cpp" in results[0].relpath
+
+ assert "disallowed characters" in results[1].message
+ assert results[1].level == "error"
+ assert "early-return.py" in results[1].relpath
+
+ assert "disallowed characters" in results[2].message
+ assert results[2].level == "error"
+ assert "invisible-function.rs" in results[2].relpath
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_updatebot.py b/tools/lint/test/test_updatebot.py
new file mode 100644
index 0000000000..55842e99c0
--- /dev/null
+++ b/tools/lint/test/test_updatebot.py
@@ -0,0 +1,44 @@
+import os
+
+import mozunit
+
+LINTER = "updatebot"
+
+
+def test_basic(lint, paths):
+ results = []
+
+ for p in paths():
+ for (root, dirs, files) in os.walk(p):
+ for f in files:
+ if f == ".yamllint":
+ continue
+
+ filepath = os.path.join(root, f)
+ result = lint(filepath, testing=True)
+ if result:
+ results.append(result)
+
+ assert len(results) == 2
+
+ expected_results = 0
+
+ for r in results:
+ if "no-revision.yaml" in r[0].path:
+ expected_results += 1
+ assert "no-revision.yaml" in r[0].path
+ assert (
+ 'If "vendoring" is present, "revision" must be present in "origin"'
+ in r[0].message
+ )
+
+ if "cargo-mismatch.yaml" in r[0].path:
+ expected_results += 1
+ assert "cargo-mismatch.yaml" in r[0].path
+ assert "wasn't found in Cargo.lock" in r[0].message
+
+ assert expected_results == 2
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/tools/lint/test/test_yaml.py b/tools/lint/test/test_yaml.py
new file mode 100644
index 0000000000..63b678d152
--- /dev/null
+++ b/tools/lint/test/test_yaml.py
@@ -0,0 +1,28 @@
+import mozunit
+
+LINTER = "yaml"
+
+
+def test_basic(lint, paths):
+ results = lint(paths())
+
+ assert len(results) == 3
+
+ assert "line too long (122 > 80 characters)" in results[0].message
+ assert results[0].level == "error"
+ assert "bad.yml" in results[0].relpath
+ assert results[0].lineno == 3
+
+ assert "wrong indentation: expected 4 but found 8" in results[1].message
+ assert results[1].level == "error"
+ assert "bad.yml" in results[1].relpath
+ assert results[0].lineno == 3
+
+ assert "could not find expected" in results[2].message
+ assert results[2].level == "error"
+ assert "bad.yml" in results[2].relpath
+ assert results[2].lineno == 9
+
+
+if __name__ == "__main__":
+ mozunit.main()