github.com/jbking/gohan@v0.0.0-20151217002006-b41ccf1c2a96/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  	"os"
    22  	"path/filepath"
    23  	"reflect"
    24  
    25  	"github.com/cloudwan/gohan/db"
    26  	"github.com/cloudwan/gohan/db/transaction"
    27  	"github.com/cloudwan/gohan/extension"
    28  	"github.com/cloudwan/gohan/schema"
    29  	"github.com/cloudwan/gohan/server/middleware"
    30  	"github.com/dop251/otto"
    31  
    32  	//Import otto underscore lib
    33  	_ "github.com/dop251/otto/underscore"
    34  
    35  	gohan_otto "github.com/cloudwan/gohan/extension/otto"
    36  )
    37  
    38  const (
    39  	pathVar    = "PATH"
    40  	schemasVar = "SCHEMAS"
    41  )
    42  
    43  // Environment of a single test runner
    44  type Environment struct {
    45  	*gohan_otto.Environment
    46  	mockedFunctions []string
    47  	testFileName    string
    48  	testSource      []byte
    49  	dbFile          *os.File
    50  	dbConnection    db.DB
    51  	dbTransactions  []transaction.Transaction
    52  }
    53  
    54  // NewEnvironment creates a new test environment based on provided DB connection
    55  func NewEnvironment(testFileName string, testSource []byte) *Environment {
    56  	env := &Environment{
    57  		mockedFunctions: []string{},
    58  		testFileName:    testFileName,
    59  		testSource:      testSource,
    60  	}
    61  	return env
    62  }
    63  
    64  // InitializeEnvironment creates new transaction for the test
    65  func (env *Environment) InitializeEnvironment() error {
    66  	var err error
    67  	_, file := filepath.Split(env.testFileName)
    68  	env.dbFile, err = ioutil.TempFile(os.TempDir(), file)
    69  	if err != nil {
    70  		return fmt.Errorf("Failed to create a temporary file in %s: %s", os.TempDir(), err.Error())
    71  	}
    72  	env.dbConnection, err = newDBConnection(env.dbFile.Name())
    73  	if err != nil {
    74  		return fmt.Errorf("Failed to connect to database: %s", err.Error())
    75  	}
    76  	env.Environment = gohan_otto.NewEnvironment(env.dbConnection, &middleware.FakeIdentity{})
    77  	env.SetUp()
    78  	env.addTestingAPI()
    79  
    80  	script, err := env.VM.Compile(env.testFileName, env.testSource)
    81  	if err != nil {
    82  		return fmt.Errorf("Failed to compile the file '%s': %s", env.testFileName, err.Error())
    83  	}
    84  
    85  	env.VM.Run(script)
    86  	err = env.loadSchemas()
    87  	if err != nil {
    88  		schema.ClearManager()
    89  		return fmt.Errorf("Failed to load schema for '%s': %s", env.testFileName, err.Error())
    90  	}
    91  
    92  	err = db.InitDBWithSchemas("sqlite3", env.dbFile.Name(), true, false)
    93  	if err != nil {
    94  		schema.ClearManager()
    95  		return fmt.Errorf("Failed to init DB: %s", err.Error())
    96  	}
    97  
    98  	return nil
    99  }
   100  
   101  // ClearEnvironment clears mock calls between tests and rollbacks test transaction
   102  func (env *Environment) ClearEnvironment() {
   103  	for _, functionName := range env.mockedFunctions {
   104  		env.setToOtto(functionName, "requests", [][]otto.Value{})
   105  		env.setToOtto(functionName, "responses", []otto.Value{})
   106  	}
   107  
   108  	for _, tx := range env.dbTransactions {
   109  		tx.Close()
   110  	}
   111  	toDelete := env.dbFile.Name()
   112  	env.dbFile.Close()
   113  	os.Remove(toDelete)
   114  	schema.ClearManager()
   115  }
   116  
   117  // CheckAllMockCallsMade check if all declared mock calls were made
   118  func (env *Environment) CheckAllMockCallsMade() error {
   119  	for _, functionName := range env.mockedFunctions {
   120  		requests := env.getFromOtto(functionName, "requests").([][]otto.Value)
   121  		responses := env.getFromOtto(functionName, "responses").([]otto.Value)
   122  		if len(requests) > 0 || len(responses) > 0 {
   123  			err := env.checkSpecified(functionName)
   124  			if err != nil {
   125  				return err
   126  			}
   127  			return fmt.Errorf("Expected call to %s(%v) with return value %v, but not made",
   128  				functionName, valueSliceToString(requests[0]), responses[0])
   129  		}
   130  	}
   131  	return nil
   132  }
   133  
   134  func newDBConnection(dbfilename string) (db.DB, error) {
   135  	connection, err := db.ConnectDB("sqlite3", dbfilename)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	return connection, nil
   140  }
   141  
   142  func (env *Environment) addTestingAPI() {
   143  	builtins := map[string]interface{}{
   144  		"Fail": func(call otto.FunctionCall) otto.Value {
   145  			if len(call.ArgumentList) == 0 {
   146  				panic(fmt.Errorf("Fail!"))
   147  			}
   148  
   149  			if !call.ArgumentList[0].IsString() {
   150  				panic(fmt.Errorf("Invalid call to 'Fail': format string expected first"))
   151  			}
   152  
   153  			format, _ := call.ArgumentList[0].ToString()
   154  			args := []interface{}{}
   155  			for _, value := range call.ArgumentList[1:] {
   156  				args = append(args, gohan_otto.ConvertOttoToGo(value))
   157  			}
   158  
   159  			panic(fmt.Errorf(format, args...))
   160  		},
   161  		"MockTransaction": func(call otto.FunctionCall) otto.Value {
   162  			transactionValue, _ := call.Otto.ToValue(env.getTransaction())
   163  			return transactionValue
   164  		},
   165  		"CommitMockTransaction": func(call otto.FunctionCall) otto.Value {
   166  			tx := env.getTransaction()
   167  			tx.Commit()
   168  			tx.Close()
   169  			return otto.Value{}
   170  		},
   171  		"MockPolicy": func(call otto.FunctionCall) otto.Value {
   172  			policyValue, _ := call.Otto.ToValue(schema.NewEmptyPolicy())
   173  			return policyValue
   174  		},
   175  		"MockAuthorization": func(call otto.FunctionCall) otto.Value {
   176  			authorizationValue, _ := call.Otto.ToValue(schema.NewAuthorization("", "", "", []string{}, []*schema.Catalog{}))
   177  			return authorizationValue
   178  		},
   179  	}
   180  	for name, object := range builtins {
   181  		env.VM.Set(name, object)
   182  	}
   183  	// NOTE: There is no way to return error back to Otto after calling a Go
   184  	// function, so the following function has to be written in pure JavaScript.
   185  	env.VM.Run(`function GohanTrigger(event, context) { gohan_handle_event(event, context); }`)
   186  	env.mockFunction("gohan_http")
   187  }
   188  
   189  func (env *Environment) getTransaction() transaction.Transaction {
   190  	for _, tx := range env.dbTransactions {
   191  		if !tx.Closed() {
   192  			return tx
   193  		}
   194  	}
   195  	tx, _ := env.dbConnection.Begin()
   196  	env.dbTransactions = append(env.dbTransactions, tx)
   197  	return tx
   198  }
   199  
   200  func (env *Environment) mockFunction(functionName string) {
   201  	env.VM.Set(functionName, func(call otto.FunctionCall) otto.Value {
   202  		responses := env.getFromOtto(functionName, "responses").([]otto.Value)
   203  		requests := env.getFromOtto(functionName, "requests").([][]otto.Value)
   204  
   205  		err := env.checkSpecified(functionName)
   206  		if err != nil {
   207  			call.Otto.Call("Fail", nil, err.Error())
   208  		}
   209  
   210  		readableArguments := valueSliceToString(call.ArgumentList)
   211  
   212  		if len(responses) == 0 {
   213  			call.Otto.Call("Fail", nil, fmt.Sprintf("Unexpected call to %s(%v)", functionName, readableArguments))
   214  		}
   215  
   216  		expectedArguments := requests[0]
   217  		actualArguments := call.ArgumentList
   218  		if !argumentsEqual(expectedArguments, actualArguments) {
   219  			call.Otto.Call("Fail", nil, fmt.Sprintf("Wrong arguments for call %s(%v), expected %s",
   220  				functionName, readableArguments, valueSliceToString(expectedArguments)))
   221  		}
   222  
   223  		response := responses[0]
   224  		responses = responses[1:]
   225  		env.setToOtto(functionName, "responses", responses)
   226  
   227  		requests = requests[1:]
   228  		env.setToOtto(functionName, "requests", requests)
   229  
   230  		return response
   231  	})
   232  
   233  	env.setToOtto(functionName, "requests", [][]otto.Value{})
   234  	env.setToOtto(functionName, "Expect", func(call otto.FunctionCall) otto.Value {
   235  		requests := env.getFromOtto(functionName, "requests").([][]otto.Value)
   236  		if len(call.ArgumentList) == 0 {
   237  			call.Otto.Call("Fail", nil, "Expect() should be called with at least one argument")
   238  		}
   239  		requests = append(requests, call.ArgumentList)
   240  		env.setToOtto(functionName, "requests", requests)
   241  
   242  		function, _ := env.VM.Get(functionName)
   243  		return function
   244  	})
   245  
   246  	env.setToOtto(functionName, "responses", []otto.Value{})
   247  	env.setToOtto(functionName, "Return", func(call otto.FunctionCall) otto.Value {
   248  		responses := env.getFromOtto(functionName, "responses").([]otto.Value)
   249  		if len(call.ArgumentList) != 1 {
   250  			call.Otto.Call("Fail", nil, "Return() should be called with exactly one argument")
   251  		}
   252  		responses = append(responses, call.ArgumentList[0])
   253  		env.setToOtto(functionName, "responses", responses)
   254  
   255  		return otto.NullValue()
   256  	})
   257  	env.mockedFunctions = append(env.mockedFunctions, functionName)
   258  }
   259  
   260  func (env *Environment) checkSpecified(functionName string) error {
   261  	responses := env.getFromOtto(functionName, "responses").([]otto.Value)
   262  	requests := env.getFromOtto(functionName, "requests").([][]otto.Value)
   263  	if len(requests) > len(responses) {
   264  		return fmt.Errorf("Return() should be specified for each call to %s", functionName)
   265  	} else if len(requests) < len(responses) {
   266  		return fmt.Errorf("Expect() should be specified for each call to %s", functionName)
   267  	}
   268  	return nil
   269  }
   270  
   271  func (env *Environment) getFromOtto(sourceFunctionName, variableName string) interface{} {
   272  	sourceFunction, _ := env.VM.Get(sourceFunctionName)
   273  	variableRaw, _ := sourceFunction.Object().Get(variableName)
   274  	exportedVariable, _ := variableRaw.Export()
   275  	return exportedVariable
   276  }
   277  
   278  func (env *Environment) setToOtto(destinationFunctionName, variableName string, variableValue interface{}) {
   279  	destinationFunction, _ := env.VM.Get(destinationFunctionName)
   280  	destinationFunction.Object().Set(variableName, variableValue)
   281  }
   282  
   283  func argumentsEqual(a, b []otto.Value) bool {
   284  	if len(a) != len(b) {
   285  		return false
   286  	}
   287  	for i := range a {
   288  		if !reflect.DeepEqual(a[i], b[i]) {
   289  			return false
   290  		}
   291  	}
   292  	return true
   293  }
   294  
   295  func valueSliceToString(input []otto.Value) string {
   296  	output := "["
   297  	for _, v := range input {
   298  		output += fmt.Sprintf("%v, ", gohan_otto.ConvertOttoToGo(v))
   299  	}
   300  	output = output[:len(output)-2] + "]"
   301  	return output
   302  }
   303  
   304  func (env *Environment) loadSchemas() error {
   305  	schemaValue, err := env.VM.Get(schemasVar)
   306  	if err != nil {
   307  		return fmt.Errorf("%s string array not specified", schemasVar)
   308  	}
   309  	schemaFilenamesRaw := gohan_otto.ConvertOttoToGo(schemaValue)
   310  	schemaFilenames, ok := schemaFilenamesRaw.([]interface{})
   311  	if !ok {
   312  		return fmt.Errorf("Bad type of %s - expected an array of strings", schemasVar)
   313  	}
   314  
   315  	manager := schema.GetManager()
   316  	for _, schemaRaw := range schemaFilenames {
   317  		schema, ok := schemaRaw.(string)
   318  		if !ok {
   319  			return fmt.Errorf("Bad type of schema - expected a string")
   320  		}
   321  		err = manager.LoadSchemaFromFile(schema)
   322  		if err != nil {
   323  			return err
   324  		}
   325  	}
   326  	environmentManager := extension.GetManager()
   327  	for schemaID := range manager.Schemas() {
   328  		environmentManager.RegisterEnvironment(schemaID, env)
   329  	}
   330  
   331  	pathValue, err := env.VM.Get(pathVar)
   332  	if err != nil || !pathValue.IsString() {
   333  		return fmt.Errorf("%s string not specified", pathVar)
   334  	}
   335  	pathString, _ := pathValue.ToString()
   336  
   337  	return env.LoadExtensionsForPath(manager.Extensions, pathString)
   338  }