1#![allow(dead_code)]
5use anyhow::{Context, Result};
6use byte_unit::Byte;
7use chrono::Local;
8use colored::*;
9use indicatif::{ProgressBar, ProgressStyle};
11use serde_json::Value;
12use std::collections::HashMap;
13use std::fmt::Write as FmtWrite;
14use std::path::{Path, PathBuf};
15use std::time::{Duration, Instant};
16use sysinfo::System;
17use tracing::{debug, info};
18
19pub fn display_banner() {
21 let banner = r#"
22 ______ _____ _____ _
23 |__ _| | __ \ / ____| |
24 | | ___ _ _| |__) | (___ | |__
25 | |/ _ \| '__| _ / \___ \| '_ \
26 _| | (_) | | | | \ \ ____) | | | |
27 |_| \___/|_| |_| \_\_____/|_| |_|
28
29"#;
30
31 println!("{}", banner.bright_cyan().bold());
32 println!(
33 "{}",
34 "ToRSh CLI - Advanced Deep Learning Framework Tools"
35 .bright_white()
36 .bold()
37 );
38 println!(
39 "{}",
40 format!("Version: {} | Build: {}", env!("CARGO_PKG_VERSION"), "dev").bright_black()
41 );
42 println!();
43}
44
45pub mod output {
47 use super::*;
48 use serde::Serialize;
49
50 pub fn format_output<T: Serialize>(data: &T, format: &str) -> Result<String> {
52 match format {
53 "json" => {
54 serde_json::to_string_pretty(data).with_context(|| "Failed to serialize to JSON")
55 }
56 "yaml" => serde_norway::to_string(data).with_context(|| "Failed to serialize to YAML"),
57 "table" => {
58 format_as_table(data)
61 }
62 _ => {
63 anyhow::bail!("Unsupported output format: {}", format)
64 }
65 }
66 }
67
68 fn format_as_table<T: Serialize>(data: &T) -> Result<String> {
70 let json_value = serde_json::to_value(data)?;
71 format_json_as_table(&json_value, 0)
72 }
73
74 fn format_json_as_table(value: &Value, indent: usize) -> Result<String> {
75 let mut output = String::new();
76 let indent_str = " ".repeat(indent);
77
78 match value {
79 Value::Object(map) => {
80 for (key, val) in map {
81 match val {
82 Value::Object(_) | Value::Array(_) => {
83 writeln!(output, "{}{}:", indent_str, key.bright_cyan())?;
84 output.push_str(&format_json_as_table(val, indent + 1)?);
85 }
86 _ => {
87 writeln!(
88 output,
89 "{}{}: {}",
90 indent_str,
91 key.bright_cyan(),
92 format_json_value(val)
93 )?;
94 }
95 }
96 }
97 }
98 Value::Array(arr) => {
99 for (i, val) in arr.iter().enumerate() {
100 writeln!(output, "{}[{}]:", indent_str, i.to_string().bright_yellow())?;
101 output.push_str(&format_json_as_table(val, indent + 1)?);
102 }
103 }
104 _ => {
105 writeln!(output, "{}{}", indent_str, format_json_value(value))?;
106 }
107 }
108
109 Ok(output)
110 }
111
112 fn format_json_value(value: &Value) -> String {
113 match value {
114 Value::String(s) => s.green().to_string(),
115 Value::Number(n) => n.to_string().yellow().to_string(),
116 Value::Bool(b) => {
117 if *b {
118 "true".bright_green().to_string()
119 } else {
120 "false".bright_red().to_string()
121 }
122 }
123 Value::Null => "null".bright_black().to_string(),
124 _ => value.to_string(),
125 }
126 }
127
128 pub fn print_table<T: Serialize>(title: &str, data: &T, format: &str) -> Result<()> {
130 println!("{}", title.bright_cyan().bold());
131 println!("{}", "=".repeat(title.len()).bright_cyan());
132 println!();
133
134 let formatted = format_output(data, format)?;
135 println!("{}", formatted);
136
137 Ok(())
138 }
139
140 pub fn print_success(message: &str) {
142 println!("{} {}", "✓".bright_green().bold(), message);
143 }
144
145 pub fn print_error(message: &str) {
147 eprintln!("{} {}", "✗".bright_red().bold(), message);
148 }
149
150 pub fn print_warning(message: &str) {
152 println!("{} {}", "⚠".bright_yellow().bold(), message);
153 }
154
155 pub fn print_info(message: &str) {
157 println!("{} {}", "ℹ".bright_blue().bold(), message);
158 }
159}
160
161pub mod progress {
163 use super::*;
164
165 pub fn create_progress_bar(len: u64, message: &str) -> ProgressBar {
167 let pb = ProgressBar::new(len);
168 pb.set_style(
169 ProgressStyle::default_bar()
170 .template("{msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos:>7}/{len:7} {eta}")
171 .expect("Invalid progress bar template")
172 .progress_chars("█▉▊▋▌▍▎▏ "),
173 );
174 pb.set_message(message.to_string());
175 pb
176 }
177
178 pub fn create_spinner(message: &str) -> ProgressBar {
180 let pb = ProgressBar::new_spinner();
181 pb.set_style(
182 ProgressStyle::default_spinner()
183 .template("{spinner:.cyan} {msg}")
184 .expect("Invalid spinner template")
185 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ "),
186 );
187 pb.set_message(message.to_string());
188 pb
189 }
190}
191
192pub mod fs {
194 use super::*;
195
196 pub fn format_file_size(size: u64) -> String {
198 Byte::from_u128(size as u128)
199 .unwrap_or_else(|| Byte::from_u128(0).expect("zero bytes should always be valid"))
200 .get_appropriate_unit(byte_unit::UnitType::Binary)
201 .to_string()
202 }
203
204 pub fn get_directory_size(
206 path: &Path,
207 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<u64>> + Send + '_>> {
208 Box::pin(async move {
209 let mut total_size = 0u64;
210 let mut read_dir = tokio::fs::read_dir(path).await?;
211
212 while let Some(entry) = read_dir.next_entry().await? {
213 let metadata = entry.metadata().await?;
214 if metadata.is_file() {
215 total_size += metadata.len();
216 } else if metadata.is_dir() {
217 total_size += get_directory_size(&entry.path()).await?;
218 }
219 }
220
221 Ok(total_size)
222 })
223 }
224
225 pub fn find_files(directory: &Path, pattern: &str) -> Result<Vec<PathBuf>> {
227 let mut files = Vec::new();
228 let walker = walkdir::WalkDir::new(directory);
229
230 for entry in walker {
231 let entry = entry?;
232 if entry.file_type().is_file() {
233 let path = entry.path();
234 if glob::Pattern::new(pattern)?.matches_path(path) {
235 files.push(path.to_path_buf());
236 }
237 }
238 }
239
240 Ok(files)
241 }
242
243 pub async fn backup_file(file_path: &Path) -> Result<PathBuf> {
245 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
246 let backup_path = file_path.with_extension(format!(
247 "{}.backup_{}",
248 file_path.extension().unwrap_or_default().to_string_lossy(),
249 timestamp
250 ));
251
252 tokio::fs::copy(file_path, &backup_path).await?;
253 info!("Created backup: {}", backup_path.display());
254
255 Ok(backup_path)
256 }
257
258 pub async fn cleanup_temp_files(temp_dir: &Path) -> Result<()> {
260 if temp_dir.exists() {
261 tokio::fs::remove_dir_all(temp_dir).await?;
262 debug!("Cleaned up temporary directory: {}", temp_dir.display());
263 }
264 Ok(())
265 }
266}
267
268pub mod system {
270 use super::*;
271
272 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
274 pub struct SystemInfo {
275 pub os: String,
276 pub kernel_version: String,
277 pub total_memory: String,
278 pub available_memory: String,
279 pub cpu_count: usize,
280 pub cpu_brand: String,
281 pub cpu_frequency: u64,
282 pub load_average: Vec<f64>,
283 pub uptime: String,
284 }
285
286 pub fn get_system_info() -> SystemInfo {
288 let mut sys = System::new_all();
289 sys.refresh_all();
290
291 SystemInfo {
292 os: format!(
293 "{} {}",
294 System::name().unwrap_or_default(),
295 System::os_version().unwrap_or_default()
296 ),
297 kernel_version: System::kernel_version().unwrap_or_default(),
298 total_memory: format_memory(sys.total_memory()),
299 available_memory: format_memory(sys.available_memory()),
300 cpu_count: sys.cpus().len(),
301 cpu_brand: sys
302 .cpus()
303 .first()
304 .map(|cpu| cpu.brand())
305 .unwrap_or("Unknown")
306 .to_string(),
307 cpu_frequency: sys.cpus().first().map(|cpu| cpu.frequency()).unwrap_or(0),
308 load_average: {
309 let load = System::load_average();
310 vec![load.one, load.five, load.fifteen]
311 },
312 uptime: format_duration(Duration::from_secs(System::uptime())),
313 }
314 }
315
316 fn format_memory(memory_bytes: u64) -> String {
322 Byte::from_u128(memory_bytes as u128)
323 .unwrap_or_else(|| Byte::from_u128(0).expect("zero bytes should always be valid"))
324 .get_appropriate_unit(byte_unit::UnitType::Binary)
325 .to_string()
326 }
327
328 pub fn check_gpu_availability() -> HashMap<String, bool> {
330 let mut gpu_info = HashMap::new();
331
332 {
339 gpu_info.insert("CUDA".to_string(), detect_cuda_runtime());
341 }
342
343 {
350 gpu_info.insert("ROCm".to_string(), detect_rocm_runtime());
351 }
352
353 #[cfg(target_os = "macos")]
355 {
356 gpu_info.insert("Metal".to_string(), detect_metal_availability());
357 }
358
359 gpu_info.insert("Vulkan".to_string(), detect_vulkan_availability());
361
362 gpu_info.insert("OpenCL".to_string(), detect_opencl_availability());
364
365 gpu_info
366 }
367
368 #[allow(dead_code)]
371 fn detect_cuda_availability() -> bool {
372 detect_cuda_runtime()
375 }
376
377 fn detect_cuda_runtime() -> bool {
378 std::process::Command::new("nvidia-smi")
380 .arg("--query-gpu=name")
381 .arg("--format=csv,noheader")
382 .output()
383 .map(|output| output.status.success())
384 .unwrap_or(false)
385 }
386
387 #[allow(dead_code)]
390 fn detect_rocm_availability() -> bool {
391 detect_rocm_runtime()
392 }
393
394 fn detect_rocm_runtime() -> bool {
395 std::process::Command::new("rocm-smi")
397 .arg("--showproductname")
398 .output()
399 .map(|output| output.status.success())
400 .unwrap_or(false)
401 }
402
403 #[cfg(target_os = "macos")]
405 fn detect_metal_availability() -> bool {
406 std::process::Command::new("system_profiler")
408 .arg("SPDisplaysDataType")
409 .output()
410 .map(|output| {
411 output.status.success() && String::from_utf8_lossy(&output.stdout).contains("Metal")
412 })
413 .unwrap_or(true) }
415
416 fn detect_vulkan_availability() -> bool {
417 std::process::Command::new("vulkaninfo")
419 .arg("--summary")
420 .output()
421 .map(|output| output.status.success())
422 .unwrap_or(false)
423 }
424
425 fn detect_opencl_availability() -> bool {
426 std::process::Command::new("clinfo")
428 .output()
429 .map(|output| output.status.success())
430 .unwrap_or(false)
431 }
432
433 pub fn get_device_info() -> HashMap<String, serde_json::Value> {
435 let mut device_info = HashMap::new();
436
437 let sys_info = get_system_info();
439
440 device_info.insert(
442 "cpu".to_string(),
443 serde_json::json!({
444 "available": true,
445 "device_type": "cpu",
446 "description": "CPU device",
447 "brand": sys_info.cpu_brand,
448 "cores": sys_info.cpu_count,
449 "frequency_mhz": sys_info.cpu_frequency,
450 "capabilities": get_cpu_capabilities(),
451 }),
452 );
453
454 let gpu_availability = check_gpu_availability();
456 for (gpu_type, available) in gpu_availability {
457 let detailed_info = if available {
458 match gpu_type.as_str() {
459 "CUDA" => get_cuda_device_details(),
460 "ROCm" => get_rocm_device_details(),
461 "Metal" => get_metal_device_details(),
462 "Vulkan" => get_vulkan_device_details(),
463 "OpenCL" => get_opencl_device_details(),
464 _ => serde_json::json!({}),
465 }
466 } else {
467 serde_json::json!({
468 "reason": "Runtime or drivers not detected"
469 })
470 };
471
472 device_info.insert(
473 gpu_type.to_lowercase(),
474 serde_json::json!({
475 "available": available,
476 "device_type": "gpu",
477 "description": format!("{} GPU device", gpu_type),
478 "details": detailed_info
479 }),
480 );
481 }
482
483 device_info
484 }
485
486 fn get_cpu_capabilities() -> Vec<String> {
488 let mut capabilities = Vec::new();
489
490 #[cfg(target_arch = "x86_64")]
492 {
493 if is_x86_feature_detected!("sse") {
494 capabilities.push("SSE".to_string());
495 }
496 if is_x86_feature_detected!("sse2") {
497 capabilities.push("SSE2".to_string());
498 }
499 if is_x86_feature_detected!("sse3") {
500 capabilities.push("SSE3".to_string());
501 }
502 if is_x86_feature_detected!("sse4.1") {
503 capabilities.push("SSE4.1".to_string());
504 }
505 if is_x86_feature_detected!("sse4.2") {
506 capabilities.push("SSE4.2".to_string());
507 }
508 if is_x86_feature_detected!("avx") {
509 capabilities.push("AVX".to_string());
510 }
511 if is_x86_feature_detected!("avx2") {
512 capabilities.push("AVX2".to_string());
513 }
514 if is_x86_feature_detected!("fma") {
515 capabilities.push("FMA".to_string());
516 }
517 }
518
519 #[cfg(target_arch = "aarch64")]
520 {
521 if std::arch::is_aarch64_feature_detected!("neon") {
522 capabilities.push("NEON".to_string());
523 }
524 }
525
526 capabilities
527 }
528
529 fn get_cuda_device_details() -> serde_json::Value {
531 if let Ok(output) = std::process::Command::new("nvidia-smi")
533 .arg("--query-gpu=name,memory.total,driver_version,cuda_version")
534 .arg("--format=csv,noheader,nounits")
535 .output()
536 {
537 if output.status.success() {
538 let info = String::from_utf8_lossy(&output.stdout);
539 let lines: Vec<&str> = info.trim().split('\n').collect();
540
541 return serde_json::json!({
542 "devices": lines.iter().enumerate().map(|(i, line)| {
543 let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
544 if parts.len() >= 4 {
545 serde_json::json!({
546 "id": i,
547 "name": parts[0],
548 "memory_mb": parts[1],
549 "driver_version": parts[2],
550 "cuda_version": parts[3]
551 })
552 } else {
553 serde_json::json!({
554 "id": i,
555 "name": "Unknown GPU",
556 "error": "Failed to parse GPU info"
557 })
558 }
559 }).collect::<Vec<_>>()
560 });
561 }
562 }
563
564 serde_json::json!({ "error": "Failed to query CUDA devices" })
565 }
566
567 fn get_rocm_device_details() -> serde_json::Value {
569 if let Ok(output) = std::process::Command::new("rocm-smi")
570 .arg("--showproductname")
571 .arg("--showmeminfo=vram")
572 .output()
573 {
574 if output.status.success() {
575 return serde_json::json!({
576 "detected": true,
577 "raw_output": String::from_utf8_lossy(&output.stdout)
578 });
579 }
580 }
581
582 serde_json::json!({ "error": "Failed to query ROCm devices" })
583 }
584
585 #[cfg(target_os = "macos")]
587 fn get_metal_device_details() -> serde_json::Value {
588 if let Ok(output) = std::process::Command::new("system_profiler")
589 .arg("SPDisplaysDataType")
590 .arg("-detailLevel")
591 .arg("full")
592 .output()
593 {
594 if output.status.success() {
595 let info = String::from_utf8_lossy(&output.stdout);
596 return serde_json::json!({
597 "detected": true,
598 "metal_support": info.contains("Metal"),
599 "summary": "Metal GPU acceleration available"
600 });
601 }
602 }
603
604 serde_json::json!({ "error": "Failed to query Metal devices" })
605 }
606
607 #[cfg(not(target_os = "macos"))]
608 fn get_metal_device_details() -> serde_json::Value {
609 serde_json::json!({ "error": "Metal is only available on macOS" })
610 }
611
612 fn get_vulkan_device_details() -> serde_json::Value {
614 if let Ok(output) = std::process::Command::new("vulkaninfo")
615 .arg("--summary")
616 .output()
617 {
618 if output.status.success() {
619 return serde_json::json!({
620 "detected": true,
621 "summary": "Vulkan runtime available"
622 });
623 }
624 }
625
626 serde_json::json!({ "error": "Failed to query Vulkan devices" })
627 }
628
629 fn get_opencl_device_details() -> serde_json::Value {
631 if let Ok(output) = std::process::Command::new("clinfo").arg("--list").output() {
632 if output.status.success() {
633 let info = String::from_utf8_lossy(&output.stdout);
634 return serde_json::json!({
635 "detected": true,
636 "devices_summary": info.lines().take(10).collect::<Vec<_>>()
637 });
638 }
639 }
640
641 serde_json::json!({ "error": "Failed to query OpenCL devices" })
642 }
643
644 #[cfg(test)]
645 mod tests {
646 use super::*;
647
648 #[test]
649 fn test_format_memory_treats_input_as_bytes() {
650 let eight_gib_in_bytes: u64 = 8 * 1024 * 1024 * 1024;
653 let formatted = format_memory(eight_gib_in_bytes);
654
655 assert!(
656 formatted.contains("GiB"),
657 "expected GiB-scale output for 8 GiB of bytes, got: {formatted}"
658 );
659 assert!(
660 !formatted.contains("TiB"),
661 "format_memory inflated bytes by 1024x (regression), got: {formatted}"
662 );
663 }
664 }
665}
666
667pub mod time {
669 use super::*;
670
671 pub fn format_duration(duration: Duration) -> String {
673 let secs = duration.as_secs();
674 if secs < 60 {
675 format!("{}s", secs)
676 } else if secs < 3600 {
677 format!("{}m {}s", secs / 60, secs % 60)
678 } else if secs < 86400 {
679 format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
680 } else {
681 format!("{}d {}h", secs / 86400, (secs % 86400) / 3600)
682 }
683 }
684
685 pub fn current_timestamp() -> String {
687 Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
688 }
689
690 pub fn parse_duration(s: &str) -> Result<Duration> {
692 humantime::parse_duration(s).with_context(|| format!("Failed to parse duration: {}", s))
693 }
694
695 pub async fn measure_time<F, T>(f: F) -> (T, Duration)
697 where
698 F: std::future::Future<Output = T>,
699 {
700 let start = Instant::now();
701 let result = f.await;
702 let duration = start.elapsed();
703 (result, duration)
704 }
705}
706
707pub mod network {
709 use super::*;
710
711 pub async fn download_file_with_progress(
713 url: &str,
714 output_path: &Path,
715 show_progress: bool,
716 ) -> Result<()> {
717 let client = crate::tls::client_builder()?.build()?;
718 let response = client.get(url).send().await?;
719
720 let total_size = response.content_length().unwrap_or(0);
721
722 let pb = if show_progress && total_size > 0 {
723 Some(progress::create_progress_bar(
724 total_size,
725 &format!(
726 "Downloading {}",
727 output_path
728 .file_name()
729 .unwrap_or_default()
730 .to_string_lossy()
731 ),
732 ))
733 } else {
734 None
735 };
736
737 let mut file = tokio::fs::File::create(output_path).await?;
738 let mut downloaded = 0u64;
739 let mut stream = response.bytes_stream();
740
741 use futures_util::StreamExt;
742 use tokio::io::AsyncWriteExt;
743
744 while let Some(chunk) = stream.next().await {
745 let chunk = chunk?;
746 file.write_all(&chunk).await?;
747 downloaded += chunk.len() as u64;
748
749 if let Some(pb) = &pb {
750 pb.set_position(downloaded);
751 }
752 }
753
754 if let Some(pb) = pb {
755 pb.finish_with_message("Download completed");
756 }
757
758 Ok(())
759 }
760
761 pub async fn check_url_accessible(url: &str) -> bool {
763 let client = match crate::tls::client_builder().and_then(|b| b.build().map_err(Into::into))
764 {
765 Ok(client) => client,
766 Err(_) => return false,
767 };
768 client.head(url).send().await.is_ok()
769 }
770}
771
772pub mod validation {
774 use super::*;
775
776 pub fn validate_file_exists(path: &Path) -> Result<()> {
778 if !path.exists() {
779 anyhow::bail!("File does not exist: {}", path.display());
780 }
781 if !path.is_file() {
782 anyhow::bail!("Path is not a file: {}", path.display());
783 }
784 Ok(())
785 }
786
787 pub fn validate_directory_exists(path: &Path) -> Result<()> {
789 if !path.exists() {
790 anyhow::bail!("Directory does not exist: {}", path.display());
791 }
792 if !path.is_dir() {
793 anyhow::bail!("Path is not a directory: {}", path.display());
794 }
795 Ok(())
796 }
797
798 pub fn validate_model_format(format: &str) -> Result<()> {
800 let supported_formats = ["torsh", "pytorch", "onnx", "tensorflow", "tflite"];
801 if !supported_formats.contains(&format) {
802 anyhow::bail!(
803 "Unsupported model format: {}. Supported formats: {}",
804 format,
805 supported_formats.join(", ")
806 );
807 }
808 Ok(())
809 }
810
811 pub fn validate_device(device: &str) -> Result<()> {
813 if device == "cpu" {
814 return Ok(());
815 }
816
817 if device.starts_with("cuda") {
818 let parts: Vec<&str> = device.split(':').collect();
819 if parts.len() == 2 {
820 if parts[1].parse::<usize>().is_err() {
821 anyhow::bail!("Invalid CUDA device ID: {}", parts[1]);
822 }
823 return Ok(());
824 } else if parts.len() == 1 && parts[0] == "cuda" {
825 return Ok(());
826 }
827 }
828
829 if device == "metal" {
830 return Ok(());
831 }
832
833 anyhow::bail!(
834 "Invalid device format: {}. Use 'cpu', 'cuda', 'cuda:N', or 'metal'",
835 device
836 );
837 }
838}
839
840pub mod interactive {
842 use super::*;
843 use dialoguer::{Confirm, Input, Select};
844
845 pub fn confirm(message: &str, default: bool) -> Result<bool> {
847 Confirm::new()
848 .with_prompt(message)
849 .default(default)
850 .interact()
851 .with_context(|| "Failed to get user confirmation")
852 }
853
854 pub fn input<T>(message: &str, default: Option<T>) -> Result<T>
856 where
857 T: Clone + std::fmt::Display + std::str::FromStr,
858 T::Err: std::fmt::Display + std::fmt::Debug + Send + Sync + 'static,
859 {
860 let mut input = Input::new().with_prompt(message);
861
862 if let Some(default_value) = default {
863 input = input.default(default_value);
864 }
865
866 input
867 .interact_text()
868 .with_context(|| "Failed to get user input")
869 }
870
871 pub fn select(message: &str, options: &[String]) -> Result<usize> {
873 Select::new()
874 .with_prompt(message)
875 .items(options)
876 .interact()
877 .with_context(|| "Failed to get user selection")
878 }
879}
880
881pub use time::format_duration;
883
884#[cfg(test)]
885mod tests {
886 use super::*;
887 use tempfile::tempdir;
888
889 #[test]
890 fn test_format_duration() {
891 assert_eq!(time::format_duration(Duration::from_secs(30)), "30s");
892 assert_eq!(time::format_duration(Duration::from_secs(90)), "1m 30s");
893 assert_eq!(time::format_duration(Duration::from_secs(3661)), "1h 1m");
894 }
895
896 #[test]
897 fn test_validation() {
898 assert!(validation::validate_model_format("torsh").is_ok());
899 assert!(validation::validate_model_format("invalid").is_err());
900
901 assert!(validation::validate_device("cpu").is_ok());
902 assert!(validation::validate_device("cuda:0").is_ok());
903 assert!(validation::validate_device("invalid").is_err());
904 }
905
906 #[tokio::test]
907 async fn test_file_operations() {
908 let temp_dir = tempdir().unwrap();
909 let test_file = temp_dir.path().join("test.txt");
910
911 tokio::fs::write(&test_file, "test content").await.unwrap();
912
913 let size = fs::get_directory_size(temp_dir.path()).await.unwrap();
914 assert!(size > 0);
915
916 let backup = fs::backup_file(&test_file).await.unwrap();
917 assert!(backup.exists());
918 }
919
920 #[test]
921 fn test_output_formatting() {
922 use serde_json::json;
923
924 let data = json!({
925 "name": "test",
926 "value": 42,
927 "active": true
928 });
929
930 let json_output = output::format_output(&data, "json").unwrap();
931 assert!(json_output.contains("test"));
932
933 let yaml_output = output::format_output(&data, "yaml").unwrap();
934 assert!(yaml_output.contains("name: test"));
935 }
936}