Skip to content
Open
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
],
"type": "module",
"scripts": {
"clean": "node scripts/run-workspace-scripts.mjs clean",
"clean-dist": "ut run clean --workspaces --if-present",
"build": "tsdown",
"prelint": "ut run clean-dist",
"lint": "oxlint --type-aware --type-check --quiet",
"fmt": "oxfmt",
"typecheck": "ut run clean-dist && ut run typecheck --workspaces --if-present",
"fmtcheck": "oxfmt --check .",
"pretest": "ut run clean-dist && ut run pretest --workspaces --if-present",
"pretest": "ut run clean-dist",
"test": "vitest run --bail 1 --retry 2 --testTimeout 20000 --hookTimeout 20000",
"test:cov": "ut run test -- --coverage",
"preci": "ut run pretest --workspaces --if-present",
Expand Down
64 changes: 59 additions & 5 deletions packages/cluster/test/app_worker.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { strict as assert } from 'node:assert';
import { randomBytes } from 'node:crypto';
import { rm } from 'node:fs/promises';
import { createConnection } from 'node:net';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { scheduler } from 'node:timers/promises';

import { mm, type MockApplication } from '@eggjs/mock';
Expand All @@ -8,7 +12,47 @@ import { ip } from 'address';
import urllib from 'urllib';
import { describe, it, afterEach, beforeEach, beforeAll, afterAll } from 'vitest';

import { cluster, getFilepath } from './utils.ts';
import { cluster } from './utils.ts';

async function waitForSocket(filepath: string) {
const start = Date.now();
const timeout = 5000;
while (Date.now() - start < timeout) {
const remaining = timeout - (Date.now() - start);
const connected = await new Promise<boolean>((resolve) => {
const socket = createConnection(filepath);
let settled = false;
const finish = (result: boolean) => {
if (settled) {
return;
}
settled = true;
socket.setTimeout(0);
if (result) {
socket.end();
} else {
socket.destroy();
}
resolve(result);
};
socket.setTimeout(Math.max(1, Math.min(remaining, 500)));
socket.once('connect', () => {
finish(true);
});
socket.once('error', () => {
finish(false);
});
socket.once('timeout', () => {
finish(false);
});
});
if (connected) {
return;
}
await scheduler.wait(50);
}
throw new Error(`Socket ${filepath} did not become connectable`);
}

// node v24 will hang when test this file
// FIXME: should enable this test after node v24 is stable
Expand Down Expand Up @@ -204,15 +248,16 @@ describe.skipIf(process.version.startsWith('v24') || process.platform === 'win32
});

