1use std::{
82 cell::RefCell,
83 mem::MaybeUninit,
84 num::NonZeroUsize,
85 path::{Path, PathBuf},
86 ptr::NonNull,
87 sync::OnceLock,
88};
89
90use jsony::{
91 JsonError, JsonParserConfig,
92 json::{DecodeError, FieldVisitor, FromJsonFieldVisitor, Parser},
93};
94
95#[derive(Debug)]
96pub enum Search<'a> {
98 Flag(&'a str),
104 Path(&'a std::path::Path),
106 Upwards {
112 file_stem: &'a str,
113 override_file_stem: Option<&'a str>,
114 },
115}
116
117pub mod relative_path {
140 use std::path::PathBuf;
141
142 use jsony::{FromJson, TextWriter, ToJson, json::DecodeError};
143
144 use crate::ConfigContext;
145
146 pub fn encode_json<T: ToJson>(value: &T, output: &mut TextWriter) {
147 value.encode_json__jsony(output);
148 }
149
150 pub fn decode_json<T: From<PathBuf>>(
155 parser: &mut jsony::parser::Parser<'_>,
156 ) -> Result<T, &'static DecodeError> {
157 pub fn decode_pathbuf(
158 parser: &mut jsony::parser::Parser<'_>,
159 ) -> Result<PathBuf, &'static DecodeError> {
160 let mut path = PathBuf::decode_json(parser)?;
161 if let Some(context) = unsafe { ConfigContext::current() } {
162 path = context
163 .config_file
164 .parent()
165 .unwrap_or(&context.config_file)
166 .join(path);
167 if let Ok(abs_path) = std::path::absolute(&path) {
168 path = abs_path;
169 }
170 if let Ok(canonical_path) = path.canonicalize() {
171 path = canonical_path;
172 }
173 }
174 Ok(path)
175 }
176 Ok(decode_pathbuf(parser)?.into())
177 }
178}
179
180pub struct GlobalConfig<C: JsonyConfig> {
188 config: std::sync::OnceLock<C>,
189 strategy: &'static [Search<'static>],
190 transform: Option<fn(&mut C) -> Result<(), Error>>,
191}
192
193impl<C: JsonyConfig + std::fmt::Debug> std::fmt::Debug for GlobalConfig<C> {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 if let Some(config) = self.config.get() {
196 config.fmt(f)
197 } else {
198 f.write_str("GlobalConfig::None")
199 }
200 }
201}
202
203impl<C: JsonyConfig> GlobalConfig<C> {
204 pub fn initialize(&self, handler: &mut DiagnosticHandler) -> Result<&C, Error> {
211 if let Some(value) = self.config.get() {
212 return Ok(value);
213 }
214 let result = load::<C>(&self.strategy, handler);
215 match result {
216 Ok(mut config) => {
217 if let Some(transform) = self.transform {
218 if let Err(err) = transform(&mut config) {
219 handler(Diagnostic {
220 level: DiagnosticLevel::Error,
221 message: &format!("Failed to transform config: {:?}", err),
222 file: None,
223 line: None,
224 });
225 return Err(err);
226 }
227 }
228 Ok(self.config.get_or_init(|| config))
229 }
230 Err(err) => Err(err),
231 }
232 }
233}
234
235impl<C: JsonyConfig> GlobalConfig<C> {
236 pub const fn new(strategy: &'static [Search<'static>]) -> GlobalConfig<C> {
241 GlobalConfig {
242 config: OnceLock::new(),
243 strategy,
244 transform: None,
245 }
246 }
247
248 pub const fn new_with_transform(
253 strategy: &'static [Search<'static>],
254 transform: fn(&mut C) -> Result<(), Error>,
255 ) -> GlobalConfig<C> {
256 GlobalConfig {
257 config: OnceLock::new(),
258 strategy,
259 transform: Some(transform),
260 }
261 }
262
263 #[cold]
264 #[inline(never)]
265 fn lazy_init(&self) -> &C {
266 #[cfg(feature = "kvlog")]
267 let logger: &mut DiagnosticHandler = &mut kvlog_diagnostics;
268 #[cfg(not(feature = "kvlog"))]
269 let logger: &mut DiagnosticHandler = &mut print_diagnostics;
270 match self.initialize(logger) {
271 Ok(config) => config,
272 Err(err) => panic!("Failed to lazy initialize config: {:?}", err),
273 }
274 }
275}
276
277impl<C: JsonyConfig> std::ops::Deref for GlobalConfig<C> {
278 type Target = C;
279
280 fn deref(&self) -> &Self::Target {
281 if let Some(config) = self.config.get() {
283 config
284 } else {
285 self.lazy_init()
286 }
287 }
288}
289pub type DiagnosticHandler = dyn FnMut(Diagnostic);
293
294#[derive(Debug)]
295pub enum DiagnosticLevel {
297 Info,
298 Warn,
299 Error,
300}
301
302#[cfg(feature = "kvlog")]
303pub fn kvlog_diagnostics(dia: Diagnostic) {
307 use kvlog::encoding::Encode;
308 let mut log = kvlog::global_logger();
309 let mut fields = log.encoder.append_now(match dia.level {
310 DiagnosticLevel::Info => kvlog::LogLevel::Info,
311 DiagnosticLevel::Warn => kvlog::LogLevel::Warn,
312 DiagnosticLevel::Error => kvlog::LogLevel::Error,
313 });
314 if let Some(line) = &dia.line {
315 line.get().encode_log_value_into(fields.dynamic_key("line"));
316 }
317 if let Some(file) = &dia.file {
318 (fields.dynamic_key("file")).value_via_display(&(file.display()));
319 }
320 dia.message.encode_log_value_into(fields.key("err"));
322 module_path!().encode_log_value_into(fields.key("target"));
323 "Parsing Config".encode_log_value_into(fields.key("msg"));
324 fields.apply_current_span();
325 log.poke();
326}
327
328pub fn print_diagnostics(dia: Diagnostic) {
338 let level = match dia.level {
339 DiagnosticLevel::Info => "INFO",
340 DiagnosticLevel::Warn => "WARN",
341 DiagnosticLevel::Error => "ERROR",
342 };
343 print!("{}: JSONY_CONFIG: {}", level, dia.message);
344 if let Some(file) = &dia.file {
345 print!(" @ {}", file.display());
346 }
347 if let Some(line) = &dia.line {
348 print!(":{line}");
349 }
350 println!();
351}
352
353#[derive(Debug)]
354pub struct Diagnostic<'a> {
356 pub level: DiagnosticLevel,
358 pub message: &'a str,
360 pub file: Option<&'a Path>,
362 pub line: Option<NonZeroUsize>,
364}
365#[diagnostic::on_unimplemented(note = "You can derive JsonyConfig via `#[jsony(Flattenable)]`")]
383pub trait JsonyConfig {
384 #[doc(hidden)]
385 unsafe fn config_field_visitor<'a>(
386 config: NonNull<()>,
387 ) -> jsony::__internal::DynamicFieldDecoder<'a>;
388}
389
390#[diagnostic::do_not_recommend]
391impl<T: for<'a> FromJsonFieldVisitor<'a, Visitor = jsony::__internal::DynamicFieldDecoder<'a>>>
392 JsonyConfig for T
393{
394 unsafe fn config_field_visitor<'a>(
395 config: NonNull<()>,
396 ) -> jsony::__internal::DynamicFieldDecoder<'a> {
397 let parser = Parser::new("", JsonParserConfig::default());
398 let visitor = unsafe { T::new_field_visitor(config, &parser) };
399 visitor
400 }
401}
402
403fn load_config_file(output: &mut Vec<ConfigFile>, path: PathBuf) -> Result<(), Error> {
404 let raw_contents = match std::fs::read_to_string(&path) {
405 Ok(value) => value,
406 Err(err) => {
407 return Err(Error::IOError(path.clone(), err));
408 }
409 };
410 let prelude_length = if path.extension().is_some_and(|ext| ext == "js") {
411 let header = "const CONFIG =";
412 if let Some(idx) = raw_contents.find(header) {
413 idx + header.len()
414 } else {
415 0
417 }
418 } else {
419 0
420 };
421 output.push(ConfigFile {
422 path,
423 raw_contents,
424 prelude_length,
425 });
426 Ok(())
427}
428
429fn warn_missing_config_file(handler: &mut DiagnosticHandler, path: &Path) {
430 handler(Diagnostic {
431 level: DiagnosticLevel::Warn,
432 message: "Configuration file does not exist",
433 file: Some(path),
434 line: None,
435 });
436}
437
438fn load_flag_config_files(
439 flag: &str,
440 args: impl IntoIterator<Item = String>,
441 output: &mut Vec<ConfigFile>,
442 cwd: &std::path::Path,
443 handler: &mut DiagnosticHandler,
444) -> Result<(), Error> {
445 let loaded_before = output.len();
446 let mut missing_configs = Vec::new();
447 let mut args = args.into_iter();
448 while args.by_ref().find(|a| a == flag).is_some() {
449 if let Some(config) = args.next() {
450 let path = cwd.join(config);
451 match load_config_file(output, path) {
452 Ok(()) => {}
453 Err(Error::IOError(path, err)) if err.kind() == std::io::ErrorKind::NotFound => {
454 missing_configs.push((path, err));
455 }
456 Err(err) => return Err(err),
457 }
458 }
459 }
460 for (path, _) in &missing_configs {
461 warn_missing_config_file(handler, path);
462 }
463 if output.len() == loaded_before {
464 if let Some((path, err)) = missing_configs.into_iter().next() {
465 return Err(Error::IOError(path, err));
466 }
467 }
468 Ok(())
469}
470
471impl<'a> Search<'a> {
472 fn search_until_found(
473 many: &'a [Search<'a>],
474 output: &mut Vec<ConfigFile>,
475 cwd: &std::path::Path,
476 handler: &mut DiagnosticHandler,
477 ) -> Result<(), Error> {
478 for strategy in many {
479 strategy.search(output, cwd, handler)?;
480 if !output.is_empty() {
481 return Ok(());
482 }
483 }
484 Ok(())
485 }
486 fn search(
487 &self,
488 output: &mut Vec<ConfigFile>,
489 cwd: &std::path::Path,
490 handler: &mut DiagnosticHandler,
491 ) -> Result<(), Error> {
492 match self {
493 Search::Flag(flag) => {
494 load_flag_config_files(flag, std::env::args(), output, cwd, handler)?;
495 }
496 Search::Path(path) => {
497 if path.exists() {
498 return load_config_file(output, cwd.join(path));
499 }
500 return Ok(());
501 }
502 Search::Upwards {
503 file_stem,
504 override_file_stem,
505 } => {
506 let mut cwd = cwd.to_path_buf();
507 let filename = format!("{file_stem}.js");
508 loop {
509 let mut path = cwd.join(&filename);
510 for _ in 0..2 {
511 if !path.exists() {
512 path.set_extension("json");
513 continue;
514 }
515 if let Some(override_file_stem) = override_file_stem {
516 let mut override_path = cwd.join(format!("{override_file_stem}.js"));
517 if override_path.exists() {
518 load_config_file(output, override_path)?;
519 } else {
520 override_path.set_extension("json");
521 if override_path.exists() {
522 load_config_file(output, override_path)?;
523 }
524 }
525 }
526 return load_config_file(output, path);
527 }
528 if !cwd.pop() {
529 break;
530 }
531 }
532 }
533 }
534 Ok(())
535 }
536}
537
538struct ConfigContext {
539 config_file: PathBuf,
540 handler: RefCell<&'static mut DiagnosticHandler>,
541 config_line_offset: usize,
542}
543
544thread_local! {
545 static CONFIG_CONTEXT: std::cell::Cell<Option<&'static ConfigContext>> = const {std::cell::Cell::new(None)};
546}
547
548struct ConfigContextDropGuard;
549
550impl Drop for ConfigContextDropGuard {
551 fn drop(&mut self) {
552 CONFIG_CONTEXT.set(None)
553 }
554}
555
556impl ConfigContext {
557 unsafe fn current() -> Option<&'static ConfigContext> {
560 CONFIG_CONTEXT.get()
561 }
562}
563
564struct ConfigFile {
565 path: PathBuf,
566 raw_contents: String,
567 prelude_length: usize,
568}
569
570impl ConfigFile {
571 fn prelude(&self) -> &str {
572 &self.raw_contents[..self.prelude_length]
573 }
574 fn config_text(&self) -> &str {
575 &self.raw_contents[self.prelude_length..]
576 }
577}
578
579pub fn load<T: JsonyConfig>(
584 locations: &[Search],
585 diagnostic_handler: &mut DiagnosticHandler,
586) -> Result<T, Error> {
587 let mut conf = MaybeUninit::<T>::uninit();
588 let mut configs = Vec::new();
589 let cwd = match std::env::current_dir() {
590 Ok(cwd) => cwd,
591 Err(err) => return Err(Error::Other(err.to_string())),
592 };
593 if let Err(err) = Search::search_until_found(locations, &mut configs, &cwd, diagnostic_handler)
594 {
595 return Err(err);
596 };
597 if configs.is_empty() {
598 diagnostic_handler(Diagnostic {
599 level: DiagnosticLevel::Warn,
600 message: &format!(
601 "Using Default config, no configuration files found from: {:#?}",
602 locations
603 ),
604 file: None,
605 line: None,
606 })
607 }
608 let mut visitor =
609 unsafe { T::config_field_visitor(NonNull::new_unchecked(conf.as_mut_ptr().cast())) };
610 match initialize_config_internal(&configs, &mut visitor, diagnostic_handler) {
611 Ok(()) => unsafe { Ok(conf.assume_init()) },
612 Err(err) => Err(err),
613 }
614}
615
616fn initialize_config_internal<'a>(
617 paths: &'a [ConfigFile],
618 visitor: &mut jsony::__internal::DynamicFieldDecoder<'a>,
619 handler: &mut DiagnosticHandler,
620) -> Result<(), Error> {
621 let res = unsafe { initialize_configs_inner(paths, visitor, handler) };
622 match res {
623 Ok(()) => match visitor.complete() {
624 Ok(()) => Ok(()),
625 Err(err) => {
626 if err == &jsony::error::MISSING_REQUIRED_FIELDS {
627 let missing = !visitor.bitset & visitor.required;
628 let mut message = format!("Missing required root config fields: [");
629 for (i, field) in visitor.schema.fields().iter().enumerate() {
630 if missing & (1 << i) != 0 {
631 use std::fmt::Write;
632 let _ = write!(message, "\n {:?},", field.name);
633 }
634 }
635 message.push_str("\n]");
636 handler(Diagnostic {
637 level: DiagnosticLevel::Error,
638 message: &message,
639 file: None,
640 line: None,
641 });
642 return Err(Error::Other(message));
643 }
644 return Err(Error::JsonError(JsonError::new(err, None)));
645 }
646 },
647 Err(err) => unsafe {
648 visitor.destroy();
649 Err(err)
650 },
651 }
652}
653
654pub enum Error {
656 JsonError(jsony::JsonError),
657 IOError(PathBuf, std::io::Error),
658 Other(String),
659 Custom(Box<dyn std::error::Error + Send + Sync>),
660}
661
662impl<T: Into<Box<dyn std::error::Error + Send + Sync>>> From<T> for Error {
663 fn from(value: T) -> Self {
664 Error::Custom(value.into())
665 }
666}
667
668impl std::fmt::Display for Error {
669 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
670 <Error as std::fmt::Debug>::fmt(self, f)
671 }
672}
673
674impl std::fmt::Debug for Error {
675 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
676 match self {
677 Self::JsonError(arg0) => f.debug_tuple("JsonError").field(arg0).finish(),
678 Self::IOError(arg0, arg1) => f.debug_tuple("IOError").field(arg0).field(arg1).finish(),
679 Self::Other(arg0) => f.write_str(arg0),
680 Self::Custom(arg0) => arg0.fmt(f),
681 }
682 }
683}
684
685unsafe fn initialize_configs_inner<'a>(
686 configs: &'a [ConfigFile],
687 decoder: &mut jsony::__internal::DynamicFieldDecoder<'a>,
688 handler: &mut DiagnosticHandler,
689) -> Result<(), Error> {
690 let mut ctx = ConfigContext {
691 config_file: PathBuf::default(),
692 config_line_offset: 0,
693 handler: RefCell::new(unsafe {
696 std::mem::transmute::<&mut DiagnosticHandler, &mut DiagnosticHandler>(handler)
697 }),
698 };
699 'to_next_config_file: for config in configs {
700 {
701 ctx.config_file.clone_from(&config.path);
702 ctx.config_line_offset = config.prelude().matches('\n').count();
703 }
704 let ctx: &ConfigContext = &ctx;
705 let mut duplicate_ignore = decoder.bitset;
706
707 let _guard = ConfigContextDropGuard;
708 unsafe {
711 CONFIG_CONTEXT.set(Some(&*(ctx as *const ConfigContext)));
712 }
713
714 let mut parser = jsony::parser::Parser::new(
715 &config.config_text(),
716 JsonParserConfig {
717 allow_comments: true,
718 allow_unquoted_field_keys: true,
719 allow_trailing_data: true,
720 allow_trailing_commas: true,
721 ..Default::default()
722 },
723 );
724
725 parser.attach_unused_field_hook(|info| {
726 if let Some(ctx) = unsafe { ConfigContext::current() } {
727 let message = format!("Unused field: `{}`", info.key());
728 let parser = info.into_parser();
729 let prefix = &parser.at.ctx.as_bytes()[..parser.at.index];
730 let lines =
731 prefix.iter().filter(|ch| **ch == b'\n').count() + ctx.config_line_offset;
732 ctx.handler.borrow_mut()(Diagnostic {
733 level: DiagnosticLevel::Warn,
734 file: Some(&ctx.config_file),
735 line: Some(NonZeroUsize::MIN.saturating_add(lines)),
736 message: &message,
737 });
738 }
739 });
740 let error: &DecodeError = 'err: {
741 match parser.at.enter_object(&mut parser.scratch) {
742 Ok(Some(mut key)) => 'key_loop: loop {
743 'next: {
744 'unused: {
745 let (index, field) = 'found: {
746 let fields = decoder.schema.fields();
747 for (index, field) in fields.iter().enumerate() {
748 if field.name != key {
749 continue;
750 }
751 break 'found (index, field);
752 }
753 for (index, alias_name) in decoder.alias {
754 if *alias_name == key {
755 break 'found (*index, &fields[*index]);
756 }
757 }
758 {
759 if let Some(ctx) = unsafe { ConfigContext::current() } {
760 let message = format!("Unused field: `{}`", key);
761 let prefix = &parser.at.ctx.as_bytes()[..parser.at.index];
762 let lines =
763 prefix.iter().filter(|ch| **ch == b'\n').count()
764 + ctx.config_line_offset;
765 ctx.handler.borrow_mut()(Diagnostic {
766 level: DiagnosticLevel::Warn,
767 file: Some(&ctx.config_file),
768 line: Some(NonZeroUsize::MIN.saturating_add(lines)),
769 message: &message,
770 });
771 }
772 }
773 break 'unused;
774 };
775 let mask = 1 << index;
776 if decoder.bitset & mask != 0 {
777 if mask & duplicate_ignore != 0 {
778 duplicate_ignore ^= mask;
779 } else {
780 let message = format!(
781 "Duplicate field in same file is ignored: `{}`",
782 key
783 );
784 let prefix = &parser.at.ctx.as_bytes()[..parser.at.index];
785 let lines = prefix.iter().filter(|ch| **ch == b'\n').count()
786 + ctx.config_line_offset;
787 ctx.handler.borrow_mut()(Diagnostic {
788 level: DiagnosticLevel::Warn,
789 file: Some(&ctx.config_file),
790 line: Some(NonZeroUsize::MIN.saturating_add(lines)),
791 message: &message,
792 });
793 }
794 break 'unused;
795 }
796 if let Err(err) = unsafe {
797 (field.decode)(
798 decoder.destination.byte_add(field.offset),
799 &mut parser,
800 )
801 } {
802 break 'err err;
803 }
804 decoder.bitset |= mask;
805 break 'next;
806 }
807
808 if let Err(error) = parser.at.skip_value() {
809 return Err(Error::JsonError(JsonError::extract(error, &mut parser)));
810 }
811 }
812
813 match parser.at.object_step(&mut parser.scratch) {
814 Ok(Some(next_key2)) => {
815 key = next_key2;
816 continue 'key_loop;
817 }
818 Ok(None) => break 'key_loop,
819 Err(err) => break 'err err,
820 }
821 },
822 Ok(None) => {}
823 Err(err) => break 'err err,
824 };
825 continue 'to_next_config_file;
826 };
827 let err = JsonError::extract(error, &mut parser);
828 let beat = parser.at.ctx.as_bytes();
829 let prefix = beat.get(..err.index()).unwrap_or(beat);
830 let line = prefix.iter().filter(|ch| **ch == b'\n').count() + ctx.config_line_offset;
831 ctx.handler.borrow_mut()(Diagnostic {
832 level: DiagnosticLevel::Error,
833 message: &err.to_string(),
834 file: Some(&ctx.config_file),
835 line: Some(NonZeroUsize::MIN.saturating_add(line)),
836 });
837 return Err(Error::JsonError(err));
838 }
839 Ok(())
840}
841
842#[cfg(test)]
843mod tests {
844 use super::*;
845 use std::{
846 cell::RefCell,
847 rc::Rc,
848 sync::atomic::{AtomicUsize, Ordering},
849 };
850
851 static TEST_DIR_COUNTER: AtomicUsize = AtomicUsize::new(0);
852 type RecordedDiagnostics = Rc<RefCell<Vec<(bool, String, Option<PathBuf>)>>>;
853
854 fn temp_test_dir(name: &str) -> PathBuf {
855 let id = TEST_DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
856 let path =
857 std::env::temp_dir().join(format!("jsony_config_{name}_{}_{}", std::process::id(), id));
858 std::fs::create_dir(&path).unwrap();
859 path
860 }
861
862 fn string_args(args: &[&str]) -> Vec<String> {
863 args.iter().map(|arg| (*arg).to_owned()).collect()
864 }
865
866 #[test]
867 fn missing_flag_overlay_is_warning_when_another_flag_config_loads() {
868 let cwd = temp_test_dir("missing_flag_overlay");
869 let found_path = cwd.join("found.json");
870 let missing_path = cwd.join("missing.json");
871 std::fs::write(&found_path, "{}").unwrap();
872
873 let mut configs = Vec::new();
874 let diagnostics: RecordedDiagnostics = Rc::new(RefCell::new(Vec::new()));
875 {
876 let diagnostics = Rc::clone(&diagnostics);
877 let mut handler = move |diagnostic: Diagnostic<'_>| {
878 diagnostics.borrow_mut().push((
879 matches!(diagnostic.level, DiagnosticLevel::Warn),
880 diagnostic.message.to_owned(),
881 diagnostic.file.map(std::path::Path::to_path_buf),
882 ));
883 };
884 let result = load_flag_config_files(
885 "--config",
886 string_args(&["app", "--config", "missing.json", "--config", "found.json"]),
887 &mut configs,
888 &cwd,
889 &mut handler,
890 );
891 assert!(result.is_ok(), "{result:?}");
892 }
893
894 assert_eq!(configs.len(), 1);
895 assert_eq!(configs[0].path, found_path);
896 let diagnostics = diagnostics.borrow();
897 assert_eq!(diagnostics.len(), 1);
898 assert!(diagnostics[0].0);
899 assert_eq!(diagnostics[0].1, "Configuration file does not exist");
900 assert_eq!(diagnostics[0].2.as_deref(), Some(missing_path.as_path()));
901
902 let _ = std::fs::remove_dir_all(cwd);
903 }
904
905 #[test]
906 fn missing_flag_config_is_fatal_when_no_flag_config_loads() {
907 let cwd = temp_test_dir("missing_only_flag_config");
908 let missing_path = cwd.join("missing.json");
909
910 let mut configs = Vec::new();
911 let diagnostics: RecordedDiagnostics = Rc::new(RefCell::new(Vec::new()));
912 let error = {
913 let diagnostics = Rc::clone(&diagnostics);
914 let mut handler = move |diagnostic: Diagnostic<'_>| {
915 diagnostics.borrow_mut().push((
916 matches!(diagnostic.level, DiagnosticLevel::Warn),
917 diagnostic.message.to_owned(),
918 diagnostic.file.map(std::path::Path::to_path_buf),
919 ));
920 };
921 load_flag_config_files(
922 "--config",
923 string_args(&["app", "--config", "missing.json"]),
924 &mut configs,
925 &cwd,
926 &mut handler,
927 )
928 .expect_err("missing only config should be fatal")
929 };
930
931 assert!(configs.is_empty());
932 match error {
933 Error::IOError(path, err) => {
934 assert_eq!(path, missing_path);
935 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
936 }
937 other => panic!("expected not found IO error, got {other:?}"),
938 }
939 let diagnostics = diagnostics.borrow();
940 assert_eq!(diagnostics.len(), 1);
941 assert!(diagnostics[0].0);
942 assert_eq!(diagnostics[0].1, "Configuration file does not exist");
943 assert_eq!(diagnostics[0].2.as_deref(), Some(missing_path.as_path()));
944
945 let _ = std::fs::remove_dir_all(cwd);
946 }
947}