mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-07-21 01:25:32 -04:00
631 lines
21 KiB
JavaScript
631 lines
21 KiB
JavaScript
const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.js');
|
|
|
|
describe('Use-after-free detection (MemoryTracked)', function () {
|
|
let gd = null;
|
|
beforeAll(async () => {
|
|
gd = await initializeGDevelopJs();
|
|
});
|
|
|
|
describe('Untracked class (VectorString)', function () {
|
|
it('works normally when alive', function () {
|
|
const vec = new gd.VectorString();
|
|
vec.push_back('hello');
|
|
expect(vec.size()).toBe(1);
|
|
expect(vec.at(0)).toBe('hello');
|
|
vec.delete();
|
|
});
|
|
|
|
it('throws UseAfterFreeError after delete()', function () {
|
|
const vec = new gd.VectorString();
|
|
vec.push_back('hello');
|
|
vec.delete();
|
|
|
|
expect(() => vec.size()).toThrow(gd.UseAfterFreeError);
|
|
});
|
|
|
|
it('includes destruction context in the error after delete()', function () {
|
|
const vec = new gd.VectorString();
|
|
vec.push_back('hello');
|
|
vec.delete();
|
|
|
|
try {
|
|
vec.size();
|
|
fail('Expected UseAfterFreeError');
|
|
} catch (e) {
|
|
expect(e).toBeInstanceOf(gd.UseAfterFreeError);
|
|
expect(e.useAfterFreeContext).toBeDefined();
|
|
expect(e.useAfterFreeContext.timeSinceDestroyedInMs).not.toBe(
|
|
undefined
|
|
);
|
|
expect(e.useAfterFreeContext.destroyedBy).toContain(
|
|
'Object destroyed here'
|
|
);
|
|
expect(e.useAfterFreeContext.destroyedBy).toContain('.delete');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Tracked class (Layout) - JS deletion', function () {
|
|
it('works normally when alive', function () {
|
|
const layout = new gd.Layout();
|
|
layout.setName('TestScene');
|
|
expect(layout.getName()).toBe('TestScene');
|
|
layout.delete();
|
|
});
|
|
|
|
it('throws UseAfterFreeError after delete()', function () {
|
|
const layout = new gd.Layout();
|
|
layout.setName('TestScene');
|
|
layout.delete();
|
|
|
|
expect(() => layout.getName()).toThrow(gd.UseAfterFreeError);
|
|
});
|
|
|
|
it('includes destruction stack and timing in the error for JS deletion', function () {
|
|
const layout = new gd.Layout();
|
|
layout.setName('TestScene');
|
|
layout.delete();
|
|
|
|
try {
|
|
layout.getName();
|
|
fail('Expected UseAfterFreeError');
|
|
} catch (e) {
|
|
expect(e).toBeInstanceOf(gd.UseAfterFreeError);
|
|
expect(e.message).toContain('destroyed from JavaScript');
|
|
expect(e.useAfterFreeContext.timeSinceDestroyedInMs).not.toBe(
|
|
undefined
|
|
);
|
|
expect(e.useAfterFreeContext.destroyedBy).toContain(
|
|
'Object destroyed here'
|
|
);
|
|
expect(e.useAfterFreeContext.destroyedBy).toContain(
|
|
'Object destroyed here'
|
|
);
|
|
expect(e.useAfterFreeContext.destroyedBy).toContain('.delete');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Tracked class (Layout) - C++ deletion', function () {
|
|
it('throws UseAfterFreeError when C++ deletes the underlying object', function () {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
project.insertNewLayout('MyScene', 0);
|
|
|
|
// Get a reference to the layout.
|
|
const layout = project.getLayout('MyScene');
|
|
expect(layout.getName()).toBe('MyScene');
|
|
|
|
// C++ deletes the layout when we remove it from the project.
|
|
project.removeLayout('MyScene');
|
|
|
|
// The JS wrapper still has a non-zero ptr, but the object is dead.
|
|
expect(() => layout.getName()).toThrow(gd.UseAfterFreeError);
|
|
|
|
project.delete();
|
|
});
|
|
|
|
it('includes destruction context and last successful call in error for C++ deletion', function () {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
project.insertNewLayout('MyScene', 0);
|
|
|
|
const layout = project.getLayout('MyScene');
|
|
// Call a few methods
|
|
layout.setName('RenamedScene');
|
|
expect(layout.getName()).toBe('RenamedScene');
|
|
layout.setName('RenamedScene');
|
|
|
|
// C++ deletes the layout — the call context "Project.removeLayout"
|
|
// is captured by the ring buffer in MemoryTrackedRegistry.
|
|
project.removeLayout('RenamedScene');
|
|
|
|
try {
|
|
layout.getName();
|
|
fail('Expected UseAfterFreeError');
|
|
} catch (e) {
|
|
expect(e).toBeInstanceOf(gd.UseAfterFreeError);
|
|
expect(e.message).toContain('destroyed on C++ side');
|
|
// The call that triggered the destruction.
|
|
expect(e.useAfterFreeContext.destroyedByCallTo).toContain(
|
|
'Project.removeLayout'
|
|
);
|
|
// Timestamp should be present.
|
|
expect(e.useAfterFreeContext.timeSinceDestroyedInMs).not.toBe(
|
|
undefined
|
|
);
|
|
// Last successful call on the stale wrapper.
|
|
expect(e.useAfterFreeContext.lastSuccessfulCallToThisWrapper).toBe(
|
|
'Layout.setName'
|
|
);
|
|
}
|
|
|
|
project.delete();
|
|
});
|
|
|
|
it('includes destruction context when a project deletion kills child layouts', function () {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
project.insertNewLayout('ChildScene', 0);
|
|
const layout = project.getLayout('ChildScene');
|
|
expect(layout.getName()).toBe('ChildScene');
|
|
|
|
// Deleting the project destroys all child layouts via the C++
|
|
// destructor chain. The delete() wrapper sets the call-context ID
|
|
// to "Project.delete" before calling gd.destroy(), so the ring
|
|
// buffer correctly attributes the child destructions.
|
|
project.delete();
|
|
|
|
try {
|
|
layout.getName();
|
|
fail('Expected UseAfterFreeError');
|
|
} catch (e) {
|
|
expect(e).toBeInstanceOf(gd.UseAfterFreeError);
|
|
expect(e.message).toContain('destroyed on C++ side');
|
|
// The call that triggered the destruction.
|
|
expect(e.useAfterFreeContext.destroyedByCallTo).toContain(
|
|
'Project.delete'
|
|
);
|
|
// Timestamp should be present.
|
|
expect(e.useAfterFreeContext.timeSinceDestroyedInMs).not.toBe(
|
|
undefined
|
|
);
|
|
// Last successful call on the stale wrapper.
|
|
expect(e.useAfterFreeContext.lastSuccessfulCallToThisWrapper).toBe(
|
|
'Layout.getName'
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('MemoryTrackedRegistry API', function () {
|
|
it('reports dead objects correctly', function () {
|
|
const count = gd.MemoryTrackedRegistry.getDeadCount();
|
|
expect(typeof count).toBe('number');
|
|
});
|
|
|
|
it('pruneDead clears dead set when over limit', function () {
|
|
const countBefore = gd.MemoryTrackedRegistry.getDeadCount();
|
|
// Pruning with a high limit should not clear.
|
|
gd.MemoryTrackedRegistry.pruneDead(countBefore + 1000);
|
|
expect(gd.MemoryTrackedRegistry.getDeadCount()).toBe(countBefore);
|
|
|
|
// Pruning with 0 should clear everything.
|
|
gd.MemoryTrackedRegistry.pruneDead(0);
|
|
expect(gd.MemoryTrackedRegistry.getDeadCount()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Per-class stats', function () {
|
|
it('reports per-class alive and dead counts', function () {
|
|
const aliveBefore =
|
|
gd.MemoryTrackedRegistry.getAliveCountForClass('Layout');
|
|
const layout1 = new gd.Layout();
|
|
const layout2 = new gd.Layout();
|
|
expect(gd.MemoryTrackedRegistry.getAliveCountForClass('Layout')).toBe(
|
|
aliveBefore + 2
|
|
);
|
|
|
|
layout1.delete();
|
|
expect(gd.MemoryTrackedRegistry.getAliveCountForClass('Layout')).toBe(
|
|
aliveBefore + 1
|
|
);
|
|
expect(
|
|
gd.MemoryTrackedRegistry.getDeadCountForClass('Layout')
|
|
).toBeGreaterThan(0);
|
|
|
|
layout2.delete();
|
|
expect(gd.MemoryTrackedRegistry.getAliveCountForClass('Layout')).toBe(
|
|
aliveBefore
|
|
);
|
|
});
|
|
|
|
it('returns 0 for unknown classes', function () {
|
|
expect(
|
|
gd.MemoryTrackedRegistry.getAliveCountForClass('NonExistent')
|
|
).toBe(0);
|
|
expect(gd.MemoryTrackedRegistry.getDeadCountForClass('NonExistent')).toBe(
|
|
0
|
|
);
|
|
});
|
|
|
|
it('returns totals when given empty string', function () {
|
|
const totalAlive = gd.MemoryTrackedRegistry.getAliveCount();
|
|
expect(totalAlive).toBeGreaterThan(0);
|
|
const totalDead = gd.MemoryTrackedRegistry.getDeadCount();
|
|
expect(typeof totalDead).toBe('number');
|
|
});
|
|
|
|
it('tracks different classes independently', function () {
|
|
const layoutAliveBefore =
|
|
gd.MemoryTrackedRegistry.getAliveCountForClass('Layout');
|
|
const projectAliveBefore =
|
|
gd.MemoryTrackedRegistry.getAliveCountForClass('Project');
|
|
|
|
const layout = new gd.Layout();
|
|
expect(gd.MemoryTrackedRegistry.getAliveCountForClass('Layout')).toBe(
|
|
layoutAliveBefore + 1
|
|
);
|
|
expect(gd.MemoryTrackedRegistry.getAliveCountForClass('Project')).toBe(
|
|
projectAliveBefore
|
|
);
|
|
|
|
layout.delete();
|
|
});
|
|
});
|
|
|
|
describe('Tracked class (Project)', function () {
|
|
it('throws UseAfterFreeError after delete()', function () {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
project.setName('Test');
|
|
expect(project.getName()).toBe('Test');
|
|
project.delete();
|
|
|
|
expect(() => project.getName()).toThrow(gd.UseAfterFreeError);
|
|
});
|
|
});
|
|
|
|
describe('Multiple instances with mixed lifetimes', function () {
|
|
it('independently tracks alive and dead layouts', function () {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
|
|
project.insertNewLayout('SceneA', 0);
|
|
project.insertNewLayout('SceneB', 1);
|
|
project.insertNewLayout('SceneC', 2);
|
|
|
|
const layoutA = project.getLayout('SceneA');
|
|
const layoutB = project.getLayout('SceneB');
|
|
const layoutC = project.getLayout('SceneC');
|
|
|
|
// All alive.
|
|
expect(layoutA.getName()).toBe('SceneA');
|
|
expect(layoutB.getName()).toBe('SceneB');
|
|
expect(layoutC.getName()).toBe('SceneC');
|
|
|
|
// Kill only B via C++ deletion.
|
|
project.removeLayout('SceneB');
|
|
|
|
// A and C still alive, B is dead.
|
|
expect(layoutA.getName()).toBe('SceneA');
|
|
expect(() => layoutB.getName()).toThrow(gd.UseAfterFreeError);
|
|
expect(layoutC.getName()).toBe('SceneC');
|
|
|
|
// Now kill A too.
|
|
project.removeLayout('SceneA');
|
|
expect(() => layoutA.getName()).toThrow(gd.UseAfterFreeError);
|
|
expect(layoutC.getName()).toBe('SceneC');
|
|
|
|
// C survives until project deletion.
|
|
project.delete();
|
|
expect(() => layoutC.getName()).toThrow(gd.UseAfterFreeError);
|
|
});
|
|
|
|
it('handles interleaved JS and C++ deletions', function () {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
|
|
// Create a standalone layout (JS-owned) and a project layout (C++-owned).
|
|
const standaloneLayout = new gd.Layout();
|
|
standaloneLayout.setName('Standalone');
|
|
|
|
project.insertNewLayout('Owned', 0);
|
|
const ownedLayout = project.getLayout('Owned');
|
|
|
|
// Both alive.
|
|
expect(standaloneLayout.getName()).toBe('Standalone');
|
|
expect(ownedLayout.getName()).toBe('Owned');
|
|
|
|
// Delete standalone via JS.
|
|
standaloneLayout.delete();
|
|
expect(() => standaloneLayout.getName()).toThrow(gd.UseAfterFreeError);
|
|
expect(ownedLayout.getName()).toBe('Owned');
|
|
|
|
// Delete owned via C++.
|
|
project.removeLayout('Owned');
|
|
expect(() => ownedLayout.getName()).toThrow(gd.UseAfterFreeError);
|
|
|
|
project.delete();
|
|
});
|
|
|
|
it('handles multiple projects with their own layouts', function () {
|
|
const project1 = gd.ProjectHelper.createNewGDJSProject();
|
|
const project2 = gd.ProjectHelper.createNewGDJSProject();
|
|
|
|
project1.insertNewLayout('Scene1', 0);
|
|
project2.insertNewLayout('Scene2', 0);
|
|
|
|
const layout1 = project1.getLayout('Scene1');
|
|
const layout2 = project2.getLayout('Scene2');
|
|
|
|
expect(layout1.getName()).toBe('Scene1');
|
|
expect(layout2.getName()).toBe('Scene2');
|
|
|
|
// Delete project1 entirely - layout1 dies, layout2 survives.
|
|
project1.delete();
|
|
expect(() => layout1.getName()).toThrow(gd.UseAfterFreeError);
|
|
expect(layout2.getName()).toBe('Scene2');
|
|
|
|
project2.delete();
|
|
expect(() => layout2.getName()).toThrow(gd.UseAfterFreeError);
|
|
});
|
|
|
|
it('re-adding a layout after removal gives a working fresh reference', function () {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
|
|
project.insertNewLayout('MyScene', 0);
|
|
const oldRef = project.getLayout('MyScene');
|
|
expect(oldRef.getName()).toBe('MyScene');
|
|
|
|
// Remove and re-add with the same name.
|
|
project.removeLayout('MyScene');
|
|
|
|
project.insertNewLayout('MyScene', 0);
|
|
const newRef = project.getLayout('MyScene');
|
|
// The new reference works fine, even though the name is the same.
|
|
expect(newRef.getName()).toBe('MyScene');
|
|
|
|
// Note: oldRef may or may not throw depending on whether the allocator
|
|
// reused the same address. If it did, add() cleared the dead entry and
|
|
// the old wrapper happens to point at a valid object (no false positive).
|
|
// This is a known limitation: we guarantee no false positives, not
|
|
// detection of every stale reference when addresses are reused.
|
|
|
|
project.delete();
|
|
});
|
|
|
|
it('tracks objects inside layouts independently', function () {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
project.insertNewLayout('Scene', 0);
|
|
const layout = project.getLayout('Scene');
|
|
|
|
layout.getObjects().insertNewObject(project, 'Sprite', 'Player', 0);
|
|
layout.getObjects().insertNewObject(project, 'Sprite', 'Enemy', 1);
|
|
|
|
const player = layout.getObjects().getObject('Player');
|
|
const enemy = layout.getObjects().getObject('Enemy');
|
|
|
|
expect(player.getName()).toBe('Player');
|
|
expect(enemy.getName()).toBe('Enemy');
|
|
|
|
// Remove only Player.
|
|
layout.getObjects().removeObject('Player');
|
|
expect(() => player.getName()).toThrow(gd.UseAfterFreeError);
|
|
expect(enemy.getName()).toBe('Enemy');
|
|
|
|
// Removing the layout kills the enemy too.
|
|
project.removeLayout('Scene');
|
|
expect(() => enemy.getName()).toThrow(gd.UseAfterFreeError);
|
|
|
|
project.delete();
|
|
});
|
|
});
|
|
|
|
describe('Pointer reuse (dead then alive at same address)', function () {
|
|
it('does not false-positive when a new object reuses a dead address', function () {
|
|
// We can't control the allocator, but we can create/destroy many
|
|
// objects to maximize the chance of address reuse, and verify
|
|
// that no false positive occurs.
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
|
|
for (let round = 0; round < 20; round++) {
|
|
project.insertNewLayout('TempScene', 0);
|
|
const layout = project.getLayout('TempScene');
|
|
layout.setName('TempScene');
|
|
expect(layout.getName()).toBe('TempScene');
|
|
|
|
project.removeLayout('TempScene');
|
|
// Old reference is dead.
|
|
expect(() => layout.getName()).toThrow(gd.UseAfterFreeError);
|
|
}
|
|
|
|
// Project itself should still be alive after all that.
|
|
expect(project.getLayoutsCount()).toBe(0);
|
|
project.delete();
|
|
});
|
|
|
|
it('handles rapid create/destroy of standalone tracked objects', function () {
|
|
const deadRefs = [];
|
|
for (let i = 0; i < 50; i++) {
|
|
const layout = new gd.Layout();
|
|
layout.setName('Layout_' + i);
|
|
expect(layout.getName()).toBe('Layout_' + i);
|
|
layout.delete();
|
|
deadRefs.push(layout);
|
|
}
|
|
|
|
// All should be detected as dead.
|
|
for (const ref of deadRefs) {
|
|
expect(() => ref.getName()).toThrow(gd.UseAfterFreeError);
|
|
}
|
|
|
|
// New objects created after should work fine.
|
|
const alive = new gd.Layout();
|
|
alive.setName('StillAlive');
|
|
expect(alive.getName()).toBe('StillAlive');
|
|
alive.delete();
|
|
});
|
|
});
|
|
|
|
describe('Stress test', function () {
|
|
it('handles many layouts created and destroyed in order', function () {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
const COUNT = 100;
|
|
|
|
// Create many layouts.
|
|
for (let i = 0; i < COUNT; i++) {
|
|
project.insertNewLayout('Scene_' + i, i);
|
|
}
|
|
expect(project.getLayoutsCount()).toBe(COUNT);
|
|
|
|
// Grab references to all.
|
|
const refs = [];
|
|
for (let i = 0; i < COUNT; i++) {
|
|
refs.push(project.getLayout('Scene_' + i));
|
|
}
|
|
|
|
// Verify all alive.
|
|
for (let i = 0; i < COUNT; i++) {
|
|
expect(refs[i].getName()).toBe('Scene_' + i);
|
|
}
|
|
|
|
// Remove them in forward order.
|
|
for (let i = 0; i < COUNT; i++) {
|
|
project.removeLayout('Scene_' + i);
|
|
}
|
|
|
|
// All references should be dead.
|
|
for (let i = 0; i < COUNT; i++) {
|
|
expect(() => refs[i].getName()).toThrow(gd.UseAfterFreeError);
|
|
}
|
|
|
|
project.delete();
|
|
});
|
|
|
|
it('handles many layouts created and destroyed in reverse order', function () {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
const COUNT = 100;
|
|
|
|
for (let i = 0; i < COUNT; i++) {
|
|
project.insertNewLayout('Scene_' + i, i);
|
|
}
|
|
|
|
const refs = [];
|
|
for (let i = 0; i < COUNT; i++) {
|
|
refs.push(project.getLayout('Scene_' + i));
|
|
}
|
|
|
|
// Remove in reverse order.
|
|
for (let i = COUNT - 1; i >= 0; i--) {
|
|
// Verify still alive before removal.
|
|
expect(refs[i].getName()).toBe('Scene_' + i);
|
|
project.removeLayout('Scene_' + i);
|
|
expect(() => refs[i].getName()).toThrow(gd.UseAfterFreeError);
|
|
}
|
|
|
|
project.delete();
|
|
});
|
|
|
|
it('handles interleaved create/destroy cycles', function () {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
const CYCLES = 50;
|
|
const allDeadRefs = [];
|
|
|
|
for (let cycle = 0; cycle < CYCLES; cycle++) {
|
|
const name = 'Cycle_' + cycle;
|
|
project.insertNewLayout(name, 0);
|
|
const layout = project.getLayout(name);
|
|
expect(layout.getName()).toBe(name);
|
|
|
|
// Every other cycle, also add and remove an object.
|
|
if (cycle % 2 === 0) {
|
|
layout
|
|
.getObjects()
|
|
.insertNewObject(project, 'Sprite', 'Obj_' + cycle, 0);
|
|
const obj = layout.getObjects().getObject('Obj_' + cycle);
|
|
expect(obj.getName()).toBe('Obj_' + cycle);
|
|
}
|
|
|
|
project.removeLayout(name);
|
|
allDeadRefs.push(layout);
|
|
expect(() => layout.getName()).toThrow(gd.UseAfterFreeError);
|
|
}
|
|
|
|
// Verify all accumulated dead refs are still detected.
|
|
for (const ref of allDeadRefs) {
|
|
expect(() => ref.getName()).toThrow(gd.UseAfterFreeError);
|
|
}
|
|
|
|
project.delete();
|
|
});
|
|
|
|
it('handles many standalone objects created and destroyed', function () {
|
|
const COUNT = 200;
|
|
const alive = [];
|
|
const dead = [];
|
|
|
|
// Create many, keep half alive.
|
|
for (let i = 0; i < COUNT; i++) {
|
|
const layout = new gd.Layout();
|
|
layout.setName('L' + i);
|
|
|
|
if (i % 2 === 0) {
|
|
layout.delete();
|
|
dead.push(layout);
|
|
} else {
|
|
alive.push(layout);
|
|
}
|
|
}
|
|
|
|
// Verify alive ones work.
|
|
for (const ref of alive) {
|
|
expect(typeof ref.getName()).toBe('string');
|
|
}
|
|
|
|
// Verify dead ones throw.
|
|
for (const ref of dead) {
|
|
expect(() => ref.getName()).toThrow(gd.UseAfterFreeError);
|
|
}
|
|
|
|
// Clean up alive ones.
|
|
for (const ref of alive) {
|
|
ref.delete();
|
|
}
|
|
|
|
// Now they're dead too.
|
|
for (const ref of alive) {
|
|
expect(() => ref.getName()).toThrow(gd.UseAfterFreeError);
|
|
}
|
|
});
|
|
|
|
it('survives rapid create/destroy of projects with layouts', function () {
|
|
const PROJECTS = 20;
|
|
const LAYOUTS_PER = 10;
|
|
|
|
for (let p = 0; p < PROJECTS; p++) {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
|
|
const layoutRefs = [];
|
|
for (let l = 0; l < LAYOUTS_PER; l++) {
|
|
const name = 'P' + p + '_L' + l;
|
|
project.insertNewLayout(name, l);
|
|
layoutRefs.push(project.getLayout(name));
|
|
}
|
|
|
|
// All alive.
|
|
for (let l = 0; l < LAYOUTS_PER; l++) {
|
|
expect(layoutRefs[l].getName()).toBe('P' + p + '_L' + l);
|
|
}
|
|
|
|
// Delete project (kills all layouts).
|
|
project.delete();
|
|
|
|
// All layout refs dead.
|
|
for (const ref of layoutRefs) {
|
|
expect(() => ref.getName()).toThrow(gd.UseAfterFreeError);
|
|
}
|
|
|
|
// Project itself is dead.
|
|
expect(() => project.getName()).toThrow(gd.UseAfterFreeError);
|
|
}
|
|
});
|
|
|
|
it('pruneDead does not cause false positives', function () {
|
|
const project = gd.ProjectHelper.createNewGDJSProject();
|
|
|
|
// Create and destroy some layouts to populate the dead set.
|
|
for (let i = 0; i < 10; i++) {
|
|
project.insertNewLayout('Prunable_' + i, 0);
|
|
project.removeLayout('Prunable_' + i);
|
|
}
|
|
expect(gd.MemoryTrackedRegistry.getDeadCount()).toBeGreaterThan(0);
|
|
|
|
// Prune the dead set.
|
|
gd.MemoryTrackedRegistry.pruneDead(0);
|
|
expect(gd.MemoryTrackedRegistry.getDeadCount()).toBe(0);
|
|
|
|
// Living objects must still work — no false positives.
|
|
project.insertNewLayout('Alive', 0);
|
|
const alive = project.getLayout('Alive');
|
|
expect(alive.getName()).toBe('Alive');
|
|
|
|
project.delete();
|
|
});
|
|
});
|
|
});
|