X-Git-Url: https://git.madduck.net/etc/vim.git/blobdiff_plain/6fdbdb4ee301d8f9466da4da8364ac05611a1b19..1d3fb871bec82d1263940ba1d32095d660fea2ab:/tests/test_black.py?ds=inline

diff --git a/tests/test_black.py b/tests/test_black.py
index 53d1750..fdd1994 100644
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -2,25 +2,15 @@
 import asyncio
 import logging
 from concurrent.futures import ThreadPoolExecutor
-from contextlib import contextmanager, redirect_stderr
-from functools import partial, wraps
+from contextlib import contextmanager
+from functools import partial
 from io import BytesIO, TextIOWrapper
 import os
 from pathlib import Path
-import re
+import regex as re
 import sys
 from tempfile import TemporaryDirectory
-from typing import (
-    Any,
-    BinaryIO,
-    Callable,
-    Coroutine,
-    Generator,
-    List,
-    Tuple,
-    Iterator,
-    TypeVar,
-)
+from typing import Any, BinaryIO, Generator, List, Tuple, Iterator, TypeVar
 import unittest
 from unittest.mock import patch, MagicMock
 
@@ -32,16 +22,20 @@ from black import Feature, TargetVersion
 
 try:
     import blackd
-    from aiohttp.test_utils import TestClient, TestServer
+    from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
+    from aiohttp import web
 except ImportError:
     has_blackd_deps = False
 else:
     has_blackd_deps = True
 
+from pathspec import PathSpec
+
 ff = partial(black.format_file_in_place, mode=black.FileMode(), fast=True)
 fs = partial(black.format_str, mode=black.FileMode())
 THIS_FILE = Path(__file__)
 THIS_DIR = THIS_FILE.parent
+DETERMINISTIC_HEADER = "[Deterministic header]"
 EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)"
 PY36_ARGS = [
     f"--target-version={version.name.lower()}" for version in black.PY36_VERSIONS
@@ -90,25 +84,25 @@ def cache_dir(exists: bool = True) -> Iterator[Path]:
 @contextmanager
 def event_loop(close: bool) -> Iterator[None]:
     policy = asyncio.get_event_loop_policy()
-    old_loop = policy.get_event_loop()
     loop = policy.new_event_loop()
     asyncio.set_event_loop(loop)
     try:
         yield
 
     finally:
-        policy.set_event_loop(old_loop)
         if close:
             loop.close()
 
 
-def async_test(f: Callable[..., Coroutine[Any, None, R]]) -> Callable[..., None]:
-    @event_loop(close=True)
-    @wraps(f)
-    def wrapper(*args: Any, **kwargs: Any) -> None:
-        asyncio.get_event_loop().run_until_complete(f(*args, **kwargs))
-
-    return wrapper
+@contextmanager
+def skip_if_exception(e: str) -> Iterator[None]:
+    try:
+        yield
+    except Exception as exc:
+        if exc.__class__.__name__ == e:
+            unittest.skip(f"Encountered expected exception {exc}, skipping")
+        else:
+            raise
 
 
 class BlackRunner(CliRunner):
@@ -167,6 +161,16 @@ class BlackTestCase(unittest.TestCase):
         result = runner.invoke(black.main, args)
         self.assertEqual(result.exit_code, exit_code, msg=runner.stderr_bytes.decode())
 
+    @patch("black.dump_to_file", dump_to_stderr)
+    def checkSourceFile(self, name: str) -> None:
+        path = THIS_DIR.parent / name
+        source, expected = read_data(str(path), data=False)
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
+        self.assertFalse(ff(path))
+
     @patch("black.dump_to_file", dump_to_stderr)
     def test_empty(self) -> None:
         source = expected = ""
@@ -186,23 +190,44 @@ class BlackTestCase(unittest.TestCase):
             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", data=False)
-        actual = fs(source)
-        self.assertFormatEqual(expected, actual)
-        black.assert_equivalent(source, actual)
-        black.assert_stable(source, actual, black.FileMode())
-        self.assertFalse(ff(THIS_FILE))
+        self.checkSourceFile("tests/test_black.py")
 
-    @patch("black.dump_to_file", dump_to_stderr)
     def test_black(self) -> None:
-        source, expected = read_data("../black", data=False)
-        actual = fs(source)
-        self.assertFormatEqual(expected, actual)
-        black.assert_equivalent(source, actual)
-        black.assert_stable(source, actual, black.FileMode())
-        self.assertFalse(ff(THIS_DIR / ".." / "black.py"))
+        self.checkSourceFile("black.py")
+
+    def test_pygram(self) -> None:
+        self.checkSourceFile("blib2to3/pygram.py")
+
+    def test_pytree(self) -> None:
+        self.checkSourceFile("blib2to3/pytree.py")
+
+    def test_conv(self) -> None:
+        self.checkSourceFile("blib2to3/pgen2/conv.py")
+
+    def test_driver(self) -> None:
+        self.checkSourceFile("blib2to3/pgen2/driver.py")
+
+    def test_grammar(self) -> None:
+        self.checkSourceFile("blib2to3/pgen2/grammar.py")
+
+    def test_literals(self) -> None:
+        self.checkSourceFile("blib2to3/pgen2/literals.py")
+
+    def test_parse(self) -> None:
+        self.checkSourceFile("blib2to3/pgen2/parse.py")
+
+    def test_pgen(self) -> None:
+        self.checkSourceFile("blib2to3/pgen2/pgen.py")
+
+    def test_tokenize(self) -> None:
+        self.checkSourceFile("blib2to3/pgen2/tokenize.py")
+
+    def test_token(self) -> None:
+        self.checkSourceFile("blib2to3/pgen2/token.py")
+
+    def test_setup(self) -> None:
+        self.checkSourceFile("setup.py")
 
     def test_piping(self) -> None:
         source, expected = read_data("../black", data=False)
@@ -218,8 +243,8 @@ class BlackTestCase(unittest.TestCase):
 
     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"
+            rf"(STDIN|STDOUT)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d "
+            rf"\+\d\d\d\d"
         )
         source, _ = read_data("expression.py")
         expected, _ = read_data("expression.diff")
