mirror of
https://github.com/oven-sh/bun
synced 2026-02-05 00:18:53 +00:00
Compare commits
23 Commits
ciro/fix-a
...
kai/readli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6608a56960 | ||
|
|
67aae542ad | ||
|
|
a76def9d93 | ||
|
|
f2462b17b3 | ||
|
|
ed7644709c | ||
|
|
fe03672278 | ||
|
|
af493f9eeb | ||
|
|
7c9548afac | ||
|
|
6e3219b9f9 | ||
|
|
622d18ea31 | ||
|
|
952154bb80 | ||
|
|
ff584f8eea | ||
|
|
bda25a04e8 | ||
|
|
a82293bb43 | ||
|
|
ba557c81aa | ||
|
|
671f8a9fb7 | ||
|
|
b51d01af6f | ||
|
|
f47277988f | ||
|
|
ac75182fcd | ||
|
|
4b63922335 | ||
|
|
eccf42a2e4 | ||
|
|
c5e8f93a68 | ||
|
|
dea30a047a |
@@ -29,7 +29,7 @@ for x in $(git ls-files test/js/node/test/parallel --exclude-standard --others |
|
||||
do
|
||||
i=$((i+1))
|
||||
echo ./$x
|
||||
if timeout 2 $PWD/build/debug/bun-debug ./$x
|
||||
if timeout --foreground 2 "$PWD/build/debug/bun-debug" ./$x
|
||||
then
|
||||
j=$((j+1))
|
||||
git add ./$x
|
||||
|
||||
@@ -2571,6 +2571,12 @@ pub const VirtualMachine = struct {
|
||||
} else {
|
||||
return error.ModuleNotFound;
|
||||
}
|
||||
} else if (ModuleLoader.is_allowed_to_use_internal_testing_apis) {
|
||||
if (JSC.internal_modules.get(specifier) != null) {
|
||||
ret.result = null;
|
||||
ret.path = specifier;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const is_special_source = strings.eqlComptime(source, main_file_name) or js_ast.Macro.isMacroPath(source);
|
||||
|
||||
@@ -89,7 +89,7 @@ const String = bun.String;
|
||||
const debug = Output.scoped(.ModuleLoader, true);
|
||||
const panic = std.debug.panic;
|
||||
|
||||
inline fn jsSyntheticModule(comptime name: ResolvedSource.Tag, specifier: String) ResolvedSource {
|
||||
inline fn jsSyntheticModule(name: ResolvedSource.Tag, specifier: String) ResolvedSource {
|
||||
return ResolvedSource{
|
||||
.allocator = null,
|
||||
.source_code = bun.String.empty,
|
||||
@@ -223,7 +223,7 @@ pub const RuntimeTranspilerStore = struct {
|
||||
};
|
||||
}
|
||||
|
||||
// Thsi is run at the top of the event loop on the JS thread.
|
||||
// This is run at the top of the event loop on the JS thread.
|
||||
pub fn drain(this: *RuntimeTranspilerStore) void {
|
||||
var batch = this.queue.popBatch();
|
||||
var iter = batch.iterator();
|
||||
@@ -2476,15 +2476,6 @@ pub const ModuleLoader = struct {
|
||||
return jsSyntheticModule(.InternalForTesting, specifier);
|
||||
},
|
||||
|
||||
.@"internal/test/binding" => {
|
||||
if (!Environment.isDebug) {
|
||||
if (!is_allowed_to_use_internal_testing_apis)
|
||||
return null;
|
||||
}
|
||||
|
||||
return jsSyntheticModule(.@"internal:test/binding", specifier);
|
||||
},
|
||||
|
||||
// These are defined in src/js/*
|
||||
.@"bun:ffi" => return jsSyntheticModule(.@"bun:ffi", specifier),
|
||||
.@"bun:sql" => {
|
||||
@@ -2598,6 +2589,10 @@ pub const ModuleLoader = struct {
|
||||
.is_commonjs_module = file.module_format == .cjs,
|
||||
};
|
||||
}
|
||||
} else if (is_allowed_to_use_internal_testing_apis) {
|
||||
if (internal_modules.getWithEql(specifier, bun.String.eqlComptime)) |tag| {
|
||||
return jsSyntheticModule(tag, specifier);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -2689,6 +2684,23 @@ pub const FetchFlags = enum {
|
||||
|
||||
const SavedSourceMap = JSC.SavedSourceMap;
|
||||
|
||||
pub const internal_modules = brk: {
|
||||
const all_modules = std.enums.values(ResolvedSource.Tag);
|
||||
var internal_module_entries: std.BoundedArray(
|
||||
struct { []const u8, ResolvedSource.Tag },
|
||||
all_modules.len,
|
||||
) = .{};
|
||||
for (all_modules) |module| {
|
||||
if (std.mem.startsWith(u8, @tagName(module), "internal:")) {
|
||||
internal_module_entries.appendAssumeCapacity(.{
|
||||
"internal/" ++ @tagName(module)["internal:".len..],
|
||||
module,
|
||||
});
|
||||
}
|
||||
}
|
||||
break :brk bun.ComptimeStringMap(ResolvedSource.Tag, internal_module_entries.slice());
|
||||
};
|
||||
|
||||
pub const HardcodedModule = enum {
|
||||
bun,
|
||||
@"abort-controller",
|
||||
@@ -2760,7 +2772,6 @@ pub const HardcodedModule = enum {
|
||||
@"node:cluster",
|
||||
// these are gated behind '--expose-internals'
|
||||
@"bun:internal-for-testing",
|
||||
@"internal/test/binding",
|
||||
|
||||
/// Already resolved modules go in here.
|
||||
/// This does not remap the module name, it is just a hash table.
|
||||
@@ -2840,8 +2851,6 @@ pub const HardcodedModule = enum {
|
||||
.{ "@vercel/fetch", HardcodedModule.@"@vercel/fetch" },
|
||||
.{ "utf-8-validate", HardcodedModule.@"utf-8-validate" },
|
||||
.{ "abort-controller", HardcodedModule.@"abort-controller" },
|
||||
|
||||
.{ "internal/test/binding", HardcodedModule.@"internal/test/binding" },
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -67,26 +67,17 @@ export function getStdinStream(fd) {
|
||||
const native = Bun.stdin.stream();
|
||||
|
||||
var reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
||||
var readerRef;
|
||||
|
||||
var shouldUnref = false;
|
||||
|
||||
function ref() {
|
||||
$debug("ref();", reader ? "already has reader" : "getting reader");
|
||||
reader ??= native.getReader();
|
||||
// TODO: remove this. likely we are dereferencing the stream
|
||||
// when there is still more data to be read.
|
||||
readerRef ??= setInterval(() => {}, 1 << 30);
|
||||
shouldUnref = false;
|
||||
}
|
||||
|
||||
function unref() {
|
||||
$debug("unref();");
|
||||
if (readerRef) {
|
||||
clearInterval(readerRef);
|
||||
readerRef = undefined;
|
||||
$debug("cleared timeout");
|
||||
}
|
||||
if (reader) {
|
||||
try {
|
||||
reader.releaseLock();
|
||||
@@ -144,21 +135,6 @@ export function getStdinStream(fd) {
|
||||
|
||||
stream.fd = fd;
|
||||
|
||||
const originalPause = stream.pause;
|
||||
stream.pause = function () {
|
||||
$debug("pause();");
|
||||
let r = originalPause.$call(this);
|
||||
unref();
|
||||
return r;
|
||||
};
|
||||
|
||||
const originalResume = stream.resume;
|
||||
stream.resume = function () {
|
||||
$debug("resume();");
|
||||
ref();
|
||||
return originalResume.$call(this);
|
||||
};
|
||||
|
||||
async function internalRead(stream) {
|
||||
$debug("internalRead();");
|
||||
try {
|
||||
@@ -214,7 +190,6 @@ export function getStdinStream(fd) {
|
||||
|
||||
stream.on("resume", () => {
|
||||
$debug('on("resume");');
|
||||
ref();
|
||||
stream._undestroy();
|
||||
stream_destroyed = false;
|
||||
});
|
||||
@@ -222,6 +197,7 @@ export function getStdinStream(fd) {
|
||||
stream._readableState.reading = false;
|
||||
|
||||
stream.on("pause", () => {
|
||||
$debug('on("pause");');
|
||||
process.nextTick(() => {
|
||||
if (!stream.readableFlowing) {
|
||||
stream._readableState.reading = false;
|
||||
@@ -230,6 +206,7 @@ export function getStdinStream(fd) {
|
||||
});
|
||||
|
||||
stream.on("close", () => {
|
||||
$debug('on("close");');
|
||||
if (!stream_destroyed) {
|
||||
stream_destroyed = true;
|
||||
process.nextTick(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// TODO: Use native code and JSC intrinsics for everything in this file.
|
||||
// Do not use this file for new code, many things here will be slow especailly when intrinsics for these operations is available.
|
||||
// Do not use this file for new code, many things here will be slow especially when intrinsics for these operations is available.
|
||||
// It is primarily used for `internal/util`
|
||||
|
||||
const createSafeIterator = (factory, next) => {
|
||||
|
||||
18
src/js/internal/readline/utils.ts
Normal file
18
src/js/internal/readline/utils.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
const kEscape = "\x1b";
|
||||
|
||||
function CSI(strings, ...args) {
|
||||
var ret = `${kEscape}[`;
|
||||
for (var n = 0; n < strings.length; n++) {
|
||||
ret += strings[n];
|
||||
if (n < args.length) ret += args[n];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
CSI.kEscape = kEscape;
|
||||
CSI.kClearLine = CSI`2K`;
|
||||
CSI.kClearScreenDown = CSI`0J`;
|
||||
CSI.kClearToLineBeginning = CSI`1K`;
|
||||
CSI.kClearToLineEnd = CSI`0K`;
|
||||
|
||||
export default { CSI };
|
||||
@@ -1,3 +1,5 @@
|
||||
process.emitWarning("These APIs are for internal testing only. Do not use them.", "internal/test/binding");
|
||||
|
||||
function internalBinding(name: string) {
|
||||
switch (name) {
|
||||
case "async_wrap":
|
||||
|
||||
@@ -2699,6 +2699,7 @@ export default {
|
||||
format,
|
||||
formatWithOptions,
|
||||
stripVTControlCharacters,
|
||||
getStringWidth,
|
||||
//! non-standard properties, should these be kept? (not currently exposed)
|
||||
//stylizeWithColor,
|
||||
//stylizeWithHTML(str, styleType) {
|
||||
|
||||
@@ -742,6 +742,7 @@ const NativeReadable = Stream._getNativeReadableStreamPrototype(2, Stream.Readab
|
||||
const NativeReadablePrototype = NativeReadable.prototype;
|
||||
const kFs = Symbol("kFs");
|
||||
const kHandle = Symbol("kHandle");
|
||||
const kDeferredError = Symbol("kDeferredError");
|
||||
|
||||
const kinternalRead = Symbol("kinternalRead");
|
||||
const kerrorOrDestroy = Symbol("kerrorOrDestroy");
|
||||
@@ -825,12 +826,13 @@ function ReadStream(this: typeof ReadStream, pathOrFd, options) {
|
||||
// If fd not open for this file, open it
|
||||
if (this.fd == null) {
|
||||
// NOTE: this fs is local to constructor, from options
|
||||
this.fd = overridden_fs.openSync(pathOrFd, flags, mode);
|
||||
try {
|
||||
this.fd = overridden_fs.openSync(pathOrFd, flags, mode);
|
||||
} catch (e) {
|
||||
this[kDeferredError] = e;
|
||||
}
|
||||
}
|
||||
|
||||
// Get FileRef from fd
|
||||
var fileRef = Bun.file(this.fd);
|
||||
|
||||
// Get the stream controller
|
||||
// We need the pointer to the underlying stream controller for the NativeReadable
|
||||
if (start !== undefined) {
|
||||
@@ -845,13 +847,20 @@ function ReadStream(this: typeof ReadStream, pathOrFd, options) {
|
||||
}
|
||||
}
|
||||
|
||||
const stream = blobToStreamWithOffset.$apply(fileRef, [start]);
|
||||
var ptr = stream.$bunNativePtr;
|
||||
if (!ptr) {
|
||||
throw new Error("Failed to get internal stream controller. This is a bug in Bun");
|
||||
}
|
||||
if (this.fd != null) {
|
||||
// Get FileRef from fd
|
||||
var fileRef = Bun.file(this.fd);
|
||||
|
||||
NativeReadable.$apply(this, [ptr, options]);
|
||||
const stream = blobToStreamWithOffset.$apply(fileRef, [start]);
|
||||
var ptr = stream.$bunNativePtr;
|
||||
if (!ptr) {
|
||||
throw new Error("Failed to get internal stream controller. This is a bug in Bun");
|
||||
}
|
||||
|
||||
NativeReadable.$apply(this, [ptr, options]);
|
||||
} else {
|
||||
NativeReadable.$apply(this, [null, options]);
|
||||
}
|
||||
|
||||
this[kHandle] = handle;
|
||||
this.end = end;
|
||||
@@ -885,8 +894,14 @@ ReadStream.prototype._construct = function (callback) {
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
this.emit("open", this.fd);
|
||||
this.emit("ready");
|
||||
|
||||
if (this[kDeferredError]) {
|
||||
this.emit("error", this[kDeferredError]);
|
||||
delete this[kDeferredError];
|
||||
} else {
|
||||
this.emit("open", this.fd);
|
||||
this.emit("ready");
|
||||
}
|
||||
};
|
||||
|
||||
ReadStream.prototype._destroy = function (err, cb) {
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
// ----------------------------------------------------------------------------
|
||||
const EventEmitter = require("node:events");
|
||||
const { StringDecoder } = require("node:string_decoder");
|
||||
const { CSI } = require("internal/readline/utils");
|
||||
const { kClearLine, kClearScreenDown, kClearToLineBeginning, kClearToLineEnd } = CSI;
|
||||
|
||||
const {
|
||||
validateFunction,
|
||||
@@ -36,6 +38,7 @@ const {
|
||||
validateBoolean,
|
||||
validateInteger,
|
||||
validateUint32,
|
||||
validateNumber,
|
||||
} = require("internal/validators");
|
||||
|
||||
const internalGetStringWidth = $newZigFunction("string.zig", "String.jsGetStringWidth", 1);
|
||||
@@ -43,7 +46,7 @@ const internalGetStringWidth = $newZigFunction("string.zig", "String.jsGetString
|
||||
const ObjectGetPrototypeOf = Object.getPrototypeOf;
|
||||
const ObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors;
|
||||
const ObjectValues = Object.values;
|
||||
const PromiseReject = Promise.reject;
|
||||
const PromiseReject = Promise.reject.bind(Promise);
|
||||
|
||||
var isWritable;
|
||||
|
||||
@@ -302,11 +305,17 @@ class ERR_USE_AFTER_CLOSE extends NodeError {
|
||||
}
|
||||
}
|
||||
|
||||
// Node uses an AbortError that isn't exactly the same as the DOMException
|
||||
// to make usage of the error in userland and readable-stream easier.
|
||||
// It is a regular error with `.code` and `.name`.
|
||||
class AbortError extends Error {
|
||||
code;
|
||||
constructor() {
|
||||
super("The operation was aborted");
|
||||
constructor(message = "The operation was aborted", options = undefined) {
|
||||
if (options !== undefined && typeof options !== "object") {
|
||||
throw $ERR_INVALID_ARG_TYPE("options", "Object", options);
|
||||
}
|
||||
super(message, options);
|
||||
this.code = "ABORT_ERR";
|
||||
this.name = "AbortError";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,23 +323,6 @@ class AbortError extends Error {
|
||||
// Section: Utils
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function CSI(strings, ...args) {
|
||||
var ret = `${kEscape}[`;
|
||||
for (var n = 0; n < strings.length; n++) {
|
||||
ret += strings[n];
|
||||
if (n < args.length) ret += args[n];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
var kClearLine, kClearScreenDown, kClearToLineBeginning, kClearToLineEnd;
|
||||
|
||||
CSI.kEscape = kEscape;
|
||||
CSI.kClearLine = kClearLine = CSI`2K`;
|
||||
CSI.kClearScreenDown = kClearScreenDown = CSI`0J`;
|
||||
CSI.kClearToLineBeginning = kClearToLineBeginning = CSI`1K`;
|
||||
CSI.kClearToLineEnd = kClearToLineEnd = CSI`0K`;
|
||||
|
||||
function charLengthLeft(str: string, i: number) {
|
||||
if (i <= 0) return 0;
|
||||
if (
|
||||
@@ -387,6 +379,7 @@ function* emitKeys(stream) {
|
||||
var keyCtrl = false;
|
||||
var keyMeta = false;
|
||||
var keyShift = false;
|
||||
var keyCode = {};
|
||||
|
||||
// var key = {
|
||||
// sequence: null,
|
||||
@@ -776,6 +769,8 @@ function* emitKeys(stream) {
|
||||
keyName = "undefined";
|
||||
break;
|
||||
}
|
||||
|
||||
keyCode.code = code;
|
||||
} else if (ch === "\r") {
|
||||
// carriage return
|
||||
keyName = "return";
|
||||
@@ -829,6 +824,7 @@ function* emitKeys(stream) {
|
||||
ctrl: keyCtrl,
|
||||
meta: keyMeta,
|
||||
shift: keyShift,
|
||||
...keyCode,
|
||||
});
|
||||
} else if (charLengthAt(s, 0) === s.length) {
|
||||
/* Single unnamed character, e.g. "." */
|
||||
@@ -838,6 +834,7 @@ function* emitKeys(stream) {
|
||||
ctrl: keyCtrl,
|
||||
meta: keyMeta,
|
||||
shift: keyShift,
|
||||
...keyCode,
|
||||
});
|
||||
}
|
||||
/* Unrecognized or broken escape sequence, don't emit anything */
|
||||
@@ -1312,9 +1309,7 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
||||
historySize = kHistorySize;
|
||||
}
|
||||
|
||||
if (typeof historySize !== "number" || NumberIsNaN(historySize) || historySize < 0) {
|
||||
throw new ERR_INVALID_ARG_VALUE("historySize", historySize);
|
||||
}
|
||||
validateNumber(historySize, "historySize", 0);
|
||||
|
||||
// Backwards compat; check the isTTY prop of the output stream
|
||||
// when `terminal` was not specified
|
||||
@@ -1425,8 +1420,7 @@ var _Interface = class Interface extends InterfaceConstructor {
|
||||
return this[kPrompt];
|
||||
}
|
||||
|
||||
[kSetRawMode](flag) {
|
||||
const mode = flag + 0;
|
||||
[kSetRawMode](mode) {
|
||||
const wasInRawMode = this.input.isRaw;
|
||||
|
||||
var setRawMode = this.input.setRawMode;
|
||||
@@ -1647,6 +1641,7 @@ var _Interface = class Interface extends InterfaceConstructor {
|
||||
if (this[kLine_buffer]) {
|
||||
string = this[kLine_buffer] + string;
|
||||
this[kLine_buffer] = null;
|
||||
lineEnding.lastIndex = 0; // Start the search from the beginning of the string.
|
||||
newPartContainsEnding = RegExpPrototypeExec.$call(lineEnding, string);
|
||||
}
|
||||
this[kSawReturnAt] = StringPrototypeEndsWith.$call(string, "\r") ? DateNow() : 0;
|
||||
@@ -1739,7 +1734,7 @@ var _Interface = class Interface extends InterfaceConstructor {
|
||||
|
||||
// Apply/show completions.
|
||||
var completionsWidth = ArrayPrototypeMap.$call(completions, e => getStringWidth(e));
|
||||
var width = MathMax.$apply(completionsWidth) + 2; // 2 space padding
|
||||
var width = MathMax.$apply(null, completionsWidth) + 2; // 2 space padding
|
||||
var maxColumns = MathFloor(this.columns / width) || 1;
|
||||
if (maxColumns === Infinity) {
|
||||
maxColumns = 1;
|
||||
@@ -2315,19 +2310,21 @@ var _Interface = class Interface extends InterfaceConstructor {
|
||||
// falls through
|
||||
default:
|
||||
if (typeof s === "string" && s) {
|
||||
var nextMatch = RegExpPrototypeExec.$call(lineEnding, s);
|
||||
if (nextMatch !== null) {
|
||||
// Erase state of previous searches.
|
||||
lineEnding.lastIndex = 0;
|
||||
var nextMatch;
|
||||
// Keep track of the end of the last match.
|
||||
var lastIndex = 0;
|
||||
while ((nextMatch = RegExpPrototypeExec.$call(lineEnding, s)) !== null) {
|
||||
this[kInsertString](StringPrototypeSlice.$call(s, 0, nextMatch.index));
|
||||
var { lastIndex } = lineEnding;
|
||||
while ((nextMatch = RegExpPrototypeExec.$call(lineEnding, s)) !== null) {
|
||||
this[kLine]();
|
||||
this[kInsertString](StringPrototypeSlice.$call(s, lastIndex, nextMatch.index));
|
||||
({ lastIndex } = lineEnding);
|
||||
}
|
||||
if (lastIndex === s.length) this[kLine]();
|
||||
} else {
|
||||
this[kInsertString](s);
|
||||
({ lastIndex } = lineEnding);
|
||||
this[kLine]();
|
||||
// Restore lastIndex as the call to kLine could have mutated it.
|
||||
lineEnding.lastIndex = lastIndex;
|
||||
}
|
||||
// This ensures that the last line is written if it doesn't end in a newline.
|
||||
// Note that the last line may be the first line, in which case this still works.
|
||||
this[kInsertString](StringPrototypeSlice.$call(s, lastIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2901,21 +2898,21 @@ var PromisesInterface = class Interface extends _Interface {
|
||||
return PromiseReject(new AbortError(undefined, { cause: signal.reason }));
|
||||
}
|
||||
}
|
||||
const { promise, resolve, reject } = $newPromiseCapability(Promise);
|
||||
var cb = resolve;
|
||||
if (options?.signal) {
|
||||
var onAbort = () => {
|
||||
this[kQuestionCancel]();
|
||||
reject(new AbortError(undefined, { cause: signal.reason }));
|
||||
};
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
cb = answer => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve(answer);
|
||||
};
|
||||
}
|
||||
this[kQuestion](query, cb);
|
||||
return promise;
|
||||
return new Promise((resolve, reject) => {
|
||||
var cb = resolve;
|
||||
if (options?.signal) {
|
||||
var onAbort = () => {
|
||||
this[kQuestionCancel]();
|
||||
reject(new AbortError(undefined, { cause: signal.reason }));
|
||||
};
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
cb = answer => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve(answer);
|
||||
};
|
||||
}
|
||||
this[kQuestion](query, cb);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3108,7 +3108,7 @@ var require_readable = __commonJS({
|
||||
state.flowing = !state.readableListening;
|
||||
resume(this, state);
|
||||
}
|
||||
state[kPaused] = false;
|
||||
state.paused = false;
|
||||
return this;
|
||||
};
|
||||
function resume(stream, state) {
|
||||
@@ -5444,8 +5444,10 @@ function createNativeStreamReadable(Readable) {
|
||||
this[constructed] = false;
|
||||
this[remainingChunk] = undefined;
|
||||
this[pendingRead] = false;
|
||||
ptr.onClose = this[_onClose].bind(this);
|
||||
ptr.onDrain = this[_onDrain].bind(this);
|
||||
if (ptr) {
|
||||
ptr.onClose = this[_onClose].bind(this);
|
||||
ptr.onDrain = this[_onDrain].bind(this);
|
||||
}
|
||||
}
|
||||
$toClass(NativeReadable, "NativeReadable", Readable);
|
||||
|
||||
|
||||
@@ -5998,7 +5998,7 @@ pub fn isFullWidthCodepointType(comptime T: type, cp: T) bool {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isAmgiguousCodepointType(comptime T: type, cp: T) bool {
|
||||
pub fn isAmbiguousCodepointType(comptime T: type, cp: T) bool {
|
||||
return switch (cp) {
|
||||
0xA1,
|
||||
0xA4,
|
||||
@@ -6240,7 +6240,7 @@ pub fn visibleCodepointWidthType(comptime T: type, cp: T, ambiguousAsWide: bool)
|
||||
if (isFullWidthCodepointType(T, cp)) {
|
||||
return 2;
|
||||
}
|
||||
if (ambiguousAsWide and isAmgiguousCodepointType(T, cp)) {
|
||||
if (ambiguousAsWide and isAmbiguousCodepointType(T, cp)) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -483,7 +483,7 @@ describe("readline.Interface", () => {
|
||||
});
|
||||
|
||||
it("should throw if historySize is not a positive number", () => {
|
||||
["not a number", -1, NaN, {}, true, Symbol(), null].forEach(historySize => {
|
||||
["not a number", {}, true, Symbol(), null].forEach(historySize => {
|
||||
assert.throws(
|
||||
() => {
|
||||
readline.createInterface({
|
||||
@@ -492,10 +492,22 @@ describe("readline.Interface", () => {
|
||||
});
|
||||
},
|
||||
{
|
||||
// TODO: Revert to Range error when properly implemented errors with multiple bases
|
||||
// name: "RangeError",
|
||||
name: "TypeError",
|
||||
code: "ERR_INVALID_ARG_VALUE",
|
||||
code: "ERR_INVALID_ARG_TYPE",
|
||||
},
|
||||
);
|
||||
});
|
||||
[-1, NaN].forEach(historySize => {
|
||||
assert.throws(
|
||||
() => {
|
||||
readline.createInterface({
|
||||
input,
|
||||
historySize,
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "RangeError",
|
||||
code: "ERR_OUT_OF_RANGE",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { spawn } from "child_process";
|
||||
|
||||
const localDir = path.resolve(import.meta.dirname, "./parallel");
|
||||
const upstreamDir = path.resolve(import.meta.dirname, "../../../node.js/upstream/test/parallel");
|
||||
|
||||
const localFiles = fs.readdirSync(localDir);
|
||||
const upstreamFiles = fs.readdirSync(upstreamDir);
|
||||
|
||||
const newFiles = upstreamFiles.filter((file) => !localFiles.includes(file));
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log("SIGTERM received");
|
||||
});
|
||||
process.on('SIGINT', () => {
|
||||
console.log("SIGINT received");
|
||||
});
|
||||
|
||||
const stdin = process.stdin;
|
||||
if (stdin.isTTY) {
|
||||
stdin.setRawMode(true);
|
||||
stdin.on('data', (data) => {
|
||||
if (data[0] === 0x03) {
|
||||
stdin.setRawMode(false);
|
||||
console.log("Cancelled");
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
process.on('exit', () => {
|
||||
if (stdin.isTTY) {
|
||||
stdin.setRawMode(false);
|
||||
}
|
||||
});
|
||||
|
||||
for (const file of newFiles) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// Run with a timeout of 5 seconds
|
||||
const proc = spawn("bun-debug", ["run", path.join(upstreamDir, file)], {
|
||||
timeout: 5000,
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_DEBUG_QUIET_LOGS: "1",
|
||||
},
|
||||
});
|
||||
|
||||
proc.on("error", (err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
proc.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
console.log(`New Pass: ${file}`);
|
||||
fs.appendFileSync("new-passes.txt", file + "\n");
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const { Readable } = require('stream');
|
||||
const readline = require('readline');
|
||||
|
||||
const CONTENT = 'content';
|
||||
const LINES_PER_PUSH = 2051;
|
||||
const REPETITIONS = 3;
|
||||
|
||||
(async () => {
|
||||
const readable = new Readable({ read() {} });
|
||||
let salt = 0;
|
||||
for (let i = 0; i < REPETITIONS; i++) {
|
||||
readable.push(`${CONTENT}\n`.repeat(LINES_PER_PUSH + i));
|
||||
salt += i;
|
||||
}
|
||||
const TOTAL_LINES = LINES_PER_PUSH * REPETITIONS + salt;
|
||||
|
||||
const rli = readline.createInterface({
|
||||
input: readable,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
const it = rli[Symbol.asyncIterator]();
|
||||
const watermarkData = it[Symbol.for('nodejs.watermarkData')];
|
||||
const highWaterMark = watermarkData.high;
|
||||
|
||||
// For this test to work, we have to queue up more than the number of
|
||||
// highWaterMark items in rli. Make sure that is the case.
|
||||
assert(TOTAL_LINES > highWaterMark, `TOTAL_LINES (${TOTAL_LINES}) isn't greater than highWaterMark (${highWaterMark})`);
|
||||
|
||||
let iterations = 0;
|
||||
let readableEnded = false;
|
||||
let notPaused = 0;
|
||||
for await (const line of it) {
|
||||
assert.strictEqual(readableEnded, false);
|
||||
assert.strictEqual(line, CONTENT);
|
||||
assert.ok(watermarkData.size <= TOTAL_LINES);
|
||||
assert.strictEqual(readable.isPaused(), watermarkData.size >= 1);
|
||||
if (!readable.isPaused()) {
|
||||
notPaused++;
|
||||
}
|
||||
|
||||
iterations += 1;
|
||||
|
||||
// We have to end the input stream asynchronously for back pressure to work.
|
||||
// Only end when we have reached the final line.
|
||||
if (iterations === TOTAL_LINES) {
|
||||
readable.push(null);
|
||||
readableEnded = true;
|
||||
}
|
||||
}
|
||||
|
||||
assert.strictEqual(iterations, TOTAL_LINES);
|
||||
assert.strictEqual(notPaused, REPETITIONS);
|
||||
})().then(common.mustCall());
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
|
||||
const assert = require('node:assert');
|
||||
const readline = require('node:readline');
|
||||
const { Readable } = require('node:stream');
|
||||
|
||||
|
||||
const input = Readable.from((function*() {
|
||||
yield 'a\nb';
|
||||
yield '\r\n';
|
||||
})());
|
||||
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
||||
let carriageReturns = 0;
|
||||
|
||||
rl.on('line', (line) => {
|
||||
if (line.includes('\r')) carriageReturns++;
|
||||
});
|
||||
|
||||
rl.on('close', common.mustCall(() => {
|
||||
assert.strictEqual(carriageReturns, 0);
|
||||
}));
|
||||
176
test/js/node/test/parallel/test-readline-csi.js
Normal file
176
test/js/node/test/parallel/test-readline-csi.js
Normal file
@@ -0,0 +1,176 @@
|
||||
// Flags: --expose-internals
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const readline = require('readline');
|
||||
const { Writable } = require('stream');
|
||||
const { CSI } = require('internal/readline/utils');
|
||||
|
||||
{
|
||||
assert(CSI);
|
||||
assert.strictEqual(CSI.kClearToLineBeginning, '\x1b[1K');
|
||||
assert.strictEqual(CSI.kClearToLineEnd, '\x1b[0K');
|
||||
assert.strictEqual(CSI.kClearLine, '\x1b[2K');
|
||||
assert.strictEqual(CSI.kClearScreenDown, '\x1b[0J');
|
||||
assert.strictEqual(CSI`1${2}3`, '\x1b[123');
|
||||
}
|
||||
|
||||
class TestWritable extends Writable {
|
||||
constructor() {
|
||||
super();
|
||||
this.data = '';
|
||||
}
|
||||
_write(chunk, encoding, callback) {
|
||||
this.data += chunk.toString();
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
const writable = new TestWritable();
|
||||
|
||||
assert.strictEqual(readline.clearScreenDown(writable), true);
|
||||
assert.deepStrictEqual(writable.data, CSI.kClearScreenDown);
|
||||
assert.strictEqual(readline.clearScreenDown(writable, common.mustCall()), true);
|
||||
|
||||
// Verify that clearScreenDown() throws on invalid callback.
|
||||
assert.throws(() => {
|
||||
readline.clearScreenDown(writable, null);
|
||||
}, /ERR_INVALID_ARG_TYPE|TypeError: The "\w+" argument must be of type function/);
|
||||
|
||||
// Verify that clearScreenDown() does not throw on null or undefined stream.
|
||||
assert.strictEqual(readline.clearScreenDown(null, common.mustCall((err) => {
|
||||
assert.strictEqual(err, null);
|
||||
})), true);
|
||||
assert.strictEqual(readline.clearScreenDown(undefined, common.mustCall()),
|
||||
true);
|
||||
|
||||
writable.data = '';
|
||||
assert.strictEqual(readline.clearLine(writable, -1), true);
|
||||
assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning);
|
||||
|
||||
writable.data = '';
|
||||
assert.strictEqual(readline.clearLine(writable, 1), true);
|
||||
assert.deepStrictEqual(writable.data, CSI.kClearToLineEnd);
|
||||
|
||||
writable.data = '';
|
||||
assert.strictEqual(readline.clearLine(writable, 0), true);
|
||||
assert.deepStrictEqual(writable.data, CSI.kClearLine);
|
||||
|
||||
writable.data = '';
|
||||
assert.strictEqual(readline.clearLine(writable, -1, common.mustCall()), true);
|
||||
assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning);
|
||||
|
||||
// Verify that clearLine() throws on invalid callback.
|
||||
assert.throws(() => {
|
||||
readline.clearLine(writable, 0, null);
|
||||
}, /ERR_INVALID_ARG_TYPE|TypeError: The "\w+" argument must be of type function/);
|
||||
|
||||
// Verify that clearLine() does not throw on null or undefined stream.
|
||||
assert.strictEqual(readline.clearLine(null, 0), true);
|
||||
assert.strictEqual(readline.clearLine(undefined, 0), true);
|
||||
assert.strictEqual(readline.clearLine(null, 0, common.mustCall((err) => {
|
||||
assert.strictEqual(err, null);
|
||||
})), true);
|
||||
assert.strictEqual(readline.clearLine(undefined, 0, common.mustCall()), true);
|
||||
|
||||
// Nothing is written when moveCursor 0, 0
|
||||
[
|
||||
[0, 0, ''],
|
||||
[1, 0, '\x1b[1C'],
|
||||
[-1, 0, '\x1b[1D'],
|
||||
[0, 1, '\x1b[1B'],
|
||||
[0, -1, '\x1b[1A'],
|
||||
[1, 1, '\x1b[1C\x1b[1B'],
|
||||
[-1, 1, '\x1b[1D\x1b[1B'],
|
||||
[-1, -1, '\x1b[1D\x1b[1A'],
|
||||
[1, -1, '\x1b[1C\x1b[1A'],
|
||||
].forEach((set) => {
|
||||
writable.data = '';
|
||||
assert.strictEqual(readline.moveCursor(writable, set[0], set[1]), true);
|
||||
assert.deepStrictEqual(writable.data, set[2]);
|
||||
writable.data = '';
|
||||
assert.strictEqual(
|
||||
readline.moveCursor(writable, set[0], set[1], common.mustCall()),
|
||||
true
|
||||
);
|
||||
assert.deepStrictEqual(writable.data, set[2]);
|
||||
});
|
||||
|
||||
// Verify that moveCursor() throws on invalid callback.
|
||||
assert.throws(() => {
|
||||
readline.moveCursor(writable, 1, 1, null);
|
||||
}, /ERR_INVALID_ARG_TYPE|TypeError: The "\w+" argument must be of type function/);
|
||||
|
||||
// Verify that moveCursor() does not throw on null or undefined stream.
|
||||
assert.strictEqual(readline.moveCursor(null, 1, 1), true);
|
||||
assert.strictEqual(readline.moveCursor(undefined, 1, 1), true);
|
||||
assert.strictEqual(readline.moveCursor(null, 1, 1, common.mustCall((err) => {
|
||||
assert.strictEqual(err, null);
|
||||
})), true);
|
||||
assert.strictEqual(readline.moveCursor(undefined, 1, 1, common.mustCall()),
|
||||
true);
|
||||
|
||||
// Undefined or null as stream should not throw.
|
||||
assert.strictEqual(readline.cursorTo(null), true);
|
||||
assert.strictEqual(readline.cursorTo(), true);
|
||||
assert.strictEqual(readline.cursorTo(null, 1, 1, common.mustCall()), true);
|
||||
assert.strictEqual(readline.cursorTo(undefined, 1, 1, common.mustCall((err) => {
|
||||
assert.strictEqual(err, null);
|
||||
})), true);
|
||||
|
||||
writable.data = '';
|
||||
assert.strictEqual(readline.cursorTo(writable, 'a'), true);
|
||||
assert.strictEqual(writable.data, '');
|
||||
|
||||
writable.data = '';
|
||||
assert.strictEqual(readline.cursorTo(writable, 'a', 'b'), true);
|
||||
assert.strictEqual(writable.data, '');
|
||||
|
||||
writable.data = '';
|
||||
assert.throws(
|
||||
() => readline.cursorTo(writable, 'a', 1),
|
||||
{
|
||||
name: 'TypeError',
|
||||
code: 'ERR_INVALID_CURSOR_POS',
|
||||
message: 'Cannot set cursor row without setting its column'
|
||||
});
|
||||
assert.strictEqual(writable.data, '');
|
||||
|
||||
writable.data = '';
|
||||
assert.strictEqual(readline.cursorTo(writable, 1, 'a'), true);
|
||||
assert.strictEqual(writable.data, '\x1b[2G');
|
||||
|
||||
writable.data = '';
|
||||
assert.strictEqual(readline.cursorTo(writable, 1), true);
|
||||
assert.strictEqual(writable.data, '\x1b[2G');
|
||||
|
||||
writable.data = '';
|
||||
assert.strictEqual(readline.cursorTo(writable, 1, 2), true);
|
||||
assert.strictEqual(writable.data, '\x1b[3;2H');
|
||||
|
||||
writable.data = '';
|
||||
assert.strictEqual(readline.cursorTo(writable, 1, 2, common.mustCall()), true);
|
||||
assert.strictEqual(writable.data, '\x1b[3;2H');
|
||||
|
||||
writable.data = '';
|
||||
assert.strictEqual(readline.cursorTo(writable, 1, common.mustCall()), true);
|
||||
assert.strictEqual(writable.data, '\x1b[2G');
|
||||
|
||||
// Verify that cursorTo() throws on invalid callback.
|
||||
assert.throws(() => {
|
||||
readline.cursorTo(writable, 1, 1, null);
|
||||
}, /ERR_INVALID_ARG_TYPE|TypeError: The "\w+" argument must be of type function/);
|
||||
|
||||
// Verify that cursorTo() throws if x or y is NaN.
|
||||
assert.throws(() => {
|
||||
readline.cursorTo(writable, NaN);
|
||||
}, /ERR_INVALID_ARG_VALUE/);
|
||||
|
||||
assert.throws(() => {
|
||||
readline.cursorTo(writable, 1, NaN);
|
||||
}, /ERR_INVALID_ARG_VALUE/);
|
||||
|
||||
assert.throws(() => {
|
||||
readline.cursorTo(writable, NaN, NaN);
|
||||
}, /ERR_INVALID_ARG_VALUE/);
|
||||
40
test/js/node/test/parallel/test-readline-input-onerror.js
Normal file
40
test/js/node/test/parallel/test-readline-input-onerror.js
Normal file
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const fs = require('fs');
|
||||
const readline = require('readline');
|
||||
const path = require('path');
|
||||
|
||||
async function processLineByLine_SymbolAsyncError(filename) {
|
||||
const fileStream = fs.createReadStream(filename);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for await (const line of rl) {
|
||||
/* check SymbolAsyncIterator `errorListener` */
|
||||
}
|
||||
}
|
||||
|
||||
const f = path.join(__dirname, 'file.txt');
|
||||
|
||||
// catch-able SymbolAsyncIterator `errorListener` error
|
||||
processLineByLine_SymbolAsyncError(f).catch(common.expectsError({
|
||||
code: 'ENOENT',
|
||||
message: /no such file or directory/i
|
||||
}));
|
||||
|
||||
async function processLineByLine_InterfaceErrorEvent(filename) {
|
||||
const fileStream = fs.createReadStream(filename);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
rl.on('error', common.expectsError({
|
||||
code: 'ENOENT',
|
||||
message: /no such file or directory/i
|
||||
}));
|
||||
}
|
||||
|
||||
// check Interface 'error' event
|
||||
processLineByLine_InterfaceErrorEvent(f);
|
||||
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const ArrayStream = require('../common/arraystream');
|
||||
const assert = require('assert');
|
||||
|
||||
common.skipIfDumbTerminal();
|
||||
|
||||
const readline = require('readline');
|
||||
const rli = new readline.Interface({
|
||||
terminal: true,
|
||||
input: new ArrayStream(),
|
||||
output: new ArrayStream(),
|
||||
});
|
||||
|
||||
// Minimal reproduction for #47305
|
||||
const testInput = '{\n}';
|
||||
|
||||
let accum = '';
|
||||
|
||||
rli.output.write = (data) => accum += data.replace('\r', '');
|
||||
|
||||
rli.write(testInput);
|
||||
|
||||
assert.strictEqual(accum, testInput);
|
||||
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const ArrayStream = require('../common/arraystream');
|
||||
const assert = require('assert');
|
||||
|
||||
common.skipIfDumbTerminal();
|
||||
|
||||
const readline = require('readline');
|
||||
const rli = new readline.Interface({
|
||||
terminal: true,
|
||||
input: new ArrayStream(),
|
||||
});
|
||||
|
||||
let recursionDepth = 0;
|
||||
|
||||
// Minimal reproduction for #46731
|
||||
const testInput = ' \n}\n';
|
||||
const numberOfExpectedLines = testInput.match(/\n/g).length;
|
||||
|
||||
rli.on('line', () => {
|
||||
// Abort in case of infinite loop
|
||||
if (recursionDepth > numberOfExpectedLines) {
|
||||
return;
|
||||
}
|
||||
recursionDepth++;
|
||||
// Write something recursively to readline
|
||||
rli.write('foo');
|
||||
});
|
||||
|
||||
|
||||
rli.write(testInput);
|
||||
|
||||
assert.strictEqual(recursionDepth, numberOfExpectedLines);
|
||||
344
test/js/node/test/parallel/test-readline-keys.js
Normal file
344
test/js/node/test/parallel/test-readline-keys.js
Normal file
@@ -0,0 +1,344 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const PassThrough = require('stream').PassThrough;
|
||||
const assert = require('assert');
|
||||
const Interface = require('readline').Interface;
|
||||
|
||||
class FakeInput extends PassThrough {}
|
||||
|
||||
function extend(k) {
|
||||
return Object.assign({ ctrl: false, meta: false, shift: false }, k);
|
||||
}
|
||||
|
||||
|
||||
const fi = new FakeInput();
|
||||
const fo = new FakeInput();
|
||||
new Interface({ input: fi, output: fo, terminal: true });
|
||||
|
||||
let keys = [];
|
||||
fi.on('keypress', (s, k) => {
|
||||
keys.push(k);
|
||||
});
|
||||
|
||||
|
||||
function addTest(sequences, expectedKeys) {
|
||||
if (!Array.isArray(sequences)) {
|
||||
sequences = [ sequences ];
|
||||
}
|
||||
|
||||
if (!Array.isArray(expectedKeys)) {
|
||||
expectedKeys = [ expectedKeys ];
|
||||
}
|
||||
|
||||
expectedKeys = expectedKeys.map(extend);
|
||||
|
||||
keys = [];
|
||||
|
||||
for (const sequence of sequences) {
|
||||
fi.write(sequence);
|
||||
}
|
||||
assert.deepStrictEqual(keys, expectedKeys);
|
||||
}
|
||||
|
||||
// Simulate key interval test cases
|
||||
// Returns a function that takes `next` test case and returns a thunk
|
||||
// that can be called to run tests in sequence
|
||||
// e.g.
|
||||
// addKeyIntervalTest(..)
|
||||
// (addKeyIntervalTest(..)
|
||||
// (addKeyIntervalTest(..)(noop)))()
|
||||
// where noop is a terminal function(() => {}).
|
||||
|
||||
const addKeyIntervalTest = (sequences, expectedKeys, interval = 550,
|
||||
assertDelay = 550) => {
|
||||
const fn = common.mustCall((next) => () => {
|
||||
|
||||
if (!Array.isArray(sequences)) {
|
||||
sequences = [ sequences ];
|
||||
}
|
||||
|
||||
if (!Array.isArray(expectedKeys)) {
|
||||
expectedKeys = [ expectedKeys ];
|
||||
}
|
||||
|
||||
expectedKeys = expectedKeys.map(extend);
|
||||
|
||||
const keys = [];
|
||||
fi.on('keypress', (s, k) => keys.push(k));
|
||||
|
||||
const emitKeys = ([head, ...tail]) => {
|
||||
if (head) {
|
||||
fi.write(head);
|
||||
setTimeout(() => emitKeys(tail), interval);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
next();
|
||||
assert.deepStrictEqual(keys, expectedKeys);
|
||||
}, assertDelay);
|
||||
}
|
||||
};
|
||||
emitKeys(sequences);
|
||||
});
|
||||
return fn;
|
||||
};
|
||||
|
||||
// Regular alphanumerics
|
||||
addTest('io.JS', [
|
||||
{ name: 'i', sequence: 'i' },
|
||||
{ name: 'o', sequence: 'o' },
|
||||
{ name: undefined, sequence: '.' },
|
||||
{ name: 'j', sequence: 'J', shift: true },
|
||||
{ name: 's', sequence: 'S', shift: true },
|
||||
]);
|
||||
|
||||
// Named characters
|
||||
addTest('\n\r\t\x1b\n\x1b\r\x1b\t', [
|
||||
{ name: 'enter', sequence: '\n' },
|
||||
{ name: 'return', sequence: '\r' },
|
||||
{ name: 'tab', sequence: '\t' },
|
||||
{ name: 'enter', sequence: '\x1b\n', meta: true },
|
||||
{ name: 'return', sequence: '\x1b\r', meta: true },
|
||||
{ name: 'tab', sequence: '\x1b\t', meta: true },
|
||||
]);
|
||||
|
||||
// Space and backspace
|
||||
addTest('\b\x7f\x1b\b\x1b\x7f\x1b\x1b \x1b ', [
|
||||
{ name: 'backspace', sequence: '\b' },
|
||||
{ name: 'backspace', sequence: '\x7f' },
|
||||
{ name: 'backspace', sequence: '\x1b\b', meta: true },
|
||||
{ name: 'backspace', sequence: '\x1b\x7f', meta: true },
|
||||
{ name: 'space', sequence: '\x1b\x1b ', meta: true },
|
||||
{ name: 'space', sequence: ' ' },
|
||||
{ name: 'space', sequence: '\x1b ', meta: true },
|
||||
]);
|
||||
|
||||
// Escape key
|
||||
addTest('\x1b\x1b\x1b', [
|
||||
{ name: 'escape', sequence: '\x1b\x1b\x1b', meta: true },
|
||||
]);
|
||||
|
||||
// Escape sequence
|
||||
addTest('\x1b]', [{ name: undefined, sequence: '\x1B]', meta: true }]);
|
||||
|
||||
// Control keys
|
||||
addTest('\x01\x0b\x10', [
|
||||
{ name: 'a', sequence: '\x01', ctrl: true },
|
||||
{ name: 'k', sequence: '\x0b', ctrl: true },
|
||||
{ name: 'p', sequence: '\x10', ctrl: true },
|
||||
]);
|
||||
|
||||
// Alt keys
|
||||
addTest('a\x1baA\x1bA', [
|
||||
{ name: 'a', sequence: 'a' },
|
||||
{ name: 'a', sequence: '\x1ba', meta: true },
|
||||
{ name: 'a', sequence: 'A', shift: true },
|
||||
{ name: 'a', sequence: '\x1bA', meta: true, shift: true },
|
||||
]);
|
||||
|
||||
// xterm/gnome ESC [ letter (with modifiers)
|
||||
addTest('\x1b[2P\x1b[3P\x1b[4P\x1b[5P\x1b[6P\x1b[7P\x1b[8P\x1b[3Q\x1b[8Q\x1b[3R\x1b[8R\x1b[3S\x1b[8S', [
|
||||
{ name: 'f1', sequence: '\x1b[2P', code: '[P', shift: true, meta: false, ctrl: false },
|
||||
{ name: 'f1', sequence: '\x1b[3P', code: '[P', shift: false, meta: true, ctrl: false },
|
||||
{ name: 'f1', sequence: '\x1b[4P', code: '[P', shift: true, meta: true, ctrl: false },
|
||||
{ name: 'f1', sequence: '\x1b[5P', code: '[P', shift: false, meta: false, ctrl: true },
|
||||
{ name: 'f1', sequence: '\x1b[6P', code: '[P', shift: true, meta: false, ctrl: true },
|
||||
{ name: 'f1', sequence: '\x1b[7P', code: '[P', shift: false, meta: true, ctrl: true },
|
||||
{ name: 'f1', sequence: '\x1b[8P', code: '[P', shift: true, meta: true, ctrl: true },
|
||||
{ name: 'f2', sequence: '\x1b[3Q', code: '[Q', meta: true },
|
||||
{ name: 'f2', sequence: '\x1b[8Q', code: '[Q', shift: true, meta: true, ctrl: true },
|
||||
{ name: 'f3', sequence: '\x1b[3R', code: '[R', meta: true },
|
||||
{ name: 'f3', sequence: '\x1b[8R', code: '[R', shift: true, meta: true, ctrl: true },
|
||||
{ name: 'f4', sequence: '\x1b[3S', code: '[S', meta: true },
|
||||
{ name: 'f4', sequence: '\x1b[8S', code: '[S', shift: true, meta: true, ctrl: true },
|
||||
]);
|
||||
|
||||
// xterm/gnome ESC O letter
|
||||
addTest('\x1bOP\x1bOQ\x1bOR\x1bOS', [
|
||||
{ name: 'f1', sequence: '\x1bOP', code: 'OP' },
|
||||
{ name: 'f2', sequence: '\x1bOQ', code: 'OQ' },
|
||||
{ name: 'f3', sequence: '\x1bOR', code: 'OR' },
|
||||
{ name: 'f4', sequence: '\x1bOS', code: 'OS' },
|
||||
]);
|
||||
|
||||
// xterm/rxvt ESC [ number ~ */
|
||||
addTest('\x1b[11~\x1b[12~\x1b[13~\x1b[14~', [
|
||||
{ name: 'f1', sequence: '\x1b[11~', code: '[11~' },
|
||||
{ name: 'f2', sequence: '\x1b[12~', code: '[12~' },
|
||||
{ name: 'f3', sequence: '\x1b[13~', code: '[13~' },
|
||||
{ name: 'f4', sequence: '\x1b[14~', code: '[14~' },
|
||||
]);
|
||||
|
||||
// From Cygwin and used in libuv
|
||||
addTest('\x1b[[A\x1b[[B\x1b[[C\x1b[[D\x1b[[E', [
|
||||
{ name: 'f1', sequence: '\x1b[[A', code: '[[A' },
|
||||
{ name: 'f2', sequence: '\x1b[[B', code: '[[B' },
|
||||
{ name: 'f3', sequence: '\x1b[[C', code: '[[C' },
|
||||
{ name: 'f4', sequence: '\x1b[[D', code: '[[D' },
|
||||
{ name: 'f5', sequence: '\x1b[[E', code: '[[E' },
|
||||
]);
|
||||
|
||||
// Common
|
||||
addTest('\x1b[15~\x1b[17~\x1b[18~\x1b[19~\x1b[20~\x1b[21~\x1b[23~\x1b[24~', [
|
||||
{ name: 'f5', sequence: '\x1b[15~', code: '[15~' },
|
||||
{ name: 'f6', sequence: '\x1b[17~', code: '[17~' },
|
||||
{ name: 'f7', sequence: '\x1b[18~', code: '[18~' },
|
||||
{ name: 'f8', sequence: '\x1b[19~', code: '[19~' },
|
||||
{ name: 'f9', sequence: '\x1b[20~', code: '[20~' },
|
||||
{ name: 'f10', sequence: '\x1b[21~', code: '[21~' },
|
||||
{ name: 'f11', sequence: '\x1b[23~', code: '[23~' },
|
||||
{ name: 'f12', sequence: '\x1b[24~', code: '[24~' },
|
||||
]);
|
||||
|
||||
// xterm ESC [ letter
|
||||
addTest('\x1b[A\x1b[B\x1b[C\x1b[D\x1b[E\x1b[F\x1b[H', [
|
||||
{ name: 'up', sequence: '\x1b[A', code: '[A' },
|
||||
{ name: 'down', sequence: '\x1b[B', code: '[B' },
|
||||
{ name: 'right', sequence: '\x1b[C', code: '[C' },
|
||||
{ name: 'left', sequence: '\x1b[D', code: '[D' },
|
||||
{ name: 'clear', sequence: '\x1b[E', code: '[E' },
|
||||
{ name: 'end', sequence: '\x1b[F', code: '[F' },
|
||||
{ name: 'home', sequence: '\x1b[H', code: '[H' },
|
||||
]);
|
||||
|
||||
// xterm/gnome ESC O letter
|
||||
addTest('\x1bOA\x1bOB\x1bOC\x1bOD\x1bOE\x1bOF\x1bOH', [
|
||||
{ name: 'up', sequence: '\x1bOA', code: 'OA' },
|
||||
{ name: 'down', sequence: '\x1bOB', code: 'OB' },
|
||||
{ name: 'right', sequence: '\x1bOC', code: 'OC' },
|
||||
{ name: 'left', sequence: '\x1bOD', code: 'OD' },
|
||||
{ name: 'clear', sequence: '\x1bOE', code: 'OE' },
|
||||
{ name: 'end', sequence: '\x1bOF', code: 'OF' },
|
||||
{ name: 'home', sequence: '\x1bOH', code: 'OH' },
|
||||
]);
|
||||
|
||||
// Old xterm shift-arrows
|
||||
addTest('\x1bO2A\x1bO2B', [
|
||||
{ name: 'up', sequence: '\x1bO2A', code: 'OA', shift: true },
|
||||
{ name: 'down', sequence: '\x1bO2B', code: 'OB', shift: true },
|
||||
]);
|
||||
|
||||
// xterm/rxvt ESC [ number ~
|
||||
addTest('\x1b[1~\x1b[2~\x1b[3~\x1b[4~\x1b[5~\x1b[6~', [
|
||||
{ name: 'home', sequence: '\x1b[1~', code: '[1~' },
|
||||
{ name: 'insert', sequence: '\x1b[2~', code: '[2~' },
|
||||
{ name: 'delete', sequence: '\x1b[3~', code: '[3~' },
|
||||
{ name: 'end', sequence: '\x1b[4~', code: '[4~' },
|
||||
{ name: 'pageup', sequence: '\x1b[5~', code: '[5~' },
|
||||
{ name: 'pagedown', sequence: '\x1b[6~', code: '[6~' },
|
||||
]);
|
||||
|
||||
// putty
|
||||
addTest('\x1b[[5~\x1b[[6~', [
|
||||
{ name: 'pageup', sequence: '\x1b[[5~', code: '[[5~' },
|
||||
{ name: 'pagedown', sequence: '\x1b[[6~', code: '[[6~' },
|
||||
]);
|
||||
|
||||
// rxvt
|
||||
addTest('\x1b[7~\x1b[8~', [
|
||||
{ name: 'home', sequence: '\x1b[7~', code: '[7~' },
|
||||
{ name: 'end', sequence: '\x1b[8~', code: '[8~' },
|
||||
]);
|
||||
|
||||
// gnome terminal
|
||||
addTest('\x1b[A\x1b[B\x1b[2A\x1b[2B', [
|
||||
{ name: 'up', sequence: '\x1b[A', code: '[A' },
|
||||
{ name: 'down', sequence: '\x1b[B', code: '[B' },
|
||||
{ name: 'up', sequence: '\x1b[2A', code: '[A', shift: true },
|
||||
{ name: 'down', sequence: '\x1b[2B', code: '[B', shift: true },
|
||||
]);
|
||||
|
||||
// `rxvt` keys with modifiers.
|
||||
addTest('\x1b[20~\x1b[2$\x1b[2^\x1b[3$\x1b[3^\x1b[5$\x1b[5^\x1b[6$\x1b[6^\x1b[7$\x1b[7^\x1b[8$\x1b[8^', [
|
||||
{ name: 'f9', sequence: '\x1b[20~', code: '[20~' },
|
||||
{ name: 'insert', sequence: '\x1b[2$', code: '[2$', shift: true },
|
||||
{ name: 'insert', sequence: '\x1b[2^', code: '[2^', ctrl: true },
|
||||
{ name: 'delete', sequence: '\x1b[3$', code: '[3$', shift: true },
|
||||
{ name: 'delete', sequence: '\x1b[3^', code: '[3^', ctrl: true },
|
||||
{ name: 'pageup', sequence: '\x1b[5$', code: '[5$', shift: true },
|
||||
{ name: 'pageup', sequence: '\x1b[5^', code: '[5^', ctrl: true },
|
||||
{ name: 'pagedown', sequence: '\x1b[6$', code: '[6$', shift: true },
|
||||
{ name: 'pagedown', sequence: '\x1b[6^', code: '[6^', ctrl: true },
|
||||
{ name: 'home', sequence: '\x1b[7$', code: '[7$', shift: true },
|
||||
{ name: 'home', sequence: '\x1b[7^', code: '[7^', ctrl: true },
|
||||
{ name: 'end', sequence: '\x1b[8$', code: '[8$', shift: true },
|
||||
{ name: 'end', sequence: '\x1b[8^', code: '[8^', ctrl: true },
|
||||
]);
|
||||
|
||||
// Misc
|
||||
addTest('\x1b[Z', [
|
||||
{ name: 'tab', sequence: '\x1b[Z', code: '[Z', shift: true },
|
||||
]);
|
||||
|
||||
// xterm + modifiers
|
||||
addTest('\x1b[20;5~\x1b[6;5^', [
|
||||
{ name: 'f9', sequence: '\x1b[20;5~', code: '[20~', ctrl: true },
|
||||
{ name: 'pagedown', sequence: '\x1b[6;5^', code: '[6^', ctrl: true },
|
||||
]);
|
||||
|
||||
addTest('\x1b[H\x1b[5H\x1b[1;5H', [
|
||||
{ name: 'home', sequence: '\x1b[H', code: '[H' },
|
||||
{ name: 'home', sequence: '\x1b[5H', code: '[H', ctrl: true },
|
||||
{ name: 'home', sequence: '\x1b[1;5H', code: '[H', ctrl: true },
|
||||
]);
|
||||
|
||||
// Escape sequences broken into multiple data chunks
|
||||
addTest('\x1b[D\x1b[C\x1b[D\x1b[C'.split(''), [
|
||||
{ name: 'left', sequence: '\x1b[D', code: '[D' },
|
||||
{ name: 'right', sequence: '\x1b[C', code: '[C' },
|
||||
{ name: 'left', sequence: '\x1b[D', code: '[D' },
|
||||
{ name: 'right', sequence: '\x1b[C', code: '[C' },
|
||||
]);
|
||||
|
||||
// Escape sequences mixed with regular ones
|
||||
addTest('\x1b[DD\x1b[2DD\x1b[2^D', [
|
||||
{ name: 'left', sequence: '\x1b[D', code: '[D' },
|
||||
{ name: 'd', sequence: 'D', shift: true },
|
||||
{ name: 'left', sequence: '\x1b[2D', code: '[D', shift: true },
|
||||
{ name: 'd', sequence: 'D', shift: true },
|
||||
{ name: 'insert', sequence: '\x1b[2^', code: '[2^', ctrl: true },
|
||||
{ name: 'd', sequence: 'D', shift: true },
|
||||
]);
|
||||
|
||||
// Color sequences
|
||||
addTest('\x1b[31ma\x1b[39ma', [
|
||||
{ name: 'undefined', sequence: '\x1b[31m', code: '[31m' },
|
||||
{ name: 'a', sequence: 'a' },
|
||||
{ name: 'undefined', sequence: '\x1b[39m', code: '[39m' },
|
||||
{ name: 'a', sequence: 'a' },
|
||||
]);
|
||||
|
||||
// `rxvt` keys with modifiers.
|
||||
addTest('\x1b[a\x1b[b\x1b[c\x1b[d\x1b[e', [
|
||||
{ name: 'up', sequence: '\x1b[a', code: '[a', shift: true },
|
||||
{ name: 'down', sequence: '\x1b[b', code: '[b', shift: true },
|
||||
{ name: 'right', sequence: '\x1b[c', code: '[c', shift: true },
|
||||
{ name: 'left', sequence: '\x1b[d', code: '[d', shift: true },
|
||||
{ name: 'clear', sequence: '\x1b[e', code: '[e', shift: true },
|
||||
]);
|
||||
|
||||
addTest('\x1bOa\x1bOb\x1bOc\x1bOd\x1bOe', [
|
||||
{ name: 'up', sequence: '\x1bOa', code: 'Oa', ctrl: true },
|
||||
{ name: 'down', sequence: '\x1bOb', code: 'Ob', ctrl: true },
|
||||
{ name: 'right', sequence: '\x1bOc', code: 'Oc', ctrl: true },
|
||||
{ name: 'left', sequence: '\x1bOd', code: 'Od', ctrl: true },
|
||||
{ name: 'clear', sequence: '\x1bOe', code: 'Oe', ctrl: true },
|
||||
]);
|
||||
|
||||
// Reduce array of addKeyIntervalTest(..) right to left
|
||||
// with () => {} as initial function.
|
||||
const runKeyIntervalTests = [
|
||||
// Escape character
|
||||
addKeyIntervalTest('\x1b', [
|
||||
{ name: 'escape', sequence: '\x1b', meta: true },
|
||||
]),
|
||||
// Chain of escape characters.
|
||||
addKeyIntervalTest('\x1b\x1b\x1b\x1b'.split(''), [
|
||||
{ name: 'escape', sequence: '\x1b', meta: true },
|
||||
{ name: 'escape', sequence: '\x1b', meta: true },
|
||||
{ name: 'escape', sequence: '\x1b', meta: true },
|
||||
{ name: 'escape', sequence: '\x1b', meta: true },
|
||||
]),
|
||||
].reverse().reduce((acc, fn) => fn(acc), () => {});
|
||||
|
||||
// Run key interval tests one after another.
|
||||
runKeyIntervalTests();
|
||||
1155
test/js/node/test/parallel/test-readline-promises-interface.js
Normal file
1155
test/js/node/test/parallel/test-readline-promises-interface.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
// Flags: --expose-internals
|
||||
|
||||
const common = require('../common');
|
||||
const readline = require('readline/promises');
|
||||
const assert = require('assert');
|
||||
const { EventEmitter } = require('events');
|
||||
const { getStringWidth } = require('internal/util/inspect');
|
||||
|
||||
common.skipIfDumbTerminal();
|
||||
|
||||
// This test verifies that the tab completion supports unicode and the writes
|
||||
// are limited to the minimum.
|
||||
[
|
||||
'あ',
|
||||
'𐐷',
|
||||
'🐕',
|
||||
].forEach((char) => {
|
||||
[true, false].forEach((lineBreak) => {
|
||||
[
|
||||
(line) => [
|
||||
['First group', '',
|
||||
`${char}${'a'.repeat(10)}`,
|
||||
`${char}${'b'.repeat(10)}`,
|
||||
char.repeat(11),
|
||||
],
|
||||
line,
|
||||
],
|
||||
|
||||
async (line) => [
|
||||
['First group', '',
|
||||
`${char}${'a'.repeat(10)}`,
|
||||
`${char}${'b'.repeat(10)}`,
|
||||
char.repeat(11),
|
||||
],
|
||||
line,
|
||||
],
|
||||
].forEach((completer) => {
|
||||
|
||||
let output = '';
|
||||
const width = getStringWidth(char) - 1;
|
||||
|
||||
class FakeInput extends EventEmitter {
|
||||
columns = ((width + 1) * 10 + (lineBreak ? 0 : 10)) * 3;
|
||||
|
||||
write = common.mustCall((data) => {
|
||||
output += data;
|
||||
}, 6);
|
||||
|
||||
resume() {}
|
||||
pause() {}
|
||||
end() {}
|
||||
}
|
||||
|
||||
const fi = new FakeInput();
|
||||
const rli = new readline.Interface({
|
||||
input: fi,
|
||||
output: fi,
|
||||
terminal: true,
|
||||
completer: common.mustCallAtLeast(completer),
|
||||
});
|
||||
|
||||
const last = '\r\nFirst group\r\n\r\n' +
|
||||
`${char}${'a'.repeat(10)}${' '.repeat(2 + width * 10)}` +
|
||||
`${char}${'b'.repeat(10)}` +
|
||||
(lineBreak ? '\r\n' : ' '.repeat(2 + width * 10)) +
|
||||
`${char.repeat(11)}\r\n` +
|
||||
`\r\n\u001b[1G\u001b[0J> ${char}\u001b[${4 + width}G`;
|
||||
|
||||
const expectations = [char, '', last];
|
||||
|
||||
rli.on('line', common.mustNotCall());
|
||||
for (const character of `${char}\t\t`) {
|
||||
fi.emit('data', character);
|
||||
queueMicrotask(() => {
|
||||
assert.strictEqual(output, expectations.shift());
|
||||
output = '';
|
||||
});
|
||||
}
|
||||
rli.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
{
|
||||
let output = '';
|
||||
class FakeInput extends EventEmitter {
|
||||
columns = 80;
|
||||
|
||||
write = common.mustCall((data) => {
|
||||
output += data;
|
||||
}, 1);
|
||||
|
||||
resume() {}
|
||||
pause() {}
|
||||
end() {}
|
||||
}
|
||||
|
||||
const fi = new FakeInput();
|
||||
const rli = new readline.Interface({
|
||||
input: fi,
|
||||
output: fi,
|
||||
terminal: true,
|
||||
completer:
|
||||
common.mustCallAtLeast(() => Promise.reject(new Error('message'))),
|
||||
});
|
||||
|
||||
rli.on('line', common.mustNotCall());
|
||||
fi.emit('data', '\t');
|
||||
queueMicrotask(() => {
|
||||
assert.match(output, /^Tab completion error:[^]+Error: message/i);
|
||||
output = '';
|
||||
});
|
||||
rli.close();
|
||||
}
|
||||
90
test/js/node/test/parallel/test-readline-set-raw-mode.js
Normal file
90
test/js/node/test/parallel/test-readline-set-raw-mode.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
// persons to whom the Software is furnished to do so, subject to the
|
||||
// following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
'use strict';
|
||||
require('../common');
|
||||
const assert = require('assert');
|
||||
const readline = require('readline');
|
||||
const Stream = require('stream');
|
||||
|
||||
const stream = new Stream();
|
||||
let expectedRawMode = true;
|
||||
let rawModeCalled = false;
|
||||
let resumeCalled = false;
|
||||
let pauseCalled = false;
|
||||
|
||||
stream.setRawMode = function(mode) {
|
||||
rawModeCalled = true;
|
||||
assert.strictEqual(mode, expectedRawMode);
|
||||
};
|
||||
stream.resume = function() {
|
||||
resumeCalled = true;
|
||||
};
|
||||
stream.pause = function() {
|
||||
pauseCalled = true;
|
||||
};
|
||||
|
||||
// When the "readline" starts in "terminal" mode,
|
||||
// then setRawMode(true) should be called
|
||||
const rli = readline.createInterface({
|
||||
input: stream,
|
||||
output: stream,
|
||||
terminal: true
|
||||
});
|
||||
assert(rli.terminal);
|
||||
assert(rawModeCalled);
|
||||
assert(resumeCalled);
|
||||
assert(!pauseCalled);
|
||||
|
||||
|
||||
// pause() should call *not* call setRawMode()
|
||||
rawModeCalled = false;
|
||||
resumeCalled = false;
|
||||
pauseCalled = false;
|
||||
rli.pause();
|
||||
assert(!rawModeCalled);
|
||||
assert(!resumeCalled);
|
||||
assert(pauseCalled);
|
||||
|
||||
|
||||
// resume() should *not* call setRawMode()
|
||||
rawModeCalled = false;
|
||||
resumeCalled = false;
|
||||
pauseCalled = false;
|
||||
rli.resume();
|
||||
assert(!rawModeCalled);
|
||||
assert(resumeCalled);
|
||||
assert(!pauseCalled);
|
||||
|
||||
|
||||
// close() should call setRawMode(false)
|
||||
expectedRawMode = false;
|
||||
rawModeCalled = false;
|
||||
resumeCalled = false;
|
||||
pauseCalled = false;
|
||||
rli.close();
|
||||
assert(rawModeCalled);
|
||||
assert(!resumeCalled);
|
||||
assert(pauseCalled);
|
||||
|
||||
assert.deepStrictEqual(stream.listeners('keypress'), []);
|
||||
// One data listener for the keypress events.
|
||||
assert.strictEqual(stream.listeners('data').length, 1);
|
||||
138
test/js/node/test/parallel/test-readline-tab-complete.js
Normal file
138
test/js/node/test/parallel/test-readline-tab-complete.js
Normal file
@@ -0,0 +1,138 @@
|
||||
'use strict';
|
||||
|
||||
// Flags: --expose-internals
|
||||
|
||||
const common = require('../common');
|
||||
const readline = require('readline');
|
||||
const assert = require('assert');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const { getStringWidth } = require('internal/util/inspect');
|
||||
|
||||
common.skipIfDumbTerminal();
|
||||
|
||||
// This test verifies that the tab completion supports unicode and the writes
|
||||
// are limited to the minimum.
|
||||
[
|
||||
'あ',
|
||||
'𐐷',
|
||||
'🐕',
|
||||
].forEach((char) => {
|
||||
[true, false].forEach((lineBreak) => {
|
||||
const completer = (line) => [
|
||||
[
|
||||
'First group',
|
||||
'',
|
||||
`${char}${'a'.repeat(10)}`, `${char}${'b'.repeat(10)}`, char.repeat(11),
|
||||
],
|
||||
line,
|
||||
];
|
||||
|
||||
let output = '';
|
||||
const width = getStringWidth(char) - 1;
|
||||
|
||||
class FakeInput extends EventEmitter {
|
||||
columns = ((width + 1) * 10 + (lineBreak ? 0 : 10)) * 3;
|
||||
|
||||
write = common.mustCall((data) => {
|
||||
output += data;
|
||||
}, 6);
|
||||
|
||||
resume() {}
|
||||
pause() {}
|
||||
end() {}
|
||||
}
|
||||
|
||||
const fi = new FakeInput();
|
||||
const rli = new readline.Interface({
|
||||
input: fi,
|
||||
output: fi,
|
||||
terminal: true,
|
||||
completer: common.mustCallAtLeast(completer),
|
||||
});
|
||||
|
||||
const last = '\r\nFirst group\r\n\r\n' +
|
||||
`${char}${'a'.repeat(10)}${' '.repeat(2 + width * 10)}` +
|
||||
`${char}${'b'.repeat(10)}` +
|
||||
(lineBreak ? '\r\n' : ' '.repeat(2 + width * 10)) +
|
||||
`${char.repeat(11)}\r\n` +
|
||||
`\r\n\u001b[1G\u001b[0J> ${char}\u001b[${4 + width}G`;
|
||||
|
||||
const expectations = [char, '', last];
|
||||
|
||||
rli.on('line', common.mustNotCall());
|
||||
for (const character of `${char}\t\t`) {
|
||||
fi.emit('data', character);
|
||||
assert.strictEqual(output, expectations.shift());
|
||||
output = '';
|
||||
}
|
||||
rli.close();
|
||||
});
|
||||
});
|
||||
|
||||
{
|
||||
let output = '';
|
||||
class FakeInput extends EventEmitter {
|
||||
columns = 80;
|
||||
|
||||
write = common.mustCall((data) => {
|
||||
output += data;
|
||||
}, 1);
|
||||
|
||||
resume() {}
|
||||
pause() {}
|
||||
end() {}
|
||||
}
|
||||
|
||||
const fi = new FakeInput();
|
||||
const rli = new readline.Interface({
|
||||
input: fi,
|
||||
output: fi,
|
||||
terminal: true,
|
||||
completer:
|
||||
common.mustCallAtLeast((_, cb) => cb(new Error('message'))),
|
||||
});
|
||||
|
||||
rli.on('line', common.mustNotCall());
|
||||
fi.emit('data', '\t');
|
||||
queueMicrotask(() => {
|
||||
assert.match(output, /^Tab completion error:[^]+Error: message/i);
|
||||
output = '';
|
||||
});
|
||||
rli.close();
|
||||
}
|
||||
|
||||
{
|
||||
let output = '';
|
||||
class FakeInput extends EventEmitter {
|
||||
columns = 80;
|
||||
|
||||
write = common.mustCall((data) => {
|
||||
output += data;
|
||||
}, 9);
|
||||
|
||||
resume() {}
|
||||
pause() {}
|
||||
end() {}
|
||||
}
|
||||
|
||||
const fi = new FakeInput();
|
||||
const rli = new readline.Interface({
|
||||
input: fi,
|
||||
output: fi,
|
||||
terminal: true,
|
||||
completer: common.mustCall((input, cb) => {
|
||||
cb(null, [[input[0].toUpperCase() + input.slice(1)], input]);
|
||||
}),
|
||||
});
|
||||
|
||||
rli.on('line', common.mustNotCall());
|
||||
fi.emit('data', 'input');
|
||||
queueMicrotask(() => {
|
||||
fi.emit('data', '\t');
|
||||
queueMicrotask(() => {
|
||||
assert.match(output, /> Input/);
|
||||
output = '';
|
||||
rli.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user