tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1//! **El explorador entero corriendo en otro proceso, medido.**
2//!
3//! `el_lazo_cerrado` prueba el mecanismo con el huésped más chico posible.
4//! Éste lo prueba con **la app real**: `nahual-shell-llimphi` —veinte visores,
5//! dos sidebars, el listado del directorio— compuesto hacia el cable en vez de
6//! hacia una ventana.
7//!
8//! Es la pregunta que ninguna de las piezas contesta por separado: *treinta y
9//! dos crates cruzaron el backend, ¿el conjunto pinta algo?*
10//!
11//! **Sin ventana** (regla 8): lo que hay que saber son números —cuántas órdenes
12//! cruzaron, cuánto texto se shapeó de este lado, cuántas medidas preguntó— y
13//! los números se leen mejor que un pantallazo. Un tablado gris y uno con una
14//! escena vacía se ven idénticos.
15//!
16//! ```bash
17//! ./scripts/check-jaula-linux.sh
18//! ```
19
20use std::time::Duration;
21
22use llimphi_cable::vida::lanzar;
23use llimphi_cable::Anfitrion;
24use llimphi_compositor::llimphi_layout::taffy::prelude::{length, Rect as TRect, Size, Style};
25use llimphi_compositor::llimphi_layout::LayoutTree;
26use llimphi_compositor::{mount, paint, View};
27use llimphi_text::{Lienzo, Typesetter};
28use llimphi_widget_tablado::Tablado;
29
30const ANCHO: f32 = 1200.0;
31const ALTO: f32 = 800.0;
32/// El tablado va **corrido**, para que las coordenadas locales no coincidan con
33/// las absolutas por casualidad.
34const ORIGEN: (f32, f32) = (24.0, 16.0);
35const CAJA: (f32, f32) = (1100.0, 740.0);
36
37/// Dónde quedó el huésped. Vive en el `target` de SU workspace: está excluido
38/// del principal porque enciende `llimphi-text/wire`, excluyente con `vello`.
39fn binario_del_huesped() -> Option<std::path::PathBuf> {
40 let raiz = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(4)?.to_path_buf();
41 let p = raiz.join("02_ruway/llimphi/llimphi-jaula/target/debug/nahual-shell-jailed");
42 p.exists().then_some(p)
43}
44
45/// ¿Alguien exige que este test corra de verdad?
46///
47/// Lo pone `scripts/check-jaula-linux.sh`, que compila el huésped justo antes.
48/// Existe porque saltearse **informando `ok`** es la peor manera de fallar: ya
49/// pasó una vez acá mismo, con la ruta mal por un nivel.
50fn exigido() -> bool {
51 std::env::var_os("LLIMPHI_JAULA_REQUIRE").is_some()
52}
53
54#[test]
55fn el_explorador_entero_cruza_y_pinta() {
56 let Some(binario) = binario_del_huesped() else {
57 assert!(!exigido(), "el explorador enjaulado tendría que estar compilado y no está");
58 eprintln!("salteado: falta el huésped. Generalo con\n ./scripts/check-jaula-linux.sh");
59 return;
60 };
61 eprintln!("explorador · huésped: {}", binario.display());
62
63 // Se lo apunta al repo mismo: una carpeta con cientos de entradas de
64 // verdad, no un directorio de mentira con tres archivos. Lo que se quiere
65 // saber es si un árbol REAL cruza.
66 let carpeta = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(4).unwrap().to_path_buf();
67 let (mut hijo, flujo) = lanzar("prueba-explorador", |ruta| {
68 let mut c = std::process::Command::new(&binario);
69 c.arg(ruta).arg(&carpeta);
70 c
71 })
72 .expect("lanzar el explorador enjaulado");
73
74 let mut anfitrion = Anfitrion::new(flujo);
75 let mut tablado = Tablado::nuevo();
76 let mut ts = Typesetter::new();
77 let mut medidas = 0u32;
78
79 // El plazo es generoso a propósito: el explorador lee un directorio real en
80 // su primer cuadro, y un plazo corto convertiría un disco lento en un test
81 // que falla sin decir por qué.
82 let cuadro = anfitrion
83 .atender_hasta(Duration::from_secs(30), |p| {
84 medidas += 1;
85 // **El que contesta es el que pinta**: el mismo typesetter que va a
86 // shapear. Medir con otro movería la caja de línea.
87 llimphi_reproductor::medir_texto(&mut ts, &p.cadena, &p.estilo, p.ancho_max)
88 })
89 .expect("atender al explorador");
90 tablado.poner_cuadro(*cuadro.expect("el explorador tiene que mandar su primer cuadro"));
91
92 // Montar el árbol del anfitrión con el tablado adentro y pintarlo de verdad.
93 let hijo_vista = tablado.vista::<()>(Style {
94 size: Size { width: length(CAJA.0), height: length(CAJA.1) },
95 ..Default::default()
96 });
97 let raiz = View::new(Style {
98 size: Size { width: length(ANCHO), height: length(ALTO) },
99 padding: TRect {
100 left: length(ORIGEN.0),
101 right: length(0.0),
102 top: length(ORIGEN.1),
103 bottom: length(0.0),
104 },
105 ..Default::default()
106 })
107 .children(vec![hijo_vista]);
108 let mut arbol = LayoutTree::new();
109 let montado = mount(&mut arbol, raiz);
110 let computado = arbol.compute(montado.root, (ANCHO, ALTO)).expect("layout");
111 let mut lienzo = Lienzo::new();
112 let mut ts2 = Typesetter::new();
113 paint(&mut lienzo, &montado, &computado, &mut ts2, None, None);
114
115 let c = tablado.contadores();
116 eprintln!(
117 "explorador · {} órdenes · {} textos shapeados acá · {} medidas preguntadas · {} salteadas",
118 c.ordenes(),
119 c.textos(),
120 medidas,
121 c.salteadas()
122 );
123
124 // Un explorador con un directorio real adentro no puede pintar cuatro
125 // rectángulos: si el número es chico, cruzó el chrome y no el contenido.
126 assert!(
127 c.ordenes() > 100,
128 "el explorador compuso su interfaz entera: {} órdenes es demasiado poco \
129 para dos sidebars más un listado de carpeta",
130 c.ordenes()
131 );
132 assert!(
133 c.textos() > 20,
134 "y el texto se shapeó DE ESTE LADO: {} nodos de texto es demasiado poco \
135 — es lo que prueba que el reparto funciona (el huésped mandó cadenas, no glifos)",
136 c.textos()
137 );
138 assert_eq!(c.salteadas(), 0, "nada vino mal formado");
139 assert_eq!(c.cuadros_pintados(), 1);
140 assert_eq!(c.pintadas_sin_escena(), 0, "hubo escena desde el primer cuadro");
141 assert!(medidas > 0, "el huésped preguntó las medidas en vez de shapear");
142
143 // La muerte se ve y borra el cuadro: un proceso muerto que deja su interfaz
144 // pegada es la peor versión, porque el operador le hace clic.
145 anfitrion.despedirse().expect("decirle que se vaya");
146 let _ = hijo.wait();
147 tablado.olvidar();
148 assert!(!tablado.hay_cuadro(), "un proceso muerto no deja su interfaz pegada");
149}