@@ -235,30 +260,29 @@ class BlackTestCase(unittest.TestCase):
             black.main, args, input=BytesIO(source.encode("utf8"))
         )
         self.assertEqual(result.exit_code, 0)
-        actual = diff_header.sub("[Deterministic header]", result.output)
+        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", data=False)
+    def test_function(self) -> None:
+        source, expected = read_data("function")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, black.FileMode())
-        self.assertFalse(ff(THIS_DIR / ".." / "setup.py"))
 
     @patch("black.dump_to_file", dump_to_stderr)
-    def test_function(self) -> None:
-        source, expected = read_data("function")
+    def test_function2(self) -> None:
+        source, expected = read_data("function2")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, black.FileMode())
 
     @patch("black.dump_to_file", dump_to_stderr)
-    def test_function2(self) -> None:
-        source, expected = read_data("function2")
+    def test_function_trailing_comma(self) -> None:
+        source, expected = read_data("function_trailing_comma")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
         black.assert_equivalent(source, actual)
@@ -272,6 +296,23 @@ class BlackTestCase(unittest.TestCase):
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, black.FileMode())
 
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_pep_572(self) -> None:
+        source, expected = read_data("pep_572")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_stable(source, actual, black.FileMode())
+        if sys.version_info >= (3, 8):
+            black.assert_equivalent(source, actual)
+
+    def test_pep_572_version_detection(self) -> None:
+        source, _ = read_data("pep_572")
+        root = black.lib2to3_parse(source)
+        features = black.get_features_used(root)
+        self.assertIn(black.Feature.ASSIGNMENT_EXPRESSIONS, features)
+        versions = black.detect_target_versions(root)
+        self.assertIn(black.TargetVersion.PY38, versions)
+
     def test_expression_ff(self) -> None:
         source, expected = read_data("expression")
         tmp_file = Path(black.dump_to_file(source))
@@ -292,7 +333,7 @@ class BlackTestCase(unittest.TestCase):
         tmp_file = Path(black.dump_to_file(source))
         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"
+            r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
         )
         try:
             result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
@@ -300,14 +341,14 @@ class BlackTestCase(unittest.TestCase):
         finally:
             os.unlink(tmp_file)
         actual = result.output
-        actual = diff_header.sub("[Deterministic header]", actual)
+        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)
             msg = (
-                f"Expected diff isn't equal to the actual. If you made changes "
-                f"to expression.py and this is an anticipated difference, "
-                f"overwrite tests/data/expression.diff with {dump}"
+                "Expected diff isn't equal to the actual. If you made changes to"
+                " expression.py and this is an anticipated difference, overwrite"
+                f" tests/data/expression.diff with {dump}"
             )
             self.assertEqual(expected, actual, msg)
 
