186 lines
6.7 KiB
Python
186 lines
6.7 KiB
Python
"""Exercise the Windows bootstrap in disposable projects without installing host tools."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
pytestmark = pytest.mark.skipif(
|
|
sys.platform != "win32" or not shutil.which("uv"), reason="Windows and uv required"
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def project(tmp_path):
|
|
# Import fixtures let the final smoke check run without downloading project dependencies.
|
|
path = tmp_path / "source with spaces!"
|
|
path.mkdir()
|
|
(path / "pyproject.toml").write_text(
|
|
'[project]\nname="bootstrap-fixture"\nversion="0.1.0"\nrequires-python=">=3.12"\ndependencies=[]\n'
|
|
)
|
|
for name in ("artemis", "uvicorn", "fastapi", "langchain_openai"):
|
|
(path / f"{name}.py").write_text("")
|
|
subprocess.run(["uv", "lock", "--offline"], cwd=path, check=True, capture_output=True)
|
|
return path
|
|
|
|
|
|
def run_bootstrap(
|
|
project, *, missing_uv=False, installer_failure=False, sync_failure=False, mirror_index=""
|
|
):
|
|
script = project / "check.ps1"
|
|
script.write_text(
|
|
"""
|
|
$ErrorActionPreference = 'Continue'
|
|
$realUv = (Get-Command uv).Source
|
|
. $env:BOOTSTRAP_SCRIPT
|
|
. (Join-Path (Split-Path $env:BOOTSTRAP_SCRIPT -Parent) 'download_config.ps1')
|
|
if ($env:TEST_MIRROR) { $script:ArtemisDownloads = @{ python_index = $env:TEST_MIRROR } }
|
|
$script:uvInstalled = $env:SIMULATE_MISSING_UV -ne '1'
|
|
function Test-CommandExists { param($Command); return $script:uvInstalled }
|
|
function Update-EnvironmentPath {}
|
|
function Invoke-DownloadFile {
|
|
param($Uri, $OutFile, $TimeoutSec)
|
|
if ($Uri -ne 'https://astral.sh/uv/install.ps1') { throw 'Unexpected installer source' }
|
|
Set-Content -LiteralPath $OutFile -Value '# fixture installer'
|
|
return $true
|
|
}
|
|
function powershell.exe {
|
|
Write-Host 'INSTALLER_CALLED'
|
|
if ($env:SIMULATE_INSTALL_FAILURE -eq '1') { $global:LASTEXITCODE = 25; return }
|
|
$script:uvInstalled = $true
|
|
$global:LASTEXITCODE = 0
|
|
}
|
|
function uv {
|
|
if ($env:SIMULATE_SYNC_FAILURE -eq '1' -and $args[0] -eq 'sync') {
|
|
$global:LASTEXITCODE = 9
|
|
return
|
|
}
|
|
& $realUv @args
|
|
}
|
|
try {
|
|
$python = Initialize-ArtemisPython -ProjectRoot $PSScriptRoot
|
|
Write-Host "PYTHON_READY=$python"
|
|
exit 0
|
|
} catch { Write-Host "EXPECTED_ERROR=$_"; exit 1 }
|
|
""",
|
|
encoding="ascii",
|
|
)
|
|
env = dict(os.environ)
|
|
env.update(
|
|
BOOTSTRAP_SCRIPT=str(ROOT / "scripts/bootstrap_python.ps1"),
|
|
SIMULATE_MISSING_UV=str(int(missing_uv)),
|
|
SIMULATE_INSTALL_FAILURE=str(int(installer_failure)),
|
|
SIMULATE_SYNC_FAILURE=str(int(sync_failure)),
|
|
UV_OFFLINE="0" if mirror_index else "1",
|
|
TEST_MIRROR=mirror_index,
|
|
)
|
|
return subprocess.run(
|
|
["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(script)],
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
|
|
|
|
def test_first_setup_and_repeat_reuse_environment(project):
|
|
first = run_bootstrap(project, missing_uv=True)
|
|
assert first.returncode == 0, first.stdout + first.stderr
|
|
assert "INSTALLER_CALLED" in first.stdout
|
|
assert "Creating .venv" in first.stdout
|
|
config = project / ".venv/pyvenv.cfg"
|
|
before = config.stat().st_mtime_ns
|
|
second = run_bootstrap(project)
|
|
assert second.returncode == 0, second.stdout + second.stderr
|
|
assert "INSTALLER_CALLED" not in second.stdout
|
|
assert "Reusing the existing" in second.stdout
|
|
assert "dependencies are already ready" in second.stdout
|
|
assert config.stat().st_mtime_ns == before
|
|
|
|
|
|
def test_installer_failure_stops_before_environment_creation(project):
|
|
result = run_bootstrap(project, missing_uv=True, installer_failure=True)
|
|
assert result.returncode == 1
|
|
assert "uv installation failed" in result.stdout
|
|
assert not (project / ".venv").exists()
|
|
|
|
|
|
def test_dependency_failure_stops_before_launch(project):
|
|
result = run_bootstrap(project, sync_failure=True)
|
|
assert result.returncode == 1
|
|
assert "Dependency installation failed" in result.stdout
|
|
assert "PYTHON_READY=" not in result.stdout
|
|
|
|
|
|
def test_incomplete_environment_is_preserved(project):
|
|
(project / ".venv").mkdir()
|
|
sentinel = project / ".venv/keep.txt"
|
|
sentinel.write_text("existing data")
|
|
result = run_bootstrap(project)
|
|
assert result.returncode == 1
|
|
assert "incomplete" in result.stdout
|
|
assert sentinel.read_text() == "existing data"
|
|
|
|
|
|
@pytest.mark.parametrize("exit_code", [0, 7])
|
|
def test_batch_preserves_exit_code_and_handles_space_and_exclamation_paths(tmp_path, exit_code):
|
|
root = tmp_path / "source with spaces!"
|
|
(root / "scripts").mkdir(parents=True)
|
|
shutil.copy2(ROOT / "start.bat", root / "start.bat")
|
|
(root / "scripts/start.ps1").write_text(
|
|
"param([switch]$SetupOnly)\n"
|
|
"if (-not $SetupOnly) { exit 19 }\n"
|
|
"if ((Get-Location).Path -ne (Split-Path $PSScriptRoot -Parent)) { exit 20 }\n"
|
|
f"exit {exit_code}\n",
|
|
encoding="ascii",
|
|
)
|
|
result = subprocess.run(
|
|
["cmd.exe", "/d", "/c", "call", str(root / "start.bat"), "-SetupOnly"],
|
|
cwd=tmp_path,
|
|
env={**os.environ, "ARTEMIS_NO_PAUSE": "1"},
|
|
capture_output=True,
|
|
timeout=30,
|
|
)
|
|
assert result.returncode == exit_code, result.stdout + result.stderr
|
|
|
|
|
|
@pytest.mark.parametrize("exit_code", [0, 7])
|
|
def test_chinese_batch_prints_complete_lines_and_preserves_exit_code(tmp_path, exit_code):
|
|
root = tmp_path / "source with spaces!"
|
|
root.mkdir()
|
|
shutil.copy2(ROOT / "start-cn.bat", root / "start-cn.bat")
|
|
shutil.copy2(ROOT / "start.bat", root / "start.bat")
|
|
(root / "scripts").mkdir()
|
|
# Exercise the real Chinese banner/help prologue, stopping before installs,
|
|
# update checks or downloads. Chinese now belongs to PowerShell, not CMD.
|
|
prologue = (
|
|
(ROOT / "scripts/start.ps1")
|
|
.read_text(encoding="utf-8-sig")
|
|
.split("# Ensure modern TLS", 1)[0]
|
|
)
|
|
(root / "scripts/start.ps1").write_text(
|
|
prologue + f"\nif ($Mirror -ne 'China') {{ exit 20 }}\nexit {exit_code}\n",
|
|
encoding="utf-8-sig",
|
|
)
|
|
result = subprocess.run(
|
|
["cmd.exe", "/d", "/c", "call", str(root / "start-cn.bat")],
|
|
env={**os.environ, "ARTEMIS_NO_PAUSE": "1"},
|
|
capture_output=True,
|
|
timeout=15,
|
|
)
|
|
assert result.returncode == exit_code, result.stdout + result.stderr
|
|
assert not result.stderr, result.stderr
|
|
assert "镜像地址和下载超时可在" in result.stdout.decode("utf-8")
|
|
help_result = subprocess.run(
|
|
["cmd.exe", "/d", "/c", "call", str(root / "start-cn.bat"), "-Help"],
|
|
capture_output=True,
|
|
timeout=15,
|
|
)
|
|
assert help_result.returncode == 0
|
|
assert not help_result.stderr
|
|
assert "Codex MCP" in help_result.stdout.decode("utf-8")
|