From 75841f5b1ada823966cbd5598df85a9478516bd2 Mon Sep 17 00:00:00 2001 From: RaphaelIT7 <64648134+RaphaelIT7@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:16:29 +0100 Subject: [PATCH] Recover subject name when called with pcall to get proper error messages modified: utils/caller_recovery.lua modified: ../../tests/gluatest/expectations/callerRecovery.lua --- lua/gluatest/expectations/negative.lua | 7 +- lua/gluatest/expectations/positive.lua | 7 +- .../expectations/utils/caller_recovery.lua | 134 ++++++++++++++++++ .../gluatest/expectations/callerRecovery.lua | 17 +++ 4 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 lua/gluatest/expectations/utils/caller_recovery.lua create mode 100644 lua/tests/gluatest/expectations/callerRecovery.lua diff --git a/lua/gluatest/expectations/negative.lua b/lua/gluatest/expectations/negative.lua index b5a938f5..7a11824c 100644 --- a/lua/gluatest/expectations/negative.lua +++ b/lua/gluatest/expectations/negative.lua @@ -4,6 +4,7 @@ local IsValid = IsValid local isstring = isstring local string_format = string.format local GetDiff = include( "utils/table_diff.lua" ) +local DoPCallWithSubject = include( "utils/caller_recovery.lua" ) -- Inverse checks return function( subject, ... ) @@ -178,7 +179,7 @@ return function( subject, ... ) function expectations.succeed() assert( TypeID( subject ) == TYPE_FUNCTION, ".succeed expects a function" ) - local success = pcall( subject, unpack( args ) ) + local success = DoPCallWithSubject( subject, unpack( args ) ) if success ~= false then i.expected( "to not succeed" ) @@ -189,7 +190,7 @@ return function( subject, ... ) function expectations.err() assert( TypeID( subject ) == TYPE_FUNCTION, ".err expects a function" ) - local success = pcall( subject, unpack( args ) ) + local success = DoPCallWithSubject( subject, unpack( args ) ) if success ~= true then i.expected( "to not error" ) @@ -202,7 +203,7 @@ return function( subject, ... ) assert( TypeID( subject ) == TYPE_FUNCTION, ".errWith expects a function" ) assert( isstring( comparison ), "errWith expects a string" ) - local success, err = pcall( subject, unpack( args ) ) + local success, err = DoPCallWithSubject( subject, unpack( args ) ) if success == true then i.expected( "to error" ) diff --git a/lua/gluatest/expectations/positive.lua b/lua/gluatest/expectations/positive.lua index 92464d9b..679d8a09 100644 --- a/lua/gluatest/expectations/positive.lua +++ b/lua/gluatest/expectations/positive.lua @@ -4,6 +4,7 @@ local IsValid = IsValid local isstring = isstring local string_format = string.format local GetDiff = include( "utils/table_diff.lua" ) +local DoPCallWithSubject = include( "utils/caller_recovery.lua" ) -- Positive checks return function( subject, ... ) @@ -177,7 +178,7 @@ return function( subject, ... ) function expectations.succeed() assert( TypeID( subject ) == TYPE_FUNCTION, ".succeed expects a function" ) - local success, err = pcall( subject, unpack( args ) ) + local success, err = DoPCallWithSubject( subject, unpack( args ) ) if success == false then i.expected( "to succeed, got: %s", err ) @@ -188,7 +189,7 @@ return function( subject, ... ) function expectations.err() assert( TypeID( subject ) == TYPE_FUNCTION, ".err expects a function" ) - local success = pcall( subject, unpack( args ) ) + local success = DoPCallWithSubject( subject, unpack( args ) ) if success == true then i.expected( "to error" ) @@ -201,7 +202,7 @@ return function( subject, ... ) assert( TypeID( subject ) == TYPE_FUNCTION, ".errWith expects a function" ) assert( TypeID( comparison ) == TYPE_STRING, ".errWith expects a string" ) - local success, err = pcall( subject, unpack( args ) ) + local success, err = DoPCallWithSubject( subject, unpack( args ) ) if success == true then i.expected( "to error with '%s'", comparison ) diff --git a/lua/gluatest/expectations/utils/caller_recovery.lua b/lua/gluatest/expectations/utils/caller_recovery.lua new file mode 100644 index 00000000..acaef0f6 --- /dev/null +++ b/lua/gluatest/expectations/utils/caller_recovery.lua @@ -0,0 +1,134 @@ +-- As the name implies, finds the last occurrence of the given needle +local function findLast( haystack, needle ) + local lastPos = string.find( haystack, needle, nil, true ) + local nextPos = lastPos and string.find( haystack, needle, lastPos + 1, true ) + while nextPos ~= nil do + lastPos = nextPos + nextPos = string.find( haystack, needle, lastPos + 1, true ) + end + + return lastPos +end + +-- Removes ( and ) without breaking stuff hopefully +-- This means you can do crazy stuff like expect( test123(test456(test789(123))) ) which will be stripped to just test123 +local function removeBraces( line ) + local findStartPos = findLast( line, "(" ) + while findStartPos do + local findEndPos = string.find( line, ")", findStartPos + 1 ) + if not findEndPos then break end + + line = string.Trim( line:sub( 0, findStartPos - 1 ) .. line:sub( findEndPos + 1 ) ) + + findStartPos = findLast( line, "(" ) + end + + return line +end + + +--[[ + Name recovery function for expect( test.123 ) + to retrieve 123 so that functions like errWith will work properly if given directly the function that will error + + This is mainly required due to Lua losing track of the caller function resulting in '?' inside an error message + you could always wrap it like this to workaround this issue: local testFunc = function(...) test.123(...) end + Using which this entire stuff would not be necessary, as then the function you pass to pcall will not be the one erroring. + and since you call your function inside of it, Lua still knows the name of it, causing errWith to work properly. +]] +local function GetExpectationSubjectName( additionalShift ) + additionalShift = additionalShift or 0 + -- We gotta figure out the caller name as its lost when we get called as it isn't kept track off + local ourDebugInfo = debug.getinfo( 2 + additionalShift, "n" ) -- Makes copy-pasting this easier (also allows us to move this somewhere else later) + local callerDebugInfo = debug.getinfo( 3 + additionalShift, "Sln" ) -- 3 = caller, 2 = expect.errWith or such, 1 = us + if callerDebugInfo and ourDebugInfo and ourDebugInfo.name ~= "" and callerDebugInfo.currentline ~= 1 then + -- ToDo: Cache the file content to reduce filesystem usage (Could quickly escalate, slowing everything down immensely) + local fileHandle = file.Open( callerDebugInfo.short_src, "rb", "GAME" ) --[[@as File]] + if fileHandle then + -- Skipping to our current line + for _ = 1, callerDebugInfo.currentline - 1 do + fileHandle:ReadLine() + end + local line = string.Trim( fileHandle:ReadLine() ) + fileHandle:Close() -- No longer need our handle + + -- Remove expected + local _, findPos = string.find( line, "expect" ) + if findPos then + line = string.Trim( line:sub( findPos + 1 ) ) + end + + -- Remove everything after "errWith" including itself + findPos = string.find( line, ourDebugInfo.name ) + if findPos then + line = string.Trim( line:sub( 0, findPos - 1 ) ) + end + + -- First remove the left over . from ".errWith" as we only removed errWith + findPos = findLast( line, "." ) + if findPos then + line = string.Trim( line:sub( 0, findPos - 1 ) ) + end + + -- Now remove the ".to" + findPos = findLast( line, "." ) + if findPos then + line = string.Trim( line:sub( 0, findPos - 1 ) ) + end + + -- Remove leftover "(" wrapping + if line:StartsWith( "(" ) then + line = string.Trim( line:sub( 2 ) ) + end + + -- Remove leftover ")" wrapping + if line:EndsWith( ")" ) then + line = string.Trim( line:sub( 0, line:len() - 1 ) ) + end + + line = removeBraces( line ) + + -- Now, finally remove the the rest in case it had arguments like expect( test123, 123 ) + findPos = string.find( line, "," ) + if findPos then + line = string.Trim( line:sub( 0, findPos - 1 ) ) + end + + findPos = findLast( line, "." ) + if findPos then + line = string.Trim( line:sub( findPos + 1 ) ) + end + + findPos = findLast( line, ":" ) + if findPos then + line = string.Trim( line:sub( findPos + 1 ) ) + end + + return line + end + end + + return "?" +end + +local function DoPCallWithSubject( subject, ... ) + local callerName = GetExpectationSubjectName( 1 ) -- 1 since this function also shifts things again + + __GLUA_RUN = {} -- To not conflict and to avoid setting up an entire fenv + local callSubject = callerName ~= "?" and CompileString( [[__GLUA_RUN.]] .. callerName .. [[( ... )]], "", false ) or subject + -- Instead of doing __GLUA_RUN_callerName, we do __GLUA_RUN.callerName + -- so that the name won't be modified in the resulting error messsage as it ignores the table name + __GLUA_RUN[callerName] = subject + + local success, err = pcall( callSubject, ... ) + __GLUA_RUN = nil -- Cleanup :^ + + -- This can exist because of the use of CompileString, so nuke it + if err and err:StartsWith( ":1: " ) then + err = err:sub( 5 ) + end + + return success, err +end + +return DoPCallWithSubject \ No newline at end of file diff --git a/lua/tests/gluatest/expectations/callerRecovery.lua b/lua/tests/gluatest/expectations/callerRecovery.lua new file mode 100644 index 00000000..4b19615b --- /dev/null +++ b/lua/tests/gluatest/expectations/callerRecovery.lua @@ -0,0 +1,17 @@ +---@diagnostic disable: param-type-mismatch +return { + groupName = "callerRecovery", + cases = { + { + name = "Called function name is included in error message", + func = function() + -- This never worked, instead of 'Left' it had given '?' + expect( string.Left, nil ).to.errWith( "bad argument #1 to 'Left' (string expected, got nil)" ) + + -- This is how it always had worked fine + local testFunc = function() string.Left( nil, nil ) end + expect( testFunc ).to.errWith( "bad argument #1 to 'Left' (string expected, got nil)" ) + end + } + } +}