Replacing errors with warns in non-critical areas

This commit is contained in:
Michael Freno
2025-11-17 01:56:02 -05:00
parent f4d514bf2e
commit e5e7b55709
25 changed files with 3596 additions and 313 deletions

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ docs/doc.json
docs/doc.md docs/doc.md
docs/node_modules docs/node_modules
releases/ releases/
*.log*

View File

@@ -9,6 +9,7 @@ local utils = req("utils")
local Units = req("Units") local Units = req("Units")
local Context = req("Context") local Context = req("Context")
local StateManager = req("StateManager") local StateManager = req("StateManager")
local ErrorCodes = req("ErrorCodes")
local ErrorHandler = req("ErrorHandler") local ErrorHandler = req("ErrorHandler")
local ImageRenderer = req("ImageRenderer") local ImageRenderer = req("ImageRenderer")
local NinePatch = req("NinePatch") local NinePatch = req("NinePatch")
@@ -58,12 +59,32 @@ Element.defaultDependencies = {
---@class FlexLove ---@class FlexLove
local flexlove = Context local flexlove = Context
-- Initialize ErrorHandler with ErrorCodes dependency
ErrorHandler.init({ ErrorCodes = ErrorCodes })
-- Initialize modules that use ErrorHandler via DI
local errorHandlerDeps = { ErrorHandler = ErrorHandler }
if ImageRenderer.init then ImageRenderer.init(errorHandlerDeps) end
if ImageScaler then
local ImageScaler = req("ImageScaler")
if ImageScaler.init then ImageScaler.init(errorHandlerDeps) end
end
if NinePatch.init then NinePatch.init(errorHandlerDeps) end
local ImageDataReader = req("ImageDataReader")
if ImageDataReader.init then ImageDataReader.init(errorHandlerDeps) end
-- Initialize Units module with Context dependency -- Initialize Units module with Context dependency
Units.initialize(Context) Units.initialize(Context)
Units.initializeErrorHandler(ErrorHandler) Units.initializeErrorHandler(ErrorHandler)
-- Initialize ErrorHandler for Color module
Color.initializeErrorHandler(ErrorHandler)
-- Initialize ErrorHandler for utils
utils.initializeErrorHandler(ErrorHandler)
-- Add version and metadata -- Add version and metadata
flexlove._VERSION = "0.2.1" flexlove._VERSION = "0.2.2"
flexlove._DESCRIPTION = "0I Library for LÖVE Framework based on flexbox" flexlove._DESCRIPTION = "0I Library for LÖVE Framework based on flexbox"
flexlove._URL = "https://github.com/mikefreno/FlexLove" flexlove._URL = "https://github.com/mikefreno/FlexLove"
flexlove._LICENSE = [[ flexlove._LICENSE = [[
@@ -90,10 +111,20 @@ flexlove._LICENSE = [[
SOFTWARE. SOFTWARE.
]] ]]
---@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition, immediateMode?: boolean, stateRetentionFrames?: number, maxStateEntries?: number, autoFrameManagement?: boolean} ---@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition, immediateMode?: boolean, stateRetentionFrames?: number, maxStateEntries?: number, autoFrameManagement?: boolean, errorLogFile?: string, enableErrorLogging?: boolean}
function flexlove.init(config) function flexlove.init(config)
config = config or {} config = config or {}
-- Configure error logging
if config.errorLogFile then
ErrorHandler.setLogTarget("file")
ErrorHandler.setLogFile(config.errorLogFile)
elseif config.enableErrorLogging == true then
-- Use default log file if logging enabled but no path specified
ErrorHandler.setLogTarget("file")
ErrorHandler.setLogFile("flexlove-errors.log")
end
if config.baseScale then if config.baseScale then
flexlove.baseScale = { flexlove.baseScale = {
width = config.baseScale.width or 1920, width = config.baseScale.width or 1920,

View File

@@ -1,4 +1,4 @@
# FlexLöve v0.2.1 # FlexLöve v0.2.2
**A comprehensive UI library providing flexbox/grid layouts, theming, animations, and event handling for LÖVE2D games.** **A comprehensive UI library providing flexbox/grid layouts, theming, animations, and event handling for LÖVE2D games.**

View File

@@ -1,4 +1,12 @@
--- Standardized error message formatter local ErrorHandler = nil
--- Initialize ErrorHandler dependency
---@param errorHandler table The ErrorHandler module
local function initializeErrorHandler(errorHandler)
ErrorHandler = errorHandler
end
--- Standardized error message formatter (fallback for when ErrorHandler not available)
---@param module string -- Module name (e.g., "Color", "Theme", "Units") ---@param module string -- Module name (e.g., "Color", "Theme", "Units")
---@param message string ---@param message string
---@return string ---@return string
@@ -77,7 +85,14 @@ function Color.fromHex(hexWithTag)
local g = tonumber("0x" .. hex:sub(3, 4)) local g = tonumber("0x" .. hex:sub(3, 4))
local b = tonumber("0x" .. hex:sub(5, 6)) local b = tonumber("0x" .. hex:sub(5, 6))
if not r or not g or not b then if not r or not g or not b then
error(formatError("Color", string.format("Invalid hex string format: '%s'. Contains invalid hex digits", hexWithTag))) if ErrorHandler then
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
input = hexWithTag,
issue = "invalid hex digits",
fallback = "white (#FFFFFF)"
})
end
return Color.new(1, 1, 1, 1) -- Return white as fallback
end end
return Color.new(r / 255, g / 255, b / 255, 1) return Color.new(r / 255, g / 255, b / 255, 1)
elseif #hex == 8 then elseif #hex == 8 then
@@ -86,11 +101,26 @@ function Color.fromHex(hexWithTag)
local b = tonumber("0x" .. hex:sub(5, 6)) local b = tonumber("0x" .. hex:sub(5, 6))
local a = tonumber("0x" .. hex:sub(7, 8)) local a = tonumber("0x" .. hex:sub(7, 8))
if not r or not g or not b or not a then if not r or not g or not b or not a then
error(formatError("Color", string.format("Invalid hex string format: '%s'. Contains invalid hex digits", hexWithTag))) if ErrorHandler then
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
input = hexWithTag,
issue = "invalid hex digits",
fallback = "white (#FFFFFFFF)"
})
end
return Color.new(1, 1, 1, 1) -- Return white as fallback
end end
return Color.new(r / 255, g / 255, b / 255, a / 255) return Color.new(r / 255, g / 255, b / 255, a / 255)
else else
error(formatError("Color", string.format("Invalid hex string format: '%s'. Expected #RRGGBB or #RRGGBBAA", hexWithTag))) if ErrorHandler then
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
input = hexWithTag,
expected = "#RRGGBB or #RRGGBBAA",
hexLength = #hex,
fallback = "white (#FFFFFF)"
})
end
return Color.new(1, 1, 1, 1) -- Return white as fallback
end end
end end
@@ -373,4 +403,7 @@ function Color.parse(value)
return Color.sanitizeColor(value, Color.new(0, 0, 0, 1)) return Color.sanitizeColor(value, Color.new(0, 0, 0, 1))
end end
-- Export ErrorHandler initializer
Color.initializeErrorHandler = initializeErrorHandler
return Color return Color

View File

@@ -148,7 +148,8 @@ Element.__index = Element
---@return Element ---@return Element
function Element.new(props, deps) function Element.new(props, deps)
if not deps then if not deps then
error("[Element] deps parameter is required. Pass Element.defaultDependencies from FlexLove.") -- Can't use ErrorHandler yet since deps contains it
error("[FlexLove - Element] Error: deps parameter is required. Pass Element.defaultDependencies from FlexLove.")
end end
local self = setmetatable({}, Element) local self = setmetatable({}, Element)

View File

@@ -1,5 +1,3 @@
--- Error code definitions for FlexLove
--- Provides centralized error codes, descriptions, and suggested fixes
---@class ErrorCodes ---@class ErrorCodes
local ErrorCodes = {} local ErrorCodes = {}

View File

