gioser

tawasuyu/llimphi

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

Rust 614Markdown 274TOML 149RON 310606 Sutichaykuna

⎇ main llimphi-voxel/tests/documento.rs Rust · 207 siq'ikuna · 7.4 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1// =============================================================================
2//  llimphi-voxel :: tests/documento — abrir, guardar, combinar, extraer
3// -----------------------------------------------------------------------------
4//  Certifica la administración por archivos: round-trip del format (RON y
5//  postcard), combinar proyectos sin colisiones ni referencias rotas, y extraer
6//  una escena con toda su clausura de dependencias como fragmento portable.
7//  (`Project` no es `PartialEq` por historia, así que se compara por
8//  serialización — el round-trip idempotente es prueba suficiente.)
9// =============================================================================
10
11use llimphi_voxel::{Documento, ErrorDoc, Project, Suceso, TipoSuceso, MAGIA_DOC, VERSION_DOC};
12
13#[test]
14fn round_trip_ron_preserva_el_proyecto() {
15    let p = Project::starter();
16    let doc = Documento::nuevo(p.clone());
17    let texto = doc.a_ron().unwrap();
18    let recuperado = Documento::de_ron(&texto).unwrap();
19    // Re-serializar da el MISMO texto: el proyecto sobrevivió íntegro.
20    assert_eq!(recuperado.a_ron().unwrap(), texto);
21    assert_eq!(recuperado.magia, MAGIA_DOC);
22    assert_eq!(recuperado.version, VERSION_DOC);
23    assert_eq!(recuperado.proyecto.escenas.len(), p.escenas.len());
24}
25
26#[test]
27fn round_trip_postcard_nativo() {
28    let doc = Documento::nuevo(Project::starter());
29    let bytes = doc.a_bytes().unwrap();
30    let recuperado = Documento::de_bytes(&bytes).unwrap();
31    // El format nativo compacto también preserva el proyecto (comparado vía RON).
32    assert_eq!(recuperado.a_ron().unwrap(), doc.a_ron().unwrap());
33    // Y es más chico que el RON legible.
34    assert!(bytes.len() < doc.a_ron().unwrap().len());
35}
36
37#[test]
38fn un_ron_ajeno_se_rechaza() {
39    // Un RON que no parsea como documento se rechaza (no se malinterpreta).
40    assert!(matches!(
41        Documento::de_ron("(foo: 1, bar: 2)"),
42        Err(ErrorDoc::Parsear(_))
43    ));
44}
45
46#[test]
47fn marca_ajena_pero_ron_valido_se_detecta() {
48    let doc = Documento::nuevo(Project::starter());
49    let texto = doc.a_ron().unwrap().replace(MAGIA_DOC, "otra-cosa");
50    assert_eq!(Documento::de_ron(&texto).unwrap_err(), ErrorDoc::MagiaAjena);
51}
52
53#[test]
54fn version_futura_se_rechaza() {
55    let mut doc = Documento::nuevo(Project::starter());
56    doc.version = VERSION_DOC + 1;
57    let texto = doc.a_ron().unwrap();
58    assert_eq!(
59        Documento::de_ron(&texto).unwrap_err(),
60        ErrorDoc::VersionFutura(VERSION_DOC + 1)
61    );
62}
63
64#[test]
65fn el_starter_es_integro() {
66    // El proyecto de arranque no tiene referencias rotas.
67    assert!(Project::starter().validar_referencias().is_empty());
68}
69
70#[test]
71fn combinar_no_colisiona_ni_rompe_referencias() {
72    let mut a = Project::starter();
73    let b = Project::starter();
74    let (leyes_a, mats_a, seres_a, biomas_a, mundos_a, escenas_a) = (
75        a.leyes.len(),
76        a.materiales.len(),
77        a.seres.len(),
78        a.biomas.len(),
79        a.mundos.len(),
80        a.escenas.len(),
81    );
82
83    a.combinar(&b);
84
85    // Se duplicó todo (dos starters).
86    assert_eq!(a.leyes.len(), leyes_a + b.leyes.len());
87    assert_eq!(a.materiales.len(), mats_a + b.materiales.len());
88    assert_eq!(a.seres.len(), seres_a + b.seres.len());
89    assert_eq!(a.biomas.len(), biomas_a + b.biomas.len());
90    assert_eq!(a.mundos.len(), mundos_a + b.mundos.len());
91    assert_eq!(a.escenas.len(), escenas_a + b.escenas.len());
92
93    // Ningún id repetido en todo el proyecto combinado.
94    let mut ids: Vec<u64> = a
95        .leyes
96        .iter()
97        .map(|x| x.id)
98        .chain(a.materiales.iter().map(|x| x.id))
99        .chain(a.seres.iter().map(|x| x.id))
100        .chain(a.biomas.iter().map(|x| x.id))
101        .chain(a.mundos.iter().map(|x| x.id))
102        .chain(a.escenas.iter().map(|x| x.id))
103        .collect();
104    let total = ids.len();
105    ids.sort_unstable();
106    ids.dedup();
107    assert_eq!(ids.len(), total, "hay ids repetidos tras combinar");
108
109    // Y TODAS las referencias cruzadas siguen resolviendo.
110    let rotas = a.validar_referencias();
111    assert!(rotas.is_empty(), "combinar rompió referencias: {rotas:?}");
112}
113
114#[test]
115fn extraer_escena_produce_un_fragmento_integro_y_minimo() {
116    let p = Project::starter();
117    let escena_id = p.escenas[0].id;
118    let frag = p.extraer_escena(escena_id).expect("la escena existe");
119
120    // El fragmento tiene exactamente una escena y es íntegro.
121    assert_eq!(frag.escenas.len(), 1);
122    assert_eq!(frag.escenas[0].id, escena_id);
123    let rotas = frag.validar_referencias();
124    assert!(rotas.is_empty(), "fragmento roto: {rotas:?}");
125
126    // Es MÍNIMO: no arrastra el mundo/bioma que la escena no usa. El starter
127    // tiene 2 mundos; la escena demo usa uno → el fragmento trae 1.
128    assert_eq!(frag.mundos.len(), 1);
129    assert!(frag.mundos.len() < p.mundos.len());
130    assert_eq!(frag.mundos[0].id, p.escenas[0].mundo);
131}
132
133#[test]
134fn extraer_luego_combinar_reconstituye_una_escena_en_otro_proyecto() {
135    let origen = Project::starter();
136    let escena_id = origen.escenas[0].id;
137
138    // Un proyecto destino vacío recibe SÓLO esa escena, con su clausura.
139    let mut destino = Project { next_id: 1, ..Default::default() };
140    destino.combinar_escena(&origen, escena_id).unwrap();
141
142    assert_eq!(destino.escenas.len(), 1);
143    assert!(destino.validar_referencias().is_empty());
144    // El destino quedó auto-contenido: resuelve el mundo de la escena.
145    let esc = &destino.escenas[0];
146    assert!(destino.mundo(esc.mundo).is_some());
147    // Y los actores apuntan a seres válidos del destino.
148    for a in &esc.actors {
149        assert!(destino.seres.get(a.character).is_some());
150    }
151}
152
153#[test]
154fn combinar_escena_inexistente_es_error() {
155    let mut destino = Project::starter();
156    let otro = Project::starter();
157    assert_eq!(
158        destino.combinar_escena(&otro, 999_999).unwrap_err(),
159        ErrorDoc::EscenaAusente(999_999)
160    );
161}
162
163#[test]
164fn los_sucesos_arrastran_su_ser_en_la_clausura_y_se_validan() {
165    // Partimos del starter (tiene seres); dictamos en su escena un suceso
166    // "aparece el primer ser".
167    let mut p = Project::starter();
168    let ser_id = p.seres[0].id;
169    let escena_id = p.escenas[0].id;
170    p.escenas[0].sucesos.push(Suceso {
171        t: 1.0,
172        pos: [10.0, 2.0, 10.0],
173        tipo: TipoSuceso::Aparece(ser_id),
174        nota: "aquí aparece".into(),
175    });
176    assert!(p.validar_referencias().is_empty(), "el suceso apunta a un ser válido");
177
178    // Extraer la escena arrastra ESE ser a la clausura (aunque el bioma no lo use).
179    let frag = p.extraer_escena(escena_id).unwrap();
180    assert!(frag.seres.iter().any(|s| s.id == ser_id), "el ser del suceso viaja");
181    assert!(frag.validar_referencias().is_empty());
182    assert_eq!(frag.escenas[0].sucesos.len(), 1);
183
184    // Un suceso que apunta a un ser inexistente se delata.
185    let mut roto = Project::starter();
186    roto.escenas[0].sucesos.push(Suceso {
187        t: 0.0,
188        pos: [0.0; 3],
189        tipo: TipoSuceso::Aparece(999_999),
190        nota: String::new(),
191    });
192    assert!(!roto.validar_referencias().is_empty());
193}
194
195#[test]
196fn guardar_y_abrir_en_disco() {
197    let dir = std::env::temp_dir().join(format!("voxeldoc-{}", std::process::id()));
198    std::fs::create_dir_all(&dir).unwrap();
199    let path = dir.join("escena.voxstudio.ron");
200
201    let doc = Documento::nuevo(Project::starter());
202    doc.guardar(&path).unwrap();
203    let leido = Documento::abrir(&path).unwrap();
204    assert_eq!(leido.a_ron().unwrap(), doc.a_ron().unwrap());
205
206    let _ = std::fs::remove_file(&path);
207}