@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,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
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Stack: Go.
|
|
2
|
+
#
|
|
3
|
+
# Toma las ubicaciones de `servicio-con-integracion` (1.1.0): cada paquete en
|
|
4
|
+
# `internal/`, con las pruebas `<nombre>_test.go` junto a lo que prueban; cada
|
|
5
|
+
# ubicación de código las excluye, para que solo las reclame `prueba`.
|
|
6
|
+
#
|
|
7
|
+
# Cambios:
|
|
8
|
+
# - 1.1.0: las fijaciones de `juego-terminal` y de `aplicacion-por-capas` (los suelos y la ayuda e2e por
|
|
9
|
+
# terminal, de caja negra en Python), sus comandos y `arrancar.sh` como
|
|
10
|
+
# arranque; las pruebas e2e en `tests/e2e/`; lo que genera Python.
|
|
11
|
+
# - 1.2.0: `noEjecutan`, los argumentos con que el ejecutor de la suite e2e
|
|
12
|
+
# (pytest) no ejecuta ninguna prueba, que antes sabía Arketum. TRANSICIÓN: la
|
|
13
|
+
# huella del stack y la del efectivo cambian; los descensos certificados con la
|
|
14
|
+
# anterior dan `stale_policy` hasta recertificarlos.
|
|
15
|
+
name: go
|
|
16
|
+
version: 1.2.0
|
|
17
|
+
description: >-
|
|
18
|
+
Go: un módulo por artefacto, con sus paquetes en `internal/` y las pruebas `<nombre>_test.go` junto al código; `go.mod` y `go.sum` como módulo, `cmd/<binario>/main.go` y `arrancar.sh` como arranque, y las pruebas de extremo a extremo, de caja negra, en `tests/e2e/`.
|
|
19
|
+
|
|
20
|
+
ubicaciones:
|
|
21
|
+
modulo: { pattern: "go.<fichero>" }
|
|
22
|
+
dominio: { pattern: "internal/dominio/<nombre>.go", excluding: ["internal/dominio/<nombre>_test.go"] }
|
|
23
|
+
almacen: { pattern: "internal/almacen/<nombre>.go", excluding: ["internal/almacen/<nombre>_test.go"] }
|
|
24
|
+
servicio: { pattern: "internal/servicios/<nombre>.go", excluding: ["internal/servicios/<nombre>_test.go"] }
|
|
25
|
+
interfaz: { pattern: "internal/interfaz/<nombre>.go", excluding: ["internal/interfaz/<nombre>_test.go"] }
|
|
26
|
+
api: { pattern: "internal/api/<nombre>.go", excluding: ["internal/api/<nombre>_test.go"] }
|
|
27
|
+
cliente: { pattern: "internal/clientes/<nombre>.go", excluding: ["internal/clientes/<nombre>_test.go"] }
|
|
28
|
+
arranque: { pattern: ["cmd/<binario>/main.go", "arrancar.sh"] }
|
|
29
|
+
prueba: { pattern: ["internal/<paquete>/<nombre>_test.go", "tests/e2e/<nombre>.py"] }
|
|
30
|
+
|
|
31
|
+
muestras:
|
|
32
|
+
modulo: [go.mod, go.sum]
|
|
33
|
+
dominio: [internal/dominio/pedido.go]
|
|
34
|
+
almacen: [internal/almacen/sqlite.go]
|
|
35
|
+
servicio: [internal/servicios/pedidos.go]
|
|
36
|
+
interfaz: [internal/interfaz/web.go]
|
|
37
|
+
api: [internal/api/rutas.go]
|
|
38
|
+
cliente: [internal/clientes/catalogo.go]
|
|
39
|
+
arranque: [cmd/tienda/main.go, arrancar.sh]
|
|
40
|
+
prueba: [internal/servicios/pedidos_test.go, internal/dominio/pedido_test.go, tests/e2e/test_partida.py, tests/e2e/conftest.py]
|
|
41
|
+
|
|
42
|
+
# Lo que generan las pruebas e2e y las fijaciones, que son de Python.
|
|
43
|
+
generados: [__pycache__/, .pytest_cache/]
|
|
44
|
+
|
|
45
|
+
oficio:
|
|
46
|
+
- Toda frontera con otro sistema es una interfaz que el servicio declara tal como la necesita, con un doble para probar sin el sistema real.
|
|
47
|
+
- 'Los errores se devuelven y se envuelven con contexto (`fmt.Errorf("…: %w", err)`); nunca se ignoran ni se convierten en `panic`.'
|
|
48
|
+
- Ningún paquete de `internal/` importa `cmd/`; el cableado vive en `cmd/<binario>/main.go`.
|
|
49
|
+
- El contexto (`context.Context`) viaja como primer parámetro en lo que habla con fuera.
|
|
50
|
+
- '`arrancar.sh` compila y lanza el binario (`go run ./cmd/<binario> "$@"`, o el binario ya construido): es lo que lanzan el suelo y las pruebas e2e.'
|
|
51
|
+
- Las pruebas e2e (`tests/e2e/`, de caja negra, con pytest) arrancan el programa con el lanzador `app` (o `juego`) de la fijación e2e y lo usan por la terminal, como una persona. Las fijaciones necesitan `python3` con `pyte` y `pytest` (`python3 -m pip install -r arketum/fijaciones/e2e/requirements.txt`), que son de las pruebas, no del producto.
|
|
52
|
+
|
|
53
|
+
# Fijas del catálogo: se copian tal cual en `arketum/fijaciones/<nombre>/` del
|
|
54
|
+
# artefacto y no se editan (el cierre compara su checksum).
|
|
55
|
+
fijaciones:
|
|
56
|
+
- { de: stacks/go/suelo-juego-terminal, nombre: suelo, para: suelo, formas: [juego-terminal] }
|
|
57
|
+
- { de: stacks/go/suelo-aplicacion-por-capas, nombre: suelo, para: suelo, formas: [aplicacion-por-capas] }
|
|
58
|
+
- { de: stacks/go/e2e-terminal, nombre: e2e, para: e2e, formas: [juego-terminal, aplicacion-por-capas] }
|
|
59
|
+
|
|
60
|
+
# Los argumentos con los que su ejecutor de pruebas de la suite e2e (pytest, por la ayuda e2e de caja negra) no ejecuta ninguna: una
|
|
61
|
+
# ejecución de la suite e2e con uno de ellos no cuenta como e2e.
|
|
62
|
+
noEjecutan: [--co, --collect-only, --collectonly, --trace-config, --help, -h, --version, -V, --fixtures, --fixtures-per-test, --markers, --setup-plan, --setup-only]
|
|
63
|
+
|
|
64
|
+
comandos:
|
|
65
|
+
pruebas: go test ./...
|
|
66
|
+
suelo: python3 -B arketum/fijaciones/suelo/suelo.py .
|
|
67
|
+
e2e: PYTHONPATH=arketum/fijaciones/e2e python3 -B -m pytest -q -c arketum/fijaciones/e2e/pytest.ini --rootdir=. tests/e2e
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
selfReview:
|
|
71
|
+
criteria:
|
|
72
|
+
- check: La cobertura de pruebas del código propio es al menos del 80%
|
|
73
|
+
how: Corre `go test -cover ./...` y lee el total.
|
|
74
|
+
evidence: medida
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Peticiones HTTP para las pruebas de extremo a extremo (fijación del catálogo de Arketum: no se edita). Solo la biblioteca estándar."""
|
|
2
|
+
import json, urllib.error, urllib.parse, urllib.request
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _hacer(req, timeout=15):
|
|
6
|
+
try:
|
|
7
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
8
|
+
return r.status, r.read().decode('utf8', 'replace')
|
|
9
|
+
except urllib.error.HTTPError as e:
|
|
10
|
+
return e.code, e.read().decode('utf8', 'replace')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get(url, **params):
|
|
14
|
+
if params:
|
|
15
|
+
url += ('&' if '?' in url else '?') + urllib.parse.urlencode(params)
|
|
16
|
+
return _hacer(url)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_json(url, **params):
|
|
20
|
+
st, t = get(url, **params)
|
|
21
|
+
try:
|
|
22
|
+
return st, json.loads(t)
|
|
23
|
+
except ValueError:
|
|
24
|
+
return st, t
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def post_json(url, datos, metodo='POST'):
|
|
28
|
+
st, t = _hacer(urllib.request.Request(url, data=json.dumps(datos).encode(), method=metodo, headers={'Content-Type': 'application/json'}))
|
|
29
|
+
try:
|
|
30
|
+
return st, json.loads(t)
|
|
31
|
+
except ValueError:
|
|
32
|
+
return st, t
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def post_form(url, datos):
|
|
36
|
+
return _hacer(urllib.request.Request(url, data=urllib.parse.urlencode(datos).encode(), headers={'Content-Type': 'application/x-www-form-urlencoded'}))
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Fijación del catálogo de Arketum: no se edita. La suite e2e se corre desde la
|
|
2
|
+
# raíz del artefacto con:
|
|
3
|
+
# PYTHONPATH=arketum/fijaciones/e2e python3 -m pytest -q -c arketum/fijaciones/e2e/pytest.ini --rootdir=. tests/e2e
|
|
4
|
+
[pytest]
|
|
5
|
+
addopts = -p terminal_e2e -p no:cacheprovider --confcutdir=.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AYUDA E2E (fijación del catálogo de Arketum: se copia tal cual en
|
|
3
|
+
`arketum/fijaciones/e2e/` y no se edita; el cierre compara su checksum). Es un
|
|
4
|
+
complemento de pytest que carga `pytest.ini` (`-p terminal_e2e`), con la carpeta en `PYTHONPATH`:
|
|
5
|
+
|
|
6
|
+
PYTHONPATH=arketum/fijaciones/e2e python3 -m pytest -q -c arketum/fijaciones/e2e/pytest.ini --rootdir=. tests/e2e
|
|
7
|
+
|
|
8
|
+
Arranca el artefacto por el camino real, con su contrato de arranque
|
|
9
|
+
(`sh arrancar.sh` desde su raíz), con datos nuevos: `HOME` y `DATOS_DIR` en una
|
|
10
|
+
carpeta temporal propia y `PUERTO` libre. Las pruebas de `tests/e2e/` usan:
|
|
11
|
+
|
|
12
|
+
- `app` (o `juego`): un lanzador.
|
|
13
|
+
- `app()` (o `app(columnas, filas)`): la aplicación en una terminal emulada
|
|
14
|
+
(`Terminal` de `terminal_prueba`: `tecla`, `esperar`, `pantalla`, `vivo`…).
|
|
15
|
+
- `app.reabrir()`: cierra lo abierto y vuelve a arrancar con los mismos datos.
|
|
16
|
+
- `app.servidor()`: la arranca sin terminal (para una aplicación que sirve
|
|
17
|
+
HTTP en `PUERTO`) y espera a que responda; devuelve su URL base.
|
|
18
|
+
- `app.url`, `app.datos`: la URL base (`http://127.0.0.1:<PUERTO>`) y la
|
|
19
|
+
carpeta de datos.
|
|
20
|
+
- `e2e_http` (módulo): `get`, `get_json`, `post_json`, `post_form`.
|
|
21
|
+
|
|
22
|
+
Una prueba e2e recorre lo que haría una persona por la interfaz real: nunca
|
|
23
|
+
importa el código del artefacto ni llama a sus funciones por dentro.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import os, signal, socket, subprocess, sys, tempfile, time, urllib.error, urllib.request
|
|
27
|
+
|
|
28
|
+
import pytest
|
|
29
|
+
|
|
30
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
31
|
+
|
|
32
|
+
ARRANQUE = ['sh', 'arrancar.sh']
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _libre():
|
|
36
|
+
with socket.socket() as s:
|
|
37
|
+
s.bind(('127.0.0.1', 0))
|
|
38
|
+
return s.getsockname()[1]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _responde(url):
|
|
42
|
+
try:
|
|
43
|
+
urllib.request.urlopen(url, timeout=2)
|
|
44
|
+
return True
|
|
45
|
+
except urllib.error.HTTPError:
|
|
46
|
+
return True
|
|
47
|
+
except Exception: # noqa: BLE001
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Lanzador:
|
|
52
|
+
def __init__(self, raiz):
|
|
53
|
+
self.raiz = raiz
|
|
54
|
+
self.casa = tempfile.mkdtemp(prefix='e2e-')
|
|
55
|
+
self.datos = os.path.join(self.casa, 'datos')
|
|
56
|
+
self.puerto = _libre()
|
|
57
|
+
self.url = f'http://127.0.0.1:{self.puerto}'
|
|
58
|
+
self.abiertas = []
|
|
59
|
+
self.procesos = []
|
|
60
|
+
|
|
61
|
+
def entorno(self):
|
|
62
|
+
return {'HOME': self.casa, 'DATOS_DIR': self.datos, 'PUERTO': str(self.puerto), 'PYTHONUNBUFFERED': '1'}
|
|
63
|
+
|
|
64
|
+
def __call__(self, columnas=80, filas=24):
|
|
65
|
+
from terminal_prueba import Terminal # necesita pyte; solo si se usa la terminal
|
|
66
|
+
|
|
67
|
+
t = Terminal(ARRANQUE, cwd=self.raiz, columnas=columnas, filas=filas, env=self.entorno())
|
|
68
|
+
self.abiertas.append(t)
|
|
69
|
+
return t
|
|
70
|
+
|
|
71
|
+
def reabrir(self, columnas=80, filas=24):
|
|
72
|
+
self.cerrar()
|
|
73
|
+
return self(columnas, filas)
|
|
74
|
+
|
|
75
|
+
def servidor(self, ruta='/', segundos=25):
|
|
76
|
+
p = subprocess.Popen(ARRANQUE, cwd=self.raiz, env=dict(os.environ, **self.entorno()), stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
|
|
77
|
+
self.procesos.append(p)
|
|
78
|
+
fin = time.time() + segundos
|
|
79
|
+
while time.time() < fin:
|
|
80
|
+
if p.poll() is not None:
|
|
81
|
+
raise AssertionError(f'`sh arrancar.sh` terminó con código {p.returncode} antes de responder en {self.url}')
|
|
82
|
+
if _responde(self.url + ruta):
|
|
83
|
+
return self.url
|
|
84
|
+
time.sleep(0.2)
|
|
85
|
+
raise AssertionError(f'`sh arrancar.sh` no respondió en {self.url}{ruta} en {segundos} s')
|
|
86
|
+
|
|
87
|
+
def cerrar(self):
|
|
88
|
+
for t in self.abiertas:
|
|
89
|
+
t.cerrar()
|
|
90
|
+
self.abiertas = []
|
|
91
|
+
for p in self.procesos:
|
|
92
|
+
try:
|
|
93
|
+
os.killpg(p.pid, signal.SIGTERM)
|
|
94
|
+
p.wait(timeout=5)
|
|
95
|
+
except Exception: # noqa: BLE001
|
|
96
|
+
try:
|
|
97
|
+
os.killpg(p.pid, signal.SIGKILL)
|
|
98
|
+
except Exception: # noqa: BLE001
|
|
99
|
+
pass
|
|
100
|
+
self.procesos = []
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.fixture
|
|
104
|
+
def app(request):
|
|
105
|
+
l = Lanzador(str(request.config.rootpath))
|
|
106
|
+
yield l
|
|
107
|
+
l.cerrar()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.fixture
|
|
111
|
+
def juego(app):
|
|
112
|
+
return app
|