diff --git a/package.json b/package.json index 778ef17f26..60cb6b1789 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ ], "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", @@ -20,7 +21,7 @@ "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", diff --git a/packages/cluster/test/app_worker.test.ts b/packages/cluster/test/app_worker.test.ts index 61a2d0cfa4..45f8499656 100644 --- a/packages/cluster/test/app_worker.test.ts +++ b/packages/cluster/test/app_worker.test.ts @@ -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'; @@ -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((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 @@ -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'); @@ -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); }); diff --git a/packages/cluster/test/fixtures/apps/app-listen-path/config/config.default.js b/packages/cluster/test/fixtures/apps/app-listen-path/config/config.default.js index 1db6860b4e..e289bde542 100644 --- a/packages/cluster/test/fixtures/apps/app-listen-path/config/config.default.js +++ b/packages/cluster/test/fixtures/apps/app-listen-path/config/config.default.js @@ -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'), }, }, }; diff --git a/plugins/schedule/test/fixtures/stop/app/schedule/interval.js b/plugins/schedule/test/fixtures/stop/app/schedule/interval.js index 57ca2bfa38..456363cc49 100644 --- a/plugins/schedule/test/fixtures/stop/app/schedule/interval.js +++ b/plugins/schedule/test/fixtures/stop/app/schedule/interval.js @@ -2,7 +2,7 @@ exports.schedule = { type: 'worker', - interval: 10000, + interval: 5000, }; exports.task = async function (ctx) { diff --git a/plugins/schedule/test/stop.test.ts b/plugins/schedule/test/stop.test.ts index 06b1c20e3d..f3284dbb34 100644 --- a/plugins/schedule/test/stop.test.ts +++ b/plugins/schedule/test/stop.test.ts @@ -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); }); }); diff --git a/scripts/run-workspace-scripts.mjs b/scripts/run-workspace-scripts.mjs new file mode 100644 index 0000000000..0b0b505d53 --- /dev/null +++ b/scripts/run-workspace-scripts.mjs @@ -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