# -*- coding: utf-8 -*- r""" Pruebas automáticas de OrganiLAB. Se corren con: py -3.12 probar.py - reglas: detección de cámara, mes, año, basura y proxies - firma: firmar/verificar y rechazo de manifiestos alterados o sin firmar - servidor (extremo a extremo): copia el programa a una carpeta temporal, lo arranca en un puerto libre, registra una carpeta de ensayo, la escanea, calcula el plan, lo aplica, lo deshace, y la desconecta. No toca datos\ ni el inventario real: todo ocurre en la carpeta temporal. """ import os import sys import json import time import shutil import socket import tempfile import unittest import subprocess import urllib.parse import urllib.error import urllib.request AQUI = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, AQUI) import reglas as R # noqa: E402 import firma # noqa: E402 class Reglas(unittest.TestCase): def test_firma_camara(self): self.assertEqual(R.firma_archivo("C0001.MP4"), "SONY") self.assertEqual(R.firma_archivo("C0001M01.XML"), "SONY") self.assertEqual(R.firma_archivo("20260301_labstream0001.mp4"), "SONY") self.assertIsNone(R.firma_archivo("informe final.docx")) def test_mes(self): self.assertEqual(R.detectar_mes("09- Septiembre"), (9, False)) self.assertEqual(R.detectar_mes("02-marzo"), (3, True)) # gana el nombre, con conflicto self.assertEqual(R.detectar_mes("MAYO"), (5, False)) self.assertEqual(R.detectar_mes("3"), (3, True)) # número solo: confianza media self.assertEqual(R.detectar_mes("13"), (None, False)) self.assertEqual(R.detectar_mes("Brigard"), (None, False)) def test_anio(self): self.assertEqual(R.detectar_anio("2026"), 2026) self.assertIsNone(R.detectar_anio("2026 - agosto")) def test_app_proyecto(self): self.assertEqual(R.app_proyecto(".prproj"), "Premiere") self.assertEqual(R.app_proyecto(".AEP"), "After Effects") self.assertEqual(R.app_proyecto(".drp"), "DaVinci Resolve") self.assertEqual(R.app_proyecto(".psd"), "Photoshop") self.assertIsNone(R.app_proyecto(".mp4")) # la plantilla de proyectos separa por programa self.assertIn("{app}", R.PLANTILLAS["contenido"]["PROYECTOS"]) def test_dia(self): self.assertEqual(R.nombre_dia("2026-05-15"), "15- Viernes") self.assertEqual(R.nombre_dia("2025-12-17"), "17- Miércoles") self.assertEqual(R.nombre_dia("nada"), "") # la plantilla de grabación lleva el día, que se rellena solo cuando hace falta self.assertIn("{dia}", R.PLANTILLAS["contenido"]["GRABACION"]) def test_basura_y_proxies(self): self.assertTrue(R.es_basura_archivo("Thumbs.db")) self.assertTrue(R.es_basura_archivo(".DS_Store")) self.assertFalse(R.es_basura_archivo("C0001.MP4")) self.assertTrue(R.es_proxy_dir("Proxy")) self.assertFalse(R.es_proxy_dir("Entregas")) class Config(unittest.TestCase): def test_config_roto_se_aparta(self): """Un config.json ilegible no se pisa en silencio: queda una copia .roto-* y se parte de cero.""" tmp = tempfile.mkdtemp(prefix="organilab-cfg-") try: ruta = os.path.join(tmp, "config.json") with open(ruta, "w", encoding="utf-8") as fh: fh.write('{"raices": [ esto no es json') src = open(os.path.join(AQUI, "organizador.py"), encoding="utf-8").read() ini = src.index("def cargar_json(") fin = src.index("def guardar_json(") ns = {"json": json, "os": os, "shutil": shutil, "time": time} exec(src[ini:fin], ns) out = ns["cargar_json"](ruta, {"raices": [], "a": 1}) self.assertEqual(out, {"raices": [], "a": 1}) self.assertTrue([f for f in os.listdir(tmp) if f.startswith("config.json.roto-")], "no dejó copia del roto") finally: shutil.rmtree(tmp, ignore_errors=True) @unittest.skipUnless(os.name == "nt", "la ventana y los accesos directos son de Windows") class Windows(unittest.TestCase): def test_tablas_de_la_ventana(self): """Todos los identificadores de interfaz de ventana.py están bien escritos, y preguntar si hay WebView2 no revienta en ningún equipo (la respuesta depende del equipo).""" import ventana ids = [v for k, v in vars(ventana).items() if k.startswith(("IID_", "CLSID_", "FMT_")) and isinstance(v, str)] self.assertGreater(len(ids), 20) for g in ids: self.assertEqual(len(bytes(ventana._guid(g))), 16) ok, detalle = ventana.disponible(AQUI) self.assertIsInstance(ok, bool) self.assertTrue(detalle) self.assertFalse(ventana.traer_al_frente(123456789)) # nadie abierto con esa marca def test_acceso_directo(self): """El acceso directo que se escribe dice lo que se le pidió, identidad de OrganiLAB incluida: sin ella, la barra de tareas trataría el acceso anclado y la ventana abierta como dos programas.""" import ventana import instalar tmp = tempfile.mkdtemp(prefix="organilab-lnk-") try: exe = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "notepad.exe") ruta = instalar.acceso_directo(os.path.join(tmp, "OrganiLAB.lnk"), exe, '"una carpeta\\organizador.py"', tmp, os.path.join(AQUI, "static", "organilab.ico")) a = instalar.leer_acceso(ruta) self.assertEqual(os.path.normcase(a["exe"]), os.path.normcase(exe)) self.assertEqual(a["args"], '"una carpeta\\organizador.py"') self.assertEqual(os.path.normcase(a["carpeta"]), os.path.normcase(tmp)) self.assertTrue(a["icono"].lower().endswith("organilab.ico")) self.assertEqual(a["identidad"], ventana.IDENTIDAD) finally: shutil.rmtree(tmp, ignore_errors=True) def test_instalar_y_quitar(self): """Dar de alta el programa en Windows y quitarlo, de punta a punta pero sin tocar el menú Inicio ni el Escritorio de verdad: los accesos van a una carpeta temporal y la línea de Aplicaciones a una clave de ensayo.""" import winreg import instalar tmp = tempfile.mkdtemp(prefix="organilab-alta-") base = os.path.join(tmp, "programa") os.makedirs(os.path.join(base, "static")) inicio, escritorio = os.path.join(tmp, "inicio", "OrganiLAB.lnk"), os.path.join(tmp, "escritorio", "OrganiLAB.lnk") os.makedirs(os.path.dirname(inicio)) os.makedirs(os.path.dirname(escritorio)) viejos = instalar._accesos, instalar.CLAVE instalar._accesos, instalar.CLAVE = (lambda: (inicio, escritorio)), r"Software\OrganiLAB-ensayo\Alta" try: self.assertIsNone(instalar.instalado()) r = instalar.instalar(base, "3.0.0") self.assertEqual(sorted(r["accesos"]), sorted([inicio, escritorio])) self.assertTrue(instalar.instalado_aqui(base)) self.assertFalse(instalar.instalado_aqui(tmp)) self.assertEqual(os.path.normcase(instalar.leer_acceso(inicio)["carpeta"]), os.path.normcase(base)) # tras una actualización: sube la versión que enseña Windows, y no repone el acceso que alguien quitó self.assertFalse(instalar.al_dia(base, "3.0.0")) os.remove(escritorio) self.assertTrue(instalar.al_dia(base, "3.0.1")) self.assertEqual(instalar.instalado()["version"], "3.0.1") self.assertFalse(os.path.exists(escritorio)) # un acceso directo que no se deja reescribir (pasó con el del Escritorio en OneDrive) no impide # apuntar la versión nueva; y uno que ya abre lo que debe ni se toca open(os.path.join(base, "OrganizadorLabstream.exe"), "wb").close() # ahora el programa se abre con otra cosa de_verdad, intentos = instalar.acceso_directo, [] instalar.acceso_directo = lambda *a, **k: (intentos.append(a[0]), (_ for _ in ()).throw(OSError("ocupado")))[1] try: self.assertTrue(instalar.al_dia(base, "3.0.2")) finally: instalar.acceso_directo = de_verdad self.assertEqual(intentos, [inicio]) self.assertEqual(instalar.instalado()["version"], "3.0.2") self.assertTrue(instalar.al_dia(base, "3.0.2")) # ahora sí se deja: lo pone al día self.assertTrue(instalar._misma(instalar.leer_acceso(inicio)["exe"], os.path.join(base, "OrganizadorLabstream.exe"))) self.assertFalse(instalar.al_dia(base, "3.0.2")) # y ya no queda nada que cambiar os.remove(os.path.join(base, "OrganizadorLabstream.exe")) instalar.al_dia(base, "3.0.2") # quitarlo desde otra copia del programa no toca lo de esta; desde esta, sí self.assertEqual(instalar.desinstalar(tmp)["accesos"], []) self.assertTrue(instalar.instalado_aqui(base)) # desinstalar de verdad quita también la tarea del orden automático; contra la clave de ensayo # no debe ni intentarlo: aquí se comprueba que no llama al Programador de tareas llamadas, correr = [], instalar.subprocess.run instalar.subprocess.run = lambda *a, **k: llamadas.append(a) try: self.assertEqual(instalar.desinstalar(base)["accesos"], [inicio]) finally: instalar.subprocess.run = correr self.assertEqual(llamadas, []) self.assertIsNone(instalar.instalado()) finally: for clave in (instalar.CLAVE, r"Software\OrganiLAB-ensayo"): try: winreg.DeleteKey(winreg.HKEY_CURRENT_USER, clave) except OSError: pass instalar._accesos, instalar.CLAVE = viejos shutil.rmtree(tmp, ignore_errors=True) def test_sin_ventana_y_con_el_puerto_ocupado(self): """Dos imprevistos a la vez. El puerto de siempre lo tiene otro programa: se toma uno libre en vez de abrir la página de ese otro. Y este Windows no puede abrir la ventana (aquí, porque falta el cargador de WebView2): la interfaz va al navegador y el programa se va solo cuando esa página deja de preguntar.""" tmp = tempfile.mkdtemp(prefix="organilab-imprevistos-") # quien ocupa el puerto es un servidor como el de otro OrganiLAB: pide «reutilizar dirección», y en # Windows eso dejaba que un segundo programa escuchara en el mismo puerto sin que nada fallara import http.server tapon = http.server.ThreadingHTTPServer(("127.0.0.1", 0), http.server.BaseHTTPRequestHandler) ocupado = tapon.server_address[1] try: for f in ("organizador.py", "reglas.py", "asistente.py", "actualizador.py", "resolve_bridge.py", "firma.py", "ventana.py", "instalar.py"): shutil.copy(os.path.join(AQUI, f), tmp) shutil.copytree(os.path.join(AQUI, "static"), os.path.join(tmp, "static")) guion = ("import sys; sys.argv = ['organizador.py']; import organizador as O; " f"O.PUERTO = {ocupado}; O.SILENCIO_NAVEGADOR = 2; " "O.webbrowser.open = lambda u: print('ABRE', u, flush=True); O.main(); print('FIN', O.PUERTO, flush=True)") env = {k: v for k, v in os.environ.items() if k != "ORGANIZADOR_PUERTO"} env["PYTHONIOENCODING"] = "utf-8" p = subprocess.run([sys.executable, "-c", guion], cwd=tmp, env=env, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=90) self.assertEqual(p.returncode, 0, p.stderr[-600:]) abre = [l for l in p.stdout.splitlines() if l.startswith("ABRE ")] self.assertEqual(len(abre), 1, p.stdout[-600:]) self.assertNotIn(f":{ocupado}/", abre[0]) self.assertIn("Sin ventana propia", p.stdout) self.assertTrue(p.stdout.strip().splitlines()[-1].startswith("FIN ")) self.assertFalse(os.path.exists(os.path.join(tmp, "datos", "sesion.json"))) finally: tapon.server_close() shutil.rmtree(tmp, ignore_errors=True) def test_instalarse_en_su_sitio(self): """Instalar un paquete lo lleva a su sitio definitivo con sus datos, sin borrar nada del origen. No viaja lo que es de la sesión abierta ni lo que se rehace solo, y la copia de origen queda sabiendo adónde se fue.""" import sqlite3 import winreg import instalar tmp = tempfile.mkdtemp(prefix="organilab-mudanza-") base, destino = os.path.join(tmp, "Descargas", "OrganizadorLabstream"), os.path.join(tmp, "Programs", "OrganiLAB") for rel in ("OrganizadorLabstream.exe", "organizador.py", "static/index.html", "static/organilab.ico", "ffmpeg/ffmpeg.exe", "datos/config.json", "datos/bitacora.jsonl", "datos/sesion.json", "datos/ventana/EBWebView/cache.bin", "actualizacion/nuevo.zip", "__pycache__/organizador.cpython-312.pyc"): p = os.path.join(base, rel) os.makedirs(os.path.dirname(p), exist_ok=True) with open(p, "wb") as fh: fh.write(rel.encode() * 50) con = sqlite3.connect(os.path.join(base, "datos", "inventario.sqlite")) con.execute("CREATE TABLE archivos (ruta TEXT)") con.execute("INSERT INTO archivos VALUES ('V:/Cliente/C0001.MP4')") con.commit() con.close() inicio, escritorio = os.path.join(tmp, "inicio", "OrganiLAB.lnk"), os.path.join(tmp, "escritorio", "OrganiLAB.lnk") os.makedirs(os.path.dirname(inicio)) os.makedirs(os.path.dirname(escritorio)) viejos = instalar._accesos, instalar.CLAVE, instalar.sitio_definitivo instalar._accesos, instalar.CLAVE, instalar.sitio_definitivo = (lambda: (inicio, escritorio)), r"Software\OrganiLAB-ensayo\Mudanza", (lambda: destino) try: self.assertTrue(instalar.hay_que_mudar(base)) self.assertEqual(instalar.mudado(base), "") pasos = [] r = instalar.poner(base, "3.1.0", avance=pasos.append) self.assertTrue(r["mudado"]) self.assertTrue(instalar._misma(r["base"], destino)) self.assertTrue(pasos and 0 < pasos[-1] <= 1) for rel in ("OrganizadorLabstream.exe", "organizador.py", "static/index.html", "ffmpeg/ffmpeg.exe", "datos/config.json", "datos/bitacora.jsonl"): self.assertTrue(os.path.exists(os.path.join(destino, rel)), rel) for rel in ("datos/sesion.json", "datos/ventana", "actualizacion", "__pycache__", instalar.MARCA_MUDANZA): self.assertFalse(os.path.exists(os.path.join(destino, rel)), rel) con = sqlite3.connect(os.path.join(destino, "datos", "inventario.sqlite")) self.assertEqual(con.execute("SELECT ruta FROM archivos").fetchall(), [("V:/Cliente/C0001.MP4",)]) con.close() # del origen no se ha ido nada, y ya sabe adónde se instaló for rel in ("OrganizadorLabstream.exe", "ffmpeg/ffmpeg.exe", "datos/sesion.json", "datos/inventario.sqlite"): self.assertTrue(os.path.exists(os.path.join(base, rel)), rel) self.assertTrue(instalar._misma(os.path.dirname(instalar.mudado(base)), destino)) # dado de alta desde su sitio, no desde Descargas self.assertTrue(instalar.instalado_aqui(destino)) self.assertTrue(instalar._misma(instalar.leer_acceso(inicio)["exe"], os.path.join(destino, "OrganizadorLabstream.exe"))) # instalar otra vez desde el origen avisa de que ya hay uno; desde su sitio, es solo darlo de alta with self.assertRaises(instalar.YaInstalado): instalar.poner(base, "3.1.0") self.assertFalse(instalar.poner(destino, "3.1.0")["mudado"]) # y un destino ocupado no se pisa nunca instalar.desinstalar(destino) with self.assertRaises(FileExistsError): instalar.mudar(base) self.assertTrue(os.path.exists(os.path.join(destino, "organizador.py"))) finally: for clave in (instalar.CLAVE, r"Software\OrganiLAB-ensayo"): try: winreg.DeleteKey(winreg.HKEY_CURRENT_USER, clave) except OSError: pass instalar._accesos, instalar.CLAVE, instalar.sitio_definitivo = viejos shutil.rmtree(tmp, ignore_errors=True) def test_orden_de_arranque(self): """Desde el código el programa se abre con el Python sin consola; en el paquete, con su propio .exe.""" import instalar tmp = tempfile.mkdtemp(prefix="organilab-orden-") try: exe, args = instalar.orden(tmp) self.assertTrue(exe.lower().endswith(("pythonw.exe", "python.exe"))) self.assertEqual(args, '"%s"' % os.path.join(tmp, "organizador.py")) open(os.path.join(tmp, "OrganizadorLabstream.exe"), "wb").close() self.assertEqual(instalar.orden(tmp), (os.path.join(tmp, "OrganizadorLabstream.exe"), "")) finally: shutil.rmtree(tmp, ignore_errors=True) def test_arranque_no_estorba(self): """arranque.py es el sitecustomize del paquete. Fuera de un paquete (este Python no vive junto a un organizador.py) y usado como intérprete normal, no hace nada: ni lanza el programa ni corta el proceso.""" p = subprocess.run([sys.executable, "-c", "import arranque; print('sigue')"], cwd=AQUI, capture_output=True, text=True, timeout=60) self.assertEqual(p.stdout.strip(), "sigue") class Manual(unittest.TestCase): def test_manual_al_dia(self): """El manual vive dentro de la interfaz, con un apartado por cada pestaña del programa: si se añade una vista y no se documenta, esto falla.""" import re with open(os.path.join(AQUI, "static", "index.html"), encoding="utf-8") as fh: html = fh.read() vistas = re.findall(r']*>([^<]+)', html[:html.index("")]) manual = html[html.index('
'):html.index("