mod common;
use common::run_lua;
#[tokio::test]
async fn test_fs_read() {
let script = r#"
local content = fs.read("Cargo.toml")
assert.contains(content, "assay")
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_fs_read_nonexistent() {
let result = run_lua(r#"fs.read("/nonexistent/file.txt")"#).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_write_basic() {
let dir = std::env::temp_dir().join("assay_test_write_basic");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("test.txt");
let script = format!(
r#"fs.write("{}", "hello from assay")"#,
path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, "hello from assay");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_write_creates_parent_dirs() {
let dir = std::env::temp_dir().join("assay_test_write_nested/a/b/c");
let _ = std::fs::remove_dir_all(std::env::temp_dir().join("assay_test_write_nested"));
let path = dir.join("deep.txt");
let script = format!(
r#"fs.write("{}", "nested content")"#,
path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, "nested content");
let _ = std::fs::remove_dir_all(std::env::temp_dir().join("assay_test_write_nested"));
}
#[tokio::test]
async fn test_fs_write_overwrite() {
let dir = std::env::temp_dir().join("assay_test_write_overwrite");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("overwrite.txt");
std::fs::write(&path, "original").unwrap();
let script = format!(
r#"fs.write("{}", "replaced")"#,
path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, "replaced");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_write_then_read() {
let dir = std::env::temp_dir().join("assay_test_write_read");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("roundtrip.txt");
let script = format!(
r#"
fs.write("{path}", "roundtrip data")
local content = fs.read("{path}")
assert.eq(content, "roundtrip data")
"#,
path = path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_write_invalid_path() {
let result = run_lua(r#"fs.write("/proc/0/nonexistent", "data")"#).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_exists() {
let script = r#"
assert.eq(fs.exists("Cargo.toml"), true)
assert.eq(fs.exists("/nonexistent/path"), false)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_fs_mkdir_and_remove() {
let dir = std::env::temp_dir().join("assay_test_mkdir");
let _ = std::fs::remove_dir_all(&dir);
let script = format!(
r#"
fs.mkdir("{}")
assert.eq(fs.exists("{}"), true)
fs.remove("{}")
assert.eq(fs.exists("{}"), false)
"#,
dir.display(),
dir.display(),
dir.display(),
dir.display()
);
run_lua(&script).await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_stat() {
let dir = std::env::temp_dir().join("assay_test_stat");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("stat_test.txt");
std::fs::write(&path, "hello stat").unwrap();
let script = format!(
r#"
local info = fs.stat("{}")
assert.eq(info.is_file, true)
assert.eq(info.is_dir, false)
assert.eq(info.size, 10)
assert.not_nil(info.modified)
"#,
path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_stat_directory() {
let script = r#"
local info = fs.stat("src")
assert.eq(info.is_dir, true)
assert.eq(info.is_file, false)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_fs_stat_nonexistent() {
let result = run_lua(r#"fs.stat("/nonexistent/file")"#).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_list() {
let dir = std::env::temp_dir().join("assay_test_list");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("a.txt"), "a").unwrap();
std::fs::write(dir.join("b.txt"), "b").unwrap();
std::fs::create_dir_all(dir.join("subdir")).unwrap();
let script = format!(
r#"
local entries = fs.list("{}")
assert.not_nil(entries)
local count = 0
local has_file = false
local has_dir = false
for _, e in ipairs(entries) do
count = count + 1
if e.type == "file" then has_file = true end
if e.type == "directory" then has_dir = true end
end
assert.eq(count, 3)
assert.eq(has_file, true)
assert.eq(has_dir, true)
"#,
dir.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_remove_file() {
let dir = std::env::temp_dir().join("assay_test_remove_file");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("removeme.txt");
std::fs::write(&path, "bye").unwrap();
let script = format!(
r#"
assert.eq(fs.exists("{}"), true)
fs.remove("{}")
assert.eq(fs.exists("{}"), false)
"#,
path.display().to_string().replace('\\', "\\\\"),
path.display().to_string().replace('\\', "\\\\"),
path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_remove_nonexistent() {
let result = run_lua(r#"fs.remove("/nonexistent/file.txt")"#).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_copy() {
let script = r#"
local dir = fs.tempdir()
fs.write(dir .. "/source.txt", "copy me")
local bytes = fs.copy(dir .. "/source.txt", dir .. "/dest.txt")
assert.eq(bytes, 7)
local content = fs.read(dir .. "/dest.txt")
assert.eq(content, "copy me")
-- Source should still exist
assert.eq(fs.exists(dir .. "/source.txt"), true)
fs.remove(dir)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_fs_copy_nonexistent() {
let result = run_lua(r#"fs.copy("/nonexistent/src", "/tmp/dst")"#).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_rename() {
let script = r#"
local dir = fs.tempdir()
fs.write(dir .. "/before.txt", "rename me")
fs.rename(dir .. "/before.txt", dir .. "/after.txt")
assert.eq(fs.exists(dir .. "/before.txt"), false)
assert.eq(fs.exists(dir .. "/after.txt"), true)
local content = fs.read(dir .. "/after.txt")
assert.eq(content, "rename me")
fs.remove(dir)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_fs_rename_nonexistent() {
let result = run_lua(r#"fs.rename("/nonexistent/src", "/tmp/dst")"#).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_glob() {
let script = r#"
local dir = fs.tempdir()
fs.write(dir .. "/a.txt", "a")
fs.write(dir .. "/b.txt", "b")
fs.write(dir .. "/c.log", "c")
local matches = fs.glob(dir .. "/*.txt")
local count = 0
for _ in ipairs(matches) do count = count + 1 end
assert.eq(count, 2, "should match 2 .txt files")
local all = fs.glob(dir .. "/*")
local all_count = 0
for _ in ipairs(all) do all_count = all_count + 1 end
assert.eq(all_count, 3, "should match all 3 files")
fs.remove(dir)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_fs_glob_recursive() {
let script = r#"
local dir = fs.tempdir()
fs.mkdir(dir .. "/sub")
fs.write(dir .. "/top.txt", "top")
fs.write(dir .. "/sub/nested.txt", "nested")
local matches = fs.glob(dir .. "/**/*.txt")
local count = 0
for _ in ipairs(matches) do count = count + 1 end
assert.eq(count, 2, "should match 2 .txt files recursively")
fs.remove(dir)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_fs_glob_no_match() {
let script = r#"
local matches = fs.glob("/tmp/assay-nonexistent-pattern-*.xyz")
local count = 0
for _ in ipairs(matches) do count = count + 1 end
assert.eq(count, 0)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_fs_glob_invalid_pattern() {
let result = run_lua(r#"fs.glob("[invalid")"#).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_tempdir() {
let script = r#"
local dir = fs.tempdir()
assert.not_nil(dir)
assert.gt(#dir, 0, "tempdir path should not be empty")
assert.eq(fs.exists(dir), true)
-- Should be able to write into it
fs.write(dir .. "/test.txt", "hello")
assert.eq(fs.read(dir .. "/test.txt"), "hello")
fs.remove(dir)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_fs_chmod() {
let script = r#"
local dir = fs.tempdir()
local path = dir .. "/chmod_test.txt"
fs.write(path, "test")
-- Set to 0o644 = 420 decimal
fs.chmod(path, 420)
local stat = fs.stat(path)
assert.eq(stat.is_file, true)
-- Set to 0o755 = 493 decimal
fs.chmod(path, 493)
fs.remove(dir)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_fs_chmod_nonexistent() {
let result = run_lua(r#"fs.chmod("/nonexistent/file", 420)"#).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_readdir() {
let script = r#"
local dir = fs.tempdir()
fs.write(dir .. "/a.txt", "a")
fs.mkdir(dir .. "/sub")
fs.write(dir .. "/sub/b.txt", "b")
local entries = fs.readdir(dir)
local count = 0
local has_file = false
local has_dir = false
local has_nested = false
for _, e in ipairs(entries) do
count = count + 1
if e.path == "a.txt" and e.type == "file" then has_file = true end
if e.path == "sub" and e.type == "directory" then has_dir = true end
if e.path == "sub/b.txt" and e.type == "file" then has_nested = true end
end
assert.eq(count, 3, "should have 3 entries (a.txt, sub, sub/b.txt)")
assert.eq(has_file, true, "should have a.txt")
assert.eq(has_dir, true, "should have sub directory")
assert.eq(has_nested, true, "should have sub/b.txt")
fs.remove(dir)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_fs_readdir_with_depth() {
let script = r#"
local dir = fs.tempdir()
fs.write(dir .. "/top.txt", "top")
fs.mkdir(dir .. "/a")
fs.write(dir .. "/a/mid.txt", "mid")
fs.mkdir(dir .. "/a/b")
fs.write(dir .. "/a/b/deep.txt", "deep")
-- depth=1 should only list top level
local entries = fs.readdir(dir, {depth = 1})
local count = 0
local has_deep = false
for _, e in ipairs(entries) do
count = count + 1
if e.path == "a/b/deep.txt" then has_deep = true end
end
-- Should have: top.txt, a, a/mid.txt (a is listed, mid.txt at depth 1 inside a)
-- but NOT a/b/deep.txt (that's depth 2)
assert.eq(has_deep, false, "should not include deep entries at depth=1")
fs.remove(dir)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_fs_readdir_nonexistent() {
let result = run_lua(r#"fs.readdir("/nonexistent/path")"#).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_lines_basic() {
let dir = std::env::temp_dir().join("assay_test_lines_basic");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("three.txt");
std::fs::write(&path, "alpha\nbeta\ngamma\n").unwrap();
let script = format!(
r#"
local out = {{}}
for line in fs.lines("{}") do
table.insert(out, line)
end
assert.eq(#out, 3)
assert.eq(out[1], "alpha")
assert.eq(out[2], "beta")
assert.eq(out[3], "gamma")
"#,
path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_lines_empty_file() {
let dir = std::env::temp_dir().join("assay_test_lines_empty");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("empty.txt");
std::fs::write(&path, "").unwrap();
let script = format!(
r#"
local count = 0
for _ in fs.lines("{}") do count = count + 1 end
assert.eq(count, 0, "empty file yields zero lines")
"#,
path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_lines_no_trailing_newline() {
let dir = std::env::temp_dir().join("assay_test_lines_no_trailing");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("no_trail.txt");
std::fs::write(&path, "one\ntwo").unwrap();
let script = format!(
r#"
local out = {{}}
for line in fs.lines("{}") do table.insert(out, line) end
assert.eq(#out, 2, "final line without trailing \\n still yielded")
assert.eq(out[1], "one")
assert.eq(out[2], "two")
"#,
path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_lines_nonexistent() {
let result = run_lua(r#"
for _ in fs.lines("/nonexistent/file.txt") do end
"#)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_sub_in_file_basic() {
let dir = std::env::temp_dir().join("assay_test_sub_basic");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("conf.yaml");
std::fs::write(&path, "image: assay:v0.10.1\nother: unchanged\n").unwrap();
let script = format!(
r#"
local n = fs.sub_in_file("{}", "v0%.10%.1", "v0.12.1")
assert.eq(n, 1, "one substitution")
local after = fs.read("{}")
assert.contains(after, "assay:v0.12.1")
assert.contains(after, "other: unchanged")
"#,
path.display().to_string().replace('\\', "\\\\"),
path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_sub_in_file_no_match() {
let dir = std::env::temp_dir().join("assay_test_sub_no_match");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("conf.yaml");
std::fs::write(&path, "nothing to see\n").unwrap();
let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
let script = format!(
r#"
local n = fs.sub_in_file("{}", "v0%.11%.x", "v0.12.1")
assert.eq(n, 0, "no match = no substitution")
"#,
path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let new_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
assert_eq!(
original_mtime, new_mtime,
"file should not be rewritten when no match"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_sub_in_file_multiple_occurrences() {
let dir = std::env::temp_dir().join("assay_test_sub_multi");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("conf.yaml");
std::fs::write(&path, "a: v0.10.1\nb: v0.10.1\nc: v0.10.1\n").unwrap();
let script = format!(
r#"
local n = fs.sub_in_file("{}", "v0%.10%.1", "v0.12.1")
assert.eq(n, 3, "three substitutions")
"#,
path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let after = std::fs::read_to_string(&path).unwrap();
assert_eq!(after, "a: v0.12.1\nb: v0.12.1\nc: v0.12.1\n");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_sub_in_file_backref() {
let dir = std::env::temp_dir().join("assay_test_sub_backref");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("conf.yaml");
std::fs::write(&path, "image: assay:v0.10.1\n").unwrap();
let script = format!(
r#"
local n = fs.sub_in_file("{}", "(v%d+%.%d+%.%d+)", "[%1]")
assert.eq(n, 1)
local after = fs.read("{}")
assert.contains(after, "[v0.10.1]")
"#,
path.display().to_string().replace('\\', "\\\\"),
path.display().to_string().replace('\\', "\\\\")
);
run_lua(&script).await.unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_fs_sub_in_file_nonexistent() {
let result = run_lua(r#"fs.sub_in_file("/nonexistent/file", "x", "y")"#).await;
assert!(result.is_err());
}