#!/usr/bin/env python3 # test-yupana-radio.py — REGRESIÓN del bug de libdrm (2026-07-22). No es un test cualquiera: es la # cicatriz. Si el radio de un paquete compartido entre colas vuelve a medirse sobre una sola cola, # esto falla. La propiedad que blinda: el radio SIEMPRE cruza todas las colas del disco. import sys, subprocess from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "scripts")) import yupana def main(): colas = yupana.cargar_colas() rev = yupana.dependientes(colas) memb = yupana.membresia(colas) fallos = [] # 1) libdrm lo consumen recetas de MÁS de una cola (corpus + incoming-kde). Medirlo sobre una # sola cola es exactamente el bug. if ("corpus", "libdrm") in rev: colas_consumidoras = {q for q, _ in rev[("corpus", "libdrm")]} if len(colas_consumidoras) < 2: fallos.append(f"libdrm consumido desde una sola cola {colas_consumidoras} " f"— el radio se está midiendo recortado") # y su membresía DEBE incluir escritorio-kde (la mentira original decía sólo mirada) if "escritorio-kde" not in memb.get(("corpus", "libdrm"), set()): fallos.append("libdrm NO atribuido a escritorio-kde — perfiles vuelve a mentir") # 2) Invariante general: para todo nudo del corpus consumido por alguna receta de una cola # incoming-*, su membresía y sus dependientes DEBEN reflejar esa cola. (Detecta el recorte # en cualquier paquete compartido, no sólo libdrm.) for (q, n), consumidores in rev.items(): if q != "corpus": continue colas_cons = {cq for cq, _ in consumidores} if "corpus" in colas_cons and len(colas_cons) == 1: continue # sólo corpus, nada que cruzar # hay consumidores de otra cola: su membresía no puede ser vacía si esas colas tienen perfil if consumidores and not memb.get((q, n)) and any(cq != "corpus" for cq in colas_cons): pass # puede no tener perfil declarado; no es fallo duro, sólo libdrm es el guardián # 3) INVARIANTE (c): el detector debe AGRUPAR por fuente (sha), no por nombre — la identidad de un # nudo es lo que BAJA. Ancla: itstool y gettext-tiny comparten sha (itstool le presta el tarball # como carrier). Si el detector deja de ver ese grupo, está agrupando por nombre = invariante ciego. recs_f = yupana._recetas_con_fuente() por_sha = {} for k, r in recs_f.items(): if r["sha"]: por_sha.setdefault(r["sha"], set()).add(k) sha_it = recs_f.get(("corpus", "itstool"), {}).get("sha") if not sha_it or ("corpus", "gettext-tiny") not in por_sha.get(sha_it, set()): fallos.append("el detector no agrupa itstool con gettext-tiny por sha — " "¿agrupa por nombre en vez de por fuente? (invariante c ciego)") # 3b) NO crIar lobos: itstool NO es un bug — es un CARRIER (stub que presta el tarball, no # construye la fuente). Si el detector lo llama construye_fuente=True, lo reportaría como # colisión falsa (casi se "arregla" un stub apuntándolo al itstool real que no compila en el lab). if recs_f.get(("corpus", "itstool"), {}).get("construye_fuente"): fallos.append("itstool marcado como que CONSTRUYE su fuente — es un carrier/stub, " "el detector lo reportaría como colisión falsa") # 3c) dbus: corpus (estático) vs incoming-kde (dinámico, para Qt6DBus) — misma fuente, BUILD # distinto ⇒ variante deliberada, NO sombra redundante. Casi se borra load-bearing (2026-07-22). if ("corpus", "dbus") in recs_f and ("incoming-kde", "dbus") in recs_f: if recs_f[("corpus", "dbus")]["build_fp"] == recs_f[("incoming-kde", "dbus")]["build_fp"]: fallos.append("corpus/dbus e incoming-kde/dbus tienen build_fp IGUAL — el detector " "los marcaría redundantes y son variantes estático/dinámico deliberadas") # 4) MODELO EXTRA-GRAFO: el patrón de overlay debe seguir marcando kio (la cicatriz) y su # estimación seguir calibrada contra el fallo real (5315B). Si el estimador se descalibra # (p.ej. alguien cambia el prefijo del hub por el del sandbox), el pre-flight miente. if ("incoming-kde", "kio") in {(q, n) for q in colas for n in colas[q]}: deps_kio = colas["incoming-kde"]["kio"] est = yupana._lowerdir_estimado(deps_kio) if yupana.patron_overlay("kio", deps_kio) is None: fallos.append("el patrón de overlay NO marca a kio — el pre-flight dejó de ver el hueco") elif abs(est - 5315) > 600: # el fallo real midió 5315B; margen por variación de nombres fallos.append(f"el estimador de lowerdir se descalibró: kio={est}B vs real 5315B " f"(¿prefijo equivocado? el del hub da corto)") if fallos: print("✗ REGRESIÓN de yupana:") for f in fallos: print(" -", f) return 1 d = len(rev.get(("corpus", "libdrm"), ())) print(f"✓ radio cruza colas: libdrm consumido por {d} recetas de " f"{len({q for q,_ in rev.get(('corpus','libdrm'),())})} colas, atribuido a " f"{sorted(memb.get(('corpus','libdrm'), set()))}") print(f"✓ detector agrupa por fuente (itstool↔gettext-tiny) y no cría lobos " f"(itstool=carrier, dbus=variante)") print(f"✓ modelo extra-grafo: preflight marca kio (lowerdir ~{yupana._lowerdir_estimado(colas['incoming-kde']['kio'])}B " f"vs real 5315B, calibrado)") return 0 if __name__ == "__main__": sys.exit(main())