@@ -319,6 +360,23 @@ class BlackTestCase(unittest.TestCase):
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, black.FileMode())
 
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_pep_570(self) -> None:
+        source, expected = read_data("pep_570")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_stable(source, actual, black.FileMode())
+        if sys.version_info >= (3, 8):
+            black.assert_equivalent(source, actual)
+
+    def test_detect_pos_only_arguments(self) -> None:
+        source, _ = read_data("pep_570")
+        root = black.lib2to3_parse(source)
+        features = black.get_features_used(root)
+        self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
+        versions = black.detect_target_versions(root)
+        self.assertIn(black.TargetVersion.PY38, versions)
+
     @patch("black.dump_to_file", dump_to_stderr)
     def test_string_quotes(self) -> None:
         source, expected = read_data("string_quotes")
@@ -328,10 +386,44 @@ class BlackTestCase(unittest.TestCase):
         black.assert_stable(source, actual, black.FileMode())
         mode = black.FileMode(string_normalization=False)
         not_normalized = fs(source, mode=mode)
-        self.assertFormatEqual(source, not_normalized)
+        self.assertFormatEqual(source.replace("\\\n", ""), not_normalized)
         black.assert_equivalent(source, not_normalized)
         black.assert_stable(source, not_normalized, mode=mode)
 
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_docstring(self) -> None:
+        source, expected = read_data("docstring")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
+
+    def test_long_strings(self) -> None:
+        """Tests for splitting long strings."""
+        source, expected = read_data("long_strings")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
+
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_long_strings__edge_case(self) -> None:
+        """Edge-case tests for splitting long strings."""
+        source, expected = read_data("long_strings__edge_case")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
+
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_long_strings__regression(self) -> None:
+        """Regression tests for splitting long strings."""
+        source, expected = read_data("long_strings__regression")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
+
     @patch("black.dump_to_file", dump_to_stderr)
     def test_slices(self) -> None:
         source, expected = read_data("slices")
@@ -436,6 +528,14 @@ class BlackTestCase(unittest.TestCase):
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, black.FileMode())
 
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_remove_parens(self) -> None:
+        source, expected = read_data("remove_parens")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
+
     @patch("black.dump_to_file", dump_to_stderr)
     def test_string_prefixes(self) -> None:
         source, expected = read_data("string_prefixes")
@@ -474,7 +574,7 @@ class BlackTestCase(unittest.TestCase):
         source, expected = read_data("python2")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
-        # black.assert_equivalent(source, actual)
+        black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, black.FileMode())
 
     @patch("black.dump_to_file", dump_to_stderr)
@@ -483,6 +583,7 @@ class BlackTestCase(unittest.TestCase):
         mode = black.FileMode(target_versions={TargetVersion.PY27})
         actual = fs(source, mode=mode)
         self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, mode)
 
     @patch("black.dump_to_file", dump_to_stderr)
@@ -490,6 +591,7 @@ class BlackTestCase(unittest.TestCase):
         source, expected = read_data("python2_unicode_literals")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, black.FileMode())
 
     @patch("black.dump_to_file", dump_to_stderr)
@@ -500,8 +602,24 @@ class BlackTestCase(unittest.TestCase):
         self.assertFormatEqual(expected, actual)
         black.assert_stable(source, actual, mode)
 
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_async_as_identifier(self) -> None:
+        source_path = (THIS_DIR / "data" / "async_as_identifier.py").resolve()
+        source, expected = read_data("async_as_identifier")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        major, minor = sys.version_info[:2]
+        if major < 3 or (major <= 3 and minor < 7):
+            black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
+        # ensure black can parse this when the target is 3.6
+        self.invokeBlack([str(source_path), "--target-version", "py36"])
+        # but not on 3.7, because async/await is no longer an identifier
+        self.invokeBlack([str(source_path), "--target-version", "py37"], exit_code=123)
+
     @patch("black.dump_to_file", dump_to_stderr)
     def test_python37(self) -> None:
+        source_path = (THIS_DIR / "data" / "python37.py").resolve()
         source, expected = read_data("python37")
         actual = fs(source)
         self.assertFormatEqual(expected, actual)
@@ -509,6 +627,20 @@ class BlackTestCase(unittest.TestCase):
         if major > 3 or (major == 3 and minor >= 7):
             black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, black.FileMode())
+        # ensure black can parse this when the target is 3.7
+        self.invokeBlack([str(source_path), "--target-version", "py37"])
+        # but not on 3.6, because we use async as a reserved keyword
+        self.invokeBlack([str(source_path), "--target-version", "py36"], exit_code=123)
+
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_python38(self) -> None:
+        source, expected = read_data("python38")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        major, minor = sys.version_info[:2]
+        if major > 3 or (major == 3 and minor >= 8):
+            black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
 
     @patch("black.dump_to_file", dump_to_stderr)
     def test_fmtonoff(self) -> None:
