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  }