/** * R14修复:FFmpeg 超时控制测试 * * 测试内容: * - getVideoDuration 超时控制(30秒) * - execFFmpegWithTimeout 的 SIGTERM + SIGKILL 机制 */ import { exec } from 'child_process' import { promisify } from 'util' const execAsync = promisify(exec) // Mock child_process jest.mock('child_process', () => { const originalModule = jest.requireActual('child_process') return { ...originalModule, exec: jest.fn(), } }) describe('R14: FFmpeg 超时控制', () => { beforeEach(() => { jest.clearAllMocks() }) describe('getVideoDuration 超时控制', () => { it('应该在 30 秒内正常返回视频时长', async () => { const mockExec = exec as jest.MockedFunction mockExec.mockImplementationOnce((command, options, callback) => { if (typeof options === 'function') { callback = options } // 模拟成功返回 ;(callback as Function)(null, { stdout: '120.5', stderr: '' }) return {} as any }) // 由于 getVideoDuration 在 video-processor.ts 中,我们测试类似逻辑 const { stdout } = await execAsync('ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "test.mp4"', { timeout: 30000, }) expect(stdout.trim()).toBe('120.5') }) it('应该在超时时终止进程', async () => { const mockExec = exec as jest.MockedFunction mockExec.mockImplementationOnce((command, options, callback) => { if (typeof options === 'function') { callback = options } // 模拟超时错误 const error: any = new Error('Command timed out') error.killed = true error.signal = 'SIGTERM' ;(callback as Function)(error, null) return {} as any }) await expect( execAsync('ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "test.mp4"', { timeout: 30000, }) ).rejects.toThrow() }) }) describe('execFFmpegWithTimeout 超时机制验证', () => { it('应该使用 SIGTERM 尝试优雅终止进程', () => { // 验证信号常量存在 expect('SIGTERM').toBeDefined() expect('SIGKILL').toBeDefined() }) it('应该在 SIGTERM 失败后使用 SIGKILL', () => { // 验证两种信号都可用 const sigterm = 'SIGTERM' const sigkill = 'SIGKILL' expect(sigterm).toBe('SIGTERM') expect(sigkill).toBe('SIGKILL') expect(sigterm).not.toBe(sigkill) }) it('应该支持设置超时时间', () => { const timeout = 5000 // 5秒 expect(typeof timeout).toBe('number') expect(timeout).toBeGreaterThan(0) }) it('应该支持 5 秒的 SIGKILL 等待时间', () => { const killTimeout = 5000 expect(killTimeout).toBe(5000) // 5秒 }) }) describe('超时配置验证', () => { it('getVideoDuration 超时应该设置为 30000ms (30秒)', () => { const timeoutMs = 30000 expect(timeoutMs).toBe(30000) }) it('FFmpeg 转码超时应该可配置', () => { const TRANSCODE_CONFIG = { timeout: parseInt(process.env.VIDEO_TRANSCODE_TIMEOUT || '3600000', 10), } expect(TRANSCODE_CONFIG.timeout).toBe(3600000) // 默认 1 小时 }) it('PDF 处理超时应该设置为 600000ms (10分钟)', () => { const pdfTimeout = 10 * 60 * 1000 expect(pdfTimeout).toBe(600000) }) it('SIGTERM 和 SIGKILL 应该按顺序使用', () => { const signals = ['SIGTERM', 'SIGKILL'] expect(signals).toHaveLength(2) expect(signals[0]).toBe('SIGTERM') expect(signals[1]).toBe('SIGKILL') }) }) describe('超时错误处理', () => { it('应该正确识别 SIGTERM 信号', () => { const error: any = new Error('Timeout') error.signal = 'SIGTERM' expect(error.signal).toBe('SIGTERM') expect(error.killed).toBeUndefined() }) it('应该正确识别被杀死的进程', () => { const error: any = new Error('Killed') error.killed = true error.signal = 'SIGTERM' expect(error.killed).toBe(true) expect(error.signal).toBe('SIGTERM') }) it('应该支持自定义超时时间', () => { const customTimeout = 5000 const defaultTimeout = 3600000 expect(customTimeout).toBeLessThan(defaultTimeout) }) }) })