refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
139
packages/framework/core/tests/Utils/BinarySerializer.test.ts
Normal file
139
packages/framework/core/tests/Utils/BinarySerializer.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { BinarySerializer } from '../../src/Utils/BinarySerializer';
|
||||
|
||||
describe('BinarySerializer', () => {
|
||||
describe('encode and decode', () => {
|
||||
it('应该正确编码和解码简单对象', () => {
|
||||
const data = { name: 'test', value: 123 };
|
||||
const encoded = BinarySerializer.encode(data);
|
||||
const decoded = BinarySerializer.decode(encoded);
|
||||
|
||||
expect(decoded).toEqual(data);
|
||||
});
|
||||
|
||||
it('应该正确编码和解码包含中文的对象', () => {
|
||||
const data = {
|
||||
name: '测试',
|
||||
description: '这是一个包含中文的测试对象',
|
||||
value: 456
|
||||
};
|
||||
const encoded = BinarySerializer.encode(data);
|
||||
const decoded = BinarySerializer.decode(encoded);
|
||||
|
||||
expect(decoded).toEqual(data);
|
||||
});
|
||||
|
||||
it('应该正确编码和解码数组', () => {
|
||||
const data = {
|
||||
items: [1, 2, 3, 4, 5],
|
||||
names: ['Alice', 'Bob', 'Charlie']
|
||||
};
|
||||
const encoded = BinarySerializer.encode(data);
|
||||
const decoded = BinarySerializer.decode(encoded);
|
||||
|
||||
expect(decoded).toEqual(data);
|
||||
});
|
||||
|
||||
it('应该正确编码和解码嵌套对象', () => {
|
||||
const data = {
|
||||
user: {
|
||||
name: 'John',
|
||||
age: 30,
|
||||
address: {
|
||||
city: 'Beijing',
|
||||
street: 'Main St'
|
||||
}
|
||||
},
|
||||
scores: [90, 85, 95]
|
||||
};
|
||||
const encoded = BinarySerializer.encode(data);
|
||||
const decoded = BinarySerializer.decode(encoded);
|
||||
|
||||
expect(decoded).toEqual(data);
|
||||
});
|
||||
|
||||
it('应该正确编码和解码包含特殊字符的字符串', () => {
|
||||
const data = {
|
||||
text: 'Hello\nWorld\t!@#$%^&*()',
|
||||
emoji: '😀🎉🚀',
|
||||
special: 'a\u0000b'
|
||||
};
|
||||
const encoded = BinarySerializer.encode(data);
|
||||
const decoded = BinarySerializer.decode(encoded);
|
||||
|
||||
expect(decoded).toEqual(data);
|
||||
});
|
||||
|
||||
it('应该正确编码和解码空对象', () => {
|
||||
const data = {};
|
||||
const encoded = BinarySerializer.encode(data);
|
||||
const decoded = BinarySerializer.decode(encoded);
|
||||
|
||||
expect(decoded).toEqual(data);
|
||||
});
|
||||
|
||||
it('应该正确编码和解码空数组', () => {
|
||||
const data: any[] = [];
|
||||
const encoded = BinarySerializer.encode(data);
|
||||
const decoded = BinarySerializer.decode(encoded);
|
||||
|
||||
expect(decoded).toEqual(data);
|
||||
});
|
||||
|
||||
it('应该正确编码和解码包含null和undefined的对象', () => {
|
||||
const data = {
|
||||
nullValue: null,
|
||||
undefinedValue: undefined,
|
||||
normalValue: 'test'
|
||||
};
|
||||
const encoded = BinarySerializer.encode(data);
|
||||
const decoded = BinarySerializer.decode(encoded);
|
||||
|
||||
expect(decoded.nullValue).toBeNull();
|
||||
expect(decoded.undefinedValue).toBeUndefined();
|
||||
expect(decoded.normalValue).toBe('test');
|
||||
});
|
||||
|
||||
it('应该正确编码和解码布尔值', () => {
|
||||
const data = {
|
||||
isTrue: true,
|
||||
isFalse: false
|
||||
};
|
||||
const encoded = BinarySerializer.encode(data);
|
||||
const decoded = BinarySerializer.decode(encoded);
|
||||
|
||||
expect(decoded).toEqual(data);
|
||||
});
|
||||
|
||||
it('应该正确编码和解码数字类型', () => {
|
||||
const data = {
|
||||
integer: 42,
|
||||
float: 3.14159,
|
||||
negative: -100,
|
||||
zero: 0,
|
||||
large: 1234567890
|
||||
};
|
||||
const encoded = BinarySerializer.encode(data);
|
||||
const decoded = BinarySerializer.decode(encoded);
|
||||
|
||||
expect(decoded).toEqual(data);
|
||||
});
|
||||
|
||||
it('应该返回Uint8Array类型', () => {
|
||||
const data = { test: 'value' };
|
||||
const encoded = BinarySerializer.encode(data);
|
||||
|
||||
expect(encoded).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
|
||||
it('应该正确处理包含emoji的复杂字符串', () => {
|
||||
const data = {
|
||||
text: '🌟 测试 Test 👍',
|
||||
emoji: '🎮🎯🎲'
|
||||
};
|
||||
const encoded = BinarySerializer.encode(data);
|
||||
const decoded = BinarySerializer.decode(encoded);
|
||||
|
||||
expect(decoded).toEqual(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,342 @@
|
||||
import { AdvancedProfilerCollector } from '../../../src/Utils/Debug/AdvancedProfilerCollector';
|
||||
import { ProfilerSDK } from '../../../src/Utils/Profiler/ProfilerSDK';
|
||||
import { ProfileCategory } from '../../../src/Utils/Profiler/ProfilerTypes';
|
||||
|
||||
describe('AdvancedProfilerCollector', () => {
|
||||
let collector: AdvancedProfilerCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
collector = new AdvancedProfilerCollector();
|
||||
ProfilerSDK.reset();
|
||||
ProfilerSDK.setEnabled(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
describe('collectAdvancedData', () => {
|
||||
test('should collect basic frame data', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestSystem', () => {
|
||||
// Simulate work
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 1000; i++) sum += i;
|
||||
}, ProfileCategory.ECS);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.currentFrame).toBeDefined();
|
||||
expect(data.currentFrame.frameNumber).toBeGreaterThanOrEqual(0);
|
||||
expect(data.currentFrame.fps).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('should collect category stats', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('ECSSystem', () => {}, ProfileCategory.ECS);
|
||||
ProfilerSDK.measure('RenderSystem', () => {}, ProfileCategory.Rendering);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.categoryStats).toBeDefined();
|
||||
expect(data.categoryStats.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should collect hotspots sorted by time', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('FastFunction', () => {}, ProfileCategory.Script);
|
||||
ProfilerSDK.measure('SlowFunction', () => {
|
||||
const start = performance.now();
|
||||
while (performance.now() - start < 2) {
|
||||
// busy wait
|
||||
}
|
||||
}, ProfileCategory.Script);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.hotspots).toBeDefined();
|
||||
expect(data.hotspots.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should include frame time history', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.frameTimeHistory).toBeDefined();
|
||||
expect(data.frameTimeHistory.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should include memory information', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.currentFrame.memory).toBeDefined();
|
||||
expect(data.currentFrame.memory.timestamp).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should include summary statistics', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.summary).toBeDefined();
|
||||
expect(data.summary.totalFrames).toBeGreaterThan(0);
|
||||
expect(typeof data.summary.averageFrameTime).toBe('number');
|
||||
expect(typeof data.summary.minFrameTime).toBe('number');
|
||||
expect(typeof data.summary.maxFrameTime).toBe('number');
|
||||
});
|
||||
|
||||
test('should include long tasks list', () => {
|
||||
const data = collector.collectAdvancedData();
|
||||
expect(data.longTasks).toBeDefined();
|
||||
expect(Array.isArray(data.longTasks)).toBe(true);
|
||||
});
|
||||
|
||||
test('should include memory trend', () => {
|
||||
const data = collector.collectAdvancedData();
|
||||
expect(data.memoryTrend).toBeDefined();
|
||||
expect(Array.isArray(data.memoryTrend)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSelectedFunction', () => {
|
||||
test('should set selected function for call graph', () => {
|
||||
collector.setSelectedFunction('TestFunction');
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
const parentHandle = ProfilerSDK.beginSample('ParentFunction', ProfileCategory.Script);
|
||||
const childHandle = ProfilerSDK.beginSample('TestFunction', ProfileCategory.Script);
|
||||
ProfilerSDK.endSample(childHandle);
|
||||
ProfilerSDK.endSample(parentHandle);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.callGraph).toBeDefined();
|
||||
expect(data.callGraph.currentFunction).toBe('TestFunction');
|
||||
});
|
||||
|
||||
test('should clear selected function with null', () => {
|
||||
collector.setSelectedFunction('TestFunction');
|
||||
collector.setSelectedFunction(null);
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.callGraph.currentFunction).toBeNull();
|
||||
});
|
||||
|
||||
test('should return empty callers/callees when no function selected', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('Test', () => {}, ProfileCategory.Script);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.callGraph.currentFunction).toBeNull();
|
||||
expect(data.callGraph.callers).toEqual([]);
|
||||
expect(data.callGraph.callees).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collectFromLegacyMonitor', () => {
|
||||
test('should handle null performance monitor', () => {
|
||||
const data = collector.collectFromLegacyMonitor(null);
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.currentFrame.frameNumber).toBe(0);
|
||||
expect(data.categoryStats).toEqual([]);
|
||||
expect(data.hotspots).toEqual([]);
|
||||
});
|
||||
|
||||
test('should build data from legacy monitor', () => {
|
||||
const mockMonitor = {
|
||||
getAllSystemStats: () => new Map([
|
||||
['TestSystem', {
|
||||
averageTime: 5,
|
||||
minTime: 2,
|
||||
maxTime: 10,
|
||||
executionCount: 100
|
||||
}]
|
||||
]),
|
||||
getAllSystemData: () => new Map([
|
||||
['TestSystem', {
|
||||
executionTime: 5,
|
||||
entityCount: 50
|
||||
}]
|
||||
])
|
||||
};
|
||||
|
||||
const data = collector.collectFromLegacyMonitor(mockMonitor);
|
||||
|
||||
expect(data.categoryStats.length).toBeGreaterThan(0);
|
||||
expect(data.hotspots.length).toBeGreaterThan(0);
|
||||
expect(data.hotspots[0].name).toBe('TestSystem');
|
||||
});
|
||||
|
||||
test('should calculate percentages correctly', () => {
|
||||
const mockMonitor = {
|
||||
getAllSystemStats: () => new Map([
|
||||
['System1', { averageTime: 10, executionCount: 1 }],
|
||||
['System2', { averageTime: 20, executionCount: 1 }]
|
||||
]),
|
||||
getAllSystemData: () => new Map([
|
||||
['System1', { executionTime: 10 }],
|
||||
['System2', { executionTime: 20 }]
|
||||
])
|
||||
};
|
||||
|
||||
const data = collector.collectFromLegacyMonitor(mockMonitor);
|
||||
|
||||
// Check that percentages are calculated
|
||||
const ecsCat = data.categoryStats.find(c => c.category === 'ECS');
|
||||
expect(ecsCat).toBeDefined();
|
||||
expect(ecsCat!.totalTime).toBe(30);
|
||||
});
|
||||
|
||||
test('should handle empty stats', () => {
|
||||
const mockMonitor = {
|
||||
getAllSystemStats: () => new Map(),
|
||||
getAllSystemData: () => new Map()
|
||||
};
|
||||
|
||||
const data = collector.collectFromLegacyMonitor(mockMonitor);
|
||||
|
||||
expect(data.categoryStats).toEqual([]);
|
||||
expect(data.hotspots).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IAdvancedProfilerData structure', () => {
|
||||
test('should have all required fields', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
// Verify structure
|
||||
expect(data).toHaveProperty('currentFrame');
|
||||
expect(data).toHaveProperty('frameTimeHistory');
|
||||
expect(data).toHaveProperty('categoryStats');
|
||||
expect(data).toHaveProperty('hotspots');
|
||||
expect(data).toHaveProperty('callGraph');
|
||||
expect(data).toHaveProperty('longTasks');
|
||||
expect(data).toHaveProperty('memoryTrend');
|
||||
expect(data).toHaveProperty('summary');
|
||||
|
||||
// Verify currentFrame structure
|
||||
expect(data.currentFrame).toHaveProperty('frameNumber');
|
||||
expect(data.currentFrame).toHaveProperty('frameTime');
|
||||
expect(data.currentFrame).toHaveProperty('fps');
|
||||
expect(data.currentFrame).toHaveProperty('memory');
|
||||
|
||||
// Verify callGraph structure
|
||||
expect(data.callGraph).toHaveProperty('currentFunction');
|
||||
expect(data.callGraph).toHaveProperty('callers');
|
||||
expect(data.callGraph).toHaveProperty('callees');
|
||||
|
||||
// Verify summary structure
|
||||
expect(data.summary).toHaveProperty('totalFrames');
|
||||
expect(data.summary).toHaveProperty('averageFrameTime');
|
||||
expect(data.summary).toHaveProperty('minFrameTime');
|
||||
expect(data.summary).toHaveProperty('maxFrameTime');
|
||||
expect(data.summary).toHaveProperty('p95FrameTime');
|
||||
expect(data.summary).toHaveProperty('p99FrameTime');
|
||||
expect(data.summary).toHaveProperty('currentMemoryMB');
|
||||
expect(data.summary).toHaveProperty('peakMemoryMB');
|
||||
expect(data.summary).toHaveProperty('gcCount');
|
||||
expect(data.summary).toHaveProperty('longTaskCount');
|
||||
});
|
||||
|
||||
test('hotspot items should have correct structure', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestFunction', () => {}, ProfileCategory.Script);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
const hotspot = data.hotspots[0];
|
||||
|
||||
if (hotspot) {
|
||||
expect(hotspot).toHaveProperty('name');
|
||||
expect(hotspot).toHaveProperty('category');
|
||||
expect(hotspot).toHaveProperty('inclusiveTime');
|
||||
expect(hotspot).toHaveProperty('inclusiveTimePercent');
|
||||
expect(hotspot).toHaveProperty('exclusiveTime');
|
||||
expect(hotspot).toHaveProperty('exclusiveTimePercent');
|
||||
expect(hotspot).toHaveProperty('callCount');
|
||||
expect(hotspot).toHaveProperty('avgCallTime');
|
||||
}
|
||||
});
|
||||
|
||||
test('category stats items should have correct structure', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('TestFunction', () => {}, ProfileCategory.ECS);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
const category = data.categoryStats[0];
|
||||
|
||||
if (category) {
|
||||
expect(category).toHaveProperty('category');
|
||||
expect(category).toHaveProperty('totalTime');
|
||||
expect(category).toHaveProperty('percentOfFrame');
|
||||
expect(category).toHaveProperty('sampleCount');
|
||||
expect(category).toHaveProperty('items');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
test('should handle no profiler data', () => {
|
||||
ProfilerSDK.reset();
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data).toBeDefined();
|
||||
expect(data.currentFrame.frameNumber).toBe(0);
|
||||
});
|
||||
|
||||
test('should track peak memory', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
collector.collectAdvancedData();
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
// Peak should be maintained or increased
|
||||
expect(data.summary.peakMemoryMB).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('should handle multiple frames with varying data', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
if (i % 2 === 0) {
|
||||
ProfilerSDK.measure('EvenFrame', () => {}, ProfileCategory.ECS);
|
||||
} else {
|
||||
ProfilerSDK.measure('OddFrame', () => {}, ProfileCategory.Rendering);
|
||||
}
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const data = collector.collectAdvancedData();
|
||||
|
||||
expect(data.frameTimeHistory.length).toBe(10);
|
||||
expect(data.summary.totalFrames).toBe(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,415 @@
|
||||
import { EntityDataCollector } from '../../../src/Utils/Debug/EntityDataCollector';
|
||||
import { Scene } from '../../../src/ECS/Scene';
|
||||
import { Component } from '../../../src/ECS/Component';
|
||||
import { HierarchySystem } from '../../../src/ECS/Systems/HierarchySystem';
|
||||
import { HierarchyComponent } from '../../../src/ECS/Components/HierarchyComponent';
|
||||
import { ECSComponent } from '../../../src/ECS/Decorators';
|
||||
|
||||
@ECSComponent('EDC_Position')
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('EDC_Velocity')
|
||||
class VelocityComponent extends Component {
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('EDC_ComponentWithPrivate')
|
||||
class ComponentWithPrivate extends Component {
|
||||
public publicValue: number = 1;
|
||||
private _privateValue: number = 2;
|
||||
}
|
||||
|
||||
@ECSComponent('EDC_ComponentWithNested')
|
||||
class ComponentWithNested extends Component {
|
||||
public nested = { value: 42 };
|
||||
}
|
||||
|
||||
@ECSComponent('EDC_ComponentWithArray')
|
||||
class ComponentWithArray extends Component {
|
||||
public items = [{ id: 1 }, { id: 2 }];
|
||||
}
|
||||
|
||||
@ECSComponent('EDC_ComponentWithLongString')
|
||||
class ComponentWithLongString extends Component {
|
||||
public longText = 'x'.repeat(300);
|
||||
}
|
||||
|
||||
@ECSComponent('EDC_ComponentWithLargeArray')
|
||||
class ComponentWithLargeArray extends Component {
|
||||
public items = Array.from({ length: 20 }, (_, i) => i);
|
||||
}
|
||||
|
||||
describe('EntityDataCollector', () => {
|
||||
let collector: EntityDataCollector;
|
||||
let scene: Scene;
|
||||
|
||||
beforeEach(() => {
|
||||
collector = new EntityDataCollector();
|
||||
scene = new Scene({ name: 'TestScene' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
scene.end();
|
||||
});
|
||||
|
||||
describe('collectEntityData', () => {
|
||||
test('should return empty data when scene is null', () => {
|
||||
const data = collector.collectEntityData(null);
|
||||
|
||||
expect(data.totalEntities).toBe(0);
|
||||
expect(data.activeEntities).toBe(0);
|
||||
expect(data.pendingAdd).toBe(0);
|
||||
expect(data.pendingRemove).toBe(0);
|
||||
expect(data.entitiesPerArchetype).toEqual([]);
|
||||
expect(data.topEntitiesByComponents).toEqual([]);
|
||||
});
|
||||
|
||||
test('should return empty data when scene is undefined', () => {
|
||||
const data = collector.collectEntityData(undefined);
|
||||
|
||||
expect(data.totalEntities).toBe(0);
|
||||
expect(data.activeEntities).toBe(0);
|
||||
});
|
||||
|
||||
test('should collect entity data from scene', () => {
|
||||
const entity1 = scene.createEntity('Entity1');
|
||||
entity1.addComponent(new PositionComponent(10, 20));
|
||||
|
||||
const entity2 = scene.createEntity('Entity2');
|
||||
entity2.addComponent(new VelocityComponent());
|
||||
|
||||
const data = collector.collectEntityData(scene);
|
||||
|
||||
expect(data.totalEntities).toBe(2);
|
||||
expect(data.activeEntities).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('should collect archetype distribution', () => {
|
||||
const entity1 = scene.createEntity('Entity1');
|
||||
entity1.addComponent(new PositionComponent());
|
||||
|
||||
const entity2 = scene.createEntity('Entity2');
|
||||
entity2.addComponent(new PositionComponent());
|
||||
|
||||
const entity3 = scene.createEntity('Entity3');
|
||||
entity3.addComponent(new VelocityComponent());
|
||||
|
||||
const data = collector.collectEntityData(scene);
|
||||
|
||||
expect(data.entitiesPerArchetype.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRawEntityList', () => {
|
||||
test('should return empty array when scene is null', () => {
|
||||
const list = collector.getRawEntityList(null);
|
||||
expect(list).toEqual([]);
|
||||
});
|
||||
|
||||
test('should return raw entity list from scene', () => {
|
||||
const entity1 = scene.createEntity('Entity1');
|
||||
entity1.addComponent(new PositionComponent(10, 20));
|
||||
entity1.tag = 0x01;
|
||||
|
||||
const entity2 = scene.createEntity('Entity2');
|
||||
entity2.addComponent(new VelocityComponent());
|
||||
|
||||
const list = collector.getRawEntityList(scene);
|
||||
|
||||
expect(list.length).toBe(2);
|
||||
expect(list[0].name).toBe('Entity1');
|
||||
expect(list[0].componentCount).toBe(1);
|
||||
expect(list[0].tag).toBe(0x01);
|
||||
});
|
||||
|
||||
test('should include hierarchy information', () => {
|
||||
const hierarchySystem = new HierarchySystem();
|
||||
scene.addSystem(hierarchySystem);
|
||||
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
hierarchySystem.setParent(child, parent);
|
||||
|
||||
const list = collector.getRawEntityList(scene);
|
||||
const childInfo = list.find(e => e.name === 'Child');
|
||||
|
||||
expect(childInfo).toBeDefined();
|
||||
expect(childInfo!.parentId).toBe(parent.id);
|
||||
expect(childInfo!.depth).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntityDetails', () => {
|
||||
test('should return null when scene is null', () => {
|
||||
const details = collector.getEntityDetails(1, null);
|
||||
expect(details).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null when entity not found', () => {
|
||||
const details = collector.getEntityDetails(9999, scene);
|
||||
expect(details).toBeNull();
|
||||
});
|
||||
|
||||
test('should return entity details', () => {
|
||||
const entity = scene.createEntity('TestEntity');
|
||||
entity.addComponent(new PositionComponent(100, 200));
|
||||
entity.tag = 42;
|
||||
|
||||
const details = collector.getEntityDetails(entity.id, scene);
|
||||
|
||||
expect(details).not.toBeNull();
|
||||
expect(details.componentCount).toBe(1);
|
||||
expect(details.scene).toBeDefined();
|
||||
});
|
||||
|
||||
test('should handle errors gracefully', () => {
|
||||
const details = collector.getEntityDetails(-1, scene);
|
||||
expect(details).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('collectEntityDataWithMemory', () => {
|
||||
test('should return empty data when scene is null', () => {
|
||||
const data = collector.collectEntityDataWithMemory(null);
|
||||
|
||||
expect(data.totalEntities).toBe(0);
|
||||
expect(data.entityHierarchy).toEqual([]);
|
||||
expect(data.entityDetailsMap).toEqual({});
|
||||
});
|
||||
|
||||
test('should collect entity data with memory information', () => {
|
||||
const entity = scene.createEntity('Entity');
|
||||
entity.addComponent(new PositionComponent(10, 20));
|
||||
|
||||
const data = collector.collectEntityDataWithMemory(scene);
|
||||
|
||||
expect(data.totalEntities).toBe(1);
|
||||
expect(data.topEntitiesByComponents.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should include entity details map', () => {
|
||||
const entity = scene.createEntity('Entity');
|
||||
entity.addComponent(new PositionComponent());
|
||||
|
||||
const data = collector.collectEntityDataWithMemory(scene);
|
||||
|
||||
expect(data.entityDetailsMap).toBeDefined();
|
||||
expect(data.entityDetailsMap![entity.id]).toBeDefined();
|
||||
});
|
||||
|
||||
test('should build entity hierarchy tree', () => {
|
||||
const hierarchySystem = new HierarchySystem();
|
||||
scene.addSystem(hierarchySystem);
|
||||
|
||||
const root = scene.createEntity('Root');
|
||||
root.addComponent(new HierarchyComponent());
|
||||
|
||||
const child = scene.createEntity('Child');
|
||||
hierarchySystem.setParent(child, root);
|
||||
|
||||
const data = collector.collectEntityDataWithMemory(scene);
|
||||
|
||||
expect(data.entityHierarchy).toBeDefined();
|
||||
expect(data.entityHierarchy!.length).toBe(1);
|
||||
expect(data.entityHierarchy![0].name).toBe('Root');
|
||||
});
|
||||
});
|
||||
|
||||
describe('estimateEntityMemoryUsage', () => {
|
||||
test('should estimate memory for entity', () => {
|
||||
const entity = scene.createEntity('Entity');
|
||||
entity.addComponent(new PositionComponent(10, 20));
|
||||
|
||||
const memory = collector.estimateEntityMemoryUsage(entity);
|
||||
|
||||
expect(memory).toBeGreaterThanOrEqual(0);
|
||||
expect(typeof memory).toBe('number');
|
||||
});
|
||||
|
||||
test('should return 0 for invalid entity', () => {
|
||||
const memory = collector.estimateEntityMemoryUsage(null);
|
||||
expect(memory).toBe(0);
|
||||
});
|
||||
|
||||
test('should handle errors and return 0', () => {
|
||||
const badEntity = { components: null };
|
||||
const memory = collector.estimateEntityMemoryUsage(badEntity);
|
||||
expect(memory).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateObjectSize', () => {
|
||||
test('should return 0 for null/undefined', () => {
|
||||
expect(collector.calculateObjectSize(null)).toBe(0);
|
||||
expect(collector.calculateObjectSize(undefined)).toBe(0);
|
||||
});
|
||||
|
||||
test('should calculate size for simple object', () => {
|
||||
const obj = { x: 10, y: 20, name: 'test' };
|
||||
const size = collector.calculateObjectSize(obj);
|
||||
|
||||
expect(size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should respect exclude keys', () => {
|
||||
const obj = { x: 10, excluded: 'large string'.repeat(100) };
|
||||
const sizeWithExclude = collector.calculateObjectSize(obj, ['excluded']);
|
||||
const sizeWithoutExclude = collector.calculateObjectSize(obj);
|
||||
|
||||
expect(sizeWithExclude).toBeLessThan(sizeWithoutExclude);
|
||||
});
|
||||
|
||||
test('should handle nested objects with limited depth', () => {
|
||||
const obj = {
|
||||
level1: {
|
||||
level2: {
|
||||
level3: {
|
||||
value: 42
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const size = collector.calculateObjectSize(obj);
|
||||
expect(size).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractComponentDetails', () => {
|
||||
test('should extract component details', () => {
|
||||
const component = new PositionComponent(100, 200);
|
||||
const details = collector.extractComponentDetails([component]);
|
||||
|
||||
expect(details.length).toBe(1);
|
||||
expect(details[0].typeName).toBe('EDC_Position');
|
||||
expect(details[0].properties.x).toBe(100);
|
||||
expect(details[0].properties.y).toBe(200);
|
||||
});
|
||||
|
||||
test('should handle empty components array', () => {
|
||||
const details = collector.extractComponentDetails([]);
|
||||
expect(details).toEqual([]);
|
||||
});
|
||||
|
||||
test('should skip private properties', () => {
|
||||
const component = new ComponentWithPrivate();
|
||||
const details = collector.extractComponentDetails([component]);
|
||||
|
||||
expect(details[0].properties.publicValue).toBe(1);
|
||||
expect(details[0].properties._privateValue).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getComponentProperties', () => {
|
||||
test('should return empty object when scene is null', () => {
|
||||
const props = collector.getComponentProperties(1, 0, null);
|
||||
expect(props).toEqual({});
|
||||
});
|
||||
|
||||
test('should return empty object when entity not found', () => {
|
||||
const props = collector.getComponentProperties(9999, 0, scene);
|
||||
expect(props).toEqual({});
|
||||
});
|
||||
|
||||
test('should return empty object when component index is out of bounds', () => {
|
||||
const entity = scene.createEntity('Entity');
|
||||
entity.addComponent(new PositionComponent());
|
||||
|
||||
const props = collector.getComponentProperties(entity.id, 99, scene);
|
||||
expect(props).toEqual({});
|
||||
});
|
||||
|
||||
test('should return component properties', () => {
|
||||
const entity = scene.createEntity('Entity');
|
||||
entity.addComponent(new PositionComponent(50, 75));
|
||||
|
||||
const props = collector.getComponentProperties(entity.id, 0, scene);
|
||||
|
||||
expect(props.x).toBe(50);
|
||||
expect(props.y).toBe(75);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandLazyObject', () => {
|
||||
test('should return null when scene is null', () => {
|
||||
const result = collector.expandLazyObject(1, 0, 'path', null);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null when entity not found', () => {
|
||||
const result = collector.expandLazyObject(9999, 0, 'path', scene);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null when component index is out of bounds', () => {
|
||||
const entity = scene.createEntity('Entity');
|
||||
entity.addComponent(new PositionComponent());
|
||||
|
||||
const result = collector.expandLazyObject(entity.id, 99, '', scene);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('should expand object at path', () => {
|
||||
const entity = scene.createEntity('Entity');
|
||||
entity.addComponent(new ComponentWithNested());
|
||||
|
||||
const result = collector.expandLazyObject(entity.id, 0, 'nested', scene);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.value).toBe(42);
|
||||
});
|
||||
|
||||
test('should handle array index in path', () => {
|
||||
const entity = scene.createEntity('Entity');
|
||||
entity.addComponent(new ComponentWithArray());
|
||||
|
||||
const result = collector.expandLazyObject(entity.id, 0, 'items[1]', scene);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.id).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
test('should handle scene without entities buffer', () => {
|
||||
const mockScene = {
|
||||
entities: null,
|
||||
getSystem: () => null
|
||||
};
|
||||
|
||||
const data = collector.collectEntityData(mockScene as any);
|
||||
expect(data.totalEntities).toBe(0);
|
||||
});
|
||||
|
||||
test('should handle entity with long string properties', () => {
|
||||
const entity = scene.createEntity('Entity');
|
||||
entity.addComponent(new ComponentWithLongString());
|
||||
|
||||
const details = collector.extractComponentDetails(entity.components);
|
||||
|
||||
expect(details[0].properties.longText).toContain('[长字符串:');
|
||||
});
|
||||
|
||||
test('should handle entity with large arrays', () => {
|
||||
const entity = scene.createEntity('Entity');
|
||||
entity.addComponent(new ComponentWithLargeArray());
|
||||
|
||||
const details = collector.extractComponentDetails(entity.components);
|
||||
|
||||
expect(details[0].properties.items).toBeDefined();
|
||||
expect(details[0].properties.items._isLazyArray).toBe(true);
|
||||
expect(details[0].properties.items._arrayLength).toBe(20);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import { NumberExtension } from '../../../src/Utils/Extensions/NumberExtension';
|
||||
|
||||
describe('NumberExtension - 数字扩展工具类测试', () => {
|
||||
describe('toNumber 方法测试', () => {
|
||||
it('应该能够转换数字类型', () => {
|
||||
expect(NumberExtension.toNumber(42)).toBe(42);
|
||||
expect(NumberExtension.toNumber(0)).toBe(0);
|
||||
expect(NumberExtension.toNumber(-42)).toBe(-42);
|
||||
expect(NumberExtension.toNumber(3.14)).toBe(3.14);
|
||||
expect(NumberExtension.toNumber(-3.14)).toBe(-3.14);
|
||||
});
|
||||
|
||||
it('应该能够转换字符串数字', () => {
|
||||
expect(NumberExtension.toNumber('42')).toBe(42);
|
||||
expect(NumberExtension.toNumber('0')).toBe(0);
|
||||
expect(NumberExtension.toNumber('-42')).toBe(-42);
|
||||
expect(NumberExtension.toNumber('3.14')).toBe(3.14);
|
||||
expect(NumberExtension.toNumber('-3.14')).toBe(-3.14);
|
||||
});
|
||||
|
||||
it('应该能够转换科学计数法字符串', () => {
|
||||
expect(NumberExtension.toNumber('1e5')).toBe(100000);
|
||||
expect(NumberExtension.toNumber('1.5e2')).toBe(150);
|
||||
expect(NumberExtension.toNumber('2e-3')).toBe(0.002);
|
||||
});
|
||||
|
||||
it('应该能够转换十六进制字符串', () => {
|
||||
expect(NumberExtension.toNumber('0xFF')).toBe(255);
|
||||
expect(NumberExtension.toNumber('0x10')).toBe(16);
|
||||
expect(NumberExtension.toNumber('0x0')).toBe(0);
|
||||
});
|
||||
|
||||
it('应该能够转换布尔值', () => {
|
||||
expect(NumberExtension.toNumber(true)).toBe(1);
|
||||
expect(NumberExtension.toNumber(false)).toBe(0);
|
||||
});
|
||||
|
||||
it('undefined 和 null 应该返回0', () => {
|
||||
expect(NumberExtension.toNumber(undefined)).toBe(0);
|
||||
expect(NumberExtension.toNumber(null)).toBe(0);
|
||||
});
|
||||
|
||||
it('应该能够处理空字符串和空白字符串', () => {
|
||||
expect(NumberExtension.toNumber('')).toBe(0);
|
||||
expect(NumberExtension.toNumber(' ')).toBe(0);
|
||||
expect(NumberExtension.toNumber('\t')).toBe(0);
|
||||
expect(NumberExtension.toNumber('\n')).toBe(0);
|
||||
});
|
||||
|
||||
it('无效的字符串应该返回NaN', () => {
|
||||
expect(Number.isNaN(NumberExtension.toNumber('abc'))).toBe(true);
|
||||
expect(Number.isNaN(NumberExtension.toNumber('hello'))).toBe(true);
|
||||
expect(Number.isNaN(NumberExtension.toNumber('12abc'))).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能够转换数组(第一个元素)', () => {
|
||||
expect(NumberExtension.toNumber([42])).toBe(42);
|
||||
expect(NumberExtension.toNumber(['42'])).toBe(42);
|
||||
expect(NumberExtension.toNumber([])).toBe(0);
|
||||
});
|
||||
|
||||
it('应该能够转换Date对象(时间戳)', () => {
|
||||
const date = new Date(2023, 0, 1);
|
||||
const timestamp = date.getTime();
|
||||
expect(NumberExtension.toNumber(date)).toBe(timestamp);
|
||||
});
|
||||
|
||||
it('应该能够处理BigInt转换', () => {
|
||||
expect(NumberExtension.toNumber(BigInt(42))).toBe(42);
|
||||
expect(NumberExtension.toNumber(BigInt(0))).toBe(0);
|
||||
});
|
||||
|
||||
it('应该能够处理Infinity和-Infinity', () => {
|
||||
expect(NumberExtension.toNumber(Infinity)).toBe(Infinity);
|
||||
expect(NumberExtension.toNumber(-Infinity)).toBe(-Infinity);
|
||||
expect(NumberExtension.toNumber('Infinity')).toBe(Infinity);
|
||||
expect(NumberExtension.toNumber('-Infinity')).toBe(-Infinity);
|
||||
});
|
||||
|
||||
it('对象转换应该调用valueOf或toString', () => {
|
||||
const objWithValueOf = {
|
||||
valueOf: () => 42
|
||||
};
|
||||
expect(NumberExtension.toNumber(objWithValueOf)).toBe(42);
|
||||
|
||||
const objWithToString = {
|
||||
toString: () => '123'
|
||||
};
|
||||
expect(NumberExtension.toNumber(objWithToString)).toBe(123);
|
||||
});
|
||||
|
||||
it('复杂对象应该返回NaN', () => {
|
||||
const complexObj = { a: 1, b: 2 };
|
||||
expect(Number.isNaN(NumberExtension.toNumber(complexObj))).toBe(true);
|
||||
});
|
||||
|
||||
it('Symbol转换应该抛出错误', () => {
|
||||
expect(() => {
|
||||
NumberExtension.toNumber(Symbol('test'));
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该处理特殊数值', () => {
|
||||
expect(NumberExtension.toNumber(Number.MAX_VALUE)).toBe(Number.MAX_VALUE);
|
||||
expect(NumberExtension.toNumber(Number.MIN_VALUE)).toBe(Number.MIN_VALUE);
|
||||
expect(NumberExtension.toNumber(Number.MAX_SAFE_INTEGER)).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(NumberExtension.toNumber(Number.MIN_SAFE_INTEGER)).toBe(Number.MIN_SAFE_INTEGER);
|
||||
});
|
||||
|
||||
it('应该处理parseFloat可解析的字符串', () => {
|
||||
// NumberExtension.toNumber使用Number(),不支持parseFloat的部分解析
|
||||
expect(Number.isNaN(NumberExtension.toNumber('42.5px'))).toBe(true);
|
||||
expect(Number.isNaN(NumberExtension.toNumber('100%'))).toBe(true);
|
||||
expect(Number.isNaN(NumberExtension.toNumber('3.14em'))).toBe(true);
|
||||
});
|
||||
|
||||
it('边界情况测试', () => {
|
||||
// 非常大的数字
|
||||
expect(NumberExtension.toNumber('1e308')).toBe(1e308);
|
||||
|
||||
// 非常小的数字
|
||||
expect(NumberExtension.toNumber('1e-308')).toBe(1e-308);
|
||||
|
||||
// 精度问题
|
||||
expect(NumberExtension.toNumber('0.1')).toBe(0.1);
|
||||
expect(NumberExtension.toNumber('0.2')).toBe(0.2);
|
||||
});
|
||||
|
||||
it('应该处理带符号的字符串', () => {
|
||||
expect(NumberExtension.toNumber('+42')).toBe(42);
|
||||
expect(NumberExtension.toNumber('+3.14')).toBe(3.14);
|
||||
expect(NumberExtension.toNumber('-0')).toBe(-0);
|
||||
});
|
||||
|
||||
it('应该处理八进制字符串(不推荐使用)', () => {
|
||||
// 注意:现代JavaScript中八进制字面量是不推荐的
|
||||
expect(NumberExtension.toNumber('010')).toBe(10); // 被当作十进制处理
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('类型兼容性测试', () => {
|
||||
it('应该与Number()函数行为一致', () => {
|
||||
const testValues = [
|
||||
42, '42', true, false, '', ' ',
|
||||
'3.14', 'abc', [], [42], {}, Infinity, -Infinity
|
||||
];
|
||||
|
||||
testValues.forEach(value => {
|
||||
const extensionResult = NumberExtension.toNumber(value);
|
||||
const nativeResult = Number(value);
|
||||
|
||||
if (Number.isNaN(extensionResult) && Number.isNaN(nativeResult)) {
|
||||
// 两个都是NaN,认为相等
|
||||
expect(true).toBe(true);
|
||||
} else {
|
||||
expect(extensionResult).toBe(nativeResult);
|
||||
}
|
||||
});
|
||||
|
||||
// 特殊处理null和undefined的情况
|
||||
expect(NumberExtension.toNumber(null)).toBe(0);
|
||||
expect(NumberExtension.toNumber(undefined)).toBe(0);
|
||||
expect(Number(null)).toBe(0);
|
||||
expect(Number(undefined)).toBeNaN();
|
||||
});
|
||||
|
||||
it('应该正确处理特殊的相等性', () => {
|
||||
// -0 和 +0
|
||||
expect(Object.is(NumberExtension.toNumber('-0'), -0)).toBe(true);
|
||||
expect(Object.is(NumberExtension.toNumber('+0'), +0)).toBe(true);
|
||||
|
||||
// NaN
|
||||
expect(Number.isNaN(NumberExtension.toNumber('notANumber'))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
321
packages/framework/core/tests/Utils/Extensions/TypeUtils.test.ts
Normal file
321
packages/framework/core/tests/Utils/Extensions/TypeUtils.test.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import { TypeUtils } from '../../../src/Utils/Extensions/TypeUtils';
|
||||
|
||||
describe('TypeUtils - 类型工具类测试', () => {
|
||||
// 测试用的类和对象
|
||||
class TestClass {
|
||||
public value: number = 0;
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
if (args.length >= 1) this.value = args[0] as number;
|
||||
}
|
||||
}
|
||||
|
||||
class AnotherTestClass {
|
||||
public name: string = '';
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
if (args.length >= 1) this.name = args[0] as string;
|
||||
}
|
||||
}
|
||||
|
||||
function TestFunction() {
|
||||
// @ts-ignore
|
||||
this.prop = 'test';
|
||||
}
|
||||
|
||||
describe('getType 方法测试', () => {
|
||||
it('应该能够获取基本类型对象的构造函数', () => {
|
||||
expect(TypeUtils.getType(42)).toBe(Number);
|
||||
expect(TypeUtils.getType('hello')).toBe(String);
|
||||
expect(TypeUtils.getType(true)).toBe(Boolean);
|
||||
expect(TypeUtils.getType(false)).toBe(Boolean);
|
||||
});
|
||||
|
||||
it('应该能够获取数组的构造函数', () => {
|
||||
expect(TypeUtils.getType([])).toBe(Array);
|
||||
expect(TypeUtils.getType([1, 2, 3])).toBe(Array);
|
||||
expect(TypeUtils.getType(new Array())).toBe(Array);
|
||||
});
|
||||
|
||||
it('应该能够获取对象的构造函数', () => {
|
||||
expect(TypeUtils.getType({})).toBe(Object);
|
||||
expect(TypeUtils.getType(new Object())).toBe(Object);
|
||||
});
|
||||
|
||||
it('应该能够获取Date对象的构造函数', () => {
|
||||
expect(TypeUtils.getType(new Date())).toBe(Date);
|
||||
});
|
||||
|
||||
it('应该能够获取RegExp对象的构造函数', () => {
|
||||
expect(TypeUtils.getType(/test/)).toBe(RegExp);
|
||||
expect(TypeUtils.getType(new RegExp('test'))).toBe(RegExp);
|
||||
});
|
||||
|
||||
it('应该能够获取Error对象的构造函数', () => {
|
||||
expect(TypeUtils.getType(new Error())).toBe(Error);
|
||||
expect(TypeUtils.getType(new TypeError())).toBe(TypeError);
|
||||
expect(TypeUtils.getType(new ReferenceError())).toBe(ReferenceError);
|
||||
});
|
||||
|
||||
it('应该能够获取Map和Set的构造函数', () => {
|
||||
expect(TypeUtils.getType(new Map())).toBe(Map);
|
||||
expect(TypeUtils.getType(new Set())).toBe(Set);
|
||||
expect(TypeUtils.getType(new WeakMap())).toBe(WeakMap);
|
||||
expect(TypeUtils.getType(new WeakSet())).toBe(WeakSet);
|
||||
});
|
||||
|
||||
it('应该能够获取Promise的构造函数', () => {
|
||||
const promise = Promise.resolve(42);
|
||||
expect(TypeUtils.getType(promise)).toBe(Promise);
|
||||
});
|
||||
|
||||
it('应该能够获取自定义类实例的构造函数', () => {
|
||||
const instance = new TestClass(42);
|
||||
expect(TypeUtils.getType(instance)).toBe(TestClass);
|
||||
});
|
||||
|
||||
it('应该能够区分不同的自定义类', () => {
|
||||
const testInstance = new TestClass(42);
|
||||
const anotherInstance = new AnotherTestClass('test');
|
||||
|
||||
expect(TypeUtils.getType(testInstance)).toBe(TestClass);
|
||||
expect(TypeUtils.getType(anotherInstance)).toBe(AnotherTestClass);
|
||||
expect(TypeUtils.getType(testInstance)).not.toBe(AnotherTestClass);
|
||||
});
|
||||
|
||||
it('应该能够获取函数构造的对象的构造函数', () => {
|
||||
// @ts-ignore
|
||||
const instance = new TestFunction();
|
||||
expect(TypeUtils.getType(instance)).toBe(TestFunction);
|
||||
});
|
||||
|
||||
it('应该能够获取内置类型包装器的构造函数', () => {
|
||||
expect(TypeUtils.getType(new Number(42))).toBe(Number);
|
||||
expect(TypeUtils.getType(new String('hello'))).toBe(String);
|
||||
expect(TypeUtils.getType(new Boolean(true))).toBe(Boolean);
|
||||
});
|
||||
|
||||
it('应该能够获取Symbol的构造函数', () => {
|
||||
const sym = Symbol('test');
|
||||
expect(TypeUtils.getType(sym)).toBe(Symbol);
|
||||
});
|
||||
|
||||
it('应该能够获取BigInt的构造函数', () => {
|
||||
const bigInt = BigInt(42);
|
||||
expect(TypeUtils.getType(bigInt)).toBe(BigInt);
|
||||
});
|
||||
|
||||
it('应该处理具有修改过构造函数的对象', () => {
|
||||
const obj = {};
|
||||
// @ts-ignore - 测试边界情况
|
||||
obj.constructor = TestClass;
|
||||
expect(TypeUtils.getType(obj)).toBe(TestClass);
|
||||
});
|
||||
|
||||
it('应该处理继承关系', () => {
|
||||
class Parent {
|
||||
public value: number = 0;
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
if (args.length >= 1) this.value = args[0] as number;
|
||||
}
|
||||
}
|
||||
|
||||
class Child extends Parent {
|
||||
public name: string = '';
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
super(args[0]);
|
||||
if (args.length >= 2) this.name = args[1] as string;
|
||||
}
|
||||
}
|
||||
|
||||
const childInstance = new Child(42, 'test');
|
||||
expect(TypeUtils.getType(childInstance)).toBe(Child);
|
||||
expect(TypeUtils.getType(childInstance)).not.toBe(Parent);
|
||||
});
|
||||
|
||||
it('应该处理原型链修改的情况', () => {
|
||||
class Original {}
|
||||
class Modified {}
|
||||
|
||||
const instance = new Original();
|
||||
// 修改原型链
|
||||
Object.setPrototypeOf(instance, Modified.prototype);
|
||||
|
||||
// 构造函数属性应该仍然指向Modified
|
||||
expect(TypeUtils.getType(instance)).toBe(Modified);
|
||||
});
|
||||
|
||||
it('应该处理null原型的对象', () => {
|
||||
const nullProtoObj = Object.create(null);
|
||||
// 没有constructor属性的对象会返回undefined
|
||||
const result = TypeUtils.getType(nullProtoObj);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该处理ArrayBuffer和TypedArray', () => {
|
||||
expect(TypeUtils.getType(new ArrayBuffer(8))).toBe(ArrayBuffer);
|
||||
expect(TypeUtils.getType(new Uint8Array(8))).toBe(Uint8Array);
|
||||
expect(TypeUtils.getType(new Int32Array(8))).toBe(Int32Array);
|
||||
expect(TypeUtils.getType(new Float64Array(8))).toBe(Float64Array);
|
||||
});
|
||||
|
||||
it('应该处理生成器函数和生成器对象', () => {
|
||||
function* generatorFunction() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
}
|
||||
|
||||
const generator = generatorFunction();
|
||||
expect(TypeUtils.getType(generator)).toBe(Object.getPrototypeOf(generator).constructor);
|
||||
});
|
||||
|
||||
it('应该处理async函数返回的Promise', () => {
|
||||
async function asyncFunction() {
|
||||
return 42;
|
||||
}
|
||||
|
||||
const asyncResult = asyncFunction();
|
||||
expect(TypeUtils.getType(asyncResult)).toBe(Promise);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况和错误处理', () => {
|
||||
it('应该处理undefined输入', () => {
|
||||
expect(() => {
|
||||
TypeUtils.getType(undefined);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该处理null输入', () => {
|
||||
expect(() => {
|
||||
TypeUtils.getType(null);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('应该处理构造函数被删除的对象', () => {
|
||||
const obj = new TestClass();
|
||||
// @ts-ignore - 测试边界情况
|
||||
delete obj.constructor;
|
||||
|
||||
// 应该回退到原型链上的constructor
|
||||
expect(TypeUtils.getType(obj)).toBe(TestClass);
|
||||
});
|
||||
|
||||
it('应该处理constructor属性被重写为非函数的情况', () => {
|
||||
const obj = {};
|
||||
// @ts-ignore - 测试边界情况
|
||||
obj.constructor = 'not a function';
|
||||
|
||||
expect(TypeUtils.getType(obj)).toBe('not a function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能测试', () => {
|
||||
it('大量类型获取应该高效', () => {
|
||||
const testObjects = [
|
||||
42, 'string', true, [], {}, new Date(), new TestClass(),
|
||||
new Map(), new Set(), Symbol('test'), BigInt(42)
|
||||
];
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
testObjects.forEach(obj => {
|
||||
TypeUtils.getType(obj);
|
||||
});
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
expect(endTime - startTime).toBeLessThan(100); // 应该在100ms内完成
|
||||
});
|
||||
});
|
||||
|
||||
describe('实际使用场景测试', () => {
|
||||
it('应该能够用于类型检查', () => {
|
||||
function isInstanceOf(obj: any, Constructor: any): boolean {
|
||||
return TypeUtils.getType(obj) === Constructor;
|
||||
}
|
||||
|
||||
expect(isInstanceOf(42, Number)).toBe(true);
|
||||
expect(isInstanceOf('hello', String)).toBe(true);
|
||||
expect(isInstanceOf([], Array)).toBe(true);
|
||||
expect(isInstanceOf(new TestClass(), TestClass)).toBe(true);
|
||||
expect(isInstanceOf(new TestClass(), AnotherTestClass)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能够用于多态类型识别', () => {
|
||||
class Animal {
|
||||
public name: string = '';
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
if (args.length >= 1) this.name = args[0] as string;
|
||||
}
|
||||
}
|
||||
|
||||
class Dog extends Animal {
|
||||
public breed: string = '';
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
super(args[0]);
|
||||
if (args.length >= 2) this.breed = args[1] as string;
|
||||
}
|
||||
}
|
||||
|
||||
class Cat extends Animal {
|
||||
public color: string = '';
|
||||
|
||||
constructor(...args: unknown[]) {
|
||||
super(args[0]);
|
||||
if (args.length >= 2) this.color = args[1] as string;
|
||||
}
|
||||
}
|
||||
|
||||
const animals = [
|
||||
new Dog('Buddy', 'Golden Retriever'),
|
||||
new Cat('Whiskers', 'Orange'),
|
||||
new Animal('Generic')
|
||||
];
|
||||
|
||||
const types = animals.map(animal => TypeUtils.getType(animal));
|
||||
expect(types).toEqual([Dog, Cat, Animal]);
|
||||
});
|
||||
|
||||
it('应该能够用于工厂模式', () => {
|
||||
class ComponentFactory {
|
||||
static create(type: any, ...args: any[]) {
|
||||
return new type(...args);
|
||||
}
|
||||
|
||||
static getTypeName(instance: any): string {
|
||||
return TypeUtils.getType(instance).name;
|
||||
}
|
||||
}
|
||||
|
||||
const testInstance = ComponentFactory.create(TestClass, 42);
|
||||
expect(testInstance).toBeInstanceOf(TestClass);
|
||||
expect(testInstance.value).toBe(42);
|
||||
expect(ComponentFactory.getTypeName(testInstance)).toBe('TestClass');
|
||||
});
|
||||
|
||||
it('应该能够用于序列化/反序列化的类型信息', () => {
|
||||
function serialize(obj: any): string {
|
||||
return JSON.stringify({
|
||||
type: TypeUtils.getType(obj).name,
|
||||
data: obj
|
||||
});
|
||||
}
|
||||
|
||||
function getTypeFromSerialized(serialized: string): string {
|
||||
return JSON.parse(serialized).type;
|
||||
}
|
||||
|
||||
const testObj = new TestClass(42);
|
||||
const serialized = serialize(testObj);
|
||||
const typeName = getTypeFromSerialized(serialized);
|
||||
|
||||
expect(typeName).toBe('TestClass');
|
||||
});
|
||||
});
|
||||
});
|
||||
277
packages/framework/core/tests/Utils/Logger.test.ts
Normal file
277
packages/framework/core/tests/Utils/Logger.test.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import {
|
||||
ConsoleLogger,
|
||||
LogLevel,
|
||||
createLogger,
|
||||
LoggerManager,
|
||||
setLoggerColors,
|
||||
resetLoggerColors,
|
||||
Colors
|
||||
} from '../../src/Utils/Logger';
|
||||
|
||||
describe('Logger', () => {
|
||||
describe('ConsoleLogger', () => {
|
||||
let consoleSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleSpy = jest.spyOn(console, 'info').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('应该使用绿色(green)颜色输出INFO级别日志', () => {
|
||||
const logger = new ConsoleLogger({
|
||||
level: LogLevel.Info,
|
||||
enableColors: true,
|
||||
enableTimestamp: false
|
||||
});
|
||||
|
||||
logger.info('测试消息');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('\x1b[32m[INFO] 测试消息\x1b[0m');
|
||||
});
|
||||
|
||||
it('应该在颜色禁用时输出纯文本', () => {
|
||||
const logger = new ConsoleLogger({
|
||||
level: LogLevel.Info,
|
||||
enableColors: false,
|
||||
enableTimestamp: false
|
||||
});
|
||||
|
||||
logger.info('测试消息');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('[INFO] 测试消息');
|
||||
});
|
||||
|
||||
it('应该正确设置不同日志级别的颜色', () => {
|
||||
const debugSpy = jest.spyOn(console, 'debug').mockImplementation();
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
const logger = new ConsoleLogger({
|
||||
level: LogLevel.Debug,
|
||||
enableColors: true,
|
||||
enableTimestamp: false
|
||||
});
|
||||
|
||||
logger.debug('调试消息');
|
||||
logger.info('信息消息');
|
||||
logger.warn('警告消息');
|
||||
logger.error('错误消息');
|
||||
|
||||
expect(debugSpy).toHaveBeenCalledWith('\x1b[90m[DEBUG] 调试消息\x1b[0m');
|
||||
expect(consoleSpy).toHaveBeenCalledWith('\x1b[32m[INFO] 信息消息\x1b[0m');
|
||||
expect(warnSpy).toHaveBeenCalledWith('\x1b[33m[WARN] 警告消息\x1b[0m');
|
||||
expect(errorSpy).toHaveBeenCalledWith('\x1b[31m[ERROR] 错误消息\x1b[0m');
|
||||
|
||||
debugSpy.mockRestore();
|
||||
warnSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('应该正确添加前缀和时间戳', () => {
|
||||
const logger = new ConsoleLogger({
|
||||
level: LogLevel.Info,
|
||||
enableColors: false,
|
||||
enableTimestamp: true,
|
||||
prefix: 'TestLogger'
|
||||
});
|
||||
|
||||
logger.info('测试消息');
|
||||
|
||||
const call = consoleSpy.mock.calls[0][0];
|
||||
expect(call).toMatch(/\[INFO\] \[TestLogger\] \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] 测试消息/);
|
||||
});
|
||||
|
||||
it('应该支持自定义颜色配置', () => {
|
||||
const logger = new ConsoleLogger({
|
||||
level: LogLevel.Info,
|
||||
enableColors: true,
|
||||
enableTimestamp: false,
|
||||
colors: {
|
||||
info: Colors.CYAN
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('测试消息');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('\x1b[36m[INFO] 测试消息\x1b[0m');
|
||||
});
|
||||
|
||||
it('应该支持运行时修改颜色配置', () => {
|
||||
const logger = new ConsoleLogger({
|
||||
level: LogLevel.Info,
|
||||
enableColors: true,
|
||||
enableTimestamp: false
|
||||
});
|
||||
|
||||
logger.setColors({
|
||||
info: Colors.BRIGHT_BLUE
|
||||
});
|
||||
|
||||
logger.info('测试消息');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('\x1b[94m[INFO] 测试消息\x1b[0m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLogger', () => {
|
||||
it('应该创建具有指定名称的logger', () => {
|
||||
const logger = createLogger('TestLogger');
|
||||
const consoleSpy = jest.spyOn(console, 'info').mockImplementation();
|
||||
|
||||
logger.info('测试消息');
|
||||
|
||||
const call = consoleSpy.mock.calls[0][0];
|
||||
expect(call).toContain('[TestLogger]');
|
||||
expect(call).toContain('[INFO]');
|
||||
expect(call).toContain('测试消息');
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('LoggerManager', () => {
|
||||
it('应该返回相同名称的相同logger实例', () => {
|
||||
const manager = LoggerManager.getInstance();
|
||||
const logger1 = manager.getLogger('TestLogger');
|
||||
const logger2 = manager.getLogger('TestLogger');
|
||||
|
||||
expect(logger1).toBe(logger2);
|
||||
});
|
||||
|
||||
it('应该正确创建子logger', () => {
|
||||
const manager = LoggerManager.getInstance();
|
||||
const childLogger = manager.createChildLogger('Parent', 'Child');
|
||||
const consoleSpy = jest.spyOn(console, 'info').mockImplementation();
|
||||
|
||||
childLogger.info('测试消息');
|
||||
|
||||
const call = consoleSpy.mock.calls[0][0];
|
||||
expect(call).toContain('[Parent.Child]');
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('LoggerManager - 自定义工厂', () => {
|
||||
let manager: LoggerManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = LoggerManager.getInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 重置工厂,恢复默认 ConsoleLogger
|
||||
(manager as any)._loggerFactory = undefined;
|
||||
(manager as any)._defaultLogger = undefined;
|
||||
(manager as any)._loggers.clear();
|
||||
});
|
||||
|
||||
it('应该支持设置自定义日志器工厂', () => {
|
||||
const mockLogger = {
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn()
|
||||
};
|
||||
|
||||
manager.setLoggerFactory(() => mockLogger);
|
||||
const logger = manager.getLogger('CustomLogger');
|
||||
|
||||
expect(logger).toBe(mockLogger);
|
||||
});
|
||||
|
||||
it('应该将日志器名称传递给工厂方法', () => {
|
||||
const factorySpy = jest.fn(() => ({
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn()
|
||||
}));
|
||||
|
||||
manager.setLoggerFactory(factorySpy);
|
||||
manager.getLogger('TestLogger');
|
||||
|
||||
expect(factorySpy).toHaveBeenCalledWith('TestLogger');
|
||||
});
|
||||
|
||||
it('应该在设置工厂后清空已创建的日志器', () => {
|
||||
const logger1 = manager.getLogger('TestLogger');
|
||||
|
||||
manager.setLoggerFactory(() => ({
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn()
|
||||
}));
|
||||
|
||||
const logger2 = manager.getLogger('TestLogger');
|
||||
expect(logger2).not.toBe(logger1);
|
||||
});
|
||||
|
||||
it('应该延迟创建默认日志器直到首次使用', () => {
|
||||
const factorySpy = jest.fn(() => ({
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn()
|
||||
}));
|
||||
|
||||
manager.setLoggerFactory(factorySpy);
|
||||
// 此时不应该调用工厂
|
||||
expect(factorySpy).not.toHaveBeenCalled();
|
||||
|
||||
// 获取默认日志器时才调用
|
||||
manager.getLogger();
|
||||
expect(factorySpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('全局颜色配置', () => {
|
||||
let consoleSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleSpy = jest.spyOn(console, 'info').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
resetLoggerColors();
|
||||
});
|
||||
|
||||
it('应该支持全局设置颜色配置', () => {
|
||||
const logger = createLogger('TestLogger');
|
||||
|
||||
setLoggerColors({
|
||||
info: Colors.MAGENTA
|
||||
});
|
||||
|
||||
logger.info('测试消息');
|
||||
|
||||
const call = consoleSpy.mock.calls[0][0];
|
||||
expect(call).toContain('\x1b[35m');
|
||||
expect(call).toContain('\x1b[0m');
|
||||
});
|
||||
|
||||
it('应该支持重置颜色配置为默认值', () => {
|
||||
const logger = createLogger('TestLogger');
|
||||
|
||||
setLoggerColors({
|
||||
info: Colors.MAGENTA
|
||||
});
|
||||
|
||||
resetLoggerColors();
|
||||
|
||||
logger.info('测试消息');
|
||||
|
||||
const call = consoleSpy.mock.calls[0][0];
|
||||
expect(call).toContain('\x1b[32m');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,417 @@
|
||||
import { AutoProfiler, Profile, ProfileClass } from '../../../src/Utils/Profiler/AutoProfiler';
|
||||
import { ProfilerSDK } from '../../../src/Utils/Profiler/ProfilerSDK';
|
||||
import { ProfileCategory } from '../../../src/Utils/Profiler/ProfilerTypes';
|
||||
|
||||
describe('AutoProfiler', () => {
|
||||
beforeEach(() => {
|
||||
AutoProfiler.resetInstance();
|
||||
ProfilerSDK.reset();
|
||||
ProfilerSDK.setEnabled(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
AutoProfiler.resetInstance();
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
describe('getInstance', () => {
|
||||
test('should return singleton instance', () => {
|
||||
const instance1 = AutoProfiler.getInstance();
|
||||
const instance2 = AutoProfiler.getInstance();
|
||||
expect(instance1).toBe(instance2);
|
||||
});
|
||||
|
||||
test('should accept custom config', () => {
|
||||
const instance = AutoProfiler.getInstance({ minDuration: 1.0 });
|
||||
expect(instance).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetInstance', () => {
|
||||
test('should reset the singleton instance', () => {
|
||||
const instance1 = AutoProfiler.getInstance();
|
||||
AutoProfiler.resetInstance();
|
||||
const instance2 = AutoProfiler.getInstance();
|
||||
expect(instance1).not.toBe(instance2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEnabled', () => {
|
||||
test('should enable/disable auto profiling', () => {
|
||||
AutoProfiler.setEnabled(false);
|
||||
const instance = AutoProfiler.getInstance();
|
||||
expect(instance).toBeDefined();
|
||||
|
||||
AutoProfiler.setEnabled(true);
|
||||
expect(instance).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('wrapFunction', () => {
|
||||
test('should wrap a synchronous function', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const originalFn = (a: number, b: number) => a + b;
|
||||
const wrappedFn = AutoProfiler.wrapFunction(originalFn, 'add', ProfileCategory.Custom);
|
||||
|
||||
const result = wrappedFn(2, 3);
|
||||
expect(result).toBe(5);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
|
||||
test('should preserve function behavior', () => {
|
||||
const originalFn = (x: number) => x * 2;
|
||||
const wrappedFn = AutoProfiler.wrapFunction(originalFn, 'double', ProfileCategory.Script);
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
expect(wrappedFn(5)).toBe(10);
|
||||
expect(wrappedFn(0)).toBe(0);
|
||||
expect(wrappedFn(-3)).toBe(-6);
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
|
||||
test('should handle async functions', async () => {
|
||||
const asyncFn = async (x: number) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
return x * 2;
|
||||
};
|
||||
|
||||
const wrappedFn = AutoProfiler.wrapFunction(asyncFn, 'asyncDouble', ProfileCategory.Script);
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
const result = await wrappedFn(5);
|
||||
expect(result).toBe(10);
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
|
||||
test('should handle function that throws error', () => {
|
||||
const errorFn = () => {
|
||||
throw new Error('Test error');
|
||||
};
|
||||
|
||||
const wrappedFn = AutoProfiler.wrapFunction(errorFn, 'errorFn', ProfileCategory.Script);
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
expect(() => wrappedFn()).toThrow('Test error');
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
|
||||
test('should return original function when disabled', () => {
|
||||
AutoProfiler.setEnabled(false);
|
||||
|
||||
const originalFn = (x: number) => x + 1;
|
||||
const wrappedFn = AutoProfiler.wrapFunction(originalFn, 'increment', ProfileCategory.Script);
|
||||
|
||||
expect(wrappedFn(5)).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('wrapInstance', () => {
|
||||
test('should wrap all methods of an object', () => {
|
||||
class Calculator {
|
||||
add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
subtract(a: number, b: number): number {
|
||||
return a - b;
|
||||
}
|
||||
}
|
||||
|
||||
const calc = new Calculator();
|
||||
AutoProfiler.wrapInstance(calc, 'Calculator', ProfileCategory.Script);
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
expect(calc.add(5, 3)).toBe(8);
|
||||
expect(calc.subtract(5, 3)).toBe(2);
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
|
||||
test('should not wrap already wrapped objects', () => {
|
||||
class MyClass {
|
||||
getValue(): number {
|
||||
return 42;
|
||||
}
|
||||
}
|
||||
|
||||
const obj = new MyClass();
|
||||
AutoProfiler.wrapInstance(obj, 'MyClass', ProfileCategory.Custom);
|
||||
AutoProfiler.wrapInstance(obj, 'MyClass', ProfileCategory.Custom);
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
expect(obj.getValue()).toBe(42);
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
|
||||
test('should return object unchanged when disabled', () => {
|
||||
AutoProfiler.setEnabled(false);
|
||||
|
||||
class MyClass {
|
||||
getValue(): number {
|
||||
return 42;
|
||||
}
|
||||
}
|
||||
|
||||
const obj = new MyClass();
|
||||
const wrapped = AutoProfiler.wrapInstance(obj, 'MyClass', ProfileCategory.Custom);
|
||||
|
||||
expect(wrapped).toBe(obj);
|
||||
});
|
||||
|
||||
test('should exclude methods matching exclude patterns', () => {
|
||||
class MyClass {
|
||||
getValue(): number {
|
||||
return 42;
|
||||
}
|
||||
_privateMethod(): number {
|
||||
return 1;
|
||||
}
|
||||
getName(): string {
|
||||
return 'test';
|
||||
}
|
||||
isValid(): boolean {
|
||||
return true;
|
||||
}
|
||||
hasData(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const obj = new MyClass();
|
||||
AutoProfiler.wrapInstance(obj, 'MyClass', ProfileCategory.Custom);
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
expect(obj.getValue()).toBe(42);
|
||||
expect(obj._privateMethod()).toBe(1);
|
||||
expect(obj.getName()).toBe('test');
|
||||
expect(obj.isValid()).toBe(true);
|
||||
expect(obj.hasData()).toBe(true);
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerClass', () => {
|
||||
test('should register a class for auto profiling', () => {
|
||||
class MySystem {
|
||||
update(): void {
|
||||
// Do something
|
||||
}
|
||||
}
|
||||
|
||||
const RegisteredClass = AutoProfiler.registerClass(MySystem, ProfileCategory.ECS);
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
const instance = new RegisteredClass();
|
||||
instance.update();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
expect(instance).toBeInstanceOf(MySystem);
|
||||
});
|
||||
|
||||
test('should accept custom class name', () => {
|
||||
class MySystem {
|
||||
process(): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const RegisteredClass = AutoProfiler.registerClass(MySystem, ProfileCategory.ECS, 'CustomSystem');
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
const instance = new RegisteredClass();
|
||||
expect(instance.process()).toBe(1);
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sampling profiler', () => {
|
||||
test('should start and stop sampling', () => {
|
||||
AutoProfiler.startSampling();
|
||||
const samples = AutoProfiler.stopSampling();
|
||||
|
||||
expect(Array.isArray(samples)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return empty array when sampling was never started', () => {
|
||||
const samples = AutoProfiler.stopSampling();
|
||||
expect(samples).toEqual([]);
|
||||
});
|
||||
|
||||
test('should collect samples during execution', async () => {
|
||||
AutoProfiler.startSampling();
|
||||
|
||||
// Do some work
|
||||
for (let i = 0; i < 100; i++) {
|
||||
Math.sqrt(i);
|
||||
}
|
||||
|
||||
// Wait a bit for samples to accumulate
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const samples = AutoProfiler.stopSampling();
|
||||
expect(Array.isArray(samples)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispose', () => {
|
||||
test('should clean up resources', () => {
|
||||
const instance = AutoProfiler.getInstance();
|
||||
instance.startSampling();
|
||||
instance.dispose();
|
||||
|
||||
// After dispose, stopping sampling should return empty array
|
||||
const samples = instance.stopSampling();
|
||||
expect(samples).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('minDuration filtering', () => {
|
||||
test('should respect minDuration setting', () => {
|
||||
AutoProfiler.resetInstance();
|
||||
const instance = AutoProfiler.getInstance({ minDuration: 1000 });
|
||||
|
||||
const quickFn = () => 1;
|
||||
const wrappedFn = instance.wrapFunction(quickFn, 'quickFn', ProfileCategory.Script);
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
expect(wrappedFn()).toBe(1);
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('@Profile decorator', () => {
|
||||
beforeEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
ProfilerSDK.setEnabled(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
test('should profile decorated methods', () => {
|
||||
class TestClass {
|
||||
@Profile()
|
||||
calculate(): number {
|
||||
return 42;
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new TestClass();
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
const result = instance.calculate();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
|
||||
test('should use custom name when provided', () => {
|
||||
class TestClass {
|
||||
@Profile('CustomMethodName', ProfileCategory.Physics)
|
||||
compute(): number {
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new TestClass();
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
expect(instance.compute()).toBe(100);
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
|
||||
test('should handle async methods', async () => {
|
||||
class TestClass {
|
||||
@Profile()
|
||||
async asyncMethod(): Promise<number> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
return 99;
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new TestClass();
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
const result = await instance.asyncMethod();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
expect(result).toBe(99);
|
||||
});
|
||||
|
||||
test('should handle method that throws error', () => {
|
||||
class TestClass {
|
||||
@Profile()
|
||||
throwingMethod(): void {
|
||||
throw new Error('Decorated error');
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new TestClass();
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
expect(() => instance.throwingMethod()).toThrow('Decorated error');
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
|
||||
test('should skip profiling when ProfilerSDK is disabled', () => {
|
||||
ProfilerSDK.setEnabled(false);
|
||||
|
||||
class TestClass {
|
||||
@Profile()
|
||||
simpleMethod(): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new TestClass();
|
||||
expect(instance.simpleMethod()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('@ProfileClass decorator', () => {
|
||||
beforeEach(() => {
|
||||
AutoProfiler.resetInstance();
|
||||
ProfilerSDK.reset();
|
||||
ProfilerSDK.setEnabled(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
AutoProfiler.resetInstance();
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
test('should profile all methods of decorated class', () => {
|
||||
@ProfileClass(ProfileCategory.ECS)
|
||||
class GameSystem {
|
||||
update(): void {
|
||||
// Update logic
|
||||
}
|
||||
|
||||
render(): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
const system = new GameSystem();
|
||||
system.update();
|
||||
expect(system.render()).toBe(1);
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
|
||||
test('should use default category when not specified', () => {
|
||||
@ProfileClass()
|
||||
class DefaultSystem {
|
||||
process(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
const system = new DefaultSystem();
|
||||
expect(system.process()).toBe(true);
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
});
|
||||
370
packages/framework/core/tests/Utils/Profiler/ProfilerSDK.test.ts
Normal file
370
packages/framework/core/tests/Utils/Profiler/ProfilerSDK.test.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
import { ProfilerSDK } from '../../../src/Utils/Profiler/ProfilerSDK';
|
||||
import {
|
||||
ProfileCategory,
|
||||
DEFAULT_PROFILER_CONFIG
|
||||
} from '../../../src/Utils/Profiler/ProfilerTypes';
|
||||
|
||||
describe('ProfilerSDK', () => {
|
||||
beforeEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
ProfilerSDK.setEnabled(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ProfilerSDK.reset();
|
||||
});
|
||||
|
||||
describe('Configuration', () => {
|
||||
test('should be disabled by default after resetInstance', () => {
|
||||
ProfilerSDK.resetInstance();
|
||||
expect(ProfilerSDK.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
test('should enable and disable correctly', () => {
|
||||
ProfilerSDK.setEnabled(true);
|
||||
expect(ProfilerSDK.isEnabled()).toBe(true);
|
||||
|
||||
ProfilerSDK.setEnabled(false);
|
||||
expect(ProfilerSDK.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
test('should use default config values', () => {
|
||||
expect(DEFAULT_PROFILER_CONFIG.enabled).toBe(false);
|
||||
expect(DEFAULT_PROFILER_CONFIG.maxFrameHistory).toBe(300);
|
||||
expect(DEFAULT_PROFILER_CONFIG.maxSampleDepth).toBe(32);
|
||||
expect(DEFAULT_PROFILER_CONFIG.collectMemory).toBe(true);
|
||||
expect(DEFAULT_PROFILER_CONFIG.detectLongTasks).toBe(true);
|
||||
expect(DEFAULT_PROFILER_CONFIG.longTaskThreshold).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sample Operations', () => {
|
||||
test('should begin and end sample', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
const handle = ProfilerSDK.beginSample('TestSample', ProfileCategory.Custom);
|
||||
expect(handle).not.toBeNull();
|
||||
expect(handle?.name).toBe('TestSample');
|
||||
expect(handle?.category).toBe(ProfileCategory.Custom);
|
||||
|
||||
ProfilerSDK.endSample(handle);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame).not.toBeNull();
|
||||
expect(frame?.samples.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should handle nested samples', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const outerHandle = ProfilerSDK.beginSample('OuterSample', ProfileCategory.ECS);
|
||||
const innerHandle = ProfilerSDK.beginSample('InnerSample', ProfileCategory.Script);
|
||||
|
||||
expect(innerHandle?.depth).toBe(1);
|
||||
expect(innerHandle?.parentId).toBe(outerHandle?.id);
|
||||
|
||||
ProfilerSDK.endSample(innerHandle);
|
||||
ProfilerSDK.endSample(outerHandle);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.samples.length).toBe(2);
|
||||
});
|
||||
|
||||
test('should return null when disabled', () => {
|
||||
ProfilerSDK.setEnabled(false);
|
||||
const handle = ProfilerSDK.beginSample('TestSample');
|
||||
expect(handle).toBeNull();
|
||||
});
|
||||
|
||||
test('should handle null handle in endSample gracefully', () => {
|
||||
expect(() => ProfilerSDK.endSample(null)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('measure() wrapper', () => {
|
||||
test('should measure synchronous function execution', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const result = ProfilerSDK.measure('TestFunction', () => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 100; i++) sum += i;
|
||||
return sum;
|
||||
}, ProfileCategory.Script);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
expect(result).toBe(4950);
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
const sample = frame?.samples.find((s) => s.name === 'TestFunction');
|
||||
expect(sample).toBeDefined();
|
||||
expect(sample?.category).toBe(ProfileCategory.Script);
|
||||
});
|
||||
|
||||
test('should propagate exceptions from measured function', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
expect(() => {
|
||||
ProfilerSDK.measure('ThrowingFunction', () => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
}).toThrow('Test error');
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
});
|
||||
|
||||
test('should still record sample even when function throws', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
try {
|
||||
ProfilerSDK.measure('ThrowingFunction', () => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
const sample = frame?.samples.find((s) => s.name === 'ThrowingFunction');
|
||||
expect(sample).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Frame Operations', () => {
|
||||
test('should track frame numbers', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.frameNumber).toBe(2);
|
||||
});
|
||||
|
||||
test('should calculate frame duration', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
// Simulate some work
|
||||
const start = performance.now();
|
||||
while (performance.now() - start < 5) {
|
||||
// busy wait for ~5ms
|
||||
}
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.duration).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should collect category stats', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const ecsHandle = ProfilerSDK.beginSample('ECSSystem', ProfileCategory.ECS);
|
||||
ProfilerSDK.endSample(ecsHandle);
|
||||
|
||||
const renderHandle = ProfilerSDK.beginSample('Render', ProfileCategory.Rendering);
|
||||
ProfilerSDK.endSample(renderHandle);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.categoryStats.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should maintain frame history', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const history = ProfilerSDK.getFrameHistory();
|
||||
expect(history.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Counter Operations', () => {
|
||||
test('should increment counter without error', () => {
|
||||
// Test that counter operations don't throw
|
||||
expect(() => {
|
||||
ProfilerSDK.incrementCounter('draw_calls', 1, ProfileCategory.Rendering);
|
||||
ProfilerSDK.incrementCounter('draw_calls', 1, ProfileCategory.Rendering);
|
||||
ProfilerSDK.incrementCounter('draw_calls', 5, ProfileCategory.Rendering);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should set gauge value without error', () => {
|
||||
// Test that gauge operations don't throw
|
||||
expect(() => {
|
||||
ProfilerSDK.setGauge('entity_count', 100, ProfileCategory.ECS);
|
||||
ProfilerSDK.setGauge('entity_count', 150, ProfileCategory.ECS);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should track counters in frame', () => {
|
||||
ProfilerSDK.incrementCounter('test_counter', 5, ProfileCategory.Custom);
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
// Frame should exist and have counters map
|
||||
expect(frame).toBeDefined();
|
||||
expect(frame?.counters).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Report Generation', () => {
|
||||
test('should generate report with hotspots', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
const handle1 = ProfilerSDK.beginSample('SlowFunction', ProfileCategory.Script);
|
||||
ProfilerSDK.endSample(handle1);
|
||||
const handle2 = ProfilerSDK.beginSample('FastFunction', ProfileCategory.Script);
|
||||
ProfilerSDK.endSample(handle2);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
expect(report).toBeDefined();
|
||||
expect(report.totalFrames).toBe(1);
|
||||
expect(report.hotspots.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should calculate frame time statistics', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
// Simulate varying frame times
|
||||
const start = performance.now();
|
||||
while (performance.now() - start < (i + 1)) {
|
||||
// busy wait
|
||||
}
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
expect(report.averageFrameTime).toBeGreaterThan(0);
|
||||
expect(report.minFrameTime).toBeLessThanOrEqual(report.averageFrameTime);
|
||||
expect(report.maxFrameTime).toBeGreaterThanOrEqual(report.averageFrameTime);
|
||||
});
|
||||
|
||||
test('should generate report with limited frame count', () => {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const report = ProfilerSDK.getReport(10);
|
||||
expect(report.totalFrames).toBe(10);
|
||||
});
|
||||
|
||||
test('should build call graph', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
const parentHandle = ProfilerSDK.beginSample('Parent', ProfileCategory.Script);
|
||||
const childHandle = ProfilerSDK.beginSample('Child', ProfileCategory.Script);
|
||||
ProfilerSDK.endSample(childHandle);
|
||||
ProfilerSDK.endSample(parentHandle);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
// Call graph should contain at least the sampled functions
|
||||
expect(report.callGraph.size).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Verify samples were recorded
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.samples.length).toBe(2);
|
||||
expect(frame?.samples.some((s) => s.name === 'Parent')).toBe(true);
|
||||
expect(frame?.samples.some((s) => s.name === 'Child')).toBe(true);
|
||||
});
|
||||
|
||||
test('should track category breakdown', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('ECS1', () => {}, ProfileCategory.ECS);
|
||||
ProfilerSDK.measure('ECS2', () => {}, ProfileCategory.ECS);
|
||||
ProfilerSDK.measure('Render1', () => {}, ProfileCategory.Rendering);
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
expect(report.categoryBreakdown.size).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProfileCategory', () => {
|
||||
test('should have all expected categories', () => {
|
||||
expect(ProfileCategory.ECS).toBe('ECS');
|
||||
expect(ProfileCategory.Rendering).toBe('Rendering');
|
||||
expect(ProfileCategory.Physics).toBe('Physics');
|
||||
expect(ProfileCategory.Audio).toBe('Audio');
|
||||
expect(ProfileCategory.Network).toBe('Network');
|
||||
expect(ProfileCategory.Script).toBe('Script');
|
||||
expect(ProfileCategory.Memory).toBe('Memory');
|
||||
expect(ProfileCategory.Animation).toBe('Animation');
|
||||
expect(ProfileCategory.AI).toBe('AI');
|
||||
expect(ProfileCategory.Input).toBe('Input');
|
||||
expect(ProfileCategory.Loading).toBe('Loading');
|
||||
expect(ProfileCategory.Custom).toBe('Custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Memory Tracking', () => {
|
||||
test('should collect memory snapshot', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
expect(frame?.memory).toBeDefined();
|
||||
expect(frame?.memory.timestamp).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should track memory trend in report', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.endFrame();
|
||||
}
|
||||
|
||||
const report = ProfilerSDK.getReport();
|
||||
expect(report.memoryTrend.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reset', () => {
|
||||
test('should clear all data on reset', () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
ProfilerSDK.measure('Test', () => {});
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
ProfilerSDK.reset();
|
||||
|
||||
// reset() clears data but maintains enabled state from beforeEach
|
||||
expect(ProfilerSDK.getFrameHistory().length).toBe(0);
|
||||
expect(ProfilerSDK.getCurrentFrame()).toBeNull();
|
||||
});
|
||||
|
||||
test('should disable profiler after resetInstance', () => {
|
||||
ProfilerSDK.resetInstance();
|
||||
expect(ProfilerSDK.isEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Async measurement', () => {
|
||||
test('should measure async function execution', async () => {
|
||||
ProfilerSDK.beginFrame();
|
||||
|
||||
const result = await ProfilerSDK.measureAsync('AsyncFunction', async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
return 42;
|
||||
}, ProfileCategory.Network);
|
||||
|
||||
ProfilerSDK.endFrame();
|
||||
|
||||
expect(result).toBe(42);
|
||||
|
||||
const frame = ProfilerSDK.getCurrentFrame();
|
||||
const sample = frame?.samples.find((s) => s.name === 'AsyncFunction');
|
||||
expect(sample).toBeDefined();
|
||||
// Allow some timing variance due to setTimeout not being exact
|
||||
expect(sample?.duration).toBeGreaterThanOrEqual(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
307
packages/framework/core/tests/Utils/Timers/Timer.test.ts
Normal file
307
packages/framework/core/tests/Utils/Timers/Timer.test.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
import { Timer } from '../../../src/Utils/Timers/Timer';
|
||||
import { ITimer } from '../../../src/Utils/Timers/ITimer';
|
||||
import { Time } from '../../../src/Utils/Time';
|
||||
|
||||
// Mock Time.deltaTime
|
||||
jest.mock('../../../src/Utils/Time', () => ({
|
||||
Time: {
|
||||
deltaTime: 0.016 // 默认16ms,约60FPS
|
||||
}
|
||||
}));
|
||||
|
||||
describe('Timer - 定时器测试', () => {
|
||||
let timer: Timer;
|
||||
let mockCallback: jest.Mock;
|
||||
let mockContext: any;
|
||||
|
||||
beforeEach(() => {
|
||||
timer = new Timer();
|
||||
mockCallback = jest.fn();
|
||||
mockContext = { id: 'test-context' };
|
||||
|
||||
// 重置deltaTime
|
||||
(Time as any).deltaTime = 0.016;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
timer.unload();
|
||||
});
|
||||
|
||||
describe('基本初始化和属性', () => {
|
||||
it('应该能够创建定时器实例', () => {
|
||||
expect(timer).toBeInstanceOf(Timer);
|
||||
expect(timer.isDone).toBe(false);
|
||||
expect(timer.elapsedTime).toBe(0);
|
||||
});
|
||||
|
||||
it('应该能够初始化定时器', () => {
|
||||
timer.initialize(1.0, false, mockContext, mockCallback);
|
||||
|
||||
expect(timer.context).toBe(mockContext);
|
||||
expect(timer.isDone).toBe(false);
|
||||
expect(timer.elapsedTime).toBe(0);
|
||||
});
|
||||
|
||||
it('应该能够获取泛型上下文', () => {
|
||||
interface TestContext {
|
||||
id: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const testContext: TestContext = { id: 'test', value: 42 };
|
||||
timer.initialize(1.0, false, testContext, mockCallback);
|
||||
|
||||
const context = timer.getContext<TestContext>();
|
||||
expect(context.id).toBe('test');
|
||||
expect(context.value).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('定时器tick逻辑', () => {
|
||||
beforeEach(() => {
|
||||
timer.initialize(1.0, false, mockContext, mockCallback);
|
||||
});
|
||||
|
||||
it('应该正确累加经过时间', () => {
|
||||
(Time as any).deltaTime = 0.5;
|
||||
timer.tick(); // 先累加时间
|
||||
expect(timer.elapsedTime).toBe(0.5);
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('当经过时间超过目标时间时应该触发回调', () => {
|
||||
// 第一次tick:累加时间到1.1,但还没检查触发条件
|
||||
(Time as any).deltaTime = 1.1;
|
||||
timer.tick();
|
||||
expect(timer.elapsedTime).toBe(1.1);
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
|
||||
// 第二次tick:检查条件并触发
|
||||
(Time as any).deltaTime = 0.1;
|
||||
timer.tick();
|
||||
expect(mockCallback).toHaveBeenCalledWith(timer);
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
expect(timer.isDone).toBe(true); // 非重复定时器应该完成
|
||||
});
|
||||
|
||||
it('应该在触发后调整剩余时间', () => {
|
||||
// 第一次累加到1.5
|
||||
(Time as any).deltaTime = 1.5;
|
||||
timer.tick();
|
||||
expect(timer.elapsedTime).toBe(1.5);
|
||||
|
||||
// 第二次检查并触发:1.5 - 1.0 = 0.5,然后加上当前的deltaTime
|
||||
(Time as any).deltaTime = 0.3;
|
||||
timer.tick();
|
||||
expect(timer.elapsedTime).toBe(0.8); // 0.5 + 0.3
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('重复定时器', () => {
|
||||
beforeEach(() => {
|
||||
timer.initialize(1.0, true, mockContext, mockCallback);
|
||||
});
|
||||
|
||||
it('重复定时器不应该自动标记为完成', () => {
|
||||
// 累加时间超过目标
|
||||
(Time as any).deltaTime = 1.1;
|
||||
timer.tick();
|
||||
|
||||
// 检查并触发,但不应该标记为完成
|
||||
(Time as any).deltaTime = 0.1;
|
||||
const isDone = timer.tick();
|
||||
|
||||
expect(isDone).toBe(false);
|
||||
expect(timer.isDone).toBe(false);
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('重复定时器可以多次触发', () => {
|
||||
// 第一次触发
|
||||
(Time as any).deltaTime = 1.1;
|
||||
timer.tick(); // 累加时间
|
||||
(Time as any).deltaTime = 0.1;
|
||||
timer.tick(); // 触发
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
|
||||
// 第二次触发 - 从剩余的0.1开始
|
||||
(Time as any).deltaTime = 0.9; // 0.1 + 0.9 = 1.0
|
||||
timer.tick(); // 累加到1.0
|
||||
(Time as any).deltaTime = 0.1;
|
||||
timer.tick(); // 检查并触发
|
||||
expect(mockCallback).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('定时器控制方法', () => {
|
||||
beforeEach(() => {
|
||||
timer.initialize(1.0, false, mockContext, mockCallback);
|
||||
});
|
||||
|
||||
it('reset应该重置经过时间', () => {
|
||||
(Time as any).deltaTime = 0.5;
|
||||
timer.tick();
|
||||
expect(timer.elapsedTime).toBe(0.5);
|
||||
|
||||
timer.reset();
|
||||
expect(timer.elapsedTime).toBe(0);
|
||||
expect(timer.isDone).toBe(false);
|
||||
});
|
||||
|
||||
it('stop应该标记定时器为完成', () => {
|
||||
timer.stop();
|
||||
expect(timer.isDone).toBe(true);
|
||||
});
|
||||
|
||||
it('停止的定时器不应该触发回调', () => {
|
||||
timer.stop();
|
||||
(Time as any).deltaTime = 2.0;
|
||||
const isDone = timer.tick();
|
||||
|
||||
expect(isDone).toBe(true);
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('上下文绑定', () => {
|
||||
it('回调应该正确绑定到上下文', () => {
|
||||
let contextValue = 0;
|
||||
const testContext = {
|
||||
value: 42,
|
||||
callback: function(this: any, timer: ITimer) {
|
||||
contextValue = this.value;
|
||||
}
|
||||
};
|
||||
|
||||
timer.initialize(1.0, false, testContext, testContext.callback);
|
||||
|
||||
// 触发定时器
|
||||
(Time as any).deltaTime = 1.1;
|
||||
timer.tick(); // 累加时间
|
||||
(Time as any).deltaTime = 0.1;
|
||||
timer.tick(); // 触发
|
||||
|
||||
expect(contextValue).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('内存管理', () => {
|
||||
it('unload应该清空对象引用', () => {
|
||||
timer.initialize(1.0, false, mockContext, mockCallback);
|
||||
|
||||
timer.unload();
|
||||
|
||||
expect(timer.context).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
it('零秒定时器应该立即触发', () => {
|
||||
timer.initialize(0, false, mockContext, mockCallback);
|
||||
|
||||
// 第一次累加时间
|
||||
(Time as any).deltaTime = 0.001;
|
||||
timer.tick();
|
||||
expect(timer.elapsedTime).toBe(0.001);
|
||||
|
||||
// 第二次检查并触发(elapsedTime > 0)
|
||||
timer.tick();
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('负数时间定时器应该立即触发', () => {
|
||||
timer.initialize(-1, false, mockContext, mockCallback);
|
||||
|
||||
(Time as any).deltaTime = 0.001;
|
||||
timer.tick(); // 累加时间,elapsedTime = 0.001 > -1
|
||||
timer.tick(); // 检查并触发
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('极小deltaTime应该正确累积', () => {
|
||||
timer.initialize(0.1, false, mockContext, mockCallback);
|
||||
(Time as any).deltaTime = 0.05;
|
||||
|
||||
// 第一次不触发
|
||||
timer.tick();
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
expect(timer.elapsedTime).toBe(0.05);
|
||||
|
||||
// 第二次累加到0.1,但条件是 > 0.1 才触发,所以不触发
|
||||
timer.tick();
|
||||
expect(timer.elapsedTime).toBe(0.1);
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
|
||||
// 第三次累加到0.11,但在检查之前elapsedTime还是0.1,所以不触发
|
||||
(Time as any).deltaTime = 0.01; // 0.1 + 0.01 = 0.11 > 0.1
|
||||
timer.tick();
|
||||
expect(timer.elapsedTime).toBe(0.11);
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
|
||||
// 第四次检查并触发(elapsedTime = 0.11 > 0.1)
|
||||
(Time as any).deltaTime = 0.01; // 保持相同的deltaTime
|
||||
timer.tick();
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能测试', () => {
|
||||
it('大量tick调用应该高效', () => {
|
||||
timer.initialize(1000, false, mockContext, mockCallback);
|
||||
(Time as any).deltaTime = 0.016;
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
timer.tick();
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
expect(endTime - startTime).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('实际使用场景', () => {
|
||||
it('延迟执行功能', () => {
|
||||
let executed = false;
|
||||
|
||||
timer.initialize(1.0, false, null, () => {
|
||||
executed = true;
|
||||
});
|
||||
|
||||
// 累加时间但不触发
|
||||
(Time as any).deltaTime = 0.9;
|
||||
timer.tick();
|
||||
expect(executed).toBe(false);
|
||||
|
||||
// 继续累加到超过目标时间
|
||||
(Time as any).deltaTime = 0.2; // 总共1.1 > 1.0
|
||||
timer.tick();
|
||||
expect(executed).toBe(false); // 还没检查触发条件
|
||||
|
||||
// 下一次tick才会检查并触发
|
||||
timer.tick();
|
||||
expect(executed).toBe(true);
|
||||
});
|
||||
|
||||
it('重复任务执行', () => {
|
||||
let counter = 0;
|
||||
timer.initialize(0.5, true, null, () => {
|
||||
counter++;
|
||||
});
|
||||
|
||||
// 第一次触发 - 需要超过0.5
|
||||
(Time as any).deltaTime = 0.6;
|
||||
timer.tick(); // 累加到0.6,检查 0.6 > 0.5,触发,剩余0.1
|
||||
timer.tick(); // 再加0.6变成0.7,检查 0.1 <= 0.5不触发,最后加0.6变成0.7
|
||||
expect(counter).toBe(1);
|
||||
|
||||
// 第二次触发条件
|
||||
(Time as any).deltaTime = 0.3; // 0.7 + 0.3 = 1.0 > 0.5 应该触发
|
||||
timer.tick(); // 检查并触发第二次
|
||||
expect(counter).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user