|
35 | 35 | LAST_LOGS = "/tmp/last_test_log_dir.txt" |
36 | 36 |
|
37 | 37 |
|
| 38 | +def _download_minio_binary(dest: Path): |
| 39 | + """Download MinIO binary to dest if not already cached. |
| 40 | +
|
| 41 | + Downloads to a temporary file first, then renames atomically to avoid |
| 42 | + leaving a corrupt binary on interrupted downloads. |
| 43 | + """ |
| 44 | + import platform |
| 45 | + import urllib.request |
| 46 | + |
| 47 | + system = platform.system().lower() |
| 48 | + arch = platform.machine() |
| 49 | + arch_map = {"x86_64": "amd64", "aarch64": "arm64", "arm64": "arm64"} |
| 50 | + arch = arch_map.get(arch, arch) |
| 51 | + url = f"https://dl.min.io/server/minio/release/{system}-{arch}/minio" |
| 52 | + logging.info(f"Downloading MinIO binary from {url}") |
| 53 | + tmp_dest = dest.with_suffix(".tmp") |
| 54 | + try: |
| 55 | + urllib.request.urlretrieve(url, tmp_dest) |
| 56 | + tmp_dest.chmod(0o755) |
| 57 | + tmp_dest.rename(dest) |
| 58 | + except Exception: |
| 59 | + tmp_dest.unlink(missing_ok=True) |
| 60 | + raise |
| 61 | + |
| 62 | + |
| 63 | +def _start_minio_server(endpoint): |
| 64 | + """Start MinIO subprocess and configure env vars for S3 tests.""" |
| 65 | + import boto3 |
| 66 | + from urllib.parse import urlparse |
| 67 | + |
| 68 | + cache_dir = Path.home() / ".cache" / "dragonfly-tests" |
| 69 | + cache_dir.mkdir(parents=True, exist_ok=True) |
| 70 | + minio_bin = cache_dir / "minio" |
| 71 | + |
| 72 | + if not minio_bin.exists(): |
| 73 | + _download_minio_binary(minio_bin) |
| 74 | + |
| 75 | + parsed = urlparse(endpoint) |
| 76 | + address = f":{parsed.port or 9000}" |
| 77 | + |
| 78 | + data_dir = Path(mkdtemp(prefix="minio_data_")) |
| 79 | + minio_log = data_dir / "minio.log" |
| 80 | + log_file = open(minio_log, "w") |
| 81 | + proc = subprocess.Popen( |
| 82 | + [str(minio_bin), "server", str(data_dir), "--address", address], |
| 83 | + env={**os.environ, "MINIO_ROOT_USER": "minioadmin", "MINIO_ROOT_PASSWORD": "minioadmin"}, |
| 84 | + stdout=log_file, |
| 85 | + stderr=subprocess.STDOUT, |
| 86 | + ) |
| 87 | + |
| 88 | + bucket = "dragonfly-test" |
| 89 | + s3 = boto3.client( |
| 90 | + "s3", |
| 91 | + endpoint_url=endpoint, |
| 92 | + aws_access_key_id="minioadmin", |
| 93 | + aws_secret_access_key="minioadmin", |
| 94 | + region_name="us-east-1", |
| 95 | + ) |
| 96 | + |
| 97 | + for attempt in range(30): |
| 98 | + try: |
| 99 | + s3.create_bucket(Bucket=bucket) |
| 100 | + break |
| 101 | + except Exception: |
| 102 | + if proc.poll() is not None: |
| 103 | + log_file.close() |
| 104 | + logs = minio_log.read_text() |
| 105 | + shutil.rmtree(data_dir, ignore_errors=True) |
| 106 | + raise RuntimeError( |
| 107 | + f"MinIO process exited with code {proc.returncode}.\nLogs:\n{logs}" |
| 108 | + ) |
| 109 | + time.sleep(1) |
| 110 | + else: |
| 111 | + proc.terminate() |
| 112 | + log_file.close() |
| 113 | + logs = minio_log.read_text() |
| 114 | + shutil.rmtree(data_dir, ignore_errors=True) |
| 115 | + raise RuntimeError(f"MinIO did not become ready in time.\nLogs:\n{logs}") |
| 116 | + |
| 117 | + log_file.close() |
| 118 | + os.environ["DRAGONFLY_S3_BUCKET"] = bucket |
| 119 | + os.environ["AWS_ACCESS_KEY_ID"] = "minioadmin" |
| 120 | + os.environ["AWS_SECRET_ACCESS_KEY"] = "minioadmin" |
| 121 | + os.environ["AWS_ENDPOINT_URL"] = endpoint |
| 122 | + |
| 123 | + return proc, data_dir |
| 124 | + |
| 125 | + |
| 126 | +_minio_proc = None |
| 127 | +_minio_data_dir = None |
| 128 | + |
| 129 | + |
38 | 130 | # runs on pytest start |
39 | 131 | def pytest_configure(config): |
| 132 | + global _minio_proc, _minio_data_dir |
| 133 | + |
40 | 134 | # clean everything |
41 | 135 | if os.path.exists(FAILED_PATH): |
42 | 136 | shutil.rmtree(FAILED_PATH) |
43 | 137 | if os.path.exists(BASE_LOG_DIR): |
44 | 138 | shutil.rmtree(BASE_LOG_DIR) |
45 | 139 |
|
| 140 | + # Start MinIO if S3_ENDPOINT is set (must happen before test collection |
| 141 | + # so that @pytest.mark.skipif checking DRAGONFLY_S3_BUCKET sees it) |
| 142 | + endpoint = os.environ.get("S3_ENDPOINT") |
| 143 | + if endpoint: |
| 144 | + _minio_proc, _minio_data_dir = _start_minio_server(endpoint) |
| 145 | + |
| 146 | + |
| 147 | +def pytest_unconfigure(config): |
| 148 | + global _minio_proc, _minio_data_dir |
| 149 | + |
| 150 | + if _minio_proc is not None: |
| 151 | + _minio_proc.terminate() |
| 152 | + try: |
| 153 | + _minio_proc.wait(timeout=10) |
| 154 | + except subprocess.TimeoutExpired: |
| 155 | + _minio_proc.kill() |
| 156 | + _minio_proc.wait() |
| 157 | + _minio_proc = None |
| 158 | + |
| 159 | + if _minio_data_dir is not None: |
| 160 | + shutil.rmtree(_minio_data_dir, ignore_errors=True) |
| 161 | + _minio_data_dir = None |
| 162 | + |
46 | 163 |
|
47 | 164 | @pytest.fixture(scope="class") |
48 | 165 | def df_log_dir(request): |
|
0 commit comments