From b223c7a1318f56a1eba8bdfd2903f9199a1bbd97 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 21 Jan 2025 09:25:29 +0800 Subject: [PATCH] fix: mv single to @eggjs/core (#5387) /~https://github.com/eggjs/core/pull/288 ## Summary by CodeRabbit Based on the comprehensive summary, here are the release notes: - **New Features** - Added new CI workflows for different testing clusters and library testing - Enhanced continuous integration configuration - **Dependency Updates** - Updated `@eggjs/core` from version 6.2.13 to 6.3.0 - **Testing Improvements** - Added new test scripts for specific test clusters - Expanded error handling and logging test coverage - Restructured test file organization - **Code Refactoring** - Removed singleton implementation from core library - Updated type assertions and import paths - Simplified error handling in test cases - **Chores** - Updated GitHub Actions workflows - Reorganized test directory structure --- .github/workflows/nodejs-cluster1.yml | 18 + .github/workflows/nodejs-cluster2.yml | 18 + .github/workflows/nodejs-lib-core.yml | 18 + .github/workflows/nodejs-lib-plugins.yml | 18 + .github/workflows/nodejs.yml | 5 +- package.json | 9 +- src/index.ts | 8 +- src/lib/core/singleton.ts | 149 ------- src/lib/egg.ts | 26 -- test/agent.test.ts | 77 +++- test/app/extend/agent.test.ts | 7 +- test/app/extend/application.test.ts | 21 +- test/{lib => }/application.test.ts | 6 +- .../cluster => cluster1}/app_worker.test.ts | 4 +- .../cluster-client-error.test.ts | 4 +- .../cluster-client.test.ts | 4 +- test/{lib/cluster => cluster1}/master.test.ts | 104 +---- test/cluster2/master.test.ts | 106 +++++ test/{lib => }/egg.test.ts | 4 +- test/index.test-d.ts | 2 +- test/lib/agent.test.ts | 61 --- test/lib/core/singleton.test.ts | 397 ------------------ test/{lib => }/start.test.ts | 0 test/{ts/index.test.ts => typescript.test.ts} | 4 +- 24 files changed, 289 insertions(+), 781 deletions(-) create mode 100644 .github/workflows/nodejs-cluster1.yml create mode 100644 .github/workflows/nodejs-cluster2.yml create mode 100644 .github/workflows/nodejs-lib-core.yml create mode 100644 .github/workflows/nodejs-lib-plugins.yml delete mode 100644 src/lib/core/singleton.ts rename test/{lib => }/application.test.ts (98%) rename test/{lib/cluster => cluster1}/app_worker.test.ts (97%) rename test/{lib/cluster => cluster1}/cluster-client-error.test.ts (83%) rename test/{lib/cluster => cluster1}/cluster-client.test.ts (97%) rename test/{lib/cluster => cluster1}/master.test.ts (65%) create mode 100644 test/cluster2/master.test.ts rename test/{lib => }/egg.test.ts (99%) delete mode 100644 test/lib/agent.test.ts delete mode 100644 test/lib/core/singleton.test.ts rename test/{lib => }/start.test.ts (100%) rename test/{ts/index.test.ts => typescript.test.ts} (97%) diff --git a/.github/workflows/nodejs-cluster1.yml b/.github/workflows/nodejs-cluster1.yml new file mode 100644 index 0000000000..0a43423522 --- /dev/null +++ b/.github/workflows/nodejs-cluster1.yml @@ -0,0 +1,18 @@ +name: CI cluster1 + +on: + push: + branches: [ master, next ] + pull_request: + branches: [ master, next ] + +jobs: + Job: + name: Node.js + uses: node-modules/github-actions/.github/workflows/node-test.yml@master + with: + os: 'ubuntu-latest, windows-latest' + version: '20' + test: 'npm run ci:cluster1' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/nodejs-cluster2.yml b/.github/workflows/nodejs-cluster2.yml new file mode 100644 index 0000000000..65d6edd734 --- /dev/null +++ b/.github/workflows/nodejs-cluster2.yml @@ -0,0 +1,18 @@ +name: CI cluster2 + +on: + push: + branches: [ master, next ] + pull_request: + branches: [ master, next ] + +jobs: + Job: + name: Node.js + uses: node-modules/github-actions/.github/workflows/node-test.yml@master + with: + os: 'ubuntu-latest, windows-latest' + version: '22' + test: 'npm run ci:cluster2' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/nodejs-lib-core.yml b/.github/workflows/nodejs-lib-core.yml new file mode 100644 index 0000000000..6169943103 --- /dev/null +++ b/.github/workflows/nodejs-lib-core.yml @@ -0,0 +1,18 @@ +name: CI lib/core + +on: + push: + branches: [ master, next ] + pull_request: + branches: [ master, next ] + +jobs: + Job: + name: Node.js + uses: node-modules/github-actions/.github/workflows/node-test.yml@master + with: + os: 'ubuntu-latest, windows-latest' + version: '18, 20' + test: 'npm run ci:lib/core' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/nodejs-lib-plugins.yml b/.github/workflows/nodejs-lib-plugins.yml new file mode 100644 index 0000000000..c2e1fd78a6 --- /dev/null +++ b/.github/workflows/nodejs-lib-plugins.yml @@ -0,0 +1,18 @@ +name: CI lib/plugins + +on: + push: + branches: [ master, next ] + pull_request: + branches: [ master, next ] + +jobs: + Job: + name: Node.js + uses: node-modules/github-actions/.github/workflows/node-test.yml@master + with: + os: 'ubuntu-latest, windows-latest' + version: '18, 20' + test: 'npm run ci:lib/plugins' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index efae51d99f..ac968fafdb 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,4 +1,4 @@ -name: CI +name: CI app on: push: @@ -12,6 +12,7 @@ jobs: uses: node-modules/github-actions/.github/workflows/node-test.yml@master with: os: 'ubuntu-latest, macos-latest, windows-latest' - version: '18.19.0, 18, 20, 22' + version: '18, 20, 22' + test: 'npm run ci:app' secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/package.json b/package.json index aa96686386..1d076860f8 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "@eggjs/cluster": "^3.0.0", "@eggjs/cookies": "^3.0.0", - "@eggjs/core": "^6.2.13", + "@eggjs/core": "^6.3.0", "@eggjs/development": "^4.0.0", "@eggjs/i18n": "^3.0.1", "@eggjs/jsonp": "^3.0.0", @@ -94,10 +94,15 @@ "test": "egg-bin test", "test-local": "egg-bin test", "test:changed": "egg-bin test --changed", - "preci": "npm run clean && npm run lint", + "preci": "npm run clean && npm run lint", "ci": "egg-bin cov", "postci": "npm run prepublishOnly && npm run clean", "prepublishOnly": "tshy && tshy-after && attw --pack --profile node16", + "ci:app": "npm run ci \"test/app/**/*.test.ts,test/*.test.ts\"", + "ci:cluster1": "npm run ci \"test/cluster1/**/*.test.ts\"", + "ci:cluster2": "npm run ci \"test/cluster2/**/*.test.ts\"", + "ci:lib/core": "npm run ci \"test/lib/core/**/*.test.ts\"", + "ci:lib/plugins": "npm run ci \"test/lib/plugins/**/*.test.ts\"", "site:dev": "cross-env APP_ROOT=./site dumi dev", "site:build": "cross-env APP_ROOT=./site dumi build", "site:prettier": "prettier --config site/.prettierrc --ignore-path site/.prettierignore --write \"site/**/*.{js,jsx,tsx,ts,less,md,json}\"", diff --git a/src/index.ts b/src/index.ts index 2b5366c9ab..6976debfb3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,13 @@ export type { export * from './lib/egg.js'; export * from './lib/types.js'; export * from './lib/start.js'; -export * from './lib/core/singleton.js'; + +// export singleton +export { + Singleton, + type SingletonCreateMethod, + type SingletonOptions, +} from '@eggjs/core'; // export errors export * from './lib/error/index.js'; diff --git a/src/lib/core/singleton.ts b/src/lib/core/singleton.ts deleted file mode 100644 index 0db65ac4ed..0000000000 --- a/src/lib/core/singleton.ts +++ /dev/null @@ -1,149 +0,0 @@ -import assert from 'node:assert'; -import { isAsyncFunction } from 'is-type-of'; -import type { EggCore } from '@eggjs/core'; - -export type SingletonCreateMethod = - (config: Record, app: EggCore, clientName: string) => unknown | Promise; - -export interface SingletonOptions { - name: string; - app: EggCore; - create: SingletonCreateMethod; -} - -export class Singleton { - readonly clients = new Map(); - readonly app: EggCore; - readonly create: SingletonCreateMethod; - readonly name: string; - readonly options: Record; - - constructor(options: SingletonOptions) { - assert(options.name, '[egg:singleton] Singleton#constructor options.name is required'); - assert(options.app, '[egg:singleton] Singleton#constructor options.app is required'); - assert(options.create, '[egg:singleton] Singleton#constructor options.create is required'); - assert(!(options.name in options.app), `[egg:singleton] ${options.name} is already exists in app`); - this.app = options.app; - this.name = options.name; - this.create = options.create; - this.options = options.app.config[this.name] ?? {}; - } - - init() { - return isAsyncFunction(this.create) ? this.initAsync() : this.initSync(); - } - - initSync() { - const options = this.options; - assert(!(options.client && options.clients), - `[egg:singleton] ${this.name} can not set options.client and options.clients both`); - - // alias app[name] as client, but still support createInstance method - if (options.client) { - const client = this.createInstance(options.client, options.name); - this.#setClientToApp(client); - this.#extendDynamicMethods(client); - return; - } - - // multi client, use app[name].getSingletonInstance(id) - if (options.clients) { - Object.keys(options.clients).forEach(id => { - const client = this.createInstance(options.clients[id], id); - this.clients.set(id, client); - }); - this.#setClientToApp(this); - return; - } - - // no config.clients and config.client - this.#setClientToApp(this); - } - - async initAsync() { - const options = this.options; - assert(!(options.client && options.clients), - `[egg:singleton] ${this.name} can not set options.client and options.clients both`); - - // alias app[name] as client, but still support createInstance method - if (options.client) { - const client = await this.createInstanceAsync(options.client, options.name); - this.#setClientToApp(client); - this.#extendDynamicMethods(client); - return; - } - - // multi client, use app[name].getInstance(id) - if (options.clients) { - await Promise.all(Object.keys(options.clients).map((id: string) => { - return this.createInstanceAsync(options.clients[id], id) - .then(client => this.clients.set(id, client)); - })); - this.#setClientToApp(this); - return; - } - - // no config.clients and config.client - this.#setClientToApp(this); - } - - #setClientToApp(client: unknown) { - Reflect.set(this.app, this.name, client); - } - - /** - * @deprecated please use `getSingletonInstance(id)` instead - */ - get(id: string) { - return this.clients.get(id)!; - } - - /** - * Get singleton instance by id - */ - getSingletonInstance(id: string) { - return this.clients.get(id)!; - } - - createInstance(config: Record, clientName: string) { - // async creator only support createInstanceAsync - assert(!isAsyncFunction(this.create), - `egg:singleton ${this.name} only support create asynchronous, please use createInstanceAsync`); - // options.default will be merge in to options.clients[id] - config = { - ...this.options.default, - ...config, - }; - return (this.create as SingletonCreateMethod)(config, this.app, clientName) as T; - } - - async createInstanceAsync(config: Record, clientName: string) { - // options.default will be merge in to options.clients[id] - config = { - ...this.options.default, - ...config, - }; - return await this.create(config, this.app, clientName) as T; - } - - #extendDynamicMethods(client: any) { - assert(!client.createInstance, 'singleton instance should not have createInstance method'); - assert(!client.createInstanceAsync, 'singleton instance should not have createInstanceAsync method'); - - try { - let extendable = client; - // Object.preventExtensions() or Object.freeze() - if (!Object.isExtensible(client) || Object.isFrozen(client)) { - // eslint-disable-next-line no-proto - extendable = client.__proto__ || client; - } - extendable.createInstance = this.createInstance.bind(this); - extendable.createInstanceAsync = this.createInstanceAsync.bind(this); - } catch (err) { - this.app.coreLogger.warn( - '[egg:singleton] %s dynamic create is disabled because of client is un-extendable', - this.name); - this.app.coreLogger.warn(err); - } - } -} diff --git a/src/lib/egg.ts b/src/lib/egg.ts index 585535d238..5583f81002 100644 --- a/src/lib/egg.ts +++ b/src/lib/egg.ts @@ -36,9 +36,6 @@ import { type HttpClientOptions, } from './core/httpclient.js'; import { createLoggers } from './core/logger.js'; -import { - Singleton, type SingletonCreateMethod, type SingletonOptions, -} from './core/singleton.js'; import { convertObject } from './core/utils.js'; import { BaseContextClass } from './core/base_context_class.js'; import { BaseHookClass } from './core/base_hook_class.js'; @@ -595,26 +592,6 @@ export class EggApplicationCore extends EggCore { /* eslint no-empty-function: off */ set proxy(_) {} - /** - * create a singleton instance - * @param {String} name - unique name for singleton - * @param {Function|AsyncFunction} create - method will be invoked when singleton instance create - */ - addSingleton(name: string, create: SingletonCreateMethod) { - const options: SingletonOptions = { - name, - create, - app: this, - }; - const singleton = new Singleton(options); - const initPromise = singleton.init(); - if (initPromise) { - this.beforeStart(async () => { - await initPromise; - }); - } - } - #patchClusterClient(client: any) { const rawCreate = client.create; client.create = (...args: any) => { @@ -695,13 +672,10 @@ declare module '@eggjs/core' { inspect(): any; get currentContext(): EggContext | undefined; ctxStorage: AsyncLocalStorage; - get logger(): EggLogger; - get coreLogger(): EggLogger; getLogger(name: string): EggLogger; createHttpClient(options?: HttpClientOptions): HttpClient; HttpClient: typeof HttpClient; get httpClient(): HttpClient; curl(url: HttpClientRequestURL, options?: HttpClientRequestOptions): Promise>; - addSingleton(name: string, create: SingletonCreateMethod): void; } } diff --git a/test/agent.test.ts b/test/agent.test.ts index 8eb8225a0b..30ecdf5aa2 100644 --- a/test/agent.test.ts +++ b/test/agent.test.ts @@ -1,19 +1,76 @@ import { strict as assert } from 'node:assert'; +import fs from 'node:fs'; import path from 'node:path'; -import { createApp, MockApplication, restore } from './utils.js'; +import { mm } from '@eggjs/mock'; +import { createApp, getFilepath, MockApplication, cluster } from './utils.js'; describe('test/agent.test.ts', () => { - afterEach(restore); - let app: MockApplication; + afterEach(mm.restore); - before(() => { - app = createApp('apps/agent-logger-config'); - return app.ready(); + describe('agent-logger-config', () => { + let app: MockApplication; + + before(() => { + app = createApp('apps/agent-logger-config'); + return app.ready(); + }); + after(() => app.close()); + + it('agent logger config should work', () => { + const fileTransport = app._agent.logger.get('file'); + assert.equal(fileTransport.options.file, path.join('/tmp/foo', 'egg-agent.log')); + }); }); - after(() => app.close()); - it('agent logger config should work', () => { - const fileTransport = app._agent.logger.get('file'); - assert.equal(fileTransport.options.file, path.join('/tmp/foo', 'egg-agent.log')); + describe('agent throw', () => { + const baseDir = getFilepath('apps/agent-throw'); + let app: MockApplication; + before(() => { + app = cluster('apps/agent-throw'); + return app.ready(); + }); + after(() => app.close()); + + it('should catch unhandled exception', done => { + app.httpRequest() + .get('/agent-throw-async') + .expect(200, err => { + assert(!err); + setTimeout(() => { + const body = fs.readFileSync(path.join(baseDir, 'logs/agent-throw/common-error.log'), 'utf8'); + assert.match(body, /nodejs\.MessageUnhandledRejectionError: event: agent-throw-async, error: agent error in async function/); + app.notExpect('stderr', /nodejs.AgentWorkerDiedError/); + done(); + }, 1000); + }); + }); + + it('should exit on sync error throw', done => { + app.httpRequest() + .get('/agent-throw') + .expect(200, err => { + assert(!err); + setTimeout(() => { + const body = fs.readFileSync(path.join(baseDir, 'logs/agent-throw/common-error.log'), 'utf8'); + assert.match(body, /nodejs\.MessageUnhandledRejectionError: event: agent-throw, error: agent error in sync function/); + app.notExpect('stderr', /nodejs.AgentWorkerDiedError/); + done(); + }, 1000); + }); + }); + + it('should catch uncaughtException string error', done => { + app.httpRequest() + .get('/agent-throw-string') + .expect(200, err => { + assert(!err); + setTimeout(() => { + const body = fs.readFileSync(path.join(baseDir, 'logs/agent-throw/common-error.log'), 'utf8'); + assert.match(body, /nodejs\.MessageUnhandledRejectionError: event: agent-throw-string, error: agent error string/); + app.notExpect('stderr', /nodejs.AgentWorkerDiedError/); + done(); + }, 1000); + }); + }); }); }); diff --git a/test/app/extend/agent.test.ts b/test/app/extend/agent.test.ts index e8868bc337..490fdae924 100644 --- a/test/app/extend/agent.test.ts +++ b/test/app/extend/agent.test.ts @@ -29,12 +29,9 @@ describe('test/app/extend/agent.test.ts', () => { assert(config.foo === 'bar'); assert(config.foo2 === 'bar2'); - try { + assert.throws(() => { app.agent.dataServiceAsync.createInstance({ foo: 'bar2' }); - throw new Error('should not execute'); - } catch (err: any) { - assert(err.message === 'egg:singleton dataServiceAsync only support create asynchronous, please use createInstanceAsync'); - } + }, /dataServiceAsync only support synchronous creation, please use createInstanceAsync/); const ds4 = await app.agent.dataServiceAsync.createInstanceAsync({ foo: 'bar2' }); config = await ds4.getConfig(); diff --git a/test/app/extend/application.test.ts b/test/app/extend/application.test.ts index 4bdae4a6be..96f7ac28de 100644 --- a/test/app/extend/application.test.ts +++ b/test/app/extend/application.test.ts @@ -139,31 +139,28 @@ describe('test/app/extend/application.test.ts', () => { it('should add singleton success', async () => { let config = await app.dataService.get('first').getConfig(); - assert(config.foo === 'bar'); - assert(config.foo1 === 'bar1'); + assert.equal(config.foo, 'bar'); + assert.equal(config.foo1, 'bar1'); const ds = app.dataService.createInstance({ foo: 'barrr' }); config = await ds.getConfig(); - assert(config.foo === 'barrr'); + assert.equal(config.foo, 'barrr'); const ds2 = await app.dataService.createInstanceAsync({ foo: 'barrr' }); config = await ds2.getConfig(); - assert(config.foo === 'barrr'); + assert.equal(config.foo, 'barrr'); config = await app.dataServiceAsync.get('first').getConfig(); - assert(config.foo === 'bar'); - assert(config.foo1 === 'bar1'); + assert.equal(config.foo, 'bar'); + assert.equal(config.foo1, 'bar1'); - try { + assert.throws(() => { app.dataServiceAsync.createInstance({ foo: 'barrr' }); - throw new Error('should not execute'); - } catch (err: any) { - assert(err.message === 'egg:singleton dataServiceAsync only support create asynchronous, please use createInstanceAsync'); - } + }, /dataServiceAsync only support synchronous creation, please use createInstanceAsync/); const ds4 = await app.dataServiceAsync.createInstanceAsync({ foo: 'barrr' }); config = await ds4.getConfig(); - assert(config.foo === 'barrr'); + assert.equal(config.foo, 'barrr'); }); }); diff --git a/test/lib/application.test.ts b/test/application.test.ts similarity index 98% rename from test/lib/application.test.ts rename to test/application.test.ts index f896e7f380..8589f64a93 100644 --- a/test/lib/application.test.ts +++ b/test/application.test.ts @@ -4,10 +4,10 @@ import fs from 'node:fs'; import path from 'node:path'; import { scheduler } from 'node:timers/promises'; import { pending } from 'pedding'; -import { Application, CookieLimitExceedError } from '../../src/index.js'; -import { MockApplication, cluster, createApp, getFilepath, startLocalServer } from '../utils.js'; +import { Application, CookieLimitExceedError } from '../src/index.js'; +import { MockApplication, cluster, createApp, getFilepath, startLocalServer } from './utils.js'; -describe('test/lib/application.test.ts', () => { +describe('test/application.test.ts', () => { let app: MockApplication; afterEach(mm.restore); diff --git a/test/lib/cluster/app_worker.test.ts b/test/cluster1/app_worker.test.ts similarity index 97% rename from test/lib/cluster/app_worker.test.ts rename to test/cluster1/app_worker.test.ts index 2ed12bb857..b52a9c29bc 100644 --- a/test/lib/cluster/app_worker.test.ts +++ b/test/cluster1/app_worker.test.ts @@ -3,7 +3,7 @@ import { strict as assert } from 'node:assert'; import { scheduler } from 'node:timers/promises'; import { request } from '@eggjs/supertest'; import { ip } from 'address'; -import { cluster, MockApplication } from '../../utils.js'; +import { cluster, MockApplication } from '../utils.js'; const DEFAULT_BAD_REQUEST_HTML = ` 400 Bad Request @@ -13,7 +13,7 @@ const DEFAULT_BAD_REQUEST_HTML = ` `; -describe('test/lib/cluster/app_worker.test.ts', () => { +describe('test/cluster1/app_worker.test.ts', () => { let app: MockApplication; before(() => { app = cluster('apps/app-server'); diff --git a/test/lib/cluster/cluster-client-error.test.ts b/test/cluster1/cluster-client-error.test.ts similarity index 83% rename from test/lib/cluster/cluster-client-error.test.ts rename to test/cluster1/cluster-client-error.test.ts index 0ad2f5f2b9..7ea36a0509 100644 --- a/test/lib/cluster/cluster-client-error.test.ts +++ b/test/cluster1/cluster-client-error.test.ts @@ -1,9 +1,9 @@ import { readFile } from 'node:fs/promises'; import { strict as assert } from 'node:assert'; import { scheduler } from 'node:timers/promises'; -import { MockApplication, createApp, getFilepath } from '../../utils.js'; +import { MockApplication, createApp, getFilepath } from '../utils.js'; -describe('test/lib/cluster/cluster-client-error.test.ts', () => { +describe('test/cluster1/cluster-client-error.test.ts', () => { let app: MockApplication; before(async () => { app = createApp('apps/cluster-client-error'); diff --git a/test/lib/cluster/cluster-client.test.ts b/test/cluster1/cluster-client.test.ts similarity index 97% rename from test/lib/cluster/cluster-client.test.ts rename to test/cluster1/cluster-client.test.ts index eb9f81d284..bdea52dfb7 100644 --- a/test/lib/cluster/cluster-client.test.ts +++ b/test/cluster1/cluster-client.test.ts @@ -1,10 +1,10 @@ import { strict as assert } from 'node:assert'; import { mm } from '@eggjs/mock'; -import { MockApplication, createApp, singleProcessApp } from '../../utils.js'; +import { MockApplication, createApp, singleProcessApp } from '../utils.js'; const innerClient = Symbol.for('ClusterClient#innerClient'); -describe('test/lib/cluster/cluster-client.test.ts', () => { +describe('test/cluster1/cluster-client.test.ts', () => { let app: MockApplication; describe('common mode', () => { before(async () => { diff --git a/test/lib/cluster/master.test.ts b/test/cluster1/master.test.ts similarity index 65% rename from test/lib/cluster/master.test.ts rename to test/cluster1/master.test.ts index fa9b142330..edb452af0e 100644 --- a/test/lib/cluster/master.test.ts +++ b/test/cluster1/master.test.ts @@ -1,110 +1,10 @@ -import { scheduler } from 'node:timers/promises'; import { mm } from '@eggjs/mock'; import coffee, { Coffee } from 'coffee'; -import { MockApplication, cluster, getFilepath } from '../../utils.js'; +import { MockApplication, cluster, getFilepath } from '../utils.js'; -describe('test/lib/cluster/master.test.ts', () => { +describe('test/cluster1/master.test.ts', () => { afterEach(mm.restore); - describe('app worker die', () => { - let app: MockApplication; - before(() => { - mm.env('default'); - app = cluster('apps/app-die'); - app.coverage(false); - return app.ready(); - }); - after(() => app.close()); - - it('should restart after app worker exit', async () => { - try { - await app.httpRequest() - .get('/exit'); - } catch (_) { - // do nothing - } - - // wait for app worker restart - await scheduler.wait(20000); - - // error pipe to console - app.expect('stdout', /app_worker#1:\d+ disconnect/); - app.expect('stderr', /nodejs\.AppWorkerDiedError: \[master]/); - app.expect('stderr', /app_worker#1:\d+ died/); - app.expect('stdout', /app_worker#2:\d+ started/); - }); - - it('should restart when app worker throw uncaughtException', async () => { - try { - await app.httpRequest() - .get('/uncaughtException'); - } catch (_) { - // do nothing - } - - // wait for app worker restart - await scheduler.wait(20000); - - app.expect('stderr', /\[graceful:worker:\d+:uncaughtException] throw error 1 times/); - app.expect('stdout', /app_worker#\d:\d+ started/); - }); - }); - - describe('app worker should not die with matched serverGracefulIgnoreCode', () => { - let app: MockApplication; - before(() => { - mm.env('default'); - app = cluster('apps/app-die-ignore-code'); - app.coverage(false); - return app.ready(); - }); - after(() => app.close()); - - it('should not restart when matched uncaughtException happened', async () => { - try { - await app.httpRequest() - .get('/uncaughtException'); - } catch (_) { - // do nothing - } - - // wait for app worker restart - await scheduler.wait(5000); - - // error pipe to console - app.notExpect('stdout', /app_worker#1:\d+ disconnect/); - }); - - it('should still log uncaughtException when matched uncaughtException happened', async () => { - try { - await app.httpRequest() - .get('/uncaughtException'); - } catch (_) { - // do nothing - } - - // wait for app worker restart - await scheduler.wait(5000); - - app.expect('stderr', /\[graceful:worker:\d+:uncaughtException] throw error 1 times/); - app.expect('stderr', /matches ignore list/); - app.notExpect('stdout', /app_worker#1:\d+ disconnect/); - }); - }); - - describe('Master start fail', () => { - let master: MockApplication; - - after(() => master.close()); - - it('should master exit with 1', done => { - mm.consoleLevel('NONE'); - master = cluster('apps/worker-die'); - master.coverage(false); - master.expect('code', 1).ready(done); - }); - }); - describe('Master started log', () => { let app: MockApplication; diff --git a/test/cluster2/master.test.ts b/test/cluster2/master.test.ts new file mode 100644 index 0000000000..9d09f0079f --- /dev/null +++ b/test/cluster2/master.test.ts @@ -0,0 +1,106 @@ +import { scheduler } from 'node:timers/promises'; +import { mm } from '@eggjs/mock'; +import { MockApplication, cluster } from '../utils.js'; + +describe('test/cluster2/master.test.ts', () => { + afterEach(mm.restore); + + describe('app worker die', () => { + let app: MockApplication; + before(() => { + mm.env('default'); + app = cluster('apps/app-die'); + app.coverage(false); + return app.ready(); + }); + after(() => app.close()); + + it('should restart after app worker exit', async () => { + try { + await app.httpRequest() + .get('/exit'); + } catch (_) { + // do nothing + } + + // wait for app worker restart + await scheduler.wait(20000); + + // error pipe to console + app.expect('stdout', /app_worker#1:\d+ disconnect/); + app.expect('stderr', /nodejs\.AppWorkerDiedError: \[master]/); + app.expect('stderr', /app_worker#1:\d+ died/); + app.expect('stdout', /app_worker#2:\d+ started/); + }); + + it('should restart when app worker throw uncaughtException', async () => { + try { + await app.httpRequest() + .get('/uncaughtException'); + } catch (_) { + // do nothing + } + + // wait for app worker restart + await scheduler.wait(20000); + + app.expect('stderr', /\[graceful:worker:\d+:uncaughtException] throw error 1 times/); + app.expect('stdout', /app_worker#\d:\d+ started/); + }); + }); + + describe('app worker should not die with matched serverGracefulIgnoreCode', () => { + let app: MockApplication; + before(() => { + mm.env('default'); + app = cluster('apps/app-die-ignore-code'); + app.coverage(false); + return app.ready(); + }); + after(() => app.close()); + + it('should not restart when matched uncaughtException happened', async () => { + try { + await app.httpRequest() + .get('/uncaughtException'); + } catch (_) { + // do nothing + } + + // wait for app worker restart + await scheduler.wait(5000); + + // error pipe to console + app.notExpect('stdout', /app_worker#1:\d+ disconnect/); + }); + + it('should still log uncaughtException when matched uncaughtException happened', async () => { + try { + await app.httpRequest() + .get('/uncaughtException'); + } catch (_) { + // do nothing + } + + // wait for app worker restart + await scheduler.wait(5000); + + app.expect('stderr', /\[graceful:worker:\d+:uncaughtException] throw error 1 times/); + app.expect('stderr', /matches ignore list/); + app.notExpect('stdout', /app_worker#1:\d+ disconnect/); + }); + }); + + describe('Master start fail', () => { + let master: MockApplication; + + after(() => master.close()); + + it('should master exit with 1', done => { + mm.consoleLevel('NONE'); + master = cluster('apps/worker-die'); + master.coverage(false); + master.expect('code', 1).ready(done); + }); + }); +}); diff --git a/test/lib/egg.test.ts b/test/egg.test.ts similarity index 99% rename from test/lib/egg.test.ts rename to test/egg.test.ts index cb7d3bee3a..5ed078bf6b 100644 --- a/test/lib/egg.test.ts +++ b/test/egg.test.ts @@ -4,11 +4,11 @@ import fs from 'node:fs'; import { scheduler } from 'node:timers/promises'; import { mm } from '@eggjs/mock'; import { Transport } from 'egg-logger'; -import { createApp, cluster, getFilepath, MockApplication } from '../utils.js'; +import { createApp, cluster, getFilepath, MockApplication } from './utils.js'; import assertFile from 'assert-file'; import { readJSONSync } from 'utility'; -describe('test/lib/egg.test.ts', () => { +describe('test/egg.test.ts', () => { afterEach(mm.restore); describe('dumpConfig()', () => { diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 6c3e5cdcb7..d3824520aa 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -168,4 +168,4 @@ expectType(redis); expectType(redis.get('foo')); expectType(redis.getSingletonInstance('client1').get('foo')); expectType(redis.getSingletonInstance('client1')); -// expectType(redis.get('client1')); +expectType(redis.get('client1') as unknown as Redis); diff --git a/test/lib/agent.test.ts b/test/lib/agent.test.ts deleted file mode 100644 index ecfd611f49..0000000000 --- a/test/lib/agent.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { strict as assert } from 'node:assert'; -import fs from 'node:fs'; -import path from 'node:path'; -import { mm } from '@eggjs/mock'; -import { getFilepath, MockApplication, cluster } from '../utils.js'; - -describe('test/lib/agent.test.ts', () => { - afterEach(mm.restore); - - describe('agent throw', () => { - const baseDir = getFilepath('apps/agent-throw'); - let app: MockApplication; - before(() => { - app = cluster('apps/agent-throw'); - return app.ready(); - }); - after(() => app.close()); - - it('should catch unhandled exception', done => { - app.httpRequest() - .get('/agent-throw-async') - .expect(200, err => { - assert(!err); - setTimeout(() => { - const body = fs.readFileSync(path.join(baseDir, 'logs/agent-throw/common-error.log'), 'utf8'); - assert.match(body, /nodejs\.MessageUnhandledRejectionError: event: agent-throw-async, error: agent error in async function/); - app.notExpect('stderr', /nodejs.AgentWorkerDiedError/); - done(); - }, 1000); - }); - }); - - it('should exit on sync error throw', done => { - app.httpRequest() - .get('/agent-throw') - .expect(200, err => { - assert(!err); - setTimeout(() => { - const body = fs.readFileSync(path.join(baseDir, 'logs/agent-throw/common-error.log'), 'utf8'); - assert.match(body, /nodejs\.MessageUnhandledRejectionError: event: agent-throw, error: agent error in sync function/); - app.notExpect('stderr', /nodejs.AgentWorkerDiedError/); - done(); - }, 1000); - }); - }); - - it('should catch uncaughtException string error', done => { - app.httpRequest() - .get('/agent-throw-string') - .expect(200, err => { - assert(!err); - setTimeout(() => { - const body = fs.readFileSync(path.join(baseDir, 'logs/agent-throw/common-error.log'), 'utf8'); - assert.match(body, /nodejs\.MessageUnhandledRejectionError: event: agent-throw-string, error: agent error string/); - app.notExpect('stderr', /nodejs.AgentWorkerDiedError/); - done(); - }, 1000); - }); - }); - }); -}); diff --git a/test/lib/core/singleton.test.ts b/test/lib/core/singleton.test.ts deleted file mode 100644 index 09c95f5751..0000000000 --- a/test/lib/core/singleton.test.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { strict as assert } from 'node:assert'; -import { scheduler } from 'node:timers/promises'; -import { Singleton } from '../../../src/lib/core/singleton.js'; - -class DataService { - config: any; - constructor(config: any) { - this.config = config; - } - - async query() { - return {}; - } -} - -function create(config: any) { - return new DataService(config); -} - -async function asyncCreate(config: any) { - await scheduler.wait(10); - return new DataService(config); -} - -describe('test/lib/core/singleton.test.ts', () => { - afterEach(() => { - delete (DataService as any).prototype.createInstance; - delete (DataService as any).prototype.createInstanceAsync; - }); - - describe('sync singleton creation tests', () => { - it('should init with client', async () => { - const name = 'dataService'; - - const clients = [ - { foo: 'bar' }, - ]; - for (const client of clients) { - const app: any = { config: { dataService: { client } } }; - const singleton = new Singleton({ - name, - app, - create, - }); - singleton.init(); - assert(app.dataService instanceof DataService); - assert.equal(app.dataService.config.foo, 'bar'); - assert.equal(typeof app.dataService.createInstance, 'function'); - } - }); - - it('should init with clients', async () => { - const name = 'dataService'; - - const clients = { - first: { foo: 'bar1' }, - second: { foo: 'bar2' }, - }; - - const app: any = { config: { dataService: { clients } } }; - const singleton = new Singleton({ - name, - app, - create, - }); - singleton.init(); - assert(app.dataService instanceof Singleton); - assert.equal(app.dataService.get('first').config.foo, 'bar1'); - assert.equal(app.dataService.get('second').config.foo, 'bar2'); - assert.equal(typeof app.dataService.createInstance, 'function'); - }); - - it('should client support default', async () => { - const app: any = { - config: { - dataService: { - client: { foo: 'bar' }, - default: { foo1: 'bar1' }, - }, - }, - }; - const name = 'dataService'; - - const singleton = new Singleton({ - name, - app, - create, - }); - singleton.init(); - assert(app.dataService instanceof DataService); - assert.equal(app.dataService.config.foo, 'bar'); - assert.equal(app.dataService.config.foo1, 'bar1'); - assert.equal(typeof app.dataService.createInstance, 'function'); - }); - - it('should clients support default', async () => { - const app: any = { - config: { - dataService: { - clients: { - first: { foo: 'bar1' }, - second: { }, - }, - default: { foo: 'bar' }, - }, - }, - }; - const name = 'dataService'; - - const singleton = new Singleton({ - name, - app, - create, - }); - singleton.init(); - assert(app.dataService instanceof Singleton); - assert(app.dataService.get('first').config.foo === 'bar1'); - assert(app.dataService.getSingletonInstance('first').config.foo === 'bar1'); - assert(app.dataService.get('first'), app.dataService.getSingletonInstance('first')); - assert(app.dataService.get('second').config.foo === 'bar'); - assert(app.dataService.getSingletonInstance('second').config.foo === 'bar'); - assert(app.dataService.get('second'), app.dataService.getSingletonInstance('second')); - assert(typeof app.dataService.createInstance === 'function'); - }); - - it('should createInstance without client/clients support default', async () => { - const app: any = { - config: { - dataService: { - default: { foo: 'bar' }, - }, - }, - }; - const name = 'dataService'; - - const singleton = new Singleton({ - name, - app, - create, - }); - singleton.init(); - assert(app.dataService === singleton); - assert(app.dataService instanceof Singleton); - app.dataService = app.dataService.createInstance({ foo1: 'bar1' }); - assert(app.dataService instanceof DataService); - assert(app.dataService.config.foo1 === 'bar1'); - assert(app.dataService.config.foo === 'bar'); - }); - - it('should work with unextensible', async () => { - function create(config: any) { - const d = new DataService(config); - Object.preventExtensions(d); - return d; - } - const app: any = { - config: { - dataService: { - client: { foo: 'bar' }, - default: { foo: 'bar' }, - }, - }, - }; - const name = 'dataService'; - - const singleton = new Singleton({ - name, - app, - create, - }); - singleton.init(); - const dataService = await app.dataService.createInstanceAsync({ foo1: 'bar1' }); - assert(dataService instanceof DataService); - assert(dataService.config.foo1 === 'bar1'); - assert(dataService.config.foo === 'bar'); - }); - - it('should work with frozen', async () => { - function create(config: any) { - const d = new DataService(config); - Object.freeze(d); - return d; - } - const app: any = { - config: { - dataService: { - client: { foo: 'bar' }, - default: { foo: 'bar' }, - }, - }, - }; - const name = 'dataService'; - - const singleton = new Singleton({ - name, - app, - create, - }); - singleton.init(); - - const dataService = await app.dataService.createInstanceAsync({ foo1: 'bar1' }); - assert(dataService instanceof DataService); - assert(dataService.config.foo1 === 'bar1'); - assert(dataService.config.foo === 'bar'); - }); - - it('should work with no prototype and frozen', async () => { - let warn = false; - function create() { - const d = Object.create(null); - Object.freeze(d); - return d; - } - const app: any = { - config: { - dataService: { - client: { foo: 'bar' }, - default: { foo: 'bar' }, - }, - }, - coreLogger: { - warn(_msg: string, name?: string) { - if (name) { - assert.equal(name, 'dataService'); - warn = true; - } - }, - }, - }; - const name = 'dataService'; - - const singleton = new Singleton({ - name, - app, - create, - }); - singleton.init(); - - assert(!app.dataService.createInstance); - assert(!app.dataService.createInstanceAsync); - assert(warn); - }); - - it('should return client name when create', async () => { - let success = true; - const name = 'dataService'; - const clientName = 'customClient'; - function create(_config: any, _app: any, client: string) { - if (client !== clientName) { - success = false; - } - } - const app: any = { - config: { - dataService: { - clients: { - customClient: { foo: 'bar1' }, - }, - }, - }, - }; - const singleton = new Singleton({ - name, - app, - create, - }); - singleton.init(); - - assert(success); - }); - }); - - describe('async singleton creation tests', () => { - it('should init with client', async () => { - const name = 'dataService'; - - const clients = [ - { foo: 'bar' }, - ]; - for (const client of clients) { - const app: any = { config: { dataService: { client } } }; - const singleton = new Singleton({ - name, - app, - create: asyncCreate, - }); - await singleton.init(); - assert(app.dataService instanceof DataService); - assert(app.dataService.config.foo === 'bar'); - assert(typeof app.dataService.createInstance === 'function'); - } - }); - - - it('should init with clients', async () => { - const name = 'dataService'; - - const clients = { - first: { foo: 'bar1' }, - second: { foo: 'bar2' }, - }; - - const app: any = { config: { dataService: { clients } } }; - const singleton = new Singleton({ - name, - app, - create: asyncCreate, - }); - await singleton.init(); - assert(app.dataService instanceof Singleton); - assert(app.dataService.get('first').config.foo === 'bar1'); - assert(app.dataService.get('second').config.foo === 'bar2'); - assert(typeof app.dataService.createInstance === 'function'); - }); - - it('should createInstanceAsync without client/clients support default', async () => { - const app: any = { - config: { - dataService: { - default: { foo: 'bar' }, - }, - }, - }; - const name = 'dataService'; - - const singleton = new Singleton({ - name, - app, - create: asyncCreate, - }); - await singleton.init(); - assert(app.dataService === singleton); - assert(app.dataService instanceof Singleton); - app.dataService = await app.dataService.createInstanceAsync({ foo1: 'bar1' }); - assert(app.dataService instanceof DataService); - assert(app.dataService.config.foo1 === 'bar1'); - assert(app.dataService.config.foo === 'bar'); - }); - - it('should createInstanceAsync throw error', async () => { - const app: any = { - config: { - dataService: { - default: { foo: 'bar' }, - }, - }, - }; - const name = 'dataService'; - - const singleton = new Singleton({ - name, - app, - create: asyncCreate, - }); - await singleton.init(); - assert(app.dataService === singleton); - assert(app.dataService instanceof Singleton); - try { - app.dataService = await app.dataService.createInstance({ foo1: 'bar1' }); - throw new Error('should not execute'); - } catch (err: any) { - assert.equal(err.message, - 'egg:singleton dataService only support create asynchronous, please use createInstanceAsync'); - } - }); - - it('should return client name when create', async () => { - let success = true; - const name = 'dataService'; - const clientName = 'customClient'; - - async function _create(_config: any, _app: any, client: string) { - if (client !== clientName) { - success = false; - } - } - const app: any = { - config: { - dataService: { - clients: { - customClient: { foo: 'bar1' }, - }, - }, - }, - }; - const singleton = new Singleton({ - name, - app, - create: _create, - }); - - await singleton.init(); - - assert(success); - }); - }); -}); diff --git a/test/lib/start.test.ts b/test/start.test.ts similarity index 100% rename from test/lib/start.test.ts rename to test/start.test.ts diff --git a/test/ts/index.test.ts b/test/typescript.test.ts similarity index 97% rename from test/ts/index.test.ts rename to test/typescript.test.ts index b125ee41a6..367496e915 100644 --- a/test/ts/index.test.ts +++ b/test/typescript.test.ts @@ -1,9 +1,9 @@ import { strict as assert } from 'node:assert'; // import coffee from 'coffee'; // import { importResolve } from '@eggjs/utils'; -import { MockApplication, createApp } from '../utils.js'; +import { MockApplication, createApp } from './utils.js'; -describe('test/ts/index.test.ts', () => { +describe('test/typescript.test.ts', () => { describe('compiler code', () => { let app: MockApplication; before(async () => {