新增protobuf依赖(为网络和序列化做准备)
更新readme
This commit is contained in:
441
tests/Utils/Serialization/Performance.test.ts
Normal file
441
tests/Utils/Serialization/Performance.test.ts
Normal file
@@ -0,0 +1,441 @@
|
||||
/**
|
||||
* Protobuf序列化性能测试
|
||||
*/
|
||||
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { Entity } from '../../../src/ECS/Entity';
|
||||
import { Scene } from '../../../src/ECS/Scene';
|
||||
import { SnapshotManager } from '../../../src/Utils/Snapshot/SnapshotManager';
|
||||
import { ProtobufSerializer } from '../../../src/Utils/Serialization/ProtobufSerializer';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool
|
||||
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||
|
||||
// 性能测试组件
|
||||
@ProtoSerializable('PerfPosition')
|
||||
class PerfPositionComponent extends Component {
|
||||
@ProtoFloat(1) public x: number = 0;
|
||||
@ProtoFloat(2) public y: number = 0;
|
||||
@ProtoFloat(3) public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('PerfVelocity')
|
||||
class PerfVelocityComponent extends Component {
|
||||
@ProtoFloat(1) public vx: number = 0;
|
||||
@ProtoFloat(2) public vy: number = 0;
|
||||
@ProtoFloat(3) public vz: number = 0;
|
||||
|
||||
constructor(vx: number = 0, vy: number = 0, vz: number = 0) {
|
||||
super();
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
this.vz = vz;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('PerfHealth')
|
||||
class PerfHealthComponent extends Component {
|
||||
@ProtoInt32(1) public maxHealth: number = 100;
|
||||
@ProtoInt32(2) public currentHealth: number = 100;
|
||||
@ProtoBool(3) public isDead: boolean = false;
|
||||
@ProtoFloat(4) public regenerationRate: number = 0.5;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('PerfPlayer')
|
||||
class PerfPlayerComponent extends Component {
|
||||
@ProtoString(1) public name: string = '';
|
||||
@ProtoInt32(2) public level: number = 1;
|
||||
@ProtoInt32(3) public experience: number = 0;
|
||||
@ProtoInt32(4) public score: number = 0;
|
||||
@ProtoBool(5) public isOnline: boolean = true;
|
||||
|
||||
constructor(name: string = 'Player', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
// 传统JSON序列化组件(用于对比)
|
||||
class JsonPositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
class JsonPlayerComponent extends Component {
|
||||
public name: string = '';
|
||||
public level: number = 1;
|
||||
public experience: number = 0;
|
||||
public score: number = 0;
|
||||
public isOnline: boolean = true;
|
||||
|
||||
constructor(name: string = 'Player', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
// Mock protobuf.js for performance testing
|
||||
const createMockProtobuf = () => {
|
||||
const mockEncodedData = new Uint8Array(32); // 模拟32字节的编码数据
|
||||
mockEncodedData.fill(1);
|
||||
|
||||
return {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
root: {
|
||||
lookupType: jest.fn().mockImplementation((typeName: string) => ({
|
||||
verify: jest.fn().mockReturnValue(null),
|
||||
create: jest.fn().mockImplementation((data) => data),
|
||||
encode: jest.fn().mockReturnValue({
|
||||
finish: jest.fn().mockReturnValue(mockEncodedData)
|
||||
}),
|
||||
decode: jest.fn().mockReturnValue({
|
||||
x: 10, y: 20, z: 30,
|
||||
vx: 1, vy: 2, vz: 3,
|
||||
maxHealth: 100, currentHealth: 80, isDead: false, regenerationRate: 0.5,
|
||||
name: 'TestPlayer', level: 5, experience: 1000, score: 5000, isOnline: true
|
||||
}),
|
||||
toObject: jest.fn().mockImplementation((message) => message)
|
||||
}))
|
||||
}
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
describe('Protobuf序列化性能测试', () => {
|
||||
let protobufSerializer: ProtobufSerializer;
|
||||
let snapshotManager: SnapshotManager;
|
||||
let scene: Scene;
|
||||
|
||||
beforeEach(() => {
|
||||
protobufSerializer = ProtobufSerializer.getInstance();
|
||||
protobufSerializer.initialize(createMockProtobuf());
|
||||
|
||||
snapshotManager = new SnapshotManager();
|
||||
snapshotManager.initializeProtobuf(createMockProtobuf());
|
||||
|
||||
scene = new Scene();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('单组件序列化性能', () => {
|
||||
const iterations = 1000;
|
||||
|
||||
it('应该比较protobuf和JSON序列化速度', () => {
|
||||
const protobufComponents: PerfPositionComponent[] = [];
|
||||
const jsonComponents: JsonPositionComponent[] = [];
|
||||
|
||||
// 准备测试数据
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
protobufComponents.push(new PerfPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
|
||||
jsonComponents.push(new JsonPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
}
|
||||
|
||||
// 测试Protobuf序列化
|
||||
const protobufStartTime = performance.now();
|
||||
let protobufTotalSize = 0;
|
||||
|
||||
for (const component of protobufComponents) {
|
||||
const result = protobufSerializer.serialize(component);
|
||||
protobufTotalSize += result.size;
|
||||
}
|
||||
|
||||
const protobufEndTime = performance.now();
|
||||
const protobufTime = protobufEndTime - protobufStartTime;
|
||||
|
||||
// 测试JSON序列化
|
||||
const jsonStartTime = performance.now();
|
||||
let jsonTotalSize = 0;
|
||||
|
||||
for (const component of jsonComponents) {
|
||||
const jsonString = JSON.stringify({
|
||||
x: component.x,
|
||||
y: component.y,
|
||||
z: component.z
|
||||
});
|
||||
jsonTotalSize += new Blob([jsonString]).size;
|
||||
}
|
||||
|
||||
const jsonEndTime = performance.now();
|
||||
const jsonTime = jsonEndTime - jsonStartTime;
|
||||
|
||||
// 性能断言
|
||||
console.log(`\\n=== 单组件序列化性能对比 (${iterations} 次迭代) ===`);
|
||||
console.log(`Protobuf时间: ${protobufTime.toFixed(2)}ms`);
|
||||
console.log(`JSON时间: ${jsonTime.toFixed(2)}ms`);
|
||||
console.log(`Protobuf总大小: ${protobufTotalSize} bytes`);
|
||||
console.log(`JSON总大小: ${jsonTotalSize} bytes`);
|
||||
|
||||
if (jsonTime > 0) {
|
||||
const speedImprovement = ((jsonTime - protobufTime) / jsonTime * 100);
|
||||
console.log(`速度提升: ${speedImprovement.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
if (jsonTotalSize > 0) {
|
||||
const sizeReduction = ((jsonTotalSize - protobufTotalSize) / jsonTotalSize * 100);
|
||||
console.log(`大小减少: ${sizeReduction.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
// 基本性能验证
|
||||
expect(protobufTime).toBeLessThan(1000); // 不应该超过1秒
|
||||
expect(jsonTime).toBeLessThan(1000);
|
||||
expect(protobufTotalSize).toBeGreaterThan(0);
|
||||
expect(jsonTotalSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该测试复杂组件的序列化性能', () => {
|
||||
const protobufPlayers: PerfPlayerComponent[] = [];
|
||||
const jsonPlayers: JsonPlayerComponent[] = [];
|
||||
|
||||
// 创建测试数据
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
protobufPlayers.push(new PerfPlayerComponent(
|
||||
`Player${i}`,
|
||||
Math.floor(Math.random() * 100) + 1
|
||||
));
|
||||
|
||||
jsonPlayers.push(new JsonPlayerComponent(
|
||||
`Player${i}`,
|
||||
Math.floor(Math.random() * 100) + 1
|
||||
));
|
||||
}
|
||||
|
||||
// Protobuf序列化测试
|
||||
const protobufStart = performance.now();
|
||||
for (const player of protobufPlayers) {
|
||||
protobufSerializer.serialize(player);
|
||||
}
|
||||
const protobufTime = performance.now() - protobufStart;
|
||||
|
||||
// JSON序列化测试
|
||||
const jsonStart = performance.now();
|
||||
for (const player of jsonPlayers) {
|
||||
JSON.stringify({
|
||||
name: player.name,
|
||||
level: player.level,
|
||||
experience: player.experience,
|
||||
score: player.score,
|
||||
isOnline: player.isOnline
|
||||
});
|
||||
}
|
||||
const jsonTime = performance.now() - jsonStart;
|
||||
|
||||
console.log(`\\n=== 复杂组件序列化性能 (${iterations} 次迭代) ===`);
|
||||
console.log(`Protobuf时间: ${protobufTime.toFixed(2)}ms`);
|
||||
console.log(`JSON时间: ${jsonTime.toFixed(2)}ms`);
|
||||
|
||||
expect(protobufTime).toBeLessThan(1000);
|
||||
expect(jsonTime).toBeLessThan(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('批量实体序列化性能', () => {
|
||||
it('应该测试大量实体的快照创建性能', () => {
|
||||
const entityCount = 100;
|
||||
const entities: Entity[] = [];
|
||||
|
||||
// 创建测试实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const entity = scene.createEntity(`Entity${i}`);
|
||||
entity.addComponent(new PerfPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
entity.addComponent(new PerfVelocityComponent(
|
||||
Math.random() * 10 - 5,
|
||||
Math.random() * 10 - 5,
|
||||
Math.random() * 2 - 1
|
||||
));
|
||||
entity.addComponent(new PerfHealthComponent(100 + Math.floor(Math.random() * 50)));
|
||||
entity.addComponent(new PerfPlayerComponent(`Player${i}`, Math.floor(Math.random() * 50) + 1));
|
||||
|
||||
entities.push(entity);
|
||||
}
|
||||
|
||||
// 测试快照创建性能
|
||||
const snapshotStart = performance.now();
|
||||
const snapshot = snapshotManager.createSceneSnapshot(entities);
|
||||
const snapshotTime = performance.now() - snapshotStart;
|
||||
|
||||
console.log(`\\n=== 批量实体序列化性能 ===`);
|
||||
console.log(`实体数量: ${entityCount}`);
|
||||
console.log(`每个实体组件数: 4`);
|
||||
console.log(`总组件数: ${entityCount * 4}`);
|
||||
console.log(`快照创建时间: ${snapshotTime.toFixed(2)}ms`);
|
||||
console.log(`平均每组件时间: ${(snapshotTime / (entityCount * 4)).toFixed(3)}ms`);
|
||||
|
||||
expect(snapshot.entities).toHaveLength(entityCount);
|
||||
expect(snapshotTime).toBeLessThan(5000); // 不应该超过5秒
|
||||
|
||||
// 计算快照大小
|
||||
let totalSnapshotSize = 0;
|
||||
for (const entitySnapshot of snapshot.entities) {
|
||||
for (const componentSnapshot of entitySnapshot.components) {
|
||||
if (componentSnapshot.data && typeof componentSnapshot.data === 'object' && 'size' in componentSnapshot.data) {
|
||||
totalSnapshotSize += (componentSnapshot.data as any).size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`快照总大小: ${totalSnapshotSize} bytes`);
|
||||
console.log(`平均每实体大小: ${(totalSnapshotSize / entityCount).toFixed(1)} bytes`);
|
||||
|
||||
expect(totalSnapshotSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('反序列化性能', () => {
|
||||
it('应该测试快照恢复性能', () => {
|
||||
const entityCount = 50;
|
||||
const originalEntities: Entity[] = [];
|
||||
|
||||
// 创建原始实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const entity = scene.createEntity(`Original${i}`);
|
||||
entity.addComponent(new PerfPositionComponent(i * 10, i * 20, i));
|
||||
entity.addComponent(new PerfHealthComponent(100 + i));
|
||||
originalEntities.push(entity);
|
||||
}
|
||||
|
||||
// 创建快照
|
||||
const snapshotStart = performance.now();
|
||||
const snapshot = snapshotManager.createSceneSnapshot(originalEntities);
|
||||
const snapshotTime = performance.now() - snapshotStart;
|
||||
|
||||
// 创建目标实体
|
||||
const targetEntities: Entity[] = [];
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const entity = scene.createEntity(`Target${i}`);
|
||||
entity.addComponent(new PerfPositionComponent());
|
||||
entity.addComponent(new PerfHealthComponent());
|
||||
targetEntities.push(entity);
|
||||
}
|
||||
|
||||
// 测试恢复性能
|
||||
const restoreStart = performance.now();
|
||||
snapshotManager.restoreFromSnapshot(snapshot, targetEntities);
|
||||
const restoreTime = performance.now() - restoreStart;
|
||||
|
||||
console.log(`\\n=== 反序列化性能测试 ===`);
|
||||
console.log(`实体数量: ${entityCount}`);
|
||||
console.log(`序列化时间: ${snapshotTime.toFixed(2)}ms`);
|
||||
console.log(`反序列化时间: ${restoreTime.toFixed(2)}ms`);
|
||||
console.log(`总往返时间: ${(snapshotTime + restoreTime).toFixed(2)}ms`);
|
||||
console.log(`平均每实体往返时间: ${((snapshotTime + restoreTime) / entityCount).toFixed(3)}ms`);
|
||||
|
||||
expect(restoreTime).toBeLessThan(2000); // 不应该超过2秒
|
||||
expect(snapshotTime + restoreTime).toBeLessThan(3000); // 总时间不超过3秒
|
||||
});
|
||||
});
|
||||
|
||||
describe('内存使用', () => {
|
||||
it('应该监控序列化过程中的内存使用', () => {
|
||||
const entityCount = 200;
|
||||
const entities: Entity[] = [];
|
||||
|
||||
// 创建大量实体
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const entity = scene.createEntity(`MemoryTest${i}`);
|
||||
entity.addComponent(new PerfPositionComponent(
|
||||
Math.random() * 1000,
|
||||
Math.random() * 1000,
|
||||
Math.random() * 100
|
||||
));
|
||||
entity.addComponent(new PerfVelocityComponent(
|
||||
Math.random() * 10,
|
||||
Math.random() * 10,
|
||||
Math.random() * 2
|
||||
));
|
||||
entity.addComponent(new PerfHealthComponent(Math.floor(Math.random() * 200) + 50));
|
||||
entities.push(entity);
|
||||
}
|
||||
|
||||
// 记录初始内存(如果可用)
|
||||
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
||||
|
||||
// 执行序列化
|
||||
const snapshot = snapshotManager.createSceneSnapshot(entities);
|
||||
|
||||
// 记录序列化后内存
|
||||
const afterMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
||||
const memoryIncrease = afterMemory - initialMemory;
|
||||
|
||||
if (initialMemory > 0) {
|
||||
console.log(`\\n=== 内存使用测试 ===`);
|
||||
console.log(`实体数量: ${entityCount}`);
|
||||
console.log(`初始内存: ${(initialMemory / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`序列化后内存: ${(afterMemory / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`内存增加: ${(memoryIncrease / 1024).toFixed(2)} KB`);
|
||||
console.log(`平均每实体内存: ${(memoryIncrease / entityCount).toFixed(1)} bytes`);
|
||||
}
|
||||
|
||||
expect(snapshot.entities).toHaveLength(entityCount);
|
||||
|
||||
// 清理
|
||||
entities.length = 0;
|
||||
});
|
||||
});
|
||||
|
||||
describe('极端情况性能', () => {
|
||||
it('应该处理大量小组件的性能', () => {
|
||||
const componentCount = 5000;
|
||||
const components: PerfPositionComponent[] = [];
|
||||
|
||||
// 创建大量小组件
|
||||
for (let i = 0; i < componentCount; i++) {
|
||||
components.push(new PerfPositionComponent(i, i * 2, i * 3));
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
for (const component of components) {
|
||||
protobufSerializer.serialize(component);
|
||||
}
|
||||
const time = performance.now() - start;
|
||||
|
||||
console.log(`\\n=== 大量小组件性能测试 ===`);
|
||||
console.log(`组件数量: ${componentCount}`);
|
||||
console.log(`总时间: ${time.toFixed(2)}ms`);
|
||||
console.log(`平均每组件: ${(time / componentCount).toFixed(4)}ms`);
|
||||
console.log(`每秒处理: ${Math.floor(componentCount / (time / 1000))} 个组件`);
|
||||
|
||||
expect(time).toBeLessThan(10000); // 不超过10秒
|
||||
expect(time / componentCount).toBeLessThan(2); // 每个组件不超过2ms
|
||||
});
|
||||
});
|
||||
});
|
||||
278
tests/Utils/Serialization/ProtobufDecorators.test.ts
Normal file
278
tests/Utils/Serialization/ProtobufDecorators.test.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* Protobuf装饰器测试
|
||||
*/
|
||||
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoField,
|
||||
ProtoFieldType,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool,
|
||||
ProtobufRegistry,
|
||||
isProtoSerializable,
|
||||
getProtoName
|
||||
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||
|
||||
// 测试组件
|
||||
@ProtoSerializable('TestPosition')
|
||||
class TestPositionComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public x: number = 0;
|
||||
|
||||
@ProtoFloat(2)
|
||||
public y: number = 0;
|
||||
|
||||
@ProtoFloat(3)
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('TestPlayer')
|
||||
class TestPlayerComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public name: string = '';
|
||||
|
||||
@ProtoInt32(2)
|
||||
public level: number = 1;
|
||||
|
||||
@ProtoInt32(3)
|
||||
public health: number = 100;
|
||||
|
||||
@ProtoBool(4)
|
||||
public isAlive: boolean = true;
|
||||
|
||||
constructor(name: string = '', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
// 没有装饰器的组件
|
||||
class PlainComponent extends Component {
|
||||
public data: string = 'test';
|
||||
}
|
||||
|
||||
// 测试字段编号冲突的组件
|
||||
const createConflictingComponent = () => {
|
||||
try {
|
||||
@ProtoSerializable('Conflict')
|
||||
class ConflictComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public x: number = 0;
|
||||
|
||||
@ProtoFloat(1) // 故意使用相同的字段编号
|
||||
public y: number = 0;
|
||||
}
|
||||
return ConflictComponent;
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
describe('ProtobufDecorators', () => {
|
||||
let registry: ProtobufRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
// 获取注册表实例
|
||||
registry = ProtobufRegistry.getInstance();
|
||||
});
|
||||
|
||||
describe('@ProtoSerializable装饰器', () => {
|
||||
it('应该正确标记组件为可序列化', () => {
|
||||
const component = new TestPositionComponent(10, 20, 30);
|
||||
|
||||
expect(isProtoSerializable(component)).toBe(true);
|
||||
expect(getProtoName(component)).toBe('TestPosition');
|
||||
});
|
||||
|
||||
it('应该在注册表中注册组件定义', () => {
|
||||
expect(registry.hasProtoDefinition('TestPosition')).toBe(true);
|
||||
expect(registry.hasProtoDefinition('TestPlayer')).toBe(true);
|
||||
});
|
||||
|
||||
it('应该正确处理没有装饰器的组件', () => {
|
||||
const component = new PlainComponent();
|
||||
|
||||
expect(isProtoSerializable(component)).toBe(false);
|
||||
expect(getProtoName(component)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('@ProtoField装饰器', () => {
|
||||
it('应该正确定义字段', () => {
|
||||
const definition = registry.getComponentDefinition('TestPosition');
|
||||
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition!.fields.size).toBe(3);
|
||||
|
||||
const xField = definition!.fields.get('x');
|
||||
expect(xField).toEqual({
|
||||
fieldNumber: 1,
|
||||
type: ProtoFieldType.FLOAT,
|
||||
repeated: false,
|
||||
optional: false,
|
||||
name: 'x'
|
||||
});
|
||||
|
||||
const yField = definition!.fields.get('y');
|
||||
expect(yField).toEqual({
|
||||
fieldNumber: 2,
|
||||
type: ProtoFieldType.FLOAT,
|
||||
repeated: false,
|
||||
optional: false,
|
||||
name: 'y'
|
||||
});
|
||||
});
|
||||
|
||||
it('应该支持不同的字段类型', () => {
|
||||
const definition = registry.getComponentDefinition('TestPlayer');
|
||||
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition!.fields.size).toBe(4);
|
||||
|
||||
const nameField = definition!.fields.get('name');
|
||||
expect(nameField!.type).toBe(ProtoFieldType.STRING);
|
||||
|
||||
const levelField = definition!.fields.get('level');
|
||||
expect(levelField!.type).toBe(ProtoFieldType.INT32);
|
||||
|
||||
const healthField = definition!.fields.get('health');
|
||||
expect(healthField!.type).toBe(ProtoFieldType.INT32);
|
||||
|
||||
const isAliveField = definition!.fields.get('isAlive');
|
||||
expect(isAliveField!.type).toBe(ProtoFieldType.BOOL);
|
||||
});
|
||||
|
||||
it('应该检测字段编号冲突', () => {
|
||||
const result = createConflictingComponent();
|
||||
expect(result).toBeInstanceOf(Error);
|
||||
expect((result as Error).message).toContain('字段编号 1 已被字段');
|
||||
});
|
||||
|
||||
it('应该验证字段编号有效性', () => {
|
||||
expect(() => {
|
||||
class InvalidFieldComponent extends Component {
|
||||
@ProtoField(0) // 无效的字段编号
|
||||
public invalid: number = 0;
|
||||
}
|
||||
}).toThrow('字段编号必须大于0');
|
||||
|
||||
expect(() => {
|
||||
class InvalidFieldComponent extends Component {
|
||||
@ProtoField(-1) // 无效的字段编号
|
||||
public invalid: number = 0;
|
||||
}
|
||||
}).toThrow('字段编号必须大于0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('便捷装饰器', () => {
|
||||
it('ProtoFloat应该设置正确的字段类型', () => {
|
||||
@ProtoSerializable('FloatTest')
|
||||
class FloatTestComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public value: number = 0;
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('FloatTest');
|
||||
const field = definition!.fields.get('value');
|
||||
expect(field!.type).toBe(ProtoFieldType.FLOAT);
|
||||
});
|
||||
|
||||
it('ProtoInt32应该设置正确的字段类型', () => {
|
||||
@ProtoSerializable('Int32Test')
|
||||
class Int32TestComponent extends Component {
|
||||
@ProtoInt32(1)
|
||||
public value: number = 0;
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('Int32Test');
|
||||
const field = definition!.fields.get('value');
|
||||
expect(field!.type).toBe(ProtoFieldType.INT32);
|
||||
});
|
||||
|
||||
it('ProtoString应该设置正确的字段类型', () => {
|
||||
@ProtoSerializable('StringTest')
|
||||
class StringTestComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public value: string = '';
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('StringTest');
|
||||
const field = definition!.fields.get('value');
|
||||
expect(field!.type).toBe(ProtoFieldType.STRING);
|
||||
});
|
||||
|
||||
it('ProtoBool应该设置正确的字段类型', () => {
|
||||
@ProtoSerializable('BoolTest')
|
||||
class BoolTestComponent extends Component {
|
||||
@ProtoBool(1)
|
||||
public value: boolean = false;
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('BoolTest');
|
||||
const field = definition!.fields.get('value');
|
||||
expect(field!.type).toBe(ProtoFieldType.BOOL);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProtobufRegistry', () => {
|
||||
it('应该正确生成proto定义', () => {
|
||||
const protoDefinition = registry.generateProtoDefinition();
|
||||
|
||||
expect(protoDefinition).toContain('syntax = "proto3";');
|
||||
expect(protoDefinition).toContain('package ecs;');
|
||||
expect(protoDefinition).toContain('message TestPosition');
|
||||
expect(protoDefinition).toContain('message TestPlayer');
|
||||
expect(protoDefinition).toContain('float x = 1;');
|
||||
expect(protoDefinition).toContain('float y = 2;');
|
||||
expect(protoDefinition).toContain('string name = 1;');
|
||||
expect(protoDefinition).toContain('int32 level = 2;');
|
||||
expect(protoDefinition).toContain('bool isAlive = 4;');
|
||||
});
|
||||
|
||||
it('应该正确管理组件注册', () => {
|
||||
const allComponents = registry.getAllComponents();
|
||||
|
||||
expect(allComponents.size).toBeGreaterThanOrEqual(2);
|
||||
expect(allComponents.has('TestPosition')).toBe(true);
|
||||
expect(allComponents.has('TestPlayer')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('字段选项', () => {
|
||||
it('应该支持repeated字段', () => {
|
||||
@ProtoSerializable('RepeatedTest')
|
||||
class RepeatedTestComponent extends Component {
|
||||
@ProtoField(1, ProtoFieldType.INT32, { repeated: true })
|
||||
public values: number[] = [];
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('RepeatedTest');
|
||||
const field = definition!.fields.get('values');
|
||||
expect(field!.repeated).toBe(true);
|
||||
});
|
||||
|
||||
it('应该支持optional字段', () => {
|
||||
@ProtoSerializable('OptionalTest')
|
||||
class OptionalTestComponent extends Component {
|
||||
@ProtoField(1, ProtoFieldType.STRING, { optional: true })
|
||||
public optionalValue?: string;
|
||||
}
|
||||
|
||||
const definition = registry.getComponentDefinition('OptionalTest');
|
||||
const field = definition!.fields.get('optionalValue');
|
||||
expect(field!.optional).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
314
tests/Utils/Serialization/ProtobufSerializer.test.ts
Normal file
314
tests/Utils/Serialization/ProtobufSerializer.test.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Protobuf序列化器测试
|
||||
*/
|
||||
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { ProtobufSerializer, SerializedData } from '../../../src/Utils/Serialization/ProtobufSerializer';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool,
|
||||
ProtobufRegistry
|
||||
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||
|
||||
// 测试组件
|
||||
@ProtoSerializable('Position')
|
||||
class PositionComponent extends Component {
|
||||
@ProtoFloat(1)
|
||||
public x: number = 0;
|
||||
|
||||
@ProtoFloat(2)
|
||||
public y: number = 0;
|
||||
|
||||
@ProtoFloat(3)
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('Health')
|
||||
class HealthComponent 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;
|
||||
}
|
||||
|
||||
takeDamage(damage: number): void {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
this.isDead = this.currentHealth <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('Player')
|
||||
class PlayerComponent extends Component {
|
||||
@ProtoString(1)
|
||||
public playerName: string = '';
|
||||
|
||||
@ProtoInt32(2)
|
||||
public playerId: number = 0;
|
||||
|
||||
@ProtoInt32(3)
|
||||
public level: number = 1;
|
||||
|
||||
constructor(playerId: number = 0, playerName: string = '') {
|
||||
super();
|
||||
this.playerId = playerId;
|
||||
this.playerName = playerName;
|
||||
}
|
||||
}
|
||||
|
||||
// 没有protobuf装饰器的组件
|
||||
class CustomComponent extends Component {
|
||||
public customData = {
|
||||
settings: { volume: 0.8 },
|
||||
achievements: ['first_kill', 'level_up'],
|
||||
inventory: new Map([['sword', 1], ['potion', 3]])
|
||||
};
|
||||
|
||||
// 自定义序列化方法
|
||||
serialize(): any {
|
||||
return {
|
||||
customData: {
|
||||
settings: this.customData.settings,
|
||||
achievements: this.customData.achievements,
|
||||
inventory: Array.from(this.customData.inventory.entries())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(data: any): void {
|
||||
if (data.customData) {
|
||||
this.customData.settings = data.customData.settings || this.customData.settings;
|
||||
this.customData.achievements = data.customData.achievements || this.customData.achievements;
|
||||
if (data.customData.inventory) {
|
||||
this.customData.inventory = new Map(data.customData.inventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock protobuf.js
|
||||
const mockProtobuf = {
|
||||
parse: jest.fn().mockReturnValue({
|
||||
root: {
|
||||
lookupType: jest.fn().mockImplementation((typeName: string) => {
|
||||
// 模拟protobuf消息类型
|
||||
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().mockImplementation(() => ({
|
||||
x: 10, y: 20, z: 30,
|
||||
maxHealth: 100, currentHealth: 80, isDead: false,
|
||||
playerName: 'TestPlayer', playerId: 1001, level: 5
|
||||
})),
|
||||
toObject: jest.fn().mockImplementation((message) => message)
|
||||
};
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
describe('ProtobufSerializer', () => {
|
||||
let serializer: ProtobufSerializer;
|
||||
|
||||
beforeEach(() => {
|
||||
serializer = ProtobufSerializer.getInstance();
|
||||
// 重置mock
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('初始化', () => {
|
||||
it('应该正确初始化protobuf支持', () => {
|
||||
serializer.initialize(mockProtobuf);
|
||||
|
||||
expect(mockProtobuf.parse).toHaveBeenCalled();
|
||||
expect(serializer.canSerialize(new PositionComponent())).toBe(true);
|
||||
});
|
||||
|
||||
it('没有初始化时应该无法序列化protobuf组件', () => {
|
||||
const newSerializer = new (ProtobufSerializer as any)();
|
||||
expect(newSerializer.canSerialize(new PositionComponent())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('序列化', () => {
|
||||
beforeEach(() => {
|
||||
serializer.initialize(mockProtobuf);
|
||||
});
|
||||
|
||||
it('应该正确序列化protobuf组件', () => {
|
||||
const position = new PositionComponent(10, 20, 30);
|
||||
const result = serializer.serialize(position);
|
||||
|
||||
expect(result.type).toBe('protobuf');
|
||||
expect(result.componentType).toBe('PositionComponent');
|
||||
expect(result.data).toBeInstanceOf(Uint8Array);
|
||||
expect(result.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该正确序列化复杂protobuf组件', () => {
|
||||
const health = new HealthComponent(150);
|
||||
health.takeDamage(50);
|
||||
|
||||
const result = serializer.serialize(health);
|
||||
|
||||
expect(result.type).toBe('protobuf');
|
||||
expect(result.componentType).toBe('HealthComponent');
|
||||
expect(result.data).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
|
||||
it('应该回退到JSON序列化非protobuf组件', () => {
|
||||
const custom = new CustomComponent();
|
||||
const result = serializer.serialize(custom);
|
||||
|
||||
expect(result.type).toBe('json');
|
||||
expect(result.componentType).toBe('CustomComponent');
|
||||
expect(result.data).toEqual(custom.serialize());
|
||||
});
|
||||
|
||||
it('protobuf序列化失败时应该回退到JSON', () => {
|
||||
// 模拟protobuf验证失败
|
||||
const mockType = mockProtobuf.parse().root.lookupType('ecs.Position');
|
||||
mockType.verify.mockReturnValue('验证失败');
|
||||
|
||||
const position = new PositionComponent(10, 20, 30);
|
||||
const result = serializer.serialize(position);
|
||||
|
||||
expect(result.type).toBe('json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('反序列化', () => {
|
||||
beforeEach(() => {
|
||||
serializer.initialize(mockProtobuf);
|
||||
});
|
||||
|
||||
it('应该正确反序列化protobuf数据', () => {
|
||||
const position = new PositionComponent();
|
||||
const serializedData: SerializedData = {
|
||||
type: 'protobuf',
|
||||
componentType: 'PositionComponent',
|
||||
data: new Uint8Array([1, 2, 3, 4]),
|
||||
size: 4
|
||||
};
|
||||
|
||||
serializer.deserialize(position, serializedData);
|
||||
|
||||
// 验证decode和toObject被调用
|
||||
const mockType = mockProtobuf.parse().root.lookupType('ecs.Position');
|
||||
expect(mockType.decode).toHaveBeenCalled();
|
||||
expect(mockType.toObject).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确反序列化JSON数据', () => {
|
||||
const custom = new CustomComponent();
|
||||
const originalData = custom.serialize();
|
||||
|
||||
const serializedData: SerializedData = {
|
||||
type: 'json',
|
||||
componentType: 'CustomComponent',
|
||||
data: originalData,
|
||||
size: 100
|
||||
};
|
||||
|
||||
// 修改组件数据
|
||||
custom.customData.settings.volume = 0.5;
|
||||
|
||||
// 反序列化
|
||||
serializer.deserialize(custom, serializedData);
|
||||
|
||||
// 验证数据被恢复
|
||||
expect(custom.customData.settings.volume).toBe(0.8);
|
||||
});
|
||||
|
||||
it('应该处理反序列化错误', () => {
|
||||
const position = new PositionComponent();
|
||||
const invalidData: SerializedData = {
|
||||
type: 'protobuf',
|
||||
componentType: 'PositionComponent',
|
||||
data: new Uint8Array([255, 255, 255, 255]), // 无效数据
|
||||
size: 4
|
||||
};
|
||||
|
||||
// 模拟解码失败
|
||||
const mockType = mockProtobuf.parse().root.lookupType('ecs.Position');
|
||||
mockType.decode.mockImplementation(() => {
|
||||
throw new Error('解码失败');
|
||||
});
|
||||
|
||||
// 应该不抛出异常
|
||||
expect(() => {
|
||||
serializer.deserialize(position, invalidData);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('统计信息', () => {
|
||||
it('应该返回正确的统计信息', () => {
|
||||
serializer.initialize(mockProtobuf);
|
||||
const stats = serializer.getStats();
|
||||
|
||||
expect(stats.protobufAvailable).toBe(true);
|
||||
expect(stats.registeredComponents).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('未初始化时应该返回正确的状态', () => {
|
||||
const newSerializer = new (ProtobufSerializer as any)();
|
||||
const stats = newSerializer.getStats();
|
||||
|
||||
expect(stats.protobufAvailable).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
beforeEach(() => {
|
||||
serializer.initialize(mockProtobuf);
|
||||
});
|
||||
|
||||
it('应该处理空值和undefined', () => {
|
||||
const position = new PositionComponent();
|
||||
// 设置一些undefined值
|
||||
(position as any).undefinedProp = undefined;
|
||||
(position as any).nullProp = null;
|
||||
|
||||
const result = serializer.serialize(position);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该处理循环引用', () => {
|
||||
const custom = new CustomComponent();
|
||||
// 创建循环引用
|
||||
(custom as any).circular = custom;
|
||||
|
||||
const result = serializer.serialize(custom);
|
||||
expect(result.type).toBe('json');
|
||||
});
|
||||
|
||||
it('应该处理非常大的数值', () => {
|
||||
const position = new PositionComponent(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, 0);
|
||||
|
||||
const result = serializer.serialize(position);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
393
tests/Utils/Serialization/RealPerformance.test.ts
Normal file
393
tests/Utils/Serialization/RealPerformance.test.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
/**
|
||||
* 真实 Protobuf 序列化性能测试
|
||||
* 使用实际的 protobufjs 库进行性能对比
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import {
|
||||
ProtoSerializable,
|
||||
ProtoFloat,
|
||||
ProtoInt32,
|
||||
ProtoString,
|
||||
ProtoBool,
|
||||
ProtobufRegistry
|
||||
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||
|
||||
// 测试组件
|
||||
@ProtoSerializable('Position')
|
||||
class PositionComponent extends Component {
|
||||
@ProtoFloat(1) public x: number = 0;
|
||||
@ProtoFloat(2) public y: number = 0;
|
||||
@ProtoFloat(3) public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
@ProtoSerializable('Player')
|
||||
class PlayerComponent extends Component {
|
||||
@ProtoString(1) public name: string = '';
|
||||
@ProtoInt32(2) public level: number = 1;
|
||||
@ProtoInt32(3) public experience: number = 0;
|
||||
@ProtoInt32(4) public score: number = 0;
|
||||
@ProtoBool(5) public isOnline: boolean = true;
|
||||
@ProtoFloat(6) public health: number = 100.0;
|
||||
|
||||
constructor(name: string = 'Player', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
this.experience = level * 1000;
|
||||
this.score = level * 500;
|
||||
this.health = 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
// JSON 对比组件
|
||||
class JsonPositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
public z: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
|
||||
class JsonPlayerComponent extends Component {
|
||||
public name: string = '';
|
||||
public level: number = 1;
|
||||
public experience: number = 0;
|
||||
public score: number = 0;
|
||||
public isOnline: boolean = true;
|
||||
public health: number = 100.0;
|
||||
|
||||
constructor(name: string = 'Player', level: number = 1) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
this.experience = level * 1000;
|
||||
this.score = level * 500;
|
||||
this.health = 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
describe('真实 Protobuf 性能测试', () => {
|
||||
let protobuf: any;
|
||||
let root: any;
|
||||
let PositionType: any;
|
||||
let PlayerType: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
try {
|
||||
// 尝试加载真实的 protobufjs
|
||||
protobuf = require('protobufjs');
|
||||
|
||||
// 生成 proto 定义
|
||||
const registry = ProtobufRegistry.getInstance();
|
||||
const protoDefinition = registry.generateProtoDefinition();
|
||||
|
||||
console.log('Generated proto definition:');
|
||||
console.log(protoDefinition);
|
||||
|
||||
// 解析 proto 定义
|
||||
root = protobuf.parse(protoDefinition).root;
|
||||
PositionType = root.lookupType('ecs.Position');
|
||||
PlayerType = root.lookupType('ecs.Player');
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Protobuf not available, skipping real performance tests:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const skipIfNoProtobuf = () => {
|
||||
if (!protobuf || !root) {
|
||||
console.log('Skipping test: protobufjs not available');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
describe('简单组件性能对比', () => {
|
||||
it('Position 组件序列化性能', () => {
|
||||
if (skipIfNoProtobuf()) return;
|
||||
|
||||
const iterations = 1000;
|
||||
const protobufComponents: PositionComponent[] = [];
|
||||
const jsonComponents: JsonPositionComponent[] = [];
|
||||
|
||||
// 准备测试数据
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const x = Math.random() * 1000;
|
||||
const y = Math.random() * 1000;
|
||||
const z = Math.random() * 100;
|
||||
|
||||
protobufComponents.push(new PositionComponent(x, y, z));
|
||||
jsonComponents.push(new JsonPositionComponent(x, y, z));
|
||||
}
|
||||
|
||||
// Protobuf 序列化测试
|
||||
const protobufStartTime = performance.now();
|
||||
let protobufTotalSize = 0;
|
||||
const protobufResults: Uint8Array[] = [];
|
||||
|
||||
for (const component of protobufComponents) {
|
||||
const message = PositionType.create({
|
||||
x: component.x,
|
||||
y: component.y,
|
||||
z: component.z
|
||||
});
|
||||
const buffer = PositionType.encode(message).finish();
|
||||
protobufResults.push(buffer);
|
||||
protobufTotalSize += buffer.length;
|
||||
}
|
||||
|
||||
const protobufEndTime = performance.now();
|
||||
const protobufTime = protobufEndTime - protobufStartTime;
|
||||
|
||||
// JSON 序列化测试
|
||||
const jsonStartTime = performance.now();
|
||||
let jsonTotalSize = 0;
|
||||
const jsonResults: string[] = [];
|
||||
|
||||
for (const component of jsonComponents) {
|
||||
const jsonString = JSON.stringify({
|
||||
x: component.x,
|
||||
y: component.y,
|
||||
z: component.z
|
||||
});
|
||||
jsonResults.push(jsonString);
|
||||
jsonTotalSize += new Blob([jsonString]).size;
|
||||
}
|
||||
|
||||
const jsonEndTime = performance.now();
|
||||
const jsonTime = jsonEndTime - jsonStartTime;
|
||||
|
||||
// 计算性能指标
|
||||
const speedImprovement = jsonTime > 0 ? ((jsonTime - protobufTime) / jsonTime * 100) : 0;
|
||||
const sizeReduction = jsonTotalSize > 0 ? ((jsonTotalSize - protobufTotalSize) / jsonTotalSize * 100) : 0;
|
||||
|
||||
console.log(`\\n=== Position 组件性能对比 (${iterations} 次迭代) ===`);
|
||||
console.log(`Protobuf 时间: ${protobufTime.toFixed(2)}ms`);
|
||||
console.log(`JSON 时间: ${jsonTime.toFixed(2)}ms`);
|
||||
console.log(`速度变化: ${speedImprovement > 0 ? '+' : ''}${speedImprovement.toFixed(1)}%`);
|
||||
console.log('');
|
||||
console.log(`Protobuf 总大小: ${protobufTotalSize} bytes`);
|
||||
console.log(`JSON 总大小: ${jsonTotalSize} bytes`);
|
||||
console.log(`大小变化: ${sizeReduction > 0 ? '-' : '+'}${Math.abs(sizeReduction).toFixed(1)}%`);
|
||||
console.log(`平均 Protobuf 大小: ${(protobufTotalSize / iterations).toFixed(1)} bytes`);
|
||||
console.log(`平均 JSON 大小: ${(jsonTotalSize / iterations).toFixed(1)} bytes`);
|
||||
|
||||
// 验证反序列化
|
||||
let deserializeTime = performance.now();
|
||||
for (const buffer of protobufResults.slice(0, 10)) { // 只测试前10个
|
||||
const decoded = PositionType.decode(buffer);
|
||||
expect(typeof decoded.x).toBe('number');
|
||||
expect(typeof decoded.y).toBe('number');
|
||||
expect(typeof decoded.z).toBe('number');
|
||||
}
|
||||
deserializeTime = performance.now() - deserializeTime;
|
||||
console.log(`Protobuf 反序列化 10 个: ${deserializeTime.toFixed(2)}ms`);
|
||||
|
||||
// 基本验证
|
||||
expect(protobufTime).toBeGreaterThan(0);
|
||||
expect(jsonTime).toBeGreaterThan(0);
|
||||
expect(protobufTotalSize).toBeGreaterThan(0);
|
||||
expect(jsonTotalSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('复杂 Player 组件序列化性能', () => {
|
||||
if (skipIfNoProtobuf()) return;
|
||||
|
||||
const iterations = 500;
|
||||
const protobufPlayers: PlayerComponent[] = [];
|
||||
const jsonPlayers: JsonPlayerComponent[] = [];
|
||||
|
||||
// 创建测试数据
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const name = `Player_${i}_${'x'.repeat(10 + Math.floor(Math.random() * 20))}`;
|
||||
const level = Math.floor(Math.random() * 100) + 1;
|
||||
|
||||
protobufPlayers.push(new PlayerComponent(name, level));
|
||||
jsonPlayers.push(new JsonPlayerComponent(name, level));
|
||||
}
|
||||
|
||||
// Protobuf 序列化测试
|
||||
const protobufStart = performance.now();
|
||||
let protobufSize = 0;
|
||||
|
||||
for (const player of protobufPlayers) {
|
||||
const message = PlayerType.create({
|
||||
name: player.name,
|
||||
level: player.level,
|
||||
experience: player.experience,
|
||||
score: player.score,
|
||||
isOnline: player.isOnline,
|
||||
health: player.health
|
||||
});
|
||||
const buffer = PlayerType.encode(message).finish();
|
||||
protobufSize += buffer.length;
|
||||
}
|
||||
|
||||
const protobufTime = performance.now() - protobufStart;
|
||||
|
||||
// JSON 序列化测试
|
||||
const jsonStart = performance.now();
|
||||
let jsonSize = 0;
|
||||
|
||||
for (const player of jsonPlayers) {
|
||||
const jsonString = JSON.stringify({
|
||||
name: player.name,
|
||||
level: player.level,
|
||||
experience: player.experience,
|
||||
score: player.score,
|
||||
isOnline: player.isOnline,
|
||||
health: player.health
|
||||
});
|
||||
jsonSize += new Blob([jsonString]).size;
|
||||
}
|
||||
|
||||
const jsonTime = performance.now() - jsonStart;
|
||||
|
||||
const speedChange = jsonTime > 0 ? ((jsonTime - protobufTime) / jsonTime * 100) : 0;
|
||||
const sizeReduction = jsonSize > 0 ? ((jsonSize - protobufSize) / jsonSize * 100) : 0;
|
||||
|
||||
console.log(`\\n=== Player 组件性能对比 (${iterations} 次迭代) ===`);
|
||||
console.log(`Protobuf 时间: ${protobufTime.toFixed(2)}ms`);
|
||||
console.log(`JSON 时间: ${jsonTime.toFixed(2)}ms`);
|
||||
console.log(`速度变化: ${speedChange > 0 ? '+' : ''}${speedChange.toFixed(1)}%`);
|
||||
console.log('');
|
||||
console.log(`Protobuf 总大小: ${protobufSize} bytes`);
|
||||
console.log(`JSON 总大小: ${jsonSize} bytes`);
|
||||
console.log(`大小变化: ${sizeReduction > 0 ? '-' : '+'}${Math.abs(sizeReduction).toFixed(1)}%`);
|
||||
console.log(`平均 Protobuf 大小: ${(protobufSize / iterations).toFixed(1)} bytes`);
|
||||
console.log(`平均 JSON 大小: ${(jsonSize / iterations).toFixed(1)} bytes`);
|
||||
|
||||
expect(protobufTime).toBeGreaterThan(0);
|
||||
expect(jsonTime).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('批量数据性能测试', () => {
|
||||
it('大量小对象序列化', () => {
|
||||
if (skipIfNoProtobuf()) return;
|
||||
|
||||
const count = 5000;
|
||||
console.log(`\\n=== 大量小对象测试 (${count} 个 Position) ===`);
|
||||
|
||||
// 准备数据
|
||||
const positions = Array.from({ length: count }, (_, i) => ({
|
||||
x: i * 0.1,
|
||||
y: i * 0.2,
|
||||
z: i * 0.05
|
||||
}));
|
||||
|
||||
// Protobuf 批量序列化
|
||||
const protobufStart = performance.now();
|
||||
let protobufSize = 0;
|
||||
|
||||
for (const pos of positions) {
|
||||
const message = PositionType.create(pos);
|
||||
const buffer = PositionType.encode(message).finish();
|
||||
protobufSize += buffer.length;
|
||||
}
|
||||
|
||||
const protobufTime = performance.now() - protobufStart;
|
||||
|
||||
// JSON 批量序列化
|
||||
const jsonStart = performance.now();
|
||||
let jsonSize = 0;
|
||||
|
||||
for (const pos of positions) {
|
||||
const jsonString = JSON.stringify(pos);
|
||||
jsonSize += jsonString.length;
|
||||
}
|
||||
|
||||
const jsonTime = performance.now() - jsonStart;
|
||||
|
||||
console.log(`Protobuf: ${protobufTime.toFixed(2)}ms, ${protobufSize} bytes`);
|
||||
console.log(`JSON: ${jsonTime.toFixed(2)}ms, ${jsonSize} bytes`);
|
||||
console.log(`速度: ${protobufTime < jsonTime ? 'Protobuf 更快' : 'JSON 更快'} (${Math.abs(protobufTime - jsonTime).toFixed(2)}ms 差异)`);
|
||||
console.log(`大小: Protobuf ${protobufSize < jsonSize ? '更小' : '更大'} (${Math.abs(protobufSize - jsonSize)} bytes 差异)`);
|
||||
console.log(`处理速度: Protobuf ${Math.floor(count / (protobufTime / 1000))} ops/s, JSON ${Math.floor(count / (jsonTime / 1000))} ops/s`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('真实网络场景模拟', () => {
|
||||
it('游戏状态同步场景', () => {
|
||||
if (skipIfNoProtobuf()) return;
|
||||
|
||||
console.log(`\\n=== 游戏状态同步场景 ===`);
|
||||
|
||||
// 模拟 100 个玩家的位置更新
|
||||
const playerCount = 100;
|
||||
const updateData = Array.from({ length: playerCount }, (_, i) => ({
|
||||
playerId: i,
|
||||
x: Math.random() * 1000,
|
||||
y: Math.random() * 1000,
|
||||
z: Math.random() * 100,
|
||||
health: Math.floor(Math.random() * 100),
|
||||
isMoving: Math.random() > 0.5
|
||||
}));
|
||||
|
||||
// 创建组合消息类型(模拟)
|
||||
const GameUpdateType = root.lookupType('ecs.Position'); // 简化使用 Position
|
||||
|
||||
// Protobuf 序列化所有更新
|
||||
const protobufStart = performance.now();
|
||||
let protobufTotalSize = 0;
|
||||
|
||||
for (const update of updateData) {
|
||||
const message = GameUpdateType.create({
|
||||
x: update.x,
|
||||
y: update.y,
|
||||
z: update.z
|
||||
});
|
||||
const buffer = GameUpdateType.encode(message).finish();
|
||||
protobufTotalSize += buffer.length;
|
||||
}
|
||||
|
||||
const protobufTime = performance.now() - protobufStart;
|
||||
|
||||
// JSON 序列化所有更新
|
||||
const jsonStart = performance.now();
|
||||
let jsonTotalSize = 0;
|
||||
|
||||
for (const update of updateData) {
|
||||
const jsonString = JSON.stringify({
|
||||
playerId: update.playerId,
|
||||
x: update.x,
|
||||
y: update.y,
|
||||
z: update.z,
|
||||
health: update.health,
|
||||
isMoving: update.isMoving
|
||||
});
|
||||
jsonTotalSize += jsonString.length;
|
||||
}
|
||||
|
||||
const jsonTime = performance.now() - jsonStart;
|
||||
|
||||
console.log(`${playerCount} 个玩家位置更新:`);
|
||||
console.log(`Protobuf: ${protobufTime.toFixed(2)}ms, ${protobufTotalSize} bytes`);
|
||||
console.log(`JSON: ${jsonTime.toFixed(2)}ms, ${jsonTotalSize} bytes`);
|
||||
|
||||
// 计算网络传输节省
|
||||
const sizeSaving = jsonTotalSize - protobufTotalSize;
|
||||
const percentSaving = (sizeSaving / jsonTotalSize * 100);
|
||||
|
||||
console.log(`数据大小节省: ${sizeSaving} bytes (${percentSaving.toFixed(1)}%)`);
|
||||
console.log(`每秒 60 次更新的带宽节省: ${(sizeSaving * 60 / 1024).toFixed(2)} KB/s`);
|
||||
|
||||
expect(protobufTotalSize).toBeGreaterThan(0);
|
||||
expect(jsonTotalSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
370
tests/Utils/Serialization/SnapshotManagerIntegration.test.ts
Normal file
370
tests/Utils/Serialization/SnapshotManagerIntegration.test.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
17
tests/Utils/Serialization/index.test.ts
Normal file
17
tests/Utils/Serialization/index.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 序列化模块集成测试
|
||||
*/
|
||||
|
||||
// 导入所有测试
|
||||
import './ProtobufDecorators.test';
|
||||
import './ProtobufSerializer.test';
|
||||
import './SnapshotManagerIntegration.test';
|
||||
import './Performance.test';
|
||||
|
||||
// 这个文件确保所有序列化相关的测试都被包含在测试套件中
|
||||
describe('序列化模块集成测试', () => {
|
||||
it('应该包含所有序列化测试', () => {
|
||||
// 这个测试确保模块正确加载
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user