370 lines
14 KiB
TypeScript
370 lines
14 KiB
TypeScript
/**
|
||
* 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 = {
|
||
'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) => 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) => 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);
|
||
const restoredHealth = newEntity.getComponent(TestHealthComponent);
|
||
|
||
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数据被正确恢复
|
||
expect(newTraditional.customData.name).toBe('modified');
|
||
expect(newTraditional.customData.values).toEqual([4, 5, 6]);
|
||
});
|
||
|
||
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('mixed_test');
|
||
expect(restoredSimple!.value).toBe(99);
|
||
expect(restoredSimple!.text).toBe('updated');
|
||
});
|
||
});
|
||
|
||
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(123);
|
||
expect(component!.text).toBe('legacy');
|
||
expect(component!.flag).toBe(false);
|
||
});
|
||
});
|
||
|
||
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);
|
||
});
|
||
});
|
||
}); |