Christian Weichel 8d1664f81f [ws-daemon] Search and replace ws-sync
This should have happened in #2003

/werft https=true
2020-10-19 10:26:18 +02:00

327 lines
7.9 KiB
Go

// Copyright (c) 2020 TypeFox GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.
package session
import (
"context"
"io/ioutil"
"math/rand"
"path/filepath"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/gitpod-io/gitpod/content-service/pkg/storage"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/sirupsen/logrus"
)
func init() {
logrus.SetLevel(logrus.WarnLevel)
}
type emptySP struct{}
func (*emptySP) NewStorage() (storage.DirectAccess, error) {
return &storage.DirectNoopStorage{}, nil
}
func getTestStore() (*Store, error) {
loc, err := ioutil.TempDir("", "wsdaemon-test")
if err != nil {
return nil, err
}
return NewStore(context.Background(), loc, nil)
}
func addRandomWorkspace(s *Store) (*Workspace, error) {
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
randString := func(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
var (
instanceID = randString(16)
owner = randString(16)
workspaceID = randString(16)
)
factory := func(ctx context.Context, loc string) (*Workspace, error) {
return &Workspace{
Location: loc,
CheckoutLocation: randString(16),
CreatedAt: time.Now(),
Owner: owner,
WorkspaceID: workspaceID,
InstanceID: instanceID,
}, nil
}
location := filepath.Join(s.Location, instanceID)
return s.NewWorkspace(context.Background(), instanceID, location, factory)
}
func TestPersistAndLoadWorkspace(t *testing.T) {
store, err := getTestStore()
if err != nil {
t.Errorf("cannot create test store: %v", err)
return
}
var hookFired bool
store.hooks = map[WorkspaceState][]WorkspaceLivecycleHook{
WorkspaceReady: {
func(ctx context.Context, ws *Workspace) error {
hookFired = true
return nil
},
},
}
originalWS, err := addRandomWorkspace(store)
if err != nil {
t.Errorf("cannot create test workspace: %v", err)
return
}
err = originalWS.MarkInitDone(context.Background())
if err != nil {
t.Errorf("cannot mark workspace as init done: %v", err)
return
}
workspaces, err := filepath.Glob(filepath.Join(store.Location, "*.workspace.json"))
if err != nil {
t.Errorf("cannot glob workspaces: %v", err)
return
}
if len(workspaces) != 1 {
t.Errorf("found more than one workspace in our test store")
return
}
reloadedWS, err := loadWorkspace(context.Background(), workspaces[0])
if err != nil {
t.Errorf("cannot load workspace %s: %v", workspaces[0], err)
return
}
if diff := cmp.Diff(originalWS, reloadedWS, cmpopts.IgnoreUnexported(Workspace{})); diff != "" {
t.Errorf("unexpected workspace change (-want +got):\n%s", diff)
}
if !hookFired {
t.Errorf("hook did not fire")
}
}
func TestWaitForInit(t *testing.T) {
tests := []struct {
Desc string
State WorkspaceState
MarkInit bool
ExpectReady bool
}{
{"regular init", WorkspaceInitializing, true, true},
{"ready", WorkspaceReady, false, true},
{"init again", WorkspaceReady, false, true},
{"disposing", WorkspaceDisposing, false, false},
{"init while disposing", WorkspaceDisposing, true, false},
{"disposed", WorkspaceDisposed, false, false},
{"init while disposed", WorkspaceDisposed, true, false},
}
store, err := getTestStore()
if err != nil {
t.Errorf("cannot create test store: %v", err)
return
}
for _, test := range tests {
ws, err := addRandomWorkspace(store)
if err != nil {
t.Errorf("%s: cannot create test workspace: %v", test.Desc, err)
continue
}
ws.state = test.State
waitStarted := make(chan struct{})
waitComplete := make(chan struct{})
markInitComplete := make(chan struct{})
go func() {
waitStarted <- struct{}{}
ready := ws.WaitForInit(context.Background())
if ready != test.ExpectReady {
t.Errorf("%s: WaitForInit returned ready == %v, expected %v", test.Desc, ready, test.ExpectReady)
}
waitComplete <- struct{}{}
}()
go func() {
<-waitStarted
if !test.MarkInit {
markInitComplete <- struct{}{}
return
}
<-time.After(500 * time.Millisecond) // just for good measure
err := ws.MarkInitDone(context.Background())
if err != nil {
t.Errorf("%s: MarkInitDone returned an error: %v", test.Desc, err)
}
markInitComplete <- struct{}{}
}()
select {
case <-waitComplete:
// all is well
case <-time.After(1 * time.Second):
t.Errorf("%s: WaitForInit did not complete", test.Desc)
}
select {
case <-markInitComplete:
// all is well
case <-time.After(1 * time.Second):
t.Errorf("%s: MarkInitDone did not complete", test.Desc)
}
}
}
func TestWaitOrMarkForDisposalRace(t *testing.T) {
store, err := getTestStore()
if err != nil {
t.Errorf("cannot create test store: %v", err)
return
}
ws, err := addRandomWorkspace(store)
if err != nil {
t.Errorf("cannot create test workspace: %v", err)
return
}
ws.state = WorkspaceReady
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var (
c int32
wg sync.WaitGroup
)
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
done, _, err := ws.WaitOrMarkForDisposal(ctx)
if err != nil {
t.Errorf("WaitOrMarkForDisposal failed: %w", err)
}
if !done {
atomic.AddInt32(&c, 1)
time.Sleep(1 * time.Second)
t.Logf("num go routines: %03d", runtime.NumGoroutine())
ws.Dispose(ctx)
}
}()
}
wg.Wait()
switch c {
case 0:
t.Errorf("WaitOrMarkForDisposal never returned !done: count = %d", c)
case 1:
// all is well
default:
t.Errorf("WaitOrMarkForDisposal was racey: count = %d", c)
}
}
func TestWaitOrMarkForDisposal(t *testing.T) {
tests := []struct {
Desc string
State WorkspaceState
Dispose bool
ExpectDone1 bool
ExpectDone2 bool
}{
{"regular dispose", WorkspaceReady, true, false, true},
{"disposed", WorkspaceDisposed, false, true, true},
{"dispose again", WorkspaceDisposed, false, true, true},
{"dispose while initializing", WorkspaceInitializing, true, false, true},
}
store, err := getTestStore()
if err != nil {
t.Errorf("cannot create test store: %v", err)
return
}
for _, test := range tests {
ws, err := addRandomWorkspace(store)
if err != nil {
t.Errorf("%s: cannot create test workspace: %v", test.Desc, err)
continue
}
ws.state = test.State
waitStarted := make(chan struct{})
waitComplete := make(chan struct{})
disposeComplete := make(chan struct{})
go func() {
waitStarted <- struct{}{}
done, _, err := ws.WaitOrMarkForDisposal(context.Background())
if err != nil {
t.Errorf("%s: WaitOrMarkForDisposal (1st) returned an error: %v", test.Desc, err)
}
if done != test.ExpectDone1 {
t.Errorf("%s: WaitOrMarkForDisposal (1st) returned done == %v, expected %v", test.Desc, done, test.ExpectDone1)
}
done, _, err = ws.WaitOrMarkForDisposal(context.Background())
if err != nil {
t.Errorf("%s: WaitOrMarkForDisposal (2nd) returned an error: %v", test.Desc, err)
}
if done != test.ExpectDone2 {
t.Errorf("%s: WaitOrMarkForDisposal (2nd) returned done == %v, expected %v", test.Desc, done, test.ExpectDone2)
}
waitComplete <- struct{}{}
}()
go func() {
<-waitStarted
if !test.Dispose {
disposeComplete <- struct{}{}
return
}
<-time.After(500 * time.Millisecond) // just for good measure
err := ws.Dispose(context.Background())
if err != nil {
t.Errorf("%s: Dispose returned an error: %v", test.Desc, err)
}
disposeComplete <- struct{}{}
}()
select {
case <-waitComplete:
// all is well
case <-time.After(1 * time.Second):
t.Errorf("%s: WaitForInit did not complete", test.Desc)
}
select {
case <-disposeComplete:
// all is well
case <-time.After(1 * time.Second):
t.Errorf("%s: disposeComplete did not complete", test.Desc)
}
}
}