1use clap::{Args, Subcommand};
5use colored::*;
6use rand::Rng;
7use std::fs::{self, File};
8use std::io::Write;
9use std::path::{Path, PathBuf};
10
11#[derive(Args)]
12pub struct GenerateArgs {
13 #[command(subcommand)]
14 pub command: GenerateCommands,
15}
16
17#[derive(Subcommand)]
18pub enum GenerateCommands {
19 Files {
21 #[arg(short, long, default_value = "3")]
23 count: u32,
24
25 #[arg(long = "out-dir", default_value = "./generated-files")]
27 out_dir: PathBuf,
28
29 #[arg(long, default_value = "medium")]
31 complexity: String,
32 },
33}
34
35pub async fn execute(args: GenerateArgs) -> anyhow::Result<()> {
36 match args.command {
37 GenerateCommands::Files {
38 count,
39 out_dir,
40 complexity,
41 } => generate_files(count, out_dir, &complexity).await,
42 }
43}
44
45async fn generate_files(count: u32, output: PathBuf, complexity: &str) -> anyhow::Result<()> {
46 let settings = match complexity {
47 "simple" => ComplexitySettings {
48 vertices: 8,
49 elements: 50,
50 points: 1000,
51 },
52 "complex" => ComplexitySettings {
53 vertices: 200,
54 elements: 1000,
55 points: 100_000,
56 },
57 _ => ComplexitySettings {
58 vertices: 50,
59 elements: 200,
60 points: 10000,
61 }, };
63
64 println!(
65 "\n{}",
66 "╔══════════════════════════════════════════════════════════════╗".cyan()
67 );
68 println!(
69 "{}",
70 "║ Engineering Files Generator (Rust) ║".cyan()
71 );
72 println!(
73 "{}",
74 "╚══════════════════════════════════════════════════════════════╝".cyan()
75 );
76 println!("\nOutput: {}", output.display());
77 println!("Count: {} of each type", count);
78 println!(
79 "Complexity: {} (vertices: {}, elements: {})",
80 complexity, settings.vertices, settings.elements
81 );
82
83 fs::create_dir_all(&output)?;
85
86 let mut stats = Stats::default();
87 let mut total_bytes: u64 = 0;
88
89 println!(
91 "\n{}",
92 "[1/6] Generating OBJ files (3D mesh geometry)...".yellow()
93 );
94 for i in 1..=count {
95 let (size, mtl_size) = generate_obj(&output, i, &settings)?;
96 total_bytes += size + mtl_size;
97 stats.obj += 1;
98 println!(
99 " {} building-model-{}.obj ({:.1} KB)",
100 "✓".green(),
101 i,
102 size as f64 / 1024.0
103 );
104 }
105
106 println!(
108 "\n{}",
109 "[2/6] Generating DXF files (AutoCAD drawings)...".yellow()
110 );
111 for i in 1..=count {
112 let size = generate_dxf(&output, i)?;
113 total_bytes += size;
114 stats.dxf += 1;
115 println!(
116 " {} floorplan-{}.dxf ({:.1} KB)",
117 "✓".green(),
118 i,
119 size as f64 / 1024.0
120 );
121 }
122
123 println!(
125 "\n{}",
126 "[3/6] Generating STL files (3D printing meshes)...".yellow()
127 );
128 for i in 1..=count {
129 let size = generate_stl(&output, i)?;
130 total_bytes += size;
131 stats.stl += 1;
132 println!(
133 " {} part-{}.stl ({:.1} KB)",
134 "✓".green(),
135 i,
136 size as f64 / 1024.0
137 );
138 }
139
140 println!(
142 "\n{}",
143 "[4/6] Generating IFC files (BIM models)...".yellow()
144 );
145 for i in 1..=count {
146 let size = generate_ifc(&output, i, &settings)?;
147 total_bytes += size;
148 stats.ifc += 1;
149 println!(
150 " {} building-{}.ifc ({:.1} KB)",
151 "✓".green(),
152 i,
153 size as f64 / 1024.0
154 );
155 }
156
157 println!("\n{}", "[5/6] Generating JSON metadata files...".yellow());
159 for i in 1..=count {
160 let size = generate_json(&output, i, &settings)?;
161 total_bytes += size;
162 stats.json += 1;
163 println!(
164 " {} project-{}-metadata.json ({:.1} KB)",
165 "✓".green(),
166 i,
167 size as f64 / 1024.0
168 );
169 }
170
171 println!("\n{}", "[6/6] Generating point cloud files...".yellow());
173 for i in 1..=count {
174 let size = generate_xyz(&output, i, &settings)?;
175 total_bytes += size;
176 stats.xyz += 1;
177 println!(
178 " {} scan-{}.xyz ({:.1} KB)",
179 "✓".green(),
180 i,
181 size as f64 / 1024.0
182 );
183 }
184
185 println!(
187 "\n{}",
188 "╔══════════════════════════════════════════════════════════════╗".cyan()
189 );
190 println!(
191 "{}",
192 "║ Generation Complete ║".cyan()
193 );
194 println!(
195 "{}",
196 "╚══════════════════════════════════════════════════════════════╝".cyan()
197 );
198
199 println!("\n Output: {}", fs::canonicalize(&output)?.display());
200 println!("\n Files Generated:");
201 println!(" OBJ (3D mesh): {} files", stats.obj);
202 println!(" DXF (AutoCAD): {} files", stats.dxf);
203 println!(" STL (3D print): {} files", stats.stl);
204 println!(" IFC (BIM): {} files", stats.ifc);
205 println!(" JSON (metadata): {} files", stats.json);
206 println!(" XYZ (point cloud): {} files", stats.xyz);
207 println!(" ────────────────────────────");
208 let total = stats.obj + stats.dxf + stats.stl + stats.ifc + stats.json + stats.xyz;
209 println!(
210 " Total: {} files ({:.1} KB)",
211 total,
212 total_bytes as f64 / 1024.0
213 );
214
215 println!("\n {}", "Compatible with APS Translation:".green());
216 println!(" ✓ OBJ → SVF/SVF2 viewer format");
217 println!(" ✓ DXF → SVF/SVF2 viewer format");
218 println!(" ✓ STL → SVF/SVF2 viewer format");
219 println!(" ✓ IFC → SVF/SVF2 viewer format");
220
221 println!("\n{}", "=== Generation Complete ===".cyan());
222
223 Ok(())
224}
225
226struct ComplexitySettings {
227 vertices: u32,
228 elements: u32,
229 points: u32,
230}
231
232#[derive(Default)]
233struct Stats {
234 obj: u32,
235 dxf: u32,
236 stl: u32,
237 ifc: u32,
238 json: u32,
239 xyz: u32,
240}
241
242fn generate_obj(
243 output: &Path,
244 index: u32,
245 _settings: &ComplexitySettings,
246) -> anyhow::Result<(u64, u64)> {
247 let obj_path = output.join(format!("building-model-{}.obj", index));
248 let mtl_path = output.join(format!("building-model-{}.mtl", index));
249
250 let mut obj_content = format!(
251 "# APS Demo - Building Model {}\n# Generated by raps\n\nmtllib building-model-{}.mtl\n\n",
252 index, index
253 );
254
255 let mut vertex_offset = 0u32;
256
257 let components = vec![
259 ("Foundation", 20.0, 1.0, 15.0, 0.0, -0.5, 0.0),
260 ("Floor1", 18.0, 3.0, 13.0, 0.0, 2.0, 0.0),
261 ("Floor2", 18.0, 3.0, 13.0, 0.0, 5.5, 0.0),
262 ("Roof", 20.0, 0.5, 15.0, 0.0, 7.5, 0.0),
263 ];
264
265 for (name, w, h, d, cx, cy, cz) in components {
266 obj_content.push_str(&format!("\no {}\nusemtl {}_material\n", name, name));
267
268 let hw = w / 2.0;
270 let hh = h / 2.0;
271 let hd = d / 2.0;
272 let verts = vec![
273 (cx - hw, cy - hh, cz + hd),
274 (cx + hw, cy - hh, cz + hd),
275 (cx + hw, cy + hh, cz + hd),
276 (cx - hw, cy + hh, cz + hd),
277 (cx - hw, cy - hh, cz - hd),
278 (cx + hw, cy - hh, cz - hd),
279 (cx + hw, cy + hh, cz - hd),
280 (cx - hw, cy + hh, cz - hd),
281 ];
282
283 for (x, y, z) in &verts {
284 obj_content.push_str(&format!("v {:.6} {:.6} {:.6}\n", x, y, z));
285 }
286
287 let o = vertex_offset + 1;
289 obj_content.push_str(&format!("f {} {} {} {}\n", o, o + 1, o + 2, o + 3));
290 obj_content.push_str(&format!("f {} {} {} {}\n", o + 7, o + 6, o + 5, o + 4));
291 obj_content.push_str(&format!("f {} {} {} {}\n", o + 3, o + 2, o + 6, o + 7));
292 obj_content.push_str(&format!("f {} {} {} {}\n", o + 4, o + 5, o + 1, o));
293 obj_content.push_str(&format!("f {} {} {} {}\n", o + 1, o + 5, o + 6, o + 2));
294 obj_content.push_str(&format!("f {} {} {} {}\n", o + 4, o, o + 3, o + 7));
295
296 vertex_offset += 8;
297 }
298
299 let mut obj_file = File::create(&obj_path)?;
301 obj_file.write_all(obj_content.as_bytes())?;
302
303 let mtl_content = format!(
305 "# Material Library for building-model-{}.obj\n\n\
306 newmtl Foundation_material\nKd 0.5 0.5 0.5\nKa 0.1 0.1 0.1\n\n\
307 newmtl Floor1_material\nKd 0.8 0.8 0.7\nKa 0.1 0.1 0.1\n\n\
308 newmtl Floor2_material\nKd 0.8 0.8 0.7\nKa 0.1 0.1 0.1\n\n\
309 newmtl Roof_material\nKd 0.3 0.3 0.4\nKa 0.1 0.1 0.1\n",
310 index
311 );
312
313 let mut mtl_file = File::create(&mtl_path)?;
314 mtl_file.write_all(mtl_content.as_bytes())?;
315
316 Ok((obj_path.metadata()?.len(), mtl_path.metadata()?.len()))
317}
318
319fn generate_dxf(output: &Path, index: u32) -> anyhow::Result<u64> {
320 let mut rng = rand::thread_rng();
321 let width: f64 = rng.gen_range(20.0..40.0);
322 let height: f64 = rng.gen_range(15.0..30.0);
323 let rooms: u32 = rng.gen_range(3..8);
324
325 let path = output.join(format!("floorplan-{}.dxf", index));
326
327 let mut content = String::from(
328 "0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1015\n9\n$INSUNITS\n70\n4\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n",
329 );
330
331 let walls = vec![
333 (0.0, 0.0, width, 0.0),
334 (width, 0.0, width, height),
335 (width, height, 0.0, height),
336 (0.0, height, 0.0, 0.0),
337 ];
338
339 for (x1, y1, x2, y2) in walls {
340 content.push_str(&format!(
341 "0\nLINE\n8\nWalls\n10\n{:.1}\n20\n{:.1}\n30\n0.0\n11\n{:.1}\n21\n{:.1}\n31\n0.0\n",
342 x1, y1, x2, y2
343 ));
344 }
345
346 for r in 1..rooms {
348 let div_x = width * r as f64 / rooms as f64;
349 content.push_str(&format!(
350 "0\nLINE\n8\nInterior_Walls\n10\n{:.1}\n20\n0.0\n30\n0.0\n11\n{:.1}\n21\n{:.1}\n31\n0.0\n",
351 div_x, div_x, height
352 ));
353 }
354
355 for d in 0..rooms {
357 let door_x = width * (d as f64 + 0.5) / rooms as f64;
358 content.push_str(&format!(
359 "0\nCIRCLE\n8\nDoors\n10\n{:.1}\n20\n0.5\n30\n0.0\n40\n0.8\n",
360 door_x
361 ));
362 }
363
364 content.push_str(&format!(
366 "0\nTEXT\n8\nDimensions\n10\n{:.1}\n20\n-2.0\n30\n0.0\n40\n1.0\n1\n{:.0}m x {:.0}m\n",
367 width / 2.0,
368 width,
369 height
370 ));
371
372 content.push_str("0\nENDSEC\n0\nEOF\n");
373
374 let mut file = File::create(&path)?;
375 file.write_all(content.as_bytes())?;
376
377 Ok(path.metadata()?.len())
378}
379
380fn generate_stl(output: &Path, index: u32) -> anyhow::Result<u64> {
381 let mut rng = rand::thread_rng();
382 let scale: f64 = rng.gen_range(10.0..30.0);
383
384 let path = output.join(format!("part-{}.stl", index));
385
386 let content = format!(
387 "solid Part_{}\n\
388 facet normal 0 0 1\n outer loop\n vertex 0 0 {s}\n vertex {s} 0 {s}\n vertex {s} {s} {s}\n endloop\n endfacet\n\
389 facet normal 0 0 1\n outer loop\n vertex 0 0 {s}\n vertex {s} {s} {s}\n vertex 0 {s} {s}\n endloop\n endfacet\n\
390 facet normal 0 0 -1\n outer loop\n vertex 0 0 0\n vertex {s} {s} 0\n vertex {s} 0 0\n endloop\n endfacet\n\
391 facet normal 0 0 -1\n outer loop\n vertex 0 0 0\n vertex 0 {s} 0\n vertex {s} {s} 0\n endloop\n endfacet\n\
392 facet normal 0 -1 0\n outer loop\n vertex 0 0 0\n vertex {s} 0 0\n vertex {s} 0 {s}\n endloop\n endfacet\n\
393 facet normal 0 -1 0\n outer loop\n vertex 0 0 0\n vertex {s} 0 {s}\n vertex 0 0 {s}\n endloop\n endfacet\n\
394 facet normal 0 1 0\n outer loop\n vertex 0 {s} 0\n vertex {s} {s} {s}\n vertex {s} {s} 0\n endloop\n endfacet\n\
395 facet normal 0 1 0\n outer loop\n vertex 0 {s} 0\n vertex 0 {s} {s}\n vertex {s} {s} {s}\n endloop\n endfacet\n\
396 facet normal -1 0 0\n outer loop\n vertex 0 0 0\n vertex 0 {s} {s}\n vertex 0 {s} 0\n endloop\n endfacet\n\
397 facet normal -1 0 0\n outer loop\n vertex 0 0 0\n vertex 0 0 {s}\n vertex 0 {s} {s}\n endloop\n endfacet\n\
398 facet normal 1 0 0\n outer loop\n vertex {s} 0 0\n vertex {s} {s} 0\n vertex {s} {s} {s}\n endloop\n endfacet\n\
399 facet normal 1 0 0\n outer loop\n vertex {s} 0 0\n vertex {s} {s} {s}\n vertex {s} 0 {s}\n endloop\n endfacet\n\
400 endsolid Part_{}\n",
401 index,
402 index,
403 s = scale
404 );
405
406 let mut file = File::create(&path)?;
407 file.write_all(content.as_bytes())?;
408
409 Ok(path.metadata()?.len())
410}
411
412fn generate_ifc(output: &Path, index: u32, settings: &ComplexitySettings) -> anyhow::Result<u64> {
413 let mut rng = rand::thread_rng();
414 let path = output.join(format!("building-{}.ifc", index));
415
416 let timestamp = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S").to_string();
417 let project_guid = generate_ifc_guid();
418 let site_guid = generate_ifc_guid();
419 let building_guid = generate_ifc_guid();
420
421 let mut content = format!(
422 "ISO-10303-21;\n\
423 HEADER;\n\
424 FILE_DESCRIPTION(('ViewDefinition [CoordinationView_V2.0]'),'2;1');\n\
425 FILE_NAME('building-{}.ifc','{}',('RAPS Generator'),('Demo Organization'),'IFC4','raps','');\n\
426 FILE_SCHEMA(('IFC4'));\n\
427 ENDSEC;\n\n\
428 DATA;\n\
429 #1=IFCPROJECT('{}',#2,'Building Project {}','Demo building model',`$,`$,`$,(#7),#11);\n\
430 #2=IFCOWNERHISTORY(#3,#6,`$,.NOCHANGE.,`$,`$,`$,1234567890);\n\
431 #3=IFCPERSONANDORGANIZATION(#4,#5,`$);\n\
432 #4=IFCPERSON(`$,'Generator','CLI',`$,`$,`$,`$,`$);\n\
433 #5=IFCORGANIZATION(`$,'Demo Corp','Demo Organization',`$,`$);\n\
434 #6=IFCAPPLICATION(#5,'1.0','APS CLI Generator','APSCLI');\n\
435 #7=IFCGEOMETRICREPRESENTATIONCONTEXT(`$,'Model',3,1.E-05,#8,#9);\n\
436 #8=IFCAXIS2PLACEMENT3D(#10,`$,`$);\n\
437 #9=IFCDIRECTION((0.,1.,0.));\n\
438 #10=IFCCARTESIANPOINT((0.,0.,0.));\n\
439 #11=IFCUNITASSIGNMENT((#12,#13,#14,#15));\n\
440 #12=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);\n\
441 #13=IFCSIUNIT(*,.AREAUNIT.,`$,.SQUARE_METRE.);\n\
442 #14=IFCSIUNIT(*,.VOLUMEUNIT.,`$,.CUBIC_METRE.);\n\
443 #15=IFCSIUNIT(*,.PLANEANGLEUNIT.,`$,.RADIAN.);\n\n\
444 #20=IFCSITE('{}',#2,'Site','Building site',`$,#21,`$,`$,.ELEMENT.,`$,`$,`$,`$,`$);\n\
445 #21=IFCLOCALPLACEMENT(`$,#8);\n\n\
446 #30=IFCBUILDING('{}',#2,'Building {}','Main building',`$,#31,`$,`$,.ELEMENT.,`$,`$,`$);\n\
447 #31=IFCLOCALPLACEMENT(#21,#8);\n\n\
448 #40=IFCRELAGGREGATES('{}',#2,`$,`$,#1,(#20));\n\
449 #41=IFCRELAGGREGATES('{}',#2,`$,`$,#20,(#30));\n\n",
450 index,
451 timestamp,
452 project_guid,
453 index,
454 site_guid,
455 building_guid,
456 index,
457 generate_ifc_guid(),
458 generate_ifc_guid()
459 );
460
461 let storey_count = rng.gen_range(2..5);
463 let mut entity_id = 100u32;
464
465 let ifc_categories = [
466 "IfcWall",
467 "IfcDoor",
468 "IfcWindow",
469 "IfcSlab",
470 "IfcColumn",
471 "IfcBeam",
472 ];
473
474 for s in 0..storey_count {
475 let elevation = s * 3000;
476 content.push_str(&format!(
477 "#{}=IFCBUILDINGSTOREY('{}',#2,'Level {}','Storey at {}mm',`$,#{},`$,`$,.ELEMENT.,{}.0);\n\
478 #{}=IFCLOCALPLACEMENT(#31,#8);\n",
479 entity_id, generate_ifc_guid(), s + 1, elevation, entity_id + 1, elevation, entity_id + 1
480 ));
481 entity_id += 2;
482 }
483
484 let _elements_per_storey = settings.elements / storey_count;
486 for _ in 0..settings.elements {
487 let cat = ifc_categories[rng.gen_range(0..ifc_categories.len())];
488 content.push_str(&format!(
489 "#{}={}('{}',#2,'{}_{}',' ',`$,`$,`$,`$);\n",
490 entity_id,
491 cat,
492 generate_ifc_guid(),
493 cat,
494 entity_id
495 ));
496 entity_id += 1;
497 }
498
499 content.push_str("ENDSEC;\nEND-ISO-10303-21;\n");
500
501 let mut file = File::create(&path)?;
502 file.write_all(content.as_bytes())?;
503
504 Ok(path.metadata()?.len())
505}
506
507fn generate_json(output: &Path, index: u32, settings: &ComplexitySettings) -> anyhow::Result<u64> {
508 let mut rng = rand::thread_rng();
509 let path = output.join(format!("project-{}-metadata.json", index));
510
511 let categories = [
512 "Walls", "Doors", "Windows", "Floors", "Ceilings", "Columns", "Beams",
513 ];
514 let levels = ["Basement", "Level 1", "Level 2", "Level 3", "Roof"];
515 let materials = ["Concrete", "Steel", "Wood", "Glass", "Aluminum", "Brick"];
516
517 let mut elements = Vec::new();
518 let mut total_area = 0.0f64;
519 let mut total_volume = 0.0f64;
520
521 for i in 1..=settings.elements {
522 let area: f64 = rng.gen_range(1.0..500.0);
523 let volume: f64 = rng.gen_range(1.0..200.0);
524 total_area += area;
525 total_volume += volume;
526
527 elements.push(serde_json::json!({
528 "dbId": i,
529 "externalId": uuid::Uuid::new_v4().to_string(),
530 "name": format!("{}_{}", categories[rng.gen_range(0..categories.len())], i),
531 "category": categories[rng.gen_range(0..categories.len())],
532 "level": levels[rng.gen_range(0..levels.len())],
533 "material": materials[rng.gen_range(0..materials.len())],
534 "geometry": {
535 "area": (area * 100.0).round() / 100.0,
536 "volume": (volume * 100.0).round() / 100.0,
537 },
538 "visible": rng.gen_bool(0.9),
539 }));
540 }
541
542 let metadata = serde_json::json!({
543 "projectInfo": {
544 "id": uuid::Uuid::new_v4().to_string(),
545 "name": format!("Demo Project {}", index),
546 "number": format!("PRJ-{:04}", rng.gen_range(1000..9999)),
547 },
548 "modelInfo": {
549 "version": format!("2024.{}", index),
550 "units": "millimeters",
551 },
552 "statistics": {
553 "totalElements": settings.elements,
554 "totalArea": (total_area * 100.0).round() / 100.0,
555 "totalVolume": (total_volume * 100.0).round() / 100.0,
556 },
557 "elements": elements,
558 });
559
560 let mut file = File::create(&path)?;
561 file.write_all(serde_json::to_string_pretty(&metadata)?.as_bytes())?;
562
563 Ok(path.metadata()?.len())
564}
565
566fn generate_xyz(output: &Path, index: u32, settings: &ComplexitySettings) -> anyhow::Result<u64> {
567 let mut rng = rand::thread_rng();
568 let path = output.join(format!("scan-{}.xyz", index));
569
570 let mut content = format!(
571 "# XYZ Point Cloud - Scan {}\n# Points: {}\n# Format: X Y Z R G B Intensity\n",
572 index, settings.points
573 );
574
575 for _ in 0..settings.points {
576 let surface = rng.gen_range(0..6);
578 let (x, y, z) = match surface {
579 0 => (rng.gen_range(-20.0..20.0), -10.0, rng.gen_range(0.0..10.0)),
580 1 => (rng.gen_range(-20.0..20.0), 10.0, rng.gen_range(0.0..10.0)),
581 2 => (-20.0, rng.gen_range(-10.0..10.0), rng.gen_range(0.0..10.0)),
582 3 => (20.0, rng.gen_range(-10.0..10.0), rng.gen_range(0.0..10.0)),
583 4 => (rng.gen_range(-20.0..20.0), rng.gen_range(-10.0..10.0), 0.0),
584 _ => (rng.gen_range(-20.0..20.0), rng.gen_range(-10.0..10.0), 10.0),
585 };
586
587 let x = x + rng.gen_range(-0.5..0.5);
589 let y = y + rng.gen_range(-0.5..0.5);
590 let z = z + rng.gen_range(-0.5..0.5);
591
592 let r: u8 = rng.gen_range(100..200);
593 let g: u8 = rng.gen_range(100..200);
594 let b: u8 = rng.gen_range(100..200);
595 let intensity: f64 = rng.gen_range(0.5..1.0);
596
597 content.push_str(&format!(
598 "{:.3} {:.3} {:.3} {} {} {} {:.2}\n",
599 x, y, z, r, g, b, intensity
600 ));
601 }
602
603 let mut file = File::create(&path)?;
604 file.write_all(content.as_bytes())?;
605
606 Ok(path.metadata()?.len())
607}
608
609fn generate_ifc_guid() -> String {
610 let uuid = uuid::Uuid::new_v4();
611 let bytes = uuid.as_bytes();
612 let mut result = String::with_capacity(22);
613
614 const CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
615
616 for i in 0..22 {
617 let idx = (bytes[i % 16] as usize + i) % 64;
618 result.push(CHARS[idx] as char);
619 }
620
621 result
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn test_generate_ifc_guid_format() {
630 let guid = generate_ifc_guid();
631
632 assert_eq!(guid.len(), 22);
634
635 const VALID_CHARS: &str =
637 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
638 for ch in guid.chars() {
639 assert!(
640 VALID_CHARS.contains(ch),
641 "Invalid character in IFC GUID: {}",
642 ch
643 );
644 }
645 }
646
647 #[test]
648 fn test_generate_ifc_guid_uniqueness() {
649 let guid1 = generate_ifc_guid();
651 let guid2 = generate_ifc_guid();
652 let guid3 = generate_ifc_guid();
653
654 assert_ne!(guid1, guid2);
655 assert_ne!(guid2, guid3);
656 assert_ne!(guid1, guid3);
657 }
658
659 #[test]
660 fn test_generate_ifc_guid_multiple_calls() {
661 for _ in 0..100 {
663 let guid = generate_ifc_guid();
664 assert_eq!(guid.len(), 22);
665 }
666 }
667}