@@ -1,27 +1,509 @@
-- modules/ErrorHandler.lua
local ErrorHandler = {} local ErrorHandler = {}
local ErrorCodes = nil -- Will be injected via init
--- Format an error or warning message local LOG_LEVELS = {
CRITICAL = 1,
ERROR = 2,
WARNING = 3,
INFO = 4,
DEBUG = 5,
}
local config = {
debugMode = false,
includeStackTrace = false,
logLevel = LOG_LEVELS.WARNING, -- Default: log errors and warnings
logTarget = "console", -- Options: "console", "file", "both", "none"
logFormat = "human", -- Options: "human", "json"
logFile = "flexlove-errors.log",
maxLogSize = 10 * 1024 * 1024, -- 10MB default
maxLogFiles = 5, -- Keep 5 rotated logs
enableRotation = true,
}
-- Internal state
local logFileHandle = nil
local currentLogSize = 0
--- Initialize ErrorHandler with dependencies
---@param deps table Dependencies table with ErrorCodes
function ErrorHandler.init(deps)
if deps and deps.ErrorCodes then
ErrorCodes = deps.ErrorCodes
else
-- Try to require if not provided (backward compatibility)
local success, module = pcall(require, "modules.ErrorCodes")
if success then
ErrorCodes = module
else
-- Create minimal stub if ErrorCodes not available
ErrorCodes = {
get = function() return nil end,
describe = function(code) return code end,
getSuggestion = function() return "" end,
}
end
end
end
--- Set debug mode (enables stack traces and verbose output)
---@param enabled boolean Enable debug mode
function ErrorHandler.setDebugMode(enabled)
config.debugMode = enabled
config.includeStackTrace = enabled
if enabled then
config.logLevel = LOG_LEVELS.DEBUG
end
end
--- Set whether to include stack traces
---@param enabled boolean Enable stack traces
function ErrorHandler.setStackTrace(enabled)
config.includeStackTrace = enabled
end
--- Set log level (minimum level to log)
---@param level string|number Log level ("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG") or number
function ErrorHandler.setLogLevel(level)
if type(level) == "string" then
config.logLevel = LOG_LEVELS[level:upper()] or LOG_LEVELS.WARNING
elseif type(level) == "number" then
config.logLevel = level
end
end
--- Set log target
---@param target string "console", "file", "both", or "none"
function ErrorHandler.setLogTarget(target)
config.logTarget = target
-- Note: File will be opened lazily on first write
if target == "console" or target == "none" then
-- Close log file if open
if logFileHandle then
logFileHandle:close()
logFileHandle = nil
currentLogSize = 0
end
end
end
--- Set log format
---@param format string "human" or "json"
function ErrorHandler.setLogFormat(format)
config.logFormat = format
end
--- Set log file path
---@param path string Path to log file
function ErrorHandler.setLogFile(path)
-- Close existing log file
if logFileHandle then
logFileHandle:close()
logFileHandle = nil
end
config.logFile = path
currentLogSize = 0
-- Note: File will be opened lazily on first write
end
--- Enable/disable log rotation
---@param enabled boolean|table Enable rotation or config table
function ErrorHandler.enableLogRotation(enabled)
if type(enabled) == "boolean" then
config.enableRotation = enabled
elseif type(enabled) == "table" then
config.enableRotation = true
if enabled.maxSize then
config.maxLogSize = enabled.maxSize
end
if enabled.maxFiles then
config.maxLogFiles = enabled.maxFiles
end
end
end
--- Get current timestamp with milliseconds
---@return string Formatted timestamp
local function getTimestamp()
local time = os.time()
local date = os.date("%Y-%m-%d %H:%M:%S", time)
-- Note: Lua doesn't have millisecond precision by default, so we approximate
return date
end
--- Rotate log file if needed
local function rotateLogIfNeeded()
if not config.enableRotation then
return
end
if currentLogSize < config.maxLogSize then
return
end
-- Close current log
if logFileHandle then
logFileHandle:close()
logFileHandle = nil
end
-- Rotate existing logs
for i = config.maxLogFiles - 1, 1, -1 do
local oldName = config.logFile .. "." .. i
local newName = config.logFile .. "." .. (i + 1)
os.rename(oldName, newName) -- Will fail silently if file doesn't exist
end
-- Move current log to .1
os.rename(config.logFile, config.logFile .. ".1")
-- Create new log file
logFileHandle = io.open(config.logFile, "a")
currentLogSize = 0
end
--- Escape string for JSON
---@param str string String to escape
---@return string Escaped string
local function escapeJson(str)
str = tostring(str)
str = str:gsub("\\", "\\\\")
str = str:gsub('"', '\\"')
str = str:gsub("\n", "\\n")
str = str:gsub("\r", "\\r")
str = str:gsub("\t", "\\t")
return str
end
--- Format details as JSON object
---@param details table|nil Details object
---@return string JSON string
local function formatDetailsJson(details)
if not details or type(details) ~= "table" then
return "{}"
end
local parts = {}
for key, value in pairs(details) do
local jsonKey = escapeJson(tostring(key))
local jsonValue = escapeJson(tostring(value))
table.insert(parts, string.format('"%s":"%s"', jsonKey, jsonValue))
end
return "{" .. table.concat(parts, ",") .. "}"
end
--- Format details object as readable key-value pairs
---@param details table|nil Details object
---@return string Formatted details
local function formatDetails(details)
if not details or type(details) ~= "table" then
return ""
end
local lines = {}
for key, value in pairs(details) do
local formattedKey = tostring(key):gsub("^%l", string.upper)
local formattedValue = tostring(value)
-- Truncate very long values
if #formattedValue > 100 then
formattedValue = formattedValue:sub(1, 97) .. "..."
end
table.insert(lines, string.format(" %s: %s", formattedKey, formattedValue))
end
if #lines > 0 then
return "\n\nDetails:\n" .. table.concat(lines, "\n")
end
return ""
end
--- Extract and format stack trace
---@param level number Stack level to start from
---@return string Formatted stack trace
local function formatStackTrace(level)
if not config.includeStackTrace then
return ""
end
local lines = {}
local currentLevel = level or 3
while true do
local info = debug.getinfo(currentLevel, "Sl")
if not info then
break
end
-- Skip internal Lua files
if info.source:match("^@") and not info.source:match("loveStub") then
local source = info.source:sub(2) -- Remove @ prefix
local location = string.format("%s:%d", source, info.currentline)
table.insert(lines, " " .. location)
end
currentLevel = currentLevel + 1
if currentLevel > level + 10 then
break
end -- Limit depth
end
if #lines > 0 then
return "\n\nStack trace:\n" .. table.concat(lines, "\n")
end
return ""
end
--- Format an error or warning message with optional error code
---@param module string The module name (e.g., "Element", "Units", "Theme") ---@param module string The module name (e.g., "Element", "Units", "Theme")
---@param level string "Error" or "Warning" ---@param level string "Error" or "Warning"
---@param message string The error/warning message ---@param codeOrMessage string Error code (e.g., "VAL_001") or message
---@param messageOrDetails string|table|nil Message or details object
---@param detailsOrSuggestion table|string|nil Details or suggestion
---@param suggestionOrNil string|nil Suggestion
---@return string Formatted message ---@return string Formatted message
local function formatMessage(module, level, message) local function formatMessage(module, level, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestionOrNil)
return string.format("[FlexLove - %s] %s: %s", module, level, message) local code = nil
local message = codeOrMessage
local details = nil
local suggestion = nil
-- Parse arguments (support multiple signatures)
if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then
-- Called with error code
code = codeOrMessage
message = messageOrDetails or ErrorCodes.describe(code)
if type(detailsOrSuggestion) == "table" then
details = detailsOrSuggestion
suggestion = suggestionOrNil or ErrorCodes.getSuggestion(code)
elseif type(detailsOrSuggestion) == "string" then
suggestion = detailsOrSuggestion
else
suggestion = ErrorCodes.getSuggestion(code)
end
else
-- Called with message only (backward compatibility)
message = codeOrMessage
if type(messageOrDetails) == "table" then
details = messageOrDetails
suggestion = detailsOrSuggestion
elseif type(messageOrDetails) == "string" then
suggestion = messageOrDetails
end
end
-- Build formatted message
local parts = {}
-- Header: [FlexLove - Module] Level [CODE]: Message
if code then
local codeInfo = ErrorCodes.get(code)
table.insert(parts, string.format("[FlexLove - %s] %s [%s]: %s", module, level, codeInfo.code, message))
else
table.insert(parts, string.format("[FlexLove - %s] %s: %s", module, level, message))
end
-- Details section
if details then
table.insert(parts, formatDetails(details))
end
-- Suggestion section
if suggestion and suggestion ~= "" then
table.insert(parts, string.format("\n\nSuggestion: %s", suggestion))
end
return table.concat(parts, "")
end
--- Write log entry to file and/or console
---@param level string Log level
---@param levelNum number Log level number
---@param module string Module name
---@param code string|nil Error code
---@param message string Message
---@param details table|nil Details
---@param suggestion string|nil Suggestion
local function writeLog(level, levelNum, module, code, message, details, suggestion)
-- Check if we should log this level
if levelNum > config.logLevel then
return
end
local timestamp = getTimestamp()
local logEntry
if config.logFormat == "json" then
-- JSON format
local jsonParts = {
string.format('"timestamp":"%s"', escapeJson(timestamp)),
string.format('"level":"%s"', level),
string.format('"module":"%s"', escapeJson(module)),
string.format('"message":"%s"', escapeJson(message)),
}
if code then
table.insert(jsonParts, string.format('"code":"%s"', escapeJson(code)))
end
if details then
table.insert(jsonParts, string.format('"details":%s', formatDetailsJson(details)))
end
if suggestion then
table.insert(jsonParts, string.format('"suggestion":"%s"', escapeJson(suggestion)))
end
logEntry = "{" .. table.concat(jsonParts, ",") .. "}\n"
else
-- Human-readable format
local parts = {
string.format("[%s] [%s] [%s]", timestamp, level, module),
}
if code then
table.insert(parts, string.format("[%s]", code))
end
table.insert(parts, message)
logEntry = table.concat(parts, " ") .. "\n"
if details then
logEntry = logEntry .. formatDetails(details):gsub("^\n\n", "") .. "\n"
end
if suggestion then
logEntry = logEntry .. "Suggestion: " .. suggestion .. "\n"
end
logEntry = logEntry .. "\n"
end
-- Write to console
if config.logTarget == "console" or config.logTarget == "both" then
io.write(logEntry)
io.flush()
end
-- Write to file
if config.logTarget == "file" or config.logTarget == "both" then
-- Lazy file opening: open on first write
if not logFileHandle then
logFileHandle = io.open(config.logFile, "a")
if logFileHandle then
-- Get current file size
local currentPos = logFileHandle:seek("end")
currentLogSize = currentPos or 0
end
end
if logFileHandle then
rotateLogIfNeeded()
-- Reopen if rotation closed it
if not logFileHandle then
logFileHandle = io.open(config.logFile, "a")
end
if logFileHandle then
logFileHandle:write(logEntry)
logFileHandle:flush()
currentLogSize = currentLogSize + #logEntry
end
end
end
end end
--- Throw a critical error (stops execution) --- Throw a critical error (stops execution)
---@param module string The module name ---@param module string The module name
---@param message string The error message ---@param codeOrMessage string Error code or message
function ErrorHandler.error(module, message) ---@param messageOrDetails string|table|nil Message or details
error(formatMessage(module, "Error", message), 2) ---@param detailsOrSuggestion table|string|nil Details or suggestion
---@param suggestion string|nil Suggestion
function ErrorHandler.error(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
local formattedMessage = formatMessage(module, "Error", codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
-- Parse arguments for logging
local code = nil
local message = codeOrMessage
local details = nil
local logSuggestion = nil
if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then
code = codeOrMessage
message = messageOrDetails or ErrorCodes.describe(code)
if type(detailsOrSuggestion) == "table" then
details = detailsOrSuggestion
logSuggestion = suggestion or ErrorCodes.getSuggestion(code)
elseif type(detailsOrSuggestion) == "string" then
logSuggestion = detailsOrSuggestion
else
logSuggestion = ErrorCodes.getSuggestion(code)
end
else
message = codeOrMessage
if type(messageOrDetails) == "table" then
details = messageOrDetails
logSuggestion = detailsOrSuggestion
elseif type(messageOrDetails) == "string" then
logSuggestion = messageOrDetails
end
end
-- Log the error
writeLog("ERROR", LOG_LEVELS.ERROR, module, code, message, details, logSuggestion)
-- Add stack trace if enabled
if config.includeStackTrace then
formattedMessage = formattedMessage .. formatStackTrace(3)
end
error(formattedMessage, 2)
end end
--- Print a warning (non-critical, continues execution) --- Print a warning (non-critical, continues execution)
---@param module string The module name ---@param module string The module name
---@param message string The warning message ---@param codeOrMessage string Warning code or message
function ErrorHandler.warn(module, message) ---@param messageOrDetails string|table|nil Message or details
print(formatMessage(module, "Warning", message)) ---@param detailsOrSuggestion table|string|nil Details or suggestion
---@param suggestion string|nil Suggestion
function ErrorHandler.warn(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
-- Parse arguments for logging
local code = nil
local message = codeOrMessage
local details = nil
local logSuggestion = nil
if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then
code = codeOrMessage
message = messageOrDetails or ErrorCodes.describe(code)
if type(detailsOrSuggestion) == "table" then
details = detailsOrSuggestion
logSuggestion = suggestion or ErrorCodes.getSuggestion(code)
elseif type(detailsOrSuggestion) == "string" then
logSuggestion = detailsOrSuggestion
else
logSuggestion = ErrorCodes.getSuggestion(code)
end
else
message = codeOrMessage
if type(messageOrDetails) == "table" then
details = messageOrDetails
logSuggestion = detailsOrSuggestion
elseif type(messageOrDetails) == "string" then
logSuggestion = messageOrDetails
end
end
-- Log the warning
writeLog("WARNING", LOG_LEVELS.WARNING, module, code, message, details, logSuggestion)
local formattedMessage = formatMessage(module, "Warning", codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
print(formattedMessage)
end end
--- Validate that a value is not nil --- Validate that a value is not nil
@@ -31,7 +513,9 @@ end
---@return boolean True if valid ---@return boolean True if valid
function ErrorHandler.assertNotNil(module, value, paramName) function ErrorHandler.assertNotNil(module, value, paramName)
if value == nil then if value == nil then
ErrorHandler.error(module, string.format("Parameter '%s' cannot be nil", paramName)) ErrorHandler.error(module, "VAL_003", "Required parameter missing", {
parameter = paramName,
})
return false return false
end end
return true return true
@@ -46,10 +530,11 @@ end
function ErrorHandler.assertType(module, value, expectedType, paramName) function ErrorHandler.assertType(module, value, expectedType, paramName)
local actualType = type(value) local actualType = type(value)
if actualType ~= expectedType then if actualType ~= expectedType then
ErrorHandler.error(module, string.format( ErrorHandler.error(module, "VAL_001", "Invalid property type", {
"Parameter '%s' must be %s, got %s", property = paramName,
paramName, expectedType, actualType expected = expectedType,
)) got = actualType,
})
return false return false
end end
return true return true
@@ -64,10 +549,12 @@ end
---@return boolean True if valid ---@return boolean True if valid
function ErrorHandler.assertRange(module, value, min, max, paramName) function ErrorHandler.assertRange(module, value, min, max, paramName)
if value < min or value > max then if value < min or value > max then
ErrorHandler.error(module, string.format( ErrorHandler.error(module, "VAL_002", "Property value out of range", {
"Parameter '%s' must be between %s and %s, got %s", property = paramName,
paramName, tostring(min), tostring(max), tostring(value) min = tostring(min),
)) max = tostring(max),
value = tostring(value),
})
return false return false
end end
return true return true
@@ -78,10 +565,7 @@ end
---@param oldName string The deprecated name ---@param oldName string The deprecated name
---@param newName string The new name to use ---@param newName string The new name to use
function ErrorHandler.warnDeprecated(module, oldName, newName) function ErrorHandler.warnDeprecated(module, oldName, newName)
ErrorHandler.warn(module, string.format( ErrorHandler.warn(module, string.format("'%s' is deprecated. Use '%s' instead", oldName, newName))
"'%s' is deprecated. Use '%s' instead",
oldName, newName
))
end end
--- Warn about a common mistake --- Warn about a common mistake
@@ -89,10 +573,7 @@ end
---@param issue string Description of the issue ---@param issue string Description of the issue
---@param suggestion string Suggested fix ---@param suggestion string Suggested fix
function ErrorHandler.warnCommonMistake(module, issue, suggestion) function ErrorHandler.warnCommonMistake(module, issue, suggestion)
ErrorHandler.warn(module, string.format( ErrorHandler.warn(module, string.format("%s. Suggestion: %s", issue, suggestion))
"%s. Suggestion: %s",
issue, suggestion
))
end end
return ErrorHandler return ErrorHandler

View File

@@ -1,18 +1,21 @@
--- Standardized error message formatter
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
---@param message string -- Error message
---@return string -- Formatted error message
local function formatError(module, message)
return string.format("[FlexLove.%s] %s", module, message)
end
local ImageDataReader = {} local ImageDataReader = {}
-- ErrorHandler will be injected via init
local ErrorHandler = nil
--- Initialize ImageDataReader with dependencies
---@param deps table Dependencies table with ErrorHandler
function ImageDataReader.init(deps)
if deps and deps.ErrorHandler then
ErrorHandler = deps.ErrorHandler
end
end
---@param imagePath string ---@param imagePath string
---@return love.ImageData ---@return love.ImageData
function ImageDataReader.loadImageData(imagePath) function ImageDataReader.loadImageData(imagePath)
if not imagePath then if not imagePath then
error(formatError("ImageDataReader", "Image path cannot be nil")) ErrorHandler.error("ImageDataReader", "VAL_001", "Image path cannot be nil")
end end
local success, result = pcall(function() local success, result = pcall(function()
@@ -20,7 +23,10 @@ function ImageDataReader.loadImageData(imagePath)
end) end)
if not success then if not success then
error(formatError("ImageDataReader", "Failed to load image data from '" .. imagePath .. "': " .. tostring(result))) ErrorHandler.error("ImageDataReader", "RES_001", "Failed to load image data from '" .. imagePath .. "': " .. tostring(result), {
imagePath = imagePath,
error = tostring(result),
})
end end
return result return result
@@ -32,14 +38,17 @@ end
---@return table -- Array of {r, g, b, a} values (0-255 range) ---@return table -- Array of {r, g, b, a} values (0-255 range)
function ImageDataReader.getRow(imageData, rowIndex) function ImageDataReader.getRow(imageData, rowIndex)
if not imageData then if not imageData then
error(formatError("ImageDataReader", "ImageData cannot be nil")) ErrorHandler.error("ImageDataReader", "VAL_001", "ImageData cannot be nil")
end end
local width = imageData:getWidth() local width = imageData:getWidth()
local height = imageData:getHeight() local height = imageData:getHeight()
if rowIndex < 0 or rowIndex >= height then if rowIndex < 0 or rowIndex >= height then
error(formatError("ImageDataReader", string.format("Row index %d out of bounds (height: %d)", rowIndex, height))) ErrorHandler.error("ImageDataReader", "VAL_002", string.format("Row index %d out of bounds (height: %d)", rowIndex, height), {
rowIndex = rowIndex,
height = height,
})
end end
local pixels = {} local pixels = {}
@@ -62,14 +71,17 @@ end
---@return table -- Array of {r, g, b, a} values (0-255 range) ---@return table -- Array of {r, g, b, a} values (0-255 range)
function ImageDataReader.getColumn(imageData, colIndex) function ImageDataReader.getColumn(imageData, colIndex)
if not imageData then if not imageData then
error(formatError("ImageDataReader", "ImageData cannot be nil")) ErrorHandler.error("ImageDataReader", "VAL_001", "ImageData cannot be nil")
end end
local width = imageData:getWidth() local width = imageData:getWidth()
local height = imageData:getHeight() local height = imageData:getHeight()
if colIndex < 0 or colIndex >= width then if colIndex < 0 or colIndex >= width then
error(formatError("ImageDataReader", string.format("Column index %d out of bounds (width: %d)", colIndex, width))) ErrorHandler.error("ImageDataReader", "VAL_002", string.format("Column index %d out of bounds (width: %d)", colIndex, width), {
colIndex = colIndex,
width = width,
})
end end
local pixels = {} local pixels = {}

View File

@@ -1,14 +1,17 @@
--- Standardized error message formatter
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
---@param message string -- Error message
---@return string -- Formatted error message
local function formatError(module, message)
return string.format("[FlexLove.%s] %s", module, message)
end
---@class ImageRenderer ---@class ImageRenderer
local ImageRenderer = {} local ImageRenderer = {}
-- ErrorHandler will be injected via init
local ErrorHandler = nil
--- Initialize ImageRenderer with dependencies
---@param deps table Dependencies table with ErrorHandler
function ImageRenderer.init(deps)
if deps and deps.ErrorHandler then
ErrorHandler = deps.ErrorHandler
end
end
--- Calculate rendering parameters for object-fit modes --- Calculate rendering parameters for object-fit modes
--- Returns source and destination rectangles for rendering --- Returns source and destination rectangles for rendering
---@param imageWidth number -- Natural width of the image ---@param imageWidth number -- Natural width of the image
@@ -23,7 +26,12 @@ function ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, bounds
objectPosition = objectPosition or "center center" objectPosition = objectPosition or "center center"
if imageWidth <= 0 or imageHeight <= 0 or boundsWidth <= 0 or boundsHeight <= 0 then if imageWidth <= 0 or imageHeight <= 0 or boundsWidth <= 0 or boundsHeight <= 0 then
error(formatError("ImageRenderer", "Dimensions must be positive")) ErrorHandler.error("ImageRenderer", "VAL_002", "Dimensions must be positive", {
imageWidth = imageWidth,
imageHeight = imageHeight,
boundsWidth = boundsWidth,
boundsHeight = boundsHeight,
})
end end
local result = { local result = {
@@ -104,7 +112,12 @@ function ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, bounds
return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "contain", objectPosition) return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "contain", objectPosition)
end end
else else
error(formatError("ImageRenderer", string.format("Invalid fit mode: '%s'. Must be one of: fill, contain, cover, scale-down, none", tostring(fitMode)))) ErrorHandler.warn("ImageRenderer", "VAL_007", string.format("Invalid fit mode: '%s'. Must be one of: fill, contain, cover, scale-down, none", tostring(fitMode)), {
fitMode = fitMode,
fallback = "fill"
})
-- Use 'fill' as fallback
return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "fill", objectPosition)
end end
return result return result

View File

@@ -1,17 +1,20 @@
--- Standardized error message formatter
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
---@param message string -- Error message
---@return string -- Formatted error message
local function formatError(module, message)
return string.format("[FlexLove.%s] %s", module, message)
end
-- ==================== -- ====================
-- ImageScaler -- ImageScaler
-- ==================== -- ====================
local ImageScaler = {} local ImageScaler = {}
-- ErrorHandler will be injected via init
local ErrorHandler = nil
--- Initialize ImageScaler with dependencies
---@param deps table Dependencies table with ErrorHandler
function ImageScaler.init(deps)
if deps and deps.ErrorHandler then
ErrorHandler = deps.ErrorHandler
end
end
--- Scale an ImageData region using nearest-neighbor sampling --- Scale an ImageData region using nearest-neighbor sampling
--- Produces sharp, pixelated scaling - ideal for pixel art --- Produces sharp, pixelated scaling - ideal for pixel art
---@param sourceImageData love.ImageData -- Source image data ---@param sourceImageData love.ImageData -- Source image data
@@ -24,11 +27,21 @@ local ImageScaler = {}
---@return love.ImageData -- Scaled image data ---@return love.ImageData -- Scaled image data
function ImageScaler.scaleNearest(sourceImageData, srcX, srcY, srcW, srcH, destW, destH) function ImageScaler.scaleNearest(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
if not sourceImageData then if not sourceImageData then
error(formatError("ImageScaler", "Source ImageData cannot be nil")) ErrorHandler.error("ImageScaler", "VAL_001", "Source ImageData cannot be nil")
end end
if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then
error(formatError("ImageScaler", "Dimensions must be positive")) ErrorHandler.warn("ImageScaler", "VAL_002", "Dimensions must be positive", {
srcW = srcW,
srcH = srcH,
destW = destW,
destH = destH,
fallback = "1x1 transparent image"
})
-- Return a minimal 1x1 transparent image as fallback
local fallbackImageData = love.image.newImageData(1, 1)
fallbackImageData:setPixel(0, 0, 0, 0, 0, 0)
return fallbackImageData
end end
-- Create destination ImageData -- Create destination ImageData
@@ -82,11 +95,21 @@ end
---@return love.ImageData -- Scaled image data ---@return love.ImageData -- Scaled image data
function ImageScaler.scaleBilinear(sourceImageData, srcX, srcY, srcW, srcH, destW, destH) function ImageScaler.scaleBilinear(sourceImageData, srcX, srcY, srcW, srcH, destW, destH)
if not sourceImageData then if not sourceImageData then
error(formatError("ImageScaler", "Source ImageData cannot be nil")) ErrorHandler.error("ImageScaler", "VAL_001", "Source ImageData cannot be nil")
end end
if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then
error(formatError("ImageScaler", "Dimensions must be positive")) ErrorHandler.warn("ImageScaler", "VAL_002", "Dimensions must be positive", {
srcW = srcW,
srcH = srcH,
destW = destW,
destH = destH,
fallback = "1x1 transparent image"
})
-- Return a minimal 1x1 transparent image as fallback
local fallbackImageData = love.image.newImageData(1, 1)
fallbackImageData:setPixel(0, 0, 0, 0, 0, 0)
return fallbackImageData
end end
-- Create destination ImageData -- Create destination ImageData

View File

@@ -1,16 +1,23 @@
local modulePath = (...):match("(.-)[^%.]+$") local modulePath = (...):match("(.-)[^%.]+$")
local ImageScaler = require(modulePath .. "ImageScaler") local ImageScaler = require(modulePath .. "ImageScaler")
--- Standardized error message formatter
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
---@param message string -- Error message
---@return string -- Formatted error message
local function formatError(module, message)
return string.format("[FlexLove.%s] %s", module, message)
end
local NinePatch = {} local NinePatch = {}
-- ErrorHandler will be injected via init
local ErrorHandler = nil
--- Initialize NinePatch with dependencies
---@param deps table Dependencies table with ErrorHandler
function NinePatch.init(deps)
if deps and deps.ErrorHandler then
ErrorHandler = deps.ErrorHandler
end
-- Also initialize ImageScaler since it's a dependency
if ImageScaler.init then
ImageScaler.init(deps)
end
end
--- Draw a 9-patch component using Android-style rendering --- Draw a 9-patch component using Android-style rendering
--- Corners are scaled by scaleCorners multiplier, edges stretch in one dimension only --- Corners are scaled by scaleCorners multiplier, edges stretch in one dimension only
---@param component ThemeComponent ---@param component ThemeComponent
@@ -92,7 +99,9 @@ function NinePatch.draw(component, atlas, x, y, width, height, opacity, elementS
-- Get ImageData from component (stored during theme loading) -- Get ImageData from component (stored during theme loading)
local atlasData = component._loadedAtlasData local atlasData = component._loadedAtlasData
if not atlasData then if not atlasData then
error(formatError("NinePatch", "No ImageData available for atlas. Image must be loaded with safeLoadImage.")) ErrorHandler.error("NinePatch", "REN_007", "No ImageData available for atlas. Image must be loaded with safeLoadImage.", {
componentType = component.type,
})
end end
local scaledData local scaledData

View File

@@ -30,6 +30,9 @@
local Renderer = {} local Renderer = {}
Renderer.__index = Renderer Renderer.__index = Renderer
-- Lazy-loaded ErrorHandler
local ErrorHandler
--- Create a new Renderer instance --- Create a new Renderer instance
---@param config table Configuration table with rendering properties ---@param config table Configuration table with rendering properties
---@param deps table Dependencies {Color, RoundedRect, NinePatch, ImageRenderer, ImageCache, Theme, Blur, utils} ---@param deps table Dependencies {Color, RoundedRect, NinePatch, ImageRenderer, ImageCache, Theme, Blur, utils}
@@ -309,7 +312,12 @@ function Renderer:draw(backdropCanvas)
-- Element must be initialized before drawing -- Element must be initialized before drawing
if not self._element then if not self._element then
error("Renderer:draw() called before initialize(). Call renderer:initialize(element) first.") if not ErrorHandler then
ErrorHandler = require("modules.ErrorHandler")
end
ErrorHandler.error("Renderer", "SYS_002", "Method called before initialization", {
method = "draw"
}, "Call renderer:initialize(element) before rendering")
end end
local element = self._element local element = self._element

View File

@@ -26,9 +26,13 @@
---@field _scrollbarPressHandled boolean -- Track if scrollbar press was handled this frame ---@field _scrollbarPressHandled boolean -- Track if scrollbar press was handled this frame
---@field _Color table ---@field _Color table
---@field _utils table ---@field _utils table
---@field _ErrorHandler table?
local ScrollManager = {} local ScrollManager = {}
ScrollManager.__index = ScrollManager ScrollManager.__index = ScrollManager
-- Lazy-loaded ErrorHandler
local ErrorHandler
--- Create a new ScrollManager instance --- Create a new ScrollManager instance
---@param config table Configuration options ---@param config table Configuration options
---@param deps table Dependencies {Color: Color module, utils: utils module} ---@param deps table Dependencies {Color: Color module, utils: utils module}
@@ -92,7 +96,12 @@ end
--- Detect if content overflows container bounds --- Detect if content overflows container bounds
function ScrollManager:detectOverflow() function ScrollManager:detectOverflow()
if not self._element then if not self._element then
error("ScrollManager:detectOverflow() called before initialize()") if not ErrorHandler then
ErrorHandler = require("modules.ErrorHandler")
end
ErrorHandler.error("ScrollManager", "SYS_002", "Method called before initialization", {
method = "detectOverflow"
}, "Call scrollManager:initialize(element) before using scroll methods")
end end
local element = self._element local element = self._element
@@ -219,7 +228,12 @@ end
---@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}} ---@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}}
function ScrollManager:calculateScrollbarDimensions() function ScrollManager:calculateScrollbarDimensions()
if not self._element then if not self._element then
error("ScrollManager:calculateScrollbarDimensions() called before initialize()") if not ErrorHandler then
ErrorHandler = require("modules.ErrorHandler")
end
ErrorHandler.error("ScrollManager", "SYS_002", "Method called before initialization", {
method = "calculateScrollbarDimensions"
}, "Call scrollManager:initialize(element) before using scroll methods")
end end
local element = self._element local element = self._element
@@ -312,7 +326,12 @@ end
---@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"} ---@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"}
function ScrollManager:getScrollbarAtPosition(mouseX, mouseY) function ScrollManager:getScrollbarAtPosition(mouseX, mouseY)
if not self._element then if not self._element then
error("ScrollManager:getScrollbarAtPosition() called before initialize()") if not ErrorHandler then
ErrorHandler = require("modules.ErrorHandler")
end
ErrorHandler.error("ScrollManager", "SYS_002", "Method called before initialization", {
method = "getScrollbarAtPosition"
}, "Call scrollManager:initialize(element) before using scroll methods")
end end
local element = self._element local element = self._element
@@ -381,7 +400,12 @@ end
---@return boolean -- True if event was consumed ---@return boolean -- True if event was consumed
function ScrollManager:handleMousePress(mouseX, mouseY, button) function ScrollManager:handleMousePress(mouseX, mouseY, button)
if not self._element then if not self._element then
error("ScrollManager:handleMousePress() called before initialize()") if not ErrorHandler then
ErrorHandler = require("modules.ErrorHandler")
end
ErrorHandler.error("ScrollManager", "SYS_002", "Method called before initialization", {
method = "handleMousePress"
}, "Call scrollManager:initialize(element) before using scroll methods")
end end
if button ~= 1 then if button ~= 1 then

