Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

accounts/keystore: faster tests #25827

Merged
merged 6 commits into from
Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions accounts/keystore/account_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ func (ac *accountCache) deleteByFile(path string) {
}
}

// watcherStarted returns true if the watcher loop started running (even if it
// has since also ended).
func (ac *accountCache) watcherStarted() bool {
ac.mu.Lock()
defer ac.mu.Unlock()
return ac.watcher.running || ac.watcher.runEnded
}

func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.Account {
for i := range slice {
if slice[i] == elem {
Expand Down
91 changes: 47 additions & 44 deletions accounts/keystore/account_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,48 @@ var (
}
)

// waitWatcherStarts waits up to 1s for the keystore watcher to start.
func waitWatcherStart(ks *KeyStore) bool {
// On systems where file watch is not supported, just return "ok".
if !ks.cache.watcher.enabled() {
return true
}
// The watcher should start, and then exit.
for t0 := time.Now(); time.Since(t0) < 1*time.Second; time.Sleep(100 * time.Millisecond) {
if ks.cache.watcherStarted() {
return true
}
}
return false
}

func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error {
var list []accounts.Account
for t0 := time.Now(); time.Since(t0) < 5*time.Second; time.Sleep(200 * time.Millisecond) {
list = ks.Accounts()
if reflect.DeepEqual(list, wantAccounts) {
// ks should have also received change notifications
select {
case <-ks.changes:
default:
return fmt.Errorf("wasn't notified of new accounts")
}
return nil
}
}
return fmt.Errorf("\ngot %v\nwant %v", list, wantAccounts)
}

func TestWatchNewFile(t *testing.T) {
t.Parallel()

dir, ks := tmpKeyStore(t, false)

// Ensure the watcher is started before adding any files.
ks.Accounts()
time.Sleep(1000 * time.Millisecond)

if !waitWatcherStart(ks) {
t.Fatal("keystore watcher didn't start in time")
}
// Move in the files.
wantAccounts := make([]accounts.Account, len(cachetestAccounts))
for i := range cachetestAccounts {
Expand All @@ -72,37 +105,25 @@ func TestWatchNewFile(t *testing.T) {
}

// ks should see the accounts.
var list []accounts.Account
for d := 200 * time.Millisecond; d < 5*time.Second; d *= 2 {
list = ks.Accounts()
if reflect.DeepEqual(list, wantAccounts) {
// ks should have also received change notifications
select {
case <-ks.changes:
default:
t.Fatalf("wasn't notified of new accounts")
}
return
}
time.Sleep(d)
if err := waitForAccounts(wantAccounts, ks); err != nil {
t.Error(err)
}
t.Errorf("got %s, want %s", spew.Sdump(list), spew.Sdump(wantAccounts))
}

func TestWatchNoDir(t *testing.T) {
t.Parallel()

// Create ks but not the directory that it watches.
rand.Seed(time.Now().UnixNano())
dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watchnodir-test-%d-%d", os.Getpid(), rand.Int()))
ks := NewKeyStore(dir, LightScryptN, LightScryptP)

list := ks.Accounts()
if len(list) > 0 {
t.Error("initial account list not empty:", list)
}
time.Sleep(100 * time.Millisecond)

// The watcher should start, and then exit.
if !waitWatcherStart(ks) {
t.Fatal("keystore watcher didn't start in time")
}
// Create the directory and copy a key file into it.
os.MkdirAll(dir, 0700)
defer os.RemoveAll(dir)
Expand Down Expand Up @@ -295,24 +316,6 @@ func TestCacheFind(t *testing.T) {
}
}

func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error {
var list []accounts.Account
for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 {
list = ks.Accounts()
if reflect.DeepEqual(list, wantAccounts) {
// ks should have also received change notifications
select {
case <-ks.changes:
default:
return fmt.Errorf("wasn't notified of new accounts")
}
return nil
}
time.Sleep(d)
}
return fmt.Errorf("\ngot %v\nwant %v", list, wantAccounts)
}

// TestUpdatedKeyfileContents tests that updating the contents of a keystore file
// is noticed by the watcher, and the account cache is updated accordingly
func TestUpdatedKeyfileContents(t *testing.T) {
Expand All @@ -327,8 +330,9 @@ func TestUpdatedKeyfileContents(t *testing.T) {
if len(list) > 0 {
t.Error("initial account list not empty:", list)
}
time.Sleep(100 * time.Millisecond)

if !waitWatcherStart(ks) {
t.Fatal("keystore watcher didn't start in time")
}
// Create the directory and copy a key file into it.
os.MkdirAll(dir, 0700)
defer os.RemoveAll(dir)
Expand All @@ -346,9 +350,8 @@ func TestUpdatedKeyfileContents(t *testing.T) {
t.Error(err)
return
}

// needed so that modTime of `file` is different to its current value after forceCopyFile
time.Sleep(1000 * time.Millisecond)
time.Sleep(time.Second)

// Now replace file contents
if err := forceCopyFile(file, cachetestAccounts[1].URL.Path); err != nil {
Expand All @@ -364,7 +367,7 @@ func TestUpdatedKeyfileContents(t *testing.T) {
}

// needed so that modTime of `file` is different to its current value after forceCopyFile
time.Sleep(1000 * time.Millisecond)
time.Sleep(time.Second)

// Now replace file contents again
if err := forceCopyFile(file, cachetestAccounts[2].URL.Path); err != nil {
Expand All @@ -380,7 +383,7 @@ func TestUpdatedKeyfileContents(t *testing.T) {
}

// needed so that modTime of `file` is different to its current value after os.WriteFile
time.Sleep(1000 * time.Millisecond)
time.Sleep(time.Second)

// Now replace file contents with crap
if err := os.WriteFile(file, []byte("foo"), 0600); err != nil {
Expand Down
8 changes: 8 additions & 0 deletions accounts/keystore/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,14 @@ func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (account
return a, nil
}

// isUpdating returns whether the event notification loop is running.
// This method is mainly meant for tests.
func (ks *KeyStore) isUpdating() bool {
ks.mu.RLock()
defer ks.mu.RUnlock()
return ks.updating
}

// zeroKey zeroes a private key in memory.
func zeroKey(k *ecdsa.PrivateKey) {
b := k.D.Bits()
Expand Down
63 changes: 34 additions & 29 deletions accounts/keystore/keystore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func TestSignWithPassphrase(t *testing.T) {
}

func TestTimedUnlock(t *testing.T) {
t.Parallel()
_, ks := tmpKeyStore(t, true)

pass := "foo"
Expand Down Expand Up @@ -147,6 +148,7 @@ func TestTimedUnlock(t *testing.T) {
}

func TestOverrideUnlock(t *testing.T) {
t.Parallel()
_, ks := tmpKeyStore(t, false)

pass := "foo"
Expand Down Expand Up @@ -187,6 +189,7 @@ func TestOverrideUnlock(t *testing.T) {

// This test should fail under -race if signing races the expiration goroutine.
func TestSignRace(t *testing.T) {
t.Parallel()
_, ks := tmpKeyStore(t, false)

// Create a test account.
Expand All @@ -211,19 +214,33 @@ func TestSignRace(t *testing.T) {
t.Errorf("Account did not lock within the timeout")
}

// waitForKsUpdating waits until the updating-status of the ks reaches the
// desired wantStatus.
// It waits for a maximum time of maxTime, and returns false if it does not
// finish in time
func waitForKsUpdating(t *testing.T, ks *KeyStore, wantStatus bool, maxTime time.Duration) bool {
t.Helper()
// Wait max 250 ms, then return false
for t0 := time.Now(); time.Since(t0) < maxTime; {
if ks.isUpdating() == wantStatus {
return true
}
time.Sleep(25 * time.Millisecond)
}
return false
}

// Tests that the wallet notifier loop starts and stops correctly based on the
// addition and removal of wallet event subscriptions.
func TestWalletNotifierLifecycle(t *testing.T) {
t.Parallel()
// Create a temporary keystore to test with
_, ks := tmpKeyStore(t, false)

// Ensure that the notification updater is not running yet
time.Sleep(250 * time.Millisecond)
ks.mu.RLock()
updating := ks.updating
ks.mu.RUnlock()

if updating {
if ks.isUpdating() {
t.Errorf("wallet notifier running without subscribers")
}
// Subscribe to the wallet feed and ensure the updater boots up
Expand All @@ -233,38 +250,26 @@ func TestWalletNotifierLifecycle(t *testing.T) {
for i := 0; i < len(subs); i++ {
// Create a new subscription
subs[i] = ks.Subscribe(updates)

// Ensure the notifier comes online
time.Sleep(250 * time.Millisecond)
ks.mu.RLock()
updating = ks.updating
ks.mu.RUnlock()

if !updating {
if !waitForKsUpdating(t, ks, true, 250*time.Millisecond) {
t.Errorf("sub %d: wallet notifier not running after subscription", i)
}
}
// Unsubscribe and ensure the updater terminates eventually
for i := 0; i < len(subs); i++ {
// Close all but one sub
for i := 0; i < len(subs)-1; i++ {
// Close an existing subscription
subs[i].Unsubscribe()
}
// Check that it is still running
time.Sleep(250 * time.Millisecond)

// Ensure the notifier shuts down at and only at the last close
for k := 0; k < int(walletRefreshCycle/(250*time.Millisecond))+2; k++ {
ks.mu.RLock()
updating = ks.updating
ks.mu.RUnlock()

if i < len(subs)-1 && !updating {
t.Fatalf("sub %d: event notifier stopped prematurely", i)
}
if i == len(subs)-1 && !updating {
return
}
time.Sleep(250 * time.Millisecond)
}
if !ks.isUpdating() {
t.Fatal("event notifier stopped prematurely")
}
// Unsubscribe the last one and ensure the updater terminates eventually.
subs[len(subs)-1].Unsubscribe()
if !waitForKsUpdating(t, ks, false, 4*time.Second) {
t.Errorf("wallet notifier didn't terminate after unsubscribe")
}
t.Errorf("wallet notifier didn't terminate after unsubscribe")
}

type walletEvent struct {
Expand Down
9 changes: 7 additions & 2 deletions accounts/keystore/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ import (

type watcher struct {
ac *accountCache
starting bool
running bool
running bool // set to true when runloop begins
runEnded bool // set to true when runloop ends
starting bool // set to true prior to runloop starting
ev chan notify.EventInfo
quit chan struct{}
}
Expand All @@ -42,6 +43,9 @@ func newWatcher(ac *accountCache) *watcher {
}
}

// enabled returns false on systems not supported.
func (*watcher) enabled() bool { return true }

// starts the watcher loop in the background.
// Start a watcher in the background if that's not already in progress.
// The caller must hold w.ac.mu.
Expand All @@ -62,6 +66,7 @@ func (w *watcher) loop() {
w.ac.mu.Lock()
w.running = false
w.starting = false
w.runEnded = true
w.ac.mu.Unlock()
}()
logger := log.New("path", w.ac.keydir)
Expand Down
8 changes: 7 additions & 1 deletion accounts/keystore/watch_fallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@

package keystore

type watcher struct{ running bool }
type watcher struct {
running bool
runEnded bool
}

func newWatcher(*accountCache) *watcher { return new(watcher) }
func (*watcher) start() {}
func (*watcher) close() {}

// enabled returns false on systems not supported.
func (*watcher) enabled() bool { return false }