diff --git a/pkg/picol/commands.go b/pkg/picol/commands.go index a1b8d7a..1c21af3 100644 --- a/pkg/picol/commands.go +++ b/pkg/picol/commands.go @@ -5,7 +5,6 @@ import ( "fmt" "strconv" "strings" - "testing" ) /* @@ -18,9 +17,9 @@ func incorrectArgCountError(i *Interpreter, name string, argv []string) error { } /* - * needleInHaystack returns true if the string is in a slice + * NeedleInHaystack returns true if the string is in a slice */ -func needleInHaystack(needle string, haystack []string) bool { +func NeedleInHaystack(needle string, haystack []string) bool { for _, haystackMember := range haystack { if haystackMember == needle { return true @@ -29,27 +28,6 @@ func needleInHaystack(needle string, haystack []string) bool { return false } -/* - * Test_needleInHaystack tests the return value of needleInHaystack - */ -func Test_needleInHaystack(t *testing.T) { - var haystack = []string{"a", "b", "c"} - var needle = "a" - if !needleInHaystack(needle, haystack) { - t.Errorf("%s not in %s", needle, haystack) - } - - needle = "j" - if needleInHaystack(needle, haystack) { - t.Errorf("%s in %s", needle, haystack) - } - - needle = "ab" - if needleInHaystack(needle, haystack) { - t.Errorf("%s in %s", needle, haystack) - } -} - /* * CommandMath is the math command for TCL */ @@ -274,7 +252,7 @@ func CommandPuts(i *Interpreter, argv []string, pd interface{}) (string, error) /* * RegisterCoreCommands is a callable to register TCL commands. */ -func (i *Interpreter) RegisterCoreCommands() { +func (i *Interpreter) RegisterCoreCommands() error { name := [...]string{"+", "-", "*", "/", ">", ">=", "<", "<=", "==", "!="} for _, n := range name { _ = i.RegisterCommand(n, CommandMath, nil) @@ -289,4 +267,6 @@ func (i *Interpreter) RegisterCoreCommands() { _ = i.RegisterCommand("return", CommandReturn, nil) _ = i.RegisterCommand("error", CommandError, nil) _ = i.RegisterCommand("puts", CommandPuts, nil) + + return nil } diff --git a/pkg/picol/commands_test.go b/pkg/picol/commands_test.go new file mode 100644 index 0000000..66e8ed8 --- /dev/null +++ b/pkg/picol/commands_test.go @@ -0,0 +1,300 @@ +package picol_test + +import ( + "testing" + + "github.com/dragonheim/gagent/pkg/picol" +) + +func Test_NeedleInHaystack(t *testing.T) { + var haystack = []string{"a", "b", "c"} + var needle = "a" + if !picol.NeedleInHaystack(needle, haystack) { + t.Errorf("%s not in %s", needle, haystack) + } + + needle = "j" + if picol.NeedleInHaystack(needle, haystack) { + t.Errorf("%s in %s", needle, haystack) + } + + needle = "ab" + if picol.NeedleInHaystack(needle, haystack) { + t.Errorf("%s in %s", needle, haystack) + } +} + +func Test_CommandMath(t *testing.T) { + // You can add more test cases for various operations and edge cases + testCases := []struct { + argv []string + result string + err error + }{ + {[]string{"+", "2", "3"}, "5", nil}, + {[]string{"-", "8", "3"}, "5", nil}, + {[]string{"*", "2", "3"}, "6", nil}, + {[]string{"/", "6", "3"}, "2", nil}, + {[]string{">", "4", "2"}, "1", nil}, + } + + i := picol.NewInterpreter() + for _, tc := range testCases { + result, err := picol.CommandMath(i, tc.argv, nil) + if result != tc.result || (err != nil && tc.err != nil && err.Error() != tc.err.Error()) { + t.Errorf("CommandMath(%v) = (%v, %v); expected (%v, %v)", tc.argv, result, err, tc.result, tc.err) + } + } +} + +func Test_CommandSet(t *testing.T) { + testCases := []struct { + argv []string + result string + err error + }{ + {[]string{"set", "x", "42"}, "42", nil}, + {[]string{"set", "y", "abc"}, "abc", nil}, + } + + i := picol.NewInterpreter() + for _, tc := range testCases { + result, err := picol.CommandSet(i, tc.argv, nil) + if result != tc.result || (err != nil && tc.err != nil && err.Error() != tc.err.Error()) { + t.Errorf("CommandSet(%v) = (%v, %v); expected (%v, %v)", tc.argv, result, err, tc.result, tc.err) + } + } +} + +func Test_CommandUnset(t *testing.T) { + testCases := []struct { + argv []string + result string + err error + }{ + {[]string{"unset", "x"}, "", nil}, + {[]string{"unset", "y"}, "", nil}, + } + + i := picol.NewInterpreter() + i.SetVariable("x", "42") + i.SetVariable("y", "abc") + + for _, tc := range testCases { + result, err := picol.CommandUnset(i, tc.argv, nil) + if result != tc.result || (err != nil && tc.err != nil && err.Error() != tc.err.Error()) { + t.Errorf("CommandUnset(%v) = (%v, %v); expected (%v, %v)", tc.argv, result, err, tc.result, tc.err) + } + } +} + +func Test_CommandIf(t *testing.T) { + testCases := []struct { + argv []string + result string + err error + }{ + {[]string{"unset", "x"}, "", nil}, + {[]string{"unset", "y"}, "", nil}, + } + + i := picol.NewInterpreter() + i.SetVariable("x", "42") + i.SetVariable("y", "abc") + + for _, tc := range testCases { + result, err := picol.CommandUnset(i, tc.argv, nil) + if result != tc.result || (err != nil && tc.err != nil && err.Error() != tc.err.Error()) { + t.Errorf("CommandUnset(%v) = (%v, %v); expected (%v, %v)", tc.argv, result, err, tc.result, tc.err) + } + } +} + +func Test_CommandWhile(t *testing.T) { + i := picol.NewInterpreter() + + // Test simple while loop + err := i.RegisterCoreCommands() + if err != nil { + t.Fatal(err) + } + + script := ` + set i 0 + set result 0 + while { < $i 5 } { + set result [+ $result $i] + set i [+ $i 1] + } + ` + + res, err := i.Eval(script) + if err != nil { + t.Fatalf("Error during while loop evaluation: %s", err) + } + + expectedResult := "10" + if res != expectedResult { + t.Errorf("Expected %s, got %s", expectedResult, res) + } + + // Test nested while loops + script = ` + set i 0 + set j 0 + set result 0 + while { < $i 3 } { + set j 0 + while { < $j 3 } { + set result [+ $result $j] + set j [+ $j 1] + } + set i [+ $i 1] + } + ` + + res, err = i.Eval(script) + if err != nil { + t.Fatalf("Error during nested while loop evaluation: %s", err) + } + + expectedResult = "9" + if res != expectedResult { + t.Errorf("Expected %s, got %s", expectedResult, res) + } +} + +// You can also add test functions for other commands like CommandProc, CommandReturn, etc. +func Test_CommandRetCodes(t *testing.T) { + i := picol.NewInterpreter() + + err := i.RegisterCoreCommands() + if err != nil { + t.Fatal(err) + } + + // Test break + script := ` + while { 1 } { + break + } + ` + _, err = i.Eval(script) + if err != nil { + t.Fatalf("Error during break evaluation: %s", err) + } + + // Test continue + script = ` + set i 0 + set result 0 + while { < $i 5 } { + if { == $i 2 } { + continue + } + set result [+ $result $i] + set i [+ $i 1] + } + ` + expectedResult := "7" + res, err := i.Eval(script) + if err != nil { + t.Fatalf("Error during continue evaluation: %s", err) + } + if res != expectedResult { + t.Errorf("Expected %s, got %s", expectedResult, res) + } +} + +func Test_CommandProc(t *testing.T) { + i := picol.NewInterpreter() + + err := i.RegisterCoreCommands() + if err != nil { + t.Fatal(err) + } + + script := ` + proc sum {a b} { + return [+ $a $b] + } + set res [sum 3 4] + ` + expectedResult := "7" + res, err := i.Eval(script) + if err != nil { + t.Fatalf("Error during proc evaluation: %s", err) + } + if res != expectedResult { + t.Errorf("Expected %s, got %s", expectedResult, res) + } +} + +func Test_CommandReturn(t *testing.T) { + i := picol.NewInterpreter() + + err := i.RegisterCoreCommands() + if err != nil { + t.Fatal(err) + } + + script := ` + proc testReturn {val} { + return $val + } + set res [testReturn 42] + ` + expectedResult := "42" + res, err := i.Eval(script) + if err != nil { + t.Fatalf("Error during return evaluation: %s", err) + } + if res != expectedResult { + t.Errorf("Expected %s, got %s", expectedResult, res) + } +} + +func Test_CommandError(t *testing.T) { + i := picol.NewInterpreter() + + err := i.RegisterCoreCommands() + if err != nil { + t.Fatal(err) + } + + script := ` + error "An error occurred" + ` + _, err = i.Eval(script) + if err == nil || err.Error() != "An error occurred" { + t.Fatalf("Error not raised or incorrect error message: %s", err) + } +} + +func Test_CommandPuts(t *testing.T) { + i := picol.NewInterpreter() + + err := i.RegisterCoreCommands() + if err != nil { + t.Fatal(err) + } + + // The following test checks if the "puts" command runs without any error. + // However, it doesn't check the printed output since it's not straightforward to capture stdout in tests. + script := ` + puts "Hello, world!" + ` + _, err = i.Eval(script) + if err != nil { + t.Fatalf("Error during puts evaluation: %s", err) + } +} + +func Test_RegisterCoreCommands(t *testing.T) { + i := picol.NewInterpreter() + + err := i.RegisterCoreCommands() + if err != nil { + t.Fatalf("Error during core command registration: %s", err) + } +} diff --git a/pkg/picol/parser.go b/pkg/picol/parser.go index e2cbb8b..f2cc441 100644 --- a/pkg/picol/parser.go +++ b/pkg/picol/parser.go @@ -7,13 +7,13 @@ import ( // Define parser token types const ( - ptESC = iota - ptSTR - ptCMD - ptVAR - ptSEP - ptEOL - ptEOF + ParserTokenESC = iota + ParserTokenSTR + ParserTokenCMD + ParserTokenVAR + ParserTokenSEP + ParserTokenEOL + ParserTokenEOF ) // parserStruct represents the parser state @@ -26,7 +26,7 @@ type parserStruct struct { // initParser initializes a new parserStruct instance func initParser(text string) *parserStruct { - return &parserStruct{text: text, ln: len(text), Type: ptEOL} + return &parserStruct{text: text, ln: len(text), Type: ParserTokenEOL} } // next advances the parser position by one rune @@ -57,7 +57,7 @@ func (p *parserStruct) parseSep() string { } } p.end = p.p - p.Type = ptSEP + p.Type = ParserTokenSEP return p.token() } @@ -74,7 +74,7 @@ func (p *parserStruct) parseEol() string { } p.end = p.p - p.Type = ptEOL + p.Type = ParserTokenEOL return p.token() } @@ -105,7 +105,7 @@ Loop: p.next() } p.end = p.p - p.Type = ptCMD + p.Type = ParserTokenCMD if p.p < len(p.text) && p.current() == ']' { p.next() } @@ -118,7 +118,7 @@ func (p *parserStruct) parseVar() string { p.start = p.p if p.current() == '{' { - p.Type = ptVAR + p.Type = ParserTokenVAR return p.parseBrace() } @@ -134,10 +134,10 @@ func (p *parserStruct) parseVar() string { if p.start == p.p { // It's just a single char string "$" p.start = p.p - 1 p.end = p.p - p.Type = ptSTR + p.Type = ParserTokenSTR } else { p.end = p.p - p.Type = ptVAR + p.Type = ParserTokenVAR } return p.token() } @@ -173,10 +173,10 @@ Loop: // parseString parses a string with or without quotes func (p *parserStruct) parseString() string { - newword := p.Type == ptSEP || p.Type == ptEOL || p.Type == ptSTR + newword := p.Type == ParserTokenSEP || p.Type == ParserTokenEOL || p.Type == ParserTokenSTR if c := p.current(); newword && c == '{' { - p.Type = ptSTR + p.Type = ParserTokenSTR return p.parseBrace() } else if newword && c == '"' { p.insidequote = 1 @@ -197,7 +197,7 @@ Loop: case '"': if p.insidequote != 0 { p.end = p.p - p.Type = ptESC + p.Type = ParserTokenESC p.next() p.insidequote = 0 return p.token() @@ -211,7 +211,7 @@ Loop: } p.end = p.p - p.Type = ptESC + p.Type = ParserTokenESC return p.token() } @@ -227,10 +227,10 @@ func (p *parserStruct) parseComment() string { func (p *parserStruct) GetToken() string { for { if p.ln == 0 { - if p.Type != ptEOL && p.Type != ptEOF { - p.Type = ptEOL + if p.Type != ParserTokenEOL && p.Type != ParserTokenEOF { + p.Type = ParserTokenEOL } else { - p.Type = ptEOF + p.Type = ParserTokenEOF } return p.token() } @@ -251,7 +251,7 @@ func (p *parserStruct) GetToken() string { case '$': return p.parseVar() case '#': - if p.Type == ptEOL { + if p.Type == ParserTokenEOL { p.parseComment() continue } diff --git a/pkg/picol/parser_test.go b/pkg/picol/parser_test.go new file mode 100644 index 0000000..a5247f7 --- /dev/null +++ b/pkg/picol/parser_test.go @@ -0,0 +1,79 @@ +package picol_test + +import ( + "testing" + + "github.com/dragonheim/gagent/pkg/picol" +) + +func TestParser(t *testing.T) { + testCases := []struct { + name string + input string + expected []int + }{ + { + "Simple test", + "set x 10\nincr x", + []int{ + picol.ParserTokenSTR, + picol.ParserTokenSEP, + picol.ParserTokenSTR, + picol.ParserTokenSEP, + picol.ParserTokenSTR, + picol.ParserTokenEOL, + picol.ParserTokenSTR, + picol.ParserTokenSEP, + picol.ParserTokenSTR, + picol.ParserTokenEOL, + picol.ParserTokenEOF, + }, + }, + { + "Variable and command test", + "set x $y\nputs [expr $x * 2]", + []int{ + picol.ParserTokenSTR, + picol.ParserTokenSEP, + picol.ParserTokenSTR, + picol.ParserTokenSEP, + picol.ParserTokenVAR, + picol.ParserTokenEOL, + picol.ParserTokenSTR, + picol.ParserTokenSEP, + picol.ParserTokenCMD, + picol.ParserTokenEOL, + picol.ParserTokenEOF, + }, + }, + { + "Braces and quotes test", + `set x {"Hello World"}`, + []int{ + picol.ParserTokenSTR, + picol.ParserTokenSEP, + picol.ParserTokenSTR, + picol.ParserTokenSEP, + picol.ParserTokenSTR, + picol.ParserTokenEOL, + picol.ParserTokenEOF, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + parser := picol.InitParser(tc.input) + + for _, expectedType := range tc.expected { + token := parser.GetToken() + if parser.Type != expectedType { + t.Errorf("Expected token type %d, got %d", expectedType, parser.Type) + } + if parser.Type == picol.ParserTokenEOF { + break + } + } + }) + } +} diff --git a/pkg/picol/picol.go b/pkg/picol/picol.go index 5b53ecb..7390d5f 100644 --- a/pkg/picol/picol.go +++ b/pkg/picol/picol.go @@ -125,33 +125,33 @@ func (interp *Interpreter) Eval(script string) (string, error) { for { prevType := parser.Type token := parser.GetToken() - if parser.Type == ptEOF { + if parser.Type == ParserTokenEOF { break } switch parser.Type { - case ptVAR: + case ParserTokenVAR: v, ok := interp.Variable(token) if !ok { return "", fmt.Errorf("no such variable '%s'", token) } token = string(v) - case ptCMD: + case ParserTokenCMD: result, err = interp.Eval(token) if err != nil { return result, err } token = result - case ptESC: + case ParserTokenESC: /* * TODO: escape handling missing! */ - case ptSEP: + case ParserTokenSEP: // prevType = parser.Type continue } - if parser.Type == ptEOL { + if parser.Type == ParserTokenEOL { // prevType = parser.Type if len(argv) != 0 { cmd := interp.Command(argv[0]) @@ -173,7 +173,7 @@ func (interp *Interpreter) Eval(script string) (string, error) { /* * We have a new token, append to the previous or as new arg? */ - if prevType == ptSEP || prevType == ptEOL { + if prevType == ParserTokenSEP || prevType == ParserTokenEOL { argv = append(argv, token) } else { // Interpolation argv[len(argv)-1] = strings.Join([]string{argv[len(argv)-1], token}, "") diff --git a/pkg/picol/picol_test.go b/pkg/picol/picol_test.go new file mode 100644 index 0000000..3137dcc --- /dev/null +++ b/pkg/picol/picol_test.go @@ -0,0 +1,56 @@ +package picol_test + +import ( + "testing" + + "github.com/dragonheim/gagent/pkg/picol" +) + +func TestInterpreter(t *testing.T) { + interp := picol.NewInterpreter() + + // Register a command + err := interp.RegisterCommand("test", testCommand, nil) + if err != nil { + t.Fatalf("Error registering test command: %v", err) + } + + // Test command execution + script := "test hello world" + result, err := interp.Eval(script) + if err != nil { + t.Fatalf("Error executing script: %v", err) + } + expected := "hello world" + if result != expected { + t.Errorf("Expected result '%s', got '%s'", expected, result) + } + + // Test variable setting + interp.SetVariable("x", "42") + + // Test variable retrieval + val, ok := interp.Variable("x") + if !ok { + t.Fatalf("Variable 'x' not found") + } + expectedVar := "42" + if val != picol.Variable(expectedVar) { + t.Errorf("Expected variable value '%s', got '%s'", expectedVar, val) + } + + // Test variable unsetting + interp.UnsetVariable("x") + _, ok = interp.Variable("x") + if ok { + t.Fatalf("Variable 'x' should have been unset") + } +} + +// testCommand is a simple custom command for testing +func testCommand(interp *picol.Interpreter, argv []string, privdata interface{}) (string, error) { + if len(argv) != 3 { + return "", nil + } + return argv[1] + " " + argv[2], nil +} diff --git a/pkg/picol/picol_unused/main.go_unused b/pkg/picol/picol_unused/main.go_unused index c1fb8fa..4db8dd1 100644 --- a/pkg/picol/picol_unused/main.go_unused +++ b/pkg/picol/picol_unused/main.go_unused @@ -12,16 +12,15 @@ import ( var fname = flag.String("f", "", "file name") -func main() { - flag.Parse() +func RunPicol(fname string) error { interp := picol.NewInterpreter() interp.RegisterCoreCommands() - buf, err := ioutil.ReadFile(*fname) + buf, err := ioutil.ReadFile(fname) if err == nil { result, err := interp.Eval(string(buf)) if err != nil { - fmt.Println("ERRROR", result, err) + return fmt.Errorf("Error: %s, Result: %s", err, result) } } else { for { @@ -30,8 +29,17 @@ func main() { clibuf, _ := scanner.ReadString('\n') result, err := interp.Eval(clibuf[:len(clibuf)-1]) if len(result) != 0 { - fmt.Println("ERRROR", result, err) + return fmt.Errorf("Error: %s, Result: %s", err, result) } } } + return nil +} + +func main() { + flag.Parse() + err := RunPicol(*fname) + if err != nil { + fmt.Println(err) + } } diff --git a/pkg/picol/picol_unused/main_test.go_unused b/pkg/picol/picol_unused/main_test.go_unused new file mode 100644 index 0000000..896671d --- /dev/null +++ b/pkg/picol/picol_unused/main_test.go_unused @@ -0,0 +1,31 @@ +package main_test + +import ( + "io/ioutil" + "os" + "testing" + + picol "github.com/dragonheim/gagent/pkg/picol/picol_unused" +) + +func Test_RunPicol(t *testing.T) { + // Create a temporary test file + content := []byte("set a 5\nset b 7\n+ $a $b\n") + tmpfile, err := ioutil.TempFile("", "picol_test") + if err != nil { + t.Fatalf("Error creating temporary test file: %v", err) + } + defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write(content); err != nil { + t.Fatalf("Error writing content to temporary test file: %v", err) + } + if err := tmpfile.Close(); err != nil { + t.Fatalf("Error closing temporary test file: %v", err) + } + + err = picol.RunPicol(tmpfile.Name()) + if err != nil { + t.Errorf("Error during RunPicol: %v", err) + } +}