View File

@@ -1,6 +1,9 @@
---@class StateManager ---@class StateManager
local StateManager = {} local StateManager = {}
-- Load error handler (loaded lazily since it's in a sibling module)
local ErrorHandler
-- State storage: ID -> state table -- State storage: ID -> state table
local stateStore = {} local stateStore = {}
@@ -181,7 +184,14 @@ end
---@return table state State table for the element ---@return table state State table for the element
function StateManager.getState(id, defaultState) function StateManager.getState(id, defaultState)
if not id then if not id then
error("StateManager.getState: id is required") -- Lazy load ErrorHandler
if not ErrorHandler then
ErrorHandler = require("modules.ErrorHandler")
end
ErrorHandler.error("StateManager", "SYS_001", "Invalid state ID", {
parameter = "id",
value = "nil"
}, "Provide a valid non-nil ID string to getState()")
end end
-- Create state if it doesn't exist -- Create state if it doesn't exist
@@ -303,7 +313,14 @@ end
---@param state table State to store ---@param state table State to store
function StateManager.setState(id, state) function StateManager.setState(id, state)
if not id then if not id then
error("StateManager.setState: id is required") -- Lazy load ErrorHandler
if not ErrorHandler then
ErrorHandler = require("modules.ErrorHandler")
end
ErrorHandler.error("StateManager", "SYS_001", "Invalid state ID", {
parameter = "id",
value = "nil"
}, "Provide a valid non-nil ID string to setState()")
end end
stateStore[id] = state stateStore[id] = state

View File

@@ -6,14 +6,7 @@ end
local NinePatchParser = req("NinePatchParser") local NinePatchParser = req("NinePatchParser")
local Color = req("Color") local Color = req("Color")
local utils = req("utils") local utils = req("utils")
local ErrorHandler = req("ErrorHandler")
--- Standardized error message formatter
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
---@param message string -- Error message
---@return string -- Formatted error message
local function formatError(module, message)
return string.format("[FlexLove.%s] %s", module, message)
end
--- Auto-detect the base path where FlexLove is located --- Auto-detect the base path where FlexLove is located
---@return string modulePath, string filesystemPath ---@return string modulePath, string filesystemPath
@@ -135,7 +128,9 @@ function Theme.new(definition)
-- Validate theme definition -- Validate theme definition
local valid, err = validateThemeDefinition(definition) local valid, err = validateThemeDefinition(definition)
if not valid then if not valid then
error("[FlexLove] Invalid theme definition: " .. tostring(err)) ErrorHandler.error("Theme", "THM_001", "Invalid theme definition", {
error = tostring(err)
})
end end
local self = setmetatable({}, Theme) local self = setmetatable({}, Theme)
@@ -150,7 +145,11 @@ function Theme.new(definition)
self.atlas = image self.atlas = image
self.atlasData = imageData self.atlasData = imageData
else else
print("[FlexLove] Warning: Failed to load global atlas for theme '" .. definition.name .. "'" .. "(" .. loaderr .. ")") ErrorHandler.warn("Theme", "RES_001", "Failed to load global atlas", {
theme = definition.name,
path = resolvedPath,
error = loaderr
})
end end
else else
self.atlas = definition.atlas self.atlas = definition.atlas
@@ -174,7 +173,11 @@ function Theme.new(definition)
local contentHeight = srcHeight - 2 local contentHeight = srcHeight - 2
if contentWidth <= 0 or contentHeight <= 0 then if contentWidth <= 0 or contentHeight <= 0 then
error(formatError("NinePatch", "Image too small to strip border")) ErrorHandler.error("Theme", "RES_002", "Nine-patch image too small", {
width = srcWidth,
height = srcHeight,
reason = "Image must be larger than 2x2 pixels to have content after stripping 1px border"
})
end end
-- Create new ImageData for content only -- Create new ImageData for content only
@@ -204,7 +207,11 @@ function Theme.new(definition)
comp.insets = parseResult.insets comp.insets = parseResult.insets
comp._ninePatchData = parseResult comp._ninePatchData = parseResult
else else
print("[FlexLove] Warning: Failed to parse 9-patch " .. errorContext .. ": " .. tostring(parseErr)) ErrorHandler.warn("Theme", "RES_003", "Failed to parse nine-patch image", {
context = errorContext,
path = resolvedPath,
error = tostring(parseErr)
})
end end
end end
@@ -221,7 +228,11 @@ function Theme.new(definition)
comp._loadedAtlasData = imageData comp._loadedAtlasData = imageData
end end
else else
print("[FlexLove] Warning: Failed to load atlas " .. errorContext .. ": " .. tostring(loaderr)) ErrorHandler.warn("Theme", "RES_001", "Failed to load atlas", {
context = errorContext,
path = resolvedPath,
error = tostring(loaderr)
})
end end
end end
@@ -310,7 +321,13 @@ function Theme.load(path)
if success then if success then
definition = result definition = result
else else
error("Failed to load theme '" .. path .. "'\nTried: " .. themePath .. "\nError: " .. tostring(result)) ErrorHandler.warn("Theme", "RES_004", "Failed to load theme file", {
theme = path,
tried = themePath,
error = tostring(result),
fallback = "nil (no theme loaded)"
}, "Check that the theme file exists in the themes/ directory or provide a valid module path")
return nil
end end
end end
@@ -334,7 +351,12 @@ function Theme.setActive(themeOrName)
end end
if not activeTheme then if not activeTheme then
error("Failed to set active theme: " .. tostring(themeOrName)) ErrorHandler.warn("Theme", "THM_002", "Failed to set active theme", {
theme = tostring(themeOrName),
reason = "Theme not found or not loaded",
fallback = "current theme unchanged"
}, "Ensure the theme is loaded with Theme.load() before setting it active")
-- Keep current activeTheme unchanged (fallback behavior)
end end
end end

View File

@@ -24,7 +24,13 @@ function Units.parse(value)
if type(value) ~= "string" then if type(value) ~= "string" then
if ErrorHandler then if ErrorHandler then
ErrorHandler.error("Units", string.format("Invalid unit value type. Expected string or number, got %s", type(value))) ErrorHandler.warn("Units", "VAL_001", "Invalid property type", {
property = "unit value",
expected = "string or number",
got = type(value)
}, "Using fallback: 0px")
else
print(string.format("[FlexLove - Units] Warning: Invalid unit value type. Expected string or number, got %s. Using fallback: 0px", type(value)))
end end
return 0, "px" return 0, "px"
end end
@@ -33,7 +39,12 @@ function Units.parse(value)
local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true } local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true }
if validUnits[value] then if validUnits[value] then
if ErrorHandler then if ErrorHandler then
ErrorHandler.error("Units", string.format("Missing numeric value before unit '%s'. Use format like '50%s' (e.g., '50px', '10%%', '2vw')", value, value)) ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
input = value,
expected = "number + unit (e.g., '50" .. value .. "')"
}, string.format("Add a numeric value before '%s', like '50%s'. Using fallback: 0px", value, value))
else
print(string.format("[FlexLove - Units] Warning: Missing numeric value before unit '%s'. Use format like '50%s'. Using fallback: 0px", value, value))
end end
return 0, "px" return 0, "px"
end end
@@ -41,7 +52,12 @@ function Units.parse(value)
-- Check for invalid format (space between number and unit) -- Check for invalid format (space between number and unit)
if value:match("%d%s+%a") then if value:match("%d%s+%a") then
if ErrorHandler then if ErrorHandler then
ErrorHandler.error("Units", string.format("Invalid unit string '%s' (contains space). Use format like '50px' or '50%%'", value)) ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
input = value,
issue = "contains space between number and unit"
}, "Remove spaces: use '50px' not '50 px'. Using fallback: 0px")
else
print(string.format("[FlexLove - Units] Warning: Invalid unit string '%s' (contains space). Use format like '50px' or '50%%'. Using fallback: 0px", value))
end end
return 0, "px" return 0, "px"
end end
@@ -50,7 +66,11 @@ function Units.parse(value)
local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$") local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$")
if not numStr then if not numStr then
if ErrorHandler then if ErrorHandler then
ErrorHandler.error("Units", string.format("Invalid unit format '%s'. Expected format: number + unit (e.g., '50px', '10%%', '2vw')", value)) ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
input = value
}, "Expected format: number + unit (e.g., '50px', '10%', '2vw'). Using fallback: 0px")
else
print(string.format("[FlexLove - Units] Warning: Invalid unit format '%s'. Expected format: number + unit (e.g., '50px', '10%%', '2vw'). Using fallback: 0px", value))
end end
return 0, "px" return 0, "px"
end end
@@ -58,7 +78,12 @@ function Units.parse(value)
local num = tonumber(numStr) local num = tonumber(numStr)
if not num then if not num then
if ErrorHandler then if ErrorHandler then
ErrorHandler.error("Units", string.format("Invalid numeric value in '%s'", value)) ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
input = value,
issue = "numeric value cannot be parsed"
}, "Using fallback: 0px")
else
print(string.format("[FlexLove - Units] Warning: Invalid numeric value in '%s'. Using fallback: 0px", value))
end end
return 0, "px" return 0, "px"
end end
@@ -71,7 +96,13 @@ function Units.parse(value)
-- validUnits is already defined at the top of the function -- validUnits is already defined at the top of the function
if not validUnits[unit] then if not validUnits[unit] then
if ErrorHandler then if ErrorHandler then
ErrorHandler.error("Units", string.format("Unknown unit '%s' in '%s'. Valid units: px, %%, vw, vh, ew, eh", unit, value)) ErrorHandler.warn("Units", "VAL_005", "Invalid unit format", {
input = value,
unit = unit,
validUnits = "px, %, vw, vh, ew, eh"
}, string.format("Treating '%s' as pixels", value))
else
print(string.format("[FlexLove - Units] Warning: Unknown unit '%s' in '%s'. Valid units: px, %%, vw, vh, ew, eh. Treating as pixels", unit, value))
end end
return num, "px" return num, "px"
end end
@@ -93,7 +124,10 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
elseif unit == "%" then elseif unit == "%" then
if not parentSize then if not parentSize then
if ErrorHandler then if ErrorHandler then
ErrorHandler.error("Units", "Percentage units require parent dimension. Element has no parent or parent dimension not available") ErrorHandler.error("Units", "LAY_003", "Invalid dimensions", {
unit = "%",
issue = "parent dimension not available"
}, "Percentage units require a parent element with explicit dimensions")
else else
error("Percentage units require parent dimension") error("Percentage units require parent dimension")
end end
@@ -105,7 +139,10 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
return (value / 100) * viewportHeight return (value / 100) * viewportHeight
else else
if ErrorHandler then if ErrorHandler then
ErrorHandler.error("Units", string.format("Unknown unit '%s'. Valid units: px, %%, vw, vh, ew, eh", unit)) ErrorHandler.error("Units", "VAL_005", "Invalid unit format", {
unit = unit,
validUnits = "px, %, vw, vh, ew, eh"
})
else else
error(string.format("Unknown unit type: '%s'", unit)) error(string.format("Unknown unit type: '%s'", unit))
end end

View File

@@ -36,10 +36,17 @@ if ! git diff-index --quiet HEAD --; then
echo "" echo ""
fi fi
CURRENT_VERSION=$(grep -m 1 "_VERSION" FlexLove.lua | sed -E 's/.*"([^"]+)".*/\1/') # Get current version from latest git tag
CURRENT_VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//')
if [ -z "$CURRENT_VERSION" ]; then if [ -z "$CURRENT_VERSION" ]; then
echo -e "${RED}Error: Could not extract version from FlexLove.lua${NC}" echo -e "${YELLOW}Warning: No existing git tags found${NC}"
echo -e "${YELLOW}Attempting to read version from FlexLove.lua...${NC}"
CURRENT_VERSION=$(grep -m 1 "_VERSION" FlexLove.lua | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "$CURRENT_VERSION" ]; then
echo -e "${RED}Error: Could not extract version from git tags or FlexLove.lua${NC}"
exit 1 exit 1
fi
echo -e "${YELLOW}Using version from FlexLove.lua as fallback${NC}"
fi fi
echo -e "${CYAN}Current version:${NC} ${GREEN}v${CURRENT_VERSION}${NC}" echo -e "${CYAN}Current version:${NC} ${GREEN}v${CURRENT_VERSION}${NC}"
@@ -158,12 +165,91 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 0 exit 0
fi fi
# Commit changes
echo ""
echo -e "${YELLOW}Ready to commit and create tag${NC}"
echo -e "${CYAN}Default commit message:${NC} v${NEW_VERSION} release"
echo ""
read -p "Add a custom commit message? (y/n) " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
read -p "Enter commit message: " CUSTOM_COMMIT_MSG
COMMIT_MSG="$CUSTOM_COMMIT_MSG"
else
COMMIT_MSG="v${NEW_VERSION} release"
fi
echo ""
echo -e "${CYAN}Commit message:${NC} ${COMMIT_MSG}"
echo -e "${CYAN}Tag:${NC} v${NEW_VERSION}"
echo ""
read -p "Commit changes and create tag? (y/n) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo -e "${YELLOW}Changes staged but not committed${NC}"
echo "You can:"
echo " - Review changes: git diff --cached"
echo " - Commit manually: git commit -m 'v${NEW_VERSION} release'"
echo " - Unstage: git restore --staged FlexLove.lua README.md"
exit 0
fi
# Commit changes # Commit changes
echo "" echo ""
echo -e "${CYAN}[4/4]${NC} Committing and tagging..." echo -e "${CYAN}[4/4]${NC} Committing and tagging..."
git commit -m "v${NEW_VERSION} release" git commit -m "$COMMIT_MSG"
git tag -a "v${NEW_VERSION}" -m "Release version ${NEW_VERSION}" git tag -a "v${NEW_VERSION}" -m "Release version ${NEW_VERSION}"
echo -e "${CYAN}Pushing commits...${NC}"
if ! git push 2>&1; then
echo ""
echo -e "${RED}═══════════════════════════════════════${NC}"
echo -e "${RED}✗ Push failed!${NC}"
echo -e "${RED}═══════════════════════════════════════${NC}"
echo ""
echo -e "${YELLOW}Common reasons:${NC}"
echo " • No network connection"
echo " • Authentication failed (check credentials/SSH keys)"
echo " • Branch protection rules preventing direct push"
echo " • Remote branch diverged (pull needed first)"
echo " • No push permissions for this repository"
echo ""
echo -e "${YELLOW}The commit and tag were created locally but not pushed.${NC}"
echo ""
echo -e "${CYAN}To retry pushing:${NC}"
echo " git push"
echo " git push origin tag \"v${NEW_VERSION}\""
echo ""
echo -e "${CYAN}To undo the tag:${NC}"
echo " git tag -d \"v${NEW_VERSION}\""
echo " git reset --soft HEAD~1"
echo ""
exit 1
fi
echo -e "${CYAN}Pushing tags...${NC}"
if ! git push origin tag "v${NEW_VERSION}" 2>&1; then
echo ""
echo -e "${RED}═══════════════════════════════════════${NC}"
echo -e "${RED}✗ Tag push failed!${NC}"
echo -e "${RED}═══════════════════════════════════════${NC}"
echo ""
echo -e "${YELLOW}Common reasons:${NC}"
echo " • No network connection"
echo " • Authentication failed (check credentials/SSH keys)"
echo " • Tag already exists on remote"
echo " • No push permissions for this repository"
echo ""
echo -e "${GREEN}The commit was pushed successfully, but the tag was not.${NC}"
echo ""
echo -e "${CYAN}To retry pushing the tag:${NC}"
echo " git push origin tag \"v${NEW_VERSION}\""
echo ""
echo -e "${CYAN}To delete the local tag:${NC}"
echo " git tag -d \"v${NEW_VERSION}\""
echo ""
exit 1
fi
echo "" echo ""
echo -e "${GREEN}═══════════════════════════════════════${NC}" echo -e "${GREEN}═══════════════════════════════════════${NC}"
echo -e "${GREEN}✓ Version bump complete!${NC}" echo -e "${GREEN}✓ Version bump complete!${NC}"

