github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/lang/test_units.go (about) 1 package lang 2 3 /* 4 This test library relates to the testing framework within the murex 5 language itself rather than Go's test framework within the murex project. 6 7 The naming convention here is basically the inverse of Go's test naming 8 convention. ie Go source files will be named "test_unit.go" (because 9 calling it unit_test.go would mean it's a Go test rather than murex test) 10 and the code is named UnitTestPlans (etc) rather than TestUnitPlans (etc) 11 because the latter might suggest they would be used by `go test`. This 12 naming convention is a little counterintuitive but it at least avoids 13 naming conflicts with `go test`. 14 */ 15 16 import ( 17 "errors" 18 "fmt" 19 "regexp" 20 "strings" 21 "sync" 22 23 "github.com/lmorg/murex/lang/ref" 24 "github.com/lmorg/murex/lang/types" 25 ) 26 27 // UnitTests is a class for all things murex unit tests 28 type UnitTests struct { 29 units []*unitTest 30 mutex sync.Mutex 31 } 32 33 type unitTest struct { 34 Function string // if private it should contain path module path 35 FileRef *ref.File 36 TestPlan *UnitTestPlan 37 } 38 39 // Add a new unit test 40 func (ut *UnitTests) Add(function string, test *UnitTestPlan, fileRef *ref.File) { 41 newUnitTest := &unitTest{ 42 Function: function, 43 TestPlan: test, 44 FileRef: fileRef, 45 } 46 47 ut.mutex.Lock() 48 ut.units = append(ut.units, newUnitTest) 49 ut.mutex.Unlock() 50 } 51 52 const testName = "(unit)" 53 54 const ( 55 // UnitTestAutocomplete is the pseudo-module name for autocomplete blocks 56 UnitTestAutocomplete = "(autocomplete)" 57 58 // UnitTestOpen is the pseudo-module name for open handler blocks 59 UnitTestOpen = "(open)" 60 61 // UnitTestEvent is the pseudo-module name for event blocks 62 UnitTestEvent = "(event)" 63 ) 64 65 // Run all unit tests against a specific murex function 66 func (ut *UnitTests) Run(p *Process, function string) bool { 67 ut.mutex.Lock() 68 utCopy := make([]*unitTest, len(ut.units)) 69 copy(utCopy, ut.units) 70 ut.mutex.Unlock() 71 72 var ( 73 passed = true 74 exists bool 75 ) 76 77 autoreport, err := p.Config.Get("test", "auto-report", "bool") 78 if err != nil { 79 autoreport = true 80 } 81 82 for i := range utCopy { 83 if function == "*" || utCopy[i].Function == function { 84 passed = runTest(p.Tests.Results, utCopy[i].FileRef, utCopy[i].TestPlan, utCopy[i].Function) && passed 85 exists = true 86 } 87 88 if autoreport.(bool) { 89 p.Tests.WriteResults(p.Config, p.Stdout) 90 } 91 } 92 93 if !exists { 94 passed = false 95 p.Tests.Results.Add(&TestResult{ 96 Exec: function, 97 TestName: testName, 98 Status: TestError, 99 Message: fmt.Sprintf("No unit tests exist for: `%s`", function), 100 }) 101 102 if autoreport.(bool) { 103 p.Tests.WriteResults(p.Config, p.Stdout) 104 } 105 } 106 107 if passed { 108 p.ExitNum = 0 109 } else { 110 p.ExitNum = 1 111 } 112 113 return passed 114 } 115 116 // Dump the defined unit tests in a JSONable structure 117 func (ut *UnitTests) Dump() interface{} { 118 ut.mutex.Lock() 119 dump := ut.units 120 ut.mutex.Unlock() 121 122 return dump 123 } 124 125 // UnitTestPlan is defined via JSON and specifies an individual test plan 126 type UnitTestPlan struct { 127 Parameters []string 128 Stdin string 129 StdoutMatch string 130 StderrMatch string 131 StdinType string 132 StdoutType string 133 StderrType string 134 StdoutRegex string 135 StderrRegex string 136 StdoutBlock string 137 StderrBlock string 138 StdoutIsArray bool 139 StderrIsArray bool 140 StdoutIsMap bool 141 StderrIsMap bool 142 ExitNum int 143 StdoutGreaterThan int 144 PreBlock string 145 PostBlock string 146 } 147 148 func utAddReport(results *TestResults, fileRef *ref.File, plan *UnitTestPlan, function string, status TestStatus, message string) { 149 results.Add(&TestResult{ 150 ColNumber: fileRef.Column, 151 LineNumber: fileRef.Line, 152 Exec: function, 153 Params: plan.Parameters, 154 TestName: testName, 155 Status: status, 156 Message: message, 157 }) 158 } 159 160 func runTest(results *TestResults, fileRef *ref.File, plan *UnitTestPlan, function string) bool { 161 var ( 162 preExitNum, testExitNum, postExitNum int 163 preForkErr, testForkErr, postForkErr error 164 stdoutType, stderrType string 165 166 fStdin int 167 passed = true 168 ) 169 170 addReport := func(status TestStatus, message string) { 171 utAddReport(results, fileRef, plan, function, status, message) 172 } 173 174 if len(plan.Stdin) == 0 { 175 fStdin = F_NO_STDIN 176 } else { 177 fStdin = F_CREATE_STDIN 178 } 179 180 fork := ShellProcess.Fork(F_FUNCTION | F_NEW_MODULE | F_BACKGROUND | fStdin | F_CREATE_STDOUT | F_CREATE_STDERR) 181 fork.FileRef = fileRef 182 fork.Parameters.DefineParsed(plan.Parameters) 183 184 if len(plan.Stdin) > 0 { 185 if plan.StdinType == "" { 186 plan.StdinType = types.Generic 187 } 188 fork.Stdin.SetDataType(plan.StdinType) 189 _, err := fork.Stdin.Write([]byte(plan.Stdin)) 190 if err != nil { 191 fmt.Println(err) 192 return false 193 } 194 } 195 196 // run any initializing code...if defined 197 if len(plan.PreBlock) > 0 { 198 preFork := ShellProcess.Fork(F_FUNCTION | F_NEW_MODULE | F_BACKGROUND | F_NO_STDIN | F_CREATE_STDOUT | F_CREATE_STDERR) 199 preFork.FileRef = fileRef 200 preFork.Name.Set("(unit test PreBlock)") 201 preExitNum, preForkErr = preFork.Execute([]rune(plan.PreBlock)) 202 203 if preForkErr != nil { 204 passed = false 205 addReport(TestError, tMsgCompileErr("PreBlock", preForkErr)) 206 } 207 208 if preExitNum != 0 { 209 addReport(TestInfo, tMsgNoneZeroExit("PreBlock", preExitNum)) 210 } 211 212 utReadAllOut(preFork.Stdout, results, plan, fileRef, "PreBlock", function, &passed) 213 utReadAllErr(preFork.Stderr, results, plan, fileRef, "PreBlock", function, &passed) 214 } 215 216 // run function 217 testExitNum, testForkErr = runFunction(function, plan.Stdin != "", fork) 218 if testForkErr != nil { 219 addReport(TestError, tMsgCompileErr(function, testForkErr)) 220 return false 221 } 222 223 // run any clear down code...if defined 224 if len(plan.PostBlock) > 0 { 225 postFork := ShellProcess.Fork(F_FUNCTION | F_NEW_MODULE | F_BACKGROUND | F_NO_STDIN | F_CREATE_STDOUT | F_CREATE_STDERR) 226 postFork.Name.Set("(unit test PostBlock)") 227 postFork.FileRef = fileRef 228 postExitNum, postForkErr = postFork.Execute([]rune(plan.PostBlock)) 229 230 if postForkErr != nil { 231 passed = false 232 addReport(TestError, tMsgCompileErr("PostBlock", postForkErr)) 233 } 234 235 if postExitNum != 0 { 236 addReport(TestInfo, tMsgNoneZeroExit("PostBlock", preExitNum)) 237 } 238 239 utReadAllOut(postFork.Stdout, results, plan, fileRef, "PostBlock", function, &passed) 240 utReadAllErr(postFork.Stderr, results, plan, fileRef, "PostBlock", function, &passed) 241 } 242 243 // stdout 244 245 stdout, err := fork.Stdout.ReadAll() 246 if err != nil { 247 addReport(TestFailed, tMsgReadErr("stdout", function, err)) 248 return false 249 } 250 stdoutType = fork.Stdout.GetDataType() 251 252 // stderr 253 254 stderr, err := fork.Stderr.ReadAll() 255 if err != nil { 256 addReport(TestFailed, tMsgReadErr("stderr", function, err)) 257 return false 258 } 259 stderrType = fork.Stderr.GetDataType() 260 261 // test exit number 262 263 if testExitNum == plan.ExitNum { 264 addReport(TestInfo, tMsgExitNumMatch()) 265 } else { 266 passed = false 267 addReport(TestFailed, tMsgExitNumMismatch(plan.ExitNum, testExitNum)) 268 } 269 270 // test stdout stream 271 272 if plan.StdoutIsArray { 273 status, message := testIsArray(stdout, stdoutType, "StdoutIsArray") 274 if status == TestPassed { 275 addReport(TestInfo, message) 276 } else { 277 addReport(status, message) 278 passed = false 279 } 280 } 281 282 if plan.StdoutIsMap { 283 status, message := testIsMap(stdout, stdoutType, "StdoutIsMap") 284 if status == TestPassed { 285 addReport(TestInfo, message) 286 } else { 287 addReport(status, message) 288 passed = false 289 } 290 } 291 292 if plan.StdoutGreaterThan > 0 { 293 status, message := testIsGreaterThanOrEqualTo(stdout, stdoutType, "StdoutGreaterThan", plan.StdoutGreaterThan) 294 if status == TestPassed { 295 addReport(TestInfo, message) 296 } else { 297 addReport(status, message) 298 passed = false 299 } 300 } 301 302 if plan.StdoutMatch != "" { 303 if string(stdout) == plan.StdoutMatch { 304 addReport(TestInfo, tMsgStringMatch("StdoutMatch")) 305 } else { 306 passed = false 307 addReport(TestFailed, tMsgStringMismatch("StdoutMatch", stdout)) 308 } 309 } 310 311 if plan.StdoutRegex != "" { 312 rx, err := regexp.Compile(plan.StdoutRegex) 313 switch { 314 case err != nil: 315 passed = false 316 addReport(TestError, tMsgRegexCompileErr("StdoutRegex", err)) 317 318 case !rx.Match(stdout): 319 passed = false 320 addReport(TestFailed, tMsgRegexMismatch("StdoutRegex", stdout)) 321 322 default: 323 addReport(TestInfo, tMsgRegexMatch("StdoutRegex")) 324 } 325 } 326 327 if plan.StdoutBlock != "" { 328 utBlock(plan, fileRef, []rune(plan.StdoutBlock), stdout, stdoutType, "StdoutBlock", function, results, &passed) 329 } 330 331 if plan.StdoutType != "" { 332 if stdoutType == plan.StdoutType { 333 addReport(TestInfo, tMsgDataTypeMatch("stdout")) 334 } else { 335 passed = false 336 addReport(TestFailed, tMsgDataTypeMismatch("stdout", stdoutType)) 337 } 338 } 339 340 // test stderr stream 341 342 if plan.StderrIsArray { 343 status, message := testIsArray(stderr, stderrType, "StderrIsArray") 344 if status == TestPassed { 345 addReport(TestInfo, message) 346 } else { 347 addReport(status, message) 348 passed = false 349 } 350 } 351 352 if plan.StderrIsMap { 353 status, message := testIsMap(stderr, stderrType, "StderrIsMap") 354 if status == TestPassed { 355 addReport(TestInfo, message) 356 } else { 357 addReport(status, message) 358 passed = false 359 } 360 } 361 362 if string(stderr) == plan.StderrMatch { 363 addReport(TestInfo, tMsgStringMatch("StderrMatch")) 364 } else { 365 if plan.StderrMatch != "" || plan.StderrRegex == "" { 366 passed = false 367 addReport(TestFailed, tMsgStringMismatch("StderrMatch", stderr)) 368 } 369 } 370 371 if plan.StderrRegex != "" { 372 rx, err := regexp.Compile(plan.StderrRegex) 373 switch { 374 case err != nil: 375 passed = false 376 addReport(TestError, tMsgRegexCompileErr("StderrRegex", err)) 377 378 case !rx.Match(stderr): 379 passed = false 380 addReport(TestFailed, tMsgRegexMismatch("StderrRegex", stderr)) 381 382 default: 383 addReport(TestInfo, tMsgRegexMatch("StderrRegex")) 384 } 385 } 386 387 if plan.StderrBlock != "" { 388 utBlock(plan, fileRef, []rune(plan.StderrBlock), stderr, stderrType, "StderrBlock", function, results, &passed) 389 } 390 391 if plan.StderrType != "" { 392 if stderrType == plan.StderrType { 393 addReport(TestInfo, tMsgDataTypeMatch("stdout")) 394 } else { 395 passed = false 396 addReport(TestFailed, tMsgDataTypeMismatch("stderr", stderrType)) 397 } 398 } 399 400 // lastly, a passed message if no errors 401 402 if passed { 403 addReport(TestPassed, tMsgPassed()) 404 } 405 406 return passed 407 } 408 409 func runFunction(function string, isMethod bool, fork *Fork) (int, error) { 410 fork.IsMethod = isMethod 411 412 if function[0] == '/' { 413 function = function[1:] 414 } 415 416 if strings.Contains(function, "/") { 417 return altFunc(function, fork) 418 } 419 420 fork.Name.Set(function) 421 422 if !MxFunctions.Exists(function) { 423 return 0, errors.New("function does not exist") 424 } 425 426 block, err := MxFunctions.Block(function) 427 if err != nil { 428 return 0, err 429 } 430 431 return fork.Execute(block) 432 } 433 434 func altFunc(path string, fork *Fork) (int, error) { 435 split := strings.Split(path, "/") 436 437 switch split[0] { 438 case UnitTestAutocomplete: 439 return runAutocomplete(path, split, fork) 440 case UnitTestOpen: 441 return runOpen(path, split, fork) 442 case UnitTestEvent: 443 return runEvent(path, split, fork) 444 default: 445 return runPrivate(path, split, fork) 446 } 447 } 448 449 func runAutocomplete(path string, split []string, fork *Fork) (int, error) { 450 return 0, errors.New("TODO: Not currently supported") 451 } 452 453 func runOpen(path string, split []string, fork *Fork) (int, error) { 454 return 0, errors.New("TODO: not currently supported") 455 } 456 457 func runEvent(path string, split []string, fork *Fork) (int, error) { 458 return 0, errors.New("TODO: not currently supported") 459 } 460 461 func runPrivate(path string, split []string, fork *Fork) (int, error) { 462 if len(split) < 2 { 463 return 0, fmt.Errorf("invalid module and private function path: `%s`", path) 464 } 465 466 function := split[len(split)-1] 467 module := strings.Join(split[:len(split)-1], "/") 468 469 fork.Name.Set(function) 470 471 if !PrivateFunctions.ExistsString(function, module) { 472 return 0, fmt.Errorf("private (%s) does not exist or module name (%s) is wrong", function, module) 473 } 474 475 block, err := PrivateFunctions.BlockString(function, module) 476 if err != nil { 477 return 0, err 478 } 479 480 return fork.Execute(block) 481 }