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.
6 from contextlib import contextmanager
7 from copy import deepcopy
8 from io import StringIO
10 from pathlib import Path
11 from platform import system
12 from subprocess import CalledProcessError
13 from tempfile import TemporaryDirectory, gettempdir
14 from typing import Any, Callable, Iterator, Tuple
15 from unittest.mock import Mock, patch
17 from click.testing import CliRunner
19 from black_primer import cli, lib
22 EXPECTED_ANALYSIS_OUTPUT = """\
23 -- primer results 📊 --
25 68 / 69 succeeded (98.55%) ✅
26 1 / 69 FAILED (1.45%) 💩
27 - 0 projects disabled by config
28 - 0 projects skipped due to Python version
29 - 0 skipped due to long checkout
39 FAKE_PROJECT_CONFIG = {
40 "cli_arguments": ["--unittest"],
41 "expect_formatting_changes": False,
42 "git_clone_url": "https://github.com/psf/black.git",
48 command: Callable[..., Any], *args: Any, **kwargs: Any
50 old_stdout, sys.stdout = sys.stdout, StringIO()
52 command(*args, **kwargs)
54 yield sys.stdout.read()
56 sys.stdout = old_stdout
60 def event_loop() -> Iterator[None]:
61 policy = asyncio.get_event_loop_policy()
62 loop = policy.new_event_loop()
63 asyncio.set_event_loop(loop)
64 if sys.platform == "win32":
65 asyncio.set_event_loop(asyncio.ProactorEventLoop())
72 async def raise_subprocess_error_1(*args: Any, **kwargs: Any) -> None:
73 raise CalledProcessError(1, ["unittest", "error"], b"", b"")
76 async def raise_subprocess_error_123(*args: Any, **kwargs: Any) -> None:
77 raise CalledProcessError(123, ["unittest", "error"], b"", b"")
80 async def return_false(*args: Any, **kwargs: Any) -> bool:
84 async def return_subproccess_output(*args: Any, **kwargs: Any) -> Tuple[bytes, bytes]:
85 return (b"stdout", b"stderr")
88 async def return_zero(*args: Any, **kwargs: Any) -> int:
92 class PrimerLibTests(unittest.TestCase):
93 def test_analyze_results(self) -> None:
94 fake_results = lib.Results(
98 "skipped_long_checkout": 0,
102 {"black": CalledProcessError(69, ["black"], b"Black didn't work", b"")},
104 with capture_stdout(lib.analyze_results, 69, fake_results) as analyze_stdout:
105 self.assertEqual(EXPECTED_ANALYSIS_OUTPUT, analyze_stdout)
108 def test_black_run(self) -> None:
109 """Pretend to run Black to ensure we cater for all scenarios"""
110 loop = asyncio.get_event_loop()
111 project_name = "unittest"
112 repo_path = Path(gettempdir())
113 project_config = deepcopy(FAKE_PROJECT_CONFIG)
114 results = lib.Results({"failed": 0, "success": 0}, {})
116 # Test a successful Black run
117 with patch("black_primer.lib._gen_check_output", return_subproccess_output):
118 loop.run_until_complete(
119 lib.black_run(project_name, repo_path, project_config, results)
121 self.assertEqual(1, results.stats["success"])
122 self.assertFalse(results.failed_projects)
124 # Test a fail based on expecting formatting changes but not getting any
125 project_config["expect_formatting_changes"] = True
126 results = lib.Results({"failed": 0, "success": 0}, {})
127 with patch("black_primer.lib._gen_check_output", return_subproccess_output):
128 loop.run_until_complete(
129 lib.black_run(project_name, repo_path, project_config, results)
131 self.assertEqual(1, results.stats["failed"])
132 self.assertTrue(results.failed_projects)
134 # Test a fail based on returning 1 and not expecting formatting changes
135 project_config["expect_formatting_changes"] = False
136 results = lib.Results({"failed": 0, "success": 0}, {})
137 with patch("black_primer.lib._gen_check_output", raise_subprocess_error_1):
138 loop.run_until_complete(
139 lib.black_run(project_name, repo_path, project_config, results)
141 self.assertEqual(1, results.stats["failed"])
142 self.assertTrue(results.failed_projects)
144 # Test a formatting error based on returning 123
145 with patch("black_primer.lib._gen_check_output", raise_subprocess_error_123):
146 loop.run_until_complete(
147 lib.black_run(project_name, repo_path, project_config, results)
149 self.assertEqual(2, results.stats["failed"])
151 def test_flatten_cli_args(self) -> None:
152 fake_long_args = ["--arg", ["really/", "|long", "|regex", "|splitup"], "--done"]
153 expected = ["--arg", "really/|long|regex|splitup", "--done"]
154 self.assertEqual(expected, lib._flatten_cli_args(fake_long_args))
157 def test_gen_check_output(self) -> None:
158 loop = asyncio.get_event_loop()
159 stdout, stderr = loop.run_until_complete(
160 lib._gen_check_output([lib.BLACK_BINARY, "--help"])
162 self.assertTrue("The uncompromising code formatter" in stdout.decode("utf8"))
163 self.assertEqual(None, stderr)
165 # TODO: Add a test to see failure works on Windows
169 false_bin = "/usr/bin/false" if system() == "Darwin" else "/bin/false"
170 with self.assertRaises(CalledProcessError):
171 loop.run_until_complete(lib._gen_check_output([false_bin]))
173 with self.assertRaises(asyncio.TimeoutError):
174 loop.run_until_complete(
175 lib._gen_check_output(["/bin/sleep", "2"], timeout=0.1)
179 def test_git_checkout_or_rebase(self) -> None:
180 loop = asyncio.get_event_loop()
181 project_config = deepcopy(FAKE_PROJECT_CONFIG)
182 work_path = Path(gettempdir())
184 expected_repo_path = work_path / "black"
185 with patch("black_primer.lib._gen_check_output", return_subproccess_output):
186 returned_repo_path = loop.run_until_complete(
187 lib.git_checkout_or_rebase(work_path, project_config)
189 self.assertEqual(expected_repo_path, returned_repo_path)
191 @patch("sys.stdout", new_callable=StringIO)
193 def test_process_queue(self, mock_stdout: Mock) -> None:
194 """Test the process queue on primer itself
195 - If you have non black conforming formatting in primer itself this can fail"""
196 loop = asyncio.get_event_loop()
197 config_path = Path(lib.__file__).parent / "primer.json"
198 with patch("black_primer.lib.git_checkout_or_rebase", return_false):
199 with TemporaryDirectory() as td:
200 return_val = loop.run_until_complete(
201 lib.process_queue(str(config_path), Path(td), 2)
203 self.assertEqual(0, return_val)
206 class PrimerCLITests(unittest.TestCase):
208 def test_async_main(self) -> None:
209 loop = asyncio.get_event_loop()
210 work_dir = Path(gettempdir()) / f"primer_ut_{getpid()}"
215 "long_checkouts": False,
217 "workdir": str(work_dir),
221 with patch("black_primer.cli.lib.process_queue", return_zero):
222 return_val = loop.run_until_complete(cli.async_main(**args)) # type: ignore
223 self.assertEqual(0, return_val)
225 def test_handle_debug(self) -> None:
226 self.assertTrue(cli._handle_debug(None, None, True))
228 def test_help_output(self) -> None:
230 result = runner.invoke(cli.main, ["--help"])
231 self.assertEqual(result.exit_code, 0)
234 if __name__ == "__main__":