@rlabs-cl/arketum 0.0.0-stage → 1.0.1
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 +140 -0
- package/LICENSE +42 -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,167 @@
|
|
|
1
|
+
# Stack: Python, con la biblioteca estándar y pytest.
|
|
2
|
+
#
|
|
3
|
+
# Un stack no es un arquetipo: no tiene niveles ni tipos propios. Dice dónde vive
|
|
4
|
+
# cada tipo de pieza que declaran los componentes de las formas, las reglas del
|
|
5
|
+
# oficio para quien construye y sus comandos (informativos). Arketum lo compone
|
|
6
|
+
# con el componente de la variante de la forma del proyecto en un arquetipo
|
|
7
|
+
# efectivo.
|
|
8
|
+
#
|
|
9
|
+
# Cambios:
|
|
10
|
+
# - 1.1.0: un solo paquete con una subcarpeta por tipo y varios ficheros en
|
|
11
|
+
# cada una (`<paquete>/dominio/<nombre>.py`), como en Go; `pyproject.toml` en
|
|
12
|
+
# la raíz del artefacto; las plantillas y los estáticos de la interfaz;
|
|
13
|
+
# `conftest.py`, los ayudantes y los grupos anidados de `tests/`; los módulos
|
|
14
|
+
# sueltos del paquete, `py.typed` y `README.md`; las carpetas generadas.
|
|
15
|
+
# - 1.2.0: las fijaciones (los suelos de `juego-terminal` y de
|
|
16
|
+
# `aplicacion-por-capas` y la ayuda e2e por terminal), sus comandos (`suelo`
|
|
17
|
+
# y `e2e`, que las invocan por ruta) y `arrancar.sh` como arranque, el
|
|
18
|
+
# contrato de arranque de las formas.
|
|
19
|
+
# - 1.3.0: la ubicación `configura-pruebas` (`conftest.py`, `pytest.ini`,
|
|
20
|
+
# `setup.cfg`, `tox.ini`, `pyproject.toml`): lo que el cierre enseña cuando
|
|
21
|
+
# cambia, porque puede saltarse pruebas sin tocarlas. Antes lo sabía Arketum
|
|
22
|
+
# por el nombre; ahora lo dice el stack. TRANSICIÓN: la huella del stack y la
|
|
23
|
+
# del efectivo cambian; los descensos certificados con la anterior dan
|
|
24
|
+
# `stale_policy` hasta recertificarlos. Desde el catálogo oficial, con la
|
|
25
|
+
# versión nueva, la política fijada de una unidad viva no lo cuenta como cambio
|
|
26
|
+
# de política (lo vigila `stale_policy`); desde un catálogo de otra confianza,
|
|
27
|
+
# sí sale como cambio de la huella del stack. Un stack propio, o una copia de
|
|
28
|
+
# la 1.2.0, sin `configura-pruebas` deja de enseñar al cerrar los cambios a lo
|
|
29
|
+
# que configura las pruebas: `status` de una unidad lo avisa una vez.
|
|
30
|
+
# Y `noEjecutan`: los argumentos con que pytest no ejecuta ninguna prueba,
|
|
31
|
+
# que antes sabía Arketum.
|
|
32
|
+
name: python
|
|
33
|
+
version: 1.3.0
|
|
34
|
+
description: >-
|
|
35
|
+
Python con la biblioteca estándar y pytest: un paquete por artefacto, con una subcarpeta por tipo de pieza (dominio, almacén, servicios, interfaz, API, clientes) y varios ficheros en cada una, `pyproject.toml` en la raíz del artefacto y las pruebas en `tests/<grupo>/`, unitarias y de extremo a extremo.
|
|
36
|
+
|
|
37
|
+
# Rutas relativas a la raíz del artefacto. `<paquete>` casa con cualquier nombre
|
|
38
|
+
# de carpeta: se usa como UN paquete, no una carpeta por tipo (Arketum no comprueba
|
|
39
|
+
# que sea el mismo en todo el artefacto). Un tipo que vive en varios sitios lleva una
|
|
40
|
+
# lista en `pattern`. Las exclusiones evitan que un grupo de pruebas que se llama
|
|
41
|
+
# como un tipo (`tests/dominio/`) lo reclame ese tipo.
|
|
42
|
+
ubicaciones:
|
|
43
|
+
# Los módulos sueltos del paquete (`config.py`, `errores.py`), su `__init__.py`
|
|
44
|
+
# y `py.typed`, el manifiesto y el README del artefacto. Un fichero suelto con
|
|
45
|
+
# el nombre de un tipo (`dominio.py`) no es un módulo: es código fusionado, y su
|
|
46
|
+
# sitio es la subcarpeta del tipo.
|
|
47
|
+
modulo:
|
|
48
|
+
pattern: ["<paquete>/<nombre>.py", "<paquete>/py.typed", "pyproject.toml", "README.md"]
|
|
49
|
+
excluding:
|
|
50
|
+
- "<paquete>/__main__.py"
|
|
51
|
+
- "<paquete>/dominio.py"
|
|
52
|
+
- "<paquete>/almacen.py"
|
|
53
|
+
- "<paquete>/servicios.py"
|
|
54
|
+
- "<paquete>/interfaz.py"
|
|
55
|
+
- "<paquete>/api.py"
|
|
56
|
+
- "<paquete>/clientes.py"
|
|
57
|
+
- "tests/<nombre>.py"
|
|
58
|
+
- "tests/py.typed"
|
|
59
|
+
dominio: { pattern: "<paquete>/dominio/<nombre>.py", excluding: ["tests/dominio/<nombre>.py"] }
|
|
60
|
+
almacen: { pattern: "<paquete>/almacen/<nombre>.py", excluding: ["tests/almacen/<nombre>.py"] }
|
|
61
|
+
servicio: { pattern: "<paquete>/servicios/<nombre>.py", excluding: ["tests/servicios/<nombre>.py"] }
|
|
62
|
+
interfaz:
|
|
63
|
+
pattern:
|
|
64
|
+
- "<paquete>/interfaz/<nombre>.py"
|
|
65
|
+
- "<paquete>/interfaz/plantillas/<nombre>.<ext>"
|
|
66
|
+
- "<paquete>/interfaz/plantillas/<carpeta>/<nombre>.<ext>"
|
|
67
|
+
- "<paquete>/interfaz/estaticos/<nombre>.<ext>"
|
|
68
|
+
- "<paquete>/interfaz/estaticos/<carpeta>/<nombre>.<ext>"
|
|
69
|
+
excluding:
|
|
70
|
+
- "tests/interfaz/<nombre>.py"
|
|
71
|
+
- "tests/interfaz/plantillas/<nombre>.<ext>"
|
|
72
|
+
- "tests/interfaz/plantillas/<carpeta>/<nombre>.<ext>"
|
|
73
|
+
- "tests/interfaz/estaticos/<nombre>.<ext>"
|
|
74
|
+
- "tests/interfaz/estaticos/<carpeta>/<nombre>.<ext>"
|
|
75
|
+
api: { pattern: "<paquete>/api/<nombre>.py", excluding: ["tests/api/<nombre>.py"] }
|
|
76
|
+
cliente: { pattern: "<paquete>/clientes/<nombre>.py", excluding: ["tests/clientes/<nombre>.py"] }
|
|
77
|
+
# `arrancar.sh` en la raíz del artefacto: el contrato de arranque de las formas (lo lanzan el suelo y la ayuda e2e).
|
|
78
|
+
arranque: { pattern: ["<paquete>/__main__.py", "arrancar.sh"], excluding: ["tests/__main__.py"] }
|
|
79
|
+
# Lo de `tests/`, hasta tres carpetas por debajo: pruebas, `conftest.py`,
|
|
80
|
+
# ayudantes (`helpers.py`, `fixtures.py`), `__init__.py`, grupos anidados y datos
|
|
81
|
+
# de prueba (`tests/datos/pedidos.json`). Un patrón por profundidad: un
|
|
82
|
+
# contenedor (`tests/`) clasificaría también las carpetas.
|
|
83
|
+
prueba:
|
|
84
|
+
pattern: ["tests/<nombre>.<ext>", "tests/<grupo>/<nombre>.<ext>", "tests/<grupo>/<carpeta>/<nombre>.<ext>", "tests/<grupo>/<carpeta>/<sub>/<nombre>.<ext>"]
|
|
85
|
+
# Lo que configura cómo se ejecutan las pruebas: no es un tipo de pieza (un
|
|
86
|
+
# `conftest.py` es también una prueba y `pyproject.toml` un módulo). El cierre
|
|
87
|
+
# lo enseña a la persona cuando cambia: puede saltarse pruebas sin tocarlas
|
|
88
|
+
# (de `pyproject.toml` y `setup.cfg` no se sabe si cambió su sección de pytest:
|
|
89
|
+
# salen siempre que cambian).
|
|
90
|
+
configura-pruebas:
|
|
91
|
+
pattern:
|
|
92
|
+
- pyproject.toml
|
|
93
|
+
- setup.cfg
|
|
94
|
+
- tox.ini
|
|
95
|
+
- pytest.ini
|
|
96
|
+
- conftest.py
|
|
97
|
+
- tests/conftest.py
|
|
98
|
+
- tests/<grupo>/conftest.py
|
|
99
|
+
- tests/<grupo>/<carpeta>/conftest.py
|
|
100
|
+
- tests/<grupo>/<carpeta>/<sub>/conftest.py
|
|
101
|
+
|
|
102
|
+
# Cada muestra tiene que derivar exactamente su tipo: es la comprobación de solape.
|
|
103
|
+
muestras:
|
|
104
|
+
modulo: [tienda/__init__.py, tienda/config.py, tienda/errores.py, tienda/py.typed, pyproject.toml, README.md]
|
|
105
|
+
dominio: [tienda/dominio/__init__.py, tienda/dominio/producto.py, tienda/dominio/pedido.py]
|
|
106
|
+
almacen: [tienda/almacen/__init__.py, tienda/almacen/repositorio.py]
|
|
107
|
+
servicio: [tienda/servicios/__init__.py, tienda/servicios/carrito.py, tienda/servicios/admin.py]
|
|
108
|
+
interfaz: [tienda/interfaz/__init__.py, tienda/interfaz/web.py, tienda/interfaz/plantillas/carrito.html, tienda/interfaz/plantillas/parciales/cabecera.html, tienda/interfaz/estaticos/estilo.css, tienda/interfaz/estaticos/css/estilo.css]
|
|
109
|
+
api: [tienda/api/__init__.py, tienda/api/rutas.py]
|
|
110
|
+
cliente: [tienda/clientes/__init__.py, tienda/clientes/pagos.py]
|
|
111
|
+
arranque: [tienda/__main__.py, arrancar.sh]
|
|
112
|
+
prueba:
|
|
113
|
+
- tests/conftest.py
|
|
114
|
+
- tests/__init__.py
|
|
115
|
+
- tests/helpers.py
|
|
116
|
+
- tests/test_humo.py
|
|
117
|
+
- tests/unidad/__init__.py
|
|
118
|
+
- tests/unidad/fixtures.py
|
|
119
|
+
- tests/unidad/test_carrito.py
|
|
120
|
+
- tests/unidad/dominio/test_pedido.py
|
|
121
|
+
- tests/dominio/test_pedido.py
|
|
122
|
+
- tests/interfaz/test_web.py
|
|
123
|
+
- tests/interfaz/plantillas/a.html
|
|
124
|
+
- tests/interfaz/estaticos/b.css
|
|
125
|
+
- tests/interfaz/estaticos/css/a.css
|
|
126
|
+
- tests/interfaz/plantillas/parciales/c.html
|
|
127
|
+
- tests/datos/pedidos.json
|
|
128
|
+
- tests/pedidos.json
|
|
129
|
+
- tests/e2e/conftest.py
|
|
130
|
+
- tests/e2e/test_compra.py
|
|
131
|
+
configura-pruebas: [pyproject.toml, pytest.ini, tox.ini, setup.cfg, tests/conftest.py, tests/e2e/conftest.py]
|
|
132
|
+
|
|
133
|
+
# Lo que generan Python y pytest: no se reclama ni cuenta como cambio.
|
|
134
|
+
generados: [__pycache__/, .pytest_cache/]
|
|
135
|
+
|
|
136
|
+
oficio:
|
|
137
|
+
- Solo la biblioteca estándar; las pruebas con pytest. Una dependencia nueva se declara en `pyproject.toml` y se justifica.
|
|
138
|
+
- Un fichero por concepto dentro de la subcarpeta de su tipo (`dominio/pedido.py`, `servicios/carrito.py`); no se junta todo en uno.
|
|
139
|
+
- El dominio no lee ficheros, ni la terminal, ni el reloj ni la red; el instante entra como parámetro.
|
|
140
|
+
- Un fallo al leer lo guardado no se traga - se avisa y se para; nunca se sigue con vacío ni se sobrescribe.
|
|
141
|
+
- Las rutas a ficheros del propio código se resuelven desde `__file__`, nunca desde el directorio de trabajo.
|
|
142
|
+
- La carpeta de datos sale del entorno (`DATOS_DIR`), y las pruebas la ponen en una carpeta temporal.
|
|
143
|
+
- '`arrancar.sh` lanza el paquete con el intérprete del entorno (`exec python3 -m <paquete> "$@"`): es lo que lanzan el suelo y las pruebas e2e.'
|
|
144
|
+
- Las pruebas e2e (`tests/e2e/`) arrancan la aplicación con el lanzador `app` de la fijación e2e y la usan por la interfaz real, como una persona; nunca importan el paquete. Las fijaciones necesitan `pyte` y `pytest` (`python3 -m pip install -r arketum/fijaciones/e2e/requirements.txt`), que son de las pruebas, no del producto.
|
|
145
|
+
|
|
146
|
+
# Fijas del catálogo: se copian tal cual en `arketum/fijaciones/<nombre>/` del
|
|
147
|
+
# artefacto y no se editan (el cierre compara su checksum). Una por forma y cosa.
|
|
148
|
+
fijaciones:
|
|
149
|
+
- { de: stacks/python/suelo-juego-terminal, nombre: suelo, para: suelo, formas: [juego-terminal] }
|
|
150
|
+
- { de: stacks/python/suelo-aplicacion-por-capas, nombre: suelo, para: suelo, formas: [aplicacion-por-capas] }
|
|
151
|
+
- { de: stacks/python/e2e-terminal, nombre: e2e, para: e2e, formas: [juego-terminal, aplicacion-por-capas] }
|
|
152
|
+
|
|
153
|
+
# Los argumentos con los que su ejecutor de pruebas (pytest) no ejecuta ninguna: una
|
|
154
|
+
# ejecución de la suite e2e con uno de ellos no cuenta como e2e.
|
|
155
|
+
noEjecutan: [--co, --collect-only, --collectonly, --trace-config, --help, -h, --version, -V, --fixtures, --fixtures-per-test, --markers, --setup-plan, --setup-only]
|
|
156
|
+
|
|
157
|
+
comandos:
|
|
158
|
+
pruebas: python3 -m pytest -q --ignore=tests/e2e
|
|
159
|
+
suelo: python3 -B arketum/fijaciones/suelo/suelo.py .
|
|
160
|
+
e2e: PYTHONPATH=arketum/fijaciones/e2e python3 -B -m pytest -q -c arketum/fijaciones/e2e/pytest.ini --rootdir=. tests/e2e
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
selfReview:
|
|
164
|
+
criteria:
|
|
165
|
+
- check: Ninguna prueba depende de la red, del reloj de la máquina ni de una carpeta fuera de la temporal
|
|
166
|
+
how: Lee las fijaciones y las llamadas a `datetime.now`, `time.time` y `urllib` desde las pruebas.
|
|
167
|
+
evidence: medida
|
|
@@ -0,0 +1,182 @@
|
|
|
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).
|
|
4
|
+
*
|
|
5
|
+
* Arranca el artefacto por el camino real, con su contrato de arranque
|
|
6
|
+
* (`sh arrancar.sh` desde su raíz), con datos nuevos: `HOME` y `DATOS_DIR` en
|
|
7
|
+
* una carpeta temporal propia y `PUERTO` libre. Las pruebas de `src/e2e/`:
|
|
8
|
+
*
|
|
9
|
+
* import { arrancar } from '../../arketum/fijaciones/e2e/app.mjs'
|
|
10
|
+
* const app = await arrancar() // sin esperar a nada
|
|
11
|
+
* await app.servidor('/') // espera a que responda en PUERTO; devuelve la URL
|
|
12
|
+
* const r = await fetch(`${app.url}/`) // HTTP con el fetch de Node
|
|
13
|
+
* app.escribir('q\n') // la entrada de la aplicación
|
|
14
|
+
* await app.esperarSalida('Hasta luego') // lo que escribe, hasta que aparezca
|
|
15
|
+
* await app.cerrar() // y `app.reabrir()`, con los mismos datos
|
|
16
|
+
*
|
|
17
|
+
* Para un programa de terminal, `abrirTerminal` (más abajo) lo arranca en una
|
|
18
|
+
* terminal emulada.
|
|
19
|
+
*
|
|
20
|
+
* Una prueba e2e recorre lo que haría una persona por la interfaz real: nunca
|
|
21
|
+
* importa el código del artefacto ni llama a sus funciones por dentro. `arrancar`
|
|
22
|
+
* solo usa Node; `abrirTerminal` necesita `python3` con `pyte`.
|
|
23
|
+
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { spawn } from 'node:child_process'
|
|
27
|
+
import { mkdtempSync } from 'node:fs'
|
|
28
|
+
import { createServer } from 'node:net'
|
|
29
|
+
import { tmpdir } from 'node:os'
|
|
30
|
+
import { dirname, join } from 'node:path'
|
|
31
|
+
import { fileURLToPath } from 'node:url'
|
|
32
|
+
|
|
33
|
+
const libre = () =>
|
|
34
|
+
new Promise((resolver, rechazar) => {
|
|
35
|
+
const s = createServer()
|
|
36
|
+
s.unref()
|
|
37
|
+
s.on('error', rechazar)
|
|
38
|
+
s.listen(0, '127.0.0.1', () => {
|
|
39
|
+
const { port } = s.address()
|
|
40
|
+
s.close(() => resolver(port))
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const esperar = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* El artefacto en una terminal emulada (para un programa de terminal, como un
|
|
48
|
+
* juego): por el puente de Python de esta fijación, que necesita `python3` con
|
|
49
|
+
* `pyte`. Mismo arranque y mismas variables que `arrancar`.
|
|
50
|
+
*
|
|
51
|
+
* const t = await abrirTerminal({ columnas: 80, filas: 24 })
|
|
52
|
+
* await t.esperar(1); await t.tecla('ARRIBA'); const texto = await t.pantalla()
|
|
53
|
+
* await t.tecla('q'); await t.esperar(1); expect(await t.vivo()).toBe(false)
|
|
54
|
+
* await t.cerrar()
|
|
55
|
+
*/
|
|
56
|
+
export async function abrirTerminal(opciones = {}) {
|
|
57
|
+
const casa = opciones.casa ?? mkdtempSync(join(tmpdir(), 'e2e-'))
|
|
58
|
+
const puerto = opciones.puerto ?? (await libre())
|
|
59
|
+
const raiz = opciones.raiz ?? process.cwd()
|
|
60
|
+
const puente = join(dirname(fileURLToPath(import.meta.url)), 'terminal_puente.py')
|
|
61
|
+
const hijo = spawn('python3', ['-B', puente, String(opciones.columnas ?? 80), String(opciones.filas ?? 24)], {
|
|
62
|
+
cwd: raiz,
|
|
63
|
+
env: { ...process.env, HOME: casa, DATOS_DIR: join(casa, 'datos'), PUERTO: String(puerto) },
|
|
64
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
65
|
+
})
|
|
66
|
+
let resto = ''
|
|
67
|
+
let errores = ''
|
|
68
|
+
const esperando = []
|
|
69
|
+
hijo.stderr.on('data', (d) => (errores += d))
|
|
70
|
+
hijo.stdout.on('data', (d) => {
|
|
71
|
+
resto += d
|
|
72
|
+
let i
|
|
73
|
+
while ((i = resto.indexOf('\n')) >= 0) {
|
|
74
|
+
const linea = resto.slice(0, i)
|
|
75
|
+
resto = resto.slice(i + 1)
|
|
76
|
+
const r = JSON.parse(linea)
|
|
77
|
+
const espera = esperando.shift()
|
|
78
|
+
// Una orden que el puente no pudo cumplir es un error de la prueba, no una respuesta.
|
|
79
|
+
if (espera !== undefined) r.ok === false ? espera.rechazar(new Error(`el puente de terminal no pudo: ${r.error ?? JSON.stringify(r)}`)) : espera.resolver(r)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
const fin = new Promise((r) => hijo.on('exit', r))
|
|
83
|
+
// Si el puente termina con órdenes sin contestar, son errores: la prueba no se queda esperando.
|
|
84
|
+
hijo.on('exit', () => {
|
|
85
|
+
for (const e of esperando.splice(0)) e.rechazar(new Error(`el puente de terminal terminó: ${errores}`))
|
|
86
|
+
})
|
|
87
|
+
const pedir = (orden) =>
|
|
88
|
+
new Promise((resolver, rechazar) => {
|
|
89
|
+
if (hijo.exitCode !== null) return rechazar(new Error(`el puente de terminal terminó: ${errores}`))
|
|
90
|
+
esperando.push({ resolver, rechazar })
|
|
91
|
+
hijo.stdin.write(`${JSON.stringify(orden)}\n`)
|
|
92
|
+
})
|
|
93
|
+
const estado = async () => pedir({ op: 'pantalla' })
|
|
94
|
+
return {
|
|
95
|
+
datos: join(casa, 'datos'),
|
|
96
|
+
tecla: async (k) => void (await pedir({ op: 'tecla', k })),
|
|
97
|
+
esperar: async (s) => void (await pedir({ op: 'esperar', s })),
|
|
98
|
+
pantalla: async () => (await estado()).pantalla,
|
|
99
|
+
vivo: async () => (await estado()).vivo,
|
|
100
|
+
codigo: async () => (await estado()).codigo,
|
|
101
|
+
async cerrar() {
|
|
102
|
+
if (hijo.exitCode === null) {
|
|
103
|
+
await pedir({ op: 'cerrar' }).catch(() => undefined)
|
|
104
|
+
await Promise.race([fin, esperar(3000)])
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function arrancar(opciones = {}) {
|
|
111
|
+
const casa = opciones.casa ?? mkdtempSync(join(tmpdir(), 'e2e-'))
|
|
112
|
+
const puerto = opciones.puerto ?? (await libre())
|
|
113
|
+
const datos = join(casa, 'datos')
|
|
114
|
+
const raiz = opciones.raiz ?? process.cwd()
|
|
115
|
+
const hijo = spawn('sh', ['arrancar.sh'], {
|
|
116
|
+
cwd: raiz,
|
|
117
|
+
env: { ...process.env, HOME: casa, DATOS_DIR: datos, PUERTO: String(puerto) },
|
|
118
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
119
|
+
detached: true,
|
|
120
|
+
})
|
|
121
|
+
let salida = ''
|
|
122
|
+
hijo.stdout.on('data', (d) => (salida += d))
|
|
123
|
+
hijo.stderr.on('data', (d) => (salida += d))
|
|
124
|
+
let codigo = null
|
|
125
|
+
const termino = new Promise((r) => hijo.on('exit', (c) => r((codigo = c))))
|
|
126
|
+
const app = {
|
|
127
|
+
url: `http://127.0.0.1:${puerto}`,
|
|
128
|
+
puerto,
|
|
129
|
+
datos,
|
|
130
|
+
get codigo() {
|
|
131
|
+
return codigo
|
|
132
|
+
},
|
|
133
|
+
salida: () => salida,
|
|
134
|
+
vivo: () => codigo === null,
|
|
135
|
+
escribir: (texto) => hijo.stdin.write(texto),
|
|
136
|
+
async esperarSalida(texto, ms = 10000) {
|
|
137
|
+
const fin = Date.now() + ms
|
|
138
|
+
while (Date.now() < fin) {
|
|
139
|
+
if (salida.includes(texto)) return salida
|
|
140
|
+
if (codigo !== null) break
|
|
141
|
+
await esperar(50)
|
|
142
|
+
}
|
|
143
|
+
throw new Error(`no apareció «${texto}» en la salida de \`sh arrancar.sh\`:\n${salida}`)
|
|
144
|
+
},
|
|
145
|
+
async servidor(ruta = '/', ms = 25000) {
|
|
146
|
+
const fin = Date.now() + ms
|
|
147
|
+
while (Date.now() < fin) {
|
|
148
|
+
if (codigo !== null) throw new Error(`\`sh arrancar.sh\` terminó con código ${codigo} antes de responder en ${app.url}:\n${salida}`)
|
|
149
|
+
try {
|
|
150
|
+
await fetch(`${app.url}${ruta}`)
|
|
151
|
+
return app.url
|
|
152
|
+
} catch {
|
|
153
|
+
await esperar(200)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
throw new Error(`\`sh arrancar.sh\` no respondió en ${app.url}${ruta}`)
|
|
157
|
+
},
|
|
158
|
+
async cerrar() {
|
|
159
|
+
if (codigo === null) {
|
|
160
|
+
try {
|
|
161
|
+
process.kill(-hijo.pid, 'SIGTERM')
|
|
162
|
+
} catch {}
|
|
163
|
+
await Promise.race([termino, esperar(5000)])
|
|
164
|
+
if (codigo === null) {
|
|
165
|
+
try {
|
|
166
|
+
process.kill(-hijo.pid, 'SIGKILL')
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return codigo
|
|
171
|
+
},
|
|
172
|
+
async esperarFin(ms = 5000) {
|
|
173
|
+
await Promise.race([termino, esperar(ms)])
|
|
174
|
+
return codigo
|
|
175
|
+
},
|
|
176
|
+
async reabrir() {
|
|
177
|
+
await app.cerrar()
|
|
178
|
+
return arrancar({ ...opciones, casa, puerto })
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
return app
|
|
182
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyte
|
|
@@ -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,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PUENTE DE TERMINAL (fijación del catálogo de Arketum: no se edita). Lo lanza
|
|
3
|
+
`app.mjs` (`abrirTerminal`): arranca el artefacto con `sh arrancar.sh` en una
|
|
4
|
+
terminal emulada (`terminal_prueba`, con pyte) y obedece órdenes en JSON, una
|
|
5
|
+
por línea, por su entrada; contesta una línea JSON por orden:
|
|
6
|
+
|
|
7
|
+
{"op": "tecla", "k": "ARRIBA"} → {"ok": true}
|
|
8
|
+
{"op": "esperar", "s": 0.5} → {"ok": true}
|
|
9
|
+
{"op": "pantalla"} → {"ok": true, "pantalla": "...", "vivo": true, "codigo": null}
|
|
10
|
+
{"op": "cerrar"} → {"ok": true, "codigo": ...}
|
|
11
|
+
|
|
12
|
+
Uso: python3 -B terminal_puente.py <columnas> <filas> (desde la raíz del artefacto)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json, os, sys
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
18
|
+
from terminal_prueba import Terminal # noqa: E402
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
columnas, filas = int(sys.argv[1]), int(sys.argv[2])
|
|
23
|
+
t = Terminal(['sh', 'arrancar.sh'], cwd=os.getcwd(), columnas=columnas, filas=filas)
|
|
24
|
+
try:
|
|
25
|
+
for linea in sys.stdin:
|
|
26
|
+
orden = json.loads(linea)
|
|
27
|
+
op = orden.get('op')
|
|
28
|
+
if op == 'tecla':
|
|
29
|
+
t.tecla(orden['k'])
|
|
30
|
+
r = {'ok': True}
|
|
31
|
+
elif op == 'esperar':
|
|
32
|
+
t.esperar(float(orden.get('s', 0.1)))
|
|
33
|
+
r = {'ok': True}
|
|
34
|
+
elif op == 'pantalla':
|
|
35
|
+
vivo = t.vivo()
|
|
36
|
+
r = {'ok': True, 'pantalla': t.pantalla(), 'vivo': vivo, 'codigo': t.codigo}
|
|
37
|
+
elif op == 'cerrar':
|
|
38
|
+
t.cerrar()
|
|
39
|
+
r = {'ok': True, 'codigo': t.codigo}
|
|
40
|
+
else:
|
|
41
|
+
r = {'ok': False, 'error': f'orden desconocida: {op}'}
|
|
42
|
+
sys.stdout.write(json.dumps(r) + '\n')
|
|
43
|
+
sys.stdout.flush()
|
|
44
|
+
if op == 'cerrar':
|
|
45
|
+
break
|
|
46
|
+
finally:
|
|
47
|
+
t.cerrar()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == '__main__':
|
|
51
|
+
main()
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
// npx vitest run --config arketum/fijaciones/e2e/vitest.config.mjs
|
|
4
|
+
// Solo las pruebas de `src/e2e/`, que arrancan el artefacto con `app.mjs`.
|
|
5
|
+
export default {
|
|
6
|
+
root: process.cwd(),
|
|
7
|
+
test: {
|
|
8
|
+
include: ['src/e2e/**/*.test.ts'],
|
|
9
|
+
testTimeout: 60000,
|
|
10
|
+
hookTimeout: 60000,
|
|
11
|
+
},
|
|
12
|
+
}
|
|
@@ -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)
|