describe('listen config', () => {
const sockFile = getFilepath('apps/app-listen-path/my.sock');
beforeEach(() => {
const sockFile = path.join(tmpdir(), `egg-app-listen-path-${process.pid}-${randomBytes(4).toString('hex')}.sock`);
beforeEach(async () => {
mm.env('default');
await rm(sockFile, { force: true });
});
afterEach(async () => {
await app.close();
await mm.restore();
});
afterEach(() => rm(sockFile, { force: true, recursive: true }));
afterEach(() => rm(sockFile, { force: true }));

it.skip('should set default port 170xx then config.listen.port is null', async () => {
app = cluster('apps/app-listen-without-port');
Expand Down Expand Up @@ -276,13 +321,22 @@ describe.skipIf(process.version.startsWith('v24') || process.platform === 'win32
});

it('should use path in config', async () => {
app = cluster('apps/app-listen-path');
app = cluster('apps/app-listen-path', {
opt: {
execArgv: [],
env: {
...process.env,
EGG_APP_LISTEN_PATH_SOCKET: sockFile,
},
},
});
// app.debug();
await app.ready();

app.expect('code', 0);
app.expect('stdout', new RegExp(`egg started on ${sockFile}`));

await waitForSocket(sockFile);
const sock = encodeURIComponent(sockFile);
await request(`http+unix://${sock}`).get('/').expect('done').expect(200);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module.exports = (app) => {
keys: '123',
cluster: {
listen: {
path: path.join(app.baseDir, 'my.sock'),
path: process.env.EGG_APP_LISTEN_PATH_SOCKET || path.join(app.baseDir, 'my.sock'),
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

exports.schedule = {
type: 'worker',
interval: 10000,
interval: 5000,
};

exports.task = async function (ctx) {
Expand Down
47 changes: 41 additions & 6 deletions plugins/schedule/test/stop.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,57 @@
import { readFileSync } from 'node:fs';
import { setTimeout as sleep } from 'node:timers/promises';

import { mm, type MockApplication } from '@eggjs/mock';
import { describe, it, afterAll, beforeAll, expect } from 'vitest';

import { contains, getFixtures, getLogContent } from './utils.ts';
import { contains, getFixtures } from './utils.ts';

function readLogIfExists(logPath: string) {
try {
return readFileSync(logPath, 'utf8');
} catch (err) {
const error = err as { code?: string };
if (error.code === 'ENOENT') {
return '';
}
throw err;
}
}

async function waitForNewLog(logPath: string, match: string, previousLog: string, timeout = 5000) {
const start = Date.now();
while (Date.now() - start < timeout) {
const log = readLogIfExists(logPath);
const appendedLog = log.startsWith(previousLog) ? log.slice(previousLog.length) : log;
if (appendedLog.includes(match)) {
return log;
}
await sleep(100);
}
throw new Error(`Log ${logPath} did not contain "${match}"`);
}

describe.skipIf(process.platform === 'win32')('test/stop.test.ts', () => {
let app: MockApplication;
let app: MockApplication | undefined;
let intervalLogBeforeStart = '';
beforeAll(async () => {
intervalLogBeforeStart = readLogIfExists(getFixtures('stop/logs/stop/stop-web.log'));
app = mm.cluster({ baseDir: getFixtures('stop'), workers: 2 });
// app.debug();
await app.ready();
});
afterAll(() => app.close());
afterAll(() => app?.close());

it('should stop interval timer after cluster closes', async () => {
const logPath = getFixtures('stop/logs/stop/stop-web.log');
await waitForNewLog(logPath, 'interval', intervalLogBeforeStart, 12000);

await app!.close();
app = undefined;
const afterCloseCount = contains(readLogIfExists(logPath), 'interval');

it('should thrown', async () => {
await sleep(10000);
const log = getLogContent('stop');
expect(contains(log, 'interval')).toBe(0);
const log = readLogIfExists(logPath);
expect(contains(log, 'interval')).toBe(afterCloseCount);
});
});
75 changes: 75 additions & 0 deletions scripts/run-workspace-scripts.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { spawnSync } from 'node:child_process';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';

const script = process.argv[2];
if (!script) {
console.error('Usage: node scripts/run-workspace-scripts.mjs <script>');
process.exit(1);
}

const root = resolve(import.meta.dirname, '..');
const workspaceFile = join(root, 'pnpm-workspace.yaml');

function readWorkspacePatterns() {
const patterns = [];
let inPackages = false;

for (const line of readFileSync(workspaceFile, 'utf8').split('\n')) {
if (line.trim() === 'packages:') {
inPackages = true;
continue;
}
if (inPackages && line.length > 0 && !line.startsWith(' ')) {
break;
}

const match = /^\s+-\s+(.+?)\s*$/.exec(line);
if (inPackages && match) {
patterns.push(match[1].replace(/^['"]|['"]$/g, ''));
}
}

return patterns.sort((a, b) => a.localeCompare(b));
}

function expandWorkspacePattern(pattern) {
if (!pattern.endsWith('/*')) {
return [join(root, pattern)];
}

const baseDir = join(root, pattern.slice(0, -2));
return readdirSync(baseDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.sort((a, b) => a.name.localeCompare(b.name))
.map((entry) => join(baseDir, entry.name));
}

let matched = 0;
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';

for (const workspaceDir of readWorkspacePatterns().flatMap(expandWorkspacePattern)) {
Comment thread
killagu marked this conversation as resolved.
const packageJsonPath = join(workspaceDir, 'package.json');
if (!existsSync(packageJsonPath)) {
continue;
}

const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
if (!packageJson.scripts?.[script]) {
continue;
}

matched++;
console.log(`> ${packageJson.name ?? workspaceDir} ${script}`);
const result = spawnSync(npmCommand, ['run', '--silent', script], {
cwd: workspaceDir,
stdio: 'inherit',
});
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}

if (matched === 0) {
console.log(`No workspace scripts found for "${script}"`);
}
19 changes: 15 additions & 4 deletions tegg/plugin/tegg/test/ManifestCollection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import { getAppBaseDir } from './utils.ts';

describe('plugin/tegg/test/ManifestCollection.test.ts', () => {
let app: MockApplication;
let teggExtCache: TeggManifestExtension | undefined;

function getTeggManifestExtension() {
if (!teggExtCache) {
teggExtCache =
(app.loader.manifest.getExtension(TEGG_MANIFEST_KEY) as TeggManifestExtension | undefined) ??
(app.loader.generateManifest().extensions[TEGG_MANIFEST_KEY] as TeggManifestExtension | undefined);
Comment thread
killagu marked this conversation as resolved.
}
return teggExtCache;
}
Comment thread
killagu marked this conversation as resolved.

afterEach(async () => {
return mm.restore();
Expand All @@ -19,6 +29,7 @@ describe('plugin/tegg/test/ManifestCollection.test.ts', () => {
app = mm.app({
baseDir: getAppBaseDir('egg-app'),
});
teggExtCache = undefined;
await app.ready();
});

Expand All @@ -27,12 +38,12 @@ describe('plugin/tegg/test/ManifestCollection.test.ts', () => {
});

it('should collect tegg manifest extension after ready', () => {
const teggExt = app.loader.manifest.getExtension(TEGG_MANIFEST_KEY) as TeggManifestExtension | undefined;
const teggExt = getTeggManifestExtension();
assert.ok(teggExt, 'tegg manifest extension should be set');
});

it('should have moduleReferences matching app.moduleReferences', () => {
const teggExt = app.loader.manifest.getExtension(TEGG_MANIFEST_KEY) as TeggManifestExtension;
const teggExt = getTeggManifestExtension()!;
assert.ok(teggExt.moduleReferences);
assert.ok(teggExt.moduleReferences.length > 0);

Expand All @@ -52,7 +63,7 @@ describe('plugin/tegg/test/ManifestCollection.test.ts', () => {
});

it('should have moduleDescriptors with decoratedFiles', () => {
const teggExt = app.loader.manifest.getExtension(TEGG_MANIFEST_KEY) as TeggManifestExtension;
const teggExt = getTeggManifestExtension()!;
assert.ok(teggExt.moduleDescriptors);
assert.ok(teggExt.moduleDescriptors.length > 0);

Expand All @@ -64,7 +75,7 @@ describe('plugin/tegg/test/ManifestCollection.test.ts', () => {
});

it('should have non-empty decoratedFiles for modules with prototypes', () => {
const teggExt = app.loader.manifest.getExtension(TEGG_MANIFEST_KEY) as TeggManifestExtension;
const teggExt = getTeggManifestExtension()!;
// At least one module should have decorated files
const hasFiles = teggExt.moduleDescriptors.some((d) => d.decoratedFiles.length > 0);
assert.ok(hasFiles, 'at least one module should have decorated files');
Expand Down
Loading