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); }); });