`atuq-en-imagen.py` ahora lee `toolkit.startup.recent_crashes` del perfil ANTES y DESPUÉS de cada corrida y lo anota junto al tiempo de primera pintura (`recent_crashes_antes` / `_despues` / `_fijado`). Los dos volcados, no uno: Gecko BORRA el contador al mostrar el diálogo de Modo de resolución de problemas, así que un perfil envenenado se ve limpio si se lo mira después — y la corrida siguiente parece arreglarse sola mientras la anterior parece intermitente. Con eso, `vigia-imagen.py` saca de la tasa las corridas con el contador > 3 (ahí el sujeto no es el navegador sino un diálogo modal) y para las 21 anteriores al campo dice que NO SE PUEDEN CLASIFICAR en vez de contarlas como buenas. Un denominador se marca, no se borra. Mandos y guardas nuevas: · `--crashes N` fija el contador en el `user.js` del perfil: 0 limpia, >3 reproduce. Es el control en los dos sentidos, que es lo único que distingue causa de correlación con suerte; · `WidgetScreen:5` en el MOZ_LOG y una sonda `ATUQ-DIAG` en la propia página (screen/outer/inner por `dump()`), que es lo que refutó la carrera con `wl_output`; · la lista ordenada del protocolo ya no se ahoga en los ~60 modos que anuncia QEMU —el `head` cortaba antes del `set_window_geometry`— y trae `set_title`/`set_app_id`, que es lo que identificó la ventana; · si QEMU no arranca, se dice en el primer segundo leyendo `qemu.log`. El fallo real es «Failed to get write lock» con otra VM sobre la misma imagen, y sin esta guarda se veía como diez minutos esperando una marca del serial: el socket queda creado y el `connect()` funciona. Las fases (`--xulstore`), el `ATUQ-EXIT=$?`, la copia del log del arranque anterior y el `find` del perfil sobre la raíz entera los escribió la otra sesión que trabaja este frente; conviven acá porque el fichero es compartido.
427 lines
25 KiB
Python
427 lines
25 KiB
Python
#!/usr/bin/env python3
|
||
# vigia-imagen.py — ¿qué le falta a una IMAGEN que su clausura no puede echar de menos?
|
||
#
|
||
# ══ EL PUNTO CIEGO QUE ESTE VIGÍA CUBRE ════════════════════════════════════════════════════════
|
||
# `build-state.py` contesta «¿está sellado?» y `vigia-sonames.py` contesta «¿algún `.so` que pide
|
||
# esta imagen no lo provee nadie?». Ninguno de los dos contesta la pregunta que se hace un usuario
|
||
# al arrancar el escritorio: **¿se puede USAR?**
|
||
#
|
||
# Y hay dos formas de que no se pueda, las dos medidas el 2026-09-03 arrancando KDE en QEMU:
|
||
#
|
||
# 1. **DATA de runtime que ninguna arista de BUILD alcanza.** El panel de Plasma salía con el reloj
|
||
# y nada más. Los `.so` de todos los applets estaban instalados; lo que faltaba era el TEMA DE
|
||
# ICONOS — `kickoff`, `systemtray` y `showdesktop` SON un icono, así que sin tema quedan de ancho
|
||
# cero y el panel parece vacío. `breeze-icons` estaba sellada desde siempre y no la declaraba
|
||
# ningún perfil. Es la figura de las fuentes ([[gnome-sin-fuentes]]) y de `foot` (sway llegó a
|
||
# 121/121 sin emulador de terminal): **la métrica de clausura mide lo DECLARADO**, y estas cosas
|
||
# no las declara nadie porque no son dep de build de nada.
|
||
#
|
||
# 2. **Una porción OPCIONAL de una librería que sí está.** El menú de aplicaciones no abría:
|
||
# `kcoreaddons.toml` pasaba `-DKCOREADDONS_USE_QML=OFF` ⇒ el módulo QML `org.kde.coreaddons` no
|
||
# existía ⇒ `kickoff` moría en el import. No falta una receta ni una librería: falta una FEATURE.
|
||
# Ninguna métrica basada en recetas puede verlo.
|
||
#
|
||
# ══ QUÉ MIDE, Y CONTRA QUÉ ═════════════════════════════════════════════════════════════════════
|
||
# Recorre los ARTEFACTOS del cierre de cada perfil (no las recetas: el artefacto es lo que se
|
||
# instala) y comprueba cinco invariantes de imagen usable:
|
||
#
|
||
# iconos ≥1 tema con `index.theme` Y ficheros de icono adentro. Un directorio sin `index.theme`
|
||
# NO es un tema: la búsqueda no lo recorre. `hicolor` se cuenta aparte porque es el
|
||
# FALLBACK obligatorio del estándar y no trae iconos propios: es el contrato, no el
|
||
# contenido.
|
||
# cursores ≥1 tema con `cursors/` poblado. Con el cursor por software —obligatorio en virtio-gpu—
|
||
# sin tema el puntero se mueve INVISIBLE. A diferencia de los iconos, acá NO se exige
|
||
# `index.theme`: libXcursor abre `<tema>/cursors/<nombre>` directo.
|
||
# fuentes ≥1 `.ttf/.otf/.ttc/.pfb`. fontconfig es el motor que BUSCA fuentes, no una fuente.
|
||
# terminal ≥1 `.desktop` con `TerminalEmulator` en Categories. Un escritorio sin terminal no es
|
||
# uno incompleto: es uno del que no se puede salir cuando algo falla.
|
||
# qml todo `import <módulo>` de los `.qml` INSTALADOS tiene un módulo con `qmldir` en el
|
||
# cierre. Así se caza la clase (2).
|
||
#
|
||
# ══ Y UN SEXTO DATO QUE ESTE VIGÍA NO MIDE: CUÁNTO TARDA EN VERSE ══════════════════════════════
|
||
# Los cinco de arriba se miden sobre artefactos, en segundos, y contestan «¿está lo que hace falta?».
|
||
# Ninguno contesta «¿y cuándo se ve?» — y esa pregunta ya cobró: el §6.10.ter del SDD 26 dio por
|
||
# hecho que `atuq` «no pinta nunca» en la imagen arrancada CON ESTE VIGÍA EN ✓, y la medición
|
||
# repetida mostró que pintaba a los +137…+204 s. Lo medido había sido la paciencia.
|
||
# Y el 2026-09-14, peor: con DOS tarjetas DRM la ventana cae en cualquiera de las dos salidas, y las
|
||
# capturas miraban UNA ⇒ el «3 de 13» que se publicó medía dónde apuntaba la cámara. Por eso el dato
|
||
# que se lee acá trae en qué PANTALLA apareció, y las corridas ciegas (dos salidas, una captura) no
|
||
# se cuentan.
|
||
# Arrancar la imagen son ~15 min de QEMU sin KVM, así que no se hace acá: `scripts/cosmic/atuq-en-
|
||
# imagen.py` lo mide y deja el número en `docs/state/primera-pintura.json`, y este vigía lo LEE y lo
|
||
# dice con sus condiciones (lanzamiento, vídeo, RAM, vcpus) — el tiempo depende de la carga de la
|
||
# anfitriona, y un número sin condiciones se lee como si fuera del producto. Sin medición, lo dice.
|
||
#
|
||
# ══ RUIDO LEGÍTIMO, para poder triar ═══════════════════════════════════════════════════════════
|
||
# Hay módulos QML que NO se instalan en disco porque el binario los REGISTRA en runtime
|
||
# (`org.kde.plasma.shell` lo registra plasmashell; los `Qt*` los trae el propio Qt en otra ruta).
|
||
# No se excluyen por una lista escrita a mano —eso envejece y no dice por qué— sino comprobando que
|
||
# el URI esté como literal en un ELF del cierre: ver `registrado_en_binario()`. Se reportan aparte,
|
||
# nombrando al registrador. Todo lo demás que salga acá es accionable.
|
||
#
|
||
# uso: python3 scripts/vigia-imagen.py [perfil...] [--fail] [--qml]
|
||
# --fail → exit 1 si algún invariante falla; exit 2 si NO SE PUDO medir (nodos del cierre
|
||
# sin artefacto sellado). Los dos son accionables y no son lo mismo.
|
||
# --qml → además del resumen, listar los imports sin módulo
|
||
import os, sys, glob, subprocess, re, json
|
||
|
||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__))))
|
||
import yupana
|
||
import targets as targets_mod
|
||
|
||
ROOT = yupana.ROOT
|
||
STORE = os.environ.get("HAMMER_STORE", str(ROOT / "store"))
|
||
HAMMER = str(ROOT / "target/release/takana")
|
||
|
||
# Invariantes que un perfil NO cumple A PROPÓSITO. Un ✗ permanente por una decisión ya tomada es
|
||
# deuda fantasma —la misma figura que la terna GNOME que hubo que sacar de targets.toml para que el
|
||
# grafo dejara de mentir—, así que se declaran acá CON EL MOTIVO y se imprimen como ⊘, no como fallo.
|
||
EXCEPCIONES = {
|
||
("escritorio-mirada", "iconos"): "imagen SLIM a propósito: compositor + greeter, sin escritorio",
|
||
("escritorio-mirada", "hicolor"): "idem",
|
||
("escritorio-mirada", "cursores"): "el greeter dibuja su propio puntero",
|
||
("escritorio-mirada", "fuentes"): "idem slim",
|
||
("escritorio-mirada", "terminal"): "no es un escritorio de uso general",
|
||
("escritorio-sway", "iconos"): "ninguna de sus 4 apps gráficas pide iconos de tema y zathura "
|
||
"es GTK4 (iconos de UI compilados); un tema COMPLETO exige "
|
||
"promover adwaita o breeze al corpus — decisión, no olvido",
|
||
}
|
||
|
||
# (Acá vivía `QML_EN_RUNTIME`, una lista escrita a mano de módulos que nadie instala en disco. La
|
||
# reemplaza `registrado_en_binario()`, que contesta lo mismo con evidencia y además NOMBRA al
|
||
# registrador. Al vaciarla, los cuatro que tenía se resolvieron solos —y `org.kde.kwin.effect` ni
|
||
# siquiera se importa en ningún cierre: era una entrada muerta que nadie podía saber que sobraba.)
|
||
|
||
_cache = {}
|
||
def artefacto(cola, nombre):
|
||
"""Directorio del artefacto de (cola,nombre) en el store, o None. Mismo método que vigia-sonames:
|
||
`hammer hash` sobre la receta de HOY, sin construir — no heurística de mtime."""
|
||
if (cola, nombre) in _cache:
|
||
return _cache[(cola, nombre)]
|
||
receta = ROOT / ("recipes/%s.toml" % nombre if cola == "corpus"
|
||
else "recipes/%s/%s.toml" % (cola, nombre))
|
||
d = None
|
||
if receta.exists():
|
||
r = subprocess.run([HAMMER, "--store", STORE, "hash", str(receta)],
|
||
capture_output=True, text=True)
|
||
h = r.stdout.strip().replace("b3:", "")
|
||
if len(h) == 64:
|
||
cand = glob.glob(os.path.join(STORE, h + "-*"))
|
||
d = cand[0] if cand else None
|
||
_cache[(cola, nombre)] = d
|
||
return d
|
||
|
||
|
||
ICONO_EXT = (".png", ".svg", ".svgz", ".xpm")
|
||
FUENTE_EXT = (".ttf", ".otf", ".ttc", ".pfb", ".pcf", ".pcf.gz")
|
||
|
||
|
||
def registrado_en_binario(uri, dirs):
|
||
"""¿Algún ELF del cierre lleva `uri` como literal? Entonces lo registra en C++ y NO falta.
|
||
|
||
── POR QUÉ HACE FALTA ──────────────────────────────────────────────────────────────────────
|
||
Un `import org.kde.kscreenlocker` no siempre se satisface con un directorio y su `qmldir`. El
|
||
greeter de la pantalla de bloqueo hace `qmlRegisterUncreatableType<PamAuthenticator>(
|
||
"org.kde.kscreenlocker", …)`: el módulo existe SÓLO dentro del proceso que carga ese `.qml`, y
|
||
en disco no hay nada que instalar. Buscar el módulo en el sistema de ficheros da ✗ y la imagen
|
||
está perfecta.
|
||
|
||
Medido el 2026-09-05: los DOS únicos ✗ de qml que reportaba este vigía eran de esta clase
|
||
—`org.kde.kscreenlocker` en `libexec/kscreenlocker_greet` y `org.kde.newstuff.core` en
|
||
`libnewstuffqmlplugin.so`— o sea que el 100% de sus hallazgos eran falsos. Y un guardián que
|
||
grita en falso no se endurece: se ignora. Antes esto se tapaba anotando el módulo a mano en
|
||
una lista escrita a mano, que envejece y no dice por qué.
|
||
|
||
El literal queda en `.rodata` del binario, así que la pregunta se contesta con evidencia. Se
|
||
miran primero los artefactos cuyo nombre comparte una palabra con el URI (`kscreenlocker` →
|
||
`kscreenlocker`), que es donde está casi siempre, y sólo si no aparece se recorre el resto.
|
||
"""
|
||
aguja = uri.encode()
|
||
tokens = [t for t in re.split(r"[.\-_]", uri.lower()) if len(t) > 3]
|
||
|
||
def primero_los_parecidos(par):
|
||
return 0 if any(t in par[0].lower() for t in tokens) else 1
|
||
|
||
for nombre, d in sorted(dirs.items(), key=primero_los_parecidos):
|
||
for raiz, _, ficheros in os.walk(d):
|
||
for f in ficheros:
|
||
ruta = os.path.join(raiz, f)
|
||
try:
|
||
if os.path.getsize(ruta) > 256 * 1024 * 1024:
|
||
continue
|
||
with open(ruta, "rb") as fh:
|
||
if fh.read(4) != b"\x7fELF":
|
||
continue
|
||
fh.seek(0)
|
||
if aguja in fh.read():
|
||
return "%s (%s)" % (os.path.relpath(ruta, d), nombre)
|
||
except OSError:
|
||
continue
|
||
return None
|
||
|
||
|
||
def escanear(nodos):
|
||
"""Recorre los artefactos del cierre una sola vez y junta los cinco hechos."""
|
||
temas, cursores, fuentes, terminales = {}, {}, 0, {}
|
||
qml_modulos, qml_imports = set(), {}
|
||
sin_artefacto = []
|
||
dirs = {}
|
||
for (cola, nombre) in sorted(nodos):
|
||
d = artefacto(cola, nombre)
|
||
if not d:
|
||
sin_artefacto.append(nombre)
|
||
continue
|
||
dirs[nombre] = d
|
||
# ── temas de iconos y de cursores ───────────────────────────────────────────────────────
|
||
base = os.path.join(d, "usr/share/icons")
|
||
if os.path.isdir(base):
|
||
for t in sorted(os.listdir(base)):
|
||
td = os.path.join(base, t)
|
||
# ⚠ ICONOS y CURSORES no se buscan igual, y medirlos con la misma regla dio un ✗
|
||
# falso el 2026-09-04: `adwaita-cursors` instalaba `Adwaita/cursors/` poblado y este
|
||
# vigía seguía diciendo «NINGÚN tema de cursor» porque el directorio no traía
|
||
# `index.theme`. Para un tema de ICONOS ese requisito es correcto —la búsqueda XDG
|
||
# recorre `Directories=` y sin el índice el tema no existe—, pero libXcursor y
|
||
# wlroots abren `<tema>/cursors/<nombre>` DIRECTO; `index.theme` sólo hace falta
|
||
# para seguir un `Inherits=`. Exigirlo también acá medía el invariante de al lado.
|
||
tiene_indice = os.path.exists(os.path.join(td, "index.theme"))
|
||
n_ico = n_cur = 0
|
||
for dp, _, fn in os.walk(td):
|
||
for f in fn:
|
||
if f.endswith(ICONO_EXT):
|
||
n_ico += 1
|
||
elif os.path.basename(dp) == "cursors":
|
||
n_cur += 1
|
||
if n_ico and tiene_indice:
|
||
temas.setdefault(t, (nombre, n_ico))
|
||
if n_cur:
|
||
cursores.setdefault(t, (nombre, n_cur))
|
||
if t == "hicolor" and tiene_indice:
|
||
temas.setdefault("hicolor", (nombre, n_ico))
|
||
# ── el resto, en UNA pasada por el árbol ────────────────────────────────────────────────
|
||
for dp, _, fn in os.walk(d):
|
||
for f in fn:
|
||
p = os.path.join(dp, f)
|
||
if f.endswith(FUENTE_EXT):
|
||
fuentes += 1
|
||
elif f == "qmldir":
|
||
# el módulo es la ruta relativa desde .../qml/ con puntos
|
||
m = re.search(r"/qt\d*/qml/(.+)$", dp) or re.search(r"/qml/(.+)$", dp)
|
||
if m:
|
||
qml_modulos.add(m.group(1).replace("/", "."))
|
||
elif f.endswith(".qml"):
|
||
# `designer/`: los `*Specifics.qml` de Qt son la integración con Qt Design Studio
|
||
# (importan `HelperWidgets`, que sólo existe dentro de Qt Creator). Nunca se
|
||
# cargan en runtime; contarlos era ruido puro.
|
||
if "/designer/" in dp + "/":
|
||
continue
|
||
try:
|
||
txt = open(p, encoding="utf-8", errors="replace").read()
|
||
except OSError:
|
||
continue
|
||
# Y los imports DENTRO DE UN COMENTARIO no son imports: `QtMultimedia/Video.qml`
|
||
# trae `import QtSystemInfo 5.0` en un bloque \qml de la documentación. Sin
|
||
# despojar comentarios, el vigía reporta como hueco un ejemplo de un docstring.
|
||
txt = re.sub(r"/\*.*?\*/", "", txt, flags=re.S)
|
||
txt = re.sub(r"^\s*//.*$", "", txt, flags=re.M)
|
||
for mod in re.findall(r"^\s*import\s+([A-Za-z][\w.]*)", txt, re.M):
|
||
qml_imports.setdefault(mod, set()).add(nombre)
|
||
elif f.endswith(".desktop") and "/applications" in dp:
|
||
try:
|
||
txt = open(p, encoding="utf-8", errors="replace").read()
|
||
except OSError:
|
||
continue
|
||
if re.search(r"^Categories=.*TerminalEmulator", txt, re.M):
|
||
terminales.setdefault(f[:-8], nombre)
|
||
candidatos = {m: v for m, v in qml_imports.items()
|
||
if m not in qml_modulos
|
||
and not m.startswith(("QtQuick", "QtQml", "QtCore", "QtTest", "Qt5", "Qt."))
|
||
and m != "Qt"}
|
||
# Último filtro, y el que decide: un módulo registrado en C++ no está «sin instalar».
|
||
faltan_qml, en_binario = {}, {}
|
||
for m, v in candidatos.items():
|
||
donde = registrado_en_binario(m, dirs)
|
||
if donde:
|
||
en_binario[m] = donde
|
||
else:
|
||
faltan_qml[m] = v
|
||
return temas, cursores, fuentes, terminales, faltan_qml, sin_artefacto, en_binario
|
||
|
||
|
||
def informe_primera_pintura():
|
||
"""El sexto dato: CUÁNTO TARDA la ventana en aparecer en la imagen ARRANCADA.
|
||
|
||
Los cinco invariantes de arriba se miden sobre artefactos y contestan «¿está lo que hace falta?».
|
||
Ninguno contesta «¿y cuándo se ve?», y esa pregunta ya cobró: el §6.10.ter del SDD 26 concluyó
|
||
que `atuq` «no pinta nunca» en la imagen con este vigía en ✓ — y la medición repetida mostró que
|
||
pintaba a los +137…+204 s, o sea que lo que se había medido era la paciencia del que miraba.
|
||
|
||
No se mide acá a propósito: arrancar la imagen son ~15 min de QEMU sin KVM y este guion corre en
|
||
segundos sobre el store. Lo que hace es LEER el número que dejó `scripts/cosmic/atuq-en-imagen.py`
|
||
y decirlo con sus condiciones —tiempo sin condiciones se lee como si fuera del producto, y
|
||
depende de la carga de la máquina anfitriona—. Si no hay medición, se dice; un hueco callado acá
|
||
es un ✓ que no se ganó.
|
||
"""
|
||
ruta = os.path.join(str(ROOT), "docs/state/primera-pintura.json")
|
||
print("\n== primera pintura en la imagen arrancada (no lo mide este vigía)")
|
||
if not os.path.exists(ruta):
|
||
print(" ⊘ sin medición — corré: scripts/cosmic/atuq-en-imagen.py --as-user --until-paint")
|
||
return False
|
||
try:
|
||
with open(ruta) as f:
|
||
corridas = json.load(f).get("corridas", [])
|
||
except (OSError, ValueError) as e:
|
||
print(" ⚠ %s ilegible: %s" % (ruta, e))
|
||
return False
|
||
if not corridas:
|
||
print(" ⊘ el fichero no tiene corridas")
|
||
return False
|
||
|
||
# Se informan TODAS las corridas y no la última, porque el fenómeno es INTERMITENTE. Y se separan
|
||
# por CUÁNTAS SALIDAS fotografió cada una: medido el 2026-09-14, con dos tarjetas DRM la ventana
|
||
# cae en cualquiera de las dos salidas, así que un ✗ de una corrida que sólo miró una pantalla NO
|
||
# dice que la ventana no esté: dice que no estaba EN ESA. Contarlos juntos fue lo que produjo un
|
||
# «3 de 13» que medía la cámara y no el producto.
|
||
ciegas = [c for c in corridas if c.get("pantallas_fotografiadas", 1) < 2
|
||
and "dos DRM" in (c.get("video") or "")
|
||
and c.get("segundos_primera_pintura") is None]
|
||
corridas = [c for c in corridas if c not in ciegas]
|
||
# ⚠ Y SE SEPARAN LAS CORRIDAS CON EL PERFIL ENVENENADO. Medido el 2026-09-15 (§6.10.terdecies):
|
||
# con `toolkit.startup.recent_crashes` por encima de `max_resumed_crashes` —3 por defecto— el
|
||
# navegador NO abre una ventana de navegador: abre el diálogo de Modo de resolución de problemas,
|
||
# que es modal y mide 117×70. El ✗ de esas corridas es cierto y no dice NADA del producto. Y el
|
||
# contador lo subía este mismo andamiaje al matar la VM con el navegador vivo, así que la serie
|
||
# vieja está contaminada: las corridas anteriores a que el arnés anotara el campo NO SE PUEDEN
|
||
# clasificar, y eso se dice en vez de contarlas como si fueran buenas.
|
||
def envenenada(c):
|
||
fijado = c.get("recent_crashes_fijado") # lo que PUSO el arnés (user.js gana)
|
||
valor = fijado if fijado is not None else c.get("recent_crashes_antes")
|
||
return valor is not None and valor > 3
|
||
|
||
envenenadas = [c for c in corridas if envenenada(c)]
|
||
corridas = [c for c in corridas if c not in envenenadas]
|
||
sin_clasificar = [c for c in corridas if "recent_crashes_antes" not in c
|
||
and c.get("recent_crashes_fijado") is None]
|
||
|
||
pintaron = [c["segundos_primera_pintura"] for c in corridas
|
||
if c.get("segundos_primera_pintura") is not None]
|
||
n, m = len(pintaron), len(corridas)
|
||
marca = "✓" if n == m else ("✗" if n == 0 else "⚠")
|
||
if ciegas:
|
||
marca = "⚠" # con corridas ciegas de por medio, un 6/6 no es un ✓: es un «lo que se pudo medir»
|
||
if sin_clasificar:
|
||
marca = "⚠" # una tasa con corridas de procedencia desconocida adentro no es una tasa
|
||
if pintaron:
|
||
print(" %s pintaron %d de %d corridas CONCLUYENTES · primera pintura %s"
|
||
% (marca, n, m,
|
||
("+%ds" % pintaron[0]) if n == 1 else "+%d…+%ds" % (min(pintaron), max(pintaron))))
|
||
else:
|
||
print(" %s NINGUNA de %d corridas pintó" % (marca, m))
|
||
if envenenadas:
|
||
print(" ⊘ %d corridas APARTE: el perfil tenía recent_crashes > 3 ⇒ lo que abrió fue el "
|
||
"diálogo de Modo de resolución de problemas, no el navegador (SDD 26 §6.10.terdecies)"
|
||
% len(envenenadas))
|
||
if sin_clasificar:
|
||
print(" ⚠ %d de esas %d corridas son de ANTES de que se anotara el contador de caídas: "
|
||
"no se puede decir si midieron el navegador o el diálogo" % (len(sin_clasificar), m))
|
||
for c in corridas[-6:]:
|
||
seg = c.get("segundos_primera_pintura")
|
||
print(" %s %-9s %-10s %-22s %-26s %s"
|
||
% ("✓" if seg is not None else "✗",
|
||
("+%ds" % seg) if seg is not None else "—",
|
||
(c.get("pantalla") or "")[:10],
|
||
c.get("lanzamiento", "?"), c.get("video", "?"),
|
||
(c.get("medido") or "")[:16]))
|
||
if ciegas:
|
||
print(" ⚠ %d corridas NO se cuentan: con dos salidas fotografiaron UNA sola, y su ✗ sólo"
|
||
" dice que la ventana no estaba en esa pantalla" % len(ciegas))
|
||
fallidas = [c for c in corridas if c.get("segundos_primera_pintura") is None]
|
||
if fallidas:
|
||
print(" ⚠ en las que NO pintó, el MOZ_LOG igual decía `mapped 1` + «has buffer» +"
|
||
" WaylandBufferSHM commiteado:")
|
||
print(" que Gecko commitee cuadros NO prueba que la ventana se vea. La evidencia es"
|
||
" el píxel, no el log.")
|
||
return n == 0
|
||
|
||
|
||
def main():
|
||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||
fallar = "--fail" in sys.argv
|
||
detalle_qml = "--qml" in sys.argv
|
||
colas = yupana.cargar_colas()
|
||
perfiles = targets_mod.load()
|
||
memb = yupana.membresia(colas, perfiles)
|
||
quiero = args or [p for p in perfiles if p.startswith("escritorio")]
|
||
fallos = 0
|
||
parciales = set()
|
||
for perfil in quiero:
|
||
nodos = {n for n, ps in memb.items() if perfil in ps and n[0] != "wanted"}
|
||
if not nodos:
|
||
continue
|
||
temas, cursores, fuentes, terminales, faltan_qml, sin_art, en_binario = escanear(nodos)
|
||
reales = {t: v for t, v in temas.items() if t != "hicolor"}
|
||
print("== %-20s %3d nodos del cierre" % (perfil, len(nodos)))
|
||
parcial = bool(sin_art)
|
||
if parcial:
|
||
parciales.add(perfil)
|
||
# UN AUSENTE FALLA RUIDOSAMENTE: sin el artefacto no se puede afirmar NI que falta NI que
|
||
# está. Pasó al minuto de escribir este vigía: prender el QML de kcoreaddons dejó 71
|
||
# artefactos en deuda y el informe dijo «KDE no tiene terminal» teniendo konsole.
|
||
print(" ⚠ MEDICIÓN PARCIAL: %d de %d nodos sin artefacto sellado (deuda, o store "
|
||
"incompleto) ⇒ los ✗ de abajo pueden ser de la deuda, no de la imagen"
|
||
% (len(sin_art), len(nodos)))
|
||
print(" sin artefacto: %s%s" % (", ".join(sorted(sin_art)[:8]),
|
||
" …" if len(sin_art) > 8 else ""))
|
||
|
||
def linea(ok, etiqueta, texto):
|
||
nonlocal fallos
|
||
motivo = EXCEPCIONES.get((perfil, etiqueta))
|
||
if not ok and motivo:
|
||
print(" ⊘ %-10s %s" % (etiqueta, motivo))
|
||
return
|
||
if not ok and not parcial:
|
||
fallos += 1
|
||
print(" %s %-10s %s" % ("✓" if ok else "✗", etiqueta, texto))
|
||
|
||
linea(bool(reales), "iconos",
|
||
", ".join("%s (%s, %d)" % (t, v[0], v[1]) for t, v in sorted(reales.items()))
|
||
or "NINGÚN tema con iconos — los applets/indicadores quedan de ancho cero")
|
||
linea("hicolor" in temas, "hicolor",
|
||
"presente" if "hicolor" in temas
|
||
else "FALTA el fallback freedesktop: los iconos que las apps instalan en "
|
||
"usr/share/icons/hicolor son invisibles")
|
||
linea(bool(cursores), "cursores",
|
||
", ".join("%s (%s)" % (t, v[0]) for t, v in sorted(cursores.items()))
|
||
or "NINGÚN tema de cursor — con cursor por software el puntero se mueve invisible")
|
||
linea(fuentes > 0, "fuentes",
|
||
"%d ficheros de fuente" % fuentes if fuentes
|
||
else "CERO fuentes (fontconfig es el motor que las busca, no una fuente)")
|
||
linea(bool(terminales), "terminal",
|
||
", ".join("%s (%s)" % (k, v) for k, v in sorted(terminales.items()))
|
||
or "NINGÚN emulador de terminal — no se puede salir de un fallo")
|
||
nota = ""
|
||
if en_binario:
|
||
nota = " (%d registrado(s) en C++, no en disco)" % len(en_binario)
|
||
linea(not faltan_qml, "qml",
|
||
("todos los import resuelven" + nota) if not faltan_qml
|
||
else "%d módulo(s) importado(s) y NO instalado(s)" % len(faltan_qml))
|
||
if en_binario and detalle_qml:
|
||
for m, donde in sorted(en_binario.items()):
|
||
print(" · %-38s lo registra: %s" % (m, donde))
|
||
if faltan_qml and (detalle_qml or True):
|
||
for m, quienes in sorted(faltan_qml.items()):
|
||
print(" FALTA %-34s ← lo importa: %s"
|
||
% (m, ", ".join(sorted(quienes)[:4])))
|
||
fallo_pintura = informe_primera_pintura()
|
||
if parciales:
|
||
print("\n⚠ perfiles medidos a medias (deuda en el store): %s" % ", ".join(sorted(parciales)))
|
||
if fallar and (fallos or fallo_pintura):
|
||
return 1 # medí y FALTA
|
||
if fallar and parciales:
|
||
return 2 # NO pude medir — distinto de «está bien»
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|