diff --git a/test/emulator_backend.go b/test/emulator_backend.go index 1e9360e9..c4a01d3c 100644 --- a/test/emulator_backend.go +++ b/test/emulator_backend.go @@ -24,6 +24,7 @@ import ( "fmt" "os" "strings" + "time" "github.com/onflow/cadence" "github.com/onflow/cadence/encoding/json" @@ -52,6 +53,18 @@ const helperFilePrefix = "\x00helper/" var _ stdlib.TestFramework = &EmulatorBackend{} +type SystemClock struct { + TimeDelta int64 +} + +func (sc SystemClock) Now() time.Time { + return time.Now().Add(time.Second * time.Duration(sc.TimeDelta)).UTC() +} + +func NewSystemClock() *SystemClock { + return &SystemClock{} +} + // EmulatorBackend is the emulator-backed implementation of the interpreter.TestFramework. type EmulatorBackend struct { blockchain *emulator.Blockchain @@ -76,6 +89,9 @@ type EmulatorBackend struct { // logCollection is a hook attached in the server logger, in order // to aggregate and expose log messages from the blockchain. logCollection *LogCollectionHook + + // clock allows manipulating the blockchain's clock. + clock *SystemClock } type keyInfo struct { @@ -130,6 +146,8 @@ func NewEmulatorBackend( } else { blockchain = newBlockchain(logCollectionHook) } + clock := NewSystemClock() + blockchain.SetClock(clock) return &EmulatorBackend{ blockchain: blockchain, @@ -139,6 +157,7 @@ func NewEmulatorBackend( configuration: baseConfiguration(), stdlibHandler: stdlibHandler, logCollection: logCollectionHook, + clock: clock, } } @@ -668,6 +687,14 @@ func (e *EmulatorBackend) Events( ) } +// Moves the time of the Blockchain's clock, by the +// given time delta, in the form of seconds. +func (e *EmulatorBackend) MoveTime(timeDelta int64) { + e.clock.TimeDelta += timeDelta + e.blockchain.SetClock(e.clock) + e.CommitBlock() +} + // excludeCommonLocations excludes the common contracts from appearing // in the coverage report, as they skew the coverage metrics. func excludeCommonLocations(coverageReport *runtime.CoverageReport) { diff --git a/test/go.mod b/test/go.mod index d8778212..8c7f280b 100644 --- a/test/go.mod +++ b/test/go.mod @@ -4,9 +4,9 @@ go 1.18 require ( github.com/logrusorgru/aurora v2.0.3+incompatible - github.com/onflow/cadence v0.40.1-0.20230808215334-fcb488b659bf + github.com/onflow/cadence v0.40.1-0.20230828191216-1bbc078efdb3 github.com/onflow/flow-emulator v0.54.0 - github.com/onflow/flow-go v0.31.1-0.20230808172820-f074502a67e3 + github.com/onflow/flow-go v0.31.1-0.20230901090702-eeeef3a7bd58 github.com/onflow/flow-go-sdk v0.41.10 github.com/rs/zerolog v1.29.0 github.com/stretchr/testify v1.8.4 diff --git a/test/go.sum b/test/go.sum index 7435f07c..c79ce8be 100644 --- a/test/go.sum +++ b/test/go.sum @@ -519,8 +519,8 @@ github.com/onflow/atree v0.1.0-beta1.0.20211027184039-559ee654ece9/go.mod h1:+6x github.com/onflow/atree v0.6.0 h1:j7nQ2r8npznx4NX39zPpBYHmdy45f4xwoi+dm37Jk7c= github.com/onflow/atree v0.6.0/go.mod h1:gBHU0M05qCbv9NN0kijLWMgC47gHVNBIp4KmsVFi0tc= github.com/onflow/cadence v0.20.1/go.mod h1:7mzUvPZUIJztIbr9eTvs+fQjWWHTF8veC+yk4ihcNIA= -github.com/onflow/cadence v0.40.1-0.20230808215334-fcb488b659bf h1:XdC0uYL2jjnyGk+XhX9klfNOi0/ewCpjmI8ZwoRVaE0= -github.com/onflow/cadence v0.40.1-0.20230808215334-fcb488b659bf/go.mod h1:2ggmhENvPPZXRnv9SmqN2xiYvluu4wx/96GCpeRh8lU= +github.com/onflow/cadence v0.40.1-0.20230828191216-1bbc078efdb3 h1:Ncadvbf2t4IRY1wJpGgTDs7GEpGO5RgT5HmKSifhoaQ= +github.com/onflow/cadence v0.40.1-0.20230828191216-1bbc078efdb3/go.mod h1:2ggmhENvPPZXRnv9SmqN2xiYvluu4wx/96GCpeRh8lU= github.com/onflow/flow-core-contracts/lib/go/contracts v1.2.4-0.20230703193002-53362441b57d h1:B7PdhdUNkve5MVrekWDuQf84XsGBxNZ/D3x+QQ8XeVs= github.com/onflow/flow-core-contracts/lib/go/contracts v1.2.4-0.20230703193002-53362441b57d/go.mod h1:xAiV/7TKhw863r6iO3CS5RnQ4F+pBY1TxD272BsILlo= github.com/onflow/flow-core-contracts/lib/go/templates v1.2.3 h1:X25A1dNajNUtE+KoV76wQ6BR6qI7G65vuuRXxDDqX7E= @@ -529,8 +529,8 @@ github.com/onflow/flow-emulator v0.54.0 h1:GzqMPIjsNweiyBORs8naUXhgs3PhD0X4Ep4j/ github.com/onflow/flow-emulator v0.54.0/go.mod h1:cPKNx2eaxUDtXNHN9nnrt/qydWUHNQRTa/9QnsaCSpo= github.com/onflow/flow-ft/lib/go/contracts v0.7.0 h1:XEKE6qJUw3luhsYmIOteXP53gtxNxrwTohgxJXCYqBE= github.com/onflow/flow-ft/lib/go/contracts v0.7.0/go.mod h1:kTMFIySzEJJeupk+7EmXs0EJ6CBWY/MV9fv9iYQk+RU= -github.com/onflow/flow-go v0.31.1-0.20230808172820-f074502a67e3 h1:3iDV59Das0YkeFnjI0UkOZMz+gS1JKpTNZ4oMGH4bDM= -github.com/onflow/flow-go v0.31.1-0.20230808172820-f074502a67e3/go.mod h1:PdmGmlNDu9HOhg31NYAKLrIhmuTvFDgCS56CTs0af9Y= +github.com/onflow/flow-go v0.31.1-0.20230901090702-eeeef3a7bd58 h1:SJS/WqckE6lcXmCOL/7Gp39zY66mCcEhy9xfmyomKrw= +github.com/onflow/flow-go v0.31.1-0.20230901090702-eeeef3a7bd58/go.mod h1:PdmGmlNDu9HOhg31NYAKLrIhmuTvFDgCS56CTs0af9Y= github.com/onflow/flow-go-sdk v0.24.0/go.mod h1:IoptMLPyFXWvyd9yYA6/4EmSeeozl6nJoIv4FaEMg74= github.com/onflow/flow-go-sdk v0.41.10 h1:Cio6GJhtx532TUY+cqrqWglD5sZCXkWeM5QvaRha3p4= github.com/onflow/flow-go-sdk v0.41.10/go.mod h1:0a0LiQFbFt8RW/ptoMUU7YkvW9ArVcbjLE0XS78uz1E= diff --git a/test/test_framework_test.go b/test/test_framework_test.go index 291497ac..3cf0c692 100644 --- a/test/test_framework_test.go +++ b/test/test_framework_test.go @@ -4181,3 +4181,128 @@ func TestTestFunctionValidSignature(t *testing.T) { assert.ErrorContains(t, err, "test functions should have no return values") }) } + +func TestBlockchainMoveTime(t *testing.T) { + t.Parallel() + + const contractCode = ` + pub contract TimeLocker { + pub let lockPeriod: UFix64 + pub let lockedAt: UFix64 + + init(lockedAt: UFix64) { + self.lockedAt = lockedAt + // Lock period is 30 days, in the form of seconds. + self.lockPeriod = UFix64(30 * 24 * 60 * 60) + } + + pub fun isOpen(): Bool { + let currentTime = getCurrentBlock().timestamp + return currentTime > (self.lockedAt + self.lockPeriod) + } + } + ` + + const scriptCode = ` + import "TimeLocker" + + pub fun main(): Bool { + return TimeLocker.isOpen() + } + ` + + const currentBlockTimestamp = ` + pub fun main(): UFix64 { + return getCurrentBlock().timestamp + } + ` + + const testCode = ` + import Test + + pub let blockchain = Test.newEmulatorBlockchain() + pub let account = blockchain.createAccount() + pub var lockedAt: UFix64 = 0.0 + + pub fun setup() { + let currentBlockTimestamp = Test.readFile("current_block_timestamp.cdc") + let result = blockchain.executeScript(currentBlockTimestamp, []) + lockedAt = result.returnValue! as! UFix64 + + let contractCode = Test.readFile("TimeLocker.cdc") + let err = blockchain.deployContract( + name: "TimeLocker", + code: contractCode, + account: account, + arguments: [lockedAt] + ) + + Test.expect(err, Test.beNil()) + + blockchain.useConfiguration(Test.Configuration({ + "TimeLocker": account.address + })) + } + + pub fun testIsNotOpen() { + let isLockerOpen = Test.readFile("is_locker_open.cdc") + let result = blockchain.executeScript(isLockerOpen, []) + + Test.expect(result, Test.beSucceeded()) + Test.assertEqual(false, result.returnValue! as! Bool) + } + + pub fun testIsOpen() { + // timeDelta is the representation of 20 days, in seconds + let timeDelta = Fix64(20 * 24 * 60 * 60) + blockchain.moveTime(by: timeDelta) + + let isLockerOpen = Test.readFile("is_locker_open.cdc") + var result = blockchain.executeScript(isLockerOpen, []) + + Test.expect(result, Test.beSucceeded()) + Test.assertEqual(false, result.returnValue! as! Bool) + + // We move time forward by another 20 days + blockchain.moveTime(by: timeDelta) + + result = blockchain.executeScript(isLockerOpen, []) + + Test.assertEqual(true, result.returnValue! as! Bool) + + // We move time backward by 20 days + blockchain.moveTime(by: timeDelta * -1.0) + + result = blockchain.executeScript(isLockerOpen, []) + + Test.assertEqual(false, result.returnValue! as! Bool) + } + ` + + fileResolver := func(path string) (string, error) { + switch path { + case "TimeLocker.cdc": + return contractCode, nil + case "is_locker_open.cdc": + return scriptCode, nil + case "current_block_timestamp.cdc": + return currentBlockTimestamp, nil + default: + return "", fmt.Errorf("cannot find import location: %s", path) + } + } + + importResolver := func(location common.Location) (string, error) { + return "", nil + } + + runner := NewTestRunner(). + WithFileResolver(fileResolver). + WithImportResolver(importResolver) + + results, err := runner.RunTests(testCode) + require.NoError(t, err) + for _, result := range results { + require.NoError(t, result.Error) + } +}