483 lines
13 KiB
JavaScript
483 lines
13 KiB
JavaScript
const React = require("react");
|
|
const { render } = require("ink-testing-library");
|
|
const {
|
|
MemoryLeakDetector,
|
|
getGlobalDetector,
|
|
useMemoryLeakDetection,
|
|
MemoryLeakUtils,
|
|
} = require("../../../src/tui/utils/memoryLeakDetector.js");
|
|
|
|
/**
|
|
* Memory leak detection tests
|
|
* Requirements: 4.2, 4.5
|
|
*/
|
|
|
|
describe("MemoryLeakDetector", () => {
|
|
let detector;
|
|
|
|
beforeEach(() => {
|
|
detector = new MemoryLeakDetector({
|
|
checkInterval: 100, // Fast interval for testing
|
|
sampleSize: 5,
|
|
growthThreshold: 1024 * 1024, // 1MB
|
|
verbose: false,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
detector.stop();
|
|
});
|
|
|
|
describe("Basic Functionality", () => {
|
|
test("should start and stop monitoring", () => {
|
|
expect(detector.isMonitoring).toBe(false);
|
|
|
|
detector.start();
|
|
expect(detector.isMonitoring).toBe(true);
|
|
|
|
detector.stop();
|
|
expect(detector.isMonitoring).toBe(false);
|
|
});
|
|
|
|
test("should take memory samples", () => {
|
|
// Mock process.memoryUsage
|
|
const mockMemoryUsage = jest.fn(() => ({
|
|
heapUsed: 50 * 1024 * 1024,
|
|
heapTotal: 60 * 1024 * 1024,
|
|
external: 5 * 1024 * 1024,
|
|
rss: 70 * 1024 * 1024,
|
|
}));
|
|
|
|
const originalMemoryUsage = process.memoryUsage;
|
|
process.memoryUsage = mockMemoryUsage;
|
|
|
|
detector.takeSample();
|
|
expect(detector.samples).toHaveLength(1);
|
|
expect(detector.samples[0]).toMatchObject({
|
|
heapUsed: 50 * 1024 * 1024,
|
|
heapTotal: 60 * 1024 * 1024,
|
|
});
|
|
|
|
process.memoryUsage = originalMemoryUsage;
|
|
});
|
|
|
|
test("should limit sample size", () => {
|
|
const mockMemoryUsage = jest.fn(() => ({
|
|
heapUsed: Math.random() * 100 * 1024 * 1024,
|
|
heapTotal: 100 * 1024 * 1024,
|
|
external: 5 * 1024 * 1024,
|
|
rss: 120 * 1024 * 1024,
|
|
}));
|
|
|
|
const originalMemoryUsage = process.memoryUsage;
|
|
process.memoryUsage = mockMemoryUsage;
|
|
|
|
// Take more samples than the limit
|
|
for (let i = 0; i < 10; i++) {
|
|
detector.takeSample();
|
|
}
|
|
|
|
expect(detector.samples).toHaveLength(5); // Should be limited to sampleSize
|
|
process.memoryUsage = originalMemoryUsage;
|
|
});
|
|
});
|
|
|
|
describe("Component Registration", () => {
|
|
test("should register and unregister components", () => {
|
|
detector.registerComponent("TestComponent", 1);
|
|
expect(detector.componentRegistry.has("TestComponent")).toBe(true);
|
|
expect(detector.componentRegistry.get("TestComponent").instances).toBe(1);
|
|
|
|
detector.registerComponent("TestComponent", 1);
|
|
expect(detector.componentRegistry.get("TestComponent").instances).toBe(2);
|
|
|
|
detector.unregisterComponent("TestComponent");
|
|
expect(detector.componentRegistry.get("TestComponent").instances).toBe(1);
|
|
|
|
detector.unregisterComponent("TestComponent");
|
|
expect(detector.componentRegistry.has("TestComponent")).toBe(false);
|
|
});
|
|
|
|
test("should detect suspicious components", () => {
|
|
detector.registerComponent("LeakyComponent", 1);
|
|
|
|
// Register many instances
|
|
for (let i = 0; i < 10; i++) {
|
|
detector.registerComponent("LeakyComponent", 1);
|
|
}
|
|
|
|
const suspicious = detector.getSuspiciousComponents();
|
|
expect(suspicious).toHaveLength(1);
|
|
expect(suspicious[0].name).toBe("LeakyComponent");
|
|
expect(suspicious[0].instances).toBe(11);
|
|
expect(suspicious[0].ratio).toBeGreaterThan(2);
|
|
});
|
|
});
|
|
|
|
describe("Leak Detection", () => {
|
|
test("should detect steady memory growth", () => {
|
|
const mockMemoryUsage = jest.fn();
|
|
const originalMemoryUsage = process.memoryUsage;
|
|
process.memoryUsage = mockMemoryUsage;
|
|
|
|
// Simulate steady growth
|
|
const baseMemory = 50 * 1024 * 1024;
|
|
const growthPerSample = 5 * 1024 * 1024;
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
mockMemoryUsage.mockReturnValue({
|
|
heapUsed: baseMemory + i * growthPerSample,
|
|
heapTotal: 100 * 1024 * 1024,
|
|
external: 5 * 1024 * 1024,
|
|
rss: 120 * 1024 * 1024,
|
|
});
|
|
detector.takeSample();
|
|
}
|
|
|
|
const analysis = detector.analyzeLeaks();
|
|
expect(analysis.hasLeak).toBe(true);
|
|
expect(analysis.analysis.steadyGrowth).toBe(true);
|
|
|
|
process.memoryUsage = originalMemoryUsage;
|
|
});
|
|
|
|
test("should detect rapid memory growth", () => {
|
|
const mockMemoryUsage = jest.fn();
|
|
const originalMemoryUsage = process.memoryUsage;
|
|
process.memoryUsage = mockMemoryUsage;
|
|
|
|
// Simulate rapid growth
|
|
mockMemoryUsage.mockReturnValueOnce({
|
|
heapUsed: 50 * 1024 * 1024,
|
|
heapTotal: 100 * 1024 * 1024,
|
|
external: 5 * 1024 * 1024,
|
|
rss: 120 * 1024 * 1024,
|
|
});
|
|
detector.takeSample();
|
|
|
|
// Wait a bit then add large growth
|
|
setTimeout(() => {
|
|
mockMemoryUsage.mockReturnValueOnce({
|
|
heapUsed: 60 * 1024 * 1024, // 10MB growth
|
|
heapTotal: 100 * 1024 * 1024,
|
|
external: 5 * 1024 * 1024,
|
|
rss: 130 * 1024 * 1024,
|
|
});
|
|
detector.takeSample();
|
|
|
|
mockMemoryUsage.mockReturnValueOnce({
|
|
heapUsed: 70 * 1024 * 1024, // Another 10MB growth
|
|
heapTotal: 100 * 1024 * 1024,
|
|
external: 5 * 1024 * 1024,
|
|
rss: 140 * 1024 * 1024,
|
|
});
|
|
detector.takeSample();
|
|
|
|
const analysis = detector.analyzeLeaks();
|
|
expect(analysis.hasLeak).toBe(true);
|
|
expect(analysis.analysis.rapidGrowth).toBe(true);
|
|
|
|
process.memoryUsage = originalMemoryUsage;
|
|
}, 10);
|
|
});
|
|
|
|
test("should generate appropriate recommendations", () => {
|
|
const mockMemoryUsage = jest.fn();
|
|
const originalMemoryUsage = process.memoryUsage;
|
|
process.memoryUsage = mockMemoryUsage;
|
|
|
|
// Simulate component leak
|
|
detector.registerComponent("LeakyComponent", 1);
|
|
for (let i = 0; i < 5; i++) {
|
|
detector.registerComponent("LeakyComponent", 1);
|
|
}
|
|
|
|
// Simulate memory growth
|
|
for (let i = 0; i < 3; i++) {
|
|
mockMemoryUsage.mockReturnValue({
|
|
heapUsed: 50 * 1024 * 1024 + i * 2 * 1024 * 1024,
|
|
heapTotal: 100 * 1024 * 1024,
|
|
external: 5 * 1024 * 1024,
|
|
rss: 120 * 1024 * 1024,
|
|
});
|
|
detector.takeSample();
|
|
}
|
|
|
|
const analysis = detector.analyzeLeaks();
|
|
expect(analysis.analysis.recommendations).toBeDefined();
|
|
expect(analysis.analysis.recommendations.length).toBeGreaterThan(0);
|
|
|
|
const hasComponentRecommendation = analysis.analysis.recommendations.some(
|
|
(rec) => rec.type === "component-leak"
|
|
);
|
|
expect(hasComponentRecommendation).toBe(true);
|
|
|
|
process.memoryUsage = originalMemoryUsage;
|
|
});
|
|
});
|
|
|
|
describe("Event Listeners", () => {
|
|
test("should notify listeners of events", () => {
|
|
const listener = jest.fn();
|
|
detector.addListener(listener);
|
|
|
|
const mockMemoryUsage = jest.fn(() => ({
|
|
heapUsed: 50 * 1024 * 1024,
|
|
heapTotal: 100 * 1024 * 1024,
|
|
external: 5 * 1024 * 1024,
|
|
rss: 120 * 1024 * 1024,
|
|
}));
|
|
|
|
const originalMemoryUsage = process.memoryUsage;
|
|
process.memoryUsage = mockMemoryUsage;
|
|
|
|
detector.takeSample();
|
|
expect(listener).toHaveBeenCalledWith("sample", expect.any(Object));
|
|
|
|
detector.removeListener(listener);
|
|
detector.takeSample();
|
|
expect(listener).toHaveBeenCalledTimes(1); // Should not be called again
|
|
|
|
process.memoryUsage = originalMemoryUsage;
|
|
});
|
|
|
|
test("should handle listener errors gracefully", () => {
|
|
const consoleSpy = jest
|
|
.spyOn(console, "error")
|
|
.mockImplementation(() => {});
|
|
const badListener = jest.fn(() => {
|
|
throw new Error("Listener error");
|
|
});
|
|
const goodListener = jest.fn();
|
|
|
|
detector.addListener(badListener);
|
|
detector.addListener(goodListener);
|
|
|
|
const mockMemoryUsage = jest.fn(() => ({
|
|
heapUsed: 50 * 1024 * 1024,
|
|
heapTotal: 100 * 1024 * 1024,
|
|
external: 5 * 1024 * 1024,
|
|
rss: 120 * 1024 * 1024,
|
|
}));
|
|
|
|
const originalMemoryUsage = process.memoryUsage;
|
|
process.memoryUsage = mockMemoryUsage;
|
|
|
|
detector.takeSample();
|
|
|
|
expect(badListener).toHaveBeenCalled();
|
|
expect(goodListener).toHaveBeenCalled();
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
"[MemoryLeakDetector] Error in listener:",
|
|
expect.any(Error)
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
process.memoryUsage = originalMemoryUsage;
|
|
});
|
|
});
|
|
|
|
describe("Statistics and Reporting", () => {
|
|
test("should provide memory statistics", () => {
|
|
const mockMemoryUsage = jest.fn();
|
|
const originalMemoryUsage = process.memoryUsage;
|
|
process.memoryUsage = mockMemoryUsage;
|
|
|
|
// Take a few samples
|
|
for (let i = 0; i < 3; i++) {
|
|
mockMemoryUsage.mockReturnValue({
|
|
heapUsed: 50 * 1024 * 1024 + i * 1024 * 1024,
|
|
heapTotal: 100 * 1024 * 1024,
|
|
external: 5 * 1024 * 1024,
|
|
rss: 120 * 1024 * 1024,
|
|
});
|
|
detector.takeSample();
|
|
}
|
|
|
|
const stats = detector.getStatistics();
|
|
expect(stats).toBeDefined();
|
|
expect(stats.current).toBeDefined();
|
|
expect(stats.growth).toBeDefined();
|
|
expect(stats.trend).toBeDefined();
|
|
expect(stats.samples).toBe(3);
|
|
|
|
process.memoryUsage = originalMemoryUsage;
|
|
});
|
|
|
|
test("should generate comprehensive report", () => {
|
|
detector.registerComponent("TestComponent", 1);
|
|
|
|
const mockMemoryUsage = jest.fn(() => ({
|
|
heapUsed: 50 * 1024 * 1024,
|
|
heapTotal: 100 * 1024 * 1024,
|
|
external: 5 * 1024 * 1024,
|
|
rss: 120 * 1024 * 1024,
|
|
}));
|
|
|
|
const originalMemoryUsage = process.memoryUsage;
|
|
process.memoryUsage = mockMemoryUsage;
|
|
|
|
detector.takeSample();
|
|
const report = detector.generateReport();
|
|
|
|
expect(report).toMatchObject({
|
|
timestamp: expect.any(Number),
|
|
monitoring: expect.any(Boolean),
|
|
statistics: expect.any(Object),
|
|
components: expect.any(Array),
|
|
recommendations: expect.any(Array),
|
|
});
|
|
|
|
expect(report.components).toHaveLength(1);
|
|
expect(report.components[0].name).toBe("TestComponent");
|
|
|
|
process.memoryUsage = originalMemoryUsage;
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("useMemoryLeakDetection Hook", () => {
|
|
test("should register and unregister component on mount/unmount", () => {
|
|
const TestComponent = () => {
|
|
useMemoryLeakDetection("TestComponent");
|
|
return React.createElement("div", null, "Test");
|
|
};
|
|
|
|
const detector = getGlobalDetector();
|
|
expect(detector.componentRegistry.has("TestComponent")).toBe(false);
|
|
|
|
const { unmount } = render(React.createElement(TestComponent));
|
|
expect(detector.componentRegistry.has("TestComponent")).toBe(true);
|
|
|
|
unmount();
|
|
expect(detector.componentRegistry.has("TestComponent")).toBe(false);
|
|
});
|
|
|
|
test("should provide detector utilities", () => {
|
|
let detectorUtils = {};
|
|
|
|
const TestComponent = () => {
|
|
detectorUtils = useMemoryLeakDetection("TestComponent");
|
|
return React.createElement("div", null, "Test");
|
|
};
|
|
|
|
render(React.createElement(TestComponent));
|
|
|
|
expect(typeof detectorUtils.detector).toBe("object");
|
|
expect(typeof detectorUtils.forceGC).toBe("function");
|
|
expect(typeof detectorUtils.getReport).toBe("function");
|
|
expect(typeof detectorUtils.getStats).toBe("function");
|
|
});
|
|
});
|
|
|
|
describe("MemoryLeakUtils", () => {
|
|
describe("checkObjectForLeaks", () => {
|
|
test("should detect circular references", () => {
|
|
const obj = { name: "test" };
|
|
obj.self = obj; // Create circular reference
|
|
|
|
const issues = MemoryLeakUtils.checkObjectForLeaks(obj, "testObj");
|
|
const circularIssue = issues.find(
|
|
(issue) => issue.type === "circular-reference"
|
|
);
|
|
|
|
expect(circularIssue).toBeDefined();
|
|
expect(circularIssue.message).toContain("Circular reference");
|
|
});
|
|
|
|
test("should detect large arrays", () => {
|
|
const largeArray = new Array(15000).fill("item");
|
|
const issues = MemoryLeakUtils.checkObjectForLeaks(
|
|
largeArray,
|
|
"largeArray"
|
|
);
|
|
|
|
const arrayIssue = issues.find((issue) => issue.type === "large-array");
|
|
expect(arrayIssue).toBeDefined();
|
|
expect(arrayIssue.length).toBe(15000);
|
|
});
|
|
|
|
test("should detect objects with many properties", () => {
|
|
const obj = {};
|
|
for (let i = 0; i < 1500; i++) {
|
|
obj[`prop${i}`] = i;
|
|
}
|
|
|
|
const issues = MemoryLeakUtils.checkObjectForLeaks(obj, "manyPropsObj");
|
|
const propsIssue = issues.find(
|
|
(issue) => issue.type === "many-properties"
|
|
);
|
|
|
|
expect(propsIssue).toBeDefined();
|
|
expect(propsIssue.count).toBe(1500);
|
|
});
|
|
|
|
test("should handle null and undefined objects", () => {
|
|
expect(MemoryLeakUtils.checkObjectForLeaks(null)).toEqual([]);
|
|
expect(MemoryLeakUtils.checkObjectForLeaks(undefined)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("checkDOMNodeForLeaks", () => {
|
|
test("should detect excessive event listeners", () => {
|
|
const mockNode = {
|
|
_events: {},
|
|
};
|
|
|
|
// Add many event listeners
|
|
for (let i = 0; i < 60; i++) {
|
|
mockNode._events[`event${i}`] = () => {};
|
|
}
|
|
|
|
const issues = MemoryLeakUtils.checkDOMNodeForLeaks(mockNode);
|
|
const listenerIssue = issues.find(
|
|
(issue) => issue.type === "excessive-listeners"
|
|
);
|
|
|
|
expect(listenerIssue).toBeDefined();
|
|
expect(listenerIssue.count).toBe(60);
|
|
});
|
|
|
|
test("should detect detached DOM nodes", () => {
|
|
const mockNode = {
|
|
parentNode: null,
|
|
};
|
|
|
|
const issues = MemoryLeakUtils.checkDOMNodeForLeaks(mockNode);
|
|
const detachedIssue = issues.find(
|
|
(issue) => issue.type === "detached-node"
|
|
);
|
|
|
|
expect(detachedIssue).toBeDefined();
|
|
});
|
|
|
|
test("should handle invalid nodes", () => {
|
|
expect(MemoryLeakUtils.checkDOMNodeForLeaks(null)).toEqual([]);
|
|
expect(MemoryLeakUtils.checkDOMNodeForLeaks("not an object")).toEqual([]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Global Detector", () => {
|
|
test("should return the same instance", () => {
|
|
const detector1 = getGlobalDetector();
|
|
const detector2 = getGlobalDetector();
|
|
|
|
expect(detector1).toBe(detector2);
|
|
});
|
|
|
|
test("should accept options on first call", () => {
|
|
// Reset global detector
|
|
const MemoryLeakDetectorModule = require("../../../src/tui/utils/memoryLeakDetector.js");
|
|
MemoryLeakDetectorModule.globalDetector = null;
|
|
|
|
const detector = getGlobalDetector({
|
|
checkInterval: 5000,
|
|
verbose: true,
|
|
});
|
|
|
|
expect(detector.options.checkInterval).toBe(5000);
|
|
expect(detector.options.verbose).toBe(true);
|
|
});
|
|
});
|