From 5517cfb9a1fc117dae8eb4935b4289568b5128c5 Mon Sep 17 00:00:00 2001 From: Cisphyx Date: Thu, 26 Dec 2024 11:30:24 -0500 Subject: [PATCH] Add line number information to exceptions raised during function calls (#4038) --- changes/e25248c44d5ba33a43ae937a0bb25fd5.yaml | 6 +++ synapse/lib/ast.py | 34 ++++++++++----- synapse/lib/stormtypes.py | 19 +++++++++ synapse/tests/test_lib_agenda.py | 4 +- synapse/tests/test_lib_ast.py | 42 +++++++++++++++++++ 5 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 changes/e25248c44d5ba33a43ae937a0bb25fd5.yaml diff --git a/changes/e25248c44d5ba33a43ae937a0bb25fd5.yaml b/changes/e25248c44d5ba33a43ae937a0bb25fd5.yaml new file mode 100644 index 00000000000..1019c9ead55 --- /dev/null +++ b/changes/e25248c44d5ba33a43ae937a0bb25fd5.yaml @@ -0,0 +1,6 @@ +--- +desc: Fixed an issue where certain exceptions raised while calling a function in Storm + were not providing appropriate details about the origin of the exception. +prs: [] +type: bug +... diff --git a/synapse/lib/ast.py b/synapse/lib/ast.py index bf85543c01a..a934853308b 100644 --- a/synapse/lib/ast.py +++ b/synapse/lib/ast.py @@ -3579,10 +3579,23 @@ async def compute(self, runt, path): kwargs = {k: v for (k, v) in await self.kids[2].compute(runt, path)} with s_scope.enter({'runt': runt}): - retn = func(*argv, **kwargs) - if s_coro.iscoro(retn): - return await retn - return retn + try: + retn = func(*argv, **kwargs) + if s_coro.iscoro(retn): + return await retn + return retn + + except TypeError as e: + mesg = str(e) + if (funcpath := getattr(func, '_storm_funcpath', None)) is not None: + mesg = f"{funcpath}(){mesg.split(')', 1)[1]}" + + raise self.addExcInfo(s_exc.StormRuntimeError(mesg=mesg)) + + except s_exc.SynErr as e: + if getattr(func, '_storm_runtime_lib_func', None) is not None: + e.errinfo.pop('highlight', None) + raise self.addExcInfo(e) class DollarExpr(Value): ''' @@ -4891,8 +4904,9 @@ async def once(): @s_stormtypes.stormfunc(readonly=True) async def realfunc(*args, **kwargs): - return await self.callfunc(runt, argdefs, args, kwargs) + return await self.callfunc(runt, argdefs, args, kwargs, realfunc._storm_funcpath) + realfunc._storm_funcpath = self.name await runt.setVar(self.name, realfunc) count = 0 @@ -4914,7 +4928,7 @@ def validate(self, runt): # var scope validation occurs in the sub-runtime pass - async def callfunc(self, runt, argdefs, args, kwargs): + async def callfunc(self, runt, argdefs, args, kwargs, funcpath): ''' Execute a function call using the given runtime. @@ -4925,7 +4939,7 @@ async def callfunc(self, runt, argdefs, args, kwargs): argcount = len(args) + len(kwargs) if argcount > len(argdefs): - mesg = f'{self.name}() takes {len(argdefs)} arguments but {argcount} were provided' + mesg = f'{funcpath}() takes {len(argdefs)} arguments but {argcount} were provided' raise self.kids[1].addExcInfo(s_exc.StormRuntimeError(mesg=mesg)) # Fill in the positional arguments @@ -4939,7 +4953,7 @@ async def callfunc(self, runt, argdefs, args, kwargs): valu = kwargs.pop(name, s_common.novalu) if valu is s_common.novalu: if defv is s_common.novalu: - mesg = f'{self.name}() missing required argument {name}' + mesg = f'{funcpath}() missing required argument {name}' raise self.kids[1].addExcInfo(s_exc.StormRuntimeError(mesg=mesg)) valu = defv @@ -4950,11 +4964,11 @@ async def callfunc(self, runt, argdefs, args, kwargs): # used a kwarg not defined. kwkeys = list(kwargs.keys()) if kwkeys[0] in posnames: - mesg = f'{self.name}() got multiple values for parameter {kwkeys[0]}' + mesg = f'{funcpath}() got multiple values for parameter {kwkeys[0]}' raise self.kids[1].addExcInfo(s_exc.StormRuntimeError(mesg=mesg)) plural = 's' if len(kwargs) > 1 else '' - mesg = f'{self.name}() got unexpected keyword argument{plural}: {",".join(kwkeys)}' + mesg = f'{funcpath}() got unexpected keyword argument{plural}: {",".join(kwkeys)}' raise self.kids[1].addExcInfo(s_exc.StormRuntimeError(mesg=mesg)) assert len(mergargs) == len(argdefs) diff --git a/synapse/lib/stormtypes.py b/synapse/lib/stormtypes.py index e9435f0bb99..d8987e05650 100644 --- a/synapse/lib/stormtypes.py +++ b/synapse/lib/stormtypes.py @@ -201,11 +201,29 @@ def registerLib(self, ctor): raise Exception('no key!') self.addStormLib(path, ctor) + for info in ctor._storm_locals: + rtype = info.get('type') + if isinstance(rtype, dict) and rtype.get('type') == 'function': + if (fname := rtype.get('_funcname')) == '_storm_query': + continue + + if (func := getattr(ctor, fname, None)) is not None: + funcpath = '.'.join(('lib',) + ctor._storm_lib_path + (info['name'],)) + func._storm_funcpath = f"${funcpath}" + return ctor def registerType(self, ctor): '''Decorator to register a StormPrim''' self.addStormType(ctor.__name__, ctor) + + for info in ctor._storm_locals: + rtype = info.get('type') + if isinstance(rtype, dict) and rtype.get('type') == 'function': + fname = rtype.get('_funcname') + if (func := getattr(ctor, fname, None)) is not None: + func._storm_funcpath = f"{ctor._storm_typename}.{info['name']}" + return ctor def iterLibs(self): @@ -628,6 +646,7 @@ async def initLibAsync(self): if callable(v) and v.__name__ == 'realfunc': v._storm_runtime_lib = self v._storm_runtime_lib_func = k + v._storm_funcpath = f'${".".join(("lib",) + self.name + (k,))}' self.locls[k] = v diff --git a/synapse/tests/test_lib_agenda.py b/synapse/tests/test_lib_agenda.py index ed5934c77ae..285b793610e 100644 --- a/synapse/tests/test_lib_agenda.py +++ b/synapse/tests/test_lib_agenda.py @@ -362,7 +362,9 @@ def looptime(): appt = await agenda.get(guid) self.eq(appt.isrunning, False) - self.eq(appt.lastresult, "raised exception StormRaise: errname='OmgWtfBbq' mesg='boom'") + self.isin("raised exception StormRaise: errname='OmgWtfBbq'", appt.lastresult) + self.isin("highlight={'hash': '6736b8252d9413221a9b693b2b19cf53'", appt.lastresult) + self.isin("mesg='boom'", appt.lastresult) # Test setting the global enable/disable flag await agenda.delete(guid) diff --git a/synapse/tests/test_lib_ast.py b/synapse/tests/test_lib_ast.py index ed5006d8f06..50673db478a 100644 --- a/synapse/tests/test_lib_ast.py +++ b/synapse/tests/test_lib_ast.py @@ -3119,6 +3119,48 @@ async def test_ast_highlight(self): off, end = errm[1][1]['highlight']['offsets'] self.eq('newp', text[off:end]) + visi = (await core.addUser('visi'))['iden'] + text = '$users=$lib.auth.users.list() $lib.print($users.0.profile)' + msgs = await core.stormlist(text, opts={'user': visi}) + errm = [m for m in msgs if m[0] == 'err'][0] + off, end = errm[1][1]['highlight']['offsets'] + self.eq('lib.print($users.0.profile)', text[off:end]) + + text = '$lib.len(foo, bar)' + msgs = await core.stormlist(text) + errm = [m for m in msgs if m[0] == 'err'][0] + off, end = errm[1][1]['highlight']['offsets'] + self.eq('lib.len(foo, bar)', text[off:end]) + self.stormIsInErr('$lib.len()', msgs) + + text = '$foo=$lib.pkg.get $foo()' + msgs = await core.stormlist(text) + errm = [m for m in msgs if m[0] == 'err'][0] + off, end = errm[1][1]['highlight']['offsets'] + self.eq('foo()', text[off:end]) + self.stormIsInErr('$lib.pkg.get()', msgs) + + text = '$obj = $lib.pipe.gen(${ $obj.put() }) $obj.put(foo, bar, baz)' + msgs = await core.stormlist(text) + errm = [m for m in msgs if m[0] == 'err'][0] + off, end = errm[1][1]['highlight']['offsets'] + self.eq('obj.put(foo, bar, baz)', text[off:end]) + self.stormIsInErr('pipe.put()', msgs) + + text = '$lib.gen.campaign(foo, bar, baz)' + msgs = await core.stormlist(text) + errm = [m for m in msgs if m[0] == 'err'][0] + off, end = errm[1][1]['highlight']['offsets'] + self.eq('lib.gen.campaign(foo, bar, baz)', text[off:end]) + self.stormIsInErr('$lib.gen.campaign()', msgs) + + text = '$gen = $lib.gen.campaign $gen(foo, bar, baz)' + msgs = await core.stormlist(text) + errm = [m for m in msgs if m[0] == 'err'][0] + off, end = errm[1][1]['highlight']['offsets'] + self.eq('gen(foo, bar, baz)', text[off:end]) + self.stormIsInErr('$lib.gen.campaign()', msgs) + async def test_ast_bulkedges(self): async with self.getTestCore() as core: