UNPKG

iostress

Version:

🚀 Blast your Socket.IO server with this quick and powerful JavaScript testing tool!

352 lines (345 loc) • 10.3 kB
// src/runner/test-runner.ts import { workerData, parentPort } from "node:worker_threads"; // src/runner/client.ts import { io } from "socket.io-client"; // src/utils.ts import ts from "typescript"; import fs from "fs"; import path from "path"; import kleur from "kleur"; import EventEmitter from "events"; var Performance = class { timers = {}; incrementorIdx = 0; measure(timerId, keepTimer = false) { if (!this.timers[timerId]) return -1; const [seconds, nanoseconds] = process.hrtime(this.timers[timerId]); const milliseconds = seconds * 1e3 + nanoseconds / 1e6; if (!keepTimer) this.delete(timerId); return milliseconds; } delete(timerId) { if (this.timers[timerId]) delete this.timers[timerId]; } start() { this.timers[this.incrementorIdx] = process.hrtime(); return this.incrementorIdx++; } }; var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); var random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; // src/runner/client.ts import { isPromise } from "node:util/types"; import EventEmitter3 from "node:events"; // src/runner/logger.ts import EventEmitter2 from "events"; import { createLogger, format, transports } from "winston"; var Logger = class extends EventEmitter2 { winston; constructor(logsDir2) { super(); const { printf, combine, timestamp } = format; const fileFormat = printf(({ level, message, label, timestamp: timestamp2 }) => { const spaces = " ".repeat(7 - level.length); return `${timestamp2} ${spaces}${level.toUpperCase()} [${label}] ${message}`; }); this.winston = createLogger({ level: "debug", format: combine(timestamp(), fileFormat), transports: [ new transports.File({ dirname: logsDir2, filename: "iostress.error.log", level: "error" }), new transports.File({ dirname: logsDir2, filename: "iostress.info.log", level: "info" }), new transports.File({ dirname: logsDir2, filename: "iostress.combined.log" }) ] }); } log(message, type = "Business") { this.winston.info(message, { label: type }); } error(message, type = "Business") { this.winston.error( (typeof message === "string" ? new Error(message).stack : message.stack) ?? "Unknown error", { label: type } ); this.emit("error", type); } warn(message, type = "Business") { this.winston.warn(message, { label: type }); } debug(message, type = "Business") { this.winston.debug(message, { label: type }); } }; // src/runner/client.ts var Client = class extends EventEmitter3 { constructor(options) { super(); this.options = options; this.logger = new Logger(options.logsDir); this.logger.on("error", (type) => { this.report.errors.total++; this.report.errors.byType[type] = (this.report.errors.byType[type] ?? 0) + 1; }); const connTimer = this.performance.start(); this.socket = io(this.options.target, options.initializer); this.socket.on("connect", () => { if (this.report.connection.latency === -1) { this.report.connection.latency = this.performance.measure( connTimer, true ); this.report.connection.success = true; } this.report.connection.attempted = true; }); this.socket.on("connect_error", (error) => { this.logger.error(error, "Connection"); if (!this.socket.active) { this.emit("finished", this.report); } }); this.socket.on("reconnect_attempt", () => { this.report.connection.reconnectAttempts++; }); this.socket.on("reconnect", () => { this.report.connection.reconnectSuccess++; }); this.socket.on("reconnect_error", (error) => { this.logger.error(error, "Connection"); }); const originalEmit = this.socket.emit.bind(this.socket); this.socket.emit = (ev, ...args) => { const emitTimer = this.performance.start(); const lastArg = args[args.length - 1]; if (typeof lastArg === "function") { const originalAck = lastArg; args[args.length - 1] = async (...ackArgs) => { try { const latency = this.performance.measure(emitTimer); this.report.events.latencyFrames.push(latency); let result; if (isPromise(originalAck)) result = await originalAck(...ackArgs); else result = originalAck(...ackArgs); this.report.events.successful++; return result; } catch (error) { this.report.events.failed++; this.logger.error(error, "Business"); } }; } try { const result = originalEmit(ev, ...args); if (typeof lastArg !== "function") { const latency = this.performance.measure(emitTimer); this.report.events.latencyFrames.push(latency); this.report.events.successful++; } return result; } catch (error) { this.report.events.failed++; this.logger.error(error, "Business"); throw error; } }; this.socket.emitWithAck = (ev, ...args) => { return new Promise((resolve, reject) => { try { this.socket.emit(ev, ...args, (result) => { resolve(result); }); } catch (error) { reject(error); } }); }; this.socket.onAnyOutgoing(() => { this.report.events.sent++; }); this.socket.onAny(() => { this.report.events.received++; }); this.socket.on("disconnect", (reason) => { if (reason === "io server disconnect" || reason === "io client disconnect") { this.emit("finished", this.report); } }); this.on("SIGTERM", () => { if (this.socket.connected) { this.socket.disconnect(); } else { this.socket.off(); this.socket.offAny(); this.socket.offAnyOutgoing(); this.socket.disconnect(); this.socket.close(); this.emit("finished", this.report); } }); } socket; report = { connection: { attempted: false, latency: -1, success: false, reconnectAttempts: 0, reconnectSuccess: 0 }, errors: { total: 0, byType: {} }, events: { sent: 0, received: 0, successful: 0, failed: 0, latencyFrames: [] } }; performance = new Performance(); logger; async runTest() { let timeout; try { let fn = (await import(this.options.scenarioPath)).default; if (typeof fn !== "function") fn = fn.default; if (this.options.scenarioTimeout) { timeout = setTimeout(() => { this.socket.disconnect(); }, this.options.scenarioTimeout); } fn(this.socket, this.logger); this.emit("running"); } catch (error) { if (timeout) clearTimeout(timeout); this.logger.error(error, "Business"); this.socket.disconnect(); } } }; // src/runner/test-runner.ts var { target, starterInitializers, finalInitializers, rampDelayRate, scenarioPath, scenarioTimeout, logsDir } = workerData; var runnerReport = { connections: { attempted: 0, successful: 0, failed: 0, reconnectAttempts: 0, reconnectSuccess: 0, latencyFrames: [] }, errors: { total: 0, byType: {} }, events: { sent: 0, received: 0, successful: 0, failed: 0, latencyFrames: [] } }; var clientsCount = starterInitializers.length + finalInitializers.length; var readyClients = 0; var runningClients = 0; var finishedClients = 0; var terminators = []; var runPhaseSlice = async () => { let lazy = false; for (const initializer of [...starterInitializers, ...finalInitializers]) { if (lazy) await sleep(random(1, 10) * rampDelayRate); readyClients++; if (!lazy && readyClients >= starterInitializers.length) { lazy = true; } const client = new Client({ target, scenarioPath, scenarioTimeout, logsDir, initializer }); client.on("running", () => { readyClients--; runningClients++; parentPort?.postMessage({ event: "status", data: { readyClients, runningClients, finishedClients } }); }); client.on("finished", (report) => { runningClients--; finishedClients++; parentPort?.postMessage({ event: "status", data: { readyClients, runningClients, finishedClients } }); if (report.connection.attempted) runnerReport.connections.attempted++; if (report.connection.success) runnerReport.connections.successful++; else runnerReport.connections.failed++; runnerReport.connections.reconnectAttempts += report.connection.reconnectAttempts; runnerReport.connections.reconnectSuccess += report.connection.reconnectSuccess; runnerReport.connections.latencyFrames.push(report.connection.latency); runnerReport.errors.total += report.errors.total; for (const errorType in report.errors.byType) { if (runnerReport.errors.byType[errorType]) { runnerReport.errors.byType[errorType] += report.errors.byType[errorType]; } else { runnerReport.errors.byType[errorType] = report.errors.byType[errorType]; } } runnerReport.events.sent += report.events.sent; runnerReport.events.received += report.events.received; runnerReport.events.successful += report.events.successful; runnerReport.events.failed += report.events.failed; runnerReport.events.latencyFrames.push(...report.events.latencyFrames); if (clientsCount === finishedClients) { parentPort?.postMessage({ event: "finished", report: runnerReport }); process.exit(0); } }); terminators.push(() => { client.emit("SIGTERM"); }); client.runTest(); } parentPort?.on("message", (message) => { if (message.event === "SIGTERM") { terminators.forEach((terminator) => terminator()); } }); }; runPhaseSlice();