mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
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>
363 lines
13 KiB
TypeScript
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"
|
|
}`;
|
|
}
|