Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 185 additions & 5 deletions turbopack/crates/turbo-tasks-fuzz/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![cfg_attr(windows, feature(junction_point))]

use std::{
fs::OpenOptions,
io::Write,
Expand All @@ -7,7 +9,7 @@ use std::{
time::Duration,
};

use clap::{Args, Parser, Subcommand};
use clap::{Args, Parser, Subcommand, ValueEnum};
use rand::{Rng, RngCore, SeedableRng};
use rustc_hash::FxHashSet;
use tokio::time::sleep;
Expand Down Expand Up @@ -55,6 +57,28 @@ struct FsWatcher {
/// Call `start_watching` after the initial read of files instead of before (the default).
#[arg(long)]
start_watching_late: bool,
/// Enable symlink testing. The mode controls what kind of targets the symlinks point to.
#[arg(long, value_enum)]
symlinks: Option<SymlinkMode>,
/// Total number of symlinks to create.
#[arg(long, default_value_t = 80, requires = "symlinks")]
symlink_count: u32,
/// Number of symlink modifications per iteration (only used when --symlinks is set).
#[arg(long, default_value_t = 20, requires = "symlinks")]
symlink_modifications: u32,
}

#[derive(Clone, Copy, Debug, ValueEnum)]
enum SymlinkMode {
/// Test file symlinks
#[cfg_attr(windows, doc = "(requires developer mode or admin)")]
File,
/// Test directory symlinks
#[cfg_attr(windows, doc = "(requires developer mode or admin)")]
Directory,
/// Test junction points (Windows-only)
#[cfg(windows)]
Junction,
}

#[tokio::main]
Expand All @@ -80,6 +104,7 @@ async fn fuzz_fs_watcher(args: FsWatcher) -> anyhow::Result<()> {
BackendOptions::default(),
noop_backing_storage(),
));

