github.com/mmatczuk/gohan@v0.0.0-20170206152520-30e45d9bdb69/extension/framework/runner/environment.go (about)

     1  // Copyright (C) 2015 NTT Innovation Institute, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    12  // implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  
    16  package runner
    17  
    18  import (
    19  	"fmt"
    20  	"io/ioutil"
    21  	"path/filepath"
    22  	"reflect"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/cloudwan/gohan/db"
    27  	"github.com/cloudwan/gohan/db/transaction"
    28  	"github.com/cloudwan/gohan/extension"
    29  	"github.com/cloudwan/gohan/schema"
    30  	"github.com/cloudwan/gohan/server/middleware"
    31  	"github.com/cloudwan/gohan/sync/noop"
    32  	"github.com/robertkrimen/otto"
    33  
    34  	//Import otto underscore lib
    35  	_ "github.com/robertkrimen/otto/underscore"
    36  
    37  	gohan_otto "github.com/cloudwan/gohan/extension/otto"
    38  )
    39  
    40  const (
    41  	pathVar           = "PATH"
    42  	schemasVar        = "SCHEMAS"
    43  	schemaIncludesVar = "SCHEMA_INCLUDES"
    44  )
    45  
    46  // Environment of a single test runner
    47  type Environment struct {
    48  	*gohan_otto.Environment
    49  	mockedFunctions []string
    50  	schemaDir       string
    51  	testFileName    string
    52  	testSource      []byte
    53  	dbConnection    db.DB
    54  	dbTransactions  []transaction.Transaction
    55  }
    56  
    57  // NewEnvironment creates a new test environment based on provided DB connection
    58  func NewEnvironment(testFileName string, testSource []byte) *Environment {
    59  	env := &Environment{
    60  		mockedFunctions: []string{},
    61  		schemaDir:       filepath.Dir(testFileName),
    62  		testFileName:    testFileName,
    63  		testSource:      testSource,
    64  	}
    65  	return env
    66  }
    67  
    68  // InitializeEnvironment creates new transaction for the test
    69  func (env *Environment) InitializeEnvironment() error {
    70  	var err error
    71  
    72  	env.dbConnection, err = newDBConnection(env.memoryDbConn())
    73  	if err != nil {
    74  		return fmt.Errorf("Failed to connect to database: %s", err)
    75  	}
    76  	envName := strings.TrimSuffix(
    77  		filepath.Base(env.testFileName),
    78  		filepath.Ext(env.testFileName))
    79  	env.Environment = gohan_otto.NewEnvironment(envName, env.dbConnection, &middleware.FakeIdentity{}, 30*time.Second, noop.NewSync())
    80  	env.SetUp()
    81  	env.addTestingAPI()
    82  
    83  	script, err := env.VM.Otto.Compile(env.testFileName, env.testSource)
    84  	if err != nil {
    85  		return fmt.Errorf("Failed to compile the file '%s': %s", env.testFileName, err)
    86  	}
    87  
    88  	env.VM.Otto.Run(script)
    89  
    90  	err = env.loadSchemaIncludes()
    91  
    92  	if err != nil {
    93  		schema.ClearManager()
    94  		return fmt.Errorf("Failed to load schema includes for '%s': %s", env.testFileName, err)
    95  	}
    96  
    97  	err = env.loadSchemas()
    98  
    99  	if err != nil {
   100  		schema.ClearManager()
   101  		return fmt.Errorf("Failed to load schemas for '%s': %s", env.testFileName, err)
   102  	}
   103  
   104  	err = env.registerEnvironments()
   105  
   106  	if err != nil {
   107  		schema.ClearManager()
   108  		return fmt.Errorf("Failed to register environments for '%s': %s", env.testFileName, err)
   109  	}
   110  
   111  	err = env.loadExtensions()
   112  
   113  	if err != nil {
   114  		schema.ClearManager()
   115  		return fmt.Errorf("Failed to load extensions for '%s': %s", env.testFileName, err)
   116  	}
   117  
   118  	err = db.InitDBWithSchemas("sqlite3", env.memoryDbConn(), true, false)
   119  	if err != nil {
   120  		schema.ClearManager()
   121  		return fmt.Errorf("Failed to init DB: %s", err)
   122  	}
   123  
   124  	return nil
   125  }
   126  
   127  func (env *Environment) memoryDbConn() string {
   128  	return fmt.Sprintf("file:%s?mode=memory&cache=shared", env.testFileName)
   129  }
   130  
   131  // ClearEnvironment clears mock calls between tests and rollbacks test transaction
   132  func (env *Environment) ClearEnvironment() {
   133  	for _, functionName := range env.mockedFunctions {
   134  		env.setToOtto(functionName, "requests", [][]otto.Value{})
   135  		env.setToOtto(functionName, "responses", []otto.Value{})
   136  	}
   137  
   138  	for _, tx := range env.dbTransactions {
   139  		tx.Close()
   140  	}
   141  	env.Environment.ClearEnvironment()
   142  	schema.ClearManager()
   143  }
   144  
   145  // CheckAllMockCallsMade check if all declared mock calls were made
   146  func (env *Environment) CheckAllMockCallsMade() error {
   147  	for _, functionName := range env.mockedFunctions {
   148  		requests := env.getFromOtto(functionName, "requests").([][]otto.Value)
   149  		responses := env.getFromOtto(functionName, "responses").([]otto.Value)
   150  		if len(requests) > 0 || len(responses) > 0 {
   151  			err := env.checkSpecified(functionName)
   152  			if err != nil {
   153  				return err
   154  			}
   155  			return fmt.Errorf("Expected call to %s(%v) with return value %v, but not made",
   156  				functionName, valueSliceToString(requests[0]), responses[0])
   157  		}
   158  	}
   159  	return nil
   160  }
   161  
   162  func newDBConnection(dbfilename string) (db.DB, error) {
   163  	connection, err := db.ConnectDB("sqlite3", dbfilename, db.DefaultMaxOpenConn)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	return connection, nil
   168  }
   169  
   170  func (env *Environment) addTestingAPI() {
   171  	builtins := map[string]interface{}{
   172  		"Fail": func(call otto.FunctionCall) otto.Value {
   173  			if len(call.ArgumentList) == 0 {
   174  				panic(fmt.Errorf("Fail!"))
   175  			}
   176  
   177  			if !call.ArgumentList[0].IsString() {
   178  				panic(fmt.Errorf("Invalid call to 'Fail': format string expected first"))
   179  			}
   180  
   181  			format, _ := call.ArgumentList[0].ToString()
   182  			args := []interface{}{}
   183  			for _, value := range call.ArgumentList[1:] {
   184  				args = append(args, gohan_otto.ConvertOttoToGo(value))
   185  			}
   186  
   187  			panic(fmt.Errorf(format, args...))
   188  		},
   189  		"MockTransaction": func(call otto.FunctionCall) otto.Value {
   190  			newTransaction := false
   191  			if len(call.ArgumentList) > 1 {
   192  				panic("Wrong number of arguments in MockTransaction call.")
   193  			} else if len(call.ArgumentList) == 1 {
   194  				rawNewTransaction, _ := call.Argument(0).Export()
   195  				newTransaction = rawNewTransaction.(bool)
   196  			}
   197  			transactionValue, _ := call.Otto.ToValue(env.getTransaction(newTransaction))
   198  			return transactionValue
   199  		},
   200  		"CommitMockTransaction": func(call otto.FunctionCall) otto.Value {
   201  			tx := env.getTransaction(false)
   202  			tx.Commit()
   203  			tx.Close()
   204  			return otto.Value{}
   205  		},
   206  		"MockPolicy": func(call otto.FunctionCall) otto.Value {
   207  			policyValue, _ := call.Otto.ToValue(schema.NewEmptyPolicy())
   208  			return policyValue
   209  		},
   210  		"MockAuthorization": func(call otto.FunctionCall) otto.Value {
   211  			authorizationValue, _ := call.Otto.ToValue(schema.NewAuthorization("", "", "", []string{}, []*schema.Catalog{}))
   212  			return authorizationValue
   213  		},
   214  	}
   215  	for name, object := range builtins {
   216  		env.VM.Set(name, object)
   217  	}
   218  	// NOTE: There is no way to return error back to Otto after calling a Go
   219  	// function, so the following function has to be written in pure JavaScript.
   220  	env.VM.Otto.Run(`function GohanTrigger(event, context) { gohan_handle_event(event, context); }`)
   221  	env.mockFunction("gohan_http")
   222  	env.mockFunction("gohan_raw_http")
   223  	env.mockFunction("gohan_db_transaction")
   224  	env.mockFunction("gohan_config")
   225  	env.mockFunction("gohan_sync_fetch")
   226  	env.mockFunction("gohan_sync_watch")
   227  }
   228  
   229  func (env *Environment) getTransaction(isNew bool) transaction.Transaction {
   230  	if !isNew {
   231  		for _, tx := range env.dbTransactions {
   232  			if !tx.Closed() {
   233  				return tx
   234  			}
   235  		}
   236  	}
   237  	tx, _ := env.dbConnection.Begin()
   238  	env.dbTransactions = append(env.dbTransactions, tx)
   239  	return tx
   240  }
   241  
   242  func (env *Environment) mockFunction(functionName string) {
   243  	env.VM.Set(functionName, func(call otto.FunctionCall) otto.Value {
   244  		responses := env.getFromOtto(functionName, "responses").([]otto.Value)
   245  		requests := env.getFromOtto(functionName, "requests").([][]otto.Value)
   246  
   247  		err := env.checkSpecified(functionName)
   248  		if err != nil {
   249  			call.Otto.Call("Fail", nil, err)
   250  		}
   251  
   252  		readableArguments := valueSliceToString(call.ArgumentList)
   253  
   254  		if len(responses) == 0 {
   255  			call.Otto.Call("Fail", nil, fmt.Sprintf("Unexpected call to %s(%v)", functionName, readableArguments))
   256  		}
   257  
   258  		expectedArguments := requests[0]
   259  		actualArguments := call.ArgumentList
   260  		if !argumentsEqual(expectedArguments, actualArguments) {
   261  			call.Otto.Call("Fail", nil, fmt.Sprintf("Wrong arguments for call %s(%v), expected %s",
   262  				functionName, readableArguments, valueSliceToString(expectedArguments)))
   263  		}
   264  
   265  		response := responses[0]
   266  		responses = responses[1:]
   267  		env.setToOtto(functionName, "responses", responses)
   268  
   269  		requests = requests[1:]
   270  		env.setToOtto(functionName, "requests", requests)
   271  
   272  		return response
   273  	})
   274  
   275  	env.setToOtto(functionName, "requests", [][]otto.Value{})
   276  	env.setToOtto(functionName, "Expect", func(call otto.FunctionCall) otto.Value {
   277  		requests := env.getFromOtto(functionName, "requests").([][]otto.Value)
   278  		requests = append(requests, call.ArgumentList)
   279  		env.setToOtto(functionName, "requests", requests)
   280  
   281  		function, _ := env.VM.Get(functionName)
   282  		return function
   283  	})
   284  
   285  	env.setToOtto(functionName, "responses", []otto.Value{})
   286  	env.setToOtto(functionName, "Return", func(call otto.FunctionCall) otto.Value {
   287  		responses := env.getFromOtto(functionName, "responses").([]otto.Value)
   288  		if len(call.ArgumentList) != 1 {
   289  			call.Otto.Call("Fail", nil, "Return() should be called with exactly one argument")
   290  		}
   291  		responses = append(responses, call.ArgumentList[0])
   292  		env.setToOtto(functionName, "responses", responses)
   293  
   294  		return otto.NullValue()
   295  	})
   296  	env.mockedFunctions = append(env.mockedFunctions, functionName)
   297  }
   298  
   299  func (env *Environment) checkSpecified(functionName string) error {
   300  	responses := env.getFromOtto(functionName, "responses").([]otto.Value)
   301  	requests := env.getFromOtto(functionName, "requests").([][]otto.Value)
   302  	if len(requests) > len(responses) {
   303  		return fmt.Errorf("Return() should be specified for each call to %s", functionName)
   304  	} else if len(requests) < len(responses) {
   305  		return fmt.Errorf("Expect() should be specified for each call to %s", functionName)
   306  	}
   307  	return nil
   308  }
   309  
   310  func (env *Environment) getFromOtto(sourceFunctionName, variableName string) interface{} {
   311  	sourceFunction, _ := env.VM.Get(sourceFunctionName)
   312  	variableRaw, _ := sourceFunction.Object().Get(variableName)
   313  	exportedVariable, _ := variableRaw.Export()
   314  	return exportedVariable
   315  }
   316  
   317  func (env *Environment) setToOtto(destinationFunctionName, variableName string, variableValue interface{}) {
   318  	destinationFunction, _ := env.VM.Get(destinationFunctionName)
   319  	destinationFunction.Object().Set(variableName, variableValue)
   320  }
   321  
   322  func argumentsEqual(a, b []otto.Value) bool {
   323  	if len(a) != len(b) {
   324  		return false
   325  	}
   326  	for i := range a {
   327  		if !reflect.DeepEqual(a[i], b[i]) {
   328  			return false
   329  		}
   330  	}
   331  	return true
   332  }
   333  
   334  func valueSliceToString(input []otto.Value) string {
   335  	values := make([]string, len(input))
   336  	for i, v := range input {
   337  		values[i] = fmt.Sprintf("%v", gohan_otto.ConvertOttoToGo(v))
   338  	}
   339  	return "[" + strings.Join(values, ", ") + "]"
   340  }
   341  
   342  func (env *Environment) loadSchemaIncludes() error {
   343  	manager := schema.GetManager()
   344  	schemaIncludeValue, err := env.VM.Get(schemaIncludesVar)
   345  	if err != nil {
   346  		return fmt.Errorf("%s string array not specified", schemaIncludesVar)
   347  	}
   348  	schemaIncludesFilenames, err := gohan_otto.GetStringList(schemaIncludeValue)
   349  	if err != nil {
   350  		return fmt.Errorf("Bad type of %s - expected an array of strings but the type is %s",
   351  			schemaIncludesVar, schemaIncludeValue.Class())
   352  	}
   353  
   354  	for _, schemaIncludes := range schemaIncludesFilenames {
   355  		var data []byte
   356  		schemaPath := env.schemaPath(schemaIncludes)
   357  		if data, err = ioutil.ReadFile(schemaPath); err != nil {
   358  			return err
   359  		}
   360  
   361  		schemas := strings.Split(string(data), "\n")
   362  		for _, schema := range schemas {
   363  			if schema == "" || strings.HasPrefix(schema, "#") {
   364  				continue
   365  			}
   366  
   367  			schemaPath := env.schemaPath(filepath.Dir(schemaIncludes), schema)
   368  			if err = manager.LoadSchemaFromFile(schemaPath); err != nil {
   369  				return err
   370  			}
   371  		}
   372  	}
   373  	return nil
   374  }
   375  
   376  func (env *Environment) loadSchemas() error {
   377  	schemaValue, err := env.VM.Get(schemasVar)
   378  	if err != nil {
   379  		return fmt.Errorf("%s string array not specified", schemasVar)
   380  	}
   381  	schemaFilenames, err := gohan_otto.GetStringList(schemaValue)
   382  	if err != nil {
   383  		return fmt.Errorf("Bad type of %s - expected an array of strings but the type is %s",
   384  			schemasVar, schemaValue.Class())
   385  	}
   386  
   387  	manager := schema.GetManager()
   388  	for _, schema := range schemaFilenames {
   389  		schemaPath := env.schemaPath(schema)
   390  		if err = manager.LoadSchemaFromFile(schemaPath); err != nil {
   391  			return err
   392  		}
   393  	}
   394  	return nil
   395  }
   396  
   397  func (env *Environment) schemaPath(s ...string) string {
   398  	s = append([]string{env.schemaDir}, s...)
   399  	return filepath.Join(s...)
   400  }
   401  
   402  func (env *Environment) registerEnvironments() error {
   403  	manager := schema.GetManager()
   404  	environmentManager := extension.GetManager()
   405  	for schemaID := range manager.Schemas() {
   406  		// Note: the following code ignores errors related to registration
   407  		//       of an environment that has already been registered
   408  		environmentManager.RegisterEnvironment(schemaID, env)
   409  	}
   410  	return nil
   411  }
   412  
   413  func (env *Environment) loadExtensions() error {
   414  	manager := schema.GetManager()
   415  	pathValue, err := env.VM.Get(pathVar)
   416  	if err != nil || !pathValue.IsString() {
   417  		return fmt.Errorf("%s string not specified", pathVar)
   418  	}
   419  	pathString, _ := pathValue.ToString()
   420  
   421  	return env.LoadExtensionsForPath(manager.Extensions, pathString)
   422  }