From 247c54527fbd954fc0b95f7a65105a5d44d106e2 Mon Sep 17 00:00:00 2001 From: TZ Date: Mon, 19 Dec 2022 23:19:12 +0800 Subject: [PATCH 01/14] refactor: ts --- .eslintignore | 2 + .eslintrc | 19 ++++ .github/workflows/ci.yml | 22 +++++ .github/workflows/nodejs.yml | 47 ---------- .gitignore | 17 +--- lib/validator.js | 4 +- package.json | 28 +++--- src/utils.ts | 128 ++++++++++++++++++++++++++ test/{test-utils.js => test-utils.ts} | 0 test/{utils.test.js => utils.test.ts} | 4 +- tsconfig.json | 14 +++ 11 files changed, 209 insertions(+), 76 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/nodejs.yml create mode 100644 src/utils.ts rename test/{test-utils.js => test-utils.ts} (100%) rename test/{utils.test.js => utils.test.ts} (96%) create mode 100644 tsconfig.json diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1eae0cf --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..370fb68 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,19 @@ +{ + "extends": "@artus/eslint-config-artus/typescript", + "parserOptions": { + "project": "./tsconfig.json", + "createDefaultProgram": true + }, + "rules": { + "prefer-spread": "off", + "no-return-assign": "off", + "no-case-declarations": "off", + "prefer-const": "off", + "no-regex-spaces": "off", + "no-return-await": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-var-requires": "off" + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e370466 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: + push: + branches: [ master, main ] + + pull_request: + branches: [ master, main, next, beta, "*.x" ] + + schedule: + - cron: '0 2 * * *' + + workflow_dispatch: + +jobs: + Job: + name: Node.js + uses: artusjs/github-actions/.github/workflows/node-test.yml@master + # pass these inputs only if you need to custom + # with: + # os: 'ubuntu-latest, macos-latest, windows-latest' + # version: '16, 18' diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml deleted file mode 100644 index 7218472..0000000 --- a/.github/workflows/nodejs.yml +++ /dev/null @@ -1,47 +0,0 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - -name: Node.js CI - -on: - workflow_dispatch: {} - push: - branches: - - main - - master - pull_request: - branches: - - main - - master - schedule: - - cron: '0 2 * * *' - -jobs: - build: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - node-version: [14, 16, 18] - os: [ubuntu-latest, windows-latest, macos-latest] - - steps: - - name: Checkout Git Source - uses: actions/checkout@v2 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - - name: Install Dependencies - run: npm i - - - name: Continuous Integration - run: npm run ci - - - name: Code Coverage - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index f44b8bf..cfce3c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,8 @@ -logs/ -npm-debug.log node_modules/ +dist/ coverage/ -.idea/ -run/ -.DS_Store -*.swp -*-lock.json -*-lock.yaml -.vscode/history +*-lock*[.yaml, .json] +**/*.js +**/*.js.map +**/*.d.ts .tmp -.vscode -.tempCodeRunnerFile.js -/snippet/ diff --git a/lib/validator.js b/lib/validator.js index 94c8f4f..1b2df3f 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -21,7 +21,7 @@ export function expect(fn) { } const extractPathRegex = /\s+at.*[(\s](.*):\d+:\d+\)?/; -const __filename = filename(import.meta); +const currentFileName = types.isObject(meta) ? filename(import.meta) : __filename; function mergeError(buildError, runError) { buildError.message = runError.message; @@ -38,7 +38,7 @@ function mergeError(buildError, runError) { if (line.trim() === '') return false; const pathMatches = line.match(extractPathRegex); if (pathMatches === null || !pathMatches[1]) return true; - if (pathMatches[1] === __filename) return false; + if (pathMatches[ 1 ] === currentFileName) return false; return true; }) .join('\n'); diff --git a/package.json b/package.json index 8043d99..1bf55a2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "clet", "version": "1.0.1", "description": "Command Line E2E Testing", - "type": "module", + "type": "commonjs", "main": "./lib/runner.js", "exports": "./lib/runner.js", "types": "./lib/index.d.ts", @@ -21,31 +21,33 @@ "trash": "^8.1.0" }, "devDependencies": { + "@artus/eslint-config-artus": "^0.0.1", + "@artus/tsconfig": "^1", + "@types/mocha": "^9.1.1", + "@types/node": "^18.7.14", "@vitest/coverage-c8": "^0.22.1", "@vitest/ui": "^0.22.1", "cross-env": "^7.0.3", - "egg-ci": "^1.19.0", "enquirer": "^2.3.6", "eslint": "^7", "eslint-config-egg": "^9", "supertest": "^6.2.3", - "vitest": "^0.22.1" + "vitest": "^0.22.1", + "ts-node": "^10.9.1", + "tslib": "^2.4.0", + "typescript": "^4.8.2" }, "files": [ - "bin", - "lib", - "index.js" + "dist" ], "scripts": { - "lint": "eslint .", + "lint": "eslint . --ext .ts", + "postlint": "tsc --noEmit", "test": "vitest", "cov": "vitest run --coverage", - "ci": "npm run lint && npm run cov" - }, - "ci": { - "version": "14, 16, 18", - "type": "github", - "npminstall": false + "ci": "npm run cov", + "tsc": "rm -rf dist && tsc", + "prepack": "npm run tsc" }, "eslintConfig": { "extends": "eslint-config-egg", diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..c77c815 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,128 @@ +import { promises as fs } from 'fs'; +import util from 'util'; +import path from 'path'; + +import { dirname } from 'dirname-filename-esm'; +import isMatch from 'lodash.ismatch'; +import trash from 'trash'; + +const types = { + ...util.types, + isString(v: any): v is string { + return typeof v === 'string'; + }, + isObject(v: any): v is object { + return v !== null && typeof v === 'object'; + }, + isFunction(v: any): v is (...args: any[]) => any { + return typeof v === 'function'; + }, +}; + +export { types, isMatch }; + +/** + * validate input with expected rules + * + * @param {string|object} input - target + * @param {string|regexp|object|function|array} expected - rules + * @return {boolean} pass or not + */ +export function validate(input, expected) { + if (Array.isArray(expected)) { + return expected.some(rule => validate(input, rule)); + } else if (types.isRegExp(expected)) { + return expected.test(input); + } else if (types.isString(expected)) { + return input && input.includes(expected); + } else if (types.isObject(expected)) { + return isMatch(input, expected); + } + return expected(input); +} + +/** + * Check whether is parent + * + * @param {string} parent - parent file path + * @param {string} child - child file path + * @return {boolean} true if parent >= child + */ +export function isParent(parent: string, child: string): boolean { + const p = path.relative(parent, child); + return !(p === '' || p.startsWith('..')); +} + +/** + * mkdirp -p + * + * @param {string} dir - dir path + * @param {object} [opts] - see fsPromises.mkdirp + */ +export async function mkdir(dir: string, opts?: any) { + return await fs.mkdir(dir, { recursive: true, ...opts }); +} + +/** + * removes files and directories. + * + * by default it will only moves them to the trash, which is much safer and reversible. + * + * @param {string|string[]} p - accepts paths and [glob patterns](https://github.com/sindresorhus/globby#globbing-patterns) + * @param {object} [opts] - options of [trash](https://github.com/sindresorhus/trash) or [fsPromises.rm](https://nodejs.org/api/fs.html#fs_fspromises_rm_path_options) + * @param {boolean} [opts.trash=true] - whether to move to [trash](https://github.com/sindresorhus/trash) or permanently delete + */ +export async function rm(p, opts = {}) { + /* istanbul ignore if */ + if (opts.trash === false || process.env.CI) { + return await fs.rm(p, { force: true, recursive: true, ...opts }); + } + /* istanbul ignore next */ + return await trash(p, opts); +} + + +/** + * write file, will auto create parent dir + * + * @param {string} filePath - file path + * @param {string|object} content - content to write, if pass object, will `JSON.stringify` + * @param {object} [opts] - see fsPromises.writeFile + */ +export async function writeFile(filePath: string, content: string | Record, opts?: any) { + await mkdir(path.dirname(filePath)); + if (types.isObject(content)) { + content = JSON.stringify(content, null, 2); + } + return await fs.writeFile(filePath, content, opts); +} + +/** + * check exists due to `fs.exists` is deprecated + */ +export async function exists(filePath: string) { + return await fs.access(filePath).then(() => true).catch(() => false); +} + +/** + * resolve file path by import.meta, kind of __dirname for esm + * + * @param {Object} meta - import.meta + * @param {...String} args - other paths + * @return {String} file path + */ +export function resolve(meta, ...args) { + const p = types.isObject(meta) ? dirname(meta) : meta; + return path.resolve(p, ...args); +} + +/** + * take a sleep + * + * @param {number} ms - millisecond + */ +export async function sleep(ms: number) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} diff --git a/test/test-utils.js b/test/test-utils.ts similarity index 100% rename from test/test-utils.js rename to test/test-utils.ts diff --git a/test/utils.test.js b/test/utils.test.ts similarity index 96% rename from test/utils.test.js rename to test/utils.test.ts index b7c1595..1533b0b 100644 --- a/test/utils.test.js +++ b/test/utils.test.ts @@ -2,10 +2,10 @@ import { it, describe, beforeEach } from 'vitest'; import fs from 'fs'; import path from 'path'; import { strict as assert } from 'assert'; -import * as utils from '../lib/utils.js'; +import * as utils from '../src/utils.js'; import * as testUtils from './test-utils.js'; -describe('test/utils.test.js', () => { +describe.only('test/utils.test.ts', () => { const tmpDir = testUtils.getTempDir(); beforeEach(() => testUtils.initDir(tmpDir)); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..aab23a6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@artus/tsconfig", + "compilerOptions": { + "baseUrl": ".", + "noUnusedParameters": false, + "noUnusedLocals":false, + "resolveJsonModule": true, + "outDir": "dist" + }, + "include": [ + "src/**/*.ts", + "src/**/*.json" + ] +} From f3d690fbd5dcc0b080f2cf12414cc8de443e1ab4 Mon Sep 17 00:00:00 2001 From: TZ Date: Tue, 20 Dec 2022 12:08:18 +0800 Subject: [PATCH 02/14] f --- package.json | 25 +---- src/assert.ts | 142 ++++++++++++++++++++++++ src/utils.ts | 6 +- test/{assert.test.js => assert.test.ts} | 5 +- 4 files changed, 149 insertions(+), 29 deletions(-) create mode 100644 src/assert.ts rename test/{assert.test.js => assert.test.ts} (97%) diff --git a/package.json b/package.json index 1bf55a2..14739cd 100644 --- a/package.json +++ b/package.json @@ -43,34 +43,11 @@ "scripts": { "lint": "eslint . --ext .ts", "postlint": "tsc --noEmit", - "test": "vitest", + "test": "vitest run", "cov": "vitest run --coverage", "ci": "npm run cov", "tsc": "rm -rf dist && tsc", "prepack": "npm run tsc" }, - "eslintConfig": { - "extends": "eslint-config-egg", - "root": true, - "env": { - "node": true, - "browser": false, - "jest": true - }, - "rules": { - "node/file-extension-in-import": [ - "error", - "always" - ] - }, - "parserOptions": { - "sourceType": "module" - }, - "ignorePatterns": [ - "dist", - "coverage", - "node_modules" - ] - }, "license": "MIT" } diff --git a/src/assert.ts b/src/assert.ts new file mode 100644 index 0000000..379d496 --- /dev/null +++ b/src/assert.ts @@ -0,0 +1,142 @@ +import fs from 'node:fs/promises'; +import node_assert from 'node:assert/strict'; +import { match, doesNotMatch, AssertionError } from 'node:assert/strict'; + +import isMatch from 'lodash.ismatch'; +import { types, exists } from './utils.js'; + +export const assert = { + ...node_assert, + matchRule, + doesNotMatchRule, + matchFile, + doesNotMatchFile, +}; + +/** + * assert the `actual` is match `expected` + * - when `expected` is regexp, detect by `RegExp.test` + * - when `expected` is json, detect by `lodash.ismatch` + * - when `expected` is string, detect by `String.includes` + * + * @param {String|Object} actual - actual string + * @param {String|RegExp|Object} expected - rule to validate + */ +export function matchRule(actual, expected) { + if (types.isRegExp(expected)) { + match(actual.toString(), expected); + } else if (types.isObject(expected)) { + // if pattern is `json`, then convert actual to json and check whether contains pattern + const content = types.isString(actual) ? JSON.parse(actual) : actual; + const result = isMatch(content, expected); + if (!result) { + // print diff + throw new AssertionError({ + operator: 'should partial includes', + actual: content, + expected, + stackStartFn: matchRule, + }); + } + } else if (actual === undefined || !actual.includes(expected)) { + throw new AssertionError({ + operator: 'should includes', + actual, + expected, + stackStartFn: matchRule, + }); + } +} + +/** + * assert the `actual` is not match `expected` + * - when `expected` is regexp, detect by `RegExp.test` + * - when `expected` is json, detect by `lodash.ismatch` + * - when `expected` is string, detect by `String.includes` + * + * @param {String|Object} actual - actual string + * @param {String|RegExp|Object} expected - rule to validate + */ +export function doesNotMatchRule(actual, expected) { + if (types.isRegExp(expected)) { + doesNotMatch(actual.toString(), expected); + } else if (types.isObject(expected)) { + // if pattern is `json`, then convert actual to json and check whether contains pattern + const content = types.isString(actual) ? JSON.parse(actual) : actual; + const result = isMatch(content, expected); + if (result) { + // print diff + throw new AssertionError({ + operator: 'should not partial includes', + actual: content, + expected, + stackStartFn: doesNotMatchRule, + }); + } + } else if (actual === undefined || actual.includes(expected)) { + throw new AssertionError({ + operator: 'should not includes', + actual, + expected, + stackStartFn: doesNotMatchRule, + }); + } +} + +/** + * validate file + * + * - `matchFile('/path/to/file')`: check whether file exists + * - `matchFile('/path/to/file', /\w+/)`: check whether file match regexp + * - `matchFile('/path/to/file', 'usage')`: check whether file includes specified string + * - `matchFile('/path/to/file', { version: '1.0.0' })`: checke whether file content partial includes specified JSON + * + * @param {String} filePath - target path to validate, could be relative path + * @param {String|RegExp|Object} [expected] - rule to validate + * @throws {AssertionError} + */ +export async function matchFile(filePath, expected) { + // check whether file exists + const isExists = await exists(filePath); + node_assert(isExists, `Expected ${filePath} to be exists`); + + // compare content, support string/json/regex + if (expected) { + const content = await fs.readFile(filePath, 'utf-8'); + try { + matchRule(content, expected); + } catch (err) { + err.message = `file(${filePath}) with content: ${err.message}`; + throw err; + } + } +} + +/** + * validate file with opposite rule + * + * - `doesNotMatchFile('/path/to/file')`: check whether file don't exists + * - `doesNotMatchFile('/path/to/file', /\w+/)`: check whether file don't match regex + * - `doesNotMatchFile('/path/to/file', 'usage')`: check whether file don't includes specified string + * - `doesNotMatchFile('/path/to/file', { version: '1.0.0' })`: checke whether file content don't partial includes specified JSON + * + * @param {String} filePath - target path to validate, could be relative path + * @param {String|RegExp|Object} [expected] - rule to validate + * @throws {AssertionError} + */ +export async function doesNotMatchFile(filePath, expected) { + // check whether file exists + const isExists = await exists(filePath); + if (!expected) { + node_assert(!isExists, `Expected ${filePath} to not be exists`); + } else { + node_assert(isExists, `Expected file(${filePath}) not to match \`${expected}\` but file not exists`); + const content = await fs.readFile(filePath, 'utf-8'); + try { + doesNotMatchRule(content, expected); + } catch (err) { + err.message = `file(${filePath}) with content: ${err.message}`; + throw err; + } + } +} diff --git a/src/utils.ts b/src/utils.ts index c77c815..746f56c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ -import { promises as fs } from 'fs'; -import util from 'util'; -import path from 'path'; +import fs from 'node:fs/promises'; +import util from 'node:util'; +import path from 'node:path'; import { dirname } from 'dirname-filename-esm'; import isMatch from 'lodash.ismatch'; diff --git a/test/assert.test.js b/test/assert.test.ts similarity index 97% rename from test/assert.test.js rename to test/assert.test.ts index 1c56fa4..2f2cf7c 100644 --- a/test/assert.test.js +++ b/test/assert.test.ts @@ -1,9 +1,9 @@ import path from 'path'; import { it, describe } from 'vitest'; -import { assert, matchRule, doesNotMatchRule } from '../lib/assert.js'; +import { assert, matchRule, doesNotMatchRule } from '../src/assert.js'; -describe('test/assert.test.js', () => { +describe('test/assert.test.ts', () => { const pkgInfo = { name: 'clet', version: '1.0.0', @@ -12,6 +12,7 @@ describe('test/assert.test.js', () => { }, }; + assert.deepEqual(1, 2); it('should export', () => { assert.equal(assert.matchRule, matchRule); assert.equal(assert.doesNotMatchRule, doesNotMatchRule); From 5c8dae4ce69596341ba18a471913fbfb4618cb97 Mon Sep 17 00:00:00 2001 From: TZ Date: Thu, 22 Dec 2022 16:02:50 +0800 Subject: [PATCH 03/14] feat: runner --- lib/runner.js | 4 +- package.json | 5 +- src/assert.ts | 39 +++----- src/constant.ts | 10 +++ src/logger.ts | 113 ++++++++++++++++++++++++ src/runner.ts | 113 ++++++++++++++++++++++++ src/types.ts | 11 +++ src/validator.ts | 5 ++ test/assert.test.ts | 36 ++++---- test/{logger.test.js => logger.test.ts} | 6 +- 10 files changed, 286 insertions(+), 56 deletions(-) create mode 100644 src/constant.ts create mode 100644 src/logger.ts create mode 100644 src/runner.ts create mode 100644 src/types.ts create mode 100644 src/validator.ts rename test/{logger.test.js => logger.test.ts} (95%) diff --git a/lib/runner.js b/lib/runner.js index 4a8fb62..24e3875 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -5,10 +5,10 @@ import stripAnsi from 'strip-ansi'; import stripFinalNewline from 'strip-final-newline'; import { pEvent } from 'p-event'; import { compose } from 'throwback'; +import consola from 'consola'; import * as utils from './utils.js'; import { assert } from './assert.js'; -import { Logger, LogLevel } from './logger.js'; import * as validatorPlugin from './validator.js'; import * as operationPlugin from './operation.js'; @@ -22,7 +22,7 @@ class TestRunner extends EventEmitter { this.assert = assert; this.utils = utils; - this.logger = new Logger({ tag: 'CLET' }); + this.logger = consola.withDefaults({ tag: 'CLET' }); this.childLogger = this.logger.child('PROC', { indent: 4, showTag: false }); // middleware.pre -> before -> fork -> running -> after -> end -> middleware.post -> cleanup diff --git a/package.json b/package.json index 14739cd..07f3e34 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "homepage": "https://github.com/node-modules/clet", "repository": "git@github.com:node-modules/clet.git", "dependencies": { + "consola": "^2.15.3", "dirname-filename-esm": "^1.1.1", "dot-prop": "^7.2.0", "execa": "^6.1.0", @@ -32,10 +33,10 @@ "eslint": "^7", "eslint-config-egg": "^9", "supertest": "^6.2.3", - "vitest": "^0.22.1", "ts-node": "^10.9.1", "tslib": "^2.4.0", - "typescript": "^4.8.2" + "typescript": "^4.8.2", + "vitest": "^0.22.1" }, "files": [ "dist" diff --git a/src/assert.ts b/src/assert.ts index 379d496..9154e4d 100644 --- a/src/assert.ts +++ b/src/assert.ts @@ -1,28 +1,20 @@ import fs from 'node:fs/promises'; -import node_assert from 'node:assert/strict'; +import assert from 'node:assert/strict'; import { match, doesNotMatch, AssertionError } from 'node:assert/strict'; import isMatch from 'lodash.ismatch'; import { types, exists } from './utils.js'; -export const assert = { - ...node_assert, - matchRule, - doesNotMatchRule, - matchFile, - doesNotMatchFile, -}; +type Actual = string | number | Record; +type Expected = string | RegExp | Record; /** * assert the `actual` is match `expected` * - when `expected` is regexp, detect by `RegExp.test` * - when `expected` is json, detect by `lodash.ismatch` * - when `expected` is string, detect by `String.includes` - * - * @param {String|Object} actual - actual string - * @param {String|RegExp|Object} expected - rule to validate */ -export function matchRule(actual, expected) { +export function matchRule(actual: Actual, expected: Expected) { if (types.isRegExp(expected)) { match(actual.toString(), expected); } else if (types.isObject(expected)) { @@ -53,11 +45,8 @@ export function matchRule(actual, expected) { * - when `expected` is regexp, detect by `RegExp.test` * - when `expected` is json, detect by `lodash.ismatch` * - when `expected` is string, detect by `String.includes` - * - * @param {String|Object} actual - actual string - * @param {String|RegExp|Object} expected - rule to validate */ -export function doesNotMatchRule(actual, expected) { +export function doesNotMatchRule(actual: Actual, expected: Expected) { if (types.isRegExp(expected)) { doesNotMatch(actual.toString(), expected); } else if (types.isObject(expected)) { @@ -90,15 +79,11 @@ export function doesNotMatchRule(actual, expected) { * - `matchFile('/path/to/file', /\w+/)`: check whether file match regexp * - `matchFile('/path/to/file', 'usage')`: check whether file includes specified string * - `matchFile('/path/to/file', { version: '1.0.0' })`: checke whether file content partial includes specified JSON - * - * @param {String} filePath - target path to validate, could be relative path - * @param {String|RegExp|Object} [expected] - rule to validate - * @throws {AssertionError} */ -export async function matchFile(filePath, expected) { +export async function matchFile(filePath: string, expected?: Expected) { // check whether file exists const isExists = await exists(filePath); - node_assert(isExists, `Expected ${filePath} to be exists`); + assert(isExists, `Expected ${filePath} to be exists`); // compare content, support string/json/regex if (expected) { @@ -119,18 +104,14 @@ export async function matchFile(filePath, expected) { * - `doesNotMatchFile('/path/to/file', /\w+/)`: check whether file don't match regex * - `doesNotMatchFile('/path/to/file', 'usage')`: check whether file don't includes specified string * - `doesNotMatchFile('/path/to/file', { version: '1.0.0' })`: checke whether file content don't partial includes specified JSON - * - * @param {String} filePath - target path to validate, could be relative path - * @param {String|RegExp|Object} [expected] - rule to validate - * @throws {AssertionError} */ -export async function doesNotMatchFile(filePath, expected) { +export async function doesNotMatchFile(filePath: string, expected?: Expected) { // check whether file exists const isExists = await exists(filePath); if (!expected) { - node_assert(!isExists, `Expected ${filePath} to not be exists`); + assert(!isExists, `Expected ${filePath} to not be exists`); } else { - node_assert(isExists, `Expected file(${filePath}) not to match \`${expected}\` but file not exists`); + assert(isExists, `Expected file(${filePath}) not to match \`${expected}\` but file not exists`); const content = await fs.readFile(filePath, 'utf-8'); try { doesNotMatchRule(content, expected); diff --git a/src/constant.ts b/src/constant.ts new file mode 100644 index 0000000..77321ae --- /dev/null +++ b/src/constant.ts @@ -0,0 +1,10 @@ +import { EOL } from 'os'; + +export const KEYS = { + UP: '\u001b[A', + DOWN: '\u001b[B', + LEFT: '\u001b[D', + RIGHT: '\u001b[C', + ENTER: EOL, + SPACE: ' ', +}; diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..605833e --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,113 @@ +import assert from 'node:assert/strict'; +import { info } from 'node:console'; +import util from 'node:util'; + +export enum LogLevel { + ERROR = 0, + WARN = 1, + LOG = 2, + INFO = 3, + DEBUG = 4, + TRACE = 5, + Silent = -Infinity, + Verbose = Infinity, +} + +export interface LoggerOptions { + level?: LogLevel; + tag?: string | string[]; + showTag?: boolean; + showTime?: boolean; + indent?: number; +} + +type LogMethods = { + [key in Lowercase]: (message: any, ...args: any[]) => void; +}; + +export interface Logger extends LogMethods { } + +export class Logger { + private options: LoggerOptions; + private childMaps: Record; + + // Declare the type of the dynamically-registered methods + // [key in Lowercase]: (message: any, ...args: any[]) => void; + + constructor(tag?: string | LoggerOptions, opts: LoggerOptions = {}) { + if (typeof tag === 'string') { + opts.tag = opts.tag || tag || ''; + } else { + opts = tag; + } + opts.tag = [].concat(opts.tag || []); + + this.options = { + level: LogLevel.INFO, + indent: 0, + showTag: true, + showTime: false, + ...opts, + }; + + this.childMaps = {}; + + // register methods + for (const [ key, value ] of Object.entries(LogLevel)) { + const fnName = key.toLowerCase(); + const fn = console[fnName] || console.debug; + this[fnName] = (message: any, ...args: any[]) => { + if (value > this.options.level) return; + const msg = this.format(message, args, this.options); + return fn(msg); + }; + } + + return this as unknown as typeof Logger.prototype & LogMethods; + } + + format(message: any, args: any[], options?: LoggerOptions) { + const time = options.showTime ? `[${formatTime(new Date())}] ` : ''; + const tag = options.showTag && options.tag.length ? `[${options.tag.join(':')}] ` : ''; + const indent = ' '.repeat(options.indent); + const prefix = time + indent + tag; + const content = util.format(message, ...args).replace(/^/gm, prefix); + return content; + } + + get level() { + return this.options.level; + } + + set level(v: number | string) { + this.options.level = normalize(v); + } + + child(tag: string, opts?: LoggerOptions) { + assert(tag, 'tag is required'); + if (!this.childMaps[tag]) { + this.childMaps[tag] = new Logger({ + ...this.options, + indent: this.options.indent + 2, + ...opts, + tag: [ ...this.options.tag, tag ], + }); + } + return this.childMaps[tag]; + } +} + +function normalize(level: number | string) { + if (typeof level === 'number') return level; + const levelNum = LogLevel[level.toUpperCase()]; + assert(levelNum, `unknown loglevel ${level}`); + return levelNum; +} + +function formatTime(date: Date) { + date = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); + return date.toISOString() + .replace('T', ' ') + .replace(/\..+$/, ''); +} + diff --git a/src/runner.ts b/src/runner.ts new file mode 100644 index 0000000..e1b5a4b --- /dev/null +++ b/src/runner.ts @@ -0,0 +1,113 @@ +import EventEmitter from 'events'; + +import { MountPlugin, PluginLike, AsyncFunction, RestParam } from './types'; + +// interface Pluginable { +// [key: string]: (runner: T, options?: any) => AsyncFunction; +// } + +export class TestRunner extends EventEmitter { + private logger = console; + private middlewares: any[] = []; + private hooks = { + // before: [], + // running: [], + // after: [], + prerun: [], + run: [], + postrun: [], + end: [], + }; + + // prerun 准备现场环境 + // run 处理 stdin + // postrun 检查 assert + // end 检查 code,清理现场 + + plugin(plugins: PluginLike): MountPlugin { + for (const key of Object.keys(plugins)) { + const initFn = plugins[key]; + + this[key] = (...args: RestParam) => { + console.log('mount %s with %j', key, ...args); + this.use(initFn(this, ...args)); + return this; + }; + } + return this as any; + } + + hook(event: string, fn: AsyncFunction) { + this.hooks[event].push(fn); + return this; + } + + async runHook(event: string) { + for (const fn of this.hooks[event]) { + // TODO: ctx + await fn(); + } + } + + someMethod() { + console.log('someMethod'); + } + + // hook? + use(fn: AsyncFunction) { + this.middlewares.push(fn); + return this; + } + + end() { + try { + // prerun + // run + // postrun + // end + this.logger.info('✔ Test pass.\n'); + } catch (err) { + this.logger.error('⚠ Test failed.\n'); + throw err; + } finally { + // clean up + } + + // prepare/prerun + // - init dir, init env, init ctx + // run + // - run cli + // - collect stdout/stderr, emit event + // - stdin (expect) + // postrun + // - wait event(end, message, error, stdout, stderr) + // - check assert + // end + // - clean up, kill, log result, error hander + + console.log(this.middlewares); + return Promise.all(this.middlewares.map(fn => fn())); + } +} + + +function file(runner: TestRunner, opts: { a: string }) { + runner.someMethod(); + return async function fileMiddleare() { + console.log('run file'); + }; +} + +function sleep(runner: TestRunner, b: number) { + // console.log('sleep init', b); + return async function sleepMiddleare() { + console.log('run sleep'); + }; +} + +new TestRunner() + .plugin({ file, sleep }) + .file({ 'a': 'b' }) + .sleep(1) + .sleep(222) + .end().then(() => console.log('done')); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..ec4668b --- /dev/null +++ b/src/types.ts @@ -0,0 +1,11 @@ +export type MountPlugin = { + [key in keyof T]: T[key] extends (core: Core, ...args: infer I) => any ? (...args: I) => MountPlugin : T[key] +} & Core; + +export type AsyncFunction = (...args: any[]) => Promise; + +export type RestParam = T extends (first: any, ...args: infer R) => any ? R : any; + +export interface PluginLike { + [key: string]: (core: any, options?: any) => AsyncFunction; +} diff --git a/src/validator.ts b/src/validator.ts new file mode 100644 index 0000000..b298a00 --- /dev/null +++ b/src/validator.ts @@ -0,0 +1,5 @@ +export function expect(runner, fn) { + runner.hook('run', async () => { + await fn(runner); + }); +} diff --git a/test/assert.test.ts b/test/assert.test.ts index 2f2cf7c..1b3c88a 100644 --- a/test/assert.test.ts +++ b/test/assert.test.ts @@ -1,7 +1,9 @@ -import path from 'path'; +import path from 'node:path'; +import assert from 'node:assert/strict'; + import { it, describe } from 'vitest'; -import { assert, matchRule, doesNotMatchRule } from '../src/assert.js'; +import { matchRule, doesNotMatchRule, matchFile, doesNotMatchFile } from '../src/assert.js'; describe('test/assert.test.ts', () => { const pkgInfo = { @@ -12,12 +14,6 @@ describe('test/assert.test.ts', () => { }, }; - assert.deepEqual(1, 2); - it('should export', () => { - assert.equal(assert.matchRule, matchRule); - assert.equal(assert.doesNotMatchRule, doesNotMatchRule); - }); - describe('matchRule', () => { it('should support regexp', () => { matchRule(123456, /\d+/); @@ -126,19 +122,19 @@ describe('test/assert.test.ts', () => { describe('matchFile', () => { const fixtures = path.resolve('test/fixtures/file'); it('should check exists', async () => { - await assert.matchFile(`${fixtures}/test.md`); + await matchFile(`${fixtures}/test.md`); await assert.rejects(async () => { - await assert.matchFile(`${fixtures}/not-exist.md`); + await matchFile(`${fixtures}/not-exist.md`); }, /not-exist.md to be exists/); }); it('should check content', async () => { - await assert.matchFile(`${fixtures}/test.md`, 'this is a README'); - await assert.matchFile(`${fixtures}/test.md`, /this is a README/); - await assert.matchFile(`${fixtures}/test.json`, { name: 'test', config: { port: 8080 } }); + await matchFile(`${fixtures}/test.md`, 'this is a README'); + await matchFile(`${fixtures}/test.md`, /this is a README/); + await matchFile(`${fixtures}/test.json`, { name: 'test', config: { port: 8080 } }); await assert.rejects(async () => { - await assert.matchFile(`${fixtures}/test.md`, 'abc'); + await matchFile(`${fixtures}/test.md`, 'abc'); }, /file.*test\.md.*this is.*should includes 'abc'/); }); }); @@ -146,20 +142,20 @@ describe('test/assert.test.ts', () => { describe('doesNotMatchFile', () => { const fixtures = path.resolve('test/fixtures/file'); it('should check not exists', async () => { - await assert.doesNotMatchFile(`${fixtures}/a/b/c/d.md`); + await doesNotMatchFile(`${fixtures}/a/b/c/d.md`); await assert.rejects(async () => { - await assert.doesNotMatchFile(`${fixtures}/not-exist.md`, 'abc'); + await doesNotMatchFile(`${fixtures}/not-exist.md`, 'abc'); }, /Expected file\(.*not-exist.md\) not to match.*but file not exists/); }); it('should check not content', async () => { - await assert.doesNotMatchFile(`${fixtures}/test.md`, 'abc'); - await assert.doesNotMatchFile(`${fixtures}/test.md`, /abcccc/); - await assert.doesNotMatchFile(`${fixtures}/test.json`, { name: 'test', config: { a: 1 } }); + await doesNotMatchFile(`${fixtures}/test.md`, 'abc'); + await doesNotMatchFile(`${fixtures}/test.md`, /abcccc/); + await doesNotMatchFile(`${fixtures}/test.json`, { name: 'test', config: { a: 1 } }); await assert.rejects(async () => { - await assert.doesNotMatchFile(`${fixtures}/test.md`, 'this is a README'); + await doesNotMatchFile(`${fixtures}/test.md`, 'this is a README'); }, /file.*test\.md.*this is.*should not includes 'this is a README'/); }); }); diff --git a/test/logger.test.js b/test/logger.test.ts similarity index 95% rename from test/logger.test.js rename to test/logger.test.ts index 87acb85..d88de6c 100644 --- a/test/logger.test.js +++ b/test/logger.test.ts @@ -1,10 +1,10 @@ import { it, describe, beforeEach, afterEach, expect, vi } from 'vitest'; -import { Logger, LogLevel } from '../lib/logger.js'; +import { Logger, LogLevel } from '../src/logger.js'; -describe('test/logger.test.js', () => { +describe.skip('test/logger.test.js', () => { beforeEach(() => { - for (const name of [ 'error', 'warn', 'info', 'log', 'debug' ]) { + for (const name of [ 'error', 'warn', 'info', 'log', 'debug' ] as const) { vi.spyOn(global.console, name); } }); From 6ea9c97d30fa7b71d9e2893965623b48d99761cd Mon Sep 17 00:00:00 2001 From: TZ Date: Fri, 23 Dec 2022 18:27:15 +0800 Subject: [PATCH 04/14] f --- package.json | 8 +- src/process.ts | 160 +++++++++++++++++++++++ src/runner.ts | 56 +++++--- src/try/child.ts | 29 ++++ src/try/main.ts | 41 ++++++ src/types.ts | 4 +- src/validator.ts | 22 +++- test.log | 0 test/fixtures/color.ts | 8 ++ test/fixtures/{version.js => version.ts} | 0 test/process.test.ts | 73 +++++++++++ 11 files changed, 377 insertions(+), 24 deletions(-) create mode 100644 src/process.ts create mode 100644 src/try/child.ts create mode 100644 src/try/main.ts create mode 100644 test.log create mode 100644 test/fixtures/color.ts rename test/fixtures/{version.js => version.ts} (100%) create mode 100644 test/process.test.ts diff --git a/package.json b/package.json index 07f3e34..74bf68c 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,11 @@ "consola": "^2.15.3", "dirname-filename-esm": "^1.1.1", "dot-prop": "^7.2.0", - "execa": "^6.1.0", + "execa": "^5", "lodash.ismatch": "^4.4.0", - "p-event": "^5.0.1", - "strip-ansi": "^7.0.1", - "strip-final-newline": "^3.0.0", + "p-event": "^4", + "strip-ansi": "^6", + "strip-final-newline": "^2", "throwback": "^4.1.0", "trash": "^8.1.0" }, diff --git a/src/process.ts b/src/process.ts new file mode 100644 index 0000000..8615e64 --- /dev/null +++ b/src/process.ts @@ -0,0 +1,160 @@ +import assert from 'node:assert/strict'; +import EventEmitter from 'node:events'; +import { PassThrough } from 'node:stream'; + +import * as execa from 'execa'; +import pEvent from 'p-event'; +import stripFinalNewline from 'strip-final-newline'; +import stripAnsi from 'strip-ansi'; +import { EOL } from 'node:os'; + +export interface ProcessResult { + code: number; + stdout: string; + stderr: string; +} + +export type ProcessOptions = { + -readonly [ key in keyof execa.NodeOptions ]: execa.NodeOptions[key]; +} & { + execArgv?: execa.NodeOptions['nodeOptions']; +}; + +export class Process extends EventEmitter { + type: 'fork' | 'spawn'; + cmd: string; + args: string[]; + opts: ProcessOptions; + result: ProcessResult; + proc: execa.ExecaChildProcess; + + constructor(type: Process['type'], cmd: string, args: string[] = [], opts: ProcessOptions = {}) { + super(); + // assert(!this.cmd, 'cmd can not be registered twice'); + + this.type = type; + this.cmd = cmd; + this.args = args; + // this.cwd = opts?.cwd || process.cwd(); + // this.env = opts?.env || process.env; + + const { execArgv, nodeOptions, ...restOpts } = opts; + // TODO: execArgv nodeOptions only allow once and in fork mode + + this.opts = { + reject: false, + cwd: process.cwd(), + nodeOptions: execArgv || nodeOptions, + input: new PassThrough(), + preferLocal: true, + ...restOpts, + }; + + // stdout stderr use passthrough, so don't need to on event and recollect + // need to test color + + this.result = { + code: undefined, + stdout: '', + stderr: '', + }; + } + + write(data: string) { + // FIXME: when stdin.write, stdout will recieve duplicate output + // auto add \n + this.proc.stdin.write(data.replace(/\r?\n$/, '') + EOL); + // (this.opts.input as PassThrough).write(data); + // (this.opts.stdin as Readable).write(data); + + // hook rl event to find whether prompt is trigger? + } + + env(key: string, value: string) { + this.opts.env[key] = value; + } + + cwd(cwd: string) { + this.opts.cwd = cwd; + } + + async exec() { + if (this.type === 'fork') { + this.proc = execa.node(this.cmd, this.args, this.opts); + } else { + const cmdString = [ this.cmd, ...this.args ].join(' '); + this.proc = execa.command(cmdString, this.opts); + } + + // this.proc.stdin.setEncoding('utf8'); + + this.proc.stdout.on('data', data => { + const origin = stripFinalNewline(data.toString()); + const content = stripAnsi(origin); + this.result.stdout += content; + // console.log('stdout', origin); + 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); + }); + + this.proc.on('message', data => { + this.emit('message', data); + // console.log('message event:', data); + }); + + this.proc.once('exit', code => { + this.result.code = code; + // console.log('close event:', code); + }); + + // this.proc.once('close', code => { + // // this.emit('close', code); + // this.result.code = code; + // // console.log('close event:', code); + // }); + + return this.proc; + } + + // stdin -> wait(stdout) -> write + async wait(type, expected) { + let promise; + switch (type) { + case 'stdout': + case 'stderr': { + promise = pEvent(this.proc[type], 'data', { + rejectionEvents: ['close'], + filter: () => { + return expected.test(this.result[type]); + }, + })//.then(() => this.result[type]); + break; + } + + + case 'message': { + promise = pEvent(this.proc, 'message', { + rejectionEvents: ['close'], + // filter: input => utils.validate(input, expected), + }); + break; + } + + case 'close': + default: { + promise = pEvent(this.proc, 'close')//.then(() => this.result); + break; + } + } + return promise; + } +} + +// use readable stream to write stdin diff --git a/src/runner.ts b/src/runner.ts index e1b5a4b..0e27238 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -13,16 +13,18 @@ export class TestRunner extends EventEmitter { // before: [], // running: [], // after: [], + prepare: [], prerun: [], run: [], postrun: [], end: [], }; - // prerun 准备现场环境 + // prepare 准备现场环境 + // prerun 检查参数,在 fork 定义之后 // run 处理 stdin // postrun 检查 assert - // end 检查 code,清理现场 + // end 检查 code,清理现场,相当于 finnaly plugin(plugins: PluginLike): MountPlugin { for (const key of Object.keys(plugins)) { @@ -30,7 +32,7 @@ export class TestRunner extends EventEmitter { this[key] = (...args: RestParam) => { console.log('mount %s with %j', key, ...args); - this.use(initFn(this, ...args)); + initFn(this, ...args); return this; }; } @@ -42,10 +44,10 @@ export class TestRunner extends EventEmitter { return this; } - async runHook(event: string) { + async runHook(event: string, ctx) { for (const fn of this.hooks[event]) { // TODO: ctx - await fn(); + await fn(ctx); } } @@ -59,12 +61,21 @@ export class TestRunner extends EventEmitter { return this; } - end() { + async end() { try { + const ctx = { a: 1}; // prerun + await this.runHook('prerun', ctx); + // run + await this.runHook('run', ctx); + // postrun + await this.runHook('postrun', ctx); + // end + await this.runHook('end', ctx); + this.logger.info('✔ Test pass.\n'); } catch (err) { this.logger.error('⚠ Test failed.\n'); @@ -85,29 +96,40 @@ export class TestRunner extends EventEmitter { // end // - clean up, kill, log result, error hander - console.log(this.middlewares); - return Promise.all(this.middlewares.map(fn => fn())); + // console.log(this.middlewares); + // return Promise.all(this.middlewares.map(fn => fn())); } } +function fork(runner: TestRunner, cmd, args, opts) { + runner.hook('prerun', async ctx => { + ctx.cmd = cmd; + ctx.args = args; + ctx.opts = opts; + console.log('run fork', cmd, args, opts); + }); +} + function file(runner: TestRunner, opts: { a: string }) { - runner.someMethod(); - return async function fileMiddleare() { - console.log('run file'); - }; + runner.hook('postrun', async ctx => { + console.log('run file', ctx, opts); + }); } function sleep(runner: TestRunner, b: number) { - // console.log('sleep init', b); - return async function sleepMiddleare() { - console.log('run sleep'); - }; + runner.hook('postrun', async ctx => { + console.log('run sleep', ctx, b); + }); } new TestRunner() - .plugin({ file, sleep }) + .plugin({ file, sleep, fork }) .file({ 'a': 'b' }) + .fork('node', '-v') .sleep(1) .sleep(222) .end().then(() => console.log('done')); + +// koa middleware +// 初始化 -> fork -> await next() -> 校验 -> 结束 diff --git a/src/try/child.ts b/src/try/child.ts new file mode 100644 index 0000000..60c4462 --- /dev/null +++ b/src/try/child.ts @@ -0,0 +1,29 @@ +import * as readline from 'node:readline'; +import { stdin as input, stdout as output } from 'node:process'; + +const rl = readline.createInterface({ input, output }); + +rl.on('pause', () => { + console.log('Readline paused.'); +}); + +rl.on('resume', () => { + console.log('Readline resumed.'); +}); + +rl.question('What is your favorite food? ', (answer) => { + console.log(`Oh, so your favorite food is ${answer}`); + let i = 0; + + rl.close(); + // const id = setInterval(() => { + // i++; + // console.log(`#${i}.`, new Date()); + // }, 1000); + + // setTimeout(() => { + // console.error('some error'); + // console.log('end'); + // clearInterval(id); + // }, 1000 * 10); +}); diff --git a/src/try/main.ts b/src/try/main.ts new file mode 100644 index 0000000..3371d81 --- /dev/null +++ b/src/try/main.ts @@ -0,0 +1,41 @@ +// import execa from 'execa'; + +// async function run() { +// console.log(execa); +// const proc = execa.node('src/try/child.ts', ['--foo', 'bar'], { +// cwd: process.cwd(), +// nodeOptions: ['--inspect-brk'], +// }); +// proc.stdout.on('data', (data) => { +// console.log('stdout', data.toString()); +// }); +// proc.stderr.on('data', (data) => { +// console.log('stderr', data.toString()); +// }); +// const result = await proc; +// console.log('end', result); +// } + +// run().catch(console.error); + +import { Process } from '../process'; + +async function run() { + const proc = new Process('fork', 'src/try/child.ts', ['--foo', 'bar'], { + cwd: process.cwd(), + // nodeOptions: ['--inspect-brk'], + }); + + const x = proc.exec(); + const y = await proc.wait('stdout', /What is your/); + proc.write('sss'); + const z = await proc.wait('stderr', /error/); + + console.log('@@', y.toString()) + console.log('@@@@', z.toString()) + // proc.write('hello'); + // proc.write('world'); + await x; +} + +run().catch(console.error); diff --git a/src/types.ts b/src/types.ts index ec4668b..beb97bc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ export type MountPlugin = { - [key in keyof T]: T[key] extends (core: Core, ...args: infer I) => any ? (...args: I) => MountPlugin : T[key] + [key in keyof T]: T[key] extends (core: Core, ...args: infer I) => any ? (...args: I) => MountPlugin : T[key]; } & Core; export type AsyncFunction = (...args: any[]) => Promise; @@ -7,5 +7,5 @@ export type AsyncFunction = (...args: any[]) => Promise; export type RestParam = T extends (first: any, ...args: infer R) => any ? R : any; export interface PluginLike { - [key: string]: (core: any, options?: any) => AsyncFunction; + [key: string]: (core: any, ...args: any[]) => void; } diff --git a/src/validator.ts b/src/validator.ts index b298a00..35013cb 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -1,5 +1,25 @@ export function expect(runner, fn) { - runner.hook('run', async () => { + runner.hook('postrun', async () => { await fn(runner); }); } + +export function stdout(runner, expected) { + assert(expected, '`expected` is required'); + expect(runner, async function stdout({ result, assert }) { + assert.matchRule(result.stdout, expected); + }); +} + +export function stderr(runner, expected) { + runner.hook('postrun', async () => { + assert.matchRule(result.stdout, expected); + }); + + runner.hook({ + async prerun() { + }, + async postrun() { + }, + }); +} diff --git a/test.log b/test.log new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/color.ts b/test/fixtures/color.ts new file mode 100644 index 0000000..27fdcfa --- /dev/null +++ b/test/fixtures/color.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +const cli = '\x1b[37;1m' + 'CLI' + '\x1b[0m'; +const hub = '\x1b[43;1m' + 'Hub' + '\x1b[0m'; +const msg = '\x1b[37;1m' + 'MSG' + '\x1b[0m'; + +console.log(cli + hub); +console.warn(msg + hub); diff --git a/test/fixtures/version.js b/test/fixtures/version.ts similarity index 100% rename from test/fixtures/version.js rename to test/fixtures/version.ts diff --git a/test/process.test.ts b/test/process.test.ts new file mode 100644 index 0000000..5b52dfc --- /dev/null +++ b/test/process.test.ts @@ -0,0 +1,73 @@ +import path from 'node:path'; +import assert from 'node:assert/strict'; +import { PassThrough } from 'node:stream'; +import { it, describe } from 'vitest'; +import execa from 'execa'; +import { Process } from '../src/process.js'; +import fs from 'fs'; + +describe('test/process.test.ts', () => { + describe('options', () => { + it('should merge options', () => { + const proc = new Process('fork', 'src/try/child.ts', ['--foo', 'bar'], { + nodeOptions: ['--inspect-brk'], + }); + + assert.strictEqual(proc.opts.cwd, process.cwd()); + assert.strictEqual(proc.opts.nodeOptions?.[0], '--inspect-brk'); + }); + }); + + it('should spawn', async () => { + const proc = new Process('spawn', 'node', ['-p', 'process.version', '--inspect'], {}); + await proc.exec(); + // console.log(proc.result); + assert.match(proc.result.stdout, /v\d+\.\d+\.\d+/); + assert.match(proc.result.stderr, /Debugger listening on/); + assert.strictEqual(proc.result.code, 0); + }); + + it('should fork', async () => { + const cli = path.resolve(__dirname, 'fixtures/version.ts'); + const proc = new Process('fork', cli, [], { nodeOptions: ['--inspect'] }); + await proc.exec(); + // console.log(proc.result); + assert.match(proc.result.stdout, /v\d+\.\d+\.\d+/); + assert.match(proc.result.stderr, /Debugger listening on/); + assert.strictEqual(proc.result.code, 0); + }); + + it('should strip color', async () => { + const cli = path.resolve(__dirname, 'fixtures/color.ts'); + const proc = new Process('fork', cli, []); + await proc.exec(); + console.log(proc.result); + assert.match(proc.result.stdout, /CLIHub/); + assert.match(proc.result.stderr, /MSGHub/); + assert.strictEqual(proc.result.code, 0); + }); + + + it.skip('should execa work', async () => { + const proc = execa.node('src/try/child.ts', ['--foo', 'bar'], { + nodeOptions: [ '--require', 'ts-node/register' ], + }); + + proc.stdout.on('data', data => { + console.log('stdout', data.toString()); + }); + proc.stdin.setEncoding('utf8'); + + // const stdin = new PassThrough(); + // stdin.pipe(proc.stdin); + + setTimeout(() => { + console.log('write stdin'); + proc.stdin.write('hello\n'); + proc.stdin?.end(); + // stdin.end(); + }, 1500); + + await proc; + }); +}); From 90703d2f6b76f6e4bdaecd5d452a934fa154fa6c Mon Sep 17 00:00:00 2001 From: TZ Date: Sat, 24 Dec 2022 22:46:35 +0800 Subject: [PATCH 05/14] f --- test/fixtures/{ => process}/color.ts | 0 test/fixtures/process/error.ts | 4 ++++ test/fixtures/{version.ts => process/fork.ts} | 1 + test/fixtures/process/message.ts | 0 test/process.test.ts | 20 ++++++++++++++----- 5 files changed, 20 insertions(+), 5 deletions(-) rename test/fixtures/{ => process}/color.ts (100%) create mode 100644 test/fixtures/process/error.ts rename test/fixtures/{version.ts => process/fork.ts} (60%) create mode 100644 test/fixtures/process/message.ts diff --git a/test/fixtures/color.ts b/test/fixtures/process/color.ts similarity index 100% rename from test/fixtures/color.ts rename to test/fixtures/process/color.ts diff --git a/test/fixtures/process/error.ts b/test/fixtures/process/error.ts new file mode 100644 index 0000000..e4a0bc4 --- /dev/null +++ b/test/fixtures/process/error.ts @@ -0,0 +1,4 @@ +#!/usr/bin/env node + +console.log('this is an error test'); +throw new Error('some error'); diff --git a/test/fixtures/version.ts b/test/fixtures/process/fork.ts similarity index 60% rename from test/fixtures/version.ts rename to test/fixtures/process/fork.ts index 758fd05..8e5be4c 100644 --- a/test/fixtures/version.ts +++ b/test/fixtures/process/fork.ts @@ -1,3 +1,4 @@ #!/usr/bin/env node console.log(process.version); +console.warn('this is testing'); diff --git a/test/fixtures/process/message.ts b/test/fixtures/process/message.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/process.test.ts b/test/process.test.ts index 5b52dfc..ba9adbe 100644 --- a/test/process.test.ts +++ b/test/process.test.ts @@ -28,25 +28,35 @@ describe('test/process.test.ts', () => { }); it('should fork', async () => { - const cli = path.resolve(__dirname, 'fixtures/version.ts'); - const proc = new Process('fork', cli, [], { nodeOptions: ['--inspect'] }); + const cli = path.resolve(__dirname, 'fixtures/process/fork.ts'); + const proc = new Process('fork', cli); await proc.exec(); // console.log(proc.result); assert.match(proc.result.stdout, /v\d+\.\d+\.\d+/); - assert.match(proc.result.stderr, /Debugger listening on/); + assert.match(proc.result.stderr, /this is testing/); assert.strictEqual(proc.result.code, 0); }); it('should strip color', async () => { - const cli = path.resolve(__dirname, 'fixtures/color.ts'); + const cli = path.resolve(__dirname, 'fixtures/process/color.ts'); const proc = new Process('fork', cli, []); await proc.exec(); - console.log(proc.result); + // console.log(proc.result); assert.match(proc.result.stdout, /CLIHub/); assert.match(proc.result.stderr, /MSGHub/); assert.strictEqual(proc.result.code, 0); }); + it('should exit with fail', async () => { + const cli = path.resolve(__dirname, 'fixtures/process/error.ts'); + const proc = new Process('fork', cli, []); + await proc.exec(); + console.log(proc.result); + assert.match(proc.result.stdout, /this is an error test/); + assert.match(proc.result.stderr, /Error: some error/); + assert.strictEqual(proc.result.code, 1); + }); + it.skip('should execa work', async () => { const proc = execa.node('src/try/child.ts', ['--foo', 'bar'], { From 1793a75fb04dc1091368b5c26ce64d8df5997367 Mon Sep 17 00:00:00 2001 From: TZ Date: Sun, 25 Dec 2022 13:40:30 +0800 Subject: [PATCH 06/14] f --- src/process.ts | 5 +++ src/runner.ts | 20 +++++++++ src/try/main.ts | 41 ------------------- {test => test-old}/command.test.js | 0 {test => test-old}/commonjs.test.cjs | 0 {test => test-old}/example.test.js | 0 {test => test-old}/file.test.js | 0 .../fixtures/command/bin/cli.js | 0 .../fixtures/command/package.json | 0 .../fixtures/example/bin/cli.js | 0 .../fixtures/example/package.json | 0 {test => test-old}/fixtures/file.js | 0 test-old/fixtures/file/test.json | 7 ++++ test-old/fixtures/file/test.md | 2 + {test => test-old}/fixtures/logger.js | 0 {test => test-old}/fixtures/long-run.js | 0 {test => test-old}/fixtures/middleware.js | 0 {test => test-old}/fixtures/process.js | 0 {test => test-old}/fixtures/prompt.js | 0 {test => test-old}/fixtures/readline.js | 0 {test => test-old}/fixtures/server/bin/cli.js | 0 {test => test-old}/fixtures/server/index.js | 0 .../fixtures/server/package.json | 0 test-old/fixtures/version.js | 3 ++ {test => test-old}/fixtures/wait.js | 0 {test => test-old}/middleware.test.js | 0 {test => test-old}/operation.test.js | 0 {test => test-old}/plugin.test.js | 0 {test => test-old}/process.test.js | 0 {test => test-old}/prompt.test.js | 0 {test => test-old}/runner.test.js | 0 {test => test-old}/setup.js | 0 {test => test-old}/stack.test.js | 0 {test => test-old}/test-utils.ts | 0 {test => test-old}/wait.test.js | 0 test.log | 0 test/assert.test.ts | 1 - test/fixtures/process/message.ts | 9 ++++ .../fixtures/process/prompt.ts | 0 test/logger.test.ts | 2 +- test/process.test.ts | 9 ++-- test/runner.test.ts | 5 +++ test/utils.test.ts | 21 ++++++---- vitest.config.js | 5 ++- 44 files changed, 72 insertions(+), 58 deletions(-) delete mode 100644 src/try/main.ts rename {test => test-old}/command.test.js (100%) rename {test => test-old}/commonjs.test.cjs (100%) rename {test => test-old}/example.test.js (100%) rename {test => test-old}/file.test.js (100%) rename {test => test-old}/fixtures/command/bin/cli.js (100%) rename {test => test-old}/fixtures/command/package.json (100%) rename {test => test-old}/fixtures/example/bin/cli.js (100%) rename {test => test-old}/fixtures/example/package.json (100%) rename {test => test-old}/fixtures/file.js (100%) create mode 100644 test-old/fixtures/file/test.json create mode 100644 test-old/fixtures/file/test.md rename {test => test-old}/fixtures/logger.js (100%) rename {test => test-old}/fixtures/long-run.js (100%) rename {test => test-old}/fixtures/middleware.js (100%) rename {test => test-old}/fixtures/process.js (100%) rename {test => test-old}/fixtures/prompt.js (100%) rename {test => test-old}/fixtures/readline.js (100%) rename {test => test-old}/fixtures/server/bin/cli.js (100%) rename {test => test-old}/fixtures/server/index.js (100%) rename {test => test-old}/fixtures/server/package.json (100%) create mode 100644 test-old/fixtures/version.js rename {test => test-old}/fixtures/wait.js (100%) rename {test => test-old}/middleware.test.js (100%) rename {test => test-old}/operation.test.js (100%) rename {test => test-old}/plugin.test.js (100%) rename {test => test-old}/process.test.js (100%) rename {test => test-old}/prompt.test.js (100%) rename {test => test-old}/runner.test.js (100%) rename {test => test-old}/setup.js (100%) rename {test => test-old}/stack.test.js (100%) rename {test => test-old}/test-utils.ts (100%) rename {test => test-old}/wait.test.js (100%) delete mode 100644 test.log rename src/try/child.ts => test/fixtures/process/prompt.ts (100%) create mode 100644 test/runner.test.ts diff --git a/src/process.ts b/src/process.ts index 8615e64..fb12f5b 100644 --- a/src/process.ts +++ b/src/process.ts @@ -123,6 +123,11 @@ export class Process extends EventEmitter { return this.proc; } + kill(signal?: string) { + // TODO: kill process use cancel()? + this.proc.kill(signal); + } + // stdin -> wait(stdout) -> write async wait(type, expected) { let promise; diff --git a/src/runner.ts b/src/runner.ts index 0e27238..912f315 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -1,6 +1,9 @@ import EventEmitter from 'events'; +import assert from 'node:assert/strict'; import { MountPlugin, PluginLike, AsyncFunction, RestParam } from './types'; +import { Process, ProcessOptions } from './process'; + // interface Pluginable { // [key: string]: (runner: T, options?: any) => AsyncFunction; @@ -8,6 +11,7 @@ import { MountPlugin, PluginLike, AsyncFunction, RestParam } from './types'; export class TestRunner extends EventEmitter { private logger = console; + private proc: Process; private middlewares: any[] = []; private hooks = { // before: [], @@ -51,6 +55,18 @@ export class TestRunner extends EventEmitter { } } + fork(cmd: string, args?: string[], opts?: ProcessOptions) { + assert(!this.proc, 'cmd can not be registered twice'); + this.proc = new Process('fork', cmd, args, opts); + return this; + } + + spawn(cmd: string, args?: string[], opts?: ProcessOptions) { + assert(!this.proc, 'cmd can not be registered twice'); + this.proc = new Process('spawn', cmd, args, opts); + return this; + } + someMethod() { console.log('someMethod'); } @@ -67,6 +83,9 @@ export class TestRunner extends EventEmitter { // prerun await this.runHook('prerun', ctx); + // exec child process, don't await it + this.proc.exec(); + // run await this.runHook('run', ctx); @@ -82,6 +101,7 @@ export class TestRunner extends EventEmitter { throw err; } finally { // clean up + this.proc.kill(); } // prepare/prerun diff --git a/src/try/main.ts b/src/try/main.ts deleted file mode 100644 index 3371d81..0000000 --- a/src/try/main.ts +++ /dev/null @@ -1,41 +0,0 @@ -// import execa from 'execa'; - -// async function run() { -// console.log(execa); -// const proc = execa.node('src/try/child.ts', ['--foo', 'bar'], { -// cwd: process.cwd(), -// nodeOptions: ['--inspect-brk'], -// }); -// proc.stdout.on('data', (data) => { -// console.log('stdout', data.toString()); -// }); -// proc.stderr.on('data', (data) => { -// console.log('stderr', data.toString()); -// }); -// const result = await proc; -// console.log('end', result); -// } - -// run().catch(console.error); - -import { Process } from '../process'; - -async function run() { - const proc = new Process('fork', 'src/try/child.ts', ['--foo', 'bar'], { - cwd: process.cwd(), - // nodeOptions: ['--inspect-brk'], - }); - - const x = proc.exec(); - const y = await proc.wait('stdout', /What is your/); - proc.write('sss'); - const z = await proc.wait('stderr', /error/); - - console.log('@@', y.toString()) - console.log('@@@@', z.toString()) - // proc.write('hello'); - // proc.write('world'); - await x; -} - -run().catch(console.error); diff --git a/test/command.test.js b/test-old/command.test.js similarity index 100% rename from test/command.test.js rename to test-old/command.test.js diff --git a/test/commonjs.test.cjs b/test-old/commonjs.test.cjs similarity index 100% rename from test/commonjs.test.cjs rename to test-old/commonjs.test.cjs diff --git a/test/example.test.js b/test-old/example.test.js similarity index 100% rename from test/example.test.js rename to test-old/example.test.js diff --git a/test/file.test.js b/test-old/file.test.js similarity index 100% rename from test/file.test.js rename to test-old/file.test.js diff --git a/test/fixtures/command/bin/cli.js b/test-old/fixtures/command/bin/cli.js similarity index 100% rename from test/fixtures/command/bin/cli.js rename to test-old/fixtures/command/bin/cli.js diff --git a/test/fixtures/command/package.json b/test-old/fixtures/command/package.json similarity index 100% rename from test/fixtures/command/package.json rename to test-old/fixtures/command/package.json diff --git a/test/fixtures/example/bin/cli.js b/test-old/fixtures/example/bin/cli.js similarity index 100% rename from test/fixtures/example/bin/cli.js rename to test-old/fixtures/example/bin/cli.js diff --git a/test/fixtures/example/package.json b/test-old/fixtures/example/package.json similarity index 100% rename from test/fixtures/example/package.json rename to test-old/fixtures/example/package.json diff --git a/test/fixtures/file.js b/test-old/fixtures/file.js similarity index 100% rename from test/fixtures/file.js rename to test-old/fixtures/file.js diff --git a/test-old/fixtures/file/test.json b/test-old/fixtures/file/test.json new file mode 100644 index 0000000..54f9bac --- /dev/null +++ b/test-old/fixtures/file/test.json @@ -0,0 +1,7 @@ +{ + "name": "test", + "version": "1.0.0", + "config": { + "port": 8080 + } +} \ No newline at end of file diff --git a/test-old/fixtures/file/test.md b/test-old/fixtures/file/test.md new file mode 100644 index 0000000..73a2e53 --- /dev/null +++ b/test-old/fixtures/file/test.md @@ -0,0 +1,2 @@ +# test +this is a README \ No newline at end of file diff --git a/test/fixtures/logger.js b/test-old/fixtures/logger.js similarity index 100% rename from test/fixtures/logger.js rename to test-old/fixtures/logger.js diff --git a/test/fixtures/long-run.js b/test-old/fixtures/long-run.js similarity index 100% rename from test/fixtures/long-run.js rename to test-old/fixtures/long-run.js diff --git a/test/fixtures/middleware.js b/test-old/fixtures/middleware.js similarity index 100% rename from test/fixtures/middleware.js rename to test-old/fixtures/middleware.js diff --git a/test/fixtures/process.js b/test-old/fixtures/process.js similarity index 100% rename from test/fixtures/process.js rename to test-old/fixtures/process.js diff --git a/test/fixtures/prompt.js b/test-old/fixtures/prompt.js similarity index 100% rename from test/fixtures/prompt.js rename to test-old/fixtures/prompt.js diff --git a/test/fixtures/readline.js b/test-old/fixtures/readline.js similarity index 100% rename from test/fixtures/readline.js rename to test-old/fixtures/readline.js diff --git a/test/fixtures/server/bin/cli.js b/test-old/fixtures/server/bin/cli.js similarity index 100% rename from test/fixtures/server/bin/cli.js rename to test-old/fixtures/server/bin/cli.js diff --git a/test/fixtures/server/index.js b/test-old/fixtures/server/index.js similarity index 100% rename from test/fixtures/server/index.js rename to test-old/fixtures/server/index.js diff --git a/test/fixtures/server/package.json b/test-old/fixtures/server/package.json similarity index 100% rename from test/fixtures/server/package.json rename to test-old/fixtures/server/package.json diff --git a/test-old/fixtures/version.js b/test-old/fixtures/version.js new file mode 100644 index 0000000..758fd05 --- /dev/null +++ b/test-old/fixtures/version.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +console.log(process.version); diff --git a/test/fixtures/wait.js b/test-old/fixtures/wait.js similarity index 100% rename from test/fixtures/wait.js rename to test-old/fixtures/wait.js diff --git a/test/middleware.test.js b/test-old/middleware.test.js similarity index 100% rename from test/middleware.test.js rename to test-old/middleware.test.js diff --git a/test/operation.test.js b/test-old/operation.test.js similarity index 100% rename from test/operation.test.js rename to test-old/operation.test.js diff --git a/test/plugin.test.js b/test-old/plugin.test.js similarity index 100% rename from test/plugin.test.js rename to test-old/plugin.test.js diff --git a/test/process.test.js b/test-old/process.test.js similarity index 100% rename from test/process.test.js rename to test-old/process.test.js diff --git a/test/prompt.test.js b/test-old/prompt.test.js similarity index 100% rename from test/prompt.test.js rename to test-old/prompt.test.js diff --git a/test/runner.test.js b/test-old/runner.test.js similarity index 100% rename from test/runner.test.js rename to test-old/runner.test.js diff --git a/test/setup.js b/test-old/setup.js similarity index 100% rename from test/setup.js rename to test-old/setup.js diff --git a/test/stack.test.js b/test-old/stack.test.js similarity index 100% rename from test/stack.test.js rename to test-old/stack.test.js diff --git a/test/test-utils.ts b/test-old/test-utils.ts similarity index 100% rename from test/test-utils.ts rename to test-old/test-utils.ts diff --git a/test/wait.test.js b/test-old/wait.test.js similarity index 100% rename from test/wait.test.js rename to test-old/wait.test.js diff --git a/test.log b/test.log deleted file mode 100644 index e69de29..0000000 diff --git a/test/assert.test.ts b/test/assert.test.ts index 1b3c88a..775420f 100644 --- a/test/assert.test.ts +++ b/test/assert.test.ts @@ -2,7 +2,6 @@ import path from 'node:path'; import assert from 'node:assert/strict'; -import { it, describe } from 'vitest'; import { matchRule, doesNotMatchRule, matchFile, doesNotMatchFile } from '../src/assert.js'; describe('test/assert.test.ts', () => { diff --git a/test/fixtures/process/message.ts b/test/fixtures/process/message.ts index e69de29..b88b595 100644 --- a/test/fixtures/process/message.ts +++ b/test/fixtures/process/message.ts @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +console.log('starting...'); + +setTimeout(() => { + if (process.send) { + process.send('ready'); + } +}, 500); diff --git a/src/try/child.ts b/test/fixtures/process/prompt.ts similarity index 100% rename from src/try/child.ts rename to test/fixtures/process/prompt.ts diff --git a/test/logger.test.ts b/test/logger.test.ts index d88de6c..c7a7132 100644 --- a/test/logger.test.ts +++ b/test/logger.test.ts @@ -1,5 +1,5 @@ -import { it, describe, beforeEach, afterEach, expect, vi } from 'vitest'; +import { expect, vi } from 'vitest'; import { Logger, LogLevel } from '../src/logger.js'; describe.skip('test/logger.test.js', () => { diff --git a/test/process.test.ts b/test/process.test.ts index ba9adbe..11ed259 100644 --- a/test/process.test.ts +++ b/test/process.test.ts @@ -1,10 +1,7 @@ import path from 'node:path'; import assert from 'node:assert/strict'; -import { PassThrough } from 'node:stream'; -import { it, describe } from 'vitest'; import execa from 'execa'; import { Process } from '../src/process.js'; -import fs from 'fs'; describe('test/process.test.ts', () => { describe('options', () => { @@ -63,17 +60,17 @@ describe('test/process.test.ts', () => { nodeOptions: [ '--require', 'ts-node/register' ], }); - proc.stdout.on('data', data => { + proc.stdout?.on('data', data => { console.log('stdout', data.toString()); }); - proc.stdin.setEncoding('utf8'); + // proc.stdin.setEncoding('utf8'); // const stdin = new PassThrough(); // stdin.pipe(proc.stdin); setTimeout(() => { console.log('write stdin'); - proc.stdin.write('hello\n'); + proc.stdin?.write('hello\n'); proc.stdin?.end(); // stdin.end(); }, 1500); diff --git a/test/runner.test.ts b/test/runner.test.ts new file mode 100644 index 0000000..28cfe53 --- /dev/null +++ b/test/runner.test.ts @@ -0,0 +1,5 @@ +describe('test/runner.test.ts', () => { + it('should work', async () => { + + }); +}); diff --git a/test/utils.test.ts b/test/utils.test.ts index 1533b0b..d5cb216 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -3,10 +3,11 @@ import fs from 'fs'; import path from 'path'; import { strict as assert } from 'assert'; import * as utils from '../src/utils.js'; -import * as testUtils from './test-utils.js'; +import * as testUtils from '../test-old/test-utils.js'; + +describe('test/utils.test.ts', () => { + const tmpDir = path.join(__dirname, './tmp'); -describe.only('test/utils.test.ts', () => { - const tmpDir = testUtils.getTempDir(); beforeEach(() => testUtils.initDir(tmpDir)); it('types', () => { @@ -43,11 +44,17 @@ describe.only('test/utils.test.ts', () => { }); it('writeFile', async () => { - await utils.writeFile(`${tmpDir}/test.md`, 'this is a test'); - assert(fs.readFileSync(`${tmpDir}/test.md`, 'utf-8') === 'this is a test'); + const targetDir = path.resolve(tmpDir, './b'); + await utils.mkdir(targetDir); + + const fileName = `${targetDir}/test-${Date.now()}.md`; + await utils.writeFile(`${tmpDir}/${fileName}.md`, 'this is a test'); + assert(fs.readFileSync(`${tmpDir}/${fileName}.md`, 'utf-8') === 'this is a test'); - await utils.writeFile(`${tmpDir}/test.json`, { name: 'test' }); - assert(fs.readFileSync(`${tmpDir}/test.json`, 'utf-8').match(/"name": "test"/)); + await utils.writeFile(`${tmpDir}/${fileName}.json`, { name: 'test' }); + assert(fs.readFileSync(`${tmpDir}/${fileName}.json`, 'utf-8').match(/"name": "test"/)); + + await utils.mkdir(targetDir); }); it('exists', async () => { diff --git a/vitest.config.js b/vitest.config.js index a0e14ea..42100a8 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -13,9 +13,10 @@ export default defineConfig({ // }), // } ], test: { - // globals: true, + globals: true, + include: [ 'test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}' ], globalSetup: [ - './test/setup.js', + // './test/setup.js', ], }, }); From 2fad8addd7cd90f9ab63060d809c316a57507a55 Mon Sep 17 00:00:00 2001 From: TZ Date: Mon, 26 Dec 2022 23:11:06 +0800 Subject: [PATCH 07/14] f --- DEVELOPMENT.md | 14 +++ src/{ => lib}/assert.ts | 2 +- src/{ => lib}/constant.ts | 0 src/{ => lib}/logger.ts | 1 - src/{ => lib}/process.ts | 32 +++++- src/{ => lib}/utils.ts | 0 src/plugins/operation.ts | 0 src/plugins/validator.ts | 48 ++++++++ src/runner.ts | 184 ++++++++++++++++++++---------- src/types.ts | 2 +- src/validator.ts | 25 ---- test/fixtures/process/long-run.ts | 27 +++++ test/process.test.ts | 33 +++++- test/runner.test.ts | 44 ++++++- 14 files changed, 311 insertions(+), 101 deletions(-) create mode 100644 DEVELOPMENT.md rename src/{ => lib}/assert.ts (98%) rename src/{ => lib}/constant.ts (100%) rename src/{ => lib}/logger.ts (98%) rename src/{ => lib}/process.ts (85%) rename src/{ => lib}/utils.ts (100%) create mode 100644 src/plugins/operation.ts create mode 100644 src/plugins/validator.ts delete mode 100644 src/validator.ts create mode 100644 test/fixtures/process/long-run.ts diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..d4bb06b --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,14 @@ + // prepare/prerun + // - init dir, init env, init ctx + // run + // - run cli + // - collect stdout/stderr, emit event + // - stdin (expect) + // postrun + // - wait event(end, message, error, stdout, stderr) + // - check assert + // end + // - clean up, kill, log result, error hander + + // console.log(this.middlewares); + // return Promise.all(this.middlewares.map(fn => fn())); diff --git a/src/assert.ts b/src/lib/assert.ts similarity index 98% rename from src/assert.ts rename to src/lib/assert.ts index 9154e4d..103c696 100644 --- a/src/assert.ts +++ b/src/lib/assert.ts @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { match, doesNotMatch, AssertionError } from 'node:assert/strict'; import isMatch from 'lodash.ismatch'; -import { types, exists } from './utils.js'; +import { types, exists } from './utils'; type Actual = string | number | Record; type Expected = string | RegExp | Record; diff --git a/src/constant.ts b/src/lib/constant.ts similarity index 100% rename from src/constant.ts rename to src/lib/constant.ts diff --git a/src/logger.ts b/src/lib/logger.ts similarity index 98% rename from src/logger.ts rename to src/lib/logger.ts index 605833e..01fb7bd 100644 --- a/src/logger.ts +++ b/src/lib/logger.ts @@ -1,5 +1,4 @@ import assert from 'node:assert/strict'; -import { info } from 'node:console'; import util from 'node:util'; export enum LogLevel { diff --git a/src/process.ts b/src/lib/process.ts similarity index 85% rename from src/process.ts rename to src/lib/process.ts index fb12f5b..3e28614 100644 --- a/src/process.ts +++ b/src/lib/process.ts @@ -1,12 +1,13 @@ import assert from 'node:assert/strict'; import EventEmitter from 'node:events'; import { PassThrough } from 'node:stream'; +import { EOL } from 'node:os'; import * as execa from 'execa'; import pEvent from 'p-event'; import stripFinalNewline from 'strip-final-newline'; import stripAnsi from 'strip-ansi'; -import { EOL } from 'node:os'; +import { exists } from './utils'; export interface ProcessResult { code: number; @@ -20,6 +21,8 @@ export type ProcessOptions = { execArgv?: execa.NodeOptions['nodeOptions']; }; +export type ProcessEvents = 'stdout' | 'stderr' | 'message' | 'exit' | 'close'; + export class Process extends EventEmitter { type: 'fork' | 'spawn'; cmd: string; @@ -78,7 +81,7 @@ export class Process extends EventEmitter { this.opts.cwd = cwd; } - async exec() { + async start() { if (this.type === 'fork') { this.proc = execa.node(this.cmd, this.args, this.opts); } else { @@ -86,6 +89,16 @@ export class Process extends EventEmitter { this.proc = execa.command(cmdString, this.opts); } + this.proc.then(res => { + if (res instanceof Error) { + this.result.code = res.exitCode; + if ((res as any).code === 'ENOENT') { + this.result.code = 127; + this.result.stderr += (res as any).originalMessage; + } + } + }); + // this.proc.stdin.setEncoding('utf8'); this.proc.stdout.on('data', data => { @@ -119,8 +132,12 @@ export class Process extends EventEmitter { // this.result.code = code; // // console.log('close event:', code); // }); + // return this.proc; + return this; + } - return this.proc; + async end() { + return await this.proc; } kill(signal?: string) { @@ -129,7 +146,7 @@ export class Process extends EventEmitter { } // stdin -> wait(stdout) -> write - async wait(type, expected) { + async wait(type: ProcessEvents, expected) { let promise; switch (type) { case 'stdout': @@ -152,9 +169,14 @@ export class Process extends EventEmitter { break; } + case 'exit': { + promise = pEvent(this.proc, 'exit'); + break; + } + case 'close': default: { - promise = pEvent(this.proc, 'close')//.then(() => this.result); + promise = pEvent(this.proc, 'close'); break; } } diff --git a/src/utils.ts b/src/lib/utils.ts similarity index 100% rename from src/utils.ts rename to src/lib/utils.ts diff --git a/src/plugins/operation.ts b/src/plugins/operation.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/validator.ts b/src/plugins/validator.ts new file mode 100644 index 0000000..107b53b --- /dev/null +++ b/src/plugins/validator.ts @@ -0,0 +1,48 @@ +import path from 'node:path'; +import assert from 'node:assert/strict'; +import type { TestRunner } from '../runner'; +import { matchRule, doesNotMatchRule, matchFile, doesNotMatchFile } from '../lib/assert'; + +export function stdout(runner: TestRunner, expected: string | RegExp) { + return runner.hook('after', async ctx => { + matchRule(ctx.result.stdout, expected); + }); +} + +export function notStdout(runner: TestRunner, expected: string | RegExp) { + return runner.hook('after', async ctx => { + doesNotMatchRule(ctx.result.stdout, expected); + }); +} + +export function stderr(runner: TestRunner, expected: string | RegExp) { + return runner.hook('after', async ctx => { + matchRule(ctx.result.stderr, expected); + }); +} + +export function notStderr(runner: TestRunner, expected: string | RegExp) { + return runner.hook('after', async ({ result }) => { + doesNotMatchRule(result.stderr, expected); + }); +} + +export function file(runner: TestRunner, filePath: string, expected: string | RegExp) { + return runner.hook('after', async ({ cwd }) => { + const fullPath = path.resolve(cwd, filePath); + await matchFile(fullPath, expected); + }); +} + +export function notFile(runner: TestRunner, filePath: string, expected: string | RegExp) { + return runner.hook('after', async ({ cwd }) => { + const fullPath = path.resolve(cwd, filePath); + await doesNotMatchFile(fullPath, expected); + }); +} + +export function code(runner: TestRunner, expected: number) { + return runner.hook('after', async ({ result }) => { + assert.equal(result.code, expected); + }); +} diff --git a/src/runner.ts b/src/runner.ts index 912f315..5608736 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -2,28 +2,43 @@ import EventEmitter from 'events'; import assert from 'node:assert/strict'; import { MountPlugin, PluginLike, AsyncFunction, RestParam } from './types'; -import { Process, ProcessOptions } from './process'; +import { Process, ProcessEvents, ProcessOptions } from './lib/process'; +import * as validator from './plugins/validator'; +import { doesNotMatchRule, matchRule, matchFile, doesNotMatchFile } from './lib/assert'; +import path from 'node:path'; - -// interface Pluginable { -// [key: string]: (runner: T, options?: any) => AsyncFunction; -// } +interface RunnerOptions { + autoWait?: boolean; +} export class TestRunner extends EventEmitter { private logger = console; private proc: Process; + private options: RunnerOptions = {}; private middlewares: any[] = []; private hooks = { - // before: [], - // running: [], - // after: [], - prepare: [], - prerun: [], - run: [], - postrun: [], + before: [], + running: [], + after: [], + // prepare: [], + // prerun: [], + // run: [], + // postrun: [], end: [], }; + private waitType: 'end' | 'custom' = 'end'; + + constructor(opts?: RunnerOptions) { + super(); + this.options = { + autoWait: true, + ...opts, + }; + + // this.plugin({ ...validator }); + } + // prepare 准备现场环境 // prerun 检查参数,在 fork 定义之后 // run 处理 stdin @@ -67,10 +82,6 @@ export class TestRunner extends EventEmitter { return this; } - someMethod() { - console.log('someMethod'); - } - // hook? use(fn: AsyncFunction) { this.middlewares.push(fn); @@ -79,18 +90,27 @@ export class TestRunner extends EventEmitter { async end() { try { - const ctx = { a: 1}; - // prerun - await this.runHook('prerun', ctx); + const ctx = { + proc: this.proc, + cwd: this.proc.opts.cwd, + result: this.proc.result, + }; + + // before + await this.runHook('before', ctx); // exec child process, don't await it - this.proc.exec(); + await this.proc.start(); + + // running + await this.runHook('running', ctx); - // run - await this.runHook('run', ctx); + if (this.options.autoWait) { + await this.proc.end(); + } // postrun - await this.runHook('postrun', ctx); + await this.runHook('after', ctx); // end await this.runHook('end', ctx); @@ -103,53 +123,95 @@ export class TestRunner extends EventEmitter { // clean up this.proc.kill(); } + } - // prepare/prerun - // - init dir, init env, init ctx - // run - // - run cli - // - collect stdout/stderr, emit event - // - stdin (expect) - // postrun - // - wait event(end, message, error, stdout, stderr) - // - check assert - // end - // - clean up, kill, log result, error hander - - // console.log(this.middlewares); - // return Promise.all(this.middlewares.map(fn => fn())); + wait(type: ProcessEvents, expected) { + this.options.autoWait = false; + // wait for process ready then assert + return this.hook('running', async ({ proc }) => { + // ctx.autoWait = false; + await proc.wait(type, expected); + }); } + + // stdout(expected: string | RegExp) { + // return this.hook('postrun', async ctx => { + // matchRule(ctx.result.stdout, expected); + // }); + // } + + // notStdout(expected: string | RegExp) { + // return this.hook('postrun', async ctx => { + // doesNotMatchRule(ctx.result.stdout, expected); + // }); + // } + + // stderr(expected: string | RegExp) { + // return this.hook('postrun', async ctx => { + // matchRule(ctx.result.stderr, expected); + // }); + // } + + // notStderr(expected: string | RegExp) { + // return this.hook('postrun', async ({ result }) => { + // doesNotMatchRule(result.stderr, expected); + // }); + // } + + // file(filePath: string, expected: string | RegExp) { + // return this.hook('postrun', async ({ cwd }) => { + // const fullPath = path.resolve(cwd, filePath); + // await matchFile(fullPath, expected); + // }); + // } + + // notFile(filePath: string, expected: string | RegExp) { + // return this.hook('postrun', async ({ cwd }) => { + // const fullPath = path.resolve(cwd, filePath); + // await doesNotMatchFile(fullPath, expected); + // }); + // } + + // code(expected: number) { + // return this.hook('postrun', async ({ result }) => { + // assert.equal(result.code, expected); + // }); + // } } -function fork(runner: TestRunner, cmd, args, opts) { - runner.hook('prerun', async ctx => { - ctx.cmd = cmd; - ctx.args = args; - ctx.opts = opts; - console.log('run fork', cmd, args, opts); - }); +export function runner() { + return new TestRunner().plugin({ ...validator }); } +// function fork(runner: TestRunner, cmd, args, opts) { +// runner.hook('prerun', async ctx => { +// ctx.cmd = cmd; +// ctx.args = args; +// ctx.opts = opts; +// console.log('run fork', cmd, args, opts); +// }); +// } + -function file(runner: TestRunner, opts: { a: string }) { - runner.hook('postrun', async ctx => { - console.log('run file', ctx, opts); - }); -} +// function file(runner: TestRunner, opts: { a: string }) { +// runner.hook('postrun', async ctx => { +// console.log('run file', ctx, opts); +// }); +// } -function sleep(runner: TestRunner, b: number) { - runner.hook('postrun', async ctx => { - console.log('run sleep', ctx, b); - }); -} +// function sleep(runner: TestRunner, b: number) { +// runner.hook('postrun', async ctx => { +// console.log('run sleep', ctx, b); +// }); +// } -new TestRunner() - .plugin({ file, sleep, fork }) - .file({ 'a': 'b' }) - .fork('node', '-v') - .sleep(1) - .sleep(222) - .end().then(() => console.log('done')); +// new TestRunner() +// .plugin({ file, sleep, fork }) +// .file({ 'a': 'b' }) +// .fork('node', '-v') +// .sleep(1) +// .sleep(222) +// .end().then(() => console.log('done')); // koa middleware // 初始化 -> fork -> await next() -> 校验 -> 结束 diff --git a/src/types.ts b/src/types.ts index beb97bc..9c11916 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,5 +7,5 @@ export type AsyncFunction = (...args: any[]) => Promise; export type RestParam = T extends (first: any, ...args: infer R) => any ? R : any; export interface PluginLike { - [key: string]: (core: any, ...args: any[]) => void; + [key: string]: (core: any, ...args: any[]) => any; } diff --git a/src/validator.ts b/src/validator.ts deleted file mode 100644 index 35013cb..0000000 --- a/src/validator.ts +++ /dev/null @@ -1,25 +0,0 @@ -export function expect(runner, fn) { - runner.hook('postrun', async () => { - await fn(runner); - }); -} - -export function stdout(runner, expected) { - assert(expected, '`expected` is required'); - expect(runner, async function stdout({ result, assert }) { - assert.matchRule(result.stdout, expected); - }); -} - -export function stderr(runner, expected) { - runner.hook('postrun', async () => { - assert.matchRule(result.stdout, expected); - }); - - runner.hook({ - async prerun() { - }, - async postrun() { - }, - }); -} diff --git a/test/fixtures/process/long-run.ts b/test/fixtures/process/long-run.ts new file mode 100644 index 0000000..ae718eb --- /dev/null +++ b/test/fixtures/process/long-run.ts @@ -0,0 +1,27 @@ +import http from 'node:http'; + +const port = process.env.PORT || 3000; + +const app = http.createServer((req, res) => { + console.log(`Receive: ${req.url}`); + + if (req.url === '/exit') { + res.end('byebye'); + process.exit(0); + } else { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello World'); + } +}); + +app.listen(port, () => { + console.log(`Server running at http://localhost:${port}/`); + if (process.send) { + process.send('ready'); + } + + setTimeout(() => { + console.log('timeout'); + process.exit(0); + }, 5000); +}); diff --git a/test/process.test.ts b/test/process.test.ts index 11ed259..f9c3ae7 100644 --- a/test/process.test.ts +++ b/test/process.test.ts @@ -6,7 +6,7 @@ import { Process } from '../src/process.js'; describe('test/process.test.ts', () => { describe('options', () => { it('should merge options', () => { - const proc = new Process('fork', 'src/try/child.ts', ['--foo', 'bar'], { + const proc = new Process('fork', 'fixtures/process/fork.ts', ['--foo', 'bar'], { nodeOptions: ['--inspect-brk'], }); @@ -17,7 +17,8 @@ describe('test/process.test.ts', () => { it('should spawn', async () => { const proc = new Process('spawn', 'node', ['-p', 'process.version', '--inspect'], {}); - await proc.exec(); + await proc.start(); + await proc.end(); // console.log(proc.result); assert.match(proc.result.stdout, /v\d+\.\d+\.\d+/); assert.match(proc.result.stderr, /Debugger listening on/); @@ -27,17 +28,37 @@ describe('test/process.test.ts', () => { it('should fork', async () => { const cli = path.resolve(__dirname, 'fixtures/process/fork.ts'); const proc = new Process('fork', cli); - await proc.exec(); + await proc.start(); + await proc.end(); // console.log(proc.result); assert.match(proc.result.stdout, /v\d+\.\d+\.\d+/); assert.match(proc.result.stderr, /this is testing/); assert.strictEqual(proc.result.code, 0); }); + it.only('should spawn not-exits', async () => { + const proc = new Process('spawn', 'not-exists'); + await proc.start(); + const a = await proc.end(); + console.log(proc.result, a); + // assert.match(proc.result.stdout, /Cannot find module/); + assert.strictEqual(proc.result.code, 127); + }); + + it.only('should fork not-exits', async () => { + const proc = new Process('fork', 'not-exists.ts'); + await proc.start(); + const a = await proc.end(); + console.log(proc.result, a); + assert.match(proc.result.stderr, /Cannot find module/); + assert.strictEqual(proc.result.code, 127); + }); + it('should strip color', async () => { const cli = path.resolve(__dirname, 'fixtures/process/color.ts'); const proc = new Process('fork', cli, []); - await proc.exec(); + await proc.start(); + await proc.end(); // console.log(proc.result); assert.match(proc.result.stdout, /CLIHub/); assert.match(proc.result.stderr, /MSGHub/); @@ -47,14 +68,14 @@ describe('test/process.test.ts', () => { it('should exit with fail', async () => { const cli = path.resolve(__dirname, 'fixtures/process/error.ts'); const proc = new Process('fork', cli, []); - await proc.exec(); + await proc.start(); + await proc.end(); console.log(proc.result); assert.match(proc.result.stdout, /this is an error test/); assert.match(proc.result.stderr, /Error: some error/); assert.strictEqual(proc.result.code, 1); }); - it.skip('should execa work', async () => { const proc = execa.node('src/try/child.ts', ['--foo', 'bar'], { nodeOptions: [ '--require', 'ts-node/register' ], diff --git a/test/runner.test.ts b/test/runner.test.ts index 28cfe53..5d9cabc 100644 --- a/test/runner.test.ts +++ b/test/runner.test.ts @@ -1,5 +1,47 @@ +import { runner } from '../src/runner'; + describe('test/runner.test.ts', () => { - it('should work', async () => { + describe('process', () => { + it('should spawn', async () => { + await runner() + .spawn('node', [ '-p', 'process.version', '--inspect' ]) + .stdout(/v\d+\.\d+\.\d+/) + .notStdout('some text') + .stderr(/Debugger listening on/) + .notStderr('some error') + .end(); + }); + + it('should fork', async () => { + await runner() + .fork('test/fixtures/process/fork.ts') + .stdout(/v\d+\.\d+\.\d+/) + .notStdout('some text') + .stderr(/this is testing/) + .notStderr('some error') + .end(); + }); + + // it.only('should wait stdout', async () => { + // await runner() + // .fork('test/fixtures/process/long-run.ts') + // .wait('stdout', /Server running at/) + // .stdout(/ready/) + // .end(); + // }); + + it.skip('should not fail when spawn not exists', async () => { + await runner() + .spawn('not-exists') + .stderr(/Cannot find module/) + .end(); + }); + it.skip('should not fail when fork not exists', async () => { + await runner() + .fork('test/fixtures/not-exists.ts') + .stderr(/Cannot find module/) + .end(); + }); }); }); From 1059f918e8bbda76311d17fff09d5be6e7d12852 Mon Sep 17 00:00:00 2001 From: TZ Date: Tue, 27 Dec 2022 10:15:26 +0800 Subject: [PATCH 08/14] f --- DEVELOPMENT.md | 5 +++ src/plugins/operation.ts | 7 ++++ src/runner.ts | 80 +++++++++++++++++++--------------------- src/types.ts | 4 ++ test/runner.test.ts | 3 ++ 5 files changed, 56 insertions(+), 43 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d4bb06b..3d62f94 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -12,3 +12,8 @@ // console.log(this.middlewares); // return Promise.all(this.middlewares.map(fn => fn())); + + // prepare: [], + // prerun: [], + // run: [], + // postrun: [], diff --git a/src/plugins/operation.ts b/src/plugins/operation.ts index e69de29..36d5930 100644 --- a/src/plugins/operation.ts +++ b/src/plugins/operation.ts @@ -0,0 +1,7 @@ +import type { TestRunner } from '../runner'; + +export function tap(runner: TestRunner, fn: (runner: TestRunner) => Promise) { + return runner.hook('after', async ctx => { + await fn(runner); + }); +} diff --git a/src/runner.ts b/src/runner.ts index 5608736..339adeb 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -4,31 +4,34 @@ import assert from 'node:assert/strict'; import { MountPlugin, PluginLike, AsyncFunction, RestParam } from './types'; import { Process, ProcessEvents, ProcessOptions } from './lib/process'; import * as validator from './plugins/validator'; +import * as operation from './plugins/operation'; import { doesNotMatchRule, matchRule, matchFile, doesNotMatchFile } from './lib/assert'; -import path from 'node:path'; interface RunnerOptions { autoWait?: boolean; } +type BuiltinPlugin = { + [key in keyof T]: (...args: RestParam) => Core; +}; + +export function runner(opts?: RunnerOptions) { + return new TestRunner(opts); // .plugin({ ...validator, ...operation }); +} + +export interface TestRunner extends BuiltinPlugin, BuiltinPlugin {} + export class TestRunner extends EventEmitter { private logger = console; private proc: Process; private options: RunnerOptions = {}; - private middlewares: any[] = []; private hooks = { before: [], running: [], after: [], - // prepare: [], - // prerun: [], - // run: [], - // postrun: [], end: [], }; - private waitType: 'end' | 'custom' = 'end'; - constructor(opts?: RunnerOptions) { super(); this.options = { @@ -36,7 +39,7 @@ export class TestRunner extends EventEmitter { ...opts, }; - // this.plugin({ ...validator }); + this.plugin({ ...validator, ...operation }); } // prepare 准备现场环境 @@ -50,7 +53,7 @@ export class TestRunner extends EventEmitter { const initFn = plugins[key]; this[key] = (...args: RestParam) => { - console.log('mount %s with %j', key, ...args); + console.log('mount %s with %s', key, ...args); initFn(this, ...args); return this; }; @@ -63,31 +66,6 @@ export class TestRunner extends EventEmitter { return this; } - async runHook(event: string, ctx) { - for (const fn of this.hooks[event]) { - // TODO: ctx - await fn(ctx); - } - } - - fork(cmd: string, args?: string[], opts?: ProcessOptions) { - assert(!this.proc, 'cmd can not be registered twice'); - this.proc = new Process('fork', cmd, args, opts); - return this; - } - - spawn(cmd: string, args?: string[], opts?: ProcessOptions) { - assert(!this.proc, 'cmd can not be registered twice'); - this.proc = new Process('spawn', cmd, args, opts); - return this; - } - - // hook? - use(fn: AsyncFunction) { - this.middlewares.push(fn); - return this; - } - async end() { try { const ctx = { @@ -97,23 +75,31 @@ export class TestRunner extends EventEmitter { }; // before - await this.runHook('before', ctx); + for (const fn of this.hooks['before']) { + await fn(ctx); + } // exec child process, don't await it await this.proc.start(); // running - await this.runHook('running', ctx); + for (const fn of this.hooks['running']) { + await fn(ctx); + } if (this.options.autoWait) { await this.proc.end(); } // postrun - await this.runHook('after', ctx); + for (const fn of this.hooks['after']) { + await fn(ctx); + } // end - await this.runHook('end', ctx); + for (const fn of this.hooks['end']) { + await fn(ctx); + } this.logger.info('✔ Test pass.\n'); } catch (err) { @@ -125,6 +111,18 @@ export class TestRunner extends EventEmitter { } } + fork(cmd: string, args?: string[], opts?: ProcessOptions) { + assert(!this.proc, 'cmd can not be registered twice'); + this.proc = new Process('fork', cmd, args, opts); + return this; + } + + spawn(cmd: string, args?: string[], opts?: ProcessOptions) { + assert(!this.proc, 'cmd can not be registered twice'); + this.proc = new Process('spawn', cmd, args, opts); + return this; + } + wait(type: ProcessEvents, expected) { this.options.autoWait = false; // wait for process ready then assert @@ -179,10 +177,6 @@ export class TestRunner extends EventEmitter { // } } -export function runner() { - return new TestRunner().plugin({ ...validator }); -} - // function fork(runner: TestRunner, cmd, args, opts) { // runner.hook('prerun', async ctx => { // ctx.cmd = cmd; diff --git a/src/types.ts b/src/types.ts index 9c11916..cefe32b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,10 @@ export type MountPlugin = { [key in keyof T]: T[key] extends (core: Core, ...args: infer I) => any ? (...args: I) => MountPlugin : T[key]; } & Core; +export type BuiltinPlugin = { + [key in keyof T]: (...args: RestParam) => Core; +}; + export type AsyncFunction = (...args: any[]) => Promise; export type RestParam = T extends (first: any, ...args: infer R) => any ? R : any; diff --git a/test/runner.test.ts b/test/runner.test.ts index 5d9cabc..78193dd 100644 --- a/test/runner.test.ts +++ b/test/runner.test.ts @@ -9,6 +9,9 @@ describe('test/runner.test.ts', () => { .notStdout('some text') .stderr(/Debugger listening on/) .notStderr('some error') + .tap(async runner => { + console.log('@@@', runner); + }) .end(); }); From 57c8de8ed9d35a15ca5d12417b0438abf6327b02 Mon Sep 17 00:00:00 2001 From: TZ Date: Tue, 27 Dec 2022 11:43:00 +0800 Subject: [PATCH 09/14] mocha --- .eslintrc | 12 --- .mocharc.yml | 5 + DEVELOPMENT.md | 3 + package.json | 42 ++++---- src/lib/assert.ts | 2 +- src/lib/logger.ts | 222 +++++++++++++++++++-------------------- src/lib/process.ts | 20 ++-- src/lib/utils.ts | 15 ++- src/plugins/operation.ts | 2 +- src/runner.ts | 96 ++--------------- test/assert.test.ts | 11 +- test/logger.test.ts | 117 ++++++++++----------- test/process.test.ts | 2 +- test/runner.test.ts | 41 +++++--- test/tsconfig.json | 13 +++ test/utils.test.ts | 3 +- tsconfig.json | 3 +- vitest.config.js | 22 ---- 18 files changed, 270 insertions(+), 361 deletions(-) create mode 100644 .mocharc.yml create mode 100644 test/tsconfig.json delete mode 100644 vitest.config.js diff --git a/.eslintrc b/.eslintrc index 370fb68..7c05da9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,17 +3,5 @@ "parserOptions": { "project": "./tsconfig.json", "createDefaultProgram": true - }, - "rules": { - "prefer-spread": "off", - "no-return-assign": "off", - "no-case-declarations": "off", - "prefer-const": "off", - "no-regex-spaces": "off", - "no-return-await": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-use-before-define": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-var-requires": "off" } } diff --git a/.mocharc.yml b/.mocharc.yml new file mode 100644 index 0000000..7a6e646 --- /dev/null +++ b/.mocharc.yml @@ -0,0 +1,5 @@ +spec: test/**/*.test.ts +extension: ts +require: ts-node/register +timeout: 120000 +exclude: test/fixtures/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 3d62f94..aa26689 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -17,3 +17,6 @@ // prerun: [], // run: [], // postrun: [], + +// koa middleware +// 初始化 -> fork -> await next() -> 校验 -> 结束 diff --git a/package.json b/package.json index 74bf68c..1a4cf9c 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,30 @@ "version": "1.0.1", "description": "Command Line E2E Testing", "type": "commonjs", - "main": "./lib/runner.js", - "exports": "./lib/runner.js", - "types": "./lib/index.d.ts", + "main": "./dist/runner.js", + "types": "./lib/runner.d.ts", + "exports": { + ".": { + "import": "./dist/runner.js", + "require": "./dist/runner.js" + }, + "./package.json": "./package.json" + }, "author": "TZ (https://github.com/atian25)", "homepage": "https://github.com/node-modules/clet", "repository": "git@github.com:node-modules/clet.git", + "scripts": { + "lint": "eslint . --ext .ts", + "postlint": "tsc --noEmit", + "test": "cross-env TS_NODE_PROJECT=test/tsconfig.json mocha", + "cov": "c8 -n src/ npm test", + "ci": "npm run cov", + "tsc": "rm -rf dist && tsc", + "prepack": "npm run tsc" + }, + "files": [ + "dist" + ], "dependencies": { "consola": "^2.15.3", "dirname-filename-esm": "^1.1.1", @@ -26,8 +44,6 @@ "@artus/tsconfig": "^1", "@types/mocha": "^9.1.1", "@types/node": "^18.7.14", - "@vitest/coverage-c8": "^0.22.1", - "@vitest/ui": "^0.22.1", "cross-env": "^7.0.3", "enquirer": "^2.3.6", "eslint": "^7", @@ -36,19 +52,9 @@ "ts-node": "^10.9.1", "tslib": "^2.4.0", "typescript": "^4.8.2", - "vitest": "^0.22.1" - }, - "files": [ - "dist" - ], - "scripts": { - "lint": "eslint . --ext .ts", - "postlint": "tsc --noEmit", - "test": "vitest run", - "cov": "vitest run --coverage", - "ci": "npm run cov", - "tsc": "rm -rf dist && tsc", - "prepack": "npm run tsc" + "c8": "^7.12.0", + "mocha": "^10.0.0", + "ts-mocha": "^10.0.0" }, "license": "MIT" } diff --git a/src/lib/assert.ts b/src/lib/assert.ts index 103c696..9ab66a8 100644 --- a/src/lib/assert.ts +++ b/src/lib/assert.ts @@ -5,7 +5,7 @@ import { match, doesNotMatch, AssertionError } from 'node:assert/strict'; import isMatch from 'lodash.ismatch'; import { types, exists } from './utils'; -type Actual = string | number | Record; +type Actual = string | Record; type Expected = string | RegExp | Record; /** diff --git a/src/lib/logger.ts b/src/lib/logger.ts index 01fb7bd..e2ded8f 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -1,112 +1,112 @@ -import assert from 'node:assert/strict'; -import util from 'node:util'; - -export enum LogLevel { - ERROR = 0, - WARN = 1, - LOG = 2, - INFO = 3, - DEBUG = 4, - TRACE = 5, - Silent = -Infinity, - Verbose = Infinity, -} - -export interface LoggerOptions { - level?: LogLevel; - tag?: string | string[]; - showTag?: boolean; - showTime?: boolean; - indent?: number; -} - -type LogMethods = { - [key in Lowercase]: (message: any, ...args: any[]) => void; -}; - -export interface Logger extends LogMethods { } - -export class Logger { - private options: LoggerOptions; - private childMaps: Record; - - // Declare the type of the dynamically-registered methods - // [key in Lowercase]: (message: any, ...args: any[]) => void; - - constructor(tag?: string | LoggerOptions, opts: LoggerOptions = {}) { - if (typeof tag === 'string') { - opts.tag = opts.tag || tag || ''; - } else { - opts = tag; - } - opts.tag = [].concat(opts.tag || []); - - this.options = { - level: LogLevel.INFO, - indent: 0, - showTag: true, - showTime: false, - ...opts, - }; - - this.childMaps = {}; - - // register methods - for (const [ key, value ] of Object.entries(LogLevel)) { - const fnName = key.toLowerCase(); - const fn = console[fnName] || console.debug; - this[fnName] = (message: any, ...args: any[]) => { - if (value > this.options.level) return; - const msg = this.format(message, args, this.options); - return fn(msg); - }; - } - - return this as unknown as typeof Logger.prototype & LogMethods; - } - - format(message: any, args: any[], options?: LoggerOptions) { - const time = options.showTime ? `[${formatTime(new Date())}] ` : ''; - const tag = options.showTag && options.tag.length ? `[${options.tag.join(':')}] ` : ''; - const indent = ' '.repeat(options.indent); - const prefix = time + indent + tag; - const content = util.format(message, ...args).replace(/^/gm, prefix); - return content; - } - - get level() { - return this.options.level; - } - - set level(v: number | string) { - this.options.level = normalize(v); - } - - child(tag: string, opts?: LoggerOptions) { - assert(tag, 'tag is required'); - if (!this.childMaps[tag]) { - this.childMaps[tag] = new Logger({ - ...this.options, - indent: this.options.indent + 2, - ...opts, - tag: [ ...this.options.tag, tag ], - }); - } - return this.childMaps[tag]; - } -} - -function normalize(level: number | string) { - if (typeof level === 'number') return level; - const levelNum = LogLevel[level.toUpperCase()]; - assert(levelNum, `unknown loglevel ${level}`); - return levelNum; -} - -function formatTime(date: Date) { - date = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); - return date.toISOString() - .replace('T', ' ') - .replace(/\..+$/, ''); -} +// import assert from 'node:assert/strict'; +// import util from 'node:util'; + +// export enum LogLevel { +// ERROR = 0, +// WARN = 1, +// LOG = 2, +// INFO = 3, +// DEBUG = 4, +// TRACE = 5, +// Silent = -Infinity, +// Verbose = Infinity, +// } + +// export interface LoggerOptions { +// level?: LogLevel; +// tag?: string | string[]; +// showTag?: boolean; +// showTime?: boolean; +// indent?: number; +// } + +// type LogMethods = { +// [key in Lowercase]: (message: any, ...args: any[]) => void; +// }; + +// export interface Logger extends LogMethods { } + +// export class Logger { +// private options: LoggerOptions; +// private childMaps: Record; + +// // Declare the type of the dynamically-registered methods +// // [key in Lowercase]: (message: any, ...args: any[]) => void; + +// constructor(tag?: string | LoggerOptions, opts: LoggerOptions = {}) { +// if (typeof tag === 'string') { +// opts.tag = opts.tag || tag || ''; +// } else { +// opts = tag; +// } +// opts.tag = [].concat(opts.tag || []); + +// this.options = { +// level: LogLevel.INFO, +// indent: 0, +// showTag: true, +// showTime: false, +// ...opts, +// }; + +// this.childMaps = {}; + +// // register methods +// for (const [ key, value ] of Object.entries(LogLevel)) { +// const fnName = key.toLowerCase(); +// const fn = console[fnName] || console.debug; +// this[fnName] = (message: any, ...args: any[]) => { +// if (value > this.options.level) return; +// const msg = this.format(message, args, this.options); +// return fn(msg); +// }; +// } + +// return this as unknown as typeof Logger.prototype & LogMethods; +// } + +// format(message: any, args: any[], options?: LoggerOptions) { +// const time = options.showTime ? `[${formatTime(new Date())}] ` : ''; +// const tag = options.showTag && options.tag.length ? `[${options.tag.join(':')}] ` : ''; +// const indent = ' '.repeat(options.indent); +// const prefix = time + indent + tag; +// const content = util.format(message, ...args).replace(/^/gm, prefix); +// return content; +// } + +// get level() { +// return this.options.level; +// } + +// set level(v: number | string) { +// this.options.level = normalize(v); +// } + +// child(tag: string, opts?: LoggerOptions) { +// assert(tag, 'tag is required'); +// if (!this.childMaps[tag]) { +// this.childMaps[tag] = new Logger({ +// ...this.options, +// indent: this.options.indent + 2, +// ...opts, +// tag: [ ...this.options.tag, tag ], +// }); +// } +// return this.childMaps[tag]; +// } +// } + +// function normalize(level: number | string) { +// if (typeof level === 'number') return level; +// const levelNum = LogLevel[level.toUpperCase()]; +// assert(levelNum, `unknown loglevel ${level}`); +// return levelNum; +// } + +// function formatTime(date: Date) { +// date = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); +// return date.toISOString() +// .replace('T', ' ') +// .replace(/\..+$/, ''); +// } diff --git a/src/lib/process.ts b/src/lib/process.ts index 3e28614..6e96b43 100644 --- a/src/lib/process.ts +++ b/src/lib/process.ts @@ -1,4 +1,3 @@ -import assert from 'node:assert/strict'; import EventEmitter from 'node:events'; import { PassThrough } from 'node:stream'; import { EOL } from 'node:os'; @@ -7,10 +6,9 @@ import * as execa from 'execa'; import pEvent from 'p-event'; import stripFinalNewline from 'strip-final-newline'; import stripAnsi from 'strip-ansi'; -import { exists } from './utils'; export interface ProcessResult { - code: number; + code: number | null; stdout: string; stderr: string; } @@ -57,7 +55,7 @@ export class Process extends EventEmitter { // need to test color this.result = { - code: undefined, + code: null, stdout: '', stderr: '', }; @@ -66,7 +64,7 @@ export class Process extends EventEmitter { write(data: string) { // FIXME: when stdin.write, stdout will recieve duplicate output // auto add \n - this.proc.stdin.write(data.replace(/\r?\n$/, '') + EOL); + this.proc.stdin!.write(data.replace(/\r?\n$/, '') + EOL); // (this.opts.input as PassThrough).write(data); // (this.opts.stdin as Readable).write(data); @@ -74,7 +72,7 @@ export class Process extends EventEmitter { } env(key: string, value: string) { - this.opts.env[key] = value; + this.opts.env![key] = value; } cwd(cwd: string) { @@ -101,7 +99,7 @@ export class Process extends EventEmitter { // this.proc.stdin.setEncoding('utf8'); - this.proc.stdout.on('data', data => { + this.proc.stdout!.on('data', data => { const origin = stripFinalNewline(data.toString()); const content = stripAnsi(origin); this.result.stdout += content; @@ -109,7 +107,7 @@ export class Process extends EventEmitter { console.log(origin); }); - this.proc.stderr.on('data', data => { + this.proc.stderr!.on('data', data => { const origin = stripFinalNewline(data.toString()); const content = stripAnsi(origin); this.result.stderr += content; @@ -137,7 +135,7 @@ export class Process extends EventEmitter { } async end() { - return await this.proc; + return this.proc; } kill(signal?: string) { @@ -151,12 +149,12 @@ export class Process extends EventEmitter { switch (type) { case 'stdout': case 'stderr': { - promise = pEvent(this.proc[type], 'data', { + promise = pEvent(this.proc[type] as any, 'data', { rejectionEvents: ['close'], filter: () => { return expected.test(this.result[type]); }, - })//.then(() => this.result[type]); + }); // .then(() => this.result[type]); break; } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 746f56c..9e9b5f5 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -2,7 +2,6 @@ import fs from 'node:fs/promises'; import util from 'node:util'; import path from 'node:path'; -import { dirname } from 'dirname-filename-esm'; import isMatch from 'lodash.ismatch'; import trash from 'trash'; @@ -74,9 +73,9 @@ export async function mkdir(dir: string, opts?: any) { */ export async function rm(p, opts = {}) { /* istanbul ignore if */ - if (opts.trash === false || process.env.CI) { - return await fs.rm(p, { force: true, recursive: true, ...opts }); - } + // if (opts.trash === false || process.env.CI) { + // return await fs.rm(p, { force: true, recursive: true, ...opts }); + // } /* istanbul ignore next */ return await trash(p, opts); } @@ -111,10 +110,10 @@ export async function exists(filePath: string) { * @param {...String} args - other paths * @return {String} file path */ -export function resolve(meta, ...args) { - const p = types.isObject(meta) ? dirname(meta) : meta; - return path.resolve(p, ...args); -} +// export function resolve(meta, ...args) { +// // const p = types.isObject(meta) ? dirname(meta) : meta; +// // return path.resolve(p, ...args); +// } /** * take a sleep diff --git a/src/plugins/operation.ts b/src/plugins/operation.ts index 36d5930..754efa1 100644 --- a/src/plugins/operation.ts +++ b/src/plugins/operation.ts @@ -1,7 +1,7 @@ import type { TestRunner } from '../runner'; export function tap(runner: TestRunner, fn: (runner: TestRunner) => Promise) { - return runner.hook('after', async ctx => { + return runner.hook('after', async () => { await fn(runner); }); } diff --git a/src/runner.ts b/src/runner.ts index 339adeb..16dbb99 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -5,27 +5,22 @@ import { MountPlugin, PluginLike, AsyncFunction, RestParam } from './types'; import { Process, ProcessEvents, ProcessOptions } from './lib/process'; import * as validator from './plugins/validator'; import * as operation from './plugins/operation'; -import { doesNotMatchRule, matchRule, matchFile, doesNotMatchFile } from './lib/assert'; +// import { doesNotMatchRule, matchRule, matchFile, doesNotMatchFile } from './lib/assert'; -interface RunnerOptions { +export interface RunnerOptions { autoWait?: boolean; } -type BuiltinPlugin = { - [key in keyof T]: (...args: RestParam) => Core; -}; - export function runner(opts?: RunnerOptions) { - return new TestRunner(opts); // .plugin({ ...validator, ...operation }); + return new TestRunner(opts) + .plugin({ ...validator, ...operation }); } -export interface TestRunner extends BuiltinPlugin, BuiltinPlugin {} - export class TestRunner extends EventEmitter { private logger = console; private proc: Process; private options: RunnerOptions = {}; - private hooks = { + private hooks: Record = { before: [], running: [], after: [], @@ -38,8 +33,6 @@ export class TestRunner extends EventEmitter { autoWait: true, ...opts, }; - - this.plugin({ ...validator, ...operation }); } // prepare 准备现场环境 @@ -62,6 +55,8 @@ export class TestRunner extends EventEmitter { } hook(event: string, fn: AsyncFunction) { + const buildError = new Error('only for stack'); + console.log(buildError.stack); this.hooks[event].push(fn); return this; } @@ -131,81 +126,4 @@ export class TestRunner extends EventEmitter { await proc.wait(type, expected); }); } - - // stdout(expected: string | RegExp) { - // return this.hook('postrun', async ctx => { - // matchRule(ctx.result.stdout, expected); - // }); - // } - - // notStdout(expected: string | RegExp) { - // return this.hook('postrun', async ctx => { - // doesNotMatchRule(ctx.result.stdout, expected); - // }); - // } - - // stderr(expected: string | RegExp) { - // return this.hook('postrun', async ctx => { - // matchRule(ctx.result.stderr, expected); - // }); - // } - - // notStderr(expected: string | RegExp) { - // return this.hook('postrun', async ({ result }) => { - // doesNotMatchRule(result.stderr, expected); - // }); - // } - - // file(filePath: string, expected: string | RegExp) { - // return this.hook('postrun', async ({ cwd }) => { - // const fullPath = path.resolve(cwd, filePath); - // await matchFile(fullPath, expected); - // }); - // } - - // notFile(filePath: string, expected: string | RegExp) { - // return this.hook('postrun', async ({ cwd }) => { - // const fullPath = path.resolve(cwd, filePath); - // await doesNotMatchFile(fullPath, expected); - // }); - // } - - // code(expected: number) { - // return this.hook('postrun', async ({ result }) => { - // assert.equal(result.code, expected); - // }); - // } } - -// function fork(runner: TestRunner, cmd, args, opts) { -// runner.hook('prerun', async ctx => { -// ctx.cmd = cmd; -// ctx.args = args; -// ctx.opts = opts; -// console.log('run fork', cmd, args, opts); -// }); -// } - - -// function file(runner: TestRunner, opts: { a: string }) { -// runner.hook('postrun', async ctx => { -// console.log('run file', ctx, opts); -// }); -// } - -// function sleep(runner: TestRunner, b: number) { -// runner.hook('postrun', async ctx => { -// console.log('run sleep', ctx, b); -// }); -// } - -// new TestRunner() -// .plugin({ file, sleep, fork }) -// .file({ 'a': 'b' }) -// .fork('node', '-v') -// .sleep(1) -// .sleep(222) -// .end().then(() => console.log('done')); - -// koa middleware -// 初始化 -> fork -> await next() -> 校验 -> 结束 diff --git a/test/assert.test.ts b/test/assert.test.ts index 775420f..0150acd 100644 --- a/test/assert.test.ts +++ b/test/assert.test.ts @@ -1,8 +1,7 @@ - import path from 'node:path'; import assert from 'node:assert/strict'; -import { matchRule, doesNotMatchRule, matchFile, doesNotMatchFile } from '../src/assert.js'; +import { matchRule, doesNotMatchRule, matchFile, doesNotMatchFile } from '../src/lib/assert'; describe('test/assert.test.ts', () => { const pkgInfo = { @@ -15,11 +14,10 @@ describe('test/assert.test.ts', () => { describe('matchRule', () => { it('should support regexp', () => { - matchRule(123456, /\d+/); matchRule('abc', /\w+/); assert.throws(() => { - matchRule(123456, /abc/); + matchRule('123456', /abc/); }, { name: 'AssertionError', message: /The input did not match the regular expression/, @@ -68,11 +66,10 @@ describe('test/assert.test.ts', () => { describe('doesNotMatchRule', () => { it('should support regexp', () => { - doesNotMatchRule(123456, /abc/); doesNotMatchRule('abc', /\d+/); assert.throws(() => { - doesNotMatchRule(123456, /\d+/); + doesNotMatchRule('123456', /\d+/); }, { name: 'AssertionError', message: /The input was expected to not match the regular expression/, @@ -159,5 +156,5 @@ describe('test/assert.test.ts', () => { }); }); - it.todo('error stack'); + // it.todo('error stack'); }); diff --git a/test/logger.test.ts b/test/logger.test.ts index c7a7132..88285d5 100644 --- a/test/logger.test.ts +++ b/test/logger.test.ts @@ -1,72 +1,71 @@ -import { expect, vi } from 'vitest'; -import { Logger, LogLevel } from '../src/logger.js'; +// import { Logger, LogLevel } from '../src/lib/logger'; -describe.skip('test/logger.test.js', () => { - beforeEach(() => { - for (const name of [ 'error', 'warn', 'info', 'log', 'debug' ] as const) { - vi.spyOn(global.console, name); - } - }); +// describe.skip('test/logger.test.js', () => { +// beforeEach(() => { +// for (const name of [ 'error', 'warn', 'info', 'log', 'debug' ] as const) { +// vi.spyOn(global.console, name); +// } +// }); - afterEach(() => { - vi.resetAllMocks(); - }); +// afterEach(() => { +// vi.resetAllMocks(); +// }); - it('should support level', () => { - const logger = new Logger(); - expect(logger.level === LogLevel.INFO); +// it('should support level', () => { +// const logger = new Logger(); +// expect(logger.level === LogLevel.INFO); - logger.level = LogLevel.DEBUG; - logger.debug('debug log'); - expect(console.debug).toHaveBeenCalledWith('debug log'); +// logger.level = LogLevel.DEBUG; +// logger.debug('debug log'); +// expect(console.debug).toHaveBeenCalledWith('debug log'); - logger.level = 'WARN'; - logger.info('info log'); - expect(console.info).not.toHaveBeenCalled(); - }); +// logger.level = 'WARN'; +// logger.info('info log'); +// expect(console.info).not.toHaveBeenCalled(); +// }); - it('should logger verbose', () => { - const logger = new Logger({ level: LogLevel.Verbose }); - logger.error('error log'); - logger.warn('warn log'); - logger.info('info log'); - logger.debug('debug log'); - logger.verbose('verbose log'); +// it('should logger verbose', () => { +// const logger = new Logger({ level: LogLevel.Verbose }); +// logger.error('error log'); +// logger.warn('warn log'); +// logger.info('info log'); +// logger.debug('debug log'); +// logger.verbose('verbose log'); - expect(console.error).toHaveBeenCalledWith('error log'); - expect(console.warn).toHaveBeenCalledWith('warn log'); - expect(console.info).toHaveBeenCalledWith('info log'); - expect(console.debug).toHaveBeenCalledWith('debug log'); - expect(console.debug).toHaveBeenCalledWith('verbose log'); - }); +// expect(console.error).toHaveBeenCalledWith('error log'); +// expect(console.warn).toHaveBeenCalledWith('warn log'); +// expect(console.info).toHaveBeenCalledWith('info log'); +// expect(console.debug).toHaveBeenCalledWith('debug log'); +// expect(console.debug).toHaveBeenCalledWith('verbose log'); +// }); - it('should logger as default', () => { - const logger = new Logger(); - logger.error('error log'); - logger.warn('warn log'); - logger.info('info log'); - logger.debug('debug log'); - logger.verbose('verbose log'); +// it('should logger as default', () => { +// const logger = new Logger(); +// logger.error('error log'); +// logger.warn('warn log'); +// logger.info('info log'); +// logger.debug('debug log'); +// logger.verbose('verbose log'); - expect(console.error).toHaveBeenCalledWith('error log'); - expect(console.warn).toHaveBeenCalledWith('warn log'); - expect(console.info).toHaveBeenCalledWith('info log'); - expect(console.debug).not.toHaveBeenCalled(); - }); +// expect(console.error).toHaveBeenCalledWith('error log'); +// expect(console.warn).toHaveBeenCalledWith('warn log'); +// expect(console.info).toHaveBeenCalledWith('info log'); +// expect(console.debug).not.toHaveBeenCalled(); +// }); - it('should support tag/time', () => { - const logger = new Logger({ tag: 'A', showTag: true, showTime: true }); +// it('should support tag/time', () => { +// const logger = new Logger({ tag: 'A', showTag: true, showTime: true }); - logger.info('info log'); - expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/\[\d+-\d+-\d+ \d+:\d+:\d+\] \[A\] info log/)); - }); +// logger.info('info log'); +// expect(console.info).toHaveBeenCalledWith(expect.stringMatching(/\[\d+-\d+-\d+ \d+:\d+:\d+\] \[A\] info log/)); +// }); - it('should child()', () => { - const logger = new Logger('A'); - const childLogger = logger.child('B', { showTag: true, showTime: true }); - expect(childLogger === logger.child('B')); - childLogger.warn('info log'); - expect(console.warn).toHaveBeenCalledWith(expect.stringMatching(/\[\d+-\d+-\d+ \d+:\d+:\d+\]\s{3}\[A:B\] info log/)); - }); -}); +// it('should child()', () => { +// const logger = new Logger('A'); +// const childLogger = logger.child('B', { showTag: true, showTime: true }); +// expect(childLogger === logger.child('B')); +// childLogger.warn('info log'); +// expect(console.warn).toHaveBeenCalledWith(expect.stringMatching(/\[\d+-\d+-\d+ \d+:\d+:\d+\]\s{3}\[A:B\] info log/)); +// }); +// }); diff --git a/test/process.test.ts b/test/process.test.ts index f9c3ae7..0dba35a 100644 --- a/test/process.test.ts +++ b/test/process.test.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import assert from 'node:assert/strict'; import execa from 'execa'; -import { Process } from '../src/process.js'; +import { Process } from '../src/lib/process'; describe('test/process.test.ts', () => { describe('options', () => { diff --git a/test/runner.test.ts b/test/runner.test.ts index 78193dd..9a9f53d 100644 --- a/test/runner.test.ts +++ b/test/runner.test.ts @@ -2,26 +2,33 @@ import { runner } from '../src/runner'; describe('test/runner.test.ts', () => { describe('process', () => { - it('should spawn', async () => { - await runner() - .spawn('node', [ '-p', 'process.version', '--inspect' ]) - .stdout(/v\d+\.\d+\.\d+/) - .notStdout('some text') - .stderr(/Debugger listening on/) - .notStderr('some error') - .tap(async runner => { - console.log('@@@', runner); - }) - .end(); - }); + // it('should spawn', async () => { + // await runner() + // .spawn('node', [ '-p', 'process.version', '--inspect' ]) + // .stdout(/v\d+\.\d+\.\d+/) + // .notStdout('some text') + // .stderr(/Debugger listening on/) + // .notStderr('some error') + // .tap(async runner => { + // console.log('@@@', runner); + // }) + // .end(); + // }); + + // it('should fork', async () => { + // await runner() + // .fork('test/fixtures/process/fork.ts') + // .stdout(/v\d+\.\d+\.\d+/) + // .notStdout('some text') + // .stderr(/this is testing/) + // .notStderr('some error') + // .end(); + // }); - it('should fork', async () => { + it.only('should correct error stack', async () => { await runner() .fork('test/fixtures/process/fork.ts') - .stdout(/v\d+\.\d+\.\d+/) - .notStdout('some text') - .stderr(/this is testing/) - .notStderr('some error') + .stdout(/abc/) .end(); }); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..d31f6c1 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "inlineSourceMap": true, + }, + "ts-node": { + "transpileOnly": true, + "require": [] + }, + "include": [ + "**/*.ts" + ] +} diff --git a/test/utils.test.ts b/test/utils.test.ts index d5cb216..55bc5a0 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,8 +1,7 @@ -import { it, describe, beforeEach } from 'vitest'; import fs from 'fs'; import path from 'path'; import { strict as assert } from 'assert'; -import * as utils from '../src/utils.js'; +import * as utils from '../src/lib/utils.js'; import * as testUtils from '../test-old/test-utils.js'; describe('test/utils.test.ts', () => { diff --git a/tsconfig.json b/tsconfig.json index aab23a6..0e3140b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,8 +2,7 @@ "extends": "@artus/tsconfig", "compilerOptions": { "baseUrl": ".", - "noUnusedParameters": false, - "noUnusedLocals":false, + "strictNullChecks": true, "resolveJsonModule": true, "outDir": "dist" }, diff --git a/vitest.config.js b/vitest.config.js deleted file mode 100644 index 42100a8..0000000 --- a/vitest.config.js +++ /dev/null @@ -1,22 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - // plugins: [ { - // name: 'vitest-setup-plugin', - // config: () => ({ - // test: { - // setupFiles: [ - // './setupFiles/add-something-to-global.ts', - // 'setupFiles/without-relative-path-prefix.ts', - // ], - // }, - // }), - // } ], - test: { - globals: true, - include: [ 'test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}' ], - globalSetup: [ - // './test/setup.js', - ], - }, -}); From b13324ec963496f400b17b70455cc4eb35f8a066 Mon Sep 17 00:00:00 2001 From: TZ Date: Tue, 27 Dec 2022 16:02:23 +0800 Subject: [PATCH 10/14] f --- lib/utils.js | 23 ++++++++++++----------- package.json | 11 ++++++----- src/lib/utils.ts | 31 +++++++++++++++++++++++++++++++ src/runner.ts | 5 ++--- test/process.test.ts | 4 ++-- test/utils.test.ts | 14 +++++++------- 6 files changed, 60 insertions(+), 28 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index e62775b..9cec79b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -12,6 +12,7 @@ types.isFunction = function(v) { return typeof v === 'function'; }; export { types, isMatch }; + /** * validate input with expected rules * @@ -103,17 +104,17 @@ export async function exists(filePath) { } } -/** - * resolve file path by import.meta, kind of __dirname for esm - * - * @param {Object} meta - import.meta - * @param {...String} args - other paths - * @return {String} file path - */ -export function resolve(meta, ...args) { - const p = types.isObject(meta) ? dirname(meta) : meta; - return path.resolve(p, ...args); -} +// /** +// * resolve file path by import.meta, kind of __dirname for esm +// * +// * @param {Object} meta - import.meta +// * @param {...String} args - other paths +// * @return {String} file path +// */ +// export function resolve(meta, ...args) { +// const p = types.isObject(meta) ? dirname(meta) : meta; +// return path.resolve(p, ...args); +// } /** * take a sleep diff --git a/package.json b/package.json index 1a4cf9c..c404442 100644 --- a/package.json +++ b/package.json @@ -32,29 +32,30 @@ "dirname-filename-esm": "^1.1.1", "dot-prop": "^7.2.0", "execa": "^5", + "extract-stack": "^2", "lodash.ismatch": "^4.4.0", "p-event": "^4", "strip-ansi": "^6", "strip-final-newline": "^2", "throwback": "^4.1.0", - "trash": "^8.1.0" + "trash": "^7" }, "devDependencies": { "@artus/eslint-config-artus": "^0.0.1", "@artus/tsconfig": "^1", "@types/mocha": "^9.1.1", "@types/node": "^18.7.14", + "c8": "^7.12.0", "cross-env": "^7.0.3", "enquirer": "^2.3.6", "eslint": "^7", "eslint-config-egg": "^9", + "mocha": "^10.0.0", "supertest": "^6.2.3", + "ts-mocha": "^10.0.0", "ts-node": "^10.9.1", "tslib": "^2.4.0", - "typescript": "^4.8.2", - "c8": "^7.12.0", - "mocha": "^10.0.0", - "ts-mocha": "^10.0.0" + "typescript": "^4.8.2" }, "license": "MIT" } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 9e9b5f5..8d90f41 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import isMatch from 'lodash.ismatch'; import trash from 'trash'; +import { EOL } from 'node:os'; const types = { ...util.types, @@ -20,6 +21,36 @@ const types = { export { types, isMatch }; +const extractPathRegex = /\s+at.*[(\s](.*):\d+:\d+\)?/; +export function wrapFn(fn: (...args: any[]) => Promise) { + let end = false; + 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; + } + return true; + }) + .reverse() + .slice(0, 10) + .join(EOL); + + return async (...args: any[]) => { + 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); + throw err; + } + }; +} + /** * validate input with expected rules * diff --git a/src/runner.ts b/src/runner.ts index 16dbb99..d8796ec 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -3,6 +3,7 @@ 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 { doesNotMatchRule, matchRule, matchFile, doesNotMatchFile } from './lib/assert'; @@ -55,9 +56,7 @@ export class TestRunner extends EventEmitter { } hook(event: string, fn: AsyncFunction) { - const buildError = new Error('only for stack'); - console.log(buildError.stack); - this.hooks[event].push(fn); + this.hooks[event].push(wrapFn(fn)); return this; } diff --git a/test/process.test.ts b/test/process.test.ts index 0dba35a..1b1be5b 100644 --- a/test/process.test.ts +++ b/test/process.test.ts @@ -36,7 +36,7 @@ describe('test/process.test.ts', () => { assert.strictEqual(proc.result.code, 0); }); - it.only('should spawn not-exits', async () => { + it('should spawn not-exits', async () => { const proc = new Process('spawn', 'not-exists'); await proc.start(); const a = await proc.end(); @@ -45,7 +45,7 @@ describe('test/process.test.ts', () => { assert.strictEqual(proc.result.code, 127); }); - it.only('should fork not-exits', async () => { + it('should fork not-exits', async () => { const proc = new Process('fork', 'not-exists.ts'); await proc.start(); const a = await proc.end(); diff --git a/test/utils.test.ts b/test/utils.test.ts index 55bc5a0..7a87dbf 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,8 +1,8 @@ import fs from 'fs'; import path from 'path'; import { strict as assert } from 'assert'; -import * as utils from '../src/lib/utils.js'; -import * as testUtils from '../test-old/test-utils.js'; +import * as utils from '../src/lib/utils'; +import * as testUtils from '../test-old/test-utils'; describe('test/utils.test.ts', () => { const tmpDir = path.join(__dirname, './tmp'); @@ -61,11 +61,11 @@ describe('test/utils.test.ts', () => { assert(!await utils.exists('not-exists-file')); }); - it('resolve meta', async () => { - const p = utils.resolve(import.meta, '../test', './fixtures'); - const isExist = await utils.exists(p); - assert(isExist); - }); + // it('resolve meta', async () => { + // const p = utils.resolve(import.meta, '../test', './fixtures'); + // const isExist = await utils.exists(p); + // assert(isExist); + // }); it('resolve', async () => { const p = utils.resolve('test', './fixtures'); From e527c7d6c0f3eda2576884e23f3d5f9ea0b4b7f7 Mon Sep 17 00:00:00 2001 From: TZ Date: Wed, 28 Dec 2022 17:35:54 +0800 Subject: [PATCH 11/14] f --- .mocharc.cjs | 9 ++++ .mocharc.yml | 5 --- DEVELOPMENT.md | 7 ++++ package.json | 10 ++--- src/index.ts | 11 +++++ src/lib/process.ts | 24 ++++++++--- src/{ => lib}/types.ts | 6 --- src/lib/utils.ts | 25 +++++++---- src/plugins/operation.ts | 8 ++-- src/plugins/validator.ts | 8 +++- src/runner.ts | 65 ++++++++++++++++++----------- test/plugins/validator.test.ts | 76 ++++++++++++++++++++++++++++++++++ test/runner.test.ts | 56 ++++++++++++++----------- test/utils.test.ts | 8 ++-- 14 files changed, 229 insertions(+), 89 deletions(-) create mode 100644 .mocharc.cjs delete mode 100644 .mocharc.yml create mode 100644 src/index.ts rename src/{ => lib}/types.ts (65%) create mode 100644 test/plugins/validator.test.ts diff --git a/.mocharc.cjs b/.mocharc.cjs new file mode 100644 index 0000000..a88b0c1 --- /dev/null +++ b/.mocharc.cjs @@ -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/', +}; diff --git a/.mocharc.yml b/.mocharc.yml deleted file mode 100644 index 7a6e646..0000000 --- a/.mocharc.yml +++ /dev/null @@ -1,5 +0,0 @@ -spec: test/**/*.test.ts -extension: ts -require: ts-node/register -timeout: 120000 -exclude: test/fixtures/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index aa26689..babf456 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -20,3 +20,10 @@ // koa middleware // 初始化 -> fork -> await next() -> 校验 -> 结束 + + + // prepare 准备现场环境 + // prerun 检查参数,在 fork 定义之后 + // run 处理 stdin + // postrun 检查 assert + // end 检查 code,清理现场,相当于 finnaly diff --git a/package.json b/package.json index c404442..e5192ac 100644 --- a/package.json +++ b/package.json @@ -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" }, @@ -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", diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..8dbc787 --- /dev/null +++ b/src/index.ts @@ -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); +} diff --git a/src/lib/process.ts b/src/lib/process.ts index 6e96b43..fcfe827 100644 --- a/src/lib/process.ts +++ b/src/lib/process.ts @@ -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'); @@ -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); @@ -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; } }); @@ -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 => { @@ -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; diff --git a/src/types.ts b/src/lib/types.ts similarity index 65% rename from src/types.ts rename to src/lib/types.ts index cefe32b..7577b19 100644 --- a/src/types.ts +++ b/src/lib/types.ts @@ -2,12 +2,6 @@ export type MountPlugin = { [key in keyof T]: T[key] extends (core: Core, ...args: infer I) => any ? (...args: I) => MountPlugin : T[key]; } & Core; -export type BuiltinPlugin = { - [key in keyof T]: (...args: RestParam) => Core; -}; - -export type AsyncFunction = (...args: any[]) => Promise; - export type RestParam = T extends (first: any, ...args: infer R) => any ? R : any; export interface PluginLike { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 8d90f41..ddb74d2 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -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, @@ -22,17 +22,19 @@ const types = { export { types, isMatch }; const extractPathRegex = /\s+at.*[(\s](.*):\d+:\d+\)?/; -export function wrapFn(fn: (...args: any[]) => Promise) { - let end = false; +const testFileRegex = /\.(test|spec)\.(ts|mts|cts|js|cjs|mjs)$/; + +export function wrapFn 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; }) @@ -40,15 +42,22 @@ export function wrapFn(fn: (...args: any[]) => Promise) { .slice(0, 10) .join(EOL); - return async (...args: any[]) => { + const wrappedFn = async function (...args: Parameters) { 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; } /** diff --git a/src/plugins/operation.ts b/src/plugins/operation.ts index 754efa1..a1d7c42 100644 --- a/src/plugins/operation.ts +++ b/src/plugins/operation.ts @@ -1,7 +1,7 @@ -import type { TestRunner } from '../runner'; +import type { TestRunner, HookFunction } from '../runner'; -export function tap(runner: TestRunner, fn: (runner: TestRunner) => Promise) { - 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); }); } diff --git a/src/plugins/validator.ts b/src/plugins/validator.ts index 107b53b..6876d8b 100644 --- a/src/plugins/validator.ts +++ b/src/plugins/validator.ts @@ -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); diff --git a/src/runner.ts b/src/runner.ts index d8796ec..fa80dee 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -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; +export type RestParam = T extends (first: any, ...args: infer R) => any ? R : any; + +export type MountPlugin = { + [key in keyof T]: T[key] extends (core: TestRunner, ...args: infer I) => any ? (...args: I) => MountPlugin : 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 = { + private hooks: Record = { before: [], running: [], after: [], @@ -31,23 +42,17 @@ 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 { + plugin(plugins: T): MountPlugin { for (const key of Object.keys(plugins)) { const initFn = plugins[key]; this[key] = (...args: RestParam) => { - console.log('mount %s with %s', key, ...args); initFn(this, ...args); return this; }; @@ -55,17 +60,18 @@ export class TestRunner extends EventEmitter { 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 @@ -81,7 +87,7 @@ export class TestRunner extends EventEmitter { await fn(ctx); } - if (this.options.autoWait) { + if (ctx.autoWait) { await this.proc.end(); } @@ -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(); + // } } diff --git a/test/plugins/validator.test.ts b/test/plugins/validator.test.ts new file mode 100644 index 0000000..ac6f33c --- /dev/null +++ b/test/plugins/validator.test.ts @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict'; +import { runner } from '../../src/index'; + +describe.only('test/plugins/validator.test.ts', () => { + it.only('should stdout() / stderr()', async () => { + await runner() + .spawn('node1', ['-p', 'process.version', '--inspect']) + .stdout(/v\d+\.\d+\.\d+/) + .notStdout('some text') + .stderr(/Debugger listening on/) + .notStderr('some error') + .end(); + + await runner() + .fork('test/fixtures/process/fork.ts') + .stdout(/v\d+\.\d+\.\d+/) + .notStdout('some text') + .stderr(/this is testing/) + .notStderr('some error') + .end(); + }); + + it('should expect()', async () => { + await runner() + .spawn('npm', ['-v']) + .expect(ctx => { + assert.equal(ctx.result.code, 0); + }) + .end(); + + await assert.rejects(async () => { + await runner() + .spawn('npm', ['-v']) + .expect(ctx => { + assert.equal(ctx.result.code, 1); + }) + .end(); + }, /Expected values to be strictly equal/); + }); + + describe('error stack', () => { + it.only('should correct error stack', async function test_stack() { + try { + await runner() + .spawn('npm', ['-v']) + .stdout(/abc/) + .end(); + } catch (err) { + console.error(err); + const index = err.stack!.indexOf(' at '); + const lineEndIndex = err.stack!.indexOf('\n', index); + const line = err.stack!.slice(index, lineEndIndex); + // console.log(line); + assert(line.startsWith(' at Context.test_stack')); + } + }); + + it('should not correct error stack', async function test_stack() { + try { + await runner() + .spawn('npm', ['-v']) + .expect(ctx => { + assert.equal(ctx.result.stdout, 1); + }) + .stdout(/abc/) + .end(); + } catch (err) { + console.error(err); + const index = err.stack!.indexOf(' at '); + const lineEndIndex = err.stack!.indexOf('\n', index); + const line = err.stack!.slice(index, lineEndIndex); + assert(line.startsWith(' at TestRunner.')); + } + }); + }); +}); diff --git a/test/runner.test.ts b/test/runner.test.ts index 9a9f53d..79f6134 100644 --- a/test/runner.test.ts +++ b/test/runner.test.ts @@ -1,37 +1,43 @@ -import { runner } from '../src/runner'; +import assert from 'node:assert/strict'; +import { runner } from '../src/index'; describe('test/runner.test.ts', () => { describe('process', () => { - // it('should spawn', async () => { - // await runner() - // .spawn('node', [ '-p', 'process.version', '--inspect' ]) - // .stdout(/v\d+\.\d+\.\d+/) - // .notStdout('some text') - // .stderr(/Debugger listening on/) - // .notStderr('some error') - // .tap(async runner => { - // console.log('@@@', runner); - // }) - // .end(); - // }); - - // it('should fork', async () => { - // await runner() - // .fork('test/fixtures/process/fork.ts') - // .stdout(/v\d+\.\d+\.\d+/) - // .notStdout('some text') - // .stderr(/this is testing/) - // .notStderr('some error') - // .end(); - // }); + it('should spawn', async () => { + await runner() + .spawn('node', [ '-p', 'process.version', '--inspect' ]) + .stdout(/v\d+\.\d+\.\d+/) + .notStdout('some text') + .stderr(/Debugger listening on/) + .notStderr('some error') + .end(); + }); - it.only('should correct error stack', async () => { + it('should fork', async () => { await runner() .fork('test/fixtures/process/fork.ts') - .stdout(/abc/) + .stdout(/v\d+\.\d+\.\d+/) + .notStdout('some text') + .stderr(/this is testing/) + .notStderr('some error') .end(); }); + it('should correct error stack', async function test_stack() { + try { + await runner() + .spawn('npm', [ '-v' ]) + .stdout(/abc/) + .end(); + } catch (err) { + const index = err.stack!.indexOf(' at '); + const lineEndIndex = err.stack!.indexOf('\n', index); + const line = err.stack!.slice(index, lineEndIndex); + // console.log(line); + assert(line.startsWith(' at Context.test_stack')); + } + }); + // it.only('should wait stdout', async () => { // await runner() // .fork('test/fixtures/process/long-run.ts') diff --git a/test/utils.test.ts b/test/utils.test.ts index 7a87dbf..ea9f676 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -67,10 +67,10 @@ describe('test/utils.test.ts', () => { // assert(isExist); // }); - it('resolve', async () => { - const p = utils.resolve('test', './fixtures'); - assert(fs.existsSync(p)); - }); + // it('resolve', async () => { + // const p = utils.resolve('test', './fixtures'); + // assert(fs.existsSync(p)); + // }); it('sleep', async () => { const start = Date.now(); From c4fa80efc134751e359ba52aea73148b616dae07 Mon Sep 17 00:00:00 2001 From: TZ Date: Thu, 29 Dec 2022 21:22:11 +0800 Subject: [PATCH 12/14] f --- lib/utils.js | 7 +-- package.json | 2 +- src/index.ts | 6 +- src/lib/process.ts | 55 +++++++---------- src/lib/utils.ts | 10 ++- src/plugins/operation.ts | 12 ++-- src/plugins/validator.ts | 24 +++----- src/runner.ts | 80 +++++++++++++++--------- test/{ => lib}/assert.test.ts | 4 +- test/lib/process.test.ts | 107 +++++++++++++++++++++++++++++++++ test/plugins/validator.test.ts | 12 ++-- test/process.test.ts | 101 ------------------------------- 12 files changed, 221 insertions(+), 199 deletions(-) rename test/{ => lib}/assert.test.ts (98%) create mode 100644 test/lib/process.test.ts delete mode 100644 test/process.test.ts diff --git a/lib/utils.js b/lib/utils.js index 9cec79b..781f79b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,8 +1,7 @@ -import { promises as fs } from 'fs'; -import { types } from 'util'; -import path from 'path'; +import fs from 'node:fs/promises'; +import { types } from 'node:util'; +import path from 'node:path'; -import { dirname } from 'dirname-filename-esm'; import isMatch from 'lodash.ismatch'; import trash from 'trash'; diff --git a/package.json b/package.json index e5192ac..3b9a910 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "lint": "eslint . --ext .ts", "postlint": "tsc --noEmit", "test": "mocha", - "cov": "c8 -n src/ npm test", + "cov": "c8 -n src/ -r html -r text npm test", "ci": "npm run cov", "tsc": "rm -rf dist && tsc", "prepack": "npm run tsc" diff --git a/src/index.ts b/src/index.ts index 8dbc787..70d268c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ import * as validator from './plugins/validator'; import * as operation from './plugins/operation'; -import { TestRunner, RunnerOptions, PluginLike } from './runner'; +import { TestRunner, PluginLike } from './runner'; export * from './runner'; export * as assert from './lib/assert'; -export function runner(opts?: RunnerOptions) { - return new TestRunner(opts) +export function runner() { + return new TestRunner() .plugin({ ...validator, ...operation } satisfies PluginLike); } diff --git a/src/lib/process.ts b/src/lib/process.ts index fcfe827..0911da1 100644 --- a/src/lib/process.ts +++ b/src/lib/process.ts @@ -3,22 +3,18 @@ import { PassThrough } from 'node:stream'; import { EOL } from 'node:os'; import * as execa from 'execa'; +import { NodeOptions, ExecaReturnValue, ExecaChildProcess } from 'execa'; import pEvent from 'p-event'; import stripFinalNewline from 'strip-final-newline'; import stripAnsi from 'strip-ansi'; -export interface ProcessResult { - code: number | null; - stdout: string; - stderr: string; -} - export type ProcessOptions = { - -readonly [ key in keyof execa.NodeOptions ]: execa.NodeOptions[key]; + -readonly [ key in keyof NodeOptions ]: NodeOptions[key]; } & { - execArgv?: execa.NodeOptions['nodeOptions']; + execArgv?: NodeOptions['nodeOptions']; }; +export type ProcessResult = ExecaReturnValue; export type ProcessEvents = 'stdout' | 'stderr' | 'message' | 'exit' | 'close'; export class Process extends EventEmitter { @@ -27,9 +23,7 @@ export class Process extends EventEmitter { args: string[]; opts: ProcessOptions; result: ProcessResult; - proc: execa.ExecaChildProcess; - - private isDebug = false; + proc: ExecaChildProcess; constructor(type: Process['type'], cmd: string, args: string[] = [], opts: ProcessOptions = {}) { super(); @@ -57,10 +51,9 @@ export class Process extends EventEmitter { // need to test color this.result = { - code: null, stdout: '', stderr: '', - }; + } as any; } write(data: string) { @@ -81,10 +74,6 @@ 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); @@ -94,33 +83,33 @@ export class Process extends EventEmitter { } this.proc.then(res => { + this.result = { + ...res, + ...this.result, + }; + if (res instanceof Error) { - this.result.code = res.exitCode; + // when spawn not exist, code is ENOENT const { code, message } = res as any; if (code === 'ENOENT') { - this.result.code = 127; + this.result.exitCode = 127; this.result.stderr += message; } - // TODO: failed to start - // this.result.stdout = res.stdout; - // this.result.stderr = res.stderr; } }); - // this.proc.stdin.setEncoding('utf8'); - this.proc.stdout!.on('data', data => { const origin = stripFinalNewline(data.toString()); const content = stripAnsi(origin); this.result.stdout += content; - if (this.isDebug) console.log(origin); + this.emit('stdout', origin); }); this.proc.stderr!.on('data', data => { const origin = stripFinalNewline(data.toString()); const content = stripAnsi(origin); this.result.stderr += content; - if (this.isDebug) console.error(origin); + this.emit('stderr', origin); }); this.proc.on('message', data => { @@ -128,14 +117,14 @@ export class Process extends EventEmitter { // console.log('message event:', data); }); - this.proc.once('exit', code => { - this.result.code = code; - // console.log('close event:', code); - }); + // this.proc.once('exit', code => { + // this.result.exitCode = code; + // // console.log('close event:', code); + // }); - this.proc.on('error', err => { - if (this.isDebug) console.error(err); - }); + // this.proc.on('error', err => { + // console.log('@@error', err); + // }); // this.proc.once('close', code => { // // this.emit('close', code); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ddb74d2..8fb07cd 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -2,6 +2,7 @@ import fs from 'node:fs/promises'; import util from 'node:util'; import path from 'node:path'; import { EOL } from 'node:os'; +import tty from 'node:tty'; import isMatch from 'lodash.ismatch'; import trash from 'trash'; @@ -47,7 +48,7 @@ export function wrapFn any>(fn: T): T { return await fn(...args); } catch (err) { const index = err.stack!.indexOf(' at '); - const lineEndIndex = err.stack!.indexOf('\n', index); + const lineEndIndex = err.stack!.indexOf(EOL, 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); @@ -165,3 +166,10 @@ export async function sleep(ms: number) { setTimeout(resolve, ms); }); } + +export function color(str: string, startCode: number, endCode: number) { + // https://github.com/sindresorhus/yoctocolors + const hasColors = tty.WriteStream.prototype.hasColors(); + if (!hasColors) return str; + return '\u001B[' + startCode + 'm' + str + '\u001B[' + endCode + 'm'; +} diff --git a/src/plugins/operation.ts b/src/plugins/operation.ts index a1d7c42..32ae0d1 100644 --- a/src/plugins/operation.ts +++ b/src/plugins/operation.ts @@ -1,7 +1,7 @@ -import type { TestRunner, HookFunction } from '../runner'; +// import type { TestRunner, HookFunction } from '../runner'; -export function tap(runner: TestRunner, fn: HookFunction) { - return runner.hook('after', async ctx => { - await fn.call(runner, ctx); - }); -} +// export function tap(runner: TestRunner, fn: HookFunction) { +// return runner.hook('after', async ctx => { +// await fn.call(runner, ctx); +// }); +// } diff --git a/src/plugins/validator.ts b/src/plugins/validator.ts index 6876d8b..03c7221 100644 --- a/src/plugins/validator.ts +++ b/src/plugins/validator.ts @@ -1,54 +1,48 @@ import path from 'node:path'; import assert from 'node:assert/strict'; -import type { TestRunner, HookFunction } from '../runner'; +import type { TestRunner } 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 => { + return runner.expect(async ctx => { matchRule(ctx.result.stdout, expected); }); } export function notStdout(runner: TestRunner, expected: string | RegExp) { - return runner.hook('after', async ctx => { + return runner.expect(async ctx => { doesNotMatchRule(ctx.result.stdout, expected); }); } export function stderr(runner: TestRunner, expected: string | RegExp) { - return runner.hook('after', async ctx => { + return runner.expect(async ctx => { matchRule(ctx.result.stderr, expected); }); } export function notStderr(runner: TestRunner, expected: string | RegExp) { - return runner.hook('after', async ({ result }) => { + return runner.expect(async ({ result }) => { doesNotMatchRule(result.stderr, expected); }); } export function file(runner: TestRunner, filePath: string, expected: string | RegExp) { - return runner.hook('after', async ({ cwd }) => { + return runner.expect(async ({ cwd }) => { const fullPath = path.resolve(cwd, filePath); await matchFile(fullPath, expected); }); } export function notFile(runner: TestRunner, filePath: string, expected: string | RegExp) { - return runner.hook('after', async ({ cwd }) => { + return runner.expect(async ({ cwd }) => { const fullPath = path.resolve(cwd, filePath); await doesNotMatchFile(fullPath, expected); }); } export function code(runner: TestRunner, expected: number) { - return runner.hook('after', async ({ result }) => { - assert.equal(result.code, expected); + return runner.expect(async ({ result }) => { + assert.equal(result.exitCode, expected); }); } diff --git a/src/runner.ts b/src/runner.ts index fa80dee..838332a 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -1,8 +1,9 @@ import EventEmitter from 'events'; import assert from 'node:assert/strict'; +import logger from 'consola'; import { Process, ProcessEvents, ProcessOptions, ProcessResult } from './lib/process'; -import { wrapFn } from './lib/utils'; +import { wrapFn, color } from './lib/utils'; // import { doesNotMatchRule, matchRule, matchFile, doesNotMatchFile } from './lib/assert'; export type HookFunction = (ctx: RunnerContext) => void | Promise; @@ -17,21 +18,17 @@ export interface PluginLike { [key: string]: (core: TestRunner, ...args: any[]) => any; } -export interface RunnerOptions { - autoWait?: boolean; -} - export interface RunnerContext { proc: Process; cwd: string; result: ProcessResult; autoWait?: boolean; + debug?: boolean; } export class TestRunner extends EventEmitter { - private logger = console; + private logger = logger; private proc: Process; - private options: RunnerOptions = {}; private hooks: Record = { before: [], running: [], @@ -39,15 +36,6 @@ export class TestRunner extends EventEmitter { end: [], }; - constructor(opts?: RunnerOptions) { - super(); - this.options = { - // autoWait: true, - ...opts, - }; - // console.log(this.options); - } - plugin(plugins: T): MountPlugin { for (const key of Object.keys(plugins)) { const initFn = plugins[key]; @@ -65,6 +53,12 @@ export class TestRunner extends EventEmitter { return this; } + async runHook(event: string, ctx: RunnerContext) { + for (const fn of this.hooks[event]) { + await fn(ctx); + } + } + async end() { try { const ctx: RunnerContext = { @@ -74,26 +68,40 @@ export class TestRunner extends EventEmitter { autoWait: true, }; + assert(this.proc, 'cmd is not registered yet'); + // before - for (const fn of this.hooks['before']) { - await fn(ctx); - } + await this.runHook('before', ctx); // exec child process, don't await it await this.proc.start(); + this.proc.on('stdout', data => { + console.log(color(data, 2, 22)); + }); + + this.proc.on('stderr', data => { + console.error(color(data, 2, 22)); + }); + // running - for (const fn of this.hooks['running']) { - await fn(ctx); - } + await this.runHook('running', ctx); if (ctx.autoWait) { await this.proc.end(); } - // postrun - for (const fn of this.hooks['after']) { - await fn(ctx); + // after + await this.runHook('after', ctx); + + // ensure proc is exit if user forgot to call `wait('close')` after wait other event + if (!ctx.autoWait) { + await this.proc.end(); + } + + // error + if (ctx.result instanceof Error) { + await this.runHook('error', ctx); } // end @@ -101,9 +109,9 @@ export class TestRunner extends EventEmitter { await fn(ctx); } - this.logger.info('✔ Test pass.\n'); + this.logger.success('Test pass.\n'); } catch (err) { - this.logger.error('⚠ Test failed.\n'); + this.logger.error('Test failed.\n'); throw err; } finally { // clean up @@ -123,6 +131,24 @@ export class TestRunner extends EventEmitter { return this; } + debug(enabled = true) { + return this.hook('before', ctx => { + ctx.debug = enabled; + // this.proc.debug(enabled); + }); + } + + tap(fn: HookFunction) { + return this.hook(this.proc ? 'after' : 'before', fn); + } + + expect(fn: HookFunction) { + return this.tap(async ctx => { + // TODO: wrapfn here + await fn.call(this, ctx); + }); + } + wait(type: ProcessEvents, expected) { // prevent auto wait this.hook('before', ctx => { diff --git a/test/assert.test.ts b/test/lib/assert.test.ts similarity index 98% rename from test/assert.test.ts rename to test/lib/assert.test.ts index 0150acd..4352740 100644 --- a/test/assert.test.ts +++ b/test/lib/assert.test.ts @@ -1,9 +1,9 @@ import path from 'node:path'; import assert from 'node:assert/strict'; -import { matchRule, doesNotMatchRule, matchFile, doesNotMatchFile } from '../src/lib/assert'; +import { matchRule, doesNotMatchRule, matchFile, doesNotMatchFile } from '../../src/lib/assert'; -describe('test/assert.test.ts', () => { +describe('test/lib/assert.test.ts', () => { const pkgInfo = { name: 'clet', version: '1.0.0', diff --git a/test/lib/process.test.ts b/test/lib/process.test.ts new file mode 100644 index 0000000..ab00821 --- /dev/null +++ b/test/lib/process.test.ts @@ -0,0 +1,107 @@ +import path from 'node:path'; +import assert from 'node:assert/strict'; +import { Process } from '../../src/lib/process'; + +describe('test/process.test.ts', () => { + it('should merge options', () => { + const proc = new Process('fork', 'fixtures/process/fork.ts', ['--foo', 'bar'], { + nodeOptions: ['--inspect-brk'], + }); + + assert.strictEqual(proc.opts.cwd, process.cwd()); + assert.strictEqual(proc.opts.nodeOptions?.[0], '--inspect-brk'); + }); + + it('should spawn', async () => { + const proc = new Process('spawn', 'node', ['-p', 'process.version', '--inspect'], {}); + await proc.start(); + await proc.end(); + // console.log(proc.result); + assert.match(proc.result.stdout, /v\d+\.\d+\.\d+/); + assert.match(proc.result.stderr, /Debugger listening on/); + assert.strictEqual(proc.result.exitCode, 0); + }); + + it('should fork', async () => { + const cli = path.resolve(__dirname, '../fixtures/process/fork.ts'); + const proc = new Process('fork', cli); + await proc.start(); + await proc.end(); + // console.log(proc.result); + assert.match(proc.result.stdout, /v\d+\.\d+\.\d+/); + assert.match(proc.result.stderr, /this is testing/); + assert.strictEqual(proc.result.exitCode, 0); + }); + + it('should spawn not-exits', async () => { + const proc = new Process('spawn', 'not-exists'); + await proc.start(); + await proc.end(); + // console.log(proc.result); + assert.match(proc.result.stderr, /Command failed with ENOENT/); + assert.strictEqual(proc.result.exitCode, 127); + }); + + it('should fork not-exits', async () => { + const proc = new Process('fork', 'not-exists.ts'); + await proc.start(); + await proc.end(); + assert.match(proc.result.stderr, /Cannot find module/); + assert.strictEqual(proc.result.exitCode, 1); + }); + + it('should strip color', async () => { + const cli = path.resolve(__dirname, '../fixtures/process/color.ts'); + const proc = new Process('fork', cli, []); + await proc.start(); + await proc.end(); + // console.log(proc.result); + assert.match(proc.result.stdout, /CLIHub/); + assert.match(proc.result.stderr, /MSGHub/); + assert.strictEqual(proc.result.exitCode, 0); + }); + + it('should exit with fail', async () => { + const cli = path.resolve(__dirname, '../fixtures/process/error.ts'); + const proc = new Process('fork', cli, []); + await proc.start(); + await proc.end().catch(err => err); + // console.log(proc.result); + assert.match(proc.result.stdout, /this is an error test/); + assert.match(proc.result.stderr, /Error: some error/); + assert.strictEqual(proc.result.exitCode, 1); + }); + + it.skip('should env()'); + + it.skip('should cwd()'); + + it.skip('should write()'); + + it.skip('should kill()'); + + it.skip('should await()'); + + // it.skip('should execa work', async () => { + // const proc = execa.node('src/try/child.ts', ['--foo', 'bar'], { + // nodeOptions: [ '--require', 'ts-node/register' ], + // }); + + // proc.stdout?.on('data', data => { + // console.log('stdout', data.toString()); + // }); + // // proc.stdin.setEncoding('utf8'); + + // // const stdin = new PassThrough(); + // // stdin.pipe(proc.stdin); + + // setTimeout(() => { + // console.log('write stdin'); + // proc.stdin?.write('hello\n'); + // proc.stdin?.end(); + // // stdin.end(); + // }, 1500); + + // await proc; + // }); +}); diff --git a/test/plugins/validator.test.ts b/test/plugins/validator.test.ts index ac6f33c..698411b 100644 --- a/test/plugins/validator.test.ts +++ b/test/plugins/validator.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { runner } from '../../src/index'; -describe.only('test/plugins/validator.test.ts', () => { - it.only('should stdout() / stderr()', async () => { +describe('test/plugins/validator.test.ts', () => { + it('should stdout() / stderr()', async () => { await runner() - .spawn('node1', ['-p', 'process.version', '--inspect']) + .spawn('node', ['-p', 'process.version', '--inspect']) .stdout(/v\d+\.\d+\.\d+/) .notStdout('some text') .stderr(/Debugger listening on/) @@ -24,7 +24,7 @@ describe.only('test/plugins/validator.test.ts', () => { await runner() .spawn('npm', ['-v']) .expect(ctx => { - assert.equal(ctx.result.code, 0); + assert.equal(ctx.result.exitCode, 0); }) .end(); @@ -32,14 +32,14 @@ describe.only('test/plugins/validator.test.ts', () => { await runner() .spawn('npm', ['-v']) .expect(ctx => { - assert.equal(ctx.result.code, 1); + assert.equal(ctx.result.exitCode, 1); }) .end(); }, /Expected values to be strictly equal/); }); describe('error stack', () => { - it.only('should correct error stack', async function test_stack() { + it('should correct error stack', async function test_stack() { try { await runner() .spawn('npm', ['-v']) diff --git a/test/process.test.ts b/test/process.test.ts deleted file mode 100644 index 1b1be5b..0000000 --- a/test/process.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import path from 'node:path'; -import assert from 'node:assert/strict'; -import execa from 'execa'; -import { Process } from '../src/lib/process'; - -describe('test/process.test.ts', () => { - describe('options', () => { - it('should merge options', () => { - const proc = new Process('fork', 'fixtures/process/fork.ts', ['--foo', 'bar'], { - nodeOptions: ['--inspect-brk'], - }); - - assert.strictEqual(proc.opts.cwd, process.cwd()); - assert.strictEqual(proc.opts.nodeOptions?.[0], '--inspect-brk'); - }); - }); - - it('should spawn', async () => { - const proc = new Process('spawn', 'node', ['-p', 'process.version', '--inspect'], {}); - await proc.start(); - await proc.end(); - // console.log(proc.result); - assert.match(proc.result.stdout, /v\d+\.\d+\.\d+/); - assert.match(proc.result.stderr, /Debugger listening on/); - assert.strictEqual(proc.result.code, 0); - }); - - it('should fork', async () => { - const cli = path.resolve(__dirname, 'fixtures/process/fork.ts'); - const proc = new Process('fork', cli); - await proc.start(); - await proc.end(); - // console.log(proc.result); - assert.match(proc.result.stdout, /v\d+\.\d+\.\d+/); - assert.match(proc.result.stderr, /this is testing/); - assert.strictEqual(proc.result.code, 0); - }); - - it('should spawn not-exits', async () => { - const proc = new Process('spawn', 'not-exists'); - await proc.start(); - const a = await proc.end(); - console.log(proc.result, a); - // assert.match(proc.result.stdout, /Cannot find module/); - assert.strictEqual(proc.result.code, 127); - }); - - it('should fork not-exits', async () => { - const proc = new Process('fork', 'not-exists.ts'); - await proc.start(); - const a = await proc.end(); - console.log(proc.result, a); - assert.match(proc.result.stderr, /Cannot find module/); - assert.strictEqual(proc.result.code, 127); - }); - - it('should strip color', async () => { - const cli = path.resolve(__dirname, 'fixtures/process/color.ts'); - const proc = new Process('fork', cli, []); - await proc.start(); - await proc.end(); - // console.log(proc.result); - assert.match(proc.result.stdout, /CLIHub/); - assert.match(proc.result.stderr, /MSGHub/); - assert.strictEqual(proc.result.code, 0); - }); - - it('should exit with fail', async () => { - const cli = path.resolve(__dirname, 'fixtures/process/error.ts'); - const proc = new Process('fork', cli, []); - await proc.start(); - await proc.end(); - console.log(proc.result); - assert.match(proc.result.stdout, /this is an error test/); - assert.match(proc.result.stderr, /Error: some error/); - assert.strictEqual(proc.result.code, 1); - }); - - it.skip('should execa work', async () => { - const proc = execa.node('src/try/child.ts', ['--foo', 'bar'], { - nodeOptions: [ '--require', 'ts-node/register' ], - }); - - proc.stdout?.on('data', data => { - console.log('stdout', data.toString()); - }); - // proc.stdin.setEncoding('utf8'); - - // const stdin = new PassThrough(); - // stdin.pipe(proc.stdin); - - setTimeout(() => { - console.log('write stdin'); - proc.stdin?.write('hello\n'); - proc.stdin?.end(); - // stdin.end(); - }, 1500); - - await proc; - }); -}); From b0e5f484b7981280ff474bf825a233600cd7ffde Mon Sep 17 00:00:00 2001 From: TZ Date: Thu, 29 Dec 2022 22:00:52 +0800 Subject: [PATCH 13/14] f --- src/lib/process.ts | 9 ++++----- src/plugins/operation.ts | 12 ++++++------ src/plugins/validator.ts | 15 +++++++++++++-- src/runner.ts | 23 +++++++++++++++++++---- test/runner.test.ts | 17 +++++++++++++++++ 5 files changed, 59 insertions(+), 17 deletions(-) diff --git a/src/lib/process.ts b/src/lib/process.ts index 0911da1..1dfd338 100644 --- a/src/lib/process.ts +++ b/src/lib/process.ts @@ -22,8 +22,8 @@ export class Process extends EventEmitter { cmd: string; args: string[]; opts: ProcessOptions; - result: ProcessResult; proc: ExecaChildProcess; + result: ProcessResult; constructor(type: Process['type'], cmd: string, args: string[] = [], opts: ProcessOptions = {}) { super(); @@ -83,10 +83,9 @@ export class Process extends EventEmitter { } this.proc.then(res => { - this.result = { - ...res, - ...this.result, - }; + this.result = res; + this.result.stdout = stripAnsi(this.result.stdout); + this.result.stderr = stripAnsi(this.result.stderr); if (res instanceof Error) { // when spawn not exist, code is ENOENT diff --git a/src/plugins/operation.ts b/src/plugins/operation.ts index 32ae0d1..48a87f2 100644 --- a/src/plugins/operation.ts +++ b/src/plugins/operation.ts @@ -1,7 +1,7 @@ -// import type { TestRunner, HookFunction } from '../runner'; +import type { TestRunner, HookFunction } from '../runner'; -// export function tap(runner: TestRunner, fn: HookFunction) { -// return runner.hook('after', async ctx => { -// await fn.call(runner, ctx); -// }); -// } +export function tap1(runner: TestRunner, fn: HookFunction) { + return runner.hook('after', async ctx => { + await fn.call(runner, ctx); + }); +} diff --git a/src/plugins/validator.ts b/src/plugins/validator.ts index 03c7221..576b242 100644 --- a/src/plugins/validator.ts +++ b/src/plugins/validator.ts @@ -42,7 +42,18 @@ export function notFile(runner: TestRunner, filePath: string, expected: string | } export function code(runner: TestRunner, expected: number) { - return runner.expect(async ({ result }) => { - assert.equal(result.exitCode, expected); + runner.expect(async ctx => { + ctx.autoCheckCode = false; + // when using `.wait()`, it could maybe not exit at this time, so skip and will double check it later + if (ctx.result.exitCode !== undefined) { + assert.equal(ctx.result.exitCode, expected); + } + }); + + // double check + runner.hook('end', async ctx => { + assert.equal(ctx.result.exitCode, expected); }); + + return runner; } diff --git a/src/runner.ts b/src/runner.ts index 838332a..7571bfc 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -18,22 +18,26 @@ export interface PluginLike { [key: string]: (core: TestRunner, ...args: any[]) => any; } +export type HookEventType = 'before' | 'running' | 'after' | 'end' | 'error'; + export interface RunnerContext { proc: Process; cwd: string; result: ProcessResult; autoWait?: boolean; + autoCheckCode?: boolean; debug?: boolean; } export class TestRunner extends EventEmitter { private logger = logger; private proc: Process; - private hooks: Record = { + private hooks: Record = { before: [], running: [], after: [], end: [], + error: [], }; plugin(plugins: T): MountPlugin { @@ -48,12 +52,12 @@ export class TestRunner extends EventEmitter { return this as any; } - hook(event: string, fn: HookFunction) { + hook(event: HookEventType, fn: HookFunction) { this.hooks[event].push(wrapFn(fn)); return this; } - async runHook(event: string, ctx: RunnerContext) { + async runHook(event: HookEventType, ctx: RunnerContext) { for (const fn of this.hooks[event]) { await fn(ctx); } @@ -64,8 +68,12 @@ export class TestRunner extends EventEmitter { const ctx: RunnerContext = { proc: this.proc, cwd: this.proc.opts.cwd!, - result: this.proc.result, + // use getter + get result() { + return this.proc.result; + }, autoWait: true, + autoCheckCode: true, }; assert(this.proc, 'cmd is not registered yet'); @@ -109,6 +117,13 @@ export class TestRunner extends EventEmitter { await fn(ctx); } + // if developer don't call `.code()`, will rethrow proc error in order to avoid omissions + if (ctx.autoCheckCode) { + // `killed` is true only if call `kill()/cancel()` manually + const { failed, isCanceled, killed } = ctx.result; + if (failed && !isCanceled && !killed) throw ctx.result; + } + this.logger.success('Test pass.\n'); } catch (err) { this.logger.error('Test failed.\n'); diff --git a/test/runner.test.ts b/test/runner.test.ts index 79f6134..023859b 100644 --- a/test/runner.test.ts +++ b/test/runner.test.ts @@ -23,6 +23,23 @@ describe('test/runner.test.ts', () => { .end(); }); + describe('code()', () => { + it('should skip auto check code when .code(1)', async () => { + await runner() + .fork('test/fixtures/process/error.ts') + .code(1) + .end(); + }); + + it('should auto check code when fail', async () => { + await assert.rejects(async () => { + await runner() + .fork('test/fixtures/process/error.ts') + .end(); + }, /Command failed with exit code 1/); + }); + }); + it('should correct error stack', async function test_stack() { try { await runner() From 635408dbb4fb50812d7f59d7b3d81e3f0f24f39c Mon Sep 17 00:00:00 2001 From: TZ Date: Fri, 30 Dec 2022 18:21:11 +0800 Subject: [PATCH 14/14] f --- Untitled-1.ts | 51 ++++++++++++++++++++++++++++ Untitled-2.ts | 61 ++++++++++++++++++++++++++++++++++ src/runner.ts | 32 +++++++++--------- test/plugins/validator.test.ts | 2 +- test/runner.test.ts | 7 ++++ 5 files changed, 137 insertions(+), 16 deletions(-) create mode 100644 Untitled-1.ts create mode 100644 Untitled-2.ts diff --git a/Untitled-1.ts b/Untitled-1.ts new file mode 100644 index 0000000..1a95c57 --- /dev/null +++ b/Untitled-1.ts @@ -0,0 +1,51 @@ + +class TestRunner { + // TODO: write a gymnastics for this. + plugin(plugins) { + for (const key of Object.keys(plugins)) { + const initFn = plugins[key]; + this[key] = (...args) => { + initFn(this, ...args); + return this; + }; + } + return this; + } + end() { + console.log('done'); + } +} + +// test case +const test_plugins = { + stdout(runner: TestRunner, expected: string) { + console.log('stdout', expected); + }, + code(runner: TestRunner, expected: number) { + console.log('code', expected); + }, +}; + +new TestRunner() + .plugin({ ...test_plugins }) + // should know the type of arg1 + .stdout('a test') + // should invalid, expected is not string + .stdout(/aaaa/) + .code(0) + .end(); + +// invalid case +const invalidPlugin = { + // invalid plugin, the first params should be runner: TestRunner + xx: (str: string) => { + console.log('### xx', typeof str); + }, +}; + +new TestRunner() + .plugin({ ...test_plugins, ...invalidPlugin }) + .stdout('a test') + .xx('a test') + .code(0) + .end(); diff --git a/Untitled-2.ts b/Untitled-2.ts new file mode 100644 index 0000000..4c14a13 --- /dev/null +++ b/Untitled-2.ts @@ -0,0 +1,61 @@ + +type MountPlugin = { + [key in keyof T]: T[key] extends (core: TestRunner, ...args: infer I) => any ? (...args: I) => MountPlugin : undefined; +} & TestRunner; + +interface PluginLike { + [key: string]: (core: any, ...args: any[]) => any; +} + +type RestParam = T extends (first: any, ...args: infer R) => any ? R : any; + +class TestRunner { + // TODO: write a gymnastics for this. + plugin(plugins: T): MountPlugin { + for (const key of Object.keys(plugins)) { + const initFn = plugins[key]; + (this as any)[key] = (...args: RestParam) => { + initFn(this, ...args); + return this; + }; + } + return this as any; + } + end() { + console.log('done'); + } +} + +// test case +const test_plugins = { + stdout(runner: TestRunner, expected: string) { + console.log('stdout', expected); + }, + code(runner: TestRunner, expected: number) { + console.log('code', expected); + }, +}; + +new TestRunner() + .plugin({ ...test_plugins } satisfies PluginLike) + // should know the type of arg1 + .stdout('a test') + // should invalid, expected is not string + .stdout(/aaaa/) + .code(0) + .end(); + +// invalid case +const invalidPlugin = { + // invalid plugin, the first params should be runner: TestRunner + xx: (str: string) => { + console.log('### xx', typeof str); + }, +}; + +new TestRunner() + .plugin({ ...test_plugins, ...invalidPlugin } satisfies PluginLike) + .stdout('a test') + .xx('a test') + .code(0) + .end(); diff --git a/src/runner.ts b/src/runner.ts index 7571bfc..2951a06 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -9,8 +9,8 @@ import { wrapFn, color } from './lib/utils'; export type HookFunction = (ctx: RunnerContext) => void | Promise; export type RestParam = T extends (first: any, ...args: infer R) => any ? R : any; -export type MountPlugin = { - [key in keyof T]: T[key] extends (core: TestRunner, ...args: infer I) => any ? (...args: I) => MountPlugin : undefined; +export type MountPlugin = { + [P in keyof T]: T[P] extends (core: TestRunner, ...args: infer I) => any ? (...args: I) => MountPlugin : undefined; } & TestRunner; // use `satisfies` @@ -40,7 +40,7 @@ export class TestRunner extends EventEmitter { error: [], }; - plugin(plugins: T): MountPlugin { + plugin(plugins: T) { for (const key of Object.keys(plugins)) { const initFn = plugins[key]; @@ -49,7 +49,7 @@ export class TestRunner extends EventEmitter { return this; }; } - return this as any; + return this as MountPlugin; } hook(event: HookEventType, fn: HookFunction) { @@ -57,12 +57,6 @@ export class TestRunner extends EventEmitter { return this; } - async runHook(event: HookEventType, ctx: RunnerContext) { - for (const fn of this.hooks[event]) { - await fn(ctx); - } - } - async end() { try { const ctx: RunnerContext = { @@ -79,7 +73,9 @@ export class TestRunner extends EventEmitter { assert(this.proc, 'cmd is not registered yet'); // before - await this.runHook('before', ctx); + for (const fn of this.hooks['before']) { + await fn(ctx); + } // exec child process, don't await it await this.proc.start(); @@ -93,14 +89,18 @@ export class TestRunner extends EventEmitter { }); // running - await this.runHook('running', ctx); + for (const fn of this.hooks['running']) { + await fn(ctx); + } if (ctx.autoWait) { await this.proc.end(); } // after - await this.runHook('after', ctx); + for (const fn of this.hooks['after']) { + await fn(ctx); + } // ensure proc is exit if user forgot to call `wait('close')` after wait other event if (!ctx.autoWait) { @@ -108,8 +108,10 @@ export class TestRunner extends EventEmitter { } // error - if (ctx.result instanceof Error) { - await this.runHook('error', ctx); + for (const fn of this.hooks['error']) { + if (ctx.result instanceof Error) { + await fn(ctx); + } } // end diff --git a/test/plugins/validator.test.ts b/test/plugins/validator.test.ts index 698411b..dd8100e 100644 --- a/test/plugins/validator.test.ts +++ b/test/plugins/validator.test.ts @@ -38,7 +38,7 @@ describe('test/plugins/validator.test.ts', () => { }, /Expected values to be strictly equal/); }); - describe('error stack', () => { + describe.only('error stack', () => { it('should correct error stack', async function test_stack() { try { await runner() diff --git a/test/runner.test.ts b/test/runner.test.ts index 023859b..f3a218b 100644 --- a/test/runner.test.ts +++ b/test/runner.test.ts @@ -23,6 +23,13 @@ describe('test/runner.test.ts', () => { .end(); }); + it('should color', async () => { + await runner() + .fork('test/fixtures/process/color.ts') + .stdout(/CLIHub/) + .end(); + }); + describe('code()', () => { it('should skip auto check code when .code(1)', async () => { await runner()