X-Git-Url: https://git.madduck.net/etc/vim.git/blobdiff_plain/8a82e0bf9af1182563abb8bfb0c4e87e836feb95..048efa7476bec1b2e7df156f2790f3565ab5c6d8:/tests/test_black.py diff --git a/tests/test_black.py b/tests/test_black.py index 08a3f31..8a37197 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -3,21 +3,22 @@ import asyncio from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from functools import partial -from io import StringIO +from io import BytesIO, TextIOWrapper import os from pathlib import Path +import re import sys from tempfile import TemporaryDirectory -from typing import Any, List, Tuple, Iterator +from typing import Any, BinaryIO, Generator, List, Tuple, Iterator import unittest -from unittest.mock import patch -import re +from unittest.mock import patch, MagicMock from click import unstyle from click.testing import CliRunner import black + ll = 88 ff = partial(black.format_file_in_place, line_length=ll, fast=True) fs = partial(black.format_str, line_length=ll) @@ -30,13 +31,14 @@ def dump_to_stderr(*output: str) -> str: return "\n" + "\n".join(output) + "\n" -def read_data(name: str) -> Tuple[str, str]: +def read_data(name: str, data: bool = True) -> Tuple[str, str]: """read_data('test_name') -> 'input', 'output'""" if not name.endswith((".py", ".pyi", ".out", ".diff")): name += ".py" _input: List[str] = [] _output: List[str] = [] - with open(THIS_DIR / name, "r", encoding="utf8") as test: + base_dir = THIS_DIR / "data" if data else THIS_DIR + with open(base_dir / name, "r", encoding="utf8") as test: lines = test.readlines() result = _input for line in lines: @@ -77,6 +79,26 @@ def event_loop(close: bool) -> Iterator[None]: loop.close() +class BlackRunner(CliRunner): + """Modify CliRunner so that stderr is not merged with stdout. + + This is a hack that can be removed once we depend on Click 7.x""" + + def __init__(self, stderrbuf: BinaryIO) -> None: + self.stderrbuf = stderrbuf + super().__init__() + + @contextmanager + def isolation(self, *args: Any, **kwargs: Any) -> Generator[BinaryIO, None, None]: + with super().isolation(*args, **kwargs) as output: + try: + hold_stderr = sys.stderr + sys.stderr = TextIOWrapper(self.stderrbuf, encoding=self.charset) + yield output + finally: + sys.stderr = hold_stderr + + class BlackTestCase(unittest.TestCase): maxDiff = None @@ -99,9 +121,28 @@ class BlackTestCase(unittest.TestCase): black.err(str(ve)) self.assertEqual(expected, actual) + @patch("black.dump_to_file", dump_to_stderr) + def test_empty(self) -> None: + source = expected = "" + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, line_length=ll) + + def test_empty_ff(self) -> None: + expected = "" + tmp_file = Path(black.dump_to_file()) + try: + self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES)) + with open(tmp_file, encoding="utf8") as f: + actual = f.read() + finally: + os.unlink(tmp_file) + self.assertFormatEqual(expected, actual) + @patch("black.dump_to_file", dump_to_stderr) def test_self(self) -> None: - source, expected = read_data("test_black") + source, expected = read_data("test_black", data=False) actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) @@ -110,7 +151,7 @@ class BlackTestCase(unittest.TestCase): @patch("black.dump_to_file", dump_to_stderr) def test_black(self) -> None: - source, expected = read_data("../black") + source, expected = read_data("../black", data=False) actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) @@ -118,42 +159,35 @@ class BlackTestCase(unittest.TestCase): self.assertFalse(ff(THIS_DIR / ".." / "black.py")) def test_piping(self) -> None: - source, expected = read_data("../black") - hold_stdin, hold_stdout = sys.stdin, sys.stdout - try: - sys.stdin, sys.stdout = StringIO(source), StringIO() - sys.stdin.name = "" - black.format_stdin_to_stdout( - line_length=ll, fast=True, write_back=black.WriteBack.YES - ) - sys.stdout.seek(0) - actual = sys.stdout.read() - finally: - sys.stdin, sys.stdout = hold_stdin, hold_stdout - self.assertFormatEqual(expected, actual) - black.assert_equivalent(source, actual) - black.assert_stable(source, actual, line_length=ll) + source, expected = read_data("../black", data=False) + stderrbuf = BytesIO() + result = BlackRunner(stderrbuf).invoke( + black.main, ["-", "--fast", f"--line-length={ll}"], input=source + ) + self.assertEqual(result.exit_code, 0) + self.assertFormatEqual(expected, result.output) + black.assert_equivalent(source, result.output) + black.assert_stable(source, result.output, line_length=ll) def test_piping_diff(self) -> None: + diff_header = re.compile( + rf"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d " + rf"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + ) source, _ = read_data("expression.py") expected, _ = read_data("expression.diff") - hold_stdin, hold_stdout = sys.stdin, sys.stdout - try: - sys.stdin, sys.stdout = StringIO(source), StringIO() - sys.stdin.name = "" - black.format_stdin_to_stdout( - line_length=ll, fast=True, write_back=black.WriteBack.DIFF - ) - sys.stdout.seek(0) - actual = sys.stdout.read() - finally: - sys.stdin, sys.stdout = hold_stdin, hold_stdout + config = THIS_DIR / "data" / "empty_pyproject.toml" + stderrbuf = BytesIO() + args = ["-", "--fast", f"--line-length={ll}", "--diff", f"--config={config}"] + result = BlackRunner(stderrbuf).invoke(black.main, args, input=source) + self.assertEqual(result.exit_code, 0) + actual = diff_header.sub("[Deterministic header]", result.output) actual = actual.rstrip() + "\n" # the diff output has a trailing space self.assertEqual(expected, actual) @patch("black.dump_to_file", dump_to_stderr) def test_setup(self) -> None: - source, expected = read_data("../setup") + source, expected = read_data("../setup", data=False) actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) @@ -202,16 +236,20 @@ class BlackTestCase(unittest.TestCase): source, _ = read_data("expression.py") expected, _ = read_data("expression.diff") tmp_file = Path(black.dump_to_file(source)) - hold_stdout = sys.stdout + diff_header = re.compile( + rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d " + rf"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + ) + stderrbuf = BytesIO() try: - sys.stdout = StringIO() - self.assertTrue(ff(tmp_file, write_back=black.WriteBack.DIFF)) - sys.stdout.seek(0) - actual = sys.stdout.read() - actual = actual.replace(str(tmp_file), "") + result = BlackRunner(stderrbuf).invoke( + black.main, ["--diff", str(tmp_file)] + ) + self.assertEqual(result.exit_code, 0) finally: - sys.stdout = hold_stdout os.unlink(tmp_file) + actual = result.output + actual = diff_header.sub("[Deterministic header]", actual) actual = actual.rstrip() + "\n" # the diff output has a trailing space if expected != actual: dump = black.dump_to_file(actual) @@ -362,6 +400,14 @@ class BlackTestCase(unittest.TestCase): black.assert_equivalent(source, actual) black.assert_stable(source, actual, line_length=ll) + @patch("black.dump_to_file", dump_to_stderr) + def test_fmtonoff2(self) -> None: + source, expected = read_data("fmtonoff2") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, line_length=ll) + @patch("black.dump_to_file", dump_to_stderr) def test_remove_empty_parentheses_after_class(self) -> None: source, expected = read_data("class_blank_parentheses") @@ -378,8 +424,8 @@ class BlackTestCase(unittest.TestCase): black.assert_equivalent(source, actual) black.assert_stable(source, actual, line_length=ll) - def test_report(self) -> None: - report = black.Report() + def test_report_verbose(self) -> None: + report = black.Report(verbose=True) out_lines = [] err_lines = [] @@ -446,9 +492,19 @@ class BlackTestCase(unittest.TestCase): "2 files failed to reformat.", ) self.assertEqual(report.return_code, 123) - report.done(Path("f4"), black.Changed.NO) + report.path_ignored(Path("wat"), "no match") self.assertEqual(len(out_lines), 5) self.assertEqual(len(err_lines), 2) + self.assertEqual(out_lines[-1], "wat ignored: no match") + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, " + "2 files failed to reformat.", + ) + self.assertEqual(report.return_code, 123) + report.done(Path("f4"), black.Changed.NO) + self.assertEqual(len(out_lines), 6) + self.assertEqual(len(err_lines), 2) self.assertEqual(out_lines[-1], "f4 already well formatted, good job.") self.assertEqual( unstyle(str(report)), @@ -463,6 +519,183 @@ class BlackTestCase(unittest.TestCase): "2 files would fail to reformat.", ) + def test_report_quiet(self) -> None: + report = black.Report(quiet=True) + out_lines = [] + err_lines = [] + + def out(msg: str, **kwargs: Any) -> None: + out_lines.append(msg) + + def err(msg: str, **kwargs: Any) -> None: + err_lines.append(msg) + + with patch("black.out", out), patch("black.err", err): + report.done(Path("f1"), black.Changed.NO) + self.assertEqual(len(out_lines), 0) + self.assertEqual(len(err_lines), 0) + self.assertEqual(unstyle(str(report)), "1 file left unchanged.") + self.assertEqual(report.return_code, 0) + report.done(Path("f2"), black.Changed.YES) + self.assertEqual(len(out_lines), 0) + self.assertEqual(len(err_lines), 0) + self.assertEqual( + unstyle(str(report)), "1 file reformatted, 1 file left unchanged." + ) + report.done(Path("f3"), black.Changed.CACHED) + self.assertEqual(len(out_lines), 0) + self.assertEqual(len(err_lines), 0) + self.assertEqual( + unstyle(str(report)), "1 file reformatted, 2 files left unchanged." + ) + self.assertEqual(report.return_code, 0) + report.check = True + self.assertEqual(report.return_code, 1) + report.check = False + report.failed(Path("e1"), "boom") + self.assertEqual(len(out_lines), 0) + self.assertEqual(len(err_lines), 1) + self.assertEqual(err_lines[-1], "error: cannot format e1: boom") + self.assertEqual( + unstyle(str(report)), + "1 file reformatted, 2 files left unchanged, " + "1 file failed to reformat.", + ) + self.assertEqual(report.return_code, 123) + report.done(Path("f3"), black.Changed.YES) + self.assertEqual(len(out_lines), 0) + self.assertEqual(len(err_lines), 1) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, " + "1 file failed to reformat.", + ) + self.assertEqual(report.return_code, 123) + report.failed(Path("e2"), "boom") + self.assertEqual(len(out_lines), 0) + self.assertEqual(len(err_lines), 2) + self.assertEqual(err_lines[-1], "error: cannot format e2: boom") + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, " + "2 files failed to reformat.", + ) + self.assertEqual(report.return_code, 123) + report.path_ignored(Path("wat"), "no match") + self.assertEqual(len(out_lines), 0) + self.assertEqual(len(err_lines), 2) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, " + "2 files failed to reformat.", + ) + self.assertEqual(report.return_code, 123) + report.done(Path("f4"), black.Changed.NO) + self.assertEqual(len(out_lines), 0) + self.assertEqual(len(err_lines), 2) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 3 files left unchanged, " + "2 files failed to reformat.", + ) + self.assertEqual(report.return_code, 123) + report.check = True + self.assertEqual( + unstyle(str(report)), + "2 files would be reformatted, 3 files would be left unchanged, " + "2 files would fail to reformat.", + ) + + def test_report_normal(self) -> None: + report = black.Report() + out_lines = [] + err_lines = [] + + def out(msg: str, **kwargs: Any) -> None: + out_lines.append(msg) + + def err(msg: str, **kwargs: Any) -> None: + err_lines.append(msg) + + with patch("black.out", out), patch("black.err", err): + report.done(Path("f1"), black.Changed.NO) + self.assertEqual(len(out_lines), 0) + self.assertEqual(len(err_lines), 0) + self.assertEqual(unstyle(str(report)), "1 file left unchanged.") + self.assertEqual(report.return_code, 0) + report.done(Path("f2"), black.Changed.YES) + self.assertEqual(len(out_lines), 1) + self.assertEqual(len(err_lines), 0) + self.assertEqual(out_lines[-1], "reformatted f2") + self.assertEqual( + unstyle(str(report)), "1 file reformatted, 1 file left unchanged." + ) + report.done(Path("f3"), black.Changed.CACHED) + self.assertEqual(len(out_lines), 1) + self.assertEqual(len(err_lines), 0) + self.assertEqual(out_lines[-1], "reformatted f2") + self.assertEqual( + unstyle(str(report)), "1 file reformatted, 2 files left unchanged." + ) + self.assertEqual(report.return_code, 0) + report.check = True + self.assertEqual(report.return_code, 1) + report.check = False + report.failed(Path("e1"), "boom") + self.assertEqual(len(out_lines), 1) + self.assertEqual(len(err_lines), 1) + self.assertEqual(err_lines[-1], "error: cannot format e1: boom") + self.assertEqual( + unstyle(str(report)), + "1 file reformatted, 2 files left unchanged, " + "1 file failed to reformat.", + ) + self.assertEqual(report.return_code, 123) + report.done(Path("f3"), black.Changed.YES) + self.assertEqual(len(out_lines), 2) + self.assertEqual(len(err_lines), 1) + self.assertEqual(out_lines[-1], "reformatted f3") + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, " + "1 file failed to reformat.", + ) + self.assertEqual(report.return_code, 123) + report.failed(Path("e2"), "boom") + self.assertEqual(len(out_lines), 2) + self.assertEqual(len(err_lines), 2) + self.assertEqual(err_lines[-1], "error: cannot format e2: boom") + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, " + "2 files failed to reformat.", + ) + self.assertEqual(report.return_code, 123) + report.path_ignored(Path("wat"), "no match") + self.assertEqual(len(out_lines), 2) + self.assertEqual(len(err_lines), 2) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, " + "2 files failed to reformat.", + ) + self.assertEqual(report.return_code, 123) + report.done(Path("f4"), black.Changed.NO) + self.assertEqual(len(out_lines), 2) + self.assertEqual(len(err_lines), 2) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 3 files left unchanged, " + "2 files failed to reformat.", + ) + self.assertEqual(report.return_code, 123) + report.check = True + self.assertEqual( + unstyle(str(report)), + "2 files would be reformatted, 3 files would be left unchanged, " + "2 files would fail to reformat.", + ) + def test_is_python36(self) -> None: node = black.lib2to3_parse("def f(*, arg): ...\n") self.assertFalse(black.is_python36(node)) @@ -667,10 +900,10 @@ class BlackTestCase(unittest.TestCase): cached_but_changed.touch() cache = {cached: black.get_cache_info(cached), cached_but_changed: (0.0, 0)} todo, done = black.filter_cached( - cache, [uncached, cached, cached_but_changed] + cache, {uncached, cached, cached_but_changed} ) - self.assertEqual(todo, [uncached, cached_but_changed]) - self.assertEqual(done, [cached]) + self.assertEqual(todo, {uncached, cached_but_changed}) + self.assertEqual(done, {cached}) def test_write_cache_creates_directory_if_needed(self) -> None: mode = black.FileMode.AUTO_DETECT @@ -707,15 +940,13 @@ class BlackTestCase(unittest.TestCase): def test_check_diff_use_together(self) -> None: with cache_dir(): # Files which will be reformatted. - src1 = (THIS_DIR / "string_quotes.py").resolve() + src1 = (THIS_DIR / "data" / "string_quotes.py").resolve() result = CliRunner().invoke(black.main, [str(src1), "--diff", "--check"]) - self.assertEqual(result.exit_code, 1) - + self.assertEqual(result.exit_code, 1, result.output) # Files which will not be reformatted. - src2 = (THIS_DIR / "composition.py").resolve() + src2 = (THIS_DIR / "data" / "composition.py").resolve() result = CliRunner().invoke(black.main, [str(src2), "--diff", "--check"]) - self.assertEqual(result.exit_code, 0) - + self.assertEqual(result.exit_code, 0, result.output) # Multi file command. result = CliRunner().invoke( black.main, [str(src1), str(src2), "--diff", "--check"] @@ -731,7 +962,10 @@ class BlackTestCase(unittest.TestCase): def test_broken_symlink(self) -> None: with cache_dir() as workspace: symlink = workspace / "broken_link.py" - symlink.symlink_to("nonexistent.py") + try: + symlink.symlink_to("nonexistent.py") + except OSError as e: + self.skipTest(f"Can't create symlinks: {e}") result = CliRunner().invoke(black.main, [str(workspace.resolve())]) self.assertEqual(result.exit_code, 0) @@ -853,19 +1087,24 @@ class BlackTestCase(unittest.TestCase): self.assertFormatEqual(actual, expected) def test_include_exclude(self) -> None: - path = THIS_DIR / "include_exclude_tests" + path = THIS_DIR / "data" / "include_exclude_tests" include = re.compile(r"\.pyi?$") exclude = re.compile(r"/exclude/|/\.definitely_exclude/") + report = black.Report() sources: List[Path] = [] expected = [ - Path(THIS_DIR / "include_exclude_tests/b/dont_exclude/a.py"), - Path(THIS_DIR / "include_exclude_tests/b/dont_exclude/a.pyi"), + Path(path / "b/dont_exclude/a.py"), + Path(path / "b/dont_exclude/a.pyi"), ] - sources.extend(black.gen_python_files_in_dir(path, include, exclude)) + this_abs = THIS_DIR.resolve() + sources.extend( + black.gen_python_files_in_dir(path, this_abs, include, exclude, report) + ) self.assertEqual(sorted(expected), sorted(sources)) def test_empty_include(self) -> None: - path = THIS_DIR / "include_exclude_tests" + path = THIS_DIR / "data" / "include_exclude_tests" + report = black.Report() empty = re.compile(r"") sources: List[Path] = [] expected = [ @@ -879,15 +1118,17 @@ class BlackTestCase(unittest.TestCase): Path(path / "b/.definitely_exclude/a.py"), Path(path / "b/.definitely_exclude/a.pyi"), ] + this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files_in_dir( - path, empty, re.compile(black.DEFAULT_EXCLUDES) + path, this_abs, empty, re.compile(black.DEFAULT_EXCLUDES), report ) ) self.assertEqual(sorted(expected), sorted(sources)) def test_empty_exclude(self) -> None: - path = THIS_DIR / "include_exclude_tests" + path = THIS_DIR / "data" / "include_exclude_tests" + report = black.Report() empty = re.compile(r"") sources: List[Path] = [] expected = [ @@ -898,9 +1139,10 @@ class BlackTestCase(unittest.TestCase): Path(path / "b/.definitely_exclude/a.py"), Path(path / "b/.definitely_exclude/a.pyi"), ] + this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files_in_dir( - path, re.compile(black.DEFAULT_INCLUDES), empty + path, this_abs, re.compile(black.DEFAULT_INCLUDES), empty, report ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -910,6 +1152,75 @@ class BlackTestCase(unittest.TestCase): result = CliRunner().invoke(black.main, ["-", option, "**()(!!*)"]) self.assertEqual(result.exit_code, 2) + def test_preserves_line_endings(self) -> None: + with TemporaryDirectory() as workspace: + test_file = Path(workspace) / "test.py" + for nl in ["\n", "\r\n"]: + contents = nl.join(["def f( ):", " pass"]) + test_file.write_bytes(contents.encode()) + ff(test_file, write_back=black.WriteBack.YES) + updated_contents: bytes = test_file.read_bytes() + self.assertIn(nl.encode(), updated_contents) # type: ignore + if nl == "\n": + self.assertNotIn(b"\r\n", updated_contents) # type: ignore + + def test_assert_equivalent_different_asts(self) -> None: + with self.assertRaises(AssertionError): + black.assert_equivalent("{}", "None") + + def test_symlink_out_of_root_directory(self) -> None: + path = MagicMock() + root = THIS_DIR + child = MagicMock() + include = re.compile(black.DEFAULT_INCLUDES) + exclude = re.compile(black.DEFAULT_EXCLUDES) + report = black.Report() + # `child` should behave like a symlink which resolved path is clearly + # outside of the `root` directory. + path.iterdir.return_value = [child] + child.resolve.return_value = Path("/a/b/c") + child.is_symlink.return_value = True + try: + list(black.gen_python_files_in_dir(path, root, include, exclude, report)) + except ValueError as ve: + self.fail("`get_python_files_in_dir()` failed: {ve}") + path.iterdir.assert_called_once() + child.resolve.assert_called_once() + child.is_symlink.assert_called_once() + # `child` should behave like a strange file which resolved path is clearly + # outside of the `root` directory. + child.is_symlink.return_value = False + with self.assertRaises(ValueError): + list(black.gen_python_files_in_dir(path, root, include, exclude, report)) + path.iterdir.assert_called() + self.assertEqual(path.iterdir.call_count, 2) + child.resolve.assert_called() + self.assertEqual(child.resolve.call_count, 2) + child.is_symlink.assert_called() + self.assertEqual(child.is_symlink.call_count, 2) + + def test_shhh_click(self) -> None: + try: + from click import _unicodefun # type: ignore + except ModuleNotFoundError: + self.skipTest("Incompatible Click version") + if not hasattr(_unicodefun, "_verify_python3_env"): + self.skipTest("Incompatible Click version") + # First, let's see if Click is crashing with a preferred ASCII charset. + with patch("locale.getpreferredencoding") as gpe: + gpe.return_value = "ASCII" + with self.assertRaises(RuntimeError): + _unicodefun._verify_python3_env() + # Now, let's silence Click... + black.patch_click() + # ...and confirm it's silent. + with patch("locale.getpreferredencoding") as gpe: + gpe.return_value = "ASCII" + try: + _unicodefun._verify_python3_env() + except RuntimeError as re: + self.fail(f"`patch_click()` failed, exception still raised: {re}") + if __name__ == "__main__": - unittest.main() + unittest.main(module="test_black")