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 }