View File

@@ -0,0 +1,789 @@
-- Critical Failure Tests for FlexLove
-- These tests are designed to find ACTUAL BUGS:
-- 1. Memory leaks / garbage creation without cleanup
-- 2. Layout calculation bugs causing incorrect positioning
-- 3. Unsafe input access (nil dereference, division by zero, etc.)
package.path = package.path .. ";./?.lua;./modules/?.lua"
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local FlexLove = require("FlexLove")
TestCriticalFailures = {}
function TestCriticalFailures:setUp()
collectgarbage("collect")
FlexLove.destroy()
FlexLove.setMode("retained")
end
function TestCriticalFailures:tearDown()
FlexLove.destroy()
collectgarbage("collect")
end
-- ============================================================
-- MEMORY LEAK TESTS - Find garbage that's not cleaned up
-- ============================================================
-- Test: Canvas objects should be cleaned up on resize
function TestCriticalFailures:test_canvas_cleanup_on_resize()
FlexLove.init()
-- Create initial canvases
FlexLove.draw(function() end)
local canvas1 = FlexLove._gameCanvas
-- Resize should invalidate old canvases
FlexLove.resize()
-- Draw again to create new canvases
FlexLove.draw(function() end)
local canvas2 = FlexLove._gameCanvas
-- Old canvas should be replaced
luaunit.assertNotEquals(canvas1, canvas2)
-- Check canvas is actually nil after resize (before draw)
FlexLove.resize()
luaunit.assertNil(FlexLove._gameCanvas)
end
-- Test: Elements should be cleaned up from topElements on destroy
function TestCriticalFailures:test_element_cleanup_from_top_elements()
local element1 = FlexLove.new({ width = 100, height = 100 })
local element2 = FlexLove.new({ width = 100, height = 100 })
luaunit.assertEquals(#FlexLove.topElements, 2)
element1:destroy()
luaunit.assertEquals(#FlexLove.topElements, 1)
element2:destroy()
luaunit.assertEquals(#FlexLove.topElements, 0)
end
-- Test: Child elements should be destroyed when parent is destroyed
function TestCriticalFailures:test_child_cleanup_on_parent_destroy()
local parent = FlexLove.new({ width = 200, height = 200 })
local child = FlexLove.new({ width = 50, height = 50, parent = parent })
luaunit.assertEquals(#parent.children, 1)
-- Destroy parent should also clear children
parent:destroy()
-- Child should have no parent reference (potential memory leak if not cleared)
luaunit.assertNil(child.parent)
luaunit.assertEquals(#parent.children, 0)
end
-- Test: Event handlers should be cleared on destroy (closure leak)
function TestCriticalFailures:test_event_handler_cleanup()
local captured_data = { large_array = {} }
for i = 1, 1000 do
captured_data.large_array[i] = i
end
local element = FlexLove.new({
width = 100,
height = 100,
onEvent = function(el, event)
-- This closure captures captured_data
print(captured_data.large_array[1])
end,
})
element:destroy()
-- onEvent should be nil after destroy (prevent closure leak)
luaunit.assertNil(element.onEvent)
end
-- Test: Immediate mode state should not grow unbounded
function TestCriticalFailures:test_immediate_mode_state_cleanup()
FlexLove.setMode("immediate")
FlexLove.init({ stateRetentionFrames = 2 })
-- Create elements for multiple frames
for frame = 1, 10 do
FlexLove.beginFrame()
FlexLove.new({ id = "element_" .. frame, width = 100, height = 100 })
FlexLove.endFrame()
end
-- State count should be limited by stateRetentionFrames
local stateCount = FlexLove.getStateCount()
-- Should be much less than 10 due to cleanup
luaunit.assertTrue(stateCount < 10, "State count: " .. stateCount .. " (should be cleaned up)")
end
-- ============================================================
-- LAYOUT CALCULATION BUGS - Find incorrect positioning
-- ============================================================
-- Test: Flex layout with overflow should not position children outside container
function TestCriticalFailures:test_flex_overflow_positioning()
local parent = FlexLove.new({
width = 100,
height = 100,
positioning = "flex",
flexDirection = "horizontal",
flexWrap = "nowrap",
})
-- Add children that exceed parent width
local child1 = FlexLove.new({ width = 80, height = 50, parent = parent })
local child2 = FlexLove.new({ width = 80, height = 50, parent = parent })
-- Children should be positioned, even if they overflow
-- Check that x positions are at least valid numbers
luaunit.assertNotNil(child1.x)
luaunit.assertNotNil(child2.x)
luaunit.assertTrue(child1.x >= 0)
luaunit.assertTrue(child2.x > child1.x)
end
-- Test: Percentage width with zero parent width (division by zero)
function TestCriticalFailures:test_percentage_width_zero_parent()
local parent = FlexLove.new({ width = 0, height = 100 })
-- This should not crash (division by zero in percentage calculation)
local success, child = pcall(function()
return FlexLove.new({ width = "50%", height = 50, parent = parent })
end)
luaunit.assertTrue(success, "Should not crash with zero parent width")
if success then
-- Width should be 0 or handled gracefully
luaunit.assertTrue(child.width >= 0)
end
end
-- Test: Auto-sizing with circular dependency
function TestCriticalFailures:test_autosizing_circular_dependency()
-- Parent auto-sizes to child, child uses percentage of parent
local parent = FlexLove.new({ height = 100 }) -- No width = auto
-- Child width is percentage of parent, but parent width depends on child
local success, child = pcall(function()
return FlexLove.new({ width = "50%", height = 50, parent = parent })
end)
luaunit.assertTrue(success, "Should not crash with circular sizing")
-- Check that we don't get NaN or negative values
if success then
luaunit.assertFalse(parent.width ~= parent.width, "Parent width should not be NaN")
luaunit.assertFalse(child.width ~= child.width, "Child width should not be NaN")
luaunit.assertTrue(parent.width >= 0, "Parent width should be non-negative")
luaunit.assertTrue(child.width >= 0, "Child width should be non-negative")
end
end
-- Test: Negative padding should not cause negative content dimensions
function TestCriticalFailures:test_negative_padding_content_dimensions()
local element = FlexLove.new({
width = 100,
height = 100,
padding = { top = -50, left = -50, right = -50, bottom = -50 },
})
-- Content width/height should never be negative
luaunit.assertTrue(element.width >= 0, "Content width should be non-negative: " .. element.width)
luaunit.assertTrue(element.height >= 0, "Content height should be non-negative: " .. element.height)
end
-- Test: Grid layout with zero rows/columns (division by zero)
function TestCriticalFailures:test_grid_zero_dimensions()
local parent = FlexLove.new({
width = 300,
height = 200,
positioning = "grid",
gridRows = 0,
gridColumns = 0,
})
-- This should not crash when adding children
local success = pcall(function()
FlexLove.new({ width = 50, height = 50, parent = parent })
end)
luaunit.assertTrue(success, "Should not crash with zero grid dimensions")
end
-- ============================================================
-- UNSAFE INPUT ACCESS - Find nil dereference and type errors
-- ============================================================
-- Test: setText with number should not crash (type coercion)
function TestCriticalFailures:test_set_text_with_number()
local element = FlexLove.new({ width = 100, height = 100, text = "initial" })
-- Many Lua APIs expect string but get number
local success = pcall(function()
element:setText(12345)
end)
luaunit.assertTrue(success, "Should handle number text gracefully")
end
-- Test: Image path with special characters should not crash file system
function TestCriticalFailures:test_image_path_special_characters()
local success = pcall(function()
FlexLove.new({
width = 100,
height = 100,
imagePath = "../../../etc/passwd", -- Path traversal attempt
})
end)
luaunit.assertTrue(success, "Should handle malicious paths gracefully")
end
-- Test: onEvent callback that errors should not crash the system
function TestCriticalFailures:test_on_event_error_handling()
local element = FlexLove.new({
width = 100,
height = 100,
onEvent = function(el, event)
error("Intentional error in callback")
end,
})
-- Simulate mouse click
local success = pcall(function()
local InputEvent = require("modules.InputEvent")
local event = InputEvent.new({ type = "pressed", button = 1 })
if element.onEvent then
element.onEvent(element, event)
end
end)
-- Should error (no protection), but shouldn't leave system in bad state
luaunit.assertFalse(success)
-- Element should still be valid
luaunit.assertNotNil(element)
end
-- Test: Text with null bytes should not cause buffer issues
function TestCriticalFailures:test_text_with_null_bytes()
local success = pcall(function()
FlexLove.new({
width = 200,
height = 100,
text = "Hello\0World\0\0\0",
})
end)
luaunit.assertTrue(success, "Should handle null bytes in text")
end
-- Test: Extremely deep nesting should not cause stack overflow
function TestCriticalFailures:test_extreme_nesting_stack_overflow()
local parent = FlexLove.new({ width = 500, height = 500 })
local current = parent
-- Try to create 1000 levels of nesting
local success = pcall(function()
for i = 1, 1000 do
local child = FlexLove.new({
width = 10,
height = 10,
parent = current,
})
current = child
end
end)
-- This might fail due to legitimate recursion limits
-- But it should fail gracefully, not segfault
if not success then
print("Deep nesting failed (expected for extreme depth)")
end
luaunit.assertTrue(true) -- If we get here, no segfault
end
-- Test: Gap with NaN value
function TestCriticalFailures:test_gap_nan_value()
local success = pcall(function()
FlexLove.new({
width = 300,
height = 200,
positioning = "flex",
gap = 0 / 0, -- NaN
})
end)
-- NaN in calculations can propagate and cause issues
luaunit.assertTrue(success, "Should handle NaN gap")
end
-- Test: Border-box model with huge padding
function TestCriticalFailures:test_huge_padding_overflow()
local element = FlexLove.new({
width = 100,
height = 100,
padding = { top = 1000000, left = 1000000, right = 1000000, bottom = 1000000 },
})
-- Content dimensions should be clamped to 0, not underflow
luaunit.assertTrue(element.width >= 0, "Width should not underflow: " .. element.width)
luaunit.assertTrue(element.height >= 0, "Height should not underflow: " .. element.height)
end
-- Test: Scroll position race condition in immediate mode
function TestCriticalFailures:test_scroll_position_race_immediate_mode()
FlexLove.setMode("immediate")
-- Create scrollable element
FlexLove.beginFrame()
local element = FlexLove.new({
id = "scroll_test",
width = 200,
height = 200,
overflow = "scroll",
})
FlexLove.endFrame()
-- Set scroll position
element:setScrollPosition(50, 50)
-- Create same element next frame (state should restore)
FlexLove.beginFrame()
local element2 = FlexLove.new({
id = "scroll_test",
width = 200,
height = 200,
overflow = "scroll",
})
FlexLove.endFrame()
-- Scroll position should persist (or at least not crash)
local scrollX, scrollY = element2:getScrollPosition()
luaunit.assertNotNil(scrollX)
luaunit.assertNotNil(scrollY)
end
-- Test: Theme with missing required properties
function TestCriticalFailures:test_theme_missing_properties()
local Theme = require("modules.Theme")
-- Create theme with minimal properties
local success = pcall(function()
local theme = Theme.new({
name = "broken",
-- Missing components table
})
FlexLove.init({ theme = theme })
end)
-- Should handle gracefully or error clearly
luaunit.assertTrue(true) -- If we get here, no segfault
end
-- Test: Blur with zero or negative quality
function TestCriticalFailures:test_blur_invalid_quality()
local success = pcall(function()
FlexLove.new({
width = 100,
height = 100,
contentBlur = { intensity = 50, quality = 0 },
})
end)
luaunit.assertTrue(success, "Should handle zero blur quality")
success = pcall(function()
FlexLove.new({
width = 100,
height = 100,
contentBlur = { intensity = 50, quality = -5 },
})
end)
luaunit.assertTrue(success, "Should handle negative blur quality")
end
-- ============================================================
-- NIL DEREFERENCE BUGS - Target specific LSP warnings
-- ============================================================
-- Test: 9-patch padding with corrupted theme state (Element.lua:752-755)
function TestCriticalFailures:test_ninepatch_padding_nil_dereference()
local Theme = require("modules.Theme")
-- Create a theme with 9-patch data
local theme = Theme.new({
name = "test_theme",
components = {
container = {
ninePatch = {
imagePath = "themes/metal.lua", -- Invalid path to trigger edge case
contentPadding = { top = 10, left = 10, right = 10, bottom = 10 }
}
}
}
})
FlexLove.init({ theme = theme })
-- Try to create element that uses 9-patch padding
-- If ninePatchContentPadding becomes nil but use9PatchPadding is true, this will crash
local success, err = pcall(function()
return FlexLove.new({
width = 100,
height = 100,
component = "container",
-- No explicit padding, should use 9-patch padding
})
end)
if not success then
print("ERROR: " .. tostring(err))
end
luaunit.assertTrue(success, "Should handle 9-patch padding gracefully")
end
-- Test: Theme with malformed 9-patch data
function TestCriticalFailures:test_malformed_ninepatch_data()
local Theme = require("modules.Theme")
-- Create theme with incomplete 9-patch data
local success = pcall(function()
local theme = Theme.new({
name = "broken_nine_patch",
components = {
container = {
ninePatch = {
-- Missing imagePath
contentPadding = { top = 10, left = 10 }, -- Incomplete padding
}
}
}
})
FlexLove.init({ theme = theme })
FlexLove.new({
width = 100,
height = 100,
component = "container",
})
end)
-- Should either succeed or fail with clear error (not nil dereference)
luaunit.assertTrue(true) -- If we get here, no segfault
end
-- ============================================================
-- INTEGRATION TESTS - Combine features in unexpected ways
-- ============================================================
-- Test: Scrollable element with overflow content + immediate mode + state restoration
function TestCriticalFailures:test_scroll_overflow_immediate_mode_integration()
FlexLove.setMode("immediate")
for frame = 1, 3 do
FlexLove.beginFrame()
local scrollContainer = FlexLove.new({
id = "scroll_container",
width = 200,
height = 150,
overflow = "scroll",
positioning = "flex",
flexDirection = "vertical",
})
-- Add children that exceed container height
for i = 1, 10 do
FlexLove.new({
id = "child_" .. i,
width = 180,
height = 50,
parent = scrollContainer,
})
end
FlexLove.endFrame()
-- Scroll on second frame
if frame == 2 then
scrollContainer:setScrollPosition(0, 100)
end
-- Check scroll position restored on third frame
if frame == 3 then
local scrollX, scrollY = scrollContainer:getScrollPosition()
luaunit.assertNotNil(scrollX, "Scroll X should be preserved")
luaunit.assertNotNil(scrollY, "Scroll Y should be preserved")
end
end
end
-- Test: Grid layout with auto-sized children and percentage gaps
function TestCriticalFailures:test_grid_autosized_children_percentage_gap()
local grid = FlexLove.new({
width = 300,
height = 300,
positioning = "grid",
gridRows = 3,
gridColumns = 3,
gap = "5%", -- Percentage gap
})
-- Add auto-sized children (no explicit dimensions)
for i = 1, 9 do
local child = FlexLove.new({
parent = grid,
text = "Cell " .. i,
-- Auto-sizing based on text
})
-- Verify child dimensions are valid
luaunit.assertNotNil(child.width)
luaunit.assertNotNil(child.height)
luaunit.assertTrue(child.width >= 0)
luaunit.assertTrue(child.height >= 0)
end
end
-- Test: Nested flex containers with conflicting alignment
function TestCriticalFailures:test_nested_flex_conflicting_alignment()
local outer = FlexLove.new({
width = 400,
height = 400,
positioning = "flex",
flexDirection = "vertical",
alignItems = "stretch",
justifyContent = "center",
})
local middle = FlexLove.new({
parent = outer,
height = 200,
-- Auto width (should stretch)
positioning = "flex",
flexDirection = "horizontal",
alignItems = "flex-end",
justifyContent = "space-between",
})
local inner1 = FlexLove.new({
parent = middle,
width = 50,
-- Auto height
text = "A",
})
local inner2 = FlexLove.new({
parent = middle,
width = 50,
height = 100,
text = "B",
})
-- Verify all elements have valid dimensions and positions
luaunit.assertTrue(outer.width > 0)
luaunit.assertTrue(middle.width > 0)
luaunit.assertTrue(inner1.height > 0)
luaunit.assertNotNil(inner1.x)
luaunit.assertNotNil(inner2.x)
end
-- Test: Element with multiple conflicting size sources
function TestCriticalFailures:test_conflicting_size_sources()
-- Element with explicit size + auto-sizing content + parent constraints
local parent = FlexLove.new({
width = 200,
height = 200,
positioning = "flex",
alignItems = "stretch",
})
local success = pcall(function()
FlexLove.new({
parent = parent,
width = 300, -- Exceeds parent width
height = "50%", -- Percentage height
text = "Very long text that should cause auto-sizing",
padding = { top = 50, left = 50, right = 50, bottom = 50 },
})
end)
luaunit.assertTrue(success, "Should handle conflicting size sources")
end
-- Test: Image element with resize during load
function TestCriticalFailures:test_image_resize_during_load()
local element = FlexLove.new({
width = 100,
height = 100,
imagePath = "nonexistent.png", -- Won't load, but should handle gracefully
})
-- Simulate resize while "loading"
FlexLove.resize(1920, 1080)
-- Element should still be valid
luaunit.assertNotNil(element.width)
luaunit.assertNotNil(element.height)
luaunit.assertTrue(element.width > 0)
luaunit.assertTrue(element.height > 0)
end
-- Test: Rapid theme switching with active elements
function TestCriticalFailures:test_rapid_theme_switching()
local Theme = require("modules.Theme")
local theme1 = Theme.new({ name = "theme1", components = {} })
local theme2 = Theme.new({ name = "theme2", components = {} })
FlexLove.init({ theme = theme1 })
-- Create elements with theme1
local element1 = FlexLove.new({ width = 100, height = 100 })
local element2 = FlexLove.new({ width = 100, height = 100 })
-- Switch theme
FlexLove.destroy()
FlexLove.init({ theme = theme2 })
-- Old elements should be invalidated (accessing them might crash)
local success = pcall(function()
element1:setText("test")
end)
-- It's OK if this fails (element destroyed), but shouldn't segfault
luaunit.assertTrue(true)
end
-- Test: Update properties during layout calculation
function TestCriticalFailures:test_update_during_layout()
local parent = FlexLove.new({
width = 300,
height = 300,
positioning = "flex",
})
local child = FlexLove.new({
width = 100,
height = 100,
parent = parent,
})
-- Modify child properties immediately after creation (during layout)
child:setText("Modified during layout")
child.backgroundColor = { r = 1, g = 0, b = 0, a = 1 }
-- Trigger another layout
parent:resize(400, 400)
-- Everything should still be valid
luaunit.assertNotNil(child.text)
luaunit.assertEquals(child.text, "Modified during layout")
end
-- ============================================================
-- STATE CORRUPTION SCENARIOS
-- ============================================================
-- Test: Destroy element with active event listeners
function TestCriticalFailures:test_destroy_with_active_listeners()
local eventFired = false
local element = FlexLove.new({
width = 100,
height = 100,
onEvent = function(el, event)
eventFired = true
end,
})
-- Simulate an event via InputEvent
local InputEvent = require("modules.InputEvent")
local event = InputEvent.new({ type = "pressed", button = 1, x = 50, y = 50 })
if element.onEvent and element:contains(50, 50) then
element.onEvent(element, event)
end
luaunit.assertTrue(eventFired, "Event should fire before destroy")
-- Destroy element
element:destroy()
-- onEvent should be nil after destroy
luaunit.assertNil(element.onEvent, "onEvent should be cleared after destroy")
end
-- Test: Double destroy should be safe
function TestCriticalFailures:test_double_destroy_safety()
local element = FlexLove.new({ width = 100, height = 100 })
element:destroy()
-- Second destroy should be safe (idempotent)
local success = pcall(function()
element:destroy()
end)
luaunit.assertTrue(success, "Double destroy should be safe")
end
-- Test: Circular parent-child reference (should never happen, but test safety)
function TestCriticalFailures:test_circular_parent_child_reference()
local parent = FlexLove.new({ width = 200, height = 200 })
local child = FlexLove.new({ width = 100, height = 100, parent = parent })
-- Try to create circular reference (should be prevented)
local success = pcall(function()
parent.parent = child -- This should never be allowed
parent:layoutChildren() -- This would cause infinite recursion
end)
-- Even if we set circular reference, layout should not crash
luaunit.assertTrue(true) -- If we get here, no stack overflow
end
-- Test: Modify children array during iteration
function TestCriticalFailures:test_modify_children_during_iteration()
local parent = FlexLove.new({
width = 300,
height = 300,
positioning = "flex",
})
-- Add several children
local children = {}
for i = 1, 5 do
children[i] = FlexLove.new({
width = 50,
height = 50,
parent = parent,
})
end
-- Remove child during layout (simulates user code modifying structure)
local success = pcall(function()
-- Trigger layout
parent:layoutChildren()
-- Remove a child (modifies children array)
children[3]:destroy()
-- Trigger layout again
parent:layoutChildren()
end)
luaunit.assertTrue(success, "Should handle children modification during layout")
end
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -1223,6 +1223,771 @@ function TestElementAdditional:test_element_with_userdata()
luaunit.assertEquals(element.userdata.count, 42) luaunit.assertEquals(element.userdata.count, 42)
end end
-- ==========================================
-- UNHAPPY PATH TESTS
-- ==========================================
TestElementUnhappyPaths = {}
function TestElementUnhappyPaths:setUp()
FlexLove.beginFrame(1920, 1080)
end
function TestElementUnhappyPaths:tearDown()
FlexLove.endFrame()
end
-- Test: Element with missing deps parameter
function TestElementUnhappyPaths:test_element_without_deps()
local Element = require("modules.Element")
local success = pcall(function()
Element.new({}, nil)
end)
luaunit.assertFalse(success) -- Should error without deps
end
-- Test: Element with negative dimensions
function TestElementUnhappyPaths:test_element_negative_dimensions()
local element = FlexLove.new({
id = "negative",
x = 0,
y = 0,
width = -100,
height = -50,
})
luaunit.assertNotNil(element)
-- Element should still be created (negative values handled)
end
-- Test: Element with zero dimensions
function TestElementUnhappyPaths:test_element_zero_dimensions()
local element = FlexLove.new({
id = "zero",
x = 0,
y = 0,
width = 0,
height = 0,
})
luaunit.assertNotNil(element)
end
-- Test: Element with extreme dimensions
function TestElementUnhappyPaths:test_element_extreme_dimensions()
local element = FlexLove.new({
id = "huge",
x = 0,
y = 0,
width = 1000000,
height = 1000000,
})
luaunit.assertNotNil(element)
end
-- Test: Element with invalid opacity values
function TestElementUnhappyPaths:test_element_invalid_opacity()
-- Opacity > 1
local success = pcall(function()
FlexLove.new({
id = "high_opacity",
width = 100,
height = 100,
opacity = 2.5,
})
end)
luaunit.assertFalse(success) -- Should error (validateRange)
-- Negative opacity
success = pcall(function()
FlexLove.new({
id = "negative_opacity",
width = 100,
height = 100,
opacity = -0.5,
})
end)
luaunit.assertFalse(success) -- Should error (validateRange)
end
-- Test: Element with invalid imageOpacity values
function TestElementUnhappyPaths:test_element_invalid_image_opacity()
-- imageOpacity > 1
local success = pcall(function()
FlexLove.new({
id = "high_img_opacity",
width = 100,
height = 100,
imageOpacity = 3.0,
})
end)
luaunit.assertFalse(success)
-- Negative imageOpacity
success = pcall(function()
FlexLove.new({
id = "negative_img_opacity",
width = 100,
height = 100,
imageOpacity = -1.0,
})
end)
luaunit.assertFalse(success)
end
-- Test: Element with invalid textSize
function TestElementUnhappyPaths:test_element_invalid_text_size()
-- Zero textSize
local success = pcall(function()
FlexLove.new({
id = "zero_text",
width = 100,
height = 100,
textSize = 0,
})
end)
luaunit.assertFalse(success)
-- Negative textSize
success = pcall(function()
FlexLove.new({
id = "negative_text",
width = 100,
height = 100,
textSize = -12,
})
end)
luaunit.assertFalse(success)
end
-- Test: Element with invalid textAlign enum
function TestElementUnhappyPaths:test_element_invalid_text_align()
local success = pcall(function()
FlexLove.new({
id = "invalid_align",
width = 100,
height = 100,
textAlign = "invalid_value",
})
end)
luaunit.assertFalse(success) -- Should error (validateEnum)
end
-- Test: Element with invalid positioning enum
function TestElementUnhappyPaths:test_element_invalid_positioning()
local success = pcall(function()
FlexLove.new({
id = "invalid_pos",
width = 100,
height = 100,
positioning = "invalid_positioning",
})
end)
luaunit.assertFalse(success) -- Should error (validateEnum)
end
-- Test: Element with invalid flexDirection enum
function TestElementUnhappyPaths:test_element_invalid_flex_direction()
local success = pcall(function()
FlexLove.new({
id = "invalid_flex",
width = 100,
height = 100,
positioning = "flex",
flexDirection = "diagonal",
})
end)
luaunit.assertFalse(success) -- Should error (validateEnum)
end
-- Test: Element with invalid objectFit enum
function TestElementUnhappyPaths:test_element_invalid_object_fit()
local success = pcall(function()
FlexLove.new({
id = "invalid_fit",
width = 100,
height = 100,
objectFit = "stretch",
})
end)
luaunit.assertFalse(success) -- Should error (validateEnum)
end
-- Test: Element with nonexistent image path
function TestElementUnhappyPaths:test_element_nonexistent_image()
local element = FlexLove.new({
id = "no_image",
width = 100,
height = 100,
imagePath = "/nonexistent/path/to/image.png",
})
luaunit.assertNotNil(element)
luaunit.assertNil(element._loadedImage) -- Image should fail to load silently
end
-- Test: Element with passwordMode and multiline (conflicting)
function TestElementUnhappyPaths:test_element_password_multiline_conflict()
local element = FlexLove.new({
id = "conflict",
width = 200,
height = 100,
editable = true,
passwordMode = true,
multiline = true, -- Should be disabled by passwordMode
})
luaunit.assertNotNil(element)
luaunit.assertFalse(element.multiline) -- multiline should be forced to false
end
-- Test: Element addChild with nil child
function TestElementUnhappyPaths:test_add_nil_child()
local parent = FlexLove.new({
id = "parent",
width = 200,
height = 200,
})
local success = pcall(function()
parent:addChild(nil)
end)
luaunit.assertFalse(success) -- Should error
end
-- Test: Element removeChild that doesn't exist
function TestElementUnhappyPaths:test_remove_nonexistent_child()
local parent = FlexLove.new({
id = "parent",
width = 200,
height = 200,
})
local notAChild = FlexLove.new({
id = "orphan",
width = 50,
height = 50,
})
parent:removeChild(notAChild) -- Should not crash
luaunit.assertEquals(#parent.children, 0)
end
-- Test: Element removeChild with nil
function TestElementUnhappyPaths:test_remove_nil_child()
local parent = FlexLove.new({
id = "parent",
width = 200,
height = 200,
})
parent:removeChild(nil) -- Should not crash
luaunit.assertTrue(true)
end
-- Test: Element clearChildren on empty parent
function TestElementUnhappyPaths:test_clear_children_empty()
local parent = FlexLove.new({
id = "parent",
width = 200,
height = 200,
})
parent:clearChildren() -- Should not crash
luaunit.assertEquals(#parent.children, 0)
end
-- Test: Element clearChildren called twice
function TestElementUnhappyPaths:test_clear_children_twice()
local parent = FlexLove.new({
id = "parent",
width = 200,
height = 200,
})
local child = FlexLove.new({
id = "child",
width = 50,
height = 50,
parent = parent,
})
parent:clearChildren()
parent:clearChildren() -- Call again
luaunit.assertEquals(#parent.children, 0)
end
-- Test: Element contains with extreme coordinates
function TestElementUnhappyPaths:test_contains_extreme_coordinates()
local element = FlexLove.new({
id = "test",
x = 10,
y = 20,
width = 100,
height = 50,
})
luaunit.assertFalse(element:contains(math.huge, math.huge))
luaunit.assertFalse(element:contains(-math.huge, -math.huge))
end
-- Test: Element contains with NaN coordinates
function TestElementUnhappyPaths:test_contains_nan_coordinates()
local element = FlexLove.new({
id = "test",
x = 10,
y = 20,
width = 100,
height = 50,
})
local nan = 0 / 0
local result = element:contains(nan, nan)
-- NaN comparisons return false, so this should be false
luaunit.assertFalse(result)
end
-- Test: Element setScrollPosition without ScrollManager
function TestElementUnhappyPaths:test_scroll_without_manager()
local element = FlexLove.new({
id = "no_scroll",
width = 100,
height = 100,
-- No overflow property, so no ScrollManager
})
element:setScrollPosition(50, 50) -- Should not crash
luaunit.assertTrue(true)
end
-- Test: Element setScrollPosition with extreme values
function TestElementUnhappyPaths:test_scroll_extreme_values()
local element = FlexLove.new({
id = "scrollable",
width = 200,
height = 200,
overflow = "scroll",
})
element:setScrollPosition(1000000, 1000000) -- Should clamp
luaunit.assertTrue(true)
element:setScrollPosition(-1000000, -1000000) -- Should clamp to 0
local scrollX, scrollY = element:getScrollPosition()
luaunit.assertEquals(scrollX, 0)
luaunit.assertEquals(scrollY, 0)
end
-- Test: Element scrollBy with nil values
function TestElementUnhappyPaths:test_scroll_by_nil()
local element = FlexLove.new({
id = "scrollable",
width = 200,
height = 200,
overflow = "scroll",
})
element:scrollBy(nil, nil) -- Should use current position
luaunit.assertTrue(true)
end
-- Test: Element destroy on already destroyed element
function TestElementUnhappyPaths:test_destroy_twice()
local element = FlexLove.new({
id = "destroyable",
width = 100,
height = 100,
})
element:destroy()
element:destroy() -- Call again - should not crash
luaunit.assertTrue(true)
end
-- Test: Element destroy with circular reference (parent-child)
function TestElementUnhappyPaths:test_destroy_with_children()
local parent = FlexLove.new({
id = "parent",
width = 200,
height = 200,
})
local child = FlexLove.new({
id = "child",
width = 50,
height = 50,
parent = parent,
})
parent:destroy() -- Should destroy all children too
luaunit.assertEquals(#parent.children, 0)
end
-- Test: Element update with nil dt
function TestElementUnhappyPaths:test_update_nil_dt()
local element = FlexLove.new({
id = "test",
width = 100,
height = 100,
})
local success = pcall(function()
element:update(nil)
end)
-- May or may not error depending on implementation
end
-- Test: Element update with negative dt
function TestElementUnhappyPaths:test_update_negative_dt()
local element = FlexLove.new({
id = "test",
width = 100,
height = 100,
})
element:update(-0.016) -- Should not crash
luaunit.assertTrue(true)
end
-- Test: Element draw with nil backdropCanvas
function TestElementUnhappyPaths:test_draw_nil_backdrop()
local element = FlexLove.new({
id = "test",
width = 100,
height = 100,
})
element:draw(nil) -- Should not crash
luaunit.assertTrue(true)
end
-- Test: Element with invalid cornerRadius types
function TestElementUnhappyPaths:test_invalid_corner_radius()
-- String cornerRadius
local element = FlexLove.new({
id = "test",
width = 100,
height = 100,
cornerRadius = "invalid",
})
luaunit.assertNotNil(element)
-- Negative cornerRadius
element = FlexLove.new({
id = "test2",
width = 100,
height = 100,
cornerRadius = -10,
})
luaunit.assertNotNil(element)
end
-- Test: Element with partial cornerRadius table
function TestElementUnhappyPaths:test_partial_corner_radius()
local element = FlexLove.new({
id = "test",
width = 100,
height = 100,
cornerRadius = {
topLeft = 10,
-- Missing other corners
},
})
luaunit.assertNotNil(element)
luaunit.assertEquals(element.cornerRadius.topLeft, 10)
luaunit.assertEquals(element.cornerRadius.topRight, 0)
end
-- Test: Element with invalid border types
function TestElementUnhappyPaths:test_invalid_border()
-- String border
local element = FlexLove.new({
id = "test",
width = 100,
height = 100,
border = "invalid",
})
luaunit.assertNotNil(element)
-- Negative border
element = FlexLove.new({
id = "test2",
width = 100,
height = 100,
border = -5,
})
luaunit.assertNotNil(element)
end
-- Test: Element with partial border table
function TestElementUnhappyPaths:test_partial_border()
local element = FlexLove.new({
id = "test",
width = 100,
height = 100,
border = {
top = 2,
left = 3,
-- Missing right and bottom
},
})
luaunit.assertNotNil(element)
luaunit.assertEquals(element.border.top, 2)
luaunit.assertEquals(element.border.left, 3)
luaunit.assertFalse(element.border.right)
luaunit.assertFalse(element.border.bottom)
end
-- Test: Element with invalid padding types
function TestElementUnhappyPaths:test_invalid_padding()
-- String padding
local element = FlexLove.new({
id = "test",
width = 100,
height = 100,
padding = "invalid",
})
luaunit.assertNotNil(element)
-- Negative padding
element = FlexLove.new({
id = "test2",
width = 100,
height = 100,
padding = { top = -10, left = -10, right = -10, bottom = -10 },
})
luaunit.assertNotNil(element)
end
-- Test: Element with invalid margin types
function TestElementUnhappyPaths:test_invalid_margin()
-- String margin
local element = FlexLove.new({
id = "test",
width = 100,
height = 100,
margin = "invalid",
})
luaunit.assertNotNil(element)
-- Huge margin
element = FlexLove.new({
id = "test2",
width = 100,
height = 100,
margin = { top = 1000000, left = 1000000, right = 1000000, bottom = 1000000 },
})
luaunit.assertNotNil(element)
end
-- Test: Element with invalid gap value
function TestElementUnhappyPaths:test_invalid_gap()
-- Negative gap
local element = FlexLove.new({
id = "test",
width = 300,
height = 200,
positioning = "flex",
gap = -10,
})
luaunit.assertNotNil(element)
-- Huge gap
element = FlexLove.new({
id = "test2",
width = 300,
height = 200,
positioning = "flex",
gap = 1000000,
})
luaunit.assertNotNil(element)
end
-- Test: Element with invalid grid properties
function TestElementUnhappyPaths:test_invalid_grid_properties()
-- Zero rows/columns
local element = FlexLove.new({
id = "test",
width = 300,
height = 200,
positioning = "grid",
gridRows = 0,
gridColumns = 0,
})
luaunit.assertNotNil(element)
-- Negative rows/columns
element = FlexLove.new({
id = "test2",
width = 300,
height = 200,
positioning = "grid",
gridRows = -5,
gridColumns = -5,
})
luaunit.assertNotNil(element)
end
-- Test: Element setText on non-text element
function TestElementUnhappyPaths:test_set_text_on_non_text()
local element = FlexLove.new({
id = "no_text",
width = 100,
height = 100,
})
element:setText("New text") -- Should not crash
luaunit.assertEquals(element.text, "New text")
end
-- Test: Element setText with nil
function TestElementUnhappyPaths:test_set_text_nil()
local element = FlexLove.new({
id = "text",
width = 100,
height = 100,
text = "Initial",
})
element:setText(nil)
luaunit.assertNil(element.text)
end
-- Test: Element setText with extreme length
function TestElementUnhappyPaths:test_set_text_extreme_length()
local element = FlexLove.new({
id = "text",
width = 100,
height = 100,
text = "Initial",
})
local longText = string.rep("a", 100000)
element:setText(longText)
luaunit.assertEquals(element.text, longText)
end
-- Test: Element with conflicting size constraints
function TestElementUnhappyPaths:test_conflicting_size_constraints()
-- Width less than padding
local element = FlexLove.new({
id = "conflict",
width = 10,
height = 10,
padding = { top = 20, left = 20, right = 20, bottom = 20 },
})
luaunit.assertNotNil(element)
-- Content width should be clamped to 0 or handled gracefully
end
-- Test: Element textinput on non-editable element
function TestElementUnhappyPaths:test_textinput_non_editable()
local element = FlexLove.new({
id = "not_editable",
width = 100,
height = 100,
editable = false,
})
local success = pcall(function()
element:textinput("a")
end)
-- Should either do nothing or handle gracefully
end
-- Test: Element keypressed on non-editable element
function TestElementUnhappyPaths:test_keypressed_non_editable()
local element = FlexLove.new({
id = "not_editable",
width = 100,
height = 100,
editable = false,
})
local success = pcall(function()
element:keypressed("return", "return", false)
end)
-- Should either do nothing or handle gracefully
end
-- Test: Element with invalid blur configuration
function TestElementUnhappyPaths:test_invalid_blur_config()
-- Negative intensity
local element = FlexLove.new({
id = "blur",
width = 100,
height = 100,
contentBlur = { intensity = -10, quality = 5 },
})
luaunit.assertNotNil(element)
-- Intensity > 100
element = FlexLove.new({
id = "blur2",
width = 100,
height = 100,
backdropBlur = { intensity = 150, quality = 5 },
})
luaunit.assertNotNil(element)
-- Invalid quality
element = FlexLove.new({
id = "blur3",
width = 100,
height = 100,
contentBlur = { intensity = 50, quality = 0 },
})
luaunit.assertNotNil(element)
end
-- Test: Element getAvailableContentWidth/Height on element with no padding
function TestElementUnhappyPaths:test_available_content_no_padding()
local element = FlexLove.new({
id = "test",
width = 100,
height = 100,
})
local availWidth = element:getAvailableContentWidth()
local availHeight = element:getAvailableContentHeight()
luaunit.assertEquals(availWidth, 100)
luaunit.assertEquals(availHeight, 100)
end
-- Test: Element with maxLines but no multiline
function TestElementUnhappyPaths:test_max_lines_without_multiline()
local element = FlexLove.new({
id = "text",
width = 200,
height = 100,
editable = true,
multiline = false,
maxLines = 5, -- Should be ignored for single-line
})
luaunit.assertNotNil(element)
end
-- Test: Element with maxLength 0
function TestElementUnhappyPaths:test_max_length_zero()
local element = FlexLove.new({
id = "text",
width = 200,
height = 40,
editable = true,
maxLength = 0,
})
luaunit.assertNotNil(element)
end
-- Test: Element with negative maxLength
function TestElementUnhappyPaths:test_max_length_negative()
local element = FlexLove.new({
id = "text",
width = 200,
height = 40,
editable = true,
maxLength = -10,
})
luaunit.assertNotNil(element)
end
if not _G.RUNNING_ALL_TESTS then if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run()) os.exit(luaunit.LuaUnit.run())
end end

View File

@@ -4,10 +4,25 @@ package.path = package.path .. ";./?.lua;./modules/?.lua"
require("testing.loveStub") require("testing.loveStub")
local luaunit = require("testing.luaunit") local luaunit = require("testing.luaunit")
local ErrorHandler = require("modules.ErrorHandler") local ErrorHandler = require("modules.ErrorHandler")
local ErrorCodes = require("modules.ErrorCodes")
TestErrorHandler = {} TestErrorHandler = {}
-- Test: error() throws with correct format function TestErrorHandler:setUp()
-- Reset debug mode and logging before each test
ErrorHandler.setDebugMode(false)
ErrorHandler.setLogTarget("none") -- Disable logging during tests
end
function TestErrorHandler:tearDown()
-- Clean up any test log files
os.remove("test-errors.log")
for i = 1, 5 do
os.remove("test-errors.log." .. i)
end
end
-- Test: error() throws with correct format (backward compatibility)
function TestErrorHandler:test_error_throws_with_format() function TestErrorHandler:test_error_throws_with_format()
local success, err = pcall(function() local success, err = pcall(function()
ErrorHandler.error("TestModule", "Something went wrong") ErrorHandler.error("TestModule", "Something went wrong")
@@ -17,7 +32,65 @@ function TestErrorHandler:test_error_throws_with_format()
luaunit.assertStrContains(err, "[FlexLove - TestModule] Error: Something went wrong") luaunit.assertStrContains(err, "[FlexLove - TestModule] Error: Something went wrong")
end end
-- Test: warn() prints with correct format -- Test: error() with error code
function TestErrorHandler:test_error_with_code()
local success, err = pcall(function()
ErrorHandler.error("TestModule", "VAL_001", "Invalid property type")
end)
luaunit.assertFalse(success, "error() should throw")
luaunit.assertStrContains(err, "[FlexLove - TestModule] Error [FLEXLOVE_VAL_001]")
luaunit.assertStrContains(err, "Invalid property type")
end
-- Test: error() with error code and details
function TestErrorHandler:test_error_with_code_and_details()
local success, err = pcall(function()
ErrorHandler.error("TestModule", "VAL_001", "Invalid property type", {
property = "width",
expected = "number",
got = "string",
})
end)
luaunit.assertFalse(success, "error() should throw")
luaunit.assertStrContains(err, "[FLEXLOVE_VAL_001]")
luaunit.assertStrContains(err, "Details:")
luaunit.assertStrContains(err, "Property: width")
luaunit.assertStrContains(err, "Expected: number")
luaunit.assertStrContains(err, "Got: string")
end
-- Test: error() with error code, details, and custom suggestion
function TestErrorHandler:test_error_with_code_details_and_suggestion()
local success, err = pcall(function()
ErrorHandler.error("TestModule", "VAL_001", "Invalid property type", {
property = "width",
expected = "number",
got = "string",
}, "Use a number like width = 100")
end)
luaunit.assertFalse(success, "error() should throw")
luaunit.assertStrContains(err, "Suggestion: Use a number like width = 100")
end
-- Test: error() with code uses automatic suggestion
function TestErrorHandler:test_error_with_code_uses_auto_suggestion()
local success, err = pcall(function()
ErrorHandler.error("TestModule", "VAL_001", "Invalid property type", {
property = "width",
})
end)
luaunit.assertFalse(success, "error() should throw")
luaunit.assertStrContains(err, "Suggestion:")
-- Should contain suggestion from ErrorCodes
local suggestion = ErrorCodes.getSuggestion("VAL_001")
luaunit.assertStrContains(err, suggestion)
end
-- Test: warn() prints with correct format (backward compatibility)
function TestErrorHandler:test_warn_prints_with_format() function TestErrorHandler:test_warn_prints_with_format()
-- Capture print output by mocking print -- Capture print output by mocking print
local captured = nil local captured = nil
@@ -34,20 +107,60 @@ function TestErrorHandler:test_warn_prints_with_format()
luaunit.assertEquals(captured, "[FlexLove - TestModule] Warning: This is a warning") luaunit.assertEquals(captured, "[FlexLove - TestModule] Warning: This is a warning")
end end
-- Test: warn() with error code
function TestErrorHandler:test_warn_with_code()
local captured = nil
local originalPrint = print
print = function(msg)
captured = msg
end
ErrorHandler.warn("TestModule", "VAL_001", "Potentially invalid property")
print = originalPrint
luaunit.assertNotNil(captured, "warn() should print")
luaunit.assertStrContains(captured, "[FlexLove - TestModule] Warning [FLEXLOVE_VAL_001]")
luaunit.assertStrContains(captured, "Potentially invalid property")
end
-- Test: warn() with details
function TestErrorHandler:test_warn_with_details()
local captured = nil
local originalPrint = print
print = function(msg)
captured = msg
end
ErrorHandler.warn("TestModule", "VAL_001", "Check this property", {
property = "height",
value = "auto",
})
print = originalPrint
luaunit.assertNotNil(captured, "warn() should print")
luaunit.assertStrContains(captured, "Details:")
luaunit.assertStrContains(captured, "Property: height")
luaunit.assertStrContains(captured, "Value: auto")
end
-- Test: assertNotNil returns true for non-nil value -- Test: assertNotNil returns true for non-nil value
function TestErrorHandler:test_assertNotNil_returns_true_for_valid() function TestErrorHandler:test_assertNotNil_returns_true_for_valid()
local result = ErrorHandler.assertNotNil("TestModule", "some value", "testParam") local result = ErrorHandler.assertNotNil("TestModule", "some value", "testParam")
luaunit.assertTrue(result, "assertNotNil should return true for non-nil value") luaunit.assertTrue(result, "assertNotNil should return true for non-nil value")
end end
-- Test: assertNotNil throws for nil value -- Test: assertNotNil throws for nil value (now uses error codes)
function TestErrorHandler:test_assertNotNil_throws_for_nil() function TestErrorHandler:test_assertNotNil_throws_for_nil()
local success, err = pcall(function() local success, err = pcall(function()
ErrorHandler.assertNotNil("TestModule", nil, "testParam") ErrorHandler.assertNotNil("TestModule", nil, "testParam")
end) end)
luaunit.assertFalse(success, "assertNotNil should throw for nil") luaunit.assertFalse(success, "assertNotNil should throw for nil")
luaunit.assertStrContains(err, "Parameter 'testParam' cannot be nil") luaunit.assertStrContains(err, "[FLEXLOVE_VAL_003]")
luaunit.assertStrContains(err, "Required parameter missing")
luaunit.assertStrContains(err, "testParam")
end end
-- Test: assertType returns true for correct type -- Test: assertType returns true for correct type
@@ -62,14 +175,16 @@ function TestErrorHandler:test_assertType_returns_true_for_valid()
luaunit.assertTrue(result, "assertType should return true for table") luaunit.assertTrue(result, "assertType should return true for table")
end end
-- Test: assertType throws for wrong type -- Test: assertType throws for wrong type (now uses error codes)
function TestErrorHandler:test_assertType_throws_for_wrong_type() function TestErrorHandler:test_assertType_throws_for_wrong_type()
local success, err = pcall(function() local success, err = pcall(function()
ErrorHandler.assertType("TestModule", 123, "string", "testParam") ErrorHandler.assertType("TestModule", 123, "string", "testParam")
end) end)
luaunit.assertFalse(success, "assertType should throw for wrong type") luaunit.assertFalse(success, "assertType should throw for wrong type")
luaunit.assertStrContains(err, "Parameter 'testParam' must be string, got number") luaunit.assertStrContains(err, "[FLEXLOVE_VAL_001]")
luaunit.assertStrContains(err, "Invalid property type")
luaunit.assertStrContains(err, "testParam")
end end
-- Test: assertRange returns true for value in range -- Test: assertRange returns true for value in range
@@ -84,24 +199,27 @@ function TestErrorHandler:test_assertRange_returns_true_for_valid()
luaunit.assertTrue(result, "assertRange should accept max boundary") luaunit.assertTrue(result, "assertRange should accept max boundary")
end end
-- Test: assertRange throws for value below min -- Test: assertRange throws for value below min (now uses error codes)
function TestErrorHandler:test_assertRange_throws_for_below_min() function TestErrorHandler:test_assertRange_throws_for_below_min()
local success, err = pcall(function() local success, err = pcall(function()
ErrorHandler.assertRange("TestModule", -1, 0, 10, "testParam") ErrorHandler.assertRange("TestModule", -1, 0, 10, "testParam")
end) end)
luaunit.assertFalse(success, "assertRange should throw for value below min") luaunit.assertFalse(success, "assertRange should throw for value below min")
luaunit.assertStrContains(err, "Parameter 'testParam' must be between 0 and 10, got -1") luaunit.assertStrContains(err, "[FLEXLOVE_VAL_002]")
luaunit.assertStrContains(err, "Property value out of range")
luaunit.assertStrContains(err, "testParam")
end end
-- Test: assertRange throws for value above max -- Test: assertRange throws for value above max (now uses error codes)
function TestErrorHandler:test_assertRange_throws_for_above_max() function TestErrorHandler:test_assertRange_throws_for_above_max()
local success, err = pcall(function() local success, err = pcall(function()
ErrorHandler.assertRange("TestModule", 11, 0, 10, "testParam") ErrorHandler.assertRange("TestModule", 11, 0, 10, "testParam")
end) end)
luaunit.assertFalse(success, "assertRange should throw for value above max") luaunit.assertFalse(success, "assertRange should throw for value above max")
luaunit.assertStrContains(err, "Parameter 'testParam' must be between 0 and 10, got 11") luaunit.assertStrContains(err, "[FLEXLOVE_VAL_002]")
luaunit.assertStrContains(err, "Property value out of range")
end end
-- Test: warnDeprecated prints deprecation warning -- Test: warnDeprecated prints deprecation warning
@@ -136,6 +254,181 @@ function TestErrorHandler:test_warnCommonMistake_prints_message()
luaunit.assertStrContains(captured, "Width is zero. Suggestion: Set width to positive value") luaunit.assertStrContains(captured, "Width is zero. Suggestion: Set width to positive value")
end end
-- Test: debug mode enables stack traces
function TestErrorHandler:test_debug_mode_enables_stack_trace()
ErrorHandler.setDebugMode(true)
local success, err = pcall(function()
ErrorHandler.error("TestModule", "VAL_001", "Test error")
end)
luaunit.assertFalse(success, "error() should throw")
luaunit.assertStrContains(err, "Stack trace:")
ErrorHandler.setDebugMode(false)
end
-- Test: setStackTrace independently
function TestErrorHandler:test_set_stack_trace()
ErrorHandler.setStackTrace(true)
local success, err = pcall(function()
ErrorHandler.error("TestModule", "VAL_001", "Test error")
end)
luaunit.assertFalse(success, "error() should throw")
luaunit.assertStrContains(err, "Stack trace:")
ErrorHandler.setStackTrace(false)
end
-- Test: error code validation
function TestErrorHandler:test_invalid_error_code_fallback()
local success, err = pcall(function()
ErrorHandler.error("TestModule", "INVALID_CODE", "This is a message")
end)
luaunit.assertFalse(success, "error() should throw")
-- Should treat as message (backward compatibility)
luaunit.assertStrContains(err, "INVALID_CODE")
luaunit.assertStrContains(err, "This is a message")
end
-- Test: details formatting with long values
function TestErrorHandler:test_details_with_long_values()
local longValue = string.rep("x", 150)
local success, err = pcall(function()
ErrorHandler.error("TestModule", "VAL_001", "Test", {
shortValue = "short",
longValue = longValue,
})
end)
luaunit.assertFalse(success, "error() should throw")
luaunit.assertStrContains(err, "ShortValue: short")
-- Long value should be truncated
luaunit.assertStrContains(err, "...")
end
-- Test: file logging
function TestErrorHandler:test_file_logging()
ErrorHandler.setLogTarget("file")
ErrorHandler.setLogFile("test-errors.log")
-- Trigger an error (will be caught)
local success = pcall(function()
ErrorHandler.error("TestModule", "VAL_001", "Test file logging")
end)
-- Check file was created and contains log
local file = io.open("test-errors.log", "r")
luaunit.assertNotNil(file, "Log file should be created")
if file then
local content = file:read("*all")
file:close()
luaunit.assertStrContains(content, "ERROR")
luaunit.assertStrContains(content, "TestModule")
luaunit.assertStrContains(content, "Test file logging")
end
-- Cleanup
ErrorHandler.setLogTarget("none")
os.remove("test-errors.log")
end
-- Test: log level filtering
function TestErrorHandler:test_log_level_filtering()
ErrorHandler.setLogTarget("file")
ErrorHandler.setLogFile("test-errors.log")
ErrorHandler.setLogLevel("ERROR") -- Only log errors, not warnings
-- Trigger a warning (should not be logged)
ErrorHandler.warn("TestModule", "VAL_001", "Test warning")
-- Trigger an error (should be logged)
pcall(function()
ErrorHandler.error("TestModule", "VAL_001", "Test error")
end)
-- Check file
local file = io.open("test-errors.log", "r")
if file then
local content = file:read("*all")
file:close()
luaunit.assertStrContains(content, "Test error")
luaunit.assertFalse(content:find("Test warning") ~= nil, "Warning should not be logged")
end
-- Cleanup
ErrorHandler.setLogTarget("none")
ErrorHandler.setLogLevel("WARNING")
os.remove("test-errors.log")
end
-- Test: JSON format
function TestErrorHandler:test_json_format()
ErrorHandler.setLogTarget("file")
ErrorHandler.setLogFile("test-errors.log")
ErrorHandler.setLogFormat("json")
pcall(function()
ErrorHandler.error("TestModule", "VAL_001", "Test JSON", {
property = "width",
})
end)
local file = io.open("test-errors.log", "r")
if file then
local content = file:read("*all")
file:close()
-- Should be valid JSON-like
luaunit.assertStrContains(content, '"level":"ERROR"')
luaunit.assertStrContains(content, '"module":"TestModule"')
luaunit.assertStrContains(content, '"message":"Test JSON"')
luaunit.assertStrContains(content, '"details":')
end
-- Cleanup
ErrorHandler.setLogTarget("none")
ErrorHandler.setLogFormat("human")
os.remove("test-errors.log")
end
-- Test: log rotation
function TestErrorHandler:test_log_rotation()
ErrorHandler.setLogTarget("file")
ErrorHandler.setLogFile("test-errors.log")
ErrorHandler.enableLogRotation({ maxSize = 100, maxFiles = 2 }) -- Very small for testing
-- Write multiple errors to trigger rotation
for i = 1, 10 do
pcall(function()
ErrorHandler.error("TestModule", "VAL_001", "Test rotation error number " .. i)
end)
end
-- Check that rotation occurred (main file should exist)
local file = io.open("test-errors.log", "r")
luaunit.assertNotNil(file, "Main log file should exist")
if file then
file:close()
end
-- Check that rotated files might exist (depending on log size)
-- We won't assert this as it depends on exact message size
-- Cleanup
ErrorHandler.setLogTarget("none")
ErrorHandler.enableLogRotation(true) -- Reset to defaults
os.remove("test-errors.log")
os.remove("test-errors.log.1")
os.remove("test-errors.log.2")
end
if not _G.RUNNING_ALL_TESTS then if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run()) os.exit(luaunit.LuaUnit.run())
end end

View File

@@ -21,7 +21,7 @@ end
function TestFlexLove:testModuleLoads() function TestFlexLove:testModuleLoads()
luaunit.assertNotNil(FlexLove) luaunit.assertNotNil(FlexLove)
luaunit.assertNotNil(FlexLove._VERSION) luaunit.assertNotNil(FlexLove._VERSION)
luaunit.assertEquals(FlexLove._VERSION, "0.2.0") luaunit.assertEquals(FlexLove._VERSION, "0.2.2")
luaunit.assertNotNil(FlexLove._DESCRIPTION) luaunit.assertNotNil(FlexLove._DESCRIPTION)
luaunit.assertNotNil(FlexLove._URL) luaunit.assertNotNil(FlexLove._URL)
luaunit.assertNotNil(FlexLove._LICENSE) luaunit.assertNotNil(FlexLove._LICENSE)
@@ -44,8 +44,8 @@ function TestFlexLove:testInitWithBaseScale()
FlexLove.init({ FlexLove.init({
baseScale = { baseScale = {
width = 1920, width = 1920,
height = 1080 height = 1080,
} },
}) })
luaunit.assertNotNil(FlexLove.baseScale) luaunit.assertNotNil(FlexLove.baseScale)
@@ -56,7 +56,7 @@ end
-- Test: init() with partial baseScale (uses defaults) -- Test: init() with partial baseScale (uses defaults)
function TestFlexLove:testInitWithPartialBaseScale() function TestFlexLove:testInitWithPartialBaseScale()
FlexLove.init({ FlexLove.init({
baseScale = {} baseScale = {},
}) })
luaunit.assertNotNil(FlexLove.baseScale) luaunit.assertNotNil(FlexLove.baseScale)
@@ -69,13 +69,13 @@ function TestFlexLove:testInitWithStringTheme()
-- Pre-register a theme -- Pre-register a theme
local theme = Theme.new({ local theme = Theme.new({
name = "test", name = "test",
components = {} components = {},
}) })
-- init() tries to load and then set active, which may fail if theme path doesn't exist -- init() tries to load and then set active, which may fail if theme path doesn't exist
-- Just check that it doesn't crash -- Just check that it doesn't crash
FlexLove.init({ FlexLove.init({
theme = "test" theme = "test",
}) })
-- The theme setting may fail silently, so just check it doesn't crash -- The theme setting may fail silently, so just check it doesn't crash
@@ -87,8 +87,8 @@ function TestFlexLove:testInitWithTableTheme()
FlexLove.init({ FlexLove.init({
theme = { theme = {
name = "custom", name = "custom",
components = {} components = {},
} },
}) })
luaunit.assertEquals(FlexLove.defaultTheme, "custom") luaunit.assertEquals(FlexLove.defaultTheme, "custom")
@@ -97,7 +97,7 @@ end
-- Test: init() with invalid theme (should not crash) -- Test: init() with invalid theme (should not crash)
function TestFlexLove:testInitWithInvalidTheme() function TestFlexLove:testInitWithInvalidTheme()
FlexLove.init({ FlexLove.init({
theme = "nonexistent-theme" theme = "nonexistent-theme",
}) })
-- Should not crash, just print warning -- Should not crash, just print warning
@@ -107,7 +107,7 @@ end
-- Test: init() with immediateMode = true -- Test: init() with immediateMode = true
function TestFlexLove:testInitWithImmediateMode() function TestFlexLove:testInitWithImmediateMode()
FlexLove.init({ FlexLove.init({
immediateMode = true immediateMode = true,
}) })
luaunit.assertEquals(FlexLove.getMode(), "immediate") luaunit.assertEquals(FlexLove.getMode(), "immediate")
@@ -116,7 +116,7 @@ end
-- Test: init() with immediateMode = false -- Test: init() with immediateMode = false
function TestFlexLove:testInitWithRetainedMode() function TestFlexLove:testInitWithRetainedMode()
FlexLove.init({ FlexLove.init({
immediateMode = false immediateMode = false,
}) })
luaunit.assertEquals(FlexLove.getMode(), "retained") luaunit.assertEquals(FlexLove.getMode(), "retained")
@@ -125,7 +125,7 @@ end
-- Test: init() with autoFrameManagement -- Test: init() with autoFrameManagement
function TestFlexLove:testInitWithAutoFrameManagement() function TestFlexLove:testInitWithAutoFrameManagement()
FlexLove.init({ FlexLove.init({
autoFrameManagement = true autoFrameManagement = true,
}) })
luaunit.assertEquals(FlexLove._autoFrameManagement, true) luaunit.assertEquals(FlexLove._autoFrameManagement, true)
@@ -135,7 +135,7 @@ end
function TestFlexLove:testInitWithStateConfig() function TestFlexLove:testInitWithStateConfig()
FlexLove.init({ FlexLove.init({
stateRetentionFrames = 5, stateRetentionFrames = 5,
maxStateEntries = 100 maxStateEntries = 100,
}) })
luaunit.assertTrue(true) -- Should configure StateManager luaunit.assertTrue(true) -- Should configure StateManager
@@ -229,7 +229,7 @@ function TestFlexLove:testNewImmediateMode()
local element = FlexLove.new({ local element = FlexLove.new({
id = "test-element", id = "test-element",
width = 100, width = 100,
height = 100 height = 100,
}) })
luaunit.assertNotNil(element) luaunit.assertNotNil(element)
@@ -246,7 +246,7 @@ function TestFlexLove:testNewAutoBeginFrame()
local element = FlexLove.new({ local element = FlexLove.new({
id = "auto-begin-test", id = "auto-begin-test",
width = 50, width = 50,
height = 50 height = 50,
}) })
luaunit.assertNotNil(element) luaunit.assertNotNil(element)
@@ -307,10 +307,11 @@ function TestFlexLove:testDrawWithBothFuncs()
local gameCalled = false local gameCalled = false
local postCalled = false local postCalled = false
FlexLove.draw( FlexLove.draw(function()
function() gameCalled = true end, gameCalled = true
function() postCalled = true end end, function()
) postCalled = true
end)
luaunit.assertTrue(gameCalled) luaunit.assertTrue(gameCalled)
luaunit.assertTrue(postCalled) luaunit.assertTrue(postCalled)
@@ -323,7 +324,7 @@ function TestFlexLove:testDrawWithElements()
local element = FlexLove.new({ local element = FlexLove.new({
width = 100, width = 100,
height = 100, height = 100,
backgroundColor = Color.new(1, 1, 1, 1) backgroundColor = Color.new(1, 1, 1, 1),
}) })
FlexLove.draw() FlexLove.draw()
@@ -338,7 +339,7 @@ function TestFlexLove:testDrawAutoEndFrame()
local element = FlexLove.new({ local element = FlexLove.new({
id = "auto-end-test", id = "auto-end-test",
width = 100, width = 100,
height = 100 height = 100,
}) })
-- draw() should call endFrame() if _autoBeganFrame is true -- draw() should call endFrame() if _autoBeganFrame is true
@@ -361,7 +362,7 @@ function TestFlexLove:testUpdateRetainedMode()
local element = FlexLove.new({ local element = FlexLove.new({
width = 100, width = 100,
height = 100 height = 100,
}) })
FlexLove.update(0.016) FlexLove.update(0.016)
@@ -377,7 +378,7 @@ function TestFlexLove:testUpdateImmediateMode()
local element = FlexLove.new({ local element = FlexLove.new({
id = "update-test", id = "update-test",
width = 100, width = 100,
height = 100 height = 100,
}) })
FlexLove.endFrame() FlexLove.endFrame()
@@ -399,8 +400,8 @@ function TestFlexLove:testResizeWithBaseScale()
FlexLove.init({ FlexLove.init({
baseScale = { baseScale = {
width = 1920, width = 1920,
height = 1080 height = 1080,
} },
}) })
FlexLove.resize() FlexLove.resize()
@@ -414,7 +415,7 @@ function TestFlexLove:testResizeWithElements()
local element = FlexLove.new({ local element = FlexLove.new({
width = 100, width = 100,
height = 100 height = 100,
}) })
FlexLove.resize() FlexLove.resize()
@@ -428,7 +429,7 @@ function TestFlexLove:testDestroy()
local element = FlexLove.new({ local element = FlexLove.new({
width = 100, width = 100,
height = 100 height = 100,
}) })
FlexLove.destroy() FlexLove.destroy()
@@ -453,7 +454,7 @@ function TestFlexLove:testTextInputWithFocus()
local element = FlexLove.new({ local element = FlexLove.new({
width = 100, width = 100,
height = 100, height = 100,
editable = true editable = true,
}) })
FlexLove._focusedElement = element FlexLove._focusedElement = element
@@ -477,7 +478,7 @@ function TestFlexLove:testKeyPressedWithFocus()
local element = FlexLove.new({ local element = FlexLove.new({
width = 100, width = 100,
height = 100, height = 100,
editable = true editable = true,
}) })
FlexLove._focusedElement = element FlexLove._focusedElement = element
@@ -502,7 +503,7 @@ function TestFlexLove:testWheelMovedImmediate()
local element = FlexLove.new({ local element = FlexLove.new({
id = "wheel-test", id = "wheel-test",
width = 100, width = 100,
height = 100 height = 100,
}) })
FlexLove.endFrame() FlexLove.endFrame()
@@ -527,7 +528,7 @@ function TestFlexLove:testGetStateCountImmediate()
local element = FlexLove.new({ local element = FlexLove.new({
id = "state-test", id = "state-test",
width = 100, width = 100,
height = 100 height = 100,
}) })
FlexLove.endFrame() FlexLove.endFrame()
@@ -552,7 +553,7 @@ function TestFlexLove:testClearStateImmediate()
local element = FlexLove.new({ local element = FlexLove.new({
id = "clear-test", id = "clear-test",
width = 100, width = 100,
height = 100 height = 100,
}) })
FlexLove.endFrame() FlexLove.endFrame()
@@ -615,7 +616,7 @@ function TestFlexLove:testGetElementAtPosition()
y = 0, y = 0,
width = 100, width = 100,
height = 100, height = 100,
onEvent = function() end onEvent = function() end,
}) })
local found = FlexLove.getElementAtPosition(50, 50) local found = FlexLove.getElementAtPosition(50, 50)
@@ -631,7 +632,7 @@ function TestFlexLove:testGetElementAtPositionOutside()
y = 0, y = 0,
width = 100, width = 100,
height = 100, height = 100,
onEvent = function() end onEvent = function() end,
}) })
local found = FlexLove.getElementAtPosition(200, 200) local found = FlexLove.getElementAtPosition(200, 200)
@@ -653,4 +654,586 @@ function TestFlexLove:testEnumsAccessible()
luaunit.assertNotNil(FlexLove.enums.AlignItems) luaunit.assertNotNil(FlexLove.enums.AlignItems)
end end
-- ==========================================
-- UNHAPPY PATH TESTS
-- ==========================================
TestFlexLoveUnhappyPaths = {}
function TestFlexLoveUnhappyPaths:setUp()
FlexLove.destroy()
FlexLove.setMode("retained")
end
function TestFlexLoveUnhappyPaths:tearDown()
FlexLove.destroy()
end
-- Test: init() with invalid config types
function TestFlexLoveUnhappyPaths:testInitWithInvalidConfigTypes()
-- nil and false should work (become {} via `config or {}`)
FlexLove.init(nil)
luaunit.assertTrue(true)
FlexLove.init(false)
luaunit.assertTrue(true)
-- String and number will error when trying to index them (config.errorLogFile, config.baseScale, etc.)
local success = pcall(function()
FlexLove.init("invalid")
end)
-- String indexing may work in Lua (returns nil), check actual behavior
-- Actually strings can be indexed but will return nil for most keys
-- So this might not error! Let's just verify it doesn't crash
luaunit.assertTrue(true)
success = pcall(function()
FlexLove.init(123)
end)
-- Numbers can't be indexed, should error
luaunit.assertFalse(success)
end
-- Test: init() with invalid baseScale values
function TestFlexLoveUnhappyPaths:testInitWithInvalidBaseScale()
-- Negative width/height (should work, just unusual)
FlexLove.init({
baseScale = {
width = -1920,
height = -1080,
},
})
luaunit.assertTrue(true) -- Should not crash
-- Zero width/height (division by zero risk)
local success = pcall(function()
FlexLove.init({
baseScale = {
width = 0,
height = 0,
},
})
end)
-- May or may not error depending on implementation
-- Non-numeric values (should error)
success = pcall(function()
FlexLove.init({
baseScale = {
width = "invalid",
height = "invalid",
},
})
end)
luaunit.assertFalse(success) -- Should error on division
end
-- Test: init() with invalid theme types
function TestFlexLoveUnhappyPaths:testInitWithInvalidThemeTypes()
-- Numeric theme
FlexLove.init({ theme = 123 })
luaunit.assertTrue(true)
-- Boolean theme
FlexLove.init({ theme = true })
luaunit.assertTrue(true)
-- Function theme
FlexLove.init({ theme = function() end })
luaunit.assertTrue(true)
end
-- Test: init() with invalid immediateMode values
function TestFlexLoveUnhappyPaths:testInitWithInvalidImmediateMode()
FlexLove.init({ immediateMode = "yes" })
luaunit.assertTrue(true)
FlexLove.init({ immediateMode = 1 })
luaunit.assertTrue(true)
FlexLove.init({ immediateMode = {} })
luaunit.assertTrue(true)
end
-- Test: init() with invalid state config values
function TestFlexLoveUnhappyPaths:testInitWithInvalidStateConfig()
-- Negative values
FlexLove.init({
stateRetentionFrames = -5,
maxStateEntries = -100,
})
luaunit.assertTrue(true)
-- Non-numeric values
FlexLove.init({
stateRetentionFrames = "five",
maxStateEntries = "hundred",
})
luaunit.assertTrue(true)
-- Zero values
FlexLove.init({
stateRetentionFrames = 0,
maxStateEntries = 0,
})
luaunit.assertTrue(true)
end
-- Test: setMode() with nil
function TestFlexLoveUnhappyPaths:testSetModeNil()
local success = pcall(function()
FlexLove.setMode(nil)
end)
luaunit.assertFalse(success)
end
-- Test: setMode() with number
function TestFlexLoveUnhappyPaths:testSetModeNumber()
local success = pcall(function()
FlexLove.setMode(123)
end)
luaunit.assertFalse(success)
end
-- Test: setMode() with table
function TestFlexLoveUnhappyPaths:testSetModeTable()
local success = pcall(function()
FlexLove.setMode({ mode = "immediate" })
end)
luaunit.assertFalse(success)
end
-- Test: setMode() with empty string
function TestFlexLoveUnhappyPaths:testSetModeEmptyString()
local success = pcall(function()
FlexLove.setMode("")
end)
luaunit.assertFalse(success)
end
-- Test: setMode() with case-sensitive variation
function TestFlexLoveUnhappyPaths:testSetModeCaseSensitive()
local success = pcall(function()
FlexLove.setMode("Immediate")
end)
luaunit.assertFalse(success)
success = pcall(function()
FlexLove.setMode("RETAINED")
end)
luaunit.assertFalse(success)
end
-- Test: beginFrame() multiple times without endFrame()
function TestFlexLoveUnhappyPaths:testBeginFrameMultipleTimes()
FlexLove.setMode("immediate")
FlexLove.beginFrame()
local frameNum1 = FlexLove._frameNumber
FlexLove.beginFrame() -- Call again without ending
local frameNum2 = FlexLove._frameNumber
-- Frame number should increment each time
luaunit.assertTrue(frameNum2 > frameNum1)
end
-- Test: endFrame() without beginFrame()
function TestFlexLoveUnhappyPaths:testEndFrameWithoutBegin()
FlexLove.setMode("immediate")
FlexLove.endFrame() -- Should not crash
luaunit.assertTrue(true)
end
-- Test: endFrame() multiple times
function TestFlexLoveUnhappyPaths:testEndFrameMultipleTimes()
FlexLove.setMode("immediate")
FlexLove.beginFrame()
FlexLove.endFrame()
FlexLove.endFrame() -- Call again
luaunit.assertTrue(true) -- Should not crash
end
-- Test: new() with nil props
function TestFlexLoveUnhappyPaths:testNewWithNilProps()
FlexLove.setMode("retained")
local element = FlexLove.new(nil)
luaunit.assertNotNil(element)
end
-- Test: new() with invalid width/height
function TestFlexLoveUnhappyPaths:testNewWithInvalidDimensions()
FlexLove.setMode("retained")
-- Negative dimensions
local element = FlexLove.new({ width = -100, height = -50 })
luaunit.assertNotNil(element)
-- Zero dimensions
element = FlexLove.new({ width = 0, height = 0 })
luaunit.assertNotNil(element)
-- Invalid string dimensions (now returns fallback 0px with warning)
local success, element = pcall(function()
return FlexLove.new({ width = "invalid", height = "invalid" })
end)
luaunit.assertTrue(success) -- Units.parse returns fallback (0, "px") instead of erroring
luaunit.assertNotNil(element)
end
-- Test: new() with invalid position
function TestFlexLoveUnhappyPaths:testNewWithInvalidPosition()
FlexLove.setMode("retained")
-- Negative positions
local element = FlexLove.new({ x = -1000, y = -1000, width = 100, height = 100 })
luaunit.assertNotNil(element)
-- Extreme positions
element = FlexLove.new({ x = 1000000, y = 1000000, width = 100, height = 100 })
luaunit.assertNotNil(element)
end
-- Test: new() with circular parent reference
function TestFlexLoveUnhappyPaths:testNewWithCircularParent()
FlexLove.setMode("retained")
local parent = FlexLove.new({ width = 200, height = 200 })
local child = FlexLove.new({ width = 100, height = 100, parent = parent })
-- Try to make parent a child of child (circular reference)
-- This should be prevented by the design
luaunit.assertNotEquals(parent.parent, child)
end
-- Test: new() in immediate mode without frame
function TestFlexLoveUnhappyPaths:testNewImmediateModeNoFrame()
FlexLove.setMode("immediate")
-- Don't call beginFrame()
local element = FlexLove.new({ width = 100, height = 100 })
luaunit.assertNotNil(element)
luaunit.assertTrue(FlexLove._autoBeganFrame)
end
-- Test: draw() with invalid function types
function TestFlexLoveUnhappyPaths:testDrawWithInvalidFunctions()
FlexLove.setMode("retained")
-- Non-function gameDrawFunc
FlexLove.draw("not a function", nil)
luaunit.assertTrue(true)
FlexLove.draw(123, nil)
luaunit.assertTrue(true)
FlexLove.draw({}, nil)
luaunit.assertTrue(true)
-- Non-function postDrawFunc
FlexLove.draw(nil, "not a function")
luaunit.assertTrue(true)
FlexLove.draw(nil, 456)
luaunit.assertTrue(true)
end
-- Test: draw() with function that errors
function TestFlexLoveUnhappyPaths:testDrawWithErroringFunction()
FlexLove.setMode("retained")
local success = pcall(function()
FlexLove.draw(function()
error("Intentional error")
end)
end)
luaunit.assertFalse(success)
end
-- Test: update() with invalid dt
function TestFlexLoveUnhappyPaths:testUpdateWithInvalidDt()
FlexLove.setMode("retained")
-- Negative dt
FlexLove.update(-0.016)
luaunit.assertTrue(true)
-- Zero dt
FlexLove.update(0)
luaunit.assertTrue(true)
-- Huge dt
FlexLove.update(1000)
luaunit.assertTrue(true)
-- nil dt
local success = pcall(function()
FlexLove.update(nil)
end)
-- May or may not error depending on implementation
end
-- Test: textinput() with invalid text
function TestFlexLoveUnhappyPaths:testTextInputWithInvalidText()
FlexLove.setMode("retained")
-- nil text
local success = pcall(function()
FlexLove.textinput(nil)
end)
-- Should handle gracefully
-- Number
FlexLove.textinput(123)
luaunit.assertTrue(true)
-- Empty string
FlexLove.textinput("")
luaunit.assertTrue(true)
-- Very long string
FlexLove.textinput(string.rep("a", 10000))
luaunit.assertTrue(true)
end
-- Test: keypressed() with invalid keys
function TestFlexLoveUnhappyPaths:testKeyPressedWithInvalidKeys()
FlexLove.setMode("retained")
-- nil key
local success = pcall(function()
FlexLove.keypressed(nil, nil, false)
end)
-- Empty strings
FlexLove.keypressed("", "", false)
luaunit.assertTrue(true)
-- Invalid key names
FlexLove.keypressed("invalidkey", "invalidscancode", false)
luaunit.assertTrue(true)
-- Non-boolean isrepeat
FlexLove.keypressed("a", "a", "yes")
luaunit.assertTrue(true)
end
-- Test: wheelmoved() with invalid values
function TestFlexLoveUnhappyPaths:testWheelMovedWithInvalidValues()
FlexLove.setMode("retained")
-- Extreme values
FlexLove.wheelmoved(1000000, 1000000)
luaunit.assertTrue(true)
FlexLove.wheelmoved(-1000000, -1000000)
luaunit.assertTrue(true)
-- nil values
local success = pcall(function()
FlexLove.wheelmoved(nil, nil)
end)
-- May or may not error
end
-- Test: resize() repeatedly in quick succession
function TestFlexLoveUnhappyPaths:testResizeRapidly()
FlexLove.setMode("retained")
local element = FlexLove.new({ width = 100, height = 100 })
for i = 1, 100 do
FlexLove.resize()
end
luaunit.assertTrue(true) -- Should not crash
end
-- Test: destroy() twice
function TestFlexLoveUnhappyPaths:testDestroyTwice()
FlexLove.setMode("retained")
local element = FlexLove.new({ width = 100, height = 100 })
FlexLove.destroy()
FlexLove.destroy() -- Call again
luaunit.assertTrue(true) -- Should not crash
end
-- Test: clearState() with invalid ID types
function TestFlexLoveUnhappyPaths:testClearStateWithInvalidIds()
FlexLove.setMode("immediate")
-- nil ID (should error)
local success = pcall(function()
FlexLove.clearState(nil)
end)
luaunit.assertFalse(success)
-- Number ID (should work, gets converted to string)
FlexLove.clearState(123)
luaunit.assertTrue(true)
-- Table ID (may error)
success = pcall(function()
FlexLove.clearState({})
end)
-- May or may not work depending on tostring implementation
-- Empty string (should work)
FlexLove.clearState("")
luaunit.assertTrue(true)
-- Non-existent ID (should work, just does nothing)
FlexLove.clearState("nonexistent-id-12345")
luaunit.assertTrue(true)
end
-- Test: getElementAtPosition() with invalid coordinates
function TestFlexLoveUnhappyPaths:testGetElementAtPositionWithInvalidCoords()
FlexLove.setMode("retained")
-- Negative coordinates
local element = FlexLove.getElementAtPosition(-100, -100)
luaunit.assertNil(element)
-- Extreme coordinates
element = FlexLove.getElementAtPosition(1000000, 1000000)
luaunit.assertNil(element)
-- nil coordinates
local success = pcall(function()
FlexLove.getElementAtPosition(nil, nil)
end)
-- May or may not error
end
-- Test: Creating element with conflicting properties
function TestFlexLoveUnhappyPaths:testNewWithConflictingProperties()
FlexLove.setMode("retained")
-- Both width auto and explicit
local element = FlexLove.new({
width = 100,
autosizing = { width = true },
})
luaunit.assertNotNil(element)
-- Conflicting positioning
element = FlexLove.new({
positioning = "flex",
x = 100, -- Absolute position with flex
y = 100,
})
luaunit.assertNotNil(element)
end
-- Test: Multiple mode switches
function TestFlexLoveUnhappyPaths:testMultipleModeSwitches()
for i = 1, 10 do
FlexLove.setMode("immediate")
FlexLove.setMode("retained")
end
luaunit.assertTrue(true)
end
-- Test: Creating elements during draw
function TestFlexLoveUnhappyPaths:testCreatingElementsDuringDraw()
FlexLove.setMode("retained")
local drawCalled = false
FlexLove.draw(function()
-- Try to create element during draw
local element = FlexLove.new({ width = 100, height = 100 })
luaunit.assertNotNil(element)
drawCalled = true
end)
luaunit.assertTrue(drawCalled)
end
-- Test: State operations in retained mode (should do nothing)
function TestFlexLoveUnhappyPaths:testStateOperationsInRetainedMode()
FlexLove.setMode("retained")
local count = FlexLove.getStateCount()
luaunit.assertEquals(count, 0)
FlexLove.clearState("any-id")
FlexLove.clearAllStates()
local stats = FlexLove.getStateStats()
luaunit.assertEquals(stats.stateCount, 0)
luaunit.assertEquals(stats.frameNumber, 0)
end
-- Test: Extreme z-index values
function TestFlexLoveUnhappyPaths:testExtremeZIndexValues()
FlexLove.setMode("retained")
local element1 = FlexLove.new({ width = 100, height = 100, z = -1000000 })
local element2 = FlexLove.new({ width = 100, height = 100, z = 1000000 })
luaunit.assertNotNil(element1)
luaunit.assertNotNil(element2)
FlexLove.draw() -- Should not crash during z-index sorting
end
-- Test: Creating deeply nested element hierarchy
function TestFlexLoveUnhappyPaths:testDeeplyNestedHierarchy()
FlexLove.setMode("retained")
local parent = FlexLove.new({ width = 500, height = 500 })
local current = parent
-- Create 100 levels of nesting
for i = 1, 100 do
local child = FlexLove.new({
width = 10,
height = 10,
parent = current,
})
current = child
end
luaunit.assertTrue(true) -- Should not crash
end
-- Test: Error logging configuration edge cases
function TestFlexLoveUnhappyPaths:testErrorLoggingEdgeCases()
-- Empty error log file path
FlexLove.init({ errorLogFile = "" })
luaunit.assertTrue(true)
-- Invalid path characters
FlexLove.init({ errorLogFile = "/invalid/path/\0/file.log" })
luaunit.assertTrue(true)
-- Both enableErrorLogging and errorLogFile
FlexLove.init({
enableErrorLogging = true,
errorLogFile = "test.log",
})
luaunit.assertTrue(true)
end
-- Test: Immediate mode frame management edge cases
function TestFlexLoveUnhappyPaths:testImmediateModeFrameEdgeCases()
FlexLove.setMode("immediate")
-- Begin, draw (should auto-end), then end again
FlexLove.beginFrame()
FlexLove.draw()
FlexLove.endFrame() -- Extra end
luaunit.assertTrue(true)
-- Multiple draws without frames
FlexLove.draw()
FlexLove.draw()
FlexLove.draw()
luaunit.assertTrue(true)
end
return TestFlexLove return TestFlexLove

View File

@@ -65,9 +65,12 @@ function TestImageRenderer:testCalculateFitWithNegativeBoundsHeight()
end end
function TestImageRenderer:testCalculateFitWithInvalidFitMode() function TestImageRenderer:testCalculateFitWithInvalidFitMode()
luaunit.assertError(function() -- Now uses 'fill' fallback with warning instead of error
ImageRenderer.calculateFit(100, 100, 200, 200, "invalid-mode") local result = ImageRenderer.calculateFit(100, 100, 200, 200, "invalid-mode")
end) luaunit.assertNotNil(result)
-- Should fall back to 'fill' mode behavior (scales to fill bounds)
luaunit.assertEquals(result.scaleX, 2)
luaunit.assertEquals(result.scaleY, 2)
end end
function TestImageRenderer:testCalculateFitWithNilFitMode() function TestImageRenderer:testCalculateFitWithNilFitMode()
@@ -208,9 +211,10 @@ function TestImageRenderer:testDrawWithOpacityGreaterThanOne()
end end
function TestImageRenderer:testDrawWithInvalidFitMode() function TestImageRenderer:testDrawWithInvalidFitMode()
luaunit.assertError(function() -- Now uses 'fill' fallback with warning instead of error
-- Should not throw an error, just use fill mode
ImageRenderer.draw(self.mockImage, 0, 0, 100, 100, "invalid") ImageRenderer.draw(self.mockImage, 0, 0, 100, 100, "invalid")
end) luaunit.assertTrue(true) -- If we reach here, no error was thrown
end end
function TestImageRenderer:testCalculateFitWithVerySmallBounds() function TestImageRenderer:testCalculateFitWithVerySmallBounds()

View File

@@ -24,51 +24,67 @@ function TestImageScaler:testScaleNearestWithNilSource()
end end
function TestImageScaler:testScaleNearestWithZeroSourceWidth() function TestImageScaler:testScaleNearestWithZeroSourceWidth()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 0, 10, 20, 20) local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 0, 10, 20, 20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleNearestWithZeroSourceHeight() function TestImageScaler:testScaleNearestWithZeroSourceHeight()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 0, 20, 20) local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 0, 20, 20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleNearestWithNegativeSourceWidth() function TestImageScaler:testScaleNearestWithNegativeSourceWidth()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleNearest(self.mockImageData, 0, 0, -10, 10, 20, 20) local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, -10, 10, 20, 20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleNearestWithNegativeSourceHeight() function TestImageScaler:testScaleNearestWithNegativeSourceHeight()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, -10, 20, 20) local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, -10, 20, 20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleNearestWithZeroDestWidth() function TestImageScaler:testScaleNearestWithZeroDestWidth()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 0, 20) local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 0, 20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleNearestWithZeroDestHeight() function TestImageScaler:testScaleNearestWithZeroDestHeight()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, 0) local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, 0)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleNearestWithNegativeDestWidth() function TestImageScaler:testScaleNearestWithNegativeDestWidth()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, -20, 20) local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, -20, 20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleNearestWithNegativeDestHeight() function TestImageScaler:testScaleNearestWithNegativeDestHeight()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, -20) local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, -20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
-- Unhappy path tests for scaleBilinear -- Unhappy path tests for scaleBilinear
@@ -80,51 +96,67 @@ function TestImageScaler:testScaleBilinearWithNilSource()
end end
function TestImageScaler:testScaleBilinearWithZeroSourceWidth() function TestImageScaler:testScaleBilinearWithZeroSourceWidth()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 0, 10, 20, 20) local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 0, 10, 20, 20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleBilinearWithZeroSourceHeight() function TestImageScaler:testScaleBilinearWithZeroSourceHeight()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 0, 20, 20) local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 0, 20, 20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleBilinearWithNegativeSourceWidth() function TestImageScaler:testScaleBilinearWithNegativeSourceWidth()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, -10, 10, 20, 20) local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, -10, 10, 20, 20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleBilinearWithNegativeSourceHeight() function TestImageScaler:testScaleBilinearWithNegativeSourceHeight()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, -10, 20, 20) local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, -10, 20, 20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleBilinearWithZeroDestWidth() function TestImageScaler:testScaleBilinearWithZeroDestWidth()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 0, 20) local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 0, 20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleBilinearWithZeroDestHeight() function TestImageScaler:testScaleBilinearWithZeroDestHeight()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, 0) local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, 0)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleBilinearWithNegativeDestWidth() function TestImageScaler:testScaleBilinearWithNegativeDestWidth()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, -20, 20) local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, -20, 20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
function TestImageScaler:testScaleBilinearWithNegativeDestHeight() function TestImageScaler:testScaleBilinearWithNegativeDestHeight()
luaunit.assertError(function() -- Now returns 1x1 transparent fallback with warning instead of error
ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, -20) local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, -20)
end) luaunit.assertNotNil(result)
luaunit.assertEquals(result:getWidth(), 1)
luaunit.assertEquals(result:getHeight(), 1)
end end
-- Edge case tests -- Edge case tests

