Compare commits

...

9 Commits

Author SHA1 Message Date
Dylan Conway
b7128edd99 2 less seconds 2024-09-10 16:43:44 -07:00
Dylan Conway
3e38c15114 more tests 2024-09-10 16:17:07 -07:00
Dylan Conway
1c3d6da9b2 parse seconds, minutes, hours, days 2024-09-10 15:57:22 -07:00
Dylan Conway
e565076eee non-blocking js event loop 2024-09-10 14:51:42 -07:00
Dylan Conway
e05f33a8b6 unnecessary error check 2024-09-09 18:09:05 -07:00
Dylan Conway
bf6874791c Merge branch 'main' into dylan/windows-install-ci-fixes 2024-09-09 17:36:14 -07:00
Dylan Conway
c7158e277f some tests 2024-09-09 17:26:11 -07:00
Dylan Conway
9475661845 trim leading spaces 2024-09-09 17:25:54 -07:00
Dylan Conway
660bb79228 add sleep builtin 2024-09-09 16:53:38 -07:00
5 changed files with 250 additions and 5 deletions

View File

@@ -730,6 +730,7 @@ pub const EventLoopTimer = struct {
TestRunner,
StatWatcherScheduler,
UpgradedDuplex,
ShellSleepBuiltin,
pub fn Type(comptime T: Tag) type {
return switch (T) {
@@ -738,6 +739,7 @@ pub const EventLoopTimer = struct {
.TestRunner => JSC.Jest.TestRunner,
.StatWatcherScheduler => StatWatcherScheduler,
.UpgradedDuplex => uws.UpgradedDuplex,
.ShellSleepBuiltin => bun.shell.Interpreter.Builtin.Sleep,
};
}
};
@@ -807,6 +809,10 @@ pub const EventLoopTimer = struct {
return .disarm;
}
if (comptime t.Type() == bun.shell.Interpreter.Builtin.Sleep) {
return container.onSleepFinish();
}
return container.callback(container);
},
}

View File

