66use  crate :: { 
77    errors:: { 
88        ConfigParseError ,  ConfigParseErrorKind ,  ConfigParseOverrideError ,  ProfileNotFound , 
9-         TestThreadsParseError , 
9+         TestThreadsParseError ,   ToolConfigFileParseError , 
1010    } , 
1111    reporter:: { FinalStatusLevel ,  StatusLevel ,  TestOutputDisplay } , 
1212} ; 
@@ -54,17 +54,29 @@ impl NextestConfig {
5454    /// Reads the nextest config from the given file, or if not specified from `.config/nextest.toml` 
5555     /// in the workspace root. 
5656     /// 
57-      /// If the file isn't specified and the directory doesn't have `.config/nextest.toml`, uses the 
57+      /// `tool_config_files` are lower priority than `config_file` but higher priority than the 
58+      /// default config. Files in `tool_config_files` that come earlier are higher priority than those 
59+      /// that come later. 
60+      /// 
61+      /// If no config files are specified and this file doesn't have `.config/nextest.toml`, uses the 
5862     /// default config options. 
59-      pub  fn  from_sources ( 
63+      pub  fn  from_sources < ' a ,   I > ( 
6064        workspace_root :  impl  Into < Utf8PathBuf > , 
6165        graph :  & PackageGraph , 
6266        config_file :  Option < & Utf8Path > , 
63-     )  -> Result < Self ,  ConfigParseError >  { 
67+         tool_config_files :  impl  IntoIterator < IntoIter  = I > , 
68+     )  -> Result < Self ,  ConfigParseError > 
69+     where 
70+         I :  Iterator < Item  = & ' a  ToolConfigFile >  + DoubleEndedIterator , 
71+     { 
6472        let  workspace_root = workspace_root. into ( ) ; 
65-         let  ( config_file,  config)  = Self :: read_from_sources ( & workspace_root,  config_file) ?; 
73+         let  tool_config_files_rev = tool_config_files. into_iter ( ) . rev ( ) ; 
74+         let  ( config_file,  config)  =
75+             Self :: read_from_sources ( & workspace_root,  config_file,  tool_config_files_rev) ?; 
6676        let  inner:  NextestConfigImpl  =
6777            serde_path_to_error:: deserialize ( config) . map_err ( |error| { 
78+                 // TODO: now that lowpri configs exist, we need better attribution for the exact path at which 
79+                 // an error occurred. 
6880                ConfigParseError :: new ( 
6981                    config_file. clone ( ) , 
7082                    ConfigParseErrorKind :: DeserializeError ( error) , 
@@ -109,12 +121,22 @@ impl NextestConfig {
109121    // Helper methods 
110122    // --- 
111123
112-     fn  read_from_sources ( 
124+     fn  read_from_sources < ' a > ( 
113125        workspace_root :  & Utf8Path , 
114126        file :  Option < & Utf8Path > , 
127+         tool_config_files_rev :  impl  Iterator < Item  = & ' a  ToolConfigFile > , 
115128    )  -> Result < ( Utf8PathBuf ,  Config ) ,  ConfigParseError >  { 
116129        // First, get the default config. 
117-         let  builder = Self :: make_default_config ( ) ; 
130+         let  mut  builder = Self :: make_default_config ( ) ; 
131+ 
132+         // Next, merge in tool configs. 
133+         for  ToolConfigFile  { 
134+             config_file, 
135+             tool :  _, 
136+         }  in  tool_config_files_rev
137+         { 
138+             builder = builder. add_source ( File :: new ( config_file. as_str ( ) ,  FileFormat :: Toml ) ) ; 
139+         } 
118140
119141        // Next, merge in the config from the given file. 
120142        let  ( builder,  config_path)  = match  file { 
@@ -169,6 +191,54 @@ impl NextestConfig {
169191    } 
170192} 
171193
194+ /// A tool-specific config file. 
195+ /// 
196+ /// Tool-specific config files are lower priority than repository configs, but higher priority than 
197+ /// the default config shipped with nextest. 
198+ #[ derive( Clone ,  Debug ,  Eq ,  PartialEq ) ]  
199+ pub  struct  ToolConfigFile  { 
200+     /// The name of the tool. 
201+      pub  tool :  String , 
202+ 
203+     /// The path to the config file. 
204+      pub  config_file :  Utf8PathBuf , 
205+ } 
206+ 
207+ impl  FromStr  for  ToolConfigFile  { 
208+     type  Err  = ToolConfigFileParseError ; 
209+ 
210+     fn  from_str ( input :  & str )  -> Result < Self ,  Self :: Err >  { 
211+         match  input. split_once ( ':' )  { 
212+             Some ( ( tool,  config_file) )  => { 
213+                 if  tool. is_empty ( )  { 
214+                     Err ( ToolConfigFileParseError :: EmptyToolName  { 
215+                         input :  input. to_owned ( ) , 
216+                     } ) 
217+                 }  else  if  config_file. is_empty ( )  { 
218+                     Err ( ToolConfigFileParseError :: EmptyConfigFile  { 
219+                         input :  input. to_owned ( ) , 
220+                     } ) 
221+                 }  else  { 
222+                     let  config_file = Utf8Path :: new ( config_file) ; 
223+                     if  config_file. is_absolute ( )  { 
224+                         Ok ( Self  { 
225+                             tool :  tool. to_owned ( ) , 
226+                             config_file :  Utf8PathBuf :: from ( config_file) , 
227+                         } ) 
228+                     }  else  { 
229+                         Err ( ToolConfigFileParseError :: ConfigFileNotAbsolute  { 
230+                             config_file :  config_file. to_owned ( ) , 
231+                         } ) 
232+                     } 
233+                 } 
234+             } 
235+             None  => Err ( ToolConfigFileParseError :: InvalidFormat  { 
236+                 input :  input. to_owned ( ) , 
237+             } ) , 
238+         } 
239+     } 
240+ } 
241+ 
172242/// A configuration profile for nextest. Contains most configuration used by the nextest runner. 
173243/// 
174244/// Returned by [`NextestConfig::profile`]. 
@@ -790,7 +860,7 @@ mod tests {
790860        let  graph = temp_workspace ( workspace_path,  config_contents) ; 
791861
792862        let  nextest_config_result =
793-             NextestConfig :: from_sources ( graph. workspace ( ) . root ( ) ,  & graph,  None ) ; 
863+             NextestConfig :: from_sources ( graph. workspace ( ) . root ( ) ,  & graph,  None ,   [ ] ) ; 
794864
795865        match  expected_default { 
796866            Ok ( expected_default)  => { 
@@ -918,7 +988,8 @@ mod tests {
918988        let  graph = temp_workspace ( workspace_path,  config_contents) ; 
919989        let  package_id = graph. workspace ( ) . iter ( ) . next ( ) . unwrap ( ) . id ( ) ; 
920990
921-         let  config = NextestConfig :: from_sources ( graph. workspace ( ) . root ( ) ,  & graph,  None ) . unwrap ( ) ; 
991+         let  config =
992+             NextestConfig :: from_sources ( graph. workspace ( ) . root ( ) ,  & graph,  None ,  [ ] ) . unwrap ( ) ; 
922993        let  query = TestQuery  { 
923994            binary_query :  BinaryQuery  { 
924995                package_id, 
@@ -939,6 +1010,99 @@ mod tests {
9391010        ) ; 
9401011    } 
9411012
1013+     #[ test]  
1014+     fn  parse_tool_config_file ( )  { 
1015+         cfg_if:: cfg_if! { 
1016+             if  #[ cfg( windows) ]  { 
1017+                 let  valid = [ "tool:C:\\ foo\\ bar" ,  "tool:\\ \\ ?\\ C:\\ foo\\ bar" ] ; 
1018+                 let  invalid = [ "C:\\ foo\\ bar" ,  "tool:\\ foo\\ bar" ,  "tool:" ,  ":/foo/bar" ] ; 
1019+             }  else { 
1020+                 let  valid = [ "tool:/foo/bar" ] ; 
1021+                 let  invalid = [ "/foo/bar" ,  "tool:" ,  ":/foo/bar" ,  "tool:foo/bar" ] ; 
1022+             } 
1023+         } 
1024+ 
1025+         for  valid_input in  valid { 
1026+             valid_input. parse :: < ToolConfigFile > ( ) . unwrap_or_else ( |err| { 
1027+                 panic ! ( "valid input {valid_input} should parse correctly: {err}" ) 
1028+             } ) ; 
1029+         } 
1030+ 
1031+         for  invalid_input in  invalid { 
1032+             invalid_input
1033+                 . parse :: < ToolConfigFile > ( ) 
1034+                 . expect_err ( & format ! ( "invalid input {invalid_input} should error out" ) ) ; 
1035+         } 
1036+     } 
1037+ 
1038+     #[ test]  
1039+     fn  lowpri_config ( )  { 
1040+         let  config_contents = r#" 
1041+         [profile.default] 
1042+         retries = 3 
1043+         "# ; 
1044+ 
1045+         let  lowpri1_config_contents = r#" 
1046+         [profile.default] 
1047+         retries = 4 
1048+ 
1049+         [profile.lowpri] 
1050+         retries = 12 
1051+         "# ; 
1052+ 
1053+         let  lowpri2_config_contents = r#" 
1054+         [profile.default] 
1055+         retries = 5 
1056+ 
1057+         [profile.lowpri] 
1058+         retries = 16 
1059+ 
1060+         [profile.lowpri2] 
1061+         retries = 18 
1062+         "# ; 
1063+ 
1064+         let  workspace_dir = tempdir ( ) . unwrap ( ) ; 
1065+         let  workspace_path:  & Utf8Path  = workspace_dir. path ( ) . try_into ( ) . unwrap ( ) ; 
1066+ 
1067+         let  graph = temp_workspace ( workspace_path,  config_contents) ; 
1068+         let  workspace_root = graph. workspace ( ) . root ( ) ; 
1069+         let  lowpri1_path = workspace_root. join ( ".config/lowpri1.toml" ) ; 
1070+         let  lowpri2_path = workspace_root. join ( ".config/lowpri2.toml" ) ; 
1071+         std:: fs:: write ( & lowpri1_path,  lowpri1_config_contents) . unwrap ( ) ; 
1072+         std:: fs:: write ( & lowpri2_path,  lowpri2_config_contents) . unwrap ( ) ; 
1073+ 
1074+         let  config = NextestConfig :: from_sources ( 
1075+             workspace_root, 
1076+             & graph, 
1077+             None , 
1078+             & [ 
1079+                 ToolConfigFile  { 
1080+                     tool :  "lowpri1" . to_owned ( ) , 
1081+                     config_file :  lowpri1_path, 
1082+                 } , 
1083+                 ToolConfigFile  { 
1084+                     tool :  "lowpri2" . to_owned ( ) , 
1085+                     config_file :  lowpri2_path, 
1086+                 } , 
1087+             ] , 
1088+         ) 
1089+         . expect ( "parsing config failed" ) ; 
1090+ 
1091+         let  default_profile = config
1092+             . profile ( NextestConfig :: DEFAULT_PROFILE ) 
1093+             . expect ( "default profile is present" ) ; 
1094+         // This is present in .config/nextest.toml and is the highest priority 
1095+         assert_eq ! ( default_profile. retries( ) ,  3 ) ; 
1096+ 
1097+         let  lowpri_profile = config. profile ( "lowpri" ) . expect ( "lowpri profile is present" ) ; 
1098+         assert_eq ! ( lowpri_profile. retries( ) ,  12 ) ; 
1099+ 
1100+         let  lowpri2_profile = config
1101+             . profile ( "lowpri2" ) 
1102+             . expect ( "lowpri2 profile is present" ) ; 
1103+         assert_eq ! ( lowpri2_profile. retries( ) ,  18 ) ; 
1104+     } 
1105+ 
9421106    fn  temp_workspace ( temp_dir :  & Utf8Path ,  config_contents :  & str )  -> PackageGraph  { 
9431107        Command :: new ( cargo_path ( ) ) 
9441108            . args ( [ "init" ,  "--lib" ,  "--name=test-package" ] ) 
0 commit comments