@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,207 @@
|
|
|
1
|
+
# Forma de sistema: dos o tres aplicaciones que se despliegan por separado, en
|
|
2
|
+
# un mismo repositorio, con frontera.
|
|
3
|
+
#
|
|
4
|
+
# Cambios:
|
|
5
|
+
# - 1.2.0: los papeles de la fase 2 (`implementador` y `revisor`) en las
|
|
6
|
+
# dos variantes. Sin salidas: el suelo y el e2e de varios despliegues
|
|
7
|
+
# levantan el sistema entero, y eso no cabe en las salidas de un artefacto.
|
|
8
|
+
# - 1.1.0: señales `actores`, `consumidores` y `volumen`; variantes (`simple` y
|
|
9
|
+
# `completa`, con los componentes `duena` y `consumidora`) y los escalones
|
|
10
|
+
# `varios-actores`, `consumido-por-sistemas`, `mucho-volumen` e
|
|
11
|
+
# `integraciones`. Un artefacto con capas declara `stack` y `componente`.
|
|
12
|
+
name: monorepo-de-aplicaciones
|
|
13
|
+
version: 1.2.0
|
|
14
|
+
description: >-
|
|
15
|
+
Dos o tres aplicaciones que se despliegan por separado en un mismo repositorio, con frontera: la que mantiene el dato principal es su dueña y las demás lo leen por su API.
|
|
16
|
+
orden: 3
|
|
17
|
+
senales:
|
|
18
|
+
despliegues: numero
|
|
19
|
+
integraciones: numero
|
|
20
|
+
actores: numero
|
|
21
|
+
consumidores: texto
|
|
22
|
+
volumen: numero
|
|
23
|
+
aplica:
|
|
24
|
+
- [despliegues, '>=', 2]
|
|
25
|
+
- [despliegues, '<=', 3]
|
|
26
|
+
- [integraciones, '<=', 3]
|
|
27
|
+
diseno:
|
|
28
|
+
- Son dos aplicaciones que se despliegan por separado; la que mantiene el dato principal es su dueña.
|
|
29
|
+
- La dueña del dato es la única que lo escribe; las demás (y cualquier otro sistema) solo lo leen, y solo a través de su API.
|
|
30
|
+
- Cada aplicación que no es dueña tiene que poder cambiarse o sustituirse sin tocar a la dueña.
|
|
31
|
+
- Lo que cada aplicación guarda por su cuenta es suyo y está separado del dato principal.
|
|
32
|
+
- El dato principal y lo que guarda cada aplicación sobreviven a reinicios; los datos de partida se cargan una vez desde lo que se usa hoy.
|
|
33
|
+
huecos:
|
|
34
|
+
- Cuáles son las aplicaciones (lo que usa cada tipo de persona o sistema) y si cada una tiene que poder cambiarse o sustituirse sin tocar la otra.
|
|
35
|
+
- De quién es cada dato (quién lo escribe y lo corrige) y quién solo lo lee.
|
|
36
|
+
- Qué otros sistemas usan alguna de las aplicaciones, qué pueden hacer con ellas y qué no.
|
|
37
|
+
- Qué guarda cada aplicación por su cuenta, además del dato principal.
|
|
38
|
+
- De dónde salen los datos de hoy, cuántos son, cómo se pasan al sistema y qué tiene que sobrevivir a un reinicio.
|
|
39
|
+
- Cuándo tiene que verse en una aplicación un cambio hecho en la otra.
|
|
40
|
+
|
|
41
|
+
variantes:
|
|
42
|
+
- nombre: simple
|
|
43
|
+
aplica:
|
|
44
|
+
- [despliegues, '<=', 2]
|
|
45
|
+
- [actores, '<=', 1]
|
|
46
|
+
papeles: &papeles
|
|
47
|
+
- nombre: implementador
|
|
48
|
+
para_que: construye el sistema en el paso de sistema (el código, sus pruebas y las salidas que exige el paso de evidencia)
|
|
49
|
+
texto: >-
|
|
50
|
+
PAPEL: IMPLEMENTADOR (fase 2). Construyes lo acordado, nada más: los requisitos que trae la unidad y los técnicos vigentes, con el oficio del arquetipo.
|
|
51
|
+
Desciendes cada artefacto tocado nivel a nivel y el código se queda como lo pide su stack; no lo deformas para encajar.
|
|
52
|
+
Cada funcional que construyes lleva su prueba de extremo a extremo, que arranca el artefacto por su contrato de arranque y lo usa como una persona:
|
|
53
|
+
una prueba que llama por dentro a una función que el camino real no usa no demuestra nada.
|
|
54
|
+
Copias las fijaciones del catálogo tal cual y no las editas. Corres tú las pruebas, el suelo y la suite e2e entera, y vinculas cada ejecución
|
|
55
|
+
a sus requisitos con `satisfies`. No rebajas ni borras una prueba para que pase. Si algo que funcionaba se rompe, lo arreglas;
|
|
56
|
+
si no puedes, lo reportas con `solicitar` (`pide: defecto`) y decide la persona. Nunca lo escondes.
|
|
57
|
+
- nombre: revisor
|
|
58
|
+
para_que: revisa en el paso de evidencia que lo construido funciona por el camino real, antes de entregar la evidencia
|
|
59
|
+
texto: >-
|
|
60
|
+
PAPEL: REVISOR (fase 2). No escribes el código: lo revisas como lo usaría la persona. Compruebas que cada funcional construido tiene su prueba e2e,
|
|
61
|
+
que arranca el artefacto por su contrato de arranque y no lo llama por dentro, y que pasa; que las fijaciones del catálogo no se tocaron;
|
|
62
|
+
y miras las pruebas modificadas o borradas desde la foto (las lista el resumen del paso): si alguna se rebajó para que pase, lo dices.
|
|
63
|
+
Corres el suelo y la suite e2e entera con el comando compuesto exacto y entregas esa evidencia. Lo que falla no lo das por bueno:
|
|
64
|
+
se arregla, o se reporta como defecto con `solicitar` y decide la persona. Si un funcional no admite una prueba automática
|
|
65
|
+
(un correo, algo visual, una integración externa), lo pides con `solicitar` (`pide: sin_e2e`), con el motivo.
|
|
66
|
+
componentes:
|
|
67
|
+
- nombre: duena
|
|
68
|
+
description: >-
|
|
69
|
+
La aplicación dueña del dato principal, en tres peldaños: lo que se pide, sus piezas y el código. Es la única que lo escribe, y lo ofrece por su API.
|
|
70
|
+
kinds:
|
|
71
|
+
- { name: objetivo, describes: "pieza de la solicitud (L0), sin ubicación en el árbol", ubicado: false }
|
|
72
|
+
- { name: modulo, describes: la definición del paquete o del módulo, ubicado: true }
|
|
73
|
+
- { name: dominio, describes: "las entidades del dato principal y sus reglas, puras", ubicado: true }
|
|
74
|
+
- { name: almacen, describes: guardar y cargar el dato principal, ubicado: true }
|
|
75
|
+
- { name: servicio, describes: "un caso de uso por acción, sobre el dominio y el almacén, sin HTTP", ubicado: true }
|
|
76
|
+
- { name: api, describes: "la API con que las demás leen el dato; solo traduce peticiones a servicios", ubicado: true }
|
|
77
|
+
- { name: arranque, describes: el punto de entrada de la aplicación, ubicado: true }
|
|
78
|
+
- { name: prueba, describes: fichero de prueba, ubicado: true }
|
|
79
|
+
coRequirements:
|
|
80
|
+
- { when: servicio, requires: [prueba] }
|
|
81
|
+
levels: &niveles-simples
|
|
82
|
+
- id: L0
|
|
83
|
+
requiredRoles: []
|
|
84
|
+
title: SOLICITUD
|
|
85
|
+
instruction: Declara qué tiene que hacer esta aplicación, para quién, y qué datos son suyos.
|
|
86
|
+
- id: L1
|
|
87
|
+
requiredRoles: []
|
|
88
|
+
title: PIEZAS
|
|
89
|
+
instruction: Propón sus piezas, con la frontera con la otra aplicación a la vista.
|
|
90
|
+
fanout: { min: 3, max: 16 }
|
|
91
|
+
- id: L2
|
|
92
|
+
requiredRoles: []
|
|
93
|
+
title: IMPLEMENTACIÓN
|
|
94
|
+
instruction: Escribe el código; lo que habla con la otra aplicación lo hace solo por su API.
|
|
95
|
+
fanout: { min: 1, max: 60 }
|
|
96
|
+
- nombre: consumidora
|
|
97
|
+
description: >-
|
|
98
|
+
Una aplicación que lee el dato principal por la API de la dueña, en tres peldaños. Se puede cambiar o sustituir sin tocar a la dueña.
|
|
99
|
+
kinds:
|
|
100
|
+
- { name: objetivo, describes: "pieza de la solicitud (L0), sin ubicación en el árbol", ubicado: false }
|
|
101
|
+
- { name: modulo, describes: la definición del paquete o del módulo, ubicado: true }
|
|
102
|
+
- { name: cliente, describes: el único sitio que llama a la API de la dueña, ubicado: true }
|
|
103
|
+
- { name: dominio, describes: "lo que la aplicación hace con lo que lee, puro", ubicado: true }
|
|
104
|
+
- { name: almacen, describes: "lo que guarda por su cuenta, separado del dato principal", ubicado: true }
|
|
105
|
+
- { name: interfaz, describes: lo que ve quien la usa, ubicado: true }
|
|
106
|
+
- { name: arranque, describes: el punto de entrada de la aplicación, ubicado: true }
|
|
107
|
+
- { name: prueba, describes: fichero de prueba, ubicado: true }
|
|
108
|
+
coRequirements:
|
|
109
|
+
- { when: cliente, requires: [prueba] }
|
|
110
|
+
levels: *niveles-simples
|
|
111
|
+
- nombre: completa
|
|
112
|
+
aplica: []
|
|
113
|
+
papeles: *papeles
|
|
114
|
+
componentes:
|
|
115
|
+
- nombre: duena
|
|
116
|
+
description: >-
|
|
117
|
+
La aplicación dueña del dato principal, en cinco peldaños: lo que se pide, sus piezas, sus contratos, el cableado y el código.
|
|
118
|
+
kinds:
|
|
119
|
+
- { name: objetivo, describes: "pieza de la solicitud (L0), sin ubicación en el árbol", ubicado: false }
|
|
120
|
+
- { name: modulo, describes: la definición del paquete o del módulo, ubicado: true }
|
|
121
|
+
- { name: dominio, describes: "las entidades del dato principal y sus reglas, puras", ubicado: true }
|
|
122
|
+
- { name: almacen, describes: guardar y cargar el dato principal, ubicado: true }
|
|
123
|
+
- { name: servicio, describes: "un caso de uso por acción, sobre el dominio y el almacén, sin HTTP", ubicado: true }
|
|
124
|
+
- { name: api, describes: "la API con que las demás leen el dato; solo traduce peticiones a servicios", ubicado: true }
|
|
125
|
+
- { name: arranque, describes: el punto de entrada de la aplicación, ubicado: true }
|
|
126
|
+
- { name: prueba, describes: fichero de prueba, ubicado: true }
|
|
127
|
+
- { name: dependencia, describes: "arista del cableado (L3) entre dos piezas declaradas", ubicado: false }
|
|
128
|
+
coRequirements:
|
|
129
|
+
- { when: servicio, requires: [prueba] }
|
|
130
|
+
- { when: almacen, requires: [prueba] }
|
|
131
|
+
levels: &niveles-completos
|
|
132
|
+
- id: L0
|
|
133
|
+
requiredRoles: []
|
|
134
|
+
title: SOLICITUD
|
|
135
|
+
instruction: Declara qué tiene que hacer esta aplicación, para quién, y qué datos son suyos.
|
|
136
|
+
- id: L1
|
|
137
|
+
requiredRoles: []
|
|
138
|
+
title: PIEZAS
|
|
139
|
+
instruction: Propón sus piezas, con la frontera con la otra aplicación a la vista.
|
|
140
|
+
fanout: { min: 3, max: 20 }
|
|
141
|
+
- id: L2
|
|
142
|
+
requiredRoles: []
|
|
143
|
+
title: CONTRATOS
|
|
144
|
+
instruction: Declara cada operación de la API (o cada llamada a ella) con lo que recibe, lo que devuelve y sus errores.
|
|
145
|
+
fanout: { min: 1, max: 40 }
|
|
146
|
+
- id: L3
|
|
147
|
+
requiredRoles: []
|
|
148
|
+
title: CABLEADO
|
|
149
|
+
instruction: Declara qué depende de qué; nada de una aplicación importa la otra.
|
|
150
|
+
fanout: { min: 1, max: 50 }
|
|
151
|
+
- id: L4
|
|
152
|
+
requiredRoles: []
|
|
153
|
+
title: IMPLEMENTACIÓN
|
|
154
|
+
instruction: Escribe el código; lo que habla con la otra aplicación lo hace solo por su API.
|
|
155
|
+
fanout: { min: 1, max: 90 }
|
|
156
|
+
- nombre: consumidora
|
|
157
|
+
description: >-
|
|
158
|
+
Una aplicación que lee el dato principal por la API de la dueña, en cinco peldaños. Se puede cambiar o sustituir sin tocar a la dueña.
|
|
159
|
+
kinds:
|
|
160
|
+
- { name: objetivo, describes: "pieza de la solicitud (L0), sin ubicación en el árbol", ubicado: false }
|
|
161
|
+
- { name: modulo, describes: la definición del paquete o del módulo, ubicado: true }
|
|
162
|
+
- { name: cliente, describes: el único sitio que llama a la API de la dueña, ubicado: true }
|
|
163
|
+
- { name: dominio, describes: "lo que la aplicación hace con lo que lee, puro", ubicado: true }
|
|
164
|
+
- { name: almacen, describes: "lo que guarda por su cuenta, separado del dato principal", ubicado: true }
|
|
165
|
+
- { name: interfaz, describes: lo que ve quien la usa, ubicado: true }
|
|
166
|
+
- { name: arranque, describes: el punto de entrada de la aplicación, ubicado: true }
|
|
167
|
+
- { name: prueba, describes: fichero de prueba, ubicado: true }
|
|
168
|
+
- { name: dependencia, describes: "arista del cableado (L3) entre dos piezas declaradas", ubicado: false }
|
|
169
|
+
coRequirements:
|
|
170
|
+
- { when: cliente, requires: [prueba] }
|
|
171
|
+
levels: *niveles-completos
|
|
172
|
+
|
|
173
|
+
escalones:
|
|
174
|
+
- nombre: varios-actores
|
|
175
|
+
cuando:
|
|
176
|
+
- [actores, '>=', 2]
|
|
177
|
+
diseno:
|
|
178
|
+
- Cada actor tiene su vía; mantener el dato se hace del lado de la aplicación dueña, y la que usa el público nunca escribe en él.
|
|
179
|
+
huecos:
|
|
180
|
+
- Quién mantiene el dato principal y desde dónde.
|
|
181
|
+
oficio:
|
|
182
|
+
- Mantener el dato principal se hace del lado de la dueña; la consumidora nunca escribe en él.
|
|
183
|
+
- nombre: consumido-por-sistemas
|
|
184
|
+
cuando:
|
|
185
|
+
- [consumidores, 'en', [sistemas, ambos]]
|
|
186
|
+
diseno:
|
|
187
|
+
- La API de la aplicación dueña es un contrato estable para otros sistemas.
|
|
188
|
+
huecos:
|
|
189
|
+
- Qué otros sistemas usan la API, qué pueden hacer con ella y cómo se enteran de un cambio.
|
|
190
|
+
oficio:
|
|
191
|
+
- La API es un contrato - respuestas estables, errores con código y mensaje, y cada ruta descrita con un ejemplo.
|
|
192
|
+
coRequirements:
|
|
193
|
+
- { when: api, requires: [prueba], componentes: [duena] }
|
|
194
|
+
- nombre: mucho-volumen
|
|
195
|
+
cuando:
|
|
196
|
+
- [volumen, '>=', 1000]
|
|
197
|
+
oficio:
|
|
198
|
+
- Con miles de registros nada recorre ni descarga el dato entero en cada petición; los listados se paginan.
|
|
199
|
+
- nombre: integraciones
|
|
200
|
+
cuando:
|
|
201
|
+
- [integraciones, '>=', 1]
|
|
202
|
+
diseno:
|
|
203
|
+
- Cada sistema ajeno se usa a través de un solo sitio, que se puede sustituir por un doble para probar.
|
|
204
|
+
oficio:
|
|
205
|
+
- Cada llamada a otro sistema tiene un doble para probar sin el sistema real, que cubre el fallo y la demora.
|
|
206
|
+
coRequirements:
|
|
207
|
+
- { when: servicio, requires: [prueba] }
|
|
@@ -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
|
|
@@ -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)
|