diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index ef05f18..1baa949 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -15,6 +15,8 @@ permissions: jobs: test: runs-on: ubuntu-latest + env: + PYTHONPATH: ${{ github.workspace }}/shared/python:${{ github.workspace }} strategy: matrix: python-version: [ '3.12', '3.13', '3.14' ] @@ -45,7 +47,7 @@ jobs: run: | mkdir -p tests/python/pylint/reports # Use python -m pylint and tee to ensure output is captured and visible in logs - PYTHONPATH=$(pwd) uv run python -m pylint --rcfile .pylintrc infrastructure samples setup shared 2>&1 | tee tests/python/pylint/reports/latest.txt + uv run python -m pylint --rcfile .pylintrc infrastructure samples setup shared 2>&1 | tee tests/python/pylint/reports/latest.txt - name: Upload pylint reports uses: actions/upload-artifact@v4 @@ -62,7 +64,7 @@ jobs: - name: Run pytest with coverage and generate JUnit XML id: pytest run: | - PYTHONPATH=$(pwd) COVERAGE_FILE=tests/python/.coverage-${{ matrix.python-version }} uv run pytest --cov=shared/python --cov-config=tests/python/.coveragerc --cov-report=html:tests/python/htmlcov-${{ matrix.python-version }} --cov-report=term-missing --junitxml=tests/python/junit-${{ matrix.python-version }}.xml tests/python/ + COVERAGE_FILE=tests/python/.coverage-${{ matrix.python-version }} uv run pytest --cov=shared/python --cov-config=tests/python/.coveragerc --cov-report=html:tests/python/htmlcov-${{ matrix.python-version }} --cov-report=term-missing --junitxml=tests/python/junit-${{ matrix.python-version }}.xml tests/python/ - name: Upload coverage HTML report uses: actions/upload-artifact@v4 @@ -95,7 +97,7 @@ jobs: # Coverage Percentage if [ -f "tests/python/.coverage-${{ matrix.python-version }}" ]; then - TOTAL_COV=$(PYTHONPATH=$(pwd) COVERAGE_FILE=tests/python/.coverage-${{ matrix.python-version }} uv run python -m coverage report | grep TOTAL | awk '{print $NF}') + TOTAL_COV=$(COVERAGE_FILE=tests/python/.coverage-${{ matrix.python-version }} uv run python -m coverage report | grep TOTAL | awk '{print $NF}') echo "coverage=$TOTAL_COV" >> "$GITHUB_OUTPUT" else echo "coverage=N/A" >> "$GITHUB_OUTPUT" @@ -106,7 +108,6 @@ jobs: if: github.event_name == 'pull_request' uses: marocchino/sticky-pull-request-comment@v2 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} header: python-results-${{ matrix.python-version }} message: | ## šŸ Python ${{ matrix.python-version }} Results @@ -121,13 +122,17 @@ jobs: - name: Generate Job Summary run: | + PYLINT_SCORE="${{ steps.metrics.outputs.pylint_score }}" + PYTEST_OUTCOME="${{ steps.pytest.outcome }}" + COVERAGE="${{ steps.metrics.outputs.coverage }}" + echo "## šŸ Python ${{ matrix.python-version }} Execution Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Category | Status | Detail |" >> $GITHUB_STEP_SUMMARY echo "| :--- | :---: | :--- |" >> $GITHUB_STEP_SUMMARY - echo "| **Pylint** | ${{ steps.pylint.outcome == 'success' && 'āœ…' || 'āš ļø' }} | Score: `${{ steps.metrics.outputs.pylint_score }}` |" >> $GITHUB_STEP_SUMMARY - echo "| **Pytest** | ${{ steps.pytest.outcome == 'success' && 'āœ…' || 'āŒ' }} | Outcome: `${{ steps.pytest.outcome }}` |" >> $GITHUB_STEP_SUMMARY - echo "| **Coverage** | šŸ“Š | Total: `${{ steps.metrics.outputs.coverage }}` |" >> $GITHUB_STEP_SUMMARY + echo "| **Pylint** | ${{ steps.pylint.outcome == 'success' && 'āœ…' || 'āš ļø' }} | Score: \`${PYLINT_SCORE:-N/A}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Pytest** | ${{ steps.pytest.outcome == 'success' && 'āœ…' || 'āŒ' }} | Outcome: \`${PYTEST_OUTCOME:-N/A}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Coverage** | šŸ“Š | Total: \`${COVERAGE:-N/A}\` |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "---" >> $GITHUB_STEP_SUMMARY diff --git a/setup/local_setup.py b/setup/local_setup.py index 0b33642..fdf3f30 100644 --- a/setup/local_setup.py +++ b/setup/local_setup.py @@ -99,6 +99,32 @@ def check_bicep_cli_installed(): print("āŒ Azure Bicep CLI is not installed. Install with: az bicep install") return False +def check_uv_installed(): + """Check if uv is installed and provide installation guidance if not.""" + uv_path = shutil.which('uv') + if not uv_path: + print("āŒ uv is not installed.") + print(" uv provides fast Python package management and is recommended for this project.") + print(" Installation instructions:") + if os.name == 'nt': # Windows + print(" • PowerShell: irm https://astral.sh/uv/install.ps1 | iex") + print(" • Or download from: https://github.com/astral-sh/uv/releases") + else: # macOS/Linux + print(" • curl -LsSf https://astral.sh/uv/install.sh | sh") + print(" • Official docs: https://docs.astral.sh/uv/getting-started/installation/") + return False + + try: + result = subprocess.run([uv_path, '--version'], capture_output=True, text=True, check=True) + version = result.stdout.strip() + print(f"āœ… uv is installed ({version})") + return True + except subprocess.CalledProcessError: + print("āŒ uv is installed but not functioning correctly") + print(" Try reinstalling: https://docs.astral.sh/uv/getting-started/installation/") + return False + + def check_azure_providers_registered(): """Check if required Azure resource providers are registered in the current subscription.""" az_path = shutil.which('az') or shutil.which('az.cmd') or shutil.which('az.bat') @@ -529,54 +555,139 @@ def setup_complete_environment(): print("šŸš€ Setting up complete APIM Samples environment...\n") - # Step 1: Check Azure prerequisites - print("1/5) Checking Azure prerequisites...\n") - azure_cli_ok = check_azure_cli_installed() - bicep_ok = check_bicep_cli_installed() - providers_ok = check_azure_providers_registered() + # Step 1: Check uv installation (recommended but not blocking) + print("1/7) Checking uv installation (recommended)...\n") + uv_ok = False + try: + uv_ok = check_uv_installed() + except Exception as e: + print(f"āš ļø Error checking uv installation: {e}") + print(" Continuing with setup...") - if not (azure_cli_ok and bicep_ok and providers_ok): - print("\nāš ļø Some Azure prerequisites are missing. Please address the issues above and re-run this script.") - return + # Step 2: Check Azure prerequisites + print("\n2/7) Checking Azure prerequisites...\n") + azure_cli_ok = False + bicep_ok = False + providers_ok = False - # Step 2: Generate .env file - print("\n2/5) Generating .env file for Python path configuration...") - generate_env_file() + try: + azure_cli_ok = check_azure_cli_installed() + except Exception as e: + print(f"āš ļø Error checking Azure CLI: {e}") + print(" Continuing with setup...") - # Step 3: Register Jupyter kernel - print("3/5) Registering standardized Jupyter kernel...\n") - kernel_success = install_jupyter_kernel() + try: + bicep_ok = check_bicep_cli_installed() + except Exception as e: + print(f"āš ļø Error checking Bicep CLI: {e}") + print(" Continuing with setup...") - # Step 4: Configure VS Code settings with minimal, merged defaults - print("\n4/5) Configuring VS Code workspace settings...\n") - vscode_success = create_vscode_settings() + try: + providers_ok = check_azure_providers_registered() + except Exception as e: + print(f"āš ļø Error checking Azure providers: {e}") + print(" Continuing with setup...") - # Step 5: Enforce kernel consistency - print("\n5/5) Enforcing kernel consistency for future reliability...\n") - consistency_success = force_kernel_consistency() + if not (azure_cli_ok and bicep_ok): + print("\nāš ļø Some Azure prerequisites are missing. Please address the issues above.") + print(" Continuing with environment setup...\n") + + # Step 3: Generate .env file + print("\n3/7) Generating .env file for Python path configuration...") + env_success = False + try: + generate_env_file() + env_success = True + except Exception as e: + print(f"āŒ Failed to generate .env file: {e}") + print(" Continuing with setup...") + + # Step 4: Register Jupyter kernel + print("\n4/7) Registering standardized Jupyter kernel...\n") + kernel_success = False + try: + kernel_success = install_jupyter_kernel() + except Exception as e: + print(f"āŒ Error installing Jupyter kernel: {e}") + print(" Continuing with setup...") + + # Step 5: Configure VS Code settings with minimal, merged defaults + print("\n5/7) Configuring VS Code workspace settings...\n") + vscode_success = False + try: + vscode_success = create_vscode_settings() + except Exception as e: + print(f"āŒ Error creating VS Code settings: {e}") + print(" Continuing with setup...") + + # Step 6: Enforce kernel consistency + print("\n6/7) Enforcing kernel consistency for future reliability...\n") + consistency_success = False + try: + consistency_success = force_kernel_consistency() + except Exception as e: + print(f"āŒ Error enforcing kernel consistency: {e}") + print(" Continuing with setup...") + + # Step 7: Run uv sync if uv is available + print("\n7/7) Syncing dependencies with uv (if available)...\n") + sync_success = False + if uv_ok: + try: + uv_path = shutil.which('uv') + if uv_path: + subprocess.run([uv_path, 'sync'], check=True, capture_output=True, text=True) + print("āœ… Dependencies synced successfully with uv") + sync_success = True + else: + print("āš ļø uv reported installed but executable not found in PATH; skipping sync") + print(" Install uv and run 'uv sync' for dependency management") + except subprocess.CalledProcessError as e: + print(f"āš ļø Failed to sync dependencies with uv: {e}") + print(" You can manually run 'uv sync' after setup") + except Exception as e: + print(f"āš ļø Error during uv sync: {e}") + print(" You can manually run 'uv sync' after setup") + else: + print("āš ļø Skipping dependency sync (uv not available)") + print(" Install uv and run 'uv sync' for dependency management") # Summary print("\n" + "="*50) print("šŸ“‹ Setup Summary:") - print(" āœ… Azure CLI and Bicep: Available") - print(" āœ… Azure resource providers: Registered") - print(" āœ… Python path configuration: Complete") + print(f" {'āœ…' if uv_ok else 'āš ļø '} uv installation: {'Available' if uv_ok else 'Not installed (recommended)'}") + print(f" {'āœ…' if azure_cli_ok else 'āŒ'} Azure CLI: {'Available' if azure_cli_ok else 'Not installed'}") + print(f" {'āœ…' if bicep_ok else 'āŒ'} Azure Bicep: {'Available' if bicep_ok else 'Not installed'}") + print(f" {'āœ…' if providers_ok else 'āš ļø '} Azure resource providers: {'Registered' if providers_ok else 'Not all registered'}") + print(f" {'āœ…' if env_success else 'āŒ'} Python path configuration: {'Complete' if env_success else 'Failed'}") print(f" {'āœ…' if kernel_success else 'āŒ'} Jupyter kernel registration: {'Complete' if kernel_success else 'Failed'}") print(f" {'āœ…' if vscode_success else 'āŒ'} VS Code settings: {'Complete' if vscode_success else 'Failed'}") print(f" {'āœ…' if consistency_success else 'āŒ'} Kernel trust refresh: {'Complete' if consistency_success else 'Failed'}") + print(f" {'āœ…' if sync_success else 'āš ļø '} Dependency sync: {'Complete' if sync_success else 'Skipped or failed'}") - if kernel_success and vscode_success and consistency_success: - print("\nšŸŽ‰ Setup complete! Your local environment now matches the dev container experience.") + critical_success = env_success and kernel_success and vscode_success and consistency_success + + if critical_success: + print("\nšŸŽ‰ Setup complete! Your local environment is configured.") print(f" • Notebooks can use the '{KERNEL_DISPLAY_NAME}' kernel") print(" • Python modules from shared/ directory are available") print(" • VS Code is configured for optimal workflow") print(" • User customizations are preserved across reruns") + if not uv_ok: + print("\nāš ļø Note: uv is not installed but is recommended for faster dependency management") + print(" See installation instructions above") print("\nšŸ’” Next steps:") print(" 1. Restart VS Code to apply all settings") - print(" 2. Open any notebook - it should automatically use the correct kernel") - print(" 3. The kernel should remain consistent across all notebooks") + if uv_ok and sync_success: + print(" 2. Dependencies are synced and ready to use") + elif uv_ok: + print(" 2. Run 'uv sync' to install dependencies") + else: + print(" 2. Install uv and run 'uv sync' for dependency management") + print(" 3. Open any notebook - it should automatically use the correct kernel") else: print("\nāš ļø Setup completed with some issues. Check error messages above.") + print(" The environment may still be partially functional.") def show_help(): diff --git a/setup/verify_local_setup.py b/setup/verify_local_setup.py index 2c75ac8..07b49a6 100644 --- a/setup/verify_local_setup.py +++ b/setup/verify_local_setup.py @@ -67,7 +67,7 @@ def check_uv_sync(): """Check if uv is available and sync dependencies if it is.""" uv_path = shutil.which("uv") if not uv_path: - return True, "uv is not installed (optional - install from https://docs.astral.sh/uv/)" + return False, "Install uv for faster dependency management: https://docs.astral.sh/uv/ (or use 'Complete environment setup' in Developer CLI)" venv_path = Path.cwd() / ".venv" if not venv_path.exists(): diff --git a/tests/python/test_local_setup.py b/tests/python/test_local_setup.py index a3284ad..c3ffcd7 100644 --- a/tests/python/test_local_setup.py +++ b/tests/python/test_local_setup.py @@ -354,6 +354,56 @@ def test_check_azure_providers_registered_json_error(): assert result is False +# ============================================================ +# Tests for check_uv_installed +# ============================================================ + +def test_check_uv_installed_success(): + """Test check_uv_installed returns True when uv is found and functioning.""" + with patch("shutil.which") as mock_which: + with patch("subprocess.run") as mock_run: + mock_which.return_value = "/usr/bin/uv" + mock_run.return_value = Mock(stdout="uv 0.1.0", returncode=0) + result = sps.check_uv_installed() + assert result is True + + +def test_check_uv_installed_not_found(): + """Test check_uv_installed returns False when uv is not found.""" + with patch("shutil.which") as mock_which: + mock_which.return_value = None + result = sps.check_uv_installed() + assert result is False + + +def test_check_uv_installed_windows_instructions(): + """Test check_uv_installed provides Windows-specific instructions.""" + with patch("shutil.which") as mock_which: + with patch.object(os, "name", "nt"): + mock_which.return_value = None + result = sps.check_uv_installed() + assert result is False + + +def test_check_uv_installed_unix_instructions(): + """Test check_uv_installed provides Unix-specific instructions.""" + with patch("shutil.which") as mock_which: + with patch.object(os, "name", "posix"): + mock_which.return_value = None + result = sps.check_uv_installed() + assert result is False + + +def test_check_uv_installed_subprocess_error(): + """Test check_uv_installed handles subprocess errors.""" + with patch("shutil.which") as mock_which: + with patch("subprocess.run") as mock_run: + mock_which.return_value = "/usr/bin/uv" + mock_run.side_effect = subprocess.CalledProcessError(1, "uv") + result = sps.check_uv_installed() + assert result is False + + # ============================================================ # Tests for VS Code and environment setup # ============================================================ @@ -676,23 +726,249 @@ def test_validate_kernel_setup_jupyter_not_found(monkeypatch: pytest.MonkeyPatch # ============================================================ def test_setup_complete_environment_success(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): - """Test setup_complete_environment runs all steps successfully.""" + """Test setup_complete_environment runs all steps successfully including UV.""" + monkeypatch.setattr(sps, "check_uv_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) + + with patch("shutil.which") as mock_which: + with patch("subprocess.run") as mock_run: + mock_which.return_value = "/usr/bin/uv" + mock_run.return_value = Mock(returncode=0) + # Should not raise any exceptions + sps.setup_complete_environment() + + +def test_setup_complete_environment_without_uv(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment succeeds even when UV is not available.""" + monkeypatch.setattr(sps, "check_uv_installed", lambda: False) + monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) + + # Should complete successfully without UV + sps.setup_complete_environment() + + +def test_setup_complete_environment_uv_sync_fails(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment handles UV sync failures gracefully.""" + monkeypatch.setattr(sps, "check_uv_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) + + with patch("shutil.which") as mock_which: + with patch("subprocess.run") as mock_run: + mock_which.return_value = "/usr/bin/uv" + mock_run.side_effect = subprocess.CalledProcessError(1, "uv sync") + # Should complete without raising exception + sps.setup_complete_environment() + + +def test_setup_complete_environment_check_uv_exception(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment handles exceptions in check_uv_installed.""" + def raise_exception(): + raise RuntimeError("UV check failed") + + monkeypatch.setattr(sps, "check_uv_installed", raise_exception) + monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) + + # Should continue despite exception + sps.setup_complete_environment() + + +def test_setup_complete_environment_azure_cli_exception(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment handles exceptions in Azure CLI check.""" + def raise_exception(): + raise RuntimeError("Azure CLI check failed") + + monkeypatch.setattr(sps, "check_uv_installed", lambda: False) + monkeypatch.setattr(sps, "check_azure_cli_installed", raise_exception) + monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) + + # Should continue despite exception + sps.setup_complete_environment() + + +def test_setup_complete_environment_bicep_exception(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment handles exceptions in Bicep CLI check.""" + def raise_exception(): + raise RuntimeError("Bicep CLI check failed") + + monkeypatch.setattr(sps, "check_uv_installed", lambda: False) + monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_bicep_cli_installed", raise_exception) + monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) + + # Should continue despite exception + sps.setup_complete_environment() + + +def test_setup_complete_environment_providers_exception(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment handles exceptions in Azure providers check.""" + def raise_exception(): + raise RuntimeError("Providers check failed") + + monkeypatch.setattr(sps, "check_uv_installed", lambda: False) + monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_providers_registered", raise_exception) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) + + # Should continue despite exception + sps.setup_complete_environment() + + +def test_setup_complete_environment_generate_env_exception(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment handles exceptions in generate_env_file.""" + def raise_exception(): + raise RuntimeError("Generate env failed") + + monkeypatch.setattr(sps, "check_uv_installed", lambda: False) monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "generate_env_file", raise_exception) monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) - # Should not raise any exceptions + # Should continue despite exception + sps.setup_complete_environment() + + +def test_setup_complete_environment_kernel_exception(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment handles exceptions in install_jupyter_kernel.""" + def raise_exception(): + raise RuntimeError("Kernel install failed") + + monkeypatch.setattr(sps, "check_uv_installed", lambda: False) + monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "install_jupyter_kernel", raise_exception) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) + + # Should continue despite exception + sps.setup_complete_environment() + + +def test_setup_complete_environment_vscode_exception(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment handles exceptions in create_vscode_settings.""" + def raise_exception(): + raise RuntimeError("VS Code settings failed") + + monkeypatch.setattr(sps, "check_uv_installed", lambda: False) + monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", raise_exception) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) + + # Should continue despite exception + sps.setup_complete_environment() + + +def test_setup_complete_environment_consistency_exception(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment handles exceptions in force_kernel_consistency.""" + def raise_exception(): + raise RuntimeError("Kernel consistency failed") + + monkeypatch.setattr(sps, "check_uv_installed", lambda: False) + monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", raise_exception) + + # Should continue despite exception + sps.setup_complete_environment() + + +def test_setup_complete_environment_uv_sync_exception(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment handles exceptions during UV sync.""" + monkeypatch.setattr(sps, "check_uv_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) + + with patch("shutil.which") as mock_which: + with patch("subprocess.run") as mock_run: + mock_which.return_value = "/usr/bin/uv" + mock_run.side_effect = RuntimeError("Unexpected error") + # Should continue despite exception + sps.setup_complete_environment() + + +def test_setup_complete_environment_uv_sync_path_not_found(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment when uv_path is not found after check_uv_installed returns True.""" + monkeypatch.setattr(sps, "check_uv_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) + monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) + + with patch("shutil.which") as mock_which: + mock_which.return_value = None # shutil.which returns None when executable not found + # Should complete successfully even when uv executable is not found + sps.setup_complete_environment() + + +def test_setup_complete_environment_continues_without_azure_tools(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): + """Test setup_complete_environment continues even if Azure tools are missing.""" + monkeypatch.setattr(sps, "check_uv_installed", lambda: False) + monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: False) + monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: False) + monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: False) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) + + # Should continue and complete setup sps.setup_complete_environment() def test_setup_complete_environment_missing_azure_cli(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): """Test setup_complete_environment stops when Azure CLI is missing.""" + monkeypatch.setattr(sps, "check_uv_installed", lambda: False) monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: False) monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: True) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) # Should not raise but should return early sps.setup_complete_environment() @@ -1406,9 +1682,13 @@ def test_setup_complete_environment_with_missing_bicep(temp_project_root: Path, def test_setup_complete_environment_with_missing_providers(temp_project_root: Path, monkeypatch: pytest.MonkeyPatch): """Test setup_complete_environment stops when Azure providers are not registered.""" + monkeypatch.setattr(sps, "check_uv_installed", lambda: False) monkeypatch.setattr(sps, "check_azure_cli_installed", lambda: True) monkeypatch.setattr(sps, "check_bicep_cli_installed", lambda: True) monkeypatch.setattr(sps, "check_azure_providers_registered", lambda: False) + monkeypatch.setattr(sps, "install_jupyter_kernel", lambda: True) + monkeypatch.setattr(sps, "create_vscode_settings", lambda: True) + monkeypatch.setattr(sps, "force_kernel_consistency", lambda: True) # Should return early without full setup sps.setup_complete_environment() @@ -2322,11 +2602,15 @@ def test_setup_complete_environment_all_pass(monkeypatch: pytest.MonkeyPatch): def test_setup_complete_environment_azure_cli_fails(monkeypatch: pytest.MonkeyPatch): """Test setup_complete_environment when Azure CLI check fails.""" - with patch.object(sps, "check_azure_cli_installed", return_value=False): - with patch.object(sps, "check_bicep_cli_installed", return_value=True): - with patch.object(sps, "check_azure_providers_registered", return_value=True): - # Should return early, not continue to next steps - sps.setup_complete_environment() + with patch.object(sps, "check_uv_installed", return_value=False): + with patch.object(sps, "check_azure_cli_installed", return_value=False): + with patch.object(sps, "check_bicep_cli_installed", return_value=True): + with patch.object(sps, "check_azure_providers_registered", return_value=True): + with patch.object(sps, "install_jupyter_kernel", return_value=True): + with patch.object(sps, "create_vscode_settings", return_value=True): + with patch.object(sps, "force_kernel_consistency", return_value=True): + # Should return early, not continue to next steps + sps.setup_complete_environment() def test_setup_complete_environment_kernel_fails(monkeypatch: pytest.MonkeyPatch): diff --git a/tests/python/test_verify_local_setup.py b/tests/python/test_verify_local_setup.py index ccde62d..62b2731 100644 --- a/tests/python/test_verify_local_setup.py +++ b/tests/python/test_verify_local_setup.py @@ -119,13 +119,13 @@ def test_check_virtual_environment_wrong_python(temp_cwd: Path, monkeypatch: pyt # ============================================================ def test_check_uv_sync_uv_not_installed(suppress_print) -> None: - """UV sync check should pass (with note) when uv is not installed.""" + """UV sync check should fail when uv is not installed.""" with patch("shutil.which") as mock_which: mock_which.return_value = None ok, fix = vls.check_uv_sync() - assert ok is True - assert "uv is not installed" in fix - assert "optional" in fix + assert ok is False + assert "Install uv" in fix + assert "https://docs.astral.sh/uv/" in fix def test_check_uv_sync_success_venv_exists(temp_cwd: Path, suppress_print) -> None: @@ -778,6 +778,7 @@ def test_check_azure_providers_json_error(monkeypatch: pytest.MonkeyPatch, suppr def test_main_all_pass(monkeypatch: pytest.MonkeyPatch, suppress_print): """Main function should return True when all checks pass.""" monkeypatch.setattr(vls, "check_virtual_environment", lambda: (True, "")) + monkeypatch.setattr(vls, "check_uv_sync", lambda: (True, "")) monkeypatch.setattr(vls, "check_required_packages", lambda: (True, "")) monkeypatch.setattr(vls, "check_shared_modules", lambda: (True, "")) monkeypatch.setattr(vls, "check_env_file", lambda: (True, "")) @@ -821,6 +822,7 @@ def check_azure_providers_mock(): def test_main_run_azure_providers_when_login_succeeds(monkeypatch: pytest.MonkeyPatch, suppress_print): """Main function should run Azure Providers check when Azure Login succeeds.""" monkeypatch.setattr(vls, "check_virtual_environment", lambda: (True, "")) + monkeypatch.setattr(vls, "check_uv_sync", lambda: (True, "")) monkeypatch.setattr(vls, "check_required_packages", lambda: (True, "")) monkeypatch.setattr(vls, "check_shared_modules", lambda: (True, "")) monkeypatch.setattr(vls, "check_env_file", lambda: (True, "")) diff --git a/uv.lock b/uv.lock index 4535015..d2ee2bb 100644 --- a/uv.lock +++ b/uv.lock @@ -390,11 +390,11 @@ wheels = [ [[package]] name = "dill" -version = "0.4.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, ] [[package]]