Skip to content

Commit

Permalink
feat: impl parallel app for mocha parallel mode (#130)
Browse files Browse the repository at this point in the history
- use mochaGlobalSetup/mochaGlobalTeardown to setup
agent in mocha master process.
- bootstrap app without agent to aviod port conflict.
  • Loading branch information
killagu authored Nov 4, 2022
1 parent fbbd8af commit 22f508c
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 5 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@v2

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}

Expand All @@ -41,6 +41,6 @@ jobs:
run: npm run ci

- name: Code Coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
12 changes: 9 additions & 3 deletions bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@
const assert = require('power-assert');
const path = require('path');
const mock = require('./index').default;
const mockParallelApp = require('./lib/parallel/app');
const { getEggOptions } = require('./lib/utils');

const options = {};
if (process.env.EGG_BASE_DIR) options.baseDir = process.env.EGG_BASE_DIR;
const options = getEggOptions();

// throw error when an egg plugin test is using bootstrap
const pkgInfo = require(path.join(options.baseDir || process.cwd(), 'package.json'));
if (pkgInfo.eggPlugin) throw new Error('DO NOT USE bootstrap to test plugin');

const app = mock.app(options);
let app;
if (process.env.ENABLE_MOCHA_PARALLEL && process.env.AUTO_AGENT) {
app = mockParallelApp(options);
} else {
app = mock.app(options);
}

