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 = """\
 
  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 if sys.version_info >= (3, 9):
 
 100 def collect(queue: Q) -> List[T]:
 
 104             item = queue.get_nowait()
 
 106         except asyncio.QueueEmpty:
 
 110 class PrimerLibTests(unittest.TestCase):
 
 111     def test_analyze_results(self) -> None:
 
 112         fake_results = lib.Results(
 
 116                 "skipped_long_checkout": 0,
 
 120             {"black": CalledProcessError(69, ["black"], b"Black didn't work", b"")},
 
 122         with capture_stdout(lib.analyze_results, 69, fake_results) as analyze_stdout:
 
 123             self.assertEqual(EXPECTED_ANALYSIS_OUTPUT, analyze_stdout)
 
 126     def test_black_run(self) -> None:
 
 127         """Pretend to run Black to ensure we cater for all scenarios"""
 
 128         loop = asyncio.get_event_loop()
 
 129         project_name = "unittest"
 
 130         repo_path = Path(gettempdir())
 
 131         project_config = deepcopy(FAKE_PROJECT_CONFIG)
 
 132         results = lib.Results({"failed": 0, "success": 0}, {})
 
 134         # Test a successful Black run
 
 135         with patch("black_primer.lib._gen_check_output", return_subproccess_output):
 
 136             loop.run_until_complete(
 
 137                 lib.black_run(project_name, repo_path, project_config, results)
 
 139         self.assertEqual(1, results.stats["success"])
 
 140         self.assertFalse(results.failed_projects)
 
 142         # Test a fail based on expecting formatting changes but not getting any
 
 143         project_config["expect_formatting_changes"] = True
 
 144         results = lib.Results({"failed": 0, "success": 0}, {})
 
 145         with patch("black_primer.lib._gen_check_output", return_subproccess_output):
 
 146             loop.run_until_complete(
 
 147                 lib.black_run(project_name, repo_path, project_config, results)
 
 149         self.assertEqual(1, results.stats["failed"])
 
 150         self.assertTrue(results.failed_projects)
 
 152         # Test a fail based on returning 1 and not expecting formatting changes
 
 153         project_config["expect_formatting_changes"] = False
 
 154         results = lib.Results({"failed": 0, "success": 0}, {})
 
 155         with patch("black_primer.lib._gen_check_output", raise_subprocess_error_1):
 
 156             loop.run_until_complete(
 
 157                 lib.black_run(project_name, repo_path, project_config, results)
 
 159         self.assertEqual(1, results.stats["failed"])
 
 160         self.assertTrue(results.failed_projects)
 
 162         # Test a formatting error based on returning 123
 
 163         with patch("black_primer.lib._gen_check_output", raise_subprocess_error_123):
 
 164             loop.run_until_complete(
 
 165                 lib.black_run(project_name, repo_path, project_config, results)
 
 167         self.assertEqual(2, results.stats["failed"])
 
 169     def test_flatten_cli_args(self) -> None:
 
 170         fake_long_args = ["--arg", ["really/", "|long", "|regex", "|splitup"], "--done"]
 
 171         expected = ["--arg", "really/|long|regex|splitup", "--done"]
 
 172         self.assertEqual(expected, lib._flatten_cli_args(fake_long_args))
 
 175     def test_gen_check_output(self) -> None:
 
 176         loop = asyncio.get_event_loop()
 
 177         stdout, stderr = loop.run_until_complete(
 
 178             lib._gen_check_output([lib.BLACK_BINARY, "--help"])
 
 180         self.assertTrue("The uncompromising code formatter" in stdout.decode("utf8"))
 
 181         self.assertEqual(None, stderr)
 
 183         # TODO: Add a test to see failure works on Windows
 
 187         false_bin = "/usr/bin/false" if system() == "Darwin" else "/bin/false"
 
 188         with self.assertRaises(CalledProcessError):
 
 189             loop.run_until_complete(lib._gen_check_output([false_bin]))
 
 191         with self.assertRaises(asyncio.TimeoutError):
 
 192             loop.run_until_complete(
 
 193                 lib._gen_check_output(["/bin/sleep", "2"], timeout=0.1)
 
 197     def test_git_checkout_or_rebase(self) -> None:
 
 198         loop = asyncio.get_event_loop()
 
 199         project_config = deepcopy(FAKE_PROJECT_CONFIG)
 
 200         work_path = Path(gettempdir())
 
 202         expected_repo_path = work_path / "black"
 
 203         with patch("black_primer.lib._gen_check_output", return_subproccess_output):
 
 204             returned_repo_path = loop.run_until_complete(
 
 205                 lib.git_checkout_or_rebase(work_path, project_config)
 
 207         self.assertEqual(expected_repo_path, returned_repo_path)
 
 209     @patch("sys.stdout", new_callable=StringIO)
 
 211     def test_process_queue(self, mock_stdout: Mock) -> None:
 
 212         """Test the process queue on primer itself
 
 213         - If you have non black conforming formatting in primer itself this can fail"""
 
 214         loop = asyncio.get_event_loop()
 
 215         config_path = Path(lib.__file__).parent / "primer.json"
 
 216         with patch("black_primer.lib.git_checkout_or_rebase", return_false):
 
 217             with TemporaryDirectory() as td:
 
 218                 return_val = loop.run_until_complete(
 
 220                         str(config_path), Path(td), 2, ["django", "pyramid"]
 
 223                 self.assertEqual(0, return_val)
 
 226     def test_load_projects_queue(self) -> None:
 
 227         """Test the process queue on primer itself
 
 228         - If you have non black conforming formatting in primer itself this can fail"""
 
 229         loop = asyncio.get_event_loop()
 
 230         config_path = Path(lib.__file__).parent / "primer.json"
 
 232         config, projects_queue = loop.run_until_complete(
 
 233             lib.load_projects_queue(config_path, ["django", "pyramid"])
 
 235         projects = collect(projects_queue)
 
 236         self.assertEqual(projects, ["django", "pyramid"])
 
 239 class PrimerCLITests(unittest.TestCase):
 
 241     def test_async_main(self) -> None:
 
 242         loop = asyncio.get_event_loop()
 
 243         work_dir = Path(gettempdir()) / f"primer_ut_{getpid()}"
 
 248             "long_checkouts": False,
 
 250             "workdir": str(work_dir),
 
 255         with patch("black_primer.cli.lib.process_queue", return_zero):
 
 256             return_val = loop.run_until_complete(cli.async_main(**args))  # type: ignore
 
 257             self.assertEqual(0, return_val)
 
 259     def test_handle_debug(self) -> None:
 
 260         self.assertTrue(cli._handle_debug(None, None, True))
 
 262     def test_help_output(self) -> None:
 
 264         result = runner.invoke(cli.main, ["--help"])
 
 265         self.assertEqual(result.exit_code, 0)
 
 267     def test_projects(self) -> None:
 
 270             result = runner.invoke(cli.main, ["--projects=tox,asdf"])
 
 271             self.assertEqual(result.exit_code, 0)
 
 272             assert "1 / 1 succeeded" in result.output
 
 276             result = runner.invoke(cli.main, ["--projects=tox,attrs"])
 
 277             self.assertEqual(result.exit_code, 0)
 
 278             assert "2 / 2 succeeded" in result.output
 
 281 if __name__ == "__main__":