Files
databisnisid/test_get_voters.py
T
proitlab fdb5d0de15 T17: backend penyimpanan bersama db.py (sqlite | mysql via PyMySQL)
- db.py: abstraksi connect/query/queryone/replace_snapshot; %s → ? utk sqlite;
  replace snapshot mysql = CREATE IF NOT EXISTS + DELETE + INSERT (MVCC)
- config/.env.example: VEX_DB_BACKEND (default sqlite) + VEX_DB_HOST/PORT/USER/PASS/NAME
- get_voters.persist → db.replace_snapshot; dashboard baca via db.query
- LIKE pakai ESCAPE '!' (backslash memutus literal string MySQL, B2);
  db.query selalu kembalikan list (PyMySQL tuple vs sqlite list, B3)
- docker-compose.yml: kontainer uji mariadb:11 (databisnisid-mariadb, 127.0.0.1:3306)
- test_mariadb.py: suite integrasi opt-in (skip bila bukan mysql)
- SPEC.md §V17/T17/B2/B3; AGENTS.md runbook
2026-08-05 12:41:17 +07:00

213 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Uji verifikasi invarian §V tanpa bergantung node publik (flaky).
Oracle: `./venv/bin/python test_get_voters.py` harus exit 0.
Dalam setiap contoh, 'last_vote_weight' berisi 17 desimal (bobot teliti).
Pemilih buatan memakai bobot segar (revote ~sekarang) supaya lolos saringan
V13; baris basi ditambahkan eksplisit untuk membuktikan ia dibuang.
"""
import os
import sqlite3
import tempfile
from datetime import datetime, timedelta
import requests
import db
import get_voters as gv
def _fresh_weight(staked_raw, days_ago=5):
"""Bobot untuk revote ~sekarang: staked_raw × 2^(tahun sejak 2000)."""
t = ((datetime.now() - gv.WEIGHT_EPOCH).days - days_ago) / 365.25
return f'{staked_raw * 2 ** t:.17f}'
class FakePayload:
passes = 0
@staticmethod
def call(payload):
lb = payload['lower_bound']
pages = {
'': {'rows': [
{'owner': gv.ZERO_OWNER, 'producers': [],
'staked': 10000, 'last_vote_weight': '0.00'},
{'owner': 'aaa1', 'producers': [gv.TARGET_BP],
'staked': 12000000,
'last_vote_weight': _fresh_weight(12000000)},
{'owner': 'bbb2', 'producers': ['wind'],
'staked': 9000, 'last_vote_weight': '999.0'},
], 'more': True},
'bbb2': {'rows': [
{'owner': 'aaa1', 'producers': [gv.TARGET_BP],
'staked': 12000000,
'last_vote_weight': _fresh_weight(12000000)},
{'owner': 'ccc3', 'producers': [gv.TARGET_BP],
'staked': 10000000,
'last_vote_weight': _fresh_weight(10000000)},
], 'more': True},
'ccc3': {'rows': [
{'owner': 'ccc3', 'producers': [gv.TARGET_BP],
'staked': 10000000,
'last_vote_weight': _fresh_weight(10000000)},
{'owner': 'zzz9', 'producers': [gv.TARGET_BP],
'staked': 15000000,
'last_vote_weight': _fresh_weight(15000000, days_ago=900)},
], 'more': False},
}
FakePayload.passes += 1
if FakePayload.passes > 20:
raise AssertionError('V2 gagal: loop tidak berterminasi')
return pages[lb]
def check(tag, cond):
if not cond:
raise AssertionError(f'GAGAL: {tag}')
print(f'ok: {tag}')
def main():
tmp = tempfile.mkdtemp()
db.DB_PATH = os.path.join(tmp, 'test.db')
# V1: ulang ≥3x saat node timeout → hits, lalu sukses
calls = {'n': 0}
def fake_post(url, json=None, timeout=30):
calls['n'] += 1
if calls['n'] < 3:
raise requests.exceptions.Timeout('node timeout')
resp = type('R', (), {'ok': True, 'raise_for_status': lambda self: None, 'json': lambda self: {'rows': [], 'more': False}})()
return resp
gv.requests.post = fake_post
out = gv.fetch_with_retry({})
check(f'V1 retry ≥3 lalu sukses ({calls["n"]} panggilan)', calls['n'] == 3 and out == {'rows': [], 'more': False})
# bila gagal terus → mengangkat setelah retries habis
def always_fail(url, json=None, timeout=30):
raise requests.exceptions.Timeout('node timeout')
gv.requests.post = always_fail
try:
gv.fetch_with_retry({}, max_retries=3)
raise AssertionError('V1 gagal: seharusnya mengangkat setelah retries habis')
except requests.exceptions.Timeout:
check('V1 gagal total → error dinaikkan', True)
# V2 + V3: paginasi berakhir & baris duplikat antar-halaman dibuang
gv.fetch_with_retry = FakePayload.call
raw = gv.scan_voters()
owners = [r['owner'] for r in raw]
check('V2/V3 pembuangan duplikat',
owners == [gv.ZERO_OWNER, 'aaa1', 'bbb2', 'ccc3', 'zzz9'])
# V4 + V5 + V6: saringan sentinel & BP target terjadi di normalize
norm = [gv.normalize(r) for r in raw]
norm = [n for n in norm if n is not None]
norm_owners = [n['owner'] for n in norm]
check('V4 sentinel "0" dilewati', gv.ZERO_OWNER not in norm_owners)
check('V5 hanya BP target', all(n['owner'] in ('aaa1', 'ccc3') for n in norm))
by = {n['owner']: n for n in norm}
aaa1_w = _fresh_weight(12000000)
check('V6 bobot verbatim TEXT', by['aaa1']['weight'] == aaa1_w)
check('V6 bobot bukan float-rounded',
by['aaa1']['weight'] != str(float(by['aaa1']['weight'])))
check('staked ÷10000', by['aaa1']['staked'] == 1200.0)
check('staked batas ≥1000 ÷10000', by['ccc3']['staked'] == 1000.0)
# V13: last_vote diturunkan di normalize; baris basi dibuang (⊥ disimpan)
check('V13 last_vote ikut dinormalisasi', 'last_vote' in by['aaa1'])
check('V13 last_vote segar (≤ VEX_STALE_DAYS)',
datetime.fromisoformat(by['aaa1']['last_vote'])
>= datetime.now() - timedelta(days=gv.VEX_STALE_DAYS))
check('V13 basi (zzz9) dibuang di normalize',
all(n['owner'] != 'zzz9' for n in norm))
# V13: formula — staked_raw × 2^26 → ~2026-01-01 (±2 hari, toleransi 365.25 hari/tahun)
lv = gv.derive_last_vote(f'{12000000 * 2 ** 26}.00000000000000000', 1200.0)
lv_dt = datetime.fromisoformat(lv) if lv else None
check('V13 formula 2^26 → ~2026-01-01',
lv_dt is not None and abs((lv_dt - datetime(2026, 1, 1)).days) <= 2)
check('V13 rasio 1.0 → epoch 2000',
gv.derive_last_vote('12000000.00000000000000000', 1200.0) == '2000-01-01T00:00:00')
check('V13 bobot nol → None', gv.derive_last_vote('0', 1200.0) is None)
check('V13 bobot kosong → None', gv.derive_last_vote('', 1200.0) is None)
check('V13 bobot tak valid → None', gv.derive_last_vote('abc', 1200.0) is None)
check('V13 stake nol → None', gv.derive_last_vote('1.0', 0.0) is None)
# V13: normalize membuang basi / tak dapat diverifikasi, menyimpan segar
check('V13 normalize basi → None',
gv.normalize({'owner': 'old8', 'producers': [gv.TARGET_BP],
'staked': 20000000,
'last_vote_weight': _fresh_weight(20000000, days_ago=900)})
is None)
check('V13 normalize bobot kosong (unverifiable) → None',
gv.normalize({'owner': 'unk7', 'producers': [gv.TARGET_BP],
'staked': 20000000, 'last_vote_weight': ''}) is None)
check('V13 normalize segar → disimpan',
gv.normalize({'owner': 'new6', 'producers': [gv.TARGET_BP],
'staked': 20000000,
'last_vote_weight': _fresh_weight(20000000)})['owner'] == 'new6')
# V13: estimasi yang melewati now (unstake tanpa revote) diklamp ke now
fixed_now = datetime(2026, 8, 5, 12, 0, 0)
t_future = ((fixed_now - gv.WEIGHT_EPOCH).days + 5) / 365.25
fut = gv.normalize({'owner': 'fut9', 'producers': [gv.TARGET_BP],
'staked': 20000000,
'last_vote_weight': f'{20000000 * 2 ** t_future:.17f}'},
now=fixed_now)
check('V13 last_vote masa depan diklamp ke now',
fut is not None
and fut['last_vote'] == fixed_now.isoformat(timespec='seconds'))
# V9: staked < 1000 VEX → dibuang; tepat 1000 → disimpan
check('V9 di bawah 1000 dibuang',
gv.normalize({'owner': 'low9', 'producers': [gv.TARGET_BP],
'staked': 9999999, 'last_vote_weight': '1.0'}) is None)
check('V9 tepat 1000 disimpan',
gv.normalize({'owner': 'eq10', 'producers': [gv.TARGET_BP],
'staked': 10000000,
'last_vote_weight': _fresh_weight(10000000)})['staked'] == 1000.0)
# V8: node kadang memberi staked sebagai string → tetap dinormalisasi
srow = {'owner': 'eee5', 'producers': [gv.TARGET_BP],
'staked': '10000010000',
'last_vote_weight': _fresh_weight(10000010000)}
snorm = gv.normalize(srow)
check('V8 staked string → float ÷10000', snorm['staked'] == 1000001.0)
# V7: replace-snapshot — tiap persist mengganti tabel, ⊥ append/owner ganda
set_a = [
{'owner': 'aaa1', 'weight': '111.0', 'staked': 1200.0},
{'owner': 'ccc3', 'weight': '222.0', 'staked': 1000.0},
]
gv.persist(set_a)
conn = sqlite3.connect(db.DB_PATH)
cnt = conn.execute('SELECT COUNT(*) FROM voters').fetchone()[0]
conn.close()
check('V7 snapshot pertama = 2', cnt == 2)
set_b = [ # aaa1 weight berubah; ccc3 dihapus; ddd4 baru
{'owner': 'aaa1', 'weight': '999.0', 'staked': 5000.0},
{'owner': 'ddd4', 'weight': '333.0', 'staked': 2000.0},
]
gv.persist(set_b)
conn = sqlite3.connect(db.DB_PATH)
rows = conn.execute('SELECT owner, weight, staked, scanned_at FROM voters').fetchall()
by = {r[0]: r for r in rows}
cols = [r[1] for r in conn.execute('PRAGMA table_info(voters)').fetchall()]
pk = [r[1] for r in conn.execute('PRAGMA table_info(voters)').fetchall() if r[5]]
conn.close()
check('V7 count = |set_b|', len(rows) == 2)
check('V7 ccc3 (dihapus) tak ada', 'ccc3' not in by)
check('V7 ddd4 (baru) ada', 'ddd4' in by)
check('V7 bobot aaa1 terbaru', by['aaa1'][1] == '999.0')
stamps = set(r[3] for r in rows)
check('V7 semua baris cap waktu scan ini', len(stamps) == 1)
check('V7 owner PRIMARY KEY', 'owner' in cols and pk == ['owner'])
check('V7 kolom last_vote ada', 'last_vote' in cols)
print('\nSEMUA UJI PASS')
if __name__ == '__main__':
main()