Skip to content

Commit

Permalink
Add tests for config watchers and related functions
Browse files Browse the repository at this point in the history
  • Loading branch information
jchamberlain committed Sep 18, 2023
1 parent c0ff399 commit efb0451
Show file tree
Hide file tree
Showing 7 changed files with 571 additions and 18 deletions.
49 changes: 49 additions & 0 deletions atlas/atlas_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package atlas_test

import (
"strings"
"testing"

"github.com/go-spatial/geom"
"github.com/go-spatial/tegola/atlas"
"github.com/go-spatial/tegola/internal/env"
Expand Down Expand Up @@ -51,3 +54,49 @@ var testMap = atlas.Map{
testLayer3,
},
}

func TestAddMaps(t *testing.T) {
a := &atlas.Atlas{}

// Should initialize from empty
maps := []atlas.Map{
{Name: "First Map"},
{Name: "Second Map"},
}
err := a.AddMaps(maps)
if err != nil {
t.Errorf("Unexpected error when addings maps. %s", err)
}

m, err := a.Map("Second Map")
if err != nil {
t.Errorf("Failed retrieving map from Atlas. %s", err)
} else if m.Name != "Second Map" {
t.Errorf("Expected map named \"Second Map\". Found %v.", m)
}

// Should error if duplicate name.
err = a.AddMaps([]atlas.Map{{Name: "First Map"}})
if err == nil || !strings.Contains(err.Error(), "already exists") {
t.Errorf("Should return error for duplicate map name. err=%s", err)
}
}

func TestRemoveMaps(t *testing.T) {
a := &atlas.Atlas{}
a.AddMaps([]atlas.Map{
{Name: "First Map"},
{Name: "Second Map"},
})

if len(a.AllMaps()) != 2 {
t.Error("Unexpected failure setting up Atlas. No maps added.")
return
}

a.RemoveMaps([]string{"Second Map"})
maps := a.AllMaps()
if len(maps) != 1 || maps[0].Name == "Second Map" {
t.Error("Should have deleted \"Second Map\". Didn't.")
}
}
38 changes: 26 additions & 12 deletions cmd/tegola/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,16 @@ func initConfig(configFile string, cacheRequired bool, logLevel string, logger s
return err
}

loader := appInitializer{}

// Init providers from the primary config file.
providers, err := initProviders(conf.Providers, conf.Maps, "default")
providers, err := loader.initProviders(conf.Providers, conf.Maps, "default")
if err != nil {
return err
}

