tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main llimphi-test/tests/self.rs Rust · 125 líneas · 5.0 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

1//! Self-tests del arnés: determinismo del readback, detección de diff, y un
2//! golden real de una card (sombra + gradiente + borde + radio) que ejercita
3//! las primitivas del Tier 1. Correr `LLIMPHI_GOLDEN_UPDATE=1 cargo test -p
4//! llimphi-test` para (re)generar los goldens en `tests/goldens/`.
5
6use llimphi_compositor::{Shadow, View};
7use llimphi_layout::taffy::prelude::{length, percent, FlexDirection, Size, Style};
8use llimphi_layout::taffy::{AlignItems, JustifyContent, Rect};
9use llimphi_raster::peniko::{Color, Gradient};
10use llimphi_raster::vello::kurbo::Point;
11use llimphi_test::{assert_golden, diff, with_harness};
12
13fn rgb(r: u8, g: u8, b: u8) -> Color {
14    Color::from_rgba8(r, g, b, 255)
15}
16
17/// Rect coloreado simple — para determinismo y diff.
18fn swatch(color: Color) -> View<()> {
19    View::<()>::new(Style {
20        size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
21        ..Default::default()
22    })
23    .fill(color)
24}
25
26/// Card centrada con sombra + gradiente + borde + radio + un texto. Ejercita
27/// las primitivas visuales del Tier 1 en una sola escena.
28fn card() -> View<()> {
29    let inner = View::<()>::new(Style {
30        size: Size { width: length(220.0_f32), height: length(120.0_f32) },
31        align_items: Some(AlignItems::CENTER),
32        justify_content: Some(JustifyContent::CENTER),
33        ..Default::default()
34    })
35    .fill_gradient(
36        Gradient::new_linear(Point::new(0.0, 0.0), Point::new(1.0, 1.0))
37            .with_stops([rgb(90, 120, 220), rgb(40, 60, 120)].as_slice()),
38    )
39    .radius(16.0)
40    .border(2.0, rgb(200, 210, 255))
41    .shadow(Shadow {
42        blur: 18.0,
43        dx: 0.0,
44        dy: 8.0,
45        color: Color::from_rgba8(0, 0, 0, 140),
46        spread: 2.0,
47    })
48    .children(vec![View::<()>::new(Style {
49        size: Size { width: length(180.0_f32), height: length(24.0_f32) },
50        ..Default::default()
51    })
52    .text_aligned(
53        "llimphi-test".to_string(),
54        18.0,
55        rgb(240, 244, 255),
56        llimphi_text::Alignment::Center,
57    )]);
58
59    View::<()>::new(Style {
60        size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
61        align_items: Some(AlignItems::CENTER),
62        justify_content: Some(JustifyContent::CENTER),
63        flex_direction: FlexDirection::Column,
64        padding: Rect { left: length(0.0_f32), right: length(0.0_f32), top: length(0.0_f32), bottom: length(0.0_f32) },
65        ..Default::default()
66    })
67    .fill(rgb(24, 26, 34))
68    .children(vec![inner])
69}
70
71#[test]
72fn readback_es_determinista() {
73    // El mismo view rasterizado dos veces por el adapter de software debe dar
74    // píxeles idénticos: certifica el pipeline headless + el readback sin ruido.
75    let a = with_harness(|h| h.render(card(), 400, 240, rgb(24, 26, 34)));
76    let b = with_harness(|h| h.render(card(), 400, 240, rgb(24, 26, 34)));
77    let (_, stats) = diff(&a, &b, 0).expect("mismas dims");
78    assert_eq!(
79        stats.differing_pixels, 0,
80        "render no determinista: {} px difieren (max_delta {})",
81        stats.differing_pixels, stats.max_delta
82    );
83}
84
85#[test]
86fn diff_detecta_cambio_de_color() {
87    // Dos frames de colores distintos deben diferir en ~todos los píxeles y con
88    // delta grande — prueba que el diff no da falsos negativos.
89    let rojo = with_harness(|h| h.render(swatch(rgb(220, 30, 30)), 64, 64, rgb(0, 0, 0)));
90    let verde = with_harness(|h| h.render(swatch(rgb(30, 220, 30)), 64, 64, rgb(0, 0, 0)));
91    let (_, stats) = diff(&rojo, &verde, 4).expect("mismas dims");
92    assert_eq!(stats.total_pixels, 64 * 64);
93    assert!(
94        stats.fraction > 0.95,
95        "esperaba casi todos los px distintos, fue frac {:.3}",
96        stats.fraction
97    );
98    assert!(stats.max_delta > 150, "delta de color chico: {}", stats.max_delta);
99}
100
101#[test]
102fn diff_identico_es_cero() {
103    let a = with_harness(|h| h.render(swatch(rgb(120, 120, 120)), 32, 32, rgb(0, 0, 0)));
104    let (_, stats) = diff(&a, &a.clone(), 0).expect("mismas dims");
105    assert_eq!(stats.differing_pixels, 0);
106    assert_eq!(stats.max_delta, 0);
107    assert_eq!(stats.fraction, 0.0);
108}
109
110#[test]
111fn golden_card_tier1() {
112    // Golden real: si esta card cambia de aspecto (regresión en sombra/gradiente/
113    // borde/radio/texto) el test falla con DiffStats y escribe .actual/.diff.
114    //
115    // Re-bendecido el 2026-08-09. El anterior era del 13-jul y quedó atrás de
116    // `feat(llimphi): el camino GPU-directo ya sabe pintar texto` (5-ago). Que el
117    // cambio fuera legítimo NO se decidió mirando la imagen, se midió: los 342 px
118    // que diferían caían TODOS dentro de x 154..247 · y 112..129 —una caja de
119    // 94×18, o sea la única línea de texto de la card— y el resto del lienzo era
120    // idéntico bit a bit: sombra, gradiente, borde y radio intactos. El peor
121    // delta era 23/255, y un glifo MOVIDO deja ~130 (el criterio está escrito en
122    // widgets_invariantes.rs). Glifos en su lugar, cobertura apenas distinta:
123    // cambió el rasterizado del texto, no el dibujo.
124    assert_golden!("card_tier1", card(), 400, 240, rgb(24, 26, 34));
125}