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, List, Tuple, TypeVar
15 from unittest.mock import Mock, patch
17 from click.testing import CliRunner
19 from black_primer import cli, lib
22 EXPECTED_ANALYSIS_OUTPUT = """\
31 -- primer results 📊 --
33 68 / 69 succeeded (98.55%) ✅
34 1 / 69 FAILED (1.45%) 💩
35 - 0 projects disabled by config
36 - 0 projects skipped due to Python version
37 - 0 skipped due to long checkout
39 Failed projects: black
42 FAKE_PROJECT_CONFIG = {
43 "cli_arguments": ["--unittest"],
44 "expect_formatting_changes": False,
45 "git_clone_url": "https://github.com/psf/black.git",
51 command: Callable[..., Any], *args: Any, **kwargs: Any
53 old_stdout, sys.stdout = sys.stdout, StringIO()
55 command(*args, **kwargs)
57 yield sys.stdout.read()
59 sys.stdout = old_stdout
63 def event_loop() -> Iterator[None]:
64 policy = asyncio.get_event_loop_policy()
65 loop = policy.new_event_loop()
66 asyncio.set_event_loop(loop)
67 if sys.platform == "win32":
68 asyncio.set_event_loop(asyncio.ProactorEventLoop())
75 async def raise_subprocess_error_1(*args: Any, **kwargs: Any) -> None:
76 raise CalledProcessError(1, ["unittest", "error"], b"", b"")
79 async def raise_subprocess_error_123(*args: Any, **kwargs: Any) -> None:
80 raise CalledProcessError(123, ["unittest", "error"], b"", b"")
83 async def return_false(*args: Any, **kwargs: Any) -> bool:
87 async def return_subproccess_output(*args: Any, **kwargs: Any) -> Tuple[bytes, bytes]:
88 return (b"stdout", b"stderr")
91 async def return_zero(*args: Any, **kwargs: Any) -> int:
95 if sys.version_info >= (3, 9):
103 def collect(queue: Q) -> List[T]:
107 item = queue.get_nowait()
109 except asyncio.QueueEmpty:
113 class PrimerLibTests(unittest.TestCase):
114 def test_analyze_results(self) -> None:
115 fake_results = lib.Results(
119 "skipped_long_checkout": 0,
123 {"black": CalledProcessError(69, ["black"], b"Black didn't work", b"")},
125 with capture_stdout(lib.analyze_results, 69, fake_results) as analyze_stdout:
126 self.assertEqual(EXPECTED_ANALYSIS_OUTPUT, analyze_stdout)
129 def test_black_run(self) -> None:
130 """Pretend to run Black to ensure we cater for all scenarios"""
131 loop = asyncio.get_event_loop()
132 project_name = "unittest"
133 repo_path = Path(gettempdir())
134 project_config = deepcopy(FAKE_PROJECT_CONFIG)
135 results = lib.Results({"failed": 0, "success": 0}, {})
137 # Test a successful Black run
138 with patch("black_primer.lib._gen_check_output", return_subproccess_output):
139 loop.run_until_complete(
140 lib.black_run(project_name, repo_path, project_config, results)
142 self.assertEqual(1, results.stats["success"])
143 self.assertFalse(results.failed_projects)
145 # Test a fail based on expecting formatting changes but not getting any
146 project_config["expect_formatting_changes"] = True
147 results = lib.Results({"failed": 0, "success": 0}, {})
148 with patch("black_primer.lib._gen_check_output", return_subproccess_output):
149 loop.run_until_complete(
150 lib.black_run(project_name, repo_path, project_config, results)
152 self.assertEqual(1, results.stats["failed"])
153 self.assertTrue(results.failed_projects)
155 # Test a fail based on returning 1 and not expecting formatting changes
156 project_config["expect_formatting_changes"] = False
157 results = lib.Results({"failed": 0, "success": 0}, {})
158 with patch("black_primer.lib._gen_check_output", raise_subprocess_error_1):
159 loop.run_until_complete(
160 lib.black_run(project_name, repo_path, project_config, results)
162 self.assertEqual(1, results.stats["failed"])
163 self.assertTrue(results.failed_projects)
165 # Test a formatting error based on returning 123
166 with patch("black_primer.lib._gen_check_output", raise_subprocess_error_123):
167 loop.run_until_complete(
168 lib.black_run(project_name, repo_path, project_config, results)
170 self.assertEqual(2, results.stats["failed"])
172 def test_flatten_cli_args(self) -> None:
173 fake_long_args = ["--arg", ["really/", "|long", "|regex", "|splitup"], "--done"]
174 expected = ["--arg", "really/|long|regex|splitup", "--done"]
175 self.assertEqual(expected, lib._flatten_cli_args(fake_long_args))
178 def test_gen_check_output(self) -> None:
179 loop = asyncio.get_event_loop()
180 stdout, stderr = loop.run_until_complete(
181 lib._gen_check_output([lib.BLACK_BINARY, "--help"])
183 self.assertTrue("The uncompromising code formatter" in stdout.decode("utf8"))
184 self.assertEqual(None, stderr)
186 # TODO: Add a test to see failure works on Windows
190 false_bin = "/usr/bin/false" if system() == "Darwin" else "/bin/false"
191 with self.assertRaises(CalledProcessError):
192 loop.run_until_complete(lib._gen_check_output([false_bin]))
194 with self.assertRaises(asyncio.TimeoutError):
195 loop.run_until_complete(
196 lib._gen_check_output(["/bin/sleep", "2"], timeout=0.1)
200 def test_git_checkout_or_rebase(self) -> None:
201 loop = asyncio.get_event_loop()
202 project_config = deepcopy(FAKE_PROJECT_CONFIG)
203 work_path = Path(gettempdir())
205 expected_repo_path = work_path / "black"
206 with patch("black_primer.lib._gen_check_output", return_subproccess_output):
207 returned_repo_path = loop.run_until_complete(
208 lib.git_checkout_or_rebase(work_path, project_config)
210 self.assertEqual(expected_repo_path, returned_repo_path)
212 @patch("sys.stdout", new_callable=StringIO)
214 def test_process_queue(self, mock_stdout: Mock) -> None:
215 """Test the process queue on primer itself
216 - If you have non black conforming formatting in primer itself this can fail"""
217 loop = asyncio.get_event_loop()
218 config_path = Path(lib.__file__).parent / "primer.json"
219 with patch("black_primer.lib.git_checkout_or_rebase", return_false):
220 with TemporaryDirectory() as td:
221 return_val = loop.run_until_complete(
223 str(config_path), Path(td), 2, ["django", "pyramid"]
226 self.assertEqual(0, return_val)
229 def test_load_projects_queue(self) -> None:
230 """Test the process queue on primer itself
231 - If you have non black conforming formatting in primer itself this can fail"""
232 loop = asyncio.get_event_loop()
233 config_path = Path(lib.__file__).parent / "primer.json"
235 config, projects_queue = loop.run_until_complete(
236 lib.load_projects_queue(config_path, ["django", "pyramid"])
238 projects = collect(projects_queue)
239 self.assertEqual(projects, ["django", "pyramid"])
242 class PrimerCLITests(unittest.TestCase):
244 def test_async_main(self) -> None:
245 loop = asyncio.get_event_loop()
246 work_dir = Path(gettempdir()) / f"primer_ut_{getpid()}"
251 "long_checkouts": False,
253 "workdir": str(work_dir),
258 with patch("black_primer.cli.lib.process_queue", return_zero):
259 return_val = loop.run_until_complete(cli.async_main(**args)) # type: ignore
260 self.assertEqual(0, return_val)
262 def test_handle_debug(self) -> None:
263 self.assertTrue(cli._handle_debug(None, None, True))
265 def test_help_output(self) -> None:
267 result = runner.invoke(cli.main, ["--help"])
268 self.assertEqual(result.exit_code, 0)
270 def test_projects(self) -> None:
273 result = runner.invoke(cli.main, ["--projects=tox,asdf"])
274 self.assertEqual(result.exit_code, 0)
275 assert "1 / 1 succeeded" in result.output
279 result = runner.invoke(cli.main, ["--projects=tox,attrs"])
280 self.assertEqual(result.exit_code, 0)
281 assert "2 / 2 succeeded" in result.output
284 if __name__ == "__main__":