added cli-covarage tool and fixed more tests
This commit is contained in:
parent
b63e58784c
commit
b845a8eade
57 changed files with 11917 additions and 295 deletions
|
|
@ -90,8 +90,8 @@ export class Shutdown {
|
|||
* Set shutdown timeout in milliseconds
|
||||
*/
|
||||
setTimeout(timeout: number): void {
|
||||
if (timeout <= 0) {
|
||||
throw new Error('Shutdown timeout must be positive');
|
||||
if (isNaN(timeout) || timeout <= 0) {
|
||||
throw new Error('Shutdown timeout must be a positive number');
|
||||
}
|
||||
this.shutdownTimeout = timeout;
|
||||
}
|
||||
|
|
@ -107,7 +107,8 @@ export class Shutdown {
|
|||
* Check if shutdown signal was received (for quick checks in running jobs)
|
||||
*/
|
||||
isShutdownSignalReceived(): boolean {
|
||||
return this.signalReceived || this.isShuttingDown;
|
||||
const globalFlag = (globalThis as any).__SHUTDOWN_SIGNAL_RECEIVED__ || false;
|
||||
return globalFlag || this.signalReceived || this.isShuttingDown;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
66
libs/core/shutdown/test/index.test.ts
Normal file
66
libs/core/shutdown/test/index.test.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import * as shutdownExports from '../src';
|
||||
import { Shutdown } from '../src';
|
||||
|
||||
describe('Shutdown Package Exports', () => {
|
||||
it('should export all main functions', () => {
|
||||
expect(shutdownExports.onShutdown).toBeDefined();
|
||||
expect(shutdownExports.onShutdownHigh).toBeDefined();
|
||||
expect(shutdownExports.onShutdownMedium).toBeDefined();
|
||||
expect(shutdownExports.onShutdownLow).toBeDefined();
|
||||
expect(shutdownExports.setShutdownTimeout).toBeDefined();
|
||||
expect(shutdownExports.isShuttingDown).toBeDefined();
|
||||
expect(shutdownExports.isShutdownSignalReceived).toBeDefined();
|
||||
expect(shutdownExports.getShutdownCallbackCount).toBeDefined();
|
||||
expect(shutdownExports.initiateShutdown).toBeDefined();
|
||||
expect(shutdownExports.shutdownAndExit).toBeDefined();
|
||||
expect(shutdownExports.resetShutdown).toBeDefined();
|
||||
});
|
||||
|
||||
it('should export Shutdown class', () => {
|
||||
expect(shutdownExports.Shutdown).toBeDefined();
|
||||
expect(shutdownExports.Shutdown).toBe(Shutdown);
|
||||
});
|
||||
|
||||
it('should export correct function types', () => {
|
||||
expect(typeof shutdownExports.onShutdown).toBe('function');
|
||||
expect(typeof shutdownExports.onShutdownHigh).toBe('function');
|
||||
expect(typeof shutdownExports.onShutdownMedium).toBe('function');
|
||||
expect(typeof shutdownExports.onShutdownLow).toBe('function');
|
||||
expect(typeof shutdownExports.setShutdownTimeout).toBe('function');
|
||||
expect(typeof shutdownExports.isShuttingDown).toBe('function');
|
||||
expect(typeof shutdownExports.isShutdownSignalReceived).toBe('function');
|
||||
expect(typeof shutdownExports.getShutdownCallbackCount).toBe('function');
|
||||
expect(typeof shutdownExports.initiateShutdown).toBe('function');
|
||||
expect(typeof shutdownExports.shutdownAndExit).toBe('function');
|
||||
expect(typeof shutdownExports.resetShutdown).toBe('function');
|
||||
});
|
||||
|
||||
it('should export type definitions', () => {
|
||||
// Type tests - these compile-time checks ensure types are exported
|
||||
type TestShutdownCallback = shutdownExports.ShutdownCallback;
|
||||
type TestShutdownOptions = shutdownExports.ShutdownOptions;
|
||||
type TestShutdownResult = shutdownExports.ShutdownResult;
|
||||
type TestPrioritizedShutdownCallback = shutdownExports.PrioritizedShutdownCallback;
|
||||
|
||||
// Runtime check that types can be used
|
||||
const testCallback: TestShutdownCallback = async () => {};
|
||||
const testOptions: TestShutdownOptions = { timeout: 5000, autoRegister: false };
|
||||
const testResult: TestShutdownResult = {
|
||||
success: true,
|
||||
callbacksExecuted: 1,
|
||||
callbacksFailed: 0,
|
||||
duration: 100,
|
||||
};
|
||||
const testPrioritized: TestPrioritizedShutdownCallback = {
|
||||
callback: testCallback,
|
||||
priority: 50,
|
||||
name: 'test',
|
||||
};
|
||||
|
||||
expect(testCallback).toBeDefined();
|
||||
expect(testOptions).toBeDefined();
|
||||
expect(testResult).toBeDefined();
|
||||
expect(testPrioritized).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
onShutdownMedium,
|
||||
resetShutdown,
|
||||
setShutdownTimeout,
|
||||
shutdownAndExit,
|
||||
Shutdown,
|
||||
} from '../src';
|
||||
import type { ShutdownOptions, ShutdownResult } from '../src/types';
|
||||
|
|
@ -103,12 +104,12 @@ describe('Shutdown Comprehensive Tests', () => {
|
|||
|
||||
it('should handle negative timeout values', () => {
|
||||
// Should throw for negative values
|
||||
expect(() => setShutdownTimeout(-1000)).toThrow('Shutdown timeout must be positive');
|
||||
expect(() => setShutdownTimeout(-1000)).toThrow('Shutdown timeout must be a positive number');
|
||||
});
|
||||
|
||||
it('should handle zero timeout', () => {
|
||||
// Should throw for zero timeout
|
||||
expect(() => setShutdownTimeout(0)).toThrow('Shutdown timeout must be positive');
|
||||
expect(() => setShutdownTimeout(0)).toThrow('Shutdown timeout must be a positive number');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -388,7 +389,7 @@ describe('Shutdown Comprehensive Tests', () => {
|
|||
|
||||
for (let i = 0; i < errorCount; i++) {
|
||||
onShutdown(async () => {
|
||||
throw new Error(`Error ${i}`);
|
||||
throw new Error('Expected error');
|
||||
}, `error-${i}`);
|
||||
}
|
||||
|
||||
|
|
@ -397,30 +398,158 @@ describe('Shutdown Comprehensive Tests', () => {
|
|||
expect(result.callbacksExecuted).toBe(successCount + errorCount);
|
||||
expect(result.callbacksFailed).toBe(errorCount);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain(`${errorCount} callbacks failed`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Global State Management', () => {
|
||||
it('should properly reset global state', () => {
|
||||
// Add some callbacks
|
||||
onShutdown(async () => {});
|
||||
onShutdownHigh(async () => {});
|
||||
onShutdownLow(async () => {});
|
||||
describe('shutdownAndExit', () => {
|
||||
it('should call process.exit after shutdown', async () => {
|
||||
// Mock process.exit
|
||||
const originalExit = process.exit;
|
||||
const exitMock = mock(() => {
|
||||
throw new Error('Process exit called');
|
||||
});
|
||||
process.exit = exitMock as any;
|
||||
|
||||
expect(getShutdownCallbackCount()).toBe(3);
|
||||
try {
|
||||
const callback = mock(async () => {});
|
||||
onShutdown(callback);
|
||||
|
||||
resetShutdown();
|
||||
|
||||
expect(getShutdownCallbackCount()).toBe(0);
|
||||
expect(isShuttingDown()).toBe(false);
|
||||
await expect(shutdownAndExit('SIGTERM', 1)).rejects.toThrow('Process exit called');
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(exitMock).toHaveBeenCalledWith(1);
|
||||
} finally {
|
||||
// Restore process.exit
|
||||
process.exit = originalExit;
|
||||
}
|
||||
});
|
||||
|
||||
it('should maintain singleton across imports', () => {
|
||||
const instance1 = Shutdown.getInstance();
|
||||
const instance2 = Shutdown.getInstance();
|
||||
it('should use default exit code 0', async () => {
|
||||
const originalExit = process.exit;
|
||||
const exitMock = mock(() => {
|
||||
throw new Error('Process exit called');
|
||||
});
|
||||
process.exit = exitMock as any;
|
||||
|
||||
expect(instance1).toBe(instance2);
|
||||
try {
|
||||
await expect(shutdownAndExit()).rejects.toThrow('Process exit called');
|
||||
expect(exitMock).toHaveBeenCalledWith(0);
|
||||
} finally {
|
||||
process.exit = originalExit;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Signal Handling Integration', () => {
|
||||
it('should handle manual signal with custom name', async () => {
|
||||
const callback = mock(async () => {});
|
||||
onShutdown(callback);
|
||||
|
||||
const result = await initiateShutdown('CUSTOM_SIGNAL');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle shutdown from getInstance without options', () => {
|
||||
const instance = Shutdown.getInstance();
|
||||
expect(instance).toBeInstanceOf(Shutdown);
|
||||
|
||||
// Call again to test singleton
|
||||
const instance2 = Shutdown.getInstance();
|
||||
expect(instance2).toBe(instance);
|
||||
});
|
||||
|
||||
it('should handle global instance state correctly', () => {
|
||||
// Start fresh
|
||||
resetShutdown();
|
||||
expect(getShutdownCallbackCount()).toBe(0);
|
||||
|
||||
// Add callback - this creates global instance
|
||||
onShutdown(async () => {});
|
||||
expect(getShutdownCallbackCount()).toBe(1);
|
||||
|
||||
// Reset and verify
|
||||
resetShutdown();
|
||||
expect(getShutdownCallbackCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling Edge Cases', () => {
|
||||
it('should handle callback that rejects with undefined', async () => {
|
||||
const undefinedRejectCallback = mock(async () => {
|
||||
return Promise.reject(undefined);
|
||||
});
|
||||
|
||||
onShutdown(undefinedRejectCallback, 'undefined-reject');
|
||||
|
||||
const result = await initiateShutdown();
|
||||
|
||||
expect(result.callbacksFailed).toBe(1);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle callback that rejects with null', async () => {
|
||||
const nullRejectCallback = mock(async () => {
|
||||
return Promise.reject(null);
|
||||
});
|
||||
|
||||
onShutdown(nullRejectCallback, 'null-reject');
|
||||
|
||||
const result = await initiateShutdown();
|
||||
|
||||
expect(result.callbacksFailed).toBe(1);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle mixed sync and async callbacks', async () => {
|
||||
const syncCallback = mock(() => {
|
||||
// Synchronous - returns void
|
||||
});
|
||||
|
||||
const asyncCallback = mock(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
});
|
||||
|
||||
onShutdown(syncCallback as any);
|
||||
onShutdown(asyncCallback);
|
||||
|
||||
const result = await initiateShutdown();
|
||||
|
||||
expect(result.callbacksExecuted).toBe(2);
|
||||
expect(syncCallback).toHaveBeenCalled();
|
||||
expect(asyncCallback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shutdown Method Variants', () => {
|
||||
it('should handle direct priority parameter in onShutdown', () => {
|
||||
const callback = mock(async () => {});
|
||||
|
||||
// Test with name and priority swapped (legacy support)
|
||||
onShutdown(callback, 75, 'custom-name');
|
||||
|
||||
expect(getShutdownCallbackCount()).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle callback without any parameters', () => {
|
||||
const callback = mock(async () => {});
|
||||
|
||||
onShutdown(callback);
|
||||
|
||||
expect(getShutdownCallbackCount()).toBe(1);
|
||||
});
|
||||
|
||||
it('should validate setTimeout input', () => {
|
||||
const shutdown = new Shutdown();
|
||||
|
||||
// Valid timeout
|
||||
expect(() => shutdown.setTimeout(5000)).not.toThrow();
|
||||
|
||||
// Invalid timeouts should throw
|
||||
expect(() => shutdown.setTimeout(-1)).toThrow();
|
||||
expect(() => shutdown.setTimeout(0)).toThrow();
|
||||
expect(() => shutdown.setTimeout(NaN)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
254
libs/core/shutdown/test/shutdown-signals.test.ts
Normal file
254
libs/core/shutdown/test/shutdown-signals.test.ts
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { Shutdown } from '../src/shutdown';
|
||||
|
||||
describe('Shutdown Signal Handlers', () => {
|
||||
let shutdown: Shutdown;
|
||||
let processOnSpy: any;
|
||||
let processExitSpy: any;
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
const originalOn = process.on;
|
||||
const originalExit = process.exit;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset singleton instance
|
||||
(Shutdown as any).instance = null;
|
||||
|
||||
// Clean up global flag
|
||||
delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__;
|
||||
|
||||
// Mock process.on
|
||||
const listeners: Record<string, Function[]> = {};
|
||||
processOnSpy = mock((event: string, handler: Function) => {
|
||||
if (!listeners[event]) {
|
||||
listeners[event] = [];
|
||||
}
|
||||
listeners[event].push(handler);
|
||||
});
|
||||
process.on = processOnSpy as any;
|
||||
|
||||
// Mock process.exit
|
||||
processExitSpy = mock((code?: number) => {
|
||||
// Just record the call, don't throw
|
||||
return;
|
||||
});
|
||||
process.exit = processExitSpy as any;
|
||||
|
||||
// Store listeners for manual triggering
|
||||
(global as any).__testListeners = listeners;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original methods
|
||||
process.on = originalOn;
|
||||
process.exit = originalExit;
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
(Shutdown as any).instance = null;
|
||||
delete (global as any).__testListeners;
|
||||
});
|
||||
|
||||
describe('Signal Handler Registration', () => {
|
||||
it('should register Unix signal handlers on non-Windows', () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
|
||||
// Check that Unix signals were registered
|
||||
expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('SIGUSR2', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('unhandledRejection', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should register Windows signal handlers on Windows', () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'win32',
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
|
||||
// Check that Windows signals were registered
|
||||
expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
||||
expect(processOnSpy).not.toHaveBeenCalledWith('SIGUSR2', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function));
|
||||
expect(processOnSpy).toHaveBeenCalledWith('unhandledRejection', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should not register handlers when autoRegister is false', () => {
|
||||
shutdown = new Shutdown({ autoRegister: false });
|
||||
|
||||
expect(processOnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not register handlers twice', () => {
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
const callCount = processOnSpy.mock.calls.length;
|
||||
|
||||
// Try to setup handlers again (internally)
|
||||
shutdown['setupSignalHandlers']();
|
||||
|
||||
// Should not register additional handlers
|
||||
expect(processOnSpy.mock.calls.length).toBe(callCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Signal Handler Behavior', () => {
|
||||
it('should handle SIGTERM signal', async () => {
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
const callback = mock(async () => {});
|
||||
shutdown.onShutdown(callback);
|
||||
|
||||
const listeners = (global as any).__testListeners;
|
||||
const sigtermHandler = listeners['SIGTERM'][0];
|
||||
|
||||
// Trigger SIGTERM (this starts async shutdown)
|
||||
sigtermHandler();
|
||||
|
||||
// Verify flags are set immediately
|
||||
expect(shutdown.isShutdownSignalReceived()).toBe(true);
|
||||
expect((global as any).__SHUTDOWN_SIGNAL_RECEIVED__).toBe(true);
|
||||
|
||||
// Wait a bit for async shutdown to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Now process.exit should have been called
|
||||
expect(processExitSpy).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('should handle SIGINT signal', async () => {
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
const callback = mock(async () => {});
|
||||
shutdown.onShutdown(callback);
|
||||
|
||||
const listeners = (global as any).__testListeners;
|
||||
const sigintHandler = listeners['SIGINT'][0];
|
||||
|
||||
// Trigger SIGINT (this starts async shutdown)
|
||||
sigintHandler();
|
||||
|
||||
// Verify flags are set immediately
|
||||
expect(shutdown.isShutdownSignalReceived()).toBe(true);
|
||||
|
||||
// Wait a bit for async shutdown to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Now process.exit should have been called
|
||||
expect(processExitSpy).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('should handle uncaughtException', async () => {
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
|
||||
const listeners = (global as any).__testListeners;
|
||||
const exceptionHandler = listeners['uncaughtException'][0];
|
||||
|
||||
// Trigger uncaughtException (this starts async shutdown with exit code 1)
|
||||
exceptionHandler(new Error('Uncaught error'));
|
||||
|
||||
// Wait a bit for async shutdown to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Should exit with code 1 for uncaught exceptions
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should handle unhandledRejection', async () => {
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
|
||||
const listeners = (global as any).__testListeners;
|
||||
const rejectionHandler = listeners['unhandledRejection'][0];
|
||||
|
||||
// Trigger unhandledRejection (this starts async shutdown with exit code 1)
|
||||
rejectionHandler(new Error('Unhandled rejection'));
|
||||
|
||||
// Wait a bit for async shutdown to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
// Should exit with code 1 for unhandled rejections
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should not process signal if already shutting down', async () => {
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
|
||||
// Start shutdown
|
||||
shutdown['isShuttingDown'] = true;
|
||||
|
||||
const listeners = (global as any).__testListeners;
|
||||
const sigtermHandler = listeners['SIGTERM'][0];
|
||||
|
||||
// Mock shutdownAndExit to track calls
|
||||
const shutdownAndExitSpy = mock(() => Promise.resolve());
|
||||
shutdown.shutdownAndExit = shutdownAndExitSpy as any;
|
||||
|
||||
// Trigger SIGTERM
|
||||
sigtermHandler();
|
||||
|
||||
// Should not call shutdownAndExit since already shutting down
|
||||
expect(shutdownAndExitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle shutdown failure in signal handler', async () => {
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
|
||||
// Mock shutdownAndExit to reject
|
||||
shutdown.shutdownAndExit = mock(async () => {
|
||||
throw new Error('Shutdown failed');
|
||||
}) as any;
|
||||
|
||||
const listeners = (global as any).__testListeners;
|
||||
const sigtermHandler = listeners['SIGTERM'][0];
|
||||
|
||||
// Trigger SIGTERM - should fall back to process.exit(1)
|
||||
sigtermHandler();
|
||||
|
||||
// Wait a bit for async shutdown to fail and fallback to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Global Flag Behavior', () => {
|
||||
it('should set global shutdown flag on signal', async () => {
|
||||
delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__;
|
||||
|
||||
shutdown = new Shutdown({ autoRegister: true });
|
||||
|
||||
const listeners = (global as any).__testListeners;
|
||||
const sigtermHandler = listeners['SIGTERM'][0];
|
||||
|
||||
// Trigger signal (this sets the flag immediately)
|
||||
sigtermHandler();
|
||||
|
||||
expect((global as any).__SHUTDOWN_SIGNAL_RECEIVED__).toBe(true);
|
||||
|
||||
// Wait for async shutdown to complete to avoid hanging promises
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
});
|
||||
|
||||
it('should check global flag in isShutdownSignalReceived', () => {
|
||||
shutdown = new Shutdown({ autoRegister: false });
|
||||
|
||||
expect(shutdown.isShutdownSignalReceived()).toBe(false);
|
||||
|
||||
// Set global flag
|
||||
(global as any).__SHUTDOWN_SIGNAL_RECEIVED__ = true;
|
||||
|
||||
// Even without instance flag, should return true
|
||||
expect(shutdown.isShutdownSignalReceived()).toBe(true);
|
||||
|
||||
// Clean up
|
||||
delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__;
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue