1use serde::de::Error as DeError;
2use serde::{Deserialize, Deserializer, Serialize};
3use serde_json::Value;
4use std::error::Error;
5use std::process::Command;
6use std::slice::Iter;
7use std::vec::IntoIter;
8
9#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
11pub struct BlockDevices {
12 pub blockdevices: Vec<BlockDevice>,
14}
15
16fn deserialize_mountpoints<'de, D>(deserializer: D) -> Result<Vec<Option<String>>, D::Error>
34where
35 D: Deserializer<'de>,
36{
37 let value = Value::deserialize(deserializer)?;
38 if value.is_array() {
39 serde_json::from_value(value).map_err(DeError::custom)
41 } else {
42 let single: Option<String> = serde_json::from_value(value).map_err(DeError::custom)?;
44 Ok(vec![single])
45 }
46}
47
48#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
63pub struct BlockDevice {
64 pub name: String,
66 #[serde(rename = "maj:min")]
70 pub maj_min: String,
71 pub rm: bool,
73 pub size: String,
75 pub ro: bool,
77 #[serde(rename = "type")]
81 pub device_type: String,
82 #[serde(
86 default,
87 alias = "mountpoint",
88 deserialize_with = "deserialize_mountpoints"
89 )]
90 pub mountpoints: Vec<Option<String>>,
91 #[serde(default)]
93 pub children: Option<Vec<BlockDevice>>,
94}
95
96impl BlockDevice {
97 #[must_use]
99 pub fn has_children(&self) -> bool {
100 self.children.as_ref().is_some_and(|c| !c.is_empty())
101 }
102
103 pub fn children_iter(&self) -> impl Iterator<Item = &BlockDevice> {
107 self.children.iter().flat_map(|c| c.iter())
108 }
109
110 #[must_use]
114 pub fn find_child(&self, name: &str) -> Option<&BlockDevice> {
115 self.children.as_ref()?.iter().find(|c| c.name == name)
116 }
117
118 #[must_use]
120 pub fn active_mountpoints(&self) -> Vec<&str> {
121 self.mountpoints
122 .iter()
123 .filter_map(|m| m.as_deref())
124 .collect()
125 }
126
127 #[must_use]
129 pub fn is_mounted(&self) -> bool {
130 self.mountpoints.iter().any(|m| m.is_some())
131 }
132
133 #[must_use]
136 pub fn is_system(&self) -> bool {
137 if self.mountpoints.iter().any(|m| m.as_deref() == Some("/")) {
138 return true;
139 }
140 if let Some(children) = &self.children {
141 for child in children {
142 if child.is_system() {
143 return true;
144 }
145 }
146 }
147 false
148 }
149
150 #[must_use]
152 pub fn is_disk(&self) -> bool {
153 self.device_type == "disk"
154 }
155
156 #[must_use]
158 pub fn is_partition(&self) -> bool {
159 self.device_type == "part"
160 }
161}
162
163impl BlockDevices {
164 #[must_use]
166 pub fn len(&self) -> usize {
167 self.blockdevices.len()
168 }
169
170 #[must_use]
172 pub fn is_empty(&self) -> bool {
173 self.blockdevices.is_empty()
174 }
175
176 pub fn iter(&self) -> Iter<'_, BlockDevice> {
178 self.blockdevices.iter()
179 }
180
181 #[must_use]
184 pub fn system(&self) -> Vec<&BlockDevice> {
185 self.blockdevices
186 .iter()
187 .filter(|device| device.is_system())
188 .collect()
189 }
190
191 #[must_use]
194 pub fn non_system(&self) -> Vec<&BlockDevice> {
195 self.blockdevices
196 .iter()
197 .filter(|device| !device.is_system())
198 .collect()
199 }
200
201 #[must_use]
205 pub fn find_by_name(&self, name: &str) -> Option<&BlockDevice> {
206 self.blockdevices.iter().find(|d| d.name == name)
207 }
208}
209
210impl IntoIterator for BlockDevices {
211 type Item = BlockDevice;
212 type IntoIter = IntoIter<BlockDevice>;
213
214 fn into_iter(self) -> Self::IntoIter {
215 self.blockdevices.into_iter()
216 }
217}
218
219impl<'a> IntoIterator for &'a BlockDevices {
220 type Item = &'a BlockDevice;
221 type IntoIter = Iter<'a, BlockDevice>;
222
223 fn into_iter(self) -> Self::IntoIter {
224 self.blockdevices.iter()
225 }
226}
227
228pub fn parse_lsblk(json_data: &str) -> Result<BlockDevices, serde_json::Error> {
252 serde_json::from_str(json_data)
253}
254
255pub fn get_devices() -> Result<BlockDevices, Box<dyn Error>> {
270 let output = Command::new("lsblk").arg("--json").output()?;
271
272 if !output.status.success() {
273 return Err(format!("lsblk failed: {}", String::from_utf8_lossy(&output.stderr)).into());
274 }
275
276 let json_output = String::from_utf8(output.stdout)?;
277 let lsblk = parse_lsblk(&json_output)?;
278 Ok(lsblk)
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 const SAMPLE_JSON: &str = r#"
286 {
287 "blockdevices": [
288 {"name":"nvme1n1", "maj:min":"259:0", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
289 "children": [
290 {"name":"nvme1n1p1", "maj:min":"259:1", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
291 {"name":"nvme1n1p9", "maj:min":"259:2", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
292 ]
293 },
294 {"name":"nvme7n1", "maj:min":"259:3", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
295 "children": [
296 {"name":"nvme7n1p1", "maj:min":"259:7", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
297 {"name":"nvme7n1p9", "maj:min":"259:8", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
298 ]
299 },
300 {"name":"nvme5n1", "maj:min":"259:4", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
301 "children": [
302 {"name":"nvme5n1p1", "maj:min":"259:5", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
303 {"name":"nvme5n1p9", "maj:min":"259:6", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
304 ]
305 },
306 {"name":"nvme9n1", "maj:min":"259:9", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
307 "children": [
308 {"name":"nvme9n1p1", "maj:min":"259:13", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
309 {"name":"nvme9n1p9", "maj:min":"259:14", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
310 ]
311 },
312 {"name":"nvme4n1", "maj:min":"259:10", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
313 "children": [
314 {"name":"nvme4n1p1", "maj:min":"259:11", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
315 {"name":"nvme4n1p9", "maj:min":"259:12", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
316 ]
317 },
318 {"name":"nvme8n1", "maj:min":"259:15", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
319 "children": [
320 {"name":"nvme8n1p1", "maj:min":"259:20", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
321 {"name":"nvme8n1p9", "maj:min":"259:21", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
322 ]
323 },
324 {"name":"nvme6n1", "maj:min":"259:16", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
325 "children": [
326 {"name":"nvme6n1p1", "maj:min":"259:17", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
327 {"name":"nvme6n1p9", "maj:min":"259:18", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
328 ]
329 },
330 {"name":"nvme3n1", "maj:min":"259:19", "rm":false, "size":"894.3G", "ro":false, "type":"disk", "mountpoint":null,
331 "children": [
332 {"name":"nvme3n1p1", "maj:min":"259:23", "rm":false, "size":"1M", "ro":false, "type":"part", "mountpoint":null},
333 {"name":"nvme3n1p2", "maj:min":"259:24", "rm":false, "size":"244M", "ro":false, "type":"part", "mountpoint":"/boot/efi"},
334 {"name":"nvme3n1p3", "maj:min":"259:25", "rm":false, "size":"488M", "ro":false, "type":"part", "mountpoint":null,
335 "children": [
336 {"name":"md0", "maj:min":"9:0", "rm":false, "size":"487M", "ro":false, "type":"raid1", "mountpoint":"/boot"}
337 ]
338 },
339 {"name":"nvme3n1p4", "maj:min":"259:26", "rm":false, "size":"7.6G", "ro":false, "type":"part", "mountpoint":null,
340 "children": [
341 {"name":"md1", "maj:min":"9:1", "rm":false, "size":"7.6G", "ro":false, "type":"raid1", "mountpoint":"[SWAP]"}
342 ]
343 },
344 {"name":"nvme3n1p5", "maj:min":"259:27", "rm":false, "size":"19.1G", "ro":false, "type":"part", "mountpoint":null,
345 "children": [
346 {"name":"md2", "maj:min":"9:2", "rm":false, "size":"19.1G", "ro":false, "type":"raid1", "mountpoint":"/"}
347 ]
348 },
349 {"name":"nvme3n1p6", "maj:min":"259:28", "rm":false, "size":"866.8G", "ro":false, "type":"part", "mountpoint":null}
350 ]
351 },
352 {"name":"nvme0n1", "maj:min":"259:22", "rm":false, "size":"3.5T", "ro":false, "type":"disk", "mountpoint":null,
353 "children": [
354 {"name":"nvme0n1p1", "maj:min":"259:29", "rm":false, "size":"3.5T", "ro":false, "type":"part", "mountpoint":null},
355 {"name":"nvme0n1p9", "maj:min":"259:30", "rm":false, "size":"8M", "ro":false, "type":"part", "mountpoint":null}
356 ]
357 },
358 {"name":"nvme2n1", "maj:min":"259:31", "rm":false, "size":"894.3G", "ro":false, "type":"disk", "mountpoint":null,
359 "children": [
360 {"name":"nvme2n1p1", "maj:min":"259:32", "rm":false, "size":"1M", "ro":false, "type":"part", "mountpoint":null},
361 {"name":"nvme2n1p2", "maj:min":"259:33", "rm":false, "size":"244M", "ro":false, "type":"part", "mountpoint":null},
362 {"name":"nvme2n1p3", "maj:min":"259:34", "rm":false, "size":"488M", "ro":false, "type":"part", "mountpoint":null,
363 "children": [
364 {"name":"md0", "maj:min":"9:0", "rm":false, "size":"487M", "ro":false, "type":"raid1", "mountpoint":"/boot"}
365 ]
366 },
367 {"name":"nvme2n1p4", "maj:min":"259:35", "rm":false, "size":"7.6G", "ro":false, "type":"part", "mountpoint":null,
368 "children": [
369 {"name":"md1", "maj:min":"9:1", "rm":false, "size":"7.6G", "ro":false, "type":"raid1", "mountpoint":"[SWAP]"}
370 ]
371 },
372 {"name":"nvme2n1p5", "maj:min":"259:36", "rm":false, "size":"19.1G", "ro":false, "type":"part", "mountpoint":null,
373 "children": [
374 {"name":"md2", "maj:min":"9:2", "rm":false, "size":"19.1G", "ro":false, "type":"raid1", "mountpoint":"/"}
375 ]
376 },
377 {"name":"nvme2n1p6", "maj:min":"259:37", "rm":false, "size":"866.8G", "ro":false, "type":"part", "mountpoint":null}
378 ]
379 }
380 ]
381 }
382 "#;
383
384 #[test]
385 fn test_parse_lsblk() {
386 let lsblk = parse_lsblk(SAMPLE_JSON).expect("Failed to parse JSON");
387
388 assert_eq!(
390 lsblk.blockdevices.len(),
391 10,
392 "Expected 10 top-level block devices"
393 );
394
395 for device in &lsblk.blockdevices {
397 assert!(!device.name.is_empty(), "Device name should not be empty");
398 assert!(
399 !device.maj_min.is_empty(),
400 "Device maj:min should not be empty"
401 );
402 }
403
404 let nvme3n1 = lsblk
406 .blockdevices
407 .iter()
408 .find(|d| d.name == "nvme3n1")
409 .expect("Expected to find device nvme3n1");
410
411 assert!(
413 nvme3n1
414 .mountpoints
415 .first()
416 .and_then(|opt| opt.as_deref())
417 .is_none(),
418 "nvme3n1 effective mountpoint should be None"
419 );
420
421 let children = nvme3n1
423 .children
424 .as_ref()
425 .expect("nvme3n1 should have children");
426 assert_eq!(children.len(), 6, "nvme3n1 should have 6 children");
427
428 let nvme3n1p2 = children
430 .iter()
431 .find(|c| c.name == "nvme3n1p2")
432 .expect("Expected to find nvme3n1p2");
433 assert_eq!(
434 nvme3n1p2.mountpoints.first().and_then(|opt| opt.as_deref()),
435 Some("/boot/efi"),
436 "nvme3n1p2 first mountpoint should be '/boot/efi'"
437 );
438
439 let nvme3n1p3 = children
441 .iter()
442 .find(|c| c.name == "nvme3n1p3")
443 .expect("Expected to find nvme3n1p3");
444 let nested_children = nvme3n1p3
445 .children
446 .as_ref()
447 .expect("nvme3n1p3 should have children");
448 let md0 = nested_children
449 .iter()
450 .find(|d| d.name == "md0")
451 .expect("Expected to find md0 under nvme3n1p3");
452 assert_eq!(
453 md0.mountpoints.first().and_then(|opt| opt.as_deref()),
454 Some("/boot"),
455 "md0 effective mountpoint should be '/boot'"
456 );
457
458 let non_system = lsblk.non_system();
461 assert_eq!(
462 non_system.len(),
463 8,
464 "Expected 8 non-system top-level devices, since nvme3n1/nvme2n1 is system"
465 );
466 assert!(
467 !non_system.iter().any(|d| d.name == "nvme3n1"),
468 "nvme3n1 should be excluded from non-system devices"
469 );
470 }
471
472 #[test]
473 fn test_non_system() {
474 let test_json = r#"
477 {
478 "blockdevices": [
479 {
480 "name": "sda",
481 "maj:min": "8:0",
482 "rm": false,
483 "size": "447.1G",
484 "ro": false,
485 "type": "disk",
486 "mountpoints": [
487 null
488 ],
489 "children": [
490 {
491 "name": "sda1",
492 "maj:min": "8:1",
493 "rm": false,
494 "size": "512M",
495 "ro": false,
496 "type": "part",
497 "mountpoints": [
498 null
499 ]
500 },{
501 "name": "sda2",
502 "maj:min": "8:2",
503 "rm": false,
504 "size": "446.6G",
505 "ro": false,
506 "type": "part",
507 "mountpoints": [
508 null
509 ],
510 "children": [
511 {
512 "name": "md0",
513 "maj:min": "9:0",
514 "rm": false,
515 "size": "446.6G",
516 "ro": false,
517 "type": "raid1",
518 "mountpoints": [
519 "/"
520 ]
521 }
522 ]
523 }
524 ]
525 },{
526 "name": "sdb",
527 "maj:min": "8:16",
528 "rm": false,
529 "size": "447.1G",
530 "ro": false,
531 "type": "disk",
532 "mountpoints": [
533 null
534 ],
535 "children": [
536 {
537 "name": "sdb1",
538 "maj:min": "8:17",
539 "rm": false,
540 "size": "512M",
541 "ro": false,
542 "type": "part",
543 "mountpoints": [
544 "/boot/efi"
545 ]
546 },{
547 "name": "sdb2",
548 "maj:min": "8:18",
549 "rm": false,
550 "size": "446.6G",
551 "ro": false,
552 "type": "part",
553 "mountpoints": [
554 null
555 ],
556 "children": [
557 {
558 "name": "md0",
559 "maj:min": "9:0",
560 "rm": false,
561 "size": "446.6G",
562 "ro": false,
563 "type": "raid1",
564 "mountpoints": [
565 "/"
566 ]
567 }
568 ]
569 }
570 ]
571 },{
572 "name": "nvme0n1",
573 "maj:min": "259:2",
574 "rm": false,
575 "size": "1.7T",
576 "ro": false,
577 "type": "disk",
578 "mountpoints": [
579 null
580 ]
581 },{
582 "name": "nvme1n1",
583 "maj:min": "259:3",
584 "rm": false,
585 "size": "1.7T",
586 "ro": false,
587 "type": "disk",
588 "mountpoints": [
589 null
590 ]
591 }
592 ]
593 }
594 "#;
595 let disks = parse_lsblk(test_json).unwrap();
596 let non_system = disks.non_system();
597 assert_eq!(non_system.len(), 2);
598 let names: Vec<&str> = non_system.iter().map(|d| d.name.as_str()).collect();
599 assert_eq!(names, vec!["nvme0n1", "nvme1n1"]);
600 }
601
602 #[test]
605 #[ignore = "requires lsblk command to be available on the system"]
606 fn test_get_devices() {
607 let dev = get_devices().expect("Failed to get block devices");
608 assert!(!dev.blockdevices.is_empty());
610 }
611 #[test]
612 fn test_into_iterator() {
613 let device1 = BlockDevice {
615 name: "sda".to_string(),
616 maj_min: "8:0".to_string(),
617 rm: false,
618 size: "500G".to_string(),
619 ro: false,
620 device_type: "disk".to_string(),
621 mountpoints: vec![None],
622 children: None,
623 };
624
625 let device2 = BlockDevice {
626 name: "sdb".to_string(),
627 maj_min: "8:16".to_string(),
628 rm: false,
629 size: "500G".to_string(),
630 ro: false,
631 device_type: "disk".to_string(),
632 mountpoints: vec![None],
633 children: None,
634 };
635
636 let devices = BlockDevices {
638 blockdevices: vec![device1, device2],
639 };
640
641 let names: Vec<String> = devices.into_iter().map(|dev| dev.name).collect();
643 assert_eq!(names, vec!["sda".to_string(), "sdb".to_string()]);
644 }
645
646 #[test]
647 fn test_empty_blockdevices() {
648 let json = r#"{"blockdevices": []}"#;
649 let devices = parse_lsblk(json).expect("Failed to parse empty JSON");
650 assert!(devices.is_empty());
651 assert_eq!(devices.len(), 0);
652 assert!(devices.non_system().is_empty());
653 assert!(devices.system().is_empty());
654 assert!(devices.find_by_name("sda").is_none());
655 }
656
657 #[test]
658 fn test_default_trait() {
659 let devices = BlockDevices::default();
660 assert!(devices.is_empty());
661 assert_eq!(devices.len(), 0);
662 }
663
664 #[test]
665 fn test_clone_trait() {
666 let json = r#"{"blockdevices": [{"name": "sda", "maj:min": "8:0", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": [null]}]}"#;
667 let devices = parse_lsblk(json).expect("Failed to parse JSON");
668 let cloned = devices.clone();
669 assert_eq!(devices, cloned);
670 assert_eq!(cloned.len(), 1);
671 }
672
673 #[test]
674 fn test_serialization_roundtrip() {
675 let json = r#"{"blockdevices":[{"name":"sda","maj:min":"8:0","rm":false,"size":"500G","ro":false,"type":"disk","mountpoints":[null],"children":null}]}"#;
676 let devices = parse_lsblk(json).expect("Failed to parse JSON");
677 let serialized = serde_json::to_string(&devices).expect("Failed to serialize");
678 let deserialized: BlockDevices =
679 serde_json::from_str(&serialized).expect("Failed to deserialize");
680 assert_eq!(devices, deserialized);
681 }
682
683 #[test]
684 fn test_device_with_direct_root_mount() {
685 let json = r#"{
686 "blockdevices": [{
687 "name": "sda",
688 "maj:min": "8:0",
689 "rm": false,
690 "size": "500G",
691 "ro": false,
692 "type": "disk",
693 "mountpoints": ["/"]
694 }]
695 }"#;
696 let devices = parse_lsblk(json).expect("Failed to parse JSON");
697 let device = devices.find_by_name("sda").unwrap();
698 assert!(device.is_system());
699 assert!(device.is_mounted());
700 assert_eq!(device.active_mountpoints(), vec!["/"]);
701 assert_eq!(devices.system().len(), 1);
702 assert!(devices.non_system().is_empty());
703 }
704
705 #[test]
706 fn test_block_device_methods() {
707 let device = BlockDevice {
708 name: "sda".to_string(),
709 maj_min: "8:0".to_string(),
710 rm: false,
711 size: "500G".to_string(),
712 ro: false,
713 device_type: "disk".to_string(),
714 mountpoints: vec![Some("/mnt/data".to_string()), None],
715 children: Some(vec![BlockDevice {
716 name: "sda1".to_string(),
717 maj_min: "8:1".to_string(),
718 rm: false,
719 size: "250G".to_string(),
720 ro: false,
721 device_type: "part".to_string(),
722 mountpoints: vec![Some("/home".to_string())],
723 children: None,
724 }]),
725 };
726
727 assert!(device.is_disk());
728 assert!(!device.is_partition());
729 assert!(device.has_children());
730 assert!(device.is_mounted());
731 assert_eq!(device.active_mountpoints(), vec!["/mnt/data"]);
732
733 let child = device.find_child("sda1").unwrap();
734 assert!(!child.is_disk());
735 assert!(child.is_partition());
736 assert!(!child.has_children());
737
738 assert!(device.find_child("nonexistent").is_none());
739 }
740
741 #[test]
742 fn test_children_iter() {
743 let device = BlockDevice {
744 name: "sda".to_string(),
745 maj_min: "8:0".to_string(),
746 rm: false,
747 size: "500G".to_string(),
748 ro: false,
749 device_type: "disk".to_string(),
750 mountpoints: vec![None],
751 children: Some(vec![
752 BlockDevice {
753 name: "sda1".to_string(),
754 maj_min: "8:1".to_string(),
755 rm: false,
756 size: "250G".to_string(),
757 ro: false,
758 device_type: "part".to_string(),
759 mountpoints: vec![None],
760 children: None,
761 },
762 BlockDevice {
763 name: "sda2".to_string(),
764 maj_min: "8:2".to_string(),
765 rm: false,
766 size: "250G".to_string(),
767 ro: false,
768 device_type: "part".to_string(),
769 mountpoints: vec![None],
770 children: None,
771 },
772 ]),
773 };
774
775 let names: Vec<&str> = device.children_iter().map(|c| c.name.as_str()).collect();
776 assert_eq!(names, vec!["sda1", "sda2"]);
777
778 let device_no_children = BlockDevice {
780 name: "sdb".to_string(),
781 maj_min: "8:16".to_string(),
782 rm: false,
783 size: "500G".to_string(),
784 ro: false,
785 device_type: "disk".to_string(),
786 mountpoints: vec![None],
787 children: None,
788 };
789 assert_eq!(device_no_children.children_iter().count(), 0);
790 }
791
792 #[test]
793 fn test_borrowing_iterator() {
794 let devices = BlockDevices {
795 blockdevices: vec![
796 BlockDevice {
797 name: "sda".to_string(),
798 maj_min: "8:0".to_string(),
799 rm: false,
800 size: "500G".to_string(),
801 ro: false,
802 device_type: "disk".to_string(),
803 mountpoints: vec![None],
804 children: None,
805 },
806 BlockDevice {
807 name: "sdb".to_string(),
808 maj_min: "8:16".to_string(),
809 rm: false,
810 size: "500G".to_string(),
811 ro: false,
812 device_type: "disk".to_string(),
813 mountpoints: vec![None],
814 children: None,
815 },
816 ],
817 };
818
819 let names: Vec<&str> = (&devices).into_iter().map(|d| d.name.as_str()).collect();
821 assert_eq!(names, vec!["sda", "sdb"]);
822
823 assert_eq!(devices.len(), 2);
825
826 let names2: Vec<&str> = devices.iter().map(|d| d.name.as_str()).collect();
828 assert_eq!(names2, vec!["sda", "sdb"]);
829 }
830
831 #[test]
832 fn test_find_by_name() {
833 let devices = BlockDevices {
834 blockdevices: vec![
835 BlockDevice {
836 name: "sda".to_string(),
837 maj_min: "8:0".to_string(),
838 rm: false,
839 size: "500G".to_string(),
840 ro: false,
841 device_type: "disk".to_string(),
842 mountpoints: vec![None],
843 children: None,
844 },
845 BlockDevice {
846 name: "nvme0n1".to_string(),
847 maj_min: "259:0".to_string(),
848 rm: false,
849 size: "1T".to_string(),
850 ro: false,
851 device_type: "disk".to_string(),
852 mountpoints: vec![None],
853 children: None,
854 },
855 ],
856 };
857
858 assert!(devices.find_by_name("sda").is_some());
859 assert_eq!(devices.find_by_name("sda").unwrap().size, "500G");
860 assert!(devices.find_by_name("nvme0n1").is_some());
861 assert!(devices.find_by_name("nonexistent").is_none());
862 }
863
864 #[test]
865 fn test_system_method() {
866 let json = r#"{
867 "blockdevices": [
868 {"name": "sda", "maj:min": "8:0", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": ["/"]},
869 {"name": "sdb", "maj:min": "8:16", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": [null]},
870 {"name": "sdc", "maj:min": "8:32", "rm": false, "size": "500G", "ro": false, "type": "disk", "mountpoints": ["/home"]}
871 ]
872 }"#;
873 let devices = parse_lsblk(json).expect("Failed to parse JSON");
874 let system = devices.system();
875 assert_eq!(system.len(), 1);
876 assert_eq!(system[0].name, "sda");
877 }
878
879 #[test]
880 fn test_multiple_mountpoints() {
881 let json = r#"{
882 "blockdevices": [{
883 "name": "sda",
884 "maj:min": "8:0",
885 "rm": false,
886 "size": "500G",
887 "ro": false,
888 "type": "disk",
889 "mountpoints": ["/mnt/data", "/mnt/backup", null]
890 }]
891 }"#;
892 let devices = parse_lsblk(json).expect("Failed to parse JSON");
893 let device = devices.find_by_name("sda").unwrap();
894 assert!(device.is_mounted());
895 assert_eq!(
896 device.active_mountpoints(),
897 vec!["/mnt/data", "/mnt/backup"]
898 );
899 }
900
901 #[test]
902 fn test_removable_and_readonly() {
903 let json = r#"{
904 "blockdevices": [{
905 "name": "sr0",
906 "maj:min": "11:0",
907 "rm": true,
908 "size": "4.7G",
909 "ro": true,
910 "type": "rom",
911 "mountpoints": [null]
912 }]
913 }"#;
914 let devices = parse_lsblk(json).expect("Failed to parse JSON");
915 let device = devices.find_by_name("sr0").unwrap();
916 assert!(device.rm);
917 assert!(device.ro);
918 assert_eq!(device.device_type, "rom");
919 }
920}