Merge pull request #2288 from jerch/cleanup_sequences_files

fix edge cases in inputhandler methods
This commit is contained in:
Daniel Imms
2019-07-11 15:37:42 -07:00
committed by GitHub
17 changed files with 1054 additions and 430 deletions
@@ -0,0 +1,73 @@
from glob import glob
import os
import sys
import termios
import atexit
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
def enable_echo(fd, enabled):
(iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(fd)
if enabled:
lflag |= termios.ECHO
else:
lflag &= ~termios.ECHO
new_attr = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]
termios.tcsetattr(fd, termios.TCSANOW, new_attr)
atexit.register(enable_echo, sys.stdin.fileno(), True)
output = []
def log(append=False, *s):
if append:
output[-1] += ' ' + ' '.join(str(part) for part in s)
else:
output.append(' '.join(str(part) for part in s))
def reset_terminal():
sys.stdout.write('\x1bc\x1b[H')
sys.stdout.flush()
def test():
count = 0
passed = 0
for i, testfile in enumerate(sorted(glob(os.path.join(BASE_DIR, '*.in')))):
count += 1
log(False, os.path.basename(testfile))
reset_terminal()
with open(testfile) as test:
sys.stdout.write('\x1b]0;%s\x07' % os.path.basename(testfile))
sys.stdout.write(test.read()+'\x1bt')
sys.stdout.flush()
with open(os.path.join(os.path.dirname(testfile),
os.path.basename(testfile).split('.')[0]+'.text')) as expected:
terminal_output = sys.stdin.read()
if not terminal_output:
# we are in xterm
continue
if terminal_output != expected.read():
log(True, '\x1b[31merror\x1b[0m')
with open(os.path.join(os.path.dirname(testfile), 'output',
os.path.basename(testfile)), 'w') as t_out:
t_out.write(terminal_output)
else:
passed += 1
log(True, '\x1b[32mpass\x1b[0m')
return count, passed
if __name__ == '__main__':
enable_echo(sys.stdin.fileno(), False)
count, passed = test()
enable_echo(sys.stdin.fileno(), True)
reset_terminal()
for i in range(len(output)/2+1):
if not (i+1) % 25:
sys.stdin.read()
print ''.join(i.ljust(40) for i in output[i*2:i*2+2])
print '\x1b[33mcoverage: %s/%s (%d%%) tests passed.\x1b[0m' % (passed, count, passed*100/count)
@@ -1,24 +1,25 @@
a
b
c
d
f
g
h
i
b
c
d
f
g
h
i
j
k
l
m
j
k
l
m
n
o
p
q
r
s
w
x
n
o
p
q
r
s
w
x
@@ -1,3 +1,4 @@
6 C
8 ^^^^
9 vvvv DL on line 11, expected: ACD_
10 A
@@ -12,14 +13,14 @@
19 vvvv IL on line 21, expected: A_
20 A
22 ^^^^
23 vvvv IL on line 24, expected: _A
24 A
25 B
26 ^^^^
28 A
27 vvvv DL on line 28, expected: B_
28 A
29 B
30 ^^^^
31
32
32
@@ -1,25 +1,25 @@
n
o
p
q
r
s
t
u
v
w
x
y
z
1
2
3
4
5
6
7
8
9
10
o
p
q
r
s
t
u
v
w
x
y
z
1
2
3
4
5
6
7
8
9
10
11
11
@@ -2,7 +2,7 @@
efgh
-------- set: wraparound ----------------------------------------------abcd
efgh
-------- unset: no wraparound -------------------------------------------abcd
-------- unset: no wraparound -------------------------------------------abch
this should be immediately below "no wraparound"
@@ -0,0 +1,35 @@
Test of autowrap, mixing control and print characters.
The left/right margins should have letters in order:
[?6hAa
aBB b
C cC

DdEe
eFF f
G gG

HhIi
iJJ j
K kK

LlMm
mNN n
O oO

PpQq
qRR r
S sS

TtUu
uVV v
W wW

XxYy
yZZ z
[?6lPush <RETURN>
@@ -0,0 +1,25 @@
Test of autowrap, mixing control and print characters.
I i
J j
K k
L l
M m
N n
O o
P p
Q q
R r
S s
T t
U u
V v
W w
X x
Y y
Z z
Push <RETURN>
File diff suppressed because it is too large Load Diff
+264 -196
View File
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -8,6 +8,7 @@ import { Terminal } from './Terminal';
import { MockViewport, MockCompositionHelper, MockRenderer } from './TestUtils.test';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { wcwidth } from 'common/CharWidth';
const INIT_COLS = 80;
const INIT_ROWS = 24;
@@ -750,10 +751,14 @@ describe('Terminal', () => {
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.wraparoundMode = false;
const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000);
if (width !== 1) {
continue;
}
term.write('a' + high + String.fromCharCode(i));
// auto wraparound mode should cut off the rest of the line
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(1);
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(2);
expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql('');
term.reset();
}
+6 -48
View File
@@ -1811,46 +1811,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
/**
* ESC
*/
/**
* ESC D Index (IND is 0x84).
*/
public index(): void {
this.buffer.y++;
if (this.buffer.y > this.buffer.scrollBottom) {
this.buffer.y--;
this.scroll();
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this.buffer.x >= this.cols) {
this.buffer.x--;
}
}
/**
* ESC M Reverse Index (RI is 0x8d).
*
* Move the cursor up one row, inserting a new blank line if necessary.
*/
public reverseIndex(): void {
if (this.buffer.y === this.buffer.scrollTop) {
// possibly move the code below to term.reverseScroll();
// test: echo -ne '\e[1;1H\e[44m\eM\e[0m'
// blankLine(true) is xterm/linux behavior
const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop;
this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1);
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttrData()));
this.updateRange(this.buffer.scrollTop);
this.updateRange(this.buffer.scrollBottom);
} else {
this.buffer.y--;
}
}
/**
* ESC c Full Reset (RIS).
* Reset terminal.
* Note: Calling this directly from JS is synchronous but does not clear
* input buffers and does not reset the parser, thus the terminal will
* continue to apply pending input data.
* If you need in band reset (synchronous with input data) consider
* using DECSTR (soft reset, CSI ! p) or RIS instead (hard reset, ESC c).
*/
public reset(): void {
/**
@@ -1892,14 +1858,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
}
/**
* ESC H Tab Set (HTS is 0x88).
*/
public tabSet(): void {
this.buffer.tabs[this.buffer.x] = true;
}
// TODO: Remove cancel function and cancelEvents option
public cancel(ev: Event, force?: boolean): boolean {
if (!this.options.cancelEvents && !force) {
+92 -112
View File
@@ -1,45 +1,106 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*
* This file contains integration tests for xterm.js.
*/
import * as glob from 'glob';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import * as pty from 'node-pty';
import { Terminal } from './Terminal';
import { IViewport } from './Types';
import { CellData } from 'common/buffer/CellData';
import { WHITESPACE_CELL_CHAR } from 'common/buffer/Constants';
import { IDisposable } from 'xterm';
class TestTerminal extends Terminal {
innerWrite(): void { this._innerWrite(); }
// all test files expect terminal in 80x25
const COLS = 80;
const ROWS = 25;
const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
const SKIP_FILES = [
't0070-DECSTBM_LF.in', // lineFeed not working correctly
't0071-DECSTBM_IND.in',
't0072-DECSTBM_NEL.in',
't0075-DECSTBM_CUU_CUD.in',
't0076-DECSTBM_IL_DL.in', // not working due to lineFeed
't0077-DECSTBM_quirks.in',
't0084-CBT.in',
't0101-NLM.in',
't0103-reverse_wrap.in',
't0504-vim.in'
];
if (os.platform() === 'darwin') {
// These are failing on macOS only (termios related?)
SKIP_FILES.push(
't0003-line_wrap.in',
't0005-CR.in',
't0009-NEL.in',
't0503-zsh_ls_color.in'
);
}
// filter skipFilenames
const FILES = TESTFILES.filter(value => SKIP_FILES.indexOf(value.split('/').slice(-1)[0]) === -1);
let primitivePty: any;
// fake sychronous pty write - read
// we just pipe the data from slave to master as a child program would do
// pty.js opens pipe fds with O_NONBLOCK
// just wait 10ms instead of setting fds to blocking mode
function ptyWriteRead(data: string, cb: (result: string) => void): void {
fs.writeSync(primitivePty.slave, data);
setTimeout(() => {
const b = new Buffer(64000);
const bytes = fs.readSync(primitivePty.master, b, 0, 64000, null);
cb(b.toString('utf8', 0, bytes));
describe('Escape Sequence Files', function(): void {
this.timeout(20000);
let ptyTerm: any;
let slaveEnd: any;
let term: Terminal;
let customHandler: IDisposable | undefined;
before(() => {
ptyTerm = (pty as any).open({cols: COLS, rows: ROWS});
slaveEnd = ptyTerm._slave;
term = new Terminal({cols: COLS, rows: ROWS});
ptyTerm._master.on('data', (data: string) => term.write(data));
});
}
// make sure raw pty is at x=0 and has no pending data
function ptyReset(cb: (result: string) => void): void {
ptyWriteRead('\r\n', cb);
}
after(() => {
ptyTerm._master.end();
ptyTerm._master.destroy();
});
FILES.forEach(filename => {
it(filename.split('/').slice(-1)[0], async () => {
// reset terminal and handler
if (customHandler) {
customHandler.dispose();
}
slaveEnd.write('\r\n');
term.reset();
slaveEnd.write('\x1bc\x1b[H');
// register handler to trigger viewport scraping, wait for it to finish
let content = '';
const OSC_CODE = 12345;
await new Promise(resolve => {
customHandler = term.addOscHandler(OSC_CODE, () => {
// grab terminal viewport content
content = terminalToString(term);
resolve();
return true;
});
// write file to slave
slaveEnd.write(fs.readFileSync(filename, 'utf8'));
// trigger custom sequence
slaveEnd.write(`\x1b]${OSC_CODE};\x07`);
});
// compare with expected output (right trimmed)
const expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
const expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
if (content !== expectedRightTrimmed) {
throw new Error(formatError(fs.readFileSync(filename, 'utf8'), content, expected));
}
});
});
});
/**
* Helpers
*/
/* debug helpers */
// generate colorful noisy output to compare xterm and emulator cell states
function formatError(input: string, output: string, expected: string): string {
function addLineNumber(start: number, color: string): (s: string) => string {
@@ -51,10 +112,10 @@ function formatError(input: string, output: string, expected: string): string {
}
const line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890';
let s = '';
s += '\n\x1b[34m' + JSON.stringify(input);
s += '\n\x1b[33m ' + line80 + '\n';
s += `\n\x1b[34m${JSON.stringify(input)}`;
s += `\n\x1b[33m ${line80}\n`;
s += output.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n');
s += '\n\x1b[33m ' + line80 + '\n';
s += `\n\x1b[33m ${line80}\n`;
s += expected.split('\n').map(addLineNumber(0, '\x1b[32m')).join('\n');
return s;
}
@@ -64,10 +125,7 @@ function terminalToString(term: Terminal): string {
let result = '';
let lineText = '';
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
lineText = '';
for (let cell = 0; cell < term.cols; ++cell) {
lineText += term.buffer.lines.get(line).loadCell(cell, new CellData()).getChars() || WHITESPACE_CELL_CHAR;
}
lineText = term.buffer.lines.get(line).translateToString(true);
// rtrim empty cells as xterm does
lineText = lineText.replace(/\s+$/, '');
result += lineText;
@@ -75,81 +133,3 @@ function terminalToString(term: Terminal): string {
}
return result;
}
// Skip tests on Windows since pty.open isn't supported
if (os.platform() !== 'win32') {
const consoleLog = console.log;
// expect files need terminal at 80x25!
const cols = 80;
const rows = 25;
/** some helpers for pty interaction */
// we need a pty in between to get the termios decorations
// for the basic test cases a raw pty device is enough
primitivePty = (<any>pty).native.open(cols, rows);
/** tests */
describe('xterm output comparison', function(): void {
this.timeout(10000);
let xterm: TestTerminal;
beforeEach(() => {
xterm = new TestTerminal({ cols: cols, rows: rows });
xterm.refresh = () => {};
xterm.viewport = <IViewport>{
syncScrollArea: () => {}
};
});
// omit stack trace for escape sequence files
Error.stackTraceLimit = 0;
const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
// for (let i = 0; i < files.length; ++i) console.debug(i, files[i]);
// only successful tests for now
const skip = [
10, 16, 17, 19, 32, 34, 35, 36, 39,
40, 42, 43, 44, 45, 46, 47, 48, 49, 50,
51, 52, 54, 55, 56, 57, 58, 59, 60, 61,
63, 68
];
// These are failing on macOS only
if (os.platform() === 'darwin') {
skip.push(3, 7, 11, 67);
}
for (let i = 0; i < files.length; i++) {
if (skip.indexOf(i) >= 0) {
continue;
}
((filename: string) => {
const inFile = fs.readFileSync(filename, 'utf8');
it(filename.split('/').slice(-1)[0], done => {
ptyReset(() => {
ptyWriteRead(inFile, fromPty => {
// uncomment this to get log from terminal
// console.log = function(){};
// Perform a synchronous .write(data)
xterm.writeBuffer.push(fromPty);
xterm.innerWrite();
const fromEmulator = terminalToString(xterm);
console.log = consoleLog;
const expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
// Some of the tests have whitespace on the right of lines, we trim all the linex
// from xterm.js so ignore this for now at least.
const expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
if (fromEmulator !== expectedRightTrimmed) {
// uncomment to get noisy output
throw new Error(formatError(inFile, fromEmulator, expected));
// throw new Error('mismatch');
}
done();
});
});
});
})(files[i]);
}
});
}
+2 -10
View File
@@ -6,7 +6,7 @@
import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions, ILinkifier, ILinkMatcherOptions } from './Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener } from 'common/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
import * as Browser from 'common/Platform';
import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm';
@@ -295,21 +295,12 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
addDisposableListener(type: string, handler: XtermListener): IDisposable {
throw new Error('Method not implemented.');
}
tabSet(): void {
throw new Error('Method not implemented.');
}
handler(data: string): void {
throw new Error('Method not implemented.');
}
handleTitle(title: string): void {
throw new Error('Method not implemented.');
}
index(): void {
throw new Error('Method not implemented.');
}
reverseIndex(): void {
throw new Error('Method not implemented.');
}
}
export class MockBuffer implements IBuffer {
@@ -329,6 +320,7 @@ export class MockBuffer implements IBuffer {
scrollTop: number;
savedY: number;
savedX: number;
savedCharset: ICharset | null;
savedCurAttrData = new AttributeData();
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string {
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
+1 -3
View File
@@ -70,10 +70,7 @@ export interface IInputHandlingTerminal {
showCursor(): void;
refresh(start: number, end: number): void;
error(text: string, data?: any): void;
tabSet(): void;
handleTitle(title: string): void;
index(): void;
reverseIndex(): void;
}
export interface IViewport extends IDisposable {
@@ -169,6 +166,7 @@ export interface IInputHandler {
ESC |
ESC }
ESC ~ */ setgLevel(level: number): void;
/** ESC # 8 */ screenAlignmentPattern(): void;
}
export interface ILinkMatcher {
+3 -1
View File
@@ -5,13 +5,14 @@
import { CircularList, IInsertEvent } from 'common/CircularList';
import { IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData } from 'common/Types';
import { IBufferLine, ICellData, IAttributeData, ICharset } from 'common/Types';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from 'common/buffer/Constants';
import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from 'common/buffer/BufferReflow';
import { Marker } from 'common/buffer/Marker';
import { IOptionsService, IBufferService } from 'common/services/Services';
import { DEFAULT_CHARSET } from 'common/data/Charsets';
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
@@ -35,6 +36,7 @@ export class Buffer implements IBuffer {
public savedY: number = 0;
public savedX: number = 0;
public savedCurAttrData = DEFAULT_ATTR_DATA.clone();
public savedCharset: ICharset | null = DEFAULT_CHARSET;
public markers: Marker[] = [];
private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]);
+2 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IAttributeData, ICircularList, IBufferLine, ICellData, IMarker } from 'common/Types';
import { IAttributeData, ICircularList, IBufferLine, ICellData, IMarker, ICharset } from 'common/Types';
import { IEvent } from 'common/EventEmitter';
// BufferIndex denotes a position in the buffer: [rowIndex, colIndex]
@@ -31,6 +31,7 @@ export interface IBuffer {
hasScrollback: boolean;
savedY: number;
savedX: number;
savedCharset: ICharset | null;
savedCurAttrData: IAttributeData;
isCursorInViewport: boolean;
markers: IMarker[];
+5 -8
View File
@@ -466,10 +466,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
break;
case ParserAction.EXECUTE:
this.precedingCodepoint = 0;
callback = this._executeHandlers[code];
if (callback) callback();
else this._executeHandlerFb(code);
this.precedingCodepoint = 0;
break;
case ParserAction.IGNORE:
break;
@@ -488,10 +488,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// inject values: currently not implemented
break;
case ParserAction.CSI_DISPATCH:
// dont reset preceding codepoint for REP itself
if (code !== 98) { // 'b'
this.precedingCodepoint = 0;
}
// Trigger CSI Handler
const handlers = this._csiHandlers[code];
let j = handlers ? handlers.length - 1 : -1;
@@ -504,6 +500,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
if (j < 0) {
this._csiHandlerFb(collect, params, code);
}
this.precedingCodepoint = 0;
break;
case ParserAction.PARAM:
// inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)
@@ -529,10 +526,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
collect += String.fromCharCode(code);
break;
case ParserAction.ESC_DISPATCH:
this.precedingCodepoint = 0;
callback = this._escHandlers[collect + String.fromCharCode(code)];
if (callback) callback(collect, code);
else this._escHandlerFb(collect, code);
this.precedingCodepoint = 0;
break;
case ParserAction.CLEAR:
osc = '';
@@ -541,7 +538,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
collect = '';
break;
case ParserAction.DCS_HOOK:
this.precedingCodepoint = 0;
dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)];
if (!dcsHandler) dcsHandler = this._dcsHandlerFb;
dcsHandler.hook(collect, params, code);
@@ -569,6 +565,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
params.reset();
params.addParam(0); // ZDM
collect = '';
this.precedingCodepoint = 0;
break;
case ParserAction.OSC_START:
osc = '';
@@ -584,7 +581,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
break;
case ParserAction.OSC_END:
this.precedingCodepoint = 0;
if (osc && code !== 0x18 && code !== 0x1a) {
// NOTE: OSC subparsing is not part of the original parser
// we do basic identifier parsing here to offer a jump table for OSC as well
@@ -616,6 +612,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
params.reset();
params.addParam(0); // ZDM
collect = '';
this.precedingCodepoint = 0;
break;
}
currentState = transition & TableAccess.TRANSITION_STATE_MASK;