tt.run_once(async move {
let invalidations = TransientInstance::new(PathInvalidations::default());
let fs_root_rcstr = RcStr::from(fs_root.to_str().unwrap());
Expand All @@ -91,14 +116,30 @@ async fn fuzz_fs_watcher(args: FsWatcher) -> anyhow::Result<()> {
.await?
.owned()
.await?;

create_directory_tree(&mut FxHashSet::default(), &fs_root, args.depth, args.width)?;

let mut symlink_targets = if let Some(mode) = args.symlinks {
create_initial_symlinks(&fs_root, mode, args.symlink_count, args.depth)?
} else {
Vec::new()
};

if !args.start_watching_late {
project_fs.await?.start_watching(None).await?;
}

let read_all_paths_op =
read_all_paths_operation(invalidations.clone(), project_root, args.depth, args.width);
let read_all_paths_op = read_all_paths_operation(
invalidations.clone(),
project_root.clone(),
args.depth,
args.width,
if args.symlinks.is_some() {
args.symlink_count
} else {
0
},
);
read_all_paths_op.read_strongly_consistent().await?;
{
let mut invalidations = invalidations.0.lock().unwrap();
Expand Down Expand Up @@ -134,15 +175,44 @@ async fn fuzz_fs_watcher(args: FsWatcher) -> anyhow::Result<()> {
args.width,
)?;
}

if let Some(mode) = args.symlinks
&& !symlink_targets.is_empty()
{
for _ in 0..args.symlink_modifications {
let symlink_idx = rng.random_range(0..symlink_targets.len());
let old_target = &symlink_targets[symlink_idx];

let new_target_relative = pick_random_link_target(args.depth, args.width, mode);

if new_target_relative != *old_target {
let symlink_path = fs_root.join("_symlinks").join(symlink_idx.to_string());
let relative_target = Path::new("..").join(&new_target_relative);

remove_symlink(&symlink_path, mode)?;
create_symlink(&symlink_path, &relative_target, mode)?;

modified_file_paths.insert(symlink_path);
symlink_targets[symlink_idx] = new_target_relative;
}
}
}

// there's no way to know when we've received all the pending events from the operating
// system, so just sleep and pray
sleep(Duration::from_millis(args.notify_timeout_ms)).await;
read_all_paths_op.read_strongly_consistent().await?;
{
let mut invalidations = invalidations.0.lock().unwrap();
let symlink_info = if args.symlinks.is_some() {
" and symlinks"
} else {
""
};
println!(
"modified {} files and found {} invalidations",
"modified {} files{}. found {} invalidations",
modified_file_paths.len(),
symlink_info,
invalidations.len()
);
if args.print_missing_invalidations {
Expand Down Expand Up @@ -186,12 +256,24 @@ async fn read_path(
Ok(())
}

#[turbo_tasks::function]
async fn read_link(
invalidations: TransientInstance<PathInvalidations>,
path: FileSystemPath,
) -> anyhow::Result<()> {
let path_str = path.path.clone();
invalidations.0.lock().unwrap().insert(path_str);
let _ = path.read_link().await?;
Ok(())
}

#[turbo_tasks::function(operation)]
async fn read_all_paths_operation(
invalidations: TransientInstance<PathInvalidations>,
root: FileSystemPath,
depth: usize,
width: usize,
symlink_count: u32,
) -> anyhow::Result<()> {
async fn read_all_paths_inner(
invalidations: TransientInstance<PathInvalidations>,
Expand All @@ -216,7 +298,15 @@ async fn read_all_paths_operation(
}
Ok(())
}
read_all_paths_inner(invalidations, root, depth, width).await
read_all_paths_inner(invalidations.clone(), root.clone(), depth, width).await?;

let symlinks_dir = root.join("_symlinks")?;
for i in 0..symlink_count {
let symlink_path = symlinks_dir.join(&i.to_string())?;
read_link(invalidations.clone(), symlink_path).await?;
}

Ok(())
}

fn create_directory_tree(
Expand Down Expand Up @@ -244,6 +334,87 @@ fn create_directory_tree(
Ok(())
}

fn create_initial_symlinks(
fs_root: &Path,
symlink_mode: SymlinkMode,
symlink_count: u32,
depth: usize,
) -> anyhow::Result<Vec<PathBuf>> {
// Use a dedicated "symlinks" directory to avoid conflicts
let symlinks_dir = fs_root.join("_symlinks");
std::fs::create_dir_all(&symlinks_dir)?;

let initial_target_relative = match symlink_mode {
SymlinkMode::File => {
// Point to a file at depth: 0/0/0/.../0
let mut path = PathBuf::new();
for _ in 0..depth {
path.push("0");
}
path
}
SymlinkMode::Directory => PathBuf::from("0"),
#[cfg(windows)]
SymlinkMode::Junction => PathBuf::from("0"),
};

let relative_target = Path::new("..").join(&initial_target_relative);

let mut symlink_targets = Vec::new();
for i in 0..symlink_count {
let symlink_path = symlinks_dir.join(i.to_string());
create_symlink(&symlink_path, &relative_target, symlink_mode)?;
symlink_targets.push(initial_target_relative.clone());
}

Ok(symlink_targets)
}

fn create_symlink(link_path: &Path, target: &Path, mode: SymlinkMode) -> anyhow::Result<()> {
#[cfg(unix)]
{
let _ = mode;
std::os::unix::fs::symlink(target, link_path)?;
}
#[cfg(windows)]
{
match mode {
SymlinkMode::File => {
std::os::windows::fs::symlink_file(target, link_path)?;
}
SymlinkMode::Directory => {
std::os::windows::fs::symlink_dir(target, link_path)?;
}
SymlinkMode::Junction => {
// Junction points require absolute paths
let absolute_target = link_path.parent().unwrap_or(link_path).join(target);
std::os::windows::fs::junction_point(&absolute_target, link_path)?;
}
}
}
Ok(())
}

fn remove_symlink(link_path: &Path, mode: SymlinkMode) -> anyhow::Result<()> {
#[cfg(unix)]
{
let _ = mode;
std::fs::remove_file(link_path)?;
}
#[cfg(windows)]
{
match mode {
SymlinkMode::File | SymlinkMode::Directory => {
std::fs::remove_file(link_path)?;
}
SymlinkMode::Junction => {
std::fs::remove_dir(link_path)?;
}
}
}
Ok(())
}

fn pick_random_file(depth: usize, width: usize) -> PathBuf {
let mut rng = rand::rng();
iter::repeat_with(|| rng.random_range(0..width).to_string())
Expand All @@ -266,6 +437,15 @@ fn pick_random_directory(max_depth: usize, width: usize) -> RandomDirectory {
RandomDirectory { depth, path }
}

fn pick_random_link_target(depth: usize, width: usize, mode: SymlinkMode) -> PathBuf {
match mode {
SymlinkMode::File => pick_random_file(depth, width),
SymlinkMode::Directory => pick_random_directory(depth, width).path,
#[cfg(windows)]
SymlinkMode::Junction => pick_random_directory(depth, width).path,
}
}

struct FsCleanup<'a> {
path: &'a Path,
}
Expand Down
Loading