Skip to content

Commit

Permalink
Merge 58a9b7c into da047e2
Browse files Browse the repository at this point in the history
  • Loading branch information
yasirfolio3 committed Dec 2, 2019
2 parents da047e2 + 58a9b7c commit 2c99a48
Show file tree
Hide file tree
Showing 11 changed files with 366 additions and 50 deletions.
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ install:
stages:
- 'Lint'
- 'Unit test'
- 'Benchmark test'
- 'Integration tests'
- 'Benchmark test'
- 'Source clear'
jobs:
include:
Expand Down Expand Up @@ -52,9 +52,9 @@ jobs:

- stage: 'Integration tests'
env: GIMME_GO_VERSION=1.12.x FSC_PATH="/tmp/fsc-repo"
language: node_js
before_script:
- mkdir -p $FSC_PATH
- pushd $FSC_PATH && git init && git fetch --depth=1 https://$CI_USER_TOKEN@github.com/optimizely/fullstack-sdk-compatibility-suite ${FSC_BRANCH:-master} && git checkout FETCH_HEAD && popd
- ./scripts/setup-integration-tests.sh
install:
- eval "$(gimme)"
script:
Expand Down
28 changes: 22 additions & 6 deletions pkg/config/polling_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ var cmLogger = logging.GetLogger("PollingConfigManager")
// PollingProjectConfigManager maintains a dynamic copy of the project config by continuously polling for the datafile
// from the Optimizely CDN at a given (configurable) interval.
type PollingProjectConfigManager struct {
requester utils.Requester
pollingInterval time.Duration
notificationCenter notification.Center
initDatafile []byte
lastModified string
datafileURLTemplate string
requester utils.Requester
pollingInterval time.Duration
notificationCenter notification.Center
initDatafile []byte
lastModified string
datafileURLTemplate string
projectConfigUpdateHandlers []func(notification.ProjectConfigUpdateNotification)

configLock sync.RWMutex
err error
Expand Down Expand Up @@ -84,6 +85,13 @@ func WithPollingInterval(interval time.Duration) OptionFunc {
}
}

// WithNotificationHandlers is an optional function, sets passed notification handlers
func WithNotificationHandlers(handlers ...func(notification.ProjectConfigUpdateNotification)) OptionFunc {
return func(p *PollingProjectConfigManager) {
p.projectConfigUpdateHandlers = handlers
}
}

