Browse Source

chore: Cleanup code

Matthias Ladkau 4 months ago
parent
commit
0207b5c065
44 changed files with 2739 additions and 1207 deletions
  1. 14 5
      cli/tool/debug.go
  2. 400 0
      cli/tool/debug_test.go
  3. 23 17
      cli/tool/format.go
  4. 122 0
      cli/tool/format_test.go
  5. 17 0
      cli/tool/helper.go
  6. 123 0
      cli/tool/helper_test.go
  7. 61 40
      cli/tool/interpret.go
  8. 355 0
      cli/tool/interpret_test.go
  9. 47 43
      cli/tool/pack.go
  10. 278 0
      cli/tool/pack_test.go
  11. 1 1
      ecal-support/README.md
  12. 1 1
      ecal-support/src/ecalDebugAdapter.ts
  13. 1 1
      ecal-support/src/extension.ts
  14. 18 13
      engine/processor_test.go
  15. 71 62
      engine/pubsub/eventpump_test.go
  16. 21 22
      engine/taskqueue_test.go
  17. 8 2
      examples/embedding/main.go
  18. 18 18
      interpreter/debug.go
  19. 10 10
      interpreter/debug_cmd.go
  20. 86 122
      interpreter/debug_test.go
  21. 2 2
      interpreter/func_provider.go
  22. 29 14
      interpreter/func_provider_test.go
  23. 4 1
      interpreter/rt_arithmetic_test.go
  24. 9 9
      interpreter/rt_assign_test.go
  25. 9 2
      interpreter/rt_boolean_test.go
  26. 90 65
      interpreter/rt_identifier.go
  27. 124 111
      interpreter/rt_sink.go
  28. 184 156
      interpreter/rt_statements.go
  29. 18 7
      interpreter/rt_statements_test.go
  30. 14 36
      parser/helper.go
  31. 47 14
      parser/helper_test.go
  32. 9 2
      parser/lexer.go
  33. 46 56
      parser/parser.go
  34. 184 148
      parser/prettyprinter.go
  35. 54 51
      scope/varsscope.go
  36. 17 10
      scope/varsscope_test.go
  37. 52 36
      stdlib/adapter.go
  38. 31 20
      stdlib/adapter_test.go
  39. 117 93
      stdlib/generate/generate.go
  40. 1 1
      stdlib/stdlib.go
  41. 1 1
      util/error.go
  42. 12 8
      util/import_test.go
  43. 4 1
      util/logging.go
  44. 6 6
      util/types.go

+ 14 - 5
cli/tool/debug.go

