diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6313306 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2016, David Luu +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +* Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 88e49de..32517d3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ # gorrs -Generic Robot Framework remote library server implementation in go + +Pronounced like "gore's", abbreviation for "GO Robot Remote Server", a generic [Robot Framework](http://robotframework.org) [remote library server implementation](https://github.com/robotframework/RemoteInterface) in go. + +This is a proof of concept prototype. Not fully working at the moment. See the source code for insight/details. Others are welcome to pick up where I left off. + +## Setup + +1. Have a version of [go](https://golang.org/dl/) installed. Recommend go 1.13+. And set up your $GOPATH and $GOBIN environment variables. +2. Get a copy of gorrs: ```go get -u github.com/daluu/gorrs``` + +The combination of go modules (`go.mod` + `go.sum`) & `go get -u` should pick up all the (versioned) dependencies to build gorrs. If you prefer using a different method of go dependency management, feel free to do so yourself. + +## Intended usage (when gorrs is fully working): + +1. Add an import statement/entry into ```protocol/protocol.go``` for the desired go-based library (go src path) to be served with gorrs. e.g. for the example remote library, ```import "github.com/daluu/gorrs/libraries"```. +2. Run the server: from source from repo path via ```go run main.go [args]```; or from compiled binary with ```go build``` or ```go install```, then run ```gorrs [args]```. + +With ```go build```, the executable is in repo path, and you may move it elsewhere for use. With ```go install```, the binary is set to the $GOPATH/bin or $GOBIN paths, and can typically be executed from anywhere. + +There's some issues with the gorrs XML-RPC library integration dependencies to resolve for it to fully work, and the go code reflection for dynamically serving remote libraries hasn't been implemented yet due to the existing issues. See source code for details. diff --git a/example/example_tests.robot b/example/example_tests.robot new file mode 100644 index 0000000..6e05fb6 --- /dev/null +++ b/example/example_tests.robot @@ -0,0 +1,16 @@ +*** Settings *** +Library Remote http://${ADDRESS}:${PORT} + +*** Variables *** +${ADDRESS} 127.0.0.1 +${PORT} 8270 + +*** Test Cases *** +Count Items in Directory + ${items1} = Count Items In Directory ${CURDIR} + ${items2} = Count Items In Directory ${TEMPDIR} + Log ${items1} items in '${CURDIR}' and ${items2} items in '${TEMPDIR}' + +Failing Example + Strings Should Be Equal Hello Hello + Strings Should Be Equal not equal diff --git a/example/go.mod b/example/go.mod new file mode 100644 index 0000000..5cb8fa0 --- /dev/null +++ b/example/go.mod @@ -0,0 +1,7 @@ +module github.com/daluu/gorrs/example + +go 1.13 + +require github.com/daluu/gorrs v0.0.0-20191113073619-c1ebfd7cfc64 // indirect + +replace github.com/daluu/gorrs v0.0.0-20191113073619-c1ebfd7cfc64 => ../ diff --git a/example/go.sum b/example/go.sum new file mode 100644 index 0000000..161e465 --- /dev/null +++ b/example/go.sum @@ -0,0 +1,8 @@ +github.com/daluu/gorrs v0.0.0-20191113073619-c1ebfd7cfc64 h1:xL3464UC/iZOSZfu7i+pEGZYFjQRqM/EAM7eVmtdKhM= +github.com/daluu/gorrs v0.0.0-20191113073619-c1ebfd7cfc64/go.mod h1:oo2wZm9B9woepF3VGBfQPFmhDQfajFLLeepu5QqyS5s= +github.com/divan/gorilla-xmlrpc v0.0.0-20190926132722-f0686da74fda h1:q6BJCx6rxRJv/sLreclgzu4dK4dPF8x48afqcXtRtLQ= +github.com/divan/gorilla-xmlrpc v0.0.0-20190926132722-f0686da74fda/go.mod h1:3Cp6mWQcmK3erqkPrriKEkSpok0LO1uB2M5GxGzifhc= +github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= +github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= +github.com/rogpeppe/go-charset v0.0.0-20190617161244-0dc95cdf6f31 h1:DE4LcMKyqAVa6a0CGmVxANbnVb7stzMmPkQiieyNmfQ= +github.com/rogpeppe/go-charset v0.0.0-20190617161244-0dc95cdf6f31/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..aba0721 --- /dev/null +++ b/example/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/daluu/gorrs/libraries" + "github.com/daluu/gorrs/runner" +) + +func main() { + runner.RunRemoteServer(new(libraries.ExampleRemoteLibrary)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3748b7f --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/daluu/gorrs + +go 1.13 + +require ( + github.com/divan/gorilla-xmlrpc v0.0.0-20190926132722-f0686da74fda + github.com/gorilla/rpc v1.2.0 + github.com/rogpeppe/go-charset v0.0.0-20190617161244-0dc95cdf6f31 // indirect +) + +replace github.com/divan/gorilla-xmlrpc v0.0.0-20190926132722-f0686da74fda => github.com/samirkut/gorilla-xmlrpc v0.0.0-20200110153911-8acdd7083791 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bce20d4 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/divan/gorilla-xmlrpc v0.0.0-20190926132722-f0686da74fda h1:q6BJCx6rxRJv/sLreclgzu4dK4dPF8x48afqcXtRtLQ= +github.com/divan/gorilla-xmlrpc v0.0.0-20190926132722-f0686da74fda/go.mod h1:3Cp6mWQcmK3erqkPrriKEkSpok0LO1uB2M5GxGzifhc= +github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= +github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= +github.com/rogpeppe/go-charset v0.0.0-20190617161244-0dc95cdf6f31 h1:DE4LcMKyqAVa6a0CGmVxANbnVb7stzMmPkQiieyNmfQ= +github.com/rogpeppe/go-charset v0.0.0-20190617161244-0dc95cdf6f31/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/samirkut/gorilla-xmlrpc v0.0.0-20200110153911-8acdd7083791 h1:xrBucR0ZjpmFRYe2dwzobq01njvwffuWp8CaS01VMJ4= +github.com/samirkut/gorilla-xmlrpc v0.0.0-20200110153911-8acdd7083791/go.mod h1:cpCVXo7AA8zZqhx4ApNmJXo3i+UgHUk7IVKYxgdBvD0= diff --git a/libraries/example_library.go b/libraries/example_library.go new file mode 100644 index 0000000..a25049d --- /dev/null +++ b/libraries/example_library.go @@ -0,0 +1,48 @@ +package libraries + +import ( + "errors" + "fmt" + "io/ioutil" +) + +//ExampleRemoteLibrary to be used with Robot Framework's remote server. +type ExampleRemoteLibrary struct{} + +//CountItemsInDirectory the number of items in the directory specified by `path`. +func (lib *ExampleRemoteLibrary) CountItemsInDirectory(path string) (int, error) { + fileCount := 0 + files, err := ioutil.ReadDir(path) + if err != nil { + return fileCount, err + } + fileCount = len(files) + return fileCount, err +} + +//StringsShouldBeEqual ... +func (lib *ExampleRemoteLibrary) StringsShouldBeEqual(str1 string, str2 string) error { + fmt.Printf("Comparing '%s' to '%s'.", str1, str2) + if str1 != str2 { + return errors.New("Given strings are not equal.") + } else { + return nil + } +} + +//optional extra keyword below, following phrrs (PHP robot framework remote server) +//comment out if it interferes with running example remote library tests against gorrs + +//TruthOfLife ... +func (lib *ExampleRemoteLibrary) TruthOfLife() int { + return 42 +} + +//TruthOfLife ... +func (lib *ExampleRemoteLibrary) ReturnArray() []interface{} { + var testArray []interface{} + testArray = append(testArray, "string") + testArray = append(testArray, 1) + testArray = append(testArray, 1.1) + return testArray +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..fdaa889 --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "log" +) + +/* add to import list of github.com/daluu/gorrs/protocol/protocol.go, + * the (exported) go remote (test) library packages + * to be served by this remote server via reflection. To do that since we + * have to explicitly reference packages to reflect on and not be able to + * just pass in package reference at runtime? + */ + +/* TODO: also look into whether there's any other alternative to + * gorilla/rpc and divan/gorilla-xmlrpc/xml in case of issues with XML-RPC + * support / implementation in go. Or what can be done to extend them to do + * what we need for a go-based Robot Framework generic remote library server + * + * Full spec for said server: + * https://github.com/robotframework/RemoteInterface + */ + +func main() { + log.Fatal("not runnable directly") +} diff --git a/protocol/protocol.go b/protocol/protocol.go new file mode 100644 index 0000000..ae3693e --- /dev/null +++ b/protocol/protocol.go @@ -0,0 +1,241 @@ +package protocol + +import ( + "fmt" + "log" + "net/http" + "os" + "reflect" + "time" +) + +/* add to import list above, the (exported) go remote (test) library packages + * to be served by this remote server via reflection. To do that since we + * have to explicitly reference packages to reflect on and not be able to + * just pass in package reference at runtime? + */ + +/* TODO: look into what can be reflected by go in terms of finding stuff in the + * imported packages namespace, execute an exported function, optionally + * extract out or lookup the arguments (#, name, type) for an exported function, + * and optionally extract the go documentation for an exported function, + * all via reflection (or something equivalent in go). + * If not feasible, then go users will have to statically "serve" a chosen + * package rather than dynamically serve via reflection for what's in the + * imported namespace. Test library imports would be done in the server main program "../main.go" + * + * Just search online for go reflection for resources, or here's some from a search: + * http://www.jerf.org/iri/post/2945 + * https://blog.golang.org/laws-of-reflection + * http://blog.ralch.com/tutorial/golang-reflection/ + * http://merbist.com/2011/06/27/golang-reflection-exampl/ + * https://blog.gopheracademy.com/birthday-bash-2014/advanced-reflection-with-go-at-hashicorp/ + * https://jimmyfrasche.github.io/go-reflection-codex/ + * https://gist.github.com/drewolson/4771479 + * https://golang.org/pkg/reflect/ + * https://godoc.org/?q=reflect + * + * also, how to redirect stdout & stderr (here & in reflected code), + * such that we pipe a copy of that data into variables + * for sending back with XML-RPC call for RunKeyword method? + */ + +type RobotRemoteService struct{} + +type KeywordNamesReturnValue struct { + Keywords []interface{} +} + +var offeredLibrary interface{} + +func (h *RobotRemoteService) InitilizeRemoteLibrary(library interface{}) { + offeredLibrary = library +} + +//sample XML-RPC input: GetKeywordNames +/* sample XML-RPC output: + * + * TruthOfLife + * StringsShouldBeEqual + * StopRemoteServer + * + */ +func (h *RobotRemoteService) GetKeywordNames(r *http.Request, args *struct{}, reply *KeywordNamesReturnValue) error { + //TODO: use reflection to generate array of keywords (found in the imported namespace) to return in reply + //maybe rather than all imported packages, restrict to a specific one, etc. as specified at server startup? + //keywordLibrary := new(offeredLibrary) + libraryKeywords := reflect.TypeOf(offeredLibrary) + //libraryKeywords := reflect.PtrTo(reflect.TypeOf(offeredLibrary{})) + log.Printf("Found %d keywords", libraryKeywords.NumMethod()) + for i := 0; i < libraryKeywords.NumMethod(); i++ { + reply.Keywords = append(reply.Keywords, libraryKeywords.Method(i).Name) + } + + //add special keyword built-in to the server: + reply.Keywords = append(reply.Keywords, "StopRemoteServer") + return nil +} + +func (h *RobotRemoteService) StopRemoteServer() { + //TODO: no need to call this function with goroutine if we make stopping the server more idiomatic with proper "shutdown" + //perhaps make use of channels, and have the stop server task wait on channel and only pass to channel + //when this XML-RPC method is called? And/or other ways to stop the server... + + time.Sleep(5 * time.Second) //let's arbitrarily set delay at 5 seconds + log.Printf("Remote server/library shut down at %v\n", time.Now()) + _stopRemoteServer() +} + +func _stopRemoteServer() { + os.Exit(1) +} + +type Response struct { + Content RunKeywordReturnValue +} + +type RunKeywordReturnValue struct { + Return interface{} `xml:"return"` + Status string `xml:"status"` + Stdout string `xml:"output"` + Stderr string `xml:"error"` + Traceback string `xml:"traceback"` +} + +type KeywordAndArgsInput struct { + KeywordName string + KeywordAguments []interface{} +} + +/* e.g. sample XML-RPC input + * RunKeyword + * + * KeywordName + * + * keyword_arg1 + * keyword_arg2 + * + * + * + * sample XML-RPC output + * + * + * + * + * + * return + * 42 + * + * + * status + * PASS + * + * + * output + * + * + * + * error + * + * + * + * traceback + * + * + * + * + * + * + */ +//this function doesn't fully work yet, see +//https://github.com/divan/gorilla-xmlrpc/issues/ #16 and 18 +func (h *RobotRemoteService) RunKeyword(r *http.Request, args *KeywordAndArgsInput, reply *Response) error { + //use reflection to run function "keyword name" out of 1st arg + //with 2nd arg (array) containing the args for the keyword function + //sample debug/test code for now... + log.Printf("keyword: %+v\n", args.KeywordName) + log.Printf("args: %+v\n", args.KeywordAguments) + + reply.Content.Status = "PASS" + + if args.KeywordName == "StopRemoteServer" { + go h.StopRemoteServer() + } else { + method := reflect.ValueOf(offeredLibrary).MethodByName(args.KeywordName) + if method.Type().NumIn() == len(args.KeywordAguments) { + in := make([]reflect.Value, method.Type().NumIn()) + for i := 0; i < method.Type().NumIn(); i++ { + var object interface{} + if method.Type().In(i).Kind() == reflect.Ptr { + object = offeredLibrary + } else { + object = args.KeywordAguments[i] + } + fmt.Println(i, "->", object) + in[i] = reflect.ValueOf(object) + } + returnValue := method.Call(in) + if method.Type().NumOut() == 1 { + reply.Content.Return = returnValue[0].Interface() + } else if method.Type().NumOut() > 1 { + reply.Content.Stderr = "supporting only 0 or 1 return values" + } + } else { + reply.Content.Stderr = fmt.Sprintf("incorrect amount of input variables; expected %d and got %d", method.Type().NumIn()-1, len(args.KeywordAguments)) + reply.Content.Status = "FAIL" + } + } + reply.Content.Stdout = "TODO: stdout from keyword execution gets piped into this" + //reply.Content.Stderr = "TODO: stderr from keyword execution gets piped into this" + reply.Content.Traceback = "TODO: stack trace info goes here, if any..." + return nil +} + +//the below functions & structs are optional and since not fully implemented, +//may be commented out if not wish to expose them to Robot Framework via gorrs as keywords + +type KeywordInput struct { + KeywordName string +} + +type KeywordArgumentsReturnValue struct { + KeywordAguments []interface{} +} + +//sample XML-RPC input: GetKeywordArgumentsKeywordName +//sample XML-RPC output: arg1... +func (h *RobotRemoteService) GetKeywordArguments(r *http.Request, args *KeywordInput, reply *KeywordArgumentsReturnValue) error { + //use reflection to get the arguments to keyword function and pass back to reply + log.Printf("Getting arguments for %s", args.KeywordName) + method := reflect.ValueOf(offeredLibrary).MethodByName(args.KeywordName) + j := 0 + if args.KeywordName != "StopRemoteServer" { + for i := 0; i < method.Type().NumIn(); i++ { + if method.Type().In(i).Kind() != reflect.Ptr { + methodName := method.Type().In(i).Name() + if len(methodName) == 0 || methodName == method.Type().In(i).Kind().String() { + methodName = fmt.Sprintf("arg%d", j) + } + reply.KeywordAguments = append(reply.KeywordAguments, methodName) + j++ + } + } + } + return nil +} + +type KeywordDocumentationReturnValue struct { + KeywordDocumentation string +} + +//sample XML-RPC input: GetKeywordDocumentationKeywordName +//sample XML-RPC output: godoc text +func (h *RobotRemoteService) GetKeywordDocumentation(r *http.Request, args *KeywordInput, reply *KeywordDocumentationReturnValue) error { + //makes a call shell call to godoc against the source code of the remote library package + //or equivalent go package exported function (API) if there exists such for godoc + //to then extract that go documentation for the keyword function and pass back to reply + //extract off the documentation in source code, or the documentation web endpoint (http://localhost:6060 or http://golang.org if a standard go package)? + //e.g. godoc -html -q package-name + reply.KeywordDocumentation = "Unimplemented. TODO: keyword's go documentation goes here..." + return nil +} diff --git a/runner/runner.go b/runner/runner.go new file mode 100644 index 0000000..73bea01 --- /dev/null +++ b/runner/runner.go @@ -0,0 +1,64 @@ +package runner + +import ( + "log" + "net/http" + "os" + + "github.com/daluu/gorrs/protocol" + "github.com/divan/gorilla-xmlrpc/xml" + "github.com/gorilla/rpc" +) + +/* add to import list of github.com/daluu/gorrs/protocol/protocol.go, + * the (exported) go remote (test) library packages + * to be served by this remote server via reflection. To do that since we + * have to explicitly reference packages to reflect on and not be able to + * just pass in package reference at runtime? + */ + +/* TODO: also look into whether there's any other alternative to + * gorilla/rpc and divan/gorilla-xmlrpc/xml in case of issues with XML-RPC + * support / implementation in go. Or what can be done to extend them to do + * what we need for a go-based Robot Framework generic remote library server + * + * Full spec for said server: + * https://github.com/robotframework/RemoteInterface + */ + +func RunRemoteServer(library interface{}) { + RPC := rpc.NewServer() + xmlrpcCodec := xml.NewCodec() + //map XML-RPC methods to the go implemented functions + //CamelCase mapping + xmlrpcCodec.RegisterAlias("GetKeywordNames", "RobotRemoteService.GetKeywordNames") + xmlrpcCodec.RegisterAlias("GetKeywordArguments", "RobotRemoteService.GetKeywordArguments") + xmlrpcCodec.RegisterAlias("GetKeywordDocumentation", "RobotRemoteService.GetKeywordDocumentation") + xmlrpcCodec.RegisterAlias("RunKeyword", "RobotRemoteService.RunKeyword") + //pythonic mapping + xmlrpcCodec.RegisterAlias("get_keyword_names", "RobotRemoteService.GetKeywordNames") + xmlrpcCodec.RegisterAlias("get_keyword_arguments", "RobotRemoteService.GetKeywordArguments") + xmlrpcCodec.RegisterAlias("get_keyword_documentation", "RobotRemoteService.GetKeywordDocumentation") + xmlrpcCodec.RegisterAlias("run_keyword", "RobotRemoteService.RunKeyword") + + //set server to handle both XML MIME types + RPC.RegisterCodec(xmlrpcCodec, "application/xml") + RPC.RegisterCodec(xmlrpcCodec, "text/xml") + protocolType := new(protocol.RobotRemoteService) + protocolType.InitilizeRemoteLibrary(library) + err := RPC.RegisterService(protocolType, "") + if err != nil { + log.Fatal(err) + } + http.Handle("/RPC2", RPC) //preserve option to use RPC2 endpoint + http.Handle("/", RPC) //but not make it required when using with Robot Framework + + port := os.Getenv("PORT") + if port == "" { + port = "8270" + } + + //TODO: make port and host/IP address binding be configurable via CLI flags and not fixed to localhost:8270 (the default) + log.Printf("Robot remote server started on localhost:%s under / and /RPC2 endpoints. Stop server with Ctrl+C, kill, etc. or XML-RPC method 'run_keyword' with parameter 'stop_remote_server'\n", port) + log.Fatal(http.ListenAndServe(":"+port, nil)) +}