"""Uji V31: tiap image stack mandiri — daftar COPY memuat semua impor intra-proyek. `./venv/bin/python test_images.py` harus exit 0. Memindai `Dockerfile`, `Dockerfile.scan`, `Dockerfile.dist`, `Dockerfile.claim`: untuk tiap modul .py yang di-COPY, kumpulkan impor intra-proyek (top-level `import X` / `from X import`) lalu pastikan semuanya ada di daftar COPY image itu. Menangkap kelas bug B4: modul yang diimpor modul yang di-COPY tapi ⊥ ikut disalin → ModuleNotFoundError di container (image ⊥ mandiri). """ import os import re import sys ROOT = os.path.dirname(os.path.abspath(__file__)) PROJECT = {'config', 'db', 'get_voters', 'dashboard', 'gunicorn_conf', 'scan_loop', 'distribute', 'distribute_loop', 'telegram', 'claim', 'claim_loop'} IMAGES = { 'web': 'Dockerfile', 'scan': 'Dockerfile.scan', 'dist': 'Dockerfile.dist', 'claim': 'Dockerfile.claim', } def copied_modules(dfile): """Modul .py yang di-COPY oleh `COPY ./`.""" mods = set() with open(os.path.join(ROOT, dfile)) as fh: for line in fh: if not line.startswith('COPY '): continue tokens = line.split() if tokens[-1] != './': continue for tok in tokens[1:-1]: if tok.endswith('.py'): mods.add(tok[:-3]) return mods def project_imports(mod): """Impor intra-proyek dari sumber modul `mod`.""" imports = set() path = os.path.join(ROOT, mod + '.py') if not os.path.exists(path): return imports with open(path) as fh: src = fh.read() for m in re.finditer(r'^\s*import\s+([\w.]+)', src, re.M): name = m.group(1).split('.')[0] if name in PROJECT: imports.add(name) for m in re.finditer(r'^\s*from\s+([\w.]+)\s+import', src, re.M): name = m.group(1).split('.')[0] if name in PROJECT: imports.add(name) return imports def check(tag, cond): if not cond: raise AssertionError(f'GAGAL: {tag}') print(f'ok: {tag}') def main(): for image, dfile in IMAGES.items(): copied = copied_modules(dfile) check(f'V31 {image}: ada modul .py di COPY', bool(copied) and 'config' in copied) for mod in sorted(copied): missing = project_imports(mod) - copied if missing: raise AssertionError( f'GAGAL: V31 {image}: {mod} impor {sorted(missing)} ' f'yang ⊥ di-COPY') print(f'ok: V31 {image}: {mod} impor ⊆ COPY') print('\nSEMUA UJI PASS') if __name__ == '__main__': sys.exit(main())