From 660bb79228079944e74079a2203f878f3854a58f Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 9 Sep 2024 16:53:38 -0700 Subject: [PATCH 1/8] add sleep builtin --- src/shell/interpreter.zig | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 5c79f18655fee..20f809b8c9333 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -5271,6 +5271,7 @@ pub const Interpreter = struct { dirname: Dirname, basename: Basename, cp: Cp, + sleep: Sleep, }; const Result = @import("../result.zig").Result; @@ -5296,6 +5297,7 @@ pub const Interpreter = struct { dirname, basename, cp, + sleep, pub const DISABLED_ON_POSIX: []const Kind = &.{ .cat, .cp }; @@ -5324,6 +5326,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 +5505,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 +10501,88 @@ pub const Interpreter = struct { } }; + pub const Sleep = struct { + bltn: *Builtin, + state: enum { idle, waiting_io, err, done } = .idle, + + 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: f64 = 0; + while (iter.next()) |arg| { + const slice = bun.sliceTo(arg, 0); + + if (slice.len == 0 or slice[0] == '-') { + return this.fail("sleep: invalid time interval\n"); + } + + const seconds = bun.fmt.parseFloat(f64, slice) catch { + return this.fail("sleep: invalid time interval\n"); + }; + + if (std.math.isInf(seconds)) { + // if positive infinity is seen, set total to `-1`. + // continue iterating to catch invalid args + total = -1; + } else if (std.math.isNan(seconds)) { + return this.fail("sleep: invalid time interval\n"); + } + + if (total != -1) { + total += seconds; + } + } + + if (this.state == .err) { + return Maybe(void).success; + } + + if (total != 0) { + if (total == -1) { + std.time.sleep(std.math.maxInt(u64)); + } else { + std.time.sleep(@intFromFloat(@floor(total * @as(f64, std.time.ns_per_s)))); + } + } + + 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 const Cp = struct { bltn: *Builtin, opts: Opts = .{}, @@ -12280,6 +12366,7 @@ pub const IOWriterChildPtr = struct { Interpreter.Builtin.Basename, Interpreter.Builtin.Cp, Interpreter.Builtin.Cp.ShellCpOutputTask, + Interpreter.Builtin.Sleep, shell.subproc.PipeReader.CapturedWriter, }); From 9475661845270fa27077a68ca8af02f70f13be94 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 9 Sep 2024 17:25:54 -0700 Subject: [PATCH 2/8] trim leading spaces --- src/shell/interpreter.zig | 6 +++--- src/string_immutable.zig | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 20f809b8c9333..85839849106d4 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -10513,13 +10513,13 @@ pub const Interpreter = struct { var total: f64 = 0; while (iter.next()) |arg| { - const slice = bun.sliceTo(arg, 0); + const trimmed = bun.strings.trimLeft(bun.sliceTo(arg, 0), " "); - if (slice.len == 0 or slice[0] == '-') { + if (trimmed.len == 0 or trimmed[0] == '-') { return this.fail("sleep: invalid time interval\n"); } - const seconds = bun.fmt.parseFloat(f64, slice) catch { + const seconds = bun.fmt.parseFloat(f64, trimmed) catch { return this.fail("sleep: invalid time interval\n"); }; diff --git a/src/string_immutable.zig b/src/string_immutable.zig index db72ec1b5e2d0..e1290791905ec 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -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 { From c7158e277f8a0bcef350669bc5d1899acded1598 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 9 Sep 2024 17:26:11 -0700 Subject: [PATCH 3/8] some tests --- test/js/bun/shell/commands/sleep.test.ts | 38 ++++++++++++++++++++++++ test/js/bun/shell/test_builder.ts | 18 +++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 test/js/bun/shell/commands/sleep.test.ts diff --git a/test/js/bun/shell/commands/sleep.test.ts b/test/js/bun/shell/commands/sleep.test.ts new file mode 100644 index 0000000000000..14990aee896d2 --- /dev/null +++ b/test/js/bun/shell/commands/sleep.test.ts @@ -0,0 +1,38 @@ +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"); +}); diff --git a/test/js/bun/shell/test_builder.ts b/test/js/bun/shell/test_builder.ts index 1efefa45d8654..7728a0b5c90a1 100644 --- a/test/js/bun/shell/test_builder.ts +++ b/test/js/bun/shell/test_builder.ts @@ -26,6 +26,7 @@ export function createTestBuilder(path: string) { file_equals: { [filename: string]: string | (() => string | Promise) } = {}; _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 { + duration(ms: number): this { + this._expectedDuration = ms; + return this; + } + + async doChecks(stdout: Buffer, stderr: Buffer, exitCode: number, durationMs: number): Promise { const tempdir = this.tempdir || "NO_TEMP_DIR"; + if (this._expectedDuration !== undefined) { + expect(durationMs).toBeGreaterThanOrEqual(this._expectedDuration); + } 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; From e05f33a8b6b8a981ea8dd637ab4dedef9742edde Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Mon, 9 Sep 2024 18:09:05 -0700 Subject: [PATCH 4/8] unnecessary error check --- src/shell/interpreter.zig | 40 ++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 85839849106d4..88343038020da 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -10513,31 +10513,33 @@ pub const Interpreter = struct { var total: f64 = 0; while (iter.next()) |arg| { - const trimmed = bun.strings.trimLeft(bun.sliceTo(arg, 0), " "); + invalid_interval: { + const trimmed = bun.strings.trimLeft(bun.sliceTo(arg, 0), " "); - if (trimmed.len == 0 or trimmed[0] == '-') { - return this.fail("sleep: invalid time interval\n"); - } + if (trimmed.len == 0 or trimmed[0] == '-') { + break :invalid_interval; + } - const seconds = bun.fmt.parseFloat(f64, trimmed) catch { - return this.fail("sleep: invalid time interval\n"); - }; + const seconds = bun.fmt.parseFloat(f64, trimmed) catch { + break :invalid_interval; + }; - if (std.math.isInf(seconds)) { - // if positive infinity is seen, set total to `-1`. - // continue iterating to catch invalid args - total = -1; - } else if (std.math.isNan(seconds)) { - return this.fail("sleep: invalid time interval\n"); - } + if (std.math.isInf(seconds)) { + // if positive infinity is seen, set total to `-1`. + // continue iterating to catch invalid args + total = -1; + } else if (std.math.isNan(seconds)) { + break :invalid_interval; + } - if (total != -1) { - total += seconds; + if (total != -1) { + total += seconds; + } + + continue; } - } - if (this.state == .err) { - return Maybe(void).success; + return this.fail("sleep: invalid time interval\n"); } if (total != 0) { From e565076eee3f35f1706629e9d91bd07bb456e9f0 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 10 Sep 2024 14:51:42 -0700 Subject: [PATCH 5/8] non-blocking js event loop --- src/bun.js/api/Timer.zig | 6 ++++ src/shell/interpreter.zig | 67 ++++++++++++++++++++++++++++++++------- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/bun.js/api/Timer.zig b/src/bun.js/api/Timer.zig index d785a15e42d88..abdd31b5f56a1 100644 --- a/src/bun.js/api/Timer.zig +++ b/src/bun.js/api/Timer.zig @@ -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); }, } diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 88343038020da..689b99520ead7 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -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"); @@ -10503,7 +10504,12 @@ pub const Interpreter = struct { pub const Sleep = struct { bltn: *Builtin, - state: enum { idle, waiting_io, err, done } = .idle, + 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(); @@ -10511,7 +10517,7 @@ pub const Interpreter = struct { if (args.len == 0) return this.fail(Builtin.Kind.usageString(.sleep)); - var total: f64 = 0; + var total_seconds: f64 = 0; while (iter.next()) |arg| { invalid_interval: { const trimmed = bun.strings.trimLeft(bun.sliceTo(arg, 0), " "); @@ -10525,15 +10531,15 @@ pub const Interpreter = struct { }; if (std.math.isInf(seconds)) { - // if positive infinity is seen, set total to `-1`. + // if positive infinity is seen, set total seconds to `-1`. // continue iterating to catch invalid args - total = -1; + total_seconds = -1; } else if (std.math.isNan(seconds)) { break :invalid_interval; } - if (total != -1) { - total += seconds; + if (total_seconds != -1) { + total_seconds += seconds; } continue; @@ -10542,11 +10548,34 @@ pub const Interpreter = struct { return this.fail("sleep: invalid time interval\n"); } - if (total != 0) { - if (total == -1) { - std.time.sleep(std.math.maxInt(u64)); - } else { - std.time.sleep(@intFromFloat(@floor(total * @as(f64, std.time.ns_per_s)))); + 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); + }, } } @@ -10583,6 +10612,22 @@ pub const Interpreter = struct { 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 { From 1c3d6da9b28ff4050a75e392a587e77246769cb7 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 10 Sep 2024 15:57:22 -0700 Subject: [PATCH 6/8] parse seconds, minutes, hours, days --- src/shell/interpreter.zig | 32 +++++++++++++++++++++++++++++-- test/js/bun/shell/test_builder.ts | 2 +- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 689b99520ead7..0c0261eb3ddea 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -10520,16 +10520,44 @@ pub const Interpreter = struct { var total_seconds: f64 = 0; while (iter.next()) |arg| { invalid_interval: { - const trimmed = bun.strings.trimLeft(bun.sliceTo(arg, 0), " "); + var trimmed: string = bun.strings.trimLeft(bun.sliceTo(arg, 0), " "); if (trimmed.len == 0 or trimmed[0] == '-') { break :invalid_interval; } - const seconds = bun.fmt.parseFloat(f64, trimmed) catch { + 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 diff --git a/test/js/bun/shell/test_builder.ts b/test/js/bun/shell/test_builder.ts index 7728a0b5c90a1..8f1697b131b98 100644 --- a/test/js/bun/shell/test_builder.ts +++ b/test/js/bun/shell/test_builder.ts @@ -222,7 +222,7 @@ export function createTestBuilder(path: string) { async doChecks(stdout: Buffer, stderr: Buffer, exitCode: number, durationMs: number): Promise { const tempdir = this.tempdir || "NO_TEMP_DIR"; if (this._expectedDuration !== undefined) { - expect(durationMs).toBeGreaterThanOrEqual(this._expectedDuration); + expect(durationMs >= this._expectedDuration && durationMs <= this._expectedDuration + 200).toBeTrue(); } if (this.expected_stdout !== undefined) { if (typeof this.expected_stdout === "string") { From 3e38c151142fa7c078f3d04c939e74b8f306c47f Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 10 Sep 2024 16:17:07 -0700 Subject: [PATCH 7/8] more tests --- test/js/bun/shell/commands/sleep.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/js/bun/shell/commands/sleep.test.ts b/test/js/bun/shell/commands/sleep.test.ts index 14990aee896d2..0e76a827f5aa5 100644 --- a/test/js/bun/shell/commands/sleep.test.ts +++ b/test/js/bun/shell/commands/sleep.test.ts @@ -35,4 +35,9 @@ describe("sleep", async () => { .stderr("") .duration(1000) .runAsTest("sleeps for sum of arguments"); + + Builder.command`sleep 1s`.exitCode(0).stdout("").stderr("").duration(1000).runAsTest("sleeps for seconds"); + Builder.command`sleep 0.0167m`.exitCode(0).stdout("").stderr("").duration(1000).runAsTest("sleeps for minutes"); + Builder.command`sleep 0.00028h`.exitCode(0).stdout("").stderr("").duration(1000).runAsTest("sleeps for hours"); + Builder.command`sleep 0.0000116d`.exitCode(0).stdout("").stderr("").duration(1000).runAsTest("sleeps for days"); }); From b7128edd995cd507544a6426531dce9ed4285c35 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 10 Sep 2024 16:43:44 -0700 Subject: [PATCH 8/8] 2 less seconds --- test/js/bun/shell/commands/sleep.test.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/test/js/bun/shell/commands/sleep.test.ts b/test/js/bun/shell/commands/sleep.test.ts index 0e76a827f5aa5..95d4399c95007 100644 --- a/test/js/bun/shell/commands/sleep.test.ts +++ b/test/js/bun/shell/commands/sleep.test.ts @@ -36,8 +36,18 @@ describe("sleep", async () => { .duration(1000) .runAsTest("sleeps for sum of arguments"); - Builder.command`sleep 1s`.exitCode(0).stdout("").stderr("").duration(1000).runAsTest("sleeps for seconds"); - Builder.command`sleep 0.0167m`.exitCode(0).stdout("").stderr("").duration(1000).runAsTest("sleeps for minutes"); - Builder.command`sleep 0.00028h`.exitCode(0).stdout("").stderr("").duration(1000).runAsTest("sleeps for hours"); - Builder.command`sleep 0.0000116d`.exitCode(0).stdout("").stderr("").duration(1000).runAsTest("sleeps for days"); + 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"); });