Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions plugins/mock/src/lib/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,15 @@ class MockApplicationWorker extends Base {
this[APP_INIT] = true;
debug('this[APP_INIT] = true');
this.#bindEvent();
// Koa normally installs this in callback(), but startup can emit errors before then.
if (!app.listenerCount('error')) {
app.on('error', (app as any).onerror.bind(app));
}
// Wait for middleware loading before app.callback() composes the request handler.
await app.ready();
debug('http server instantiate');
createServer(app);
await app.ready();
// emit `server` after ready: egg core registers its `once('server', ...)`
// listener inside `Application.load()` (during `app.ready()` above), and
// `onServer` reads loaded config, so the event must be emitted now rather
// than at createServer() time. Mirrors @eggjs/cluster's post-ready emit.
// Application.load() registers the onServer listener during app.ready().
if (app.server) {
app.emit('server', app.server);
}
Expand Down
8 changes: 2 additions & 6 deletions plugins/mock/src/lib/mock_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ export function createServer(app: any): Server {
app.server = server;
}
}
// NOTE: don't emit the `server` event here. egg core registers its
// `once('server', ...)` listener inside `Application.load()` (during
// `app.ready()`), so emitting at server-creation time (before ready) would be
// missed and `onServer` (clientError logging / graceful / timeout / websocket)
// never runs. The caller emits `server` after `app.ready()` instead — just
// like @eggjs/cluster, which emits it after the app is ready.
// Mock startup emits `server` once after app.ready(). HTTP requests also use
// this helper to get the cached server and must not emit the event again.
return server;
}
10 changes: 7 additions & 3 deletions plugins/mock/src/lib/parallel/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,15 @@ export class MockParallelApplication extends Base {
this.__APP_INIT__ = true;
debug('this[APP_INIT] = true');
this.#bindEvents();
// Koa normally installs this in callback(), but startup can emit errors before then.
if (!app.listenerCount('error')) {
app.on('error', (app as any).onerror.bind(app));
}
// Wait for middleware loading before app.callback() composes the request handler.
await app.ready();
debug('http server instantiate');
createServer(app);
await app.ready();
// emit `server` after ready so egg core's onServer listener (registered in
// Application.load()) is wired up; createServer no longer emits it.
// Application.load() registers the onServer listener during app.ready().
if (app.server) {
app.emit('server', app.server);
}
Expand Down
53 changes: 46 additions & 7 deletions plugins/mock/test/app.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { strict as assert } from 'node:assert';

import { request } from '@eggjs/supertest';
import { describe, it, beforeAll, afterAll, afterEach } from 'vitest';

import mm, { type MockApplication } from '../src/index.ts';
Expand Down Expand Up @@ -81,12 +82,8 @@ describe('test/app.test.ts', { concurrent: false }, () => {
});

it('should run onServer after ready (server event not lost)', async () => {
// @eggjs/mock emits the `server` event before `app.ready()`, while egg core
// registers its `once("server", ...)` listener inside Application.load()
// (during app.ready()). egg core re-emits `server` after the listener is
// registered so onServer still runs and wires up the `clientError` handler
// (plus graceful shutdown / server timeout / websocket). Regression guard:
// assert the clientError listener is wired after ready.
// Application.load() registers the server listener during app.ready().
// Mock startup must emit server afterward so onServer attaches clientError.
const baseDir = getFixtures('server');
const app = mm.app({
baseDir,
Expand All @@ -104,7 +101,23 @@ describe('test/app.test.ts', { concurrent: false }, () => {
}
});

it('should emit server after ready in parallel app', async () => {
it('should compose HTTP middleware after app is ready', async () => {
const app = mm.app({
baseDir: getFixtures('app'),
framework: getFixtures('snapshot-framework'),
cache: false,
});
try {
await app.ready();
assert(app.server, 'app.server not exists');
await request(app.server).get('/').expect(200, 'foo');
await app.httpRequest().get('/').expect(200, 'foo');
} finally {
await app.close();
}
});

it('should create and emit server after ready in parallel app', async () => {
// cover the parallel app worker's post-ready `server` emit (lib/parallel/app.ts)
const baseDir = getFixtures('server');
let emittedServer: unknown;
Expand All @@ -127,6 +140,7 @@ describe('test/app.test.ts', { concurrent: false }, () => {
assert(app.server, 'app.server not exists');
assert.equal(emittedServer, app.server);
assert.equal(emittedAfterReady, true);
await request(app.server).get('/').expect(200, 'ok');
} finally {
await app.close();
}
Expand Down Expand Up @@ -159,6 +173,31 @@ describe('test/app.test.ts', { concurrent: false }, () => {
await app.close();
});

it.each([false, true])('should handle parallel startup errors (custom listener: %s)', async (customListener) => {
const error = new Error('parallel startup failed');
const errors: Error[] = [];
const app = createParallelApp({
baseDir: getFixtures('server'),
framework: getFixtures('parallel-framework'),
cache: false,
clean: false,
beforeInit: async (parallelApp) => {
parallelApp.options.clusterPort = 1;
parallelApp.options.readyError = error;
},
});
if (customListener) {
app.on('error', (err: Error) => errors.push(err));
}
try {
await assert.rejects(app.ready(), (err) => err === error);
assert.deepEqual((app as any).errors, customListener ? [] : [error]);
assert.deepEqual(errors, customListener ? [error] : []);
} finally {
await app.close();
}
});

// Node.js v20: SyntaxError: Unexpected identifier 'SingleModeApplication'
it.skipIf(process.version.startsWith('v20.'))('should FrameworkErrorformater work during app boot', async () => {
// let logMsg = '';
Expand Down
15 changes: 14 additions & 1 deletion plugins/mock/test/fixtures/parallel-framework/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,33 @@ class Application extends EventEmitter {
this.options = options;
this.context = {};
this.config = {};
this.errors = [];
this.messenger = {
messages: [],
onMessage: (msg) => this.messenger.messages.push(msg),
};
}

callback() {
return (_req, res) => res.end('ok');
const ready = this.readyAt;
return (_req, res) => {
res.statusCode = ready ? 200 : 503;
res.end(ready ? 'ok' : 'not ready');
};
}

async ready() {
if (this.options.readyError) {
this.emit('error', this.options.readyError);
throw this.options.readyError;
}
this.readyAt = true;
}

onerror(err) {
this.errors.push(err);
}

async close() {}
}

Expand Down
17 changes: 17 additions & 0 deletions plugins/mock/test/fixtures/snapshot-framework/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const egg = require('egg');

class Application extends egg.Application {
callback() {
// Model middleware composition that captures the current list, as koa-compose 4.2 does.
const middleware = this.middleware;
this.middleware = [...middleware];
try {
return super.callback();
} finally {
this.middleware = middleware;
}
}
}

exports.Application = Application;
exports.Agent = egg.Agent;
Loading