Files
bun.sh/test/js/bun/shell/commands/ls.test.ts
Zack Radisic 0a3ac50931 Refactor shell to use AllocationScope and fix memory issues (#20531)
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <Jarred-Sumner@users.noreply.github.com>
2025-06-23 22:29:48 -07:00

363 lines
13 KiB
TypeScript

import { $ } from "bun";
import { beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
import { isPosix, tempDirWithFiles } from "harness";
import { createTestBuilder } from "../util";
const TestBuilder = createTestBuilder(import.meta.path);
const fileExists = async (path: string): Promise<boolean> =>
$`ls -d ${path}`.then(o => o.stdout.toString() === `${path}\n`);
$.nothrow();
beforeAll(() => {
setDefaultTimeout(1000 * 60 * 5);
});
const BUN = process.argv0;
const DEV_NULL = process.platform === "win32" ? "NUL" : "/dev/null";
let node_modules_tempdir: string;
let allNodeModuleFiles: string[] = [];
let tempdir: string;
let allFiles: string[] = [];
const sortedLsOutput = (s: string) =>
s
.split("\n")
.map(s => s.trim().replaceAll("\\", "/"))
.filter(
s =>
s.length > 0 &&
// GNU coreutils prints out the current directory like:
//
// ```
// .:
// a b c
// ```
//
// We probably should match this
s !== ".:",
)
.sort();
describe("bunshell ls", () => {
beforeAll(async () => {
node_modules_tempdir = tempDirWithFiles("ls-node_modules", {});
tempdir = tempDirWithFiles("ls", {});
await $`echo ${packagejson()} > package.json; ${BUN} install &> ${DEV_NULL}`
.quiet()
.throws(true)
.cwd(node_modules_tempdir);
await $`touch a b c; mkdir foo; touch foo/a foo/b foo/c`.quiet().throws(true).cwd(tempdir);
allNodeModuleFiles = isPosix
? await Bun.$`ls -RA .`
.quiet()
.throws(true)
.cwd(node_modules_tempdir)
.text()
.then(s => sortedLsOutput(s))
: [];
allFiles = ["./foo:", "a", "a", "b", "b", "c", "c", "foo"];
});
describe("recursive", () => {
test.if(isPosix)("node_modules", async () => {
const s = await Bun.$`ls -RA .`.quiet().throws(true).cwd(node_modules_tempdir).text();
const lines = sortedLsOutput(s);
expect(lines).toEqual(allNodeModuleFiles);
});
test("basic", async () => {
const s = await Bun.$`ls -RA .`.quiet().throws(true).cwd(tempdir).text();
const lines = sortedLsOutput(s);
expect(lines).toEqual(allFiles);
});
});
describe("basic flags", () => {
test("no arguments (current directory)", async () => {
await TestBuilder.command`ls`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).toEqual(["a", "b", "c", "foo"].sort()))
.run();
});
test("-a flag shows all files including . and ..", async () => {
const tempdir = tempDirWithFiles("ls-show-all", {});
await $`touch .hidden regular; mkdir .hidden-dir`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls -a`
.setTempdir(tempdir)
.stdout(s => {
expect(sortedLsOutput(s)).toContain(".");
expect(sortedLsOutput(s)).toContain("..");
expect(sortedLsOutput(s)).toContain(".hidden");
expect(sortedLsOutput(s)).toContain(".hidden-dir");
})
.run();
});
test("-A flag shows almost all (excludes . and ..)", async () => {
const tempdir = tempDirWithFiles("ls-almost-all", {});
await $`touch .hidden regular`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls -A`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).not.toContain("."))
.stdout(s => expect(sortedLsOutput(s)).not.toContain(".."))
.stdout(s => expect(sortedLsOutput(s)).toContain(".hidden"))
.stdout(s => expect(sortedLsOutput(s)).toContain("regular"))
.run();
});
test("-d flag lists directories themselves", async () => {
await TestBuilder.command`ls -d foo`.setTempdir(tempdir).stdout("foo\n").run();
});
// test("-1 flag lists one file per line", async () => {
// await TestBuilder.command`ls -1`
// .setTempdir(tempdir)
// .stdout(s => expect(s.split("\n").filter(l => l.trim())).toEqual(["a", "b", "c", "foo"]))
// .run();
// });
});
describe("multiple arguments", () => {
test("multiple files", async () => {
await TestBuilder.command`ls a b c`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).toEqual(["a", "b", "c"]))
.run();
});
test("multiple directories", async () => {
const tempdir = tempDirWithFiles("ls-multi-dirs", {});
await $`mkdir dir1 dir2; touch dir1/file1 dir2/file2`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls dir1 dir2`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).toEqual(["dir1:", "dir2:", "file1", "file2"]))
.run();
});
test("mixed files and directories", async () => {
await TestBuilder.command`ls a foo`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).toEqual(["a", "foo:", "a", "b", "c"].sort()))
.run();
});
});
describe("edge cases", () => {
test("empty directory", async () => {
const tempdir = tempDirWithFiles("ls-empty", {});
await $`mkdir empty`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls empty`.setTempdir(tempdir).stdout("").run();
});
test("directory with only hidden files using -a", async () => {
const tempdir = tempDirWithFiles("ls-hidden-only-a", {});
await $`mkdir hidden-only; touch hidden-only/.hidden1 hidden-only/.hidden2`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls -a hidden-only`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).toEqual([".", "..", ".hidden1", ".hidden2"]))
.run();
});
test("very long filename", async () => {
const tempdir = tempDirWithFiles("ls-long-name", {});
const longName = "a".repeat(100);
await $`touch ${longName}`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).toContain(longName))
.run();
});
test("filename with spaces", async () => {
const tempdir = tempDirWithFiles("ls-spaces", {});
await $`touch "file with spaces"`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).toContain("file with spaces"))
.run();
});
test.if(isPosix)("filename with special characters", async () => {
const tempdir = tempDirWithFiles("ls-special", {});
await $`touch "file-with-!@#$%^&*()"`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).toContain("file-with-!@#$%^&*()"))
.run();
});
});
describe("flag combinations", () => {
test("-Ra flag (recursive + show all)", async () => {
const tempdir = tempDirWithFiles("ls-ra", {});
await $`mkdir sub; touch .hidden sub/.hidden-sub`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls -Ra`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).toContain(".hidden"))
.stdout(s => expect(sortedLsOutput(s)).toContain(".hidden-sub"))
.run();
});
test("-RA flag (recursive + almost all)", async () => {
const tempdir = tempDirWithFiles("ls-ra-caps", {});
await $`mkdir sub; touch .hidden sub/.hidden-sub`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls -RA`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).toContain(".hidden"))
.stdout(s => expect(sortedLsOutput(s)).toContain(".hidden-sub"))
.stdout(s => expect(sortedLsOutput(s)).not.toContain("."))
.run();
});
test("-d with multiple directories", async () => {
const tempdir = tempDirWithFiles("ls-d-multi", {});
await $`mkdir dir1 dir2`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls -d dir1 dir2`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).toEqual(["dir1", "dir2"]))
.run();
});
});
describe("errors", () => {
TestBuilder.command`ls lskdjflksdjf`
.stderr("ls: lskdjflksdjf: No such file or directory\n")
.exitCode(1)
.runAsTest("ls -R lskdjflksdjf");
test("multiple non-existent files", async () => {
await TestBuilder.command`ls nonexistent1 nonexistent2`
.exitCode(1)
.stderr(s => {
expect(s).toContain("nonexistent1: No such file or directory");
expect(s).toContain("nonexistent2: No such file or directory");
})
.ensureTempDir()
.run();
});
test("mixed existent and non-existent files", async () => {
await TestBuilder.command`ls a nonexistent`
.setTempdir(tempdir)
.exitCode(1)
.stdout(s => expect(sortedLsOutput(s)).toContain("a"))
.stderr(s => expect(s).toContain("nonexistent: No such file or directory"))
.run();
});
test("invalid flag", async () => {
await TestBuilder.command`ls -z`
.exitCode(1)
.stderr(s => expect(s).toContain("illegal option"))
.run();
});
test("invalid combined flags", async () => {
await TestBuilder.command`ls -az`
.exitCode(1)
.stderr(s => expect(s).toContain("illegal option"))
.run();
});
test.if(isPosix)("permission denied directory", async () => {
const tempdir = tempDirWithFiles("ls-permission", {});
await $`mkdir restricted; chmod 000 restricted`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls restricted`
.setTempdir(tempdir)
.exitCode(1)
.stderr(s => expect(s).toContain("Permission denied"))
.run();
await $`chmod 755 restricted`.quiet().throws(true).cwd(tempdir); // cleanup
});
test.if(isPosix)("permission denied directory recursive", async () => {
const tempdir = tempDirWithFiles("ls-permission-recursive", {});
// Create 3-level deep directory structure with 3+ items per level
await $`mkdir -p level1/level2/level3;
touch level1/file1 level1/file2 level1/file3;
touch level1/level2/file4 level1/level2/file5 level1/level2/file6;
touch level1/level2/level3/file7 level1/level2/level3/file8 level1/level2/level3/file9;
chmod 000 level1/level2`
.quiet()
.throws(true)
.cwd(tempdir);
await TestBuilder.command`ls -R level1`
.setTempdir(tempdir)
.exitCode(1)
.stdout(s => expect(sortedLsOutput(s)).toContain("file1"))
.stdout(s => expect(sortedLsOutput(s)).toContain("file2"))
.stdout(s => expect(sortedLsOutput(s)).toContain("file3"))
.stderr(s => expect(s).toContain("Permission denied"))
.run();
await $`chmod 755 level1/level2`.quiet().throws(true).cwd(tempdir); // cleanup
});
test.if(isPosix)("broken symlink file", async () => {
const tempdir = tempDirWithFiles("ls-broken-symlink", {});
await $`touch will-remove; ln -s will-remove broken-link; rm will-remove`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls broken-link`
.exitCode(1)
.stderr("ls: broken-link: No such file or directory\n")
.setTempdir(tempdir)
.run();
});
test.if(isPosix)("broken symlink directory", async () => {
const tempdir = tempDirWithFiles("ls-broken-symlink", {});
await $`mkdir will-remove; ln -s will-remove broken-link; rm -rf will-remove`.quiet().throws(true).cwd(tempdir);
await TestBuilder.command`ls broken-link`
.exitCode(1)
.stderr("ls: broken-link: No such file or directory\n")
.setTempdir(tempdir)
.run();
});
test.if(isPosix)("broken symlink directory recursive", async () => {
const tempdir = tempDirWithFiles("ls-broken-symlink", {});
console.log("TEMPDIR", tempdir);
await $`mkdir foo; cd foo; touch a b c; mkdir will-remove; ln -s will-remove broken-link; rm -rf will-remove`
.quiet()
.throws(true)
.cwd(tempdir);
await TestBuilder.command`ls -RA .`
.setTempdir(tempdir)
.stdout(s => expect(sortedLsOutput(s)).toEqual(["./foo:", "a", "b", "broken-link", "c", "foo"]))
.run();
});
});
});
function packagejson() {
return `{
"name": "dummy",
"dependencies": {
"@biomejs/biome": "^1.5.3",
"@vscode/debugadapter": "^1.61.0",
"esbuild": "^0.17.15",
"eslint": "^8.20.0",
"eslint-config-prettier": "^8.5.0",
"mitata": "^0.1.3",
"peechy": "0.4.34",
"prettier": "3.2.2",
"react": "next",
"react-dom": "next",
"source-map-js": "^1.0.2",
"typescript": "^5.0.2"
},
"devDependencies": {
"@types/react": "^18.0.25",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0"
},
"version": "0.0.0"
}`;
}