1use super::source::{ConfigSource, Sourced};
7use std::env;
8use std::path::PathBuf;
9use thiserror::Error;
10
11#[derive(Debug, Error)]
13pub enum EnvError {
14 #[error("Invalid value for {var}: expected {expected}, got '{value}'")]
16 InvalidValue {
17 var: String,
18 expected: String,
19 value: String,
20 },
21
22 #[error("Path not found for {var}: {path}")]
24 PathNotFound { var: String, path: PathBuf },
25
26 #[error("Invalid duration for {var}: {value}")]
28 InvalidDuration { var: String, value: String },
29
30 #[error("Value out of range for {var}: {value} (valid: {min}..={max})")]
32 OutOfRange {
33 var: String,
34 value: String,
35 min: String,
36 max: String,
37 },
38
39 #[error("Invalid log level for {var}: {value}")]
41 InvalidLogLevel { var: String, value: String },
42}
43
44pub struct EnvParser {
48 prefix: &'static str,
49 errors: Vec<EnvError>,
50}
51
52impl EnvParser {
53 pub fn new() -> Self {
55 Self {
56 prefix: "RCH_",
57 errors: Vec::new(),
58 }
59 }
60
61 pub fn errors(&self) -> &[EnvError] {
63 &self.errors
64 }
65
66 pub fn has_errors(&self) -> bool {
68 !self.errors.is_empty()
69 }
70
71 pub fn take_errors(&mut self) -> Vec<EnvError> {
73 std::mem::take(&mut self.errors)
74 }
75
76 fn var_name(&self, name: &str) -> String {
78 format!("{}{}", self.prefix, name)
79 }
80
81 pub fn get_string(&mut self, name: &str, default: &str) -> Sourced<String> {
83 let var_name = self.var_name(name);
84 match env::var(&var_name) {
85 Ok(value) => Sourced::from_env(value, var_name),
86 Err(_) => Sourced::default_value(default.to_string()),
87 }
88 }
89
90 pub fn get_bool(&mut self, name: &str, default: bool) -> Sourced<bool> {
95 let var_name = self.var_name(name);
96 match env::var(&var_name) {
97 Ok(value) => {
98 let parsed = match value.to_lowercase().as_str() {
99 "1" | "true" | "yes" | "on" => true,
100 "0" | "false" | "no" | "off" | "" => false,
101 _ => {
102 self.errors.push(EnvError::InvalidValue {
103 var: var_name.clone(),
104 expected: "boolean (true/false/1/0/yes/no)".to_string(),
105 value: value.clone(),
106 });
107 default
108 }
109 };
110 Sourced::from_env(parsed, var_name)
111 }
112 Err(_) => Sourced::default_value(default),
113 }
114 }
115
116 pub fn get_u32_range(&mut self, name: &str, default: u32, min: u32, max: u32) -> Sourced<u32> {
118 let var_name = self.var_name(name);
119 match env::var(&var_name) {
120 Ok(value) => match value.parse::<u32>() {
121 Ok(n) if n >= min && n <= max => Sourced::from_env(n, var_name),
122 Ok(n) => {
123 self.errors.push(EnvError::OutOfRange {
124 var: var_name.clone(),
125 value: n.to_string(),
126 min: min.to_string(),
127 max: max.to_string(),
128 });
129 Sourced::from_env(default, var_name)
130 }
131 Err(_) => {
132 self.errors.push(EnvError::InvalidValue {
133 var: var_name.clone(),
134 expected: "unsigned 32-bit integer".to_string(),
135 value,
136 });
137 Sourced::default_value(default)
138 }
139 },
140 Err(_) => Sourced::default_value(default),
141 }
142 }
143
144 pub fn get_u64_range(&mut self, name: &str, default: u64, min: u64, max: u64) -> Sourced<u64> {
146 let var_name = self.var_name(name);
147 match env::var(&var_name) {
148 Ok(value) => match value.parse::<u64>() {
149 Ok(n) if n >= min && n <= max => Sourced::from_env(n, var_name),
150 Ok(n) => {
151 self.errors.push(EnvError::OutOfRange {
152 var: var_name.clone(),
153 value: n.to_string(),
154 min: min.to_string(),
155 max: max.to_string(),
156 });
157 Sourced::from_env(default, var_name)
158 }
159 Err(_) => {
160 self.errors.push(EnvError::InvalidValue {
161 var: var_name.clone(),
162 expected: "unsigned 64-bit integer".to_string(),
163 value,
164 });
165 Sourced::default_value(default)
166 }
167 },
168 Err(_) => Sourced::default_value(default),
169 }
170 }
171
172 pub fn get_i32_range(&mut self, name: &str, default: i32, min: i32, max: i32) -> Sourced<i32> {
174 let var_name = self.var_name(name);
175 match env::var(&var_name) {
176 Ok(value) => match value.parse::<i32>() {
177 Ok(n) if n >= min && n <= max => Sourced::from_env(n, var_name),
178 Ok(n) => {
179 self.errors.push(EnvError::OutOfRange {
180 var: var_name.clone(),
181 value: n.to_string(),
182 min: min.to_string(),
183 max: max.to_string(),
184 });
185 Sourced::from_env(default, var_name)
186 }
187 Err(_) => {
188 self.errors.push(EnvError::InvalidValue {
189 var: var_name.clone(),
190 expected: "signed 32-bit integer".to_string(),
191 value,
192 });
193 Sourced::default_value(default)
194 }
195 },
196 Err(_) => Sourced::default_value(default),
197 }
198 }
199
200 pub fn get_f64_range(&mut self, name: &str, default: f64, min: f64, max: f64) -> Sourced<f64> {
202 let var_name = self.var_name(name);
203 match env::var(&var_name) {
204 Ok(value) => match value.parse::<f64>() {
205 Ok(n) if n >= min && n <= max => Sourced::from_env(n, var_name),
206 Ok(n) => {
207 self.errors.push(EnvError::OutOfRange {
208 var: var_name.clone(),
209 value: n.to_string(),
210 min: min.to_string(),
211 max: max.to_string(),
212 });
213 Sourced::from_env(default, var_name)
214 }
215 Err(_) => {
216 self.errors.push(EnvError::InvalidValue {
217 var: var_name.clone(),
218 expected: "floating-point number".to_string(),
219 value,
220 });
221 Sourced::default_value(default)
222 }
223 },
224 Err(_) => Sourced::default_value(default),
225 }
226 }
227
228 pub fn get_path(&mut self, name: &str, default: &str, must_exist: bool) -> Sourced<PathBuf> {
232 let var_name = self.var_name(name);
233 let (value, source) = match env::var(&var_name) {
234 Ok(v) => (v, ConfigSource::Environment),
235 Err(_) => (default.to_string(), ConfigSource::Default),
236 };
237
238 let expanded = if let Some(stripped) = value.strip_prefix("~/") {
240 if let Some(home) = dirs::home_dir() {
241 home.join(stripped)
242 } else {
243 PathBuf::from(&value)
244 }
245 } else {
246 PathBuf::from(&value)
247 };
248
249 if must_exist && !expanded.exists() {
250 self.errors.push(EnvError::PathNotFound {
251 var: var_name.clone(),
252 path: expanded.clone(),
253 });
254 }
255
256 if source == ConfigSource::Environment {
257 Sourced::from_env(expanded, var_name)
258 } else {
259 Sourced::default_value(expanded)
260 }
261 }
262
263 pub fn get_log_level(&mut self, name: &str, default: &str) -> Sourced<String> {
265 let var_name = self.var_name(name);
266 match env::var(&var_name) {
267 Ok(value) => {
268 let lower = value.to_lowercase();
269 match lower.as_str() {
270 "trace" | "debug" | "info" | "warn" | "error" | "off" => {
271 Sourced::from_env(lower, var_name)
272 }
273 _ => {
274 self.errors.push(EnvError::InvalidLogLevel {
275 var: var_name.clone(),
276 value: value.clone(),
277 });
278 Sourced::from_env(default.to_string(), var_name)
279 }
280 }
281 }
282 Err(_) => Sourced::default_value(default.to_string()),
283 }
284 }
285
286 pub fn get_string_list(&mut self, name: &str, default: Vec<String>) -> Sourced<Vec<String>> {
288 let var_name = self.var_name(name);
289 match env::var(&var_name) {
290 Ok(value) if value.is_empty() => Sourced::from_env(Vec::new(), var_name),
291 Ok(value) => {
292 let items: Vec<String> = value
293 .split(',')
294 .map(|s| s.trim().to_string())
295 .filter(|s| !s.is_empty())
296 .collect();
297 Sourced::from_env(items, var_name)
298 }
299 Err(_) => Sourced::default_value(default),
300 }
301 }
302
303 pub fn get_optional_string(&mut self, name: &str) -> Sourced<Option<String>> {
305 let var_name = self.var_name(name);
306 match env::var(&var_name) {
307 Ok(value) if value.is_empty() => Sourced::from_env(None, var_name),
308 Ok(value) => Sourced::from_env(Some(value), var_name),
309 Err(_) => Sourced::default_value(None),
310 }
311 }
312}
313
314impl Default for EnvParser {
315 fn default() -> Self {
316 Self::new()
317 }
318}
319
320#[cfg(test)]
321#[allow(unsafe_code)]
322mod tests {
323 use super::*;
324 use crate::config::env_test_lock;
325 use std::env;
326
327 fn cleanup_env(vars: &[&str]) {
328 for var in vars {
329 unsafe { env::remove_var(var) };
331 }
332 }
333
334 fn set_env(key: &str, value: &str) {
335 unsafe { env::set_var(key, value) };
337 }
338
339 fn env_guard() -> std::sync::MutexGuard<'static, ()> {
340 env_test_lock()
341 }
342
343 #[test]
344 fn test_get_bool_true_values() {
345 let _guard = env_guard();
346 let vars = ["RCH_TEST_BOOL_TRUE"];
347 cleanup_env(&vars);
348
349 for val in &["1", "true", "yes", "on", "TRUE", "Yes"] {
350 set_env("RCH_TEST_BOOL_TRUE", val);
351 let mut parser = EnvParser::new();
352 let result = parser.get_bool("TEST_BOOL_TRUE", false);
353 assert!(result.value, "Expected true for '{}'", val);
354 assert!(!parser.has_errors());
355 }
356
357 cleanup_env(&vars);
358 }
359
360 #[test]
361 fn test_get_bool_false_values() {
362 let _guard = env_guard();
363 let vars = ["RCH_TEST_BOOL_FALSE"];
364 cleanup_env(&vars);
365
366 for val in &["0", "false", "no", "off", "FALSE", ""] {
367 set_env("RCH_TEST_BOOL_FALSE", val);
368 let mut parser = EnvParser::new();
369 let result = parser.get_bool("TEST_BOOL_FALSE", true);
370 assert!(!result.value, "Expected false for '{}'", val);
371 assert!(!parser.has_errors());
372 }
373
374 cleanup_env(&vars);
375 }
376
377 #[test]
378 fn test_get_bool_invalid_uses_default() {
379 let _guard = env_guard();
380 let vars = ["RCH_BAD_BOOL"];
381 cleanup_env(&vars);
382
383 set_env("RCH_BAD_BOOL", "maybe");
384 let mut parser = EnvParser::new();
385 let result = parser.get_bool("BAD_BOOL", false);
386 assert!(!result.value);
387 assert!(parser.has_errors());
388
389 cleanup_env(&vars);
390 }
391
392 #[test]
393 fn test_get_u64_range_valid() {
394 let _guard = env_guard();
395 let vars = ["RCH_TEST_U64"];
396 cleanup_env(&vars);
397
398 set_env("RCH_TEST_U64", "50");
399 let mut parser = EnvParser::new();
400 let result = parser.get_u64_range("TEST_U64", 10, 0, 100);
401 assert_eq!(result.value, 50);
402 assert!(!parser.has_errors());
403
404 cleanup_env(&vars);
405 }
406
407 #[test]
408 fn test_get_u64_range_out_of_range() {
409 let _guard = env_guard();
410 let vars = ["RCH_TEST_U64_OOR"];
412 cleanup_env(&vars);
413
414 set_env("RCH_TEST_U64_OOR", "200");
415 let mut parser = EnvParser::new();
416 let result = parser.get_u64_range("TEST_U64_OOR", 10, 0, 100);
417 assert_eq!(result.value, 10); assert!(parser.has_errors());
419
420 cleanup_env(&vars);
421 }
422
423 #[test]
424 fn test_get_log_level_valid() {
425 let _guard = env_guard();
426 let vars = ["RCH_LOG_LEVEL"];
427 cleanup_env(&vars);
428
429 for level in &["trace", "debug", "info", "warn", "error", "DEBUG", "INFO"] {
430 set_env("RCH_LOG_LEVEL", level);
431 let mut parser = EnvParser::new();
432 let result = parser.get_log_level("LOG_LEVEL", "info");
433 assert!(!parser.has_errors(), "Expected valid for '{}'", level);
434 assert_eq!(result.value, level.to_lowercase());
435 }
436
437 cleanup_env(&vars);
438 }
439
440 #[test]
441 fn test_get_log_level_invalid() {
442 let _guard = env_guard();
443 let vars = ["RCH_LOG_LEVEL"];
444 cleanup_env(&vars);
445
446 set_env("RCH_LOG_LEVEL", "verbose");
447 let mut parser = EnvParser::new();
448 let result = parser.get_log_level("LOG_LEVEL", "info");
449 assert!(parser.has_errors());
450 assert_eq!(result.value, "info"); cleanup_env(&vars);
453 }
454
455 #[test]
456 fn test_get_string_list() {
457 let _guard = env_guard();
458 let vars = ["RCH_TEST_LIST"];
459 cleanup_env(&vars);
460
461 set_env("RCH_TEST_LIST", "a, b, c");
462 let mut parser = EnvParser::new();
463 let result = parser.get_string_list("TEST_LIST", vec![]);
464 assert_eq!(result.value, vec!["a", "b", "c"]);
465
466 cleanup_env(&vars);
467 }
468
469 #[test]
470 fn test_get_optional_string() {
471 let _guard = env_guard();
472 let vars = ["RCH_TEST_OPT"];
473 cleanup_env(&vars);
474
475 let mut parser = EnvParser::new();
477 let result = parser.get_optional_string("TEST_OPT");
478 assert!(result.value.is_none());
479
480 set_env("RCH_TEST_OPT", "");
482 let mut parser = EnvParser::new();
483 let result = parser.get_optional_string("TEST_OPT");
484 assert!(result.value.is_none());
485
486 set_env("RCH_TEST_OPT", "value");
488 let mut parser = EnvParser::new();
489 let result = parser.get_optional_string("TEST_OPT");
490 assert_eq!(result.value, Some("value".to_string()));
491
492 cleanup_env(&vars);
493 }
494
495 #[test]
496 fn test_source_tracking() {
497 let _guard = env_guard();
498 let vars = ["RCH_TEST_SRC"];
499 cleanup_env(&vars);
500
501 let mut parser = EnvParser::new();
503 let result = parser.get_string("TEST_SRC", "default");
504 assert_eq!(result.source, ConfigSource::Default);
505 assert!(result.env_var.is_none());
506
507 set_env("RCH_TEST_SRC", "from_env");
509 let mut parser = EnvParser::new();
510 let result = parser.get_string("TEST_SRC", "default");
511 assert_eq!(result.source, ConfigSource::Environment);
512 assert_eq!(result.env_var.as_deref(), Some("RCH_TEST_SRC"));
513
514 cleanup_env(&vars);
515 }
516
517 mod proptest_config_parsing {
522 use super::*;
523 use crate::config::env_test_lock;
524 use proptest::prelude::*;
525 use std::env;
526
527 fn cleanup_env(vars: &[&str]) {
531 for var in vars {
532 unsafe { env::remove_var(var) };
534 }
535 }
536
537 fn set_env(key: &str, value: &str) {
538 unsafe { env::set_var(key, value) };
540 }
541
542 fn parse_bool_string(value: &str) -> Option<bool> {
544 match value.to_lowercase().as_str() {
545 "1" | "true" | "yes" | "on" => Some(true),
546 "0" | "false" | "no" | "off" | "" => Some(false),
547 _ => None,
548 }
549 }
550
551 fn parse_log_level_string(value: &str) -> Option<String> {
553 let lower = value.to_lowercase();
554 match lower.as_str() {
555 "trace" | "debug" | "info" | "warn" | "error" | "off" => Some(lower),
556 _ => None,
557 }
558 }
559
560 fn parse_string_list(value: &str) -> Vec<String> {
562 if value.is_empty() {
563 Vec::new()
564 } else {
565 value
566 .split(',')
567 .map(|s| s.trim().to_string())
568 .filter(|s| !s.is_empty())
569 .collect()
570 }
571 }
572
573 proptest! {
574 #![proptest_config(ProptestConfig::with_cases(500))]
575
576 #[test]
578 fn test_parse_bool_no_panic(s in ".*") {
579 let _ = parse_bool_string(&s);
581 }
582
583 #[test]
585 fn test_parse_bool_valid_only(s in "[a-zA-Z0-9_-]{0,20}") {
586 let result = parse_bool_string(&s);
587 let valid_true = ["1", "true", "yes", "on"];
588 let valid_false = ["0", "false", "no", "off", ""];
589
590 let is_valid = valid_true.iter().any(|v| s.eq_ignore_ascii_case(v))
591 || valid_false.iter().any(|v| s.eq_ignore_ascii_case(v));
592
593 if is_valid {
594 prop_assert!(result.is_some(), "Expected Some for valid input: {}", s);
595 } else {
596 prop_assert!(result.is_none(), "Expected None for invalid input: {}", s);
597 }
598 }
599
600 #[test]
602 fn test_parse_log_level_no_panic(s in ".*") {
603 let _ = parse_log_level_string(&s);
604 }
605
606 #[test]
608 fn test_parse_log_level_valid_only(s in "[a-zA-Z]{0,10}") {
609 let result = parse_log_level_string(&s);
610 let valid_levels = ["trace", "debug", "info", "warn", "error", "off"];
611
612 let is_valid = valid_levels.iter().any(|v| s.eq_ignore_ascii_case(v));
613
614 if is_valid {
615 prop_assert!(result.is_some(), "Expected Some for valid level: {}", s);
616 } else {
617 prop_assert!(result.is_none(), "Expected None for invalid level: {}", s);
618 }
619 }
620
621 #[test]
623 fn test_parse_string_list_no_panic(s in ".*") {
624 let _ = parse_string_list(&s);
625 }
626
627 #[test]
629 fn test_parse_string_list_separators(
630 items in prop::collection::vec("[a-zA-Z0-9]+", 0..10)
631 ) {
632 let input = items.join(",");
633 let result = parse_string_list(&input);
634 let expected: Vec<String> = items.into_iter().filter(|s| !s.is_empty()).collect();
636 prop_assert_eq!(result, expected);
637 }
638
639 #[test]
641 fn test_integer_parsing_boundaries(
642 s in prop::sample::select(vec![
643 "0", "-1", "1", "2147483647", "-2147483648",
644 "18446744073709551615", "18446744073709551616",
645 "9999999999999999999999999999999999",
646 "abc", "", " ", "1.5", "1e10", "0x10", "0b10",
647 "+1", " 1 ", "1 ", " 1",
648 ])
649 ) {
650 let _ = s.parse::<u32>();
652 let _ = s.parse::<u64>();
653 let _ = s.parse::<i32>();
654 let _ = s.parse::<f64>();
655 }
656
657 #[test]
659 fn test_float_parsing_edge_cases(
660 s in prop::sample::select(vec![
661 "0", "0.0", "-0.0", "1.0", "-1.0",
662 "inf", "-inf", "nan", "NaN", "Infinity",
663 "1e308", "1e-308", "1e309", "1.7976931348623157e308", "abc", "", " ", "1,5", "1..0",
666 ])
667 ) {
668 let _ = s.parse::<f64>();
669 }
670 }
671
672 proptest! {
674 #![proptest_config(ProptestConfig::with_cases(100))]
675
676 #[test]
678 fn test_env_parser_get_bool(value in "[a-zA-Z0-9_-]{0,20}") {
679 let _guard = env_test_lock();
680 let var = "RCH_PROPTEST_BOOL_9";
681 cleanup_env(&[var]);
682
683 set_env(var, &value);
684 let mut parser = EnvParser::new();
685 let result = parser.get_bool("PROPTEST_BOOL_9", false);
686
687 prop_assert!(result.value == parse_bool_string(&value).unwrap_or(false));
689
690 cleanup_env(&[var]);
691 }
692
693 #[test]
695 fn test_env_parser_get_u32_range(value in "[-0-9a-zA-Z.]{0,30}") {
696 let _guard = env_test_lock();
697 let var = "RCH_PROPTEST_U32_10";
698 cleanup_env(&[var]);
699
700 set_env(var, &value);
701 let mut parser = EnvParser::new();
702 let result = parser.get_u32_range("PROPTEST_U32_10", 50, 0, 100);
703
704 let parsed = value.parse::<u32>().ok();
706 if let Some(n) = parsed {
707 if n <= 100 {
708 prop_assert_eq!(result.value, n);
709 } else {
710 prop_assert_eq!(result.value, 50); }
712 } else {
713 prop_assert_eq!(result.value, 50); }
715
716 cleanup_env(&[var]);
717 }
718
719 #[test]
721 fn test_env_parser_get_log_level(value in "[a-zA-Z]{0,15}") {
722 let _guard = env_test_lock();
723 let var = "RCH_PROPTEST_LOG_11";
724 cleanup_env(&[var]);
725
726 set_env(var, &value);
727 let mut parser = EnvParser::new();
728 let result = parser.get_log_level("PROPTEST_LOG_11", "info");
729
730 if let Some(valid_level) = parse_log_level_string(&value) {
732 prop_assert_eq!(result.value, valid_level);
733 prop_assert!(!parser.has_errors());
734 } else {
735 prop_assert_eq!(result.value, "info"); prop_assert!(parser.has_errors());
737 }
738
739 cleanup_env(&[var]);
740 }
741
742 #[test]
744 fn test_env_parser_get_string_list(value in "[a-zA-Z0-9, ]{0,100}") {
745 let _guard = env_test_lock();
746 let var = "RCH_PROPTEST_LIST_12";
747 cleanup_env(&[var]);
748
749 set_env(var, &value);
750 let mut parser = EnvParser::new();
751 let result = parser.get_string_list("PROPTEST_LIST_12", vec![]);
752
753 prop_assert_eq!(result.value, parse_string_list(&value));
755 prop_assert!(!parser.has_errors());
756
757 cleanup_env(&[var]);
758 }
759 }
760
761 #[test]
763 fn test_malformed_inputs_no_panic() {
764 let _guard = env_test_lock();
765
766 let long_string = "a".repeat(10000);
768
769 let malformed_values = [
771 "", " ", "\t\n\r", "null", "undefined", "None", "nil", "\0", "\x00\x01\x02", "🔥", "日本語", long_string.as_str(), "-", "+", ".", "e", "0x", "0b", ];
790
791 for value in &malformed_values {
792 let _ = parse_bool_string(value);
794
795 let _ = parse_log_level_string(value);
797
798 let _ = parse_string_list(value);
800
801 let _ = value.parse::<u32>();
803 let _ = value.parse::<u64>();
804 let _ = value.parse::<i32>();
805 let _ = value.parse::<i64>();
806
807 let _ = value.parse::<f64>();
809 }
810 }
811
812 #[test]
813 fn test_env_parser_with_malformed_values() {
814 let _guard = env_test_lock();
815 let vars = [
816 "RCH_PROPTEST_MAL_BOOL",
817 "RCH_PROPTEST_MAL_U32",
818 "RCH_PROPTEST_MAL_I32",
819 "RCH_PROPTEST_MAL_F64",
820 "RCH_PROPTEST_MAL_LOG",
821 ];
822 cleanup_env(&vars);
823
824 set_env("RCH_PROPTEST_MAL_BOOL", "maybe");
826 set_env("RCH_PROPTEST_MAL_U32", "not_a_number");
827 set_env("RCH_PROPTEST_MAL_I32", "9999999999999999999");
828 set_env("RCH_PROPTEST_MAL_F64", "1.2.3.4");
829 set_env("RCH_PROPTEST_MAL_LOG", "verbose");
830
831 let mut parser = EnvParser::new();
832
833 let bool_result = parser.get_bool("PROPTEST_MAL_BOOL", true);
835 assert!(bool_result.value); let u32_result = parser.get_u32_range("PROPTEST_MAL_U32", 42, 0, 100);
838 assert_eq!(u32_result.value, 42); let i32_result = parser.get_i32_range("PROPTEST_MAL_I32", -5, -100, 100);
841 assert_eq!(i32_result.value, -5); let f64_result = parser.get_f64_range("PROPTEST_MAL_F64", 4.567, 0.0, 10.0);
844 assert!((f64_result.value - 4.567).abs() < 0.001); let log_result = parser.get_log_level("PROPTEST_MAL_LOG", "warn");
847 assert_eq!(log_result.value, "warn"); assert!(parser.errors().len() >= 5);
851
852 cleanup_env(&vars);
853 }
854
855 #[test]
856 fn test_path_expansion_edge_cases() {
857 let _guard = env_test_lock();
858 let vars = ["RCH_PROPTEST_PATH_EDGE"];
859 cleanup_env(&vars);
860
861 let edge_case_paths = [
862 "", "~", "~/", "~user/file", "/absolute/path", "./relative/path", "../parent/path", "path with spaces", "path\twith\ttabs", "path/with/日本語", "/dev/null", ];
874
875 for path in &edge_case_paths {
876 set_env("RCH_PROPTEST_PATH_EDGE", path);
877 let mut parser = EnvParser::new();
878 let _ = parser.get_path("PROPTEST_PATH_EDGE", "/default", false);
880 }
882
883 cleanup_env(&vars);
884 }
885 }
886}