turbo_tasks_fuzz/
main.rs

1use std::{
2    fs::OpenOptions,
3    io::Write,
4    iter,
5    path::{Path, PathBuf},
6    sync::{Arc, Mutex},
7    time::Duration,
8};
9
10use clap::{Args, Parser, Subcommand};
11use rand::{Rng, RngCore, SeedableRng};
12use rustc_hash::FxHashSet;
13use tokio::time::sleep;
14use turbo_rcstr::{RcStr, rcstr};
15use turbo_tasks::{NonLocalValue, ResolvedVc, TransientInstance, Vc, trace::TraceRawVcs};
16use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
17use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemPath};
18
19/// A collection of fuzzers for `turbo-tasks`. These are not test cases as they're slow and (in many
20/// cases) non-deterministic.
21///
22/// It's recommend you build this with `--release`.
23///
24/// This is its own crate to avoid littering other crates with binary-only dependencies
25/// <https://github.com/rust-lang/cargo/issues/1982>.
26#[derive(Parser)]
27#[command()]
28struct Cli {
29    #[command(subcommand)]
30    command: Commands,
31}
32
33#[derive(Subcommand)]
34enum Commands {
35    /// Continuously fuzzes the filesystem watcher until ctrl+c'd.
36    FsWatcher(FsWatcher),
37}
38
39#[derive(Args)]
40struct FsWatcher {
41    #[arg(long)]
42    fs_root: PathBuf,
43    #[arg(long, default_value_t = 4)]
44    depth: usize,
45    #[arg(long, default_value_t = 6)]
46    width: usize,
47    #[arg(long, default_value_t = 100)]
48    notify_timeout_ms: u64,
49    #[arg(long, default_value_t = 200)]
50    file_modifications: u32,
51    #[arg(long, default_value_t = 2)]
52    directory_modifications: u32,
53    #[arg(long)]
54    print_missing_invalidations: bool,
55    /// Call `start_watching` after the initial read of files instead of before (the default).
56    #[arg(long)]
57    start_watching_late: bool,
58}
59
60#[tokio::main]
61async fn main() -> anyhow::Result<()> {
62    register();
63    let cli = Cli::parse();
64
65    match cli.command {
66        Commands::FsWatcher(args) => fuzz_fs_watcher(args).await,
67    }
68}
69
70#[derive(Default, NonLocalValue, TraceRawVcs)]
71struct PathInvalidations(#[turbo_tasks(trace_ignore)] Arc<Mutex<FxHashSet<RcStr>>>);
72
73async fn fuzz_fs_watcher(args: FsWatcher) -> anyhow::Result<()> {
74    std::fs::create_dir(&args.fs_root)?;
75    let fs_root = args.fs_root.canonicalize()?;
76    let _guard = FsCleanup {
77        path: &fs_root.clone(),
78    };
79
80    let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
81        BackendOptions::default(),
82        noop_backing_storage(),
83    ));
84    tt.run_once(async move {
85        let invalidations = TransientInstance::new(PathInvalidations::default());
86        let fs_root_rcstr = RcStr::from(fs_root.to_str().unwrap());
87        let project_fs = disk_file_system_operation(fs_root_rcstr.clone())
88            .resolve_strongly_consistent()
89            .await?;
90        let project_root = disk_file_system_root_operation(project_fs)
91            .resolve_strongly_consistent()
92            .await?
93            .owned()
94            .await?;
95        create_directory_tree(&mut FxHashSet::default(), &fs_root, args.depth, args.width)?;
96
97        if !args.start_watching_late {
98            project_fs.await?.start_watching(None).await?;
99        }
100
101        let read_all_paths_op =
102            read_all_paths_operation(invalidations.clone(), project_root, args.depth, args.width);
103        read_all_paths_op.read_strongly_consistent().await?;
104        {
105            let mut invalidations = invalidations.0.lock().unwrap();
106            println!("read all {} files", invalidations.len());
107            invalidations.clear();
108        }
109
110        if args.start_watching_late {
111            project_fs.await?.start_watching(None).await?;
112        }
113
114        let mut rand_buf = [0; 16];
115        let mut rng = rand::rngs::SmallRng::from_rng(&mut rand::rng());
116        loop {
117            let mut modified_file_paths = FxHashSet::default();
118            for _ in 0..args.file_modifications {
119                let path = fs_root.join(pick_random_file(args.depth, args.width));
120                let mut f = OpenOptions::new().write(true).truncate(true).open(&path)?;
121                rng.fill_bytes(&mut rand_buf);
122                f.write_all(&rand_buf)?;
123                f.flush()?;
124                modified_file_paths.insert(path);
125            }
126            for _ in 0..args.directory_modifications {
127                let dir = pick_random_directory(args.depth, args.width);
128                let path = fs_root.join(dir.path);
129                std::fs::remove_dir_all(&path)?;
130                std::fs::create_dir(&path)?;
131                create_directory_tree(
132                    &mut modified_file_paths,
133                    &path,
134                    args.depth - dir.depth,
135                    args.width,
136                )?;
137            }
138            // there's no way to know when we've received all the pending events from the operating
139            // system, so just sleep and pray
140            sleep(Duration::from_millis(args.notify_timeout_ms)).await;
141            read_all_paths_op.read_strongly_consistent().await?;
142            {
143                let mut invalidations = invalidations.0.lock().unwrap();
144                println!(
145                    "modified {} files and found {} invalidations",
146                    modified_file_paths.len(),
147                    invalidations.len()
148                );
149                if args.print_missing_invalidations {
150                    let absolute_path_invalidations = invalidations
151                        .iter()
152                        .map(|relative_path| fs_root.join(relative_path))
153                        .collect::<FxHashSet<PathBuf>>();
154                    let mut missing = modified_file_paths
155                        .difference(&absolute_path_invalidations)
156                        .collect::<Vec<_>>();
157                    missing.sort_unstable();
158                    for path in &missing {
159                        println!("  missing {path:?}");
160                    }
161                }
162                invalidations.clear();
163            }
164        }
165    })
166    .await
167}
168
169#[turbo_tasks::function(operation)]
170fn disk_file_system_operation(fs_root: RcStr) -> Vc<DiskFileSystem> {
171    DiskFileSystem::new(rcstr!("project"), fs_root)
172}
173
174#[turbo_tasks::function(operation)]
175fn disk_file_system_root_operation(fs: ResolvedVc<DiskFileSystem>) -> Vc<FileSystemPath> {
176    fs.root()
177}
178
179#[turbo_tasks::function]
180async fn read_path(
181    invalidations: TransientInstance<PathInvalidations>,
182    path: FileSystemPath,
183) -> anyhow::Result<()> {
184    let path_str = path.path.clone();
185    invalidations.0.lock().unwrap().insert(path_str);
186    let _ = path.read().await?;
187    Ok(())
188}
189
190#[turbo_tasks::function(operation)]
191async fn read_all_paths_operation(
192    invalidations: TransientInstance<PathInvalidations>,
193    root: FileSystemPath,
194    depth: usize,
195    width: usize,
196) -> anyhow::Result<()> {
197    async fn read_all_paths_inner(
198        invalidations: TransientInstance<PathInvalidations>,
199        parent: FileSystemPath,
200        depth: usize,
201        width: usize,
202    ) -> anyhow::Result<()> {
203        for child_id in 0..width {
204            let child_name = child_id.to_string();
205            let child_path = parent.join(&child_name)?;
206            if depth == 1 {
207                read_path(invalidations.clone(), child_path).await?;
208            } else {
209                Box::pin(read_all_paths_inner(
210                    invalidations.clone(),
211                    child_path,
212                    depth - 1,
213                    width,
214                ))
215                .await?;
216            }
217        }
218        Ok(())
219    }
220    read_all_paths_inner(invalidations, root, depth, width).await
221}
222
223fn create_directory_tree(
224    modified_file_paths: &mut FxHashSet<PathBuf>,
225    parent: &Path,
226    depth: usize,
227    width: usize,
228) -> anyhow::Result<()> {
229    let mut rng = rand::rng();
230    let mut rand_buf = [0; 16];
231    for child_id in 0..width {
232        let child_name = child_id.to_string();
233        let child_path = parent.join(&child_name);
234        if depth == 1 {
235            let mut f = std::fs::File::create(&child_path)?;
236            rng.fill_bytes(&mut rand_buf);
237            f.write_all(&rand_buf)?;
238            f.flush()?;
239            modified_file_paths.insert(child_path);
240        } else {
241            std::fs::create_dir(&child_path)?;
242            create_directory_tree(modified_file_paths, &child_path, depth - 1, width)?;
243        }
244    }
245    Ok(())
246}
247
248fn pick_random_file(depth: usize, width: usize) -> PathBuf {
249    let mut rng = rand::rng();
250    iter::repeat_with(|| rng.random_range(0..width).to_string())
251        .take(depth)
252        .collect()
253}
254
255struct RandomDirectory {
256    depth: usize,
257    path: PathBuf,
258}
259
260fn pick_random_directory(max_depth: usize, width: usize) -> RandomDirectory {
261    let mut rng = rand::rng();
262    // never use a depth of 0 because that would be the root directory
263    let depth = rng.random_range(1..(max_depth - 1));
264    let path = iter::repeat_with(|| rng.random_range(0..width).to_string())
265        .take(depth)
266        .collect();
267    RandomDirectory { depth, path }
268}
269
270struct FsCleanup<'a> {
271    path: &'a Path,
272}
273
274impl Drop for FsCleanup<'_> {
275    fn drop(&mut self) {
276        std::fs::remove_dir_all(self.path).unwrap();
277    }
278}
279
280fn register() {
281    turbo_tasks::register();
282    turbo_tasks_fs::register();
283    include!(concat!(env!("OUT_DIR"), "/register.rs"));
284}