Skip to main content

rucc_session/
fs.rs

1//! The file system the compiler reads through, and the include search path.
2//!
3//! Design: `spec/04-driver-and-cli.md` section 4.4 for the search order, and
4//! `spec/05-preprocessor.md` section 5.4 for what `#include_next` means.
5//!
6//! Nothing below the driver calls `std::fs`. That is what makes the compiler usable as a
7//! library, and it is what makes a preprocessor test a value rather than a temporary
8//! directory: [`MemoryFileSystem`] is a map from path to bytes, and a test that needs a
9//! twelve deep header nest builds one in twelve lines with no clean up to forget.
10//!
11//! ```
12//! use rucc_session::{FileSystem, IncludeForm, MemoryFileSystem, SearchPath};
13//!
14//! let mut fs = MemoryFileSystem::new();
15//! fs.insert("/usr/include/stdio.h", *b"int puts(const char *);\n");
16//!
17//! let mut search = SearchPath::new();
18//! search.push_system("/usr/include");
19//!
20//! let found = search.resolve(&fs, "stdio.h", IncludeForm::Angled, None, 0).unwrap();
21//! assert_eq!(found.name.replace('\\', "/"), "/usr/include/stdio.h");
22//! assert!(found.is_system);
23//! ```
24
25use std::collections::BTreeMap;
26use std::fmt;
27use std::io;
28use std::path::{Path, PathBuf};
29
30use rucc_diag::SourceBytes;
31
32use crate::runtime;
33
34/// Where the compiler reads source from.
35///
36/// There is no `exists`, deliberately. Whether a file is there is answered by trying to read
37/// it, and an interface with a separate question invites the race where the answer changes
38/// between the two calls.
39pub trait FileSystem: fmt::Debug + Send + Sync {
40    /// Reads a file.
41    ///
42    /// # Errors
43    ///
44    /// Whatever the underlying file system says. [`io::ErrorKind::NotFound`] is the ordinary
45    /// case during an include search and is not by itself a problem.
46    fn read(&self, path: &Path) -> io::Result<SourceBytes>;
47
48    /// What this file system calls a file, for deciding that two names are one file.
49    ///
50    /// The multiple include optimization has to answer whether a header has been read
51    /// already, and the name in the directive is not that answer. One header found through
52    /// `-I .` and found again through `-I /tree` arrives under two names, and a project with
53    /// more than one include directory reaches the same header both ways all day. So does a
54    /// file that includes itself, which is the whole point of `#pragma once` in a main file.
55    ///
56    /// The default is [`path_key`], the text with the `.` components taken out, which is all
57    /// a map from name to bytes can say. An implementation backed by a real file system
58    /// resolves the name instead, so that a symlink, a `..` and a relative path all land on
59    /// the same answer. Only called for a file that has already been read, so it is a
60    /// question about a file that is there rather than a probe.
61    fn identity(&self, path: &Path) -> PathBuf {
62        path_key(path)
63    }
64}
65
66/// A file system held in memory, for tests and for embedding the compiler.
67#[derive(Debug, Default)]
68pub struct MemoryFileSystem {
69    files: BTreeMap<PathBuf, SourceBytes>,
70}
71
72impl MemoryFileSystem {
73    /// An empty file system.
74    pub fn new() -> MemoryFileSystem {
75        MemoryFileSystem::default()
76    }
77
78    /// Adds a file, replacing any file already at that path.
79    pub fn insert(
80        &mut self,
81        path: impl Into<PathBuf>,
82        contents: impl AsRef<[u8]> + Send + Sync + 'static,
83    ) {
84        self.files.insert(path_key(&path.into()), SourceBytes::new(contents));
85    }
86
87    /// How many files it holds.
88    pub fn len(&self) -> usize {
89        self.files.len()
90    }
91
92    /// Whether it holds nothing.
93    pub fn is_empty(&self) -> bool {
94        self.files.is_empty()
95    }
96}
97
98impl FileSystem for MemoryFileSystem {
99    fn read(&self, path: &Path) -> io::Result<SourceBytes> {
100        // Through [`path_key`], so that `./dir/x.h` and `dir/x.h` find one file here the way
101        // they do on a real file system. A test that behaves differently from the thing it
102        // stands in for is worse than no test.
103        self.files
104            .get(&path_key(path))
105            .cloned()
106            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no such file"))
107    }
108}
109
110/// Which spelling an `#include` used.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
112pub enum IncludeForm {
113    /// `#include "local.h"`, which looks next to the including file first.
114    Quoted,
115    /// `#include <stdio.h>`, which does not.
116    Angled,
117}
118
119/// One directory on the search path.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct Dir {
122    /// The directory, as the user spelled it. Not canonicalised, because a diagnostic that
123    /// says `../include/foo.h` is more use than one naming a path the user never typed.
124    pub path: PathBuf,
125    /// Whether headers found here are system headers, which suppresses warnings in them and
126    /// sets the `3` flag on a `-E` line marker.
127    pub is_system: bool,
128}
129
130/// The key that every spelling of one path shares, as far as text can say.
131///
132/// `Path::components` is what does the work: it drops a trailing separator and the `.` inside
133/// a path, so `/usr/include/` and `/usr/include` are one directory, and `./dir/x.h` and
134/// `dir/x.h` are one file. `..` is left alone, since a component above a symlink does not go
135/// where reading the path suggests.
136///
137/// This is text, not identity. Two names for one file through a symlink or a hard link are two
138/// keys here, where a compiler that asked the file system would get one answer. Asking would
139/// mean a `stat` per include on a path where the whole point is not to open the file at all.
140pub fn path_key(path: &Path) -> PathBuf {
141    path.components().filter(|c| !matches!(c, std::path::Component::CurDir)).collect()
142}
143
144/// Whether two spellings name the same directory, as far as text can say.
145fn same_dir(a: &Path, b: &Path) -> bool {
146    path_key(a) == path_key(b)
147}
148
149/// A header that was found.
150#[derive(Debug, Clone)]
151pub struct Found {
152    /// The path to open, which is the directory joined with the name as written.
153    pub path: PathBuf,
154    /// That path as a string, for the source map and for diagnostics.
155    pub name: String,
156    /// Whether it came from a system directory.
157    pub is_system: bool,
158    /// Where an `#include_next` written in this file should start looking.
159    ///
160    /// One past the entry this header came from, or zero for a header found next to the file
161    /// that included it, because that directory is not on the path and there is nothing to
162    /// continue past. Carrying the answer rather than the position is what keeps the two
163    /// cases from being confused at the call site.
164    pub next: usize,
165    /// The contents.
166    pub bytes: SourceBytes,
167}
168
169/// The directories a header is looked for in, in order.
170///
171/// GCC's order, because a different one produces header shadowing bugs that are miserable to
172/// diagnose: `-iquote` first and only for a quoted include, then `-I`, then `-isystem`, then
173/// the configured system directories, then `-idirafter`. The directory of the including file
174/// comes before all of it for a quoted include, and it is not part of the numbered list
175/// because `#include_next` must not be able to land back on it.
176#[derive(Debug, Default, Clone, PartialEq, Eq)]
177pub struct SearchPath {
178    dirs: Vec<Dir>,
179    /// Where the `-I` directories begin, which is where an angled include starts looking.
180    quote_end: usize,
181    /// Where the `-isystem` and configured system directories begin.
182    bracket_end: usize,
183    /// Where the `-idirafter` directories begin.
184    system_end: usize,
185}
186
187impl SearchPath {
188    /// An empty search path.
189    pub fn new() -> SearchPath {
190        SearchPath::default()
191    }
192
193    /// Adds a `-iquote` directory, searched only for a quoted include.
194    pub fn push_quote(&mut self, dir: impl Into<PathBuf>) {
195        let at = self.quote_end;
196        self.insert(at, dir.into(), false);
197        self.quote_end += 1;
198        self.bracket_end += 1;
199        self.system_end += 1;
200    }
201
202    /// Adds a `-I` directory.
203    pub fn push_bracket(&mut self, dir: impl Into<PathBuf>) {
204        let at = self.bracket_end;
205        self.insert(at, dir.into(), false);
206        self.bracket_end += 1;
207        self.system_end += 1;
208    }
209
210    /// Adds a `-isystem` directory, or one of the target's configured system directories.
211    pub fn push_system(&mut self, dir: impl Into<PathBuf>) {
212        let at = self.system_end;
213        self.insert(at, dir.into(), true);
214        self.system_end += 1;
215    }
216
217    /// Adds a `-idirafter` directory, which is searched after everything else.
218    pub fn push_after(&mut self, dir: impl Into<PathBuf>) {
219        let at = self.dirs.len();
220        self.insert(at, dir.into(), true);
221    }
222
223    fn insert(&mut self, at: usize, path: PathBuf, is_system: bool) {
224        self.dirs.insert(at, Dir { path, is_system });
225    }
226
227    /// Drops the directories that are already on the path, the way GCC does.
228    ///
229    /// A duplicate is not a harmless extra entry that costs one failed open. It changes what
230    /// `#include_next` means, which is defined as continuing past the directory the current
231    /// file came from: a header found in the first `/usr/include` writes `#include_next
232    /// <stdint.h>` meaning "the one below me" and finds itself in the second, and a header set
233    /// that ends in a fixed point of its own is one that includes itself forever or answers
234    /// `__has_include_next` yes where the compiler it was written for said no. It shows up as
235    /// soon as somebody passes the system directories on the command line, which the compat
236    /// harness does deliberately and a build system does by accident.
237    ///
238    /// A `-I` that names a system directory loses to the system entry rather than the other way
239    /// round, and that is GCC's rule and is documented as one: keeping the earlier one would
240    /// move a system directory up the order and take the system treatment off the headers in
241    /// it, so the `-I` is the one that goes.
242    ///
243    /// Two names for one directory are two directories here, where GCC compares the device and
244    /// the inode and sees through a symlink. That wants a file system that can answer the
245    /// question and this one deliberately only reads.
246    pub fn remove_duplicates(&mut self) {
247        let mut keep = vec![true; self.dirs.len()];
248        for i in 0..self.dirs.len() {
249            for j in i + 1..self.dirs.len() {
250                if !keep[i] {
251                    break;
252                }
253                if !keep[j] || !same_dir(&self.dirs[i].path, &self.dirs[j].path) {
254                    continue;
255                }
256                if self.dirs[j].is_system && !self.dirs[i].is_system {
257                    keep[i] = false;
258                } else {
259                    keep[j] = false;
260                }
261            }
262        }
263        let (quote, bracket, system) = (self.quote_end, self.bracket_end, self.system_end);
264        let mut at = 0;
265        self.dirs.retain(|_| {
266            let kept = keep[at];
267            if !kept {
268                self.quote_end -= usize::from(at < quote);
269                self.bracket_end -= usize::from(at < bracket);
270                self.system_end -= usize::from(at < system);
271            }
272            at += 1;
273            kept
274        });
275    }
276
277    /// Every directory, in search order.
278    pub fn dirs(&self) -> &[Dir] {
279        &self.dirs
280    }
281
282    /// The first entry an include of this form looks at.
283    ///
284    /// An angled include skips the `-iquote` directories, which is the only difference
285    /// between the two chains once the including file's own directory is out of the way.
286    pub fn start(&self, form: IncludeForm) -> usize {
287        match form {
288            IncludeForm::Quoted => 0,
289            IncludeForm::Angled => self.quote_end,
290        }
291    }
292
293    /// Finds `name`, starting at entry `from` of the search path.
294    ///
295    /// `relative_to` is the directory of the file doing the including, tried first for a
296    /// quoted include and ignored otherwise. Pass `None` for an `#include_next`, which is
297    /// defined as continuing past the directory the current file was found in and so must not
298    /// look next to it again.
299    ///
300    /// An absolute name is opened directly and the search path is not consulted, which is
301    /// what every C compiler does and what a generated header with an absolute path needs.
302    pub fn resolve(
303        &self,
304        fs: &dyn FileSystem,
305        name: &str,
306        form: IncludeForm,
307        relative_to: Option<&Path>,
308        from: usize,
309    ) -> Option<Found> {
310        let as_path = Path::new(name);
311        if is_absolute(as_path) {
312            let bytes = open(fs, as_path).ok()?;
313            return Some(Found {
314                path: as_path.to_path_buf(),
315                name: name.to_owned(),
316                is_system: false,
317                next: 0,
318                bytes,
319            });
320        }
321        if form == IncludeForm::Quoted {
322            if let Some(dir) = relative_to {
323                let path = dir.join(as_path);
324                if let Ok(bytes) = open(fs, &path) {
325                    return Some(Found {
326                        name: display(&path),
327                        path,
328                        is_system: false,
329                        // The including file's own directory is not an entry on the path, so
330                        // an `#include_next` from a header found there starts at the top of
331                        // the path rather than one past a position that does not exist.
332                        next: 0,
333                        bytes,
334                    });
335                }
336            }
337        }
338        for (at, dir) in self.dirs.iter().enumerate().skip(from) {
339            let path = dir.path.join(as_path);
340            if let Ok(bytes) = open(fs, &path) {
341                return Some(Found {
342                    name: display(&path),
343                    path,
344                    is_system: dir.is_system,
345                    next: at + 1,
346                    bytes,
347                });
348            }
349        }
350        None
351    }
352
353    /// The directories a failed [`SearchPath::resolve`] with the same arguments looked in.
354    ///
355    /// `spec/05-preprocessor.md` section 5.7 makes printing this the required behaviour for a
356    /// failed include, because "file not found" without the list of places that were tried is
357    /// the diagnostic that wastes the most time in this part of the compiler.
358    pub fn tried(
359        &self,
360        name: &str,
361        form: IncludeForm,
362        relative_to: Option<&Path>,
363        from: usize,
364    ) -> Vec<PathBuf> {
365        if is_absolute(Path::new(name)) {
366            return Vec::new();
367        }
368        let mut list = Vec::new();
369        if form == IncludeForm::Quoted {
370            if let Some(dir) = relative_to {
371                list.push(dir.to_path_buf());
372            }
373        }
374        list.extend(self.dirs.iter().skip(from).map(|d| d.path.clone()));
375        list
376    }
377}
378
379/// Reads a path the search produced, from the shipped headers first and the disk after.
380///
381/// This is the one place the compiler's own headers are handed out, and it is here rather
382/// than in a [`FileSystem`] implementation on purpose. They are not files, they belong to
383/// every implementation of the trait equally, and the only way to reach them is through a
384/// search path entry spelled [`runtime::DIR`], which no real directory can be spelled as.
385fn open(fs: &dyn FileSystem, path: &Path) -> io::Result<SourceBytes> {
386    match runtime::read(path) {
387        Some(bytes) => Ok(bytes),
388        None => fs.read(path),
389    }
390}
391
392/// A path as a string, lossily, because a diagnostic has to say something.
393fn display(path: &Path) -> String {
394    path.to_string_lossy().into_owned()
395}
396
397/// Whether an include name names a file outright rather than one to be searched for.
398///
399/// `Path::is_absolute` is false for `/usr/include/stdio.h` on Windows, because it has no
400/// drive letter. A C file that says `#include "/usr/include/stdio.h"` means a path from the
401/// root whatever host is compiling it, so a leading separator counts here as well.
402fn is_absolute(path: &Path) -> bool {
403    path.is_absolute() || path.has_root()
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    fn fs_with(files: &[&str]) -> MemoryFileSystem {
411        let mut fs = MemoryFileSystem::new();
412        for f in files {
413            fs.insert(*f, format!("/* {f} */\n").into_bytes());
414        }
415        fs
416    }
417
418    fn text(found: &Found) -> String {
419        String::from_utf8_lossy(found.bytes.as_slice()).into_owned()
420    }
421
422    /// A path with forward slashes, because `Path::join` uses a backslash on Windows and
423    /// these tests are about the search order rather than about separators.
424    fn norm(path: &str) -> String {
425        path.replace('\\', "/")
426    }
427
428    #[test]
429    fn a_missing_file_is_not_found_rather_than_an_error() {
430        let fs = MemoryFileSystem::new();
431        let kind = fs.read(Path::new("/nope.h")).err().map(|e| e.kind());
432        assert_eq!(kind, Some(io::ErrorKind::NotFound));
433        assert!(fs.is_empty());
434    }
435
436    #[test]
437    fn quote_directories_are_invisible_to_an_angled_include() {
438        let fs = fs_with(&["/q/a.h", "/i/a.h"]);
439        let mut search = SearchPath::new();
440        search.push_quote("/q");
441        search.push_bracket("/i");
442
443        let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
444        assert_eq!(norm(&quoted.name), "/q/a.h");
445        let angled = search
446            .resolve(&fs, "a.h", IncludeForm::Angled, None, search.start(IncludeForm::Angled))
447            .unwrap();
448        assert_eq!(norm(&angled.name), "/i/a.h");
449    }
450
451    #[test]
452    fn the_including_files_own_directory_comes_first_for_a_quoted_include() {
453        let fs = fs_with(&["/src/a.h", "/i/a.h"]);
454        let mut search = SearchPath::new();
455        search.push_bracket("/i");
456        let here = Path::new("/src");
457
458        let quoted = search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).unwrap();
459        assert_eq!(norm(&quoted.name), "/src/a.h");
460        // An angled include does not look there, even though it was passed.
461        let angled = search.resolve(&fs, "a.h", IncludeForm::Angled, Some(here), 0).unwrap();
462        assert_eq!(norm(&angled.name), "/i/a.h");
463    }
464
465    #[test]
466    fn the_order_is_iquote_then_i_then_isystem_then_idirafter() {
467        let fs = fs_with(&["/after/a.h", "/sys/a.h", "/i/a.h", "/q/a.h"]);
468        let mut search = SearchPath::new();
469        // Pushed in an order that is not the search order, because a driver reads the command
470        // line left to right and the groups interleave.
471        search.push_after("/after");
472        search.push_system("/sys");
473        search.push_bracket("/i");
474        search.push_quote("/q");
475        let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
476        assert_eq!(order, ["/q", "/i", "/sys", "/after"]);
477
478        let found = search.resolve(&fs, "a.h", IncludeForm::Quoted, None, 0).unwrap();
479        assert_eq!(norm(&found.name), "/q/a.h");
480        assert_eq!(found.next, 1);
481    }
482
483    #[test]
484    fn a_directory_already_on_the_path_is_dropped_rather_than_searched_twice() {
485        let mut search = SearchPath::new();
486        search.push_system("/usr/local/include");
487        search.push_system("/usr/include");
488        search.push_system("/usr/local/include");
489        search.push_system("/usr/include/");
490        search.remove_duplicates();
491        let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
492        // The first spelling is the one kept, trailing separator and all, because it is the one
493        // a diagnostic will name and the two are the same directory.
494        assert_eq!(order, ["/usr/local/include", "/usr/include"]);
495    }
496
497    #[test]
498    fn a_duplicate_is_what_makes_include_next_find_the_file_it_is_standing_in() {
499        // The bug this exists for. A header found in the first `/usr/include` writes
500        // `#include_next <a.h>` meaning the copy below it, and with the directory on the path
501        // twice the copy below it is itself.
502        let fs = fs_with(&["/usr/include/a.h"]);
503        let mut search = SearchPath::new();
504        search.push_system("/usr/include");
505        search.push_system("/usr/include");
506        let first = search.resolve(&fs, "a.h", IncludeForm::Angled, None, 0).unwrap();
507        assert!(search.resolve(&fs, "a.h", IncludeForm::Angled, None, first.next).is_some());
508        search.remove_duplicates();
509        let first = search.resolve(&fs, "a.h", IncludeForm::Angled, None, 0).unwrap();
510        assert!(search.resolve(&fs, "a.h", IncludeForm::Angled, None, first.next).is_none());
511    }
512
513    #[test]
514    fn a_bracket_directory_that_names_a_system_one_is_the_entry_that_goes() {
515        // GCC's documented rule. Keeping the `-I` would move a system directory up the order
516        // and take the system treatment off every header in it.
517        let fs = fs_with(&["/usr/include/a.h"]);
518        let mut search = SearchPath::new();
519        search.push_bracket("/usr/include");
520        search.push_bracket("/i");
521        search.push_system("/usr/include");
522        search.remove_duplicates();
523        let order: Vec<_> = search.dirs().iter().map(|d| norm(&d.path.to_string_lossy())).collect();
524        assert_eq!(order, ["/i", "/usr/include"]);
525        assert!(search.resolve(&fs, "a.h", IncludeForm::Angled, None, 0).unwrap().is_system);
526    }
527
528    #[test]
529    fn dropping_an_entry_keeps_the_group_boundaries_where_the_groups_are() {
530        let fs = fs_with(&["/q/a.h", "/i/a.h"]);
531        let mut search = SearchPath::new();
532        search.push_quote("/q");
533        search.push_quote("/q");
534        search.push_bracket("/i");
535        search.push_bracket("/i");
536        search.remove_duplicates();
537        // An angled include still skips the one `-iquote` entry left rather than a stale two.
538        let at = search.start(IncludeForm::Angled);
539        let found = search.resolve(&fs, "a.h", IncludeForm::Angled, None, at).unwrap();
540        assert_eq!(norm(&found.name), "/i/a.h");
541    }
542
543    #[test]
544    fn a_system_directory_marks_what_it_holds_as_a_system_header() {
545        let fs = fs_with(&["/i/a.h", "/sys/b.h", "/after/c.h"]);
546        let mut search = SearchPath::new();
547        search.push_bracket("/i");
548        search.push_system("/sys");
549        search.push_after("/after");
550        let get = |n| search.resolve(&fs, n, IncludeForm::Angled, None, 0).unwrap();
551        assert!(!get("a.h").is_system);
552        assert!(get("b.h").is_system);
553        assert!(get("c.h").is_system);
554    }
555
556    #[test]
557    fn include_next_continues_past_the_directory_the_current_file_came_from() {
558        let fs = fs_with(&["/a/limits.h", "/b/limits.h", "/c/limits.h"]);
559        let mut search = SearchPath::new();
560        search.push_bracket("/a");
561        search.push_bracket("/b");
562        search.push_bracket("/c");
563
564        let first = search.resolve(&fs, "limits.h", IncludeForm::Angled, None, 0).unwrap();
565        assert_eq!(norm(&first.name), "/a/limits.h");
566        let second =
567            search.resolve(&fs, "limits.h", IncludeForm::Angled, None, first.next).unwrap();
568        assert_eq!(norm(&second.name), "/b/limits.h");
569        let third =
570            search.resolve(&fs, "limits.h", IncludeForm::Angled, None, second.next).unwrap();
571        assert_eq!(norm(&third.name), "/c/limits.h");
572        assert!(search.resolve(&fs, "limits.h", IncludeForm::Angled, None, third.next).is_none());
573    }
574
575    #[test]
576    fn a_name_with_a_directory_in_it_is_joined_onto_each_entry() {
577        let fs = fs_with(&["/i/sys/types.h"]);
578        let mut search = SearchPath::new();
579        search.push_bracket("/i");
580        let found = search.resolve(&fs, "sys/types.h", IncludeForm::Angled, None, 0).unwrap();
581        assert_eq!(norm(&found.name), "/i/sys/types.h");
582        assert_eq!(text(&found), "/* /i/sys/types.h */\n");
583    }
584
585    #[test]
586    fn an_absolute_name_ignores_the_search_path() {
587        let fs = fs_with(&["/gen/config.h", "/i/gen/config.h"]);
588        let mut search = SearchPath::new();
589        search.push_bracket("/i");
590        let found = search.resolve(&fs, "/gen/config.h", IncludeForm::Angled, None, 0).unwrap();
591        assert_eq!(norm(&found.name), "/gen/config.h");
592        assert!(search.tried("/gen/config.h", IncludeForm::Angled, None, 0).is_empty());
593    }
594
595    #[test]
596    fn the_list_of_places_tried_is_the_list_that_was_searched() {
597        let fs = MemoryFileSystem::new();
598        let mut search = SearchPath::new();
599        search.push_quote("/q");
600        search.push_bracket("/i");
601        search.push_system("/sys");
602        let here = Path::new("/src");
603
604        assert!(search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(here), 0).is_none());
605        let tried = search.tried("a.h", IncludeForm::Quoted, Some(here), 0);
606        let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
607        assert_eq!(tried, ["/src", "/q", "/i", "/sys"]);
608
609        let start = search.start(IncludeForm::Angled);
610        let tried = search.tried("a.h", IncludeForm::Angled, Some(here), start);
611        let tried: Vec<_> = tried.iter().map(|p| norm(&p.to_string_lossy())).collect();
612        assert_eq!(tried, ["/i", "/sys"]);
613    }
614
615    #[test]
616    fn a_header_found_next_to_its_includer_does_not_skip_the_whole_path_afterwards() {
617        // `at` for a file found beside its includer has to leave `at + 1` at the top of the
618        // path, because the directory it was found in is not on the path at all.
619        let fs = fs_with(&["/src/a.h", "/i/b.h"]);
620        let mut search = SearchPath::new();
621        search.push_bracket("/i");
622        let found =
623            search.resolve(&fs, "a.h", IncludeForm::Quoted, Some(Path::new("/src")), 0).unwrap();
624        assert_eq!(found.next, 0);
625        let next = search.resolve(&fs, "b.h", IncludeForm::Angled, None, found.next).unwrap();
626        assert_eq!(norm(&next.name), "/i/b.h");
627    }
628}