@@ -42,6 +42,7 @@ const windows = bun.windows;
const uv = windows.libuv;
const Maybe = JSC.Maybe;
const WTFStringImplStruct = @import("../string.zig").WTFStringImplStruct;
const timespec = bun.timespec;
const Pipe = [2]bun.FileDescriptor;
const shell = @import("./shell.zig");
@@ -5271,6 +5272,7 @@ pub const Interpreter = struct {
dirname: Dirname,
basename: Basename,
cp: Cp,
sleep: Sleep,
};
const Result = @import("../result.zig").Result;
@@ -5296,6 +5298,7 @@ pub const Interpreter = struct {
dirname,
basename,
cp,
sleep,
pub const DISABLED_ON_POSIX: []const Kind = &.{ .cat, .cp };
@@ -5324,6 +5327,7 @@ pub const Interpreter = struct {
.dirname => "usage: dirname string\n",
.basename => "usage: basename string\n",
.cp => "usage: cp [-R [-H | -L | -P]] [-fi | -n] [-aclpsvXx] source_file target_file\n cp [-R [-H | -L | -P]] [-fi | -n] [-aclpsvXx] source_file ... target_directory\n",
.sleep => "usage: sleep seconds\n",
};
}
@@ -5502,6 +5506,7 @@ pub const Interpreter = struct {
.dirname => this.callImplWithType(Dirname, Ret, "dirname", field, args_),
.basename => this.callImplWithType(Basename, Ret, "basename", field, args_),
.cp => this.callImplWithType(Cp, Ret, "cp", field, args_),
.sleep => this.callImplWithType(Sleep, Ret, "sleep", field, args_),
};
}
@@ -10497,6 +10502,162 @@ pub const Interpreter = struct {
}
};
pub const Sleep = struct {
bltn: *Builtin,
state: enum { idle, err, done } = .idle,
event_loop_timer: JSC.BunTimer.EventLoopTimer = .{
.next = .{},
.tag = .ShellSleepBuiltin,
},
pub fn start(this: *Sleep) Maybe(void) {
const args = this.bltn.argsSlice();
var iter = bun.SliceIterator([*:0]const u8).init(args);
if (args.len == 0) return this.fail(Builtin.Kind.usageString(.sleep));
var total_seconds: f64 = 0;
while (iter.next()) |arg| {
invalid_interval: {
var trimmed: string = bun.strings.trimLeft(bun.sliceTo(arg, 0), " ");
if (trimmed.len == 0 or trimmed[0] == '-') {
break :invalid_interval;
}
const maybe_unit: ?enum {
seconds,
minutes,
hours,
days,
pub fn apply(unit: @This(), value: f64) f64 {
return switch (unit) {
.seconds => value,
.minutes => value * 60,
.hours => value * 60 * 60,
.days => value * 60 * 60 * 24,
};
}
} = switch (trimmed[trimmed.len - 1]) {
's' => .seconds,
'm' => .minutes,
'h' => .hours,
'd' => .days,
else => null,
};
if (maybe_unit != null) {
trimmed = trimmed[0 .. trimmed.len - 1];
}
const value = bun.fmt.parseFloat(f64, trimmed) catch {
break :invalid_interval;
};
const seconds = if (maybe_unit) |unit| unit.apply(value) else value;
if (std.math.isInf(seconds)) {
// if positive infinity is seen, set total seconds to `-1`.
// continue iterating to catch invalid args
total_seconds = -1;
} else if (std.math.isNan(seconds)) {
break :invalid_interval;
}
if (total_seconds != -1) {
total_seconds += seconds;
}
continue;
}
return this.fail("sleep: invalid time interval\n");
}
if (total_seconds != 0) {
switch (this.bltn.eventLoop()) {
.js => |js| {
const vm = js.getVmImpl();
const milliseconds: i64 = if (total_seconds == -1)
std.math.maxInt(i64)
else
@intFromFloat(@floor(total_seconds * @as(f64, std.time.ms_per_s)));
const sleep_time = timespec.msFromNow(milliseconds);
this.event_loop_timer.next = sleep_time;
this.event_loop_timer.tag = .ShellSleepBuiltin;
this.event_loop_timer.state = .ACTIVE;
vm.timer.insert(&this.event_loop_timer);
vm.timer.incrementTimerRef(1);
return Maybe(void).success;
},
.mini => |mini| {
_ = mini;
const sleep_time: u64 = if (total_seconds == -1)
std.math.maxInt(u64)
else
@intFromFloat(@floor(total_seconds * @as(f64, std.time.ns_per_s)));
std.time.sleep(sleep_time);
},
}
}
this.state = .done;
this.bltn.done(0);
return Maybe(void).success;
}
pub fn deinit(_: *Sleep) void {}
pub fn fail(this: *Sleep, msg: string) Maybe(void) {
if (this.bltn.stderr.needsIO()) |safeguard| {
this.state = .err;
this.bltn.stderr.enqueue(this, msg, safeguard);
return Maybe(void).success;
}
_ = this.bltn.writeNoIO(.stderr, msg);
this.bltn.done(1);
return Maybe(void).success;
}
pub fn onIOWriterChunk(this: *Sleep, _: usize, maybe_e: ?JSC.SystemError) void {
if (maybe_e) |e| {
defer e.deref();
this.state = .err;
this.bltn.done(1);
return;
}
if (this.state == .done) {
this.bltn.done(0);
}
if (this.state == .err) {
this.bltn.done(1);
}
}
pub fn onSleepFinish(this: *Sleep) JSC.BunTimer.EventLoopTimer.Arm {
switch (this.bltn.eventLoop()) {
.js => |js| {
const vm = js.getVmImpl();
this.event_loop_timer.state = .FIRED;
this.event_loop_timer.heap = .{};
this.bltn.done(0);
vm.timer.incrementTimerRef(-1);
},
.mini => |mini| {
_ = mini;
},
}
return .disarm;
}
};
pub const Cp = struct {
bltn: *Builtin,
opts: Opts = .{},
@@ -12280,6 +12441,7 @@ pub const IOWriterChildPtr = struct {
Interpreter.Builtin.Basename,
Interpreter.Builtin.Cp,
Interpreter.Builtin.Cp.ShellCpOutputTask,
Interpreter.Builtin.Sleep,
shell.subproc.PipeReader.CapturedWriter,
});

View File

