Compare commits

...

23 Commits

Author SHA1 Message Date
Kai Tamkun
6608a56960 Merge branch 'kai/readline-test-fixes' of github.com:oven-sh/bun into kai/readline-test-fixes 2025-01-03 15:11:34 -08:00
Kai Tamkun
67aae542ad don't ref/unref on process.stdin resume/pause 2025-01-03 15:11:20 -08:00
Kai Tamkun
a76def9d93 Merge branch 'main' into kai/readline-test-fixes 2025-01-02 14:21:59 -08:00
Kai Tamkun
f2462b17b3 Merge branch 'main' into kai/readline-test-fixes 2025-01-02 14:13:21 -08:00
Kai Tamkun
ed7644709c Handle OUT_OF_RANGE in readline.node.test.ts 2024-12-16 13:17:50 -08:00
Kai Tamkun
fe03672278 Undo --expose-internals 2024-12-11 14:57:09 -08:00
Kai Tamkun
af493f9eeb Use --foreground in check-node.sh 2024-12-11 13:58:09 -08:00
Kai Tamkun
7c9548afac Fix test-readline-promises-interface.js 2024-12-11 13:27:47 -08:00
Kai Tamkun
6e3219b9f9 amgiguous -> ambiguous 2024-12-11 13:18:06 -08:00
Kai Tamkun
622d18ea31 Add test-readline-promises-tab-complete.js 2024-12-10 16:38:32 -08:00
Kai Tamkun
952154bb80 Add test-readline-csi.js 2024-12-10 16:36:17 -08:00
Kai Tamkun
ff584f8eea Fix test-readline-tab-complete.js 2024-12-10 16:05:52 -08:00
Kai Tamkun
bda25a04e8 Merge commit 'f472779' into kai/readline-test-fixes 2024-12-10 15:16:29 -08:00
Kai Tamkun
a82293bb43 Fix test-readline-set-raw-mode.js 2024-12-10 15:14:28 -08:00
Kai Tamkun
ba557c81aa Add test-readline-interface-recursive-writes.js 2024-12-10 15:11:03 -08:00
Kai Tamkun
671f8a9fb7 Fix test-readline-interface-no-trailing-newline.js 2024-12-10 15:09:44 -08:00
Kai Tamkun
b51d01af6f Fix test-readline-async-iterators-backpressure.js 2024-12-10 14:48:57 -08:00
dave caruso
f47277988f ok 2024-12-10 14:39:30 -08:00
dave caruso
ac75182fcd extract readline internals 2024-12-10 14:38:52 -08:00
Kai Tamkun
4b63922335 Fix test-readline-carriage-return-between-chunks.js 2024-12-10 14:33:23 -08:00
dave caruso
eccf42a2e4 allow importing internals when --expose-internals 2024-12-10 14:29:39 -08:00
Kai Tamkun
c5e8f93a68 Fix test-readline-input-onerror.js by making fs.createReadStream's ENOENT behavior align with Node's 2024-12-10 13:56:13 -08:00
Kai Tamkun
dea30a047a Fix test-readline-keys.js 2024-12-10 12:49:21 -08:00
25 changed files with 2351 additions and 176 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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" },
},
);

View File

@@ -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(() => {

View File

@@ -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) => {

View 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 };

View File

@@ -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":

View File

@@ -2699,6 +2699,7 @@ export default {
format,
formatWithOptions,
stripVTControlCharacters,
getStringWidth,
//! non-standard properties, should these be kept? (not currently exposed)
//stylizeWithColor,
//stylizeWithHTML(str, styleType) {

View File

@@ -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) {

View File

@@ -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);
});
}
};

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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",
},
);
});

View File

@@ -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();
});
});
}

View File

@@ -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());

View File

@@ -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);
}));

View 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/);

View 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);

View File

@@ -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);

View File

@@ -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);

View 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();

File diff suppressed because it is too large Load Diff

View File

@@ -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();
}

View 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);

View 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();
});
});
}