general: adding the Clock interface to aid testing
This commit adds Clock and DefaultClock and moves the private invoices.testClock under the clock package while adding basic unit tests for it. Clock is an interface currently encapsulating Now() and TickAfter(). It can be added as an external dependency to any class. This way tests can stub out time.Now() or time.After(). The DefaultClock class simply returns the real time.Now() and time.After().
This commit is contained in:
parent
ff3063daea
commit
7024f36a76
24
clock/default_clock.go
Normal file
24
clock/default_clock.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package clock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultClock implements Clock interface by simply calling the appropriate
|
||||||
|
// time functions.
|
||||||
|
type DefaultClock struct{}
|
||||||
|
|
||||||
|
// NewDefaultClock constructs a new DefaultClock.
|
||||||
|
func NewDefaultClock() Clock {
|
||||||
|
return &DefaultClock{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now simply returns time.Now().
|
||||||
|
func (DefaultClock) Now() time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TickAfter simply wraps time.After().
|
||||||
|
func (DefaultClock) TickAfter(duration time.Duration) <-chan time.Time {
|
||||||
|
return time.After(duration)
|
||||||
|
}
|
16
clock/interface.go
Normal file
16
clock/interface.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package clock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clock is an interface that provides a time functions for LND packages.
|
||||||
|
// This is useful during testing when a concrete time reference is needed.
|
||||||
|
type Clock interface {
|
||||||
|
// Now returns the current local time (as defined by the Clock).
|
||||||
|
Now() time.Time
|
||||||
|
|
||||||
|
// TickAfter returns a channel that will receive a tick after the specified
|
||||||
|
// duration has passed.
|
||||||
|
TickAfter(duration time.Duration) <-chan time.Time
|
||||||
|
}
|
@ -1,42 +1,40 @@
|
|||||||
package invoices
|
package clock
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// testClock can be used in tests to mock time.
|
// TestClock can be used in tests to mock time.
|
||||||
type testClock struct {
|
type TestClock struct {
|
||||||
currentTime time.Time
|
currentTime time.Time
|
||||||
timeChanMap map[time.Time][]chan time.Time
|
timeChanMap map[time.Time][]chan time.Time
|
||||||
timeLock sync.Mutex
|
timeLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// newTestClock returns a new test clock.
|
// NewTestClock returns a new test clock.
|
||||||
func newTestClock(startTime time.Time) *testClock {
|
func NewTestClock(startTime time.Time) *TestClock {
|
||||||
return &testClock{
|
return &TestClock{
|
||||||
currentTime: startTime,
|
currentTime: startTime,
|
||||||
timeChanMap: make(map[time.Time][]chan time.Time),
|
timeChanMap: make(map[time.Time][]chan time.Time),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// now returns the current (test) time.
|
// Now returns the current (test) time.
|
||||||
func (c *testClock) now() time.Time {
|
func (c *TestClock) Now() time.Time {
|
||||||
c.timeLock.Lock()
|
c.timeLock.Lock()
|
||||||
defer c.timeLock.Unlock()
|
defer c.timeLock.Unlock()
|
||||||
|
|
||||||
return c.currentTime
|
return c.currentTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// tickAfter returns a channel that will receive a tick at the specified time.
|
// TickAfter returns a channel that will receive a tick after the specified
|
||||||
func (c *testClock) tickAfter(duration time.Duration) <-chan time.Time {
|
// duration has passed passed by the user set test time.
|
||||||
|
func (c *TestClock) TickAfter(duration time.Duration) <-chan time.Time {
|
||||||
c.timeLock.Lock()
|
c.timeLock.Lock()
|
||||||
defer c.timeLock.Unlock()
|
defer c.timeLock.Unlock()
|
||||||
|
|
||||||
triggerTime := c.currentTime.Add(duration)
|
triggerTime := c.currentTime.Add(duration)
|
||||||
log.Debugf("tickAfter called: duration=%v, trigger_time=%v",
|
|
||||||
duration, triggerTime)
|
|
||||||
|
|
||||||
ch := make(chan time.Time, 1)
|
ch := make(chan time.Time, 1)
|
||||||
|
|
||||||
// If already expired, tick immediately.
|
// If already expired, tick immediately.
|
||||||
@ -53,8 +51,8 @@ func (c *testClock) tickAfter(duration time.Duration) <-chan time.Time {
|
|||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
// setTime sets the (test) time and triggers tick channels when they expire.
|
// SetTime sets the (test) time and triggers tick channels when they expire.
|
||||||
func (c *testClock) setTime(now time.Time) {
|
func (c *TestClock) SetTime(now time.Time) {
|
||||||
c.timeLock.Lock()
|
c.timeLock.Lock()
|
||||||
defer c.timeLock.Unlock()
|
defer c.timeLock.Unlock()
|
||||||
|
|
63
clock/test_clock_test.go
Normal file
63
clock/test_clock_test.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package clock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testTime = time.Date(2009, time.January, 3, 12, 0, 0, 0, time.UTC)
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNow(t *testing.T) {
|
||||||
|
c := NewTestClock(testTime)
|
||||||
|
now := c.Now()
|
||||||
|
|
||||||
|
if now != testTime {
|
||||||
|
t.Fatalf("expected: %v, got: %v", testTime, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
now = now.Add(time.Hour)
|
||||||
|
c.SetTime(now)
|
||||||
|
if c.Now() != now {
|
||||||
|
t.Fatalf("epected: %v, got: %v", now, c.Now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTickAfter(t *testing.T) {
|
||||||
|
c := NewTestClock(testTime)
|
||||||
|
|
||||||
|
// Should be ticking immediately.
|
||||||
|
ticker0 := c.TickAfter(0)
|
||||||
|
|
||||||
|
// Both should be ticking after SetTime
|
||||||
|
ticker1 := c.TickAfter(time.Hour)
|
||||||
|
ticker2 := c.TickAfter(time.Hour)
|
||||||
|
|
||||||
|
// We don't expect this one to tick.
|
||||||
|
ticker3 := c.TickAfter(2 * time.Hour)
|
||||||
|
|
||||||
|
tickOrTimeOut := func(ticker <-chan time.Time, expectTick bool) {
|
||||||
|
tick := false
|
||||||
|
select {
|
||||||
|
case <-ticker:
|
||||||
|
tick = true
|
||||||
|
case <-time.After(time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
if tick != expectTick {
|
||||||
|
t.Fatalf("expected tick: %v, ticked: %v", expectTick, tick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tickOrTimeOut(ticker0, true)
|
||||||
|
tickOrTimeOut(ticker1, false)
|
||||||
|
tickOrTimeOut(ticker2, false)
|
||||||
|
tickOrTimeOut(ticker3, false)
|
||||||
|
|
||||||
|
c.SetTime(c.Now().Add(time.Hour))
|
||||||
|
|
||||||
|
tickOrTimeOut(ticker1, true)
|
||||||
|
tickOrTimeOut(ticker2, true)
|
||||||
|
tickOrTimeOut(ticker3, false)
|
||||||
|
}
|
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
|
"github.com/lightningnetwork/lnd/clock"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/queue"
|
"github.com/lightningnetwork/lnd/queue"
|
||||||
@ -62,12 +63,10 @@ type RegistryConfig struct {
|
|||||||
// waiting for the other set members to arrive.
|
// waiting for the other set members to arrive.
|
||||||
HtlcHoldDuration time.Duration
|
HtlcHoldDuration time.Duration
|
||||||
|
|
||||||
// Now returns the current time.
|
// Clock holds the clock implementation that is used to provide
|
||||||
Now func() time.Time
|
// Now() and TickAfter() and is useful to stub out the clock functions
|
||||||
|
// during testing.
|
||||||
// TickAfter returns a channel that is sent on after the specified
|
Clock clock.Clock
|
||||||
// duration as passed.
|
|
||||||
TickAfter func(duration time.Duration) <-chan time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// htlcReleaseEvent describes an htlc auto-release event. It is used to release
|
// htlcReleaseEvent describes an htlc auto-release event. It is used to release
|
||||||
@ -177,8 +176,8 @@ type invoiceEvent struct {
|
|||||||
// tickAt returns a channel that ticks at the specified time. If the time has
|
// tickAt returns a channel that ticks at the specified time. If the time has
|
||||||
// already passed, it will tick immediately.
|
// already passed, it will tick immediately.
|
||||||
func (i *InvoiceRegistry) tickAt(t time.Time) <-chan time.Time {
|
func (i *InvoiceRegistry) tickAt(t time.Time) <-chan time.Time {
|
||||||
now := i.cfg.Now()
|
now := i.cfg.Clock.Now()
|
||||||
return i.cfg.TickAfter(t.Sub(now))
|
return i.cfg.Clock.TickAfter(t.Sub(now))
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceEventLoop is the dedicated goroutine responsible for accepting
|
// invoiceEventLoop is the dedicated goroutine responsible for accepting
|
||||||
|
@ -587,7 +587,7 @@ func TestSettleMpp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Simulate mpp timeout releasing htlc 1.
|
// Simulate mpp timeout releasing htlc 1.
|
||||||
ctx.clock.setTime(testTime.Add(30 * time.Second))
|
ctx.clock.SetTime(testTime.Add(30 * time.Second))
|
||||||
|
|
||||||
hodlEvent := (<-hodlChan1).(HodlEvent)
|
hodlEvent := (<-hodlChan1).(HodlEvent)
|
||||||
if hodlEvent.Preimage != nil {
|
if hodlEvent.Preimage != nil {
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/btcec"
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
|
"github.com/lightningnetwork/lnd/clock"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
@ -120,27 +121,26 @@ func newTestChannelDB() (*channeldb.DB, func(), error) {
|
|||||||
|
|
||||||
type testContext struct {
|
type testContext struct {
|
||||||
registry *InvoiceRegistry
|
registry *InvoiceRegistry
|
||||||
clock *testClock
|
clock *clock.TestClock
|
||||||
|
|
||||||
cleanup func()
|
cleanup func()
|
||||||
t *testing.T
|
t *testing.T
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestContext(t *testing.T) *testContext {
|
func newTestContext(t *testing.T) *testContext {
|
||||||
clock := newTestClock(testTime)
|
clock := clock.NewTestClock(testTime)
|
||||||
|
|
||||||
cdb, cleanup, err := newTestChannelDB()
|
cdb, cleanup, err := newTestChannelDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
cdb.Now = clock.now
|
cdb.Now = clock.Now
|
||||||
|
|
||||||
// Instantiate and start the invoice ctx.registry.
|
// Instantiate and start the invoice ctx.registry.
|
||||||
cfg := RegistryConfig{
|
cfg := RegistryConfig{
|
||||||
FinalCltvRejectDelta: testFinalCltvRejectDelta,
|
FinalCltvRejectDelta: testFinalCltvRejectDelta,
|
||||||
HtlcHoldDuration: 30 * time.Second,
|
HtlcHoldDuration: 30 * time.Second,
|
||||||
Now: clock.now,
|
Clock: clock,
|
||||||
TickAfter: clock.tickAfter,
|
|
||||||
}
|
}
|
||||||
registry := NewRegistry(cdb, &cfg)
|
registry := NewRegistry(cdb, &cfg)
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/chanfitness"
|
"github.com/lightningnetwork/lnd/chanfitness"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/channelnotifier"
|
"github.com/lightningnetwork/lnd/channelnotifier"
|
||||||
|
"github.com/lightningnetwork/lnd/clock"
|
||||||
"github.com/lightningnetwork/lnd/contractcourt"
|
"github.com/lightningnetwork/lnd/contractcourt"
|
||||||
"github.com/lightningnetwork/lnd/discovery"
|
"github.com/lightningnetwork/lnd/discovery"
|
||||||
"github.com/lightningnetwork/lnd/feature"
|
"github.com/lightningnetwork/lnd/feature"
|
||||||
@ -381,8 +382,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
|
|||||||
registryConfig := invoices.RegistryConfig{
|
registryConfig := invoices.RegistryConfig{
|
||||||
FinalCltvRejectDelta: defaultFinalCltvRejectDelta,
|
FinalCltvRejectDelta: defaultFinalCltvRejectDelta,
|
||||||
HtlcHoldDuration: invoices.DefaultHtlcHoldDuration,
|
HtlcHoldDuration: invoices.DefaultHtlcHoldDuration,
|
||||||
Now: time.Now,
|
Clock: clock.NewDefaultClock(),
|
||||||
TickAfter: time.After,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &server{
|
s := &server{
|
||||||
|
Loading…
Reference in New Issue
Block a user