Files
chaos d10009d639 Initial commit: grok-free-register-oss
Open-source Grok free registration CLI, xai_enroller auth pipeline,
local auth service, tests and docs.
2026-07-16 21:04:05 +08:00

318 lines
11 KiB
Python

"""
Fake 服务 + 测试工具
FakeTurnstile — 可控 T 生成器:可注入延迟、失败、取消点
FakeEmailService — 可控 Q 提供者:可注入延迟、超时、验证码
FakeConsumer — 可控 C 消费器:可注入延迟、失败
Conservation — slot 守恒断言器
CancelPoint — 在指定 await 边界注入取消
"""
import asyncio
import time
import dataclasses
from typing import Optional, Callable, Any
# ═══════════════════════════════════════════
# Fake T 生成器 (替代 solve_one_turnstile)
# ═══════════════════════════════════════════
class FakeTurnstile:
"""可控 token 生成器。
config:
delay: 生成延迟(秒)
fail_rate: 0.0~1.0,随机失败概率
token_seq: 预设 token 序列,None 则自动递增
"""
def __init__(self, delay=0.01, fail_rate=0.0, token_seq=None):
self.delay = delay
self._fail_rate = fail_rate
self._seq = token_seq or []
self._idx = 0
self._call_count = 0
self._fail_count = 0
self._cancel_at: Optional[int] = None # 第 N 次调用时注入取消
def set_cancel_at(self, n: int):
"""第 n 次调用时注入 CancelledError。"""
self._cancel_at = n
async def generate(self) -> Optional[str]:
"""生成一个 token。返回 None 表示失败,raise CancelledError 表示取消。"""
self._call_count += 1
if self._cancel_at is not None and self._call_count == self._cancel_at:
raise asyncio.CancelledError(f'FakeTurnstile cancel at call {self._call_count}')
await asyncio.sleep(self.delay)
if self._fail_count < self._fail_rate * self._call_count:
return None
# 简单随机: fail_rate 作为概率
import random
if random.random() < self._fail_rate:
self._fail_count += 1
return None
if self._idx < len(self._seq):
tok = self._seq[self._idx]
self._idx += 1
return tok
self._idx += 1
return f'fake_token_{self._idx}'
@property
def call_count(self):
return self._call_count
# ═══════════════════════════════════════════
# Fake 邮箱服务 (替代 create_email + poll_code)
# ═══════════════════════════════════════════
@dataclasses.dataclass
class FakeEmailResult:
handle: str
email: str
password: str
code: Optional[str] = None # None = 轮询超时
create_delay: float = 0.01
poll_delay: float = 0.01
class FakeEmailService:
"""可控邮箱 + 验证码服务。
results: 预设结果队列,每次 create+poll 消费一个。
"""
def __init__(self, results=None):
self._results = list(results or [])
self._idx = 0
self._create_count = 0
self._poll_count = 0
self._cancel_create_at: Optional[int] = None
self._cancel_poll_at: Optional[int] = None
def set_cancel_create_at(self, n: int):
self._cancel_create_at = n
def set_cancel_poll_at(self, n: int):
self._cancel_poll_at = n
async def create_email(self):
"""创建邮箱。返回 (handle, email, password)。"""
self._create_count += 1
if self._cancel_create_at and self._create_count == self._cancel_create_at:
raise asyncio.CancelledError(f'FakeEmail.create cancel at {self._create_count}')
if self._idx < len(self._results):
r = self._results[self._idx]
await asyncio.sleep(r.create_delay)
return r.handle, r.email, r.password
self._idx += 1
n = self._idx
await asyncio.sleep(0.01)
return f'handle_{n}', f'user_{n}@test.com', f'pass_{n}'
async def poll_code(self, handle: str, max_wait=90):
"""轮询验证码。返回 code 或 None。"""
self._poll_count += 1
if self._cancel_poll_at and self._poll_count == self._cancel_poll_at:
raise asyncio.CancelledError(f'FakeEmail.poll cancel at {self._poll_count}')
# 找匹配的预设结果
for r in self._results:
if r.handle == handle:
await asyncio.sleep(r.poll_delay)
return r.code
await asyncio.sleep(0.01)
return '000000'
@property
def create_count(self):
return self._create_count
@property
def poll_count(self):
return self._poll_count
# ═══════════════════════════════════════════
# Fake 注册 API (替代 grpc_verify + server_action_register)
# ═══════════════════════════════════════════
class FakeRegisterAPI:
"""可控注册 API。"""
def __init__(self, success_rate=1.0, delay=0.01):
self.success_rate = success_rate
self.delay = delay
self._register_count = 0
self._cancel_at: Optional[int] = None
self._fail_at: Optional[int] = None
def set_cancel_at(self, n: int):
self._cancel_at = n
def set_fail_at(self, n: int):
self._fail_at = n
async def register(self, email, password, code, token) -> Optional[str]:
"""执行注册。返回 SSO 或 None。"""
self._register_count += 1
if self._cancel_at and self._register_count == self._cancel_at:
raise asyncio.CancelledError(f'FakeRegister cancel at {self._register_count}')
if self._fail_at and self._register_count == self._fail_at:
return None
await asyncio.sleep(self.delay)
import random
if random.random() < self.success_rate:
return f'sso_{email}_{int(time.time())}'
return None
@property
def register_count(self):
return self._register_count
# ═══════════════════════════════════════════
# Slot 守恒断言器
# ═══════════════════════════════════════════
class Conservation:
"""检查 T/Q slot 守恒不变量。
守恒公式:
slot_total = free_slots + inventory_depth + pairleased_held + worker_held
"""
def __init__(self, t_slot_sem, q_slot_sem, q_pending_sem, inventory):
self.t_slot_sem = t_slot_sem
self.q_slot_sem = q_slot_sem
self.q_pending_sem = q_pending_sem
self.inventory = inventory
# 追踪 Worker 持有的未发布资源
self._worker_held_t = 0
self._worker_held_q = 0
def worker_acquire_t(self):
self._worker_held_t += 1
def worker_release_t(self):
self._worker_held_t -= 1
def worker_acquire_q(self):
self._worker_held_q += 1
def worker_release_q(self):
self._worker_held_q -= 1
def check_t_conservation(self, t_slot_total: int, pairleased_t: int = 0):
"""检查 T slot 守恒。
Args:
t_slot_total: T_Slot_Sem 初始容量
pairleased_t: 当前 PairLease 持有的 T 数量
"""
free = self.t_slot_sem._value
inv = self.inventory.t_depth
held = self._worker_held_t
total = free + inv + pairleased_t + held
assert total == t_slot_total, (
f'T slot 守恒违反: free({free}) + inv({inv}) + pairleased({pairleased_t}) '
f'+ worker_held({held}) = {total} != total({t_slot_total})'
)
def check_q_conservation(self, q_slot_total: int, pairleased_q: int = 0):
"""检查 Q slot 守恒。"""
free = self.q_slot_sem._value
inv = self.inventory.q_depth
held = self._worker_held_q
total = free + inv + pairleased_q + held
assert total == q_slot_total, (
f'Q slot 守恒违反: free({free}) + inv({inv}) + pairleased({pairleased_q}) '
f'+ worker_held({held}) = {total} != total({q_slot_total})'
)
def check_pending_conservation(self, pending_total: int, in_flight: int = 0):
"""检查 Q pending 守恒。"""
free = self.q_pending_sem._value
total = free + in_flight
assert total == pending_total, (
f'Q pending 守恒违反: free({free}) + in_flight({in_flight}) = {total} '
f'!= total({pending_total})'
)
def check_all(self, t_total, q_total, pend_total, pairleased_t=0, pairleased_q=0, in_flight=0):
"""一次性检查所有守恒。"""
self.check_t_conservation(t_total, pairleased_t)
self.check_q_conservation(q_total, pairleased_q)
self.check_pending_conservation(pend_total, in_flight)
# ═══════════════════════════════════════════
# 取消注入器
# ═══════════════════════════════════════════
class CancelInjector:
"""在指定 await 边界前后注入取消。"""
def __init__(self):
self._points: dict[str, asyncio.Event] = {}
self._cancel_task: Optional[asyncio.Task] = None
def register(self, name: str):
"""注册一个取消点。"""
self._points[name] = asyncio.Event()
async def wait_at(self, name: str):
"""Worker 在此挂起,等待外部触发取消。"""
if name in self._points:
await self._points[name].wait()
def trigger(self, name: str, task: asyncio.Task):
"""触发指定取消点,取消目标 task。"""
if name in self._points:
self._points[name].set()
task.cancel()
async def cancel_after(self, delay: float, task: asyncio.Task):
"""延迟后取消目标 task。"""
await asyncio.sleep(delay)
task.cancel()
# ═══════════════════════════════════════════
# 事件记录器(用于性质测试)
# ═══════════════════════════════════════════
@dataclasses.dataclass
class Event:
ts: float
kind: str # 't_produced', 't_admitted', 't_claimed', 'q_sent', 'q_returned',
# 'q_admitted', 'q_claimed', 'pair_claimed', 'pair_ok', 'pair_fail',
# 't_expired', 'q_expired', 't_discarded', 'q_discarded',
# 'sem_t_free', 'sem_q_free', 'sem_pend_free', 'inv_t', 'inv_q'
detail: Any = None
class EventLog:
"""记录所有事件,用于事后分析。"""
def __init__(self):
self.events: list[Event] = []
self._lock = asyncio.Lock()
async def record(self, kind: str, detail=None):
async with self._lock:
self.events.append(Event(ts=time.time(), kind=kind, detail=detail))
def count(self, kind: str) -> int:
return sum(1 for e in self.events if e.kind == kind)
def filter(self, kind: str) -> list[Event]:
return [e for e in self.events if e.kind == kind]
def dump(self) -> str:
lines = []
for e in self.events:
lines.append(f'{e.ts:.3f} {e.kind} {e.detail or ""}')
return '\n'.join(lines)