433 lines
15 KiB
TypeScript
433 lines
15 KiB
TypeScript
/**
|
||
* 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');
|
||
});
|
||
});
|
||
}); |