summaryrefslogtreecommitdiffstats
path: root/python/mozlint/test
diff options
context:
space:
mode:
Diffstat (limited to 'python/mozlint/test')
-rw-r--r--python/mozlint/test/__init__.py0
-rw-r--r--python/mozlint/test/conftest.py66
-rw-r--r--python/mozlint/test/files/foobar.js2
-rw-r--r--python/mozlint/test/files/foobar.py3
-rw-r--r--python/mozlint/test/files/irrelevant/file.txt1
-rw-r--r--python/mozlint/test/files/no_foobar.js2
-rw-r--r--python/mozlint/test/filter/a.js0
-rw-r--r--python/mozlint/test/filter/a.py0
-rw-r--r--python/mozlint/test/filter/foo/empty.txt0
-rw-r--r--python/mozlint/test/filter/foobar/empty.txt0
-rw-r--r--python/mozlint/test/filter/subdir1/b.js0
-rw-r--r--python/mozlint/test/filter/subdir1/b.py0
-rw-r--r--python/mozlint/test/filter/subdir1/subdir3/d.js0
-rw-r--r--python/mozlint/test/filter/subdir1/subdir3/d.py0
-rw-r--r--python/mozlint/test/filter/subdir2/c.js0
-rw-r--r--python/mozlint/test/filter/subdir2/c.py0
-rw-r--r--python/mozlint/test/linters/badreturncode.yml8
-rw-r--r--python/mozlint/test/linters/excludes.yml10
-rw-r--r--python/mozlint/test/linters/excludes_empty.yml8
-rw-r--r--python/mozlint/test/linters/explicit_path.yml8
-rw-r--r--python/mozlint/test/linters/external.py74
-rw-r--r--python/mozlint/test/linters/external.yml8
-rw-r--r--python/mozlint/test/linters/global.yml8
-rw-r--r--python/mozlint/test/linters/global_payload.py38
-rw-r--r--python/mozlint/test/linters/global_skipped.yml8
-rw-r--r--python/mozlint/test/linters/invalid_exclude.yml6
-rw-r--r--python/mozlint/test/linters/invalid_extension.ym5
-rw-r--r--python/mozlint/test/linters/invalid_include.yml6
-rw-r--r--python/mozlint/test/linters/invalid_include_with_glob.yml6
-rw-r--r--python/mozlint/test/linters/invalid_support_files.yml6
-rw-r--r--python/mozlint/test/linters/invalid_type.yml5
-rw-r--r--python/mozlint/test/linters/missing_attrs.yml3
-rw-r--r--python/mozlint/test/linters/missing_definition.yml1
-rw-r--r--python/mozlint/test/linters/multiple.yml19
-rw-r--r--python/mozlint/test/linters/non_existing_exclude.yml7
-rw-r--r--python/mozlint/test/linters/non_existing_include.yml7
-rw-r--r--python/mozlint/test/linters/non_existing_support_files.yml7
-rw-r--r--python/mozlint/test/linters/raises.yml6
-rw-r--r--python/mozlint/test/linters/regex.yml10
-rw-r--r--python/mozlint/test/linters/setup.yml9
-rw-r--r--python/mozlint/test/linters/setupfailed.yml9
-rw-r--r--python/mozlint/test/linters/setupraised.yml9
-rw-r--r--python/mozlint/test/linters/slow.yml8
-rw-r--r--python/mozlint/test/linters/string.yml9
-rw-r--r--python/mozlint/test/linters/structured.yml8
-rw-r--r--python/mozlint/test/linters/support_files.yml10
-rw-r--r--python/mozlint/test/linters/warning.yml11
-rw-r--r--python/mozlint/test/linters/warning_no_code_review.yml12
-rw-r--r--python/mozlint/test/python.ini11
-rw-r--r--python/mozlint/test/runcli.py17
-rw-r--r--python/mozlint/test/test_cli.py127
-rw-r--r--python/mozlint/test/test_editor.py92
-rw-r--r--python/mozlint/test/test_formatters.py141
-rw-r--r--python/mozlint/test/test_parser.py80
-rw-r--r--python/mozlint/test/test_pathutils.py166
-rw-r--r--python/mozlint/test/test_result.py26
-rw-r--r--python/mozlint/test/test_roller.py396
-rw-r--r--python/mozlint/test/test_types.py84
58 files changed, 1563 insertions, 0 deletions
diff --git a/python/mozlint/test/__init__.py b/python/mozlint/test/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/mozlint/test/__init__.py
diff --git a/python/mozlint/test/conftest.py b/python/mozlint/test/conftest.py
new file mode 100644
index 0000000000..9683c23b13
--- /dev/null
+++ b/python/mozlint/test/conftest.py
@@ -0,0 +1,66 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import sys
+from argparse import Namespace
+
+import pytest
+
+from mozlint import LintRoller
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+@pytest.fixture
+def lint(request):
+ lintargs = getattr(request.module, "lintargs", {})
+ lint = LintRoller(root=here, **lintargs)
+
+ # Add a few super powers to our lint instance
+ def mock_vcs(files):
+ def _fake_vcs_files(*args, **kwargs):
+ return files
+
+ setattr(lint.vcs, "get_changed_files", _fake_vcs_files)
+ setattr(lint.vcs, "get_outgoing_files", _fake_vcs_files)
+
+ setattr(lint, "vcs", Namespace())
+ setattr(lint, "mock_vcs", mock_vcs)
+ return lint
+
+
+@pytest.fixture(scope="session")
+def filedir():
+ return os.path.join(here, "files")
+
+
+@pytest.fixture(scope="module")
+def files(filedir, request):
+ suffix_filter = getattr(request.module, "files", [""])
+ return [
+ os.path.join(filedir, p)
+ for p in os.listdir(filedir)
+ if any(p.endswith(suffix) for suffix in suffix_filter)
+ ]
+
+
+@pytest.fixture(scope="session")
+def lintdir():
+ lintdir = os.path.join(here, "linters")
+ sys.path.insert(0, lintdir)
+ return lintdir
+
+
+@pytest.fixture(scope="module")
+def linters(lintdir):
+ def inner(*names):
+ return [
+ os.path.join(lintdir, p)
+ for p in os.listdir(lintdir)
+ if any(os.path.splitext(p)[0] == name for name in names)
+ if os.path.splitext(p)[1] == ".yml"
+ ]
+
+ return inner
diff --git a/python/mozlint/test/files/foobar.js b/python/mozlint/test/files/foobar.js
new file mode 100644
index 0000000000..d9754d0a2f
--- /dev/null
+++ b/python/mozlint/test/files/foobar.js
@@ -0,0 +1,2 @@
+// Oh no.. we called this variable foobar, bad!
+var foobar = "a string";
diff --git a/python/mozlint/test/files/foobar.py b/python/mozlint/test/files/foobar.py
new file mode 100644
index 0000000000..3b6416d211
--- /dev/null
+++ b/python/mozlint/test/files/foobar.py
@@ -0,0 +1,3 @@
+# Oh no.. we called this variable foobar, bad!
+
+foobar = "a string"
diff --git a/python/mozlint/test/files/irrelevant/file.txt b/python/mozlint/test/files/irrelevant/file.txt
new file mode 100644
index 0000000000..323fae03f4
--- /dev/null
+++ b/python/mozlint/test/files/irrelevant/file.txt
@@ -0,0 +1 @@
+foobar
diff --git a/python/mozlint/test/files/no_foobar.js b/python/mozlint/test/files/no_foobar.js
new file mode 100644
index 0000000000..6b95d646c0
--- /dev/null
+++ b/python/mozlint/test/files/no_foobar.js
@@ -0,0 +1,2 @@
+// What a relief
+var properlyNamed = "a string";
diff --git a/python/mozlint/test/filter/a.js b/python/mozlint/test/filter/a.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/mozlint/test/filter/a.js
diff --git a/python/mozlint/test/filter/a.py b/python/mozlint/test/filter/a.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/mozlint/test/filter/a.py
diff --git a/python/mozlint/test/filter/foo/empty.txt b/python/mozlint/test/filter/foo/empty.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/mozlint/test/filter/foo/empty.txt
diff --git a/python/mozlint/test/filter/foobar/empty.txt b/python/mozlint/test/filter/foobar/empty.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/mozlint/test/filter/foobar/empty.txt
diff --git a/python/mozlint/test/filter/subdir1/b.js b/python/mozlint/test/filter/subdir1/b.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/mozlint/test/filter/subdir1/b.js
diff --git a/python/mozlint/test/filter/subdir1/b.py b/python/mozlint/test/filter/subdir1/b.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/mozlint/test/filter/subdir1/b.py
diff --git a/python/mozlint/test/filter/subdir1/subdir3/d.js b/python/mozlint/test/filter/subdir1/subdir3/d.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/mozlint/test/filter/subdir1/subdir3/d.js
diff --git a/python/mozlint/test/filter/subdir1/subdir3/d.py b/python/mozlint/test/filter/subdir1/subdir3/d.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/mozlint/test/filter/subdir1/subdir3/d.py
diff --git a/python/mozlint/test/filter/subdir2/c.js b/python/mozlint/test/filter/subdir2/c.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/mozlint/test/filter/subdir2/c.js
diff --git a/python/mozlint/test/filter/subdir2/c.py b/python/mozlint/test/filter/subdir2/c.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/python/mozlint/test/filter/subdir2/c.py
diff --git a/python/mozlint/test/linters/badreturncode.yml b/python/mozlint/test/linters/badreturncode.yml
new file mode 100644
index 0000000000..72abf83cc7
--- /dev/null
+++ b/python/mozlint/test/linters/badreturncode.yml
@@ -0,0 +1,8 @@
+---
+BadReturnCodeLinter:
+ description: Returns an error code no matter what
+ include:
+ - files
+ type: external
+ extensions: ['.js', '.jsm']
+ payload: external:badreturncode
diff --git a/python/mozlint/test/linters/excludes.yml b/python/mozlint/test/linters/excludes.yml
new file mode 100644
index 0000000000..1fc1068735
--- /dev/null
+++ b/python/mozlint/test/linters/excludes.yml
@@ -0,0 +1,10 @@
+---
+ExcludesLinter:
+ description: >-
+ Make sure the string foobar never appears in browser js
+ files because it is bad
+ rule: no-foobar
+ exclude: ['**/foobar.js']
+ extensions: ['.js', 'jsm']
+ type: string
+ payload: foobar
diff --git a/python/mozlint/test/linters/excludes_empty.yml b/python/mozlint/test/linters/excludes_empty.yml
new file mode 100644
index 0000000000..03cd1aecab
--- /dev/null
+++ b/python/mozlint/test/linters/excludes_empty.yml
@@ -0,0 +1,8 @@
+---
+ExcludesEmptyLinter:
+ description: It's bad to have the string foobar in js files.
+ include:
+ - files
+ type: external
+ extensions: ['.js', '.jsm']
+ payload: foobar
diff --git a/python/mozlint/test/linters/explicit_path.yml b/python/mozlint/test/linters/explicit_path.yml
new file mode 100644
index 0000000000..1e7e8f4bf1
--- /dev/null
+++ b/python/mozlint/test/linters/explicit_path.yml
@@ -0,0 +1,8 @@
+---
+ExplicitPathLinter:
+ description: Only lint a specific file name
+ rule: no-foobar
+ include:
+ - files/no_foobar.js
+ type: string
+ payload: foobar
diff --git a/python/mozlint/test/linters/external.py b/python/mozlint/test/linters/external.py
new file mode 100644
index 0000000000..9c2e58909d
--- /dev/null
+++ b/python/mozlint/test/linters/external.py
@@ -0,0 +1,74 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import time
+
+from mozlint import result
+from mozlint.errors import LintException
+
+
+def badreturncode(files, config, **lintargs):
+ return 1
+
+
+def external(files, config, **lintargs):
+ if lintargs.get("fix"):
+ # mimics no results because they got fixed
+ return []
+
+ results = []
+ for path in files:
+ if os.path.isdir(path):
+ continue
+
+ with open(path, "r") as fh:
+ for i, line in enumerate(fh.readlines()):
+ if "foobar" in line:
+ results.append(
+ result.from_config(
+ config, path=path, lineno=i + 1, column=1, rule="no-foobar"
+ )
+ )
+ return results
+
+
+def raises(files, config, **lintargs):
+ raise LintException("Oh no something bad happened!")
+
+
+def slow(files, config, **lintargs):
+ time.sleep(2)
+ return []
+
+
+def structured(files, config, logger, **kwargs):
+ for path in files:
+ if os.path.isdir(path):
+ continue
+
+ with open(path, "r") as fh:
+ for i, line in enumerate(fh.readlines()):
+ if "foobar" in line:
+ logger.lint_error(
+ path=path, lineno=i + 1, column=1, rule="no-foobar"
+ )
+
+
+def passes(files, config, **lintargs):
+ return []
+
+
+def setup(**lintargs):
+ print("setup passed")
+
+
+def setupfailed(**lintargs):
+ print("setup failed")
+ return 1
+
+
+def setupraised(**lintargs):
+ print("setup raised")
+ raise LintException("oh no setup failed")
diff --git a/python/mozlint/test/linters/external.yml b/python/mozlint/test/linters/external.yml
new file mode 100644
index 0000000000..574b8df4cb
--- /dev/null
+++ b/python/mozlint/test/linters/external.yml
@@ -0,0 +1,8 @@
+---
+ExternalLinter:
+ description: It's bad to have the string foobar in js files.
+ include:
+ - files
+ type: external
+ extensions: ['.js', '.jsm']
+ payload: external:external
diff --git a/python/mozlint/test/linters/global.yml b/python/mozlint/test/linters/global.yml
new file mode 100644
index 0000000000..47d5ce81e4
--- /dev/null
+++ b/python/mozlint/test/linters/global.yml
@@ -0,0 +1,8 @@
+---
+GlobalLinter:
+ description: It's bad to have the string foobar in js files.
+ include:
+ - files
+ type: global
+ extensions: ['.js', '.jsm']
+ payload: global_payload:global_payload
diff --git a/python/mozlint/test/linters/global_payload.py b/python/mozlint/test/linters/global_payload.py
new file mode 100644
index 0000000000..ec620b6af1
--- /dev/null
+++ b/python/mozlint/test/linters/global_payload.py
@@ -0,0 +1,38 @@
+# 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 mozpack.path as mozpath
+from external import external
+from mozpack.files import FileFinder
+
+from mozlint import result
+
+
+def global_payload(config, **lintargs):
+ # A global linter that runs the external linter to actually lint.
+ finder = FileFinder(lintargs["root"])
+ files = [mozpath.join(lintargs["root"], p) for p, _ in finder.find("files/**")]
+ issues = external(files, config, **lintargs)
+ for issue in issues:
+ # Make issue look like it comes from this linter.
+ issue.linter = "global_payload"
+ return issues
+
+
+def global_skipped(config, **lintargs):
+ # A global linter that always registers a lint error. Absence of
+ # this error shows that the path exclusion mechanism can cause
+ # global lint payloads to not be invoked at all. In particular,
+ # the `extensions` field means that nothing under `files/**` will
+ # match.
+
+ finder = FileFinder(lintargs["root"])
+ files = [mozpath.join(lintargs["root"], p) for p, _ in finder.find("files/**")]
+
+ issues = []
+ issues.append(
+ result.from_config(
+ config, path=files[0], lineno=1, column=1, rule="not-skipped"
+ )
+ )
diff --git a/python/mozlint/test/linters/global_skipped.yml b/python/mozlint/test/linters/global_skipped.yml
new file mode 100644
index 0000000000..99b784e8be
--- /dev/null
+++ b/python/mozlint/test/linters/global_skipped.yml
@@ -0,0 +1,8 @@
+---
+GlobalSkippedLinter:
+ description: It's bad to run global linters when nothing matches.
+ include:
+ - files
+ type: global
+ extensions: ['.non.existent.extension']
+ payload: global_payload:global_skipped
diff --git a/python/mozlint/test/linters/invalid_exclude.yml b/python/mozlint/test/linters/invalid_exclude.yml
new file mode 100644
index 0000000000..7231d2c146
--- /dev/null
+++ b/python/mozlint/test/linters/invalid_exclude.yml
@@ -0,0 +1,6 @@
+---
+BadExcludeLinter:
+ description: Has an invalid exclude directive.
+ exclude: [0, 1] # should be a list of strings
+ type: string
+ payload: foobar
diff --git a/python/mozlint/test/linters/invalid_extension.ym b/python/mozlint/test/linters/invalid_extension.ym
new file mode 100644
index 0000000000..435fa10320
--- /dev/null
+++ b/python/mozlint/test/linters/invalid_extension.ym
@@ -0,0 +1,5 @@
+---
+BadExtensionLinter:
+ description: Has an invalid file extension.
+ type: string
+ payload: foobar
diff --git a/python/mozlint/test/linters/invalid_include.yml b/python/mozlint/test/linters/invalid_include.yml
new file mode 100644
index 0000000000..b76b3e6a61
--- /dev/null
+++ b/python/mozlint/test/linters/invalid_include.yml
@@ -0,0 +1,6 @@
+---
+BadIncludeLinter:
+ description: Has an invalid include directive.
+ include: should be a list
+ type: string
+ payload: foobar
diff --git a/python/mozlint/test/linters/invalid_include_with_glob.yml b/python/mozlint/test/linters/invalid_include_with_glob.yml
new file mode 100644
index 0000000000..857bb1376b
--- /dev/null
+++ b/python/mozlint/test/linters/invalid_include_with_glob.yml
@@ -0,0 +1,6 @@
+---
+BadIncludeLinterWithGlob:
+ description: Has an invalid include directive.
+ include: ['**/*.js']
+ type: string
+ payload: foobar
diff --git a/python/mozlint/test/linters/invalid_support_files.yml b/python/mozlint/test/linters/invalid_support_files.yml
new file mode 100644
index 0000000000..db39597d68
--- /dev/null
+++ b/python/mozlint/test/linters/invalid_support_files.yml
@@ -0,0 +1,6 @@
+---
+BadSupportFilesLinter:
+ description: Has an invalid support files directive.
+ support-files: should be a list
+ type: string
+ payload: foobar
diff --git a/python/mozlint/test/linters/invalid_type.yml b/python/mozlint/test/linters/invalid_type.yml
new file mode 100644
index 0000000000..29d82e541e
--- /dev/null
+++ b/python/mozlint/test/linters/invalid_type.yml
@@ -0,0 +1,5 @@
+---
+BadTypeLinter:
+ description: Has an invalid type.
+ type: invalid
+ payload: foobar
diff --git a/python/mozlint/test/linters/missing_attrs.yml b/python/mozlint/test/linters/missing_attrs.yml
new file mode 100644
index 0000000000..5abe15fcfc
--- /dev/null
+++ b/python/mozlint/test/linters/missing_attrs.yml
@@ -0,0 +1,3 @@
+---
+MissingAttrsLinter:
+ description: Missing type and payload
diff --git a/python/mozlint/test/linters/missing_definition.yml b/python/mozlint/test/linters/missing_definition.yml
new file mode 100644
index 0000000000..d66b2cb781
--- /dev/null
+++ b/python/mozlint/test/linters/missing_definition.yml
@@ -0,0 +1 @@
+# No definition
diff --git a/python/mozlint/test/linters/multiple.yml b/python/mozlint/test/linters/multiple.yml
new file mode 100644
index 0000000000..5b880b3691
--- /dev/null
+++ b/python/mozlint/test/linters/multiple.yml
@@ -0,0 +1,19 @@
+---
+StringLinter:
+ description: >-
+ Make sure the string foobar never appears in browser js
+ files because it is bad
+ rule: no-foobar
+ extensions: ['.js', 'jsm']
+ type: string
+ payload: foobar
+
+---
+RegexLinter:
+ description: >-
+ Make sure the string foobar never appears in browser js
+ files because it is bad
+ rule: no-foobar
+ extensions: ['.js', 'jsm']
+ type: regex
+ payload: foobar
diff --git a/python/mozlint/test/linters/non_existing_exclude.yml b/python/mozlint/test/linters/non_existing_exclude.yml
new file mode 100644
index 0000000000..8190123027
--- /dev/null
+++ b/python/mozlint/test/linters/non_existing_exclude.yml
@@ -0,0 +1,7 @@
+---
+BadExcludeLinter:
+ description: Has an invalid exclude directive.
+ exclude:
+ - files/does_not_exist
+ type: string
+ payload: foobar
diff --git a/python/mozlint/test/linters/non_existing_include.yml b/python/mozlint/test/linters/non_existing_include.yml
new file mode 100644
index 0000000000..5443d751ed
--- /dev/null
+++ b/python/mozlint/test/linters/non_existing_include.yml
@@ -0,0 +1,7 @@
+---
+BadIncludeLinter:
+ description: Has an invalid include directive.
+ include:
+ - files/does_not_exist
+ type: string
+ payload: foobar
diff --git a/python/mozlint/test/linters/non_existing_support_files.yml b/python/mozlint/test/linters/non_existing_support_files.yml
new file mode 100644
index 0000000000..e636fadf93
--- /dev/null
+++ b/python/mozlint/test/linters/non_existing_support_files.yml
@@ -0,0 +1,7 @@
+---
+BadSupportFilesLinter:
+ description: Has an invalid support-files directive.
+ support-files:
+ - files/does_not_exist
+ type: string
+ payload: foobar
diff --git a/python/mozlint/test/linters/raises.yml b/python/mozlint/test/linters/raises.yml
new file mode 100644
index 0000000000..9c0b234779
--- /dev/null
+++ b/python/mozlint/test/linters/raises.yml
@@ -0,0 +1,6 @@
+---
+RaisesLinter:
+ description: Raises an exception
+ include: ['.']
+ type: external
+ payload: external:raises
diff --git a/python/mozlint/test/linters/regex.yml b/python/mozlint/test/linters/regex.yml
new file mode 100644
index 0000000000..2c9c812428
--- /dev/null
+++ b/python/mozlint/test/linters/regex.yml
@@ -0,0 +1,10 @@
+---
+RegexLinter:
+ description: >-
+ Make sure the string foobar never appears in a js variable
+ file because it is bad.
+ rule: no-foobar
+ include: ['.']
+ extensions: ['js', '.jsm']
+ type: regex
+ payload: foobar
diff --git a/python/mozlint/test/linters/setup.yml b/python/mozlint/test/linters/setup.yml
new file mode 100644
index 0000000000..ac75d72c70
--- /dev/null
+++ b/python/mozlint/test/linters/setup.yml
@@ -0,0 +1,9 @@
+---
+SetupLinter:
+ description: It's bad to have the string foobar in js files.
+ include:
+ - files
+ type: external
+ extensions: ['.js', '.jsm']
+ payload: external:external
+ setup: external:setup
diff --git a/python/mozlint/test/linters/setupfailed.yml b/python/mozlint/test/linters/setupfailed.yml
new file mode 100644
index 0000000000..1e3543286f
--- /dev/null
+++ b/python/mozlint/test/linters/setupfailed.yml
@@ -0,0 +1,9 @@
+---
+SetupFailedLinter:
+ description: It's bad to have the string foobar in js files.
+ include:
+ - files
+ type: external
+ extensions: ['.js', '.jsm']
+ payload: external:external
+ setup: external:setupfailed
diff --git a/python/mozlint/test/linters/setupraised.yml b/python/mozlint/test/linters/setupraised.yml
new file mode 100644
index 0000000000..8c987f2d3c
--- /dev/null
+++ b/python/mozlint/test/linters/setupraised.yml
@@ -0,0 +1,9 @@
+---
+SetupRaisedLinter:
+ description: It's bad to have the string foobar in js files.
+ include:
+ - files
+ type: external
+ extensions: ['.js', '.jsm']
+ payload: external:external
+ setup: external:setupraised
diff --git a/python/mozlint/test/linters/slow.yml b/python/mozlint/test/linters/slow.yml
new file mode 100644
index 0000000000..2c47679010
--- /dev/null
+++ b/python/mozlint/test/linters/slow.yml
@@ -0,0 +1,8 @@
+---
+SlowLinter:
+ description: A linter that takes awhile to run
+ include:
+ - files
+ type: external
+ extensions: ['.js', '.jsm']
+ payload: external:slow
diff --git a/python/mozlint/test/linters/string.yml b/python/mozlint/test/linters/string.yml
new file mode 100644
index 0000000000..836d866ae2
--- /dev/null
+++ b/python/mozlint/test/linters/string.yml
@@ -0,0 +1,9 @@
+---
+StringLinter:
+ description: >-
+ Make sure the string foobar never appears in browser js
+ files because it is bad
+ rule: no-foobar
+ extensions: ['.js', 'jsm']
+ type: string
+ payload: foobar
diff --git a/python/mozlint/test/linters/structured.yml b/python/mozlint/test/linters/structured.yml
new file mode 100644
index 0000000000..01ef447ee3
--- /dev/null
+++ b/python/mozlint/test/linters/structured.yml
@@ -0,0 +1,8 @@
+---
+StructuredLinter:
+ description: "It's bad to have the string foobar in js files."
+ include:
+ - files
+ type: structured_log
+ extensions: ['.js', '.jsm']
+ payload: external:structured
diff --git a/python/mozlint/test/linters/support_files.yml b/python/mozlint/test/linters/support_files.yml
new file mode 100644
index 0000000000..0c278d51fa
--- /dev/null
+++ b/python/mozlint/test/linters/support_files.yml
@@ -0,0 +1,10 @@
+---
+SupportFilesLinter:
+ description: Linter that has a few support files
+ include:
+ - files
+ support-files:
+ - '**/*.py'
+ type: external
+ extensions: ['.js', '.jsm']
+ payload: external:passes
diff --git a/python/mozlint/test/linters/warning.yml b/python/mozlint/test/linters/warning.yml
new file mode 100644
index 0000000000..b86bfd07c7
--- /dev/null
+++ b/python/mozlint/test/linters/warning.yml
@@ -0,0 +1,11 @@
+---
+WarningLinter:
+ description: >-
+ Make sure the string foobar never appears in browser js
+ files because it is bad, but not too bad (just a warning)
+ rule: no-foobar
+ level: warning
+ include: ['.']
+ type: string
+ extensions: ['.js', 'jsm']
+ payload: foobar
diff --git a/python/mozlint/test/linters/warning_no_code_review.yml b/python/mozlint/test/linters/warning_no_code_review.yml
new file mode 100644
index 0000000000..20bfc0503b
--- /dev/null
+++ b/python/mozlint/test/linters/warning_no_code_review.yml
@@ -0,0 +1,12 @@
+---
+WarningNoCodeReviewLinter:
+ description: >-
+ Make sure the string foobar never appears in browser js
+ files because it is bad, but not too bad (just a warning)
+ rule: no-foobar-no-code-review
+ level: warning
+ include: ['.']
+ type: string
+ extensions: ['.js', 'jsm']
+ payload: foobar
+ code_review_warnings: false
diff --git a/python/mozlint/test/python.ini b/python/mozlint/test/python.ini
new file mode 100644
index 0000000000..5c2c11d73f
--- /dev/null
+++ b/python/mozlint/test/python.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+subsuite = mozlint
+
+[test_cli.py]
+[test_editor.py]
+[test_formatters.py]
+[test_parser.py]
+[test_pathutils.py]
+[test_result.py]
+[test_roller.py]
+[test_types.py]
diff --git a/python/mozlint/test/runcli.py b/python/mozlint/test/runcli.py
new file mode 100644
index 0000000000..be60a1da19
--- /dev/null
+++ b/python/mozlint/test/runcli.py
@@ -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/.
+
+import os
+import sys
+
+from mozlint import cli
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+if __name__ == "__main__":
+ parser = cli.MozlintParser()
+ args = vars(parser.parse_args(sys.argv[1:]))
+ args["root"] = here
+ args["config_paths"] = [os.path.join(here, "linters")]
+ sys.exit(cli.run(**args))
diff --git a/python/mozlint/test/test_cli.py b/python/mozlint/test/test_cli.py
new file mode 100644
index 0000000000..01aeaa74b4
--- /dev/null
+++ b/python/mozlint/test/test_cli.py
@@ -0,0 +1,127 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import subprocess
+import sys
+from distutils.spawn import find_executable
+
+import mozunit
+import pytest
+
+from mozlint import cli
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+@pytest.fixture
+def parser():
+ return cli.MozlintParser()
+
+
+@pytest.fixture
+def run(parser, lintdir, files):
+ def inner(args=None):
+ args = args or []
+ args.extend(files)
+ lintargs = vars(parser.parse_args(args))
+ lintargs["root"] = here
+ lintargs["config_paths"] = [os.path.join(here, "linters")]
+ return cli.run(**lintargs)
+
+ return inner
+
+
+def test_cli_with_ascii_encoding(run, monkeypatch, capfd):
+ cmd = [sys.executable, "runcli.py", "-l=string", "-f=stylish", "files/foobar.js"]
+ env = os.environ.copy()
+ env["PYTHONPATH"] = os.pathsep.join(sys.path)
+ env["PYTHONIOENCODING"] = "ascii"
+ proc = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ cwd=here,
+ env=env,
+ universal_newlines=True,
+ )
+ out = proc.communicate()[0]
+ assert "Traceback" not in out
+
+
+def test_cli_run_with_fix(run, capfd):
+ ret = run(["-f", "json", "--fix", "--linter", "external"])
+ out, err = capfd.readouterr()
+ assert ret == 0
+ assert out.endswith("{}\n")
+
+
+@pytest.mark.skipif(not find_executable("echo"), reason="No `echo` executable found.")
+def test_cli_run_with_edit(run, parser, capfd):
+ os.environ["EDITOR"] = "echo"
+
+ ret = run(["-f", "compact", "--edit", "--linter", "external"])
+ out, err = capfd.readouterr()
+ out = out.splitlines()
+ assert ret == 1
+ assert out[0].endswith("foobar.js") # from the `echo` editor
+ assert "foobar.js: line 1, col 1, Error" in out[1]
+ assert "foobar.js: line 2, col 1, Error" in out[2]
+ assert "2 problems" in out[-1]
+ assert len(out) == 5
+
+ del os.environ["EDITOR"]
+ with pytest.raises(SystemExit):
+ parser.parse_args(["--edit"])
+
+
+def test_cli_run_with_setup(run, capfd):
+ # implicitly call setup
+ ret = run(["-l", "setup", "-l", "setupfailed", "-l", "setupraised"])
+ out, err = capfd.readouterr()
+ assert "setup passed" in out
+ assert "setup failed" in out
+ assert "setup raised" in out
+ assert ret == 1
+
+ # explicitly call setup
+ ret = run(["-l", "setup", "-l", "setupfailed", "-l", "setupraised", "--setup"])
+ out, err = capfd.readouterr()
+ assert "setup passed" in out
+ assert "setup failed" in out
+ assert "setup raised" in out
+ assert ret == 1
+
+
+def test_cli_for_exclude_list(run, monkeypatch, capfd):
+ ret = run(["-l", "excludes", "--check-exclude-list"])
+ out, err = capfd.readouterr()
+
+ assert "**/foobar.js" in out
+ assert (
+ "The following list of paths are now green and can be removed from the exclude list:"
+ in out
+ )
+
+ ret = run(["-l", "excludes_empty", "--check-exclude-list"])
+ out, err = capfd.readouterr()
+
+ assert "No path in the exclude list is green." in out
+ assert ret == 1
+
+
+def test_cli_run_with_wrong_linters(run, capfd):
+
+ run(["-l", "external", "-l", "foobar"])
+ out, err = capfd.readouterr()
+
+ # Check if it identifes foobar as invalid linter
+ assert "A failure occurred in the foobar linter." in out
+
+ # Check for exception message
+ assert "Invalid linters given, run again using valid linters or no linters" in out
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/python/mozlint/test/test_editor.py b/python/mozlint/test/test_editor.py
new file mode 100644
index 0000000000..7a15a613a6
--- /dev/null
+++ b/python/mozlint/test/test_editor.py
@@ -0,0 +1,92 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import subprocess
+
+import mozunit
+import pytest
+
+from mozlint import editor
+from mozlint.result import Issue, ResultSummary
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+@pytest.fixture
+def capture_commands(monkeypatch):
+ def inner(commands):
+ def fake_subprocess_call(*args, **kwargs):
+ commands.append(args[0])
+
+ monkeypatch.setattr(subprocess, "call", fake_subprocess_call)
+
+ return inner
+
+
+@pytest.fixture
+def result():
+ result = ResultSummary("/fake/root")
+ result.issues["foo.py"].extend(
+ [
+ Issue(
+ linter="no-foobar",
+ path="foo.py",
+ lineno=1,
+ message="Oh no!",
+ ),
+ Issue(
+ linter="no-foobar",
+ path="foo.py",
+ lineno=3,
+ column=10,
+ message="To Yuma!",
+ ),
+ ]
+ )
+ return result
+
+
+def test_no_editor(monkeypatch, capture_commands, result):
+ commands = []
+ capture_commands(commands)
+
+ monkeypatch.delenv("EDITOR", raising=False)
+ editor.edit_issues(result)
+ assert commands == []
+
+
+def test_no_issues(monkeypatch, capture_commands, result):
+ commands = []
+ capture_commands(commands)
+
+ monkeypatch.setenv("EDITOR", "generic")
+ result.issues = {}
+ editor.edit_issues(result)
+ assert commands == []
+
+
+def test_vim(monkeypatch, capture_commands, result):
+ commands = []
+ capture_commands(commands)
+
+ monkeypatch.setenv("EDITOR", "vim")
+ editor.edit_issues(result)
+ assert len(commands) == 1
+ assert commands[0][0] == "vim"
+
+
+def test_generic(monkeypatch, capture_commands, result):
+ commands = []
+ capture_commands(commands)
+
+ monkeypatch.setenv("EDITOR", "generic")
+ editor.edit_issues(result)
+ assert len(commands) == len(result.issues)
+ assert all(c[0] == "generic" for c in commands)
+ assert all("foo.py" in c for c in commands)
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/python/mozlint/test/test_formatters.py b/python/mozlint/test/test_formatters.py
new file mode 100644
index 0000000000..5a276a1c23
--- /dev/null
+++ b/python/mozlint/test/test_formatters.py
@@ -0,0 +1,141 @@
+# 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 json
+
+import attr
+import mozpack.path as mozpath
+import mozunit
+import pytest
+
+from mozlint import formatters
+from mozlint.result import Issue, ResultSummary
+
+NORMALISED_PATHS = {
+ "abc": mozpath.normpath("a/b/c.txt"),
+ "def": mozpath.normpath("d/e/f.txt"),
+ "root": mozpath.abspath("/fake/root"),
+}
+
+EXPECTED = {
+ "compact": {
+ "kwargs": {},
+ "format": """
+/fake/root/a/b/c.txt: line 1, Error - oh no foo (foo)
+/fake/root/a/b/c.txt: line 4, col 10, Error - oh no baz (baz)
+/fake/root/a/b/c.txt: line 5, Error - oh no foo-diff (foo-diff)
+/fake/root/d/e/f.txt: line 4, col 2, Warning - oh no bar (bar-not-allowed)
+
+4 problems
+""".strip(),
+ },
+ "stylish": {
+ "kwargs": {"disable_colors": True},
+ "format": """
+/fake/root/a/b/c.txt
+ 1 error oh no foo (foo)
+ 4:10 error oh no baz (baz)
+ 5 error oh no foo-diff (foo-diff)
+ diff 1
+ - hello
+ + hello2
+
+/fake/root/d/e/f.txt
+ 4:2 warning oh no bar bar-not-allowed (bar)
+
+\u2716 4 problems (3 errors, 1 warning, 0 fixed)
+""".strip(),
+ },
+ "treeherder": {
+ "kwargs": {},
+ "format": """
+TEST-UNEXPECTED-ERROR | /fake/root/a/b/c.txt:1 | oh no foo (foo)
+TEST-UNEXPECTED-ERROR | /fake/root/a/b/c.txt:4:10 | oh no baz (baz)
+TEST-UNEXPECTED-ERROR | /fake/root/a/b/c.txt:5 | oh no foo-diff (foo-diff)
+TEST-UNEXPECTED-WARNING | /fake/root/d/e/f.txt:4:2 | oh no bar (bar-not-allowed)
+""".strip(),
+ },
+ "unix": {
+ "kwargs": {},
+ "format": """
+{abc}:1: foo error: oh no foo
+{abc}:4:10: baz error: oh no baz
+{abc}:5: foo-diff error: oh no foo-diff
+{def}:4:2: bar-not-allowed warning: oh no bar
+""".format(
+ **NORMALISED_PATHS
+ ).strip(),
+ },
+ "summary": {
+ "kwargs": {},
+ "format": """
+{root}/a: 3 errors
+{root}/d: 0 errors, 1 warning
+""".format(
+ **NORMALISED_PATHS
+ ).strip(),
+ },
+}
+
+
+@pytest.fixture
+def result(scope="module"):
+ result = ResultSummary("/fake/root")
+ containers = (
+ Issue(linter="foo", path="a/b/c.txt", message="oh no foo", lineno=1),
+ Issue(
+ linter="bar",
+ path="d/e/f.txt",
+ message="oh no bar",
+ hint="try baz instead",
+ level="warning",
+ lineno="4",
+ column="2",
+ rule="bar-not-allowed",
+ ),
+ Issue(
+ linter="baz",
+ path="a/b/c.txt",
+ message="oh no baz",
+ lineno=4,
+ column=10,
+ source="if baz:",
+ ),
+ Issue(
+ linter="foo-diff",
+ path="a/b/c.txt",
+ message="oh no foo-diff",
+ lineno=5,
+ source="if baz:",
+ diff="diff 1\n- hello\n+ hello2",
+ ),
+ )
+ result = ResultSummary("/fake/root")
+ for c in containers:
+ result.issues[c.path].append(c)
+ return result
+
+
+@pytest.mark.parametrize("name", EXPECTED.keys())
+def test_formatters(result, name):
+ opts = EXPECTED[name]
+ fmt = formatters.get(name, **opts["kwargs"])
+ # encoding to str bypasses a UnicodeEncodeError in pytest
+ assert fmt(result) == opts["format"]
+
+
+def test_json_formatter(result):
+ fmt = formatters.get("json")
+ formatted = json.loads(fmt(result))
+
+ assert set(formatted.keys()) == set(result.issues.keys())
+
+ attrs = attr.fields(Issue)
+ for errors in formatted.values():
+ for err in errors:
+ assert all(a.name in err for a in attrs)
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/python/mozlint/test/test_parser.py b/python/mozlint/test/test_parser.py
new file mode 100644
index 0000000000..2fbf26c8e5
--- /dev/null
+++ b/python/mozlint/test/test_parser.py
@@ -0,0 +1,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/.
+
+import os
+
+import mozunit
+import pytest
+
+from mozlint.errors import LinterNotFound, LinterParseError
+from mozlint.parser import Parser
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+@pytest.fixture(scope="module")
+def parse(lintdir):
+ parser = Parser(here)
+
+ def _parse(name):
+ path = os.path.join(lintdir, name)
+ return parser(path)
+
+ return _parse
+
+
+def test_parse_valid_linter(parse):
+ lintobj = parse("string.yml")
+ assert isinstance(lintobj, list)
+ assert len(lintobj) == 1
+
+ lintobj = lintobj[0]
+ assert isinstance(lintobj, dict)
+ assert "name" in lintobj
+ assert "description" in lintobj
+ assert "type" in lintobj
+ assert "payload" in lintobj
+ assert "extensions" in lintobj
+ assert "include" in lintobj
+ assert lintobj["include"] == ["."]
+ assert set(lintobj["extensions"]) == set(["js", "jsm"])
+
+
+def test_parser_valid_multiple(parse):
+ lintobj = parse("multiple.yml")
+ assert isinstance(lintobj, list)
+ assert len(lintobj) == 2
+
+ assert lintobj[0]["name"] == "StringLinter"
+ assert lintobj[1]["name"] == "RegexLinter"
+
+
+@pytest.mark.parametrize(
+ "linter",
+ [
+ "invalid_type.yml",
+ "invalid_extension.ym",
+ "invalid_include.yml",
+ "invalid_include_with_glob.yml",
+ "invalid_exclude.yml",
+ "invalid_support_files.yml",
+ "missing_attrs.yml",
+ "missing_definition.yml",
+ "non_existing_include.yml",
+ "non_existing_exclude.yml",
+ "non_existing_support_files.yml",
+ ],
+)
+def test_parse_invalid_linter(parse, linter):
+ with pytest.raises(LinterParseError):
+ parse(linter)
+
+
+def test_parse_non_existent_linter(parse):
+ with pytest.raises(LinterNotFound):
+ parse("missing_file.lint")
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/python/mozlint/test/test_pathutils.py b/python/mozlint/test/test_pathutils.py
new file mode 100644
index 0000000000..78f7883e88
--- /dev/null
+++ b/python/mozlint/test/test_pathutils.py
@@ -0,0 +1,166 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+from fnmatch import fnmatch
+
+import mozunit
+import pytest
+
+from mozlint import pathutils
+
+here = os.path.abspath(os.path.dirname(__file__))
+root = os.path.join(here, "filter")
+
+
+def assert_paths(a, b):
+ def normalize(p):
+ if not os.path.isabs(p):
+ p = os.path.join(root, p)
+ return os.path.normpath(p)
+
+ assert set(map(normalize, a)) == set(map(normalize, b))
+
+
+@pytest.mark.parametrize(
+ "test",
+ (
+ {
+ "paths": ["a.js", "subdir1/subdir3/d.js"],
+ "include": ["."],
+ "exclude": ["subdir1"],
+ "expected": ["a.js"],
+ },
+ {
+ "paths": ["a.js", "subdir1/subdir3/d.js"],
+ "include": ["subdir1/subdir3"],
+ "exclude": ["subdir1"],
+ "expected": ["subdir1/subdir3/d.js"],
+ },
+ {
+ "paths": ["."],
+ "include": ["."],
+ "exclude": ["**/c.py", "subdir1/subdir3"],
+ "extensions": ["py"],
+ "expected": ["."],
+ "expected_exclude": ["subdir2/c.py", "subdir1/subdir3"],
+ },
+ {
+ "paths": [
+ "a.py",
+ "a.js",
+ "subdir1/b.py",
+ "subdir2/c.py",
+ "subdir1/subdir3/d.py",
+ ],
+ "include": ["."],
+ "exclude": ["**/c.py", "subdir1/subdir3"],
+ "extensions": ["py"],
+ "expected": ["a.py", "subdir1/b.py"],
+ },
+ {
+ "paths": ["a.py", "a.js", "subdir2"],
+ "include": ["."],
+ "exclude": [],
+ "extensions": ["py"],
+ "expected": ["a.py", "subdir2"],
+ },
+ {
+ "paths": ["subdir1"],
+ "include": ["."],
+ "exclude": ["subdir1/subdir3"],
+ "extensions": ["py"],
+ "expected": ["subdir1"],
+ "expected_exclude": ["subdir1/subdir3"],
+ },
+ {
+ "paths": ["docshell"],
+ "include": ["docs"],
+ "exclude": [],
+ "expected": [],
+ },
+ {
+ "paths": ["does/not/exist"],
+ "include": ["."],
+ "exclude": [],
+ "expected": [],
+ },
+ ),
+)
+def test_filterpaths(test):
+ expected = test.pop("expected")
+ expected_exclude = test.pop("expected_exclude", [])
+
+ paths, exclude = pathutils.filterpaths(root, **test)
+ assert_paths(paths, expected)
+ assert_paths(exclude, expected_exclude)
+
+
+@pytest.mark.parametrize(
+ "test",
+ (
+ {
+ "paths": ["subdir1/b.js"],
+ "config": {
+ "exclude": ["subdir1"],
+ "extensions": "js",
+ },
+ "expected": [],
+ },
+ {
+ "paths": ["subdir1/subdir3"],
+ "config": {
+ "exclude": ["subdir1"],
+ "extensions": "js",
+ },
+ "expected": [],
+ },
+ ),
+)
+def test_expand_exclusions(test):
+ expected = test.pop("expected", [])
+
+ paths = list(pathutils.expand_exclusions(test["paths"], test["config"], root))
+ assert_paths(paths, expected)
+
+
+@pytest.mark.parametrize(
+ "paths,expected",
+ [
+ (["subdir1/*"], ["subdir1"]),
+ (["subdir2/*"], ["subdir2"]),
+ (["subdir1/*.*", "subdir1/subdir3/*", "subdir2/*"], ["subdir1", "subdir2"]),
+ ([root + "/*", "subdir1/*.*", "subdir1/subdir3/*", "subdir2/*"], [root]),
+ (["subdir1/b.py", "subdir1/subdir3"], ["subdir1/b.py", "subdir1/subdir3"]),
+ (["subdir1/b.py", "subdir1/b.js"], ["subdir1/b.py", "subdir1/b.js"]),
+ (["subdir1/subdir3"], ["subdir1/subdir3"]),
+ (
+ [
+ "foo",
+ "foobar",
+ ],
+ ["foo", "foobar"],
+ ),
+ ],
+)
+def test_collapse(paths, expected):
+ os.chdir(root)
+
+ inputs = []
+ for path in paths:
+ base, name = os.path.split(path)
+ if "*" in name:
+ for n in os.listdir(base):
+ if not fnmatch(n, name):
+ continue
+ inputs.append(os.path.join(base, n))
+ else:
+ inputs.append(path)
+
+ print("inputs: {}".format(inputs))
+ assert_paths(pathutils.collapse(inputs), expected)
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/python/mozlint/test/test_result.py b/python/mozlint/test/test_result.py
new file mode 100644
index 0000000000..02e8156b3c
--- /dev/null
+++ b/python/mozlint/test/test_result.py
@@ -0,0 +1,26 @@
+# 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
+
+from mozlint.result import Issue, ResultSummary
+
+
+def test_issue_defaults():
+ ResultSummary.root = "/fake/root"
+
+ issue = Issue(linter="linter", path="path", message="message", lineno=None)
+ assert issue.lineno == 0
+ assert issue.column is None
+ assert issue.level == "error"
+
+ issue = Issue(
+ linter="linter", path="path", message="message", lineno="1", column="2"
+ )
+ assert issue.lineno == 1
+ assert issue.column == 2
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/python/mozlint/test/test_roller.py b/python/mozlint/test/test_roller.py
new file mode 100644
index 0000000000..2918047cd2
--- /dev/null
+++ b/python/mozlint/test/test_roller.py
@@ -0,0 +1,396 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import platform
+import signal
+import subprocess
+import sys
+import time
+from itertools import chain
+
+import mozunit
+import pytest
+
+from mozlint.errors import LintersNotConfigured, NoValidLinter
+from mozlint.result import Issue, ResultSummary
+from mozlint.roller import LintRoller
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+def test_roll_no_linters_configured(lint, files):
+ with pytest.raises(LintersNotConfigured):
+ lint.roll(files)
+
+
+def test_roll_successful(lint, linters, files):
+ lint.read(linters("string", "regex", "external"))
+
+ result = lint.roll(files)
+ assert len(result.issues) == 1
+ assert result.failed == set([])
+
+ path = list(result.issues.keys())[0]
+ assert os.path.basename(path) == "foobar.js"
+
+ errors = result.issues[path]
+ assert isinstance(errors, list)
+ assert len(errors) == 6
+
+ container = errors[0]
+ assert isinstance(container, Issue)
+ assert container.rule == "no-foobar"
+
+
+def test_roll_from_subdir(lint, linters):
+ lint.read(linters("string", "regex", "external"))
+
+ oldcwd = os.getcwd()
+ try:
+ os.chdir(os.path.join(lint.root, "files"))
+
+ # Path relative to cwd works
+ result = lint.roll("foobar.js")
+ assert len(result.issues) == 1
+ assert len(result.failed) == 0
+ assert result.returncode == 1
+
+ # Path relative to root doesn't work
+ result = lint.roll(os.path.join("files", "foobar.js"))
+ assert len(result.issues) == 0
+ assert len(result.failed) == 0
+ assert result.returncode == 0
+
+ # Paths from vcs are always joined to root instead of cwd
+ lint.mock_vcs([os.path.join("files", "foobar.js")])
+ result = lint.roll(outgoing=True)
+ assert len(result.issues) == 1
+ assert len(result.failed) == 0
+ assert result.returncode == 1
+
+ result = lint.roll(workdir=True)
+ assert len(result.issues) == 1
+ assert len(result.failed) == 0
+ assert result.returncode == 1
+
+ result = lint.roll(rev='not public() and keyword("dummy revset expression")')
+ assert len(result.issues) == 1
+ assert len(result.failed) == 0
+ assert result.returncode == 1
+ finally:
+ os.chdir(oldcwd)
+
+
+def test_roll_catch_exception(lint, linters, files, capfd):
+ lint.read(linters("raises"))
+
+ lint.roll(files) # assert not raises
+ out, err = capfd.readouterr()
+ assert "LintException" in err
+
+
+def test_roll_with_global_excluded_path(lint, linters, files):
+ lint.exclude = ["**/foobar.js"]
+ lint.read(linters("string", "regex", "external"))
+ result = lint.roll(files)
+
+ assert len(result.issues) == 0
+ assert result.failed == set([])
+
+
+def test_roll_with_local_excluded_path(lint, linters, files):
+ lint.read(linters("excludes"))
+ result = lint.roll(files)
+
+ assert "**/foobar.js" in lint.linters[0]["local_exclude"]
+ assert len(result.issues) == 0
+ assert result.failed == set([])
+
+
+def test_roll_with_no_files_to_lint(lint, linters, capfd):
+ lint.read(linters("string", "regex", "external"))
+ lint.mock_vcs([])
+ result = lint.roll([], workdir=True)
+ assert isinstance(result, ResultSummary)
+ assert len(result.issues) == 0
+ assert len(result.failed) == 0
+
+ out, err = capfd.readouterr()
+ assert "warning: no files linted" in out
+
+
+def test_roll_with_invalid_extension(lint, linters, filedir):
+ lint.read(linters("external"))
+ result = lint.roll(os.path.join(filedir, "foobar.py"))
+ assert len(result.issues) == 0
+ assert result.failed == set([])
+
+
+def test_roll_with_failure_code(lint, linters, files):
+ lint.read(linters("badreturncode"))
+
+ result = lint.roll(files, num_procs=1)
+ assert len(result.issues) == 0
+ assert result.failed == set(["BadReturnCodeLinter"])
+
+
+def test_roll_warnings(lint, linters, files):
+ lint.read(linters("warning"))
+ result = lint.roll(files)
+ assert len(result.issues) == 0
+ assert result.total_issues == 0
+ assert len(result.suppressed_warnings) == 1
+ assert result.total_suppressed_warnings == 2
+
+ lint.lintargs["show_warnings"] = True
+ result = lint.roll(files)
+ assert len(result.issues) == 1
+ assert result.total_issues == 2
+ assert len(result.suppressed_warnings) == 0
+ assert result.total_suppressed_warnings == 0
+
+
+def test_roll_code_review(monkeypatch, linters, files):
+ monkeypatch.setenv("CODE_REVIEW", "1")
+ lint = LintRoller(root=here, show_warnings=False)
+ lint.read(linters("warning"))
+ result = lint.roll(files)
+ assert len(result.issues) == 1
+ assert result.total_issues == 2
+ assert len(result.suppressed_warnings) == 0
+ assert result.total_suppressed_warnings == 0
+ assert result.returncode == 1
+
+
+def test_roll_code_review_warnings_disabled(monkeypatch, linters, files):
+ monkeypatch.setenv("CODE_REVIEW", "1")
+ lint = LintRoller(root=here, show_warnings=False)
+ lint.read(linters("warning_no_code_review"))
+ result = lint.roll(files)
+ assert len(result.issues) == 0
+ assert result.total_issues == 0
+ assert lint.result.fail_on_warnings is True
+ assert len(result.suppressed_warnings) == 1
+ assert result.total_suppressed_warnings == 2
+ assert result.returncode == 0
+
+
+def test_roll_code_review_warnings_soft(linters, files):
+ lint = LintRoller(root=here, show_warnings="soft")
+ lint.read(linters("warning_no_code_review"))
+ result = lint.roll(files)
+ assert len(result.issues) == 1
+ assert result.total_issues == 2
+ assert lint.result.fail_on_warnings is False
+ assert len(result.suppressed_warnings) == 0
+ assert result.total_suppressed_warnings == 0
+ assert result.returncode == 0
+
+
+def fake_run_worker(config, paths, **lintargs):
+ result = ResultSummary(lintargs["root"])
+ result.issues["count"].append(1)
+ return result
+
+
+@pytest.mark.skipif(
+ platform.system() == "Windows",
+ reason="monkeypatch issues with multiprocessing on Windows",
+)
+@pytest.mark.parametrize("num_procs", [1, 4, 8, 16])
+def test_number_of_jobs(monkeypatch, lint, linters, files, num_procs):
+ monkeypatch.setattr(sys.modules[lint.__module__], "_run_worker", fake_run_worker)
+
+ linters = linters("string", "regex", "external")
+ lint.read(linters)
+ num_jobs = len(lint.roll(files, num_procs=num_procs).issues["count"])
+
+ if len(files) >= num_procs:
+ assert num_jobs == num_procs * len(linters)
+ else:
+ assert num_jobs == len(files) * len(linters)
+
+
+@pytest.mark.skipif(
+ platform.system() == "Windows",
+ reason="monkeypatch issues with multiprocessing on Windows",
+)
+@pytest.mark.parametrize("max_paths,expected_jobs", [(1, 12), (4, 6), (16, 6)])
+def test_max_paths_per_job(monkeypatch, lint, linters, files, max_paths, expected_jobs):
+ monkeypatch.setattr(sys.modules[lint.__module__], "_run_worker", fake_run_worker)
+
+ files = files[:4]
+ assert len(files) == 4
+
+ linters = linters("string", "regex", "external")[:3]
+ assert len(linters) == 3
+
+ lint.MAX_PATHS_PER_JOB = max_paths
+ lint.read(linters)
+ num_jobs = len(lint.roll(files, num_procs=2).issues["count"])
+ assert num_jobs == expected_jobs
+
+
+@pytest.mark.skipif(
+ platform.system() == "Windows",
+ reason="monkeypatch issues with multiprocessing on Windows",
+)
+@pytest.mark.parametrize("num_procs", [1, 4, 8, 16])
+def test_number_of_jobs_global(monkeypatch, lint, linters, files, num_procs):
+ monkeypatch.setattr(sys.modules[lint.__module__], "_run_worker", fake_run_worker)
+
+ linters = linters("global")
+ lint.read(linters)
+ num_jobs = len(lint.roll(files, num_procs=num_procs).issues["count"])
+
+ assert num_jobs == 1
+
+
+@pytest.mark.skipif(
+ platform.system() == "Windows",
+ reason="monkeypatch issues with multiprocessing on Windows",
+)
+@pytest.mark.parametrize("max_paths", [1, 4, 16])
+def test_max_paths_per_job_global(monkeypatch, lint, linters, files, max_paths):
+ monkeypatch.setattr(sys.modules[lint.__module__], "_run_worker", fake_run_worker)
+
+ files = files[:4]
+ assert len(files) == 4
+
+ linters = linters("global")[:1]
+ assert len(linters) == 1
+
+ lint.MAX_PATHS_PER_JOB = max_paths
+ lint.read(linters)
+ num_jobs = len(lint.roll(files, num_procs=2).issues["count"])
+ assert num_jobs == 1
+
+
+@pytest.mark.skipif(
+ platform.system() == "Windows",
+ reason="signal.CTRL_C_EVENT isn't causing a KeyboardInterrupt on Windows",
+)
+def test_keyboard_interrupt():
+ # We use two linters so we'll have two jobs. One (string.yml) will complete
+ # quickly. The other (slow.yml) will run slowly. This way the first worker
+ # will be be stuck blocking on the ProcessPoolExecutor._call_queue when the
+ # signal arrives and the other still be doing work.
+ cmd = [sys.executable, "runcli.py", "-l=string", "-l=slow", "files/foobar.js"]
+ env = os.environ.copy()
+ env["PYTHONPATH"] = os.pathsep.join(sys.path)
+ proc = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ cwd=here,
+ env=env,
+ universal_newlines=True,
+ )
+ time.sleep(1)
+ proc.send_signal(signal.SIGINT)
+
+ out = proc.communicate()[0]
+ print(out)
+ assert "warning: not all files were linted" in out
+ assert "2 problems" in out
+ assert "Traceback" not in out
+
+
+def test_support_files(lint, linters, filedir, monkeypatch, files):
+ jobs = []
+
+ # Replace the original _generate_jobs with a new one that simply
+ # adds jobs to a list (and then doesn't return anything).
+ orig_generate_jobs = lint._generate_jobs
+
+ def fake_generate_jobs(*args, **kwargs):
+ jobs.extend([job[1] for job in orig_generate_jobs(*args, **kwargs)])
+ return []
+
+ monkeypatch.setattr(lint, "_generate_jobs", fake_generate_jobs)
+
+ linter_path = linters("support_files")[0]
+ lint.read(linter_path)
+ lint.root = filedir
+
+ # Modified support files only lint entire root if --outgoing or --workdir
+ # are used.
+ path = os.path.join(filedir, "foobar.js")
+ vcs_path = os.path.join(filedir, "foobar.py")
+
+ lint.mock_vcs([vcs_path])
+ lint.roll(path)
+ actual_files = sorted(chain(*jobs))
+ assert actual_files == [path]
+
+ expected_files = sorted(files)
+
+ jobs = []
+ lint.roll(path, workdir=True)
+ actual_files = sorted(chain(*jobs))
+ assert actual_files == expected_files
+
+ jobs = []
+ lint.roll(path, outgoing=True)
+ actual_files = sorted(chain(*jobs))
+ assert actual_files == expected_files
+
+ jobs = []
+ lint.roll(path, rev='draft() and keyword("dummy revset expression")')
+ actual_files = sorted(chain(*jobs))
+ assert actual_files == expected_files
+
+ # Lint config file is implicitly added as a support file
+ lint.mock_vcs([linter_path])
+ jobs = []
+ lint.roll(path, outgoing=True, workdir=True)
+ actual_files = sorted(chain(*jobs))
+ assert actual_files == expected_files
+
+ # Avoid linting the entire root when `--fix` is passed.
+ lint.mock_vcs([vcs_path])
+ lint.lintargs["fix"] = True
+
+ jobs = []
+ lint.roll(path, outgoing=True)
+ actual_files = sorted(chain(*jobs))
+ assert actual_files == sorted([path, vcs_path]), (
+ "`--fix` with `--outgoing` on a `support-files` change should "
+ "avoid linting the entire root."
+ )
+
+ jobs = []
+ lint.roll(path, workdir=True)
+ actual_files = sorted(chain(*jobs))
+ assert actual_files == sorted([path, vcs_path]), (
+ "`--fix` with `--workdir` on a `support-files` change should "
+ "avoid linting the entire root."
+ )
+
+ jobs = []
+ lint.roll(path, rev='draft() and keyword("dummy revset expression")')
+ actual_files = sorted(chain(*jobs))
+ assert actual_files == sorted([path, vcs_path]), (
+ "`--fix` with `--rev` on a `support-files` change should "
+ "avoid linting the entire root."
+ )
+
+
+def test_setup(lint, linters, filedir, capfd):
+ with pytest.raises(NoValidLinter):
+ lint.setup()
+
+ lint.read(linters("setup", "setupfailed", "setupraised"))
+ lint.setup()
+ out, err = capfd.readouterr()
+ assert "setup passed" in out
+ assert "setup failed" in out
+ assert "setup raised" in out
+ assert "error: problem with lint setup, skipping" in out
+ assert lint.result.failed_setup == set(["SetupFailedLinter", "SetupRaisedLinter"])
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/python/mozlint/test/test_types.py b/python/mozlint/test/test_types.py
new file mode 100644
index 0000000000..6ed78747b7
--- /dev/null
+++ b/python/mozlint/test/test_types.py
@@ -0,0 +1,84 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+
+import mozpack.path as mozpath
+import mozunit
+import pytest
+
+from mozlint.result import Issue, ResultSummary
+
+
+@pytest.fixture
+def path(filedir):
+ def _path(name):
+ return mozpath.join(filedir, name)
+
+ return _path
+
+
+@pytest.fixture(
+ params=[
+ "external.yml",
+ "global.yml",
+ "regex.yml",
+ "string.yml",
+ "structured.yml",
+ ]
+)
+def linter(lintdir, request):
+ return os.path.join(lintdir, request.param)
+
+
+def test_linter_types(lint, linter, files, path):
+ lint.read(linter)
+ result = lint.roll(files)
+ assert isinstance(result, ResultSummary)
+ assert isinstance(result.issues, dict)
+ assert path("foobar.js") in result.issues
+ assert path("no_foobar.js") not in result.issues
+
+ issue = result.issues[path("foobar.js")][0]
+ assert isinstance(issue, Issue)
+
+ name = os.path.basename(linter).split(".")[0]
+ assert issue.linter.lower().startswith(name)
+
+
+def test_linter_missing_files(lint, linter, filedir):
+ # Missing files should be caught by `mozlint.cli`, so the only way they
+ # could theoretically happen is if they show up from versioncontrol. So
+ # let's just make sure they get ignored.
+ lint.read(linter)
+ files = [
+ os.path.join(filedir, "missing.js"),
+ os.path.join(filedir, "missing.py"),
+ ]
+ result = lint.roll(files)
+ assert result.returncode == 0
+
+ lint.mock_vcs(files)
+ result = lint.roll(outgoing=True)
+ assert result.returncode == 0
+
+
+def test_no_filter(lint, lintdir, files):
+ lint.read(os.path.join(lintdir, "explicit_path.yml"))
+ result = lint.roll(files)
+ assert len(result.issues) == 0
+
+ lint.lintargs["use_filters"] = False
+ result = lint.roll(files)
+ assert len(result.issues) == 3
+
+
+def test_global_skipped(lint, lintdir, files):
+ lint.read(os.path.join(lintdir, "global_skipped.yml"))
+ result = lint.roll(files)
+ assert len(result.issues) == 0
+
+
+if __name__ == "__main__":
+ mozunit.main()