|
1 | | -from contextlib import contextmanager |
| 1 | +from contextlib import contextmanager, redirect_stderr, redirect_stdout |
2 | 2 | from importlib import resources |
| 3 | +from dataclasses import dataclass |
| 4 | +import io |
| 5 | +import os |
3 | 6 | import pathlib |
4 | 7 | import sys |
5 | | -from typing import Generator |
| 8 | +from typing import Generator, cast |
6 | 9 |
|
| 10 | +from pygit2 import Repository |
7 | 11 | import pytest |
8 | 12 |
|
| 13 | +from ruyi.cli.main import main as ruyi_main |
| 14 | +from ruyi.config import GlobalConfig |
9 | 15 | from ruyi.log import RuyiConsoleLogger, RuyiLogger |
10 | | -from ruyi.utils.global_mode import GlobalModeProvider |
| 16 | +from ruyi.ruyipkg.repo import MetadataRepo |
| 17 | +from ruyi.utils.global_mode import EnvGlobalModeProvider, GlobalModeProvider |
11 | 18 |
|
12 | 19 |
|
13 | 20 | class RuyiFileFixtureFactory: |
@@ -158,3 +165,140 @@ def mock_gm() -> MockGlobalModeProvider: |
158 | 165 | def ruyi_logger(mock_gm: GlobalModeProvider) -> RuyiLogger: |
159 | 166 | """Fixture for creating a RuyiLogger instance.""" |
160 | 167 | return RuyiConsoleLogger(mock_gm) |
| 168 | + |
| 169 | + |
| 170 | +@dataclass |
| 171 | +class CLIRunResult: |
| 172 | + exit_code: int |
| 173 | + stdout: str |
| 174 | + stderr: str |
| 175 | + |
| 176 | + |
| 177 | +class MockRepository: |
| 178 | + def __init__(self, root: pathlib.Path) -> None: |
| 179 | + self.workdir = root |
| 180 | + self.path = root |
| 181 | + |
| 182 | + |
| 183 | +class IntegrationTestHarness: |
| 184 | + def __init__( |
| 185 | + self, |
| 186 | + env: dict[str, str], |
| 187 | + repo_root: pathlib.Path, |
| 188 | + repo_url: str, |
| 189 | + repo_branch: str, |
| 190 | + ) -> None: |
| 191 | + self._env = env |
| 192 | + self.repo_root = repo_root |
| 193 | + self.repo_url = repo_url |
| 194 | + self.repo_branch = repo_branch |
| 195 | + |
| 196 | + def __call__(self, *args: str) -> CLIRunResult: |
| 197 | + return self.run(*args) |
| 198 | + |
| 199 | + def run(self, *args: str) -> CLIRunResult: |
| 200 | + argv = ["ruyi", *args] |
| 201 | + stdout_io = io.StringIO() |
| 202 | + stderr_io = io.StringIO() |
| 203 | + with redirect_stdout(stdout_io), redirect_stderr(stderr_io): |
| 204 | + gm = EnvGlobalModeProvider(self._env, argv) |
| 205 | + gm.record_self_exe(argv[0], __file__, argv[0]) |
| 206 | + logger = RuyiConsoleLogger(gm, stdout=stdout_io, stderr=stderr_io) |
| 207 | + gc = GlobalConfig.load_from_config(gm, logger) |
| 208 | + gc.override_repo_dir = str(self.repo_root) |
| 209 | + gc.override_repo_url = self.repo_url |
| 210 | + gc.override_repo_branch = self.repo_branch |
| 211 | + exit_code = ruyi_main(gm, gc, argv) |
| 212 | + return CLIRunResult(exit_code, stdout_io.getvalue(), stderr_io.getvalue()) |
| 213 | + |
| 214 | + def add_package( |
| 215 | + self, |
| 216 | + category: str, |
| 217 | + name: str, |
| 218 | + version: str, |
| 219 | + manifest_toml: str, |
| 220 | + ) -> pathlib.Path: |
| 221 | + pkg_dir = self.repo_root / "manifests" / category / name |
| 222 | + pkg_dir.mkdir(parents=True, exist_ok=True) |
| 223 | + manifest_path = pkg_dir / f"{version}.toml" |
| 224 | + manifest_path.write_text(manifest_toml, encoding="utf-8") |
| 225 | + return manifest_path |
| 226 | + |
| 227 | + |
| 228 | +def _populate_default_packages_index(repo_root: pathlib.Path) -> None: |
| 229 | + repo_root.mkdir(parents=True, exist_ok=True) |
| 230 | + |
| 231 | + config_text = """\ |
| 232 | +ruyi-repo = "v1" |
| 233 | +
|
| 234 | +[[mirrors]] |
| 235 | +id = "ruyi-dist" |
| 236 | +urls = ["https://example.invalid/dist/"] |
| 237 | +""" |
| 238 | + |
| 239 | + (repo_root / "config.toml").write_text(config_text + "\n", encoding="utf-8") |
| 240 | + |
| 241 | + sha_stub = "0" * 64 |
| 242 | + manifest_text = f"""\ |
| 243 | +format = "v1" |
| 244 | +kind = ["source"] |
| 245 | +
|
| 246 | +[metadata] |
| 247 | +desc = "Sample integration package" |
| 248 | +vendor = {{ name = "Ruyi Integration Tests", eula = "" }} |
| 249 | +
|
| 250 | +[[distfiles]] |
| 251 | +name = "sample-src.tar.zst" |
| 252 | +size = 0 |
| 253 | +
|
| 254 | +[distfiles.checksums] |
| 255 | +sha256 = "{sha_stub}" |
| 256 | +""" |
| 257 | + |
| 258 | + manifest_dir = repo_root / "manifests" / "dev-tools" / "sample-cli" |
| 259 | + manifest_dir.mkdir(parents=True, exist_ok=True) |
| 260 | + (manifest_dir / "1.0.0.toml").write_text(manifest_text + "\n", encoding="utf-8") |
| 261 | + |
| 262 | + |
| 263 | +@pytest.fixture |
| 264 | +def ruyi_cli_runner( |
| 265 | + tmp_path: pathlib.Path, |
| 266 | + monkeypatch: pytest.MonkeyPatch, |
| 267 | +) -> IntegrationTestHarness: |
| 268 | + base_dir = tmp_path / "integration-env" |
| 269 | + home_dir = base_dir / "home" |
| 270 | + cache_dir = base_dir / "cache" |
| 271 | + config_dir = base_dir / "config" |
| 272 | + data_dir = base_dir / "data" |
| 273 | + state_dir = base_dir / "state" |
| 274 | + |
| 275 | + for p in (home_dir, cache_dir, config_dir, data_dir, state_dir): |
| 276 | + p.mkdir(parents=True, exist_ok=True) |
| 277 | + |
| 278 | + monkeypatch.setenv("HOME", str(home_dir)) |
| 279 | + monkeypatch.setenv("XDG_CACHE_HOME", str(cache_dir)) |
| 280 | + monkeypatch.setenv("XDG_CONFIG_HOME", str(config_dir)) |
| 281 | + monkeypatch.setenv("XDG_DATA_HOME", str(data_dir)) |
| 282 | + monkeypatch.setenv("XDG_STATE_HOME", str(state_dir)) |
| 283 | + monkeypatch.setenv("RUYI_TELEMETRY_OPTOUT", "1") |
| 284 | + |
| 285 | + repo_root = cache_dir / "ruyi" / "packages-index" |
| 286 | + _populate_default_packages_index(repo_root) |
| 287 | + |
| 288 | + def _ensure_git_repo_stub(self: MetadataRepo) -> Repository: |
| 289 | + if self.repo is None: |
| 290 | + repo_path = pathlib.Path(self.root) |
| 291 | + repo_path.mkdir(parents=True, exist_ok=True) |
| 292 | + self.repo = cast(Repository, MockRepository(repo_path)) |
| 293 | + return self.repo |
| 294 | + |
| 295 | + monkeypatch.setattr(MetadataRepo, "ensure_git_repo", _ensure_git_repo_stub) |
| 296 | + |
| 297 | + env = dict(os.environ) |
| 298 | + |
| 299 | + return IntegrationTestHarness( |
| 300 | + env=env, |
| 301 | + repo_root=repo_root, |
| 302 | + repo_url="https://example.invalid/packages-index.git", |
| 303 | + repo_branch="main", |
| 304 | + ) |
0 commit comments