Skip to content

Commit efc496c

Browse files
committed
Recover subject name when called with pcall to get proper error messages
1 parent e9d4e48 commit efc496c

5 files changed

Lines changed: 160 additions & 6 deletions

File tree

lua/gluatest/expectations/negative.lua

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ local IsValid = IsValid
44
local isstring = isstring
55
local string_format = string.format
66
local GetDiff = include( "utils/table_diff.lua" )
7+
local DoPCallWithSubject = include( "utils/caller_recovery.lua" )
78

89
-- Inverse checks
910
return function( subject, ... )
@@ -178,7 +179,7 @@ return function( subject, ... )
178179
function expectations.succeed()
179180
assert( TypeID( subject ) == TYPE_FUNCTION, ".succeed expects a function" )
180181

181-
local success = pcall( subject, unpack( args ) )
182+
local success = DoPCallWithSubject( subject, unpack( args ) )
182183

183184
if success ~= false then
184185
i.expected( "to not succeed" )
@@ -189,7 +190,7 @@ return function( subject, ... )
189190
function expectations.err()
190191
assert( TypeID( subject ) == TYPE_FUNCTION, ".err expects a function" )
191192

192-
local success = pcall( subject, unpack( args ) )
193+
local success = DoPCallWithSubject( subject, unpack( args ) )
193194

194195
if success ~= true then
195196
i.expected( "to not error" )
@@ -202,7 +203,7 @@ return function( subject, ... )
202203
assert( TypeID( subject ) == TYPE_FUNCTION, ".errWith expects a function" )
203204
assert( isstring( comparison ), "errWith expects a string" )
204205

205-
local success, err = pcall( subject, unpack( args ) )
206+
local success, err = DoPCallWithSubject( subject, unpack( args ) )
206207

207208
if success == true then
208209
i.expected( "to error" )

lua/gluatest/expectations/positive.lua

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ local IsValid = IsValid
44
local isstring = isstring
55
local string_format = string.format
66
local GetDiff = include( "utils/table_diff.lua" )
7+
local DoPCallWithSubject = include( "utils/caller_recovery.lua" )
78

89
-- Positive checks
910
return function( subject, ... )
@@ -177,7 +178,7 @@ return function( subject, ... )
177178
function expectations.succeed()
178179
assert( TypeID( subject ) == TYPE_FUNCTION, ".succeed expects a function" )
179180

180-
local success, err = pcall( subject, unpack( args ) )
181+
local success, err = DoPCallWithSubject( subject, unpack( args ) )
181182

182183
if success == false then
183184
i.expected( "to succeed, got: %s", err )
@@ -188,7 +189,7 @@ return function( subject, ... )
188189
function expectations.err()
189190
assert( TypeID( subject ) == TYPE_FUNCTION, ".err expects a function" )
190191

191-
local success = pcall( subject, unpack( args ) )
192+
local success = DoPCallWithSubject( subject, unpack( args ) )
192193

193194
if success == true then
194195
i.expected( "to error" )
@@ -201,7 +202,7 @@ return function( subject, ... )
201202
assert( TypeID( subject ) == TYPE_FUNCTION, ".errWith expects a function" )
202203
assert( TypeID( comparison ) == TYPE_STRING, ".errWith expects a string" )
203204

204-
local success, err = pcall( subject, unpack( args ) )
205+
local success, err = DoPCallWithSubject( subject, unpack( args ) )
205206