// Init maps from the primary config file.
if err = initMaps(conf.Maps, providers); err != nil {
if err = loader.initMaps(conf.Maps, providers); err != nil {
return err
}

Expand Down Expand Up @@ -158,8 +160,16 @@ func initConfig(configFile string, cacheRequired bool, logLevel string, logger s
return nil
}

type initializer interface {
initProviders(providersConfig []env.Dict, maps []provider.Map, namespace string) (map[string]provider.TilerUnion, error)
initMaps(maps []provider.Map, providers map[string]provider.TilerUnion) error
unload(app source.App)
}

type appInitializer struct{}

// initProviders translate provider config from a TOML file into usable Provider objects.
func initProviders(providersConfig []env.Dict, maps []provider.Map, namespace string) (map[string]provider.TilerUnion, error) {
func (l appInitializer) initProviders(providersConfig []env.Dict, maps []provider.Map, namespace string) (map[string]provider.TilerUnion, error) {
// first convert []env.Map -> []dict.Dicter
provArr := make([]dict.Dicter, len(providersConfig))
for i := range provArr {
Expand All @@ -175,14 +185,20 @@ func initProviders(providersConfig []env.Dict, maps []provider.Map, namespace st
}

// initMaps registers maps with Atlas to be ready for service.
func initMaps(maps []provider.Map, providers map[string]provider.TilerUnion) error {
func (l appInitializer) initMaps(maps []provider.Map, providers map[string]provider.TilerUnion) error {
if err := register.Maps(nil, maps, providers); err != nil {
return fmt.Errorf("could not register maps: %v", err)
}

return nil
}

// unload deregisters the maps and providers of an app.
func (l appInitializer) unload(app source.App) {
register.UnloadMaps(nil, getMapNames(app))
register.UnloadProviders(getProviderNames(app), app.Key)
}

// initAppConfigSource sets up an additional configuration source for "apps" (groups of providers and maps) to be loaded and unloaded on-the-fly.
func initAppConfigSource(ctx context.Context, conf config.Config) error {
// Get the config source type. If none, return.
Expand All @@ -203,13 +219,13 @@ func initAppConfigSource(ctx context.Context, conf config.Config) error {
return err
}

go watchAppUpdates(ctx, watcher)
go watchAppUpdates(ctx, watcher, appInitializer{})

return nil
}

// watchAppUpdates will pull from the channels supplied by the given watcher to process new app config.
func watchAppUpdates(ctx context.Context, watcher source.ConfigWatcher) {
func watchAppUpdates(ctx context.Context, watcher source.ConfigWatcher, init initializer) {
// Keep a record of what we've loaded so that we can unload when needed.
apps := make(map[string]source.App)

Expand All @@ -229,22 +245,21 @@ func watchAppUpdates(ctx context.Context, watcher source.ConfigWatcher) {
// If the new app is named the same as an existing app, first unload the existing one.
if old, exists := apps[app.Key]; exists {
log.Infof("Unloading app %s...", old.Key)
register.UnloadMaps(nil, getMapNames(old))
register.UnloadProviders(getProviderNames(old), old.Key)
init.unload(old)
delete(apps, old.Key)
}

log.Infof("Loading app %s...", app.Key)

// Init new providers
providers, err := initProviders(app.Providers, app.Maps, app.Key)
providers, err := init.initProviders(app.Providers, app.Maps, app.Key)
if err != nil {
log.Errorf("Failed initializing providers from %s: %s", app.Key, err)
continue
}

// Init new maps
if err = initMaps(app.Maps, providers); err != nil {
if err = init.initMaps(app.Maps, providers); err != nil {
log.Errorf("Failed initializing maps from %s: %s", app.Key, err)
continue
}
Expand All @@ -260,8 +275,7 @@ func watchAppUpdates(ctx context.Context, watcher source.ConfigWatcher) {
// Unload an app's maps if it was previously loaded.
if app, exists := apps[deleted]; exists {
log.Infof("Unloading app %s...", app.Key)
register.UnloadMaps(nil, getMapNames(app))
register.UnloadProviders(getProviderNames(app), app.Key)
init.unload(app)
delete(apps, app.Key)
} else {
log.Infof("Received an unload event for app %s, but couldn't find it.", deleted)
Expand Down
204 changes: 204 additions & 0 deletions cmd/tegola/cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package cmd

import (
"context"
"reflect"
"strings"
"testing"
"time"

"github.com/go-spatial/tegola/config"
"github.com/go-spatial/tegola/config/source"
"github.com/go-spatial/tegola/internal/env"
"github.com/go-spatial/tegola/provider"
)

type initializerMock struct {
initProvidersCalls chan bool
initMapsCalls chan bool
unloadCalls chan bool
}

func (i initializerMock) initProviders(providersConfig []env.Dict, maps []provider.Map, namespace string) (map[string]provider.TilerUnion, error) {
i.initProvidersCalls <- true
return map[string]provider.TilerUnion{}, nil
}

func (i initializerMock) initProvidersCalled() bool {
select {
case _, ok := <-i.initProvidersCalls:
return ok
case <-time.After(time.Millisecond):
return false
}
}

func (i initializerMock) initMaps(maps []provider.Map, providers map[string]provider.TilerUnion) error {
i.initMapsCalls <- true
return nil
}

func (i initializerMock) initMapsCalled() bool {
select {
case _, ok := <-i.initMapsCalls:
return ok
case <-time.After(time.Millisecond):
return false
}
}

func (i initializerMock) unload(app source.App) {
i.unloadCalls <- true
}

func (i initializerMock) unloadCalled() bool {
select {
case _, ok := <-i.unloadCalls:
return ok
case <-time.After(time.Millisecond):
return false
}
}

func TestInitAppConfigSource(t *testing.T) {
var (
cfg config.Config
err error
)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Should return nil if app config source type not specified.
cfg = config.Config{}
err = initAppConfigSource(ctx, cfg)
if err != nil {
t.Errorf("Unexpected error when app config source type is not specified: %s", err)
}

// Should return error if unable to initialize source.
cfg = config.Config{
AppConfigSource: env.Dict{
"type": "something_nonexistent",
},
}
err = initAppConfigSource(ctx, cfg)
if err == nil {
t.Error("Should return an error if invalid source type provided")
}

cfg = config.Config{
AppConfigSource: env.Dict{
"type": "file",
"dir": "something_nonexistent",
},
}
err = initAppConfigSource(ctx, cfg)
if err == nil || !strings.Contains(err.Error(), "directory") {
t.Errorf("Should return an error if unable to initialize source; expected an error about the directory, got %v", err)
}
}

func TestWatchAppUpdates(t *testing.T) {
loader := initializerMock{
initProvidersCalls: make(chan bool),
initMapsCalls: make(chan bool),
unloadCalls: make(chan bool),
}
// watcher := mock.NewWatcherMock()
watcher := source.ConfigWatcher{
Updates: make(chan source.App),
Deletions: make(chan string),
}
defer watcher.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go watchAppUpdates(ctx, watcher, loader)

// Should load new map+provider
app := source.App{
Providers: []env.Dict{},
Maps: []provider.Map{},
Key: "Test1",
}
// watcher.SendUpdate(app)
watcher.Updates <- app
if !loader.initProvidersCalled() {
t.Error("Failed to initialize providers")
}
if !loader.initMapsCalled() {
t.Error("Failed to initialize maps")
}
if loader.unloadCalled() {
t.Error("Unexpected app unload")
}

// Should load updated map+provider
// watcher.SendUpdate(app)
watcher.Updates <- app
if !loader.unloadCalled() {
t.Error("Failed to unload old app")
}
if !loader.initProvidersCalled() {
t.Error("Failed to initialize providers")
}
if !loader.initMapsCalled() {
t.Error("Failed to initialize maps")
}

// Should unload map+provider
// watcher.SendDeletion("Test1")
watcher.Deletions <- "Test1"
if !loader.unloadCalled() {
t.Error("Failed to unload old app")
}
}

func TestGetMapNames(t *testing.T) {
app := source.App{
Maps: []provider.Map{
{Name: "First Map"},
{Name: "Second Map"},
},
}
expected := []string{"First Map", "Second Map"}
names := getMapNames(app)
if !reflect.DeepEqual(expected, names) {
t.Errorf("Expected map names %v; found %v", expected, names)
}
}

func TestGetProviderNames(t *testing.T) {
var (
app source.App
names []string
expected []string
)

// Happy path
app = source.App{
Providers: []env.Dict{
{"name": "First Provider"},
{"name": "Second Provider"},
},
}
expected = []string{"First Provider", "Second Provider"}
names = getProviderNames(app)
if !reflect.DeepEqual(expected, names) {
t.Errorf("Expected provider names %v; found %v", expected, names)
}

// Error
app = source.App{
Providers: []env.Dict{
{},
{"name": "Second Provider"},
},
}
expected = []string{"Second Provider"}
names = getProviderNames(app)
if !reflect.DeepEqual(expected, names) {
t.Errorf("Expected provider names %v; found %v", expected, names)
}
}
Loading

0 comments on commit efb0451

Please sign in to comment.