@@ -526,6 +658,22 @@ class BlackTestCase(unittest.TestCase):
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, black.FileMode())
 
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_fmtonoff3(self) -> None:
+        source, expected = read_data("fmtonoff3")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
+
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_fmtonoff4(self) -> None:
+        source, expected = read_data("fmtonoff4")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
+
     @patch("black.dump_to_file", dump_to_stderr)
     def test_remove_empty_parentheses_after_class(self) -> None:
         source, expected = read_data("class_blank_parentheses")
@@ -558,6 +706,14 @@ class BlackTestCase(unittest.TestCase):
         black.assert_equivalent(source, actual)
         black.assert_stable(source, actual, black.FileMode())
 
+    @patch("black.dump_to_file", dump_to_stderr)
+    def test_beginning_backslash(self) -> None:
+        source, expected = read_data("beginning_backslash")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
+
     def test_tab_comment_indentation(self) -> None:
         contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n"
         contents_spc = "if 1:\n    if 2:\n        pass\n    # comment\n    pass\n"
@@ -624,8 +780,8 @@ class BlackTestCase(unittest.TestCase):
             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.",
+                "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)
@@ -634,8 +790,8 @@ class BlackTestCase(unittest.TestCase):
             self.assertEqual(out_lines[-1], "reformatted f3")
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files reformatted, 2 files left unchanged, "
-                "1 file failed to reformat.",
+                "2 files reformatted, 2 files left unchanged, 1 file failed to"
+                " reformat.",
             )
             self.assertEqual(report.return_code, 123)
             report.failed(Path("e2"), "boom")
@@ -644,8 +800,8 @@ class BlackTestCase(unittest.TestCase):
             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.",
+                "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")
@@ -654,8 +810,8 @@ class BlackTestCase(unittest.TestCase):
             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.",
+                "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)
@@ -664,15 +820,22 @@ class BlackTestCase(unittest.TestCase):
             self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files reformatted, 3 files left unchanged, "
-                "2 files failed to reformat.",
+                "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.",
+                "2 files would be reformatted, 3 files would be left unchanged, 2 files"
+                " would fail to reformat.",
+            )
+            report.check = False
+            report.diff = 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_quiet(self) -> None:
@@ -714,8 +877,8 @@ class BlackTestCase(unittest.TestCase):
             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.",
+                "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)
@@ -723,8 +886,8 @@ class BlackTestCase(unittest.TestCase):
             self.assertEqual(len(err_lines), 1)
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files reformatted, 2 files left unchanged, "
-                "1 file failed to reformat.",
+                "2 files reformatted, 2 files left unchanged, 1 file failed to"
+                " reformat.",
             )
             self.assertEqual(report.return_code, 123)
             report.failed(Path("e2"), "boom")
@@ -733,8 +896,8 @@ class BlackTestCase(unittest.TestCase):
             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.",
+                "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")
@@ -742,8 +905,8 @@ class BlackTestCase(unittest.TestCase):
             self.assertEqual(len(err_lines), 2)
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files reformatted, 2 files left unchanged, "
-                "2 files failed to reformat.",
+                "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)
@@ -751,15 +914,22 @@ class BlackTestCase(unittest.TestCase):
             self.assertEqual(len(err_lines), 2)
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files reformatted, 3 files left unchanged, "
-                "2 files failed to reformat.",
+                "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.",
+                "2 files would be reformatted, 3 files would be left unchanged, 2 files"
+                " would fail to reformat.",
+            )
+            report.check = False
+            report.diff = 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:
@@ -803,8 +973,8 @@ class BlackTestCase(unittest.TestCase):
             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.",
+                "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)
@@ -813,8 +983,8 @@ class BlackTestCase(unittest.TestCase):
             self.assertEqual(out_lines[-1], "reformatted f3")
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files reformatted, 2 files left unchanged, "
-                "1 file failed to reformat.",
+                "2 files reformatted, 2 files left unchanged, 1 file failed to"
+                " reformat.",
             )
             self.assertEqual(report.return_code, 123)
             report.failed(Path("e2"), "boom")
@@ -823,8 +993,8 @@ class BlackTestCase(unittest.TestCase):
             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.",
