OXYGENHUB
# 91334b7fa574 oxygen / vendor / crc-fast / src / bin / checksum.rs
23.1 KiB · Rust · last changed Strata: the cloud stores send through ureq, so every release target links 1 month ago source blame history raw
1
// Copyright 2025 Don MacAskill. Licensed under MIT or Apache-2.0.
2
3
//! This is a simple program to calculate a checksum from the command line
4
5
use crc_fast::{checksum, checksum_file, CrcAlgorithm};
6
use std::env;
7
use std::process::ExitCode;
8
use std::str::FromStr;
9
10
#[derive(Debug)]
11
struct Config {
12
    algorithm: String,
13
    file: Option<String>,
14
    string: Option<String>,
15
    format: OutputFormat,
16
    benchmark: Option<BenchmarkConfig>,
17
}
18
19
#[derive(Debug)]
20
struct BenchmarkConfig {
21
    size: Option<usize>,
22
    duration: f64,
23
}
24
25
#[derive(Debug)]
26
enum BenchmarkData {
27
    InMemory(Vec<u8>),
28
    File(String),
29
}
30
31
#[derive(Debug)]
32
struct BenchmarkRunner {
33
    algorithm: CrcAlgorithm,
34
    data: BenchmarkData,
35
    duration: f64,
36
}
37
38
#[derive(Debug)]
39
struct BenchmarkResult {
40
    iterations: u64,
41
    elapsed_seconds: f64,
42
    throughput_gibs: f64,
43
    time_per_iteration_nanos: f64,
44
    acceleration_target: String,
45
    data_size: u64,
46
}
47
48
#[derive(Debug, Clone)]
49
enum OutputFormat {
50
    Hex,
51
    Decimal,
52
}
53
54
impl BenchmarkConfig {
55
    fn validate(&self) -> Result<(), String> {
56
        if self.duration <= 0.0 {
57
            return Err("Duration must be greater than 0".to_string());
58
        }
59
60
        if let Some(size) = self.size {
61
            if size == 0 {
62
                return Err("Size must be greater than 0".to_string());
63
            }
64
        }
65
66
        Ok(())
67
    }
68
}
69
70
impl BenchmarkRunner {
71
    fn new(algorithm: CrcAlgorithm, data: BenchmarkData, duration: f64) -> Self {
72
        Self {
73
            algorithm,
74
            data,
75
            duration,
76
        }
77
    }
78
79
    fn run(&self) -> Result<BenchmarkResult, String> {
80
        use std::time::Instant;
81
82
        let start = Instant::now();
83
        let mut iterations = 0u64;
84
85
        while start.elapsed().as_secs_f64() < self.duration {
86
            match &self.data {
87
                BenchmarkData::InMemory(data) => {
88
                    std::hint::black_box(checksum(self.algorithm, data));
89
                }
90
                BenchmarkData::File(filename) => {
91
                    match checksum_file(self.algorithm, filename, None) {
92
                        Ok(result) => {
93
                            std::hint::black_box(result);
94
                        }
95
                        Err(e) => {
96
                            return Err(format!("Failed to read file during benchmark: {}", e));
97
                        }
98
                    }
99
                }
100
            }
101
            iterations += 1;
102
        }
103
104
        let elapsed = start.elapsed().as_secs_f64();
105
        let data_size = match &self.data {
106
            BenchmarkData::InMemory(data) => data.len() as u64,
107
            BenchmarkData::File(filename) => std::fs::metadata(filename)
108
                .map(|m| m.len())
109
                .map_err(|e| format!("Failed to get file size: {}", e))?,
110
        };
111
112
        let acceleration_target = crc_fast::get_calculator_target(self.algorithm);
113
114
        Ok(BenchmarkResult::new(
115
            iterations,
116
            elapsed,
117
            acceleration_target,
118
            data_size,
119
        ))
120
    }
121
}
122
123
impl BenchmarkResult {
124
    fn new(
125
        iterations: u64,
126
        elapsed_seconds: f64,
127
        acceleration_target: String,
128
        data_size: u64,
129
    ) -> Self {
130
        let throughput_gibs = if elapsed_seconds > 0.0 {
131
            (data_size as f64 * iterations as f64) / elapsed_seconds / (1024.0 * 1024.0 * 1024.0)
132
        } else {
133
            0.0
134
        };
135
136
        let time_per_iteration_nanos = if iterations > 0 {
137
            elapsed_seconds * 1_000_000_000.0 / iterations as f64
138
        } else {
139
            0.0
140
        };
141
142
        Self {
143
            iterations,
144
            elapsed_seconds,
145
            throughput_gibs,
146
            time_per_iteration_nanos,
147
            acceleration_target,
148
            data_size,
149
        }
150
    }
151
}
152
153
/// Fills a buffer with pseudo-random data using xorshift64* algorithm.
154
/// This is a deterministic PRNG that's tiny, fast, and suitable for benchmark data generation.
155
/// The seed is derived from the buffer size to provide some variability.
156
/// This solves the problem of having to use a full-featured PRNG like rand for benchmark data generation.
157
#[inline]
158
fn fill_pseudo_random(buf: &mut [u8], seed: u64) {
159
    let mut x = seed;
160
    for b in buf {
161
        x ^= x >> 12;
162
        x ^= x << 25;
163
        x ^= x >> 27;
164
        let r = x.wrapping_mul(0x2545_F491_4F6C_DD1D);
165
        *b = r as u8;
166
    }
167
}
168
169
fn generate_random_data(size: usize) -> Result<Vec<u8>, String> {
170
    // Check for reasonable size limits to prevent memory issues
171
    if size > 1_073_741_824 {
172
        // 1 GiB limit
173
        return Err("Data size too large (maximum 1 GiB)".to_string());
174
    }
175
176
    // Use vec! macro to avoid clippy warning about slow initialization
177
    let mut buf = vec![0u8; size];
178
179
    // Derive seed from size for deterministic but varied data
180
    let seed = 0x9E37_79B9_7F4A_7C15_u64.wrapping_add(size as u64);
181
    fill_pseudo_random(&mut buf, seed);
182
183
    Ok(buf)
184
}
185
186
fn print_usage() {
187
    println!("Usage: checksum -a algorithm [-f file] [-s string] [--format hex|decimal]");
188
    println!(
189
        "       checksum -a algorithm -b [--size bytes] [--duration seconds] [-f file] [-s string]"
190
    );
191
    println!();
192
    println!("Example: checksum -a CRC-32/ISCSI -f myfile.txt");
193
    println!("Example: checksum -a CRC-64/NVME -s 'Hello, world!' --format decimal");
194
    println!("Example: checksum -a CRC-32/ISCSI -b --size 1048576 --duration 5.0");
195
    println!();
196
    println!("Options:");
197
    println!("  -a algorithm        Specify the checksum algorithm (required)");
198
    println!("  -f file             Calculate checksum for the specified file");
199
    println!("  -h, --help          Show this help message");
200
    println!("  -s string           Calculate checksum for the specified string");
201
    println!("  --format hex|decimal Output format (default: hex)");
202
    println!();
203
    println!("Benchmarking:");
204
    println!("  -b                  Enable benchmark mode");
205
    println!("  --duration seconds  Benchmark duration in seconds (default: 10.0)");
206
    println!("  --size bytes        Data size for random generation in benchmark mode (default: 1048576 [1MiB])");
207
    println!();
208
    println!();
209
    println!("Note: In normal mode, either -f or -s must be provided, but not both.");
210
    println!("      In benchmark mode (-b), -f or -s are optional for using specific data.");
211
}
212
213
fn parse_args() -> Result<Config, String> {
214
    let args: Vec<String> = env::args().collect();
215
216
    if args.len() == 1 {
217
        return Err("No arguments provided".to_string());
218
    }
219
220
    // Check for help flag
221
    if args.contains(&"-h".to_string()) || args.contains(&"--help".to_string()) {
222
        return Err("help".to_string());
223
    }
224
225
    let mut algorithm: Option<String> = None;
226
    let mut file: Option<String> = None;
227
    let mut string: Option<String> = None;
228
    let mut format = OutputFormat::Hex; // Default to hex
229
    let mut benchmark_mode = false;
230
    let mut benchmark_size: Option<usize> = None;
231
    let mut benchmark_duration = 10.0; // Default duration
232
233
    let mut i = 1; // Skip program name
234
    while i < args.len() {
235
        match args[i].as_str() {
236
            "-a" => {
237
                if i + 1 >= args.len() {
238
                    return Err("Missing algorithm after -a flag".to_string());
239
                }
240
                algorithm = Some(args[i + 1].clone());
241
                i += 2;
242
            }
243
            "-f" => {
244
                if i + 1 >= args.len() {
245
                    return Err("Missing filename after -f flag".to_string());
246
                }
247
                if string.is_some() {
248
                    return Err("Cannot specify both -f and -s flags".to_string());
249
                }
250
                file = Some(args[i + 1].clone());
251
                i += 2;
252
            }
253
            "-s" => {
254
                if i + 1 >= args.len() {
255
                    return Err("Missing string after -s flag".to_string());
256
                }
257
                if file.is_some() {
258
                    return Err("Cannot specify both -f and -s flags".to_string());
259
                }
260
                string = Some(args[i + 1].clone());
261
                i += 2;
262
            }
263
            "--format" => {
264
                if i + 1 >= args.len() {
265
                    return Err("Missing format after --format flag".to_string());
266
                }
267
                match args[i + 1].as_str() {
268
                    "hex" => format = OutputFormat::Hex,
269
                    "decimal" => format = OutputFormat::Decimal,
270
                    invalid => {
271
                        return Err(format!(
272
                            "Invalid format '{}'. Use 'hex' or 'decimal'",
273
                            invalid
274
                        ))
275
                    }
276
                }
277
                i += 2;
278
            }
279
            "-b" => {
280
                benchmark_mode = true;
281
                i += 1;
282
            }
283
            "--size" => {
284
                if i + 1 >= args.len() {
285
                    return Err("Missing size value after --size flag".to_string());
286
                }
287
                benchmark_size = Some(
288
                    args[i + 1]
289
                        .parse::<usize>()
290
                        .map_err(|_| format!("Invalid size value: {}", args[i + 1]))?,
291
                );
292
                i += 2;
293
            }
294
            "--duration" => {
295
                if i + 1 >= args.len() {
296
                    return Err("Missing duration value after --duration flag".to_string());
297
                }
298
                benchmark_duration = args[i + 1]
299
                    .parse::<f64>()
300
                    .map_err(|_| format!("Invalid duration value: {}", args[i + 1]))?;
301
                i += 2;
302
            }
303
            arg => {
304
                return Err(format!("Unknown argument: {}", arg));
305
            }
306
        }
307
    }
308
309
    // Validate required arguments
310
    let algorithm = algorithm.ok_or("Algorithm (-a) is required")?;
311
312
    // Validate mutual exclusivity between benchmark and normal modes
313
    if !benchmark_mode && (benchmark_size.is_some() || benchmark_duration != 10.0) {
314
        return Err("--size and --duration can only be used with -b flag".to_string());
315
    }
316
317
    // Create benchmark config if in benchmark mode
318
    let benchmark = if benchmark_mode {
319
        let config = BenchmarkConfig {
320
            size: benchmark_size,
321
            duration: benchmark_duration,
322
        };
323
        config.validate()?;
324
        Some(config)
325
    } else {
326
        None
327
    };
328
329
    // Validate input requirements based on mode
330
    if benchmark.is_none() {
331
        // Normal mode: require either file or string input
332
        if file.is_none() && string.is_none() {
333
            return Err(
334
                "Either -f (file) or -s (string) must be provided in normal mode".to_string(),
335
            );
336
        }
337
    }
338
    // Benchmark mode: file and string are optional (will use generated data if neither provided)
339
340
    Ok(Config {
341
        algorithm,
342
        file,
343
        string,
344
        format,
345
        benchmark,
346
    })
347
}
348
349
fn calculate_checksum(config: &Config) -> Result<(), String> {
350
    let algorithm = CrcAlgorithm::from_str(&config.algorithm)
351
        .map_err(|_| format!("Invalid algorithm: {}", config.algorithm))?;
352
353
    // Check if benchmark mode is enabled
354
    if let Some(benchmark_config) = &config.benchmark {
355
        return run_benchmark(config, benchmark_config, algorithm);
356
    }
357
358
    let checksum = if let Some(ref filename) = config.file {
359
        checksum_file(algorithm, filename, None).unwrap()
360
    } else if let Some(ref text) = config.string {
361
        checksum(algorithm, text.as_bytes())
362
    } else {
363
        return Err("No input provided for checksum calculation".to_string());
364
    };
365
366
    match config.format {
367
        OutputFormat::Hex => println!("{:#x?}", checksum),
368
        OutputFormat::Decimal => println!("{}", checksum),
369
    }
370
371
    Ok(())
372
}
373
374
fn run_benchmark(
375
    config: &Config,
376
    benchmark_config: &BenchmarkConfig,
377
    algorithm: CrcAlgorithm,
378
) -> Result<(), String> {
379
    // Determine data source and create BenchmarkData
380
    let data = if let Some(ref filename) = config.file {
381
        // Validate file exists before benchmarking
382
        if !std::path::Path::new(filename).exists() {
383
            return Err(format!("File not found: {}", filename));
384
        }
385
        BenchmarkData::File(filename.clone())
386
    } else if let Some(ref text) = config.string {
387
        BenchmarkData::InMemory(text.as_bytes().to_vec())
388
    } else {
389
        // Generate random data with specified size or default (1 MiB)
390
        let size = benchmark_config.size.unwrap_or(1_048_576);
391
        let random_data = generate_random_data(size)?;
392
        BenchmarkData::InMemory(random_data)
393
    };
394
395
    // Create and run benchmark
396
    let runner = BenchmarkRunner::new(algorithm, data, benchmark_config.duration);
397
    let result = runner.run()?;
398
399
    // Display results with algorithm name
400
    display_benchmark_results(&result, &config.algorithm);
401
402
    Ok(())
403
}
404
405
// Format numbers with comma separators for better readability
406
fn format_number_with_commas(n: u64) -> String {
407
    let s = n.to_string();
408
    let mut result = String::new();
409
    let chars: Vec<char> = s.chars().collect();
410
411
    for (i, ch) in chars.iter().enumerate() {
412
        if i > 0 && (chars.len() - i).is_multiple_of(3) {
413
            result.push(',');
414
        }
415
        result.push(*ch);
416
    }
417
418
    result
419
}
420
421
fn display_benchmark_results(result: &BenchmarkResult, algorithm_name: &str) {
422
    println!("Algorithm: {}", algorithm_name);
423
    println!("Acceleration Target: {}", result.acceleration_target);
424
425
    // Format data size with appropriate units
426
    let (size_value, size_unit) = if result.data_size >= 1_048_576 {
427
        (result.data_size as f64 / 1_048_576.0, "MiB")
428
    } else if result.data_size >= 1024 {
429
        (result.data_size as f64 / 1024.0, "KiB")
430
    } else {
431
        (result.data_size as f64, "bytes")
432
    };
433
434
    println!(
435
        "Data Size: {} bytes ({:.1} {})",
436
        format_number_with_commas(result.data_size),
437
        size_value,
438
        size_unit
439
    );
440
    println!("Duration: {:.2} seconds", result.elapsed_seconds);
441
    println!(
442
        "Iterations: {}",
443
        format_number_with_commas(result.iterations)
444
    );
445
    println!("Throughput: {:.2} GiB/s", result.throughput_gibs);
446
447
    // Format time per iteration with appropriate units
448
    let (time_value, time_unit) = if result.time_per_iteration_nanos >= 1_000_000.0 {
449
        (result.time_per_iteration_nanos / 1_000_000.0, "ms")
450
    } else if result.time_per_iteration_nanos >= 1_000.0 {
451
        (result.time_per_iteration_nanos / 1_000.0, "μs")
452
    } else {
453
        (result.time_per_iteration_nanos, "ns")
454
    };
455
456
    println!("Time per iteration: {:.1} {}", time_value, time_unit);
457
}
458
459
fn main() -> ExitCode {
460
    match parse_args() {
461
        Ok(config) => {
462
            if let Err(e) = calculate_checksum(&config) {
463
                eprintln!("Error: {}", e);
464
                return ExitCode::from(1);
465
            }
466
        }
467
        Err(msg) => {
468
            if msg == "help" {
469
                print_usage();
470
                return ExitCode::SUCCESS;
471
            } else {
472
                eprintln!("Error: {}", msg);
473
                println!();
474
                print_usage();
475
                return ExitCode::from(1);
476
            }
477
        }
478
    }
479
480
    ExitCode::SUCCESS
481
}
482
483
#[cfg(test)]
484
mod tests {
485
    use super::*;
486
    use std::str::FromStr;
487
488
    #[test]
489
    fn test_benchmark_config_validation_valid() {
490
        let config = BenchmarkConfig {
491
            size: Some(1024),
492
            duration: 5.0,
493
        };
494
        assert!(config.validate().is_ok());
495
    }
496
497
    #[test]
498
    fn test_benchmark_config_validation_zero_duration() {
499
        let config = BenchmarkConfig {
500
            size: Some(1024),
501
            duration: 0.0,
502
        };
503
        assert!(config.validate().is_err());
504
        assert_eq!(
505
            config.validate().unwrap_err(),
506
            "Duration must be greater than 0"
507
        );
508
    }
509
510
    #[test]
511
    fn test_benchmark_config_validation_negative_duration() {
512
        let config = BenchmarkConfig {
513
            size: Some(1024),
514
            duration: -1.0,
515
        };
516
        assert!(config.validate().is_err());
517
        assert_eq!(
518
            config.validate().unwrap_err(),
519
            "Duration must be greater than 0"
520
        );
521
    }
522
523
    #[test]
524
    fn test_benchmark_config_validation_zero_size() {
525
        let config = BenchmarkConfig {
526
            size: Some(0),
527
            duration: 5.0,
528
        };
529
        assert!(config.validate().is_err());
530
        assert_eq!(
531
            config.validate().unwrap_err(),
532
            "Size must be greater than 0"
533
        );
534
    }
535
536
    #[test]
537
    fn test_benchmark_config_validation_none_size() {
538
        let config = BenchmarkConfig {
539
            size: None,
540
            duration: 5.0,
541
        };
542
        assert!(config.validate().is_ok());
543
    }
544
545
    #[test]
546
    fn test_generate_random_data_valid_size() {
547
        let data = generate_random_data(1024).unwrap();
548
        assert_eq!(data.len(), 1024);
549
    }
550
551
    #[test]
552
    fn test_generate_random_data_large_size() {
553
        let result = generate_random_data(1_073_741_825); // 1 GiB + 1
554
        assert!(result.is_err());
555
        assert_eq!(result.unwrap_err(), "Data size too large (maximum 1 GiB)");
556
    }
557
558
    #[test]
559
    fn test_benchmark_result_calculation() {
560
        let result = BenchmarkResult::new(
561
            1000,               // iterations
562
            2.0,                // elapsed_seconds
563
            "test".to_string(), // acceleration_target
564
            1024,               // data_size
565
        );
566
567
        assert_eq!(result.iterations, 1000);
568
        assert_eq!(result.elapsed_seconds, 2.0);
569
        assert_eq!(result.data_size, 1024);
570
571
        // Throughput should be (1024 * 1000) / 2.0 / (1024^3) GiB/s
572
        let expected_throughput = (1024.0 * 1000.0) / 2.0 / (1024.0 * 1024.0 * 1024.0);
573
        assert!((result.throughput_gibs - expected_throughput).abs() < 1e-10);
574
575
        // Time per iteration should be 2.0 * 1e9 / 1000 nanoseconds
576
        let expected_time_per_iter = 2.0 * 1_000_000_000.0 / 1000.0;
577
        assert!((result.time_per_iteration_nanos - expected_time_per_iter).abs() < 1e-6);
578
    }
579
580
    #[test]
581
    fn test_benchmark_runner_creation() {
582
        let algorithm = CrcAlgorithm::from_str("CRC-32/ISCSI").unwrap();
583
        let data = BenchmarkData::InMemory(vec![1, 2, 3, 4]);
584
        let runner = BenchmarkRunner::new(algorithm, data, 1.0);
585
586
        assert_eq!(runner.duration, 1.0);
587
        assert_eq!(runner.algorithm, algorithm);
588
    }
589
590
    #[test]
591
    fn test_benchmark_runner_execution_in_memory() {
592
        let algorithm = CrcAlgorithm::from_str("CRC-32/ISCSI").unwrap();
593
        let data = BenchmarkData::InMemory(vec![1, 2, 3, 4]);
594
        let runner = BenchmarkRunner::new(algorithm, data, 0.1); // Short duration for test
595
596
        let result = runner.run().unwrap();
597
        assert!(result.iterations > 0);
598
        assert!(result.elapsed_seconds > 0.0);
599
        assert_eq!(result.data_size, 4);
600
        assert!(result.throughput_gibs >= 0.0);
601
    }
602
603
    #[test]
604
    fn test_parse_args_benchmark_mode() {
605
        // We can't easily mock std::env::args in unit tests, so we'll test the parsing logic
606
        // by creating a Config directly and validating its structure
607
        let config = Config {
608
            algorithm: "CRC-32/ISCSI".to_string(),
609
            file: None,
610
            string: None,
611
            format: OutputFormat::Hex,
612
            benchmark: Some(BenchmarkConfig {
613
                size: Some(1024),
614
                duration: 5.0,
615
            }),
616
        };
617
618
        assert!(config.benchmark.is_some());
619
        let benchmark_config = config.benchmark.unwrap();
620
        assert_eq!(benchmark_config.size, Some(1024));
621
        assert_eq!(benchmark_config.duration, 5.0);
622
    }
623
624
    #[test]
625
    fn test_parse_args_normal_mode() {
626
        let config = Config {
627
            algorithm: "CRC-32/ISCSI".to_string(),
628
            file: Some("test.txt".to_string()),
629
            string: None,
630
            format: OutputFormat::Hex,
631
            benchmark: None,
632
        };
633
634
        assert!(config.benchmark.is_none());
635
        assert_eq!(config.file, Some("test.txt".to_string()));
636
    }
637
638
    #[test]
639
    fn test_data_source_selection_file() {
640
        // Test that file input creates File variant
641
        let config = Config {
642
            algorithm: "CRC-32/ISCSI".to_string(),
643
            file: Some("test.txt".to_string()),
644
            string: None,
645
            format: OutputFormat::Hex,
646
            benchmark: Some(BenchmarkConfig {
647
                size: None,
648
                duration: 1.0,
649
            }),
650
        };
651
652
        // This would be tested in the run_benchmark function
653
        // We can verify the config structure is correct for file input
654
        assert!(config.file.is_some());
655
        assert!(config.string.is_none());
656
    }
657
658
    #[test]
659
    fn test_data_source_selection_string() {
660
        let config = Config {
661
            algorithm: "CRC-32/ISCSI".to_string(),
662
            file: None,
663
            string: Some("test data".to_string()),
664
            format: OutputFormat::Hex,
665
            benchmark: Some(BenchmarkConfig {
666
                size: None,
667
                duration: 1.0,
668
            }),
669
        };
670
671
        assert!(config.file.is_none());
672
        assert!(config.string.is_some());
673
    }
674
675
    #[test]
676
    fn test_data_source_selection_generated() {
677
        let config = Config {
678
            algorithm: "CRC-32/ISCSI".to_string(),
679
            file: None,
680
            string: None,
681
            format: OutputFormat::Hex,
682
            benchmark: Some(BenchmarkConfig {
683
                size: Some(1024),
684
                duration: 1.0,
685
            }),
686
        };
687
688
        // When neither file nor string is provided, generated data should be used
689
        assert!(config.file.is_none());
690
        assert!(config.string.is_none());
691
        assert_eq!(config.benchmark.as_ref().unwrap().size, Some(1024));
692
    }
693
694
    #[test]
695
    fn test_throughput_calculation_accuracy() {
696
        // Test with known values to verify calculation accuracy
697
        let data_size = 1_048_576u64; // 1 MiB
698
        let iterations = 1000u64;
699
        let elapsed_seconds = 1.0;
700
701
        let result =
702
            BenchmarkResult::new(iterations, elapsed_seconds, "test".to_string(), data_size);
703
704
        // Expected throughput: (1 MiB * 1000) / 1 second = 1000 MiB/s = ~0.9537 GiB/s
705
        let expected_gibs =
706
            (data_size as f64 * iterations as f64) / elapsed_seconds / (1024.0 * 1024.0 * 1024.0);
707
        assert!((result.throughput_gibs - expected_gibs).abs() < 1e-10);
708
    }
709
710
    #[test]
711
    fn test_output_format_variants() {
712
        let hex_format = OutputFormat::Hex;
713
        let decimal_format = OutputFormat::Decimal;
714
715
        // Test that both variants can be created and are different
716
        assert!(matches!(hex_format, OutputFormat::Hex));
717
        assert!(matches!(decimal_format, OutputFormat::Decimal));
718
    }
719
720
    #[test]
721
    fn test_format_number_with_commas() {
722
        assert_eq!(format_number_with_commas(0), "0");
723
        assert_eq!(format_number_with_commas(123), "123");
724
        assert_eq!(format_number_with_commas(1234), "1,234");
725
        assert_eq!(format_number_with_commas(12345), "12,345");
726
        assert_eq!(format_number_with_commas(123456), "123,456");
727
        assert_eq!(format_number_with_commas(1234567), "1,234,567");
728
        assert_eq!(format_number_with_commas(12345678), "12,345,678");
729
        assert_eq!(format_number_with_commas(123456789), "123,456,789");
730
        assert_eq!(format_number_with_commas(1000000000), "1,000,000,000");
731
    }
732
}