mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 00:48:55 +00:00
Compare commits
5 Commits
dylan/pyth
...
jarred/std
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b34568280 | ||
|
|
e34950b993 | ||
|
|
c8fe9523cb | ||
|
|
ec644923ee | ||
|
|
8566078023 |
@@ -22,6 +22,8 @@ pub const Stdio = union(enum) {
|
||||
array_buffer: JSC.ArrayBuffer.Strong,
|
||||
memfd: bun.FileDescriptor,
|
||||
pipe,
|
||||
buffer,
|
||||
text,
|
||||
ipc,
|
||||
readable_stream: JSC.WebCore.ReadableStream,
|
||||
|
||||
@@ -196,6 +198,7 @@ pub const Stdio = union(enum) {
|
||||
},
|
||||
.dup2 => .{ .dup2 = .{ .out = stdio.dup2.out, .to = stdio.dup2.to } },
|
||||
.capture, .pipe, .array_buffer, .readable_stream => .{ .buffer = {} },
|
||||
.buffer, .text => .{ .buffer = {} },
|
||||
.ipc => .{ .ipc = {} },
|
||||
.fd => |fd| .{ .pipe = fd },
|
||||
.memfd => |fd| .{ .pipe = fd },
|
||||
@@ -249,6 +252,7 @@ pub const Stdio = union(enum) {
|
||||
},
|
||||
.ipc => .{ .ipc = bun.default_allocator.create(uv.Pipe) catch bun.outOfMemory() },
|
||||
.capture, .pipe, .array_buffer, .readable_stream => .{ .buffer = bun.default_allocator.create(uv.Pipe) catch bun.outOfMemory() },
|
||||
.buffer, .text => .{ .buffer = bun.default_allocator.create(uv.Pipe) catch bun.outOfMemory() },
|
||||
.fd => |fd| .{ .pipe = fd },
|
||||
.dup2 => .{ .dup2 = .{ .out = stdio.dup2.out, .to = stdio.dup2.to } },
|
||||
.path => |pathlike| .{ .path = pathlike.slice() },
|
||||
@@ -262,7 +266,7 @@ pub const Stdio = union(enum) {
|
||||
|
||||
pub fn toSync(this: *@This(), i: u32) void {
|
||||
// Piping an empty stdin doesn't make sense
|
||||
if (i == 0 and this.* == .pipe) {
|
||||
if (i == 0 and (this.* == .pipe or this.* == .buffer or this.* == .text)) {
|
||||
this.* = .{ .ignore = {} };
|
||||
}
|
||||
}
|
||||
@@ -281,7 +285,7 @@ pub const Stdio = union(enum) {
|
||||
|
||||
pub fn isPiped(self: Stdio) bool {
|
||||
return switch (self) {
|
||||
.capture, .array_buffer, .blob, .pipe, .readable_stream => true,
|
||||
.capture, .array_buffer, .blob, .pipe, .readable_stream, .buffer, .text => true,
|
||||
.ipc => Environment.isWindows,
|
||||
else => false,
|
||||
};
|
||||
@@ -357,10 +361,14 @@ pub const Stdio = union(enum) {
|
||||
out_stdio.* = Stdio{ .ignore = {} };
|
||||
} else if (str.eqlComptime("pipe") or str.eqlComptime("overlapped")) {
|
||||
out_stdio.* = Stdio{ .pipe = {} };
|
||||
} else if (str.eqlComptime("buffer")) {
|
||||
out_stdio.* = Stdio{ .buffer = {} };
|
||||
} else if (str.eqlComptime("text")) {
|
||||
out_stdio.* = Stdio{ .text = {} };
|
||||
} else if (str.eqlComptime("ipc")) {
|
||||
out_stdio.* = Stdio{ .ipc = {} };
|
||||
} else {
|
||||
return globalThis.throwInvalidArguments("stdio must be an array of 'inherit', 'pipe', 'ignore', Bun.file(pathOrFd), number, or null", .{});
|
||||
return globalThis.throwInvalidArguments("stdio must be an array of 'inherit', 'pipe', 'ignore', 'buffer', 'text', 'ipc', Bun.file(pathOrFd), number, or null", .{});
|
||||
}
|
||||
return;
|
||||
} else if (value.isNumber()) {
|
||||
|
||||
@@ -318,15 +318,20 @@ pub fn onCloseIO(this: *Subprocess, kind: StdioKind) void {
|
||||
},
|
||||
inline .stdout, .stderr => |tag| {
|
||||
const out: *Readable = &@field(this, @tagName(tag));
|
||||
|
||||
switch (out.*) {
|
||||
.pipe => |pipe| {
|
||||
if (pipe.state == .done) {
|
||||
out.* = .{ .buffer = CowString.initOwned(pipe.state.done, bun.default_allocator) };
|
||||
pipe.state = .{ .done = &.{} };
|
||||
} else {
|
||||
out.* = .{ .ignore = {} };
|
||||
pipe.deref();
|
||||
}
|
||||
pipe.deref();
|
||||
},
|
||||
inline .buffer_promise, .text_promise => |pipe| {
|
||||
// Keep the pipe reference for now - the promise resolution
|
||||
// already happened in onReaderDone but we need to keep the state
|
||||
// so getStdout/getStderr can still work
|
||||
_ = pipe;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
@@ -383,6 +388,8 @@ const Readable = union(enum) {
|
||||
fd: bun.FileDescriptor,
|
||||
memfd: bun.FileDescriptor,
|
||||
pipe: *PipeReader,
|
||||
buffer_promise: *PipeReader, // New variant for "buffer"
|
||||
text_promise: *PipeReader, // New variant for "text"
|
||||
inherit: void,
|
||||
ignore: void,
|
||||
closed: void,
|
||||
@@ -397,7 +404,8 @@ const Readable = union(enum) {
|
||||
pub fn memoryCost(this: *const Readable) usize {
|
||||
return switch (this.*) {
|
||||
.pipe => @sizeOf(PipeReader) + this.pipe.memoryCost(),
|
||||
.buffer => this.buffer.length(),
|
||||
inline .buffer_promise, .text_promise => |p| @sizeOf(PipeReader) + p.memoryCost(),
|
||||
.buffer => |buf| buf.length(),
|
||||
else => 0,
|
||||
};
|
||||
}
|
||||
@@ -405,6 +413,7 @@ const Readable = union(enum) {
|
||||
pub fn hasPendingActivity(this: *const Readable) bool {
|
||||
return switch (this.*) {
|
||||
.pipe => this.pipe.hasPendingActivity(),
|
||||
inline .buffer_promise, .text_promise => |p| p.hasPendingActivity(),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
@@ -414,6 +423,9 @@ const Readable = union(enum) {
|
||||
.pipe => {
|
||||
this.pipe.updateRef(true);
|
||||
},
|
||||
inline .buffer_promise, .text_promise => |p| {
|
||||
p.updateRef(true);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
@@ -423,6 +435,9 @@ const Readable = union(enum) {
|
||||
.pipe => {
|
||||
this.pipe.updateRef(false);
|
||||
},
|
||||
inline .buffer_promise, .text_promise => |p| {
|
||||
p.updateRef(false);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
@@ -433,7 +448,7 @@ const Readable = union(enum) {
|
||||
assertStdioResult(result);
|
||||
|
||||
if (comptime Environment.isPosix) {
|
||||
if (stdio == .pipe) {
|
||||
if (stdio == .pipe or stdio == .buffer or stdio == .text) {
|
||||
_ = bun.sys.setNonblocking(result.?);
|
||||
}
|
||||
}
|
||||
@@ -445,6 +460,8 @@ const Readable = union(enum) {
|
||||
.memfd => if (Environment.isPosix) Readable{ .memfd = stdio.memfd } else Readable{ .ignore = {} },
|
||||
.dup2 => |dup2| if (Environment.isPosix) Output.panic("TODO: implement dup2 support in Stdio readable", .{}) else Readable{ .fd = dup2.out.toFd() },
|
||||
.pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result, max_size) },
|
||||
.buffer => Readable{ .buffer_promise = PipeReader.create(event_loop, process, result, max_size) },
|
||||
.text => Readable{ .text_promise = PipeReader.create(event_loop, process, result, max_size) },
|
||||
.array_buffer, .blob => Output.panic("TODO: implement ArrayBuffer & Blob support in Stdio readable", .{}),
|
||||
.capture => Output.panic("TODO: implement capture support in Stdio readable", .{}),
|
||||
.readable_stream => Readable{ .ignore = {} }, // ReadableStream is handled separately
|
||||
@@ -471,6 +488,9 @@ const Readable = union(enum) {
|
||||
.pipe => {
|
||||
this.pipe.close();
|
||||
},
|
||||
inline .buffer_promise, .text_promise => |p| {
|
||||
p.close();
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
@@ -488,6 +508,10 @@ const Readable = union(enum) {
|
||||
defer pipe.detach();
|
||||
this.* = .{ .closed = {} };
|
||||
},
|
||||
inline .buffer_promise, .text_promise => |p| {
|
||||
defer p.detach();
|
||||
this.* = .{ .closed = {} };
|
||||
},
|
||||
.buffer => |*buf| {
|
||||
buf.deinit(bun.default_allocator);
|
||||
},
|
||||
@@ -497,6 +521,7 @@ const Readable = union(enum) {
|
||||
|
||||
pub fn toJS(this: *Readable, globalThis: *JSC.JSGlobalObject, exited: bool) JSValue {
|
||||
_ = exited; // autofix
|
||||
log("Readable.toJS called, variant = {s}", .{@tagName(this.*)});
|
||||
switch (this.*) {
|
||||
// should only be reachable when the entire output is buffered.
|
||||
.memfd => return this.toBufferedValue(globalThis) catch .zero,
|
||||
@@ -509,6 +534,59 @@ const Readable = union(enum) {
|
||||
this.* = .{ .closed = {} };
|
||||
return pipe.toJS(globalThis);
|
||||
},
|
||||
.buffer_promise => |pipe| {
|
||||
log("toJS buffer_promise: pipe state = {s}", .{@tagName(pipe.state)});
|
||||
|
||||
// If we already have a pending promise, return it
|
||||
if (pipe.pending_promise) |promise| {
|
||||
log("toJS buffer_promise: returning existing promise", .{});
|
||||
return promise.toJS();
|
||||
}
|
||||
|
||||
// Check if the PipeReader has already finished reading
|
||||
if (pipe.state == .done) {
|
||||
log("toJS buffer_promise: state is done, creating resolved promise", .{});
|
||||
const bytes = pipe.state.done;
|
||||
// Don't use toOwnedSlice as it might have already been called
|
||||
const bytes_copy = bun.default_allocator.alloc(u8, bytes.len) catch {
|
||||
globalThis.throwOutOfMemory() catch return .zero;
|
||||
};
|
||||
@memcpy(bytes_copy, bytes);
|
||||
defer this.* = .{ .closed = {} };
|
||||
const buffer = JSC.MarkedArrayBuffer.fromBytes(bytes_copy, bun.default_allocator, .Uint8Array).toNodeBuffer(globalThis);
|
||||
return JSC.JSPromise.resolvedPromiseValue(globalThis, buffer);
|
||||
}
|
||||
|
||||
log("toJS buffer_promise: state is pending, creating new promise", .{});
|
||||
// Create a new pending promise and store it
|
||||
const promise = JSC.JSPromise.create(globalThis);
|
||||
pipe.pending_promise = promise;
|
||||
return promise.toJS();
|
||||
},
|
||||
.text_promise => |pipe| {
|
||||
// If we already have a pending promise, return it
|
||||
if (pipe.pending_promise) |promise| {
|
||||
return promise.toJS();
|
||||
}
|
||||
|
||||
// Check if the PipeReader has already finished reading
|
||||
if (pipe.state == .done) {
|
||||
const bytes = pipe.state.done;
|
||||
const bytes_copy = bun.default_allocator.alloc(u8, bytes.len) catch {
|
||||
globalThis.throwOutOfMemory() catch return .zero;
|
||||
};
|
||||
@memcpy(bytes_copy, bytes);
|
||||
defer this.* = .{ .closed = {} };
|
||||
var str = bun.SliceWithUnderlyingString.transcodeFromOwnedSlice(bytes_copy, .utf8);
|
||||
defer str.deinit();
|
||||
return JSC.JSPromise.resolvedPromiseValue(globalThis, str.toJS(globalThis));
|
||||
}
|
||||
|
||||
// Create a new pending promise and store it
|
||||
const promise = JSC.JSPromise.create(globalThis);
|
||||
pipe.pending_promise = promise;
|
||||
return promise.toJS();
|
||||
},
|
||||
.buffer => |*buffer| {
|
||||
defer this.* = .{ .closed = {} };
|
||||
|
||||
@@ -544,6 +622,19 @@ const Readable = union(enum) {
|
||||
this.* = .{ .closed = {} };
|
||||
return pipe.toBuffer(globalThis);
|
||||
},
|
||||
inline .buffer_promise, .text_promise => |pipe, tag| {
|
||||
defer pipe.detach();
|
||||
this.* = .{ .closed = {} };
|
||||
|
||||
// For text mode, return a string instead of a buffer
|
||||
if (tag == .text_promise) {
|
||||
const bytes = pipe.toOwnedSlice();
|
||||
// Use createUTF8ForJS which properly handles the memory for JS
|
||||
return bun.String.createUTF8ForJS(globalThis, bytes);
|
||||
}
|
||||
|
||||
return pipe.toBuffer(globalThis);
|
||||
},
|
||||
.buffer => |*buf| {
|
||||
defer this.* = .{ .closed = {} };
|
||||
const own = buf.takeSlice(bun.default_allocator) catch {
|
||||
@@ -564,6 +655,24 @@ pub fn getStderr(
|
||||
globalThis: *JSGlobalObject,
|
||||
) JSValue {
|
||||
this.observable_getters.insert(.stderr);
|
||||
|
||||
// For buffer_promise and text_promise modes, we need to handle the race condition
|
||||
// where the process might have already exited before the getter is called
|
||||
switch (this.stderr) {
|
||||
inline .buffer_promise, .text_promise => {
|
||||
// Check if there's already a cached promise
|
||||
if (this.this_jsvalue != .zero) {
|
||||
if (JSC.Codegen.JSSubprocess.stderrGetCached(this.this_jsvalue)) |cached| {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
// Create and cache the promise (resolved or pending based on variant)
|
||||
const promise_value = this.stderr.toJS(globalThis, this.hasExited());
|
||||
return promise_value;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
return this.stderr.toJS(globalThis, this.hasExited());
|
||||
}
|
||||
|
||||
@@ -580,6 +689,28 @@ pub fn getStdout(
|
||||
globalThis: *JSGlobalObject,
|
||||
) JSValue {
|
||||
this.observable_getters.insert(.stdout);
|
||||
|
||||
log("getStdout called, stdout variant = {s}", .{@tagName(this.stdout)});
|
||||
|
||||
// For buffer_promise and text_promise modes, we need to handle the race condition
|
||||
// where the process might have already exited before the getter is called
|
||||
switch (this.stdout) {
|
||||
inline .buffer_promise, .text_promise => {
|
||||
// Check if there's already a cached promise
|
||||
if (this.this_jsvalue != .zero) {
|
||||
if (JSC.Codegen.JSSubprocess.stdoutGetCached(this.this_jsvalue)) |cached| {
|
||||
log("getStdout returning cached promise", .{});
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
// Create and cache the promise
|
||||
const promise_value = this.stdout.toJS(globalThis, this.hasExited());
|
||||
log("getStdout created promise", .{});
|
||||
return promise_value;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
// NOTE: ownership of internal buffers is transferred to the JSValue, which
|
||||
// gets cached on JSSubprocess (created via bindgen). This makes it
|
||||
// re-accessable to JS code but not via `this.stdout`, which is now `.closed`.
|
||||
@@ -992,6 +1123,7 @@ pub const PipeReader = struct {
|
||||
err: bun.sys.Error,
|
||||
} = .{ .pending = {} },
|
||||
stdio_result: StdioResult,
|
||||
pending_promise: ?*JSC.JSPromise = null,
|
||||
pub const IOReader = bun.io.BufferedReader;
|
||||
pub const Poll = IOReader;
|
||||
|
||||
@@ -1067,18 +1199,61 @@ pub const PipeReader = struct {
|
||||
this.state = .{ .done = owned };
|
||||
if (this.process) |process| {
|
||||
this.process = null;
|
||||
process.onCloseIO(this.kind(process));
|
||||
|
||||
// Determine if this is stdout or stderr
|
||||
const stdio_kind = this.kind(process);
|
||||
|
||||
// If we have a pending promise for buffer/text mode, we need to resolve it now
|
||||
if (this.pending_promise) |promise| {
|
||||
const globalThis = process.globalThis;
|
||||
const event_loop = globalThis.bunVM().eventLoop();
|
||||
event_loop.enter();
|
||||
defer event_loop.exit();
|
||||
|
||||
const is_stdout = stdio_kind == .stdout;
|
||||
const readable = if (is_stdout) &process.stdout else &process.stderr;
|
||||
|
||||
// Resolve the promise based on the mode
|
||||
switch (readable.*) {
|
||||
.buffer_promise => {
|
||||
// Don't use 'owned' directly as it will be freed later
|
||||
const bytes = bun.default_allocator.alloc(u8, owned.len) catch bun.outOfMemory();
|
||||
@memcpy(bytes, owned);
|
||||
const buffer = JSC.MarkedArrayBuffer.fromBytes(bytes, bun.default_allocator, .Uint8Array).toNodeBuffer(globalThis);
|
||||
promise.resolve(globalThis, buffer);
|
||||
},
|
||||
.text_promise => {
|
||||
// Don't use 'owned' directly as it will be freed later
|
||||
const bytes = bun.default_allocator.alloc(u8, owned.len) catch bun.outOfMemory();
|
||||
@memcpy(bytes, owned);
|
||||
var str = bun.SliceWithUnderlyingString.transcodeFromOwnedSlice(bytes, .utf8);
|
||||
defer str.deinit();
|
||||
promise.resolve(globalThis, str.toJS(globalThis));
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
this.pending_promise = null;
|
||||
}
|
||||
|
||||
process.onCloseIO(stdio_kind);
|
||||
this.deref();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(reader: *const PipeReader, process: *const Subprocess) StdioKind {
|
||||
if (process.stdout == .pipe and process.stdout.pipe == reader) {
|
||||
return .stdout;
|
||||
switch (process.stdout) {
|
||||
inline .pipe, .buffer_promise, .text_promise => |p| {
|
||||
if (p == reader) return .stdout;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
if (process.stderr == .pipe and process.stderr.pipe == reader) {
|
||||
return .stderr;
|
||||
|
||||
switch (process.stderr) {
|
||||
inline .pipe, .buffer_promise, .text_promise => |p| {
|
||||
if (p == reader) return .stderr;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
@panic("We should be either stdout or stderr");
|
||||
@@ -1392,6 +1567,10 @@ const Writable = union(enum) {
|
||||
.pipe = pipe,
|
||||
};
|
||||
},
|
||||
.buffer, .text => {
|
||||
// stdin cannot be in buffer or text mode
|
||||
return Writable{ .ignore = {} };
|
||||
},
|
||||
|
||||
.blob => |blob| {
|
||||
return Writable{
|
||||
@@ -2568,6 +2747,16 @@ pub fn spawnMaybeSync(
|
||||
}
|
||||
}
|
||||
|
||||
switch (subprocess.stdout) {
|
||||
inline .buffer_promise, .text_promise => |pipe| {
|
||||
pipe.start(subprocess, loop).assert();
|
||||
if ((is_sync or !lazy)) {
|
||||
pipe.readAll();
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
if (subprocess.stderr == .pipe) {
|
||||
subprocess.stderr.pipe.start(subprocess, loop).assert();
|
||||
|
||||
@@ -2576,6 +2765,16 @@ pub fn spawnMaybeSync(
|
||||
}
|
||||
}
|
||||
|
||||
switch (subprocess.stderr) {
|
||||
inline .buffer_promise, .text_promise => |pipe| {
|
||||
pipe.start(subprocess, loop).assert();
|
||||
if ((is_sync or !lazy)) {
|
||||
pipe.readAll();
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
should_close_memfd = false;
|
||||
|
||||
if (comptime !is_sync) {
|
||||
@@ -2644,6 +2843,16 @@ pub fn spawnMaybeSync(
|
||||
subprocess.stdout.pipe.watch();
|
||||
}
|
||||
|
||||
switch (subprocess.stderr) {
|
||||
inline .buffer_promise, .text_promise => |pipe| pipe.watch(),
|
||||
else => {},
|
||||
}
|
||||
|
||||
switch (subprocess.stdout) {
|
||||
inline .buffer_promise, .text_promise => |pipe| pipe.watch(),
|
||||
else => {},
|
||||
}
|
||||
|
||||
jsc_vm.tick();
|
||||
jsc_vm.eventLoop().autoTick();
|
||||
}
|
||||
|
||||
@@ -209,6 +209,10 @@ pub const ShellSubprocess = struct {
|
||||
// The shell never uses this
|
||||
@panic("Unimplemented stdin pipe");
|
||||
},
|
||||
.buffer, .text => {
|
||||
// The shell never uses this
|
||||
@panic("Unimplemented stdin buffer/text");
|
||||
},
|
||||
|
||||
.blob => |blob| {
|
||||
return Writable{
|
||||
@@ -380,6 +384,7 @@ pub const ShellSubprocess = struct {
|
||||
.blob => Readable{ .ignore = {} },
|
||||
.memfd => Readable{ .ignore = {} },
|
||||
.pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result, null, out_type) },
|
||||
.buffer, .text => Readable{ .pipe = PipeReader.create(event_loop, process, result, null, out_type) },
|
||||
.array_buffer => {
|
||||
const readable = Readable{ .pipe = PipeReader.create(event_loop, process, result, null, out_type) };
|
||||
readable.pipe.buffered_output = .{
|
||||
@@ -402,6 +407,7 @@ pub const ShellSubprocess = struct {
|
||||
.blob => Readable{ .ignore = {} },
|
||||
.memfd => Readable{ .memfd = stdio.memfd },
|
||||
.pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result, null, out_type) },
|
||||
.buffer, .text => Readable{ .pipe = PipeReader.create(event_loop, process, result, null, out_type) },
|
||||
.array_buffer => {
|
||||
const readable = Readable{ .pipe = PipeReader.create(event_loop, process, result, null, out_type) };
|
||||
readable.pipe.buffered_output = .{
|
||||
|
||||
26
test-simple-buffer.js
Normal file
26
test-simple-buffer.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const proc = Bun.spawn({
|
||||
cmd: ['echo', 'hello'],
|
||||
stdout: 'buffer'
|
||||
});
|
||||
|
||||
console.log('1. Created process');
|
||||
console.log('2. proc.stdout is Promise:', proc.stdout instanceof Promise);
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log('TIMEOUT: Promise never resolved!');
|
||||
process.exit(1);
|
||||
}, 2000);
|
||||
|
||||
proc.stdout.then(buffer => {
|
||||
clearTimeout(timeoutId);
|
||||
console.log('3. Promise resolved!');
|
||||
console.log('4. buffer:', buffer);
|
||||
console.log('5. buffer instanceof Buffer:', buffer instanceof Buffer);
|
||||
console.log('6. buffer.toString():', buffer.toString());
|
||||
console.log('SUCCESS');
|
||||
process.exit(0);
|
||||
}).catch(err => {
|
||||
clearTimeout(timeoutId);
|
||||
console.log('ERROR:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
79
test/js/bun/spawn/spawn-buffer-text-simple.test.ts
Normal file
79
test/js/bun/spawn/spawn-buffer-text-simple.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { test, expect, describe } from "bun:test";
|
||||
import { spawn, spawnSync } from "bun";
|
||||
|
||||
describe("Bun.spawn() buffer and text modes - simple", () => {
|
||||
test("async buffer mode works", async () => {
|
||||
const proc = spawn({
|
||||
cmd: ["echo", "hello buffer"],
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
expect(proc.stdout).toBeInstanceOf(Promise);
|
||||
const buffer = await proc.stdout;
|
||||
expect(buffer).toBeInstanceOf(Buffer);
|
||||
expect(buffer.toString()).toBe("hello buffer\n");
|
||||
});
|
||||
|
||||
test("async text mode works", async () => {
|
||||
const proc = spawn({
|
||||
cmd: ["echo", "hello text"],
|
||||
stdout: "text",
|
||||
});
|
||||
|
||||
expect(proc.stdout).toBeInstanceOf(Promise);
|
||||
const text = await proc.stdout;
|
||||
expect(typeof text).toBe("string");
|
||||
expect(text).toBe("hello text\n");
|
||||
});
|
||||
|
||||
test("sync buffer mode works", () => {
|
||||
const result = spawnSync({
|
||||
cmd: ["echo", "hello buffer"],
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.stdout).toBeDefined();
|
||||
expect(result.stdout).toBeInstanceOf(Buffer);
|
||||
if (result.stdout instanceof Buffer) {
|
||||
expect(result.stdout.toString()).toBe("hello buffer\n");
|
||||
}
|
||||
});
|
||||
|
||||
test("sync text mode works", () => {
|
||||
const result = spawnSync({
|
||||
cmd: ["echo", "hello text"],
|
||||
stdout: "text",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.stdout).toBeDefined();
|
||||
expect(typeof result.stdout).toBe("string");
|
||||
expect(result.stdout).toBe("hello text\n");
|
||||
});
|
||||
|
||||
test("stderr buffer mode works", async () => {
|
||||
const proc = spawn({
|
||||
cmd: ["sh", "-c", "echo error >&2"],
|
||||
stderr: "buffer",
|
||||
});
|
||||
|
||||
const stderr = await proc.stderr;
|
||||
expect(stderr).toBeInstanceOf(Buffer);
|
||||
expect(stderr.toString()).toBe("error\n");
|
||||
});
|
||||
|
||||
test("both stdout and stderr work together", async () => {
|
||||
const proc = spawn({
|
||||
cmd: ["sh", "-c", "echo out && echo err >&2"],
|
||||
stdout: "buffer",
|
||||
stderr: "text",
|
||||
});
|
||||
|
||||
const [stdout, stderr] = await Promise.all([proc.stdout, proc.stderr]);
|
||||
expect(stdout).toBeInstanceOf(Buffer);
|
||||
expect(stdout.toString()).toBe("out\n");
|
||||
expect(typeof stderr).toBe("string");
|
||||
expect(stderr).toBe("err\n");
|
||||
});
|
||||
});
|
||||
521
test/js/bun/spawn/spawn-stdout-buffer-text.test.ts
Normal file
521
test/js/bun/spawn/spawn-stdout-buffer-text.test.ts
Normal file
@@ -0,0 +1,521 @@
|
||||
import { test, expect, describe } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
import path from "node:path";
|
||||
|
||||
describe("Bun.spawn() stdout: 'buffer' and 'text'", () => {
|
||||
describe("stdout: 'buffer'", () => {
|
||||
test("returns a promise that resolves to a Buffer", async () => {
|
||||
const dir = tempDirWithFiles("spawn-buffer-test", {
|
||||
"echo.js": `console.log("Hello, world!");`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "echo.js")],
|
||||
env: bunEnv,
|
||||
stdout: "buffer",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Should return a promise
|
||||
expect(proc.stdout).toBeInstanceOf(Promise);
|
||||
|
||||
const buffer = await proc.stdout;
|
||||
expect(buffer).toBeInstanceOf(Buffer);
|
||||
expect(buffer.toString()).toBe("Hello, world!\n");
|
||||
|
||||
// Accessing again should return the same promise
|
||||
const buffer2 = await proc.stdout;
|
||||
expect(buffer2).toBe(buffer);
|
||||
|
||||
// stderr should still be a stream
|
||||
const stderr = await new Response(proc.stderr).text();
|
||||
expect(stderr).toBe("");
|
||||
});
|
||||
|
||||
test("handles binary data correctly", async () => {
|
||||
const dir = tempDirWithFiles("spawn-buffer-binary", {
|
||||
"binary.js": `
|
||||
const buf = Buffer.from([0xFF, 0xFE, 0x00, 0x01, 0x02, 0x03]);
|
||||
process.stdout.write(buf);
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "binary.js")],
|
||||
env: bunEnv,
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
const buffer = await proc.stdout;
|
||||
expect(buffer).toBeInstanceOf(Buffer);
|
||||
expect(Array.from(buffer)).toEqual([0xFF, 0xFE, 0x00, 0x01, 0x02, 0x03]);
|
||||
});
|
||||
|
||||
test("handles large output", async () => {
|
||||
const dir = tempDirWithFiles("spawn-buffer-large", {
|
||||
"large.js": `
|
||||
const chunk = Buffer.alloc(1024 * 1024, 'A').toString();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
process.stdout.write(chunk);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "large.js")],
|
||||
env: bunEnv,
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
const buffer = await proc.stdout;
|
||||
expect(buffer).toBeInstanceOf(Buffer);
|
||||
expect(buffer.length).toBe(10 * 1024 * 1024);
|
||||
expect(buffer.every(byte => byte === 65)).toBe(true); // All 'A's
|
||||
});
|
||||
|
||||
test("works with stderr: 'buffer' too", async () => {
|
||||
const dir = tempDirWithFiles("spawn-buffer-stderr", {
|
||||
"both.js": `
|
||||
console.log("stdout message");
|
||||
console.error("stderr message");
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "both.js")],
|
||||
env: bunEnv,
|
||||
stdout: "buffer",
|
||||
stderr: "buffer",
|
||||
});
|
||||
|
||||
const [stdout, stderr] = await Promise.all([proc.stdout, proc.stderr]);
|
||||
|
||||
expect(stdout).toBeInstanceOf(Buffer);
|
||||
expect(stdout.toString()).toBe("stdout message\n");
|
||||
|
||||
expect(stderr).toBeInstanceOf(Buffer);
|
||||
expect(stderr.toString()).toBe("stderr message\n");
|
||||
});
|
||||
|
||||
test("handles empty output", async () => {
|
||||
const dir = tempDirWithFiles("spawn-buffer-empty", {
|
||||
"empty.js": `// No output`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "empty.js")],
|
||||
env: bunEnv,
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
const buffer = await proc.stdout;
|
||||
expect(buffer).toBeInstanceOf(Buffer);
|
||||
expect(buffer.length).toBe(0);
|
||||
expect(buffer.toString()).toBe("");
|
||||
});
|
||||
|
||||
test("resolves after process exits", async () => {
|
||||
const dir = tempDirWithFiles("spawn-buffer-timing", {
|
||||
"delayed.js": `
|
||||
setTimeout(() => {
|
||||
console.log("delayed output");
|
||||
}, 100);
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "delayed.js")],
|
||||
env: bunEnv,
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
const buffer = await proc.stdout;
|
||||
expect(buffer.toString()).toBe("delayed output\n");
|
||||
|
||||
// Process should have exited by now
|
||||
const exitCode = await proc.exited;
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("works with maxBuffer", async () => {
|
||||
const dir = tempDirWithFiles("spawn-buffer-maxbuf", {
|
||||
"overflow.js": `
|
||||
const chunk = Buffer.alloc(1024, 'A').toString();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
process.stdout.write(chunk);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "overflow.js")],
|
||||
env: bunEnv,
|
||||
stdout: "buffer",
|
||||
maxBuffer: 5 * 1024, // 5KB limit
|
||||
});
|
||||
|
||||
try {
|
||||
await proc.stdout;
|
||||
expect.unreachable("Should have been killed due to maxBuffer");
|
||||
} catch (e) {
|
||||
// Process should be killed
|
||||
}
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
expect(exitCode).not.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stdout: 'text'", () => {
|
||||
test("returns a promise that resolves to a UTF-8 string", async () => {
|
||||
const dir = tempDirWithFiles("spawn-text-test", {
|
||||
"echo.js": `console.log("Hello, world!");`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "echo.js")],
|
||||
env: bunEnv,
|
||||
stdout: "text",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Should return a promise
|
||||
expect(proc.stdout).toBeInstanceOf(Promise);
|
||||
|
||||
const text = await proc.stdout;
|
||||
expect(typeof text).toBe("string");
|
||||
expect(text).toBe("Hello, world!\n");
|
||||
|
||||
// Accessing again should return the same promise
|
||||
const text2 = await proc.stdout;
|
||||
expect(text2).toBe(text);
|
||||
|
||||
// stderr should still be a stream
|
||||
const stderr = await new Response(proc.stderr).text();
|
||||
expect(stderr).toBe("");
|
||||
});
|
||||
|
||||
test("handles UTF-8 correctly", async () => {
|
||||
const dir = tempDirWithFiles("spawn-text-utf8", {
|
||||
"utf8.js": `
|
||||
console.log("Hello 世界 🌍");
|
||||
console.log("Emoji: 🎉🎊🎈");
|
||||
console.log("Accents: café, naïve, résumé");
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "utf8.js")],
|
||||
env: bunEnv,
|
||||
stdout: "text",
|
||||
});
|
||||
|
||||
const text = await proc.stdout;
|
||||
expect(text).toBe("Hello 世界 🌍\nEmoji: 🎉🎊🎈\nAccents: café, naïve, résumé\n");
|
||||
});
|
||||
|
||||
test("handles multi-byte UTF-8 sequences", async () => {
|
||||
const dir = tempDirWithFiles("spawn-text-multibyte", {
|
||||
"multibyte.js": `
|
||||
// 2-byte: £ (U+00A3)
|
||||
// 3-byte: € (U+20AC)
|
||||
// 4-byte: 𝄞 (U+1D11E)
|
||||
console.log("2-byte: £");
|
||||
console.log("3-byte: €");
|
||||
console.log("4-byte: 𝄞");
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "multibyte.js")],
|
||||
env: bunEnv,
|
||||
stdout: "text",
|
||||
});
|
||||
|
||||
const text = await proc.stdout;
|
||||
expect(text).toBe("2-byte: £\n3-byte: €\n4-byte: 𝄞\n");
|
||||
});
|
||||
|
||||
test("handles invalid UTF-8 by rejecting", async () => {
|
||||
const dir = tempDirWithFiles("spawn-text-invalid", {
|
||||
"invalid.js": `
|
||||
// Output invalid UTF-8 sequence
|
||||
process.stdout.write(Buffer.from([0xFF, 0xFE, 0xFD]));
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "invalid.js")],
|
||||
env: bunEnv,
|
||||
stdout: "text",
|
||||
});
|
||||
|
||||
try {
|
||||
await proc.stdout;
|
||||
expect.unreachable("Should have rejected with invalid UTF-8");
|
||||
} catch (e) {
|
||||
// Expected to reject
|
||||
}
|
||||
});
|
||||
|
||||
test("works with stderr: 'text' too", async () => {
|
||||
const dir = tempDirWithFiles("spawn-text-stderr", {
|
||||
"both.js": `
|
||||
console.log("stdout message");
|
||||
console.error("stderr message");
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "both.js")],
|
||||
env: bunEnv,
|
||||
stdout: "text",
|
||||
stderr: "text",
|
||||
});
|
||||
|
||||
const [stdout, stderr] = await Promise.all([proc.stdout, proc.stderr]);
|
||||
|
||||
expect(typeof stdout).toBe("string");
|
||||
expect(stdout).toBe("stdout message\n");
|
||||
|
||||
expect(typeof stderr).toBe("string");
|
||||
expect(stderr).toBe("stderr message\n");
|
||||
});
|
||||
|
||||
test("handles empty output", async () => {
|
||||
const dir = tempDirWithFiles("spawn-text-empty", {
|
||||
"empty.js": `// No output`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "empty.js")],
|
||||
env: bunEnv,
|
||||
stdout: "text",
|
||||
});
|
||||
|
||||
const text = await proc.stdout;
|
||||
expect(typeof text).toBe("string");
|
||||
expect(text).toBe("");
|
||||
});
|
||||
|
||||
test("handles large text output", async () => {
|
||||
const dir = tempDirWithFiles("spawn-text-large", {
|
||||
"large.js": `
|
||||
const line = Buffer.alloc(80, 'X').toString() + '\\n';
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
process.stdout.write(line);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "large.js")],
|
||||
env: bunEnv,
|
||||
stdout: "text",
|
||||
});
|
||||
|
||||
const text = await proc.stdout;
|
||||
expect(typeof text).toBe("string");
|
||||
expect(text.length).toBe(10000 * 81); // 80 X's + newline
|
||||
const lines = text.split('\n');
|
||||
expect(lines.length).toBe(10001); // 10000 lines + empty string at end
|
||||
expect(lines[0]).toBe(Buffer.alloc(80, 'X').toString());
|
||||
});
|
||||
});
|
||||
|
||||
describe("mixed modes", () => {
|
||||
test("can mix buffer, text, and pipe", async () => {
|
||||
const dir = tempDirWithFiles("spawn-mixed", {
|
||||
"mixed.js": `
|
||||
console.log("stdout");
|
||||
console.error("stderr");
|
||||
`,
|
||||
});
|
||||
|
||||
// Test all combinations
|
||||
const configs = [
|
||||
{ stdout: "buffer", stderr: "text" },
|
||||
{ stdout: "text", stderr: "buffer" },
|
||||
{ stdout: "buffer", stderr: "pipe" },
|
||||
{ stdout: "pipe", stderr: "buffer" },
|
||||
{ stdout: "text", stderr: "pipe" },
|
||||
{ stdout: "pipe", stderr: "text" },
|
||||
] as const;
|
||||
|
||||
for (const config of configs) {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "mixed.js")],
|
||||
env: bunEnv,
|
||||
...config,
|
||||
});
|
||||
|
||||
if (config.stdout === "buffer") {
|
||||
const buffer = await proc.stdout;
|
||||
expect(buffer).toBeInstanceOf(Buffer);
|
||||
expect(buffer.toString()).toBe("stdout\n");
|
||||
} else if (config.stdout === "text") {
|
||||
const text = await proc.stdout;
|
||||
expect(typeof text).toBe("string");
|
||||
expect(text).toBe("stdout\n");
|
||||
} else {
|
||||
const text = await new Response(proc.stdout).text();
|
||||
expect(text).toBe("stdout\n");
|
||||
}
|
||||
|
||||
if (config.stderr === "buffer") {
|
||||
const buffer = await proc.stderr;
|
||||
expect(buffer).toBeInstanceOf(Buffer);
|
||||
expect(buffer.toString()).toBe("stderr\n");
|
||||
} else if (config.stderr === "text") {
|
||||
const text = await proc.stderr;
|
||||
expect(typeof text).toBe("string");
|
||||
expect(text).toBe("stderr\n");
|
||||
} else {
|
||||
const text = await new Response(proc.stderr).text();
|
||||
expect(text).toBe("stderr\n");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("sync mode (spawnSync)", () => {
|
||||
test("buffer mode returns buffer in result", () => {
|
||||
const dir = tempDirWithFiles("spawn-sync-buffer", {
|
||||
"echo.js": `console.log("sync output");`,
|
||||
});
|
||||
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), path.join(dir, "echo.js")],
|
||||
env: bunEnv,
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.stdout).toBeDefined();
|
||||
expect(result.stdout).toBeInstanceOf(Buffer);
|
||||
expect(result.stdout!.toString()).toBe("sync output\n");
|
||||
});
|
||||
|
||||
test("text mode returns string in result", () => {
|
||||
const dir = tempDirWithFiles("spawn-sync-text", {
|
||||
"echo.js": `console.log("sync text");`,
|
||||
});
|
||||
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), path.join(dir, "echo.js")],
|
||||
env: bunEnv,
|
||||
stdout: "text",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(typeof result.stdout).toBe("string");
|
||||
expect(result.stdout).toBe("sync text\n");
|
||||
});
|
||||
|
||||
test("handles UTF-8 in sync text mode", () => {
|
||||
const dir = tempDirWithFiles("spawn-sync-text-utf8", {
|
||||
"utf8.js": `console.log("Hello 世界 🌍");`,
|
||||
});
|
||||
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), path.join(dir, "utf8.js")],
|
||||
env: bunEnv,
|
||||
stdout: "text",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.stdout).toBe("Hello 世界 🌍\n");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
test("process that exits immediately", async () => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", "process.exit(0)"],
|
||||
env: bunEnv,
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
const buffer = await proc.stdout;
|
||||
expect(buffer).toBeInstanceOf(Buffer);
|
||||
expect(buffer.length).toBe(0);
|
||||
});
|
||||
|
||||
test("process that fails", async () => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", "throw new Error('test error')"],
|
||||
env: bunEnv,
|
||||
stdout: "buffer",
|
||||
stderr: "text",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
proc.stdout,
|
||||
proc.stderr,
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(stdout).toBeInstanceOf(Buffer);
|
||||
expect(stdout.length).toBe(0);
|
||||
|
||||
expect(typeof stderr).toBe("string");
|
||||
expect(stderr).toContain("test error");
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
});
|
||||
|
||||
test("stdin still works with buffer/text stdout", async () => {
|
||||
const dir = tempDirWithFiles("spawn-stdin-buffer", {
|
||||
"cat.js": `
|
||||
let data = '';
|
||||
process.stdin.on('data', chunk => data += chunk);
|
||||
process.stdin.on('end', () => console.log(data));
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(dir, "cat.js")],
|
||||
env: bunEnv,
|
||||
stdin: "pipe",
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
proc.stdin.write("Hello from stdin");
|
||||
proc.stdin.end();
|
||||
|
||||
const buffer = await proc.stdout;
|
||||
expect(buffer.toString()).toBe("Hello from stdin\n");
|
||||
});
|
||||
|
||||
test("accessing stdout after it resolves returns same value", async () => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", "console.log('test')"],
|
||||
env: bunEnv,
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
const buffer1 = await proc.stdout;
|
||||
const buffer2 = await proc.stdout;
|
||||
const buffer3 = await proc.stdout;
|
||||
|
||||
expect(buffer1).toBe(buffer2);
|
||||
expect(buffer2).toBe(buffer3);
|
||||
expect(buffer1.toString()).toBe("test\n");
|
||||
});
|
||||
|
||||
test("stdout promise is created lazily", async () => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", "console.log('lazy')"],
|
||||
env: bunEnv,
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
// Wait for process to complete
|
||||
await proc.exited;
|
||||
|
||||
// Now access stdout - should still work
|
||||
const buffer = await proc.stdout;
|
||||
expect(buffer.toString()).toBe("lazy\n");
|
||||
});
|
||||
});
|
||||
});
|
||||
51
test/regression/issue/spawn-buffer-text-modes.test.ts
Normal file
51
test/regression/issue/spawn-buffer-text-modes.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { spawn, spawnSync } from "bun";
|
||||
|
||||
// Regression test for stdout: "buffer" and stdout: "text" modes
|
||||
describe("spawn buffer and text modes", () => {
|
||||
test("stdout: 'buffer' returns a promise that resolves to a Buffer", async () => {
|
||||
const proc = spawn({
|
||||
cmd: ["echo", "hello buffer"],
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
expect(proc.stdout).toBeInstanceOf(Promise);
|
||||
const buffer = await proc.stdout;
|
||||
expect(buffer).toBeInstanceOf(Buffer);
|
||||
expect(buffer.toString()).toBe("hello buffer\n");
|
||||
});
|
||||
|
||||
test("stdout: 'text' returns a promise that resolves to a string", async () => {
|
||||
const proc = spawn({
|
||||
cmd: ["echo", "hello text"],
|
||||
stdout: "text",
|
||||
});
|
||||
|
||||
expect(proc.stdout).toBeInstanceOf(Promise);
|
||||
const text = await proc.stdout;
|
||||
expect(typeof text).toBe("string");
|
||||
expect(text).toBe("hello text\n");
|
||||
});
|
||||
|
||||
test("spawnSync with stdout: 'buffer' returns Buffer in result", () => {
|
||||
const result = spawnSync({
|
||||
cmd: ["echo", "sync buffer"],
|
||||
stdout: "buffer",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.stdout).toBeInstanceOf(Buffer);
|
||||
expect(result.stdout?.toString()).toBe("sync buffer\n");
|
||||
});
|
||||
|
||||
test("spawnSync with stdout: 'text' returns string in result", () => {
|
||||
const result = spawnSync({
|
||||
cmd: ["echo", "sync text"],
|
||||
stdout: "text",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(typeof result.stdout).toBe("string");
|
||||
expect(result.stdout).toBe("sync text\n");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user