]> git.madduck.net Git - etc/vim.git/blob - tests/test_black.py

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Add pyls-black to README (#361)
[etc/vim.git] / tests / test_black.py
1 #!/usr/bin/env python3
2 import asyncio
3 from concurrent.futures import ThreadPoolExecutor
4 from contextlib import contextmanager
5 from functools import partial
6 from io import BytesIO, TextIOWrapper
7 import os
8 from pathlib import Path
9 import re
10 import sys
11 from tempfile import TemporaryDirectory
12 from typing import Any, BinaryIO, Generator, List, Tuple, Iterator
13 import unittest
14 from unittest.mock import patch, MagicMock
15
16 from click import unstyle
17 from click.testing import CliRunner
18
19 import black
20
21
22 ll = 88
23 ff = partial(black.format_file_in_place, line_length=ll, fast=True)
24 fs = partial(black.format_str, line_length=ll)
25 THIS_FILE = Path(__file__)
26 THIS_DIR = THIS_FILE.parent
27 EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)"
28
29
30 def dump_to_stderr(*output: str) -> str:
31     return "\n" + "\n".join(output) + "\n"
32
33
34 def read_data(name: str, data: bool = True) -> Tuple[str, str]:
35     """read_data('test_name') -> 'input', 'output'"""
36     if not name.endswith((".py", ".pyi", ".out", ".diff")):
37         name += ".py"
38     _input: List[str] = []
39     _output: List[str] = []
40     base_dir = THIS_DIR / "data" if data else THIS_DIR
41     with open(base_dir / name, "r", encoding="utf8") as test:
42         lines = test.readlines()
43     result = _input
44     for line in lines:
45         line = line.replace(EMPTY_LINE, "")
46         if line.rstrip() == "# output":
47             result = _output
48             continue
49
50         result.append(line)
51     if _input and not _output:
52         # If there's no output marker, treat the entire file as already pre-formatted.
53         _output = _input[:]
54     return "".join(_input).strip() + "\n", "".join(_output).strip() + "\n"
55
56
57 @contextmanager
58 def cache_dir(exists: bool = True) -> Iterator[Path]:
59     with TemporaryDirectory() as workspace:
60         cache_dir = Path(workspace)
61         if not exists:
62             cache_dir = cache_dir / "new"
63         with patch("black.CACHE_DIR", cache_dir):
64             yield cache_dir
65
66
67 @contextmanager
68 def event_loop(close: bool) -> Iterator[None]:
69     policy = asyncio.get_event_loop_policy()
70     old_loop = policy.get_event_loop()
71     loop = policy.new_event_loop()
72     asyncio.set_event_loop(loop)
73     try:
74         yield
75
76     finally:
77         policy.set_event_loop(old_loop)
78         if close:
79             loop.close()
80
81
82 class BlackRunner(CliRunner):
83     """Modify CliRunner so that stderr is not merged with stdout.
84
85     This is a hack that can be removed once we depend on Click 7.x"""
86
87     def __init__(self, stderrbuf: BinaryIO) -> None:
88         self.stderrbuf = stderrbuf
89         super().__init__()
90
91     @contextmanager
92     def isolation(self, *args: Any, **kwargs: Any) -> Generator[BinaryIO, None, None]:
93         with super().isolation(*args, **kwargs) as output:
94             try:
95                 hold_stderr = sys.stderr
96                 sys.stderr = TextIOWrapper(self.stderrbuf, encoding=self.charset)
97                 yield output
98             finally:
99                 sys.stderr = hold_stderr
100
101
102 class BlackTestCase(unittest.TestCase):
103     maxDiff = None
104
105     def assertFormatEqual(self, expected: str, actual: str) -> None:
106         if actual != expected and not os.environ.get("SKIP_AST_PRINT"):
107             bdv: black.DebugVisitor[Any]
108             black.out("Expected tree:", fg="green")
109             try:
110                 exp_node = black.lib2to3_parse(expected)
111                 bdv = black.DebugVisitor()
112                 list(bdv.visit(exp_node))
113             except Exception as ve:
114                 black.err(str(ve))
115             black.out("Actual tree:", fg="red")
116             try:
117                 exp_node = black.lib2to3_parse(actual)
118                 bdv = black.DebugVisitor()
119                 list(bdv.visit(exp_node))
120             except Exception as ve:
121                 black.err(str(ve))
122         self.assertEqual(expected, actual)
123
124     @patch("black.dump_to_file", dump_to_stderr)
125     def test_empty(self) -> None:
126         source = expected = ""
127         actual = fs(source)
128         self.assertFormatEqual(expected, actual)
129         black.assert_equivalent(source, actual)
130         black.assert_stable(source, actual, line_length=ll)
131
132     def test_empty_ff(self) -> None:
133         expected = ""
134         tmp_file = Path(black.dump_to_file())
135         try:
136             self.assertFalse(ff(tmp_file, write_back=black.WriteBack.YES))
137             with open(tmp_file, encoding="utf8") as f:
138                 actual = f.read()
139         finally:
140             os.unlink(tmp_file)
141         self.assertFormatEqual(expected, actual)
142
143     @patch("black.dump_to_file", dump_to_stderr)
144     def test_self(self) -> None:
145         source, expected = read_data("test_black", data=False)
146         actual = fs(source)
147         self.assertFormatEqual(expected, actual)
148         black.assert_equivalent(source, actual)
149         black.assert_stable(source, actual, line_length=ll)
150         self.assertFalse(ff(THIS_FILE))
151
152     @patch("black.dump_to_file", dump_to_stderr)
153     def test_black(self) -> None:
154         source, expected = read_data("../black", data=False)
155         actual = fs(source)
156         self.assertFormatEqual(expected, actual)
157         black.assert_equivalent(source, actual)
158         black.assert_stable(source, actual, line_length=ll)
159         self.assertFalse(ff(THIS_DIR / ".." / "black.py"))
160
161     def test_piping(self) -> None:
162         source, expected = read_data("../black", data=False)
163         stderrbuf = BytesIO()
164         result = BlackRunner(stderrbuf).invoke(
165             black.main, ["-", "--fast", f"--line-length={ll}"], input=source
166         )
167         self.assertEqual(result.exit_code, 0)
168         self.assertFormatEqual(expected, result.output)
169         black.assert_equivalent(source, result.output)
170         black.assert_stable(source, result.output, line_length=ll)
171
172     def test_piping_diff(self) -> None:
173         diff_header = re.compile(
174             rf"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d "
175             rf"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
176         )
177         source, _ = read_data("expression.py")
178         expected, _ = read_data("expression.diff")
179         config = THIS_DIR / "data" / "empty_pyproject.toml"
180         stderrbuf = BytesIO()
181         args = ["-", "--fast", f"--line-length={ll}", "--diff", f"--config={config}"]
182         result = BlackRunner(stderrbuf).invoke(black.main, args, input=source)
183         self.assertEqual(result.exit_code, 0)
184         actual = diff_header.sub("[Deterministic header]", result.output)
185         actual = actual.rstrip() + "\n"  # the diff output has a trailing space
186         self.assertEqual(expected, actual)
187
188     @patch("black.dump_to_file", dump_to_stderr)
189     def test_setup(self) -> None:
190         source, expected = read_data("../setup", data=False)
191         actual = fs(source)
192         self.assertFormatEqual(expected, actual)
193         black.assert_equivalent(source, actual)
194         black.assert_stable(source, actual, line_length=ll)
195         self.assertFalse(ff(THIS_DIR / ".." / "setup.py"))
196
197     @patch("black.dump_to_file", dump_to_stderr)
198     def test_function(self) -> None:
199         source, expected = read_data("function")
200         actual = fs(source)
201         self.assertFormatEqual(expected, actual)
202         black.assert_equivalent(source, actual)
203         black.assert_stable(source, actual, line_length=ll)
204
205     @patch("black.dump_to_file", dump_to_stderr)
206     def test_function2(self) -> None:
207         source, expected = read_data("function2")
208         actual = fs(source)
209         self.assertFormatEqual(expected, actual)
210         black.assert_equivalent(source, actual)
211         black.assert_stable(source, actual, line_length=ll)
212
213     @patch("black.dump_to_file", dump_to_stderr)
214     def test_expression(self) -> None:
215         source, expected = read_data("expression")
216         actual = fs(source)
217         self.assertFormatEqual(expected, actual)
218         black.assert_equivalent(source, actual)
219         black.assert_stable(source, actual, line_length=ll)
220
221     def test_expression_ff(self) -> None:
222         source, expected = read_data("expression")
223         tmp_file = Path(black.dump_to_file(source))
224         try:
225             self.assertTrue(ff(tmp_file, write_back=black.WriteBack.YES))
226             with open(tmp_file, encoding="utf8") as f:
227                 actual = f.read()
228         finally:
229             os.unlink(tmp_file)
230         self.assertFormatEqual(expected, actual)
231         with patch("black.dump_to_file", dump_to_stderr):
232             black.assert_equivalent(source, actual)
233             black.assert_stable(source, actual, line_length=ll)
234
235     def test_expression_diff(self) -> None:
236         source, _ = read_data("expression.py")
237         expected, _ = read_data("expression.diff")
238         tmp_file = Path(black.dump_to_file(source))
239         diff_header = re.compile(
240             rf"{re.escape(str(tmp_file))}\t\d\d\d\d-\d\d-\d\d "
241             rf"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
242         )
243         stderrbuf = BytesIO()
244         try:
245             result = BlackRunner(stderrbuf).invoke(
246                 black.main, ["--diff", str(tmp_file)]
247             )
248             self.assertEqual(result.exit_code, 0)
249         finally:
250             os.unlink(tmp_file)
251         actual = result.output
252         actual = diff_header.sub("[Deterministic header]", actual)
253         actual = actual.rstrip() + "\n"  # the diff output has a trailing space
254         if expected != actual:
255             dump = black.dump_to_file(actual)
256             msg = (
257                 f"Expected diff isn't equal to the actual. If you made changes "
258                 f"to expression.py and this is an anticipated difference, "
259                 f"overwrite tests/expression.diff with {dump}"
260             )
261             self.assertEqual(expected, actual, msg)
262
263     @patch("black.dump_to_file", dump_to_stderr)
264     def test_fstring(self) -> None:
265         source, expected = read_data("fstring")
266         actual = fs(source)
267         self.assertFormatEqual(expected, actual)
268         black.assert_equivalent(source, actual)
269         black.assert_stable(source, actual, line_length=ll)
270
271     @patch("black.dump_to_file", dump_to_stderr)
272     def test_string_quotes(self) -> None:
273         source, expected = read_data("string_quotes")
274         actual = fs(source)
275         self.assertFormatEqual(expected, actual)
276         black.assert_equivalent(source, actual)
277         black.assert_stable(source, actual, line_length=ll)
278         mode = black.FileMode.NO_STRING_NORMALIZATION
279         not_normalized = fs(source, mode=mode)
280         self.assertFormatEqual(source, not_normalized)
281         black.assert_equivalent(source, not_normalized)
282         black.assert_stable(source, not_normalized, line_length=ll, mode=mode)
283
284     @patch("black.dump_to_file", dump_to_stderr)
285     def test_slices(self) -> None:
286         source, expected = read_data("slices")
287         actual = fs(source)
288         self.assertFormatEqual(expected, actual)
289         black.assert_equivalent(source, actual)
290         black.assert_stable(source, actual, line_length=ll)
291
292     @patch("black.dump_to_file", dump_to_stderr)
293     def test_comments(self) -> None:
294         source, expected = read_data("comments")
295         actual = fs(source)
296         self.assertFormatEqual(expected, actual)
297         black.assert_equivalent(source, actual)
298         black.assert_stable(source, actual, line_length=ll)
299
300     @patch("black.dump_to_file", dump_to_stderr)
301     def test_comments2(self) -> None:
302         source, expected = read_data("comments2")
303         actual = fs(source)
304         self.assertFormatEqual(expected, actual)
305         black.assert_equivalent(source, actual)
306         black.assert_stable(source, actual, line_length=ll)
307
308     @patch("black.dump_to_file", dump_to_stderr)
309     def test_comments3(self) -> None:
310         source, expected = read_data("comments3")
311         actual = fs(source)
312         self.assertFormatEqual(expected, actual)
313         black.assert_equivalent(source, actual)
314         black.assert_stable(source, actual, line_length=ll)
315
316     @patch("black.dump_to_file", dump_to_stderr)
317     def test_comments4(self) -> None:
318         source, expected = read_data("comments4")
319         actual = fs(source)
320         self.assertFormatEqual(expected, actual)
321         black.assert_equivalent(source, actual)
322         black.assert_stable(source, actual, line_length=ll)
323
324     @patch("black.dump_to_file", dump_to_stderr)
325     def test_comments5(self) -> None:
326         source, expected = read_data("comments5")
327         actual = fs(source)
328         self.assertFormatEqual(expected, actual)
329         black.assert_equivalent(source, actual)
330         black.assert_stable(source, actual, line_length=ll)
331
332     @patch("black.dump_to_file", dump_to_stderr)
333     def test_cantfit(self) -> None:
334         source, expected = read_data("cantfit")
335         actual = fs(source)
336         self.assertFormatEqual(expected, actual)
337         black.assert_equivalent(source, actual)
338         black.assert_stable(source, actual, line_length=ll)
339
340     @patch("black.dump_to_file", dump_to_stderr)
341     def test_import_spacing(self) -> None:
342         source, expected = read_data("import_spacing")
343         actual = fs(source)
344         self.assertFormatEqual(expected, actual)
345         black.assert_equivalent(source, actual)
346         black.assert_stable(source, actual, line_length=ll)
347
348     @patch("black.dump_to_file", dump_to_stderr)
349     def test_composition(self) -> None:
350         source, expected = read_data("composition")
351         actual = fs(source)
352         self.assertFormatEqual(expected, actual)
353         black.assert_equivalent(source, actual)
354         black.assert_stable(source, actual, line_length=ll)
355
356     @patch("black.dump_to_file", dump_to_stderr)
357     def test_empty_lines(self) -> None:
358         source, expected = read_data("empty_lines")
359         actual = fs(source)
360         self.assertFormatEqual(expected, actual)
361         black.assert_equivalent(source, actual)
362         black.assert_stable(source, actual, line_length=ll)
363
364     @patch("black.dump_to_file", dump_to_stderr)
365     def test_string_prefixes(self) -> None:
366         source, expected = read_data("string_prefixes")
367         actual = fs(source)
368         self.assertFormatEqual(expected, actual)
369         black.assert_equivalent(source, actual)
370         black.assert_stable(source, actual, line_length=ll)
371
372     @patch("black.dump_to_file", dump_to_stderr)
373     def test_python2(self) -> None:
374         source, expected = read_data("python2")
375         actual = fs(source)
376         self.assertFormatEqual(expected, actual)
377         # black.assert_equivalent(source, actual)
378         black.assert_stable(source, actual, line_length=ll)
379
380     @patch("black.dump_to_file", dump_to_stderr)
381     def test_python2_unicode_literals(self) -> None:
382         source, expected = read_data("python2_unicode_literals")
383         actual = fs(source)
384         self.assertFormatEqual(expected, actual)
385         black.assert_stable(source, actual, line_length=ll)
386
387     @patch("black.dump_to_file", dump_to_stderr)
388     def test_stub(self) -> None:
389         mode = black.FileMode.PYI
390         source, expected = read_data("stub.pyi")
391         actual = fs(source, mode=mode)
392         self.assertFormatEqual(expected, actual)
393         black.assert_stable(source, actual, line_length=ll, mode=mode)
394
395     @patch("black.dump_to_file", dump_to_stderr)
396     def test_fmtonoff(self) -> None:
397         source, expected = read_data("fmtonoff")
398         actual = fs(source)
399         self.assertFormatEqual(expected, actual)
400         black.assert_equivalent(source, actual)
401         black.assert_stable(source, actual, line_length=ll)
402
403     @patch("black.dump_to_file", dump_to_stderr)
404     def test_remove_empty_parentheses_after_class(self) -> None:
405         source, expected = read_data("class_blank_parentheses")
406         actual = fs(source)
407         self.assertFormatEqual(expected, actual)
408         black.assert_equivalent(source, actual)
409         black.assert_stable(source, actual, line_length=ll)
410
411     @patch("black.dump_to_file", dump_to_stderr)
412     def test_new_line_between_class_and_code(self) -> None:
413         source, expected = read_data("class_methods_new_line")
414         actual = fs(source)
415         self.assertFormatEqual(expected, actual)
416         black.assert_equivalent(source, actual)
417         black.assert_stable(source, actual, line_length=ll)
418
419     def test_report_verbose(self) -> None:
420         report = black.Report(verbose=True)
421         out_lines = []
422         err_lines = []
423
424         def out(msg: str, **kwargs: Any) -> None:
425             out_lines.append(msg)
426
427         def err(msg: str, **kwargs: Any) -> None:
428             err_lines.append(msg)
429
430         with patch("black.out", out), patch("black.err", err):
431             report.done(Path("f1"), black.Changed.NO)
432             self.assertEqual(len(out_lines), 1)
433             self.assertEqual(len(err_lines), 0)
434             self.assertEqual(out_lines[-1], "f1 already well formatted, good job.")
435             self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
436             self.assertEqual(report.return_code, 0)
437             report.done(Path("f2"), black.Changed.YES)
438             self.assertEqual(len(out_lines), 2)
439             self.assertEqual(len(err_lines), 0)
440             self.assertEqual(out_lines[-1], "reformatted f2")
441             self.assertEqual(
442                 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
443             )
444             report.done(Path("f3"), black.Changed.CACHED)
445             self.assertEqual(len(out_lines), 3)
446             self.assertEqual(len(err_lines), 0)
447             self.assertEqual(
448                 out_lines[-1], "f3 wasn't modified on disk since last run."
449             )
450             self.assertEqual(
451                 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
452             )
453             self.assertEqual(report.return_code, 0)
454             report.check = True
455             self.assertEqual(report.return_code, 1)
456             report.check = False
457             report.failed(Path("e1"), "boom")
458             self.assertEqual(len(out_lines), 3)
459             self.assertEqual(len(err_lines), 1)
460             self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
461             self.assertEqual(
462                 unstyle(str(report)),
463                 "1 file reformatted, 2 files left unchanged, "
464                 "1 file failed to reformat.",
465             )
466             self.assertEqual(report.return_code, 123)
467             report.done(Path("f3"), black.Changed.YES)
468             self.assertEqual(len(out_lines), 4)
469             self.assertEqual(len(err_lines), 1)
470             self.assertEqual(out_lines[-1], "reformatted f3")
471             self.assertEqual(
472                 unstyle(str(report)),
473                 "2 files reformatted, 2 files left unchanged, "
474                 "1 file failed to reformat.",
475             )
476             self.assertEqual(report.return_code, 123)
477             report.failed(Path("e2"), "boom")
478             self.assertEqual(len(out_lines), 4)
479             self.assertEqual(len(err_lines), 2)
480             self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
481             self.assertEqual(
482                 unstyle(str(report)),
483                 "2 files reformatted, 2 files left unchanged, "
484                 "2 files failed to reformat.",
485             )
486             self.assertEqual(report.return_code, 123)
487             report.path_ignored(Path("wat"), "no match")
488             self.assertEqual(len(out_lines), 5)
489             self.assertEqual(len(err_lines), 2)
490             self.assertEqual(out_lines[-1], "wat ignored: no match")
491             self.assertEqual(
492                 unstyle(str(report)),
493                 "2 files reformatted, 2 files left unchanged, "
494                 "2 files failed to reformat.",
495             )
496             self.assertEqual(report.return_code, 123)
497             report.done(Path("f4"), black.Changed.NO)
498             self.assertEqual(len(out_lines), 6)
499             self.assertEqual(len(err_lines), 2)
500             self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
501             self.assertEqual(
502                 unstyle(str(report)),
503                 "2 files reformatted, 3 files left unchanged, "
504                 "2 files failed to reformat.",
505             )
506             self.assertEqual(report.return_code, 123)
507             report.check = True
508             self.assertEqual(
509                 unstyle(str(report)),
510                 "2 files would be reformatted, 3 files would be left unchanged, "
511                 "2 files would fail to reformat.",
512             )
513
514     def test_report_quiet(self) -> None:
515         report = black.Report(quiet=True)
516         out_lines = []
517         err_lines = []
518
519         def out(msg: str, **kwargs: Any) -> None:
520             out_lines.append(msg)
521
522         def err(msg: str, **kwargs: Any) -> None:
523             err_lines.append(msg)
524
525         with patch("black.out", out), patch("black.err", err):
526             report.done(Path("f1"), black.Changed.NO)
527             self.assertEqual(len(out_lines), 0)
528             self.assertEqual(len(err_lines), 0)
529             self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
530             self.assertEqual(report.return_code, 0)
531             report.done(Path("f2"), black.Changed.YES)
532             self.assertEqual(len(out_lines), 0)
533             self.assertEqual(len(err_lines), 0)
534             self.assertEqual(
535                 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
536             )
537             report.done(Path("f3"), black.Changed.CACHED)
538             self.assertEqual(len(out_lines), 0)
539             self.assertEqual(len(err_lines), 0)
540             self.assertEqual(
541                 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
542             )
543             self.assertEqual(report.return_code, 0)
544             report.check = True
545             self.assertEqual(report.return_code, 1)
546             report.check = False
547             report.failed(Path("e1"), "boom")
548             self.assertEqual(len(out_lines), 0)
549             self.assertEqual(len(err_lines), 1)
550             self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
551             self.assertEqual(
552                 unstyle(str(report)),
553                 "1 file reformatted, 2 files left unchanged, "
554                 "1 file failed to reformat.",
555             )
556             self.assertEqual(report.return_code, 123)
557             report.done(Path("f3"), black.Changed.YES)
558             self.assertEqual(len(out_lines), 0)
559             self.assertEqual(len(err_lines), 1)
560             self.assertEqual(
561                 unstyle(str(report)),
562                 "2 files reformatted, 2 files left unchanged, "
563                 "1 file failed to reformat.",
564             )
565             self.assertEqual(report.return_code, 123)
566             report.failed(Path("e2"), "boom")
567             self.assertEqual(len(out_lines), 0)
568             self.assertEqual(len(err_lines), 2)
569             self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
570             self.assertEqual(
571                 unstyle(str(report)),
572                 "2 files reformatted, 2 files left unchanged, "
573                 "2 files failed to reformat.",
574             )
575             self.assertEqual(report.return_code, 123)
576             report.path_ignored(Path("wat"), "no match")
577             self.assertEqual(len(out_lines), 0)
578             self.assertEqual(len(err_lines), 2)
579             self.assertEqual(
580                 unstyle(str(report)),
581                 "2 files reformatted, 2 files left unchanged, "
582                 "2 files failed to reformat.",
583             )
584             self.assertEqual(report.return_code, 123)
585             report.done(Path("f4"), black.Changed.NO)
586             self.assertEqual(len(out_lines), 0)
587             self.assertEqual(len(err_lines), 2)
588             self.assertEqual(
589                 unstyle(str(report)),
590                 "2 files reformatted, 3 files left unchanged, "
591                 "2 files failed to reformat.",
592             )
593             self.assertEqual(report.return_code, 123)
594             report.check = True
595             self.assertEqual(
596                 unstyle(str(report)),
597                 "2 files would be reformatted, 3 files would be left unchanged, "
598                 "2 files would fail to reformat.",
599             )
600
601     def test_report_normal(self) -> None:
602         report = black.Report()
603         out_lines = []
604         err_lines = []
605
606         def out(msg: str, **kwargs: Any) -> None:
607             out_lines.append(msg)
608
609         def err(msg: str, **kwargs: Any) -> None:
610             err_lines.append(msg)
611
612         with patch("black.out", out), patch("black.err", err):
613             report.done(Path("f1"), black.Changed.NO)
614             self.assertEqual(len(out_lines), 0)
615             self.assertEqual(len(err_lines), 0)
616             self.assertEqual(unstyle(str(report)), "1 file left unchanged.")
617             self.assertEqual(report.return_code, 0)
618             report.done(Path("f2"), black.Changed.YES)
619             self.assertEqual(len(out_lines), 1)
620             self.assertEqual(len(err_lines), 0)
621             self.assertEqual(out_lines[-1], "reformatted f2")
622             self.assertEqual(
623                 unstyle(str(report)), "1 file reformatted, 1 file left unchanged."
624             )
625             report.done(Path("f3"), black.Changed.CACHED)
626             self.assertEqual(len(out_lines), 1)
627             self.assertEqual(len(err_lines), 0)
628             self.assertEqual(out_lines[-1], "reformatted f2")
629             self.assertEqual(
630                 unstyle(str(report)), "1 file reformatted, 2 files left unchanged."
631             )
632             self.assertEqual(report.return_code, 0)
633             report.check = True
634             self.assertEqual(report.return_code, 1)
635             report.check = False
636             report.failed(Path("e1"), "boom")
637             self.assertEqual(len(out_lines), 1)
638             self.assertEqual(len(err_lines), 1)
639             self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
640             self.assertEqual(
641                 unstyle(str(report)),
642                 "1 file reformatted, 2 files left unchanged, "
643                 "1 file failed to reformat.",
644             )
645             self.assertEqual(report.return_code, 123)
646             report.done(Path("f3"), black.Changed.YES)
647             self.assertEqual(len(out_lines), 2)
648             self.assertEqual(len(err_lines), 1)
649             self.assertEqual(out_lines[-1], "reformatted f3")
650             self.assertEqual(
651                 unstyle(str(report)),
652                 "2 files reformatted, 2 files left unchanged, "
653                 "1 file failed to reformat.",
654             )
655             self.assertEqual(report.return_code, 123)
656             report.failed(Path("e2"), "boom")
657             self.assertEqual(len(out_lines), 2)
658             self.assertEqual(len(err_lines), 2)
659             self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
660             self.assertEqual(
661                 unstyle(str(report)),
662                 "2 files reformatted, 2 files left unchanged, "
663                 "2 files failed to reformat.",
664             )
665             self.assertEqual(report.return_code, 123)
666             report.path_ignored(Path("wat"), "no match")
667             self.assertEqual(len(out_lines), 2)
668             self.assertEqual(len(err_lines), 2)
669             self.assertEqual(
670                 unstyle(str(report)),
671                 "2 files reformatted, 2 files left unchanged, "
672                 "2 files failed to reformat.",
673             )
674             self.assertEqual(report.return_code, 123)
675             report.done(Path("f4"), black.Changed.NO)
676             self.assertEqual(len(out_lines), 2)
677             self.assertEqual(len(err_lines), 2)
678             self.assertEqual(
679                 unstyle(str(report)),
680                 "2 files reformatted, 3 files left unchanged, "
681                 "2 files failed to reformat.",
682             )
683             self.assertEqual(report.return_code, 123)
684             report.check = True
685             self.assertEqual(
686                 unstyle(str(report)),
687                 "2 files would be reformatted, 3 files would be left unchanged, "
688                 "2 files would fail to reformat.",
689             )
690
691     def test_is_python36(self) -> None:
692         node = black.lib2to3_parse("def f(*, arg): ...\n")
693         self.assertFalse(black.is_python36(node))
694         node = black.lib2to3_parse("def f(*, arg,): ...\n")
695         self.assertTrue(black.is_python36(node))
696         node = black.lib2to3_parse("def f(*, arg): f'string'\n")
697         self.assertTrue(black.is_python36(node))
698         source, expected = read_data("function")
699         node = black.lib2to3_parse(source)
700         self.assertTrue(black.is_python36(node))
701         node = black.lib2to3_parse(expected)
702         self.assertTrue(black.is_python36(node))
703         source, expected = read_data("expression")
704         node = black.lib2to3_parse(source)
705         self.assertFalse(black.is_python36(node))
706         node = black.lib2to3_parse(expected)
707         self.assertFalse(black.is_python36(node))
708
709     def test_get_future_imports(self) -> None:
710         node = black.lib2to3_parse("\n")
711         self.assertEqual(set(), black.get_future_imports(node))
712         node = black.lib2to3_parse("from __future__ import black\n")
713         self.assertEqual({"black"}, black.get_future_imports(node))
714         node = black.lib2to3_parse("from __future__ import multiple, imports\n")
715         self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
716         node = black.lib2to3_parse("from __future__ import (parenthesized, imports)\n")
717         self.assertEqual({"parenthesized", "imports"}, black.get_future_imports(node))
718         node = black.lib2to3_parse(
719             "from __future__ import multiple\nfrom __future__ import imports\n"
720         )
721         self.assertEqual({"multiple", "imports"}, black.get_future_imports(node))
722         node = black.lib2to3_parse("# comment\nfrom __future__ import black\n")
723         self.assertEqual({"black"}, black.get_future_imports(node))
724         node = black.lib2to3_parse('"""docstring"""\nfrom __future__ import black\n')
725         self.assertEqual({"black"}, black.get_future_imports(node))
726         node = black.lib2to3_parse("some(other, code)\nfrom __future__ import black\n")
727         self.assertEqual(set(), black.get_future_imports(node))
728         node = black.lib2to3_parse("from some.module import black\n")
729         self.assertEqual(set(), black.get_future_imports(node))
730
731     def test_debug_visitor(self) -> None:
732         source, _ = read_data("debug_visitor.py")
733         expected, _ = read_data("debug_visitor.out")
734         out_lines = []
735         err_lines = []
736
737         def out(msg: str, **kwargs: Any) -> None:
738             out_lines.append(msg)
739
740         def err(msg: str, **kwargs: Any) -> None:
741             err_lines.append(msg)
742
743         with patch("black.out", out), patch("black.err", err):
744             black.DebugVisitor.show(source)
745         actual = "\n".join(out_lines) + "\n"
746         log_name = ""
747         if expected != actual:
748             log_name = black.dump_to_file(*out_lines)
749         self.assertEqual(
750             expected,
751             actual,
752             f"AST print out is different. Actual version dumped to {log_name}",
753         )
754
755     def test_format_file_contents(self) -> None:
756         empty = ""
757         with self.assertRaises(black.NothingChanged):
758             black.format_file_contents(empty, line_length=ll, fast=False)
759         just_nl = "\n"
760         with self.assertRaises(black.NothingChanged):
761             black.format_file_contents(just_nl, line_length=ll, fast=False)
762         same = "l = [1, 2, 3]\n"
763         with self.assertRaises(black.NothingChanged):
764             black.format_file_contents(same, line_length=ll, fast=False)
765         different = "l = [1,2,3]"
766         expected = same
767         actual = black.format_file_contents(different, line_length=ll, fast=False)
768         self.assertEqual(expected, actual)
769         invalid = "return if you can"
770         with self.assertRaises(ValueError) as e:
771             black.format_file_contents(invalid, line_length=ll, fast=False)
772         self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
773
774     def test_endmarker(self) -> None:
775         n = black.lib2to3_parse("\n")
776         self.assertEqual(n.type, black.syms.file_input)
777         self.assertEqual(len(n.children), 1)
778         self.assertEqual(n.children[0].type, black.token.ENDMARKER)
779
780     @unittest.skipIf(os.environ.get("SKIP_AST_PRINT"), "user set SKIP_AST_PRINT")
781     def test_assertFormatEqual(self) -> None:
782         out_lines = []
783         err_lines = []
784
785         def out(msg: str, **kwargs: Any) -> None:
786             out_lines.append(msg)
787
788         def err(msg: str, **kwargs: Any) -> None:
789             err_lines.append(msg)
790
791         with patch("black.out", out), patch("black.err", err):
792             with self.assertRaises(AssertionError):
793                 self.assertFormatEqual("l = [1, 2, 3]", "l = [1, 2, 3,]")
794
795         out_str = "".join(out_lines)
796         self.assertTrue("Expected tree:" in out_str)
797         self.assertTrue("Actual tree:" in out_str)
798         self.assertEqual("".join(err_lines), "")
799
800     def test_cache_broken_file(self) -> None:
801         mode = black.FileMode.AUTO_DETECT
802         with cache_dir() as workspace:
803             cache_file = black.get_cache_file(black.DEFAULT_LINE_LENGTH, mode)
804             with cache_file.open("w") as fobj:
805                 fobj.write("this is not a pickle")
806             self.assertEqual(black.read_cache(black.DEFAULT_LINE_LENGTH, mode), {})
807             src = (workspace / "test.py").resolve()
808             with src.open("w") as fobj:
809                 fobj.write("print('hello')")
810             result = CliRunner().invoke(black.main, [str(src)])
811             self.assertEqual(result.exit_code, 0)
812             cache = black.read_cache(black.DEFAULT_LINE_LENGTH, mode)
813             self.assertIn(src, cache)
814
815     def test_cache_single_file_already_cached(self) -> None:
816         mode = black.FileMode.AUTO_DETECT
817         with cache_dir() as workspace:
818             src = (workspace / "test.py").resolve()
819             with src.open("w") as fobj:
820                 fobj.write("print('hello')")
821             black.write_cache({}, [src], black.DEFAULT_LINE_LENGTH, mode)
822             result = CliRunner().invoke(black.main, [str(src)])
823             self.assertEqual(result.exit_code, 0)
824             with src.open("r") as fobj:
825                 self.assertEqual(fobj.read(), "print('hello')")
826
827     @event_loop(close=False)
828     def test_cache_multiple_files(self) -> None:
829         mode = black.FileMode.AUTO_DETECT
830         with cache_dir() as workspace, patch(
831             "black.ProcessPoolExecutor", new=ThreadPoolExecutor
832         ):
833             one = (workspace / "one.py").resolve()
834             with one.open("w") as fobj:
835                 fobj.write("print('hello')")
836             two = (workspace / "two.py").resolve()
837             with two.open("w") as fobj:
838                 fobj.write("print('hello')")
839             black.write_cache({}, [one], black.DEFAULT_LINE_LENGTH, mode)
840             result = CliRunner().invoke(black.main, [str(workspace)])
841             self.assertEqual(result.exit_code, 0)
842             with one.open("r") as fobj:
843                 self.assertEqual(fobj.read(), "print('hello')")
844             with two.open("r") as fobj:
845                 self.assertEqual(fobj.read(), 'print("hello")\n')
846             cache = black.read_cache(black.DEFAULT_LINE_LENGTH, mode)
847             self.assertIn(one, cache)
848             self.assertIn(two, cache)
849
850     def test_no_cache_when_writeback_diff(self) -> None:
851         mode = black.FileMode.AUTO_DETECT
852         with cache_dir() as workspace:
853             src = (workspace / "test.py").resolve()
854             with src.open("w") as fobj:
855                 fobj.write("print('hello')")
856             result = CliRunner().invoke(black.main, [str(src), "--diff"])
857             self.assertEqual(result.exit_code, 0)
858             cache_file = black.get_cache_file(black.DEFAULT_LINE_LENGTH, mode)
859             self.assertFalse(cache_file.exists())
860
861     def test_no_cache_when_stdin(self) -> None:
862         mode = black.FileMode.AUTO_DETECT
863         with cache_dir():
864             result = CliRunner().invoke(black.main, ["-"], input="print('hello')")
865             self.assertEqual(result.exit_code, 0)
866             cache_file = black.get_cache_file(black.DEFAULT_LINE_LENGTH, mode)
867             self.assertFalse(cache_file.exists())
868
869     def test_read_cache_no_cachefile(self) -> None:
870         mode = black.FileMode.AUTO_DETECT
871         with cache_dir():
872             self.assertEqual(black.read_cache(black.DEFAULT_LINE_LENGTH, mode), {})
873
874     def test_write_cache_read_cache(self) -> None:
875         mode = black.FileMode.AUTO_DETECT
876         with cache_dir() as workspace:
877             src = (workspace / "test.py").resolve()
878             src.touch()
879             black.write_cache({}, [src], black.DEFAULT_LINE_LENGTH, mode)
880             cache = black.read_cache(black.DEFAULT_LINE_LENGTH, mode)
881             self.assertIn(src, cache)
882             self.assertEqual(cache[src], black.get_cache_info(src))
883
884     def test_filter_cached(self) -> None:
885         with TemporaryDirectory() as workspace:
886             path = Path(workspace)
887             uncached = (path / "uncached").resolve()
888             cached = (path / "cached").resolve()
889             cached_but_changed = (path / "changed").resolve()
890             uncached.touch()
891             cached.touch()
892             cached_but_changed.touch()
893             cache = {cached: black.get_cache_info(cached), cached_but_changed: (0.0, 0)}
894             todo, done = black.filter_cached(
895                 cache, {uncached, cached, cached_but_changed}
896             )
897             self.assertEqual(todo, {uncached, cached_but_changed})
898             self.assertEqual(done, {cached})
899
900     def test_write_cache_creates_directory_if_needed(self) -> None:
901         mode = black.FileMode.AUTO_DETECT
902         with cache_dir(exists=False) as workspace:
903             self.assertFalse(workspace.exists())
904             black.write_cache({}, [], black.DEFAULT_LINE_LENGTH, mode)
905             self.assertTrue(workspace.exists())
906
907     @event_loop(close=False)
908     def test_failed_formatting_does_not_get_cached(self) -> None:
909         mode = black.FileMode.AUTO_DETECT
910         with cache_dir() as workspace, patch(
911             "black.ProcessPoolExecutor", new=ThreadPoolExecutor
912         ):
913             failing = (workspace / "failing.py").resolve()
914             with failing.open("w") as fobj:
915                 fobj.write("not actually python")
916             clean = (workspace / "clean.py").resolve()
917             with clean.open("w") as fobj:
918                 fobj.write('print("hello")\n')
919             result = CliRunner().invoke(black.main, [str(workspace)])
920             self.assertEqual(result.exit_code, 123)
921             cache = black.read_cache(black.DEFAULT_LINE_LENGTH, mode)
922             self.assertNotIn(failing, cache)
923             self.assertIn(clean, cache)
924
925     def test_write_cache_write_fail(self) -> None:
926         mode = black.FileMode.AUTO_DETECT
927         with cache_dir(), patch.object(Path, "open") as mock:
928             mock.side_effect = OSError
929             black.write_cache({}, [], black.DEFAULT_LINE_LENGTH, mode)
930
931     @event_loop(close=False)
932     def test_check_diff_use_together(self) -> None:
933         with cache_dir():
934             # Files which will be reformatted.
935             src1 = (THIS_DIR / "data" / "string_quotes.py").resolve()
936             result = CliRunner().invoke(black.main, [str(src1), "--diff", "--check"])
937             self.assertEqual(result.exit_code, 1, result.output)
938
939             # Files which will not be reformatted.
940             src2 = (THIS_DIR / "data" / "composition.py").resolve()
941             result = CliRunner().invoke(black.main, [str(src2), "--diff", "--check"])
942             self.assertEqual(result.exit_code, 0, result.output)
943
944             # Multi file command.
945             result = CliRunner().invoke(
946                 black.main, [str(src1), str(src2), "--diff", "--check"]
947             )
948             self.assertEqual(result.exit_code, 1, result.output)
949
950     def test_no_files(self) -> None:
951         with cache_dir():
952             # Without an argument, black exits with error code 0.
953             result = CliRunner().invoke(black.main, [])
954             self.assertEqual(result.exit_code, 0)
955
956     def test_broken_symlink(self) -> None:
957         with cache_dir() as workspace:
958             symlink = workspace / "broken_link.py"
959             try:
960                 symlink.symlink_to("nonexistent.py")
961             except OSError as e:
962                 self.skipTest(f"Can't create symlinks: {e}")
963             result = CliRunner().invoke(black.main, [str(workspace.resolve())])
964             self.assertEqual(result.exit_code, 0)
965
966     def test_read_cache_line_lengths(self) -> None:
967         mode = black.FileMode.AUTO_DETECT
968         with cache_dir() as workspace:
969             path = (workspace / "file.py").resolve()
970             path.touch()
971             black.write_cache({}, [path], 1, mode)
972             one = black.read_cache(1, mode)
973             self.assertIn(path, one)
974             two = black.read_cache(2, mode)
975             self.assertNotIn(path, two)
976
977     def test_single_file_force_pyi(self) -> None:
978         reg_mode = black.FileMode.AUTO_DETECT
979         pyi_mode = black.FileMode.PYI
980         contents, expected = read_data("force_pyi")
981         with cache_dir() as workspace:
982             path = (workspace / "file.py").resolve()
983             with open(path, "w") as fh:
984                 fh.write(contents)
985             result = CliRunner().invoke(black.main, [str(path), "--pyi"])
986             self.assertEqual(result.exit_code, 0)
987             with open(path, "r") as fh:
988                 actual = fh.read()
989             # verify cache with --pyi is separate
990             pyi_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, pyi_mode)
991             self.assertIn(path, pyi_cache)
992             normal_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, reg_mode)
993             self.assertNotIn(path, normal_cache)
994         self.assertEqual(actual, expected)
995
996     @event_loop(close=False)
997     def test_multi_file_force_pyi(self) -> None:
998         reg_mode = black.FileMode.AUTO_DETECT
999         pyi_mode = black.FileMode.PYI
1000         contents, expected = read_data("force_pyi")
1001         with cache_dir() as workspace:
1002             paths = [
1003                 (workspace / "file1.py").resolve(),
1004                 (workspace / "file2.py").resolve(),
1005             ]
1006             for path in paths:
1007                 with open(path, "w") as fh:
1008                     fh.write(contents)
1009             result = CliRunner().invoke(black.main, [str(p) for p in paths] + ["--pyi"])
1010             self.assertEqual(result.exit_code, 0)
1011             for path in paths:
1012                 with open(path, "r") as fh:
1013                     actual = fh.read()
1014                 self.assertEqual(actual, expected)
1015             # verify cache with --pyi is separate
1016             pyi_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, pyi_mode)
1017             normal_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, reg_mode)
1018             for path in paths:
1019                 self.assertIn(path, pyi_cache)
1020                 self.assertNotIn(path, normal_cache)
1021
1022     def test_pipe_force_pyi(self) -> None:
1023         source, expected = read_data("force_pyi")
1024         result = CliRunner().invoke(black.main, ["-", "-q", "--pyi"], input=source)
1025         self.assertEqual(result.exit_code, 0)
1026         actual = result.output
1027         self.assertFormatEqual(actual, expected)
1028
1029     def test_single_file_force_py36(self) -> None:
1030         reg_mode = black.FileMode.AUTO_DETECT
1031         py36_mode = black.FileMode.PYTHON36
1032         source, expected = read_data("force_py36")
1033         with cache_dir() as workspace:
1034             path = (workspace / "file.py").resolve()
1035             with open(path, "w") as fh:
1036                 fh.write(source)
1037             result = CliRunner().invoke(black.main, [str(path), "--py36"])
1038             self.assertEqual(result.exit_code, 0)
1039             with open(path, "r") as fh:
1040                 actual = fh.read()
1041             # verify cache with --py36 is separate
1042             py36_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, py36_mode)
1043             self.assertIn(path, py36_cache)
1044             normal_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, reg_mode)
1045             self.assertNotIn(path, normal_cache)
1046         self.assertEqual(actual, expected)
1047
1048     @event_loop(close=False)
1049     def test_multi_file_force_py36(self) -> None:
1050         reg_mode = black.FileMode.AUTO_DETECT
1051         py36_mode = black.FileMode.PYTHON36
1052         source, expected = read_data("force_py36")
1053         with cache_dir() as workspace:
1054             paths = [
1055                 (workspace / "file1.py").resolve(),
1056                 (workspace / "file2.py").resolve(),
1057             ]
1058             for path in paths:
1059                 with open(path, "w") as fh:
1060                     fh.write(source)
1061             result = CliRunner().invoke(
1062                 black.main, [str(p) for p in paths] + ["--py36"]
1063             )
1064             self.assertEqual(result.exit_code, 0)
1065             for path in paths:
1066                 with open(path, "r") as fh:
1067                     actual = fh.read()
1068                 self.assertEqual(actual, expected)
1069             # verify cache with --py36 is separate
1070             pyi_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, py36_mode)
1071             normal_cache = black.read_cache(black.DEFAULT_LINE_LENGTH, reg_mode)
1072             for path in paths:
1073                 self.assertIn(path, pyi_cache)
1074                 self.assertNotIn(path, normal_cache)
1075
1076     def test_pipe_force_py36(self) -> None:
1077         source, expected = read_data("force_py36")
1078         result = CliRunner().invoke(black.main, ["-", "-q", "--py36"], input=source)
1079         self.assertEqual(result.exit_code, 0)
1080         actual = result.output
1081         self.assertFormatEqual(actual, expected)
1082
1083     def test_include_exclude(self) -> None:
1084         path = THIS_DIR / "data" / "include_exclude_tests"
1085         include = re.compile(r"\.pyi?$")
1086         exclude = re.compile(r"/exclude/|/\.definitely_exclude/")
1087         report = black.Report()
1088         sources: List[Path] = []
1089         expected = [
1090             Path(path / "b/dont_exclude/a.py"),
1091             Path(path / "b/dont_exclude/a.pyi"),
1092         ]
1093         this_abs = THIS_DIR.resolve()
1094         sources.extend(
1095             black.gen_python_files_in_dir(path, this_abs, include, exclude, report)
1096         )
1097         self.assertEqual(sorted(expected), sorted(sources))
1098
1099     def test_empty_include(self) -> None:
1100         path = THIS_DIR / "data" / "include_exclude_tests"
1101         report = black.Report()
1102         empty = re.compile(r"")
1103         sources: List[Path] = []
1104         expected = [
1105             Path(path / "b/exclude/a.pie"),
1106             Path(path / "b/exclude/a.py"),
1107             Path(path / "b/exclude/a.pyi"),
1108             Path(path / "b/dont_exclude/a.pie"),
1109             Path(path / "b/dont_exclude/a.py"),
1110             Path(path / "b/dont_exclude/a.pyi"),
1111             Path(path / "b/.definitely_exclude/a.pie"),
1112             Path(path / "b/.definitely_exclude/a.py"),
1113             Path(path / "b/.definitely_exclude/a.pyi"),
1114         ]
1115         this_abs = THIS_DIR.resolve()
1116         sources.extend(
1117             black.gen_python_files_in_dir(
1118                 path, this_abs, empty, re.compile(black.DEFAULT_EXCLUDES), report
1119             )
1120         )
1121         self.assertEqual(sorted(expected), sorted(sources))
1122
1123     def test_empty_exclude(self) -> None:
1124         path = THIS_DIR / "data" / "include_exclude_tests"
1125         report = black.Report()
1126         empty = re.compile(r"")
1127         sources: List[Path] = []
1128         expected = [
1129             Path(path / "b/dont_exclude/a.py"),
1130             Path(path / "b/dont_exclude/a.pyi"),
1131             Path(path / "b/exclude/a.py"),
1132             Path(path / "b/exclude/a.pyi"),
1133             Path(path / "b/.definitely_exclude/a.py"),
1134             Path(path / "b/.definitely_exclude/a.pyi"),
1135         ]
1136         this_abs = THIS_DIR.resolve()
1137         sources.extend(
1138             black.gen_python_files_in_dir(
1139                 path, this_abs, re.compile(black.DEFAULT_INCLUDES), empty, report
1140             )
1141         )
1142         self.assertEqual(sorted(expected), sorted(sources))
1143
1144     def test_invalid_include_exclude(self) -> None:
1145         for option in ["--include", "--exclude"]:
1146             result = CliRunner().invoke(black.main, ["-", option, "**()(!!*)"])
1147             self.assertEqual(result.exit_code, 2)
1148
1149     def test_preserves_line_endings(self) -> None:
1150         with TemporaryDirectory() as workspace:
1151             test_file = Path(workspace) / "test.py"
1152             for nl in ["\n", "\r\n"]:
1153                 contents = nl.join(["def f(  ):", "    pass"])
1154                 test_file.write_bytes(contents.encode())
1155                 ff(test_file, write_back=black.WriteBack.YES)
1156                 updated_contents: bytes = test_file.read_bytes()
1157                 self.assertIn(nl.encode(), updated_contents)  # type: ignore
1158                 if nl == "\n":
1159                     self.assertNotIn(b"\r\n", updated_contents)  # type: ignore
1160
1161     def test_assert_equivalent_different_asts(self) -> None:
1162         with self.assertRaises(AssertionError):
1163             black.assert_equivalent("{}", "None")
1164
1165     def test_symlink_out_of_root_directory(self) -> None:
1166         # prepare argumens
1167         path = MagicMock()
1168         root = THIS_DIR
1169         child = MagicMock()
1170         include = re.compile(black.DEFAULT_INCLUDES)
1171         exclude = re.compile(black.DEFAULT_EXCLUDES)
1172         report = black.Report()
1173
1174         # set the behavior of mock arguments
1175         # child should behave like a symlink which resolved path is clearly
1176         # outside of the root directory
1177         path.iterdir.return_value = [child]
1178         child.resolve.return_value = Path("/a/b/c")
1179         child.is_symlink.return_value = True
1180
1181         # call the method
1182         # it should not raise any error
1183         list(black.gen_python_files_in_dir(path, root, include, exclude, report))
1184
1185         # check the call of the methods of the mock objects
1186         path.iterdir.assert_called_once()
1187         child.resolve.assert_called_once()
1188         child.is_symlink.assert_called_once()
1189
1190         # set the behavior of mock arguments
1191         # child should behave like a strange file which resolved path is clearly
1192         # outside of the root directory
1193         child.is_symlink.return_value = False
1194
1195         # call the method
1196         # it should raise a ValueError
1197         with self.assertRaises(ValueError):
1198             list(black.gen_python_files_in_dir(path, root, include, exclude, report))
1199
1200         # check the call of the methods of the mock objects
1201         path.iterdir.assert_called()
1202         self.assertEqual(path.iterdir.call_count, 2)
1203         child.resolve.assert_called()
1204         self.assertEqual(child.resolve.call_count, 2)
1205         child.is_symlink.assert_called()
1206         self.assertEqual(child.is_symlink.call_count, 2)
1207
1208
1209 if __name__ == "__main__":
1210     unittest.main(module="test_black")