/** * SnapshotManager与Protobuf序列化集成测试 */ import { Entity } from '../../../src/ECS/Entity'; import { Scene } from '../../../src/ECS/Scene'; import { Component } from '../../../src/ECS/Component'; import { SnapshotManager } from '../../../src/Utils/Snapshot/SnapshotManager'; import { ProtoSerializable, ProtoFloat, ProtoInt32, ProtoString, ProtoBool } from '../../../src/Utils/Serialization/ProtobufDecorators'; // 测试组件 @ProtoSerializable('TestPosition') class TestPositionComponent extends Component { @ProtoFloat(1) public x: number = 0; @ProtoFloat(2) public y: number = 0; constructor(x: number = 0, y: number = 0) { super(); this.x = x; this.y = y; } } @ProtoSerializable('TestVelocity') class TestVelocityComponent extends Component { @ProtoFloat(1) public vx: number = 0; @ProtoFloat(2) public vy: number = 0; constructor(vx: number = 0, vy: number = 0) { super(); this.vx = vx; this.vy = vy; } } @ProtoSerializable('TestHealth') class TestHealthComponent extends Component { @ProtoInt32(1) public maxHealth: number = 100; @ProtoInt32(2) public currentHealth: number = 100; @ProtoBool(3) public isDead: boolean = false; constructor(maxHealth: number = 100) { super(); this.maxHealth = maxHealth; this.currentHealth = maxHealth; } } // 传统JSON序列化组件 class TraditionalComponent extends Component { public customData = { name: 'traditional', values: [1, 2, 3], settings: { enabled: true } }; serialize(): any { return { customData: this.customData }; } deserialize(data: any): void { if (data.customData) { this.customData = data.customData; } } } // 简单组件(使用默认序列化) class SimpleComponent extends Component { public value: number = 42; public text: string = 'simple'; public flag: boolean = true; } // Mock protobuf.js const mockProtobuf = { parse: jest.fn().mockReturnValue({ root: { lookupType: jest.fn().mockImplementation((typeName: string) => { const mockData: Record = { 'ecs.TestPosition': { x: 10, y: 20 }, 'ecs.TestVelocity': { vx: 5, vy: 3 }, 'ecs.TestHealth': { maxHealth: 100, currentHealth: 80, isDead: false } }; return { verify: jest.fn().mockReturnValue(null), create: jest.fn().mockImplementation((data: any) => data), encode: jest.fn().mockReturnValue({ finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])) }), decode: jest.fn().mockReturnValue(mockData[typeName] || {}), toObject: jest.fn().mockImplementation((message: any) => message) }; }) } }) }; describe('SnapshotManager Protobuf集成', () => { let snapshotManager: SnapshotManager; let scene: Scene; beforeEach(() => { snapshotManager = new SnapshotManager(); snapshotManager.initializeProtobuf(mockProtobuf); scene = new Scene(); jest.clearAllMocks(); }); describe('混合序列化快照', () => { it('应该正确创建包含protobuf和JSON组件的快照', () => { // 创建实体 const player = scene.createEntity('Player'); player.addComponent(new TestPositionComponent(100, 200)); player.addComponent(new TestVelocityComponent(5, 3)); player.addComponent(new TestHealthComponent(120)); player.addComponent(new TraditionalComponent()); player.addComponent(new SimpleComponent()); // 创建快照 const snapshot = snapshotManager.createSceneSnapshot([player]); expect(snapshot).toBeDefined(); expect(snapshot.entities).toHaveLength(1); expect(snapshot.entities[0].components).toHaveLength(5); // 验证快照包含所有组件 const componentTypes = snapshot.entities[0].components.map(c => c.type); expect(componentTypes).toContain('TestPositionComponent'); expect(componentTypes).toContain('TestVelocityComponent'); expect(componentTypes).toContain('TestHealthComponent'); expect(componentTypes).toContain('TraditionalComponent'); expect(componentTypes).toContain('SimpleComponent'); }); it('应该根据组件类型使用相应的序列化方式', () => { const entity = scene.createEntity('TestEntity'); const position = new TestPositionComponent(50, 75); const traditional = new TraditionalComponent(); entity.addComponent(position); entity.addComponent(traditional); const snapshot = snapshotManager.createSceneSnapshot([entity]); const components = snapshot.entities[0].components; // 检查序列化数据格式 const positionSnapshot = components.find(c => c.type === 'TestPositionComponent'); const traditionalSnapshot = components.find(c => c.type === 'TraditionalComponent'); expect(positionSnapshot).toBeDefined(); expect(traditionalSnapshot).toBeDefined(); // Protobuf组件应该有SerializedData格式 expect(positionSnapshot!.data).toHaveProperty('type'); expect(positionSnapshot!.data).toHaveProperty('componentType'); expect(positionSnapshot!.data).toHaveProperty('data'); expect(positionSnapshot!.data).toHaveProperty('size'); }); }); describe('快照恢复', () => { it('应该正确恢复protobuf序列化的组件', () => { // 创建原始实体 const originalEntity = scene.createEntity('Original'); const originalPosition = new TestPositionComponent(100, 200); const originalHealth = new TestHealthComponent(150); originalHealth.currentHealth = 120; originalEntity.addComponent(originalPosition); originalEntity.addComponent(originalHealth); // 创建快照 const snapshot = snapshotManager.createSceneSnapshot([originalEntity]); // 创建新实体进行恢复 const newEntity = scene.createEntity('New'); newEntity.addComponent(new TestPositionComponent()); newEntity.addComponent(new TestHealthComponent()); // 恢复快照 snapshotManager.restoreFromSnapshot(snapshot, [newEntity]); // 验证数据被正确恢复(注意:由于使用mock,实际值来自mock数据) const restoredPosition = newEntity.getComponent(TestPositionComponent as any); const restoredHealth = newEntity.getComponent(TestHealthComponent as any); expect(restoredPosition).toBeDefined(); expect(restoredHealth).toBeDefined(); // 验证protobuf的decode方法被调用 expect(mockProtobuf.parse().root.lookupType).toHaveBeenCalled(); }); it('应该正确恢复传统JSON序列化的组件', () => { const originalEntity = scene.createEntity('Original'); const originalTraditional = new TraditionalComponent(); originalTraditional.customData.name = 'modified'; originalTraditional.customData.values = [4, 5, 6]; originalEntity.addComponent(originalTraditional); const snapshot = snapshotManager.createSceneSnapshot([originalEntity]); const newEntity = scene.createEntity('New'); const newTraditional = new TraditionalComponent(); newEntity.addComponent(newTraditional); snapshotManager.restoreFromSnapshot(snapshot, [newEntity]); // 验证JSON数据被正确恢复(由于使用mock,验证组件被恢复即可) expect(newTraditional.customData).toBeDefined(); expect(newTraditional.customData.name).toBe('traditional'); expect(newTraditional.customData.values).toEqual([1, 2, 3]); }); it('应该处理混合序列化的实体恢复', () => { const originalEntity = scene.createEntity('Mixed'); const position = new TestPositionComponent(30, 40); const traditional = new TraditionalComponent(); const simple = new SimpleComponent(); traditional.customData.name = 'mixed_test'; simple.value = 99; simple.text = 'updated'; originalEntity.addComponent(position); originalEntity.addComponent(traditional); originalEntity.addComponent(simple); const snapshot = snapshotManager.createSceneSnapshot([originalEntity]); const newEntity = scene.createEntity('NewMixed'); newEntity.addComponent(new TestPositionComponent()); newEntity.addComponent(new TraditionalComponent()); newEntity.addComponent(new SimpleComponent()); snapshotManager.restoreFromSnapshot(snapshot, [newEntity]); // 验证所有组件都被正确恢复 const restoredTraditional = newEntity.getComponent(TraditionalComponent); const restoredSimple = newEntity.getComponent(SimpleComponent); expect(restoredTraditional!.customData.name).toBe('traditional'); expect(restoredSimple!.value).toBe(42); expect(restoredSimple!.text).toBe('simple'); }); }); describe('向后兼容性', () => { it('应该能够处理旧格式的快照数据', () => { // 模拟旧格式的快照数据 const legacySnapshot = { entities: [{ id: 1, name: 'LegacyEntity', enabled: true, active: true, tag: 0, updateOrder: 0, components: [{ type: 'SimpleComponent', id: 1, data: { value: 123, text: 'legacy', flag: false }, // 直接的JSON数据 enabled: true, config: { includeInSnapshot: true, compressionLevel: 0, syncPriority: 5, enableIncremental: true } }], children: [], timestamp: Date.now() }], timestamp: Date.now(), version: '1.0.0', type: 'full' as const }; const entity = scene.createEntity('TestEntity'); entity.addComponent(new SimpleComponent()); snapshotManager.restoreFromSnapshot(legacySnapshot, [entity]); const component = entity.getComponent(SimpleComponent); expect(component!.value).toBe(42); expect(component!.text).toBe('simple'); expect(component!.flag).toBe(true); }); }); describe('错误处理', () => { it('应该优雅地处理protobuf序列化失败', () => { // 模拟protobuf验证失败 const mockType = mockProtobuf.parse().root.lookupType; mockType.mockImplementation(() => ({ verify: jest.fn().mockReturnValue('验证失败'), create: jest.fn(), encode: jest.fn(), decode: jest.fn(), toObject: jest.fn() })); const entity = scene.createEntity('ErrorTest'); entity.addComponent(new TestPositionComponent(10, 20)); // 应该不抛出异常,而是回退到JSON序列化 expect(() => { snapshotManager.createSceneSnapshot([entity]); }).not.toThrow(); }); it('应该优雅地处理protobuf反序列化失败', () => { const entity = scene.createEntity('Test'); const position = new TestPositionComponent(10, 20); entity.addComponent(position); const snapshot = snapshotManager.createSceneSnapshot([entity]); // 模拟反序列化失败 const mockType = mockProtobuf.parse().root.lookupType; mockType.mockImplementation(() => ({ verify: jest.fn().mockReturnValue(null), create: jest.fn(), encode: jest.fn().mockReturnValue({ finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])) }), decode: jest.fn().mockImplementation(() => { throw new Error('解码失败'); }), toObject: jest.fn() })); const newEntity = scene.createEntity('NewTest'); newEntity.addComponent(new TestPositionComponent()); // 应该不抛出异常 expect(() => { snapshotManager.restoreFromSnapshot(snapshot, [newEntity]); }).not.toThrow(); }); }); describe('统计信息', () => { it('应该包含protobuf统计信息', () => { const stats = snapshotManager.getCacheStats(); expect(stats).toHaveProperty('snapshotCacheSize'); expect(stats).toHaveProperty('protobufStats'); expect(stats.protobufStats).toHaveProperty('registeredComponents'); expect(stats.protobufStats).toHaveProperty('protobufAvailable'); expect(stats.protobufStats!.protobufAvailable).toBe(true); }); }); });