@rlabs-cl/arketum 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -0
- package/LICENSE +48 -0
- package/README.md +67 -2
- package/THIRD-PARTY-NOTICES +2428 -0
- package/catalogo-de-ejemplo/agentes.yaml +11 -0
- package/catalogo-de-ejemplo/arquetipos/ajuste.yaml +54 -0
- package/catalogo-de-ejemplo/arquetipos/arquetipo-autor.yaml +72 -0
- package/catalogo-de-ejemplo/arquetipos/arquitectura-de-sistema.yaml +72 -0
- package/catalogo-de-ejemplo/arquetipos/despliegue-compose.yaml +104 -0
- package/catalogo-de-ejemplo/arquetipos/digestion.yaml +97 -0
- package/catalogo-de-ejemplo/arquetipos/documento-academico.yaml +131 -0
- package/catalogo-de-ejemplo/arquetipos/inventario-funcional.yaml +42 -0
- package/catalogo-de-ejemplo/arquetipos/levantamiento-de-infraestructura.yaml +117 -0
- package/catalogo-de-ejemplo/arquetipos/plan-de-migracion.yaml +40 -0
- package/catalogo-de-ejemplo/arquetipos/prd.yaml +62 -0
- package/catalogo-de-ejemplo/arquetipos/radio-de-alcance.yaml +141 -0
- package/catalogo-de-ejemplo/arquetipos/rca.yaml +68 -0
- package/catalogo-de-ejemplo/arquetipos/servicio-con-integracion.yaml +154 -0
- package/catalogo-de-ejemplo/carriles/ajuste.yaml +14 -0
- package/catalogo-de-ejemplo/carriles/arquetipo.yaml +13 -0
- package/catalogo-de-ejemplo/carriles/comprobacion.yaml +23 -0
- package/catalogo-de-ejemplo/carriles/correccion.yaml +25 -0
- package/catalogo-de-ejemplo/carriles/despliegue.yaml +36 -0
- package/catalogo-de-ejemplo/carriles/digestion.yaml +14 -0
- package/catalogo-de-ejemplo/carriles/funcionalidad.yaml +34 -0
- package/catalogo-de-ejemplo/carriles/migracion.yaml +27 -0
- package/catalogo-de-ejemplo/carriles/nuevo.yaml +30 -0
- package/catalogo-de-ejemplo/formas/aplicacion-por-capas.yaml +185 -0
- package/catalogo-de-ejemplo/formas/juego-terminal.yaml +161 -0
- package/catalogo-de-ejemplo/formas/monorepo-de-aplicaciones.yaml +207 -0
- package/catalogo-de-ejemplo/incluido.yaml +3 -0
- package/catalogo-de-ejemplo/stacks/go/e2e-terminal/e2e_http.py +36 -0
- package/catalogo-de-ejemplo/stacks/go/e2e-terminal/pytest.ini +5 -0
- package/catalogo-de-ejemplo/stacks/go/e2e-terminal/requirements.txt +2 -0
- package/catalogo-de-ejemplo/stacks/go/e2e-terminal/terminal_e2e.py +112 -0
- package/catalogo-de-ejemplo/stacks/go/e2e-terminal/terminal_prueba.py +118 -0
- package/catalogo-de-ejemplo/stacks/go/suelo-aplicacion-por-capas/requirements.txt +1 -0
- package/catalogo-de-ejemplo/stacks/go/suelo-aplicacion-por-capas/suelo.py +102 -0
- package/catalogo-de-ejemplo/stacks/go/suelo-aplicacion-por-capas/terminal_prueba.py +118 -0
- package/catalogo-de-ejemplo/stacks/go/suelo-juego-terminal/requirements.txt +1 -0
- package/catalogo-de-ejemplo/stacks/go/suelo-juego-terminal/suelo.py +86 -0
- package/catalogo-de-ejemplo/stacks/go/suelo-juego-terminal/terminal_prueba.py +118 -0
- package/catalogo-de-ejemplo/stacks/go.yaml +74 -0
- package/catalogo-de-ejemplo/stacks/python/e2e-terminal/e2e_http.py +36 -0
- package/catalogo-de-ejemplo/stacks/python/e2e-terminal/pytest.ini +5 -0
- package/catalogo-de-ejemplo/stacks/python/e2e-terminal/requirements.txt +2 -0
- package/catalogo-de-ejemplo/stacks/python/e2e-terminal/terminal_e2e.py +112 -0
- package/catalogo-de-ejemplo/stacks/python/e2e-terminal/terminal_prueba.py +118 -0
- package/catalogo-de-ejemplo/stacks/python/suelo-aplicacion-por-capas/requirements.txt +1 -0
- package/catalogo-de-ejemplo/stacks/python/suelo-aplicacion-por-capas/suelo.py +102 -0
- package/catalogo-de-ejemplo/stacks/python/suelo-aplicacion-por-capas/terminal_prueba.py +118 -0
- package/catalogo-de-ejemplo/stacks/python/suelo-juego-terminal/requirements.txt +1 -0
- package/catalogo-de-ejemplo/stacks/python/suelo-juego-terminal/suelo.py +86 -0
- package/catalogo-de-ejemplo/stacks/python/suelo-juego-terminal/terminal_prueba.py +118 -0
- package/catalogo-de-ejemplo/stacks/python.yaml +167 -0
- package/catalogo-de-ejemplo/stacks/typescript/e2e-node/app.mjs +182 -0
- package/catalogo-de-ejemplo/stacks/typescript/e2e-node/requirements.txt +1 -0
- package/catalogo-de-ejemplo/stacks/typescript/e2e-node/terminal_prueba.py +118 -0
- package/catalogo-de-ejemplo/stacks/typescript/e2e-node/terminal_puente.py +51 -0
- package/catalogo-de-ejemplo/stacks/typescript/e2e-node/vitest.config.mjs +12 -0
- package/catalogo-de-ejemplo/stacks/typescript/suelo-aplicacion-por-capas/requirements.txt +1 -0
- package/catalogo-de-ejemplo/stacks/typescript/suelo-aplicacion-por-capas/suelo.py +102 -0
- package/catalogo-de-ejemplo/stacks/typescript/suelo-aplicacion-por-capas/terminal_prueba.py +118 -0
- package/catalogo-de-ejemplo/stacks/typescript/suelo-juego-terminal/requirements.txt +1 -0
- package/catalogo-de-ejemplo/stacks/typescript/suelo-juego-terminal/suelo.py +86 -0
- package/catalogo-de-ejemplo/stacks/typescript/suelo-juego-terminal/terminal_prueba.py +118 -0
- package/catalogo-de-ejemplo/stacks/typescript.yaml +88 -0
- package/dist/mcp/arketum.js +3 -0
- package/dist/mcp/ci-cli.js +2 -0
- package/dist/mcp/cli.js +2 -0
- package/dist/mcp/comun-ZEXDDERM.js +1 -0
- package/dist/mcp/comun-ZTEHRDHC.js +1 -0
- package/dist/mcp/panel-cli.js +2 -0
- package/dist/mcp/telemetria-cli.js +1 -0
- package/package.json +33 -4
- package/server.json +15 -0
- package/telemetria.json +27 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TERMINAL DE PRUEBA (fijación del catálogo de Arketum: se copia tal cual y no se
|
|
3
|
+
edita). Prueba un programa de terminal como lo usaría una persona, en una
|
|
4
|
+
pseudo-terminal emulada con pyte (`python3 -m pip install pyte`).
|
|
5
|
+
|
|
6
|
+
from terminal_prueba import Terminal
|
|
7
|
+
|
|
8
|
+
with Terminal(['sh', 'arrancar.sh'], cwd='.', columnas=80, filas=24) as t:
|
|
9
|
+
t.esperar(2) # deja correr el programa
|
|
10
|
+
t.tecla('ARRIBA') # ARRIBA, ABAJO, IZQUIERDA, DERECHA, ENTER, ESC o un carácter
|
|
11
|
+
t.esperar(0.5)
|
|
12
|
+
texto = t.pantalla() # lo que se ve, una cadena con una línea por fila
|
|
13
|
+
assert t.vivo() # sigue corriendo
|
|
14
|
+
assert 'Traceback' not in texto
|
|
15
|
+
|
|
16
|
+
`t.lineas()` da la lista de filas y `t.codigo` el código de salida cuando termina.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import fcntl, os, pty, select, signal, struct, termios, time
|
|
20
|
+
|
|
21
|
+
import pyte
|
|
22
|
+
|
|
23
|
+
TECLAS = {'ARRIBA': b'\x1b[A', 'ABAJO': b'\x1b[B', 'DERECHA': b'\x1b[C', 'IZQUIERDA': b'\x1b[D', 'ENTER': b'\r', 'ESC': b'\x1b'}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Terminal:
|
|
27
|
+
def __init__(self, comando, cwd='.', columnas=80, filas=24, env=None):
|
|
28
|
+
self._pantalla = pyte.Screen(columnas, filas)
|
|
29
|
+
self._flujo = pyte.ByteStream(self._pantalla)
|
|
30
|
+
pid, fd = pty.fork()
|
|
31
|
+
if pid == 0:
|
|
32
|
+
entorno = dict(os.environ, LANG='en_US.UTF-8', LC_ALL='en_US.UTF-8', TERM='xterm-256color', COLUMNS=str(columnas), LINES=str(filas))
|
|
33
|
+
entorno.update(env or {})
|
|
34
|
+
os.chdir(cwd)
|
|
35
|
+
os.execvpe(comando[0], comando, entorno)
|
|
36
|
+
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', filas, columnas, 0, 0))
|
|
37
|
+
self.pid, self.fd, self._vivo, self.codigo = pid, fd, True, None
|
|
38
|
+
|
|
39
|
+
def __enter__(self):
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def __exit__(self, *_):
|
|
43
|
+
self.cerrar()
|
|
44
|
+
|
|
45
|
+
def esperar(self, segundos):
|
|
46
|
+
fin = time.time() + segundos
|
|
47
|
+
while time.time() < fin:
|
|
48
|
+
r, _, _ = select.select([self.fd], [], [], 0.05)
|
|
49
|
+
if r:
|
|
50
|
+
try:
|
|
51
|
+
datos = os.read(self.fd, 65536)
|
|
52
|
+
except OSError:
|
|
53
|
+
datos = b''
|
|
54
|
+
if datos:
|
|
55
|
+
self._flujo.feed(datos)
|
|
56
|
+
else:
|
|
57
|
+
time.sleep(0.05)
|
|
58
|
+
self._comprobar()
|
|
59
|
+
self._comprobar()
|
|
60
|
+
|
|
61
|
+
def tecla(self, nombre):
|
|
62
|
+
datos = TECLAS.get(nombre, nombre.encode() if isinstance(nombre, str) else nombre)
|
|
63
|
+
# Como un terminal real: si el programa activó el modo de cursor de aplicación (DECCKM, el modo privado 1, que pyte guarda como 1 << 5; curses
|
|
64
|
+
# con keypad), las flechas van como ESC O x en vez de ESC [ x.
|
|
65
|
+
if datos[:2] == b'\x1b[' and len(datos) == 3 and datos[2:] in b'ABCD' and (1 << 5) in self._pantalla.mode:
|
|
66
|
+
datos = b'\x1bO' + datos[2:]
|
|
67
|
+
try:
|
|
68
|
+
os.write(self.fd, datos)
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def pantalla(self):
|
|
73
|
+
return '\n'.join(self._pantalla.display)
|
|
74
|
+
|
|
75
|
+
def lineas(self):
|
|
76
|
+
return list(self._pantalla.display)
|
|
77
|
+
|
|
78
|
+
def pintadas(self):
|
|
79
|
+
"""Cuántas celdas se ven: con carácter, en vídeo inverso o con un fondo distinto del
|
|
80
|
+
dominante (al borrar, pyte rellena con el fondo del cursor: ese no cuenta)."""
|
|
81
|
+
from collections import Counter
|
|
82
|
+
filas, cols = self._pantalla.lines, self._pantalla.columns
|
|
83
|
+
celdas = [self._pantalla.buffer[y][x] for y in range(filas) for x in range(cols)]
|
|
84
|
+
fondo = Counter(c.bg for c in celdas).most_common(1)[0][0]
|
|
85
|
+
return sum(1 for c in celdas if c.data.strip() or c.reverse or c.bg != fondo)
|
|
86
|
+
|
|
87
|
+
def foto(self):
|
|
88
|
+
"""Lo que se ve, con forma: el texto y dónde hay celdas en vídeo inverso o con fondo distinto del dominante."""
|
|
89
|
+
from collections import Counter
|
|
90
|
+
filas, cols = self._pantalla.lines, self._pantalla.columns
|
|
91
|
+
fondo = Counter(self._pantalla.buffer[y][x].bg for y in range(filas) for x in range(cols)).most_common(1)[0][0]
|
|
92
|
+
marcas = tuple((y, x) for y in range(filas) for x in range(cols) if self._pantalla.buffer[y][x].reverse or self._pantalla.buffer[y][x].bg != fondo)
|
|
93
|
+
return (self.pantalla(), marcas)
|
|
94
|
+
|
|
95
|
+
def vivo(self):
|
|
96
|
+
self._comprobar()
|
|
97
|
+
return self._vivo
|
|
98
|
+
|
|
99
|
+
def _comprobar(self):
|
|
100
|
+
if not self._vivo:
|
|
101
|
+
return
|
|
102
|
+
p, estado = os.waitpid(self.pid, os.WNOHANG)
|
|
103
|
+
if p != 0:
|
|
104
|
+
self._vivo = False
|
|
105
|
+
self.codigo = os.waitstatus_to_exitcode(estado)
|
|
106
|
+
|
|
107
|
+
def cerrar(self):
|
|
108
|
+
if self._vivo:
|
|
109
|
+
try:
|
|
110
|
+
os.kill(self.pid, signal.SIGKILL)
|
|
111
|
+
os.waitpid(self.pid, 0)
|
|
112
|
+
except OSError:
|
|
113
|
+
pass
|
|
114
|
+
self._vivo = False
|
|
115
|
+
try:
|
|
116
|
+
os.close(self.fd)
|
|
117
|
+
except OSError:
|
|
118
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyte
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SUELO FIJO de la forma `aplicacion-por-capas` (fijación del catálogo de Arketum:
|
|
4
|
+
se copia tal cual en `arketum/fijaciones/suelo/` y no se edita; el cierre compara
|
|
5
|
+
su checksum). Las comprobaciones de uso que no escribe quien construye: que la
|
|
6
|
+
aplicación vive, se puede usar y cerrar, y vuelve a arrancar con sus datos; no
|
|
7
|
+
que haga bien lo que pide el encargo.
|
|
8
|
+
|
|
9
|
+
Arranca el artefacto por su contrato de arranque: `sh arrancar.sh` desde su raíz,
|
|
10
|
+
en una terminal, con `DATOS_DIR` (la carpeta de sus datos), `HOME` y `PUERTO` (un
|
|
11
|
+
puerto libre, por si sirve HTTP) en una carpeta temporal propia.
|
|
12
|
+
|
|
13
|
+
arranca a 80x24, a los 3 s pinta algo y sin traza (vivo, o esperando órdenes)
|
|
14
|
+
responde tras Enter y unas flechas sigue sin traza
|
|
15
|
+
sale termina limpio (código 0) con `q`, con la orden `salir`/`q`/`exit` + Enter, con la opción de salir del menú, o con fin de entrada
|
|
16
|
+
rearranca con los mismos datos (mismo HOME y DATOS_DIR) vuelve a arrancar sin traza
|
|
17
|
+
|
|
18
|
+
Uso: python3 arketum/fijaciones/suelo/suelo.py <raíz del artefacto> → JSON; código 0 si pasa, 1 si no.
|
|
19
|
+
Necesita `pyte` (`python3 -m pip install -r arketum/fijaciones/suelo/requirements.txt`).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import faulthandler, json, os, re, socket, sys, tempfile
|
|
23
|
+
|
|
24
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
25
|
+
from terminal_prueba import Terminal # noqa: E402
|
|
26
|
+
|
|
27
|
+
ARRANQUE = ['sh', 'arrancar.sh']
|
|
28
|
+
SALIDAS = [b'q', b'\r', b'salir\r', b'q\r', b'exit\r']
|
|
29
|
+
# Una opción de menú para salir, la que se vea en pantalla: «5. Salir», «[0] Exit»…
|
|
30
|
+
OPCION_DE_SALIR = re.compile(r'[\[(]?\s*(\w)\s*[\])\.:-]\s*(?:salir|exit|quit|terminar|cerrar)\b', re.I)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def libre():
|
|
34
|
+
with socket.socket() as s:
|
|
35
|
+
s.bind(('127.0.0.1', 0))
|
|
36
|
+
return s.getsockname()[1]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def pintadas(t):
|
|
40
|
+
n = 0
|
|
41
|
+
for _ in range(5):
|
|
42
|
+
t.esperar(0.15)
|
|
43
|
+
n = max(n, t.pintadas())
|
|
44
|
+
return n
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def evaluar(raiz):
|
|
48
|
+
if not os.path.isfile(os.path.join(raiz, 'arrancar.sh')):
|
|
49
|
+
return {'ok': False, 'fallo': 'no hay `arrancar.sh` en la raíz del artefacto: es su contrato de arranque', 'pantalla': ''}
|
|
50
|
+
r = {'comando': ' '.join(ARRANQUE)}
|
|
51
|
+
casa = tempfile.mkdtemp(prefix='suelo-')
|
|
52
|
+
env = {'HOME': casa, 'DATOS_DIR': os.path.join(casa, 'datos'), 'PUERTO': str(libre())}
|
|
53
|
+
with Terminal(ARRANQUE, cwd=raiz, columnas=80, filas=24, env=env) as t:
|
|
54
|
+
t.esperar(3)
|
|
55
|
+
n = pintadas(t)
|
|
56
|
+
texto = t.pantalla()
|
|
57
|
+
r['arranca'] = 'Traceback' not in texto and n >= 10 and (t.vivo() or t.codigo == 0)
|
|
58
|
+
if not r['arranca'] or not t.vivo():
|
|
59
|
+
return {**r, 'ok': False, 'fallo': 'a 80x24 no arranca: muere, saca una traza o no pinta nada', 'pantalla': texto}
|
|
60
|
+
t.tecla('ENTER')
|
|
61
|
+
for k in ('ABAJO', 'ARRIBA'):
|
|
62
|
+
t.tecla(k)
|
|
63
|
+
t.esperar(0.3)
|
|
64
|
+
t.esperar(1)
|
|
65
|
+
texto = t.pantalla()
|
|
66
|
+
r['responde'] = 'Traceback' not in texto and (t.vivo() or t.codigo == 0)
|
|
67
|
+
if not r['responde']:
|
|
68
|
+
return {**r, 'ok': False, 'fallo': 'tras Enter y unas flechas saca una traza o muere con error', 'pantalla': texto}
|
|
69
|
+
for b in SALIDAS:
|
|
70
|
+
if not t.vivo():
|
|
71
|
+
break
|
|
72
|
+
t.tecla(b)
|
|
73
|
+
t.esperar(1.5)
|
|
74
|
+
for _ in range(2): # la opción de salir del menú que se vea, y como último recurso, fin de entrada
|
|
75
|
+
m = OPCION_DE_SALIR.search(t.pantalla())
|
|
76
|
+
if not t.vivo() or m is None:
|
|
77
|
+
break
|
|
78
|
+
t.tecla(f'{m.group(1)}\r')
|
|
79
|
+
t.esperar(1.5)
|
|
80
|
+
if t.vivo():
|
|
81
|
+
t.tecla(b'\x04')
|
|
82
|
+
t.esperar(1.5)
|
|
83
|
+
texto = t.pantalla()
|
|
84
|
+
r['sale'] = not t.vivo() and t.codigo == 0 and 'Traceback' not in texto
|
|
85
|
+
if not r['sale']:
|
|
86
|
+
return {**r, 'ok': False, 'fallo': 'no termina limpio con `q`, ni con `salir`/`q`/`exit` + Enter, ni con fin de entrada' if t.vivo() else 'al salir saca una traza o termina con error', 'pantalla': texto}
|
|
87
|
+
env['PUERTO'] = str(libre())
|
|
88
|
+
with Terminal(ARRANQUE, cwd=raiz, columnas=80, filas=24, env=env) as t:
|
|
89
|
+
t.esperar(3)
|
|
90
|
+
texto = t.pantalla()
|
|
91
|
+
r['rearranca'] = 'Traceback' not in texto and (t.vivo() or t.codigo == 0)
|
|
92
|
+
if not r['rearranca']:
|
|
93
|
+
return {**r, 'ok': False, 'fallo': 'con los datos de la primera vez, al volver a abrir saca una traza o muere', 'pantalla': texto}
|
|
94
|
+
return {**r, 'ok': True}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == '__main__':
|
|
98
|
+
# Un suelo nunca se queda colgado: a los 150 s escribe dónde estaba y termina con error (1).
|
|
99
|
+
faulthandler.dump_traceback_later(150, exit=True)
|
|
100
|
+
resultado = evaluar(os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else '.'))
|
|
101
|
+
print(json.dumps(resultado, ensure_ascii=False, indent=1))
|
|
102
|
+
sys.exit(0 if resultado['ok'] else 1)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TERMINAL DE PRUEBA (fijación del catálogo de Arketum: se copia tal cual y no se
|
|
3
|
+
edita). Prueba un programa de terminal como lo usaría una persona, en una
|
|
4
|
+
pseudo-terminal emulada con pyte (`python3 -m pip install pyte`).
|
|
5
|
+
|
|
6
|
+
from terminal_prueba import Terminal
|
|
7
|
+
|
|
8
|
+
with Terminal(['sh', 'arrancar.sh'], cwd='.', columnas=80, filas=24) as t:
|
|
9
|
+
t.esperar(2) # deja correr el programa
|
|
10
|
+
t.tecla('ARRIBA') # ARRIBA, ABAJO, IZQUIERDA, DERECHA, ENTER, ESC o un carácter
|
|
11
|
+
t.esperar(0.5)
|
|
12
|
+
texto = t.pantalla() # lo que se ve, una cadena con una línea por fila
|
|
13
|
+
assert t.vivo() # sigue corriendo
|
|
14
|
+
assert 'Traceback' not in texto
|
|
15
|
+
|
|
16
|
+
`t.lineas()` da la lista de filas y `t.codigo` el código de salida cuando termina.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import fcntl, os, pty, select, signal, struct, termios, time
|
|
20
|
+
|
|
21
|
+
import pyte
|
|
22
|
+
|
|
23
|
+
TECLAS = {'ARRIBA': b'\x1b[A', 'ABAJO': b'\x1b[B', 'DERECHA': b'\x1b[C', 'IZQUIERDA': b'\x1b[D', 'ENTER': b'\r', 'ESC': b'\x1b'}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Terminal:
|
|
27
|
+
def __init__(self, comando, cwd='.', columnas=80, filas=24, env=None):
|
|
28
|
+
self._pantalla = pyte.Screen(columnas, filas)
|
|
29
|
+
self._flujo = pyte.ByteStream(self._pantalla)
|
|
30
|
+
pid, fd = pty.fork()
|
|
31
|
+
if pid == 0:
|
|
32
|
+
entorno = dict(os.environ, LANG='en_US.UTF-8', LC_ALL='en_US.UTF-8', TERM='xterm-256color', COLUMNS=str(columnas), LINES=str(filas))
|
|
33
|
+
entorno.update(env or {})
|
|
34
|
+
os.chdir(cwd)
|
|
35
|
+
os.execvpe(comando[0], comando, entorno)
|
|
36
|
+
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', filas, columnas, 0, 0))
|
|
37
|
+
self.pid, self.fd, self._vivo, self.codigo = pid, fd, True, None
|
|
38
|
+
|
|
39
|
+
def __enter__(self):
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def __exit__(self, *_):
|
|
43
|
+
self.cerrar()
|
|
44
|
+
|
|
45
|
+
def esperar(self, segundos):
|
|
46
|
+
fin = time.time() + segundos
|
|
47
|
+
while time.time() < fin:
|
|
48
|
+
r, _, _ = select.select([self.fd], [], [], 0.05)
|
|
49
|
+
if r:
|
|
50
|
+
try:
|
|
51
|
+
datos = os.read(self.fd, 65536)
|
|
52
|
+
except OSError:
|
|
53
|
+
datos = b''
|
|
54
|
+
if datos:
|
|
55
|
+
self._flujo.feed(datos)
|
|
56
|
+
else:
|
|
57
|
+
time.sleep(0.05)
|
|
58
|
+
self._comprobar()
|
|
59
|
+
self._comprobar()
|
|
60
|
+
|
|
61
|
+
def tecla(self, nombre):
|
|
62
|
+
datos = TECLAS.get(nombre, nombre.encode() if isinstance(nombre, str) else nombre)
|
|
63
|
+
# Como un terminal real: si el programa activó el modo de cursor de aplicación (DECCKM, el modo privado 1, que pyte guarda como 1 << 5; curses
|
|
64
|
+
# con keypad), las flechas van como ESC O x en vez de ESC [ x.
|
|
65
|
+
if datos[:2] == b'\x1b[' and len(datos) == 3 and datos[2:] in b'ABCD' and (1 << 5) in self._pantalla.mode:
|
|
66
|
+
datos = b'\x1bO' + datos[2:]
|
|
67
|
+
try:
|
|
68
|
+
os.write(self.fd, datos)
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def pantalla(self):
|
|
73
|
+
return '\n'.join(self._pantalla.display)
|
|
74
|
+
|
|
75
|
+
def lineas(self):
|
|
76
|
+
return list(self._pantalla.display)
|
|
77
|
+
|
|
78
|
+
def pintadas(self):
|
|
79
|
+
"""Cuántas celdas se ven: con carácter, en vídeo inverso o con un fondo distinto del
|
|
80
|
+
dominante (al borrar, pyte rellena con el fondo del cursor: ese no cuenta)."""
|
|
81
|
+
from collections import Counter
|
|
82
|
+
filas, cols = self._pantalla.lines, self._pantalla.columns
|
|
83
|
+
celdas = [self._pantalla.buffer[y][x] for y in range(filas) for x in range(cols)]
|
|
84
|
+
fondo = Counter(c.bg for c in celdas).most_common(1)[0][0]
|
|
85
|
+
return sum(1 for c in celdas if c.data.strip() or c.reverse or c.bg != fondo)
|
|
86
|
+
|
|
87
|
+
def foto(self):
|
|
88
|
+
"""Lo que se ve, con forma: el texto y dónde hay celdas en vídeo inverso o con fondo distinto del dominante."""
|
|
89
|
+
from collections import Counter
|
|
90
|
+
filas, cols = self._pantalla.lines, self._pantalla.columns
|
|
91
|
+
fondo = Counter(self._pantalla.buffer[y][x].bg for y in range(filas) for x in range(cols)).most_common(1)[0][0]
|
|
92
|
+
marcas = tuple((y, x) for y in range(filas) for x in range(cols) if self._pantalla.buffer[y][x].reverse or self._pantalla.buffer[y][x].bg != fondo)
|
|
93
|
+
return (self.pantalla(), marcas)
|
|
94
|
+
|
|
95
|
+
def vivo(self):
|
|
96
|
+
self._comprobar()
|
|
97
|
+
return self._vivo
|
|
98
|
+
|
|
99
|
+
def _comprobar(self):
|
|
100
|
+
if not self._vivo:
|
|
101
|
+
return
|
|
102
|
+
p, estado = os.waitpid(self.pid, os.WNOHANG)
|
|
103
|
+
if p != 0:
|
|
104
|
+
self._vivo = False
|
|
105
|
+
self.codigo = os.waitstatus_to_exitcode(estado)
|
|
106
|
+
|
|
107
|
+
def cerrar(self):
|
|
108
|
+
if self._vivo:
|
|
109
|
+
try:
|
|
110
|
+
os.kill(self.pid, signal.SIGKILL)
|
|
111
|
+
os.waitpid(self.pid, 0)
|
|
112
|
+
except OSError:
|
|
113
|
+
pass
|
|
114
|
+
self._vivo = False
|
|
115
|
+
try:
|
|
116
|
+
os.close(self.fd)
|
|
117
|
+
except OSError:
|
|
118
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyte
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SUELO FIJO de la forma `juego-terminal` (fijación del catálogo de Arketum: se
|
|
4
|
+
copia tal cual en `arketum/fijaciones/suelo/` y no se edita; el cierre compara su
|
|
5
|
+
checksum). Las comprobaciones de uso que no escribe quien construye: que el
|
|
6
|
+
programa vive, no que el juego esté bien.
|
|
7
|
+
|
|
8
|
+
Arranca el artefacto por su contrato de arranque: `sh arrancar.sh` desde su raíz.
|
|
9
|
+
|
|
10
|
+
arranca a 80x24, a los 3 s sigue vivo, pinta algo y sin traza
|
|
11
|
+
se_mueve sin tocar nada, en 2.4 s se ven al menos 5 fotogramas distintos (un juego en tiempo real anima)
|
|
12
|
+
aguanta 8 s más con flechas pulsadas, vivo y sin traza
|
|
13
|
+
sale_q con `q` termina
|
|
14
|
+
pequena a 30x10 no saca traza (avisa o sale limpio)
|
|
15
|
+
|
|
16
|
+
Uso: python3 arketum/fijaciones/suelo/suelo.py <raíz del artefacto> → JSON; código 0 si pasa, 1 si no.
|
|
17
|
+
Necesita `pyte` (`python3 -m pip install -r arketum/fijaciones/suelo/requirements.txt`).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import faulthandler, json, os, sys
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
23
|
+
from terminal_prueba import Terminal # noqa: E402
|
|
24
|
+
|
|
25
|
+
ARRANQUE = ['sh', 'arrancar.sh']
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def pintadas(t):
|
|
29
|
+
"""Lo más pintado en unas cuantas muestras: un juego que borra y redibuja puede pillarse a medias."""
|
|
30
|
+
n = 0
|
|
31
|
+
for _ in range(5):
|
|
32
|
+
t.esperar(0.15)
|
|
33
|
+
n = max(n, t.pintadas())
|
|
34
|
+
return n
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def evaluar(raiz):
|
|
38
|
+
if not os.path.isfile(os.path.join(raiz, 'arrancar.sh')):
|
|
39
|
+
return {'ok': False, 'fallo': 'no hay `arrancar.sh` en la raíz del artefacto: es su contrato de arranque', 'pantalla': ''}
|
|
40
|
+
r = {'comando': ' '.join(ARRANQUE)}
|
|
41
|
+
with Terminal(ARRANQUE, cwd=raiz, columnas=80, filas=24) as t:
|
|
42
|
+
t.tecla('ENTER') # por si hay una pantalla de inicio que espera una tecla
|
|
43
|
+
t.esperar(3)
|
|
44
|
+
n = pintadas(t)
|
|
45
|
+
texto = t.pantalla()
|
|
46
|
+
r['arranca'] = t.vivo() and 'Traceback' not in texto and n >= 10
|
|
47
|
+
if not r['arranca']:
|
|
48
|
+
return {**r, 'ok': False, 'fallo': 'a 80x24 no arranca: muere, saca una traza o no pinta nada', 'pantalla': texto}
|
|
49
|
+
# Movimiento: muchos fotogramas distintos. Un juego que borra y redibuja parpadea entre dos
|
|
50
|
+
# estados (completo y vacío), y el parpadeo no es movimiento.
|
|
51
|
+
fotos = set()
|
|
52
|
+
for _ in range(12):
|
|
53
|
+
t.esperar(0.2)
|
|
54
|
+
fotos.add(t.foto())
|
|
55
|
+
r['se_mueve'] = len(fotos) >= 5
|
|
56
|
+
if not r['se_mueve']:
|
|
57
|
+
return {**r, 'ok': False, 'fallo': 'la pantalla no cambia sola: no hay nada en movimiento', 'pantalla': t.pantalla()}
|
|
58
|
+
for i in range(32):
|
|
59
|
+
t.tecla('ARRIBA' if (i // 4) % 2 else 'ABAJO')
|
|
60
|
+
t.esperar(0.25)
|
|
61
|
+
if not t.vivo():
|
|
62
|
+
break
|
|
63
|
+
texto = t.pantalla()
|
|
64
|
+
r['aguanta'] = t.vivo() and 'Traceback' not in texto
|
|
65
|
+
if not r['aguanta']:
|
|
66
|
+
return {**r, 'ok': False, 'fallo': 'jugando 8 s con flechas, el programa muere o saca una traza', 'pantalla': texto}
|
|
67
|
+
t.tecla('q')
|
|
68
|
+
t.esperar(3)
|
|
69
|
+
r['sale_q'] = not t.vivo()
|
|
70
|
+
if not r['sale_q']:
|
|
71
|
+
return {**r, 'ok': False, 'fallo': 'con `q` el programa no termina', 'pantalla': t.pantalla()}
|
|
72
|
+
with Terminal(ARRANQUE, cwd=raiz, columnas=30, filas=10) as t:
|
|
73
|
+
t.esperar(2.5)
|
|
74
|
+
texto = t.pantalla()
|
|
75
|
+
r['pequena'] = 'Traceback' not in texto and (t.vivo() or t.codigo == 0)
|
|
76
|
+
if not r['pequena']:
|
|
77
|
+
return {**r, 'ok': False, 'fallo': 'a 30x10 saca una traza o sale con error en vez de avisar', 'pantalla': texto}
|
|
78
|
+
return {**r, 'ok': True}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == '__main__':
|
|
82
|
+
# Un suelo nunca se queda colgado: a los 150 s escribe dónde estaba y termina con error (1).
|
|
83
|
+
faulthandler.dump_traceback_later(150, exit=True)
|
|
84
|
+
resultado = evaluar(os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else '.'))
|
|
85
|
+
print(json.dumps(resultado, ensure_ascii=False, indent=1))
|
|
86
|
+
sys.exit(0 if resultado['ok'] else 1)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TERMINAL DE PRUEBA (fijación del catálogo de Arketum: se copia tal cual y no se
|
|
3
|
+
edita). Prueba un programa de terminal como lo usaría una persona, en una
|
|
4
|
+
pseudo-terminal emulada con pyte (`python3 -m pip install pyte`).
|
|
5
|
+
|
|
6
|
+
from terminal_prueba import Terminal
|
|
7
|
+
|
|
8
|
+
with Terminal(['sh', 'arrancar.sh'], cwd='.', columnas=80, filas=24) as t:
|
|
9
|
+
t.esperar(2) # deja correr el programa
|
|
10
|
+
t.tecla('ARRIBA') # ARRIBA, ABAJO, IZQUIERDA, DERECHA, ENTER, ESC o un carácter
|
|
11
|
+
t.esperar(0.5)
|
|
12
|
+
texto = t.pantalla() # lo que se ve, una cadena con una línea por fila
|
|
13
|
+
assert t.vivo() # sigue corriendo
|
|
14
|
+
assert 'Traceback' not in texto
|
|
15
|
+
|
|
16
|
+
`t.lineas()` da la lista de filas y `t.codigo` el código de salida cuando termina.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import fcntl, os, pty, select, signal, struct, termios, time
|
|
20
|
+
|
|
21
|
+
import pyte
|
|
22
|
+
|
|
23
|
+
TECLAS = {'ARRIBA': b'\x1b[A', 'ABAJO': b'\x1b[B', 'DERECHA': b'\x1b[C', 'IZQUIERDA': b'\x1b[D', 'ENTER': b'\r', 'ESC': b'\x1b'}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Terminal:
|
|
27
|
+
def __init__(self, comando, cwd='.', columnas=80, filas=24, env=None):
|
|
28
|
+
self._pantalla = pyte.Screen(columnas, filas)
|
|
29
|
+
self._flujo = pyte.ByteStream(self._pantalla)
|
|
30
|
+
pid, fd = pty.fork()
|
|
31
|
+
if pid == 0:
|
|
32
|
+
entorno = dict(os.environ, LANG='en_US.UTF-8', LC_ALL='en_US.UTF-8', TERM='xterm-256color', COLUMNS=str(columnas), LINES=str(filas))
|
|
33
|
+
entorno.update(env or {})
|
|
34
|
+
os.chdir(cwd)
|
|
35
|
+
os.execvpe(comando[0], comando, entorno)
|
|
36
|
+
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', filas, columnas, 0, 0))
|
|
37
|
+
self.pid, self.fd, self._vivo, self.codigo = pid, fd, True, None
|
|
38
|
+
|
|
39
|
+
def __enter__(self):
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def __exit__(self, *_):
|
|
43
|
+
self.cerrar()
|
|
44
|
+
|
|
45
|
+
def esperar(self, segundos):
|
|
46
|
+
fin = time.time() + segundos
|
|
47
|
+
while time.time() < fin:
|
|
48
|
+
r, _, _ = select.select([self.fd], [], [], 0.05)
|
|
49
|
+
if r:
|
|
50
|
+
try:
|
|
51
|
+
datos = os.read(self.fd, 65536)
|
|
52
|
+
except OSError:
|
|
53
|
+
datos = b''
|
|
54
|
+
if datos:
|
|
55
|
+
self._flujo.feed(datos)
|
|
56
|
+
else:
|
|
57
|
+
time.sleep(0.05)
|
|
58
|
+
self._comprobar()
|
|
59
|
+
self._comprobar()
|
|
60
|
+
|
|
61
|
+
def tecla(self, nombre):
|
|
62
|
+
datos = TECLAS.get(nombre, nombre.encode() if isinstance(nombre, str) else nombre)
|
|
63
|
+
# Como un terminal real: si el programa activó el modo de cursor de aplicación (DECCKM, el modo privado 1, que pyte guarda como 1 << 5; curses
|
|
64
|
+
# con keypad), las flechas van como ESC O x en vez de ESC [ x.
|
|
65
|
+
if datos[:2] == b'\x1b[' and len(datos) == 3 and datos[2:] in b'ABCD' and (1 << 5) in self._pantalla.mode:
|
|
66
|
+
datos = b'\x1bO' + datos[2:]
|
|
67
|
+
try:
|
|
68
|
+
os.write(self.fd, datos)
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def pantalla(self):
|
|
73
|
+
return '\n'.join(self._pantalla.display)
|
|
74
|
+
|
|
75
|
+
def lineas(self):
|
|
76
|
+
return list(self._pantalla.display)
|
|
77
|
+
|
|
78
|
+
def pintadas(self):
|
|
79
|
+
"""Cuántas celdas se ven: con carácter, en vídeo inverso o con un fondo distinto del
|
|
80
|
+
dominante (al borrar, pyte rellena con el fondo del cursor: ese no cuenta)."""
|
|
81
|
+
from collections import Counter
|
|
82
|
+
filas, cols = self._pantalla.lines, self._pantalla.columns
|
|
83
|
+
celdas = [self._pantalla.buffer[y][x] for y in range(filas) for x in range(cols)]
|
|
84
|
+
fondo = Counter(c.bg for c in celdas).most_common(1)[0][0]
|
|
85
|
+
return sum(1 for c in celdas if c.data.strip() or c.reverse or c.bg != fondo)
|
|
86
|
+
|
|
87
|
+
def foto(self):
|
|
88
|
+
"""Lo que se ve, con forma: el texto y dónde hay celdas en vídeo inverso o con fondo distinto del dominante."""
|
|
89
|
+
from collections import Counter
|
|
90
|
+
filas, cols = self._pantalla.lines, self._pantalla.columns
|
|
91
|
+
fondo = Counter(self._pantalla.buffer[y][x].bg for y in range(filas) for x in range(cols)).most_common(1)[0][0]
|
|
92
|
+
marcas = tuple((y, x) for y in range(filas) for x in range(cols) if self._pantalla.buffer[y][x].reverse or self._pantalla.buffer[y][x].bg != fondo)
|
|
93
|
+
return (self.pantalla(), marcas)
|
|
94
|
+
|
|
95
|
+
def vivo(self):
|
|
96
|
+
self._comprobar()
|
|
97
|
+
return self._vivo
|
|
98
|
+
|
|
99
|
+
def _comprobar(self):
|
|
100
|
+
if not self._vivo:
|
|
101
|
+
return
|
|
102
|
+
p, estado = os.waitpid(self.pid, os.WNOHANG)
|
|
103
|
+
if p != 0:
|
|
104
|
+
self._vivo = False
|
|
105
|
+
self.codigo = os.waitstatus_to_exitcode(estado)
|
|
106
|
+
|
|
107
|
+
def cerrar(self):
|
|
108
|
+
if self._vivo:
|
|
109
|
+
try:
|
|
110
|
+
os.kill(self.pid, signal.SIGKILL)
|
|
111
|
+
os.waitpid(self.pid, 0)
|
|
112
|
+
except OSError:
|
|
113
|
+
pass
|
|
114
|
+
self._vivo = False
|
|
115
|
+
try:
|
|
116
|
+
os.close(self.fd)
|
|
117
|
+
except OSError:
|
|
118
|
+
pass
|