Performance and reporting improvements

This commit is contained in:
Michael Freno
2025-11-17 17:41:01 -05:00
parent a8be1f5342
commit 2c04f69daa
18 changed files with 1987 additions and 82 deletions

View File

@@ -517,6 +517,103 @@ function TestEventHandler:test_processTouchEvents_no_onEvent()
handler:processTouchEvents()
end
-- Test: onEventDeferred flag defers callback execution
function TestEventHandler:test_onEventDeferred()
-- Mock FlexLove module
local deferredCallbacks = {}
local MockFlexLove = {
deferCallback = function(callback)
table.insert(deferredCallbacks, callback)
end
}
package.loaded["FlexLove"] = MockFlexLove
local eventsReceived = {}
local handler = createEventHandler({
onEventDeferred = true,
onEvent = function(el, event)
table.insert(eventsReceived, event)
end
})
local element = createMockElement()
handler:initialize(element)
local originalIsDown = love.mouse.isDown
love.mouse.isDown = function(button)
return button == 1
end
-- Press and release mouse button
handler:processMouseEvents(50, 50, true, true)
love.mouse.isDown = function() return false end
handler:processMouseEvents(50, 50, true, true)
-- Events should not be immediately executed
luaunit.assertEquals(#eventsReceived, 0)
-- Should have deferred callbacks queued
luaunit.assertTrue(#deferredCallbacks > 0)
-- Execute deferred callbacks
for _, callback in ipairs(deferredCallbacks) do
callback()
end
-- Now events should be received
luaunit.assertTrue(#eventsReceived > 0)
-- Check that we got a click event
local hasClick = false
for _, event in ipairs(eventsReceived) do
if event.type == "click" then
hasClick = true
break
end
end
luaunit.assertTrue(hasClick, "Should have received click event")
love.mouse.isDown = originalIsDown
package.loaded["FlexLove"] = nil
end
-- Test: onEventDeferred = false executes immediately
function TestEventHandler:test_onEventDeferred_false()
local eventsReceived = {}
local handler = createEventHandler({
onEventDeferred = false,
onEvent = function(el, event)
table.insert(eventsReceived, event)
end
})
local element = createMockElement()
handler:initialize(element)
local originalIsDown = love.mouse.isDown
love.mouse.isDown = function(button)
return button == 1
end
-- Press and release mouse button
handler:processMouseEvents(50, 50, true, true)
love.mouse.isDown = function() return false end
handler:processMouseEvents(50, 50, true, true)
-- Events should be immediately executed
luaunit.assertTrue(#eventsReceived > 0)
-- Check that we got a click event
local hasClick = false
for _, event in ipairs(eventsReceived) do
if event.type == "click" then
hasClick = true
break
end
end
luaunit.assertTrue(hasClick, "Should have received click event")
love.mouse.isDown = originalIsDown
end
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -639,6 +639,99 @@ function TestFlexLove:testGetElementAtPositionOutside()
luaunit.assertNil(found)
end
-- Test: deferCallback() queues callback
function TestFlexLove:testDeferCallbackQueuesCallback()
FlexLove.setMode("retained")
local called = false
FlexLove.deferCallback(function()
called = true
end)
-- Callback should not be called immediately
luaunit.assertFalse(called)
-- Callback should be called after executeDeferredCallbacks
FlexLove.draw()
luaunit.assertFalse(called) -- Still not called
FlexLove.executeDeferredCallbacks()
luaunit.assertTrue(called) -- Now called
end
-- Test: deferCallback() with multiple callbacks
function TestFlexLove:testDeferCallbackMultiple()
FlexLove.setMode("retained")
local order = {}
FlexLove.deferCallback(function()
table.insert(order, 1)
end)
FlexLove.deferCallback(function()
table.insert(order, 2)
end)
FlexLove.deferCallback(function()
table.insert(order, 3)
end)
FlexLove.draw()
FlexLove.executeDeferredCallbacks()
luaunit.assertEquals(#order, 3)
luaunit.assertEquals(order[1], 1)
luaunit.assertEquals(order[2], 2)
luaunit.assertEquals(order[3], 3)
end
-- Test: deferCallback() with non-function argument
function TestFlexLove:testDeferCallbackInvalidArgument()
FlexLove.setMode("retained")
-- Should warn but not crash
FlexLove.deferCallback("not a function")
FlexLove.deferCallback(123)
FlexLove.deferCallback(nil)
FlexLove.draw()
luaunit.assertTrue(true)
end
-- Test: deferCallback() clears queue after execution
function TestFlexLove:testDeferCallbackClearsQueue()
FlexLove.setMode("retained")
local callCount = 0
FlexLove.deferCallback(function()
callCount = callCount + 1
end)
FlexLove.draw()
FlexLove.executeDeferredCallbacks() -- First execution
luaunit.assertEquals(callCount, 1)
FlexLove.draw()
FlexLove.executeDeferredCallbacks() -- Second execution should not call again
luaunit.assertEquals(callCount, 1)
end
-- Test: deferCallback() handles callback errors gracefully
function TestFlexLove:testDeferCallbackWithError()
FlexLove.setMode("retained")
local called = false
FlexLove.deferCallback(function()
error("Intentional error")
end)
FlexLove.deferCallback(function()
called = true
end)
-- Should not crash, second callback should still execute
FlexLove.draw()
FlexLove.executeDeferredCallbacks()
luaunit.assertTrue(called)
end
-- Test: External modules are exposed
function TestFlexLove:testExternalModulesExposed()
luaunit.assertNotNil(FlexLove.Animation)

View File

@@ -0,0 +1,199 @@
-- Test font cache optimizations
package.path = package.path .. ";./?.lua;./modules/?.lua"
local luaunit = require("testing.luaunit")
local loveStub = require("testing.loveStub")
-- Set up stub before requiring modules
_G.love = loveStub
local utils = require("modules.utils")
TestFontCache = {}
function TestFontCache:setUp()
utils.clearFontCache()
utils.resetFontCacheStats()
love.timer.setTime(0) -- Reset timer for consistent timestamps
end
function TestFontCache:tearDown()
utils.clearFontCache()
utils.resetFontCacheStats()
utils.setFontCacheSize(50) -- Reset to default
end
function TestFontCache:testCacheHitOnRepeatedAccess()
-- First access should be a miss
utils.FONT_CACHE.get(16, nil)
local stats1 = utils.getFontCacheStats()
luaunit.assertEquals(stats1.misses, 1)
luaunit.assertEquals(stats1.hits, 0)
-- Second access should be a hit
utils.FONT_CACHE.get(16, nil)
local stats2 = utils.getFontCacheStats()
luaunit.assertEquals(stats2.hits, 1)
luaunit.assertEquals(stats2.misses, 1)
-- Third access should also be a hit
utils.FONT_CACHE.get(16, nil)
local stats3 = utils.getFontCacheStats()
luaunit.assertEquals(stats3.hits, 2)
luaunit.assertEquals(stats3.misses, 1)
end
function TestFontCache:testCacheMissOnFirstAccess()
utils.clearFontCache()
utils.resetFontCacheStats()
utils.FONT_CACHE.get(24, nil)
local stats = utils.getFontCacheStats()
luaunit.assertEquals(stats.misses, 1)
luaunit.assertEquals(stats.hits, 0)
end
function TestFontCache:testLRUEviction()
utils.setFontCacheSize(3)
-- Load 3 fonts (fills cache) with time steps to ensure different timestamps
utils.FONT_CACHE.get(10, nil)
love.timer.step(0.001)
utils.FONT_CACHE.get(12, nil)
love.timer.step(0.001)
utils.FONT_CACHE.get(14, nil)
love.timer.step(0.001)
local stats1 = utils.getFontCacheStats()
luaunit.assertEquals(stats1.size, 3)
luaunit.assertEquals(stats1.evictions, 0)
-- Load 4th font (triggers eviction of font 10 - the oldest)
utils.FONT_CACHE.get(16, nil)
local stats2 = utils.getFontCacheStats()
luaunit.assertEquals(stats2.size, 3)
luaunit.assertEquals(stats2.evictions, 1)
-- Access first font again - it should have been evicted (miss)
local initialMisses = stats2.misses
utils.FONT_CACHE.get(10, nil)
local stats3 = utils.getFontCacheStats()
luaunit.assertEquals(stats3.misses, initialMisses + 1) -- Should be a miss
end
function TestFontCache:testCacheSizeLimitEnforced()
utils.setFontCacheSize(5)
-- Load 10 fonts
for i = 1, 10 do
utils.FONT_CACHE.get(10 + i, nil)
end
local stats = utils.getFontCacheStats()
luaunit.assertEquals(stats.size, 5)
luaunit.assertTrue(stats.evictions >= 5)
end
function TestFontCache:testFontRounding()
-- Sizes should be rounded: 14.5 and 14.7 should map to same cache entry (15)
utils.FONT_CACHE.get(14.5, nil)
local stats1 = utils.getFontCacheStats()
luaunit.assertEquals(stats1.misses, 1)
utils.FONT_CACHE.get(14.7, nil)
local stats2 = utils.getFontCacheStats()
luaunit.assertEquals(stats2.hits, 1) -- Should be a hit because both round to 15
luaunit.assertEquals(stats2.misses, 1)
end
function TestFontCache:testCacheClear()
utils.FONT_CACHE.get(16, nil)
utils.FONT_CACHE.get(18, nil)
local stats1 = utils.getFontCacheStats()
luaunit.assertEquals(stats1.size, 2)
utils.clearFontCache()
local stats2 = utils.getFontCacheStats()
luaunit.assertEquals(stats2.size, 0)
end
function TestFontCache:testCacheKeyWithPath()
-- Different cache keys for same size, different paths
utils.FONT_CACHE.get(16, nil)
utils.FONT_CACHE.get(16, "fonts/custom.ttf")
local stats = utils.getFontCacheStats()
luaunit.assertEquals(stats.misses, 2) -- Both should be misses
luaunit.assertEquals(stats.size, 2)
end
function TestFontCache:testPreloadFont()
utils.clearFontCache()
utils.resetFontCacheStats()
-- Preload multiple sizes
utils.preloadFont(nil, {12, 14, 16, 18})
local stats1 = utils.getFontCacheStats()
luaunit.assertEquals(stats1.size, 4)
luaunit.assertEquals(stats1.misses, 4) -- All preloads are misses
-- Now access one - should be a hit
utils.FONT_CACHE.get(16, nil)
local stats2 = utils.getFontCacheStats()
luaunit.assertEquals(stats2.hits, 1)
end
function TestFontCache:testCacheHitRate()
utils.clearFontCache()
utils.resetFontCacheStats()
-- 1 miss, 9 hits = 90% hit rate
utils.FONT_CACHE.get(16, nil)
for i = 1, 9 do
utils.FONT_CACHE.get(16, nil)
end
local stats = utils.getFontCacheStats()
luaunit.assertEquals(stats.hitRate, 0.9)
end
function TestFontCache:testSetCacheSizeEvictsExcess()
utils.setFontCacheSize(10)
-- Load 10 fonts
for i = 1, 10 do
utils.FONT_CACHE.get(10 + i, nil)
end
local stats1 = utils.getFontCacheStats()
luaunit.assertEquals(stats1.size, 10)
-- Reduce cache size - should trigger evictions
utils.setFontCacheSize(5)
local stats2 = utils.getFontCacheStats()
luaunit.assertEquals(stats2.size, 5)
luaunit.assertTrue(stats2.evictions >= 5)
end
function TestFontCache:testMinimalCacheSize()
-- Minimum cache size is 1
utils.setFontCacheSize(0)
utils.FONT_CACHE.get(16, nil)
local stats = utils.getFontCacheStats()
luaunit.assertEquals(stats.size, 1)
end
-- Run tests if executed directly
if arg and arg[0]:find("font_cache_test%.lua$") then
os.exit(luaunit.LuaUnit.run())
end
return TestFontCache

View File

@@ -0,0 +1,167 @@
-- Test Performance Instrumentation
package.path = package.path .. ";./?.lua;./modules/?.lua"
local luaunit = require("testing.luaunit")
local loveStub = require("testing.loveStub")
-- Set up stub before requiring modules
_G.love = loveStub
local Performance = require("modules.Performance")
TestPerformanceInstrumentation = {}
function TestPerformanceInstrumentation:setUp()
Performance.reset()
Performance.enable()
end
function TestPerformanceInstrumentation:tearDown()
Performance.disable()
Performance.reset()
end
function TestPerformanceInstrumentation:testTimerStartStop()
Performance.startTimer("test_operation")
-- Simulate some work
local sum = 0
for i = 1, 1000 do
sum = sum + i
end
local elapsed = Performance.stopTimer("test_operation")
luaunit.assertNotNil(elapsed)
luaunit.assertTrue(elapsed >= 0)
local metrics = Performance.getMetrics()
luaunit.assertNotNil(metrics.timings["test_operation"])
luaunit.assertEquals(metrics.timings["test_operation"].count, 1)
end
function TestPerformanceInstrumentation:testMultipleTimers()
-- Start multiple timers
Performance.startTimer("layout")
Performance.startTimer("render")
local sum = 0
for i = 1, 100 do sum = sum + i end
Performance.stopTimer("layout")
Performance.stopTimer("render")
local metrics = Performance.getMetrics()
luaunit.assertNotNil(metrics.timings["layout"])
luaunit.assertNotNil(metrics.timings["render"])
end
function TestPerformanceInstrumentation:testFrameTiming()
Performance.startFrame()
-- Simulate frame work
local sum = 0
for i = 1, 1000 do
sum = sum + i
end
Performance.endFrame()
local frameMetrics = Performance.getFrameMetrics()
luaunit.assertNotNil(frameMetrics)
luaunit.assertEquals(frameMetrics.frameCount, 1)
luaunit.assertTrue(frameMetrics.lastFrameTime >= 0)
end
function TestPerformanceInstrumentation:testDrawCallCounting()
Performance.incrementCounter("draw_calls", 1)
Performance.incrementCounter("draw_calls", 1)
Performance.incrementCounter("draw_calls", 1)
local counter = Performance.getFrameCounter("draw_calls")
luaunit.assertEquals(counter, 3)
-- Reset and check
Performance.resetFrameCounters()
counter = Performance.getFrameCounter("draw_calls")
luaunit.assertEquals(counter, 0)
end
function TestPerformanceInstrumentation:testHUDToggle()
luaunit.assertFalse(Performance.getConfig().hudEnabled)
Performance.toggleHUD()
luaunit.assertTrue(Performance.getConfig().hudEnabled)
Performance.toggleHUD()
luaunit.assertFalse(Performance.getConfig().hudEnabled)
end
function TestPerformanceInstrumentation:testEnableDisable()
Performance.enable()
luaunit.assertTrue(Performance.isEnabled())
Performance.disable()
luaunit.assertFalse(Performance.isEnabled())
-- Timers should not record when disabled
Performance.startTimer("disabled_test")
local elapsed = Performance.stopTimer("disabled_test")
luaunit.assertNil(elapsed)
end
function TestPerformanceInstrumentation:testMeasureFunction()
local function expensiveOperation(n)
local sum = 0
for i = 1, n do
sum = sum + i
end
return sum
end
local wrapped = Performance.measure("expensive_op", expensiveOperation)
local result = wrapped(1000)
luaunit.assertEquals(result, 500500) -- sum of 1 to 1000
local metrics = Performance.getMetrics()
luaunit.assertNotNil(metrics.timings["expensive_op"])
luaunit.assertEquals(metrics.timings["expensive_op"].count, 1)
end
function TestPerformanceInstrumentation:testMemoryTracking()
Performance.updateMemory()
local memMetrics = Performance.getMemoryMetrics()
luaunit.assertNotNil(memMetrics)
luaunit.assertTrue(memMetrics.currentKb > 0)
luaunit.assertTrue(memMetrics.currentMb > 0)
luaunit.assertTrue(memMetrics.peakKb >= memMetrics.currentKb)
end
function TestPerformanceInstrumentation:testExportJSON()
Performance.startTimer("test_op")
Performance.stopTimer("test_op")
local json = Performance.exportJSON()
luaunit.assertNotNil(json)
luaunit.assertTrue(string.find(json, "fps") ~= nil)
luaunit.assertTrue(string.find(json, "test_op") ~= nil)
end
function TestPerformanceInstrumentation:testExportCSV()
Performance.startTimer("test_op")
Performance.stopTimer("test_op")
local csv = Performance.exportCSV()
luaunit.assertNotNil(csv)
luaunit.assertTrue(string.find(csv, "Name,Average") ~= nil)
luaunit.assertTrue(string.find(csv, "test_op") ~= nil)
end
-- Run tests if executed directly
if arg and arg[0]:find("performance_instrumentation_test%.lua$") then
os.exit(luaunit.LuaUnit.run())
end
return TestPerformanceInstrumentation

View File

@@ -0,0 +1,156 @@
local luaunit = require("testing.luaunit")
require("testing.loveStub")
local FlexLove = require("FlexLove")
local Performance = require("modules.Performance")
local Element = FlexLove.Element
TestPerformanceWarnings = {}
function TestPerformanceWarnings:setUp()
-- Enable performance warnings
Performance.setConfig("warningsEnabled", true)
Performance.resetShownWarnings()
end
function TestPerformanceWarnings:tearDown()
-- Reset warnings
Performance.resetShownWarnings()
end
-- Test hierarchy depth warning
function TestPerformanceWarnings:testHierarchyDepthWarning()
-- Create a deep hierarchy (20 levels)
local root = Element.new({
id = "root",
width = 100,
height = 100,
}, Element.defaultDependencies)
local current = root
for i = 1, 20 do
local child = Element.new({
id = "child_" .. i,
width = 50,
height = 50,
parent = current,
}, Element.defaultDependencies)
table.insert(current.children, child)
current = child
end
-- This should trigger a hierarchy depth warning
root:layoutChildren()
-- Check that element was created successfully despite warning
luaunit.assertNotNil(current)
luaunit.assertEquals(current:getHierarchyDepth(), 20)
end
-- Test element count warning
function TestPerformanceWarnings:testElementCountWarning()
-- Create a container with many children (simulating 1000+ elements)
local root = Element.new({
id = "root",
width = 1000,
height = 1000,
}, Element.defaultDependencies)
-- Add many child elements
for i = 1, 50 do -- Keep test fast, just verify the counting logic works
local child = Element.new({
id = "child_" .. i,
width = 20,
height = 20,
parent = root,
}, Element.defaultDependencies)
table.insert(root.children, child)
end
local count = root:countElements()
luaunit.assertEquals(count, 51) -- root + 50 children
end
-- Test animation count warning
function TestPerformanceWarnings:testAnimationTracking()
local root = Element.new({
id = "root",
width = 100,
height = 100,
}, Element.defaultDependencies)
-- Add some animated children
for i = 1, 3 do
local child = Element.new({
id = "animated_child_" .. i,
width = 20,
height = 20,
parent = root,
}, Element.defaultDependencies)
-- Add mock animation
child.animation = {
update = function()
return false
end,
interpolate = function()
return { width = 20, height = 20 }
end,
}
table.insert(root.children, child)
end
local animCount = root:_countActiveAnimations()
luaunit.assertEquals(animCount, 3)
end
-- Test warnings can be disabled
function TestPerformanceWarnings:testWarningsCanBeDisabled()
Performance.setConfig("warningsEnabled", false)
-- Create deep hierarchy
local root = Element.new({
id = "root",
width = 100,
height = 100,
}, Element.defaultDependencies)
local current = root
for i = 1, 20 do
local child = Element.new({
id = "child_" .. i,
width = 50,
height = 50,
parent = current,
}, Element.defaultDependencies)
table.insert(current.children, child)
current = child
end
-- Should not trigger warning (but should still create elements)
root:layoutChildren()
luaunit.assertEquals(current:getHierarchyDepth(), 20)
-- Re-enable for other tests
Performance.setConfig("warningsEnabled", true)
end
-- Test layout recalculation tracking
function TestPerformanceWarnings:testLayoutRecalculationTracking()
local root = Element.new({
id = "root",
width = 100,
height = 100,
}, Element.defaultDependencies)
-- Layout multiple times (simulating layout thrashing)
for i = 1, 5 do
root:layoutChildren()
end
-- Should complete without crashing
luaunit.assertNotNil(root)
end
return TestPerformanceWarnings