Files
bun.sh/test/js/bun/shell/bunshell.test.ts
pfg d2201eb1fe Rewrite test/describe, add test.concurrent (#22534)
# bun test

Fixes #8768, Fixes #14624, Fixes #20100, Fixes #19875, Fixes #14135,
Fixes #20980, Fixes #21830, Fixes #5738, Fixes #19758, Fixes #12782,
Fixes #5585, Fixes #9548, Might fix 5996

# New features:

## Concurrent tests

Concurrent tests allow running multiple async tests at the same time.

```ts
// concurrent.test.ts
test.concurrent("this takes a while 1", async () => {
  await Bun.sleep(1000);
});
test.concurrent("this takes a while 2", async () => {
  await Bun.sleep(1000);
});
test.concurrent("this takes a while 3", async () => {
  await Bun.sleep(1000);
});
```

Without `.concurrent`, this test file takes 3 seconds to run because
each one has to wait for the one before it to finish before it can
start.

With `.concurrent`, this file takes 1 second because all three sleeps
can run at once.

```
$> bun-after test concurrent
concurrent.test.js:
✓ this takes a while 1 [1005.36ms]
✓ this takes a while 2 [1012.51ms]
✓ this takes a while 3 [1013.15ms]

 3 pass
 0 fail
Ran 3 tests across 1 file. [1081.00ms]
```

To run all tests as concurrent, pass the `--concurrent` flag when
running tests.

Limitations:

- concurrent tests cannot attribute `expect()` call counts to the test,
meaning `expect.assertions()` does not function
- concurrent tests cannot use `toMatchSnapshot`. `toMatchInlineSnapshot`
is still supported.
- `beforeAll`/`afterAll` will never be executed concurrently.
`beforeEach`/`afterEach` will.

## Chaining

Chaining multiple describe/test qualifiers is now allowed. Previously,
it would fail.

```ts
// chaining-test-qualifiers.test.ts
test.failing.each([1, 2, 3])("each %i", async i => {
  throw new Error(i);
});
```

```
$> bun-after test chaining-test-qualifiers
a.test.js:
✓ each 1
✓ each 2
✓ each 3
```

# Breaking changes:

## Describe ordering

Previously, describe callbacks were called immediately. Now, they are
deferred until the outer callback has finished running. The previous
order matched Jest. The new order is similar to Vitest, but does not
match exactly.

```ts
// describe-ordering.test.ts
describe("outer", () => {
  console.log("outer before");
  describe("inner", () => {
    console.log("inner");
  });
  console.log("outer after");
});
```

Before, this would print

```
$> bun-before test describe-ordering
outer before
inner
outer after
```

Now, this will print

```
$> bun-after test describe-ordering
outer before
outer after
inner
```

## Test ordering

Describes are no longer always called before tests. They are now in
order.

```ts
// test-ordering.test.ts
test("one", () => {});
describe("scope", () => {
  test("two", () => {});
});
test("three", () => {});
```

Before, this would print

```
$> bun-before test test-ordering
✓ scope > two
✓ one
✓ three
```

Now, this will print

```
$> bun-after test test-ordering
✓ one
✓ scope > two
✓ three
```

## Preload hooks

Previously, beforeAll in a preload ran before the first file and
afterAll ran after the last file. Now, beforeAll will run at the start
of each file and afterAll will run at the end of each file. This
behaviour matches Jest and Vitest.

```ts
// preload.ts
beforeAll(() => console.log("preload: beforeAll"));
afterAll(() => console.log("preload: afterAll"));
```

```ts
// preload-ordering-1.test.ts
test("demonstration file 1", () => {});
```

```ts
// preload-ordering-2.test.ts
test("demonstration file 2", () => {});
```

```
$> bun-before test --preload=./preload preload-ordering
preload-ordering-1.test.ts:
preload: beforeAll
✓ demonstration file 1

preload-ordering-2.test.ts:
✓ demonstration file 2
preload: afterAll
```

```
$> bun-after test --preload=./preload preload-ordering
preload-ordering-1.test.ts:
preload: beforeAll
✓ demonstration file 1
preload: afterAll

preload-ordering-2.test.ts:
preload: beforeAll
✓ demonstration file 2
preload: afterAll
```

## Describe failures

Current behaviour is that when an error is thrown inside a describe
callback, none of the tests declared there will run. Now, describes
declared inside will also not run. The new behaviour matches the
behaviour of Jest and Vitest.

```ts
// describe-failures.test.ts
describe("erroring describe", () => {
  test("this test does not run because its describe failed", () => {
    expect(true).toBe(true);
  });
  describe("inner describe", () => {
    console.log("does the inner describe callback get called?");
    test("does the inner test run?", () => {
      expect(true).toBe(true);
    });
  });
  throw new Error("uh oh!");
});
```

Before, the inner describe callback would be called and the inner test
would run, although the outer test would not:

```
$> bun-before test describe-failures
describe-failures.test.ts:
does the inner describe callback get called?

# Unhandled error between tests
-------------------------------
11 |   throw new Error("uh oh!");
             ^
error: uh oh!
-------------------------------

✓ erroring describe > inner describe > does the inner test run?

 1 pass
 0 fail
 1 error
 1 expect() calls
Ran 1 test across 1 file.
Exited with code [1]
```

Now, the inner describe callback is not called at all.

```
$> bun-after test describe-failures
describe-failures.test.ts:

# Unhandled error between tests
-------------------------------
11 |   throw new Error("uh oh!");
             ^
error: uh oh!
-------------------------------


 0 pass
 0 fail
 1 error
Ran 0 tests across 1 file.
Exited with code [1]
```

## Hook failures

Previously, a beforeAll failure would skip subsequent beforeAll()s, the
test, and the afterAll. Now, a beforeAll failure skips any subsequent
beforeAll()s and the test, but not the afterAll.

```js
beforeAll(() => {
  throw new Error("before all: uh oh!");
});
test("my test", () => {
  console.log("my test");
});
afterAll(() => console.log("after all"));
```

```
$> bun-before test hook-failures
Error: before all: uh oh!

$> bun-after test hook-failures
Error: before all: uh oh!
after all
```

Previously, an async beforeEach failure would still allow the test to
run. Now, an async beforeEach failure will prevent the test from running

```js
beforeEach(() => {
  await 0;
  throw "uh oh!";
});
it("the test", async () => {
  console.log("does the test run?");
});
```

```
$> bun-before test async-beforeeach-failure
does the test run?
error: uh oh!
uh oh!
✗ the test

$> bun-after test async-beforeeach-failure
error: uh oh!
uh oh!
✗ the test
```

## Hook timeouts

Hooks will now time out, and can have their timeout configured in an
options parameter

```js
beforeAll(async () => {
  await Bun.sleep(1000);
}, 500);
test("my test", () => {
  console.log("ran my test");
});
```

```
$> bun-before test hook-timeouts
ran my test
Ran 1 test across 1 file. [1011.00ms]

$> bun-after test hook-timeouts
✗ my test [501.15ms]
  ^ a beforeEach/afterEach hook timed out for this test.
```

## Hook execution order

beforeAll will now execute before the tests in the scope, rather than
immediately when it is called.

```ts
describe("d1", () => {
  beforeAll(() => {
    console.log("<d1>");
  });
  test("test", () => {
    console.log("  test");
  });
  afterAll(() => {
    console.log("</d1>");
  });
});
describe("d2", () => {
  beforeAll(() => {
    console.log("<d2>");
  });
  test("test", () => {
    console.log("  test");
  });
  afterAll(() => {
    console.log("</d2>");
  });
});
```

```
$> bun-before test ./beforeall-ordering.test.ts
<d1>
<d2>
  test
</d1>
  test
</d2>

$> bun-after test ./beforeall-ordering.test.ts
<d1>
  test
</d1>
<d2>
  test
</d2>
```

## test inside test

test() inside test() now errors rather than silently failing. Support
for this may be added in the future.

```ts
test("outer", () => {
    console.log("outer");
    test("inner", () => {
        console.log("inner");
    });
});
```

```
$> bun-before test
outer
✓ outer [0.06ms]

 1 pass
 0 fail
Ran 1 test across 1 file. [8.00ms]

$> bun-after test
outer
1 | test("outer", () => {
2 |     console.log("outer");
3 |     test("inner", () => {
        ^
error: Cannot call test() inside a test. Call it inside describe() instead.
✗ outer [0.71ms]

 0 pass
 1 fail
```

## afterAll inside test

afterAll inside a test is no longer allowed

```ts
test("test 1", () => {
  afterAll(() => console.log("afterAll"));
  console.log("test 1");
});
test("test 2", () => {
  console.log("test 2");
});
```

```
$> bun-before
test 1
✓ test 1 [0.05ms]
test 2
✓ test 2
afterAll

$> bun-after
error: Cannot call afterAll() inside a test. Call it inside describe() instead.
✗ test 1 [1.00ms]
test 2
✓ test 2 [0.20ms]
```

# Only inside only

Previously, an outer 'describe.only' would run all tests inside it even
if there was an inner 'test.only'. Now, only the innermost only tests
are executed.

```ts
describe.only("outer", () => {
    test("one", () => console.log("should not run"));
    test.only("two", () => console.log("should run"));
});
```

```
$> bun-before test
should not run
should run

$> bun-after test
should run
```

With no inner only, the outer only will still run all tests:

```ts
describe.only("outer", () => {
    test("test 1", () => console.log("test 1 runs"));
    test("test 2", () => console.log("test 2 runs"));
});
```

# Potential follow-up work

- [ ] for concurrent tests, display headers before console.log messages
saying which test it is for
  - this will need async context or similar
- refActiveExecutionEntry should also be able to know the current test
even in test.concurrent
- [ ] `test("rerun me", () => { console.log("run one time!"); });`
`--rerun-each=3` <- this runs the first and third time but not the
second time. fix.
- [ ] should to cache the JSValue created from
DoneCallback.callAsFunction
- [ ] implement retry and rerun params for tests.
- [ ] Remove finalizer on ScopeFunctions.zig by storing the data in 3
jsvalues passed in bind rather than using a custom class. We should also
migrate off of the ClassGenerator for ScopeFunctions
- [ ] support concurrent limit, how many concurrent tests are allowed to
run at a time. ie `--concurrent-limit=25`
- [ ] flag to run tests in random order
- [ ] `test.failing` should have its own style in the same way
`test.todo` passing marks as 'todo' insetead of 'passing'. right now
it's `✓` which is confusing.
- [ ] remove all instances of bun.jsc.Jest.Jest.current
  - [ ] test options should be in BunTestRoot
- [ ] we will need one global still, stored in the globalobject/vm/?.
but it should not be a Jest instance.
- [ ] consider allowing test() inside test(), as well as afterEach and
afterAll. could even allow describe() too. to do this we would switch
from indices to pointers and they would be in a linked list. they would
be allocated in memorypools for perf/locality. some special
consideration is needed for making sure repeated tests lose their
temporary items. this could also improve memory usage soomewhat.
- [ ] consider using a jsc Bound Function rather than CallbackWithArgs.
bound functions allow adding arguments and they are only one value for
GC instead of many. and this removes our unnecessary three copies.
- [ ] eliminate Strong.Safe. we should be using a C++ class instead.
- [ ] consider modifying the junit reporter to print the whole describe
tree at the end instead of trying to output as test results come in. and
move it into its own file.
- [ ] expect_call_count/expect_assertions is confusing. rename to
`expect_calls`, `assert_expect_calls`. or something.
- [ ] Should make line_no be an enum with a none option and a function
to get if line nombers are enabled
- [ ] looks like we don't need to use file_id anymore (remove
`bun.jsc.Jest.Jest.runner.?.getOrPutFile(file_path).file_id;`, store the
file path directly)
- [ ] 'dot' test reporter like vitest?
- [ ] `test.failing.if(false)` errors because it can't replace mode
'failing' with mode 'skip'. this should probably be allowed instead.
- [ ] trigger timeout termination exception for `while(true) {}`
- [ ] clean up unused callbacks. as soon as we advance to the next
execution group, we can fully clean out the previous one. sometimes
within an execution sequence we can do the same.
  - clean by swapping held values with undefined