@@ -21,6 +21,7 @@ import (
 	"net"
 	"os"
 	"strings"
+	"sync"
 	"time"
 
 	"devt.de/krotik/common/errorutil"
@@ -113,8 +114,12 @@ func (i *CLIDebugInterpreter) Interpret() error {
 
 			debugServer := &debugTelnetServer{*i.DebugServerAddr, "ECALDebugServer: ",
 				nil, true, *i.EchoDebugServer, i, i.RuntimeProvider.Logger}
-			go debugServer.Run()
-			time.Sleep(500 * time.Millisecond) // Too lazy to do proper signalling
+
+			wg := &sync.WaitGroup{}
+			wg.Add(1)
+			go debugServer.Run(wg)
+			wg.Wait()
+
 			defer func() {
 				if debugServer.listener != nil {
 					debugServer.listen = false
@@ -211,12 +216,16 @@ type debugTelnetServer struct {
 /*
 Run runs the debug server.
 */
-func (s *debugTelnetServer) Run() {
+func (s *debugTelnetServer) Run(wg *sync.WaitGroup) {
 	tcpaddr, err := net.ResolveTCPAddr("tcp", s.address)
 
 	if err == nil {
 
-		if s.listener, err = net.ListenTCP("tcp", tcpaddr); err == nil {
+		s.listener, err = net.ListenTCP("tcp", tcpaddr)
+
+		if err == nil {
+
+			wg.Done()
 
 			s.logger.LogInfo(s.logPrefix,
 				"Running Debug Server on ", tcpaddr.String())
@@ -225,7 +234,6 @@ func (s *debugTelnetServer) Run() {
 				var conn net.Conn
 
 				if conn, err = s.listener.Accept(); err == nil {
-
 					go s.HandleConnection(conn)
 
 				} else if s.listen {
@@ -238,6 +246,7 @@ func (s *debugTelnetServer) Run() {
 
 	if s.listen && err != nil {
 		s.logger.LogError(s.logPrefix, "Could not start debug server - ", err)
+		wg.Done()
 	}
 }
 

+ 400 - 0
cli/tool/debug_test.go

@@ -0,0 +1,400 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package tool
+
+import (
+	"bufio"
+	"bytes"
+	"flag"
+	"fmt"
+	"net"
+	"os"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"testing"
+	"time"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/ecal/config"
+	"devt.de/krotik/ecal/interpreter"
+	"devt.de/krotik/ecal/stdlib"
+	"devt.de/krotik/ecal/util"
+)
+
+var testDebugLogOut *bytes.Buffer
+
+func newTestDebugWithConfig() *CLIDebugInterpreter {
+	tdin := NewCLIDebugInterpreter(newTestInterpreterWithConfig())
+
+	testDebugLogOut = &bytes.Buffer{}
+	tdin.LogOut = testDebugLogOut
+
+	return tdin
+}
+
+func TestDebugBasicFunctions(t *testing.T) {
+	tdin := newTestDebugWithConfig()
+	defer tearDown()
+
+	// Test help output
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing
+
+	osArgs = []string{"foo", "bar", "-help"}
+	defer func() { osArgs = []string{} }()
+
+	flag.CommandLine.SetOutput(&testTerm.out)
+
+	if stop := tdin.ParseArgs(); !stop {
+		t.Error("Asking for help should request to stop the program")
+		return
+	}
+
+	if !strings.Contains(testTerm.out.String(), "Root directory for ECAL interpreter") {
+		t.Error("Helptext does not contain expected string - output:", testTerm.out.String())
+		return
+	}
+
+	if stop := tdin.ParseArgs(); stop {
+		t.Error("Asking again should be caught by the short circuit")
+		return
+	}
+
+	tdin = newTestDebugWithConfig()
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing
+
+	flag.CommandLine.SetOutput(&testTerm.out)
+
+	if err := tdin.Interpret(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+}
+
+func TestDebugInterpret(t *testing.T) {
+	tdin := newTestDebugWithConfig()
+	defer tearDown()
+
+	if stop := tdin.ParseArgs(); stop {
+		t.Error("Setting default args should be fine")
+		return
+	}
+
+	if err := tdin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	tdin.RuntimeProvider.Logger, _ = util.NewLogLevelLogger(util.NewMemoryLogger(10), "info")
+	tdin.RuntimeProvider.ImportLocator = &util.MemoryImportLocator{
+		Files: map[string]string{
+			"foo": "a := 1",
+		},
+	}
+
+	l1 := ""
+	tdin.LogFile = &l1
+	l2 := ""
+	tdin.LogLevel = &l2
+	l3 := true
+	tdin.Interactive = &l3
+	tdin.RunDebugServer = &l3
+
+	testTerm.in = []string{"xxx := 1", "q"}
+
+	// The interpret call takes quite long because the debug server is
+	// closed by the defer call when the call returns
+
+	if err := tdin.Interpret(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if testLogOut.String() != `ECAL `+config.ProductVersion+`
+Log level: info - Root directory: /home/ml/data/krotik/ecal/cli/tool
+Running in debug mode - with debug server on localhost:33274 - prefix debug commands with ##
+Type 'q' or 'quit' to exit the shell and '?' to get help
+` {
+		t.Error("Unexpected result:", testLogOut.String())
+		return
+	}
+
+	if tdin.GlobalVS.String() != `GlobalScope {
+    xxx (float64) : 1
+}` {
+		t.Error("Unexpected scope:", tdin.GlobalVS)
+		return
+	}
+}
+
+func TestDebugHandleInput(t *testing.T) {
+	tdin := newTestDebugWithConfig()
+	defer tearDown()
+
+	if stop := tdin.ParseArgs(); stop {
+		t.Error("Setting default args should be fine")
+		return
+	}
+
+	if err := tdin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	stdlib.AddStdlibPkg("foo", "bar")
+	stdlib.AddStdlibFunc("foo", "Println",
+		stdlib.NewECALFunctionAdapter(reflect.ValueOf(fmt.Println), "xxx"))
+	stdlib.AddStdlibFunc("foo", "Atoi",
+		stdlib.NewECALFunctionAdapter(reflect.ValueOf(strconv.Atoi), "xxx"))
+
+	tdin.RuntimeProvider.Logger, _ = util.NewLogLevelLogger(util.NewMemoryLogger(10), "info")
+	tdin.RuntimeProvider.ImportLocator = &util.MemoryImportLocator{}
+	tdin.CustomHelpString = "123"
+
+	l1 := ""
+	tdin.LogFile = &l1
+	l2 := ""
+	tdin.LogLevel = &l2
+	l3 := true
+	tdin.Interactive = &l3
+	l4 := false
+	tdin.RunDebugServer = &l4
+
+	testTerm.in = []string{"?", "@dbg", "##status", "##foo", "q"}
+
+	if err := tdin.Interpret(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	// Just check for a simple string no need for the whole thing
+	time.Sleep(1 * time.Second)
+
+	if !strings.Contains(testTerm.out.String(), "Set a breakpoint specifying <source>:<line>") {
+		t.Error("Unexpected result:", testTerm.out.String())
+		return
+	}
+
+	if !strings.Contains(testTerm.out.String(), "Unknown command: foo") {
+		t.Error("Unexpected result:", testTerm.out.String())
+		return
+	}
+
+	testTerm.out.Reset()
+
+	testTerm.in = []string{"@dbg status", "q"}
+
+	if err := tdin.Interpret(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if testTerm.out.String() != `╒══════════════╤═════════════════════════════════════════╕
+│Debug command │Description                              │
+╞══════════════╪═════════════════════════════════════════╡
+│status        │Shows breakpoints and suspended threads. │
+│              │                                         │
+╘══════════════╧═════════════════════════════════════════╛
+
+
+` {
+		t.Error("Unexpected result:", "#"+testTerm.out.String()+"#")
+		return
+	}
+
+	testTerm.out.Reset()
+
+	testTerm.in = []string{"1", "raise(123)", "q"}
+
+	if err := tdin.Interpret(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if testTerm.out.String() != `1
+ECAL error in foo: 123 () (Line:1 Pos:1)
+` {
+		t.Error("Unexpected result:", testTerm.out.String())
+		return
+	}
+}
+
+func TestDebugTelnetServer(t *testing.T) {
+	tdin := newTestDebugWithConfig()
+	defer tearDown()
+
+	if err := tdin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	tdin.RuntimeProvider.Logger = util.NewMemoryLogger(10)
+	tdin.RuntimeProvider.ImportLocator = &util.MemoryImportLocator{}
+	tdin.RuntimeProvider.Debugger = interpreter.NewECALDebugger(tdin.GlobalVS)
+	tdin.RuntimeProvider.Debugger.BreakOnError(false)
+	tdin.CustomHandler = tdin
+
+	addr := "localhost:33274"
+	mlog := util.NewMemoryLogger(10)
+
+	srv := &debugTelnetServer{
+		address:     addr,
+		logPrefix:   "testdebugserver",
+		listener:    nil,
+		listen:      true,
+		echo:        true,
+		interpreter: tdin,
+		logger:      mlog,
+	}
+	defer func() {
+		srv.listen = false
+		srv.listener.Close() // Attempt to cleanup
+	}()
+
+	wg := &sync.WaitGroup{}
+	wg.Add(1)
+	go srv.Run(wg)
+	wg.Wait()
+
+	conn, err := net.Dial("tcp", addr)
+	errorutil.AssertOk(err)
+	reader := bufio.NewReader(conn)
+
+	fmt.Fprintf(conn, "a:= 1; a\n")
+
+	line, err := reader.ReadString('}')
+	errorutil.AssertOk(err)
+
+	if line != `{
+  "EncodedOutput": "MQo="
+}` {
+		t.Error("Unexpected output:", line)
+		return
+	}
+
+	if tdin.GlobalVS.String() != `GlobalScope {
+    a (float64) : 1
+}` {
+		t.Error("Unexpected result:", tdin.GlobalVS)
+		return
+	}
+
+	fmt.Fprintf(conn, "##status\n")
+
+	line, err = reader.ReadString('}')
+	errorutil.AssertOk(err)
+	l, err := reader.ReadString('}')
+	errorutil.AssertOk(err)
+	line += l
+	l, err = reader.ReadString('}')
+	errorutil.AssertOk(err)
+	line += l
+	line = strings.TrimSpace(line)
+
+	if line != `{
+  "breakonstart": false,
+  "breakpoints": {},
+  "sources": [
+    "console input"
+  ],
+  "threads": {}
+}` {
+		t.Error("Unexpected output:", line)
+		return
+	}
+
+	fmt.Fprintf(conn, "@sym\n")
+
+	line, err = reader.ReadString('}')
+	errorutil.AssertOk(err)
+
+	if !strings.Contains(line, "KioqKioqKioqKioqKioqKioq") {
+		t.Error("Unexpected output:", line)
+		return
+	}
+
+	fmt.Fprintf(conn, "raise(123);1\n")
+
+	line, err = reader.ReadString('}')
+	errorutil.AssertOk(err)
+	line = strings.TrimSpace(line)
+
+	if line != `{
+  "EncodedOutput": "RUNBTCBlcnJvciBpbiBmb286IDEyMyAoKSAoTGluZToxIFBvczoxKQo="
+}` {
+		t.Error("Unexpected output:", line)
+		return
+	}
+
+	testDebugLogOut.Reset()
+
+	errorutil.AssertOk(conn.Close())
+
+	time.Sleep(10 * time.Millisecond)
+
+	if !strings.Contains(testDebugLogOut.String(), "Disconnected") {
+		t.Error("Unexpected output:", testDebugLogOut)
+		return
+	}
+
+	testDebugLogOut.Reset()
+
+	conn, err = net.Dial("tcp", addr)
+	errorutil.AssertOk(err)
+
+	if _, err := fmt.Fprintf(conn, "q\n"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	// Make sure we can't start a second server on the same port
+
+	mlog2 := util.NewMemoryLogger(10)
+
+	srv2 := &debugTelnetServer{
+		address:     addr,
+		logPrefix:   "testdebugserver",
+		listener:    nil,
+		listen:      true,
+		echo:        true,
+		interpreter: tdin,
+		logger:      mlog2,
+	}
+	defer func() {
+		srv2.listen = false
+		srv2.listener.Close() // Attempt to cleanup
+	}()
+
+	mlog2.Reset()
+
+	wg = &sync.WaitGroup{}
+	wg.Add(1)
+	go srv2.Run(wg)
+	wg.Wait()
+
+	if !strings.Contains(mlog2.String(), "address already in use") {
+		t.Error("Unexpected output:", mlog2.String())
+		return
+	}
+
+	mlog.Reset()
+
+	srv.listener.Close()
+
+	time.Sleep(5 * time.Millisecond)
+
+	if !strings.Contains(mlog.String(), "use of closed network connection") {
+		t.Error("Unexpected output:", mlog.String())
+		return
+	}
+}

+ 23 - 17
cli/tool/format.go

@@ -16,10 +16,14 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"devt.de/krotik/ecal/parser"
 )
 
+/*
+Format formats a given set of ECAL files.
+*/
 func Format() error {
 	var err error
 
@@ -30,17 +34,17 @@ func Format() error {
 	showHelp := flag.Bool("help", false, "Show this help message")
 
 	flag.Usage = func() {
-		fmt.Println()
-		fmt.Println(fmt.Sprintf("Usage of %s format [options]", os.Args[0]))
-		fmt.Println()
+		fmt.Fprintln(flag.CommandLine.Output())
+		fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Usage of %s format [options]", os.Args[0]))
+		fmt.Fprintln(flag.CommandLine.Output())
 		flag.PrintDefaults()
-		fmt.Println()
-		fmt.Println("This tool will format all ECAL files in a directory structure.")
-		fmt.Println()
+		fmt.Fprintln(flag.CommandLine.Output())
+		fmt.Fprintln(flag.CommandLine.Output(), "This tool will format all ECAL files in a directory structure.")
+		fmt.Fprintln(flag.CommandLine.Output())
 	}
 
 	if len(os.Args) >= 2 {
-		flag.CommandLine.Parse(os.Args[2:])
+		flag.CommandLine.Parse(osArgs[2:])
 
 		if *showHelp {
 			flag.Usage()
@@ -48,26 +52,28 @@ func Format() error {
 		}
 	}
 
-	fmt.Println(fmt.Sprintf("Formatting all %v files in %v", *ext, *dir))
+	fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Formatting all %v files in %v", *ext, *dir))
 
-	err = filepath.Walk(".",
+	err = filepath.Walk(*dir,
 		func(path string, i os.FileInfo, err error) error {
 			if err == nil && !i.IsDir() {
 				var data []byte
 				var ast *parser.ASTNode
 				var srcFormatted string
 
-				if data, err = ioutil.ReadFile(path); err == nil {
-					var ferr error
+				if strings.HasSuffix(path, *ext) {
+					if data, err = ioutil.ReadFile(path); err == nil {
+						var ferr error
 
-					if ast, ferr = parser.Parse(path, string(data)); ferr == nil {
-						if srcFormatted, ferr = parser.PrettyPrint(ast); ferr == nil {
-							ioutil.WriteFile(path, []byte(srcFormatted), i.Mode())
+						if ast, ferr = parser.Parse(path, string(data)); ferr == nil {
+							if srcFormatted, ferr = parser.PrettyPrint(ast); ferr == nil {
+								ioutil.WriteFile(path, []byte(srcFormatted), i.Mode())
+							}
 						}
-					}
 
-					if ferr != nil {
-						fmt.Fprintln(os.Stderr, fmt.Sprintf("Could not format %v: %v", path, ferr))
+						if ferr != nil {
+							fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Could not format %v: %v", path, ferr))
+						}
 					}
 				}
 			}

+ 122 - 0
cli/tool/format_test.go

@@ -0,0 +1,122 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package tool
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/fileutil"
+)
+
+const formatTestDir = "formattest"
+
+func setupFormatTestDir() {
+
+	if res, _ := fileutil.PathExists(formatTestDir); res {
+		os.RemoveAll(formatTestDir)
+	}
+
+	err := os.Mkdir(formatTestDir, 0770)
+	if err != nil {
+		fmt.Print("Could not create test directory:", err.Error())
+		os.Exit(1)
+	}
+}
+
+func tearDownFormatTestDir() {
+	err := os.RemoveAll(formatTestDir)
+	if err != nil {
+		fmt.Print("Could not remove test directory:", err.Error())
+	}
+}
+
+func TestFormat(t *testing.T) {
+	setupFormatTestDir()
+	defer tearDownFormatTestDir()
+
+	out := bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{"foo", "bar", "-help"}
+
+	if err := Format(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if !strings.Contains(out.String(), "Root directory for ECAL files") {
+		t.Error("Unexpected output:", out.String())
+		return
+	}
+
+	myfile := filepath.Join(formatTestDir, "myfile.ecal")
+	myfile2 := filepath.Join(formatTestDir, "myfile.eca")
+	myfile3 := filepath.Join(formatTestDir, "myinvalidfile.ecal")
+
+	originalContent := "if a == 1 { b := 1 }"
+
+	err := ioutil.WriteFile(myfile, []byte(originalContent), 0777)
+	errorutil.AssertOk(err)
+
+	err = ioutil.WriteFile(myfile2, []byte(originalContent), 0777)
+	errorutil.AssertOk(err)
+
+	err = ioutil.WriteFile(myfile3, []byte(originalContent[5:]), 0777)
+	errorutil.AssertOk(err)
+
+	out = bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{"foo", "bar", "-dir", formatTestDir}
+
+	if err := Format(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if out.String() != `Formatting all .ecal files in formattest
+Could not format formattest/myinvalidfile.ecal: Parse error in formattest/myinvalidfile.ecal: Term cannot start an expression (==) (Line:1 Pos:1)
+` {
+		t.Error("Unexpected output:", out.String())
+		return
+	}
+
+	myfileContent, err := ioutil.ReadFile(myfile)
+	errorutil.AssertOk(err)
+
+	if string(myfileContent) != `if a == 1 {
+    b := 1
+}
+` {
+		t.Error("Unexpected result:", string(myfileContent))
+		return
+	}
+
+	myfileContent, err = ioutil.ReadFile(myfile2)
+	errorutil.AssertOk(err)
+
+	if string(myfileContent) != originalContent {
+		t.Error("Unexpected result:", string(myfileContent))
+		return
+	}
+}

+ 17 - 0
cli/tool/helper.go

@@ -12,12 +12,29 @@ package tool
 
 import (
 	"fmt"
+	"io"
+	"os"
 	"regexp"
 	"strings"
 
 	"devt.de/krotik/common/stringutil"
 )
 
+/*
+osArgs is a local copy of os.Args (used for unit tests)
+*/
+var osArgs = os.Args
+
+/*
+osStderr is a local copy of os.Stderr (used for unit tests)
+*/
+var osStderr io.Writer = os.Stderr
+
+/*
+osExit is a local variable pointing to os.Exit (used for unit tests)
+*/
+var osExit func(int) = os.Exit
+
 /*
 CLIInputHandler is a handler object for CLI input.
 */

+ 123 - 0
cli/tool/helper_test.go

@@ -0,0 +1,123 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package tool
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"strings"
+	"testing"
+
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/common/termutil"
+)
+
+type testConsoleLineTerminal struct {
+	in  []string
+	out bytes.Buffer
+}
+
+func (t *testConsoleLineTerminal) StartTerm() error {
+	return nil
+}
+
+func (t *testConsoleLineTerminal) AddKeyHandler(handler termutil.KeyHandler) {
+}
+
+func (t *testConsoleLineTerminal) NextLine() (string, error) {
+	var err error
+	var ret string
+
+	if len(t.in) > 0 {
+		ret = t.in[0]
+		t.in = t.in[1:]
+	} else {
+		err = fmt.Errorf("Input is empty in testConsoleLineTerminal")
+	}
+	return ret, err
+}
+
+func (t *testConsoleLineTerminal) NextLinePrompt(prompt string, echo rune) (string, error) {
+	return t.NextLine()
+}
+
+func (t *testConsoleLineTerminal) WriteString(s string) {
+	t.out.WriteString(s)
+}
+
+func (t *testConsoleLineTerminal) Write(p []byte) (n int, err error) {
+	return t.out.Write(p)
+}
+
+func (t *testConsoleLineTerminal) StopTerm() {
+}
+
+type testCustomHandler struct {
+}
+
+func (t *testCustomHandler) CanHandle(s string) bool {
+	return s == "@cus"
+}
+
+func (t *testCustomHandler) Handle(ot OutputTerminal, input string) {}
+
+func (t *testCustomHandler) LoadInitialFile(tid uint64) error {
+	return nil
+}
+
+type testOutputTerminal struct {
+	b bytes.Buffer
+}
+
+func (t *testOutputTerminal) WriteString(s string) {
+	t.b.WriteString(s)
+}
+
+func TestMatchesFulltextSearch(t *testing.T) {
+	ot := &testOutputTerminal{}
+
+	ok := matchesFulltextSearch(ot, "abc", "s[")
+
+	if !ok && strings.HasPrefix(ot.b.String(), "Invalid search expression") {
+		t.Error("Unexpected result:", ot.b.String(), ok)
+		return
+	}
+
+	ot.b = bytes.Buffer{}
+
+	ok = matchesFulltextSearch(ot, "abc", "a*")
+
+	if !ok || ot.b.String() != "" {
+		t.Error("Unexpected result:", ot.b.String(), ok)
+		return
+	}
+
+	ok = matchesFulltextSearch(ot, "abc", "ac*")
+
+	if ok || ot.b.String() != "" {
+		t.Error("Unexpected result:", ot.b.String(), ok)
+		return
+	}
+}
+
+func TestFillTableRow(t *testing.T) {
+
+	res := fillTableRow([]string{}, "test", stringutil.GenerateRollingString("123 ", 100))
+
+	b, _ := json.Marshal(&res)
+
+	if string(b) != `["test","123 123 123 123 123 123 123 123 123 123 123 123 `+
+		`123 123 123 123 123 123 123 123","","123 123 123 123 123","",""]` {
+		t.Error("Unexpected result:", string(b))
+		return
+	}
+}

+ 61 - 40
cli/tool/interpret.go

@@ -62,6 +62,10 @@ type CLIInterpreter struct {
 	LogFile  *string // Logfile (blank for stdout)
 	LogLevel *string // Log level string (Debug, Info, Error)
 
+	// User terminal
+
+	Term termutil.ConsoleLineTerminal
+
 	// Log output
 
 	LogOut io.Writer
@@ -71,7 +75,7 @@ type CLIInterpreter struct {
 NewCLIInterpreter creates a new commandline interpreter for ECAL.
 */
 func NewCLIInterpreter() *CLIInterpreter {
-	return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "", "", nil, nil, nil, os.Stdout}
+	return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "", "", nil, nil, nil, nil, os.Stdout}
 }
 
 /*
@@ -92,15 +96,15 @@ func (i *CLIInterpreter) ParseArgs() bool {
 	showHelp := flag.Bool("help", false, "Show this help message")
 
 	flag.Usage = func() {
-		fmt.Println()
-		fmt.Println(fmt.Sprintf("Usage of %s run [options] [file]", os.Args[0]))
-		fmt.Println()
+		fmt.Fprintln(flag.CommandLine.Output())
+		fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Usage of %s run [options] [file]", osArgs[0]))
+		fmt.Fprintln(flag.CommandLine.Output())
 		flag.PrintDefaults()
-		fmt.Println()
+		fmt.Fprintln(flag.CommandLine.Output())
 	}
 
-	if len(os.Args) >= 2 {
-		flag.CommandLine.Parse(os.Args[2:])
+	if len(osArgs) >= 2 {
+		flag.CommandLine.Parse(osArgs[2:])
 
 		if cargs := flag.Args(); len(cargs) > 0 {
 			i.EntryFile = flag.Arg(0)
@@ -115,7 +119,7 @@ func (i *CLIInterpreter) ParseArgs() bool {
 }
 
 /*
-Create the runtime provider of this interpreter. This function expects Dir,
+CreateRuntimeProvider creates the runtime provider of this interpreter. This function expects Dir,
 LogFile and LogLevel to be set.
 */
 func (i *CLIInterpreter) CreateRuntimeProvider(name string) error {
@@ -130,6 +134,7 @@ func (i *CLIInterpreter) CreateRuntimeProvider(name string) error {
 
 	if i.LogFile != nil && *i.LogFile != "" {
 		var logWriter io.Writer
+
 		logFileRollover := fileutil.SizeBasedRolloverCondition(1000000) // Each file can be up to a megabyte
 		logWriter, err = fileutil.NewMultiFileBuffer(*i.LogFile, fileutil.ConsecutiveNumberIterator(10), logFileRollover)
 		logger = util.NewBufferLogger(logWriter)
@@ -180,21 +185,36 @@ func (i *CLIInterpreter) LoadInitialFile(tid uint64) error {
 
 		initFile, err = ioutil.ReadFile(i.EntryFile)
 
-		if ast, err = parser.ParseWithRuntime(i.EntryFile, string(initFile), i.RuntimeProvider); err == nil {
-			if err = ast.Runtime.Validate(); err == nil {
-				_, err = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}), tid)
-			}
-			defer func() {
-				if i.RuntimeProvider.Debugger != nil {
-					i.RuntimeProvider.Debugger.RecordThreadFinished(tid)
+		if err == nil {
+			if ast, err = parser.ParseWithRuntime(i.EntryFile, string(initFile), i.RuntimeProvider); err == nil {
+				if err = ast.Runtime.Validate(); err == nil {
+					_, err = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}), tid)
 				}
-			}()
+				defer func() {
+					if i.RuntimeProvider.Debugger != nil {
+						i.RuntimeProvider.Debugger.RecordThreadFinished(tid)
+					}
+				}()
+			}
 		}
 	}
 
 	return err
 }
 
+/*
+CreateTerm creates a new console terminal for stdout.
+*/
+func (i *CLIInterpreter) CreateTerm() error {
+	var err error
+
+	if i.Term == nil {
+		i.Term, err = termutil.NewConsoleLineTerminal(os.Stdout)
+	}
+
+	return err
+}
+
 /*
 Interpret starts the ECAL code interpreter. Starts an interactive console in
 the current tty if the interactive flag is set.
@@ -205,7 +225,7 @@ func (i *CLIInterpreter) Interpret(interactive bool) error {
 		return nil
 	}
 
-	clt, err := termutil.NewConsoleLineTerminal(os.Stdout)
+	err := i.CreateTerm()
 
 	if interactive {
 		fmt.Fprintln(i.LogOut, fmt.Sprintf("ECAL %v", config.ProductVersion))
@@ -235,39 +255,33 @@ func (i *CLIInterpreter) Interpret(interactive bool) error {
 
 			if err = i.LoadInitialFile(tid); err == nil {
 
-				if interactive {
+				// Drop into interactive shell
 
-					// Drop into interactive shell
-
-					if err == nil {
-						isExitLine := func(s string) bool {
-							return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
-						}
+				if interactive {
 
-						// Add history functionality without file persistence
+					// Add history functionality without file persistence
 
-						clt, err = termutil.AddHistoryMixin(clt, "",
-							func(s string) bool {
-								return isExitLine(s)
-							})
+					i.Term, err = termutil.AddHistoryMixin(i.Term, "",
+						func(s string) bool {
+							return i.isExitLine(s)
+						})
 
-						if err == nil {
+					if err == nil {
 
-							if err = clt.StartTerm(); err == nil {
-								var line string
+						if err = i.Term.StartTerm(); err == nil {
+							var line string
 
-								defer clt.StopTerm()
+							defer i.Term.StopTerm()
 
-								fmt.Fprintln(i.LogOut, "Type 'q' or 'quit' to exit the shell and '?' to get help")
+							fmt.Fprintln(i.LogOut, "Type 'q' or 'quit' to exit the shell and '?' to get help")
 
-								line, err = clt.NextLine()
-								for err == nil && !isExitLine(line) {
-									trimmedLine := strings.TrimSpace(line)
+							line, err = i.Term.NextLine()
+							for err == nil && !i.isExitLine(line) {
+								trimmedLine := strings.TrimSpace(line)
 
-									i.HandleInput(clt, trimmedLine, tid)
+								i.HandleInput(i.Term, trimmedLine, tid)
 
-									line, err = clt.NextLine()
-								}
+								line, err = i.Term.NextLine()
 							}
 						}
 					}
@@ -279,6 +293,13 @@ func (i *CLIInterpreter) Interpret(interactive bool) error {
 	return err
 }
 
+/*
+isExitLine returns if a given input line should exit the interpreter.
+*/
+func (i *CLIInterpreter) isExitLine(s string) bool {
+	return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
+}
+
 /*
 HandleInput handles input to this interpreter. It parses a given input line
 and outputs on the given output terminal. Requires a thread ID of the executing

+ 355 - 0
cli/tool/interpret_test.go

@@ -0,0 +1,355 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package tool
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"reflect"
+	"strconv"
+	"strings"
+	"testing"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/fileutil"
+	"devt.de/krotik/ecal/config"
+	"devt.de/krotik/ecal/interpreter"
+	"devt.de/krotik/ecal/stdlib"
+	"devt.de/krotik/ecal/util"
+)
+
+const testDir = "tooltest"
+
+var testLogOut *bytes.Buffer
+var testTerm *testConsoleLineTerminal
+
+func newTestInterpreter() *CLIInterpreter {
+	tin := NewCLIInterpreter()
+
+	// Redirect I/O bits into internal buffers
+
+	testTerm = &testConsoleLineTerminal{nil, bytes.Buffer{}}
+	tin.Term = testTerm
+
+	testLogOut = &bytes.Buffer{}
+	tin.LogOut = testLogOut
+
+	return tin
+}
+
+func newTestInterpreterWithConfig() *CLIInterpreter {
+	tin := newTestInterpreter()
+
+	// Setup
+	if res, _ := fileutil.PathExists(testDir); res {
+		os.RemoveAll(testDir)
+	}
+
+	err := os.Mkdir(testDir, 0770)
+	if err != nil {
+		fmt.Print("Could not create test directory:", err.Error())
+		os.Exit(1)
+	}
+
+	l := testDir
+	tin.Dir = &l
+
+	tin.CustomWelcomeMessage = "123"
+
+	// Teardown
+
+	return tin
+}
+
+func tearDown() {
+	err := os.RemoveAll(testDir)
+	if err != nil {
+		fmt.Print("Could not remove test directory:", err.Error())
+	}
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing
+}
+
+func TestInterpretBasicFunctions(t *testing.T) {
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing
+
+	// Test normal initialisation
+
+	tin := NewCLIInterpreter()
+	if err := tin.CreateTerm(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	tin = newTestInterpreter()
+
+	// Test help output
+
+	osArgs = []string{"foo", "bar", "-help"}
+
+	flag.CommandLine.SetOutput(&testTerm.out)
+
+	if stop := tin.ParseArgs(); !stop {
+		t.Error("Asking for help should request to stop the program")
+		return
+	}
+
+	if !strings.Contains(testTerm.out.String(), "Root directory for ECAL interpreter") {
+		t.Error("Helptext does not contain expected string - output:", testTerm.out.String())
+		return
+	}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing
+
+	// Test interpret
+
+	tin = newTestInterpreter()
+
+	osArgs = []string{"foo", "bar", "-help"}
+
+	flag.CommandLine.SetOutput(&testTerm.out)
+
+	errorutil.AssertOk(tin.Interpret(true))
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing
+
+	// Test entry file parsing
+
+	tin = NewCLIInterpreter()
+
+	osArgs = []string{"foo", "bar", "myfile"}
+
+	if stop := tin.ParseArgs(); stop {
+		t.Error("Giving an entry file should not stop the program")
+		return
+	}
+
+	if stop := tin.ParseArgs(); stop {
+		t.Error("Giving an entry file should not stop the program")
+		return
+	}
+
+	if tin.EntryFile != "myfile" {
+		t.Error("Unexpected entryfile:", tin.EntryFile)
+		return
+	}
+}
+
+func TestCreateRuntimeProvider(t *testing.T) {
+	tin := newTestInterpreterWithConfig()
+	defer tearDown()
+
+	l := filepath.Join(testDir, "test.log")
+	tin.LogFile = &l
+
+	if err := tin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if _, ok := tin.RuntimeProvider.Logger.(*util.BufferLogger); !ok {
+		t.Errorf("Unexpected logger: %#v", tin.RuntimeProvider.Logger)
+		return
+	}
+
+	tin = newTestInterpreterWithConfig()
+	defer tearDown()
+
+	l = "error"
+	tin.LogLevel = &l
+
+	if err := tin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if _, ok := tin.RuntimeProvider.Logger.(*util.LogLevelLogger); !ok {
+		t.Errorf("Unexpected logger: %#v", tin.RuntimeProvider.Logger)
+		return
+	}
+
+	if err := tin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if _, ok := tin.RuntimeProvider.Logger.(*util.LogLevelLogger); !ok {
+		t.Errorf("Unexpected logger: %#v", tin.RuntimeProvider.Logger)
+		return
+	}
+}
+
+func TestLoadInitialFile(t *testing.T) {
+	tin := NewCLIDebugInterpreter(newTestInterpreterWithConfig())
+	defer tearDown()
+
+	if err := tin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	tin.RuntimeProvider.Debugger = interpreter.NewECALDebugger(tin.GlobalVS)
+	tin.RuntimeProvider.Logger = util.NewMemoryLogger(10)
+	tin.RuntimeProvider.ImportLocator = &util.MemoryImportLocator{}
+
+	tin.EntryFile = filepath.Join(testDir, "foo.ecal")
+
+	ioutil.WriteFile(tin.EntryFile, []byte("a := 1"), 0777)
+
+	if err := tin.CLIInterpreter.LoadInitialFile(1); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if tin.GlobalVS.String() != `GlobalScope {
+    a (float64) : 1
+}` {
+		t.Error("Unexpected scope:", tin.GlobalVS)
+		return
+	}
+}
+
+func TestInterpret(t *testing.T) {
+	tin := newTestInterpreterWithConfig()
+	defer tearDown()
+
+	if err := tin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	tin.RuntimeProvider.Logger, _ = util.NewLogLevelLogger(util.NewMemoryLogger(10), "info")
+	tin.RuntimeProvider.ImportLocator = &util.MemoryImportLocator{
+		Files: map[string]string{
+			"foo": "a := 1",
+		},
+	}
+
+	l1 := ""
+	tin.LogFile = &l1
+	l2 := ""
+	tin.LogLevel = &l2
+
+	testTerm.in = []string{"xxx := 1", "q"}
+
+	if err := tin.Interpret(true); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if testLogOut.String() != `ECAL `+config.ProductVersion+`
+Log level: info - Root directory: tooltest
+123
+Type 'q' or 'quit' to exit the shell and '?' to get help
+` {
+		t.Error("Unexpected result:", testLogOut.String())
+		return
+	}
+
+	if tin.GlobalVS.String() != `GlobalScope {
+    xxx (float64) : 1
+}` {
+		t.Error("Unexpected scope:", tin.GlobalVS)
+		return
+	}
+}
+
+func TestHandleInput(t *testing.T) {
+	tin := newTestInterpreterWithConfig()
+	defer tearDown()
+
+	tin.CustomHandler = &testCustomHandler{}
+
+	if err := tin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	stdlib.AddStdlibPkg("foo", "bar")
+	stdlib.AddStdlibFunc("foo", "Println",
+		stdlib.NewECALFunctionAdapter(reflect.ValueOf(fmt.Println), "xxx"))
+	stdlib.AddStdlibFunc("foo", "Atoi",
+		stdlib.NewECALFunctionAdapter(reflect.ValueOf(strconv.Atoi), "xxx"))
+
+	tin.RuntimeProvider.Logger, _ = util.NewLogLevelLogger(util.NewMemoryLogger(10), "info")
+	tin.RuntimeProvider.ImportLocator = &util.MemoryImportLocator{}
+	tin.CustomHelpString = "123"
+
+	l1 := ""
+	tin.LogFile = &l1
+	l2 := ""
+	tin.LogLevel = &l2
+
+	testTerm.in = []string{"?", "@reload", "@sym", "@std", "@cus", "q"}
+
+	if err := tin.Interpret(true); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	// Just check for a simple string no need for the whole thing
+
+	if !strings.Contains(testTerm.out.String(), "New creates a new object instance.") {
+		t.Error("Unexpected result:", testTerm.out.String())
+		return
+	}
+
+	testTerm.out.Reset()
+
+	testTerm.in = []string{"@sym raise", "@std math.Phi", "@std foo Print", "q"}
+
+	if err := tin.Interpret(true); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if testTerm.out.String() != `╒═════════════════╤═══════════════════════════════╕
+│Inbuild function │Description                    │
+╞═════════════════╪═══════════════════════════════╡
+│raise            │Raise returns an error object. │
+│                 │                               │
+╘═════════════════╧═══════════════════════════════╛
+╒═════════╤══════════════════╕
+│Constant │Value             │
+╞═════════╪══════════════════╡
+│math.Phi │1.618033988749895 │
+│         │                  │
+╘═════════╧══════════════════╛
+╒════════════╤════════════╕
+│Function    │Description │
+╞════════════╪════════════╡
+│foo.Println │xxx         │
+│            │            │
+╘════════════╧════════════╛
+` {
+		t.Error("Unexpected result:", testTerm.out.String())
+		return
+	}
+
+	testTerm.out.Reset()
+
+	testTerm.in = []string{"1", "raise(123)", "q"}
+
+	if err := tin.Interpret(true); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if testTerm.out.String() != `1
+ECAL error in foo: 123 () (Line:1 Pos:1)
+` {
+		t.Error("Unexpected result:", testTerm.out.String())
+		return
+	}
+}

+ 47 - 43
cli/tool/pack.go

@@ -47,6 +47,9 @@ type CLIPacker struct {
 	LogOut io.Writer
 }
 
+var packmarkerend = "####"
+var packmarker = fmt.Sprintf("\n%v%v%v\n", packmarkerend, "ECALSRC", packmarkerend)
+
 /*
 NewCLIPacker creates a new commandline packer.
 */
@@ -63,7 +66,7 @@ func (p *CLIPacker) ParseArgs() bool {
 		return false
 	}
 
-	binname, err := filepath.Abs(os.Args[0])
+	binname, err := filepath.Abs(osArgs[0])
 	errorutil.AssertOk(err)
 
 	wd, _ := os.Getwd()
@@ -74,18 +77,18 @@ func (p *CLIPacker) ParseArgs() bool {
 	showHelp := flag.Bool("help", false, "Show this help message")
 
 	flag.Usage = func() {
-		fmt.Println()
-		fmt.Println(fmt.Sprintf("Usage of %s pack [options] [entry file]", os.Args[0]))
-		fmt.Println()
+		fmt.Fprintln(flag.CommandLine.Output())
+		fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Usage of %s pack [options] [entry file]", os.Args[0]))
+		fmt.Fprintln(flag.CommandLine.Output())
 		flag.PrintDefaults()
-		fmt.Println()
-		fmt.Println("This tool will collect all files in the root directory and " +
+		fmt.Fprintln(flag.CommandLine.Output())
+		fmt.Fprintln(flag.CommandLine.Output(), "This tool will collect all files in the root directory and "+
 			"build a standalone executable from the given source binary and the collected files.")
-		fmt.Println()
+		fmt.Fprintln(flag.CommandLine.Output())
 	}
 
 	if len(os.Args) >= 2 {
-		flag.CommandLine.Parse(os.Args[2:])
+		flag.CommandLine.Parse(osArgs[2:])
 
 		if cargs := flag.Args(); len(cargs) > 0 {
 			p.EntryFile = flag.Arg(0)
@@ -128,10 +131,7 @@ func (p *CLIPacker) Pack() error {
 				fmt.Fprintln(p.LogOut, fmt.Sprintf("Copied %v bytes for interpreter.", bytes))
 				var bytes int
 
-				end := "####"
-				marker := fmt.Sprintf("\n%v%v%v\n", end, "ECALSRC", end)
-
-				if bytes, err = dest.WriteString(marker); err == nil {
+				if bytes, err = dest.WriteString(packmarker); err == nil {
 					var data []byte
 					fmt.Fprintln(p.LogOut, fmt.Sprintf("Writing marker %v bytes for source archive.", bytes))
 
@@ -147,11 +147,12 @@ func (p *CLIPacker) Pack() error {
 
 								// Add files to the archive
 
-								if err = p.packFiles(w, *p.Dir, ""); err == nil {
-									err = w.Close()
-
+								defer func() {
+									w.Close()
 									os.Chmod(*p.TargetBinary, 0775) // Try a chmod but don't care about any errors
-								}
+								}()
+
+								err = p.packFiles(w, *p.Dir, "")
 							}
 						}
 					}
@@ -193,6 +194,16 @@ func (p *CLIPacker) packFiles(w *zip.Writer, filePath string, zipPath string) er
 	return err
 }
 
+var ( // Internal reading buffers
+	b1 = 4096
+	b2 = len(packmarker) + 11
+)
+
+/*
+handleError is the error handling function for runtime errors in packed binaries.
+*/
+var handleError func(error) = errorutil.AssertOk
+
 /*
 RunPackedBinary runs ECAL code is it has been attached to the currently running binary.
 Exits if attached ECAL code has been executed.
@@ -201,12 +212,9 @@ func RunPackedBinary() {
 	var retCode = 0
 	var result bool
 
-	exename, err := filepath.Abs(os.Args[0])
+	exename, err := filepath.Abs(osArgs[0])
 	errorutil.AssertOk(err)
 
-	end := "####"
-	marker := fmt.Sprintf("\n%v%v%v\n", end, "ECALSRC", end)
-
 	if ok, _ := fileutil.PathExists(exename); !ok {
 
 		// Try an optional .exe suffix which might work on Windows
@@ -224,8 +232,8 @@ func RunPackedBinary() {
 			defer f.Close()
 
 			found := false
-			buf := make([]byte, 4096)
-			buf2 := make([]byte, len(marker)+11)
+			buf := make([]byte, b1)
+			buf2 := make([]byte, b2)
 
 			// Look for the marker which marks the beginning of the attached zip file
 
@@ -240,12 +248,12 @@ func RunPackedBinary() {
 					if i2, err := f.Read(buf2); err == nil || err == io.EOF {
 						candidateString := string(append(buf, buf2...))
 
-						// Now determine the position if the zip file
+						// Now determine the position of the zip file
 
-						markerIndex := strings.Index(candidateString, marker)
+						markerIndex := strings.Index(candidateString, packmarker)
 
 						if found = markerIndex >= 0; found {
-							start := int64(markerIndex + len(marker))
+							start := int64(markerIndex + len(packmarker))
 							for unicode.IsSpace(rune(candidateString[start])) || unicode.IsControl(rune(candidateString[start])) {
 								start++ // Skip final control characters \n or \r\n
 							}
@@ -271,9 +279,8 @@ func RunPackedBinary() {
 
 					ret, err = runInterpreter(io.NewSectionReader(f, pos, zipLen), zipLen)
 
-					if retNum, ok := ret.(float64); ok {
-						retCode = int(retNum)
-					}
+					retNum, _ := ret.(float64)
+					retCode = int(retNum)
 
 					result = err == nil
 				}
@@ -281,10 +288,10 @@ func RunPackedBinary() {
 		}
 	}
 
-	errorutil.AssertOk(err)
+	handleError(err)
 
 	if result {
-		os.Exit(retCode)
+		osExit(retCode)
 	}
 }
 
@@ -299,27 +306,24 @@ func runInterpreter(reader io.ReaderAt, size int64) (interface{}, error) {
 	if err == nil {
 
 		for _, f := range r.File {
+			if err == nil {
+				if rc, err = f.Open(); err == nil {
+					var data []byte
 
-			if rc, err = f.Open(); err == nil {
-				var data []byte
-
-				defer rc.Close()
+					defer rc.Close()
 
-				if data, err = ioutil.ReadAll(rc); err == nil {
-					il.Files[f.Name] = string(data)
+					if data, err = ioutil.ReadAll(rc); err == nil {
+						il.Files[f.Name] = string(data)
+					}
 				}
 			}
-
-			if err != nil {
-				break
-			}
 		}
 	}
 
 	if err == nil {
 		var ast *parser.ASTNode
 
-		erp := interpreter.NewECALRuntimeProvider(os.Args[0], il, util.NewStdOutLogger())
+		erp := interpreter.NewECALRuntimeProvider(osArgs[0], il, util.NewStdOutLogger())
 
 		if ast, err = parser.ParseWithRuntime(os.Args[0], il.Files[".ecalsrc-entry"], erp); err == nil {
 			if err = ast.Runtime.Validate(); err == nil {
@@ -334,10 +338,10 @@ func runInterpreter(reader io.ReaderAt, size int64) (interface{}, error) {
 				res, err = ast.Runtime.Eval(vs, make(map[string]interface{}), erp.NewThreadID())
 
 				if err != nil {
-					fmt.Fprintln(os.Stderr, err.Error())
+					fmt.Fprintln(osStderr, err.Error())
 
 					if terr, ok := err.(util.TraceableRuntimeError); ok {
-						fmt.Fprintln(os.Stderr, fmt.Sprint("  ", strings.Join(terr.GetTraceString(), fmt.Sprint(fmt.Sprintln(), "  "))))
+						fmt.Fprintln(osStderr, fmt.Sprint("  ", strings.Join(terr.GetTraceString(), fmt.Sprint(fmt.Sprintln(), "  "))))
 					}
 
 					err = nil

+ 278 - 0
cli/tool/pack_test.go

@@ -0,0 +1,278 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package tool
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/fileutil"
+	"devt.de/krotik/common/stringutil"
+)
+
+const packTestDir = "packtest"
+
+var testPackOut *bytes.Buffer
+
+var lastReturnCode = 0
+var lastRuntimeError error
+
+func setupPackTestDir() {
+	if res, _ := fileutil.PathExists(packTestDir); res {
+		os.RemoveAll(packTestDir)
+	}
+
+	err := os.Mkdir(packTestDir, 0770)
+	if err != nil {
+		fmt.Print("Could not create test directory:", err.Error())
+		os.Exit(1)
+	}
+
+	err = os.Mkdir(filepath.Join(packTestDir, "sub"), 0770)
+	if err != nil {
+		fmt.Print("Could not create test directory:", err.Error())
+		os.Exit(1)
+	}
+
+	osExit = func(code int) {
+		lastReturnCode = code
+	}
+
+	handleError = func(err error) {
+		lastRuntimeError = err
+	}
+}
+
+func tearDownPackTestDir() {
+	err := os.RemoveAll(packTestDir)
+	if err != nil {
+		fmt.Print("Could not remove test directory:", err.Error())
+	}
+}
+
+func newTestCLIPacker() *CLIPacker {
+	clip := NewCLIPacker()
+
+	testPackOut = &bytes.Buffer{}
+	clip.LogOut = testPackOut
+
+	return clip
+}
+
+func TestPackParseArgs(t *testing.T) {
+	setupPackTestDir()
+	defer tearDownPackTestDir()
+
+	clip := newTestCLIPacker()
+
+	packTestSrcBin := filepath.Join(packTestDir, "source.bin")
+	out := bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{packTestSrcBin, "foo", "-help"}
+
+	if ok := clip.ParseArgs(); !ok {
+		t.Error("Asking for help should ask to finish the program")
+		return
+	}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{packTestSrcBin, "foo", "-help"}
+
+	if err := clip.Pack(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if !strings.Contains(out.String(), "Root directory for ECAL interpreter") {
+		t.Error("Unexpected output:", out.String())
+		return
+	}
+
+	out = bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{packTestSrcBin, "foo", "myentryfile"}
+
+	if ok := clip.ParseArgs(); ok {
+		t.Error("Only asking for help should finish the program")
+		return
+	}
+
+	if ok := clip.ParseArgs(); ok {
+		t.Error("Only asking for help should finish the program")
+		return
+	}
+
+	if clip.EntryFile != "myentryfile" {
+		t.Error("Unexpected output:", clip.EntryFile)
+		return
+	}
+}
+
+func TestPackPacking(t *testing.T) {
+	setupPackTestDir()
+	defer tearDownPackTestDir()
+
+	clip := newTestCLIPacker()
+
+	packTestSrcBin := filepath.Join(packTestDir, "source.bin")
+	packTestEntry := filepath.Join(packTestDir, "myentry.ecal")
+	packAnotherFile := filepath.Join(packTestDir, "sub", "anotherfile.ecal")
+	packTestDestBin := filepath.Join(packTestDir, "dest.exe")
+
+	b1 = 5
+	b2 = len(packmarker) + 11
+
+	err := ioutil.WriteFile(packTestSrcBin, []byte("mybinaryfilecontent#somemorecontent"+
+		stringutil.GenerateRollingString("123", 30)), 0777)
+	errorutil.AssertOk(err)
+
+	err = ioutil.WriteFile(packTestEntry, []byte("myvar := 1; 5"), 0777)
+	errorutil.AssertOk(err)
+
+	err = ioutil.WriteFile(packAnotherFile, []byte("func f() { raise(123) };f()"), 0777)
+	errorutil.AssertOk(err)
+
+	out := bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	// Write a binary with return code
+
+	osArgs = []string{packTestSrcBin, "foo", "-dir", packTestDir, "-target",
+		packTestDestBin, packTestEntry}
+
+	// Simulate that whitespaces are added around the pack marker
+
+	oldpackmarker := packmarker
+	packmarker = fmt.Sprintf("\n\n\n%v\n\n\n", packmarker)
+
+	if err := clip.Pack(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	packmarker = oldpackmarker
+
+	if !strings.Contains(testPackOut.String(), "bytes for intro") {
+		t.Error("Unexpected output:", testPackOut.String())
+		return
+	}
+
+	// Write a binary with which errors
+
+	clip = newTestCLIPacker()
+
+	out = bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{packTestSrcBin, "foo", "-dir", packTestDir, "-target",
+		packTestDestBin + ".error", packAnotherFile}
+
+	if err := clip.Pack(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if !strings.Contains(testPackOut.String(), "bytes for intro") {
+		t.Error("Unexpected output:", testPackOut.String())
+		return
+	}
+
+	// Write also a corrupted binary
+
+	err = ioutil.WriteFile(packTestDestBin+".corrupted", []byte(
+		"mybinaryfilecontent#somemorecontent"+
+			stringutil.GenerateRollingString("123", 30)+
+			"\n"+
+			packmarker+
+			"\n"+
+			stringutil.GenerateRollingString("123", 30)), 0777)
+
+	errorutil.AssertOk(err)
+
+	testRunningPackedBinary(t)
+}
+
+func testRunningPackedBinary(t *testing.T) {
+	packTestDestBin := filepath.Join(packTestDir, "dest") // Suffix .exe should be appended
+
+	out := bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{packTestDestBin + ".exe.corrupted"}
+
+	RunPackedBinary()
+
+	if lastRuntimeError == nil || lastRuntimeError.Error() != "zip: not a valid zip file" {
+		t.Error("Unexpected result:", lastRuntimeError)
+		return
+	}
+
+	out = bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{packTestDestBin}
+
+	RunPackedBinary()
+
+	if lastRuntimeError != nil {
+		t.Error("Unexpected result:", lastRuntimeError)
+		return
+	}
+
+	if lastReturnCode != 5 {
+		t.Error("Unexpected result:", lastReturnCode)
+		return
+	}
+
+	out = bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+	osStderr = &out
+
+	osArgs = []string{packTestDestBin + ".exe.error"}
+
+	RunPackedBinary()
+
+	if lastRuntimeError != nil {
+		t.Error("Unexpected result:", lastRuntimeError)
+		return
+	}
+
+	if !strings.HasPrefix(out.String(), "ECAL error in packtest/dest.exe.error: 123 () (Line:1 Pos:12)") ||
+		!strings.Contains(out.String(), "raise(123)") {
+		t.Error("Unexpected result:", out.String())
+		return
+	}
+}

+ 1 - 1
ecal-support/README.md

@@ -16,7 +16,7 @@ The extension supports the following features:
 
 ## Install the extension
 
-The extention can be installed using a precompiled VSIX file which can be downloaded from here:
+The extension can be installed using a precompiled VSIX file which can be downloaded from here:
 
 https://devt.de/krotik/ecal/releases
 

+ 1 - 1
ecal-support/src/ecalDebugAdapter.ts

@@ -28,7 +28,7 @@ import * as path from "path";
 
 /**
  * ECALDebugArguments are the arguments which VSCode can pass to the debug adapter.
- * This defines the parameter which a VSCode instance using the ECAL extention can pass to the
+ * This defines the parameter which a VSCode instance using the ECAL extension can pass to the
  * debug adapter from a lauch configuration ('.vscode/launch.json') in a project folder.
  */
 interface ECALDebugArguments extends DebugProtocol.LaunchRequestArguments {

+ 1 - 1
ecal-support/src/extension.ts

@@ -18,7 +18,7 @@ class InlineDebugAdapterFactory
   createDebugAdapterDescriptor(
     _session: vscode.DebugSession
   ): ProviderResult<vscode.DebugAdapterDescriptor> {
-    // Declare the ECALDebugSession as an DebugAdapterInlineImplementation so extention and adapter can
+    // Declare the ECALDebugSession as an DebugAdapterInlineImplementation so extension and adapter can
     // run in-process (making it possible to easily debug the adapter)
     return new vscode.DebugAdapterInlineImplementation(new ECALDebugSession());
   }

+ 18 - 13
engine/processor_test.go

@@ -142,7 +142,7 @@ func TestProcessorSimpleCascade(t *testing.T) {
 	rootm.SetFinishHandler(func(p Processor) {
 		log.WriteString("finished!")
 	})
-	proc.AddEvent(e, rootm)
+	proc.AddEventAndWait(e, rootm)
 
 	if err := proc.AddRule(rule3); err.Error() != "Cannot add rule if the processor has not stopped" {
 		t.Error("Unexpected error:", err)
@@ -187,7 +187,7 @@ finished!` {
 
 	// Push a root event
 
-	proc.AddEvent(&Event{
+	proc.AddEventAndWait(&Event{
 		"InitialEvent",
 		[]string{"core", "main", "event1"},
 		nil,
@@ -700,7 +700,7 @@ func TestProcessorSimpleErrorHandling(t *testing.T) {
 
 	// Push a root event
 
-	mon, err := proc.AddEvent(&Event{
+	mon, err := proc.AddEventAndWait(&Event{
 		"InitialEvent",
 		[]string{"core", "main", "event1"},
 		map[interface{}]interface{}{"name": "foo", "test": "123"},
@@ -720,7 +720,7 @@ func TestProcessorSimpleErrorHandling(t *testing.T) {
 	}
 
 	_, err = proc.AddEvent(&Event{}, nil)
-	if err.Error() != "Cannot add event if the processor is not running" {
+	if err.Error() != "Cannot add event if the processor is stopping or not running" {
 		t.Error("Unexpected error", err)
 		return
 	}
@@ -749,25 +749,30 @@ InitialEvent -> event2 -> event3 -> TestRule3 : testerror2]` {
 		return
 	}
 
+	testProcessorAdvancedErrorHandling(t, proc, &recordedErrors)
+}
+
+func testProcessorAdvancedErrorHandling(t *testing.T, proc Processor, recordedErrorsPtr *int) {
+
 	// Second test will fail on the first failed rule in an event trigger sequence
 
 	proc.SetFailOnFirstErrorInTriggerSequence(true)
 
 	proc.Start()
 
-	mon, err = proc.AddEvent(&Event{
+	mon, err := proc.AddEventAndWait(&Event{
 		"InitialEvent",
 		[]string{"core", "main", "event1"},
 		map[interface{}]interface{}{"name": "foo", "test": "123"},
 	}, nil)
-	rmon, ok = mon.(*RootMonitor)
+	rmon, ok := mon.(*RootMonitor)
 	if !ok {
 		t.Error("Root monitor expected:", mon, err)
 		return
 	}
 	proc.Finish()
 
-	errs = rmon.AllErrors()
+	errs := rmon.AllErrors()
 
 	if len(errs) != 2 {
 		t.Error("Unexpected number of errors:", len(errs))
@@ -781,8 +786,8 @@ InitialEvent -> event2 -> event3 -> TestRule3 : testerror2]` {
 		return
 	}
 
-	if recordedErrors != 1 {
-		t.Error("Unexpected number of recorded errors:", recordedErrors)
+	if *recordedErrorsPtr != 1 {
+		t.Error("Unexpected number of recorded errors:", *recordedErrorsPtr)
 		return
 	}
 
@@ -797,8 +802,8 @@ InitialEvent -> event2 -> event3 -> TestRule3 : testerror2]` {
 		map[interface{}]interface{}{"name": "foo", "test": "123"},
 	}, nil)
 
-	if mon != nil {
-		t.Error("Nothing should have triggered")
+	if mon != nil || err != nil {
+		t.Error("Nothing should have triggered: ", err)
 		return
 	}
 
@@ -833,8 +838,8 @@ InitialEvent -> event2 -> event3 -> TestRule3 : testerror2]` {
 		return
 	}
 
-	if recordedErrors != 3 {
-		t.Error("Unexpected number of recorded errors:", recordedErrors)
+	if *recordedErrorsPtr != 3 {
+		t.Error("Unexpected number of recorded errors:", *recordedErrorsPtr)
 		return
 	}
 

+ 71 - 62
engine/pubsub/eventpump_test.go

@@ -18,49 +18,46 @@ import (
 	"testing"
 )
 
-func TestEventPump(t *testing.T) {
-	var res []string
-
-	source1 := &bytes.Buffer{}
-	source2 := errors.New("TEST")
+var res []string
+var source1 = &bytes.Buffer{}
+var errSource2 error = fmt.Errorf("TEST")
+var ep = NewEventPump()
 
-	ep := NewEventPump()
+func addObservers2(t *testing.T) {
 
-	// Add observer 1
+	// Add observer 4
 
-	ep.AddObserver("event1", source1, func(event string, eventSource interface{}) {
+	ep.AddObserver("", source1, func(event string, eventSource interface{}) {
 		if eventSource != source1 {
 			t.Error("Unexpected event source:", eventSource)
 			return
 		}
-		res = append(res, "1")
+		res = append(res, "4")
 		sort.Strings(res)
-
 	})
 
-	// Add observer 2
+	// Add observer 5
 
-	ep.AddObserver("event2", source2, func(event string, eventSource interface{}) {
-		if eventSource != source2 {
-			t.Error("Unexpected event source:", eventSource)
-			return
-		}
-		res = append(res, "2")
+	ep.AddObserver("", nil, func(event string, eventSource interface{}) {
+		res = append(res, "5")
 		sort.Strings(res)
-
 	})
 
-	// Add observer 3
+	// Add observer 6
 
-	ep.AddObserver("event2", source2, func(event string, eventSource interface{}) {
-		if eventSource != source2 {
+	ep.AddObserver("", errSource2, func(event string, eventSource interface{}) {
+		if eventSource != errSource2 {
 			t.Error("Unexpected event source:", eventSource)
 			return
 		}
-		res = append(res, "3")
+		res = append(res, "6")
 		sort.Strings(res)
-
 	})
+}
+
+func TestEventPump(t *testing.T) {
+
+	addObservers1(t)
 
 	// Run the tests
 
@@ -75,7 +72,7 @@ func TestEventPump(t *testing.T) {
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event2", source2)
+	ep.PostEvent("event2", errSource2)
 
 	if fmt.Sprint(res) != "[2 3]" {
 		t.Error("Unexpected result:", res)
@@ -84,45 +81,18 @@ func TestEventPump(t *testing.T) {
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event1", source2)
+	ep.PostEvent("event1", errSource2)
 
 	if fmt.Sprint(res) != "[]" {
 		t.Error("Unexpected result:", res)
 		return
 	}
 
-	// Add observer 4
-
-	ep.AddObserver("", source1, func(event string, eventSource interface{}) {
-		if eventSource != source1 {
-			t.Error("Unexpected event source:", eventSource)
-			return
-		}
-		res = append(res, "4")
-		sort.Strings(res)
-	})
-
-	// Add observer 5
-
-	ep.AddObserver("", nil, func(event string, eventSource interface{}) {
-		res = append(res, "5")
-		sort.Strings(res)
-	})
-
-	// Add observer 6
-
-	ep.AddObserver("", source2, func(event string, eventSource interface{}) {
-		if eventSource != source2 {
-			t.Error("Unexpected event source:", eventSource)
-			return
-		}
-		res = append(res, "6")
-		sort.Strings(res)
-	})
+	addObservers2(t)
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event1", source2)
+	ep.PostEvent("event1", errSource2)
 
 	if fmt.Sprint(res) != "[5 6]" {
 		t.Error("Unexpected result:", res)
@@ -131,7 +101,7 @@ func TestEventPump(t *testing.T) {
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event3", source2)
+	ep.PostEvent("event3", errSource2)
 
 	if fmt.Sprint(res) != "[5 6]" {
 		t.Error("Unexpected result:", res)
@@ -160,28 +130,28 @@ func TestEventPump(t *testing.T) {
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event2", source2)
+	ep.PostEvent("event2", errSource2)
 
 	if fmt.Sprint(res) != "[2 3 5 6]" {
 		t.Error("Unexpected result:", res)
 		return
 	}
-	ep.RemoveObservers("event2", source2)
+	ep.RemoveObservers("event2", errSource2)
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event2", source2)
+	ep.PostEvent("event2", errSource2)
 
 	if fmt.Sprint(res) != "[5 6]" {
 		t.Error("Unexpected result:", res)
 		return
 	}
 
-	ep.RemoveObservers("", source2) // Remove all handlers specific to source 2
+	ep.RemoveObservers("", errSource2) // Remove all handlers specific to source 2
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event2", source2)
+	ep.PostEvent("event2", errSource2)
 
 	if fmt.Sprint(res) != "[5]" {
 		t.Error("Unexpected result:", res)
@@ -199,7 +169,7 @@ func TestEventPump(t *testing.T) {
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event2", source2)
+	ep.PostEvent("event2", errSource2)
 
 	if fmt.Sprint(res) != "[5]" {
 		t.Error("Unexpected result:", res)
@@ -210,7 +180,7 @@ func TestEventPump(t *testing.T) {
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event2", source2)
+	ep.PostEvent("event2", errSource2)
 
 	if fmt.Sprint(res) != "[]" {
 		t.Error("Unexpected result:", res)
@@ -227,6 +197,45 @@ func TestEventPump(t *testing.T) {
 	}
 }
 
+func addObservers1(t *testing.T) {
+
+	// Add observer 1
+
+	ep.AddObserver("event1", source1, func(event string, eventSource interface{}) {
+		if eventSource != source1 {
+			t.Error("Unexpected event source:", eventSource)
+			return
+		}
+		res = append(res, "1")
+		sort.Strings(res)
+
+	})
+
+	// Add observer 2
+
+	ep.AddObserver("event2", errSource2, func(event string, eventSource interface{}) {
+		if eventSource != errSource2 {
+			t.Error("Unexpected event source:", eventSource)
+			return
+		}
+		res = append(res, "2")
+		sort.Strings(res)
+
+	})
+
+	// Add observer 3
+
+	ep.AddObserver("event2", errSource2, func(event string, eventSource interface{}) {
+		if eventSource != errSource2 {
+			t.Error("Unexpected event source:", eventSource)
+			return
+		}
+		res = append(res, "3")
+		sort.Strings(res)
+
+	})
+}
+
 func TestWrongPostEvent(t *testing.T) {
 	defer func() {
 		if r := recover(); r == nil {

+ 21 - 22
engine/taskqueue_test.go

@@ -33,16 +33,10 @@ func TestTaskQueue(t *testing.T) {
 	// Create different root monitors with different IDs
 
 	m1 := newRootMonitor(nil, NewRuleScope(map[string]bool{"": true}), proc.(*eventProcessor).messageQueue)
-	m2 := newRootMonitor(nil, NewRuleScope(map[string]bool{"": true}), proc.(*eventProcessor).messageQueue)
-	m3 := newRootMonitor(nil, NewRuleScope(map[string]bool{"": true}), proc.(*eventProcessor).messageQueue)
 
 	// Create now different tasks which come from the different monitors
 
 	t1 := &Task{proc, m1, event}
-	t2 := &Task{proc, m2, event}
-	t3 := &Task{proc, m3, event}
-	t4 := &Task{proc, m2.NewChildMonitor(5), event}
-	t5 := &Task{proc, m2.NewChildMonitor(10), event}
 
 	tq := NewTaskQueue(proc.(*eventProcessor).messageQueue)
 
@@ -70,6 +64,19 @@ func TestTaskQueue(t *testing.T) {
 		return
 	}
 
+	testTaskQueuePushPop(t, tq, proc, event, t1)
+}
+
+func testTaskQueuePushPop(t *testing.T, tq *TaskQueue, proc Processor, event *Event, t1 *Task) {
+
+	m2 := newRootMonitor(nil, NewRuleScope(map[string]bool{"": true}), proc.(*eventProcessor).messageQueue)
+	m3 := newRootMonitor(nil, NewRuleScope(map[string]bool{"": true}), proc.(*eventProcessor).messageQueue)
+
+	t2 := &Task{proc, m2, event}
+	t3 := &Task{proc, m3, event}
+	t4 := &Task{proc, m2.NewChildMonitor(5), event}
+	t5 := &Task{proc, m2.NewChildMonitor(10), event}
+
 	tq.Push(t1)
 	tq.Push(t2)
 	tq.Push(t3)
@@ -101,35 +108,23 @@ func TestTaskQueue(t *testing.T) {
 		return
 	}
 
-	if e := tq.Pop(); e != t1 && e != t2 && e != t3 && e != t4 && e != t5 {
-		t.Error("Unexpected event:", e)
-		return
-	}
+	tq.Pop()
 
 	if res := len(tq.queues); res != 3 && res != 2 {
 		t.Error("Unexpected size:", res)
 		return
 	}
 
-	if e := tq.Pop(); e != t1 && e != t2 && e != t3 && e != t4 && e != t5 {
-		t.Error("Unexpected event:", e)
-		return
-	}
+	tq.Pop()
 
 	if s := tq.Size(); s != 2 {
 		t.Error("Unexpected result:", s)
 		return
 	}
 
-	if e := tq.Pop(); e != t1 && e != t2 && e != t3 && e != t4 && e != t5 {
-		t.Error("Unexpected event:", e)
-		return
-	}
+	tq.Pop()
 
-	if e := tq.Pop(); e != t1 && e != t2 && e != t3 && e != t4 && e != t5 {
-		t.Error("Unexpected event:", e)
-		return
-	}
+	tq.Pop()
 
 	if s := tq.Size(); s != 0 {
 		t.Error("Unexpected result:", s)
@@ -141,6 +136,10 @@ func TestTaskQueue(t *testing.T) {
 		return
 	}
 
+	testTaskQueueMisc(t, tq, t5)
+}
+
+func testTaskQueueMisc(t *testing.T, tq *TaskQueue, t5 *Task) {
 	tq.Push(t5)
 
 	if fmt.Sprint(tq.queues) != "map[2:[ Task: RumbleProcessor 1 (workers:1) Monitor 5 (parent: Monitor 2 (parent: <nil> priority: 0 activated: false finished: false) priority: 10 activated: false finished: false) Event: DummyEvent main {} (10) ]]" {

+ 8 - 2
examples/embedding/main.go

@@ -83,11 +83,11 @@ mystuff.add(compute(5), 1)
 
 	// Each thread which evaluates the Runtime of an AST should get a unique thread ID
 
-	var threadId uint64 = 1
+	var threadID uint64 = 1
 
 	// Evaluate the Runtime of an AST with a variable scope
 
-	res, err := ast.Runtime.Eval(vs, make(map[string]interface{}), threadId)
+	res, err := ast.Runtime.Eval(vs, make(map[string]interface{}), threadID)
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -123,6 +123,9 @@ AddFunc is a simple add function which calculates the sum of two numbers.
 type AddFunc struct {
 }
 
+/*
+Run executes the add function
+*/
 func (f *AddFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
 
 	// This should have some proper error checking
@@ -133,6 +136,9 @@ func (f *AddFunc) Run(instanceID string, vs parser.Scope, is map[string]interfac
 	return args[0].(float64) + args[1].(float64), nil
 }
 
+/*
+DocString returns the doc string for the add function.
+*/
 func (f *AddFunc) DocString() (string, error) {
 	return "Sum up two numbers", nil
 }

+ 18 - 18
interpreter/debug.go

@@ -90,7 +90,7 @@ func newInterrogationState(node *parser.ASTNode, vs parser.Scope) *interrogation
 }
 
 /*
-NewDebugger returns a new debugger object.
+NewECALDebugger returns a new debugger object.
 */
 func NewECALDebugger(globalVS parser.Scope) util.ECALDebugger {
 	return &ecalDebugger{
@@ -135,7 +135,7 @@ func (ed *ecalDebugger) HandleInput(input string) (interface{}, error) {
 /*
 StopThreads will continue all suspended threads and set them to be killed.
 Returns true if a waiting thread was resumed. Can wait for threads to end
-by ensuring that for at least d time no state change occured.
+by ensuring that for at least d time no state change occurred.
 */
 func (ed *ecalDebugger) StopThreads(d time.Duration) bool {
 	var ret = false
@@ -445,17 +445,17 @@ func (ed *ecalDebugger) RemoveBreakPoint(source string, line int) {
 ExtractValue copies a value from a suspended thread into the
 global variable scope.
 */
-func (ed *ecalDebugger) ExtractValue(threadId uint64, varName string, destVarName string) error {
+func (ed *ecalDebugger) ExtractValue(threadID uint64, varName string, destVarName string) error {
 	if ed.globalScope == nil {
 		return fmt.Errorf("Cannot access global scope")
 	}
 
-	err := fmt.Errorf("Cannot find suspended thread %v", threadId)
+	err := fmt.Errorf("Cannot find suspended thread %v", threadID)
 
 	ed.lock.Lock()
 	defer ed.lock.Unlock()
 
-	is, ok := ed.interrogationStates[threadId]
+	is, ok := ed.interrogationStates[threadID]
 
 	if ok && !is.running {
 		var val interface{}
@@ -475,17 +475,17 @@ func (ed *ecalDebugger) ExtractValue(threadId uint64, varName string, destVarNam
 InjectValue copies a value from an expression (using the global variable scope) into
 a suspended thread.
 */
-func (ed *ecalDebugger) InjectValue(threadId uint64, varName string, expression string) error {
+func (ed *ecalDebugger) InjectValue(threadID uint64, varName string, expression string) error {
 	if ed.globalScope == nil {
 		return fmt.Errorf("Cannot access global scope")
 	}
 
-	err := fmt.Errorf("Cannot find suspended thread %v", threadId)
+	err := fmt.Errorf("Cannot find suspended thread %v", threadID)
 
 	ed.lock.Lock()
 	defer ed.lock.Unlock()
 
-	is, ok := ed.interrogationStates[threadId]
+	is, ok := ed.interrogationStates[threadID]
 
 	if ok && !is.running {
 		var ast *parser.ASTNode
@@ -515,11 +515,11 @@ func (ed *ecalDebugger) InjectValue(threadId uint64, varName string, expression
 /*
 Continue will continue a suspended thread.
 */
-func (ed *ecalDebugger) Continue(threadId uint64, contType util.ContType) {
+func (ed *ecalDebugger) Continue(threadID uint64, contType util.ContType) {
 	ed.lock.RLock()
 	defer ed.lock.RUnlock()
 
-	if is, ok := ed.interrogationStates[threadId]; ok && !is.running {
+	if is, ok := ed.interrogationStates[threadID]; ok && !is.running {
 
 		switch contType {
 		case util.Resume:
@@ -530,7 +530,7 @@ func (ed *ecalDebugger) Continue(threadId uint64, contType util.ContType) {
 			is.cmd = StepOver
 		case util.StepOut:
 			is.cmd = StepOut
-			stack := ed.callStacks[threadId]
+			stack := ed.callStacks[threadID]
 			is.stepOutStack = stack[:len(stack)-1]
 		}
 
@@ -581,17 +581,17 @@ func (ed *ecalDebugger) Status() interface{} {
 }
 
 /*
-Describe decribes a thread currently observed by the debugger.
+Describe describes a thread currently observed by the debugger.
 */
-func (ed *ecalDebugger) Describe(threadId uint64) interface{} {
+func (ed *ecalDebugger) Describe(threadID uint64) interface{} {
 	ed.lock.RLock()
 	defer ed.lock.RUnlock()
 
 	var res map[string]interface{}
 
-	threadCallStack, ok1 := ed.callStacks[threadId]
+	threadCallStack, ok1 := ed.callStacks[threadID]
 
-	if is, ok2 := ed.interrogationStates[threadId]; ok1 && ok2 {
+	if is, ok2 := ed.interrogationStates[threadID]; ok1 && ok2 {
 		callStackNode := make([]map[string]interface{}, 0)
 
 		for _, sn := range threadCallStack {
@@ -603,8 +603,8 @@ func (ed *ecalDebugger) Describe(threadId uint64) interface{} {
 			"error":                     is.err,
 			"callStack":                 ed.prettyPrintCallStack(threadCallStack),
 			"callStackNode":             callStackNode,
-			"callStackVsSnapshot":       ed.callStackVsSnapshots[threadId],
-			"callStackVsSnapshotGlobal": ed.callStackGlobalVsSnapshots[threadId],
+			"callStackVsSnapshot":       ed.callStackVsSnapshots[threadID],
+			"callStackVsSnapshotGlobal": ed.callStackGlobalVsSnapshots[threadID],
 		}
 
 		if !is.running {
@@ -672,7 +672,7 @@ func (ed *ecalDebugger) MergeMaps(maps ...map[string]interface{}) map[string]int
 }
 
 /*
-Describe decribes a thread currently observed by the debugger.
+Describe describes a thread currently observed by the debugger.
 */
 func (ed *ecalDebugger) prettyPrintCallStack(threadCallStack []*parser.ASTNode) []string {
 	cs := []string{}

+ 10 - 10
interpreter/debug_cmd.go

@@ -23,7 +23,7 @@ import (
 )
 
 /*
-InbuildDebugCommandsMap contains the mapping of inbuild debug commands.
+DebugCommandsMap contains the mapping of inbuild debug commands.
 */
 var DebugCommandsMap = map[string]util.DebugCommand{
 	"breakonstart": &breakOnStartCommand{&inbuildDebugCommand{}},
@@ -231,7 +231,7 @@ func (c *contCommand) Run(debugger util.ECALDebugger, args []string) (interface{
 		return nil, fmt.Errorf("Need a thread ID and a command Resume, StepIn, StepOver or StepOut")
 	}
 
-	threadId, err := c.AssertNumParam(1, args[0])
+	threadID, err := c.AssertNumParam(1, args[0])
 
 	if err == nil {
 		cmdString := strings.ToLower(args[1])
@@ -248,7 +248,7 @@ func (c *contCommand) Run(debugger util.ECALDebugger, args []string) (interface{
 			return nil, fmt.Errorf("Invalid command %v - must be resume, stepin, stepover or stepout", cmdString)
 		}
 
-		debugger.Continue(threadId, cmd)
+		debugger.Continue(threadID, cmd)
 	}
 
 	return nil, err
@@ -258,7 +258,7 @@ func (c *contCommand) Run(debugger util.ECALDebugger, args []string) (interface{
 DocString returns a descriptive text about this command.
 */
 func (c *contCommand) DocString() string {
-	return "Continues a suspended thread. Specify <threadId> <Resume | StepIn | StepOver | StepOut>"
+	return "Continues a suspended thread. Specify <threadID> <Resume | StepIn | StepOver | StepOut>"
 }
 
 // describe
@@ -282,11 +282,11 @@ func (c *describeCommand) Run(debugger util.ECALDebugger, args []string) (interf
 		return nil, fmt.Errorf("Need a thread ID")
 	}
 
-	threadId, err := c.AssertNumParam(1, args[0])
+	threadID, err := c.AssertNumParam(1, args[0])
 
 	if err == nil {
 
-		res = debugger.Describe(threadId)
+		res = debugger.Describe(threadID)
 	}
 
 	return res, err
@@ -344,7 +344,7 @@ func (c *extractCommand) Run(debugger util.ECALDebugger, args []string) (interfa
 		return nil, fmt.Errorf("Need a thread ID, a variable name and a destination variable name")
 	}
 
-	threadId, err := c.AssertNumParam(1, args[0])
+	threadID, err := c.AssertNumParam(1, args[0])
 
 	if err == nil {
 		if !parser.NamePattern.MatchString(args[1]) || !parser.NamePattern.MatchString(args[2]) {
@@ -352,7 +352,7 @@ func (c *extractCommand) Run(debugger util.ECALDebugger, args []string) (interfa
 		}
 
 		if err == nil {
-			err = debugger.ExtractValue(threadId, args[1], args[2])
+			err = debugger.ExtractValue(threadID, args[1], args[2])
 		}
 	}
 
@@ -386,13 +386,13 @@ func (c *injectCommand) Run(debugger util.ECALDebugger, args []string) (interfac
 		return nil, fmt.Errorf("Need a thread ID, a variable name and an expression")
 	}
 
-	threadId, err := c.AssertNumParam(1, args[0])
+	threadID, err := c.AssertNumParam(1, args[0])
 
 	if err == nil {
 		varName := args[1]
 		expression := strings.Join(args[2:], " ")
 
-		err = debugger.InjectValue(threadId, varName, expression)
+		err = debugger.InjectValue(threadID, varName, expression)
 	}
 
 	return nil, err

+ 86 - 122
interpreter/debug_test.go

@@ -33,18 +33,12 @@ func TestSimpleDebugging(t *testing.T) {
 
 	testDebugger = NewECALDebugger(nil)
 
-	if _, err = testDebugger.HandleInput("break ECALEvalTest:3"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
-	if _, err = testDebugger.HandleInput("break ECALEvalTest:4"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
-	if _, err = testDebugger.HandleInput("disablebreak ECALEvalTest:4"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("break ECALEvalTest:3")
+	errorutil.AssertOk(err)
+	_, err = testDebugger.HandleInput("break ECALEvalTest:4")
+	errorutil.AssertOk(err)
+	_, err = testDebugger.HandleInput("disablebreak ECALEvalTest:4")
+	errorutil.AssertOk(err)
 
 	wg := &sync.WaitGroup{}
 	wg.Add(1)
@@ -145,10 +139,8 @@ log("test3")
 
 	// Continue until the end
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v Resume", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v Resume", tid))
+	errorutil.AssertOk(err)
 
 	wg.Wait()
 
@@ -160,10 +152,8 @@ test3`[1:] {
 		return
 	}
 
-	if _, err = testDebugger.HandleInput("rmbreak ECALEvalTest:4"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("rmbreak ECALEvalTest:4")
+	errorutil.AssertOk(err)
 
 	out, err = testDebugger.HandleInput(fmt.Sprintf("status"))
 
@@ -184,15 +174,11 @@ test3`[1:] {
 		return
 	}
 
-	if _, err = testDebugger.HandleInput("break ECALEvalTest:4"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("break ECALEvalTest:4")
+	errorutil.AssertOk(err)
 
-	if _, err = testDebugger.HandleInput("rmbreak ECALEvalTest"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("rmbreak ECALEvalTest")
+	errorutil.AssertOk(err)
 
 	out, err = testDebugger.HandleInput(fmt.Sprintf("status"))
 
@@ -552,17 +538,17 @@ log("test4")
 func waitForThreadSuspension(t *testing.T) uint64 {
 	var tid uint64
 
-	for i := 0; i < 100; i += 1 {
+	for i := 0; i < 100; i++ {
 		state, err := testDebugger.HandleInput("status")
 		errorutil.AssertOk(err)
 
 		threads := state.(map[string]interface{})["threads"].(map[string]map[string]interface{})
 		if len(threads) > 0 {
-			for threadId, status := range threads {
+			for threadID, status := range threads {
 
 				if r, ok := status["threadRunning"]; ok && !r.(bool) {
-					threadIdNum, _ := strconv.ParseInt(threadId, 10, 0)
-					tid = uint64(threadIdNum)
+					threadIDNum, _ := strconv.ParseInt(threadID, 10, 0)
+					tid = uint64(threadIDNum)
 					return tid
 				}
 			}
@@ -577,7 +563,7 @@ func waitForThreadSuspension(t *testing.T) uint64 {
 func waitForAllThreadSuspension(t *testing.T) uint64 {
 	var tid uint64
 
-	for i := 0; i < 100; i += 1 {
+	for i := 0; i < 100; i++ {
 		state, err := testDebugger.HandleInput("status")
 		errorutil.AssertOk(err)
 
@@ -640,15 +626,11 @@ d(d())
 log("finish")
 `
 
-	if _, err = testDebugger.HandleInput("break ECALEvalTest:10"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("break ECALEvalTest:10")
+	errorutil.AssertOk(err)
 
-	if _, err = testDebugger.HandleInput("breakonstart true"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("breakonstart true")
+	errorutil.AssertOk(err)
 
 	wg := &sync.WaitGroup{}
 	wg.Add(1)
@@ -681,10 +663,8 @@ log("finish")
 		return
 	}
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v resume", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v resume", tid))
+	errorutil.AssertOk(err)
 
 	tid = waitForThreadSuspension(t)
 
@@ -713,10 +693,8 @@ log("finish")
 
 	// Step in without a function
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepin", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepin", tid))
+	errorutil.AssertOk(err)
 
 	tid = waitForThreadSuspension(t)
 
@@ -746,10 +724,8 @@ log("finish")
 
 	// Normal step over
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepover", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepover", tid))
+	errorutil.AssertOk(err)
 
 	tid = waitForThreadSuspension(t)
 
@@ -779,10 +755,8 @@ log("finish")
 
 	// Normal step in
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepin", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepin", tid))
+	errorutil.AssertOk(err)
 
 	tid = waitForThreadSuspension(t)
 
@@ -810,10 +784,8 @@ log("finish")
 
 	// Normal step out
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepout", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepout", tid))
+	errorutil.AssertOk(err)
 
 	tid = waitForThreadSuspension(t)
 
@@ -843,12 +815,15 @@ log("finish")
 
 	// Step in and step out - we should end up on the same line as before
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepin", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepin", tid))
+	errorutil.AssertOk(err)
 
-	tid = waitForThreadSuspension(t)
+	testStepDebugging2(t, testDebugger, wg)
+}
+
+func testStepDebugging2(t *testing.T, testDebugger util.ECALDebugger, wg *sync.WaitGroup) {
+
+	tid := waitForThreadSuspension(t)
 
 	if state := getDebuggerState(tid, t); state != `{
   "breakpoints": {
@@ -872,10 +847,9 @@ log("finish")
 		return
 	}
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepout", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepout", tid))
+	errorutil.AssertOk(err)
+
 	tid = waitForThreadSuspension(t)
 
 	if state := getDebuggerState(tid, t); state != `{
@@ -904,10 +878,9 @@ log("finish")
 
 	// Normal step out
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepout", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepout", tid))
+	errorutil.AssertOk(err)
+
 	tid = waitForThreadSuspension(t)
 
 	if state := getDebuggerState(tid, t); state != `{
@@ -935,15 +908,11 @@ log("finish")
 
 	// Set a new breakpoint
 
-	if _, err = testDebugger.HandleInput("break ECALEvalTest:28"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("break ECALEvalTest:28")
+	errorutil.AssertOk(err)
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v Resume", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return