@@ -4539,11 +4539,23 @@ pub fn trim(slice: anytype, comptime values_to_strip: []const u8) @TypeOf(slice)
var begin: usize = 0;
var end: usize = slice.len;
while (begin < end and std.mem.indexOfScalar(u8, values_to_strip, slice[begin]) != null) : (begin += 1) {}
while (end > begin and std.mem.indexOfScalar(u8, values_to_strip, slice[end - 1]) != null) : (end -= 1) {}
while (begin < end and indexOfChar(values_to_strip, slice[begin]) != null) : (begin += 1) {}
while (end > begin and indexOfChar(values_to_strip, slice[end - 1]) != null) : (end -= 1) {}
return slice[begin..end];
}
pub fn trimLeft(slice: anytype, comptime values_to_strip: []const u8) @TypeOf(slice) {
var begin: usize = 0;
while (begin < slice.len and indexOfChar(values_to_strip, slice[begin]) != null) : (begin += 1) {}
return slice[begin..];
}
pub fn trimRight(slice: anytype, comptime values_to_strip: []const u8) @TypeOf(slice) {
var end: usize = slice.len;
while (end > 0 and indexOfChar(values_to_strip, slice[end - 1]) != null) : (end -= 1) {}
return slice[0..end];
}
pub const whitespace_chars = [_]u8{ ' ', '\t', '\n', '\r', std.ascii.control_code.vt, std.ascii.control_code.ff };
pub fn lengthOfLeadingWhitespaceASCII(slice: string) usize {

View File

@@ -0,0 +1,53 @@
import { describe } from "bun:test";
import { createTestBuilder } from "../test_builder";
const Builder = createTestBuilder(import.meta.path);
describe("sleep", async () => {
Builder.command`sleep`.exitCode(1).stdout("").stderr("usage: sleep seconds\n").runAsTest("prints usage");
Builder.command`sleep -1`
.exitCode(1)
.stdout("")
.stderr("sleep: invalid time interval\n")
.runAsTest("errors on negative values");
Builder.command`sleep j`
.exitCode(1)
.stdout("")
.stderr("sleep: invalid time interval\n")
.runAsTest("errors on non-numeric values");
Builder.command`sleep 1 j`
.exitCode(1)
.stdout("")
.stderr("sleep: invalid time interval\n")
.runAsTest("errors on any invalid values");
Builder.command`sleep 1`.exitCode(0).stdout("").stderr("").duration(1000).runAsTest("sleep works");
Builder.command`sleep ' 0.5'`.exitCode(0).stdout("").stderr("").duration(500).runAsTest("trims leading spaces");
Builder.command`sleep '.5 '`
.exitCode(1)
.stdout("")
.stderr("sleep: invalid time interval\n")
.runAsTest("does not trim trailing spaces");
Builder.command`sleep .5 .5`
.exitCode(0)
.stdout("")
.stderr("")
.duration(1000)
.runAsTest("sleeps for sum of arguments");
Builder.command`sleep .5s`.exitCode(0).stdout("").stderr("").duration(500).runAsTest("sleeps for seconds");
Builder.command`sleep ${1 / 60 / 2}m`.exitCode(0).stdout("").stderr("").duration(500).runAsTest("sleeps for minutes");
Builder.command`sleep ${1 / 60 / 60 / 2}h`
.exitCode(0)
.stdout("")
.stderr("")
.duration(500)
.runAsTest("sleeps for hours");
Builder.command`sleep ${1 / 60 / 60 / 24 / 2}d`
.exitCode(0)
.stdout("")
.stderr("")
.duration(500)
.runAsTest("sleeps for days");
});

View File

@@ -26,6 +26,7 @@ export function createTestBuilder(path: string) {
file_equals: { [filename: string]: string | (() => string | Promise<string>) } = {};
_doesNotExist: string[] = [];
_timeout: number | undefined = undefined;
_expectedDuration: number | undefined = undefined;
tempdir: string | undefined = undefined;
_env: { [key: string]: string } | undefined = undefined;
@@ -213,8 +214,16 @@ export function createTestBuilder(path: string) {
return this;
}
async doChecks(stdout: Buffer, stderr: Buffer, exitCode: number): Promise<void> {
duration(ms: number): this {
this._expectedDuration = ms;
return this;
}
async doChecks(stdout: Buffer, stderr: Buffer, exitCode: number, durationMs: number): Promise<void> {
const tempdir = this.tempdir || "NO_TEMP_DIR";
if (this._expectedDuration !== undefined) {
expect(durationMs >= this._expectedDuration && durationMs <= this._expectedDuration + 200).toBeTrue();
}
if (this.expected_stdout !== undefined) {
if (typeof this.expected_stdout === "string") {
expect(stdout.toString()).toEqual(this.expected_stdout.replaceAll("$TEMP_DIR", tempdir));
@@ -255,6 +264,7 @@ export function createTestBuilder(path: string) {
return Promise.resolve(undefined);
}
const startTime = performance.now();
try {
let finalPromise = Bun.$(this._scriptStr, ...this._expresssions);
if (this.tempdir) finalPromise = finalPromise.cwd(this.tempdir);
@@ -262,17 +272,19 @@ export function createTestBuilder(path: string) {
if (this._env) finalPromise = finalPromise.env(this._env);
if (this._quiet) finalPromise = finalPromise.quiet();
const output = await finalPromise;
const endTime = performance.now();
const { stdout, stderr, exitCode } = output;
await this.doChecks(stdout, stderr, exitCode);
await this.doChecks(stdout, stderr, exitCode, endTime - startTime);
} catch (err_) {
const endTime = performance.now();
const err: ShellError = err_ as any;
const { stdout, stderr, exitCode } = err;
if (this.expected_error === undefined) {
if (stdout === undefined || stderr === undefined || exitCode === undefined) {
throw err_;
}
this.doChecks(stdout, stderr, exitCode);
this.doChecks(stdout, stderr, exitCode, endTime - startTime);
return;
}
if (this.expected_error === true) return undefined;