- [ ] structure cache for performance for donecallback/scopefunctions
- [ ] consider migrating CallbackWithArgs to be a bound function. the
length of the bound function can exclude the specified args.
- [ ] setting both result and maybe_skip is not ideal, maybe there
should be a function to do both at once?
- [ ] try using a linked list rather than arraylist for describe/test
children, see how it affects performance
- [ ] consider a memory pool for describescope/executionentry. test if
it improves performance.
- [ ] consider making RefDataValue methods return the reason for failure
rather than ?value. that way we can improve error messages. the reason
could be a string or it could be a defined error set
- [ ] instead of 'description orelse (unnamed)', let's have description
default to 'unnamed' and not free it if it === the global that defines
that
- [ ] Add a phase before ordering results that inherits properties to
the parents. (eg inherit only from the child and inherit has_callback
from the child. and has_callback can be on describe/test individually
rather than on base). then we won't have that happening in an init()
function (terrible!)
- [ ] this test was incidentally passing because resolves.pass() wasn't
waiting for promise
  ```
  test("fetching with Request object - issue #1527", async () => {
    const server = createServer((req, res) => {
      res.end();
    }).listen(0);
    try {
      await once(server, "listening");

      const body = JSON.stringify({ foo: "bar" });
const request = new Request(`http://localhost:${server.address().port}`,
{
        method: "POST",
        body,
      });

      expect(fetch(request)).resolves.pass();
    } finally {
      server.closeAllConnections();
    }
  });
  ```
- [ ] the error "expect.assertions() is not supported in the describe
phase, in concurrent tests, between tests, or after test execution has
completed" is not very good. we should be able to identify which of
those it is and print the right error for the context
- [ ] consider: instead of storing weak pointers to BunTest, we can
instead give the instance an id and check that it is correct when
getting the current bun test instance from the ref
- [ ] auto_killer: add three layers of auto_killer:
  - preload (includes file & test)
  - file (includes test)
  - test
- that way at the end of the test, we kill the test processes. at the
end of the file, we kill the file processes. at the end of all, we kill
anything remaining.

AsyncLocalStorage

- store active_id & refdatavalue. active_id is a replacement for the
above weak pointers thing. refdatavalue is for determining which test it
is. this probably fits in 2×u64
- use for auto_killer so timeouts can kill even in concurrent tests

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-09-20 00:35:42 -07:00

2494 lines
89 KiB
TypeScript

/**
* Portions of these tests are derived from the [deno_task_shell](https://github.com/denoland/deno_task_shell/) tests, which are developed and maintained by the Deno authors.
* Copyright 2018-2023 the Deno authors.
*
* This code is licensed under the MIT License: https://opensource.org/licenses/MIT
*/
import { $ } from "bun";
import { afterAll, beforeAll, describe, expect, it, test } from "bun:test";
import { mkdir, rm, stat } from "fs/promises";
import { bunExe, isPosix, isWindows, runWithErrorPromise, tempDirWithFiles, tmpdirSync } from "harness";
import { join, sep } from "path";
import { createTestBuilder, sortedShellOutput } from "./util";
const TestBuilder = createTestBuilder(import.meta.path);
export const bunEnv: NodeJS.ProcessEnv = {
...process.env,
GITHUB_ACTIONS: "false",
BUN_DEBUG_QUIET_LOGS: "1",
NO_COLOR: "1",
FORCE_COLOR: undefined,
TZ: "Etc/UTC",
CI: "1",
BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0",
BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING: "1",
BUN_GARBAGE_COLLECTOR_LEVEL: process.env.BUN_GARBAGE_COLLECTOR_LEVEL || "0",
// windows doesn't set this, but we do to match posix compatibility
PWD: (process.env.PWD || process.cwd()).replaceAll("\\", "/"),
};
$.env(bunEnv);
$.cwd(process.cwd().replaceAll("\\", "/"));
$.nothrow();
let temp_dir: string;
const temp_files = ["foo.txt", "lmao.ts"];
beforeAll(async () => {
$.nothrow();
temp_dir = tmpdirSync();
await mkdir(temp_dir, { recursive: true });
for (const file of temp_files) {
const writer = Bun.file(join(temp_dir, file)).writer();
writer.write("foo");
writer.end();
}
});
afterAll(async () => {
await rm(temp_dir, { force: true, recursive: true });
});
const BUN = bunExe();
describe("bunshell", () => {
describe("exit codes", async () => {
const failing_cmds = [
process.platform === "win32" ? "cat ldkfjsldf" : null,
"touch -alskdjfakjfhasjfh",
"mkdir",
"export",
"cd lskfjlsdkjf",
process.platform !== "win32" ? "echo hi > /dev/full" : null,
"pwd sldfkj sfks jdflks flksd f",
"which",
"rm lskdjfskldjfksdjflkjsldfj",
"mv lskdjflskdjf lskdjflskjdlf",
"ls lksdjflksdjf",
"exit sldkfj sdjf ls f",
// "true",
// "false",
// "yes",
// "seq",
"dirname",
"basename",
"cp ksdjflksjdfks lkjsdflksjdfl",
];
failing_cmds.forEach(cmdstr =>
!!cmdstr
? TestBuilder.command`${{ raw: cmdstr }}`
.exitCode(c => c !== 0)
.stdout(() => {})
.stderr(() => {})
.runAsTest(cmdstr)
: "",
);
});
describe("concurrency", () => {
test("writing to stdout", async () => {
await Promise.all([
TestBuilder.command`echo 1`.stdout("1\n").run(),
TestBuilder.command`echo 2`.stdout("2\n").run(),
TestBuilder.command`echo 3`.stdout("3\n").run(),
]);
});
});
describe("js_obj_test", async () => {
function runTest(name: string, builder: TestBuilder) {
test(`js_obj_test_name_${name}`, async () => {
await builder.run();
});
}
runTest("number", TestBuilder.command`echo ${1}`.stdout("1\n"));
runTest("String", TestBuilder.command`echo ${new String("1")}`.stdout("1\n"));
runTest("bool", TestBuilder.command`echo ${true}`.stdout("true\n"));
runTest("null", TestBuilder.command`echo ${null}`.stdout("null\n"));
runTest("undefined", TestBuilder.command`echo ${undefined}`.stdout("undefined\n"));
runTest("Date", TestBuilder.command`echo hello ${new Date()}`.stdout(`hello ${new Date().toString()}\n`));
runTest("BigInt", TestBuilder.command`echo ${BigInt((2 ^ 52) - 1)}`.stdout(`${BigInt((2 ^ 52) - 1)}\n`));
runTest("Array", TestBuilder.command`echo ${[1, 2, 3]}`.stdout(`1 2 3\n`));
});
describe("escape", async () => {
function escapeTest(strToEscape: string, expected: string = strToEscape) {
test(strToEscape, async () => {
const { stdout } = await $`echo ${strToEscape}`;
expect(stdout.toString()).toEqual(`${strToEscape}\n`);
expect($.escape(strToEscape)).toEqual(expected);
});
}
escapeTest("1 2 3", '"1 2 3"');
escapeTest("nice\nlmao", '"nice\nlmao"');
escapeTest(`lol $NICE`, `"lol \\$NICE"`);
escapeTest(
`"hello" "lol" "nice"lkasjf;jdfla<>SKDJFLKSF`,
`"\\"hello\\" \\"lol\\" \\"nice\\"lkasjf;jdfla<>SKDJFLKSF"`,
);
escapeTest("✔", "✔");
escapeTest("lmao=✔", '"lmao=✔"');
escapeTest("元気かい、兄弟", "元気かい、兄弟");
escapeTest("d元気かい、兄弟", "d元気かい、兄弟");
describe("wrapped in quotes", async () => {
const url = "http://www.example.com?candy_name=M&M";
TestBuilder.command`echo url="${url}"`.stdout(`url=${url}\n`).runAsTest("double quotes");
TestBuilder.command`echo url='${url}'`.stdout(`url=${url}\n`).runAsTest("single quotes");
TestBuilder.command`echo url=${url}`.stdout(`url=${url}\n`).runAsTest("no quotes");
});
describe("escape var", async () => {
const shellvar = "$FOO";
TestBuilder.command`FOO=bar && echo "${shellvar}"`.stdout(`$FOO\n`).runAsTest("double quotes");
TestBuilder.command`FOO=bar && echo '${shellvar}'`.stdout(`$FOO\n`).runAsTest("single quotes");
TestBuilder.command`FOO=bar && echo ${shellvar}`.stdout(`$FOO\n`).runAsTest("no quotes");
});
test("can't escape a js string/obj ref", async () => {
const shellvar = "$FOO";
await TestBuilder.command`FOO=bar && echo \\${shellvar}`.stdout(`\\$FOO\n`).run();
const buf = new Uint8Array(1);
expect(async () => {
await TestBuilder.command`echo hi > \\${buf}`.error("Redirection with no file").run();
});
});
test("in command position", async () => {
const x = "echo hi";
await TestBuilder.command`${x}`.exitCode(1).stderr("bun: command not found: echo hi\n").run();
});
test("arrays", async () => {
const x = ["echo", "hi"];
await TestBuilder.command`${x}`.stdout("hi\n").run();
});
});
describe("quiet", async () => {
test("basic", async () => {
// Check its buffered
{
const { stdout, stderr } = await $`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e "console.log('hi'); console.error('lol')"`;
expect(stdout.toString()).toEqual("hi\n");
expect(stderr.toString()).toEqual("lol\n");
}
// Check it doesn't write to stdout
const { stdout, stderr } = Bun.spawnSync(
[
BUN,
"-e",
"await Bun.$`BUN_DEBUG_QUIET_LOGS=1 ${process.argv0} -e \"console.log('hi'); console.error('lol')\"`.quiet()",
],
{
env: { BUN_DEBUG_QUIET_LOGS: "1" },
},
);
expect(stdout.toString()).toBe("");
expect(stderr.toString()).toBe("");
});
test("cmd subst", async () => {
await TestBuilder.command`echo $(echo hi)`.quiet().stdout("hi\n").run();
});
});
test("failing stmt edgecase", async () => {
const { stdout } =
await $`mkdir foo; touch ./foo/lol ./foo/nice ./foo/lmao; mkdir foo/bar; touch ./foo/bar/great; touch ./foo/bar/wow; ls foo -R`.cwd(
temp_dir,
);
});
// test("invalid js obj", async () => {
// const lol = {
// hi: "lmao",
// };
// await TestBuilder.command`echo foo > ${lol}`.error("Invalid JS object used in shell: [object Object]").run();
// const r = new RegExp("hi");
// await TestBuilder.command`echo foo > ${r}`.error("Invalid JS object used in shell: /hi/").run();
// });
test("empty_input", async () => {
await TestBuilder.command``.run();
await TestBuilder.command` `.run();
await TestBuilder.command`
`.run();
await TestBuilder.command`
`.run();
await TestBuilder.command`
`.run();
});
describe("echo+cmdsubst edgecases", async () => {
async function doTest(cmd: string, expected: string) {
test(cmd, async () => {
const { stdout } = await $`${{ raw: cmd }}`;
expect(stdout.toString()).toEqual(expected);
});
}
// funny/crazy edgecases thanks to @paperclover and @Electroid
doTest(`echo "$(echo 1; echo 2)"`, "1\n2\n");
doTest(`echo "$(echo "1" ; echo "2")"`, "1\n2\n");
doTest(`echo $(echo 1; echo 2)`, "1 2\n");
// Issue: #8982
// https://github.com/oven-sh/bun/issues/8982
describe("word splitting", async () => {
TestBuilder.command`echo $(echo id)/$(echo region)`.stdout("id/region\n").runAsTest("concatenated cmd substs");
TestBuilder.command`echo $(echo hi id)/$(echo region)`
.stdout("hi id/region\n")
.runAsTest("cmd subst with whitespace gets split");
// Make sure its one whole argument
TestBuilder.command`echo {"console.log(JSON.stringify(process.argv.slice(2)))"} > temp_script.ts; BUN_DEBUG_QUIET_LOGS=1 ${BUN} run temp_script.ts $(echo id)/$(echo region)`
.stdout('["id/region"]\n')
.ensureTempDir()
.runAsTest("make sure its one whole argument");
// Make sure its two separate arguments
TestBuilder.command`echo {"console.log(JSON.stringify(process.argv.slice(2)))"} > temp_script.ts; BUN_DEBUG_QUIET_LOGS=1 ${BUN} run temp_script.ts $(echo hi id)/$(echo region)`
.stdout('["hi","id/region"]\n')
.ensureTempDir()
.runAsTest("make sure its two separate arguments");
});
});
describe("unicode", () => {
test("basic", async () => {
const whatsupbro = "元気かい、兄弟";
const { stdout } = await $`echo ${whatsupbro}`;
expect(stdout.toString("utf8")).toEqual(whatsupbro + "\n");
});
test("escape unicode", async () => {
const { stdout } = await $`echo \\\\`;
// TODO: Uncomment and replace after unicode in template tags is supported
// expect(stdout.toString("utf8")).toEqual(`\弟\気\n`);
// Set this here for now, because unicode in template tags while using .raw is broken, but should be fixed
expect(stdout.toString("utf8")).toEqual("\\u5F1F\\u6C17\n");
});
/**
* Only A-Z, a-z, 0-9, and _ are allowed in variable names
*
* Using unicode in var name will interpret the assignment as a command.
*/
//
test("varname fails", async () => {
const whatsupbro = "元気かい、兄弟";
await TestBuilder.command`${whatsupbro}=NICE; echo $${whatsupbro}`
.stdout("$元気かい、兄弟\n")
.stderr("bun: command not found: 元気かい、兄弟=NICE\n")
.run();
});
test("var value", async () => {
const error = await runWithErrorPromise(async () => {
const whatsupbro = "元気かい、兄弟";
const { stdout } = await $`FOO=${whatsupbro}; echo $FOO`;
expect(stdout.toString("utf-8")).toEqual(whatsupbro + "\n");
});
expect(error).toBeUndefined();
});
test("in compound word", async () => {
const whatsupbro = "元気かい、兄弟";
const holymoly = "ホーリーモーリー";
const { stdout } = await $`echo "${whatsupbro}&&nice"${holymoly}`;
expect(stdout.toString("utf-8")).toEqual(`${whatsupbro}&&nice${holymoly}\n`);
});
test("cmd subst", async () => {
const haha = "ハハ";
const { stdout } = await $`echo $(echo ${haha})`;
expect(stdout.toString("utf-8")).toEqual(`${haha}\n`);
});
// #9823
TestBuilder.command`AUTH_COOKIE_SECUREALKJAKJDLASJDKLSAJD=false; echo $AUTH_COOKIE_SECUREALKJAKJDLASJDKLSAJD`
.stdout("false\n")
.runAsTest("long varname");
// test("invalid lone surrogate fails", async () => {
// const err = await runWithErrorPromise(async () => {
// const loneSurrogate = randomLoneSurrogate();
// const buffer = new Uint8Array(8192);
// const result = await $`echo ${loneSurrogate} > ${buffer}`;
// });
// console.log("ERR", err)
// expect(err?.message).toEqual("Shell script string contains invalid UTF-16");
// });
// test("invalid surrogate pair fails", async () => {
// const err = await runWithErrorPromise(async () => {
// const loneSurrogate = randomInvalidSurrogatePair();
// const buffer = new Uint8Array(8192);
// const result = $`echo ${loneSurrogate} > ${buffer}`;
// });
// expect(err?.message).toEqual("Shell script string contains invalid UTF-16");
// });
});
describe("latin-1", async () => {
describe("basic", async () => {
TestBuilder.command`echo ${"à"}`.stdout("à\n").runAsTest("lone latin-1 character");
TestBuilder.command`echo ${" à"}`.stdout(" à\n").runAsTest("latin-1 character preceded by space");
TestBuilder.command`echo ${"à¿"}`.stdout("à¿\n").runAsTest("multiple latin-1 characters");
TestBuilder.command`echo ${'"à¿"'}`.stdout('"à¿"\n').runAsTest("latin-1 characters in quotes");
});
});
test("redirect Uint8Array", async () => {
const buffer = new Uint8Array(1 << 20);
const result = await $`cat ${import.meta.path} > ${buffer}`;
const sentinel = sentinelByte(buffer);
const thisFile = Bun.file(import.meta.path);
expect(new TextDecoder().decode(buffer.slice(0, sentinel))).toEqual(await thisFile.text());
});
test("redirect Buffer", async () => {
const buffer = Buffer.alloc(1 << 20);
const result = await $`cat ${import.meta.path} > ${buffer}`;
const thisFile = Bun.file(import.meta.path);
expect(new TextDecoder().decode(buffer.slice(0, sentinelByte(buffer)))).toEqual(await thisFile.text());
});
test("redirect Bun.File", async () => {
const filepath = join(temp_dir, "lmao.txt");
const file = Bun.file(filepath);
const thisFileText = await Bun.file(import.meta.path).text();
const result = await $`cat ${import.meta.path} > ${file}`;
expect(await file.text()).toEqual(thisFileText);
});
// TODO This sometimes fails
test("redirect stderr", async () => {
const buffer = Buffer.alloc(128, 0);
const code = /* ts */ `
for (let i = 0; i < 10; i++) {
console.error('LMAO')
}
`;
await $`${BUN} -e ${code} 2> ${buffer}`.env(bunEnv);
console.log(buffer);
expect(new TextDecoder().decode(buffer.slice(0, sentinelByte(buffer)))).toEqual(
`LMAO\nLMAO\nLMAO\nLMAO\nLMAO\nLMAO\nLMAO\nLMAO\nLMAO\nLMAO\n`,
);
});
test("pipeline", async () => {
const { stdout } = await $`echo "LMAO" | cat`;
expect(stdout.toString()).toEqual("LMAO\n");
});
describe("operators no spaces", async () => {
TestBuilder.command`echo LMAO|cat`.stdout("LMAO\n").runAsTest("pipeline");
TestBuilder.command`echo foo&&echo hi`.stdout("foo\nhi\n").runAsTest("&&");
TestBuilder.command`echo foo||echo hi`.stdout("foo\n").runAsTest("||");
TestBuilder.command`echo foo>hi.txt`.ensureTempDir().fileEquals("hi.txt", "foo\n").runAsTest("||");
TestBuilder.command`echo hifriends#lol`.stdout("hifriends#lol\n").runAsTest("#");
});
test("cmd subst", async () => {
const haha = "noice";
const { stdout } = await $`echo $(echo noice)`;
expect(stdout.toString()).toEqual(`noice\n`);
});
describe("empty_expansion", () => {
TestBuilder.command`$(exit 0) && echo hi`.stdout("hi\n").runAsTest("empty command subst");
TestBuilder.command`$(exit 1) && echo hi`.exitCode(1).runAsTest("empty command subst 2");
TestBuilder.command`FOO="" $FOO`.runAsTest("empty var");
});
describe("tilde_expansion", () => {
describe("with paths", async () => {
TestBuilder.command`echo ~/Documents`.stdout(`${process.env.HOME}/Documents\n`).runAsTest("normal");
TestBuilder.command`echo ~/Do"cu"me"nts"`.stdout(`${process.env.HOME}/Documents\n`).runAsTest("compound word");
TestBuilder.command`echo ~/LOL hi hello`.stdout(`${process.env.HOME}/LOL hi hello\n`).runAsTest("multiple words");
});
describe("normal", async () => {
TestBuilder.command`echo ~`.stdout(`${process.env.HOME}\n`).runAsTest("lone tilde");
TestBuilder.command`echo ~~`.stdout(`~~\n`).runAsTest("double tilde");
TestBuilder.command`echo ~ hi hello`.stdout(`${process.env.HOME} hi hello\n`).runAsTest("multiple words");
});
TestBuilder.command`HOME="" USERPROFILE="" && echo ~ && echo ~/Documents`
.stdout("\n/Documents\n")
.runAsTest("empty $HOME or $USERPROFILE");
describe("modified $HOME or $USERPROFILE", async () => {
TestBuilder.command`HOME=lmao USERPROFILE=lmao && echo ~`.stdout("lmao\n").runAsTest("1");
TestBuilder.command`HOME=lmao USERPROFILE=lmao && echo ~ && echo ~/Documents`
.stdout("lmao\nlmao/Documents\n")
.runAsTest("2");
});
});
// Ported from GNU bash "quote.tests"
// https://github.com/bminor/bash/blob/f3b6bd19457e260b65d11f2712ec3da56cef463f/tests/quote.tests#L1
// Some backtick tests are skipped, because of insane behavior:
// For some reason, even though $(...) and `...` are suppoed to be equivalent,
// doing:
// echo "`echo 'foo\
// bar'`"
//
// gives:
// foobar
//
// While doing the same, but with $(...):
// echo "$(echo 'foo\
// bar')"
//
// gives:
// foo\
// bar
//
// I'm not sure why, this isn't documented behavior, so I'm choosing to ignore it.
describe("gnu_quote", () => {
// An unfortunate consequence of our use of String.raw and tagged template
// functions for the shell make it so that we have to use { raw: string } to do
// backtick command substitution
const BACKTICK = { raw: "`" };
// Single Quote
TestBuilder.command`
echo 'foo
bar'
echo 'foo
bar'
echo 'foo\
bar'
`
.stdout("foo\nbar\nfoo\nbar\nfoo\\\nbar\n")
.runAsTest("Single Quote");
TestBuilder.command`
echo "foo
bar"
echo "foo
bar"
echo "foo\
bar"
`
.stdout("foo\nbar\nfoo\nbar\nfoobar\n")
.runAsTest("Double Quote");
TestBuilder.command`
echo ${BACKTICK}echo 'foo
bar'${BACKTICK}
echo ${BACKTICK}echo 'foo
bar'${BACKTICK}
echo ${BACKTICK}echo 'foo\
bar'${BACKTICK}
`
.stdout(
`foo bar
foo bar
foobar\n`,
)
.todo("insane backtick behavior")
.runAsTest("Backslash Single Quote");
TestBuilder.command`
echo "${BACKTICK}echo 'foo
bar'${BACKTICK}"
echo "${BACKTICK}echo 'foo
bar'${BACKTICK}"
echo "${BACKTICK}echo 'foo\
bar'${BACKTICK}"
`
.stdout(
`foo
bar
foo
bar
foobar\n`,
)
.todo("insane backtick behavior")
.runAsTest("Double Quote Backslash Single Quote");
TestBuilder.command`
echo $(echo 'foo
bar')
echo $(echo 'foo
bar')
echo $(echo 'foo\
bar')
`
.stdout(
`foo bar
foo bar
foo\\ bar\n`,
)
.runAsTest("Dollar Paren Single Quote");
TestBuilder.command`
echo "$(echo 'foo
bar')"
echo "$(echo 'foo
bar')"
echo "$(echo 'foo\
bar')"
`
.stdout(
`foo
bar
foo
bar
foo\\
bar\n`,
)
.runAsTest("Dollar Paren Double Quote");
TestBuilder.command`
echo "$(echo 'foo
bar')"
echo "$(echo 'foo
bar')"
echo "$(echo 'foo\
bar')"
`
.stdout(
`foo
bar
foo
bar
foo\\
bar\n`,
)
.runAsTest("Double Quote Dollar Paren Single Quote");
});
describe("escaped_newline", () => {
const printArgs = /* ts */ `console.log(JSON.stringify(process.argv))`;
TestBuilder.command /* sh */ `${BUN} run ./code.ts hi hello \
on a newline!
`
.ensureTempDir()
.file("code.ts", printArgs)
.stdout(out => expect(JSON.parse(out).slice(2)).toEqual(["hi", "hello", "on", "a", "newline!"]))
.runAsTest("single");
TestBuilder.command /* sh */ `${BUN} run ./code.ts hi hello \
on a newline! \
and \
a few \
others!
`
.ensureTempDir()
.file("code.ts", printArgs)
.stdout(out =>
expect(JSON.parse(out).slice(2)).toEqual(["hi", "hello", "on", "a", "newline!", "and", "a", "few", "others!"]),
)
.runAsTest("many");
TestBuilder.command /* sh */ `${BUN} run ./code.ts hi hello \
on a newline! \
ooga"
booga"
`
.ensureTempDir()
.file("code.ts", printArgs)
.stdout(out => expect(JSON.parse(out).slice(2)).toEqual(["hi", "hello", "on", "a", "newline!", "ooga\nbooga"]))
.runAsTest("quotes");
});
describe("glob expansion", () => {
// Issue #8403: https://github.com/oven-sh/bun/issues/8403
TestBuilder.command`ls *.sdfljsfsdf`
.exitCode(1)
.stderr("bun: no matches found: *.sdfljsfsdf\n")
.runAsTest("No matches should fail");
TestBuilder.command`FOO=*.lolwut; echo $FOO`
.stdout("*.lolwut\n")
.runAsTest("No matches in assignment position should print out pattern");
TestBuilder.command`FOO=hi*; echo $FOO`
.ensureTempDir()
.stdout("hi*\n")
.runAsTest("Trailing asterisk with no matches");
TestBuilder.command`touch hihello; touch hifriends; FOO=hi*; echo $FOO`
.ensureTempDir()
.stdout(s => expect(s).toBeOneOf(["hihello hifriends\n", "hifriends hihello\n"]))
.runAsTest("Trailing asterisk with matches, inline");
TestBuilder.command`ls *.js`
// Calling `ensureTempDir()` changes the cwd here
.ensureTempDir()
.file("foo.js", "foo")
.file("bar.js", "bar")
.stdout(out => {
expect(sortedShellOutput(out)).toEqual(sortedShellOutput("foo.js\nbar.js\n"));
})
.runAsTest("Should work with a different cwd");
});
describe("brace expansion", () => {
function doTest(pattern: string, expected: string) {
test(pattern, async () => {
const { stdout } = await $`echo ${{ raw: pattern }} `;
expect(stdout.toString()).toEqual(`${expected}\n`);
});
}
describe("concatenated", () => {
doTest("{a,b,c}{d,e,f}", "ad ae af bd be bf cd ce cf");
});
describe("nested", () => {
doTest("{a,b,{c,d}}", "a b c d");
doTest("{a,b,{c,d,{e,f}}}", "a b c d e f");
doTest("{a,{b,{c,d}}}", "a b c d");
doTest("{a,b,HI{c,e,LMAO{d,f}Q}}", "a b HIc HIe HILMAOdQ HILMAOfQ");
doTest("{a,{b,c}}{1,2,3}", "a1 a2 a3 b1 b2 b3 c1 c2 c3");
doTest("{a,{b,c}HEY,d}{1,2,3}", "a1 a2 a3 bHEY1 bHEY2 bHEY3 cHEY1 cHEY2 cHEY3 d1 d2 d3");
doTest("{a,{b,c},d}{1,2,3}", "a1 a2 a3 b1 b2 b3 c1 c2 c3 d1 d2 d3");
doTest(
"{a,b,HI{c,e,LMAO{d,f}Q}}{1,2,{3,4},5}",
"a1 a2 a3 a4 a5 b1 b2 b3 b4 b5 HIc1 HIc2 HIc3 HIc4 HIc5 HIe1 HIe2 HIe3 HIe4 HIe5 HILMAOdQ1 HILMAOdQ2 HILMAOdQ3 HILMAOdQ4 HILMAOdQ5 HILMAOfQ1 HILMAOfQ2 HILMAOfQ3 HILMAOfQ4 HILMAOfQ5",
);
doTest(
`{1,{2,{3,{4,{5,{6,{7,{8,{9,{10,{11,{12,{13,{14,{15,{16,{17}}}}}}}}}}}}}}}}}`,
"1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17",
);
});
test("command", async () => {
const { stdout } = await $`{echo,a,b,c} {d,e,f}`;
expect(stdout.toString()).toEqual("a b c d e f\n");
});
});
describe("variables", () => {
test("cmd_local_var", async () => {
const { stdout } = await $`FOO=bar BOOP=1 ${BUN} -e "console.log(JSON.stringify(process.env))"`;
const str = stdout.toString();
expect(JSON.parse(str)).toEqual({
...bunEnv,
FOO: "bar",
BOOP: "1",
});
});
test("expand shell var", async () => {
const { stdout } = await $`FOO=bar BAR=baz; echo $FOO $BAR`;
const str = stdout.toString();
expect(str).toEqual("bar baz\n");
});
test("shell var", async () => {
const { stdout } = await $`FOO=bar BAR=baz && BAZ=1 ${BUN} -e "console.log(JSON.stringify(process.env))"`;
const str = stdout.toString();
const procEnv = JSON.parse(str);
expect(procEnv.FOO).toBeUndefined();
expect(procEnv.BAR).toBeUndefined();
expect(procEnv).toEqual({ ...bunEnv, BAZ: "1" });
});
test("export var", async () => {
const buffer = Buffer.alloc(1 << 20);
const buffer2 = Buffer.alloc(1 << 20);
await $`export FOO=bar && BAZ=1 ${BUN} -e "console.log(JSON.stringify(process.env))" > ${buffer} && BUN_TEST_VAR=1 ${BUN} -e "console.log(JSON.stringify(process.env))" > ${buffer2}`;
const str1 = stringifyBuffer(buffer);
const str2 = stringifyBuffer(buffer2);
console.log("Str1", str1);
let procEnv = JSON.parse(str1);
expect(procEnv).toEqual({ ...bunEnv, BAZ: "1", FOO: "bar" });
procEnv = JSON.parse(str2);
expect(procEnv).toEqual({
...bunEnv,
BAZ: "1",
FOO: "bar",
BUN_TEST_VAR: "1",
});
});
test("syntax edgecase", async () => {
const buffer = new Uint8Array(1 << 20);
const shellProc = await $`FOO=bar BUN_TEST_VAR=1 ${BUN} -e "console.log(JSON.stringify(process.env))"> ${buffer}`;
const str = stringifyBuffer(buffer);
const procEnv = JSON.parse(str);
expect(procEnv).toEqual({ ...bunEnv, BUN_TEST_VAR: "1", FOO: "bar" });
});
});
describe("cd & pwd", () => {
test("cd", async () => {
const { stdout } = await $`cd ${temp_dir} && ls`;
const str = stdout.toString();
expect(
str
.split("\n")
.filter(s => s.length > 0)
.sort(),
).toEqual([...temp_files, "foo", "lmao.txt"].sort());
});
test("cd -", async () => {
const { stdout } = await $`cd ${temp_dir} && pwd && cd - && pwd`;
expect(stdout.toString()).toEqual(`${temp_dir}\n${process.cwd().replaceAll("\\", "/")}\n`);
});
});
test("which", async () => {
const bogus = "akdfjlsdjflks";
const { stdout } = await $`which ${BUN} ${bogus}`;
const bunWhich = Bun.which(BUN);
expect(stdout.toString()).toEqual(`${bunWhich}\n${bogus} not found\n`);
});
describe("rm", () => {
let temp_dir: string;
const files = {
foo: "bar",
bar: "baz",
dir: {
some: "more",
files: "here",
},
};
beforeAll(() => {
temp_dir = tempDirWithFiles("temp-rm", files);
});
test("error without recursive option", async () => {
const { stderr } = await $`rm -v ${temp_dir}`;
expect(stderr.toString()).toEqual(`rm: ${temp_dir}: Is a directory\n`);
});
test("recursive", async () => {
const { stdout } = await $`rm -vrf ${temp_dir}`;
const str = stdout.toString();
expect(
str
.split("\n")
.filter(s => s.length !== 0)
.sort(),
).toEqual(
`${join(temp_dir, "foo")}
${join(temp_dir, "dir", "files")}
${join(temp_dir, "dir", "some")}
${join(temp_dir, "dir")}
${join(temp_dir, "bar")}
${temp_dir}`
.split("\n")
.sort(),
);
});
});
/**
*
*/
describe("escaping", () => {
// Testing characters that need special handling when not quoted or in different contexts
TestBuilder.command`echo ${"$"}`.stdout("$\n").runAsTest("dollar");
TestBuilder.command`echo ${">"}`.stdout(">\n").runAsTest("right_arrow");
TestBuilder.command`echo ${"&"}`.stdout("&\n").runAsTest("ampersand");
TestBuilder.command`echo ${"|"}`.stdout("|\n").runAsTest("pipe");
TestBuilder.command`echo ${"="}`.stdout("=\n").runAsTest("equals");
TestBuilder.command`echo ${";"}`.stdout(";\n").runAsTest("semicolon");
TestBuilder.command`echo ${"\n"}`.stdout("\n").runAsTest("newline");
TestBuilder.command`echo ${"{"}`.stdout("{\n").runAsTest("left_brace");
TestBuilder.command`echo ${"}"}`.stdout("}\n").runAsTest("right_brace");
TestBuilder.command`echo ${","}`.stdout(",\n").runAsTest("comma");
TestBuilder.command`echo ${"("}`.stdout("(\n").runAsTest("left_parenthesis");
TestBuilder.command`echo ${")"}`.stdout(")\n").runAsTest("right_parenthesis");
TestBuilder.command`echo ${"\\"}`.stdout("\\\n").runAsTest("backslash");
TestBuilder.command`echo ${" "}`.stdout(" \n").runAsTest("space");
TestBuilder.command`echo ${"'hello'"}`.stdout("'hello'\n").runAsTest("single_quote");
TestBuilder.command`echo ${'"hello"'}`.stdout('"hello"\n').runAsTest("double_quote");
TestBuilder.command`echo ${"`hello`"}`.stdout("`hello`\n").runAsTest("backtick");
// Testing characters that need to be escaped within double quotes
TestBuilder.command`echo "${"$"}"`.stdout("$\n").runAsTest("dollar_in_dquotes");
TestBuilder.command`echo "${"`"}"`.stdout("`\n").runAsTest("backtick_in_dquotes");
TestBuilder.command`echo "${'"'}"`.stdout('"\n').runAsTest("double_quote_in_dquotes");
TestBuilder.command`echo "${"\\"}"`.stdout("\\\n").runAsTest("backslash_in_dquotes");
// Testing characters that need to be escaped within single quotes
TestBuilder.command`echo '${"$"}'`.stdout("$\n").runAsTest("dollar_in_squotes");
TestBuilder.command`echo '${'"'}'`.stdout('"\n').runAsTest("double_quote_in_squotes");
TestBuilder.command`echo '${"`"}'`.stdout("`\n").runAsTest("backtick_in_squotes");
TestBuilder.command`echo '${"\\\\"}'`.stdout("\\\\\n").runAsTest("backslash_in_squotes");
// Ensure that backslash escapes within single quotes are treated literally
TestBuilder.command`echo '${"\\"}'`.stdout("\\\n").runAsTest("literal_backslash_single_quote");
TestBuilder.command`echo '${"\\\\"}'`.stdout("\\\\\n").runAsTest("double_backslash_single_quote");
// Edge cases with mixed quotes
TestBuilder.command`echo "'\${"$"}'"`.stdout("'${$}'\n").runAsTest("mixed_quotes_dollar");
TestBuilder.command`echo '"${"`"}"'`.stdout('"`"\n').runAsTest("mixed_quotes_backtick");
// Compound command with special characters
TestBuilder.command`echo ${"hello; echo world"}`.stdout("hello; echo world\n").runAsTest("compound_command");
TestBuilder.command`echo ${"hello > world"}`.stdout("hello > world\n").runAsTest("redirect_in_echo");
TestBuilder.command`echo ${"$(echo nested)"}`.stdout("$(echo nested)\n").runAsTest("nested_command_substitution");
// Pathological cases involving multiple special characters
TestBuilder.command`echo ${"complex > command; $(execute)"}`
.stdout("complex > command; $(execute)\n")
.runAsTest("complex_mixed_special_chars");
});
});
describe("deno_task", () => {
describe("commands", async () => {
TestBuilder.command`echo 1`.stdout("1\n").runAsTest("echo 1");
TestBuilder.command`echo 1 2 3`.stdout("1 2 3\n").runAsTest("echo 1 2 3");
TestBuilder.command`echo "1 2 3"`.stdout("1 2 3\n").runAsTest('echo "1 2 3"');
TestBuilder.command`echo 1 2\ \ \ 3`.stdout("1 2 3\n").runAsTest("echo 1 2\\ \\ \\ 3");
TestBuilder.command`echo "1 2\ \ \ 3"`.stdout("1 2\\ \\ \\ 3\n").runAsTest('echo "1 2\\ \\ \\ 3"');
TestBuilder.command`echo test$(echo 1 2)`.stdout("test1 2\n").runAsTest("echo test$(echo 1 2)");
TestBuilder.command`echo test$(echo "1 2")`.stdout("test1 2\n").runAsTest('echo test$(echo "1 2")');
TestBuilder.command`echo "test$(echo "1 2")"`.stdout("test1 2\n").runAsTest('echo "test$(echo "1 2")"');
TestBuilder.command`echo test$(echo "1 2 3")`.stdout("test1 2 3\n").runAsTest('echo test$(echo "1 2 3")');
TestBuilder.command`VAR=1 BUN_TEST_VAR=1 ${BUN} -e 'console.log(process.env.VAR)' && echo $VAR`
.stdout("1\n\n")
.runAsTest("shell var in command");
TestBuilder.command`VAR=1 VAR2=2 BUN_TEST_VAR=1 ${BUN} -e 'console.log(process.env.VAR + process.env.VAR2)'`
.stdout("12\n")
.runAsTest("shell var in command 2");
TestBuilder.command`EMPTY= BUN_TEST_VAR=1 ${BUN} -e ${"console.log(`EMPTY: ${process.env.EMPTY}`)"}`
.stdout("EMPTY: \n")
.runAsTest("empty shell var");
TestBuilder.command`"echo" "1"`.stdout("1\n").runAsTest("echo 1 quoted");
TestBuilder.command`echo test-dashes`.stdout("test-dashes\n").runAsTest("echo test-dashes");
TestBuilder.command`echo 'a/b'/c`.stdout("a/b/c\n").runAsTest("echo 'a/b'/c");
TestBuilder.command`echo 'a/b'ctest\"te st\"'asdf'`
.stdout('a/bctest"te st"asdf\n')
.runAsTest("echoing a bunch of escapes and quotations");
TestBuilder.command`echo --test=\"2\" --test='2' test\"TEST\" TEST'test'TEST 'test''test' test'test'\"test\" \"test\"\"test\"'test'`
.stdout(`--test="2" --test=2 test"TEST" TESTtestTEST testtest testtest"test" "test""test"test\n`)
.runAsTest("echoing a bunch of escapes and quotations 2");
});
describe("boolean logic", async () => {
TestBuilder.command`echo 1 && echo 2 || echo 3`.stdout("1\n2\n").runAsTest("echo 1 && echo 2 || echo 3");
TestBuilder.command`echo 1 || echo 2 && echo 3`.stdout("1\n3\n").runAsTest("echo 1 || echo 2 && echo 3");
TestBuilder.command`echo 1 || (echo 2 && echo 3)`.stdout("1\n").runAsTest("or with subshell");
TestBuilder.command`false || false || (echo 2 && false) || echo 3`.stdout("2\n3\n").runAsTest("or with subshell 2");
TestBuilder.command`echo 1 || (echo 2 && echo 3)`.stdout("1\n").runAsTest("conditional with subshell");
TestBuilder.command`false || false || (echo 2 && false) || echo 3`
.stdout("2\n3\n")
.runAsTest("conditional with subshell2");
});
describe("command substitution", async () => {
TestBuilder.command`echo $(echo 1)`.stdout("1\n").runAsTest("nested echo cmd subst");
TestBuilder.command`echo $(echo 1 && echo 2)`.stdout("1 2\n").runAsTest("nested echo cmd subst with conditional");
// TODO Sleep tests
});
describe("shell variables", async () => {
TestBuilder.command`echo $VAR && VAR=1 && echo $VAR && ${BUN} -e ${"console.log(process.env.VAR)"}`
.stdout("\n1\nundefined\n")
.runAsTest("shell var");
TestBuilder.command`VAR=1 && echo $VAR$VAR`.stdout("11\n").runAsTest("shell var 2");
TestBuilder.command`VAR=1 && echo Test$VAR && echo $(echo "Test: $VAR") ; echo CommandSub$($VAR) ; echo $ ; echo \$VAR`
.stdout("Test1\nTest: 1\nCommandSub\n$\n$VAR\n")
.stderr("bun: command not found: 1\n")
.runAsTest("shell var 3");
});
describe("env variables", async () => {
TestBuilder.command`echo $VAR && export VAR=1 && echo $VAR && BUN_TEST_VAR=1 ${BUN} -e 'console.log(process.env.VAR)'`
.stdout("\n1\n1\n")
.runAsTest("exported vars");
TestBuilder.command`export VAR=1 VAR2=testing VAR3="test this out" && echo $VAR $VAR2 $VAR3`
.stdout("1 testing test this out\n")
.runAsTest("exported vars 2");
});
describe("pipeline", async () => {
TestBuilder.command`echo 1 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("1\n")
.runAsTest("basic pipe");
TestBuilder.command`echo 1 | echo 2 && echo 3`.stdout("2\n3\n").runAsTest("pipe in conditional");
TestBuilder.command`echo $(sleep 0.1 && echo 2 & echo 1) | BUN_DEBUG_QUIET_LOGS=1 BUN_TEST_VAR=1 ${BUN} -e 'await process.stdin.pipe(process.stdout)'`
.stdout("1 2\n")
.todo("& not supported")
.runAsTest("complicated pipeline");
TestBuilder.command`echo 2 | echo 1 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("1\n")
.runAsTest("multi pipe");
TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(2);' | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("1\n")
.stderr("2\n")
.runAsTest("piping subprocesses");
TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(2);' |& BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
// .stdout("1\n2\n")
.error("Piping stdout and stderr (`|&`) is not supported yet. Please file an issue on GitHub.")
.runAsTest("|&");
// await TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(2);' | BUN_TEST_VAR=1 ${BUN} -e 'setTimeout(async () => { await Deno.stdin.readable.pipeTo(Deno.stderr.writable) }, 10)' |& BUN_TEST_VAR=1 ${BUN} -e 'await Deno.stdin.readable.pipeTo(Deno.stderr.writable)'`
// .stderr("2\n1\n")
// .run();
TestBuilder.command`echo 1 |& BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
// .stdout("1\n")
.error("Piping stdout and stderr (`|&`) is not supported yet. Please file an issue on GitHub.")
.runAsTest("|& 2");
TestBuilder.command`echo 1 | BUN_DEBUG_QUIET_LOGS=1 BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)' > output.txt`
.fileEquals("output.txt", "1\n")
.runAsTest("pipe with redirect to file");
TestBuilder.command`echo 1 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stderr)' 2> output.txt`
.fileEquals("output.txt", "1\n")
.runAsTest("pipe with redirect stderr to file");
if (isPosix) {
TestBuilder.command`ls . | echo hi`.exitCode(0).stdout("hi\n").runAsTest("broken pipe builtin");
TestBuilder.command`grep hi src/js_parser.zig | echo hi`
.exitCode(0)
.stdout("hi\n")
.stderr("")
.runAsTest("broken pipe subproc");
}
TestBuilder.command`${BUN} -e 'process.exit(1)' | ${BUN} -e 'console.log("hi")'`
.exitCode(0)
.stdout("hi\n")
.runAsTest("last exit code");
TestBuilder.command`ls sldkfjlskdjflksdjflksjdf | ${BUN} -e 'console.log("hi")'`
.exitCode(0)
.stdout("hi\n")
.stderr("ls: sldkfjlskdjflksdjflksjdf: No such file or directory\n")
.runAsTest("last exit code");
TestBuilder.command`ksldfjsdflsdfjskdfjlskdjflksdf | ${BUN} -e 'console.log("hi")'`
.exitCode(0)
.stdout("hi\n")
.stderr("bun: command not found: ksldfjsdflsdfjskdfjlskdjflksdf\n")
.runAsTest("last exit code 2");
TestBuilder.command`echo hi | ${BUN} -e 'process.exit(69)'`.exitCode(69).stdout("").runAsTest("last exit code 3");
describe("pipeline stack behavior", () => {
// Test deep pipeline chains to stress the stack implementation
TestBuilder.command`echo 1 | echo 2 | echo 3 | echo 4 | echo 5 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("5\n")
.runAsTest("deep pipeline chain");
// Test very deep chains that could overflow a recursion-based implementation
TestBuilder.command`echo start | echo 1 | echo 2 | echo 3 | echo 4 | echo 5 | echo 6 | echo 7 | echo 8 | echo 9 | echo 10 | echo 11 | echo 12 | echo 13 | echo 14 | echo 15 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("15\n")
.runAsTest("very deep pipeline chain");
// Test nested pipelines in subshells
TestBuilder.command`echo outer | (echo inner1 | echo inner2) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("inner2\n")
.runAsTest("nested pipeline in subshell");
// Test nested pipelines with command substitution
TestBuilder.command`echo $(echo nested | echo pipe) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("pipe\n")
.runAsTest("nested pipeline in command substitution");
// Test multiple nested pipelines
TestBuilder.command`(echo a | echo b) | (echo c | echo d) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("d\n")
.runAsTest("multiple nested pipelines");
// Test pipeline with conditional that contains another pipeline
TestBuilder.command`echo test | (echo inner | echo nested && echo after) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("nested\nafter\n")
.runAsTest("pipeline with conditional containing pipeline");
// Test deeply nested subshells with pipelines
TestBuilder.command`echo start | (echo l1 | (echo l2 | (echo l3 | echo final))) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("final\n")
.runAsTest("deeply nested subshells with pipelines");
// Test pipeline stack unwinding with early termination
TestBuilder.command`echo 1 | echo 2 | echo 3 | false | echo 4 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("4\n")
.runAsTest("pipeline with failing command");
// Test interleaved pipelines and conditionals
TestBuilder.command`echo a | echo b && echo c | echo d | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("b\nd\n")
.runAsTest("interleaved pipelines and conditionals");
// Test pipeline with background process (when supported)
TestBuilder.command`echo foreground | echo pipe && (echo background &) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("pipe\n")
.todo("background processes not fully supported")
.runAsTest("pipeline with background process");
// Test rapid pipeline creation and destruction
TestBuilder.command`echo 1 | echo 2; echo 3 | echo 4; echo 5 | echo 6 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("2\n4\n6\n")
.runAsTest("rapid pipeline creation");
// Test pipeline stack with error propagation
TestBuilder.command`echo start | nonexistent_command | echo after || echo fallback`
.stdout("after\n")
.stderr("bun: command not found: nonexistent_command\n")
.runAsTest("pipeline error propagation");
// Test nested pipeline with mixed success/failure
TestBuilder.command`(echo success | echo works) | (nonexistent | echo backup) || echo final_fallback`
.stdout("backup\n")
.stderr(s => s.includes("command not found"))
.runAsTest("nested pipeline mixed success failure");
TestBuilder.command`echo 0 | echo 1 | echo 2 | echo 3 | echo 4 | echo 5 | echo 6 | echo 7 | echo 8 | echo 9 | echo 10 | echo 11 | echo 12 | echo 13 | echo 14 | echo 15 | echo 16 | echo 17 | echo 18 | echo 19 | echo 20 | echo 21 | echo 22 | echo 23 | echo 24 | echo 25 | echo 26 | echo 27 | echo 28 | echo 29 | echo 30 | echo 31 | echo 32 | echo 33 | echo 34 | echo 35 | echo 36 | echo 37 | echo 38 | echo 39 | echo 40 | echo 41 | echo 42 | echo 43 | echo 44 | echo 45 | echo 46 | echo 47 | echo 48 | echo 49 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("49\n")
.runAsTest("long pipeline builtin");
TestBuilder.command`echo 0 | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | cat | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("0\n")
.runAsTest("long pipeline");
// Test pipeline stack consistency with complex nesting
TestBuilder.command`echo outer | (echo inner1 | echo inner2 | (echo deep1 | echo deep2) | echo inner3) | echo final | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("final\n")
.runAsTest("complex nested pipeline consistency");
// Test pipeline interruption and resumption
TestBuilder.command`echo start | (echo pause; echo resume) | echo end | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("end\n")
.runAsTest("pipeline interruption resumption");
// Test extremely deep nested pipeline - this would cause stack overflow with recursion
TestBuilder.command`echo level0 | (echo level1 | (echo level2 | (echo level3 | (echo level4 | (echo level5 | (echo level6 | (echo level7 | (echo level8 | (echo level9 | (echo level10 | (echo level11 | (echo level12 | (echo level13 | (echo level14 | (echo level15 | (echo level16 | (echo level17 | (echo level18 | (echo level19 | echo deep_final))))))))))))))))))) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("deep_final\n")
.runAsTest("extremely deep nested pipeline");
// Test pathological case: deep nesting + long chains
TestBuilder.command`echo start | (echo n1 | echo n2 | echo n3 | (echo deep1 | echo deep2 | echo deep3 | (echo deeper1 | echo deeper2 | echo deeper3 | (echo deepest1 | echo deepest2 | echo deepest_final)))) | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'`
.stdout("deepest_final\n")
.runAsTest("pathological deep nesting with long chains");
});
});
describe("redirects", async function igodf() {
TestBuilder.command`echo 5 6 7 > test.txt`.fileEquals("test.txt", "5 6 7\n").runAsTest("basic redirect");
TestBuilder.command`echo 1 2 3 && echo 1 > test.txt`
.stdout("1 2 3\n")
.fileEquals("test.txt", "1\n")
.runAsTest("basic redirect with &&");
// subdir
TestBuilder.command`mkdir subdir && cd subdir && echo 1 2 3 > test.txt`
.fileEquals(`subdir/test.txt`, "1 2 3\n")
.runAsTest("redirect to file");
// absolute path
TestBuilder.command`echo 1 2 3 > "$PWD/test.txt"`
.fileEquals("test.txt", "1 2 3\n")
.runAsTest("redirection path gets expanded");
// stdout
TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(5)' 1> test.txt`
.stderr("5\n")
.fileEquals("test.txt", "1\n")
.runAsTest("redirect stdout of subproccess");
// stderr
TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(5)' 2> test.txt`
.stdout("1\n")
.fileEquals("test.txt", "5\n")
.runAsTest("redirect stderr of subprocess");
// invalid fd
// await TestBuilder.command`echo 2 3> test.txt`
// .ensureTempDir()
// .stderr("only redirecting to stdout (1) and stderr (2) is supported\n")
// .exitCode(1)
// .run();
// /dev/null
TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(5)' 2> /dev/null`
.stdout("1\n")
.runAsTest("/dev/null");
// appending
TestBuilder.command`echo 1 > test.txt && echo 2 >> test.txt`
.fileEquals("test.txt", "1\n2\n")
.runAsTest("appending");
// &> and &>> redirect
await TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); setTimeout(() => console.error(23), 10)' &> file.txt && BUN_TEST_VAR=1 ${BUN} -e 'console.log(456); setTimeout(() => console.error(789), 10)' &>> file.txt`
.fileEquals("file.txt", "1\n23\n456\n789\n")
.runAsTest("&> and &>> redirect");
// multiple arguments after re-direct
// await TestBuilder.command`export TwoArgs=testing\\ this && echo 1 > $TwoArgs`
// .stderr(
// 'redirect path must be 1 argument, but found 2 (testing this). Did you mean to quote it (ex. "testing this")?\n',
// )
// .exitCode(1)
// .run();
// zero arguments after re-direct
TestBuilder.command`echo 1 > $EMPTY`
.stderr("bun: ambiguous redirect: at `echo`\n")
.exitCode(1)
.runAsTest("zero arguments after re-direct");
TestBuilder.command`echo foo bar > file.txt; cat < file.txt`
.ensureTempDir()
.stdout("foo bar\n")
.runAsTest("redirect input");
TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Stdout'); console.error('Stderr')"} 2>&1`
.stdout("Stdout\nStderr\n")
.runAsTest("redirect stderr to stdout");
TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Stdout'); console.error('Stderr')"} 1>&2`
.stderr("Stdout\nStderr\n")
.runAsTest("redirect stdout to stderr");
TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Stdout'); console.error('Stderr')"} 2>&1`
.stdout("Stdout\nStderr\n")
.quiet()
.runAsTest("redirect stderr to stdout quiet");
TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Stdout'); console.error('Stderr')"} 1>&2`
.stderr("Stdout\nStderr\n")
.quiet()
.runAsTest("redirect stdout to stderr quiet");
TestBuilder.command`echo hi > /dev/null`.quiet().runAsTest("redirect /dev/null");
TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Hello friends')"} > /dev/null`
.quiet()
.runAsTest("subproc redirect /dev/null");
const code = /* ts */ `
import { $ } from 'bun'
await $\`echo Bunception!\`
`;
TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${code} > /dev/null`
.quiet()
.runAsTest("bunception redirect /dev/null");
});
describe("pwd", async () => {
TestBuilder.command`pwd && cd sub_dir && pwd && cd ../ && pwd`
.directory("sub_dir")
.file("file.txt", "test")
// $TEMP_DIR gets replaced with the actual temp dir by the test runner
.stdout(`$TEMP_DIR\n${join("$TEMP_DIR", "sub_dir")}\n$TEMP_DIR\n`)
.runAsTest("pwd");
});
test("change env", async () => {
{
const { stdout } = await $`echo $FOO`.env({ ...bunEnv, FOO: "bar" });
expect(stdout.toString()).toEqual("bar\n");
}
{
const { stdout } = await $`BUN_TEST_VAR=1 ${BUN} -e 'console.log(JSON.stringify(process.env))'`.env({
...bunEnv,
FOO: "bar",
});
expect(JSON.parse(stdout.toString())).toEqual({
...bunEnv,
BUN_TEST_VAR: "1",
FOO: "bar",
});
}
{
const { stdout } = await $`BUN_TEST_VAR=1 ${BUN} -e 'console.log(JSON.stringify(process.env))'`.env({
...bunEnv,
FOO: "bar",
});
expect(JSON.parse(stdout.toString())).toEqual({
...bunEnv,
BUN_TEST_VAR: "1",
FOO: "bar",
});
}
});
// https://github.com/oven-sh/bun/issues/11305
test.todoIf(isWindows)("stacktrace", async () => {
// const folder = TestBuilder.tmpdir();
const code = /* ts */ `import { $ } from 'bun'
$.throws(true)
async function someFunction() {
await $\`somecommandthatdoesnotexist\`
}
await someFunction()
`;
const [_, lineNr] = code
.split("\n")
.map((l, i) => [l, i + 1] as const)
.find(([line, _]) => line.includes("somecommandthatdoesnotexist"))!;
if (lineNr === undefined) throw new Error("uh oh");
await TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${code} 2>&1`
.exitCode(1)
.stdout(s => expect(s).toInclude(`[eval]:${lineNr}`))
.run();
});
test("big_data", async () => {
const writerCode = /* ts */ `
const writer = Bun.stdout.writer();
const buf = new Uint8Array(128 * 1024).fill('a'.charCodeAt(0))
for (let i = 0; i < 10; i++) {
writer.write(buf);
await writer.flush();
}
`;
const tmpdir = TestBuilder.tmpdir();
// I think writing 1mb of 'a's to the terminal breaks CI so redirect to a FD instead
const { stdout, stderr, exitCode } = await $`${BUN} -e ${writerCode} > ${tmpdir}/output.txt`.env(bunEnv);
expect(stderr.length).toEqual(0);
expect(stdout.length).toEqual(0);
expect(exitCode).toEqual(0);
const s = await stat(`${tmpdir}/output.txt`);
expect(s.size).toEqual(10 * 128 * 1024);
});
// https://github.com/oven-sh/bun/issues/9458
test("input", async () => {
const inputCode = /* ts */ `
const downArrow = '\\x1b[B';
const enterKey = '\\x0D';
await Bun.sleep(100)
const writer = Bun.stdout.writer();
writer.write(downArrow)
await Bun.sleep(100)
writer.write(enterKey)
writer.flush()
`;
const code = /* ts */ `
import { expect } from 'bun:test'
const expected = [
'\\x1b[B',
'\\x0D'
].join("")
let i = 0
let buf = ""
const writer = Bun.stdout.writer();
process.stdin.on("data", async chunk => {
const input = chunk.toString();
buf += input;
if (buf === expected) {
writer.write(buf);
await writer.flush();
}
});
`;
const { stdout, stderr, exitCode } =
await Bun.$`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${inputCode} | BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${code}`;
expect(exitCode).toBe(0);
expect(stderr.length).toBe(0);
expect(stdout.toString()).toEqual("\x1b[B\x0D");
});
});
describe("if_clause", () => {
TestBuilder.command /* sh */ `
# The name of the package we're interested in
package_name=react
filename=package.json
if [[ -f $filename ]]; then
echo The file $filename exists and is a regular file.
echo Checking for $package_name in dependencies...
# Attempt to extract the package version from dependencies
dep_version=$(jq -r ".dependencies[\"$package_name\"]" $filename)
# If not found in dependencies, try devDependencies
if [[ -z $dep_version ]] || [[ $dep_version == null ]]; then
dep_version=$(jq -r ".devDependencies[\"$package_name\"]" $filename)
fi
# Check if we got a non-empty, non-null version string
if [[ -n $dep_version ]] && [[ $dep_version != null ]]; then
echo The package $package_name is listed as a dependency with version: $dep_version.
else
echo The package $package_name is not listed as a dependency.
fi
else
echo The file $filename does not exist or is not a regular file.
fi
`
.file(
"package.json",
`{
"private": true,
"name": "bun",
"dependencies": {
"@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.5",
"react": "next",
"react-dom": "next",
"source-map-js": "1.0.2",
"typescript": "5.0.2"
},
"devDependencies": {
},
"scripts": {
}
}
`,
)
.stdout(
"The file package.json exists and is a regular file.\nChecking for react in dependencies...\nThe package react is listed as a dependency with version: next.\n",
);
TestBuilder.command`
if
echo cond;
then
echo then;
elif
echo elif;
then
echo elif then;
else
echo else;
fi`
.stdout("cond\nthen\n")
.runAsTest("basic");
TestBuilder.command`
if
echo cond
then
echo then
elif
echo elif
then
echo elif then
else
echo else
fi`
.stdout("cond\nthen\n")
.runAsTest("basic without semicolon");
TestBuilder.command`
if
lkfjslkdjfsldf
then
echo shouldnt see this
else
echo okay here
fi`
.stdout("okay here\n")
.stderr("bun: command not found: lkfjslkdjfsldf\n")
.runAsTest("else basic");
TestBuilder.command`
if
lkfjslkdjfsldf
then
echo shouldnt see this
elif
sdfkjsldf
then
echo shouldnt see this either
else
echo okay here
fi`
.stdout("okay here\n")
.stderr("bun: command not found: lkfjslkdjfsldf\nbun: command not found: sdfkjsldf\n")
.runAsTest("else");
TestBuilder.command`
if
echo hi
then
echo hey
else
echo uh oh
fi | cat
`
.stdout("hi\nhey\n")
.runAsTest("in pipeline");
TestBuilder.command`if echo hi; then echo lmao; fi && echo nice`
.stdout("hi\nlmao\nnice\n")
.runAsTest("no else, cond true");
TestBuilder.command`if BUNISBAD; then echo not true; fi && echo bun is good`
.stdout("bun is good\n")
.stderr("bun: command not found: BUNISBAD\n")
.runAsTest("no else, cond false");
TestBuilder.command`if [[ -f package.json ]]
then
a
b
else
c
fi`
.exitCode(1)
.file("package.json", "lol")
.stderr("bun: command not found: a\nbun: command not found: b\n")
.runAsTest("multi statement then");
TestBuilder.command`if
[[ -f package.json ]]
[[ -f lkdfjlskdf ]]
then
echo yeah...
echo nope!
else
echo okay
echo makes sense!
fi`
.file("package.json", "lol")
.stdout("okay\nmakes sense!\n")
.runAsTest("multi statement in all branches");
["if", "else", "elif", "then", "fi"].map(tok => {
TestBuilder.command`"${{ raw: tok }}"`
.stderr(`bun: command not found: ${tok}\n`)
.exitCode(1)
.runAsTest(`quoted ${tok} doesn't break`);
TestBuilder.command`echo ${{ raw: "lksdfjklsdjf" + tok }}`
.stdout(`lksdfjklsdjf${tok}\n`)
.runAsTest(`${tok} in script does not break parsing 1`);
TestBuilder.command`echo ${{ raw: "hi " + tok }}`
.stdout(`hi ${tok}\n`)
.runAsTest(`${tok} in script does not break parsing 2`);
TestBuilder.command`echo ${{ raw: tok + " hi" }}`
.stdout(`${tok} hi\n`)
.runAsTest(`${tok} in script does not break parsing 3`);
});
TestBuilder.command`echo fif hi`.stdout("fif hi\n").runAsTest("parsing edge case");
// Ported from https://github.com/posix-shell-tests/posix-shell-tests/
describe("ported from posix shell tests", () => {
// test_oE 'execution path of if, true'
TestBuilder.command`if echo foo; then echo bar; fi`.stdout("foo\nbar\n").runAsTest("execution path of if, true");
// test_oE 'execution path of if, false'
TestBuilder.command`if ! echo foo; then echo bar; fi`
.stdout("foo\n")
.todo("! not supported")
.runAsTest("execution path of if, false");
// test_oE 'execution path of if-else, true'
TestBuilder.command`if echo foo; then echo bar; else echo baz; fi`
.stdout("foo\nbar\n")
.runAsTest("execution path of if-else, true");
// test_oE 'execution path of if-else, false'
TestBuilder.command`if ! echo foo; then echo bar; else echo baz; fi`
.stdout("foo\nbaz\n")
.todo("! not supported")
.runAsTest("execution path of if-else, false");
// test_oE 'execution path of if-elif, true'
TestBuilder.command`if echo 1; then echo 2; elif echo 3; then echo 4; fi`
.stdout("1\n2\n")
.runAsTest("execution path of if-elif, true");
// test_oE 'execution path of if-elif, false-true'
TestBuilder.command`if ! echo 1; then echo 2; elif echo 3; then echo 4; fi`
.stdout("1\n3\n4\n")
.todo("! not supported")
.runAsTest("execution path of if-elif, false-true");
// test_oE 'execution path of if-elif, false-false'
TestBuilder.command`if ! echo 1; then echo 2; elif ! echo 3; then echo 4; fi`
.stdout("1\n3\n")
.todo("! not supported")
.runAsTest("execution path of if-elif, false-false");
// test_oE 'execution path of if-elif-else, true'
TestBuilder.command`if echo 1; then echo 2; elif echo 3; then echo 4; else echo 5; fi`
.stdout("1\n2\n")
.runAsTest("execution path of if-elif-else, true");
// test_oE 'execution path of if-elif-else, false-true'
TestBuilder.command`if ! echo 1; then echo 2; elif echo 3; then echo 4; else echo 5; fi`
.stdout("1\n3\n4\n")
.todo("! not supported")
.runAsTest("execution path of if-elif-else, false-true");
// test_oE 'execution path of if-elif-else, false-false'
TestBuilder.command`if ! echo 1; then echo 2; elif ! echo 3; then echo 4; else echo 5; fi`
.stdout("1\n3\n5\n")
.todo("! not supported")
.runAsTest("execution path of if-elif-else, false-false");
// test_oE 'execution path of if-elif-elif, true'
TestBuilder.command`if echo 1; then echo 2; elif echo 3; then echo 4; elif echo 5; then echo 6; fi`
.stdout("1\n2\n")
.runAsTest("execution path of if-elif-elif, true");
// test_oE 'execution path of if-elif-elif, false-true'
TestBuilder.command`if ! echo 1; then echo 2; elif echo 3; then echo 4; elif echo 5; then echo 6; fi`
.stdout("1\n3\n4\n")
.todo("! not supported")
.runAsTest("execution path of if-elif-elif, false-true");
// test_oE 'execution path of if-elif-elif, false-false-true'
TestBuilder.command`if ! echo 1; then echo 2; elif ! echo 3; then echo 4; elif echo 5; then echo 6; fi`
.stdout("1\n3\n5\n6\n")
.todo("! not supported")
.runAsTest("execution path of if-elif-elif, false-false-true");
// test_oE 'execution path of if-elif-elif, false-false-false'
TestBuilder.command`if ! echo 1; then echo 2; elif ! echo 3; then echo 4; elif ! echo 5; then echo 6; fi`
.stdout("1\n3\n5\n")
.todo("! not supported")
.runAsTest("execution path of if-elif-elif, false-false-false");
// test_oE 'execution path of if-elif-elif-else, true'
TestBuilder.command`if echo 1; then echo 2; elif echo 3; then echo 4; elif echo 5; then echo 6; else echo 7; fi`
.stdout("1\n2\n")
.runAsTest("execution path of if-elif-elif-else, true");
// test_oE 'execution path of if-elif-elif-else, false-true'
TestBuilder.command`if ! echo 1; then echo 2; elif echo 3; then echo 4; elif echo 5; then echo 6; else echo 7; fi`
.stdout("1\n3\n4\n")
.todo("! not supported")
.runAsTest("execution path of if-elif-elif-else, false-true");
// test_oE 'execution path of if-elif-elif-else, false-false-true'
TestBuilder.command`if ! echo 1; then echo 2; elif ! echo 3; then echo 4; elif echo 5; then echo 6; else echo 7; fi`
.stdout("1\n3\n5\n6\n")
.todo("! not supported")
.runAsTest("execution path of if-elif-elif-else, false-false-true");
// test_oE 'execution path of if-elif-elif-else, false-false-false'
TestBuilder.command`if ! echo 1; then echo 2; elif ! echo 3; then echo 4; elif ! echo 5; then echo 6; else echo 7; fi`
.stdout("1\n3\n5\n7\n")
.todo("! not supported")
.runAsTest("execution path of if-elif-elif-else, false-false-false");
const exit = (code: number): { raw: string } => ({
raw:
process.platform !== "win32"
? `bash -c 'exit $1' -- ${code}`
: `BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e 'process.exit(${code})'`,
});
// test_x -e 0 'exit status of if, true-true'
TestBuilder.command`if ${exit(0)}; then ${exit(0)}; fi`.exitCode(0).runAsTest("exit status of if, true-true");
// test_x -e 1 'exit status of if, true-false'
TestBuilder.command`if ${exit(0)}; then ${exit(1)}; fi`.exitCode(1).runAsTest("exit status of if, true-false");
// test_x -e 0 'exit status of if, false'
TestBuilder.command`if ${exit(1)}; then ${exit(2)}; fi`.exitCode(0).runAsTest("exit status of if, false");
// test_x -e 0 'exit status of if-else, true-true'
TestBuilder.command`if ${exit(0)}; then ${exit(0)}; else ${exit(1)}; fi`
.exitCode(0)
.runAsTest("exit status of if-else, true-true");
// test_x -e 1 'exit status of if-else, true-false'
TestBuilder.command`if ${exit(0)}; then ${exit(1)}; else ${exit(2)}; fi`
.exitCode(1)
.runAsTest("exit status of if-else, true-false");
// test_x -e 0 'exit status of if-else, false-true'
TestBuilder.command`if ${exit(1)}; then ${exit(2)}; else ${exit(0)}; fi`
.exitCode(0)
.runAsTest("exit status of if-else, false-true");
// test_x -e 2 'exit status of if-else, false-false'
TestBuilder.command`if ${exit(1)}; then ${exit(0)}; else ${exit(2)}; fi`
.exitCode(2)
.runAsTest("exit status of if-else, false-false");
// test_x -e 0 'exit status of if-elif, true-true'
TestBuilder.command`if ${exit(0)}; then ${exit(0)}; elif ${exit(1)}; then ${exit(2)}; fi`
.exitCode(0)
.runAsTest("exit status of if-elif, true-true");
// test_x -e 1 'exit status of if-elif, true-false'
TestBuilder.command`if ${exit(0)}; then ${exit(1)}; elif ${exit(2)}; then ${exit(3)}; fi`
.exitCode(1)
.runAsTest("exit status of if-elif, true-false");
// test_x -e 0 'exit status of if-elif, false-true-true'
TestBuilder.command`if ${exit(1)}; then ${exit(2)}; elif ${exit(0)}; then ${exit(0)}; fi`
.exitCode(0)
.runAsTest("exit status of if-elif, false-true-true");
// test_x -e 3 'exit status of if-elif, false-true-false'
TestBuilder.command`if ${exit(1)}; then ${exit(2)}; elif ${exit(0)}; then ${exit(3)}; fi`
.exitCode(3)
.runAsTest("exit status of if-elif, false-true-false");
// test_x -e 0 'exit status of if-elif-elif-else, true-true'
TestBuilder.command`if ${exit(0)}; then ${exit(0)}; elif ${exit(1)}; then ${exit(2)}; elif ${exit(3)}; then ${exit(4)}; else ${exit(5)}; fi`
.exitCode(0)
.runAsTest("exit status of if-elif-elif-else, true-true");
// test_x -e 11 'exit status of if-elif-elif-else, true-false'
TestBuilder.command`if ${exit(0)}; then ${exit(11)}; elif ${exit(1)}; then ${exit(2)}; elif ${exit(3)}; then ${exit(4)}; else ${exit(5)}; fi`
.exitCode(11)
.runAsTest("exit status of if-elif-elif-else, true-false");
// test_x -e 0 'exit status of if-elif-elif-else, false-true-true'
TestBuilder.command`if ${exit(1)}; then ${exit(2)}; elif ${exit(0)}; then ${exit(0)}; elif ${exit(3)}; then ${exit(4)}; else ${exit(5)}; fi`
.exitCode(0)
.runAsTest("exit status of if-elif-elif-else, false-true-true");
// test_x -e 13 'exit status of if-elif-elif-else, false-true-false'
TestBuilder.command`if ${exit(1)}; then ${exit(2)}; elif ${exit(0)}; then ${exit(13)}; elif ${exit(3)}; then ${exit(4)}; else ${exit(5)}; fi`
.exitCode(13)
.runAsTest("exit status of if-elif-elif-else, false-true-false");
// test_x -e 0 'exit status of if-elif-elif-else, false-false-true-true'
TestBuilder.command`if ${exit(1)}; then ${exit(2)}; elif ${exit(3)}; then ${exit(4)}; elif ${exit(0)}; then ${exit(0)}; else ${exit(5)}; fi`
.exitCode(0)
.runAsTest("exit status of if-elif-elif-else, false-false-true-true");
// test_x -e 5 'exit status of if-elif-elif-else, false-false-true-false'
TestBuilder.command`if ${exit(1)}; then ${exit(2)}; elif ${exit(3)}; then ${exit(4)}; elif ${exit(0)}; then ${exit(5)}; else ${exit(6)}; fi`
.exitCode(5)
.runAsTest("exit status of if-elif-elif-else, false-false-true-false");
// test_x -e 0 'exit status of if-elif-elif-else, false-false-false-true'
TestBuilder.command`if ${exit(1)}; then ${exit(2)}; elif ${exit(3)}; then ${exit(4)}; elif ${exit(5)}; then ${exit(6)}; else ${exit(0)}; fi`
.exitCode(0)
.runAsTest("exit status of if-elif-elif-else, false-false-false-true");
// test_x -e 7 'exit status of if-elif-elif-else, false-false-false-false'
TestBuilder.command`if ${exit(1)}; then ${exit(2)}; elif ${exit(3)}; then ${exit(4)}; elif ${exit(5)}; then ${exit(6)}; else ${exit(7)}; fi`
.exitCode(7)
.runAsTest("exit status of if-elif-elif-else, false-false-false-false");
// test_oE 'linebreak after if'
TestBuilder.command`if
echo foo;then echo bar;fi`
.stdout("foo\nbar\n")
.runAsTest("linebreak after if");
// test_oE 'linebreak before then (after if)'
TestBuilder.command`if echo foo
then echo bar;fi`
.stdout("foo\nbar\n")
.runAsTest("linebreak before then (after if)");
// test_oE 'linebreak after then (after if)'
TestBuilder.command`if echo foo;then
echo bar;fi`
.stdout("foo\nbar\n")
.runAsTest("linebreak after then (after if)");
// test_oE 'linebreak before fi (after then)'
TestBuilder.command`if echo foo;then echo bar
fi`
.stdout("foo\nbar\n")
.runAsTest("linebreak before fi (after then)");
// test_oE 'linebreak before elif'
TestBuilder.command`if ! echo foo;then echo bar
elif echo baz;then echo qux;fi`
.stdout("foo\nbaz\nqux\n")
.todo("! not supported")
.runAsTest("linebreak before elif");
// test_oE 'linebreak after elif'
TestBuilder.command`if ! echo foo;then echo bar;elif
echo baz;then echo qux;fi`
.stdout("foo\nbaz\nqux\n")
.todo("! not supported")
.runAsTest("linebreak after elif");
// test_oE 'linebreak before then (after elif)'
TestBuilder.command`if ! echo foo;then echo bar;elif echo baz
then echo qux;fi`
.stdout("foo\nbaz\nqux\n")
.todo("! not supported")
.runAsTest("linebreak before then (after elif)");
// test_oE 'linebreak after then (after elif)'
TestBuilder.command`if ! echo foo;then echo bar;elif echo baz;then
echo qux;fi`
.stdout("foo\nbaz\nqux\n")
.todo("! not supported")
.runAsTest("linebreak after then (after elif)");
// test_oE 'linebreak before else'
TestBuilder.command`if ! echo foo;then echo bar
else echo baz;fi`
.stdout("foo\nbaz\n")
.todo("! not supported")
.runAsTest("linebreak before else");
// test_oE 'linebreak after else'
TestBuilder.command`if ! echo foo;then echo bar;else
echo baz;fi`
.stdout("foo\nbaz\n")
.todo("! not supported")
.runAsTest("linebreak after else");
// test_oE 'linebreak before fi (after else)'
TestBuilder.command`if ! echo foo;then echo bar;else echo baz
fi`
.stdout("foo\nbaz\n")
.todo("! not supported")
.runAsTest("linebreak before fi (after else)");
// test_oE 'command ending with asynchronous command (after if)'
TestBuilder.command`if echo foo&then wait;fi`
.stdout("foo\n")
.todo("wait not implemented")
.runAsTest("command ending with asynchronous command (after if)");
// test_oE 'command ending with asynchronous command (after then)'
TestBuilder.command`if echo foo;then echo bar&fi;wait`
.stdout("foo\nbar\n")
.todo("wait not implementeeed")
.runAsTest("command ending with asynchronous command (after then)");
// test_oE 'command ending with asynchronous command (after elif)'
TestBuilder.command`if ! echo foo;then echo bar;elif echo baz&then wait;fi`
.stdout("foo\nbaz\n")
.todo("! not supported")
.runAsTest("command ending with asynchronous command (after elif)");
// test_oE 'command ending with asynchronous command (after else)'
TestBuilder.command`if ! echo foo;then echo bar;elif ! echo baz;then echo qux;else echo quux;fi;wait`
.stdout("foo\nbaz\nquux\n")
.todo("! not supported")
.runAsTest("command ending with asynchronous command (after else)");
// test_oE 'more than one inner command'
TestBuilder.command`if echo 1; echo 2
echo 3; ! echo 4; then echo x1; echo x2
echo x3; echo x4; elif echo 5; echo 6
echo 7; echo 8; then echo 9; echo 10
echo 11; echo 12; else echo x5; echo x6
echo x7; echo x8; fi`
.stdout("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n")
.todo("! not supported")
.runAsTest("more than one inner command");
// test_oE 'nest between if and then'
TestBuilder.command`if { echo foo; } then echo bar; fi`
.stdout("foo\nbar\n")
.todo("grouping with { and } not supported yet")
.runAsTest("nest between if and then");
// test_oE 'nest between then and fi'
TestBuilder.command`if echo foo; then { echo bar; } fi`
.stdout("foo\nbar\n")
.todo("grouping with { and } not supported yet")
.runAsTest("nest between then and fi");
// test_oE 'nest between then and elif'
TestBuilder.command`if echo foo; then { echo bar; } elif echo baz; then echo qux; fi`
.stdout("foo\nbar\n")
.todo("grouping with { and } not supported yet")
.runAsTest("nest between then and elif");
// test_oE 'nest between elif and then'
TestBuilder.command`if echo foo; then echo bar; elif { echo baz; } then echo qux; fi`
.stdout("foo\nbar\n")
.todo("grouping with { and } not supported yet")
.runAsTest("nest between elif and then");
// test_oE 'nest between then and else'
TestBuilder.command`if ! echo foo; then { echo bar; } else echo baz; fi`
.stdout("foo\nbaz\n")
.todo("! not supported")
.runAsTest("nest between then and else");
// test_oE 'nest between then and else'
TestBuilder.command`if ! echo foo; then echo bar; else { echo baz; } fi`
.stdout("foo\nbaz\n")
.todo("! not supported")
.runAsTest("nest between then and else");
// test_oE 'redirection on if'
TestBuilder.command`if echo foo
then echo bar
else echo baz
fi >redir_out
cat redir_out`
.stdout("foo\nbar\n")
.todo("redirecting if-else not supported yet")
.runAsTest("redirection on if");
});
});
describe("condexprs", () => {
TestBuilder.command`[[ -f package.json ]] && echo yes!`.file("package.json", "hi").stdout("yes!\n").runAsTest("-f");
TestBuilder.command`[[ -f mumbo.jumbo ]] && echo yes!`.exitCode(1).runAsTest("-f non-existent");
TestBuilder.command`[[ -d mydir ]] && echo yes!`.directory("mydir").stdout("yes!\n").runAsTest("-d");
TestBuilder.command`[[ -d mumbo.jumbo ]] && echo yes!`.exitCode(1).runAsTest("-d non-existent");
TestBuilder.command`[[ -c /dev/null ]] && echo yes!`.stdout("yes!\n").runAsTest("-c");
TestBuilder.command`[[ -c lol ]] && echo yes!`.exitCode(1).file("lol", "lol").runAsTest("-c not character device");
TestBuilder.command`[[ -c mumbo.jumbo ]] && echo yes!`.exitCode(1).runAsTest("-c non-existent");
TestBuilder.command`FOO=""; [[ -z $FOO ]] && echo yes!`.stdout("yes!\n").runAsTest("-z");
TestBuilder.command`[[ -z "skldjfldsf" ]] && echo yes!`.exitCode(1).runAsTest("-z fail");
TestBuilder.command`FOO="lkjdflskdjf"; [[ -n $FOO ]] && echo yes!`.stdout("yes!\n").runAsTest("-n");
TestBuilder.command`FOO="" [[ -n $FOO ]] && echo yes!`.exitCode(1).runAsTest("-n fail");
TestBuilder.command`[[ -n hey ]] | echo hi | cat`.stdout("hi\n").runAsTest("precedence: pipeline");
TestBuilder.command`[[ foo == foo ]] && echo yes!`.stdout("yes!\n").runAsTest("==");
TestBuilder.command`[[ foo == lol ]] && echo yes!`.exitCode(1).runAsTest("== fail");
TestBuilder.command`[[ foo != foo ]] && echo yes!`.exitCode(1).runAsTest("!= fail");
TestBuilder.command`[[ lmao != foo ]] && echo yes!`.stdout("yes!\n").runAsTest("!=");
TestBuilder.command`LOL=; [[ $LOl == $LOL ]] && echo yes!`.stdout("yes!\n").runAsTest("== empty");
TestBuilder.command`LOL=; [[ $LOl != $LOL ]] && echo yes!`.exitCode(1).runAsTest("!= empty");
describe.todo("ported from GNU bash", () => {
TestBuilder.command`
[[ foo > bar && $PWD -ef . ]]
`
.exitCode(0)
.runAsTest("this one is straight out of the ksh88 book");
TestBuilder.command`
[[ x ]]
`
.exitCode(0)
.runAsTest("[[ x ]] is equivalent to [[ -n x ]]");
TestBuilder.command`[[ ! x ]]`.exitCode(1).runAsTest("# [[ ! x ]] is equivalent to [[ ! -n x ]]");
// tests.ts
TestBuilder.command`[[ ! x || x ]]`
.exitCode(0)
.runAsTest("! binds tighter than test/[ -- it binds to a term, not an expression");
TestBuilder.command`
[[ ! 1 -eq 1 ]]; echo $?
[[ ! ! 1 -eq 1 ]]; echo $?
`
.stdout("1")
.stdout("0")
.runAsTest("! toggles on and off rather than just setting an 'invert result' flag");
TestBuilder.command`
[[ ! ! ! 1 -eq 1 ]]; echo $?
[[ ! ! ! ! 1 -eq 1 ]]; echo $?
`
.stdout("1")
.stdout("0")
.runAsTest("! toggles on and off rather than just setting an 'invert result' flag");
TestBuilder.command`[[ a ]]`.exitCode(0).runAsTest("parenthesized terms didn't work right until post-2.04");
TestBuilder.command`[[ (a) ]]`.exitCode(0).runAsTest("parenthesized terms didn't work right until post-2.04");
TestBuilder.command`[[ -n a ]]`.exitCode(0).runAsTest("parenthesized terms didn't work right until post-2.04");
TestBuilder.command`[[ (-n a) ]]`.exitCode(0).runAsTest("parenthesized terms didn't work right until post-2.04");
TestBuilder.command`[[ -n $UNSET ]]`.exitCode(1).runAsTest("unset variables don't need to be quoted");
TestBuilder.command`[[ -z $UNSET ]]`.exitCode(0).runAsTest("unset variables don't need to be quoted");
TestBuilder.command`[[ $TDIR == /usr/homes/* ]]`
.exitCode(0)
.runAsTest("the ==/= and != operators do pattern matching");
TestBuilder.command`[[ $TDIR == /usr/homes/\\* ]]`
.exitCode(1)
.runAsTest("...but you can quote any part of the pattern to have it matched as a string");
TestBuilder.command`[[ $TDIR == '/usr/homes/*' ]]`
.exitCode(1)
.runAsTest("...but you can quote any part of the pattern to have it matched as a string");
TestBuilder.command`[[ -n $UNSET && $UNSET == foo ]]`
.exitCode(1)
.runAsTest("if the first part of && fails, the second is not executed");
TestBuilder.command`[[ -z $UNSET && $UNSET == foo ]]`
.exitCode(1)
.runAsTest("if the first part of && fails, the second is not executed");
TestBuilder.command`[[ -z $UNSET || -d $PWD ]]`
.exitCode(0)
.runAsTest("if the first part of || succeeds, the second is not executed");
// TestBuilder.command`[[ -n $TDIR || $HOME -ef ${H*} ]]`.exitCode(0).runAsTest("if the rhs were executed, it would be an error");
// TestBuilder.command`[[ -n $TDIR && -z $UNSET || $HOME -ef ${H*} ]]`.exitCode(0).runAsTest("if the rhs were executed, it would be an error");
TestBuilder.command`[[ -n $TDIR && -n $UNSET || $TDIR -ef . ]]`
.exitCode(0)
.runAsTest("&& has a higher parsing precedence than ||");
TestBuilder.command`[[ -n $TDIR || -n $UNSET && $PWD -ef xyz ]]`
.exitCode(1)
.runAsTest("...but expressions in parentheses may be used to override precedence rules");
TestBuilder.command`[[ ( -n $TDIR || -n $UNSET ) && $PWD -ef xyz ]]`
.exitCode(1)
.runAsTest("...but expressions in parentheses may be used to override precedence rules");
TestBuilder.command`
unset IVAR A
[[ 7 -gt $IVAR ]]
`
.exitCode(0)
.runAsTest(
"some arithmetic tests for completeness -- see what happens with missing operands, bad expressions, makes sure arguments are evaluated as arithmetic expressions, etc.",
);
TestBuilder.command`
unset IVAR A
[[ $IVAR -gt 7 ]]
`
.exitCode(1)
.runAsTest(
"some arithmetic tests for completeness -- see what happens with missing operands, bad expressions, makes sure arguments are evaluated as arithmetic expressions, etc.",
);
TestBuilder.command`
IVAR=4
[[ $IVAR -gt 7 ]]
`
.exitCode(1)
.runAsTest(
"some arithmetic tests for completeness -- see what happens with missing operands, bad expressions, makes sure arguments are evaluated as arithmetic expressions, etc.",
);
TestBuilder.command`[[ 7 -eq 4+3 ]]`
.exitCode(0)
.runAsTest(
"some arithmetic tests for completeness -- see what happens with missing operands, bad expressions, makes sure arguments are evaluated as arithmetic expressions, etc.",
);
TestBuilder.command`[[ 7 -eq 4+ ]]`
.exitCode(1)
.stdout('./cond.tests: line 122: [[: 4+: syntax error: operand expected (error token is "+")')
.runAsTest(
"some arithmetic tests for completeness -- see what happens with missing operands, bad expressions, makes sure arguments are evaluated as arithmetic expressions, etc.",
);
TestBuilder.command`
IVAR=4+3
[[ $IVAR -eq 7 ]]
`
.exitCode(0)
.runAsTest(
"some arithmetic tests for completeness -- see what happens with missing operands, bad expressions, makes sure arguments are evaluated as arithmetic expressions, etc.",
);
TestBuilder.command`
A=7
[[ $IVAR -eq A ]]
`
.exitCode(0)
.runAsTest(
"some arithmetic tests for completeness -- see what happens with missing operands, bad expressions, makes sure arguments are evaluated as arithmetic expressions, etc.",
);
TestBuilder.command`[[ "$IVAR" -eq "7" ]]`
.exitCode(0)
.runAsTest(
"some arithmetic tests for completeness -- see what happens with missing operands, bad expressions, makes sure arguments are evaluated as arithmetic expressions, etc.",
);
TestBuilder.command`
A=7
[[ "$IVAR" -eq "A" ]]
`
.exitCode(0)
.runAsTest(
"some arithmetic tests for completeness -- see what happens with missing operands, bad expressions, makes sure arguments are evaluated as arithmetic expressions, etc.",
);
TestBuilder.command`
unset IVAR A
[[ $filename == *.c ]]
`
.exitCode(1)
.runAsTest("more pattern matching tests");
TestBuilder.command`
filename=patmatch.c
[[ $filename == *.c ]]
`
.exitCode(0)
.runAsTest("more pattern matching tests");
TestBuilder.command`
shopt -s extglob
arg=-7
[[ $arg == -+([0-9]) ]]
`
.exitCode(0)
.runAsTest("the extended globbing features may be used when matching patterns");
TestBuilder.command`
shopt -s extglob
arg=-H
[[ $arg == -+([0-9]) ]]
`
.exitCode(1)
.runAsTest("the extended globbing features may be used when matching patterns");
TestBuilder.command`
shopt -s extglob
arg=+4
[[ $arg == ++([0-9]) ]]
`
.exitCode(0)
.runAsTest("the extended globbing features may be used when matching patterns");
TestBuilder.command`
STR=file.c
PAT=
if [[ $STR = $PAT ]]; then
echo oops
fi
`.runAsTest("make sure the null string is never matched if the string is not null");
TestBuilder.command`
STR=
PAT=
if [[ $STR = $PAT ]]; then
echo ok
fi
`
.stdout("ok")
.runAsTest("but that if the string is null, a null pattern is matched correctly");
// TestBuilder.command`
// [[ jbig2dec-0.9-i586-001.tgz =~ ([^-]+)-([^-]+)-([^-]+)-0*([1-9][0-9]*)\.tgz ]]
// echo ${BASH_REMATCH[1]}
// `
// .stdout("jbig2dec")
// .runAsTest("test the regular expression conditional operator");
// TestBuilder.command`
// [[ jbig2dec-0.9-i586-001.tgz =~ \\([^-]+\\)-\\([^-]+\\)-\\([^-]+\\)-0*\\([1-9][0-9]*\\)\\.tgz ]]
// echo ${BASH_REMATCH[1]}
// `
// .runAsTest("this shouldn't echo anything");
// TestBuilder.command`
// LDD_BASH=" linux-gate.so.1 => (0xffffe000)
// libreadline.so.5 => /lib/libreadline.so.5 (0xb7f91000)
// libhistory.so.5 => /lib/libhistory.so.5 (0xb7f8a000)
// libncurses.so.5 => /lib/libncurses.so.5 (0xb7f55000)
// libdl.so.2 => /lib/libdl.so.2 (0xb7f51000)
// libc.so.6 => /lib/libc.so.6 (0xb7e34000)
// /lib/ld-linux.so.2 (0xb7fd0000)"
// [[ "$LDD_BASH" =~ "libc" ]] && echo "found 1"
// echo ${BASH_REMATCH[@]}
// `
// .stdout("found 1")
// .stdout("libc")
// .runAsTest("test the regular expression conditional operator");
// TestBuilder.command`
// LDD_BASH=" linux-gate.so.1 => (0xffffe000)
// libreadline.so.5 => /lib/libreadline.so.5 (0xb7f91000)
// libhistory.so.5 => /lib/libhistory.so.5 (0xb7f8a000)
// libncurses.so.5 => /lib/libncurses.so.5 (0xb7f55000)
// libdl.so.2 => /lib/libdl.so.2 (0xb7f51000)
// libc.so.6 => /lib/libc.so.6 (0xb7e34000)
// /lib/ld-linux.so.2 (0xb7fd0000)"
// [[ "$LDD_BASH" =~ libc ]] && echo "found 2"
// echo ${BASH_REMATCH[@]}
// `
// .stdout("found 2")
// .stdout("libc")
// .runAsTest("test the regular expression conditional operator");
TestBuilder.command`
if [[ "123abc" == *?(a)bc ]]; then echo ok 42; else echo bad 42; fi
if [[ "123abc" == *?(a)bc ]]; then echo ok 43; else echo bad 43; fi
`
.stdout("ok 42")
.stdout("ok 43")
.runAsTest("bug in all versions up to and including bash-2.05b");
// TestBuilder.command`
// match() { [[ $ 1 == $2 ]]; }
// match $'? *x\1y\177z' $'??\\*\\x\\\1\\y\\\177\\z' || echo bad 44
// foo=""
// [[ bar == *"${foo,,}"* ]] && echo ok 1
// [[ bar == *${foo,,}* ]] && echo ok 2
// shopt -s extquote
// bs='\\'
// del=$'\177'
// [[ bar == *$bs"$del"* ]] || echo ok 3
// [[ "" == "$foo" ]] && echo ok 4
// [[ "$del" == "${foo,,}" ]] || echo ok 5
// # allow reserved words after a conditional command just because
// if [[ str ]] then [[ str ]] fi
// `
// .stdout("ok 1")
// .stdout("ok 2")
// .stdout("ok 3")
// .stdout("ok 4")
// .stdout("ok 5")
// .runAsTest("various tests");
});
});
describe("subshell", () => {
const sharppkgjson = /* json */ `{
"name": "sharp-test",
"module": "index.ts",
"type": "module",
"dependencies": {
"sharp": "0.33.3"
}
}`;
TestBuilder.command /* sh */ `
mkdir sharp-test
cd sharp-test
echo ${sharppkgjson} > package.json
${BUN} i
`
.ensureTempDir()
.stdout(out => expect(out).toInclude("+ sharp@0.33.3"))
.stderr(() => {})
.exitCode(0)
.env(bunEnv)
.runAsTest("sharp");
TestBuilder.command /* sh */ `( ( ( ( echo HI! ) ) ) )`.stdout("HI!\n").runAsTest("multiple levels");
TestBuilder.command /* sh */ `(
echo HELLO! ;
echo HELLO AGAIN!
)`
.stdout("HELLO!\nHELLO AGAIN!\n")
.runAsTest("multiline");
TestBuilder.command /* sh */ `(exit 42)`.exitCode(42).runAsTest("exit code");
TestBuilder.command /* sh */ `(exit 42); echo hi`.exitCode(0).stdout("hi\n").runAsTest("exit code 2");
TestBuilder.command /* sh */ `
VAR1=VALUE1
VAR2=VALUE2
VAR3=VALUE3
(
echo $VAR1 $VAR2 $VAR3
VAR1='you cant'
VAR2='see me'
VAR3='my time is now'
echo $VAR1 $VAR2 $VAR3
)
echo $VAR1 $VAR2 $VAR3
`
.stdout("VALUE1 VALUE2 VALUE3\nyou cant see me my time is now\nVALUE1 VALUE2 VALUE3\n")
.runAsTest("copy of environment");
TestBuilder.command /* sh */ `
mkdir foo
(
echo $PWD
cd foo
echo $PWD
)
echo $PWD
`
.ensureTempDir()
.stdout(`$TEMP_DIR\n$TEMP_DIR${sep}foo\n$TEMP_DIR\n`)
.runAsTest("does not change cwd");
TestBuilder.command`
BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log(process.env.FOO)"}
(
export FOO=bar
BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log(process.env.FOO)"}
)
BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log(process.env.FOO)"}
`
.stdout("undefined\nbar\nundefined\n")
.runAsTest("does not modify export env of parent");
TestBuilder.command`\(echo hi \)`.stderr("bun: command not found: (echo\n").exitCode(1).runAsTest("escaped subshell");
TestBuilder.command`echo \\\(hi\\\)`.stdout("\\(hi\\)\n").runAsTest("escaped subshell 2");
TestBuilder.command /* sh */ `
mkdir dir
(
cd dir
pwd | cat | cat
)
pwd
`
.ensureTempDir()
.stdout(`$TEMP_DIR${sep}dir\n$TEMP_DIR\n`)
.runAsTest("pipeline in subshell");
TestBuilder.command /* sh */ `
mkdir dir
(pwd) | cat
(cd dir; pwd) | cat
pwd
`
.ensureTempDir()
.stdout(`$TEMP_DIR\n$TEMP_DIR${sep}dir\n$TEMP_DIR\n`)
.runAsTest("subshell in pipeline");
TestBuilder.command /* sh */ `
mkdir dir
(pwd) | cat
(cd dir; pwd) | cat
pwd
`
.ensureTempDir()
.stdout(`$TEMP_DIR\n$TEMP_DIR${sep}dir\n$TEMP_DIR\n`)
.runAsTest("subshell in pipeline");
TestBuilder.command /* sh */ `
mkdir foo
( ( (cd foo ; pwd) | cat) ) | ( ( (cat) ) | cat )
`
.ensureTempDir()
.stdout(`$TEMP_DIR${sep}foo\n`)
.runAsTest("imbricated subshells and pipelines");
TestBuilder.command /* sh */ `
echo (echo)
`
.error("Unexpected token: `(`")
.runAsTest("Invalid subshell use");
describe("ported", () => {
// test_oE 'effect of subshell'
TestBuilder.command /* sh */ `
a=1
# (a=2; echo $a; exit; echo not reached)
# NOTE: We actually implemented exit wrong so changing this for now until we fix it
(a=2; echo $a; exit; echo reached)
echo $a
`
.stdout("2\nreached\n1\n")
.runAsTest("effect of subshell");
// test_x -e 23 'exit status of subshell'
TestBuilder.command /* sh */ `
(true; exit 23)
`
.exitCode(23)
.runAsTest("exit status of subshell");
// test_oE 'redirection on subshell'
TestBuilder.command /* sh */ `
(echo 1; echo 2; echo 3; echo 4) >sub_out
# (tail -n 2) <sub_out
cat sub_out
`
.error("Subshells with redirections are currently not supported. Please open a GitHub issue.")
// .stdout("1\n2\n3\n4\n")
.runAsTest("redirection on subshell");
// test_oE 'subshell ending with semicolon'
TestBuilder.command /* sh */ `
(echo foo;)
`
.stdout("foo\n")
.runAsTest("subshell ending with semicolon");
// test_oE 'subshell ending with asynchronous list'
TestBuilder.command /* sh */ `
mkfifo fifo1
(echo foo >fifo1&)
cat fifo1
`
.stdout("foo\n")
.todo("async commands not implemented yet")
.runAsTest("subshell ending with asynchronous list");
// test_oE 'newlines in subshell'
TestBuilder.command /* sh */ `
(
echo foo
)
`
.stdout("foo\n")
.runAsTest("newlines in subshell");
// test_oE 'effect of brace grouping'
TestBuilder.command /* sh */ `
a=1
{ a=2; echo $a; exit; echo not reached; }
echo $a
`
.stdout("2\n1\n")
.todo("brace grouping not implemented")
.runAsTest("effect of brace grouping");
// test_x -e 29 'exit status of brace grouping'
TestBuilder.command /* sh */ `
{ true; sh -c 'exit 29'; }
`
.exitCode(29)
.todo("brace grouping not implemented")
.runAsTest("exit status of brace grouping");
// test_oE 'redirection on brace grouping'
TestBuilder.command /* sh */ `
{ echo 1; echo 2; echo 3; echo 4; } >brace_out
{ tail -n 2; } <brace_out
`
.stdout("3\n4\n")
.todo("brace grouping not implemented")
.runAsTest("redirection on brace grouping");
// test_oE 'brace grouping ending with semicolon'
TestBuilder.command /* sh */ `
{ echo foo; }
`
.stdout("foo\n")
.todo("brace grouping not implemented")
.runAsTest("brace grouping ending with semicolon");
// test_oE 'brace grouping ending with asynchronous list'
TestBuilder.command /* sh */ `
mkfifo fifo1
{ echo foo >fifo1& }
cat fifo1
`
.stdout("foo\n")
.todo("brace grouping not implemented")
.runAsTest("brace grouping ending with asynchronous list");
// test_oE 'newlines in brace grouping'
TestBuilder.command /* sh */ `
{
echo foo
}
`
.stdout("foo\n")
.todo("brace grouping not implemented")
.runAsTest("newlines in brace grouping");
});
});
describe("when a command fails", () => {
let e: Bun.$.ShellError;
beforeAll(async () => {
$.throws(true);
try {
await Bun.$`false`;
} catch (err) {
e = err as Bun.$.ShellError;
} finally {
$.nothrow();
}
});
it("is an Error instance", () => expect(e).toBeInstanceOf(Error));
it("is a ShellError instance", () => expect(e).toBeInstanceOf(Bun.$.ShellError));
it("has a stdout buffer", () => expect(e.stdout).toBeInstanceOf(Uint8Array));
it("has a stderr buffer", () => expect(e.stderr).toBeInstanceOf(Uint8Array));
it("has an exit code of 1", () => expect(e.exitCode).toBe(1));
it("is named ShellError", () => expect(e.name).toBe("ShellError"));
});
describe("ShellError constructor", () => {
test.failing("new $.ShellError()", () => {
const e = new Bun.$.ShellError();
expect(e).toBeInstanceOf(Bun.$.ShellError);
expect(e).toBeInstanceOf(Error);
// TODO(@DonIsaac) fix constructor
expect(e.name).toBe("ShellError");
});
});
describe.todo("async", () => {
TestBuilder.command`echo hi && BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${/* ts */ `await Bun.sleep(500); console.log('noice')`} &; echo hello`
.stdout("hi\nhello\nnoice\n")
.runAsTest("basic");
TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${/* ts */ `await Bun.sleep(500); console.log('noice')`} | cat &; echo hello`
.stdout("hello\nnoice\n")
.runAsTest("pipeline");
TestBuilder.command`echo start > output.txt & cat output.txt`
.file("output.txt", "hey")
.stdout(s => expect(s).toBeOneOf(["hey", "start\n"]))
.runAsTest("background_execution_with_output_redirection");
});
function stringifyBuffer(buffer: Uint8Array): string {
const sentinel = sentinelByte(buffer);
const str = new TextDecoder().decode(buffer.slice(0, sentinel));
return str;
}
function sentinelByte(buf: Uint8Array): number {
for (let i = 0; i < buf.byteLength; i++) {
if (buf[i] == 0) return i;
}
throw new Error("No sentinel byte");
}