6
6
use crate :: {
7
7
errors:: {
8
8
ConfigParseError , ConfigParseErrorKind , ConfigParseOverrideError , ProfileNotFound ,
9
- TestThreadsParseError ,
9
+ TestThreadsParseError , ToolConfigFileParseError ,
10
10
} ,
11
11
reporter:: { FinalStatusLevel , StatusLevel , TestOutputDisplay } ,
12
12
} ;
@@ -54,17 +54,29 @@ impl NextestConfig {
54
54
/// Reads the nextest config from the given file, or if not specified from `.config/nextest.toml`
55
55
/// in the workspace root.
56
56
///
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
58
62
/// default config options.
59
- pub fn from_sources (
63
+ pub fn from_sources < ' a , I > (
60
64
workspace_root : impl Into < Utf8PathBuf > ,
61
65
graph : & PackageGraph ,
62
66
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
+ {
64
72
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) ?;
66
76
let inner: NextestConfigImpl =
67
77
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.
68
80
ConfigParseError :: new (
69
81
config_file. clone ( ) ,
70
82
ConfigParseErrorKind :: DeserializeError ( error) ,
@@ -109,12 +121,22 @@ impl NextestConfig {
109
121
// Helper methods
110
122
// ---
111
123
112
- fn read_from_sources (
124
+ fn read_from_sources < ' a > (
113
125
workspace_root : & Utf8Path ,
114
126
file : Option < & Utf8Path > ,
127
+ tool_config_files_rev : impl Iterator < Item = & ' a ToolConfigFile > ,
115
128
) -> Result < ( Utf8PathBuf , Config ) , ConfigParseError > {
116
129
// 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
+ }
118
140
119
141
// Next, merge in the config from the given file.
120
142
let ( builder, config_path) = match file {
@@ -169,6 +191,54 @@ impl NextestConfig {
169
191
}
170
192
}
171
193
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
+
172
242
/// A configuration profile for nextest. Contains most configuration used by the nextest runner.
173
243
///
174
244
/// Returned by [`NextestConfig::profile`].
@@ -790,7 +860,7 @@ mod tests {
790
860
let graph = temp_workspace ( workspace_path, config_contents) ;
791
861
792
862
let nextest_config_result =
793
- NextestConfig :: from_sources ( graph. workspace ( ) . root ( ) , & graph, None ) ;
863
+ NextestConfig :: from_sources ( graph. workspace ( ) . root ( ) , & graph, None , [ ] ) ;
794
864
795
865
match expected_default {
796
866
Ok ( expected_default) => {
@@ -918,7 +988,8 @@ mod tests {
918
988
let graph = temp_workspace ( workspace_path, config_contents) ;
919
989
let package_id = graph. workspace ( ) . iter ( ) . next ( ) . unwrap ( ) . id ( ) ;
920
990
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 ( ) ;
922
993
let query = TestQuery {
923
994
binary_query : BinaryQuery {
924
995
package_id,
@@ -939,6 +1010,99 @@ mod tests {
939
1010
) ;
940
1011
}
941
1012
1013
+ #[ test]
1014
+ fn parse_tool_config_file ( ) {
1015
+ cfg_if:: cfg_if! {
1016
+ if #[ cfg( windows) ] {
1017
+ let valid = [ "tool:/foo/bar" , "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
+
942
1106
fn temp_workspace ( temp_dir : & Utf8Path , config_contents : & str ) -> PackageGraph {
943
1107
Command :: new ( cargo_path ( ) )
944
1108
. args ( [ "init" , "--lib" , "--name=test-package" ] )
0 commit comments