11package computer_test
22
33import (
4+ "bytes"
45 "context"
6+ "encoding/json"
57 "fmt"
68 "math/rand"
79 "strings"
@@ -1421,26 +1423,36 @@ func Test_ScheduledCallback(t *testing.T) {
14211423
14221424 t .Run ("process callback transaction execution error" , func (t * testing.T ) {
14231425 processCallbackError := fvmErrors .NewInvalidAddressErrorf (flow .EmptyAddress , "process callback execution failed" )
1424- testScheduledCallbackWithError (t , chain , []cadence.Event {}, 0 , processCallbackError )
1426+ testScheduledCallbackWithError (t , chain , []cadence.Event {}, 0 , processCallbackError , nil )
14251427 })
14261428
14271429 t .Run ("process callback transaction output error" , func (t * testing.T ) {
14281430 processCallbackError := fvmErrors .NewInvalidAddressErrorf (flow .EmptyAddress , "process callback output error" )
1429- testScheduledCallbackWithError (t , chain , []cadence.Event {}, 0 , processCallbackError )
1431+ testScheduledCallbackWithError (t , chain , []cadence.Event {}, 2 , nil , processCallbackError )
14301432 })
14311433}
14321434
14331435func testScheduledCallback (t * testing.T , chain flow.Chain , callbackEvents []cadence.Event , expectedTransactionCount int ) {
1434- testScheduledCallbackWithError (t , chain , callbackEvents , expectedTransactionCount , nil )
1436+ testScheduledCallbackWithError (t , chain , callbackEvents , expectedTransactionCount , nil , nil )
14351437}
14361438
1437- func testScheduledCallbackWithError (t * testing.T , chain flow.Chain , callbackEvents []cadence.Event , expectedTransactionCount int , processCallbackError fvmErrors.CodedError ) {
1439+ func testScheduledCallbackWithError (
1440+ t * testing.T ,
1441+ chain flow.Chain ,
1442+ callbackEvents []cadence.Event ,
1443+ expectedTransactionCount int ,
1444+ processExecuteError fvmErrors.CodedError ,
1445+ processOutputError fvmErrors.CodedError ,
1446+ ) {
14381447 rag := & RandomAddressGenerator {}
14391448 executorID := unittest .IdentifierFixture ()
14401449
1450+ testLogger := NewTestLogger ()
1451+
14411452 execCtx := fvm .NewContext (
14421453 fvm .WithScheduleCallbacksEnabled (true ), // Enable callbacks
14431454 fvm .WithChain (chain ),
1455+ fvm .WithLogger (testLogger .Logger ),
14441456 )
14451457
14461458 // track which transactions were executed and their details
@@ -1473,9 +1485,10 @@ func testScheduledCallbackWithError(t *testing.T, chain flow.Chain, callbackEven
14731485 vm := & callbackTestVM {
14741486 testVM : testVM {
14751487 t : t ,
1476- eventsPerTransaction : 0 , // we'll handle events manually
1477- err : processCallbackError , // inject error if provided
1488+ eventsPerTransaction : 0 , // we'll handle events manually
1489+ err : processExecuteError , // inject error if provided
14781490 },
1491+ processOutputErr : processOutputError ,
14791492 executedTransactions : executedTransactions ,
14801493 executedMutex : & executedTransactionsMutex ,
14811494 eventPayloads : eventPayloads ,
@@ -1506,15 +1519,34 @@ func testScheduledCallbackWithError(t *testing.T, chain flow.Chain, callbackEven
15061519 Return (nil ).
15071520 Times (1 )
15081521
1509- // expect the specified number of transactions
1510- exemetrics .On ("ExecutionTransactionExecuted" ,
1511- mock .Anything ,
1512- mock .MatchedBy (func (arg module.TransactionExecutionResultStats ) bool {
1513- return ! arg .Failed && arg .SystemTransaction
1514- }),
1515- mock .Anything ).
1516- Return (nil ).
1517- Times (expectedTransactionCount )
1522+ if processOutputError != nil {
1523+ // expect 1 failed transaction (process callback) + 1 successful transaction (system chunk)
1524+ exemetrics .On ("ExecutionTransactionExecuted" ,
1525+ mock .Anything ,
1526+ mock .MatchedBy (func (arg module.TransactionExecutionResultStats ) bool {
1527+ return arg .Failed && arg .SystemTransaction
1528+ }),
1529+ mock .Anything ).
1530+ Return (nil ).
1531+ Times (1 )
1532+ exemetrics .On ("ExecutionTransactionExecuted" ,
1533+ mock .Anything ,
1534+ mock .MatchedBy (func (arg module.TransactionExecutionResultStats ) bool {
1535+ return ! arg .Failed && arg .SystemTransaction
1536+ }),
1537+ mock .Anything ).
1538+ Return (nil ).
1539+ Times (expectedTransactionCount - 1 )
1540+ } else {
1541+ exemetrics .On ("ExecutionTransactionExecuted" ,
1542+ mock .Anything ,
1543+ mock .MatchedBy (func (arg module.TransactionExecutionResultStats ) bool {
1544+ return ! arg .Failed && arg .SystemTransaction
1545+ }),
1546+ mock .Anything ).
1547+ Return (nil ).
1548+ Times (expectedTransactionCount )
1549+ }
15181550
15191551 exemetrics .On (
15201552 "ExecutionChunkDataPackGenerated" ,
@@ -1555,7 +1587,7 @@ func testScheduledCallbackWithError(t *testing.T, chain flow.Chain, callbackEven
15551587 execCtx ,
15561588 exemetrics ,
15571589 trace .NewNoopTracer (),
1558- zerolog . Nop () ,
1590+ testLogger . Logger ,
15591591 committer ,
15601592 me ,
15611593 prov ,
@@ -1576,12 +1608,32 @@ func testScheduledCallbackWithError(t *testing.T, chain flow.Chain, callbackEven
15761608 derived .NewEmptyDerivedBlockData (0 ))
15771609
15781610 // If we expect an error, verify it and return early
1579- if processCallbackError != nil {
1611+ if processExecuteError != nil {
15801612 require .Error (t , err )
15811613 require .Contains (t , err .Error (), "system process transaction" )
15821614 return
15831615 }
15841616
1617+ if processOutputError != nil {
1618+ require .NoError (t , err )
1619+ require .Truef (
1620+ t ,
1621+ testLogger .HasLogWithField ("system process transaction output error" , "critical_error" , true ),
1622+ "expected critical error log not found" ,
1623+ )
1624+
1625+ // verify the process callback transaction failed as expected
1626+ require .Len (t , result .AllTransactionResults (), expectedTransactionCount )
1627+ processCallbackResult := result .AllTransactionResults ()[0 ]
1628+ require .NotEmpty (t , processCallbackResult .ErrorMessage , "process callback transaction should have failed" )
1629+ require .Contains (t , processCallbackResult .ErrorMessage , "process callback output error" )
1630+
1631+ // verify system chunk transaction succeeded
1632+ systemChunkResult := result .AllTransactionResults ()[1 ]
1633+ require .Empty (t , systemChunkResult .ErrorMessage , "system chunk transaction should not have failed" )
1634+ return
1635+ }
1636+
15851637 require .NoError (t , err )
15861638
15871639 // verify execution results
@@ -1639,7 +1691,8 @@ func testScheduledCallbackWithError(t *testing.T, chain flow.Chain, callbackEven
16391691
16401692// callbackTestVM is a custom VM for testing callback execution
16411693type callbackTestVM struct {
1642- testVM // Embed testVM
1694+ testVM
1695+ processOutputErr fvmErrors.CodedError
16431696 executedTransactions map [string ]string
16441697 executedMutex * sync.Mutex
16451698 eventPayloads [][]byte
@@ -1688,17 +1741,18 @@ func (c *callbackTestExecutor) Execute() error {
16881741// from the output of the procedure executor
16891742func (c * callbackTestExecutor ) Output () fvm.ProcedureOutput {
16901743 // Return error if one was injected for process callback transaction
1691- if c .vm .err != nil {
1744+ if c .vm .processOutputErr != nil {
16921745 txProc , ok := c .proc .(* fvm.TransactionProcedure )
16931746 if ok {
16941747 script := string (txProc .Transaction .Script )
16951748 if strings .Contains (script , "scheduler.process" ) {
16961749 return fvm.ProcedureOutput {
1697- Err : c .vm .err ,
1750+ Err : c .vm .processOutputErr ,
16981751 }
16991752 }
17001753 }
17011754 }
1755+
17021756 c .vm .executedMutex .Lock ()
17031757 defer c .vm .executedMutex .Unlock ()
17041758
@@ -2065,3 +2119,67 @@ func (p *programLoader) Compute(
20652119) {
20662120 return p .load ()
20672121}
2122+
2123+ // TestLogger captures log output for testing and provides methods to verify logged messages.
2124+ type TestLogger struct {
2125+ buffer bytes.Buffer
2126+ Logger zerolog.Logger
2127+ }
2128+
2129+ func NewTestLogger () * TestLogger {
2130+ tl := & TestLogger {}
2131+ tl .Logger = zerolog .New (& tl .buffer ).Level (zerolog .DebugLevel )
2132+ return tl
2133+ }
2134+
2135+ type LogEntry struct {
2136+ Level string
2137+ Message string
2138+ Fields map [string ]interface {}
2139+ }
2140+
2141+ func (tl * TestLogger ) Logs () []LogEntry {
2142+ var entries []LogEntry
2143+ lines := strings .Split (tl .buffer .String (), "\n " )
2144+ for _ , line := range lines {
2145+ if line == "" {
2146+ continue
2147+ }
2148+ var rawEntry map [string ]interface {}
2149+ if err := json .Unmarshal ([]byte (line ), & rawEntry ); err != nil {
2150+ continue
2151+ }
2152+ entry := LogEntry {
2153+ Fields : make (map [string ]interface {}),
2154+ }
2155+ for k , v := range rawEntry {
2156+ switch k {
2157+ case "level" :
2158+ entry .Level = fmt .Sprintf ("%v" , v )
2159+ case "message" :
2160+ entry .Message = fmt .Sprintf ("%v" , v )
2161+ default :
2162+ entry .Fields [k ] = v
2163+ }
2164+ }
2165+ entries = append (entries , entry )
2166+ }
2167+ return entries
2168+ }
2169+
2170+ func (tl * TestLogger ) HasLog (message string ) bool {
2171+ return strings .Contains (tl .buffer .String (), message )
2172+ }
2173+
2174+ func (tl * TestLogger ) HasLogWithField (message string , fieldName string , fieldValue interface {}) bool {
2175+ for _ , entry := range tl .Logs () {
2176+ if strings .Contains (entry .Message , message ) {
2177+ if val , ok := entry .Fields [fieldName ]; ok {
2178+ if val == fieldValue {
2179+ return true
2180+ }
2181+ }
2182+ }
2183+ }
2184+ return false
2185+ }
0 commit comments