+                "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")
@@ -832,8 +1002,8 @@ class BlackTestCase(unittest.TestCase):
             self.assertEqual(len(err_lines), 2)
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files reformatted, 2 files left unchanged, "
-                "2 files failed to reformat.",
+                "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)
@@ -841,15 +1011,22 @@ class BlackTestCase(unittest.TestCase):
             self.assertEqual(len(err_lines), 2)
             self.assertEqual(
                 unstyle(str(report)),
-                "2 files reformatted, 3 files left unchanged, "
-                "2 files failed to reformat.",
+                "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.",
+                "2 files would be reformatted, 3 files would be left unchanged, 2 files"
+                " would fail to reformat.",
+            )
+            report.check = False
+            report.diff = 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_lib2to3_parse(self) -> None:
@@ -970,10 +1147,10 @@ class BlackTestCase(unittest.TestCase):
         just_nl = "\n"
         with self.assertRaises(black.NothingChanged):
             black.format_file_contents(just_nl, mode=mode, fast=False)
-        same = "l = [1, 2, 3]\n"
+        same = "j = [1, 2, 3]\n"
         with self.assertRaises(black.NothingChanged):
             black.format_file_contents(same, mode=mode, fast=False)
-        different = "l = [1,2,3]"
+        different = "j = [1,2,3]"
         expected = same
         actual = black.format_file_contents(different, mode=mode, fast=False)
         self.assertEqual(expected, actual)
@@ -1001,7 +1178,7 @@ class BlackTestCase(unittest.TestCase):
 
         with patch("black.out", out), patch("black.err", err):
             with self.assertRaises(AssertionError):
-                self.assertFormatEqual("l = [1, 2, 3]", "l = [1, 2, 3,]")
+                self.assertFormatEqual("j = [1, 2, 3]", "j = [1, 2, 3,]")
 
         out_str = "".join(out_lines)
         self.assertTrue("Expected tree:" in out_str)
@@ -1174,6 +1351,13 @@ class BlackTestCase(unittest.TestCase):
             two = black.read_cache(short_mode)
             self.assertNotIn(path, two)
 
+    def test_tricky_unicode_symbols(self) -> None:
+        source, expected = read_data("tricky_unicode_symbols")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
+
     def test_single_file_force_pyi(self) -> None:
         reg_mode = black.FileMode()
         pyi_mode = black.FileMode(is_pyi=True)
@@ -1269,6 +1453,13 @@ class BlackTestCase(unittest.TestCase):
                 self.assertIn(path, pyi_cache)
                 self.assertNotIn(path, normal_cache)
 
