Files
tubestation/python/mozlint/test/test_cli.py
Beth Rennie 78f943689e Bug 1948069 - Fix pipelint on Windows r=ahal
The pipelint tool was not working correctly on Windows for three reasons:

1. The module was being imported into every child process spawned by the
   lint roller's multiprocessing executor. Because the import of
   pipelint has side-effects (the actual running of mach lint on the
   input), each child process would end up recursively triggering these
   side-effects and causing exponential process growth until they all
   eventually crashed.

   To address this we now have the logic of the pipelint script behind a
   `if __name__ == "__main__" guard.

2. We were keeping the file handle open before we called into mach,
   which resulted in a permissions error trying to read from the file
   handle after mach had finished.

   To address this we now immediately close the file before invoking
   `mach lint` and re-open it for reading after.

3. We were using default newline handling inside `pipelint` and
   `mozlint`, which resulted in LFs being translated to CRLFs
   automatically in all the intermediate temporary files.

   To address this, we open files for reading and writing in binary mode
   so that we deal directly with the underlying bytes instead of
   particular interpretations of bytes (e.g., newlines, CP252 vs UTF-8),
   including `stdin` (when reading file input from `jj`) and `stdout`
   (when outputting the final formatted files).

Differential Revision: https://phabricator.services.mozilla.com/D244513
2025-04-23 18:04:21 +00:00

166 lines
4.9 KiB
Python

# 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 io
import os
import subprocess
import sys
from shutil import which
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, files):
def inner(args=None):
args = args or []
if not any("--stdin-filename" in a for a in args):
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 which("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
def test_cli_run_with_stdin_filename(run, filedir, capfd, monkeypatch, tmp_path):
for arg in ("bar.txt", "--workdir", "--outgoing", "--rev=abc"):
with pytest.raises(SystemExit):
run(["--stdin-filename=foo.txt", arg])
capfd.readouterr()
monkeypatch.setattr("sys.stdin", io.TextIOWrapper(io.BytesIO(b"foobar\n")))
run(["-l", "string", f"--stdin-filename={filedir}/foobar.py"])
out, err = capfd.readouterr()
assert out == "✖ 0 problems (0 errors, 0 warnings, 0 fixed)\n"
monkeypatch.setattr("sys.stdin", io.TextIOWrapper(io.BytesIO(b"foobar\n")))
run(["-l", "string", f"--stdin-filename={filedir}/foobar.py", "--dump-stdin-file"])
out, err = capfd.readouterr()
assert out == "foobar\n"
monkeypatch.setattr("sys.stdin", io.TextIOWrapper(io.BytesIO(b"foobar\n")))
run(["-l", "string", f"--stdin-filename={filedir}/foobar.py", "--fix"])
out, err = capfd.readouterr()
assert out == "foobar\n"
monkeypatch.setattr("sys.stdin", io.TextIOWrapper(io.BytesIO(b"foobar\n")))
tmpfile = tmp_path / "temp"
run(
[
"-l",
"string",
f"--stdin-filename={filedir}/foobar.py",
"--dump-stdin-file",
str(tmpfile),
]
)
out, err = capfd.readouterr()
assert out == ""
assert tmpfile.read_text() == "foobar\n"
if __name__ == "__main__":
mozunit.main()