Refactor tests to be consistent for both runtimes

This commit is contained in:
Timothy Warren 2023-11-16 20:57:21 -05:00
parent 8b5fb17603
commit 2aaf1c678b
16 changed files with 270 additions and 256 deletions

12
bunfig.toml Normal file
View File

@ -0,0 +1,12 @@
logLevel = "debug" # "debug" | "warn" | "error"
telemetry = false
[install.lockfile]
save = false
[test]
# always enable coverage
coverage = true
# disable code coverage counting test files
coverageSkipTestFiles = true

View File

@ -3,14 +3,12 @@
*/
import { IRuntime, RunTimeType } from '../common/mod.ts';
import BunFFI from './ffi.ts';
import BunTerminalIO from './terminal_io.ts';
import BunFileIO from './file_io.ts';
const BunRuntime: IRuntime = {
name: RunTimeType.Bun,
file: BunFileIO,
ffi: BunFFI,
term: BunTerminalIO,
onEvent: (eventName: string, handler) => process.on(eventName, handler),
onExit: (cb: () => void): void => {

View File

@ -1,14 +1,13 @@
/**
* Wrap the runtime-specific hook into stdin
*/
import { ITerminal, ITerminalSize } from '../common/mod.ts';
import {
defaultTerminalSize,
ITerminal,
ITerminalSize,
} from '../common/mod.ts';
import Ansi from '../common/ansi.ts';
const defaultTerminalSize: ITerminalSize = {
rows: 24,
cols: 80,
};
const BunTerminalIO: ITerminal = {
// Deno only returns arguments passed to the script, so
// remove the bun runtime executable, and entry script arguments

View File

@ -1,42 +1,35 @@
/**
* Adapt the bun test interface to the shared testing interface
*/
import { expect, test as btest } from 'bun:test';
import { describe, expect, test } from 'bun:test';
import { ITestBase } from '../common/mod.ts';
class TestBase implements ITestBase {
test(name: string, fn: () => void) {
return btest(name, fn);
}
assertEquals(actual: unknown, expected: unknown): void {
return expect(actual).toEqual(expected);
}
assertExists(actual: unknown): void {
return expect(actual).toBeDefined();
}
assertInstanceOf(actual: unknown, expectedType: any): void {
return expect(actual).toBeInstanceOf(expectedType);
}
assertNotEquals(actual: unknown, expected: unknown): void {
return expect(actual).not.toBe(expected);
}
assertFalse(actual: boolean): void {
return expect(actual).toBe(false);
}
assertTrue(actual: boolean): void {
return expect(actual).toBe(true);
}
assertStrictEquals(actual: unknown, expected: unknown): void {
return expect(actual).toBe(expected);
}
export function testSuite(testObj: any) {
Object.keys(testObj).forEach((group) => {
describe(group, () => {
const groupObj = testObj[group];
Object.keys(groupObj).forEach((testName) => {
test(testName, groupObj[testName]);
});
});
});
}
const testBase = new TestBase();
export default testBase;
const BunTestBase: ITestBase = {
assertEquals: (actual: unknown, expected: unknown) =>
expect(actual).toEqual(expected),
assertExists: (actual: unknown) => expect(actual).toBeDefined(),
assertFalse: (actual: boolean) => expect(actual).toBe(false),
assertInstanceOf: (actual: unknown, expectedType: any) =>
expect(actual).toBeInstanceOf(expectedType),
assertNotEquals: (actual: unknown, expected: unknown) =>
expect(actual).not.toBe(expected),
assertStrictEquals: (actual: unknown, expected: unknown) =>
expect(actual).toBe(expected),
assertTrue: (actual: boolean) => expect(actual).toBe(true),
test,
testGroup: describe,
testSuite,
};
export default BunTestBase;

177
src/common/all_test.ts Normal file
View File

@ -0,0 +1,177 @@
import { getTestRunner } from './runtime.ts';
import { Ansi, KeyCommand, readKey } from './ansi.ts';
import { Document, Row } from './document.ts';
import Buffer from './buffer.ts';
import {
chars,
ctrl_key,
is_ascii,
is_control,
noop,
ord,
strlen,
truncate,
} from './utils.ts';
import { Editor } from './editor.ts';
import { defaultTerminalSize } from './termios.ts';
const t = await getTestRunner();
const testObj = {
'ANSI::ANSI utils': {
'Ansi.moveCursor': () => {
t.assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H');
},
'Ansi.moveCursorForward': () => {
t.assertEquals(Ansi.moveCursorForward(2), '\x1b[2C');
},
'Ansi.moveCursorDown': () => {
t.assertEquals(Ansi.moveCursorDown(7), '\x1b[7B');
},
},
'ANSI::readKey()': {
'readKey passthrough': () => {
// Ignore unhandled escape sequences
t.assertEquals(readKey('\x1b[]'), '\x1b[]');
// Pass explicitly mapped values right through
t.assertEquals(readKey(KeyCommand.ArrowUp), KeyCommand.ArrowUp);
t.assertEquals(readKey(KeyCommand.Home), KeyCommand.Home);
t.assertEquals(readKey(KeyCommand.Delete), KeyCommand.Delete);
},
'readKey Home': () => {
['\x1b[1~', '\x1b[7~', '\x1b[H', '\x1bOH'].forEach((code) => {
t.assertEquals(readKey(code), KeyCommand.Home);
});
},
'readKey End': () => {
['\x1b[4~', '\x1b[8~', '\x1b[F', '\x1bOF'].forEach((code) => {
t.assertEquals(readKey(code), KeyCommand.End);
});
},
},
Buffer: {
'Buffer exists': () => {
const b = new Buffer();
t.assertInstanceOf(b, Buffer);
t.assertEquals(b.strlen(), 0);
},
'Buffer.appendLine': () => {
const b = new Buffer();
// Carriage return and line feed
b.appendLine();
t.assertEquals(b.strlen(), 2);
b.clear();
t.assertEquals(b.strlen(), 0);
b.appendLine('foo');
t.assertEquals(b.strlen(), 5);
},
'Buffer.append': () => {
const b = new Buffer();
b.append('foobar');
t.assertEquals(b.strlen(), 6);
b.clear();
b.append('foobar', 3);
t.assertEquals(b.strlen(), 3);
},
'Buffer.flush': async () => {
const b = new Buffer();
b.appendLine('foobarbaz' + Ansi.ClearLine);
t.assertEquals(b.strlen(), 14);
await b.flush();
t.assertEquals(b.strlen(), 0);
},
},
Document: {
'Document.empty': () => {
const doc = Document.empty();
t.assertEquals(doc.numRows, 0);
t.assertTrue(doc.isEmpty());
t.assertEquals(doc.row(0), null);
},
'Document.appendRow': () => {
const doc = Document.empty();
doc.appendRow('foobar');
t.assertEquals(doc.numRows, 1);
t.assertFalse(doc.isEmpty());
t.assertInstanceOf(doc.row(0), Row);
},
},
'Document::Row': {
'new Row': () => {
const row = new Row();
t.assertEquals(row.toString(), '');
},
},
Editor: {
'new Editor': () => {
const e = new Editor(defaultTerminalSize);
t.assertInstanceOf(e, Editor);
},
},
'Util::Misc fns': {
'noop fn': () => {
t.assertExists(noop);
t.assertEquals(noop(), undefined);
},
},
'Util::String fns': {
'ord() returns 256 on invalid string': () => {
t.assertEquals(ord(''), 256);
},
'ord() returns number on valid string': () => {
t.assertEquals(ord('a'), 97);
},
'chars() properly splits strings into unicode characters': () => {
t.assertEquals(chars('😺😸😹'), ['😺', '😸', '😹']);
},
'ctrl_key() returns expected values': () => {
const ctrl_a = ctrl_key('a');
t.assertTrue(is_control(ctrl_a));
t.assertEquals(ctrl_a, String.fromCodePoint(0x01));
const invalid = ctrl_key('😺');
t.assertFalse(is_control(invalid));
t.assertEquals(invalid, '😺');
},
'is_ascii() properly discerns ascii chars': () => {
t.assertTrue(is_ascii('asjyverkjhsdf1928374'));
t.assertFalse(is_ascii('😺acalskjsdf'));
t.assertFalse(is_ascii('ab😺ac'));
},
'is_control() works as expected': () => {
t.assertFalse(is_control('abc'));
t.assertTrue(is_control(String.fromCodePoint(0x01)));
t.assertFalse(is_control('😺'));
},
'strlen() returns expected length for ascii strings': () => {
t.assertEquals(strlen('abc'), 'abc'.length);
},
'strlen() returns expected length for multibyte characters': () => {
t.assertEquals(strlen('😺😸😹'), 3);
t.assertNotEquals('😺😸😹'.length, strlen('😺😸😹'));
// Skin tone modifier + base character
t.assertEquals(strlen('🤰🏼'), 2);
t.assertNotEquals('🤰🏼'.length, strlen('🤰🏼'));
// This has 4 sub-characters, and 3 zero-width-joiners
t.assertEquals(strlen('👨‍👩‍👧‍👦'), 7);
t.assertNotEquals('👨‍👩‍👧‍👦'.length, strlen('👨‍👩‍👧‍👦'));
},
'truncate() shortens strings': () => {
t.assertEquals(truncate('😺😸😹', 1), '😺');
t.assertEquals(truncate('😺😸😹', 5), '😺😸😹');
t.assertEquals(truncate('👨‍👩‍👧‍👦', 5), '👨‍👩‍👧');
},
},
};
t.testSuite(testObj);

View File

@ -4,10 +4,6 @@
export const ANSI_PREFIX = '\x1b[';
function esc(pieces: TemplateStringsArray): string {
return ANSI_PREFIX + pieces[0];
}
/**
* ANSI escapes for various inputs
*/
@ -26,12 +22,12 @@ export enum KeyCommand {
}
export const Ansi = {
ClearLine: esc`K`,
ClearScreen: esc`2J`,
ResetCursor: esc`H`,
HideCursor: esc`?25l`,
ShowCursor: esc`?25h`,
GetCursorLocation: esc`6n`,
ClearLine: ANSI_PREFIX + 'K',
ClearScreen: ANSI_PREFIX + '2J',
ResetCursor: ANSI_PREFIX + 'H',
HideCursor: ANSI_PREFIX + '?25l',
ShowCursor: ANSI_PREFIX + '?25h',
GetCursorLocation: ANSI_PREFIX + '6n',
moveCursor: function moveCursor(row: number, col: number): string {
// Convert to 1-based counting
row++;
@ -57,15 +53,15 @@ export function readKey(parsed: string): string {
// Some keycodes have multiple potential inputs
switch (parsed) {
case '\x1bOH':
case '\x1b[7~':
case '\x1b[1~':
case '\x1b[7~':
case '\x1bOH':
case '\x1b[H':
return KeyCommand.Home;
case '\x1bOF':
case '\x1b[8~':
case '\x1b[4~':
case '\x1b[8~':
case '\x1bOF':
case '\x1b[F':
return KeyCommand.End;

View File

@ -1,33 +0,0 @@
import { getTestRunner } from './mod.ts';
import Ansi, { KeyCommand, readKey } from './ansi.ts';
getTestRunner().then((t) => {
t.test('Ansi.moveCursor', () => {
t.assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H');
});
t.test('Ansi.moveCursorForward', () => {
t.assertEquals(Ansi.moveCursorForward(2), '\x1b[2C');
});
t.test('Ansi.moveCursorDown', () => {
t.assertEquals(Ansi.moveCursorDown(7), '\x1b[7B');
});
t.test('readKey', () => {
// Ignore unhandled escape sequences
t.assertEquals(readKey('\x1b[]'), '\x1b[]');
// Pass explicitly mapped values right through
t.assertEquals(readKey(KeyCommand.ArrowUp), KeyCommand.ArrowUp);
t.assertEquals(readKey(KeyCommand.Home), KeyCommand.Home);
['\x1bOH', '\x1b[7~', '\x1b[1~', '\x1b[H'].forEach((code) => {
t.assertEquals(readKey(code), KeyCommand.Home);
});
['\x1bOF', '\x1b[8~', '\x1b[4~', '\x1b[F'].forEach((code) => {
t.assertEquals(readKey(code), KeyCommand.End);
});
});
});

View File

@ -1,5 +1,5 @@
import { strlen, truncate } from './utils.ts';
import { getRuntime, importForRuntime } from './runtime.ts';
import { getRuntime } from './runtime.ts';
class Buffer {
#b = '';
@ -23,7 +23,7 @@ class Buffer {
* Output the contents of the buffer into stdout
*/
public async flush() {
const term = await importForRuntime('terminal_io');
const { term } = await getRuntime();
await term.writeStdout(this.#b);
this.clear();
}

View File

@ -1,45 +0,0 @@
import { getTestRunner } from './runtime.ts';
import Buffer from './buffer.ts';
getTestRunner().then((t) => {
t.test('Buffer exists', () => {
const b = new Buffer();
t.assertInstanceOf(b, Buffer);
t.assertEquals(b.strlen(), 0);
});
t.test('Buffer.appendLine', () => {
const b = new Buffer();
// Carriage return and line feed
b.appendLine();
t.assertEquals(b.strlen(), 2);
b.clear();
t.assertEquals(b.strlen(), 0);
b.appendLine('foo');
t.assertEquals(b.strlen(), 5);
});
t.test('Buffer.append', () => {
const b = new Buffer();
b.append('foobar');
t.assertEquals(b.strlen(), 6);
b.clear();
b.append('foobar', 3);
t.assertEquals(b.strlen(), 3);
});
t.test('Buffer.flush', async () => {
const b = new Buffer();
b.append('foobarbaz');
t.assertEquals(b.strlen(), 9);
await b.flush();
t.assertEquals(b.strlen(), 0);
});
});

View File

@ -55,7 +55,7 @@ export class Document {
return null;
}
protected appendRow(s: string): void {
public appendRow(s: string): void {
this.#rows.push(new Row(s));
}
}

View File

@ -95,11 +95,6 @@ export interface IRuntime {
*/
name: RunTimeType;
/**
* Runtime-specific FFI
*/
ffi: IFFI;
/**
* Runtime-specific terminal functionality
*/

View File

@ -1,10 +1,15 @@
import { die, getRuntime, IFFI } from './mod.ts';
import { die, IFFI, importForRuntime, ITerminalSize } from './mod.ts';
export const STDIN_FILENO = 0;
export const TCSANOW = 0;
export const TERMIOS_SIZE = 60;
export const defaultTerminalSize: ITerminalSize = {
rows: 24,
cols: 80,
};
/**
* Implementation to toggle raw mode
*/
@ -112,7 +117,7 @@ export const getTermios = async () => {
}
// Get the runtime-specific ffi wrappers
const { ffi } = await getRuntime();
const ffi = await importForRuntime('ffi');
termiosSingleton = new Termios(ffi);
return termiosSingleton;

View File

@ -15,12 +15,14 @@ export interface IPoint {
* The shared test interface, so tests can be run by both runtimes
*/
export interface ITestBase {
test(name: string, fn: () => void): void;
assertEquals(actual: unknown, expected: unknown): void;
assertExists(actual: unknown): void;
assertFalse(actual: boolean): void;
assertInstanceOf(actual: unknown, expectedType: any): void;
assertNotEquals(actual: unknown, expected: unknown): void;
assertStrictEquals(actual: unknown, expected: unknown): void;
assertExists(actual: unknown): void;
assertInstanceOf(actual: unknown, expectedType: any): void;
assertTrue(actual: boolean): void;
assertFalse(actual: boolean): void;
test(name: string, fn: () => void, timeout?: number): void;
testGroup(name: string, fn: () => void): void;
testSuite(testObj: any): void;
}

View File

@ -1,74 +0,0 @@
import { getTestRunner } from './mod.ts';
import {
chars,
ctrl_key,
is_ascii,
is_control,
noop,
ord,
strlen,
truncate,
} from './utils.ts';
getTestRunner().then((t) => {
t.test('noop fn', () => {
t.assertExists(noop);
t.assertEquals(noop(), undefined);
});
// ---------------------------------------------------------------------------
// Strings
// ---------------------------------------------------------------------------
t.test('ord fn returns 256 on invalid string', () => {
t.assertEquals(ord(''), 256);
});
t.test('chars fn properly splits strings into unicode characters', () => {
t.assertEquals(chars('😺😸😹'), ['😺', '😸', '😹']);
});
t.test('ctrl_key fn returns expected values', () => {
const ctrl_a = ctrl_key('a');
t.assertTrue(is_control(ctrl_a));
t.assertEquals(ctrl_a, String.fromCodePoint(0x01));
const invalid = ctrl_key('😺');
t.assertFalse(is_control(invalid));
t.assertEquals(invalid, '😺');
});
t.test('is_ascii properly discerns ascii chars', () => {
t.assertTrue(is_ascii('asjyverkjhsdf1928374'));
t.assertFalse(is_ascii('😺acalskjsdf'));
t.assertFalse(is_ascii('ab😺ac'));
});
t.test('is_control fn works as expected', () => {
t.assertFalse(is_control('abc'));
t.assertTrue(is_control(String.fromCodePoint(0x01)));
t.assertFalse(is_control('😺'));
});
t.test('strlen fn returns expected length for ascii strings', () => {
t.assertEquals(strlen('abc'), 'abc'.length);
});
t.test('strlen fn returns expected length for multibyte characters', () => {
t.assertEquals(strlen('😺😸😹'), 3);
t.assertNotEquals('😺😸😹'.length, strlen('😺😸😹'));
// Skin tone modifier + base character
t.assertEquals(strlen('🤰🏼'), 2);
t.assertNotEquals('🤰🏼'.length, strlen('🤰🏼'));
// This has 4 sub-characters, and 3 zero-width-joiners
t.assertEquals(strlen('👨‍👩‍👧‍👦'), 7);
t.assertNotEquals('👨‍👩‍👧‍👦'.length, strlen('👨‍👩‍👧‍👦'));
});
t.test('truncate shortens strings', () => {
t.assertEquals(truncate('😺😸😹', 1), '😺');
t.assertEquals(truncate('😺😸😹', 5), '😺😸😹');
t.assertEquals(truncate('👨‍👩‍👧‍👦', 5), '👨‍👩‍👧');
});
});

View File

@ -2,14 +2,12 @@
* The main entrypoint when using Deno as the runtime
*/
import { IRuntime, RunTimeType } from '../common/mod.ts';
import DenoFFI from './ffi.ts';
import DenoTerminalIO from './terminal_io.ts';
import DenoFileIO from './file_io.ts';
const DenoRuntime: IRuntime = {
name: RunTimeType.Deno,
file: DenoFileIO,
ffi: DenoFFI,
term: DenoTerminalIO,
onEvent: (eventName: string, handler) =>
globalThis.addEventListener(eventName, handler),

View File

@ -9,43 +9,34 @@ const {
assertStrictEquals,
} = stdAssert;
class TestBase implements ITestBase {
test(name: string, fn: () => void): void {
return Deno.test(name, fn);
}
export function testSuite(testObj: any) {
Object.keys(testObj).forEach((group) => {
const groupObj = testObj[group];
Object.keys(groupObj).forEach((testName) => {
Deno.test(testName, groupObj[testName]);
});
});
}
assertEquals(actual: unknown, expected: unknown): void {
return assertEquals(actual, expected);
}
assertStrictEquals(actual: unknown, expected: unknown) {
return assertStrictEquals(actual, expected);
}
assertNotEquals(actual: unknown, expected: unknown): void {
return assertNotEquals(actual, expected);
}
assertExists(actual: unknown, msg?: string): void {
return assertExists(actual, msg);
}
assertInstanceOf(actual: unknown, expectedType: any): void {
return assertInstanceOf(actual, expectedType);
}
assertTrue(actual: boolean): void {
const DenoTestBase: ITestBase = {
assertEquals,
assertExists,
assertInstanceOf,
assertNotEquals,
assertStrictEquals,
assertTrue: function (actual: boolean): void {
if (actual !== true) {
throw new AssertionError(`actual: "${actual}" expected to be true"`);
}
}
},
assertFalse(actual: boolean): void {
if (actual !== false) {
throw new AssertionError(`actual: "${actual}" expected to be false"`);
}
}
}
},
test: Deno.test,
testGroup: (_name: string, fn) => fn(),
testSuite,
};
const testBase = new TestBase();
export default testBase;
export default DenoTestBase;