More tests

This commit is contained in:
Timothy Warren 2023-11-16 16:00:03 -05:00
parent 301196352a
commit 8b5fb17603
19 changed files with 277 additions and 124 deletions

9
coverage.sh Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
rm -fr /cov_profile/
deno test --allow-all --coverage=cov_profile
deno coverage cov_profile --lcov > cov_profile/cov_profile.lcov
genhtml -o cov_profile cov_profile/cov_profile.lcov
rm cov_profile/*.json
open cov_profile/index.html

View File

@ -48,7 +48,7 @@ deno-check:
# Test with deno # Test with deno
deno-test: deno-test:
deno test --allow-all deno test --allow-all --unstable
# Create test coverage report with deno # Create test coverage report with deno
deno-coverage: deno-coverage:

View File

@ -42,6 +42,7 @@ const BunFFI: IFFI = {
tcsetattr, tcsetattr,
cfmakeraw, cfmakeraw,
getPointer: ptr, getPointer: ptr,
close: cStdLib.close,
}; };
export default BunFFI; export default BunFFI;

View File

@ -1,9 +1,9 @@
import { IFIO } from '../common/runtime.ts'; import { IFileIO } from '../common/runtime.ts';
import { appendFileSync, readFileSync } from 'node:fs'; import { appendFileSync, readFileSync } from 'node:fs';
import { appendFile } from 'node:fs/promises'; import { appendFile } from 'node:fs/promises';
const BunFileIO: IFIO = { const BunFileIO: IFileIO = {
openFile: async (path: string): Promise<string> => { openFile: async (path: string): Promise<string> => {
const file = await Bun.file(path); const file = await Bun.file(path);
return await file.text(); return await file.text();

View File

@ -4,6 +4,11 @@
import { ITerminal, ITerminalSize } from '../common/mod.ts'; import { ITerminal, ITerminalSize } from '../common/mod.ts';
import Ansi from '../common/ansi.ts'; import Ansi from '../common/ansi.ts';
const defaultTerminalSize: ITerminalSize = {
rows: 24,
cols: 80,
};
const BunTerminalIO: ITerminal = { const BunTerminalIO: ITerminal = {
// Deno only returns arguments passed to the script, so // Deno only returns arguments passed to the script, so
// remove the bun runtime executable, and entry script arguments // remove the bun runtime executable, and entry script arguments
@ -32,8 +37,8 @@ const BunTerminalIO: ITerminal = {
const rawCode = (new TextDecoder()).decode(chunk); const rawCode = (new TextDecoder()).decode(chunk);
const res = rawCode.trim().replace(/^.\[([0-9]+;[0-9]+)R$/, '$1'); const res = rawCode.trim().replace(/^.\[([0-9]+;[0-9]+)R$/, '$1');
const [srows, scols] = res.split(';'); const [srows, scols] = res.split(';');
const rows = parseInt(srows, 10); const rows = parseInt(srows, 10) ?? 24;
const cols = parseInt(scols, 10); const cols = parseInt(scols, 10) ?? 80;
// Clear the screen // Clear the screen
await write(Ansi.ClearScreen + Ansi.ResetCursor); await write(Ansi.ClearScreen + Ansi.ResetCursor);
@ -44,10 +49,7 @@ const BunTerminalIO: ITerminal = {
}; };
} }
return { return defaultTerminalSize;
rows: 24,
cols: 80,
};
}, },
writeStdout: async function write(s: string): Promise<void> { writeStdout: async function write(s: string): Promise<void> {
const buffer = new TextEncoder().encode(s); const buffer = new TextEncoder().encode(s);

View File

@ -43,11 +43,13 @@ export const Ansi = {
moveCursorDown: (row: number): string => ANSI_PREFIX + `${row}B`, moveCursorDown: (row: number): string => ANSI_PREFIX + `${row}B`,
}; };
const decoder = new TextDecoder(); /**
* Convert input from ANSI escape sequences into a form
export function readKey(raw: Uint8Array): string { * that can be more easily mapped to editor commands
const parsed = decoder.decode(raw); *
* @param parsed - the decoded chunk of input
*/
export function readKey(parsed: string): string {
// Return the input if it's unambiguous // Return the input if it's unambiguous
if (parsed in KeyCommand) { if (parsed in KeyCommand) {
return parsed; return parsed;

33
src/common/ansi_test.ts Normal file
View File

@ -0,0 +1,33 @@
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 { strlen, truncate } from './utils.ts';
import { getRuntime } from './runtime.ts'; import { getRuntime, importForRuntime } from './runtime.ts';
class Buffer { class Buffer {
#b = ''; #b = '';
@ -12,7 +12,7 @@ class Buffer {
} }
public appendLine(s = ''): void { public appendLine(s = ''): void {
this.#b += (s ?? '') + '\r\n'; this.#b += s + '\r\n';
} }
public clear(): void { public clear(): void {
@ -23,7 +23,7 @@ class Buffer {
* Output the contents of the buffer into stdout * Output the contents of the buffer into stdout
*/ */
public async flush() { public async flush() {
const { term } = await getRuntime(); const term = await importForRuntime('terminal_io');
await term.writeStdout(this.#b); await term.writeStdout(this.#b);
this.clear(); this.clear();
} }

45
src/common/buffer_test.ts Normal file
View File

@ -0,0 +1,45 @@
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

@ -1,7 +1,7 @@
import Ansi, { KeyCommand } from './ansi.ts'; import Ansi, { KeyCommand } from './ansi.ts';
import Buffer from './buffer.ts'; import Buffer from './buffer.ts';
import Document from './document.ts'; import Document from './document.ts';
import { IPoint, ITerminalSize, VERSION } from './mod.ts'; import { IPoint, ITerminalSize, logToFile, VERSION } from './mod.ts';
import { ctrl_key } from './utils.ts'; import { ctrl_key } from './utils.ts';
export class Editor { export class Editor {
@ -152,12 +152,17 @@ export class Editor {
private drawFileRow(y: number): void { private drawFileRow(y: number): void {
const row = this.#document.row(y); const row = this.#document.row(y);
let len = row?.chars.length ?? 0; if (row === null) {
logToFile(`Warning: trying to draw non-existent row '${y}'`);
return this.drawPlaceholderRow(y);
}
let len = row.chars.length ?? 0;
if (len > this.#screen.cols) { if (len > this.#screen.cols) {
len = this.#screen.cols; len = this.#screen.cols;
} }
this.#buffer.append(row!.toString(), len); this.#buffer.append(row.toString(), len);
this.#buffer.appendLine(Ansi.ClearLine); this.#buffer.appendLine(Ansi.ClearLine);
} }

View File

@ -4,6 +4,7 @@ import { getTermios } from './termios.ts';
import { Editor } from './editor.ts'; import { Editor } from './editor.ts';
export async function main() { export async function main() {
const decoder = new TextDecoder();
const runTime = await getRuntime(); const runTime = await getRuntime();
const { term, file, onExit, onEvent } = runTime; const { term, file, onExit, onEvent } = runTime;
@ -12,14 +13,13 @@ export async function main() {
t.enableRawMode(); t.enableRawMode();
onExit(() => { onExit(() => {
t.disableRawMode(); t.disableRawMode();
t.cleanup();
}); });
// Setup error handler to log to file // Setup error handler to log to file
onEvent('error', (error) => { onEvent('error', (error) => {
t.disableRawMode(); t.disableRawMode();
if (error instanceof ErrorEvent) { file.appendFileSync('./scroll.err', JSON.stringify(error, null, 2));
file.appendFileSync('./scroll.err', JSON.stringify(error, null, 2));
}
}); });
const terminalSize = await term.getTerminalSize(); const terminalSize = await term.getTerminalSize();
@ -39,7 +39,7 @@ export async function main() {
// The main event loop // The main event loop
for await (const chunk of term.inputLoop()) { for await (const chunk of term.inputLoop()) {
// Process input // Process input
const char = readKey(chunk); const char = readKey(decoder.decode(chunk));
const shouldLoop = editor.processKeyPress(char); const shouldLoop = editor.processKeyPress(char);
if (!shouldLoop) { if (!shouldLoop) {
return 0; return 0;

View File

@ -1,14 +1,25 @@
import { getTermios } from './termios.ts'; import { getTermios } from './termios.ts';
import { noop } from './utils.ts';
import { ITestBase } from './types.ts';
// ----------------------------------------------------------------------------
// Runtime adapter interfaces
// ----------------------------------------------------------------------------
export enum RunTimeType { export enum RunTimeType {
Bun = 'bun', Bun = 'bun',
Deno = 'deno', Deno = 'deno',
Unknown = 'common', Unknown = 'common',
} }
// ----------------------------------------------------------------------------
// Runtime adapter interfaces
// ----------------------------------------------------------------------------
/**
* The size of terminal in rows and columns
*/
export interface ITerminalSize {
rows: number;
cols: number;
}
/** /**
* The native functions for getting/setting terminal settings * The native functions for getting/setting terminal settings
*/ */
@ -33,14 +44,11 @@ export interface IFFI {
*/ */
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
getPointer(ta: any): unknown; getPointer(ta: any): unknown;
}
/** /**
* The size of terminal in rows and columns * Closes the FFI handle
*/ */
export interface ITerminalSize { close(): void;
rows: number;
cols: number;
} }
/** /**
@ -51,10 +59,11 @@ export interface ITerminal {
* The arguments passed to the program on launch * The arguments passed to the program on launch
*/ */
argv: string[]; argv: string[];
/** /**
* The generator function returning chunks of input from the stdin stream * The generator function returning chunks of input from the stdin stream
*/ */
inputLoop(): AsyncGenerator<Uint8Array, void, unknown>; inputLoop(): AsyncGenerator<Uint8Array, void>;
/** /**
* Get the size of the terminal * Get the size of the terminal
@ -70,7 +79,7 @@ export interface ITerminal {
/** /**
* Runtime-specific file handling * Runtime-specific file handling
*/ */
export interface IFIO { export interface IFileIO {
openFile(path: string): Promise<string>; openFile(path: string): Promise<string>;
openFileSync(path: string): string; openFileSync(path: string): string;
appendFile(path: string, contents: string): Promise<void>; appendFile(path: string, contents: string): Promise<void>;
@ -99,7 +108,7 @@ export interface IRuntime {
/** /**
* Runtime-specific file system io * Runtime-specific file system io
*/ */
file: IFIO; file: IFileIO;
/** /**
* Set up an event handler * Set up an event handler
@ -132,6 +141,15 @@ export interface IRuntime {
let scrollRuntime: IRuntime | null = null; let scrollRuntime: IRuntime | null = null;
export function logToFile(s: unknown) {
importForRuntime('file_io').then((f) => {
const raw = (typeof s === 'string') ? s : JSON.stringify(s, null, 2);
const output = raw + '\n';
f.appendFile('./scroll.log', output).then(noop);
});
}
/** /**
* Kill program, displaying an error message * Kill program, displaying an error message
* @param s * @param s
@ -148,7 +166,7 @@ export function die(s: string | Error): void {
/** /**
* Determine which Typescript runtime we are operating under * Determine which Typescript runtime we are operating under
*/ */
export function getRuntimeType(): RunTimeType { export function runtimeType(): RunTimeType {
let runtime = RunTimeType.Unknown; let runtime = RunTimeType.Unknown;
if ('Deno' in globalThis) { if ('Deno' in globalThis) {
@ -166,7 +184,7 @@ export function getRuntimeType(): RunTimeType {
*/ */
export async function getRuntime(): Promise<IRuntime> { export async function getRuntime(): Promise<IRuntime> {
if (scrollRuntime === null) { if (scrollRuntime === null) {
const runtime = getRuntimeType(); const runtime = runtimeType();
const path = `../${runtime}/mod.ts`; const path = `../${runtime}/mod.ts`;
const pkg = await import(path); const pkg = await import(path);
@ -178,16 +196,30 @@ export async function getRuntime(): Promise<IRuntime> {
return Promise.resolve(scrollRuntime!); return Promise.resolve(scrollRuntime!);
} }
/**
* Get the common test interface object
*/
export async function getTestRunner(): Promise<ITestBase> {
const runtime = runtimeType();
const path = `../${runtime}/test_base.ts`;
const pkg = await import(path);
if ('default' in pkg) {
return pkg.default;
}
return pkg;
}
/** /**
* Import a runtime-specific module * Import a runtime-specific module
* *
* eg. to load "src/bun/mod.ts", if the runtime is bun, * e.g. to load "src/bun/mod.ts", if the runtime is bun,
* you can use like so `await importForRuntime('index')`; * you can use like so `await importForRuntime('index')`;
* *
* @param path - the path within the runtime module * @param path - the path within the runtime module
*/ */
export const importForRuntime = async (path: string) => { export const importForRuntime = async (path: string) => {
const runtime = getRuntimeType(); const runtime = runtimeType();
const suffix = '.ts'; const suffix = '.ts';
const base = `../${runtime}/`; const base = `../${runtime}/`;
@ -198,20 +230,10 @@ export const importForRuntime = async (path: string) => {
const cleanedPath = pathParts.join('/'); const cleanedPath = pathParts.join('/');
const importPath = base + cleanedPath + suffix; const importPath = base + cleanedPath + suffix;
return await import(importPath); const pkg = await import(importPath);
};
/**
* Import the default export for a runtime-specific module
* (this is just a simple wrapper of `importForRuntime`)
*
* @param path - the path within the runtime module
*/
export const importDefaultForRuntime = async (path: string) => {
const pkg = await importForRuntime(path);
if ('default' in pkg) { if ('default' in pkg) {
return pkg.default; return pkg.default;
} }
return null; return pkg;
}; };

View File

@ -31,13 +31,13 @@ class Termios {
* The data for the termios struct we are manipulating * The data for the termios struct we are manipulating
* @private * @private
*/ */
readonly #termios: Uint8Array; #termios: Uint8Array;
/** /**
* The pointer to the termios struct * The pointer to the termios struct
* @private * @private
*/ */
readonly #ptr: any; #ptr;
constructor(ffi: IFFI) { constructor(ffi: IFFI) {
this.#ffi = ffi; this.#ffi = ffi;
@ -51,6 +51,13 @@ class Termios {
this.#ptr = ffi.getPointer(this.#termios); this.#ptr = ffi.getPointer(this.#termios);
} }
cleanup() {
this.#ptr = null;
this.#cookedTermios = new Uint8Array(0);
this.#termios = new Uint8Array(0);
this.#ffi.close();
}
enableRawMode() { enableRawMode() {
if (this.#inRawMode) { if (this.#inRawMode) {
throw new Error('Can not enable raw mode when in raw mode'); throw new Error('Can not enable raw mode when in raw mode');

View File

@ -1,11 +1,30 @@
export function noop() {} // ----------------------------------------------------------------------------
// Misc
// ----------------------------------------------------------------------------
export const noop = () => {};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Strings // Strings
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/**
* Get the codepoint of the first byte of a string. If the string
* is empty, this will return 256
*
* @param s - the string
*/
export function ord(s: string): number {
if (s.length > 0) {
return s.codePointAt(0)!;
}
return 256;
}
/** /**
* Split a string by graphemes, not just bytes * Split a string by graphemes, not just bytes
*
* @param s - the string to split into 'characters' * @param s - the string to split into 'characters'
*/ */
export function chars(s: string): string[] { export function chars(s: string): string[] {
@ -14,6 +33,7 @@ export function chars(s: string): string[] {
/** /**
* Get the 'character length' of a string, not its UTF16 byte count * Get the 'character length' of a string, not its UTF16 byte count
*
* @param s - the string to check * @param s - the string to check
*/ */
export function strlen(s: string): number { export function strlen(s: string): number {
@ -21,19 +41,12 @@ export function strlen(s: string): number {
} }
/** /**
* Is the character part of ascii? * Are all the characters in the string in ASCII range?
* *
* @param char - a one character string to check * @param char - string to check
*/ */
export function is_ascii(char: string): boolean { export function is_ascii(char: string): boolean {
return chars(char).every((char) => { return chars(char).every((char) => ord(char) < 0x80);
const point = char.codePointAt(0);
if (point === undefined) {
return false;
}
return point < 0x80;
});
} }
/** /**
@ -42,22 +55,19 @@ export function is_ascii(char: string): boolean {
* @param char - a one character string to check * @param char - a one character string to check
*/ */
export function is_control(char: string): boolean { export function is_control(char: string): boolean {
const code = char.codePointAt(0); const code = ord(char);
if (code === undefined) {
return false;
}
return is_ascii(char) && (code === 0x7f || code < 0x20); return is_ascii(char) && (code === 0x7f || code < 0x20);
} }
/** /**
* Get the key code for a ctrl chord * Get the key code for a ctrl chord
*
* @param char - a one character string * @param char - a one character string
*/ */
export function ctrl_key(char: string): string { export function ctrl_key(char: string): string {
// This is the normal use case, of course // This is the normal use case, of course
if (is_ascii(char)) { if (is_ascii(char)) {
const point = char.codePointAt(0)!; const point = ord(char);
return String.fromCodePoint(point & 0x1f); return String.fromCodePoint(point & 0x1f);
} }

View File

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

1
src/deno/deps.ts Normal file
View File

@ -0,0 +1 @@
export * as stdAssert from 'https://deno.land/std@0.205.0/assert/mod.ts';

View File

@ -39,6 +39,7 @@ const DenoFFI: IFFI = {
tcsetattr, tcsetattr,
cfmakeraw, cfmakeraw,
getPointer: Deno.UnsafePointer.of, getPointer: Deno.UnsafePointer.of,
close: cStdLib.close,
}; };
export default DenoFFI; export default DenoFFI;

View File

@ -1,6 +1,6 @@
import { IFIO } from '../common/runtime.ts'; import { IFileIO } from '../common/runtime.ts';
const DenoFileIO: IFIO = { const DenoFileIO: IFileIO = {
openFile: async function (path: string): Promise<string> { openFile: async function (path: string): Promise<string> {
const decoder = new TextDecoder('utf-8'); const decoder = new TextDecoder('utf-8');
const data = await Deno.readFile(path); const data = await Deno.readFile(path);

View File

@ -1,13 +1,13 @@
import { ITestBase } from '../common/mod.ts'; import { ITestBase } from '../common/mod.ts';
import { stdAssert } from './deps.ts';
import { const {
assertEquals, assertEquals,
assertExists, assertExists,
assertInstanceOf, assertInstanceOf,
AssertionError, AssertionError,
assertNotEquals, assertNotEquals,
assertStrictEquals, assertStrictEquals,
} from 'https://deno.land/std/assert/mod.ts'; } = stdAssert;
class TestBase implements ITestBase { class TestBase implements ITestBase {
test(name: string, fn: () => void): void { test(name: string, fn: () => void): void {