1use crate::{EdgeSE2, Graph, GraphLoader, IoError, VertexSE2};
2use std::{fs, io::Write, path::Path};
3
4pub struct ToroLoader;
6
7impl GraphLoader for ToroLoader {
8 fn load<P: AsRef<Path>>(path: P) -> Result<Graph, IoError> {
9 let path_ref = path.as_ref();
10 let content = fs::read_to_string(path_ref).map_err(|e| {
11 IoError::Io(e).log_with_source(format!("Failed to read TORO file: {:?}", path_ref))
12 })?;
13
14 Self::parse_content(&content)
15 }
16
17 fn write<P: AsRef<Path>>(graph: &Graph, path: P) -> Result<(), IoError> {
18 if !graph.vertices_se3.is_empty() || !graph.edges_se3.is_empty() {
20 return Err(IoError::UnsupportedFormat(
21 "TORO format only supports SE2 (2D) graphs. Use G2O format for SE3 data."
22 .to_string(),
23 )
24 .log());
25 }
26
27 let path_ref = path.as_ref();
28 let mut file = fs::File::create(path_ref).map_err(|e| {
29 IoError::Io(e).log_with_source(format!("Failed to create TORO file: {:?}", path_ref))
30 })?;
31
32 let mut vertex_ids: Vec<_> = graph.vertices_se2.keys().collect();
34 vertex_ids.sort();
35
36 for id in vertex_ids {
37 let vertex = &graph.vertices_se2[id];
38 writeln!(
39 file,
40 "VERTEX2 {} {:.17e} {:.17e} {:.17e}",
41 vertex.id,
42 vertex.x(),
43 vertex.y(),
44 vertex.theta()
45 )
46 .map_err(|e| {
47 IoError::Io(e).log_with_source(format!("Failed to write TORO vertex {}", vertex.id))
48 })?;
49 }
50
51 for edge in &graph.edges_se2 {
54 let meas = &edge.measurement;
55 let info = &edge.information;
56
57 writeln!(
58 file,
59 "EDGE2 {} {} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e}",
60 edge.from,
61 edge.to,
62 meas.x(),
63 meas.y(),
64 meas.angle(),
65 info[(0, 0)], info[(0, 1)], info[(1, 1)], info[(2, 2)], info[(0, 2)], info[(1, 2)] )
72 .map_err(|e| {
73 IoError::Io(e).log_with_source(format!(
74 "Failed to write TORO edge {} -> {}",
75 edge.from, edge.to
76 ))
77 })?;
78 }
79
80 Ok(())
81 }
82}
83
84impl ToroLoader {
85 fn parse_content(content: &str) -> Result<Graph, IoError> {
86 let lines: Vec<&str> = content.lines().collect();
87 let mut graph = Graph::new();
88
89 for (line_num, line) in lines.iter().enumerate() {
90 Self::parse_line(line, line_num + 1, &mut graph)?;
91 }
92
93 Ok(graph)
94 }
95
96 fn parse_line(line: &str, line_num: usize, graph: &mut Graph) -> Result<(), IoError> {
97 let line = line.trim();
98
99 if line.is_empty() || line.starts_with('#') {
101 return Ok(());
102 }
103
104 let parts: Vec<&str> = line.split_whitespace().collect();
105 if parts.is_empty() {
106 return Ok(());
107 }
108
109 match parts[0] {
110 "VERTEX2" => {
111 let vertex = Self::parse_vertex2(&parts, line_num)?;
112 let id = vertex.id;
113 if graph.vertices_se2.insert(id, vertex).is_some() {
114 return Err(IoError::DuplicateVertex { id });
115 }
116 }
117 "EDGE2" => {
118 let edge = Self::parse_edge2(&parts, line_num)?;
119 graph.edges_se2.push(edge);
120 }
121 _ => {
122 }
124 }
125
126 Ok(())
127 }
128
129 fn parse_vertex2(parts: &[&str], line_num: usize) -> Result<VertexSE2, IoError> {
130 if parts.len() < 5 {
131 return Err(IoError::MissingFields { line: line_num });
132 }
133
134 let id = parts[1]
135 .parse::<usize>()
136 .map_err(|_| IoError::InvalidNumber {
137 line: line_num,
138 value: parts[1].to_string(),
139 })?;
140
141 let x = parts[2]
142 .parse::<f64>()
143 .map_err(|_| IoError::InvalidNumber {
144 line: line_num,
145 value: parts[2].to_string(),
146 })?;
147
148 let y = parts[3]
149 .parse::<f64>()
150 .map_err(|_| IoError::InvalidNumber {
151 line: line_num,
152 value: parts[3].to_string(),
153 })?;
154
155 let theta = parts[4]
156 .parse::<f64>()
157 .map_err(|_| IoError::InvalidNumber {
158 line: line_num,
159 value: parts[4].to_string(),
160 })?;
161
162 Ok(VertexSE2::new(id, x, y, theta))
163 }
164
165 fn parse_edge2(parts: &[&str], line_num: usize) -> Result<EdgeSE2, IoError> {
166 if parts.len() < 12 {
167 return Err(IoError::MissingFields { line: line_num });
168 }
169
170 let from = parts[1]
171 .parse::<usize>()
172 .map_err(|_| IoError::InvalidNumber {
173 line: line_num,
174 value: parts[1].to_string(),
175 })?;
176
177 let to = parts[2]
178 .parse::<usize>()
179 .map_err(|_| IoError::InvalidNumber {
180 line: line_num,
181 value: parts[2].to_string(),
182 })?;
183
184 let dx = parts[3]
186 .parse::<f64>()
187 .map_err(|_| IoError::InvalidNumber {
188 line: line_num,
189 value: parts[3].to_string(),
190 })?;
191 let dy = parts[4]
192 .parse::<f64>()
193 .map_err(|_| IoError::InvalidNumber {
194 line: line_num,
195 value: parts[4].to_string(),
196 })?;
197 let dtheta = parts[5]
198 .parse::<f64>()
199 .map_err(|_| IoError::InvalidNumber {
200 line: line_num,
201 value: parts[5].to_string(),
202 })?;
203
204 let i11 = parts[6]
206 .parse::<f64>()
207 .map_err(|_| IoError::InvalidNumber {
208 line: line_num,
209 value: parts[6].to_string(),
210 })?;
211 let i12 = parts[7]
212 .parse::<f64>()
213 .map_err(|_| IoError::InvalidNumber {
214 line: line_num,
215 value: parts[7].to_string(),
216 })?;
217 let i22 = parts[8]
218 .parse::<f64>()
219 .map_err(|_| IoError::InvalidNumber {
220 line: line_num,
221 value: parts[8].to_string(),
222 })?;
223 let i33 = parts[9]
224 .parse::<f64>()
225 .map_err(|_| IoError::InvalidNumber {
226 line: line_num,
227 value: parts[9].to_string(),
228 })?;
229 let i13 = parts[10]
230 .parse::<f64>()
231 .map_err(|_| IoError::InvalidNumber {
232 line: line_num,
233 value: parts[10].to_string(),
234 })?;
235 let i23 = parts[11]
236 .parse::<f64>()
237 .map_err(|_| IoError::InvalidNumber {
238 line: line_num,
239 value: parts[11].to_string(),
240 })?;
241
242 let information = nalgebra::Matrix3::new(i11, i12, i13, i12, i22, i23, i13, i23, i33);
243
244 Ok(EdgeSE2::new(from, to, dx, dy, dtheta, information))
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::{EdgeSE3, VertexSE3};
252 use nalgebra::{Matrix3, UnitQuaternion, Vector3};
253 use std::io::Write;
254 use tempfile::NamedTempFile;
255
256 type TestResult = Result<(), Box<dyn std::error::Error>>;
257
258 fn write_toro_content(content: &str) -> Result<NamedTempFile, Box<dyn std::error::Error>> {
259 let mut f = NamedTempFile::new()?;
260 write!(f, "{}", content)?;
261 f.flush()?;
262 Ok(f)
263 }
264
265 #[test]
266 fn test_parse_vertex2_and_edge2() -> TestResult {
267 let content = "VERTEX2 0 1.0 2.0 0.5\n\
268 VERTEX2 1 3.0 4.0 1.0\n\
269 EDGE2 0 1 0.5 0.3 0.1 500.0 0.0 500.0 200.0 0.0 0.0\n";
270 let f = write_toro_content(content)?;
271 let graph = ToroLoader::load(f.path())?;
272 assert_eq!(graph.vertices_se2.len(), 2);
273 assert_eq!(graph.edges_se2.len(), 1);
274 let v0 = &graph.vertices_se2[&0];
275 assert!((v0.x() - 1.0).abs() < 1e-10);
276 assert!((v0.y() - 2.0).abs() < 1e-10);
277 let e = &graph.edges_se2[0];
278 assert_eq!(e.from, 0);
279 assert_eq!(e.to, 1);
280 assert!((e.information[(0, 0)] - 500.0).abs() < 1e-10);
281 Ok(())
282 }
283
284 #[test]
285 fn test_write_and_reload_round_trip() -> TestResult {
286 let mut graph = Graph::new();
287 graph
288 .vertices_se2
289 .insert(0, VertexSE2::new(0, 1.0, 2.0, 0.5));
290 graph
291 .vertices_se2
292 .insert(1, VertexSE2::new(1, 3.0, 4.0, 1.0));
293 let info = Matrix3::new(500.0, 0.0, 0.0, 0.0, 500.0, 0.0, 0.0, 0.0, 200.0);
294 graph
295 .edges_se2
296 .push(EdgeSE2::new(0, 1, 0.5, 0.3, 0.1, info));
297
298 let f = NamedTempFile::new()?;
299 ToroLoader::write(&graph, f.path())?;
300 let loaded = ToroLoader::load(f.path())?;
301
302 assert_eq!(loaded.vertices_se2.len(), 2);
303 assert_eq!(loaded.edges_se2.len(), 1);
304 let v0 = &loaded.vertices_se2[&0];
305 assert!((v0.x() - 1.0).abs() < 1e-10);
306 assert!((v0.y() - 2.0).abs() < 1e-10);
307 let e = &loaded.edges_se2[0];
308 assert_eq!(e.from, 0);
309 assert_eq!(e.to, 1);
310 assert!((e.information[(0, 0)] - 500.0).abs() < 1e-10);
311 Ok(())
312 }
313
314 #[test]
315 fn test_write_rejects_se3_vertices() -> TestResult {
316 let mut graph = Graph::new();
317 graph.vertices_se3.insert(
318 0,
319 VertexSE3::new(0, Vector3::zeros(), UnitQuaternion::identity()),
320 );
321 let f = NamedTempFile::new()?;
322 let result = ToroLoader::write(&graph, f.path());
323 assert!(
324 matches!(result, Err(IoError::UnsupportedFormat(_))),
325 "should reject graph with SE3 vertices"
326 );
327 Ok(())
328 }
329
330 #[test]
331 fn test_write_rejects_se3_edges() -> TestResult {
332 let mut graph = Graph::new();
333 graph.edges_se3.push(EdgeSE3::new(
334 0,
335 1,
336 Vector3::zeros(),
337 UnitQuaternion::identity(),
338 nalgebra::Matrix6::identity(),
339 ));
340 let f = NamedTempFile::new()?;
341 let result = ToroLoader::write(&graph, f.path());
342 assert!(
343 matches!(result, Err(IoError::UnsupportedFormat(_))),
344 "should reject graph with SE3 edges"
345 );
346 Ok(())
347 }
348
349 #[test]
350 fn test_duplicate_vertex_returns_error() -> TestResult {
351 let content = "VERTEX2 5 1.0 2.0 0.0\nVERTEX2 5 3.0 4.0 0.0\n";
352 let f = write_toro_content(content)?;
353 let result = ToroLoader::load(f.path());
354 assert!(
355 matches!(result, Err(IoError::DuplicateVertex { id: 5 })),
356 "duplicate vertex ID should return DuplicateVertex error"
357 );
358 Ok(())
359 }
360
361 #[test]
362 fn test_parse_missing_vertex_fields() -> TestResult {
363 let content = "VERTEX2 0 1.0\n"; let f = write_toro_content(content)?;
366 let result = ToroLoader::load(f.path());
367 assert!(result.is_err(), "VERTEX2 with too few fields should fail");
368 Ok(())
369 }
370
371 #[test]
372 fn test_parse_missing_edge_fields() -> TestResult {
373 let content = "EDGE2 0 1 0.5 0.3\n"; let f = write_toro_content(content)?;
376 let result = ToroLoader::load(f.path());
377 assert!(result.is_err(), "EDGE2 with too few fields should fail");
378 Ok(())
379 }
380
381 #[test]
382 fn test_comment_and_empty_lines_ignored() -> TestResult {
383 let content = "# this is a comment\n\
384 VERTEX2 0 1.0 2.0 0.0\n\
385 \n\
386 VERTEX2 1 2.0 3.0 0.0\n";
387 let f = write_toro_content(content)?;
388 let graph = ToroLoader::load(f.path())?;
389 assert_eq!(
390 graph.vertices_se2.len(),
391 2,
392 "comments and blank lines should be ignored"
393 );
394 Ok(())
395 }
396
397 #[test]
398 fn test_unknown_token_ignored() -> TestResult {
399 let content = "UNKNOWN_TOKEN 1 2 3\nVERTEX2 0 0.0 0.0 0.0\n";
400 let f = write_toro_content(content)?;
401 let graph = ToroLoader::load(f.path())?;
402 assert_eq!(
403 graph.vertices_se2.len(),
404 1,
405 "unknown token lines should be silently skipped"
406 );
407 Ok(())
408 }
409
410 #[test]
411 fn test_load_nonexistent_file() -> TestResult {
412 let result = ToroLoader::load("/no/such/file.graph");
413 assert!(result.is_err(), "loading missing file should return Err");
414 Ok(())
415 }
416
417 #[test]
418 fn test_write_empty_graph() -> TestResult {
419 let graph = Graph::new();
420 let f = NamedTempFile::new()?;
421 ToroLoader::write(&graph, f.path())?;
422 let loaded = ToroLoader::load(f.path())?;
423 assert_eq!(loaded.vertices_se2.len(), 0);
424 assert_eq!(loaded.edges_se2.len(), 0);
425 Ok(())
426 }
427
428 #[test]
429 fn test_parse_vertex2_invalid_number() -> TestResult {
430 let content = "VERTEX2 0 bad 2.0 0.0\n"; let f = write_toro_content(content)?;
432 let result = ToroLoader::load(f.path());
433 assert!(result.is_err(), "invalid number in VERTEX2 should fail");
434 Ok(())
435 }
436
437 #[test]
442 fn test_parse_vertex2_invalid_id() -> TestResult {
443 let content = "VERTEX2 bad 1.0 2.0 0.0\n";
444 let f = write_toro_content(content)?;
445 let result = ToroLoader::load(f.path());
446 assert!(
447 matches!(result, Err(IoError::InvalidNumber { .. })),
448 "invalid id in VERTEX2 should return InvalidNumber"
449 );
450 Ok(())
451 }
452
453 #[test]
454 fn test_parse_vertex2_invalid_y() -> TestResult {
455 let content = "VERTEX2 0 1.0 bad 0.0\n";
456 let f = write_toro_content(content)?;
457 let result = ToroLoader::load(f.path());
458 assert!(
459 matches!(result, Err(IoError::InvalidNumber { .. })),
460 "invalid y in VERTEX2 should return InvalidNumber"
461 );
462 Ok(())
463 }
464
465 #[test]
466 fn test_parse_vertex2_invalid_theta() -> TestResult {
467 let content = "VERTEX2 0 1.0 2.0 bad\n";
468 let f = write_toro_content(content)?;
469 let result = ToroLoader::load(f.path());
470 assert!(
471 matches!(result, Err(IoError::InvalidNumber { .. })),
472 "invalid theta in VERTEX2 should return InvalidNumber"
473 );
474 Ok(())
475 }
476
477 #[test]
482 fn test_parse_edge2_invalid_from() -> TestResult {
483 let content = "EDGE2 bad 1 0.5 0.3 0.1 500.0 0.0 500.0 200.0 0.0 0.0\n";
484 let f = write_toro_content(content)?;
485 let result = ToroLoader::load(f.path());
486 assert!(
487 matches!(result, Err(IoError::InvalidNumber { .. })),
488 "invalid from-id in EDGE2 should return InvalidNumber"
489 );
490 Ok(())
491 }
492
493 #[test]
494 fn test_parse_edge2_invalid_to() -> TestResult {
495 let content = "EDGE2 0 bad 0.5 0.3 0.1 500.0 0.0 500.0 200.0 0.0 0.0\n";
496 let f = write_toro_content(content)?;
497 let result = ToroLoader::load(f.path());
498 assert!(
499 matches!(result, Err(IoError::InvalidNumber { .. })),
500 "invalid to-id in EDGE2 should return InvalidNumber"
501 );
502 Ok(())
503 }
504
505 #[test]
506 fn test_parse_edge2_invalid_dx() -> TestResult {
507 let content = "EDGE2 0 1 bad 0.3 0.1 500.0 0.0 500.0 200.0 0.0 0.0\n";
508 let f = write_toro_content(content)?;
509 let result = ToroLoader::load(f.path());
510 assert!(
511 matches!(result, Err(IoError::InvalidNumber { .. })),
512 "invalid dx in EDGE2 should return InvalidNumber"
513 );
514 Ok(())
515 }
516
517 #[test]
518 fn test_parse_edge2_invalid_dy() -> TestResult {
519 let content = "EDGE2 0 1 0.5 bad 0.1 500.0 0.0 500.0 200.0 0.0 0.0\n";
520 let f = write_toro_content(content)?;
521 let result = ToroLoader::load(f.path());
522 assert!(
523 matches!(result, Err(IoError::InvalidNumber { .. })),
524 "invalid dy in EDGE2 should return InvalidNumber"
525 );
526 Ok(())
527 }
528
529 #[test]
530 fn test_parse_edge2_invalid_dtheta() -> TestResult {
531 let content = "EDGE2 0 1 0.5 0.3 bad 500.0 0.0 500.0 200.0 0.0 0.0\n";
532 let f = write_toro_content(content)?;
533 let result = ToroLoader::load(f.path());
534 assert!(
535 matches!(result, Err(IoError::InvalidNumber { .. })),
536 "invalid dtheta in EDGE2 should return InvalidNumber"
537 );
538 Ok(())
539 }
540
541 #[test]
542 fn test_parse_edge2_invalid_i11() -> TestResult {
543 let content = "EDGE2 0 1 0.5 0.3 0.1 bad 0.0 500.0 200.0 0.0 0.0\n";
544 let f = write_toro_content(content)?;
545 let result = ToroLoader::load(f.path());
546 assert!(
547 matches!(result, Err(IoError::InvalidNumber { .. })),
548 "invalid i11 in EDGE2 should return InvalidNumber"
549 );
550 Ok(())
551 }
552
553 #[test]
554 fn test_parse_edge2_invalid_i12() -> TestResult {
555 let content = "EDGE2 0 1 0.5 0.3 0.1 500.0 bad 500.0 200.0 0.0 0.0\n";
556 let f = write_toro_content(content)?;
557 let result = ToroLoader::load(f.path());
558 assert!(
559 matches!(result, Err(IoError::InvalidNumber { .. })),
560 "invalid i12 in EDGE2 should return InvalidNumber"
561 );
562 Ok(())
563 }
564
565 #[test]
566 fn test_parse_edge2_invalid_i22() -> TestResult {
567 let content = "EDGE2 0 1 0.5 0.3 0.1 500.0 0.0 bad 200.0 0.0 0.0\n";
568 let f = write_toro_content(content)?;
569 let result = ToroLoader::load(f.path());
570 assert!(
571 matches!(result, Err(IoError::InvalidNumber { .. })),
572 "invalid i22 in EDGE2 should return InvalidNumber"
573 );
574 Ok(())
575 }
576
577 #[test]
578 fn test_parse_edge2_invalid_i33() -> TestResult {
579 let content = "EDGE2 0 1 0.5 0.3 0.1 500.0 0.0 500.0 bad 0.0 0.0\n";
580 let f = write_toro_content(content)?;
581 let result = ToroLoader::load(f.path());
582 assert!(
583 matches!(result, Err(IoError::InvalidNumber { .. })),
584 "invalid i33 in EDGE2 should return InvalidNumber"
585 );
586 Ok(())
587 }
588
589 #[test]
590 fn test_parse_edge2_invalid_i13() -> TestResult {
591 let content = "EDGE2 0 1 0.5 0.3 0.1 500.0 0.0 500.0 200.0 bad 0.0\n";
592 let f = write_toro_content(content)?;
593 let result = ToroLoader::load(f.path());
594 assert!(
595 matches!(result, Err(IoError::InvalidNumber { .. })),
596 "invalid i13 in EDGE2 should return InvalidNumber"
597 );
598 Ok(())
599 }
600
601 #[test]
602 fn test_parse_edge2_invalid_i23() -> TestResult {
603 let content = "EDGE2 0 1 0.5 0.3 0.1 500.0 0.0 500.0 200.0 0.0 bad\n";
604 let f = write_toro_content(content)?;
605 let result = ToroLoader::load(f.path());
606 assert!(
607 matches!(result, Err(IoError::InvalidNumber { .. })),
608 "invalid i23 in EDGE2 should return InvalidNumber"
609 );
610 Ok(())
611 }
612
613 #[test]
618 fn test_edge_measurement_all_components_round_trip() -> TestResult {
619 let mut graph = Graph::new();
620 graph
621 .vertices_se2
622 .insert(0, VertexSE2::new(0, 0.0, 0.0, 0.0));
623 graph
624 .vertices_se2
625 .insert(1, VertexSE2::new(1, 1.0, 0.0, 0.0));
626 let info = Matrix3::identity();
627 graph
628 .edges_se2
629 .push(EdgeSE2::new(0, 1, 1.5, 2.5, 0.7, info));
630
631 let f = NamedTempFile::new()?;
632 ToroLoader::write(&graph, f.path())?;
633 let loaded = ToroLoader::load(f.path())?;
634
635 let e = &loaded.edges_se2[0];
636 assert!((e.measurement.x() - 1.5).abs() < 1e-10, "dx mismatch");
637 assert!((e.measurement.y() - 2.5).abs() < 1e-10, "dy mismatch");
638 assert!(
639 (e.measurement.angle() - 0.7).abs() < 1e-10,
640 "dtheta mismatch"
641 );
642 Ok(())
643 }
644
645 #[test]
646 fn test_off_diagonal_info_matrix_round_trip() -> TestResult {
647 let mut graph = Graph::new();
648 graph
649 .vertices_se2
650 .insert(0, VertexSE2::new(0, 0.0, 0.0, 0.0));
651 graph
652 .vertices_se2
653 .insert(1, VertexSE2::new(1, 1.0, 0.0, 0.0));
654 let info = Matrix3::new(500.0, 10.0, 5.0, 10.0, 400.0, 3.0, 5.0, 3.0, 200.0);
656 graph
657 .edges_se2
658 .push(EdgeSE2::new(0, 1, 1.0, 0.0, 0.0, info));
659
660 let f = NamedTempFile::new()?;
661 ToroLoader::write(&graph, f.path())?;
662 let loaded = ToroLoader::load(f.path())?;
663
664 let e = &loaded.edges_se2[0];
665 assert!((e.information[(0, 0)] - 500.0).abs() < 1e-10, "i11");
666 assert!((e.information[(0, 1)] - 10.0).abs() < 1e-10, "i12");
667 assert!((e.information[(1, 1)] - 400.0).abs() < 1e-10, "i22");
668 assert!((e.information[(2, 2)] - 200.0).abs() < 1e-10, "i33");
669 assert!((e.information[(0, 2)] - 5.0).abs() < 1e-10, "i13");
670 assert!((e.information[(1, 2)] - 3.0).abs() < 1e-10, "i23");
671 Ok(())
672 }
673
674 #[test]
675 fn test_multiple_edges_round_trip() -> TestResult {
676 let mut graph = Graph::new();
677 for i in 0..4usize {
678 graph
679 .vertices_se2
680 .insert(i, VertexSE2::new(i, i as f64, 0.0, 0.0));
681 }
682 let info = Matrix3::identity();
683 for i in 0..3usize {
684 graph
685 .edges_se2
686 .push(EdgeSE2::new(i, i + 1, 1.0, 0.0, 0.0, info));
687 }
688
689 let f = NamedTempFile::new()?;
690 ToroLoader::write(&graph, f.path())?;
691 let loaded = ToroLoader::load(f.path())?;
692
693 assert_eq!(loaded.vertices_se2.len(), 4);
694 assert_eq!(loaded.edges_se2.len(), 3);
695 Ok(())
696 }
697
698 #[test]
699 fn test_vertex_theta_preserved_round_trip() -> TestResult {
700 let mut graph = Graph::new();
701 graph
702 .vertices_se2
703 .insert(0, VertexSE2::new(0, 1.0, 2.0, std::f64::consts::PI / 4.0));
704
705 let f = NamedTempFile::new()?;
706 ToroLoader::write(&graph, f.path())?;
707 let loaded = ToroLoader::load(f.path())?;
708
709 let v = &loaded.vertices_se2[&0];
710 assert!(
711 (v.theta() - std::f64::consts::PI / 4.0).abs() < 1e-10,
712 "theta not preserved"
713 );
714 Ok(())
715 }
716
717 #[test]
718 fn test_load_invalid_utf8_returns_err() -> TestResult {
719 let mut f = NamedTempFile::new()?;
720 f.write_all(&[0xFF, 0xFE, 0x80, 0x00, 0xAB])?;
721 let result = ToroLoader::load(f.path());
722 assert!(result.is_err());
723 Ok(())
724 }
725
726 #[test]
727 fn test_write_to_nonexistent_dir_returns_err() -> TestResult {
728 let mut graph = Graph::new();
729 graph
730 .vertices_se2
731 .insert(0, VertexSE2::new(0, 0.0, 0.0, 0.0));
732 let dir = tempfile::tempdir()?;
733 let path = dir.path().join("nested").join("deep").join("output.toro");
734 let result = ToroLoader::write(&graph, &path);
735 assert!(result.is_err());
736 Ok(())
737 }
738}