diff options
Diffstat (limited to '')
58 files changed, 1569 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.toml b/python/mozlint/test/python.toml new file mode 100644 index 0000000000..3876a4aaa4 --- /dev/null +++ b/python/mozlint/test/python.toml @@ -0,0 +1,18 @@ +[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..4e9219a2ea --- /dev/null +++ b/python/mozlint/test/test_cli.py @@ -0,0 +1,126 @@ +# 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 identifies 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() |