if (typeof beforeAll === 'function') {
// jest
Expand Down
135 changes: 135 additions & 0 deletions lib/parallel/agent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
'use strict';

const debug = require('debug')('egg-mock');
const Base = require('sdk-base');
const path = require('path');
const detectPort = require('detect-port');

const context = require('../context');
const formatOptions = require('../format_options');
const { sleep, rimraf } = require('../utils');
const mockCustomLoader = require('../mock_custom_loader');

const {
APP_INIT,
INIT_ONCE_LISTENER,
INIT_ON_LISTENER,
BIND_EVENT,
consoleLogger,
} = require('./util');

class MockAgent extends Base {
constructor(options) {
super({ initMethod: '_init' });
this.options = options;
this.baseDir = options.baseDir;
this.closed = false;
this[APP_INIT] = false;
this[INIT_ON_LISTENER] = new Set();
this[INIT_ONCE_LISTENER] = new Set();
// listen once, otherwise will throw exception when emit error without listenr
this.once('error', err => {
consoleLogger.error(err);
});
}

async _init() {
if (this.options.beforeInit) {
await this.options.beforeInit(this);
delete this.options.beforeInit;
}
if (this.options.clean !== false) {
const logDir = path.join(this.options.baseDir, 'logs');
try {
await rimraf(logDir);
} catch (err) {
/* istanbul ignore next */
console.error(`remove log dir ${logDir} failed: ${err.stack}`);
}
}

this.options.clusterPort = process.env.CLUSTER_PORT = await detectPort();
debug('get clusterPort %s', this.options.clusterPort);
const { Agent } = require(this.options.framework);

const agent = this._instance = new Agent(Object.assign({}, this.options));

// egg-mock plugin need to override egg context
Object.assign(agent.context, context);
mockCustomLoader(agent);

debug('agent instantiate');
this[APP_INIT] = true;
debug('this[APP_INIT] = true');
this[BIND_EVENT]();
debug('http server instantiate');
await agent.ready();

const msg = {
action: 'egg-ready',
data: this.options,
};
agent.messenger._onMessage(msg);
debug('agent ready');
}

[BIND_EVENT]() {
for (const args of this[INIT_ON_LISTENER]) {
debug('on(%s), use cache and pass to app', args);
this._instance.on(...args);
this.removeListener(...args);
}
for (const args of this[INIT_ONCE_LISTENER]) {
debug('once(%s), use cache and pass to app', args);
this._instance.on(...args);
this.removeListener(...args);
}
}

on(...args) {
if (this[APP_INIT]) {
debug('on(%s), pass to app', args);
this._instance.on(...args);
} else {
debug('on(%s), cache it because app has not init', args);
if (this[INIT_ON_LISTENER]) {
this[INIT_ON_LISTENER].add(args);
}
super.on(...args);
}
}

once(...args) {
if (this[APP_INIT]) {
debug('once(%s), pass to app', args);
this._instance.once(...args);
} else {
debug('once(%s), cache it because app has not init', args);
if (this[INIT_ONCE_LISTENER]) {
this[INIT_ONCE_LISTENER].add(args);
}
super.on(...args);
}
}

/**
* close app
* @return {Promise} promise
*/
async close() {
this.closed = true;
const self = this;
if (self._instance) {
await self._instance.close();
} else {
// when app init throws an exception, must wait for app quit gracefully
await sleep(200);
}
}
}

module.exports = function(options) {
options = formatOptions(options);

return new MockAgent(options);
};
14 changes: 14 additions & 0 deletions lib/parallel/agent_register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const Agent = require('./agent');
const { getEggOptions } = require('../utils');

let agent;
exports.mochaGlobalSetup = async () => {
agent = Agent(getEggOptions());
await agent.ready();
};

exports.mochaGlobalTeardown = async () => {
if (agent) {
await agent.close();
}
};
127 changes: 127 additions & 0 deletions lib/parallel/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
'use strict';

const debug = require('debug')('egg-mock');
const Base = require('sdk-base');

const context = require('../context');
const formatOptions = require('../format_options');
const { sleep } = require('../utils');
const mockCustomLoader = require('../mock_custom_loader');
const mockHttpServer = require('../mock_http_server');
const {
proxyApp,
APP_INIT,
INIT_ONCE_LISTENER,
INIT_ON_LISTENER,
BIND_EVENT,
consoleLogger,
} = require('./util');

class MockApplication extends Base {
constructor(options) {
super({ initMethod: '_init' });
this.options = options;
this.baseDir = options.baseDir;
this.closed = false;
this[APP_INIT] = false;
this[INIT_ON_LISTENER] = new Set();
this[INIT_ONCE_LISTENER] = new Set();
// listen once, otherwise will throw exception when emit error without listenr
this.once('error', err => {
consoleLogger.error(err);
});
}

async _init() {
if (this.options.beforeInit) {
await this.options.beforeInit(this);
delete this.options.beforeInit;
}

this.options.clusterPort = process.env.CLUSTER_PORT;
debug('get clusterPort %s', this.options.clusterPort);
const { Application } = require(this.options.framework);

const app = this._instance = new Application(Object.assign({}, this.options));

// egg-mock plugin need to override egg context
Object.assign(app.context, context);
mockCustomLoader(app);

debug('app instantiate');
this[APP_INIT] = true;
debug('this[APP_INIT] = true');
this[BIND_EVENT]();
debug('http server instantiate');
mockHttpServer(app);
await app.ready();

const msg = {
action: 'egg-ready',
data: this.options,
};
app.messenger._onMessage(msg);
debug('app ready');
}

[BIND_EVENT]() {
for (const args of this[INIT_ON_LISTENER]) {
debug('on(%s), use cache and pass to app', args);
this._instance.on(...args);
this.removeListener(...args);
}
for (const args of this[INIT_ONCE_LISTENER]) {
debug('once(%s), use cache and pass to app', args);
this._instance.on(...args);
this.removeListener(...args);
}
}

on(...args) {
if (this[APP_INIT]) {
debug('on(%s), pass to app', args);
this._instance.on(...args);
} else {
debug('on(%s), cache it because app has not init', args);
if (this[INIT_ON_LISTENER]) {
this[INIT_ON_LISTENER].add(args);
}
super.on(...args);
}
}

once(...args) {
if (this[APP_INIT]) {
debug('once(%s), pass to app', args);
this._instance.once(...args);
} else {
debug('once(%s), cache it because app has not init', args);
if (this[INIT_ONCE_LISTENER]) {
this[INIT_ONCE_LISTENER].add(args);
}
super.on(...args);
}
}

/**
* close app
* @return {Promise} promise
*/
async close() {
this.closed = true;
const self = this;
if (self._instance) {
await self._instance.close();
} else {
// when app init throws an exception, must wait for app quit gracefully
await sleep(200);
}
}
}

module.exports = function(options) {
options = formatOptions(options);

const app = new MockApplication(options);
return proxyApp(app, options);
};
Loading

0 comments on commit 22f508c

Please sign in to comment.