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 => $`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" }`; }