@@ -58,7 +58,7 @@ impl Run for TestCommand {
58
58
self . log_action_context ( "source" , & testcase. source . display ( ) ) ;
59
59
self . log_action_context ( "output" , & testcase. output_file . display ( ) ) ;
60
60
testcase. build ( manifest) ;
61
- bless ( self . bless , & testcase) ;
61
+ self . bless ( self . bless , & testcase) ;
62
62
}
63
63
TestType :: Compile => {
64
64
self . log_action_start ( "TEST Compile" , & testcase. name ) ;
@@ -73,6 +73,7 @@ impl Run for TestCommand {
73
73
testcase. build_lib ( manifest) ;
74
74
}
75
75
}
76
+ self . check_and_run_directives ( & testcase) ;
76
77
}
77
78
}
78
79
@@ -84,7 +85,6 @@ impl Run for TestCommand {
84
85
impl TestCommand {
85
86
pub fn collect_testcases ( & self , manifest : & Manifest ) -> Vec < TestCase > {
86
87
let mut cases = vec ! [ ] ;
87
-
88
88
let verbose = self . verbose ;
89
89
90
90
// Examples
@@ -107,7 +107,7 @@ impl TestCommand {
107
107
cases. push ( testcase) ;
108
108
}
109
109
110
- // Bless tests - the output should be the same as the last run
110
+ // Bless tests
111
111
for case in glob ( "tests/bless/*.rs" ) . unwrap ( ) {
112
112
let case = case. unwrap ( ) ;
113
113
let filename = case. file_stem ( ) . unwrap ( ) ;
@@ -117,27 +117,26 @@ impl TestCommand {
117
117
cases. push ( testcase) ;
118
118
}
119
119
120
- // Collect test-auxiliary
121
- let aux_use = regex:: Regex :: new ( r"(?m)//@\s*aux-build:(?P<fname>.*)" ) . unwrap ( ) ;
120
+ // Collect and process auxiliary builds from directives
122
121
let mut auxiliaries = vec ! [ ] ;
123
122
for case in cases. iter ( ) {
124
- let content = std:: fs:: read_to_string ( & case. source ) . unwrap ( ) ;
125
- for cap in aux_use. captures_iter ( & content) {
126
- println ! ( "{:?}" , case. source) ;
127
- let fname = cap. name ( "fname" ) . unwrap ( ) . as_str ( ) ;
128
- let source = Path :: new ( "tests/auxiliary" ) . join ( fname) ;
129
- let filename = source. file_stem ( ) . unwrap ( ) ;
130
- let name = format ! ( "auxiliary/{}" , filename. to_string_lossy( ) ) ;
131
-
132
- // deduplication
133
- if auxiliaries. iter ( ) . any ( |aux : & TestCase | aux. name == name) {
134
- continue ;
123
+ let directives = case. parse_directives ( ) ;
124
+ for directive in directives {
125
+ if let TestDirective :: AuxBuild ( fname) = directive {
126
+ let source = Path :: new ( "tests/auxiliary" ) . join ( & fname) ;
127
+ let filename = source. file_stem ( ) . unwrap ( ) ;
128
+ let name = format ! ( "auxiliary/{}" , filename. to_string_lossy( ) ) ;
129
+
130
+ // deduplication
131
+ if auxiliaries. iter ( ) . any ( |aux : & TestCase | aux. name == name) {
132
+ continue ;
133
+ }
134
+
135
+ let output_file = manifest. out_dir . join ( filename) ; // aux files are output to the base directory
136
+ let testcase =
137
+ TestCase :: new ( name, source, output_file, TestType :: CompileLib , verbose) ;
138
+ auxiliaries. push ( testcase) ;
135
139
}
136
-
137
- let output_file = manifest. out_dir . join ( filename) ; // aux files are output to the base directory
138
- let testcase =
139
- TestCase :: new ( name, source, output_file, TestType :: CompileLib , verbose) ;
140
- auxiliaries. push ( testcase) ;
141
140
}
142
141
}
143
142
@@ -146,6 +145,141 @@ impl TestCommand {
146
145
testcases. extend ( cases) ;
147
146
testcases
148
147
}
148
+
149
+ fn bless ( & self , update : bool , case : & TestCase ) {
150
+ let output = case. generated ( ) ;
151
+ let blessed = case. source . with_extension ( "c" ) ;
152
+
153
+ self . log_action_context ( "checking" , & blessed. display ( ) ) ;
154
+ if update {
155
+ self . log_action_context ( "updating" , & blessed. display ( ) ) ;
156
+ std:: fs:: copy ( output, & blessed) . unwrap ( ) ;
157
+ self . log_action_context ( "result" , "updated" ) ;
158
+ } else {
159
+ let output = std:: fs:: read_to_string ( output) . unwrap ( ) ;
160
+ let blessed = std:: fs:: read_to_string ( & blessed) . unwrap ( ) ;
161
+
162
+ let diff = TextDiff :: from_lines ( & blessed, & output) ;
163
+ if diff. ratio ( ) < 1.0 {
164
+ cprintln ! ( "<r,s>output does not match blessed output</r,s>" ) ;
165
+ for change in diff. iter_all_changes ( ) {
166
+ let lineno = change. old_index ( ) . unwrap_or ( change. new_index ( ) . unwrap_or ( 0 ) ) ;
167
+ match change. tag ( ) {
168
+ ChangeTag :: Equal => print ! ( " {:4}| {}" , lineno, change) ,
169
+ ChangeTag :: Insert => cprint ! ( "<g>+{:4}| {}</g>" , lineno, change) ,
170
+ ChangeTag :: Delete => cprint ! ( "<r>-{:4}| {}</r>" , lineno, change) ,
171
+ }
172
+ }
173
+ std:: process:: exit ( 1 ) ;
174
+ }
175
+ self . log_action_context ( "result" , "passed" ) ;
176
+ }
177
+ }
178
+
179
+ /// Run a runtime test and check its output against directives
180
+ fn check_and_run_directives ( & self , testcase : & TestCase ) {
181
+ // Parse directives from source
182
+ let directives = testcase. parse_directives ( ) ;
183
+ self . log_action_context ( "directives" , & format ! ( "found {} directives" , directives. len( ) ) ) ;
184
+
185
+ let mut runpass = false ;
186
+ let mut exitcode = None ;
187
+ let mut stdout = None ;
188
+ let mut stderr = None ;
189
+
190
+ // Check each directive
191
+ for directive in directives {
192
+ match directive {
193
+ TestDirective :: RunPass => runpass = true ,
194
+ TestDirective :: CheckStdout ( expected) => stdout = Some ( expected) ,
195
+ TestDirective :: CheckStderr ( expected) => stderr = Some ( expected) ,
196
+ TestDirective :: ExitCode ( expected) => exitcode = Some ( expected) ,
197
+ TestDirective :: AuxBuild ( _) => {
198
+ // AuxBuild directives are handled during test collection
199
+ // No need to check them during test execution
200
+ }
201
+ }
202
+ }
203
+
204
+ if !runpass && ( exitcode. is_some ( ) | stdout. is_some ( ) | stderr. is_some ( ) ) {
205
+ panic ! ( "Directives conflicts, lack of '//@ run-pass'" ) ;
206
+ }
207
+
208
+ if runpass {
209
+ self . run_and_check_output ( testcase, exitcode, stdout, stderr) ;
210
+ }
211
+
212
+ self . log_action_context ( "result" , "all checks passed" ) ;
213
+ }
214
+
215
+ fn run_and_check_output (
216
+ & self ,
217
+ testcase : & TestCase ,
218
+ expected_exit : Option < i32 > ,
219
+ expected_stdout : Option < String > ,
220
+ expected_stderr : Option < String > ,
221
+ ) {
222
+ // Run the test
223
+ self . log_action_context ( "running" , & testcase. output_file . display ( ) ) ;
224
+ let output = std:: process:: Command :: new ( & testcase. output_file )
225
+ . output ( )
226
+ . unwrap_or_else ( |e| panic ! ( "failed to run {}: {}" , testcase. output_file. display( ) , e) ) ;
227
+
228
+ // Get actual outputs
229
+ let actual_return = output. status . code ( ) . unwrap_or_else ( || {
230
+ panic ! ( "Process terminated by signal: {}" , testcase. output_file. display( ) )
231
+ } ) ;
232
+ let actual_stdout = String :: from_utf8_lossy ( & output. stdout ) . into_owned ( ) ;
233
+ let actual_stderr = String :: from_utf8_lossy ( & output. stderr ) . into_owned ( ) ;
234
+
235
+ {
236
+ let expected_exit = expected_exit. unwrap_or ( 0 ) ;
237
+ self . log_action_context ( "checking exit code" , & expected_exit. to_string ( ) ) ;
238
+ if actual_return != expected_exit {
239
+ cprintln ! ( "<r,s>exit code does not match expected value</r,s>" ) ;
240
+ cprintln ! ( "expected: {}" , expected_exit) ;
241
+ cprintln ! ( "actual: {}" , actual_return) ;
242
+ std:: process:: exit ( 1 ) ;
243
+ }
244
+ self . log_action_context ( "exit code" , "passed" ) ;
245
+ }
246
+
247
+ if let Some ( expected_stdout) = expected_stdout {
248
+ self . log_action_context ( "checking stdout" , & expected_stdout) ;
249
+ let diff = TextDiff :: from_lines ( & expected_stdout, & actual_stdout) ;
250
+ if diff. ratio ( ) < 1.0 {
251
+ cprintln ! ( "<r,s>stdout does not match expected output</r,s>" ) ;
252
+ for change in diff. iter_all_changes ( ) {
253
+ let lineno = change. old_index ( ) . unwrap_or ( change. new_index ( ) . unwrap_or ( 0 ) ) ;
254
+ match change. tag ( ) {
255
+ ChangeTag :: Equal => print ! ( " {:4}| {}" , lineno, change) ,
256
+ ChangeTag :: Insert => cprint ! ( "<g>+{:4}| {}</g>" , lineno, change) ,
257
+ ChangeTag :: Delete => cprint ! ( "<r>-{:4}| {}</r>" , lineno, change) ,
258
+ }
259
+ }
260
+ std:: process:: exit ( 1 ) ;
261
+ }
262
+ self . log_action_context ( "stdout" , "passed" ) ;
263
+ }
264
+
265
+ if let Some ( expected_stderr) = expected_stderr {
266
+ self . log_action_context ( "checking stderr" , & expected_stderr) ;
267
+ let diff = TextDiff :: from_lines ( & expected_stderr, & actual_stderr) ;
268
+ if diff. ratio ( ) < 1.0 {
269
+ cprintln ! ( "<r,s>stderr does not match expected output</r,s>" ) ;
270
+ for change in diff. iter_all_changes ( ) {
271
+ let lineno = change. old_index ( ) . unwrap_or ( change. new_index ( ) . unwrap_or ( 0 ) ) ;
272
+ match change. tag ( ) {
273
+ ChangeTag :: Equal => print ! ( " {:4}| {}" , lineno, change) ,
274
+ ChangeTag :: Insert => cprint ! ( "<g>+{:4}| {}</g>" , lineno, change) ,
275
+ ChangeTag :: Delete => cprint ! ( "<r>-{:4}| {}</r>" , lineno, change) ,
276
+ }
277
+ }
278
+ std:: process:: exit ( 1 ) ;
279
+ }
280
+ self . log_action_context ( "stderr" , "passed" ) ;
281
+ }
282
+ }
149
283
}
150
284
151
285
#[ derive( Debug ) ]
@@ -159,6 +293,7 @@ pub enum TestType {
159
293
/// Bless test - the output should be the same as the last run
160
294
Bless ,
161
295
}
296
+
162
297
impl TestType {
163
298
pub fn as_str ( & self ) -> & ' static str {
164
299
match self {
@@ -241,6 +376,58 @@ impl TestCase {
241
376
assert ! ( generated. is_some( ) , "could not find {case}'s generated file" ) ;
242
377
generated. unwrap ( ) . path ( )
243
378
}
379
+
380
+ /// Parse test directives from the source file
381
+ fn parse_directives ( & self ) -> Vec < TestDirective > {
382
+ let source = std:: fs:: read_to_string ( & self . source )
383
+ . unwrap_or_else ( |e| panic ! ( "failed to read {}: {}" , self . source. display( ) , e) ) ;
384
+
385
+ let mut directives = Vec :: new ( ) ;
386
+
387
+ // Regular expressions for matching directives
388
+ let run_pass = regex:: Regex :: new ( r"^//@\s*run-pass" ) . unwrap ( ) ;
389
+ let stdout_re = regex:: Regex :: new ( r"^//@\s*check-stdout:\s*(.*)" ) . unwrap ( ) ;
390
+ let stderr_re = regex:: Regex :: new ( r"^//@\s*check-stderr:\s*(.*)" ) . unwrap ( ) ;
391
+ let exit_re = regex:: Regex :: new ( r"^//@\s*exit-code:\s*(\d+)" ) . unwrap ( ) ;
392
+ let aux_re = regex:: Regex :: new ( r"^//@\s*aux-build:\s*(.*)" ) . unwrap ( ) ;
393
+ // Regex to match any directive pattern
394
+ let directive_re = regex:: Regex :: new ( r"^//@\s*([^:]+)" ) . unwrap ( ) ;
395
+
396
+ for ( line_num, line) in source. lines ( ) . enumerate ( ) {
397
+ if let Some ( _cap) = run_pass. captures ( line) {
398
+ directives. push ( TestDirective :: RunPass ) ;
399
+ } else if let Some ( cap) = stdout_re. captures ( line) {
400
+ let content = cap[ 1 ] . trim ( ) . to_string ( ) ;
401
+ directives. push ( TestDirective :: CheckStdout ( content) ) ;
402
+ } else if let Some ( cap) = stderr_re. captures ( line) {
403
+ let content = cap[ 1 ] . trim ( ) . to_string ( ) ;
404
+ directives. push ( TestDirective :: CheckStderr ( content) ) ;
405
+ } else if let Some ( cap) = exit_re. captures ( line) {
406
+ if let Ok ( code) = cap[ 1 ] . parse ( ) {
407
+ directives. push ( TestDirective :: ExitCode ( code) ) ;
408
+ } else {
409
+ panic ! (
410
+ "{}:{}: invalid exit code in directive" ,
411
+ self . source. display( ) ,
412
+ line_num + 1
413
+ ) ;
414
+ }
415
+ } else if let Some ( cap) = aux_re. captures ( line) {
416
+ let fname = cap[ 1 ] . trim ( ) . to_string ( ) ;
417
+ directives. push ( TestDirective :: AuxBuild ( fname) ) ;
418
+ } else if let Some ( cap) = directive_re. captures ( line) {
419
+ let directive_name = cap[ 1 ] . trim ( ) ;
420
+ panic ! (
421
+ "{}:{}: unknown directive '{}', supported directives are: check-stdout, check-stderr, exit-code, aux-build" ,
422
+ self . source. display( ) ,
423
+ line_num + 1 ,
424
+ directive_name
425
+ ) ;
426
+ }
427
+ }
428
+
429
+ directives
430
+ }
244
431
}
245
432
246
433
struct FileChecker {
@@ -288,27 +475,18 @@ impl FileChecker {
288
475
}
289
476
}
290
477
291
- fn bless ( update : bool , case : & TestCase ) {
292
- let output = case. generated ( ) ;
293
- let blessed = case. source . with_extension ( "c" ) ;
294
- if update {
295
- std:: fs:: copy ( output, blessed) . unwrap ( ) ;
296
- } else {
297
- let output = std:: fs:: read_to_string ( output) . unwrap ( ) ;
298
- let blessed = std:: fs:: read_to_string ( blessed) . unwrap ( ) ;
299
-
300
- let diff = TextDiff :: from_lines ( & blessed, & output) ;
301
- if diff. ratio ( ) < 1.0 {
302
- cprintln ! ( "<r,s>output does not match blessed output</r,s>" ) ;
303
- for change in diff. iter_all_changes ( ) {
304
- let lineno = change. old_index ( ) . unwrap_or ( change. new_index ( ) . unwrap_or ( 0 ) ) ;
305
- match change. tag ( ) {
306
- ChangeTag :: Equal => print ! ( " {:4}| {}" , lineno, change) ,
307
- ChangeTag :: Insert => cprint ! ( "<g>+{:4}| {}</g>" , lineno, change) ,
308
- ChangeTag :: Delete => cprint ! ( "<r>-{:4}| {}</r>" , lineno, change) ,
309
- }
310
- }
311
- std:: process:: exit ( 1 ) ;
312
- }
313
- }
478
+ /// Test directives that can appear in source files
479
+ #[ derive( Debug ) ]
480
+ enum TestDirective {
481
+ /// Compile and run a testcase,
482
+ /// expect a success (exit with 0)
483
+ RunPass ,
484
+ /// Expected stdout content
485
+ CheckStdout ( String ) ,
486
+ /// Expected stderr content
487
+ CheckStderr ( String ) ,
488
+ /// Expected exit code
489
+ ExitCode ( i32 ) ,
490
+ /// Auxiliary build requirement
491
+ AuxBuild ( String ) ,
314
492
}
0 commit comments