1 | #![allow ( |
2 | clippy::enum_variant_names, |
3 | clippy::useless_format, |
4 | clippy::too_many_arguments, |
5 | rustc::internal |
6 | )] |
7 | #![deny (missing_docs)] |
8 | |
9 | //! A crate to run the Rust compiler (or other binaries) and test their command line output. |
10 | |
11 | use bstr::ByteSlice; |
12 | pub use color_eyre; |
13 | use color_eyre::eyre::{eyre, Result}; |
14 | use crossbeam_channel::{unbounded, Receiver, Sender}; |
15 | use dependencies::{Build, BuildManager}; |
16 | use lazy_static::lazy_static; |
17 | use parser::{ErrorMatch, MaybeSpanned, OptWithLine, Revisioned, Spanned}; |
18 | use regex::bytes::{Captures, Regex}; |
19 | use rustc_stderr::{Level, Message, Span}; |
20 | use status_emitter::{StatusEmitter, TestStatus}; |
21 | use std::borrow::Cow; |
22 | use std::collections::{HashSet, VecDeque}; |
23 | use std::ffi::OsString; |
24 | use std::num::NonZeroUsize; |
25 | use std::path::{Component, Path, PathBuf, Prefix}; |
26 | use std::process::{Command, ExitStatus, Output}; |
27 | use std::thread; |
28 | |
29 | use crate::parser::{Comments, Condition}; |
30 | |
31 | mod cmd; |
32 | mod config; |
33 | mod dependencies; |
34 | mod diff; |
35 | mod error; |
36 | pub mod github_actions; |
37 | mod mode; |
38 | mod parser; |
39 | mod rustc_stderr; |
40 | pub mod status_emitter; |
41 | #[cfg (test)] |
42 | mod tests; |
43 | |
44 | pub use cmd::*; |
45 | pub use config::*; |
46 | pub use error::*; |
47 | pub use mode::*; |
48 | |
49 | /// A filter's match rule. |
50 | #[derive (Clone, Debug)] |
51 | pub enum Match { |
52 | /// If the regex matches, the filter applies |
53 | Regex(Regex), |
54 | /// If the exact byte sequence is found, the filter applies |
55 | Exact(Vec<u8>), |
56 | /// Uses a heuristic to find backslashes in windows style paths |
57 | PathBackslash, |
58 | } |
59 | impl Match { |
60 | fn replace_all<'a>(&self, text: &'a [u8], replacement: &[u8]) -> Cow<'a, [u8]> { |
61 | match self { |
62 | Match::Regex(regex) => regex.replace_all(text, replacement), |
63 | Match::Exact(needle) => text.replace(needle, replacement).into(), |
64 | Match::PathBackslash => { |
65 | lazy_static! { |
66 | static ref PATH_RE: Regex = Regex::new( |
67 | r"(?x) |
68 | (?: |
69 | # Match paths to files with extensions that don't include spaces |
70 | \\(?:[\pL\pN.\-_']+[/\\])*[\pL\pN.\-_']+\.\pL+ |
71 | | |
72 | # Allow spaces in absolute paths |
73 | [A-Z]:\\(?:[\pL\pN.\-_'\ ]+[/\\])+ |
74 | )" , |
75 | ) |
76 | .unwrap(); |
77 | } |
78 | |
79 | PATH_RE.replace_all(text, |caps: &Captures<'_>| { |
80 | caps[0].replace(r"\" , replacement) |
81 | }) |
82 | } |
83 | } |
84 | } |
85 | } |
86 | |
87 | impl From<&'_ Path> for Match { |
88 | fn from(v: &Path) -> Self { |
89 | let mut v: String = v.display().to_string(); |
90 | // Normalize away windows canonicalized paths. |
91 | if v.starts_with(r"\\?\" ) { |
92 | v.drain(range:0..4); |
93 | } |
94 | let mut v: Vec = v.into_bytes(); |
95 | // Normalize paths on windows to use slashes instead of backslashes, |
96 | // So that paths are rendered the same on all systems. |
97 | for c: &mut u8 in &mut v { |
98 | if *c == b' \\' { |
99 | *c = b'/' ; |
100 | } |
101 | } |
102 | Self::Exact(v) |
103 | } |
104 | } |
105 | |
106 | impl From<Regex> for Match { |
107 | fn from(v: Regex) -> Self { |
108 | Self::Regex(v) |
109 | } |
110 | } |
111 | |
112 | /// Replacements to apply to output files. |
113 | pub type Filter = Vec<(Match, &'static [u8])>; |
114 | |
115 | /// Run all tests as described in the config argument. |
116 | /// Will additionally process command line arguments. |
117 | pub fn run_tests(mut config: Config) -> Result<()> { |
118 | let args: Args = Args::test()?; |
119 | if let Format::Pretty = args.format { |
120 | println!("Compiler: {}" , config.program.display()); |
121 | } |
122 | |
123 | let name: String = config.root_dir.display().to_string(); |
124 | |
125 | let text: Text = match args.format { |
126 | Format::Terse => status_emitter::Text::quiet(), |
127 | Format::Pretty => status_emitter::Text::verbose(), |
128 | }; |
129 | config.with_args(&args, default_bless:true); |
130 | |
131 | run_tests_generic( |
132 | configs:vec![config], |
133 | default_file_filter, |
134 | default_per_file_config, |
135 | (text, status_emitter::Gha::<true> { name }), |
136 | ) |
137 | } |
138 | |
139 | /// The filter used by `run_tests` to only run on `.rs` files that are |
140 | /// specified by [`Config::filter_files`] and [`Config::skip_files`]. |
141 | pub fn default_file_filter(path: &Path, config: &Config) -> bool { |
142 | path.extension().is_some_and(|ext: &OsStr| ext == "rs" ) && default_any_file_filter(path, config) |
143 | } |
144 | |
145 | /// Run on all files that are specified by [`Config::filter_files`] and |
146 | /// [`Config::skip_files`]. |
147 | /// |
148 | /// To only include rust files see [`default_file_filter`]. |
149 | pub fn default_any_file_filter(path: &Path, config: &Config) -> bool { |
150 | let path: String = path.display().to_string(); |
151 | let contains_path: impl Fn(&[String]) -> bool = |files: &[String]| { |
152 | files.iter().any(|f: &String| { |
153 | if config.filter_exact { |
154 | *f == path |
155 | } else { |
156 | path.contains(f) |
157 | } |
158 | }) |
159 | }; |
160 | |
161 | if contains_path(&config.skip_files) { |
162 | return false; |
163 | } |
164 | |
165 | config.filter_files.is_empty() || contains_path(&config.filter_files) |
166 | } |
167 | |
168 | /// The default per-file config used by `run_tests`. |
169 | pub fn default_per_file_config(config: &mut Config, _path: &Path, file_contents: &[u8]) { |
170 | // Heuristic: |
171 | // * if the file contains `#[test]`, automatically pass `--cfg test`. |
172 | // * if the file does not contain `fn main()` or `#[start]`, automatically pass `--crate-type=lib`. |
173 | // This avoids having to spam `fn main() {}` in almost every test. |
174 | if file_contents.find(needle:b"#[proc_macro" ).is_some() { |
175 | config.program.args.push("--crate-type=proc-macro" .into()) |
176 | } else if file_contents.find(needle:b"#[test]" ).is_some() { |
177 | config.program.args.push("--test" .into()); |
178 | } else if file_contents.find(needle:b"fn main()" ).is_none() |
179 | && file_contents.find(needle:b"#[start]" ).is_none() |
180 | { |
181 | config.program.args.push("--crate-type=lib" .into()); |
182 | } |
183 | } |
184 | |
185 | /// Create a command for running a single file, with the settings from the `config` argument. |
186 | /// Ignores various settings from `Config` that relate to finding test files. |
187 | pub fn test_command(mut config: Config, path: &Path) -> Result<Command> { |
188 | config.fill_host_and_target()?; |
189 | let extra_args: Vec = config.build_dependencies()?; |
190 | |
191 | let comments: Comments = |
192 | Comments::parse_file(path)?.map_err(|errors: Vec| color_eyre::eyre::eyre!(" {errors:#?}" ))?; |
193 | let mut result: Command = build_command(path, &config, revision:"" , &comments).unwrap(); |
194 | result.args(extra_args); |
195 | |
196 | Ok(result) |
197 | } |
198 | |
199 | /// The possible non-failure results a single test can have. |
200 | pub enum TestOk { |
201 | /// The test passed |
202 | Ok, |
203 | /// The test was ignored due to a rule (`//@only-*` or `//@ignore-*`) |
204 | Ignored, |
205 | /// The test was filtered with the `file_filter` argument. |
206 | Filtered, |
207 | } |
208 | |
209 | /// The possible results a single test can have. |
210 | pub type TestResult = Result<TestOk, Errored>; |
211 | |
212 | /// Information about a test failure. |
213 | #[derive (Debug)] |
214 | pub struct Errored { |
215 | /// Command that failed |
216 | command: Command, |
217 | /// The errors that were encountered. |
218 | errors: Vec<Error>, |
219 | /// The full stderr of the test run. |
220 | stderr: Vec<u8>, |
221 | /// The full stdout of the test run. |
222 | stdout: Vec<u8>, |
223 | } |
224 | |
225 | struct TestRun { |
226 | result: TestResult, |
227 | status: Box<dyn status_emitter::TestStatus>, |
228 | } |
229 | |
230 | /// A version of `run_tests` that allows more fine-grained control over running tests. |
231 | /// |
232 | /// All `configs` are being run in parallel. |
233 | /// If multiple configs are provided, the [`Config::threads`] value of the first one is used; |
234 | /// the thread count of all other configs is ignored. |
235 | pub fn run_tests_generic( |
236 | mut configs: Vec<Config>, |
237 | file_filter: impl Fn(&Path, &Config) -> bool + Sync, |
238 | per_file_config: impl Fn(&mut Config, &Path, &[u8]) + Sync, |
239 | status_emitter: impl StatusEmitter + Send, |
240 | ) -> Result<()> { |
241 | // Nexttest emulation: we act as if we are one single test. |
242 | if configs.iter().any(|c| c.list) { |
243 | if configs.iter().any(|c| !c.run_only_ignored) { |
244 | println!("ui_test: test" ); |
245 | } |
246 | return Ok(()); |
247 | } |
248 | for config in &mut configs { |
249 | if config.filter_exact |
250 | && config.filter_files.len() == 1 |
251 | && config.filter_files[0] == "ui_test" |
252 | { |
253 | config.filter_exact = false; |
254 | config.filter_files.clear(); |
255 | } |
256 | } |
257 | |
258 | for config in &mut configs { |
259 | config.fill_host_and_target()?; |
260 | } |
261 | |
262 | let build_manager = BuildManager::new(&status_emitter); |
263 | |
264 | let mut results = vec![]; |
265 | |
266 | let num_threads = match configs.first().and_then(|config| config.threads) { |
267 | Some(threads) => threads, |
268 | None => match std::env::var_os("RUST_TEST_THREADS" ) { |
269 | Some(n) => n |
270 | .to_str() |
271 | .ok_or_else(|| eyre!("could not parse RUST_TEST_THREADS env var" ))? |
272 | .parse()?, |
273 | None => std::thread::available_parallelism()?, |
274 | }, |
275 | }; |
276 | |
277 | run_and_collect( |
278 | num_threads, |
279 | |submit| { |
280 | let mut todo = VecDeque::new(); |
281 | for config in &configs { |
282 | todo.push_back((config.root_dir.clone(), config)); |
283 | } |
284 | while let Some((path, config)) = todo.pop_front() { |
285 | if path.is_dir() { |
286 | if path.file_name().unwrap() == "auxiliary" { |
287 | continue; |
288 | } |
289 | // Enqueue everything inside this directory. |
290 | // We want it sorted, to have some control over scheduling of slow tests. |
291 | let mut entries = std::fs::read_dir(path) |
292 | .unwrap() |
293 | .map(|e| e.unwrap().path()) |
294 | .collect::<Vec<_>>(); |
295 | entries.sort_by(|a, b| a.file_name().cmp(&b.file_name())); |
296 | for entry in entries { |
297 | todo.push_back((entry, config)); |
298 | } |
299 | } else if file_filter(&path, config) { |
300 | let status = status_emitter.register_test(path); |
301 | // Forward .rs files to the test workers. |
302 | submit.send((status, config)).unwrap(); |
303 | } |
304 | } |
305 | }, |
306 | |receive, finished_files_sender| -> Result<()> { |
307 | for (status, config) in receive { |
308 | let path = status.path(); |
309 | let file_contents = std::fs::read(path).unwrap(); |
310 | let mut config = config.clone(); |
311 | per_file_config(&mut config, path, &file_contents); |
312 | let result = match std::panic::catch_unwind(|| { |
313 | parse_and_test_file(&build_manager, &status, config, file_contents) |
314 | }) { |
315 | Ok(Ok(res)) => res, |
316 | Ok(Err(err)) => { |
317 | finished_files_sender.send(TestRun { |
318 | result: Err(err), |
319 | status, |
320 | })?; |
321 | continue; |
322 | } |
323 | Err(err) => { |
324 | finished_files_sender.send(TestRun { |
325 | result: Err(Errored { |
326 | command: Command::new("<unknown>" ), |
327 | errors: vec![Error::Bug( |
328 | *Box::<dyn std::any::Any + Send + 'static>::downcast::<String>( |
329 | err, |
330 | ) |
331 | .unwrap(), |
332 | )], |
333 | stderr: vec![], |
334 | stdout: vec![], |
335 | }), |
336 | status, |
337 | })?; |
338 | continue; |
339 | } |
340 | }; |
341 | for result in result { |
342 | finished_files_sender.send(result)?; |
343 | } |
344 | } |
345 | Ok(()) |
346 | }, |
347 | |finished_files_recv| { |
348 | for run in finished_files_recv { |
349 | run.status.done(&run.result); |
350 | |
351 | results.push(run); |
352 | } |
353 | }, |
354 | )?; |
355 | |
356 | let mut failures = vec![]; |
357 | let mut succeeded = 0; |
358 | let mut ignored = 0; |
359 | let mut filtered = 0; |
360 | |
361 | for run in results { |
362 | match run.result { |
363 | Ok(TestOk::Ok) => succeeded += 1, |
364 | Ok(TestOk::Ignored) => ignored += 1, |
365 | Ok(TestOk::Filtered) => filtered += 1, |
366 | Err(errored) => failures.push((run.status, errored)), |
367 | } |
368 | } |
369 | |
370 | let mut failure_emitter = status_emitter.finalize(failures.len(), succeeded, ignored, filtered); |
371 | for ( |
372 | status, |
373 | Errored { |
374 | command, |
375 | errors, |
376 | stderr, |
377 | stdout, |
378 | }, |
379 | ) in &failures |
380 | { |
381 | let _guard = status.failed_test(command, stderr, stdout); |
382 | failure_emitter.test_failure(status, errors); |
383 | } |
384 | |
385 | if failures.is_empty() { |
386 | Ok(()) |
387 | } else { |
388 | Err(eyre!("tests failed" )) |
389 | } |
390 | } |
391 | |
392 | /// A generic multithreaded runner that has a thread for producing work, |
393 | /// a thread for collecting work, and `num_threads` threads for doing the work. |
394 | pub fn run_and_collect<SUBMISSION: Send, RESULT: Send>( |
395 | num_threads: NonZeroUsize, |
396 | submitter: impl FnOnce(Sender<SUBMISSION>) + Send, |
397 | runner: impl Sync + Fn(&Receiver<SUBMISSION>, Sender<RESULT>) -> Result<()>, |
398 | collector: impl FnOnce(Receiver<RESULT>) + Send, |
399 | ) -> Result<()> { |
400 | // A channel for files to process |
401 | let (submit, receive) = unbounded(); |
402 | |
403 | thread::scope(|s| { |
404 | // Create a thread that is in charge of walking the directory and submitting jobs. |
405 | // It closes the channel when it is done. |
406 | s.spawn(|| submitter(submit)); |
407 | |
408 | // A channel for the messages emitted by the individual test threads. |
409 | // Used to produce live updates while running the tests. |
410 | let (finished_files_sender, finished_files_recv) = unbounded(); |
411 | |
412 | s.spawn(|| collector(finished_files_recv)); |
413 | |
414 | let mut threads = vec![]; |
415 | |
416 | // Create N worker threads that receive files to test. |
417 | for _ in 0..num_threads.get() { |
418 | let finished_files_sender = finished_files_sender.clone(); |
419 | threads.push(s.spawn(|| runner(&receive, finished_files_sender))); |
420 | } |
421 | |
422 | for thread in threads { |
423 | thread.join().unwrap()?; |
424 | } |
425 | Ok(()) |
426 | }) |
427 | } |
428 | |
429 | fn parse_and_test_file( |
430 | build_manager: &BuildManager<'_>, |
431 | status: &dyn TestStatus, |
432 | mut config: Config, |
433 | file_contents: Vec<u8>, |
434 | ) -> Result<Vec<TestRun>, Errored> { |
435 | let comments = parse_comments(&file_contents)?; |
436 | const EMPTY: &[String] = &[String::new()]; |
437 | // Run the test for all revisions |
438 | let revisions = comments.revisions.as_deref().unwrap_or(EMPTY); |
439 | let mut built_deps = false; |
440 | Ok(revisions |
441 | .iter() |
442 | .map(|revision| { |
443 | let status = status.for_revision(revision); |
444 | // Ignore file if only/ignore rules do (not) apply |
445 | if !status.test_file_conditions(&comments, &config) { |
446 | return TestRun { |
447 | result: Ok(TestOk::Ignored), |
448 | status, |
449 | }; |
450 | } |
451 | |
452 | if !built_deps { |
453 | status.update_status("waiting for dependencies to finish building" .into()); |
454 | match build_manager.build(Build::Dependencies, &config) { |
455 | Ok(extra_args) => config.program.args.extend(extra_args), |
456 | Err(err) => { |
457 | return TestRun { |
458 | result: Err(err), |
459 | status, |
460 | } |
461 | } |
462 | } |
463 | status.update_status(String::new()); |
464 | built_deps = true; |
465 | } |
466 | |
467 | let result = status.run_test(build_manager, &config, &comments); |
468 | TestRun { result, status } |
469 | }) |
470 | .collect()) |
471 | } |
472 | |
473 | fn parse_comments(file_contents: &[u8]) -> Result<Comments, Errored> { |
474 | match Comments::parse(content:file_contents) { |
475 | Ok(comments: Comments) => Ok(comments), |
476 | Err(errors: Vec) => Err(Errored { |
477 | command: Command::new(program:"parse comments" ), |
478 | errors, |
479 | stderr: vec![], |
480 | stdout: vec![], |
481 | }), |
482 | } |
483 | } |
484 | |
485 | fn build_command( |
486 | path: &Path, |
487 | config: &Config, |
488 | revision: &str, |
489 | comments: &Comments, |
490 | ) -> Result<Command, Errored> { |
491 | let mut cmd = config.program.build(&config.out_dir); |
492 | cmd.arg(path); |
493 | if !revision.is_empty() { |
494 | cmd.arg(format!("--cfg= {revision}" )); |
495 | } |
496 | for arg in comments |
497 | .for_revision(revision) |
498 | .flat_map(|r| r.compile_flags.iter()) |
499 | { |
500 | cmd.arg(arg); |
501 | } |
502 | let edition = comments.edition(revision, config)?; |
503 | |
504 | if let Some(edition) = edition { |
505 | cmd.arg("--edition" ).arg(&*edition); |
506 | } |
507 | |
508 | cmd.envs( |
509 | comments |
510 | .for_revision(revision) |
511 | .flat_map(|r| r.env_vars.iter()) |
512 | .map(|(k, v)| (k, v)), |
513 | ); |
514 | |
515 | Ok(cmd) |
516 | } |
517 | |
518 | fn build_aux( |
519 | aux_file: &Path, |
520 | config: &Config, |
521 | build_manager: &BuildManager<'_>, |
522 | ) -> std::result::Result<Vec<OsString>, Errored> { |
523 | let file_contents = std::fs::read(aux_file).map_err(|err| Errored { |
524 | command: Command::new(format!("reading aux file ` {}`" , aux_file.display())), |
525 | errors: vec![], |
526 | stderr: err.to_string().into_bytes(), |
527 | stdout: vec![], |
528 | })?; |
529 | let comments = parse_comments(&file_contents)?; |
530 | assert_eq!( |
531 | comments.revisions, None, |
532 | "aux builds cannot specify revisions" |
533 | ); |
534 | |
535 | let mut config = config.clone(); |
536 | |
537 | // Strip any `crate-type` flags from the args, as we need to set our own, |
538 | // and they may conflict (e.g. `lib` vs `proc-macro`); |
539 | let mut prev_was_crate_type = false; |
540 | config.program.args.retain(|arg| { |
541 | if prev_was_crate_type { |
542 | prev_was_crate_type = false; |
543 | return false; |
544 | } |
545 | if arg == "--test" { |
546 | false |
547 | } else if arg == "--crate-type" { |
548 | prev_was_crate_type = true; |
549 | false |
550 | } else if let Some(arg) = arg.to_str() { |
551 | !arg.starts_with("--crate-type=" ) |
552 | } else { |
553 | true |
554 | } |
555 | }); |
556 | |
557 | default_per_file_config(&mut config, aux_file, &file_contents); |
558 | |
559 | // Put aux builds into a separate directory per path so that multiple aux files |
560 | // from different directories (but with the same file name) don't collide. |
561 | let relative = strip_path_prefix(aux_file.parent().unwrap(), &config.out_dir); |
562 | |
563 | config.out_dir.extend(relative); |
564 | |
565 | let mut aux_cmd = build_command(aux_file, &config, "" , &comments)?; |
566 | |
567 | let mut extra_args = build_aux_files( |
568 | aux_file.parent().unwrap(), |
569 | &comments, |
570 | "" , |
571 | &config, |
572 | build_manager, |
573 | )?; |
574 | // Make sure we see our dependencies |
575 | aux_cmd.args(extra_args.iter()); |
576 | |
577 | aux_cmd.arg("--emit=link" ); |
578 | let filename = aux_file.file_stem().unwrap().to_str().unwrap(); |
579 | let output = aux_cmd.output().unwrap(); |
580 | if !output.status.success() { |
581 | let error = Error::Command { |
582 | kind: "compilation of aux build failed" .to_string(), |
583 | status: output.status, |
584 | }; |
585 | return Err(Errored { |
586 | command: aux_cmd, |
587 | errors: vec![error], |
588 | stderr: rustc_stderr::process(aux_file, &output.stderr).rendered, |
589 | stdout: output.stdout, |
590 | }); |
591 | } |
592 | |
593 | // Now run the command again to fetch the output filenames |
594 | aux_cmd.arg("--print" ).arg("file-names" ); |
595 | let output = aux_cmd.output().unwrap(); |
596 | assert!(output.status.success()); |
597 | |
598 | for file in output.stdout.lines() { |
599 | let file = std::str::from_utf8(file).unwrap(); |
600 | let crate_name = filename.replace('-' , "_" ); |
601 | let path = config.out_dir.join(file); |
602 | extra_args.push("--extern" .into()); |
603 | let mut cname = OsString::from(&crate_name); |
604 | cname.push("=" ); |
605 | cname.push(path); |
606 | extra_args.push(cname); |
607 | // Help cargo find the crates added with `--extern`. |
608 | extra_args.push("-L" .into()); |
609 | extra_args.push(config.out_dir.as_os_str().to_os_string()); |
610 | } |
611 | Ok(extra_args) |
612 | } |
613 | |
614 | impl dyn TestStatus { |
615 | fn run_test( |
616 | &self, |
617 | build_manager: &BuildManager<'_>, |
618 | config: &Config, |
619 | comments: &Comments, |
620 | ) -> TestResult { |
621 | let path = self.path(); |
622 | let revision = self.revision(); |
623 | |
624 | let extra_args = build_aux_files( |
625 | &path.parent().unwrap().join("auxiliary" ), |
626 | comments, |
627 | revision, |
628 | config, |
629 | build_manager, |
630 | )?; |
631 | |
632 | let mut config = config.clone(); |
633 | |
634 | // Put aux builds into a separate directory per path so that multiple aux files |
635 | // from different directories (but with the same file name) don't collide. |
636 | let relative = strip_path_prefix(path.parent().unwrap(), &config.out_dir); |
637 | |
638 | config.out_dir.extend(relative); |
639 | |
640 | let mut cmd = build_command(path, &config, revision, comments)?; |
641 | cmd.args(&extra_args); |
642 | let stdin = path.with_extension(if revision.is_empty() { |
643 | "stdin" .into() |
644 | } else { |
645 | format!(" {revision}.stdin" ) |
646 | }); |
647 | if stdin.exists() { |
648 | cmd.stdin(std::fs::File::open(stdin).unwrap()); |
649 | } |
650 | |
651 | let (cmd, status, stderr, stdout) = self.run_command(cmd)?; |
652 | |
653 | let mode = config.mode.maybe_override(comments, revision)?; |
654 | let cmd = check_test_result( |
655 | cmd, |
656 | match *mode { |
657 | Mode::Run { .. } => Mode::Pass, |
658 | _ => *mode, |
659 | }, |
660 | path, |
661 | &config, |
662 | revision, |
663 | comments, |
664 | status, |
665 | &stdout, |
666 | &stderr, |
667 | )?; |
668 | |
669 | if let Mode::Run { .. } = *mode { |
670 | return run_test_binary(mode, path, revision, comments, cmd, &config); |
671 | } |
672 | |
673 | run_rustfix( |
674 | &stderr, &stdout, path, comments, revision, &config, *mode, extra_args, |
675 | )?; |
676 | Ok(TestOk::Ok) |
677 | } |
678 | |
679 | /// Run a command, and if it takes more than 100ms, start appending the last stderr/stdout |
680 | /// line to the current status spinner. |
681 | fn run_command( |
682 | &self, |
683 | mut cmd: Command, |
684 | ) -> Result<(Command, ExitStatus, Vec<u8>, Vec<u8>), Errored> { |
685 | match cmd.output() { |
686 | Err(err) => Err(Errored { |
687 | errors: vec![], |
688 | stderr: err.to_string().into_bytes(), |
689 | stdout: format!("could not spawn ` {:?}` as a process" , cmd.get_program()) |
690 | .into_bytes(), |
691 | command: cmd, |
692 | }), |
693 | Ok(Output { |
694 | status, |
695 | stdout, |
696 | stderr, |
697 | }) => Ok((cmd, status, stderr, stdout)), |
698 | } |
699 | } |
700 | } |
701 | |
702 | fn build_aux_files( |
703 | aux_dir: &Path, |
704 | comments: &Comments, |
705 | revision: &str, |
706 | config: &Config, |
707 | build_manager: &BuildManager<'_>, |
708 | ) -> Result<Vec<OsString>, Errored> { |
709 | let mut extra_args = vec![]; |
710 | for rev in comments.for_revision(revision) { |
711 | for aux in &rev.aux_builds { |
712 | let line = aux.line(); |
713 | let aux = &**aux; |
714 | let aux_file = if aux.starts_with(".." ) { |
715 | aux_dir.parent().unwrap().join(aux) |
716 | } else { |
717 | aux_dir.join(aux) |
718 | }; |
719 | extra_args.extend( |
720 | build_manager |
721 | .build( |
722 | Build::Aux { |
723 | aux_file: strip_path_prefix( |
724 | &aux_file.canonicalize().map_err(|err| Errored { |
725 | command: Command::new(format!( |
726 | "canonicalizing path ` {}`" , |
727 | aux_file.display() |
728 | )), |
729 | errors: vec![], |
730 | stderr: err.to_string().into_bytes(), |
731 | stdout: vec![], |
732 | })?, |
733 | &std::env::current_dir().unwrap(), |
734 | ) |
735 | .collect(), |
736 | }, |
737 | config, |
738 | ) |
739 | .map_err( |
740 | |Errored { |
741 | command, |
742 | errors, |
743 | stderr, |
744 | stdout, |
745 | }| Errored { |
746 | command, |
747 | errors: vec![Error::Aux { |
748 | path: aux_file, |
749 | errors, |
750 | line, |
751 | }], |
752 | stderr, |
753 | stdout, |
754 | }, |
755 | )?, |
756 | ); |
757 | } |
758 | } |
759 | Ok(extra_args) |
760 | } |
761 | |
762 | fn run_test_binary( |
763 | mode: MaybeSpanned<Mode>, |
764 | path: &Path, |
765 | revision: &str, |
766 | comments: &Comments, |
767 | mut cmd: Command, |
768 | config: &Config, |
769 | ) -> TestResult { |
770 | let revision = if revision.is_empty() { |
771 | "run" .to_string() |
772 | } else { |
773 | format!("run. {revision}" ) |
774 | }; |
775 | cmd.arg("--print" ).arg("file-names" ); |
776 | let output = cmd.output().unwrap(); |
777 | assert!(output.status.success()); |
778 | |
779 | let mut files = output.stdout.lines(); |
780 | let file = files.next().unwrap(); |
781 | assert_eq!(files.next(), None); |
782 | let file = std::str::from_utf8(file).unwrap(); |
783 | let exe_file = config.out_dir.join(file); |
784 | let mut exe = Command::new(&exe_file); |
785 | let stdin = path.with_extension(format!(" {revision}.stdin" )); |
786 | if stdin.exists() { |
787 | exe.stdin(std::fs::File::open(stdin).unwrap()); |
788 | } |
789 | let output = exe |
790 | .output() |
791 | .unwrap_or_else(|err| panic!("exe file: {}: {err}" , exe_file.display())); |
792 | |
793 | let mut errors = vec![]; |
794 | |
795 | check_test_output( |
796 | path, |
797 | &mut errors, |
798 | &revision, |
799 | config, |
800 | comments, |
801 | &output.stdout, |
802 | &output.stderr, |
803 | ); |
804 | |
805 | errors.extend(mode.ok(output.status).err()); |
806 | if errors.is_empty() { |
807 | Ok(TestOk::Ok) |
808 | } else { |
809 | Err(Errored { |
810 | command: exe, |
811 | errors, |
812 | stderr: vec![], |
813 | stdout: vec![], |
814 | }) |
815 | } |
816 | } |
817 | |
818 | fn run_rustfix( |
819 | stderr: &[u8], |
820 | stdout: &[u8], |
821 | path: &Path, |
822 | comments: &Comments, |
823 | revision: &str, |
824 | config: &Config, |
825 | mode: Mode, |
826 | extra_args: Vec<OsString>, |
827 | ) -> Result<(), Errored> { |
828 | let no_run_rustfix = |
829 | comments.find_one_for_revision(revision, "`no-rustfix` annotations" , |r| r.no_rustfix)?; |
830 | |
831 | let global_rustfix = match mode { |
832 | Mode::Pass | Mode::Run { .. } | Mode::Panic => RustfixMode::Disabled, |
833 | Mode::Fail { rustfix, .. } | Mode::Yolo { rustfix } => rustfix, |
834 | }; |
835 | |
836 | let fixed_code = (no_run_rustfix.is_none() && global_rustfix.enabled()) |
837 | .then_some(()) |
838 | .and_then(|()| { |
839 | let suggestions = std::str::from_utf8(stderr) |
840 | .unwrap() |
841 | .lines() |
842 | .flat_map(|line| { |
843 | if !line.starts_with('{' ) { |
844 | return vec![]; |
845 | } |
846 | rustfix::get_suggestions_from_json( |
847 | line, |
848 | &HashSet::new(), |
849 | if global_rustfix == RustfixMode::Everything { |
850 | rustfix::Filter::Everything |
851 | } else { |
852 | rustfix::Filter::MachineApplicableOnly |
853 | }, |
854 | ) |
855 | .unwrap_or_else(|err| { |
856 | panic!("could not deserialize diagnostics json for rustfix {err}: {line}" ) |
857 | }) |
858 | }) |
859 | .collect::<Vec<_>>(); |
860 | if suggestions.is_empty() { |
861 | None |
862 | } else { |
863 | Some(rustfix::apply_suggestions( |
864 | &std::fs::read_to_string(path).unwrap(), |
865 | &suggestions, |
866 | )) |
867 | } |
868 | }) |
869 | .transpose() |
870 | .map_err(|err| Errored { |
871 | command: Command::new(format!("rustfix {}" , path.display())), |
872 | errors: vec![Error::Rustfix(err)], |
873 | stderr: stderr.into(), |
874 | stdout: stdout.into(), |
875 | })?; |
876 | |
877 | let edition = comments.edition(revision, config)?; |
878 | let edition = edition |
879 | .map(|mwl| { |
880 | let line = mwl.span().unwrap_or(Span::INVALID); |
881 | Spanned::new(mwl.into_inner(), line) |
882 | }) |
883 | .into(); |
884 | let rustfix_comments = Comments { |
885 | revisions: None, |
886 | revisioned: std::iter::once(( |
887 | vec![], |
888 | Revisioned { |
889 | span: Span::INVALID, |
890 | ignore: vec![], |
891 | only: vec![], |
892 | stderr_per_bitwidth: false, |
893 | compile_flags: comments |
894 | .for_revision(revision) |
895 | .flat_map(|r| r.compile_flags.iter().cloned()) |
896 | .collect(), |
897 | env_vars: comments |
898 | .for_revision(revision) |
899 | .flat_map(|r| r.env_vars.iter().cloned()) |
900 | .collect(), |
901 | normalize_stderr: vec![], |
902 | normalize_stdout: vec![], |
903 | error_in_other_files: vec![], |
904 | error_matches: vec![], |
905 | require_annotations_for_level: Default::default(), |
906 | aux_builds: comments |
907 | .for_revision(revision) |
908 | .flat_map(|r| r.aux_builds.iter().cloned()) |
909 | .collect(), |
910 | edition, |
911 | mode: OptWithLine::new(Mode::Pass, Span::INVALID), |
912 | no_rustfix: OptWithLine::new((), Span::INVALID), |
913 | needs_asm_support: false, |
914 | }, |
915 | )) |
916 | .collect(), |
917 | }; |
918 | |
919 | let run = fixed_code.is_some(); |
920 | let mut errors = vec![]; |
921 | let rustfix_path = check_output( |
922 | // Always check for `.fixed` files, even if there were reasons not to run rustfix. |
923 | // We don't want to leave around stray `.fixed` files |
924 | fixed_code.unwrap_or_default().as_bytes(), |
925 | path, |
926 | &mut errors, |
927 | "fixed" , |
928 | &Filter::default(), |
929 | config, |
930 | &rustfix_comments, |
931 | revision, |
932 | ); |
933 | if !errors.is_empty() { |
934 | return Err(Errored { |
935 | command: Command::new(format!("checking {}" , path.display())), |
936 | errors, |
937 | stderr: vec![], |
938 | stdout: vec![], |
939 | }); |
940 | } |
941 | |
942 | if !run { |
943 | return Ok(()); |
944 | } |
945 | |
946 | let mut cmd = build_command(&rustfix_path, config, revision, &rustfix_comments)?; |
947 | cmd.args(extra_args); |
948 | // picking the crate name from the file name is problematic when `.revision_name` is inserted |
949 | cmd.arg("--crate-name" ).arg( |
950 | path.file_stem() |
951 | .unwrap() |
952 | .to_str() |
953 | .unwrap() |
954 | .replace('-' , "_" ), |
955 | ); |
956 | let output = cmd.output().unwrap(); |
957 | if output.status.success() { |
958 | Ok(()) |
959 | } else { |
960 | Err(Errored { |
961 | command: cmd, |
962 | errors: vec![Error::Command { |
963 | kind: "rustfix" .into(), |
964 | status: output.status, |
965 | }], |
966 | stderr: rustc_stderr::process(&rustfix_path, &output.stderr).rendered, |
967 | stdout: output.stdout, |
968 | }) |
969 | } |
970 | } |
971 | |
972 | fn revised(revision: &str, extension: &str) -> String { |
973 | if revision.is_empty() { |
974 | extension.to_string() |
975 | } else { |
976 | format!(" {revision}. {extension}" ) |
977 | } |
978 | } |
979 | |
980 | fn check_test_result( |
981 | command: Command, |
982 | mode: Mode, |
983 | path: &Path, |
984 | config: &Config, |
985 | revision: &str, |
986 | comments: &Comments, |
987 | status: ExitStatus, |
988 | stdout: &[u8], |
989 | stderr: &[u8], |
990 | ) -> Result<Command, Errored> { |
991 | let mut errors = vec![]; |
992 | errors.extend(mode.ok(status).err()); |
993 | // Always remove annotation comments from stderr. |
994 | let diagnostics = rustc_stderr::process(path, stderr); |
995 | check_test_output( |
996 | path, |
997 | &mut errors, |
998 | revision, |
999 | config, |
1000 | comments, |
1001 | stdout, |
1002 | &diagnostics.rendered, |
1003 | ); |
1004 | // Check error annotations in the source against output |
1005 | check_annotations( |
1006 | diagnostics.messages, |
1007 | diagnostics.messages_from_unknown_file_or_line, |
1008 | path, |
1009 | &mut errors, |
1010 | config, |
1011 | revision, |
1012 | comments, |
1013 | )?; |
1014 | if errors.is_empty() { |
1015 | Ok(command) |
1016 | } else { |
1017 | Err(Errored { |
1018 | command, |
1019 | errors, |
1020 | stderr: diagnostics.rendered, |
1021 | stdout: stdout.into(), |
1022 | }) |
1023 | } |
1024 | } |
1025 | |
1026 | fn check_test_output( |
1027 | path: &Path, |
1028 | errors: &mut Vec<Error>, |
1029 | revision: &str, |
1030 | config: &Config, |
1031 | comments: &Comments, |
1032 | stdout: &[u8], |
1033 | stderr: &[u8], |
1034 | ) { |
1035 | // Check output files (if any) |
1036 | // Check output files against actual output |
1037 | check_output( |
1038 | output:stderr, |
1039 | path, |
1040 | errors, |
1041 | kind:"stderr" , |
1042 | &config.stderr_filters, |
1043 | config, |
1044 | comments, |
1045 | revision, |
1046 | ); |
1047 | check_output( |
1048 | output:stdout, |
1049 | path, |
1050 | errors, |
1051 | kind:"stdout" , |
1052 | &config.stdout_filters, |
1053 | config, |
1054 | comments, |
1055 | revision, |
1056 | ); |
1057 | } |
1058 | |
1059 | fn check_annotations( |
1060 | mut messages: Vec<Vec<Message>>, |
1061 | mut messages_from_unknown_file_or_line: Vec<Message>, |
1062 | path: &Path, |
1063 | errors: &mut Errors, |
1064 | config: &Config, |
1065 | revision: &str, |
1066 | comments: &Comments, |
1067 | ) -> Result<(), Errored> { |
1068 | let error_patterns = comments |
1069 | .for_revision(revision) |
1070 | .flat_map(|r| r.error_in_other_files.iter()); |
1071 | |
1072 | let mut seen_error_match = None; |
1073 | for error_pattern in error_patterns { |
1074 | seen_error_match = Some(error_pattern.span()); |
1075 | // first check the diagnostics messages outside of our file. We check this first, so that |
1076 | // you can mix in-file annotations with //@error-in-other-file annotations, even if there is overlap |
1077 | // in the messages. |
1078 | if let Some(i) = messages_from_unknown_file_or_line |
1079 | .iter() |
1080 | .position(|msg| error_pattern.matches(&msg.message)) |
1081 | { |
1082 | messages_from_unknown_file_or_line.remove(i); |
1083 | } else { |
1084 | errors.push(Error::PatternNotFound { |
1085 | pattern: error_pattern.clone(), |
1086 | expected_line: None, |
1087 | }); |
1088 | } |
1089 | } |
1090 | |
1091 | // The order on `Level` is such that `Error` is the highest level. |
1092 | // We will ensure that *all* diagnostics of level at least `lowest_annotation_level` |
1093 | // are matched. |
1094 | let mut lowest_annotation_level = Level::Error; |
1095 | for &ErrorMatch { |
1096 | ref pattern, |
1097 | level, |
1098 | line, |
1099 | } in comments |
1100 | .for_revision(revision) |
1101 | .flat_map(|r| r.error_matches.iter()) |
1102 | { |
1103 | seen_error_match = Some(pattern.span()); |
1104 | // If we found a diagnostic with a level annotation, make sure that all |
1105 | // diagnostics of that level have annotations, even if we don't end up finding a matching diagnostic |
1106 | // for this pattern. |
1107 | if lowest_annotation_level > level { |
1108 | lowest_annotation_level = level; |
1109 | } |
1110 | |
1111 | if let Some(msgs) = messages.get_mut(line.get()) { |
1112 | let found = msgs |
1113 | .iter() |
1114 | .position(|msg| pattern.matches(&msg.message) && msg.level == level); |
1115 | if let Some(found) = found { |
1116 | msgs.remove(found); |
1117 | continue; |
1118 | } |
1119 | } |
1120 | |
1121 | errors.push(Error::PatternNotFound { |
1122 | pattern: pattern.clone(), |
1123 | expected_line: Some(line), |
1124 | }); |
1125 | } |
1126 | |
1127 | let required_annotation_level = comments.find_one_for_revision( |
1128 | revision, |
1129 | "`require_annotations_for_level` annotations" , |
1130 | |r| r.require_annotations_for_level, |
1131 | )?; |
1132 | |
1133 | let required_annotation_level = |
1134 | required_annotation_level.map_or(lowest_annotation_level, |l| *l); |
1135 | let filter = |mut msgs: Vec<Message>| -> Vec<_> { |
1136 | msgs.retain(|msg| msg.level >= required_annotation_level); |
1137 | msgs |
1138 | }; |
1139 | |
1140 | let mode = config.mode.maybe_override(comments, revision)?; |
1141 | |
1142 | if !matches!(config.mode, Mode::Yolo { .. }) { |
1143 | let messages_from_unknown_file_or_line = filter(messages_from_unknown_file_or_line); |
1144 | if !messages_from_unknown_file_or_line.is_empty() { |
1145 | errors.push(Error::ErrorsWithoutPattern { |
1146 | path: None, |
1147 | msgs: messages_from_unknown_file_or_line, |
1148 | }); |
1149 | } |
1150 | |
1151 | for (line, msgs) in messages.into_iter().enumerate() { |
1152 | let msgs = filter(msgs); |
1153 | if !msgs.is_empty() { |
1154 | let line = NonZeroUsize::new(line).expect("line 0 is always empty" ); |
1155 | errors.push(Error::ErrorsWithoutPattern { |
1156 | path: Some(Spanned::new( |
1157 | path.to_path_buf(), |
1158 | Span { |
1159 | line_start: line, |
1160 | ..Span::INVALID |
1161 | }, |
1162 | )), |
1163 | msgs, |
1164 | }); |
1165 | } |
1166 | } |
1167 | } |
1168 | |
1169 | match (*mode, seen_error_match) { |
1170 | (Mode::Pass, Some(span)) | (Mode::Panic, Some(span)) => { |
1171 | errors.push(Error::PatternFoundInPassTest { |
1172 | mode: mode.span(), |
1173 | span, |
1174 | }) |
1175 | } |
1176 | ( |
1177 | Mode::Fail { |
1178 | require_patterns: true, |
1179 | .. |
1180 | }, |
1181 | None, |
1182 | ) => errors.push(Error::NoPatternsFound), |
1183 | _ => {} |
1184 | } |
1185 | Ok(()) |
1186 | } |
1187 | |
1188 | fn check_output( |
1189 | output: &[u8], |
1190 | path: &Path, |
1191 | errors: &mut Errors, |
1192 | kind: &'static str, |
1193 | filters: &Filter, |
1194 | config: &Config, |
1195 | comments: &Comments, |
1196 | revision: &str, |
1197 | ) -> PathBuf { |
1198 | let target = config.target.as_ref().unwrap(); |
1199 | let output = normalize(path, output, filters, comments, revision, kind); |
1200 | let path = output_path(path, comments, revised(revision, kind), target, revision); |
1201 | match &config.output_conflict_handling { |
1202 | OutputConflictHandling::Error(bless_command) => { |
1203 | let expected_output = std::fs::read(&path).unwrap_or_default(); |
1204 | if output != expected_output { |
1205 | errors.push(Error::OutputDiffers { |
1206 | path: path.clone(), |
1207 | actual: output.clone(), |
1208 | expected: expected_output, |
1209 | bless_command: bless_command.clone(), |
1210 | }); |
1211 | } |
1212 | } |
1213 | OutputConflictHandling::Bless => { |
1214 | if output.is_empty() { |
1215 | let _ = std::fs::remove_file(&path); |
1216 | } else { |
1217 | std::fs::write(&path, &output).unwrap(); |
1218 | } |
1219 | } |
1220 | OutputConflictHandling::Ignore => {} |
1221 | } |
1222 | path |
1223 | } |
1224 | |
1225 | fn output_path( |
1226 | path: &Path, |
1227 | comments: &Comments, |
1228 | kind: String, |
1229 | target: &str, |
1230 | revision: &str, |
1231 | ) -> PathBuf { |
1232 | if commentsimpl Iterator |
1233 | .for_revision(revision) |
1234 | .any(|r: &Revisioned| r.stderr_per_bitwidth) |
1235 | { |
1236 | return path.with_extension(format!(" {}bit. {kind}" , get_pointer_width(target))); |
1237 | } |
1238 | path.with_extension(kind) |
1239 | } |
1240 | |
1241 | fn test_condition(condition: &Condition, config: &Config) -> bool { |
1242 | let target: &String = config.target.as_ref().unwrap(); |
1243 | match condition { |
1244 | Condition::Bitwidth(bits: &u8) => get_pointer_width(triple:target) == *bits, |
1245 | Condition::Target(t: &String) => target.contains(t), |
1246 | Condition::Host(t: &String) => config.host.as_ref().unwrap().contains(t), |
1247 | Condition::OnHost => target == config.host.as_ref().unwrap(), |
1248 | } |
1249 | } |
1250 | |
1251 | impl dyn TestStatus { |
1252 | /// Returns whether according to the in-file conditions, this file should be run. |
1253 | fn test_file_conditions(&self, comments: &Comments, config: &Config) -> bool { |
1254 | let revision: &str = self.revision(); |
1255 | if commentsimpl Iterator |
1256 | .for_revision(revision) |
1257 | .flat_map(|r: &Revisioned| r.ignore.iter()) |
1258 | .any(|c: &Condition| test_condition(condition:c, config)) |
1259 | { |
1260 | return config.run_only_ignored; |
1261 | } |
1262 | if commentsimpl Iterator |
1263 | .for_revision(revision) |
1264 | .any(|r: &Revisioned| r.needs_asm_support && !config.has_asm_support()) |
1265 | { |
1266 | return config.run_only_ignored; |
1267 | } |
1268 | commentsimpl Iterator |
1269 | .for_revision(revision) |
1270 | .flat_map(|r: &Revisioned| r.only.iter()) |
1271 | .all(|c: &Condition| test_condition(condition:c, config)) |
1272 | ^ config.run_only_ignored |
1273 | } |
1274 | } |
1275 | |
1276 | // Taken 1:1 from compiletest-rs |
1277 | fn get_pointer_width(triple: &str) -> u8 { |
1278 | if (triple.contains("64" ) && !triple.ends_with("gnux32" ) && !triple.ends_with("gnu_ilp32" )) |
1279 | || triple.starts_with("s390x" ) |
1280 | { |
1281 | 64 |
1282 | } else if triple.starts_with("avr" ) { |
1283 | 16 |
1284 | } else { |
1285 | 32 |
1286 | } |
1287 | } |
1288 | |
1289 | fn normalize( |
1290 | path: &Path, |
1291 | text: &[u8], |
1292 | filters: &Filter, |
1293 | comments: &Comments, |
1294 | revision: &str, |
1295 | kind: &'static str, |
1296 | ) -> Vec<u8> { |
1297 | // Useless paths |
1298 | let path_filter: (Match, &[u8]) = (Match::from(path.parent().unwrap()), b"$DIR" as &[u8]); |
1299 | let filters: impl Iterator = filters.iter().chain(std::iter::once(&path_filter)); |
1300 | let mut text: Vec = text.to_owned(); |
1301 | if let Some(lib_path: &str) = option_env!("RUSTC_LIB_PATH" ) { |
1302 | text = text.replace(needle:lib_path, replacement:"RUSTLIB" ); |
1303 | } |
1304 | |
1305 | for (rule: &Match, replacement: &&[u8]) in filters { |
1306 | text = rule.replace_all(&text, replacement).into_owned(); |
1307 | } |
1308 | |
1309 | for (from: &Regex, to: &Vec) in comments.for_revision(revision).flat_map(|r: &Revisioned| match kind { |
1310 | "fixed" => &[] as &[_], |
1311 | "stderr" => &r.normalize_stderr, |
1312 | "stdout" => &r.normalize_stdout, |
1313 | _ => unreachable!(), |
1314 | }) { |
1315 | text = from.replace_all(&text, rep:to).into_owned(); |
1316 | } |
1317 | text |
1318 | } |
1319 | /// Remove the common prefix of this path and the `root_dir`. |
1320 | fn strip_path_prefix<'a>(path: &'a Path, prefix: &Path) -> impl Iterator<Item = Component<'a>> { |
1321 | let mut components: Components<'_> = path.components(); |
1322 | for c: Component<'_> in prefix.components() { |
1323 | // Windows has some funky paths. This is probably wrong, but works well in practice. |
1324 | let deverbatimize: impl Fn(Component<'_>) -> … = |c: Component<'_>| match c { |
1325 | Component::Prefix(prefix: PrefixComponent<'_>) => Err(match prefix.kind() { |
1326 | Prefix::VerbatimUNC(a: &OsStr, b: &OsStr) => Prefix::UNC(a, b), |
1327 | Prefix::VerbatimDisk(d: u8) => Prefix::Disk(d), |
1328 | other: Prefix<'_> => other, |
1329 | }), |
1330 | c: Component<'_> => Ok(c), |
1331 | }; |
1332 | let c2: Option> = components.next(); |
1333 | if Some(deverbatimize(c)) == c2.map(deverbatimize) { |
1334 | continue; |
1335 | } |
1336 | return c2.into_iter().chain(components); |
1337 | } |
1338 | None.into_iter().chain(components) |
1339 | } |
1340 | |