+    def test_collections(self) -> None:
+        source, expected = read_data("collections")
+        actual = fs(source)
+        self.assertFormatEqual(expected, actual)
+        black.assert_equivalent(source, actual)
+        black.assert_stable(source, actual, black.FileMode())
+
     def test_pipe_force_py36(self) -> None:
         source, expected = read_data("force_py36")
         result = CliRunner().invoke(
@@ -1285,6 +1476,7 @@ class BlackTestCase(unittest.TestCase):
         include = re.compile(r"\.pyi?$")
         exclude = re.compile(r"/exclude/|/\.definitely_exclude/")
         report = black.Report()
+        gitignore = PathSpec.from_lines("gitwildmatch", [])
         sources: List[Path] = []
         expected = [
             Path(path / "b/dont_exclude/a.py"),
@@ -1292,13 +1484,37 @@ class BlackTestCase(unittest.TestCase):
         ]
         this_abs = THIS_DIR.resolve()
         sources.extend(
-            black.gen_python_files_in_dir(path, this_abs, include, exclude, report)
+            black.gen_python_files_in_dir(
+                path, this_abs, include, exclude, report, gitignore
+            )
+        )
+        self.assertEqual(sorted(expected), sorted(sources))
+
+    def test_gitignore_exclude(self) -> None:
+        path = THIS_DIR / "data" / "include_exclude_tests"
+        include = re.compile(r"\.pyi?$")
+        exclude = re.compile(r"")
+        report = black.Report()
+        gitignore = PathSpec.from_lines(
+            "gitwildmatch", ["exclude/", ".definitely_exclude"]
+        )
+        sources: List[Path] = []
+        expected = [
+            Path(path / "b/dont_exclude/a.py"),
+            Path(path / "b/dont_exclude/a.pyi"),
+        ]
+        this_abs = THIS_DIR.resolve()
+        sources.extend(
+            black.gen_python_files_in_dir(
+                path, this_abs, include, exclude, report, gitignore
+            )
         )
         self.assertEqual(sorted(expected), sorted(sources))
 
     def test_empty_include(self) -> None:
         path = THIS_DIR / "data" / "include_exclude_tests"
         report = black.Report()
+        gitignore = PathSpec.from_lines("gitwildmatch", [])
         empty = re.compile(r"")
         sources: List[Path] = []
         expected = [
@@ -1315,7 +1531,12 @@ class BlackTestCase(unittest.TestCase):
         this_abs = THIS_DIR.resolve()
         sources.extend(
             black.gen_python_files_in_dir(
-                path, this_abs, empty, re.compile(black.DEFAULT_EXCLUDES), report
+                path,
+                this_abs,
+                empty,
+                re.compile(black.DEFAULT_EXCLUDES),
+                report,
+                gitignore,
             )
         )
         self.assertEqual(sorted(expected), sorted(sources))
@@ -1323,6 +1544,7 @@ class BlackTestCase(unittest.TestCase):
     def test_empty_exclude(self) -> None:
         path = THIS_DIR / "data" / "include_exclude_tests"
         report = black.Report()
+        gitignore = PathSpec.from_lines("gitwildmatch", [])
         empty = re.compile(r"")
         sources: List[Path] = []
         expected = [
@@ -1336,7 +1558,12 @@ class BlackTestCase(unittest.TestCase):
         this_abs = THIS_DIR.resolve()
         sources.extend(
             black.gen_python_files_in_dir(
-                path, this_abs, re.compile(black.DEFAULT_INCLUDES), empty, report
+                path,
+                this_abs,
+                re.compile(black.DEFAULT_INCLUDES),
+                empty,
+                report,
+                gitignore,
             )
         )
         self.assertEqual(sorted(expected), sorted(sources))
@@ -1381,13 +1608,19 @@ class BlackTestCase(unittest.TestCase):
         include = re.compile(black.DEFAULT_INCLUDES)
         exclude = re.compile(black.DEFAULT_EXCLUDES)
         report = black.Report()
+        gitignore = PathSpec.from_lines("gitwildmatch", [])
         # `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.as_posix.return_value = "/a/b/c"
         child.is_symlink.return_value = True
         try:
-            list(black.gen_python_files_in_dir(path, root, include, exclude, report))
+            list(
+                black.gen_python_files_in_dir(
+                    path, root, include, exclude, report, gitignore
+                )
+            )
         except ValueError as ve:
             self.fail(f"`get_python_files_in_dir()` failed: {ve}")
         path.iterdir.assert_called_once()
@@ -1397,7 +1630,11 @@ class BlackTestCase(unittest.TestCase):
         # 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))
+            list(
+                black.gen_python_files_in_dir(
+                    path, root, include, exclude, report, gitignore
+                )
+            )
         path.iterdir.assert_called()
         self.assertEqual(path.iterdir.call_count, 2)
         child.resolve.assert_called()
@@ -1443,97 +1680,132 @@ class BlackTestCase(unittest.TestCase):
             ff(THIS_FILE)
 
     @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
-    @async_test
+    def test_blackd_main(self) -> None:
+        with patch("blackd.web.run_app"):
+            result = CliRunner().invoke(blackd.main, [])
+            if result.exception is not None:
+                raise result.exception
+            self.assertEqual(result.exit_code, 0)
+
+    def test_invalid_config_return_code(self) -> None:
+        tmp_file = Path(black.dump_to_file())
+        try:
+            tmp_config = Path(black.dump_to_file())
+            tmp_config.unlink()
+            args = ["--config", str(tmp_config), str(tmp_file)]
+            self.invokeBlack(args, exit_code=2, ignore_config=False)
+        finally:
+            tmp_file.unlink()
+
+
+class BlackDTestCase(AioHTTPTestCase):
+    async def get_application(self) -> web.Application:
+        return blackd.make_app()
+
+    # TODO: remove these decorators once the below is released
+    # https://github.com/aio-libs/aiohttp/pull/3727
+    @skip_if_exception("ClientOSError")
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @unittest_run_loop
     async def test_blackd_request_needs_formatting(self) -> None:
-        app = blackd.make_app()
-        async with TestClient(TestServer(app)) as client:
-            response = await client.post("/", data=b"print('hello world')")
-            self.assertEqual(response.status, 200)
-            self.assertEqual(response.charset, "utf8")
-            self.assertEqual(await response.read(), b'print("hello world")\n')
+        response = await self.client.post("/", data=b"print('hello world')")
+        self.assertEqual(response.status, 200)
+        self.assertEqual(response.charset, "utf8")
+        self.assertEqual(await response.read(), b'print("hello world")\n')
 
+    @skip_if_exception("ClientOSError")
     @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
-    @async_test
+    @unittest_run_loop
     async def test_blackd_request_no_change(self) -> None:
-        app = blackd.make_app()
-        async with TestClient(TestServer(app)) as client:
-            response = await client.post("/", data=b'print("hello world")\n')
-            self.assertEqual(response.status, 204)
-            self.assertEqual(await response.read(), b"")
+        response = await self.client.post("/", data=b'print("hello world")\n')
+        self.assertEqual(response.status, 204)
+        self.assertEqual(await response.read(), b"")
 
+    @skip_if_exception("ClientOSError")
     @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
-    @async_test
+    @unittest_run_loop
     async def test_blackd_request_syntax_error(self) -> None:
-        app = blackd.make_app()
-        async with TestClient(TestServer(app)) as client:
-            response = await client.post("/", data=b"what even ( is")
-            self.assertEqual(response.status, 400)
-            content = await response.text()
-            self.assertTrue(
-                content.startswith("Cannot parse"),
-                msg=f"Expected error to start with 'Cannot parse', got {repr(content)}",
-            )
+        response = await self.client.post("/", data=b"what even ( is")
+        self.assertEqual(response.status, 400)
+        content = await response.text()
+        self.assertTrue(
+            content.startswith("Cannot parse"),
+            msg=f"Expected error to start with 'Cannot parse', got {repr(content)}",
+        )
 
+    @skip_if_exception("ClientOSError")
     @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
-    @async_test
+    @unittest_run_loop
     async def test_blackd_unsupported_version(self) -> None:
-        app = blackd.make_app()
-        async with TestClient(TestServer(app)) as client:
-            response = await client.post(
-                "/", data=b"what", headers={blackd.VERSION_HEADER: "2"}
-            )
-            self.assertEqual(response.status, 501)
+        response = await self.client.post(
+            "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "2"}
+        )
+        self.assertEqual(response.status, 501)
 
+    @skip_if_exception("ClientOSError")
     @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
-    @async_test
+    @unittest_run_loop
     async def test_blackd_supported_version(self) -> None:
-        app = blackd.make_app()
-        async with TestClient(TestServer(app)) as client:
-            response = await client.post(
-                "/", data=b"what", headers={blackd.VERSION_HEADER: "1"}
-            )
-            self.assertEqual(response.status, 200)
+        response = await self.client.post(
+            "/", data=b"what", headers={blackd.PROTOCOL_VERSION_HEADER: "1"}
+        )
+        self.assertEqual(response.status, 200)
 
+    @skip_if_exception("ClientOSError")
     @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
-    @async_test
+    @unittest_run_loop
     async def test_blackd_invalid_python_variant(self) -> None:
-        app = blackd.make_app()
-        async with TestClient(TestServer(app)) as client:
-
-            async def check(header_value: str, expected_status: int = 400) -> None:
-                response = await client.post(
-                    "/",
-                    data=b"what",
-                    headers={blackd.PYTHON_VARIANT_HEADER: header_value},
-                )
-                self.assertEqual(response.status, expected_status)
-
-            await check("lol")
-            await check("ruby3.5")
-            await check("pyi3.6")
-            await check("py1.5")
-            await check("2.8")
-            await check("py2.8")
-            await check("3.0")
-            await check("pypy3.0")
-            await check("jython3.4")
-
+        async def check(header_value: str, expected_status: int = 400) -> None:
+            response = await self.client.post(
+                "/", data=b"what", headers={blackd.PYTHON_VARIANT_HEADER: header_value}
+            )
+            self.assertEqual(response.status, expected_status)
+
+        await check("lol")
+        await check("ruby3.5")
+        await check("pyi3.6")
+        await check("py1.5")
+        await check("2.8")
+        await check("py2.8")
+        await check("3.0")
+        await check("pypy3.0")
+        await check("jython3.4")
+
+    @skip_if_exception("ClientOSError")
     @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
-    @async_test
+    @unittest_run_loop
     async def test_blackd_pyi(self) -> None:
-        app = blackd.make_app()
-        async with TestClient(TestServer(app)) as client:
-            source, expected = read_data("stub.pyi")
-            response = await client.post(
-                "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"}
-            )
-            self.assertEqual(response.status, 200)
-            self.assertEqual(await response.text(), expected)
+        source, expected = read_data("stub.pyi")
+        response = await self.client.post(
+            "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"}
+        )
+        self.assertEqual(response.status, 200)
+        self.assertEqual(await response.text(), expected)
+
+    @skip_if_exception("ClientOSError")
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @unittest_run_loop
+    async def test_blackd_diff(self) -> None:
+        diff_header = re.compile(
+            rf"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
+        )
+
+        source, _ = read_data("blackd_diff.py")
+        expected, _ = read_data("blackd_diff.diff")
+
+        response = await self.client.post(
+            "/", data=source, headers={blackd.DIFF_HEADER: "true"}
+        )
+        self.assertEqual(response.status, 200)
+
+        actual = await response.text()
+        actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
+        self.assertEqual(actual, expected)
 
+    @skip_if_exception("ClientOSError")
     @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
-    @async_test
+    @unittest_run_loop
     async def test_blackd_python_variant(self) -> None:
-        app = blackd.make_app()
         code = (
             "def f(\n"
             "    and_has_a_bunch_of,\n"
@@ -1543,68 +1815,55 @@ class BlackTestCase(unittest.TestCase):
             "):\n"
             "    pass\n"
         )
-        async with TestClient(TestServer(app)) as client:
-
-            async def check(header_value: str, expected_status: int) -> None:
-                response = await client.post(
-                    "/", data=code, headers={blackd.PYTHON_VARIANT_HEADER: header_value}
-                )
-                self.assertEqual(response.status, expected_status)
-
-            await check("3.6", 200)
-            await check("py3.6", 200)
-            await check("3.6,3.7", 200)
-            await check("3.6,py3.7", 200)
 
-            await check("2", 204)
-            await check("2.7", 204)
-            await check("py2.7", 204)
-            await check("3.4", 204)
-            await check("py3.4", 204)
-
-    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
-    @async_test
-    async def test_blackd_fast(self) -> None:
-        with open(os.devnull, "w") as dn, redirect_stderr(dn):
-            app = blackd.make_app()
-            async with TestClient(TestServer(app)) as client:
-                response = await client.post("/", data=b"ur'hello'")
-                self.assertEqual(response.status, 500)
-                self.assertIn("failed to parse source file", await response.text())
-                response = await client.post(
-                    "/", data=b"ur'hello'", headers={blackd.FAST_OR_SAFE_HEADER: "fast"}
-                )
-                self.assertEqual(response.status, 200)
+        async def check(header_value: str, expected_status: int) -> None:
+            response = await self.client.post(
+                "/", data=code, headers={blackd.PYTHON_VARIANT_HEADER: header_value}
+            )
+            self.assertEqual(
+                response.status, expected_status, msg=await response.text()
+            )
 
+        await check("3.6", 200)
+        await check("py3.6", 200)
+        await check("3.6,3.7", 200)
+        await check("3.6,py3.7", 200)
+        await check("py36,py37", 200)
+        await check("36", 200)
+        await check("3.6.4", 200)
+
+        await check("2", 204)
+        await check("2.7", 204)
+        await check("py2.7", 204)
+        await check("3.4", 204)
+        await check("py3.4", 204)
+        await check("py34,py36", 204)
+        await check("34", 204)
+
+    @skip_if_exception("ClientOSError")
     @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
-    @async_test
+    @unittest_run_loop
     async def test_blackd_line_length(self) -> None:
-        app = blackd.make_app()
-        async with TestClient(TestServer(app)) as client:
-            response = await client.post(
-                "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"}
-            )
-            self.assertEqual(response.status, 200)
+        response = await self.client.post(
+            "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"}
+        )
+        self.assertEqual(response.status, 200)
 
+    @skip_if_exception("ClientOSError")
     @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
-    @async_test
+    @unittest_run_loop
     async def test_blackd_invalid_line_length(self) -> None:
-        app = blackd.make_app()
-        async with TestClient(TestServer(app)) as client:
-            response = await client.post(
-                "/",
-                data=b'print("hello")\n',
-                headers={blackd.LINE_LENGTH_HEADER: "NaN"},
-            )
-            self.assertEqual(response.status, 400)
+        response = await self.client.post(
+            "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "NaN"}
+        )
+        self.assertEqual(response.status, 400)
 
+    @skip_if_exception("ClientOSError")
     @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
-    def test_blackd_main(self) -> None:
-        with patch("blackd.web.run_app"):
-            result = CliRunner().invoke(blackd.main, [])
-            if result.exception is not None:
-                raise result.exception
-            self.assertEqual(result.exit_code, 0)
+    @unittest_run_loop
+    async def test_blackd_response_black_version_header(self) -> None:
+        response = await self.client.post("/")
+        self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER))
 
 
 if __name__ == "__main__":