Files
databisnisid/test_get_voters.py
T

264 lines
12 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); sejak V40 baris basi
JUGA disimpan (banding/tampil/bayar menghitung umur saat query) — contoh basi
membuktikan ia lolos ke snapshot, bukan dibuang.
"""
import os
import sqlite3
import tempfile
from datetime import datetime, timedelta, timezone
import requests
import db
import get_voters as gv
def _fresh_weight(staked_raw, days_ago=5):
"""Bobot utk revote ~sekarang: staked_raw × 2^(pekan/52) — kuantisasi
pekan utuh sejak epoch 2000 (formula stake2vote Vexanium)."""
weeks = ((datetime.now() - gv.WEIGHT_EPOCH).days - days_ago) // 7
return f'{staked_raw * 2 ** (weeks / 52.0):.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', 'zzz9')
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; V40: basi tetap 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=28))
check('V40 basi (zzz9) tetap disimpan', by['zzz9']['owner'] == 'zzz9')
check('V40 last_vote basi zzz9 ~900 hari lalu',
(datetime.now() - datetime.fromisoformat(by['zzz9']['last_vote'])).days
> 800)
# V13: formula — staked_raw × 2^(pekan/52), kuantisasi pekan utuh sejak 2000
# 2^1 → 52 pekan setelah 2000-01-01 = 2000-12-30 (52×7 = 364 hari)
lv = gv.derive_last_vote(f'{12000000 * 2 ** 1:.17f}', 1200.0)
lv_dt = datetime.fromisoformat(lv) if lv else None
check('V13 formula 2^1 (52 pekan) → 2000-12-30',
lv_dt is not None and lv_dt == datetime(2000, 12, 30))
# 2^26 → 26×52 pekan = 1352 pekan setelah 2000 = 2025-11-29
lv = gv.derive_last_vote(f'{12000000 * 2 ** 26:.17f}', 1200.0)
lv_dt = datetime.fromisoformat(lv) if lv else None
check('V13 formula 2^26 (1352 pekan) → 2025-11-29',
lv_dt is not None and lv_dt == datetime(2025, 11, 29))
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/B12: regresi formula pekan utuh — baris nyata dari chain
# ..tg: staked_raw 9000, weight → t×52 = 1291 pekan → 2024-09-28
lv = gv.derive_last_vote('267849632990.26596069335937500', 9000 / 10000)
check('V13 regresi ..tg (1291 pekan) → 2024-09-28',
lv == '2024-09-28T00:00:00')
# 1.crownz: staked_raw 17115095, t×52 = 1314 pekan → 2025-03-08
lv = gv.derive_last_vote(
f'{17115095 * 2 ** (1314 / 52.0):.17f}', 17115095 / 10000)
check('V13 regresi 1.crownz (1314 pekan) → 2025-03-08',
lv == '2025-03-08T00:00:00')
# V13/V40: normalize menyimpan basi, membuang yang tak dapat diverifikasi
old = gv.normalize({'owner': 'old8', 'producers': [gv.TARGET_BP],
'staked': 20000000,
'last_vote_weight': _fresh_weight(20000000, days_ago=900)})
check('V40 normalize basi (900 hari) → disimpan',
old is not None and old['owner'] == 'old8')
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)
weeks_future = ((fixed_now - gv.WEIGHT_EPOCH).days + 14) // 7 # 2 pekan ke depan
fut = gv.normalize({'owner': 'fut9', 'producers': [gv.TARGET_BP],
'staked': 20000000,
'last_vote_weight':
f'{20000000 * 2 ** (weeks_future / 52.0):.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},
]
scanned_a = 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},
]
scanned_b = 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)
# V62: stempel scan UTC-naive (konsisten dgn cutoff umur dashboard/
# distribusi yang pakai `datetime.now(timezone.utc)`). Dulu `datetime.now()`
# (waktu lokal container, TZ=Asia/Jakarta UTC+7) → `first_seen_at` WIB
# dibandingkan sbg string UTC → maturity pemilih BARU bergeser +7 jam
# (B24). Guard: stamp ≈ now-UTC-naive, ⊖ WIB (offset ±2 mnt toleransi).
utc_now = datetime.now(timezone.utc).replace(tzinfo=None)
for s in (scanned_a, scanned_b):
stamp = datetime.fromisoformat(s)
delta = abs((utc_now - stamp).total_seconds())
check(f'V62 stempel scan UTC-naive ({s})',
delta < 120 and stamp.tzinfo is None)
wib_delta = abs((utc_now - timedelta(hours=7) - stamp).total_seconds())
check(f'V62 stempel scan ⊖ WIB ({s})', wib_delta > 60)
# V41: first_seen — kemunculan pertama tiap owner, idempoten (⊥ overwrite)
db.record_first_seen([{'owner': 'aaa1'}], '2000-01-01T00:00:00')
conn = sqlite3.connect(db.DB_PATH)
fs = {r[0]: r[1] for r in conn.execute(
'SELECT owner, first_seen_at FROM voter_first_seen').fetchall()}
conn.close()
check('V41 first_seen tercatat utk semua owner',
set(fs) == {'aaa1', 'ccc3', 'ddd4'})
check('V41 first_seen = kemunculan pertama',
fs['aaa1'] == scanned_a and fs['ccc3'] == scanned_a
and fs['ddd4'] == scanned_b)
check('V41 first_seen idempoten (record ulang ⊥ overwrite)',
fs['aaa1'] == scanned_a and fs['aaa1'] != '2000-01-01T00:00:00')
print('\nSEMUA UJI PASS')
if __name__ == '__main__':
main()