/**
* SerialEngine 单元测试
*/
import { SerialEngine } from '@/core/SerialEngine';
import { EventBus } from '@/core/EventBus';
import { DataProcessor } from '@/core/DataProcessor';
import { WriteMutex } from '@/core/WriteMutex';
import { PortSession } from '@/core/PortSession';
import { URCDetector } from '@/core/URCDetector';
import { PlatformAdapter } from '@/adapters/PlatformAdapter';
import { SerialPortAdapter } from '@/adapters/SerialPortAdapter';
import { SerialError, EventType } from '@/types';
// Mock所有依赖
jest.mock('@/core/EventBus');
jest.mock('@/core/DataProcessor');
jest.mock('@/core/WriteMutex');
jest.mock('@/core/PortSession');
jest.mock('@/core/URCDetector');
jest.mock('@/adapters/PlatformAdapter');
jest.mock('@/adapters/SerialPortAdapter');
describe('SerialEngine', () => {
let engine: SerialEngine;
let mockEventBus: jest.Mocked<EventBus>;
let mockDataProcessor: jest.Mocked<DataProcessor>;
let mockWriteMutex: jest.Mocked<WriteMutex>;
let mockPortSession: jest.Mocked<PortSession>;
let mockURCDetector: jest.Mocked<URCDetector>;
let mockPlatformAdapter: jest.Mocked<PlatformAdapter>;
beforeEach(() => {
// 创建mock实例
mockEventBus = new EventBus() as jest.Mocked<EventBus>;
mockDataProcessor = new DataProcessor() as jest.Mocked<DataProcessor>;
mockWriteMutex = new WriteMutex() as jest.Mocked<WriteMutex>;
mockPortSession = new PortSession() as jest.Mocked<PortSession>;
mockURCDetector = new URCDetector() as jest.Mocked<URCDetector>;
mockPlatformAdapter = new PlatformAdapter() as jest.Mocked<PlatformAdapter>;
// 设置默认mock行为
mockEventBus.subscribe.mockReturnValue('subscription-id');
mockEventBus.publish.mockImplementation((type, event) => {});
mockEventBus.unsubscribe.mockImplementation(() => {});
mockEventBus.clear.mockImplementation(() => {});
mockEventBus.getAllSubscriptions.mockReturnValue([]);
mockDataProcessor.bufferToHex.mockReturnValue('48656c6c6f');
mockDataProcessor.bufferToText.mockReturnValue('Hello');
mockWriteMutex.acquire.mockResolvedValue({ id: 'lock-1', port: 'COM1', acquiredAt: Date.now(), requester: 'test', timeoutMs: 5000 });
mockWriteMutex.release.mockImplementation(() => {});
mockWriteMutex.getLockInfo.mockReturnValue({ isLocked: false, waitQueueSize: 0, maxWaitTime: 0, averageWaitTime: 0 });
mockWriteMutex.forceRelease.mockImplementation(() => {});
mockWriteMutex.dispose.mockImplementation(() => {});
mockPortSession.createSession.mockReturnValue({ sessionId: 'session-1', port: 'COM1', createdAt: Date.now(), buffer: Buffer.alloc(0), urcBuffer: [] });
mockPortSession.endSession.mockImplementation(() => {});
mockPortSession.getActiveSession.mockReturnValue(null);
mockPortSession.getSessionData.mockReturnValue(Buffer.alloc(0));
mockPortSession.getSessionURC.mockReturnValue([]);
mockPortSession.setURCDetector.mockImplementation(() => {});
mockPortSession.getActiveSessionCount.mockReturnValue(0);
mockPortSession.getAllSessionStats.mockReturnValue([]);
mockPortSession.endPortSessions.mockImplementation(() => {});
mockPortSession.getSessionStats.mockReturnValue({});
mockPortSession.dispose.mockImplementation(() => {});
mockURCDetector.loadConfig.mockResolvedValue(undefined);
mockURCDetector.extract.mockReturnValue({ urc: [], data: Buffer.from('response') });
mockURCDetector.getPatternStats.mockReturnValue({ totalPatterns: 0, patternsByModule: {} });
mockURCDetector.setEnabled.mockImplementation(() => {});
mockURCDetector.dispose.mockImplementation(() => {});
mockPlatformAdapter.normalizePortPath.mockImplementation(path => path);
mockPlatformAdapter.validatePortPath.mockReturnValue(true);
mockPlatformAdapter.getPortDetails.mockResolvedValue({
path: 'COM1',
manufacturer: 'Test',
serialNumber: '12345',
deviceType: 'COM Port',
friendlyName: 'Test COM Port'
});
jest.clearAllMocks();
// 重新设置默认mock行为(clearAllMocks会清除mock设置)
mockEventBus.subscribe.mockReturnValue('subscription-id');
mockEventBus.publish.mockImplementation((type, event) => {});
mockEventBus.unsubscribe.mockImplementation(() => {});
mockEventBus.clear.mockImplementation(() => {});
mockEventBus.getAllSubscriptions.mockReturnValue([]);
mockDataProcessor.bufferToHex.mockReturnValue('48656c6c6f');
mockDataProcessor.bufferToText.mockReturnValue('Hello');
mockWriteMutex.acquire.mockResolvedValue({ id: 'lock-1', port: 'COM1', acquiredAt: Date.now(), requester: 'test', timeoutMs: 5000 });
mockWriteMutex.release.mockImplementation(() => {});
mockWriteMutex.getLockInfo.mockReturnValue({ isLocked: false, waitQueueSize: 0, maxWaitTime: 0, averageWaitTime: 0 });
mockWriteMutex.forceRelease.mockImplementation(() => {});
mockWriteMutex.dispose.mockImplementation(() => {});
mockPortSession.createSession.mockReturnValue({ sessionId: 'session-1', port: 'COM1', createdAt: Date.now(), buffer: Buffer.alloc(0), urcBuffer: [] });
mockPortSession.endSession.mockImplementation(() => {});
mockPortSession.getActiveSession.mockReturnValue(null);
mockPortSession.getSessionData.mockReturnValue(Buffer.alloc(0));
mockPortSession.getSessionURC.mockReturnValue([]);
mockPortSession.setURCDetector.mockImplementation(() => {});
mockPortSession.getActiveSessionCount.mockReturnValue(0);
mockPortSession.getAllSessionStats.mockReturnValue([]);
mockPortSession.endPortSessions.mockImplementation(() => {});
mockPortSession.getSessionStats.mockReturnValue({});
mockPortSession.dispose.mockImplementation(() => {});
mockURCDetector.loadConfig.mockResolvedValue(undefined);
mockURCDetector.extract.mockReturnValue({ urc: [], data: Buffer.from('response') });
mockURCDetector.getPatternStats.mockReturnValue({ totalPatterns: 0, patternsByModule: {} });
mockURCDetector.setEnabled.mockImplementation(() => {});
mockURCDetector.dispose.mockImplementation(() => {});
mockPlatformAdapter.normalizePortPath.mockImplementation(path => path);
mockPlatformAdapter.validatePortPath.mockReturnValue(true);
mockPlatformAdapter.getPortDetails.mockResolvedValue({
path: 'COM1',
manufacturer: 'Test',
serialNumber: '12345',
deviceType: 'COM Port',
friendlyName: 'Test COM Port'
});
// 创建引擎实例
engine = new SerialEngine(
mockEventBus,
mockDataProcessor,
mockWriteMutex,
mockPortSession,
mockURCDetector,
mockPlatformAdapter,
{ maxPorts: 10 }
);
});
afterEach(async () => {
if (engine) {
await engine.dispose();
}
});
describe('constructor', () => {
it('should initialize with default dependencies', () => {
const defaultEngine = new SerialEngine();
expect(defaultEngine).toBeDefined();
});
it('should initialize with custom dependencies', () => {
expect(engine).toBeDefined();
});
it('should set URC detector in port session', () => {
// The setURCDetector is called in constructor, so it should have been called
expect(mockPortSession.setURCDetector).toHaveBeenCalledWith(mockURCDetector);
});
});
describe('initialize', () => {
it('should initialize successfully', async () => {
await engine.initialize();
expect(mockURCDetector.loadConfig).toHaveBeenCalled();
expect(engine['isInitialized']).toBe(true);
});
it('should not initialize twice', async () => {
await engine.initialize();
await engine.initialize(); // Second call
// Should not call loadConfig again
expect(mockURCDetector.loadConfig).toHaveBeenCalledTimes(1);
});
it('should handle URC config loading failure', async () => {
mockURCDetector.loadConfig.mockRejectedValue(new Error('Config not found'));
await engine.initialize();
// Should still be initialized but with URC disabled
expect(engine['config'].enableURCDetection).toBe(false);
});
});
describe('openPort', () => {
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
it('should open port successfully', async () => {
// Mock SerialPortAdapter constructor
const mockAdapter = {
open: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
write: jest.fn().mockResolvedValue(undefined),
drain: jest.fn().mockResolvedValue(undefined),
getSignals: jest.fn().mockResolvedValue({ cts: true }),
getStats: jest.fn().mockReturnValue({}),
isOpenPort: jest.fn().mockReturnValue(true),
destroy: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
off: jest.fn()
} as any;
// Mock the SerialPortAdapter constructor
(SerialPortAdapter as jest.MockedClass<typeof SerialPortAdapter>).mockImplementation(() => mockAdapter);
const result = await engine.openPort(validConfig);
expect(result.path).toBe('COM1');
expect(result.manufacturer).toBe('Test');
expect(mockAdapter.open).toHaveBeenCalledWith(validConfig);
expect(mockEventBus.publish).toHaveBeenCalledWith(
EventType.PORT_OPENED,
expect.objectContaining({
type: EventType.PORT_OPENED,
port: 'COM1'
})
);
});
it('should throw error if port limit reached', async () => {
// Mock SerialPortAdapter
const mockAdapter = {
open: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
isOpenPort: jest.fn().mockReturnValue(true),
destroy: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
off: jest.fn()
} as any;
(SerialPortAdapter as jest.MockedClass<typeof SerialPortAdapter>).mockImplementation(() => mockAdapter);
// Fill up to max ports
for (let i = 0; i < 10; i++) {
mockPlatformAdapter.validatePortPath.mockReturnValue(true);
mockPlatformAdapter.getPortDetails.mockResolvedValue({
path: `COM${i}`,
manufacturer: 'Test',
serialNumber: '12345',
deviceType: 'COM Port',
friendlyName: `Test COM${i}`
});
// Open each port
await engine.openPort({ ...validConfig, port: `COM${i}` });
}
// Try to open one more
await expect(engine.openPort({ ...validConfig, port: 'COM11' }))
.rejects.toThrow(SerialError);
});
it('should throw error if port already open', async () => {
const mockAdapter = {
open: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
isOpenPort: jest.fn().mockReturnValue(true),
destroy: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
off: jest.fn()
} as any;
(SerialPortAdapter as jest.MockedClass<typeof SerialPortAdapter>).mockImplementation(() => mockAdapter);
await engine.openPort(validConfig);
await expect(engine.openPort(validConfig)).rejects.toThrow(SerialError);
});
it('should validate port path', async () => {
mockPlatformAdapter.validatePortPath.mockReturnValue(false);
await expect(engine.openPort(validConfig)).rejects.toThrow(SerialError);
});
it('should cleanup on open failure', async () => {
const mockAdapter = {
open: jest.fn().mockRejectedValue(new Error('Open failed')),
close: jest.fn().mockResolvedValue(undefined),
isOpenPort: jest.fn().mockReturnValue(false),
destroy: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
off: jest.fn()
} as any;
(SerialPortAdapter as jest.MockedClass<typeof SerialPortAdapter>).mockImplementation(() => mockAdapter);
try {
await engine.openPort(validConfig);
fail('Should have thrown an error');
} catch (error) {
// Expected to fail
expect(error).toBeInstanceOf(Error);
}
// The adapter should not be cleaned up because it was never added to the ports map
// The cleanup only happens if the port was successfully added to the map
expect(mockAdapter.destroy).not.toHaveBeenCalled();
});
});
describe('writeAndRead', () => {
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
const testData = Buffer.from('test');
const sessionId = 'session-1';
beforeEach(async () => {
const mockAdapter = {
open: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
write: jest.fn().mockResolvedValue(undefined),
drain: jest.fn().mockResolvedValue(undefined),
get: jest.fn().mockResolvedValue({}),
isOpenPort: jest.fn().mockReturnValue(true),
destroy: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
off: jest.fn()
} as any;
(SerialPortAdapter as jest.MockedClass<typeof SerialPortAdapter>).mockImplementation(() => mockAdapter);
await engine.openPort(validConfig);
});
it('should write and read successfully', async () => {
// Mock the private collectResponseData method to return data immediately
jest.spyOn(engine as any, 'collectResponseData').mockResolvedValue(Buffer.from('response'));
const result = await engine.writeAndRead('COM1', testData, 5000, sessionId);
expect(result.status).toBe('ok');
expect(result.sessionId).toBe(sessionId);
expect(mockWriteMutex.acquire).toHaveBeenCalled();
expect(mockWriteMutex.release).toHaveBeenCalled();
expect(mockPortSession.createSession).toHaveBeenCalled();
expect(mockPortSession.endSession).toHaveBeenCalled();
});
it('should handle URC detection', async () => {
mockURCDetector.extract.mockReturnValue({
urc: ['+CSQ: 23,99'],
data: Buffer.from('OK')
});
// Mock the private collectResponseData method to return data immediately
jest.spyOn(engine as any, 'collectResponseData').mockResolvedValue(Buffer.from('response'));
const result = await engine.writeAndRead('COM1', testData, 5000, sessionId);
expect(result.urcDetected).toBe(true);
expect(result.filteredUrc).toEqual(['+CSQ: 23,99']);
expect(mockEventBus.publish).toHaveBeenCalledWith(
EventType.SERIAL_URC,
expect.objectContaining({
type: EventType.SERIAL_URC,
data: '+CSQ: 23,99'
})
);
});
it('should work without session isolation', async () => {
engine['config'].enableSessionIsolation = false;
// Mock the event bus to simulate data arrival
let dataHandler: any;
mockEventBus.subscribe.mockImplementation((eventType, handler) => {
if (eventType === EventType.SERIAL_DATA) {
dataHandler = handler;
}
return 'subscription-id';
});
// Simulate data arrival after a short delay
setTimeout(() => {
if (dataHandler) {
dataHandler({
id: 'test-data',
timestamp: new Date().toISOString(),
source: 'SerialEngine',
type: EventType.SERIAL_DATA,
port: 'COM1',
data: Buffer.from('response')
});
}
}, 10);
const result = await engine.writeAndRead('COM1', testData, 5000, sessionId);
expect(result.status).toBe('ok');
expect(mockPortSession.createSession).not.toHaveBeenCalled();
});
it('should work without write mutex', async () => {
engine['config'].enableWriteMutex = false;
// Mock the private collectResponseData method to return data immediately
jest.spyOn(engine as any, 'collectResponseData').mockResolvedValue(Buffer.from('response'));
const result = await engine.writeAndRead('COM1', testData, 5000, sessionId);
expect(result.status).toBe('ok');
expect(mockWriteMutex.acquire).not.toHaveBeenCalled();
});
it('should handle timeout', async () => {
// Mock the event bus but don't simulate data arrival to cause timeout
mockEventBus.subscribe.mockReturnValue('subscription-id');
await expect(engine.writeAndRead('COM1', testData, 100, sessionId))
.rejects.toThrow(SerialError);
});
it('should cleanup on error', async () => {
mockWriteMutex.acquire.mockRejectedValue(new Error('Lock failed'));
try {
await engine.writeAndRead('COM1', testData, 5000, sessionId);
fail('Should have thrown an error');
} catch (error) {
// Expected to fail
expect(error).toBeInstanceOf(Error);
}
// Since the lock acquisition failed, session was never created, so endSession won't be called
// But we can verify that the error was handled properly
expect(mockWriteMutex.acquire).toHaveBeenCalled();
});
});
describe('closePort', () => {
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
beforeEach(async () => {
const mockAdapter = {
open: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
isOpenPort: jest.fn().mockReturnValue(true),
destroy: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
off: jest.fn()
} as any;
(SerialPortAdapter as jest.MockedClass<typeof SerialPortAdapter>).mockImplementation(() => mockAdapter);
await engine.openPort(validConfig);
});
it('should close port successfully', async () => {
await engine.closePort('COM1');
expect(mockEventBus.publish).toHaveBeenCalledWith(
EventType.PORT_CLOSED,
expect.objectContaining({
type: EventType.PORT_CLOSED,
port: 'COM1',
reason: 'user_request'
})
);
});
it('should throw error if port not open', async () => {
await expect(engine.closePort('COM2')).rejects.toThrow(SerialError);
});
});
describe('getEngineStats', () => {
it('should return engine statistics', () => {
const stats = engine.getEngineStats();
expect(stats).toHaveProperty('isInitialized');
expect(stats).toHaveProperty('totalPorts');
expect(stats).toHaveProperty('activePorts');
expect(stats).toHaveProperty('config');
expect(stats).toHaveProperty('components');
});
});
describe('resetPort', () => {
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
let mockAdapter: any;
beforeEach(async () => {
mockAdapter = {
open: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
flush: jest.fn().mockResolvedValue(undefined),
isOpenPort: jest.fn().mockReturnValue(true),
destroy: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
off: jest.fn(),
getStats: jest.fn().mockReturnValue({})
} as any;
(SerialPortAdapter as jest.MockedClass<typeof SerialPortAdapter>).mockImplementation(() => mockAdapter);
await engine.openPort(validConfig);
});
it('should reset port successfully', async () => {
await engine.resetPort('COM1');
expect(mockPortSession.endPortSessions).toHaveBeenCalledWith('COM1');
expect(mockWriteMutex.forceRelease).toHaveBeenCalledWith('COM1');
expect(mockAdapter.flush).toHaveBeenCalled();
});
it('should throw error if port not open', async () => {
await expect(engine.resetPort('COM2')).rejects.toThrow(SerialError);
});
});
describe('batchOperation', () => {
it('should perform batch close', async () => {
const ports = ['COM1', 'COM2'];
const validConfig = {
port: 'COM1',
baudrate: 9600,
dataBits: 8 as const,
parity: 'none' as const,
stopBits: 1 as const,
flowControl: 'none' as const
};
const mockAdapter = {
open: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
isOpenPort: jest.fn().mockReturnValue(true),
destroy: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
off: jest.fn()
} as any;
(SerialPortAdapter as jest.MockedClass<typeof SerialPortAdapter>).mockImplementation(() => mockAdapter);
// Open ports first
await engine.openPort({ ...validConfig, port: 'COM1' });
await engine.openPort({ ...validConfig, port: 'COM2' });
const results = await engine.batchOperation(ports, 'close');
expect(results['COM1']).toEqual({ success: true });
expect(results['COM2']).toEqual({ success: true });
});
});
describe('dispose', () => {
it('should dispose engine successfully', async () => {
await engine.initialize();
await engine.dispose();
expect(mockURCDetector.dispose).toHaveBeenCalled();
expect(mockPortSession.dispose).toHaveBeenCalled();
expect(mockWriteMutex.dispose).toHaveBeenCalled();
expect(mockEventBus.clear).toHaveBeenCalled();
expect(engine['isInitialized']).toBe(false);
});
});
});