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 }