"""Unit tests for devx.tools.setup_image.""" import os import subprocess from pathlib import Path from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner from devx.tools.setup_image import ( _build_pip_extra_index_url, _fallback_to_setup_ci, _install_in_image, cli, ) class TestBuildPipExtraIndexUrl: def test_basic_url(self) -> None: url = _build_pip_extra_index_url( "git.oblachno.oblachno.fyi", "oblachno-oss", "emil", "tok123", ) assert url == "https://emil:tok123@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/" def test_custom_host_org(self) -> None: url = _build_pip_extra_index_url( "gitea.example.com", "my-org", "user", "secret", ) assert url == "https://user:secret@gitea.example.com/api/packages/my-org/pypi/simple/" class TestInstallInImage: @patch("devx.tools.setup_image.subprocess.run") @patch("devx.tools.setup_image.Path") def test_link_and_install_no_token(self, mock_path: MagicMock, mock_run: MagicMock, tmp_path: Path) -> None: venv_link = tmp_path / ".venv" mock_path.return_value.exists.return_value = False mock_path.return_value.is_symlink.return_value = False mock_path.return_value.symlink_to = MagicMock() with patch.dict(os.environ, {}, clear=True): _install_in_image(str(venv_link), "/opt/venv", "", "host", "org") mock_path.return_value.symlink_to.assert_called_once_with("/opt/venv") mock_run.assert_called_once() cmd = mock_run.call_args[0][0] assert "--no-cache-dir" in cmd assert "--no-deps" in cmd assert "-e" in cmd assert "." in cmd # No extras → spec is "." assert ".[]" not in " ".join(cmd) @patch("devx.tools.setup_image.subprocess.run") @patch("devx.tools.setup_image.Path") def test_link_and_install_with_extras( self, mock_path: MagicMock, mock_run: MagicMock, tmp_path: Path, ) -> None: venv_link = tmp_path / ".venv" mock_path.return_value.exists.return_value = False mock_path.return_value.is_symlink.return_value = False mock_path.return_value.symlink_to = MagicMock() with patch.dict(os.environ, {}, clear=True): _install_in_image(str(venv_link), "/opt/venv", "ci,lint", "host", "org") cmd = mock_run.call_args[0][0] assert ".[ci,lint]" in cmd @patch("devx.tools.setup_image.subprocess.run") @patch("devx.tools.setup_image.Path") def test_install_with_token_sets_pip_extra_index_url( self, mock_path: MagicMock, mock_run: MagicMock, tmp_path: Path, ) -> None: venv_link = tmp_path / ".venv" mock_path.return_value.exists.return_value = False mock_path.return_value.is_symlink.return_value = False mock_path.return_value.symlink_to = MagicMock() with patch.dict( os.environ, {"CI_GITEA_TOKEN": "tok123", "CI_GITEA_USERNAME": "emil"}, clear=True, ): _install_in_image(str(venv_link), "/opt/venv", "lint", "git.host", "org") env = mock_run.call_args[1]["env"] assert "PIP_EXTRA_INDEX_URL" in env assert "emil:tok123@git.host" in env["PIP_EXTRA_INDEX_URL"] @patch("devx.tools.setup_image.subprocess.run") @patch("devx.tools.setup_image.Path") def test_install_with_token_defaults_username( self, mock_path: MagicMock, mock_run: MagicMock, tmp_path: Path, ) -> None: venv_link = tmp_path / ".venv" mock_path.return_value.exists.return_value = False mock_path.return_value.is_symlink.return_value = False mock_path.return_value.symlink_to = MagicMock() with patch.dict(os.environ, {"CI_GITEA_TOKEN": "tok123"}, clear=True): _install_in_image(str(venv_link), "/opt/venv", "", "host", "org") env = mock_run.call_args[1]["env"] assert "emil:tok123@host" in env["PIP_EXTRA_INDEX_URL"] @patch("devx.tools.setup_image.subprocess.run") @patch("devx.tools.setup_image.Path") def test_install_removes_existing_link( self, mock_path: MagicMock, mock_run: MagicMock, tmp_path: Path, ) -> None: venv_link = tmp_path / ".venv" mock_path.return_value.exists.return_value = True mock_path.return_value.is_symlink.return_value = False mock_path.return_value.unlink = MagicMock() mock_path.return_value.symlink_to = MagicMock() with patch.dict(os.environ, {}, clear=True): _install_in_image(str(venv_link), "/opt/venv", "", "host", "org") mock_path.return_value.unlink.assert_called_once() @patch("devx.tools.setup_image.subprocess.run") @patch("devx.tools.setup_image.Path") def test_install_removes_existing_symlink( self, mock_path: MagicMock, mock_run: MagicMock, tmp_path: Path, ) -> None: venv_link = tmp_path / ".venv" mock_path.return_value.exists.return_value = False mock_path.return_value.is_symlink.return_value = True mock_path.return_value.unlink = MagicMock() mock_path.return_value.symlink_to = MagicMock() with patch.dict(os.environ, {}, clear=True): _install_in_image(str(venv_link), "/opt/venv", "", "host", "org") mock_path.return_value.unlink.assert_called_once() @patch("devx.tools.setup_image.subprocess.run") @patch("devx.tools.setup_image.Path") def test_install_failure_raises(self, mock_path: MagicMock, mock_run: MagicMock, tmp_path: Path) -> None: venv_link = tmp_path / ".venv" mock_path.return_value.exists.return_value = False mock_path.return_value.is_symlink.return_value = False mock_path.return_value.symlink_to = MagicMock() mock_run.side_effect = subprocess.CalledProcessError(1, ["pip"]) with patch.dict(os.environ, {}, clear=True): with pytest.raises(subprocess.CalledProcessError): _install_in_image(str(venv_link), "/opt/venv", "", "host", "org") class TestFallbackToSetupCi: @patch("devx.tools.setup_image.subprocess.run") def test_fallback_runs_make_setup_ci(self, mock_run: MagicMock) -> None: _fallback_to_setup_ci() mock_run.assert_called_once_with(["make", "setup-ci"], check=True) @patch("devx.tools.setup_image.subprocess.run") def test_fallback_failure_raises(self, mock_run: MagicMock) -> None: mock_run.side_effect = subprocess.CalledProcessError(1, ["make"]) with pytest.raises(subprocess.CalledProcessError): _fallback_to_setup_ci() class TestCli: @patch("devx.tools.setup_image._fallback_to_setup_ci") @patch("devx.tools.setup_image._install_in_image") @patch("devx.tools.setup_image.Path") def test_cli_with_opt_venv_present( self, mock_path: MagicMock, mock_install: MagicMock, mock_fallback: MagicMock, ) -> None: mock_path.return_value.is_dir.return_value = True runner = CliRunner() result = runner.invoke(cli, ["--extras", "ci,lint"]) assert result.exit_code == 0 mock_install.assert_called_once() @patch("devx.tools.setup_image._install_in_image") @patch("devx.tools.setup_image._fallback_to_setup_ci") @patch("devx.tools.setup_image.Path") def test_cli_falls_back_when_no_opt_venv( self, mock_path: MagicMock, mock_fallback: MagicMock, mock_install: MagicMock, ) -> None: mock_path.return_value.is_dir.return_value = False runner = CliRunner() result = runner.invoke(cli, []) assert result.exit_code == 0 mock_fallback.assert_called_once() @patch("devx.tools.setup_image._fallback_to_setup_ci") @patch("devx.tools.setup_image._install_in_image") @patch("devx.tools.setup_image.Path") def test_cli_default_values( self, mock_path: MagicMock, mock_install: MagicMock, mock_fallback: MagicMock, ) -> None: mock_path.return_value.is_dir.return_value = True runner = CliRunner() result = runner.invoke(cli, []) assert result.exit_code == 0 call_args = mock_install.call_args[0] assert call_args[0] == ".venv" assert call_args[1] == "/opt/venv" assert call_args[2] == "" # no extras assert call_args[3] == "git.oblachno.oblachno.fyi" assert call_args[4] == "oblachno-oss" @patch("devx.tools.setup_image._fallback_to_setup_ci") @patch("devx.tools.setup_image._install_in_image") @patch("devx.tools.setup_image.Path") def test_cli_custom_venv_and_gitea( self, mock_path: MagicMock, mock_install: MagicMock, mock_fallback: MagicMock, ) -> None: mock_path.return_value.is_dir.return_value = True runner = CliRunner() result = runner.invoke( cli, ["--venv", ".custom-venv", "--gitea-host", "gitea.io", "--gitea-org", "myorg"], ) assert result.exit_code == 0 call_args = mock_install.call_args[0] assert call_args[0] == ".custom-venv" assert call_args[3] == "gitea.io" assert call_args[4] == "myorg" @patch("devx.tools.setup_image._fallback_to_setup_ci") @patch("devx.tools.setup_image._install_in_image") @patch("devx.tools.setup_image.Path") def test_cli_with_extras( self, mock_path: MagicMock, mock_install: MagicMock, mock_fallback: MagicMock, ) -> None: mock_path.return_value.is_dir.return_value = True runner = CliRunner() result = runner.invoke(cli, ["--extras", "lint"]) assert result.exit_code == 0 assert mock_install.call_args[0][2] == "lint"