206207
if success == true then
207208
i.expected( "to error with '%s'", comparison )
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
-- As the name implies, finds the last occurrence of the given needle
2+
local function findLast( haystack, needle )
3+
local lastPos = string.find( haystack, needle, nil, true )
4+
local nextPos = lastPos and string.find( haystack, needle, lastPos + 1, true )
5+
while nextPos ~= nil do
6+
lastPos = nextPos
7+
nextPos = string.find( haystack, needle, lastPos + 1, true )
8+
end
9+
10+
return lastPos
11+
end
12+
13+
-- Removes ( and ) without breaking stuff hopefully
14+
-- This means you can do crazy stuff like expect( test123(test456(test789(123))) ) which will be stripped to just test123
15+
local function removeBraces( line )
16+
local findStartPos = findLast( line, "(" )
17+
while findStartPos do
18+
local findEndPos = string.find( line, ")", findStartPos + 1 )
19+
if not findEndPos then break end
20+
21+
line = string.Trim( line:sub( 0, findStartPos - 1 ) .. line:sub( findEndPos + 1 ) )
22+
23+
findStartPos = findLast( line, "(" )
24+
end
25+
26+
return line
27+
end
28+
29+
30+
--[[
31+
Name recovery function for expect( test.123 )
32+
to retrieve 123 so that functions like errWith will work properly if given directly the function that will error
33+
34+
This is mainly required due to Lua losing track of the caller function resulting in '?' inside an error message
35+
you could always wrap it like this to workaround this issue: local testFunc = function(...) test.123(...) end
36+
Using which this entire stuff would not be necessary, as then the function you pass to pcall will not be the one erroring.
37+
and since you call your function inside of it, Lua still knows the name of it, causing errWith to work properly.
38+
]]
39+
local function GetExpectationSubjectName( additionalShift )
40+
additionalShift = additionalShift or 0
41+
-- We gotta figure out the caller name as its lost when we get called as it isn't kept track off
42+
local ourDebugInfo = debug.getinfo( 2 + additionalShift, "n" ) -- Makes copy-pasting this easier (also allows us to move this somewhere else later)
43+
local callerDebugInfo = debug.getinfo( 3 + additionalShift, "Sln" ) -- 3 = caller, 2 = expect.errWith or such, 1 = us
44+
if callerDebugInfo and ourDebugInfo and ourDebugInfo.name ~= "" and callerDebugInfo.currentline ~= 1 then
45+
-- ToDo: Cache the file content to reduce filesystem usage (Could quickly escalate, slowing everything down immensely)
46+
local fileHandle = file.Open( callerDebugInfo.short_src, "rb", "GAME" ) --[[@as File]]
47+
if fileHandle then
48+
-- Skipping to our current line
49+
for _ = 1, callerDebugInfo.currentline - 1 do
50+
fileHandle:ReadLine()
51+
end
52+
local line = string.Trim( fileHandle:ReadLine() )
53+
fileHandle:Close() -- No longer need our handle
54+
55+
-- Remove expected
56+
local _, findPos = string.find( line, "expect" )
57+
if findPos then
58+
line = string.Trim( line:sub( findPos + 1 ) )
59+
end
60+
61+
-- Remove everything after "errWith" including itself
62+
findPos = string.find( line, ourDebugInfo.name )
63+
if findPos then
64+
line = string.Trim( line:sub( 0, findPos - 1 ) )
65+
end
66+
67+
-- First remove the left over . from ".errWith" as we only removed errWith
68+
findPos = findLast( line, "." )
69+
if findPos then
70+
line = string.Trim( line:sub( 0, findPos - 1 ) )
71+
end
72+
73+
-- Now remove the ".to"
74+
findPos = findLast( line, "." )
75+
if findPos then
76+
line = string.Trim( line:sub( 0, findPos - 1 ) )
77+
end
78+
79+
-- Remove leftover "(" wrapping
80+
if line:StartsWith( "(" ) then
81+
line = string.Trim( line:sub( 2 ) )
82+
end
83+
84+
-- Remove leftover ")" wrapping
85+
if line:EndsWith( ")" ) then
86+
line = string.Trim( line:sub( 0, line:len() - 1 ) )
87+
end
88+
89+
line = removeBraces( line )
90+
91+
-- Now, finally remove the the rest in case it had arguments like expect( test123, 123 )
92+
findPos = string.find( line, "," )
93+
if findPos then
94+
line = string.Trim( line:sub( 0, findPos - 1 ) )
95+
end
96+
97+
findPos = findLast( line, "." )
98+
if findPos then
99+
line = string.Trim( line:sub( findPos + 1 ) )
100+
end
101+
102+
findPos = findLast( line, ":" )
103+
if findPos then
104+
line = string.Trim( line:sub( findPos + 1 ) )
105+
end
106+
107+
return line
108+
end
109+
end
110+
111+
return "?"
112+
end
113+
114+
local function DoPCallWithSubject( subject, ... )
115+
local callerName = GetExpectationSubjectName( 1 ) -- 1 since this function also shifts things again
116+
117+
__GLUA_RUN = {} -- To not conflict and to avoid setting up an entire fenv
118+
local callSubject = callerName ~= "?" and CompileString( [[__GLUA_RUN.]] .. callerName .. [[( ... )]], "", false ) or subject
119+
-- Instead of doing __GLUA_RUN_callerName, we do __GLUA_RUN.callerName
120+
-- so that the name won't be modified in the resulting error messsage as it ignores the table name
121+
__GLUA_RUN[callerName] = subject
122+
123+
local success, err = pcall( callSubject, ... )
124+
__GLUA_RUN = nil -- Cleanup :^
125+
126+
-- This can exist because of the use of CompileString, so nuke it
127+
if err and err:StartsWith( ":1: " ) then
128+
err = err:sub( 5 )
129+
end
130+
131+
return success, err
132+
end
133+
134+
return DoPCallWithSubject

lua/gluatest/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ GLuaTest = {
6060

6161
-- /expectations/utils/*
6262
AddCSLuaFile( "gluatest/expectations/utils/table_diff.lua" )
63+
AddCSLuaFile( "gluatest/expectations/utils/caller_recovery.lua" )
6364

6465
-- When the server finishes its tests, notify clients to start theirs
6566
-- While it is possible to run client and server tests in parallel, that may lead to
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---@diagnostic disable: param-type-mismatch
2+
return {
3+
groupName = "callerRecovery",
4+
cases = {
5+
{
6+
name = "Called function name is included in error message",
7+
func = function()
8+
-- This never worked, instead of 'Left' it had given '?'
9+
expect( string.Left, nil ).to.errWith( "bad argument #1 to 'Left' (string expected, got nil)" )
10+
11+
-- This is how it always had worked fine
12+
local testFunc = function() string.Left( nil, nil ) end
13+
expect( testFunc ).to.errWith( "bad argument #1 to 'Left' (string expected, got nil)" )
14+
end
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)