Files
esengine/packages/network/tests/Serialization/ProtobufSerializerEdgeCases.test.ts

433 lines
15 KiB
TypeScript
Raw Normal View History

/**
* Protobuf序列化器边界情况测试
*/
import { Component } from '../../../src/ECS/Component';
import { BigIntFactory } from '../../../src/ECS/Utils/BigIntCompatibility';
import { ProtobufSerializer } from '../../../src/Utils/Serialization/ProtobufSerializer';
import {
ProtoSerializable,
ProtoFloat,
ProtoInt32,
ProtoString,
ProtoBool,
ProtoBytes,
ProtoTimestamp,
ProtoDouble,
ProtoInt64,
ProtoStruct
} from '../../../src/Utils/Serialization/ProtobufDecorators';
// 边界测试组件
@ProtoSerializable('EdgeCaseComponent')
class EdgeCaseComponent extends Component {
@ProtoFloat(1)
public floatValue: number = 0;
@ProtoDouble(2)
public doubleValue: number = 0;
@ProtoInt32(3)
public intValue: number = 0;
@ProtoInt64(4)
public bigIntValue: any = BigIntFactory.zero();
@ProtoString(5)
public stringValue: string = '';
@ProtoBool(6)
public boolValue: boolean = false;
@ProtoBytes(7)
public bytesValue: Uint8Array = new Uint8Array();
@ProtoTimestamp(8)
public timestampValue: Date = new Date();
@ProtoStruct(9)
public structValue: any = {};
@ProtoFloat(10, { repeated: true })
public arrayValue: number[] = [];
constructor() {
super();
}
}
// 不完整的组件(缺少字段)
@ProtoSerializable('IncompleteComponent')
class IncompleteComponent extends Component {
@ProtoString(1)
public name: string = '';
// 故意添加没有装饰器的字段
public undecoratedField: number = 42;
constructor(name: string = '') {
super();
this.name = name;
}
}
// 有循环引用的组件
@ProtoSerializable('CircularComponent')
class CircularComponent extends Component {
@ProtoString(1)
public name: string = '';
@ProtoStruct(2)
public circular: any = null;
constructor(name: string = '') {
super();
this.name = name;
// 创建循环引用
this.circular = this;
}
}
// 没有protobuf装饰器的组件
class NonSerializableComponent extends Component {
public data: string = 'test';
serialize(): any {
return { data: this.data };
}
deserialize(data: any): void {
this.data = data.data || this.data;
}
}
// Mock protobuf.js
const mockProtobuf = {
parse: jest.fn().mockReturnValue({
root: {
lookupType: jest.fn().mockImplementation((typeName: string) => {
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(() => ({
floatValue: 3.14,
doubleValue: 2.718,
intValue: 42,
bigIntValue: BigIntFactory.create(999),
stringValue: 'test',
boolValue: true,
bytesValue: new Uint8Array([65, 66, 67]),
timestampValue: { seconds: 1609459200, nanos: 0 },
structValue: { fields: { key: { stringValue: 'value' } } },
arrayValue: [1.1, 2.2, 3.3],
name: 'TestComponent'
})),
toObject: jest.fn().mockImplementation((message) => message)
};
}),
lookupTypeOrEnum: jest.fn().mockImplementation((typeName: string) => {
if (typeName === 'google.protobuf.Timestamp') {
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(() => ({
seconds: 1609459200,
nanos: 0
}))
};
}
return null;
})
}
})
};
describe('ProtobufSerializer边界情况测试', () => {
let serializer: ProtobufSerializer;
beforeEach(() => {
serializer = ProtobufSerializer.getInstance();
serializer.initialize(mockProtobuf);
jest.clearAllMocks();
});
describe('极值测试', () => {
it('应该处理极大值', () => {
const component = new EdgeCaseComponent();
component.floatValue = Number.MAX_VALUE;
component.doubleValue = Number.MAX_VALUE;
component.intValue = Number.MAX_SAFE_INTEGER;
component.bigIntValue = BigIntFactory.create(Number.MAX_SAFE_INTEGER);
const result = serializer.serialize(component);
expect(result.type).toBe('protobuf');
expect(result.size).toBeGreaterThan(0);
});
it('应该处理极小值', () => {
const component = new EdgeCaseComponent();
component.floatValue = Number.MIN_VALUE;
component.doubleValue = Number.MIN_VALUE;
component.intValue = Number.MIN_SAFE_INTEGER;
component.bigIntValue = BigIntFactory.create(Number.MIN_SAFE_INTEGER);
const result = serializer.serialize(component);
expect(result.type).toBe('protobuf');
});
it('应该处理特殊数值', () => {
const component = new EdgeCaseComponent();
component.floatValue = NaN;
component.doubleValue = Infinity;
component.intValue = 0;
const result = serializer.serialize(component);
expect(result.type).toBe('protobuf');
});
});
describe('空值和undefined测试', () => {
it('应该处理null值', () => {
const component = new EdgeCaseComponent();
(component as any).stringValue = null;
(component as any).structValue = null;
const result = serializer.serialize(component);
expect(result.type).toBe('protobuf');
});
it('应该处理undefined值', () => {
const component = new EdgeCaseComponent();
(component as any).stringValue = undefined;
(component as any).floatValue = undefined;
const result = serializer.serialize(component);
expect(result.type).toBe('protobuf');
});
it('应该处理空数组', () => {
const component = new EdgeCaseComponent();
component.arrayValue = [];
const result = serializer.serialize(component);
expect(result.type).toBe('protobuf');
});
});
describe('复杂数据类型测试', () => {
it('应该处理复杂对象结构', () => {
const component = new EdgeCaseComponent();
component.structValue = {
nested: {
array: [1, 2, 3],
object: { key: 'value' },
date: new Date(),
null: null,
undefined: undefined
}
};
const result = serializer.serialize(component);
expect(result.type).toBe('protobuf');
});
it('应该处理Date对象', () => {
const component = new EdgeCaseComponent();
component.timestampValue = new Date('2021-01-01T00:00:00Z');
const result = serializer.serialize(component);
expect(result.type).toBe('protobuf');
});
it('应该处理Uint8Array', () => {
const component = new EdgeCaseComponent();
component.bytesValue = new Uint8Array([0, 255, 128, 64]);
const result = serializer.serialize(component);
expect(result.type).toBe('protobuf');
});
});
describe('循环引用测试', () => {
it('应该拒绝循环引用对象并抛出错误', () => {
const component = new CircularComponent('circular');
// 循环引用应该抛出错误不再回退到JSON序列化
expect(() => {
serializer.serialize(component);
}).toThrow();
});
});
describe('不完整组件测试', () => {
it('应该处理缺少装饰器的字段', () => {
const component = new IncompleteComponent('test');
const result = serializer.serialize(component);
expect(result.type).toBe('protobuf');
expect(result.componentType).toBe('IncompleteComponent');
});
});
describe('非序列化组件测试', () => {
it('应该拒绝非protobuf组件并抛出错误', () => {
const component = new NonSerializableComponent();
// 没有protobuf装饰器的组件应该抛出错误不再回退到JSON序列化
expect(() => {
serializer.serialize(component);
}).toThrow();
});
});
describe('批量序列化边界测试', () => {
it('应该处理空数组', () => {
const results = serializer.serializeBatch([]);
expect(results).toEqual([]);
});
it('应该处理混合组件类型', () => {
const components = [
new EdgeCaseComponent(),
new NonSerializableComponent(),
new IncompleteComponent('mixed'),
];
// continueOnError: true 时,只有可序列化的组件能成功,其他会被跳过
const results = serializer.serializeBatch(components, { continueOnError: true });
expect(results.length).toBeGreaterThanOrEqual(1);
expect(results.every(r => r.type === 'protobuf')).toBe(true);
// continueOnError: false 时应该抛出错误
expect(() => {
serializer.serializeBatch(components, { continueOnError: false });
}).toThrow();
});
it('应该处理批量数据', () => {
const components = Array.from({ length: 50 }, () => new EdgeCaseComponent());
const results = serializer.serializeBatch(components);
expect(results).toHaveLength(50);
expect(results.every(r => r.type === 'protobuf')).toBe(true);
});
it('应该处理序列化错误', () => {
// 创建会导致序列化失败的组件
const components = [new NonSerializableComponent()];
// continueOnError = false 应该抛出异常
expect(() => {
serializer.serializeBatch(components, { continueOnError: false });
}).toThrow();
// continueOnError = true 应该返回空数组(跳过失败的组件)
const results = serializer.serializeBatch(components, { continueOnError: true });
expect(results).toHaveLength(0);
});
});
describe('反序列化边界测试', () => {
it('应该拒绝JSON类型的反序列化并抛出错误', () => {
const component = new NonSerializableComponent();
const serializedData = {
type: 'json' as const,
componentType: 'NonSerializableComponent',
data: { data: 'deserialized' },
size: 100
};
// JSON类型的数据应该被拒绝抛出错误
expect(() => {
serializer.deserialize(component, serializedData);
}).toThrow();
});
it('应该优雅处理反序列化错误', () => {
const component = new EdgeCaseComponent();
const invalidData = {
type: 'protobuf' as const,
componentType: 'EdgeCaseComponent',
data: new Uint8Array([255, 255, 255, 255]),
size: 4
};
// 模拟解码失败
const mockType = mockProtobuf.parse().root.lookupType('ecs.EdgeCaseComponent');
mockType.decode.mockImplementation(() => {
throw new Error('Decode failed');
});
// 不应该抛出异常
expect(() => {
serializer.deserialize(component, invalidData);
}).not.toThrow();
});
it('应该处理缺失的proto定义', () => {
const component = new EdgeCaseComponent();
// 清除proto名称以模拟缺失情况
(component as any)._protoName = undefined;
const serializedData = {
type: 'protobuf' as const,
componentType: 'EdgeCaseComponent',
data: new Uint8Array([1, 2, 3, 4]),
size: 4
};
// 不应该抛出异常
expect(() => {
serializer.deserialize(component, serializedData);
}).not.toThrow();
});
});
describe('缓存测试', () => {
it('应该能清空所有缓存', () => {
serializer.clearAllCaches();
const stats = serializer.getStats();
expect(stats.messageTypeCacheSize).toBe(0);
expect(stats.componentDataCacheSize).toBe(0);
});
});
describe('性能选项测试', () => {
it('应该能禁用数据验证', () => {
serializer.setPerformanceOptions({ enableValidation: false });
const component = new EdgeCaseComponent();
const result = serializer.serialize(component);
expect(result.type).toBe('protobuf');
});
it('应该能禁用组件数据缓存', () => {
serializer.setPerformanceOptions({ enableComponentDataCache: false });
const component = new EdgeCaseComponent();
serializer.serialize(component);
const stats = serializer.getStats();
expect(stats.componentDataCacheSize).toBe(0);
});
});
describe('统计信息测试', () => {
it('应该返回正确的统计信息', () => {
const stats = serializer.getStats();
expect(typeof stats.registeredComponents).toBe('number');
expect(typeof stats.protobufAvailable).toBe('boolean');
expect(typeof stats.messageTypeCacheSize).toBe('number');
expect(typeof stats.componentDataCacheSize).toBe('number');
expect(typeof stats.enableComponentDataCache).toBe('boolean');
expect(typeof stats.maxCacheSize).toBe('number');
});
});
});