Files
esengine/packages/core/tests/ECS/Serialization/IncrementalSerialization.test.ts

752 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 增量序列化系统测试
*/
import { Component } from '../../../src/ECS/Component';
import { Scene } from '../../../src/ECS/Scene';
import { Entity } from '../../../src/ECS/Entity';
import {
Serializable,
Serialize,
IncrementalSerializer,
ChangeOperation
} from '../../../src/ECS/Serialization';
import { ECSComponent } from '../../../src/ECS/Decorators';
import { ComponentRegistry } from '../../../src/ECS/Core/ComponentStorage';
// 测试组件定义
@ECSComponent('IncTest_Position')
@Serializable({ version: 1 })
class PositionComponent extends Component {
@Serialize()
public x: number = 0;
@Serialize()
public y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('IncTest_Velocity')
@Serializable({ version: 1 })
class VelocityComponent extends Component {
@Serialize()
public dx: number = 0;
@Serialize()
public dy: number = 0;
}
@ECSComponent('IncTest_Health')
@Serializable({ version: 1 })
class HealthComponent extends Component {
@Serialize()
public current: number = 100;
@Serialize()
public max: number = 100;
}
describe('Incremental Serialization System', () => {
let scene: Scene;
beforeEach(() => {
IncrementalSerializer.resetVersion();
ComponentRegistry.reset();
// 重新注册测试组件
ComponentRegistry.register(PositionComponent);
ComponentRegistry.register(VelocityComponent);
ComponentRegistry.register(HealthComponent);
scene = new Scene({ name: 'IncrementalTestScene' });
});
afterEach(() => {
scene.end();
});
describe('Scene Snapshot', () => {
it('应该创建场景快照', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new VelocityComponent());
scene.createIncrementalSnapshot();
expect(scene.hasIncrementalSnapshot()).toBe(true);
});
it('应该在快照中包含所有实体', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new VelocityComponent());
const snapshot = IncrementalSerializer.createSnapshot(scene);
expect(snapshot.entityIds.size).toBe(2);
expect(snapshot.entityIds.has(entity1.id)).toBe(true);
expect(snapshot.entityIds.has(entity2.id)).toBe(true);
});
it('应该在快照中包含实体基本信息', () => {
const entity = scene.createEntity('TestEntity');
entity.tag = 42;
entity.active = false;
entity.enabled = false;
entity.updateOrder = 5;
const snapshot = IncrementalSerializer.createSnapshot(scene);
const entityData = snapshot.entities.get(entity.id);
expect(entityData).toBeDefined();
expect(entityData!.name).toBe('TestEntity');
expect(entityData!.tag).toBe(42);
expect(entityData!.active).toBe(false);
expect(entityData!.enabled).toBe(false);
expect(entityData!.updateOrder).toBe(5);
});
it('应该在快照中包含组件数据', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(100, 200));
const snapshot = IncrementalSerializer.createSnapshot(scene, {
deepComponentComparison: true
});
const components = snapshot.components.get(entity.id);
expect(components).toBeDefined();
expect(components!.has('IncTest_Position')).toBe(true);
});
});
describe('Entity Changes Detection', () => {
it('应该检测新增的实体', () => {
scene.createIncrementalSnapshot();
const newEntity = scene.createEntity('NewEntity');
newEntity.addComponent(new PositionComponent(50, 100));
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(1);
expect(incremental.entityChanges[0].operation).toBe(ChangeOperation.EntityAdded);
expect(incremental.entityChanges[0].entityId).toBe(newEntity.id);
expect(incremental.entityChanges[0].entityName).toBe('NewEntity');
});
it('应该检测删除的实体', () => {
const entity = scene.createEntity('ToDelete');
scene.createIncrementalSnapshot();
entity.destroy();
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(1);
expect(incremental.entityChanges[0].operation).toBe(ChangeOperation.EntityRemoved);
expect(incremental.entityChanges[0].entityId).toBe(entity.id);
});
it('应该检测实体属性变更', () => {
const entity = scene.createEntity('Entity');
scene.createIncrementalSnapshot();
entity.name = 'UpdatedName';
entity.tag = 99;
entity.active = false;
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(1);
expect(incremental.entityChanges[0].operation).toBe(ChangeOperation.EntityUpdated);
expect(incremental.entityChanges[0].entityData!.name).toBe('UpdatedName');
expect(incremental.entityChanges[0].entityData!.tag).toBe(99);
expect(incremental.entityChanges[0].entityData!.active).toBe(false);
});
});
describe('Component Changes Detection', () => {
it('应该检测新增的组件', () => {
const entity = scene.createEntity('Entity');
scene.createIncrementalSnapshot();
entity.addComponent(new PositionComponent(10, 20));
const incremental = scene.serializeIncremental();
expect(incremental.componentChanges.length).toBe(1);
expect(incremental.componentChanges[0].operation).toBe(ChangeOperation.ComponentAdded);
expect(incremental.componentChanges[0].entityId).toBe(entity.id);
expect(incremental.componentChanges[0].componentType).toBe('IncTest_Position');
});
it('应该检测删除的组件', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(10, 20));
scene.createIncrementalSnapshot();
entity.removeComponentByType(PositionComponent);
const incremental = scene.serializeIncremental();
expect(incremental.componentChanges.length).toBe(1);
expect(incremental.componentChanges[0].operation).toBe(ChangeOperation.ComponentRemoved);
expect(incremental.componentChanges[0].componentType).toBe('IncTest_Position');
});
it('应该检测组件数据变更', () => {
const entity = scene.createEntity('Entity');
const pos = new PositionComponent(10, 20);
entity.addComponent(pos);
scene.createIncrementalSnapshot();
pos.x = 100;
pos.y = 200;
const incremental = scene.serializeIncremental();
expect(incremental.componentChanges.length).toBe(1);
expect(incremental.componentChanges[0].operation).toBe(ChangeOperation.ComponentUpdated);
expect(incremental.componentChanges[0].componentData!.data.x).toBe(100);
expect(incremental.componentChanges[0].componentData!.data.y).toBe(200);
});
it('应该检测多个组件变更', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(10, 20));
scene.createIncrementalSnapshot();
entity.addComponent(new VelocityComponent());
entity.addComponent(new HealthComponent());
entity.removeComponentByType(PositionComponent);
const incremental = scene.serializeIncremental();
expect(incremental.componentChanges.length).toBe(3);
});
});
describe('Scene Data Changes Detection', () => {
it('应该检测新增的场景数据', () => {
scene.createIncrementalSnapshot();
scene.sceneData.set('weather', 'sunny');
scene.sceneData.set('time', 12.5);
const incremental = scene.serializeIncremental();
expect(incremental.sceneDataChanges.length).toBe(2);
});
it('应该检测更新的场景数据', () => {
scene.sceneData.set('weather', 'sunny');
scene.createIncrementalSnapshot();
scene.sceneData.set('weather', 'rainy');
const incremental = scene.serializeIncremental();
expect(incremental.sceneDataChanges.length).toBe(1);
expect(incremental.sceneDataChanges[0].key).toBe('weather');
expect(incremental.sceneDataChanges[0].value).toBe('rainy');
});
it('应该检测删除的场景数据', () => {
scene.sceneData.set('temp', 'value');
scene.createIncrementalSnapshot();
scene.sceneData.delete('temp');
const incremental = scene.serializeIncremental();
expect(incremental.sceneDataChanges.length).toBe(1);
expect(incremental.sceneDataChanges[0].deleted).toBe(true);
});
});
describe('Apply Incremental Changes', () => {
it('应该应用实体添加变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
const newEntity = scene1.createEntity('NewEntity');
newEntity.addComponent(new PositionComponent(50, 100));
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(incremental);
expect(scene2.entities.count).toBe(1);
const entity = scene2.findEntity('NewEntity');
expect(entity).not.toBeNull();
expect(entity!.hasComponent(PositionComponent)).toBe(true);
scene1.end();
scene2.end();
});
it('应该应用实体删除变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity = scene1.createEntity('ToDelete');
scene1.createIncrementalSnapshot();
entity.destroy();
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('ToDelete');
Object.defineProperty(entity2, 'id', { value: entity.id, writable: true });
scene2.applyIncremental(incremental);
expect(scene2.entities.count).toBe(0);
scene1.end();
scene2.end();
});
it('应该应用实体属性更新', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity1 = scene1.createEntity('Entity');
scene1.createIncrementalSnapshot();
entity1.name = 'UpdatedName';
entity1.tag = 42;
entity1.active = false;
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('Entity');
Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true });
scene2.applyIncremental(incremental);
expect(entity2.name).toBe('UpdatedName');
expect(entity2.tag).toBe(42);
expect(entity2.active).toBe(false);
scene1.end();
scene2.end();
});
it('应该应用组件添加变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity1 = scene1.createEntity('Entity');
scene1.createIncrementalSnapshot();
entity1.addComponent(new PositionComponent(100, 200));
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('Entity');
Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true });
scene2.applyIncremental(incremental);
expect(entity2.hasComponent(PositionComponent)).toBe(true);
const pos = entity2.getComponent(PositionComponent);
expect(pos!.x).toBe(100);
expect(pos!.y).toBe(200);
scene1.end();
scene2.end();
});
it('应该应用组件删除变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity1 = scene1.createEntity('Entity');
entity1.addComponent(new PositionComponent(10, 20));
scene1.createIncrementalSnapshot();
entity1.removeComponentByType(PositionComponent);
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('Entity');
Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true });
entity2.addComponent(new PositionComponent(10, 20));
scene2.applyIncremental(incremental);
expect(entity2.hasComponent(PositionComponent)).toBe(false);
scene1.end();
scene2.end();
});
it('应该应用组件数据更新', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity1 = scene1.createEntity('Entity');
const pos1 = new PositionComponent(10, 20);
entity1.addComponent(pos1);
scene1.createIncrementalSnapshot();
pos1.x = 100;
pos1.y = 200;
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('Entity');
Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true });
entity2.addComponent(new PositionComponent(10, 20));
scene2.applyIncremental(incremental);
const pos2 = entity2.getComponent(PositionComponent);
expect(pos2!.x).toBe(100);
expect(pos2!.y).toBe(200);
scene1.end();
scene2.end();
});
it('应该应用场景数据变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
scene1.sceneData.set('weather', 'sunny');
scene1.sceneData.set('time', 12.5);
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(incremental);
expect(scene2.sceneData.get('weather')).toBe('sunny');
expect(scene2.sceneData.get('time')).toBe(12.5);
scene1.end();
scene2.end();
});
});
describe('Incremental Serialization', () => {
it('应该序列化和反序列化增量快照JSON格式', () => {
scene.createIncrementalSnapshot();
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(50, 100));
const incremental = scene.serializeIncremental();
const json = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
expect(typeof json).toBe('string');
const deserialized = IncrementalSerializer.deserializeIncremental(json);
expect(deserialized.version).toBe(incremental.version);
expect(deserialized.entityChanges.length).toBe(incremental.entityChanges.length);
expect(deserialized.componentChanges.length).toBe(incremental.componentChanges.length);
});
it('应该序列化和反序列化增量快照(二进制格式)', () => {
scene.createIncrementalSnapshot();
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(50, 100));
entity.tag = 42;
scene.sceneData.set('weather', 'sunny');
const incremental = scene.serializeIncremental();
const binary = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
expect(binary instanceof Uint8Array).toBe(true);
const deserialized = IncrementalSerializer.deserializeIncremental(binary);
expect(deserialized.version).toBe(incremental.version);
expect(deserialized.sceneName).toBe(incremental.sceneName);
expect(deserialized.entityChanges.length).toBe(incremental.entityChanges.length);
expect(deserialized.componentChanges.length).toBe(incremental.componentChanges.length);
expect(deserialized.sceneDataChanges.length).toBe(incremental.sceneDataChanges.length);
});
it('应该美化JSON输出', () => {
scene.createIncrementalSnapshot();
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(10, 20));
const incremental = scene.serializeIncremental();
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, { format: 'json', pretty: true });
expect(typeof prettyJson).toBe('string');
expect(prettyJson).toContain('\n');
expect(prettyJson).toContain(' ');
});
it('二进制格式应该比JSON格式更小', () => {
const entities = [];
for (let i = 0; i < 50; i++) {
const entity = scene.createEntity(`Entity_${i}`);
entity.addComponent(new PositionComponent(i * 10, i * 20));
entity.addComponent(new VelocityComponent());
entities.push(entity);
}
scene.createIncrementalSnapshot();
for (const entity of entities) {
const pos = entity.getComponent(PositionComponent)!;
pos.x += 100;
pos.y += 200;
}
const incremental = scene.serializeIncremental();
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
const jsonSize = Buffer.byteLength(jsonData as string);
const binarySize = (binaryData as Buffer).length;
expect(binarySize).toBeLessThan(jsonSize);
});
it('二进制和JSON格式应该包含相同的数据', () => {
scene.createIncrementalSnapshot();
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
entity1.addComponent(new VelocityComponent());
entity1.tag = 99;
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new HealthComponent());
scene.sceneData.set('level', 5);
scene.sceneData.set('score', 1000);
const incremental = scene.serializeIncremental();
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
const fromJson = IncrementalSerializer.deserializeIncremental(jsonData);
const fromBinary = IncrementalSerializer.deserializeIncremental(binaryData);
expect(fromJson.version).toBe(fromBinary.version);
expect(fromJson.timestamp).toBe(fromBinary.timestamp);
expect(fromJson.sceneName).toBe(fromBinary.sceneName);
expect(fromJson.entityChanges.length).toBe(fromBinary.entityChanges.length);
expect(fromJson.componentChanges.length).toBe(fromBinary.componentChanges.length);
expect(fromJson.sceneDataChanges.length).toBe(fromBinary.sceneDataChanges.length);
expect(fromJson.entityChanges[0].entityName).toBe(fromBinary.entityChanges[0].entityName);
expect(fromJson.entityChanges[0].entityData?.tag).toBe(fromBinary.entityChanges[0].entityData?.tag);
});
it('应该正确应用二进制格式反序列化的增量快照', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
const entity = scene1.createEntity('TestEntity');
entity.addComponent(new PositionComponent(100, 200));
entity.tag = 77;
const incremental = scene1.serializeIncremental();
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
const deserializedIncremental = IncrementalSerializer.deserializeIncremental(binaryData);
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(deserializedIncremental);
expect(scene2.entities.count).toBe(1);
const restoredEntity = scene2.findEntity('TestEntity');
expect(restoredEntity).not.toBeNull();
expect(restoredEntity!.tag).toBe(77);
expect(restoredEntity!.hasComponent(PositionComponent)).toBe(true);
const pos = restoredEntity!.getComponent(PositionComponent)!;
expect(pos.x).toBe(100);
expect(pos.y).toBe(200);
scene1.end();
scene2.end();
});
it('Scene.applyIncremental应该直接支持二进制Buffer', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
const entity1 = scene1.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
entity1.addComponent(new VelocityComponent());
const entity2 = scene1.createEntity('Entity2');
entity2.addComponent(new HealthComponent());
const incremental = scene1.serializeIncremental();
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(binaryData);
expect(scene2.entities.count).toBe(2);
const e1 = scene2.findEntity('Entity1');
expect(e1).not.toBeNull();
expect(e1!.hasComponent(PositionComponent)).toBe(true);
expect(e1!.hasComponent(VelocityComponent)).toBe(true);
const e2 = scene2.findEntity('Entity2');
expect(e2).not.toBeNull();
expect(e2!.hasComponent(HealthComponent)).toBe(true);
scene1.end();
scene2.end();
});
it('Scene.applyIncremental应该直接支持JSON字符串', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
const entity = scene1.createEntity('TestEntity');
entity.addComponent(new PositionComponent(50, 100));
entity.tag = 99;
const incremental = scene1.serializeIncremental();
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(jsonData);
expect(scene2.entities.count).toBe(1);
const restoredEntity = scene2.findEntity('TestEntity');
expect(restoredEntity).not.toBeNull();
expect(restoredEntity!.tag).toBe(99);
scene1.end();
scene2.end();
});
});
describe('Snapshot Management', () => {
it('应该更新增量快照基准', () => {
const entity = scene.createEntity('Entity');
scene.createIncrementalSnapshot();
entity.addComponent(new PositionComponent(10, 20));
const incremental1 = scene.serializeIncremental();
scene.updateIncrementalSnapshot();
const pos = entity.getComponent(PositionComponent)!;
pos.x = 100;
const incremental2 = scene.serializeIncremental();
// incremental2应该只包含Position的更新不包含添加
expect(incremental1.componentChanges.length).toBe(1);
expect(incremental2.componentChanges.length).toBe(1);
expect(incremental2.componentChanges[0].operation).toBe(ChangeOperation.ComponentUpdated);
});
it('应该清除增量快照', () => {
scene.createIncrementalSnapshot();
expect(scene.hasIncrementalSnapshot()).toBe(true);
scene.clearIncrementalSnapshot();
expect(scene.hasIncrementalSnapshot()).toBe(false);
});
it('应该在没有快照时抛出错误', () => {
expect(() => {
scene.serializeIncremental();
}).toThrow('必须先调用 createIncrementalSnapshot() 创建基础快照');
});
});
describe('Statistics and Utilities', () => {
it('应该提供增量快照统计信息', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
scene.createIncrementalSnapshot();
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new VelocityComponent());
entity1.destroy();
const incremental = scene.serializeIncremental();
const stats = IncrementalSerializer.getIncrementalStats(incremental);
expect(stats.addedEntities).toBe(1);
expect(stats.removedEntities).toBe(1);
expect(stats.addedComponents).toBe(1);
expect(stats.totalChanges).toBeGreaterThan(0);
});
});
describe('Performance and Edge Cases', () => {
it('应该处理大量实体变更', () => {
scene.createIncrementalSnapshot();
for (let i = 0; i < 100; i++) {
const entity = scene.createEntity(`Entity_${i}`);
entity.addComponent(new PositionComponent(i, i * 2));
}
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(100);
expect(incremental.componentChanges.length).toBe(100);
});
it('应该处理空变更', () => {
scene.createIncrementalSnapshot();
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(0);
expect(incremental.componentChanges.length).toBe(0);
expect(incremental.sceneDataChanges.length).toBe(0);
});
it('应该处理复杂嵌套场景数据', () => {
scene.createIncrementalSnapshot();
scene.sceneData.set('config', {
nested: {
deep: {
value: 42
}
},
array: [1, 2, 3]
});
const incremental = scene.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(incremental);
const config = scene2.sceneData.get('config');
expect(config.nested.deep.value).toBe(42);
expect(config.array).toEqual([1, 2, 3]);
scene2.end();
});
it('应该正确处理快照版本号', () => {
IncrementalSerializer.resetVersion();
const snapshot1 = IncrementalSerializer.createSnapshot(scene);
expect(snapshot1.version).toBe(1);
const snapshot2 = IncrementalSerializer.createSnapshot(scene);
expect(snapshot2.version).toBe(2);
});
});
});