I’m trying to write helper functions we can use in tests written in Daml Script, however annoyingly when there is a failure the error message points to the line inside the helper function only, without a stack trace, which means we need to find the location of the error inside the test by hand. I tried adding HasCallStack
to the helper functions to no avail (maybe it doesn’t work with Script
?).
Here is a minimal example demonstrating this:
{-# LANGUAGE AllowAmbiguousTypes #-}
module ScriptTrace where
import Daml.Script
import DA.Stack
template Asset with
assetParty : Party
where
signatory assetParty
choice Expire : ()
controller assetParty
do abort "boom"
test = do
assetParty <- allocateParty "party"
submit assetParty do
createCmd Asset with assetParty
exerciseByParty @Asset @"assetParty" assetParty Expire
-- Test functions
type Templ t = (Template t, HasAgreement t)
type TemplWField t pf = (Templ t, HasField pf t Party)
-- | The 'getByParty' function returns the contract id and contract of type 't' that is
-- visible to party 'p' and where the value of field 'pf' equals to 'p'.
-- It is assumed that only one contract like this exists.
getByParty : forall t pf. (HasCallStack, TemplWField t pf) => Party -> Script (ContractId t, t)
getByParty p = do
cs <- query @t p
case filter (\(_, t) -> getField @pf t == p) cs of
[c] -> return c
[] -> abort "getByParty: contract not found"
_ -> abort "getByParty: more than one contract exists"
exerciseByParty : forall t pf c r. (HasCallStack, TemplWField t pf, Choice t c r) => Party -> c -> Script r
exerciseByParty p c = do
(cid, _) <- getByParty @t @pf p
submit p $ exerciseCmd cid c
And the script result:
Script execution failed on commit at ScriptTrace:44:5:
Unhandled exception: DA.Exception.GeneralError:GeneralError@86828b9843465f419db1ef8a8ee741d1eef645df02375ebf509cdc8c3ddd16cb with
message = "boom"
Line 44 is the last line of exerciseByParty
which is not exactly helpful.
Is there any way to get a stack trace here?
Thank you!
There are two separate call stacks:
- The transaction tree “callstack”, which you can inspect in the transaction view.
- The purely functional callstack from the last exercise node, which you can get through
HasCallStack
and callStack
.
I’ve modified your example a little here to demonstrate both:
recurse : forall p . (HasCallStack) => Int -> p
recurse n = case n of
0 -> error (prettyCallStack callStack)
_ -> recurse (n-1)
template Asset with
assetParty : Party
where
signatory assetParty
nonconsuming choice Expire : ()
controller assetParty
do exercise self Inner
choice Inner : ()
controller assetParty
do recurse 5
This results in this output:
Script execution failed on commit at [Setup:53:5](command:daml.revealLocation?%5B%22file%3A%2F%2F%2FUsers%2Fbame%2Ftest%2Fcreate-daml-app%2Fdaml%2FSetup.daml%22%2C%2052%2C%2052%5D):
Unhandled exception: DA.Exception.GeneralError:GeneralError@86828b9843465f419db1ef8a8ee741d1eef645df02375ebf509cdc8c3ddd16cb with
message =
"CallStack (from HasCallStack):
recurse, called at .../Setup.daml:10:7 in main:Setup
recurse, called at .../Setup.daml:10:7 in main:Setup
recurse, called at .../Setup.daml:10:7 in main:Setup
recurse, called at .../Setup.daml:10:7 in main:Setup
recurse, called at .../Setup.daml:10:7 in main:Setup
recurse, called at .../Setup.daml:21:11 in main:Setup"
Ledger time: 1970-01-01T00:00:00Z
Partial transaction:
Failed exercise (unknown source):
exercises Inner on #0:0 ([Setup:Asset](command:daml.revealLocation?%5B%22file%3A%2F%2F%2FUsers%2Fbame%2Ftest%2Fcreate-daml-app%2Fdaml%2FSetup.daml%22%2C%2012%2C%2013%5D))
with
Sub-transactions:
0
└─> 'party' exercises Expire on #0:0 ([Setup:Asset](command:daml.revealLocation?%5B%22file%3A%2F%2F%2FUsers%2Fbame%2Ftest%2Fcreate-daml-app%2Fdaml%2FSetup.daml%22%2C%2012%2C%2013%5D))
children:
1
└─> 'party' exercises Inner on #0:0 ([Setup:Asset](command:daml.revealLocation?%5B%22file%3A%2F%2F%2FUsers%2Fbame%2Ftest%2Fcreate-daml-app%2Fdaml%2FSetup.daml%22%2C%2012%2C%2013%5D))
So you get three pieces of information:
- The commit that failed was the one in the setup script at line 53 -
exerciseByParty @Asset @"assetParty" assetParty Expire
- The transaction got as far as calling
Expire
and then Inner
. The error occurred in the body of Inner
on the Asset
template.
- The error occurred after calling
recurse
a bunch of times.
There is no doubt additional information that would help:
- Line numbers on the partial transaction to see where one exercise called another.
- An entry in the call stack that points to the actual location of the call to
callStack
.
Note that the reason callStack
does not cross exercise
boundaries is privacy. If Inner
and Expire
had different stakeholder sets, calling callStack
in Inner
would leak information.
1 Like
I think I wasn’t clear enough about my issue. In this instance I’m not even interested about the calls inside the choice. I would just like to know exactly which line in the test
script caused the error. I’m not sure how exactly you modified my example but pasting recurse
above Asset
and adding the other choice to Asset
led to submit p $ exerciseCmd cid c
inside exerciseByParty
move to around line 53 which is referenced in your example error output.
This is exactly my problem, that it is pointing inside the helper function which is likely used many times inside a test.
Let’s say I have a test like:
test = do
assetParty <- allocateParty "party"
submit assetParty do
createCmd Asset with assetParty
exerciseByParty @Asset @"assetParty" assetParty SomeChoice
exerciseByParty @Asset @"assetParty" assetParty SomeChoice
exerciseByParty @Asset @"assetParty" assetParty SomeChoice
exerciseByParty @Asset @"assetParty" assetParty SomeChoice
exerciseByParty @Asset @"assetParty" assetParty SomeChoice
Knowing the error happened inside exerciseByParty
is not going to be any help locating where in the test the error happened.