3.1.0: se instala solo en su sitio, manual dentro y el puerto para una sola copia
- instalar.py: instalar un paquete lo copia a la carpeta de programas del usuario (%LOCALAPPDATA%\Programs\OrganiLAB) con sus datos y su ffmpeg, lo da de alta desde ahí y lo vuelve a abrir desde su sitio. Del origen no se borra nada: queda instalado-en.txt, que además hace que abrir esa copia abra la instalada. Un destino ocupado no se pisa, y si ya hay un OrganiLAB instalado lo dice en vez de poner otro encima. - static/index.html: vista «Manual», con cada función del programa, qué hace y cómo lo hace (41 entradas, un apartado por pestaña). - organizador.py: ServidorLocal pide el puerto en exclusiva. Con «reutilizar dirección», en Windows dos copias escuchaban a la vez en el mismo puerto y se contestaban la una por la otra sin que nada fallara a la vista. - Pruebas: probar.py 19, prueba_funcional.py 26 de 26, prueba_ventana.py 18 desde el código y 17 con el paquete, incluida la instalación entera en un equipo de ensayo (ORGANILAB_ENSAYO) que no toca el Windows de verdad. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
94
probar.py
94
probar.py
@@ -169,10 +169,11 @@ class Windows(unittest.TestCase):
|
||||
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-")
|
||||
tapon = socket.socket()
|
||||
tapon.bind(("127.0.0.1", 0))
|
||||
tapon.listen(1)
|
||||
ocupado = tapon.getsockname()[1]
|
||||
# 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)
|
||||
@@ -192,7 +193,72 @@ class Windows(unittest.TestCase):
|
||||
self.assertTrue(p.stdout.strip().splitlines()[-1].startswith("FIN "))
|
||||
self.assertFalse(os.path.exists(os.path.join(tmp, "datos", "sesion.json")))
|
||||
finally:
|
||||
tapon.close()
|
||||
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):
|
||||
@@ -215,6 +281,24 @@ class Windows(unittest.TestCase):
|
||||
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'<a data-t="([a-z]+)"[^>]*>([^<]+)</a>', html[:html.index("</nav>")])
|
||||
manual = html[html.index('<section id="t-manual">'):html.index("<!-- ============ AJUSTES")]
|
||||
apartados = re.findall(r"<h3>([^<]+)</h3>", manual)
|
||||
self.assertGreaterEqual(len(vistas), 8)
|
||||
for clave, etiqueta in vistas:
|
||||
if clave != "manual":
|
||||
self.assertIn(etiqueta.replace("⚙", "").strip(), apartados)
|
||||
self.assertGreater(manual.count("<dt>"), 35)
|
||||
self.assertEqual(manual.count("<dt>"), manual.count("<dd>"))
|
||||
|
||||
|
||||
class Firma(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
||||
Referencia en una nueva incidencia
Block a user