diff --git a/.gitignore b/.gitignore index b1e5bc7..363fc82 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ docs/doc.json docs/doc.md docs/node_modules releases/ +*.log* diff --git a/FlexLove.lua b/FlexLove.lua index 2d11a96..b15aff5 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -9,6 +9,7 @@ local utils = req("utils") local Units = req("Units") local Context = req("Context") local StateManager = req("StateManager") +local ErrorCodes = req("ErrorCodes") local ErrorHandler = req("ErrorHandler") local ImageRenderer = req("ImageRenderer") local NinePatch = req("NinePatch") @@ -58,12 +59,32 @@ Element.defaultDependencies = { ---@class FlexLove 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 Units.initialize(Context) Units.initializeErrorHandler(ErrorHandler) +-- Initialize ErrorHandler for Color module +Color.initializeErrorHandler(ErrorHandler) + +-- Initialize ErrorHandler for utils +utils.initializeErrorHandler(ErrorHandler) + -- 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._URL = "https://github.com/mikefreno/FlexLove" flexlove._LICENSE = [[ @@ -90,10 +111,20 @@ flexlove._LICENSE = [[ 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) 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 flexlove.baseScale = { width = config.baseScale.width or 1920, diff --git a/README.md b/README.md index b228580..cb75264 100644 --- a/README.md +++ b/README.md @@ -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.** diff --git a/modules/Color.lua b/modules/Color.lua index f30b904..4533265 100644 --- a/modules/Color.lua +++ b/modules/Color.lua @@ -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 message string ---@return string @@ -77,7 +85,14 @@ function Color.fromHex(hexWithTag) local g = tonumber("0x" .. hex:sub(3, 4)) local b = tonumber("0x" .. hex:sub(5, 6)) 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 return Color.new(r / 255, g / 255, b / 255, 1) elseif #hex == 8 then @@ -86,11 +101,26 @@ function Color.fromHex(hexWithTag) local b = tonumber("0x" .. hex:sub(5, 6)) local a = tonumber("0x" .. hex:sub(7, 8)) 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 return Color.new(r / 255, g / 255, b / 255, a / 255) 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 @@ -373,4 +403,7 @@ function Color.parse(value) return Color.sanitizeColor(value, Color.new(0, 0, 0, 1)) end +-- Export ErrorHandler initializer +Color.initializeErrorHandler = initializeErrorHandler + return Color diff --git a/modules/Element.lua b/modules/Element.lua index 7c61eb8..dc2bb21 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -148,7 +148,8 @@ Element.__index = Element ---@return Element function Element.new(props, deps) 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 local self = setmetatable({}, Element) diff --git a/modules/ErrorCodes.lua b/modules/ErrorCodes.lua index 7527553..fa3f57b 100644 --- a/modules/ErrorCodes.lua +++ b/modules/ErrorCodes.lua @@ -1,5 +1,3 @@ ---- Error code definitions for FlexLove ---- Provides centralized error codes, descriptions, and suggested fixes ---@class ErrorCodes local ErrorCodes = {} diff --git a/modules/ErrorHandler.lua b/modules/ErrorHandler.lua index 4205241..c14b378 100644 --- a/modules/ErrorHandler.lua +++ b/modules/ErrorHandler.lua @@ -1,27 +1,509 @@ --- modules/ErrorHandler.lua 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 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 -local function formatMessage(module, level, message) - return string.format("[FlexLove - %s] %s: %s", module, level, message) +local function formatMessage(module, level, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestionOrNil) + 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 --- Throw a critical error (stops execution) ---@param module string The module name ----@param message string The error message -function ErrorHandler.error(module, message) - error(formatMessage(module, "Error", message), 2) +---@param codeOrMessage string Error code or message +---@param messageOrDetails string|table|nil Message or details +---@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 --- Print a warning (non-critical, continues execution) ---@param module string The module name ----@param message string The warning message -function ErrorHandler.warn(module, message) - print(formatMessage(module, "Warning", message)) +---@param codeOrMessage string Warning code or message +---@param messageOrDetails string|table|nil Message or details +---@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 --- Validate that a value is not nil @@ -31,7 +513,9 @@ end ---@return boolean True if valid function ErrorHandler.assertNotNil(module, value, paramName) 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 end return true @@ -46,10 +530,11 @@ end function ErrorHandler.assertType(module, value, expectedType, paramName) local actualType = type(value) if actualType ~= expectedType then - ErrorHandler.error(module, string.format( - "Parameter '%s' must be %s, got %s", - paramName, expectedType, actualType - )) + ErrorHandler.error(module, "VAL_001", "Invalid property type", { + property = paramName, + expected = expectedType, + got = actualType, + }) return false end return true @@ -64,10 +549,12 @@ end ---@return boolean True if valid function ErrorHandler.assertRange(module, value, min, max, paramName) if value < min or value > max then - ErrorHandler.error(module, string.format( - "Parameter '%s' must be between %s and %s, got %s", - paramName, tostring(min), tostring(max), tostring(value) - )) + ErrorHandler.error(module, "VAL_002", "Property value out of range", { + property = paramName, + min = tostring(min), + max = tostring(max), + value = tostring(value), + }) return false end return true @@ -78,10 +565,7 @@ end ---@param oldName string The deprecated name ---@param newName string The new name to use function ErrorHandler.warnDeprecated(module, oldName, newName) - ErrorHandler.warn(module, string.format( - "'%s' is deprecated. Use '%s' instead", - oldName, newName - )) + ErrorHandler.warn(module, string.format("'%s' is deprecated. Use '%s' instead", oldName, newName)) end --- Warn about a common mistake @@ -89,10 +573,7 @@ end ---@param issue string Description of the issue ---@param suggestion string Suggested fix function ErrorHandler.warnCommonMistake(module, issue, suggestion) - ErrorHandler.warn(module, string.format( - "%s. Suggestion: %s", - issue, suggestion - )) + ErrorHandler.warn(module, string.format("%s. Suggestion: %s", issue, suggestion)) end return ErrorHandler diff --git a/modules/ImageDataReader.lua b/modules/ImageDataReader.lua index 00f1823..d22d571 100644 --- a/modules/ImageDataReader.lua +++ b/modules/ImageDataReader.lua @@ -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 = {} +-- 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 ---@return love.ImageData function ImageDataReader.loadImageData(imagePath) if not imagePath then - error(formatError("ImageDataReader", "Image path cannot be nil")) + ErrorHandler.error("ImageDataReader", "VAL_001", "Image path cannot be nil") end local success, result = pcall(function() @@ -20,7 +23,10 @@ function ImageDataReader.loadImageData(imagePath) end) 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 return result @@ -32,14 +38,17 @@ end ---@return table -- Array of {r, g, b, a} values (0-255 range) function ImageDataReader.getRow(imageData, rowIndex) if not imageData then - error(formatError("ImageDataReader", "ImageData cannot be nil")) + ErrorHandler.error("ImageDataReader", "VAL_001", "ImageData cannot be nil") end local width = imageData:getWidth() local height = imageData:getHeight() 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 local pixels = {} @@ -62,14 +71,17 @@ end ---@return table -- Array of {r, g, b, a} values (0-255 range) function ImageDataReader.getColumn(imageData, colIndex) if not imageData then - error(formatError("ImageDataReader", "ImageData cannot be nil")) + ErrorHandler.error("ImageDataReader", "VAL_001", "ImageData cannot be nil") end local width = imageData:getWidth() local height = imageData:getHeight() 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 local pixels = {} diff --git a/modules/ImageRenderer.lua b/modules/ImageRenderer.lua index 028e1e1..f8d23df 100644 --- a/modules/ImageRenderer.lua +++ b/modules/ImageRenderer.lua @@ -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 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 --- Returns source and destination rectangles for rendering ---@param imageWidth number -- Natural width of the image @@ -23,7 +26,12 @@ function ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, bounds objectPosition = objectPosition or "center center" 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 local result = { @@ -104,7 +112,12 @@ function ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, bounds return ImageRenderer.calculateFit(imageWidth, imageHeight, boundsWidth, boundsHeight, "contain", objectPosition) end 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 return result diff --git a/modules/ImageScaler.lua b/modules/ImageScaler.lua index b0ae26f..9bedbdc 100644 --- a/modules/ImageScaler.lua +++ b/modules/ImageScaler.lua @@ -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 -- ==================== 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 --- Produces sharp, pixelated scaling - ideal for pixel art ---@param sourceImageData love.ImageData -- Source image data @@ -24,11 +27,21 @@ local ImageScaler = {} ---@return love.ImageData -- Scaled image data function ImageScaler.scaleNearest(sourceImageData, srcX, srcY, srcW, srcH, destW, destH) if not sourceImageData then - error(formatError("ImageScaler", "Source ImageData cannot be nil")) + ErrorHandler.error("ImageScaler", "VAL_001", "Source ImageData cannot be nil") end 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 -- Create destination ImageData @@ -82,11 +95,21 @@ end ---@return love.ImageData -- Scaled image data function ImageScaler.scaleBilinear(sourceImageData, srcX, srcY, srcW, srcH, destW, destH) if not sourceImageData then - error(formatError("ImageScaler", "Source ImageData cannot be nil")) + ErrorHandler.error("ImageScaler", "VAL_001", "Source ImageData cannot be nil") end 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 -- Create destination ImageData diff --git a/modules/NinePatch.lua b/modules/NinePatch.lua index 655579a..3974fc1 100644 --- a/modules/NinePatch.lua +++ b/modules/NinePatch.lua @@ -1,16 +1,23 @@ local modulePath = (...):match("(.-)[^%.]+$") 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 = {} +-- 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 --- Corners are scaled by scaleCorners multiplier, edges stretch in one dimension only ---@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) local atlasData = component._loadedAtlasData 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 local scaledData diff --git a/modules/Renderer.lua b/modules/Renderer.lua index 6f449e4..da10ef5 100644 --- a/modules/Renderer.lua +++ b/modules/Renderer.lua @@ -30,6 +30,9 @@ local Renderer = {} Renderer.__index = Renderer +-- Lazy-loaded ErrorHandler +local ErrorHandler + --- Create a new Renderer instance ---@param config table Configuration table with rendering properties ---@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 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 local element = self._element diff --git a/modules/ScrollManager.lua b/modules/ScrollManager.lua index 09eb4a1..95289f8 100644 --- a/modules/ScrollManager.lua +++ b/modules/ScrollManager.lua @@ -26,9 +26,13 @@ ---@field _scrollbarPressHandled boolean -- Track if scrollbar press was handled this frame ---@field _Color table ---@field _utils table +---@field _ErrorHandler table? local ScrollManager = {} ScrollManager.__index = ScrollManager +-- Lazy-loaded ErrorHandler +local ErrorHandler + --- Create a new ScrollManager instance ---@param config table Configuration options ---@param deps table Dependencies {Color: Color module, utils: utils module} @@ -92,7 +96,12 @@ end --- Detect if content overflows container bounds function ScrollManager:detectOverflow() 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 local element = self._element @@ -219,7 +228,12 @@ end ---@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}} function ScrollManager:calculateScrollbarDimensions() 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 local element = self._element @@ -312,7 +326,12 @@ end ---@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"} function ScrollManager:getScrollbarAtPosition(mouseX, mouseY) 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 local element = self._element @@ -381,7 +400,12 @@ end ---@return boolean -- True if event was consumed function ScrollManager:handleMousePress(mouseX, mouseY, button) 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 if button ~= 1 then diff --git a/modules/StateManager.lua b/modules/StateManager.lua index 8bbc025..51bedd6 100644 --- a/modules/StateManager.lua +++ b/modules/StateManager.lua @@ -1,6 +1,9 @@ ---@class StateManager local StateManager = {} +-- Load error handler (loaded lazily since it's in a sibling module) +local ErrorHandler + -- State storage: ID -> state table local stateStore = {} @@ -181,7 +184,14 @@ end ---@return table state State table for the element function StateManager.getState(id, defaultState) 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 -- Create state if it doesn't exist @@ -303,7 +313,14 @@ end ---@param state table State to store function StateManager.setState(id, state) 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 stateStore[id] = state diff --git a/modules/Theme.lua b/modules/Theme.lua index e9a38db..f693c7b 100644 --- a/modules/Theme.lua +++ b/modules/Theme.lua @@ -6,14 +6,7 @@ end local NinePatchParser = req("NinePatchParser") local Color = req("Color") local utils = req("utils") - ---- 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 ErrorHandler = req("ErrorHandler") --- Auto-detect the base path where FlexLove is located ---@return string modulePath, string filesystemPath @@ -135,7 +128,9 @@ function Theme.new(definition) -- Validate theme definition local valid, err = validateThemeDefinition(definition) if not valid then - error("[FlexLove] Invalid theme definition: " .. tostring(err)) + ErrorHandler.error("Theme", "THM_001", "Invalid theme definition", { + error = tostring(err) + }) end local self = setmetatable({}, Theme) @@ -150,7 +145,11 @@ function Theme.new(definition) self.atlas = image self.atlasData = imageData 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 else self.atlas = definition.atlas @@ -174,7 +173,11 @@ function Theme.new(definition) local contentHeight = srcHeight - 2 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 -- Create new ImageData for content only @@ -204,7 +207,11 @@ function Theme.new(definition) comp.insets = parseResult.insets comp._ninePatchData = parseResult 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 @@ -221,7 +228,11 @@ function Theme.new(definition) comp._loadedAtlasData = imageData end 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 @@ -310,7 +321,13 @@ function Theme.load(path) if success then definition = result 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 @@ -334,7 +351,12 @@ function Theme.setActive(themeOrName) end 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 diff --git a/modules/Units.lua b/modules/Units.lua index d52bfa7..3b68e4a 100644 --- a/modules/Units.lua +++ b/modules/Units.lua @@ -24,7 +24,13 @@ function Units.parse(value) if type(value) ~= "string" 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 return 0, "px" end @@ -33,7 +39,12 @@ function Units.parse(value) local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true } if validUnits[value] 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 return 0, "px" end @@ -41,7 +52,12 @@ function Units.parse(value) -- Check for invalid format (space between number and unit) if value:match("%d%s+%a") 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 return 0, "px" end @@ -50,7 +66,11 @@ function Units.parse(value) local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$") if not numStr 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 return 0, "px" end @@ -58,7 +78,12 @@ function Units.parse(value) local num = tonumber(numStr) if not num 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 return 0, "px" end @@ -71,7 +96,13 @@ function Units.parse(value) -- validUnits is already defined at the top of the function if not validUnits[unit] 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 return num, "px" end @@ -93,7 +124,10 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize) elseif unit == "%" then if not parentSize 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 error("Percentage units require parent dimension") end @@ -105,7 +139,10 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize) return (value / 100) * viewportHeight else 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 error(string.format("Unknown unit type: '%s'", unit)) end diff --git a/scripts/make-tag.sh b/scripts/make-tag.sh index 767127f..f61eb0f 100755 --- a/scripts/make-tag.sh +++ b/scripts/make-tag.sh @@ -36,10 +36,17 @@ if ! git diff-index --quiet HEAD --; then echo "" 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 - echo -e "${RED}Error: Could not extract version from FlexLove.lua${NC}" - exit 1 + 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 + fi + echo -e "${YELLOW}Using version from FlexLove.lua as fallback${NC}" fi echo -e "${CYAN}Current version:${NC} ${GREEN}v${CURRENT_VERSION}${NC}" @@ -158,12 +165,91 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 0 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 echo "" 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}" +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 -e "${GREEN}═══════════════════════════════════════${NC}" echo -e "${GREEN}✓ Version bump complete!${NC}" diff --git a/testing/__tests__/critical_failures_test.lua b/testing/__tests__/critical_failures_test.lua new file mode 100644 index 0000000..c544244 --- /dev/null +++ b/testing/__tests__/critical_failures_test.lua @@ -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 diff --git a/testing/__tests__/element_test.lua b/testing/__tests__/element_test.lua index 3641ae7..e919a3e 100644 --- a/testing/__tests__/element_test.lua +++ b/testing/__tests__/element_test.lua @@ -1223,6 +1223,771 @@ function TestElementAdditional:test_element_with_userdata() luaunit.assertEquals(element.userdata.count, 42) 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 os.exit(luaunit.LuaUnit.run()) end diff --git a/testing/__tests__/error_handler_test.lua b/testing/__tests__/error_handler_test.lua index dee5ea6..024f2d6 100644 --- a/testing/__tests__/error_handler_test.lua +++ b/testing/__tests__/error_handler_test.lua @@ -4,10 +4,25 @@ package.path = package.path .. ";./?.lua;./modules/?.lua" require("testing.loveStub") local luaunit = require("testing.luaunit") local ErrorHandler = require("modules.ErrorHandler") +local ErrorCodes = require("modules.ErrorCodes") 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() local success, err = pcall(function() 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") 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() -- Capture print output by mocking print local captured = nil @@ -34,20 +107,60 @@ function TestErrorHandler:test_warn_prints_with_format() luaunit.assertEquals(captured, "[FlexLove - TestModule] Warning: This is a warning") 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 function TestErrorHandler:test_assertNotNil_returns_true_for_valid() local result = ErrorHandler.assertNotNil("TestModule", "some value", "testParam") luaunit.assertTrue(result, "assertNotNil should return true for non-nil value") end --- Test: assertNotNil throws for nil value +-- Test: assertNotNil throws for nil value (now uses error codes) function TestErrorHandler:test_assertNotNil_throws_for_nil() local success, err = pcall(function() ErrorHandler.assertNotNil("TestModule", nil, "testParam") end) 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 -- 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") 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() local success, err = pcall(function() ErrorHandler.assertType("TestModule", 123, "string", "testParam") end) 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 -- 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") 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() local success, err = pcall(function() ErrorHandler.assertRange("TestModule", -1, 0, 10, "testParam") end) 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 --- 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() local success, err = pcall(function() ErrorHandler.assertRange("TestModule", 11, 0, 10, "testParam") end) 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 -- 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") 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 os.exit(luaunit.LuaUnit.run()) end diff --git a/testing/__tests__/flexlove_test.lua b/testing/__tests__/flexlove_test.lua index 408d590..589103d 100644 --- a/testing/__tests__/flexlove_test.lua +++ b/testing/__tests__/flexlove_test.lua @@ -21,7 +21,7 @@ end function TestFlexLove:testModuleLoads() luaunit.assertNotNil(FlexLove) 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._URL) luaunit.assertNotNil(FlexLove._LICENSE) @@ -44,10 +44,10 @@ function TestFlexLove:testInitWithBaseScale() FlexLove.init({ baseScale = { width = 1920, - height = 1080 - } + height = 1080, + }, }) - + luaunit.assertNotNil(FlexLove.baseScale) luaunit.assertEquals(FlexLove.baseScale.width, 1920) luaunit.assertEquals(FlexLove.baseScale.height, 1080) @@ -56,9 +56,9 @@ end -- Test: init() with partial baseScale (uses defaults) function TestFlexLove:testInitWithPartialBaseScale() FlexLove.init({ - baseScale = {} + baseScale = {}, }) - + luaunit.assertNotNil(FlexLove.baseScale) luaunit.assertEquals(FlexLove.baseScale.width, 1920) luaunit.assertEquals(FlexLove.baseScale.height, 1080) @@ -69,15 +69,15 @@ function TestFlexLove:testInitWithStringTheme() -- Pre-register a theme local theme = Theme.new({ name = "test", - components = {} + components = {}, }) - + -- init() tries to load and then set active, which may fail if theme path doesn't exist -- Just check that it doesn't crash FlexLove.init({ - theme = "test" + theme = "test", }) - + -- The theme setting may fail silently, so just check it doesn't crash luaunit.assertTrue(true) end @@ -87,19 +87,19 @@ function TestFlexLove:testInitWithTableTheme() FlexLove.init({ theme = { name = "custom", - components = {} - } + components = {}, + }, }) - + luaunit.assertEquals(FlexLove.defaultTheme, "custom") end -- Test: init() with invalid theme (should not crash) function TestFlexLove:testInitWithInvalidTheme() FlexLove.init({ - theme = "nonexistent-theme" + theme = "nonexistent-theme", }) - + -- Should not crash, just print warning luaunit.assertTrue(true) end @@ -107,27 +107,27 @@ end -- Test: init() with immediateMode = true function TestFlexLove:testInitWithImmediateMode() FlexLove.init({ - immediateMode = true + immediateMode = true, }) - + luaunit.assertEquals(FlexLove.getMode(), "immediate") end -- Test: init() with immediateMode = false function TestFlexLove:testInitWithRetainedMode() FlexLove.init({ - immediateMode = false + immediateMode = false, }) - + luaunit.assertEquals(FlexLove.getMode(), "retained") end -- Test: init() with autoFrameManagement function TestFlexLove:testInitWithAutoFrameManagement() FlexLove.init({ - autoFrameManagement = true + autoFrameManagement = true, }) - + luaunit.assertEquals(FlexLove._autoFrameManagement, true) end @@ -135,9 +135,9 @@ end function TestFlexLove:testInitWithStateConfig() FlexLove.init({ stateRetentionFrames = 5, - maxStateEntries = 100 + maxStateEntries = 100, }) - + luaunit.assertTrue(true) -- Should configure StateManager end @@ -151,8 +151,8 @@ end -- Test: setMode() to retained function TestFlexLove:testSetModeRetained() FlexLove.setMode("immediate") -- First set to immediate - FlexLove.setMode("retained") -- Then to retained - + FlexLove.setMode("retained") -- Then to retained + luaunit.assertFalse(FlexLove._immediateMode) luaunit.assertEquals(FlexLove._frameNumber, 0) end @@ -162,7 +162,7 @@ function TestFlexLove:testSetModeInvalid() local success = pcall(function() FlexLove.setMode("invalid") end) - + luaunit.assertFalse(success) end @@ -170,7 +170,7 @@ end function TestFlexLove:testGetMode() FlexLove.setMode("immediate") luaunit.assertEquals(FlexLove.getMode(), "immediate") - + FlexLove.setMode("retained") luaunit.assertEquals(FlexLove.getMode(), "retained") end @@ -179,7 +179,7 @@ end function TestFlexLove:testBeginFrameImmediate() FlexLove.setMode("immediate") FlexLove.beginFrame() - + luaunit.assertTrue(FlexLove._frameStarted) luaunit.assertEquals(#FlexLove._currentFrameElements, 0) end @@ -189,7 +189,7 @@ function TestFlexLove:testBeginFrameRetained() FlexLove.setMode("retained") local frameNumber = FlexLove._frameNumber or 0 FlexLove.beginFrame() - + -- Frame number should not change in retained mode luaunit.assertEquals(FlexLove._frameNumber or 0, frameNumber) end @@ -199,7 +199,7 @@ function TestFlexLove:testEndFrameImmediate() FlexLove.setMode("immediate") FlexLove.beginFrame() FlexLove.endFrame() - + luaunit.assertFalse(FlexLove._frameStarted) end @@ -207,7 +207,7 @@ end function TestFlexLove:testEndFrameRetained() FlexLove.setMode("retained") FlexLove.endFrame() - + luaunit.assertTrue(true) -- Should not error end @@ -215,7 +215,7 @@ end function TestFlexLove:testNewRetainedMode() FlexLove.setMode("retained") local element = FlexLove.new({ width = 100, height = 100 }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.width, 100) luaunit.assertEquals(element.height, 100) @@ -225,33 +225,33 @@ end function TestFlexLove:testNewImmediateMode() FlexLove.setMode("immediate") FlexLove.beginFrame() - + local element = FlexLove.new({ id = "test-element", width = 100, - height = 100 + height = 100, }) - + luaunit.assertNotNil(element) luaunit.assertEquals(element.width, 100) luaunit.assertEquals(element.height, 100) - + FlexLove.endFrame() end -- Test: new() auto-begins frame if not started function TestFlexLove:testNewAutoBeginFrame() FlexLove.setMode("immediate") - + local element = FlexLove.new({ id = "auto-begin-test", width = 50, - height = 50 + height = 50, }) - + luaunit.assertNotNil(element) luaunit.assertTrue(FlexLove._autoBeganFrame) - + FlexLove.endFrame() end @@ -259,12 +259,12 @@ end function TestFlexLove:testNewGeneratesID() FlexLove.setMode("immediate") FlexLove.beginFrame() - + local element = FlexLove.new({ width = 100, height = 100 }) - + luaunit.assertNotNil(element.id) luaunit.assertTrue(element.id ~= "") - + FlexLove.endFrame() end @@ -272,46 +272,47 @@ end function TestFlexLove:testDrawNoArgs() FlexLove.setMode("retained") FlexLove.draw() - + luaunit.assertTrue(true) -- Should not error end -- Test: draw() with gameDrawFunc function TestFlexLove:testDrawWithGameFunc() FlexLove.setMode("retained") - + local called = false FlexLove.draw(function() called = true end) - + luaunit.assertTrue(called) end -- Test: draw() with postDrawFunc function TestFlexLove:testDrawWithPostFunc() FlexLove.setMode("retained") - + local called = false FlexLove.draw(nil, function() called = true end) - + luaunit.assertTrue(called) end -- Test: draw() with both functions function TestFlexLove:testDrawWithBothFuncs() FlexLove.setMode("retained") - + local gameCalled = false local postCalled = false - - FlexLove.draw( - function() gameCalled = true end, - function() postCalled = true end - ) - + + FlexLove.draw(function() + gameCalled = true + end, function() + postCalled = true + end) + luaunit.assertTrue(gameCalled) luaunit.assertTrue(postCalled) end @@ -319,31 +320,31 @@ end -- Test: draw() with elements (no backdrop blur) function TestFlexLove:testDrawWithElements() FlexLove.setMode("retained") - + local element = FlexLove.new({ width = 100, height = 100, - backgroundColor = Color.new(1, 1, 1, 1) + backgroundColor = Color.new(1, 1, 1, 1), }) - + FlexLove.draw() - + luaunit.assertTrue(true) -- Should not error end -- Test: draw() auto-ends frame in immediate mode function TestFlexLove:testDrawAutoEndFrame() FlexLove.setMode("immediate") - + local element = FlexLove.new({ id = "auto-end-test", width = 100, - height = 100 + height = 100, }) - + -- draw() should call endFrame() if _autoBeganFrame is true FlexLove.draw() - + luaunit.assertFalse(FlexLove._autoBeganFrame) end @@ -351,21 +352,21 @@ end function TestFlexLove:testUpdateNoElements() FlexLove.setMode("retained") FlexLove.update(0.016) - + luaunit.assertTrue(true) -- Should not error end -- Test: update() in retained mode with elements function TestFlexLove:testUpdateRetainedMode() FlexLove.setMode("retained") - + local element = FlexLove.new({ width = 100, - height = 100 + height = 100, }) - + FlexLove.update(0.016) - + luaunit.assertTrue(true) end @@ -373,16 +374,16 @@ end function TestFlexLove:testUpdateImmediateMode() FlexLove.setMode("immediate") FlexLove.beginFrame() - + local element = FlexLove.new({ id = "update-test", width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.update(0.016) - + luaunit.assertTrue(true) end @@ -390,7 +391,7 @@ end function TestFlexLove:testResizeNoBaseScale() FlexLove.setMode("retained") FlexLove.resize() - + luaunit.assertTrue(true) -- Should not error end @@ -399,40 +400,40 @@ function TestFlexLove:testResizeWithBaseScale() FlexLove.init({ baseScale = { width = 1920, - height = 1080 - } + height = 1080, + }, }) - + FlexLove.resize() - + luaunit.assertNotNil(FlexLove.scaleFactors) end -- Test: resize() with elements function TestFlexLove:testResizeWithElements() FlexLove.setMode("retained") - + local element = FlexLove.new({ width = 100, - height = 100 + height = 100, }) - + FlexLove.resize() - + luaunit.assertTrue(true) end -- Test: destroy() clears all elements function TestFlexLove:testDestroy() FlexLove.setMode("retained") - + local element = FlexLove.new({ width = 100, - height = 100 + height = 100, }) - + FlexLove.destroy() - + luaunit.assertEquals(#FlexLove.topElements, 0) luaunit.assertNil(FlexLove.baseScale) luaunit.assertNil(FlexLove._focusedElement) @@ -442,23 +443,23 @@ end function TestFlexLove:testTextInputNoFocus() FlexLove.setMode("retained") FlexLove.textinput("a") - + luaunit.assertTrue(true) -- Should not error end -- Test: textinput() with focused element function TestFlexLove:testTextInputWithFocus() FlexLove.setMode("retained") - + local element = FlexLove.new({ width = 100, height = 100, - editable = true + editable = true, }) - + FlexLove._focusedElement = element FlexLove.textinput("a") - + luaunit.assertTrue(true) end @@ -466,23 +467,23 @@ end function TestFlexLove:testKeyPressedNoFocus() FlexLove.setMode("retained") FlexLove.keypressed("return", "return", false) - + luaunit.assertTrue(true) -- Should not error end -- Test: keypressed() with focused element function TestFlexLove:testKeyPressedWithFocus() FlexLove.setMode("retained") - + local element = FlexLove.new({ width = 100, height = 100, - editable = true + editable = true, }) - + FlexLove._focusedElement = element FlexLove.keypressed("return", "return", false) - + luaunit.assertTrue(true) end @@ -490,7 +491,7 @@ end function TestFlexLove:testWheelMovedRetainedNoElements() FlexLove.setMode("retained") FlexLove.wheelmoved(0, 1) - + luaunit.assertTrue(true) -- Should not error end @@ -498,16 +499,16 @@ end function TestFlexLove:testWheelMovedImmediate() FlexLove.setMode("immediate") FlexLove.beginFrame() - + local element = FlexLove.new({ id = "wheel-test", width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() FlexLove.wheelmoved(0, 1) - + luaunit.assertTrue(true) end @@ -515,7 +516,7 @@ end function TestFlexLove:testGetStateCountRetained() FlexLove.setMode("retained") local count = FlexLove.getStateCount() - + luaunit.assertEquals(count, 0) end @@ -523,15 +524,15 @@ end function TestFlexLove:testGetStateCountImmediate() FlexLove.setMode("immediate") FlexLove.beginFrame() - + local element = FlexLove.new({ id = "state-test", width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() - + local count = FlexLove.getStateCount() luaunit.assertTrue(count >= 0) end @@ -540,7 +541,7 @@ end function TestFlexLove:testClearStateRetained() FlexLove.setMode("retained") FlexLove.clearState("test-id") - + luaunit.assertTrue(true) end @@ -548,15 +549,15 @@ end function TestFlexLove:testClearStateImmediate() FlexLove.setMode("immediate") FlexLove.beginFrame() - + local element = FlexLove.new({ id = "clear-test", width = 100, - height = 100 + height = 100, }) - + FlexLove.endFrame() - + FlexLove.clearState("clear-test") luaunit.assertTrue(true) end @@ -565,7 +566,7 @@ end function TestFlexLove:testClearAllStatesRetained() FlexLove.setMode("retained") FlexLove.clearAllStates() - + luaunit.assertTrue(true) end @@ -574,7 +575,7 @@ function TestFlexLove:testClearAllStatesImmediate() FlexLove.setMode("immediate") FlexLove.beginFrame() FlexLove.endFrame() - + FlexLove.clearAllStates() luaunit.assertTrue(true) end @@ -583,7 +584,7 @@ end function TestFlexLove:testGetStateStatsRetained() FlexLove.setMode("retained") local stats = FlexLove.getStateStats() - + luaunit.assertEquals(stats.stateCount, 0) luaunit.assertEquals(stats.frameNumber, 0) end @@ -593,7 +594,7 @@ function TestFlexLove:testGetStateStatsImmediate() FlexLove.setMode("immediate") FlexLove.beginFrame() FlexLove.endFrame() - + local stats = FlexLove.getStateStats() luaunit.assertNotNil(stats) end @@ -602,22 +603,22 @@ end function TestFlexLove:testGetElementAtPositionNoElements() FlexLove.setMode("retained") local element = FlexLove.getElementAtPosition(50, 50) - + luaunit.assertNil(element) end -- Test: getElementAtPosition() with element at position function TestFlexLove:testGetElementAtPosition() FlexLove.setMode("retained") - + local element = FlexLove.new({ x = 0, y = 0, width = 100, height = 100, - onEvent = function() end + onEvent = function() end, }) - + local found = FlexLove.getElementAtPosition(50, 50) luaunit.assertEquals(found, element) end @@ -625,15 +626,15 @@ end -- Test: getElementAtPosition() outside element bounds function TestFlexLove:testGetElementAtPositionOutside() FlexLove.setMode("retained") - + local element = FlexLove.new({ x = 0, y = 0, width = 100, height = 100, - onEvent = function() end + onEvent = function() end, }) - + local found = FlexLove.getElementAtPosition(200, 200) luaunit.assertNil(found) end @@ -653,4 +654,586 @@ function TestFlexLove:testEnumsAccessible() luaunit.assertNotNil(FlexLove.enums.AlignItems) 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 diff --git a/testing/__tests__/image_renderer_test.lua b/testing/__tests__/image_renderer_test.lua index b6e7e1a..160f890 100644 --- a/testing/__tests__/image_renderer_test.lua +++ b/testing/__tests__/image_renderer_test.lua @@ -65,9 +65,12 @@ function TestImageRenderer:testCalculateFitWithNegativeBoundsHeight() end function TestImageRenderer:testCalculateFitWithInvalidFitMode() - luaunit.assertError(function() - ImageRenderer.calculateFit(100, 100, 200, 200, "invalid-mode") - end) + -- Now uses 'fill' fallback with warning instead of error + local result = ImageRenderer.calculateFit(100, 100, 200, 200, "invalid-mode") + 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 function TestImageRenderer:testCalculateFitWithNilFitMode() @@ -208,9 +211,10 @@ function TestImageRenderer:testDrawWithOpacityGreaterThanOne() end function TestImageRenderer:testDrawWithInvalidFitMode() - luaunit.assertError(function() - ImageRenderer.draw(self.mockImage, 0, 0, 100, 100, "invalid") - end) + -- 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") + luaunit.assertTrue(true) -- If we reach here, no error was thrown end function TestImageRenderer:testCalculateFitWithVerySmallBounds() diff --git a/testing/__tests__/image_scaler_test.lua b/testing/__tests__/image_scaler_test.lua index f1a48dc..38fadbf 100644 --- a/testing/__tests__/image_scaler_test.lua +++ b/testing/__tests__/image_scaler_test.lua @@ -24,51 +24,67 @@ function TestImageScaler:testScaleNearestWithNilSource() end function TestImageScaler:testScaleNearestWithZeroSourceWidth() - luaunit.assertError(function() - ImageScaler.scaleNearest(self.mockImageData, 0, 0, 0, 10, 20, 20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 0, 10, 20, 20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleNearestWithZeroSourceHeight() - luaunit.assertError(function() - ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 0, 20, 20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 0, 20, 20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleNearestWithNegativeSourceWidth() - luaunit.assertError(function() - ImageScaler.scaleNearest(self.mockImageData, 0, 0, -10, 10, 20, 20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, -10, 10, 20, 20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleNearestWithNegativeSourceHeight() - luaunit.assertError(function() - ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, -10, 20, 20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, -10, 20, 20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleNearestWithZeroDestWidth() - luaunit.assertError(function() - ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 0, 20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 0, 20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleNearestWithZeroDestHeight() - luaunit.assertError(function() - ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, 0) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, 0) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleNearestWithNegativeDestWidth() - luaunit.assertError(function() - ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, -20, 20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, -20, 20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleNearestWithNegativeDestHeight() - luaunit.assertError(function() - ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, -20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleNearest(self.mockImageData, 0, 0, 10, 10, 20, -20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end -- Unhappy path tests for scaleBilinear @@ -80,51 +96,67 @@ function TestImageScaler:testScaleBilinearWithNilSource() end function TestImageScaler:testScaleBilinearWithZeroSourceWidth() - luaunit.assertError(function() - ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 0, 10, 20, 20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 0, 10, 20, 20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleBilinearWithZeroSourceHeight() - luaunit.assertError(function() - ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 0, 20, 20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 0, 20, 20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleBilinearWithNegativeSourceWidth() - luaunit.assertError(function() - ImageScaler.scaleBilinear(self.mockImageData, 0, 0, -10, 10, 20, 20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, -10, 10, 20, 20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleBilinearWithNegativeSourceHeight() - luaunit.assertError(function() - ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, -10, 20, 20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, -10, 20, 20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleBilinearWithZeroDestWidth() - luaunit.assertError(function() - ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 0, 20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 0, 20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleBilinearWithZeroDestHeight() - luaunit.assertError(function() - ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, 0) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, 0) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleBilinearWithNegativeDestWidth() - luaunit.assertError(function() - ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, -20, 20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, -20, 20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end function TestImageScaler:testScaleBilinearWithNegativeDestHeight() - luaunit.assertError(function() - ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, -20) - end) + -- Now returns 1x1 transparent fallback with warning instead of error + local result = ImageScaler.scaleBilinear(self.mockImageData, 0, 0, 10, 10, 20, -20) + luaunit.assertNotNil(result) + luaunit.assertEquals(result:getWidth(), 1) + luaunit.assertEquals(result:getHeight(), 1) end -- Edge case tests diff --git a/testing/__tests__/renderer_test.lua b/testing/__tests__/renderer_test.lua index 406ed25..f981076 100644 --- a/testing/__tests__/renderer_test.lua +++ b/testing/__tests__/renderer_test.lua @@ -161,22 +161,24 @@ end -- Test: new() with imagePath (successful load via cache) function TestRenderer:testNewWithImagePathSuccessfulLoad() local mockImage = { - getDimensions = function() return 50, 50 end + getDimensions = function() + return 50, 50 + end, } - + -- Pre-populate the cache so load succeeds ImageCache._cache["test/image.png"] = { image = mockImage, - imageData = nil + imageData = nil, } - + local renderer = Renderer.new({ imagePath = "test/image.png", }, createDeps()) luaunit.assertEquals(renderer.imagePath, "test/image.png") luaunit.assertEquals(renderer._loadedImage, mockImage) - + -- Clean up cache ImageCache._cache["test/image.png"] = nil end diff --git a/testing/__tests__/units_test.lua b/testing/__tests__/units_test.lua index 3ecdd3e..4dc74cc 100644 --- a/testing/__tests__/units_test.lua +++ b/testing/__tests__/units_test.lua @@ -89,20 +89,33 @@ function TestUnitsParse:testParseZero() end 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 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 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 function TestUnitsParse:testParseWithSpace() -- 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 -- Test suite for Units.resolve() @@ -369,11 +382,17 @@ function TestUnitsEdgeCases:testResolveZeroParentSize() end 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 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 function TestUnitsEdgeCases:testResolveNegativePercentage()