#!/usr/bin/env python3 """Ejecuta TODAS las acciones del catalogo y dice cuales funcionan de verdad. Por que hace falta: hasta hoy los comandos libres del modelo corrian dentro del sandbox, donde no hay ni el sistema ni los discos. Se arreglo (virtualization a false), pero las 134 acciones del catalogo nunca se habian ejecutado todas seguidas contra la maquina real: la suite prueba que las FRASES caen en la accion correcta, no que el comando de cada accion haga algo util. Que se ejecuta y que no: - Las destructivas (`confirmar: true`) y las que piden root NO se ejecutan. En uso normal pasan por un dialogo que aprueba una persona; aqui no hay persona, y aprobarlas solas seria justo lo que el diseƱo evita. - Las que abren ventanas (firefox, xdg-open) se ejecutan solo con --abrir, porque llenan la pantalla. Sin esa opcion se comprueba que el programa existe, que es lo que puede fallar. - El resto se ejecuta entero y se mira que conteste algo con sentido. python repasar_acciones.py [--abrir] [--grupo sistema] [--json salida.json] """ import argparse import json import os import re import subprocess import sys import time RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CATALOGO = os.path.join(RAIZ, "newelle", "data", "acciones", "acciones.json") # Argumentos de mentira para las acciones que capturan uno. Se eligen inocuos: # nada que borre, mueva o mande nada fuera. ARGUMENTOS = { "buscar_fichero": "README.md", "buscar_carpeta": "config", "grep_recursivo": "JARVIS", "leer_fichero": "README.md", # un NOMBRE, que es lo que se dice por voz "abrir_carpeta": RAIZ, "tamano_carpeta": os.path.join(RAIZ, "config"), "buscar_youtube": "musica", "buscar_wikipedia": "madrid", "buscar_github": "newelle", "traducir": "hola", "buscar_web": "que hora es", "matar_proceso": "proceso_que_no_existe_jarvis", "ping": "127.0.0.1", } POR_DEFECTO = "prueba" # Lo que abre o MUEVE ventanas. ventanas.sh reordena el escritorio del usuario: # ejecutarlo en un repaso automatico le cambia las ventanas de sitio sin avisar. VENTANA = re.compile(r"\b(firefox|xdg-open|gnome-terminal|nautilus|vlc|mpv|eog|" r"gedit|gnome-text-editor|totem|ventanas\.sh|xdotool|wmctrl)\b") def carga(): with open(CATALOGO, encoding="utf-8") as f: d = json.load(f) return d.get("acciones", d if isinstance(d, list) else []) def prefijo_host(): """Las acciones llevan host:true y en uso salen del sandbox. Aqui ya estamos fuera, asi que no hace falta prefijo.""" return [] def ejecuta(accion, argumento, tope=25): import shlex import urllib.parse comando = accion.get("comando") or "" comando = comando.replace("{argumento}", shlex.quote(argumento)) comando = comando.replace("{argumento_url}", shlex.quote(urllib.parse.quote_plus(argumento))) t0 = time.monotonic() try: r = subprocess.run(["bash", "-lc", comando], capture_output=True, text=True, errors="replace", timeout=tope) salida = (r.stdout or r.stderr or "").strip() return r.returncode, salida, time.monotonic() - t0 except subprocess.TimeoutExpired: return -1, f"(no termino en {tope}s)", time.monotonic() - t0 except OSError as e: return -2, f"(no se pudo ejecutar: {e})", time.monotonic() - t0 def programa_existe(comando): """El primer ejecutable de la orden, para las que abren o mueven ventanas. Algunos son ordenes del PATH (firefox) y otros son scripts nuestros con ruta completa (config/ventanas.sh): `command -v` solo sirve para los primeros, y con los segundos hay que mirar el fichero. """ m = VENTANA.search(comando) if not m: return None nombre = m.group(1) if nombre.endswith(".sh"): # sacar la ruta entera tal como aparece en el comando for trozo in comando.split(): if trozo.endswith(nombre): ruta = os.path.expandvars(os.path.expanduser(trozo)) return os.access(ruta, os.X_OK) return False return subprocess.run(["bash", "-lc", f"command -v {nombre}"], capture_output=True).returncode == 0 def main(): p = argparse.ArgumentParser() p.add_argument("--abrir", action="store_true", help="ejecutar tambien las que abren ventanas") p.add_argument("--grupo") p.add_argument("--json") p.add_argument("--tope", type=int, default=25) args = p.parse_args() acciones = carga() if args.grupo: acciones = [a for a in acciones if a.get("grupo") == args.grupo] resultados = [] print(f" repasando {len(acciones)} acciones" f"{' del grupo ' + args.grupo if args.grupo else ''}\n", flush=True) for a in acciones: ident = a.get("id", "?") comando = a.get("comando") or "" arg = ARGUMENTOS.get(ident, POR_DEFECTO if a.get("captura") else "") if not comando: estado, detalle = "solo habla", a.get("acuse", "")[:50] elif a.get("confirmar") or "sudo" in comando: # no se aprueban solas: en uso las aprueba una persona estado, detalle = "saltada", "destructiva o con root" elif VENTANA.search(comando) and not args.abrir: hay = programa_existe(comando) estado = "programa ok" if hay else "FALTA PROGRAMA" detalle = VENTANA.search(comando).group(1) else: codigo, salida, tardo = ejecuta(a, arg, args.tope) una_linea = " ".join(salida.split())[:70] if codigo == 0: estado = "ok" if salida else "ok (sin salida)" else: estado = f"FALLO ({codigo})" detalle = f"{tardo:4.1f}s {una_linea}" resultados.append({"id": ident, "grupo": a.get("grupo"), "estado": estado, "detalle": detalle}) marca = "!" if "FALLO" in estado or "FALTA" in estado else " " print(f" {marca} {ident:26s} {estado:16s} {detalle}", flush=True) print() from collections import Counter c = Counter(r["estado"].split(" (")[0].split(" ")[0] for r in resultados) for k, v in c.most_common(): print(f" {v:3d} {k}") malas = [r for r in resultados if "FALLO" in r["estado"] or "FALTA" in r["estado"]] print(f"\n {len(resultados) - len(malas)} de {len(resultados)} responden bien") if malas: print(" fallan: " + ", ".join(r["id"] for r in malas)) if args.json: with open(args.json, "w", encoding="utf-8") as f: json.dump(resultados, f, ensure_ascii=False, indent=2) return 1 if malas else 0 if __name__ == "__main__": sys.exit(main())