View File

@@ -161,13 +161,15 @@ end
-- Test: new() with imagePath (successful load via cache) -- Test: new() with imagePath (successful load via cache)
function TestRenderer:testNewWithImagePathSuccessfulLoad() function TestRenderer:testNewWithImagePathSuccessfulLoad()
local mockImage = { local mockImage = {
getDimensions = function() return 50, 50 end getDimensions = function()
return 50, 50
end,
} }
-- Pre-populate the cache so load succeeds -- Pre-populate the cache so load succeeds
ImageCache._cache["test/image.png"] = { ImageCache._cache["test/image.png"] = {
image = mockImage, image = mockImage,
imageData = nil imageData = nil,
} }
local renderer = Renderer.new({ local renderer = Renderer.new({

View File

@@ -89,20 +89,33 @@ function TestUnitsParse:testParseZero()
end end
function TestUnitsParse:testParseInvalidType() function TestUnitsParse:testParseInvalidType()
luaunit.assertErrorMsgContains("Invalid unit value type", Units.parse, nil) -- Now returns fallback value (0, "px") with warning instead of error
local value, unit = Units.parse(nil)
luaunit.assertEquals(value, 0)
luaunit.assertEquals(unit, "px")
end end
function TestUnitsParse:testParseInvalidString() function TestUnitsParse:testParseInvalidString()
luaunit.assertErrorMsgContains("Invalid unit format", Units.parse, "abc") -- Now returns fallback value (0, "px") with warning instead of error
local value, unit = Units.parse("abc")
luaunit.assertEquals(value, 0)
luaunit.assertEquals(unit, "px")
end end
function TestUnitsParse:testParseInvalidUnit() function TestUnitsParse:testParseInvalidUnit()
luaunit.assertErrorMsgContains("Unknown unit", Units.parse, "100xyz") -- Now extracts the number and treats as pixels with warning instead of error
-- "100xyz" -> extracts 100, ignores invalid unit "xyz", treats as "100px"
local value, unit = Units.parse("100xyz")
luaunit.assertEquals(value, 100)
luaunit.assertEquals(unit, "px")
end end
function TestUnitsParse:testParseWithSpace() function TestUnitsParse:testParseWithSpace()
-- Spaces between number and unit should be invalid -- Spaces between number and unit should be invalid
luaunit.assertErrorMsgContains("contains space", Units.parse, "100 px") -- Now returns fallback value (0, "px") with warning instead of error
local value, unit = Units.parse("100 px")
luaunit.assertEquals(value, 0)
luaunit.assertEquals(unit, "px")
end end
-- Test suite for Units.resolve() -- Test suite for Units.resolve()
@@ -369,11 +382,17 @@ function TestUnitsEdgeCases:testResolveZeroParentSize()
end end
function TestUnitsEdgeCases:testParseEmptyString() function TestUnitsEdgeCases:testParseEmptyString()
luaunit.assertErrorMsgContains("Invalid unit format", Units.parse, "") -- Now returns fallback value (0, "px") with warning instead of error
local value, unit = Units.parse("")
luaunit.assertEquals(value, 0)
luaunit.assertEquals(unit, "px")
end end
function TestUnitsEdgeCases:testParseOnlyUnit() function TestUnitsEdgeCases:testParseOnlyUnit()
luaunit.assertErrorMsgContains("Missing numeric value before unit", Units.parse, "px") -- Now returns fallback value (0, "px") with warning instead of error
local value, unit = Units.parse("px")
luaunit.assertEquals(value, 0)
luaunit.assertEquals(unit, "px")
end end
function TestUnitsEdgeCases:testResolveNegativePercentage() function TestUnitsEdgeCases:testResolveNegativePercentage()