// WithInitialDatafile is an optional function, sets a passed datafile
func WithInitialDatafile(datafile []byte) OptionFunc {
return func(p *PollingProjectConfigManager) {
Expand Down Expand Up @@ -197,6 +205,14 @@ func NewPollingProjectConfigManager(sdkKey string, pollingMangerOptions ...Optio
opt(&pollingProjectConfigManager)
}

for _, handler := range pollingProjectConfigManager.projectConfigUpdateHandlers {
if _, err := pollingProjectConfigManager.OnProjectConfigUpdate(handler); err == nil {
// To bypass linter warnings for ignoring error
continue
}
break
}

initDatafile := pollingProjectConfigManager.initDatafile
pollingProjectConfigManager.SyncConfig(sdkKey, initDatafile) // initial poll
return &pollingProjectConfigManager
Expand Down
13 changes: 13 additions & 0 deletions pkg/config/polling_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,16 @@ func TestDatafileTemplate(t *testing.T) {

assert.Equal(t, datafileTemplate, configManager.datafileURLTemplate)
}

func TestNotificationHandlers(t *testing.T) {

projectConfigUpdateCallback := func(notification notification.ProjectConfigUpdateNotification) {
}

sdkKey := "test_sdk_key"
exeCtx := utils.NewCancelableExecutionCtx()
configManager := NewPollingProjectConfigManager(sdkKey, WithNotificationHandlers(projectConfigUpdateCallback))
configManager.Start(sdkKey, exeCtx)

assert.Equal(t, len(configManager.projectConfigUpdateHandlers), 1)
}
9 changes: 9 additions & 0 deletions scripts/setup-integration-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

. ~/.nvm/nvm.sh
mkdir -p $FSC_PATH
pushd $FSC_PATH && git init && git fetch --depth=1 https://$CI_USER_TOKEN@github.com/optimizely/fullstack-sdk-compatibility-suite ${FSC_BRANCH:-master} && git checkout FETCH_HEAD
ln -s features/support/datafiles/ public
pushd services/datafile && nvm install && nvm use && npm install && popd
node services/datafile/ &> /dev/null &
popd
2 changes: 2 additions & 0 deletions tests/integration/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ func FeatureContext(s *godog.Suite) {
s.Step(`^(\d+) "([^"]*)" listener is added$`, context.ListenerIsAdded)
s.Step(`^the User Profile Service is "([^"]*)"$`, context.TheUserProfileServiceIs)
s.Step(`^user "([^"]*)" has mapping "([^"]*)": "([^"]*)" in User Profile Service$`, context.UserHasMappingInUserProfileService)
s.Step(`^a datafile manager with the configuration$`, context.DatafileManagerConfigurationIs)
s.Step(`^in the response, the "([^"]*)" listener was called (\d+) times$`, context.TheListenerWasCalledNTimes)
s.Step(`^([^\\\"]*) is called with arguments$`, context.IsCalledWithArguments)
s.Step(`^the result should be (?:string )?"([^"]*)"$`, context.TheResultShouldBeString)
s.Step(`^the result should be (?:integer )?(\d+)$`, context.TheResultShouldBeInteger)
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/models/api_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ type APIOptions struct {
DatafileName string
APIName string
Arguments string
DFMConfiguration DataFileManagerConfiguration
Listeners map[string]int
UserProfileServiceType string
UPSMapping map[string]map[string]string
ScenarioID string
}
27 changes: 27 additions & 0 deletions tests/integration/models/datafilemanager_configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/****************************************************************************
* Copyright 2019, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
* You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
***************************************************************************/

package models

// DataFileManagerConfiguration represents a datafile manager configuration
type DataFileManagerConfiguration struct {
SDKKey string `yaml:"sdk_key"`
Mode string `yaml:"mode,omitempty"`
Revision *int `yaml:"revision,omitempty"`
DatafileCondition string `yaml:"datafile_condition,omitempty"`
UpdateInterval *int `yaml:"update_interval,omitempty"`
Timeout *int `yaml:"timeout,omitempty"`
}
124 changes: 124 additions & 0 deletions tests/integration/optlyplugins/test_config_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/****************************************************************************
* Copyright 2019, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
* You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
***************************************************************************/

package optlyplugins

import (
"sync"
"time"

"github.com/optimizely/go-sdk/pkg"
"github.com/optimizely/go-sdk/pkg/notification"
"github.com/optimizely/go-sdk/tests/integration/models"
)

// DefaultInitializationTimeout defines default timeout for datafile sync
const DefaultInitializationTimeout = time.Duration(3000) * time.Millisecond

// TestProjectConfigManager represents a ProjectConfigManager with custom implementations
type TestProjectConfigManager struct {
pkg.ProjectConfigManager
listenersCalled []notification.ProjectConfigUpdateNotification
}

// GetListenerCallbacks - Creates and returns listener callback array
func (c *TestProjectConfigManager) GetListenerCallbacks(apiOptions models.APIOptions) (listeners []func(notification notification.ProjectConfigUpdateNotification)) {

projectConfigUpdateCallback := func(notification notification.ProjectConfigUpdateNotification) {
c.listenersCalled = append(c.listenersCalled, notification)
}

for listenerType, count := range apiOptions.Listeners {
for i := 1; i <= count; i++ {
switch listenerType {
case "Config-update":
listeners = append(listeners, projectConfigUpdateCallback)
break
default:
break
}
}
}
return listeners
}

// Verify - Verifies configuration tests
func (c *TestProjectConfigManager) Verify(configuration models.DataFileManagerConfiguration) {
timeout := DefaultInitializationTimeout
if configuration.Timeout != nil {
timeout = time.Duration(*(configuration.Timeout)) * time.Millisecond
}

verify := func(wg *sync.WaitGroup) {
start := time.Now()
switch configuration.Mode {
case "wait_for_on_ready":
for {
t := time.Now()
elapsed := t.Sub(start)
if elapsed >= timeout {
break
}
// Check if projectconfig is ready
_, err := c.GetConfig()
if err == nil {
break
}
}
break
case "wait_for_config_update":
revision := 0
if configuration.Revision != nil {
revision = *(configuration.Revision)
}
for {
t := time.Now()
elapsed := t.Sub(start)
if elapsed >= timeout {
break
}
if revision > 0 {
// This means we want the manager to poll until we get to a specific revision
if revision == len(c.listenersCalled) {
break
}
} else if len(c.listenersCalled) == 1 {
// For cases where we are just waiting for config listener
break
}
}
break
default:
break
}
wg.Done()
}

var wg sync.WaitGroup
wg.Add(1)
go verify(&wg)
wg.Wait()
}

// GetListenersCalled - Returns listeners called
func (c *TestProjectConfigManager) GetListenersCalled() []notification.ProjectConfigUpdateNotification {
listenerCalled := c.listenersCalled
// Since for every scenario, a new sdk instance is created, emptying listenersCalled is required for scenario's
// where multiple requests are executed but no session is to be maintained among them.
// @TODO: Make it optional once event-batching(sessioned) tests are implemented.
c.listenersCalled = nil
return listenerCalled
}
92 changes: 92 additions & 0 deletions tests/integration/optlyplugins/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/****************************************************************************
* Copyright 2019, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
* You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
***************************************************************************/

package optlyplugins

import (
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"time"

"github.com/optimizely/go-sdk/pkg/config"
"github.com/optimizely/go-sdk/pkg/utils"
"github.com/optimizely/go-sdk/tests/integration/models"
)

const localDatafileURLTemplate = "http://localhost:3001/datafiles/%s.json?request_id="

// SyncConfig doesn't request for new datafile if we provide a valid datafile
// this requires us to keep defaultPollingInterval low so that the request
// initiated from Start method is executed quickly
const defaultPollingInterval = time.Duration(1000) * time.Millisecond

// CreatePollingConfigManager creates a pollingConfigManager with given configuration
func CreatePollingConfigManager(options models.APIOptions) *TestProjectConfigManager {
var pollingConfigManagerOptions []config.OptionFunc

// Setting up initial datafile
if options.DatafileName != "" {
datafile, err := GetDatafile(options.DatafileName)
if err != nil {
log.Fatal(err)
}
pollingConfigManagerOptions = append(pollingConfigManagerOptions, config.WithInitialDatafile(datafile))
}
// Setting up polling interval
pollingTimeInterval := defaultPollingInterval
if options.DFMConfiguration.UpdateInterval != nil {
pollingTimeInterval = time.Duration((*options.DFMConfiguration.UpdateInterval)) * time.Millisecond
}
pollingConfigManagerOptions = append(pollingConfigManagerOptions, config.WithPollingInterval(pollingTimeInterval))
sdkKey := GetSDKKey(options.DFMConfiguration)
urlString := localDatafileURLTemplate + options.ScenarioID
pollingConfigManagerOptions = append(pollingConfigManagerOptions, config.WithDatafileURLTemplate(urlString))

testProjectConfigManagerInstance := &TestProjectConfigManager{}
pollingConfigManagerOptions = append(pollingConfigManagerOptions, config.WithNotificationHandlers(testProjectConfigManagerInstance.GetListenerCallbacks(options)...))

configManager := config.NewPollingProjectConfigManager(
sdkKey,
pollingConfigManagerOptions...,
)
testProjectConfigManagerInstance.ProjectConfigManager = configManager
// Since we are using TestProjectConfigManager over ProjectConfigManager, factory will
// not call the start method for ProjectConfigManager, so we have to do it manually
exeCtx := utils.NewCancelableExecutionCtx()
configManager.Start(sdkKey, exeCtx)
// Verify datafile configuration tests
testProjectConfigManagerInstance.Verify(options.DFMConfiguration)

return testProjectConfigManagerInstance
}

// GetDatafile returns datafile,error for the provided datafileName
func GetDatafile(datafileName string) ([]byte, error) {
datafileDir := os.Getenv("DATAFILES_DIR")
return ioutil.ReadFile(filepath.Clean(path.Join(datafileDir, datafileName)))
}

// GetSDKKey returns SDKKey for configuration
func GetSDKKey(configuration models.DataFileManagerConfiguration) (sdkKey string) {
sdkKey = configuration.SDKKey
if configuration.DatafileCondition != "" {
sdkKey += "_" + configuration.DatafileCondition
}
return sdkKey
}

0 comments on commit 2c99a48

Please sign in to comment.