mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 13:26:48 +00:00
Consolidates NVIDIA vLLM, Apple Silicon, CPU Pearl mining support, CLI/docs, and live H100 validation.
290 lines
9.9 KiB
Python
290 lines
9.9 KiB
Python
"""Tests for openjarvis.mining.cpu_pearl.CpuPearlProvider."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
_AVAIL = "openjarvis.mining._install.pearl_packages_available"
|
|
|
|
|
|
@pytest.fixture
|
|
def darwin_apple_hw():
|
|
"""A HardwareInfo describing an Apple Silicon Mac."""
|
|
from openjarvis.core.config import GpuInfo, HardwareInfo
|
|
|
|
return HardwareInfo(
|
|
platform="darwin",
|
|
cpu_brand="Apple M2 Max",
|
|
cpu_count=12,
|
|
ram_gb=96.0,
|
|
gpu=GpuInfo(vendor="apple", name="M2 Max", vram_gb=96.0, count=1),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def linux_nvidia_hw():
|
|
"""A HardwareInfo describing an H100 box."""
|
|
from openjarvis.core.config import GpuInfo, HardwareInfo
|
|
|
|
return HardwareInfo(
|
|
platform="linux",
|
|
cpu_brand="Intel Xeon",
|
|
cpu_count=64,
|
|
ram_gb=512.0,
|
|
gpu=GpuInfo(
|
|
vendor="nvidia",
|
|
name="H100",
|
|
vram_gb=80.0,
|
|
compute_capability="9.0a",
|
|
count=1,
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def windows_hw():
|
|
"""A HardwareInfo describing a Windows host (unsupported in v1)."""
|
|
from openjarvis.core.config import HardwareInfo
|
|
|
|
return HardwareInfo(
|
|
platform="win32", cpu_brand="x86_64", cpu_count=16, ram_gb=64.0, gpu=None
|
|
)
|
|
|
|
|
|
def test_detect_supported_on_apple_silicon_when_packages_present(darwin_apple_hw):
|
|
from openjarvis.mining.cpu_pearl import CpuPearlProvider
|
|
|
|
with patch(_AVAIL, return_value=True):
|
|
cap = CpuPearlProvider.detect(darwin_apple_hw, engine_id="ollama", model="any")
|
|
assert cap.supported is True
|
|
assert cap.reason is None
|
|
|
|
|
|
def test_detect_supported_on_linux_too(linux_nvidia_hw):
|
|
"""v1 cpu-pearl is engine-independent and works on linux too."""
|
|
from openjarvis.mining.cpu_pearl import CpuPearlProvider
|
|
|
|
with patch(_AVAIL, return_value=True):
|
|
cap = CpuPearlProvider.detect(
|
|
linux_nvidia_hw, engine_id="anything", model="any"
|
|
)
|
|
assert cap.supported is True
|
|
|
|
|
|
def test_detect_unsupported_on_windows(windows_hw):
|
|
from openjarvis.mining.cpu_pearl import CpuPearlProvider
|
|
|
|
with patch(_AVAIL, return_value=True):
|
|
cap = CpuPearlProvider.detect(windows_hw, engine_id="any", model="any")
|
|
assert cap.supported is False
|
|
assert "win32" in cap.reason.lower() or "windows" in cap.reason.lower()
|
|
|
|
|
|
def test_detect_unsupported_when_pearl_not_installed(darwin_apple_hw):
|
|
from openjarvis.mining.cpu_pearl import CpuPearlProvider
|
|
|
|
with patch(_AVAIL, return_value=False):
|
|
cap = CpuPearlProvider.detect(darwin_apple_hw, engine_id="any", model="any")
|
|
assert cap.supported is False
|
|
assert "mining-pearl-cpu" in cap.reason
|
|
|
|
|
|
def test_detect_engine_independent(darwin_apple_hw):
|
|
"""v1 detect() does NOT inspect engine_id — supported regardless of engine."""
|
|
from openjarvis.mining.cpu_pearl import CpuPearlProvider
|
|
|
|
with patch(_AVAIL, return_value=True):
|
|
for engine in ("ollama", "llamacpp", "vllm", "mlx", "anthropic-cloud", ""):
|
|
cap = CpuPearlProvider.detect(
|
|
darwin_apple_hw, engine_id=engine, model="any"
|
|
)
|
|
assert cap.supported is True, f"engine_id={engine!r} should be supported"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Task 9: lifecycle tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def cpu_pearl_config():
|
|
"""A minimal MiningConfig for cpu-pearl."""
|
|
from openjarvis.mining._stubs import MiningConfig, SoloTarget
|
|
|
|
return MiningConfig(
|
|
provider="cpu-pearl",
|
|
wallet_address="prl1qtest",
|
|
submit_target=SoloTarget(pearld_rpc_url="http://localhost:44107"),
|
|
fee_bps=0,
|
|
fee_payout_address=None,
|
|
extra={
|
|
"gateway_host": "127.0.0.1",
|
|
"gateway_port": 18337,
|
|
"metrics_port": 19109,
|
|
"pearld_rpc_url": "http://localhost:44107",
|
|
"pearld_rpc_user": "rpcuser",
|
|
"pearld_rpc_password_env": "TEST_PEARLD_PASSWORD",
|
|
"m": 256,
|
|
"n": 128,
|
|
"k": 1024,
|
|
"rank": 32,
|
|
},
|
|
)
|
|
|
|
|
|
def test_start_writes_sidecar_and_is_running(cpu_pearl_config, tmp_path, monkeypatch):
|
|
"""After start(), is_running() returns True and sidecar JSON is on disk."""
|
|
from openjarvis.mining.cpu_pearl import CpuPearlProvider
|
|
|
|
sidecar = tmp_path / "mining.json"
|
|
log_dir = tmp_path / "logs"
|
|
monkeypatch.setattr("openjarvis.mining.cpu_pearl._sidecar_path", lambda: sidecar)
|
|
monkeypatch.setattr("openjarvis.mining.cpu_pearl._log_dir", lambda: log_dir)
|
|
monkeypatch.setenv("TEST_PEARLD_PASSWORD", "secret")
|
|
|
|
fake_launcher = MagicMock()
|
|
fake_launcher.is_running.return_value = True
|
|
fake_launcher.pids.return_value = (11111, 22222)
|
|
|
|
_target = "openjarvis.mining.cpu_pearl.PearlSubprocessLauncher"
|
|
with patch(_target, return_value=fake_launcher):
|
|
provider = CpuPearlProvider()
|
|
asyncio.run(provider.start(cpu_pearl_config))
|
|
assert provider.is_running() is True
|
|
|
|
assert sidecar.exists()
|
|
payload = json.loads(sidecar.read_text())
|
|
assert payload["provider"] == "cpu-pearl"
|
|
assert payload["wallet_address"] == "prl1qtest"
|
|
assert payload["gateway_pid"] == 11111
|
|
assert payload["miner_loop_pid"] == 22222
|
|
# Secret must NOT be in sidecar
|
|
assert "secret" not in sidecar.read_text()
|
|
|
|
|
|
def test_stop_terminates_and_removes_sidecar(cpu_pearl_config, tmp_path, monkeypatch):
|
|
"""After stop(), is_running() returns False, sidecar is removed."""
|
|
from openjarvis.mining.cpu_pearl import CpuPearlProvider
|
|
|
|
sidecar = tmp_path / "mining.json"
|
|
log_dir = tmp_path / "logs"
|
|
monkeypatch.setattr("openjarvis.mining.cpu_pearl._sidecar_path", lambda: sidecar)
|
|
monkeypatch.setattr("openjarvis.mining.cpu_pearl._log_dir", lambda: log_dir)
|
|
monkeypatch.setenv("TEST_PEARLD_PASSWORD", "secret")
|
|
|
|
fake_launcher = MagicMock()
|
|
fake_launcher.is_running.side_effect = [True, False]
|
|
fake_launcher.pids.return_value = (11111, 22222)
|
|
|
|
_target = "openjarvis.mining.cpu_pearl.PearlSubprocessLauncher"
|
|
with patch(_target, return_value=fake_launcher):
|
|
provider = CpuPearlProvider()
|
|
asyncio.run(provider.start(cpu_pearl_config))
|
|
asyncio.run(provider.stop())
|
|
fake_launcher.stop.assert_called_once()
|
|
assert provider.is_running() is False
|
|
assert not sidecar.exists()
|
|
|
|
|
|
def test_stats_returns_zero_stats_when_not_running():
|
|
"""stats() before start() returns a MiningStats with provider_id and zeros."""
|
|
from openjarvis.mining.cpu_pearl import CpuPearlProvider
|
|
|
|
provider = CpuPearlProvider()
|
|
stats = provider.stats()
|
|
assert stats.provider_id == "cpu-pearl"
|
|
assert stats.shares_submitted == 0
|
|
assert stats.shares_accepted == 0
|
|
|
|
|
|
def test_parse_gateway_metrics_extracts_counters():
|
|
"""The Prometheus parser extracts the metric names we expect."""
|
|
from openjarvis.mining.cpu_pearl import _parse_gateway_metrics
|
|
|
|
sample = """\
|
|
# HELP pearl_gateway_shares_submitted_total Total shares submitted.
|
|
# TYPE pearl_gateway_shares_submitted_total counter
|
|
pearl_gateway_shares_submitted_total 42
|
|
# HELP pearl_gateway_shares_accepted_total Total shares accepted.
|
|
# TYPE pearl_gateway_shares_accepted_total counter
|
|
pearl_gateway_shares_accepted_total 40
|
|
pearl_gateway_blocks_found_total 1
|
|
"""
|
|
stats = _parse_gateway_metrics(sample, provider_id="cpu-pearl")
|
|
assert stats.provider_id == "cpu-pearl"
|
|
assert stats.shares_submitted == 42
|
|
assert stats.shares_accepted == 40
|
|
assert stats.blocks_found == 1
|
|
|
|
|
|
@pytest.mark.live
|
|
@pytest.mark.slow
|
|
def test_provider_runs_end_to_end_on_this_host(tmp_path, monkeypatch):
|
|
"""Live test: start provider, run for ~30 s, assert is_running was True.
|
|
|
|
Requires that py-pearl-mining, miner-base, and pearl-gateway are installed
|
|
in the venv (e.g., via the build-from-pin path) AND that pearld is running
|
|
locally. Otherwise the test is skipped via importorskip.
|
|
|
|
Use a non-default port pair (18337/19109) so it doesn't collide with a real
|
|
cpu-pearl session.
|
|
"""
|
|
pytest.importorskip("pearl_mining")
|
|
pytest.importorskip("pearl_gateway")
|
|
|
|
from openjarvis.mining._stubs import MiningConfig, SoloTarget
|
|
from openjarvis.mining.cpu_pearl import CpuPearlProvider
|
|
|
|
sidecar = tmp_path / "mining.json"
|
|
log_dir = tmp_path / "logs"
|
|
monkeypatch.setattr("openjarvis.mining.cpu_pearl._sidecar_path", lambda: sidecar)
|
|
monkeypatch.setattr("openjarvis.mining.cpu_pearl._log_dir", lambda: log_dir)
|
|
monkeypatch.setenv("TEST_PEARLD_PASSWORD", "test")
|
|
|
|
cfg = MiningConfig(
|
|
provider="cpu-pearl",
|
|
wallet_address="prl1q" + "0" * 32,
|
|
submit_target=SoloTarget(pearld_rpc_url="http://localhost:44107"),
|
|
fee_bps=0,
|
|
fee_payout_address=None,
|
|
extra={
|
|
"gateway_host": "127.0.0.1",
|
|
"gateway_port": 18337,
|
|
"metrics_port": 19109,
|
|
"pearld_rpc_url": "http://localhost:44107",
|
|
"pearld_rpc_user": "rpcuser",
|
|
"pearld_rpc_password_env": "TEST_PEARLD_PASSWORD",
|
|
},
|
|
)
|
|
|
|
provider = CpuPearlProvider()
|
|
asyncio.run(provider.start(cfg))
|
|
still_running_at_end = False
|
|
try:
|
|
import time as _time
|
|
|
|
deadline = _time.monotonic() + 30
|
|
saw_running = False
|
|
while _time.monotonic() < deadline:
|
|
if provider.is_running():
|
|
saw_running = True
|
|
else:
|
|
if saw_running:
|
|
break
|
|
_time.sleep(1.0)
|
|
still_running_at_end = provider.is_running()
|
|
finally:
|
|
asyncio.run(provider.stop())
|
|
|
|
if log_dir.exists():
|
|
for log_file in log_dir.glob("*.log"):
|
|
print(f"--- {log_file.name} ---")
|
|
print(log_file.read_text()[:2000])
|
|
|
|
assert saw_running, "provider never reported is_running"
|
|
assert still_running_at_end, "provider exited before the live smoke window ended"
|