Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] refactor: ts #37

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
f
  • Loading branch information
atian25 committed Dec 28, 2022
commit e527c7d6c0f3eda2576884e23f3d5f9ea0b4b7f7
9 changes: 9 additions & 0 deletions .mocharc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
process.env.TS_NODE_PROJECT = 'test/tsconfig.json';

module.exports = {
spec: 'test/**/*.test.ts',
extension: 'ts',
require: 'ts-node/register',
timeout: 120000,
exclude: 'test/fixtures/',
};
5 changes: 0 additions & 5 deletions .mocharc.yml

This file was deleted.

7 changes: 7 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,10 @@

// koa middleware
// 初始化 -> fork -> await next() -> 校验 -> 结束


// prepare 准备现场环境
// prerun 检查参数,在 fork 定义之后
// run 处理 stdin
// postrun 检查 assert
// end 检查 code,清理现场,相当于 finnaly
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
"version": "1.0.1",
"description": "Command Line E2E Testing",
"type": "commonjs",
"main": "./dist/runner.js",
"types": "./lib/runner.d.ts",
"main": "./dist/index.js",
"types": "./lib/index.d.ts",
"exports": {
".": {
"import": "./dist/runner.js",
"require": "./dist/runner.js"
"import": "./dist/index.js",
"require": "./dist/index.js"
},
"./package.json": "./package.json"
},
Expand All @@ -18,7 +18,7 @@
"scripts": {
"lint": "eslint . --ext .ts",
"postlint": "tsc --noEmit",
"test": "cross-env TS_NODE_PROJECT=test/tsconfig.json mocha",
"test": "mocha",
"cov": "c8 -n src/ npm test",
"ci": "npm run cov",
"tsc": "rm -rf dist && tsc",
Expand Down
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as validator from './plugins/validator';
import * as operation from './plugins/operation';
import { TestRunner, RunnerOptions, PluginLike } from './runner';

export * from './runner';
export * as assert from './lib/assert';

export function runner(opts?: RunnerOptions) {
return new TestRunner(opts)
.plugin({ ...validator, ...operation } satisfies PluginLike);
}
24 changes: 18 additions & 6 deletions src/lib/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export class Process extends EventEmitter {
result: ProcessResult;
proc: execa.ExecaChildProcess;

private isDebug = false;

constructor(type: Process['type'], cmd: string, args: string[] = [], opts: ProcessOptions = {}) {
super();
// assert(!this.cmd, 'cmd can not be registered twice');
Expand Down Expand Up @@ -79,6 +81,10 @@ export class Process extends EventEmitter {
this.opts.cwd = cwd;
}

debug() {
this.isDebug = true;
}

async start() {
if (this.type === 'fork') {
this.proc = execa.node(this.cmd, this.args, this.opts);
Expand All @@ -90,10 +96,14 @@ export class Process extends EventEmitter {
this.proc.then(res => {
if (res instanceof Error) {
this.result.code = res.exitCode;
if ((res as any).code === 'ENOENT') {
const { code, message } = res as any;
if (code === 'ENOENT') {
this.result.code = 127;
this.result.stderr += (res as any).originalMessage;
this.result.stderr += message;
}
// TODO: failed to start
// this.result.stdout = res.stdout;
// this.result.stderr = res.stderr;
}
});

Expand All @@ -103,16 +113,14 @@ export class Process extends EventEmitter {
const origin = stripFinalNewline(data.toString());
const content = stripAnsi(origin);
this.result.stdout += content;
// console.log('stdout', origin);
console.log(origin);
if (this.isDebug) console.log(origin);
});

this.proc.stderr!.on('data', data => {
const origin = stripFinalNewline(data.toString());
const content = stripAnsi(origin);
this.result.stderr += content;
// console.log('stderr', origin);
console.error(origin);
if (this.isDebug) console.error(origin);
});

this.proc.on('message', data => {
Expand All @@ -125,6 +133,10 @@ export class Process extends EventEmitter {
// console.log('close event:', code);
});

this.proc.on('error', err => {
if (this.isDebug) console.error(err);
});

// this.proc.once('close', code => {
// // this.emit('close', code);
// this.result.code = code;
Expand Down
6 changes: 0 additions & 6 deletions src/types.ts → src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@ export type MountPlugin<T, Core> = {
[key in keyof T]: T[key] extends (core: Core, ...args: infer I) => any ? (...args: I) => MountPlugin<T, Core> : T[key];
} & Core;

export type BuiltinPlugin<T extends PluginLike, Core> = {
[key in keyof T]: (...args: RestParam<T[key]>) => Core;
};

export type AsyncFunction = (...args: any[]) => Promise<any>;

export type RestParam<T> = T extends (first: any, ...args: infer R) => any ? R : any;

export interface PluginLike {
Expand Down
25 changes: 17 additions & 8 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import fs from 'node:fs/promises';
import util from 'node:util';
import path from 'node:path';
import { EOL } from 'node:os';

import isMatch from 'lodash.ismatch';
import trash from 'trash';
import { EOL } from 'node:os';

const types = {
...util.types,
Expand All @@ -22,33 +22,42 @@ const types = {
export { types, isMatch };

const extractPathRegex = /\s+at.*[(\s](.*):\d+:\d+\)?/;
export function wrapFn(fn: (...args: any[]) => Promise<any>) {
let end = false;
const testFileRegex = /\.(test|spec)\.(ts|mts|cts|js|cjs|mjs)$/;

export function wrapFn<T extends (...args: any[]) => any>(fn: T): T {
let testFile;
const buildError = new Error('only for stack');
Error.captureStackTrace(buildError, wrapFn);
const additionalStack = buildError.stack!
.split(EOL)
.filter(line => {
const [, file] = line.match(extractPathRegex) || [];
if (!file || end) return false;
if (file.endsWith('.test.ts')) {
end = true;
if (!file || testFile) return false;
if (file.match(testFileRegex)) {
testFile = file;
}
return true;
})
.reverse()
.slice(0, 10)
.join(EOL);

return async (...args: any[]) => {
const wrappedFn = async function (...args: Parameters<T>) {
try {
return await fn(...args);
} catch (err) {
const index = err.stack!.indexOf(' at ');
err.stack = err.stack!.slice(0, index) + additionalStack + EOL + err.stack.slice(index);
const lineEndIndex = err.stack!.indexOf('\n', index);
const line = err.stack!.slice(index, lineEndIndex);
if (!line.includes(testFile)) {
err.stack = err.stack!.slice(0, index) + additionalStack + EOL + err.stack.slice(index);
}
err.cause = buildError;
throw err;
}
};

return wrappedFn as T;
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/plugins/operation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { TestRunner } from '../runner';
import type { TestRunner, HookFunction } from '../runner';

export function tap(runner: TestRunner, fn: (runner: TestRunner) => Promise<void>) {
return runner.hook('after', async () => {
await fn(runner);
export function tap(runner: TestRunner, fn: HookFunction) {
return runner.hook('after', async ctx => {
await fn.call(runner, ctx);
});
}
8 changes: 7 additions & 1 deletion src/plugins/validator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import path from 'node:path';
import assert from 'node:assert/strict';
import type { TestRunner } from '../runner';
import type { TestRunner, HookFunction } from '../runner';
import { matchRule, doesNotMatchRule, matchFile, doesNotMatchFile } from '../lib/assert';

export function expect(runner: TestRunner, fn: HookFunction) {
return runner.hook('after', async ctx => {
await fn.call(runner, ctx);
});
}

export function stdout(runner: TestRunner, expected: string | RegExp) {
return runner.hook('after', async ctx => {
matchRule(ctx.result.stdout, expected);
Expand Down
65 changes: 40 additions & 25 deletions src/runner.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
import EventEmitter from 'events';
import assert from 'node:assert/strict';

import { MountPlugin, PluginLike, AsyncFunction, RestParam } from './types';
import { Process, ProcessEvents, ProcessOptions } from './lib/process';
import { mergeError, wrapFn } from './lib/utils';
import * as validator from './plugins/validator';
import * as operation from './plugins/operation';
import { Process, ProcessEvents, ProcessOptions, ProcessResult } from './lib/process';
import { wrapFn } from './lib/utils';
// import { doesNotMatchRule, matchRule, matchFile, doesNotMatchFile } from './lib/assert';

export type HookFunction = (ctx: RunnerContext) => void | Promise<void>;
export type RestParam<T> = T extends (first: any, ...args: infer R) => any ? R : any;

export type MountPlugin<T, TestRunner> = {
[key in keyof T]: T[key] extends (core: TestRunner, ...args: infer I) => any ? (...args: I) => MountPlugin<T, TestRunner> : undefined;
} & TestRunner;

// use `satisfies`
export interface PluginLike {
[key: string]: (core: TestRunner, ...args: any[]) => any;
}

export interface RunnerOptions {
autoWait?: boolean;
}

export function runner(opts?: RunnerOptions) {
return new TestRunner(opts)
.plugin({ ...validator, ...operation });
export interface RunnerContext {
proc: Process;
cwd: string;
result: ProcessResult;
autoWait?: boolean;
}

export class TestRunner extends EventEmitter {
private logger = console;
private proc: Process;
private options: RunnerOptions = {};
private hooks: Record<string, AsyncFunction[]> = {
private hooks: Record<string, HookFunction[]> = {
before: [],
running: [],
after: [],
Expand All @@ -31,41 +42,36 @@ export class TestRunner extends EventEmitter {
constructor(opts?: RunnerOptions) {
super();
this.options = {
autoWait: true,
// autoWait: true,
...opts,
};
// console.log(this.options);
}

// prepare 准备现场环境
// prerun 检查参数,在 fork 定义之后
// run 处理 stdin
// postrun 检查 assert
// end 检查 code,清理现场,相当于 finnaly

plugin(plugins: PluginLike): MountPlugin<PluginLike, this> {
plugin<T extends PluginLike>(plugins: T): MountPlugin<T, this> {
for (const key of Object.keys(plugins)) {
const initFn = plugins[key];

this[key] = (...args: RestParam<typeof initFn>) => {
console.log('mount %s with %s', key, ...args);
initFn(this, ...args);
return this;
};
}
return this as any;
}

hook(event: string, fn: AsyncFunction) {
hook(event: string, fn: HookFunction) {
this.hooks[event].push(wrapFn(fn));
return this;
}

async end() {
try {
const ctx = {
const ctx: RunnerContext = {
proc: this.proc,
cwd: this.proc.opts.cwd,
cwd: this.proc.opts.cwd!,
result: this.proc.result,
autoWait: true,
};

// before
Expand All @@ -81,7 +87,7 @@ export class TestRunner extends EventEmitter {
await fn(ctx);
}

if (this.options.autoWait) {
if (ctx.autoWait) {
await this.proc.end();
}

Expand Down Expand Up @@ -118,11 +124,20 @@ export class TestRunner extends EventEmitter {
}

wait(type: ProcessEvents, expected) {
this.options.autoWait = false;
// prevent auto wait
this.hook('before', ctx => {
ctx.autoWait = false;
});

// wait for process ready then assert
return this.hook('running', async ({ proc }) => {
// ctx.autoWait = false;
this.hook('running', async ({ proc }) => {
await proc.wait(type, expected);
});

return this;
}

// stdin() {
// // return this.proc.stdin();
// }
}
Loading