forked from luopingyi/xc_agent_platform_demo
live: EXTERNAL_SERVICE_TOKEN precedence, baked A/B candidates, model|gpu ledger from task/page modelId+gpuType, string total fix, Iluvatar/MTT enums (59 tests)
This commit is contained in:
@@ -45,7 +45,7 @@ class TestConfigParams(unittest.TestCase):
|
||||
yaml, meta = build_config_params_with_meta('壁砺')
|
||||
self.assertEqual(meta['configParams_fallback_reason'], 'GAP: transport/timeout')
|
||||
# Unmapped GPU → no API call
|
||||
for gpu in ('天垓150', '智铠 mrv-100', 'MetaX_c-500', '<GPU>', None):
|
||||
for gpu in ('MetaX', 'MetaX_c-500', '<GPU>', None):
|
||||
with patch('src.config_params.resolve_token', return_value='synthetic'), patch('src.config_params.build_config') as client:
|
||||
_, meta = build_config_params_with_meta(gpu)
|
||||
self.assertIn('GAP: gpu enum unmapped', meta['configParams_fallback_reason'])
|
||||
|
||||
@@ -14,7 +14,7 @@ class TestLiveCycle(unittest.TestCase):
|
||||
def setUp(self):
|
||||
directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(directory.cleanup)
|
||||
self.env = patch.dict(os.environ, {'STRATEGY_ID': 'sid', 'LIVE_STATE_FILE': directory.name + '/state.json', 'CONFIG_PARAMS_SOURCE': 'sample'}, clear=True)
|
||||
self.env = patch.dict(os.environ, {'STRATEGY_ID': 'sid', 'LIVE_STATE_FILE': directory.name + '/state.json', 'CONFIG_PARAMS_SOURCE': 'sample', 'XC_TOKEN': 'synthetic-secret'}, clear=True)
|
||||
self.env.start()
|
||||
self.addCleanup(self.env.stop)
|
||||
_submitted.clear()
|
||||
@@ -64,10 +64,10 @@ class TestLiveCycle(unittest.TestCase):
|
||||
self.cycle([candidate('ok')], ())
|
||||
self.client.submit.assert_called_once()
|
||||
self.assertIn('banned:', self.logs.getvalue())
|
||||
self.assertIn('duplicate', self.logs.getvalue())
|
||||
self.assertIn('all_gpus_done_or_unmapped', self.logs.getvalue())
|
||||
|
||||
def test_remote_duplicate_and_transport_reservation(self):
|
||||
run_cycle([candidate('remote')], self.client, lambda: (1, 'ok', {'org/remote'}))
|
||||
run_cycle([candidate('remote')], self.client, lambda: (1, 'ok', {('org/remote', 'Biren_166m')}))
|
||||
self.client.submit.assert_not_called()
|
||||
self.client.submit.side_effect = OSError('do not log')
|
||||
self.cycle()
|
||||
@@ -76,7 +76,8 @@ class TestLiveCycle(unittest.TestCase):
|
||||
self.assertNotIn('do not log', self.logs.getvalue())
|
||||
|
||||
def test_missing_token_real_reader(self):
|
||||
run_cycle([candidate('a')], self.client)
|
||||
with patch.dict(os.environ, {'XC_TOKEN': ''}):
|
||||
run_cycle([candidate('a')], self.client)
|
||||
self.client.submit.assert_not_called()
|
||||
self.assertIn('missing/invalid_token', self.logs.getvalue())
|
||||
|
||||
@@ -105,5 +106,5 @@ class TestLiveCycle(unittest.TestCase):
|
||||
rows = [dict(model_address='org/unmapped', targetGpu='unknown'), candidate('<MODEL>')]
|
||||
self.cycle(rows, ())
|
||||
self.client.submit.assert_not_called()
|
||||
self.assertIn('gpu_unmapped', self.logs.getvalue())
|
||||
self.assertIn('all_gpus_done_or_unmapped', self.logs.getvalue())
|
||||
self.assertIn('placeholder', self.logs.getvalue())
|
||||
|
||||
71
tests/test_live_ledger.py
Normal file
71
tests/test_live_ledger.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
from src.live_cycle import run_cycle, _submitted
|
||||
from src.ledger import load_baked, rebuild
|
||||
from src.banned import banned_rule
|
||||
from src.task_add_client import resolve_token_with_source
|
||||
|
||||
|
||||
class TestLedger(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(temp.cleanup)
|
||||
self.state = Path(temp.name) / 'state.json'
|
||||
env = patch.dict(os.environ, dict(STRATEGY_ID='sid', XC_TOKEN='synthetic-credential', CONFIG_PARAMS_SOURCE='sample', LIVE_STATE_FILE=str(self.state)), clear=True)
|
||||
env.start()
|
||||
self.addCleanup(env.stop)
|
||||
_submitted.clear()
|
||||
self.addCleanup(_submitted.clear)
|
||||
net = patch('urllib.request.urlopen', side_effect=AssertionError('network forbidden'))
|
||||
net.start()
|
||||
self.addCleanup(net.stop)
|
||||
self.client = Mock()
|
||||
self.client.submit.return_value = dict(http_status=200, platform_code=0)
|
||||
|
||||
def cycle(self, remote=(), model='unit/model', gpus=None):
|
||||
return run_cycle([dict(model_address=model, try_next_gpus=gpus or ['壁砺', '天垓100'])], self.client, lambda: (0, 'ok', set(remote)))
|
||||
|
||||
def test_pair_and_restart(self):
|
||||
self.cycle([('unit/model', 'Biren_166m')])
|
||||
self.assertEqual(self.client.submit.call_args.args[0]['targetGpu'], 'Iluvatar_bi-100')
|
||||
self.assertEqual(json.loads(self.state.read_text()), ['unit/model|Iluvatar_bi-100'])
|
||||
_submitted.clear()
|
||||
self.cycle([('unit/model', 'Biren_166m')])
|
||||
self.client.submit.assert_called_once()
|
||||
|
||||
def test_baked_and_blocked(self):
|
||||
pairs, blocked = load_baked()
|
||||
model, gpu = next(iter(pairs))
|
||||
self.cycle(model=model, gpus=[gpu])
|
||||
self.cycle(model=next(iter(blocked)))
|
||||
self.client.submit.assert_not_called()
|
||||
|
||||
def test_failure_and_missing_token(self):
|
||||
result = run_cycle([dict(model_address='unit/model', try_next_gpus=['壁砺'])], self.client, lambda: (None, 'failed', set()))
|
||||
self.assertEqual(result[-1]['reason'], 'ledger_rebuild_failed')
|
||||
with patch.dict(os.environ, {'XC_TOKEN': ''}):
|
||||
self.cycle()
|
||||
self.client.submit.assert_not_called()
|
||||
|
||||
def test_source_and_no_value_logs(self):
|
||||
import io
|
||||
with patch.dict(os.environ, {'EXTERNAL_SERVICE_TOKEN': 'platform-credential', 'XC_TOKEN_FILE': '/nonexistent'}), patch('sys.stderr', new_callable=io.StringIO) as log:
|
||||
self.assertEqual(resolve_token_with_source(), ('platform-credential', 'EXTERNAL_SERVICE_TOKEN'))
|
||||
self.cycle()
|
||||
self.assertEqual(log.getvalue().count('token source=EXTERNAL_SERVICE_TOKEN'), 1)
|
||||
self.assertNotIn('platform-credential', log.getvalue())
|
||||
with patch.dict(os.environ, {'EXTERNAL_SERVICE_TOKEN': '<TOKEN>', 'XC_TOKEN': 'placeholder', 'MODELHUB_XC_TOKEN': 'alias-credential'}):
|
||||
self.assertEqual(resolve_token_with_source()[1], 'MODELHUB_XC_TOKEN')
|
||||
|
||||
def test_candidates_and_rebuild(self):
|
||||
rows = json.loads(Path('src/data/candidates.json').read_text())
|
||||
self.assertGreater(len(rows), 0)
|
||||
self.assertEqual([r['section'] for r in rows], sorted(r['section'] for r in rows))
|
||||
self.assertFalse(any(banned_rule(r['model_address']) for r in rows))
|
||||
count, pairs, blocked, sizes = rebuild(self.state, lambda: (0, 'ok', {('unit/model', 'Biren_166m')}))
|
||||
self.assertIn(('unit/model', 'Biren_166m'), pairs)
|
||||
self.assertEqual(sizes[0], 1)
|
||||
@@ -14,17 +14,17 @@ class TestLiveQueue(unittest.TestCase):
|
||||
return r
|
||||
|
||||
def test_pagination_and_status(self):
|
||||
pages = [self.response({'code': 0, 'data': {'total': 3, 'records': [
|
||||
{'status': 'waiting', 'modelAddress': 'org/a'}, {'status': 'success', 'verifyResult': -1}]}}),
|
||||
self.response({'code': 0, 'data': {'total': 3, 'records': [{'status': '等待中'}]}})]
|
||||
pages = [self.response({'code': 0, 'data': {'total': '3', 'records': [
|
||||
{'status': 'waiting', 'modelId': 'org/a', 'gpuType': 'Biren_166m'}, {'status': 'success', 'verifyResult': -1}]}}),
|
||||
self.response({'code': 0, 'data': {'total': '3', 'records': [{'status': '等待中'}]}})]
|
||||
with patch.dict(os.environ, {'XC_TOKEN': 'synthetic-secret'}, clear=True), patch('urllib.request.urlopen', side_effect=pages) as call:
|
||||
count, why, models = read_queue()
|
||||
self.assertEqual(count, 2)
|
||||
self.assertIn('org/a', models)
|
||||
self.assertIn(('org/a', 'Biren_166m'), models)
|
||||
self.assertIn('current=2', call.call_args.args[0].full_url)
|
||||
|
||||
def test_fail_closed(self):
|
||||
for body in ({'code': 40100}, {'code': 40101}, {'code': 0, 'data': {'total': 1, 'records': []}}, []):
|
||||
for body in ({'code': 0, 'data': {'total': 'bad', 'records': []}}, {'code': 40100}, {'code': 40101}, {'code': 0, 'data': {'total': 1, 'records': []}}, []):
|
||||
with patch.dict(os.environ, {'XC_TOKEN': 'synthetic-secret'}, clear=True), patch('urllib.request.urlopen', return_value=self.response(body)):
|
||||
self.assertIsNone(read_queue()[0])
|
||||
with patch.dict(os.environ, {}, clear=True), patch('urllib.request.urlopen') as call:
|
||||
|
||||
@@ -9,7 +9,6 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -17,36 +16,36 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
class TestSigterm(unittest.TestCase):
|
||||
def test_sigterm_exits_quickly(self) -> None:
|
||||
port = "19090"
|
||||
env = os.environ.copy()
|
||||
env["STRATEGY_ID"] = "sigterm-test"
|
||||
env["ALLOW_LIVE_SUBMIT"] = "0"
|
||||
env["PORT"] = port
|
||||
env["HOST"] = "127.0.0.1"
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "src.server"],
|
||||
[sys.executable, '-c', """
|
||||
from unittest.mock import patch
|
||||
import src.server as server
|
||||
class OfflineServer:
|
||||
def __init__(self, *args): pass
|
||||
def serve_forever(self, **kwargs): server._shutdown.wait(10)
|
||||
def shutdown(self): pass
|
||||
def server_close(self): pass
|
||||
with patch.object(server, 'ThreadingHTTPServer', OfflineServer):
|
||||
server.main()
|
||||
"""],
|
||||
cwd=str(ROOT),
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
try:
|
||||
# wait until health is up
|
||||
url = f"http://127.0.0.1:{port}/health"
|
||||
deadline = time.time() + 5
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=0.5) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
if resp.status == 200 and data.get("status") == "ok":
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
if proc.poll() is not None:
|
||||
self.fail(f"server exited before health check: {proc.stderr.read().decode(errors='replace')}")
|
||||
self.fail("server did not become healthy in time")
|
||||
# Read readiness from stderr; no sockets or network calls.
|
||||
import selectors
|
||||
selector = selectors.DefaultSelector()
|
||||
self.addCleanup(selector.close)
|
||||
selector.register(proc.stderr, selectors.EVENT_READ)
|
||||
self.assertTrue(selector.select(timeout=5), 'server readiness timeout')
|
||||
self.assertIn(b'Listening on', proc.stderr.readline())
|
||||
|
||||
t0 = time.time()
|
||||
proc.send_signal(signal.SIGTERM)
|
||||
|
||||
Reference in New Issue
Block a user