use super::glob_match;
use super::list::Ls;
use super::rmdir::Rmdir;
use crate::builtins::{Builtin, Context};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::fs::{FileSystem, InMemoryFs};
async fn create_test_ctx() -> (Arc<InMemoryFs>, PathBuf, HashMap<String, String>) {
let fs = Arc::new(InMemoryFs::new());
let cwd = PathBuf::from("/home/user");
let variables = HashMap::new();
fs.mkdir(&cwd, true).await.unwrap();
(fs, cwd, variables)
}
#[tokio::test]
async fn test_ls_empty_dir() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
let args: Vec<String> = vec![];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "");
}
#[tokio::test]
async fn test_ls_with_files() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.write_file(&cwd.join("file1.txt"), b"content1")
.await
.unwrap();
fs.write_file(&cwd.join("file2.txt"), b"content2")
.await
.unwrap();
let args: Vec<String> = vec![];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("file1.txt"));
assert!(result.stdout.contains("file2.txt"));
}
#[tokio::test]
async fn test_ls_hidden_files() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.write_file(&cwd.join(".hidden"), b"hidden")
.await
.unwrap();
fs.write_file(&cwd.join("visible"), b"visible")
.await
.unwrap();
let args: Vec<String> = vec![];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(!result.stdout.contains(".hidden"));
assert!(result.stdout.contains("visible"));
let args = vec!["-a".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains(".hidden"));
assert!(result.stdout.contains("visible"));
}
#[tokio::test]
async fn test_ls_long_format() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.write_file(&cwd.join("test.txt"), b"content")
.await
.unwrap();
let args = vec!["-l".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("rw"));
assert!(result.stdout.contains("test.txt"));
}
#[tokio::test]
async fn test_ls_nonexistent() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
let args = vec!["nonexistent".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 2);
assert!(result.stderr.contains("No such file or directory"));
}
#[tokio::test]
async fn test_ls_invalid_option() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
let args = vec!["-z".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 2);
let combined = format!("{}{}", result.stdout, result.stderr);
assert!(
combined.contains("unexpected argument") || combined.contains("invalid option"),
"expected an unknown-flag diagnostic, got: {}",
combined
);
}
#[tokio::test]
async fn test_ls_sort_by_time() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.write_file(&cwd.join("older.txt"), b"older")
.await
.unwrap();
fs.write_file(&cwd.join("newer.txt"), b"newer")
.await
.unwrap();
let args = vec!["-t".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("older.txt"));
assert!(result.stdout.contains("newer.txt"));
}
#[tokio::test]
async fn test_ls_file() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.write_file(&cwd.join("test.txt"), b"content")
.await
.unwrap();
let args = vec!["test.txt".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("test.txt"));
}
#[tokio::test]
async fn test_ls_recursive() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.mkdir(&cwd.join("subdir"), false).await.unwrap();
fs.write_file(&cwd.join("file.txt"), b"content")
.await
.unwrap();
fs.write_file(&cwd.join("subdir/nested.txt"), b"nested")
.await
.unwrap();
let args = vec!["-R".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("file.txt"));
assert!(result.stdout.contains("subdir"));
assert!(result.stdout.contains("nested.txt"));
}
#[tokio::test]
#[serial_test::serial]
async fn ls_ignores_host_time_style_and_tabsize() {
unsafe {
std::env::set_var("TIME_STYLE", "long-iso");
std::env::set_var("TABSIZE", "16");
}
let (fs, mut cwd, mut variables) = create_test_ctx().await;
fs.write_file(&cwd.join("a.txt"), b"a").await.unwrap();
let env = HashMap::new();
let args: Vec<String> = vec![];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
unsafe {
std::env::remove_var("TIME_STYLE");
std::env::remove_var("TABSIZE");
}
assert_eq!(
result.exit_code, 0,
"host TIME_STYLE/TABSIZE leaked into bashkit ls parser \
(TM-INF-024): stderr={}",
result.stderr
);
assert!(result.stdout.contains("a.txt"));
assert!(
!result.stderr.contains("not yet implemented"),
"host env tunneled through clap as a value source: stderr={}",
result.stderr
);
}
#[tokio::test]
async fn ls_honors_virtual_env_time_style() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
fs.write_file(&cwd.join("a.txt"), b"a").await.unwrap();
let mut env = HashMap::new();
env.insert("TIME_STYLE".to_string(), "long-iso".to_string());
let args: Vec<String> = vec![];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(
result.exit_code, 2,
"virtual TIME_STYLE should reach clap's parser; got stdout={} \
stderr={}",
result.stdout, result.stderr
);
assert!(
result.stderr.contains("not yet implemented") && result.stderr.contains("time-style"),
"expected unsupported-option error mentioning time-style; got stderr={}",
result.stderr
);
}
#[tokio::test]
async fn ls_argv_time_style_overrides_virtual_env() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let mut env = HashMap::new();
env.insert("TIME_STYLE".to_string(), "long-iso".to_string());
let args: Vec<String> = vec!["--time-style=iso".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 2);
assert!(
!result.stderr.contains("supplied more than once"),
"shim double-injected --time-style: stderr={}",
result.stderr
);
}
#[tokio::test]
async fn test_ls_recursive_deep() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.mkdir(&cwd.join("a"), false).await.unwrap();
fs.mkdir(&cwd.join("a/b"), false).await.unwrap();
fs.mkdir(&cwd.join("a/b/c"), false).await.unwrap();
fs.write_file(&cwd.join("a/b/c/deep.txt"), b"deep content")
.await
.unwrap();
let args = vec!["-R".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("a"), "Should list dir a");
assert!(result.stdout.contains("b"), "Should list dir b under a");
assert!(result.stdout.contains("c"), "Should list dir c under a/b");
assert!(
result.stdout.contains("deep.txt"),
"Should list deep.txt under a/b/c"
);
}
#[tokio::test]
async fn test_rmdir_empty() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.mkdir(&cwd.join("emptydir"), false).await.unwrap();
let args = vec!["emptydir".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Rmdir.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(!fs.exists(&cwd.join("emptydir")).await.unwrap());
}
#[tokio::test]
async fn test_rmdir_not_empty() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.mkdir(&cwd.join("notempty"), false).await.unwrap();
fs.write_file(&cwd.join("notempty/file.txt"), b"content")
.await
.unwrap();
let args = vec!["notempty".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Rmdir.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("not empty"));
}
#[tokio::test]
async fn test_rmdir_nonexistent() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
let args = vec!["nonexistent".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Rmdir.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("No such file or directory"));
}
#[tokio::test]
async fn test_rmdir_not_directory() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.write_file(&cwd.join("file.txt"), b"content")
.await
.unwrap();
let args = vec!["file.txt".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Rmdir.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("Not a directory"));
}
#[tokio::test]
async fn test_rmdir_parents() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.mkdir(&cwd.join("a/b/c"), true).await.unwrap();
let args = vec!["-p".to_string(), "a/b/c".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Rmdir.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(!fs.exists(&cwd.join("a/b/c")).await.unwrap());
assert!(!fs.exists(&cwd.join("a/b")).await.unwrap());
assert!(!fs.exists(&cwd.join("a")).await.unwrap());
}
#[tokio::test]
async fn test_rmdir_missing_operand() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
let args: Vec<String> = vec![];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Rmdir.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("missing operand"));
}
#[tokio::test]
async fn test_ls_recursive_with_overlay_fs() {
use crate::fs::OverlayFs;
let base = Arc::new(InMemoryFs::new());
base.mkdir(Path::new("/home/user"), true).await.unwrap();
base.mkdir(Path::new("/home/user/dir"), false)
.await
.unwrap();
base.mkdir(Path::new("/home/user/dir/subdir"), false)
.await
.unwrap();
base.write_file(Path::new("/home/user/dir/base.txt"), b"base")
.await
.unwrap();
base.write_file(Path::new("/home/user/dir/subdir/deep.txt"), b"deep")
.await
.unwrap();
let overlay: Arc<dyn FileSystem> = Arc::new(OverlayFs::new(base));
let mut cwd = PathBuf::from("/home/user");
let mut variables = HashMap::new();
let env = HashMap::new();
let args = vec!["-R".to_string(), "dir".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: overlay,
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("base.txt"), "Should list base.txt");
assert!(result.stdout.contains("subdir"), "Should list subdir");
assert!(result.stdout.contains("deep.txt"), "Should list deep.txt");
}
#[tokio::test]
async fn test_ls_recursive_with_mountable_fs() {
use crate::fs::MountableFs;
let root = Arc::new(InMemoryFs::new());
root.mkdir(Path::new("/home/user"), true).await.unwrap();
let mounted = Arc::new(InMemoryFs::new());
mounted.mkdir(Path::new("/a"), false).await.unwrap();
mounted.mkdir(Path::new("/a/b"), false).await.unwrap();
mounted
.write_file(Path::new("/a/file_a.txt"), b"a")
.await
.unwrap();
mounted
.write_file(Path::new("/a/b/file_b.txt"), b"b")
.await
.unwrap();
let mountable = MountableFs::new(root);
mountable.mount("/home/user/mnt", mounted.clone()).unwrap();
let fs: Arc<dyn FileSystem> = Arc::new(mountable);
let mut cwd = PathBuf::from("/home/user");
let mut variables = HashMap::new();
let env = HashMap::new();
let args = vec!["-R".to_string(), "mnt".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("a"), "Should list directory a");
assert!(result.stdout.contains("b"), "Should list directory b");
assert!(
result.stdout.contains("file_a.txt"),
"Should list file_a.txt"
);
assert!(
result.stdout.contains("file_b.txt"),
"Should list file_b.txt"
);
}
#[tokio::test]
async fn test_ls_root_directory() {
let fs = Arc::new(InMemoryFs::new());
let mut cwd = PathBuf::from("/home/user");
let mut variables = HashMap::new();
let env = HashMap::new();
let args = vec!["/".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(
result.exit_code, 0,
"ls / should succeed: {}",
result.stderr
);
assert!(result.stdout.contains("tmp"), "Root should contain tmp");
assert!(result.stdout.contains("home"), "Root should contain home");
assert!(result.stdout.contains("dev"), "Root should contain dev");
}
#[tokio::test]
async fn test_ls_dot_from_root() {
let fs = Arc::new(InMemoryFs::new());
let mut cwd = PathBuf::from("/");
let mut variables = HashMap::new();
let env = HashMap::new();
let args = vec![".".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(
result.exit_code, 0,
"ls . from / should succeed: {}",
result.stderr
);
assert!(result.stdout.contains("tmp"), "Root should contain tmp");
assert!(result.stdout.contains("home"), "Root should contain home");
}
#[tokio::test]
async fn test_ls_default_from_root() {
let fs = Arc::new(InMemoryFs::new());
let mut cwd = PathBuf::from("/");
let mut variables = HashMap::new();
let env = HashMap::new();
let args: Vec<String> = vec![];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(
result.exit_code, 0,
"ls from / should succeed: {}",
result.stderr
);
assert!(result.stdout.contains("tmp"), "Root should contain tmp");
assert!(result.stdout.contains("home"), "Root should contain home");
}
#[tokio::test]
async fn test_ls_root_with_overlay_fs() {
use crate::fs::OverlayFs;
let base = Arc::new(InMemoryFs::new());
let overlay: Arc<dyn FileSystem> = Arc::new(OverlayFs::new(base));
let mut cwd = PathBuf::from("/");
let mut variables = HashMap::new();
let env = HashMap::new();
let args = vec!["/".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: overlay.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(
result.exit_code, 0,
"ls / with overlay should succeed: {}",
result.stderr
);
assert!(result.stdout.contains("tmp"), "Root should contain tmp");
assert!(result.stdout.contains("home"), "Root should contain home");
}
#[tokio::test]
async fn test_ls_dot_from_root_with_overlay_fs() {
use crate::fs::OverlayFs;
let base = Arc::new(InMemoryFs::new());
let overlay: Arc<dyn FileSystem> = Arc::new(OverlayFs::new(base));
let mut cwd = PathBuf::from("/");
let mut variables = HashMap::new();
let env = HashMap::new();
let args = vec![".".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: overlay.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(
result.exit_code, 0,
"ls . from / with overlay should succeed: {}",
result.stderr
);
assert!(result.stdout.contains("tmp"), "Root should contain tmp");
assert!(result.stdout.contains("home"), "Root should contain home");
}
#[tokio::test]
async fn test_resolve_path_slash_dot_normalized() {
let fs = Arc::new(InMemoryFs::new());
assert!(fs.exists(Path::new("/.")).await.unwrap(), "/. should exist");
assert!(fs.exists(Path::new("/")).await.unwrap(), "/ should exist");
let stat_root = fs.stat(Path::new("/")).await.unwrap();
let stat_dot = fs.stat(Path::new("/.")).await.unwrap();
assert!(stat_root.file_type.is_dir());
assert!(stat_dot.file_type.is_dir());
}
#[tokio::test]
async fn test_ls_nonexistent_path() {
let fs = Arc::new(InMemoryFs::new());
let mut cwd = PathBuf::from("/home/user");
let mut variables = HashMap::new();
let env = HashMap::new();
let args = vec!["/nonexistent/path".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 2, "ls on nonexistent path should fail");
assert!(
result.stderr.contains("No such file or directory"),
"Should report file not found"
);
}
#[tokio::test]
async fn test_ls_path_traversal_normalized() {
let fs = Arc::new(InMemoryFs::new());
let mut cwd = PathBuf::from("/home/user");
let mut variables = HashMap::new();
let env = HashMap::new();
let args = vec!["../user".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(
result.exit_code, 0,
"ls with .. should succeed after normalization: {}",
result.stderr
);
}
#[tokio::test]
async fn test_ls_excessive_dotdot_stays_at_root() {
let fs = Arc::new(InMemoryFs::new());
let mut cwd = PathBuf::from("/home/user");
let mut variables = HashMap::new();
let env = HashMap::new();
let args = vec!["../../../../..".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(
result.exit_code, 0,
"Excessive .. should normalize to root: {}",
result.stderr
);
assert!(result.stdout.contains("tmp"), "Should list root (tmp)");
assert!(result.stdout.contains("home"), "Should list root (home)");
}
#[tokio::test]
async fn test_ls_dot_in_middle_of_path() {
let fs = Arc::new(InMemoryFs::new());
let mut cwd = PathBuf::from("/");
let mut variables = HashMap::new();
let env = HashMap::new();
let args = vec!["./home/./user/.".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(
result.exit_code, 0,
"Path with . components should work: {}",
result.stderr
);
}
#[tokio::test]
async fn test_ls_long_format_shows_correct_file_size() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
let content = b"hello world\n";
fs.write_file(&cwd.join("test.txt"), content).await.unwrap();
let args = vec!["-l".to_string(), "test.txt".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(
result.stdout.contains("12"),
"Expected size 12 in output, got: {}",
result.stdout
);
}
#[tokio::test]
async fn test_ls_long_format_empty_file_shows_zero_size() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.write_file(&cwd.join("empty.txt"), b"").await.unwrap();
let args = vec!["-l".to_string(), "empty.txt".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(
result.stdout.contains(" 0"),
"Expected size 0 in output, got: {}",
result.stdout
);
}
#[tokio::test]
async fn test_ls_long_format_directory_shows_zero_size() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.mkdir(&cwd.join("subdir"), false).await.unwrap();
let args = vec!["-l".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let lines: Vec<&str> = result.stdout.lines().collect();
let subdir_line = lines.iter().find(|l| l.contains("subdir")).unwrap();
assert!(
subdir_line.contains(" 0"),
"Expected directory size 0, got: {}",
subdir_line
);
}
#[tokio::test]
async fn test_ls_long_format_multiple_files_correct_sizes() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.write_file(&cwd.join("small.txt"), b"hi").await.unwrap(); fs.write_file(&cwd.join("medium.txt"), b"hello world")
.await
.unwrap(); fs.write_file(
&cwd.join("large.txt"),
b"this is a longer content string for testing",
)
.await
.unwrap();
let args = vec!["-l".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let lines: Vec<&str> = result.stdout.lines().collect();
let small_line = lines.iter().find(|l| l.contains("small.txt")).unwrap();
assert!(
small_line.contains(" 2"),
"Expected small.txt size 2, got: {}",
small_line
);
let medium_line = lines.iter().find(|l| l.contains("medium.txt")).unwrap();
assert!(
medium_line.contains(" 11"),
"Expected medium.txt size 11, got: {}",
medium_line
);
let large_line = lines.iter().find(|l| l.contains("large.txt")).unwrap();
assert!(
large_line.contains(" 43"),
"Expected large.txt size 43, got: {}",
large_line
);
}
#[test]
fn test_glob_match_star() {
assert!(glob_match("file.txt", "*.txt"));
assert!(glob_match("test.txt", "*.txt"));
assert!(!glob_match("file.md", "*.txt"));
}
#[test]
fn test_glob_match_question() {
assert!(glob_match("ab", "a?"));
assert!(glob_match("ac", "a?"));
assert!(!glob_match("abc", "a?"));
}
#[test]
fn test_glob_match_exact() {
assert!(glob_match("test", "test"));
assert!(!glob_match("test", "other"));
}
#[test]
fn test_glob_match_star_middle() {
assert!(glob_match("test.backup.txt", "test*.txt"));
assert!(glob_match("test.txt", "test*.txt"));
}
#[tokio::test]
async fn test_ls_directory_flag() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.mkdir(&cwd.join("subdir"), false).await.unwrap();
fs.write_file(&cwd.join("subdir/inner.txt"), b"x")
.await
.unwrap();
let args = vec!["-d".to_string(), "subdir".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
assert_eq!(result.stdout, "subdir\n");
assert!(!result.stdout.contains("inner.txt"));
}
#[tokio::test]
async fn test_ls_directory_long_flag() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.mkdir(&cwd.join("d1"), false).await.unwrap();
let args = vec!["--directory".to_string(), "d1".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
assert_eq!(result.stdout, "d1\n");
}
#[tokio::test]
async fn test_ls_directory_multiple() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.mkdir(&cwd.join("alpha"), false).await.unwrap();
fs.mkdir(&cwd.join("beta"), false).await.unwrap();
fs.write_file(&cwd.join("alpha/inner.txt"), b"x")
.await
.unwrap();
let args = vec!["-d".to_string(), "alpha".to_string(), "beta".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
assert!(result.stdout.contains("alpha"));
assert!(result.stdout.contains("beta"));
assert!(!result.stdout.contains("alpha:"));
assert!(!result.stdout.contains("inner.txt"));
}
#[tokio::test]
async fn test_ls_directory_classify() {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let env = HashMap::new();
fs.mkdir(&cwd.join("mydir"), false).await.unwrap();
let args = vec!["-d".to_string(), "-F".to_string(), "mydir".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
assert_eq!(result.stdout, "mydir/\n");
}
async fn ls_long_date_for(mtime_secs: u64) -> String {
let (fs, mut cwd, mut variables) = create_test_ctx().await;
let path = cwd.join("dated.txt");
fs.write_file(&path, b"x").await.unwrap();
fs.set_modified_time(
&path,
crate::time_compat::UNIX_EPOCH + std::time::Duration::from_secs(mtime_secs),
)
.await
.unwrap();
let env = HashMap::new();
let args = vec!["-l".to_string(), "dated.txt".to_string()];
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs: fs.clone(),
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
let result = Ls.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
result.stdout.to_string()
}
#[tokio::test]
async fn test_ls_long_format_renders_exact_utc_date() {
let stdout = ls_long_date_for(1_783_281_900).await;
assert!(
stdout.contains("2026-07-05 20:05"),
"expected mtime rendered as 2026-07-05 20:05, got: {}",
stdout
);
}
#[tokio::test]
async fn test_ls_long_format_date_leap_day() {
let stdout = ls_long_date_for(1_709_208_000).await;
assert!(
stdout.contains("2024-02-29 12:00"),
"expected leap day rendered as 2024-02-29 12:00, got: {}",
stdout
);
}
#[tokio::test]
async fn test_ls_long_format_date_year_boundary() {
let stdout = ls_long_date_for(1_798_761_540).await;
assert!(
stdout.contains("2026-12-31 23:59"),
"expected year boundary rendered as 2026-12-31 23:59, got: {}",
stdout
);
}