From 354f6e65db579b5297b8b42fe4293a89ee95ba35 Mon Sep 17 00:00:00 2001 From: David Luu Date: Sun, 9 Oct 2016 00:40:58 -0700 Subject: [PATCH 1/6] initial commit --- LICENSE | 27 +++++ README.md | 20 +++- example/example_tests.robot | 16 +++ libraries/example_library.go | 37 +++++++ main.go | 43 ++++++++ protocol/protocol.go | 202 +++++++++++++++++++++++++++++++++++ 6 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 LICENSE create mode 100644 example/example_tests.robot create mode 100644 libraries/example_library.go create mode 100644 main.go create mode 100644 protocol/protocol.go 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..b219bef 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,20 @@ # gorrs -Generic Robot Framework remote library server implementation in go + +Pronounced like "gore's", abbreviation for "GO Robot Remote Server", a generic Robot Framework remote library server implementation 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.5+. And set up your $GOPATH and $GOBIN environment variables. +2. Get a copy of gorrs for go: ```go get github.com/daluu/gorrs``` +3. Get gorrs external dependencies: ```go get github.com/gorilla/rpc``` and ```go get github.com/divan/gorilla-xmlrpc/xml```. Sorry there are several go package managers, and not a true single standard yet. So I don't want to stick with any at the moment. + +## 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/libraries/example_library.go b/libraries/example_library.go new file mode 100644 index 0000000..690d870 --- /dev/null +++ b/libraries/example_library.go @@ -0,0 +1,37 @@ +package libraries + +import ( + "errors" + "fmt" + "io/ioutil" +) + +//Example library to be used with Robot Framework's remote server. +type ExampleRemoteLibrary struct{} + +//Returns 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 +} + +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 + +func (lib *ExampleRemoteLibrary) TruthOfLife() int { + return 42 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..b5b8f18 --- /dev/null +++ b/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "log" + "net/http" + + "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 main() { + RPC := rpc.NewServer() + xmlrpcCodec := xml.NewCodec() + RPC.RegisterCodec(xmlrpcCodec, "text/xml") + // is there a way to register XML-RPC service such that when XML-RPC client calls the service + // they refer to service w/o a namespace? e.g. "RunKeyword" instead of "RobotRemoteService.RunKeyword"? + // see https://github.com/divan/gorilla-xmlrpc/issues/14 + // and https://github.com/gorilla/rpc/issues/48 + RPC.RegisterService(new(protocol.RobotRemoteService), "") + http.Handle("/RPC2", RPC) //preserve option to use RPC2 endpoint + http.Handle("/", RPC) //but not make it required when using with Robot Framework + + //TODO: make port and host/IP address binding be configurable via CLI flags and not fixed to localhost:8270 (the default) + log.Println("Robot remote server started on localhost:8270 under / and /RPC2 endpoints. Stop server with Ctrl+C, kill, etc. or XML-RPC method 'run_keyword' with parameter 'stop_remote_server'\n") + log.Fatal(http.ListenAndServe(":8270", nil)) +} diff --git a/protocol/protocol.go b/protocol/protocol.go new file mode 100644 index 0000000..e1f0530 --- /dev/null +++ b/protocol/protocol.go @@ -0,0 +1,202 @@ +package protocol + +import ( + "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{} +} + +//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? + + //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 RunKeywordReturnValue struct { + Return interface{} `xml:"return"` + Status string `xml:"status"` + Output string `xml:"output"` + Error 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 *RunKeywordReturnValue) 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) + if args.KeywordName == "StopRemoteServer" { + go h.StopRemoteServer() + } + log.Printf("args: %+v\n", args.KeywordAguments) + for _, a := range args.KeywordAguments { + log.Printf("arg: %+v\n", a) + switch reflect.TypeOf(a).Kind() { + case reflect.Slice: + log.Printf("args:\n") + s := reflect.ValueOf(a) + for i := 0; i < s.Len(); i++ { + log.Printf("%v: %+v\n", i, s.Index(i)) + } + default: + log.Println("Somehow didn't get an array of arguments for keyword.") + } + } + //and return the results in struct below (sample static output for now): + var retval interface{} + retval = 42 //truth of life + reply.Return = retval + reply.Status = "FAIL" + reply.Output = "stdout from keyword execution gets piped into this" + reply.Error = "gorrs not fully implemented yet, just a proof of concept design at the moment. stderr from keyword execution gets piped into this, by the way." + reply.Traceback = "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 + reply.KeywordAguments = make([]interface{}, 0) //if to pass back no arguments + //http://stackoverflow.com/questions/12990338/cannot-convert-string-to-interface + //else something like reply.KeywordAguments = append(reply.KeywordAguments,"two","arguments") + 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 +} From 16657186f158368b54591fc76683e26448700dc1 Mon Sep 17 00:00:00 2001 From: David Luu Date: Sun, 9 Oct 2016 00:43:04 -0700 Subject: [PATCH 2/6] add links to RF --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b219bef..a776909 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # gorrs -Pronounced like "gore's", abbreviation for "GO Robot Remote Server", a 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. From 5e2d495857ed3e123e99c7b177765dd0bb09e66e Mon Sep 17 00:00:00 2001 From: David Luu Date: Sat, 26 Nov 2016 21:55:39 -0800 Subject: [PATCH 3/6] XML-RPC server methods to go functions alias mapping, add handling application/xml MIME type, variable name changes --- main.go | 19 +++++++++++++++---- protocol/protocol.go | 10 +++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index b5b8f18..1fab879 100644 --- a/main.go +++ b/main.go @@ -28,11 +28,22 @@ import ( func main() { 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") - // is there a way to register XML-RPC service such that when XML-RPC client calls the service - // they refer to service w/o a namespace? e.g. "RunKeyword" instead of "RobotRemoteService.RunKeyword"? - // see https://github.com/divan/gorilla-xmlrpc/issues/14 - // and https://github.com/gorilla/rpc/issues/48 + RPC.RegisterService(new(protocol.RobotRemoteService), "") http.Handle("/RPC2", RPC) //preserve option to use RPC2 endpoint http.Handle("/", RPC) //but not make it required when using with Robot Framework diff --git a/protocol/protocol.go b/protocol/protocol.go index e1f0530..1b9814f 100644 --- a/protocol/protocol.go +++ b/protocol/protocol.go @@ -79,8 +79,8 @@ func _stopRemoteServer() { type RunKeywordReturnValue struct { Return interface{} `xml:"return"` Status string `xml:"status"` - Output string `xml:"output"` - Error string `xml:"error"` + Stdout string `xml:"output"` + Stderr string `xml:"error"` Traceback string `xml:"traceback"` } @@ -158,9 +158,9 @@ func (h *RobotRemoteService) RunKeyword(r *http.Request, args *KeywordAndArgsInp retval = 42 //truth of life reply.Return = retval reply.Status = "FAIL" - reply.Output = "stdout from keyword execution gets piped into this" - reply.Error = "gorrs not fully implemented yet, just a proof of concept design at the moment. stderr from keyword execution gets piped into this, by the way." - reply.Traceback = "stack trace info goes here, if any..." + reply.Stdout = "TODO: stdout from keyword execution gets piped into this" + reply.Stderr = "TODO: stderr from keyword execution gets piped into this" + reply.Traceback = "TODO: stack trace info goes here, if any..." return nil } From c1ebfd7cfc64a146f8bab47eb5c5981f10ed41c7 Mon Sep 17 00:00:00 2001 From: dluu Date: Tue, 12 Nov 2019 23:36:19 -0800 Subject: [PATCH 4/6] add go modules versioning and update README for it --- README.md | 7 ++++--- go.mod | 9 +++++++++ go.sum | 6 ++++++ 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 go.mod create mode 100644 go.sum diff --git a/README.md b/README.md index a776909..32517d3 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ This is a proof of concept prototype. Not fully working at the moment. See the s ## Setup -1. Have a version of [go](https://golang.org/dl/) installed. Recommend go 1.5+. And set up your $GOPATH and $GOBIN environment variables. -2. Get a copy of gorrs for go: ```go get github.com/daluu/gorrs``` -3. Get gorrs external dependencies: ```go get github.com/gorilla/rpc``` and ```go get github.com/divan/gorilla-xmlrpc/xml```. Sorry there are several go package managers, and not a true single standard yet. So I don't want to stick with any at the moment. +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): diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..792f6cc --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2d29f90 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +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= From d544de60ee75bfa86228ceec5f7802e9db7d766c Mon Sep 17 00:00:00 2001 From: Juho Saarinen Date: Fri, 10 Jan 2020 12:55:30 +0200 Subject: [PATCH 5/6] Returning keyword names Non-argument calls return value, argument calls doesn't Dynamic importing of library Example for dynamic import created Replacing gorilla-xmlrpc with forked version to get arguments in Runner for start --- example/go.mod | 7 +++ example/go.sum | 8 ++++ example/main.go | 10 +++++ go.mod | 2 + go.sum | 2 + libraries/example_library.go | 15 ++++++- main.go | 31 +------------ protocol/protocol.go | 87 ++++++++++++++++++++++++++---------- runner/runner.go | 55 +++++++++++++++++++++++ 9 files changed, 161 insertions(+), 56 deletions(-) create mode 100644 example/go.mod create mode 100644 example/go.sum create mode 100644 example/main.go create mode 100644 runner/runner.go 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 index 792f6cc..3748b7f 100644 --- a/go.mod +++ b/go.mod @@ -7,3 +7,5 @@ require ( 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 index 2d29f90..bce20d4 100644 --- a/go.sum +++ b/go.sum @@ -4,3 +4,5 @@ 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 index 690d870..a25049d 100644 --- a/libraries/example_library.go +++ b/libraries/example_library.go @@ -6,10 +6,10 @@ import ( "io/ioutil" ) -//Example library to be used with Robot Framework's remote server. +//ExampleRemoteLibrary to be used with Robot Framework's remote server. type ExampleRemoteLibrary struct{} -//Returns the number of items in the directory specified by `path`. +//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) @@ -20,6 +20,7 @@ func (lib *ExampleRemoteLibrary) CountItemsInDirectory(path string) (int, error) return fileCount, err } +//StringsShouldBeEqual ... func (lib *ExampleRemoteLibrary) StringsShouldBeEqual(str1 string, str2 string) error { fmt.Printf("Comparing '%s' to '%s'.", str1, str2) if str1 != str2 { @@ -32,6 +33,16 @@ func (lib *ExampleRemoteLibrary) StringsShouldBeEqual(str1 string, str2 string) //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 index 1fab879..fdaa889 100644 --- a/main.go +++ b/main.go @@ -2,11 +2,6 @@ package main import ( "log" - "net/http" - - "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, @@ -26,29 +21,5 @@ import ( */ func main() { - 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") - - RPC.RegisterService(new(protocol.RobotRemoteService), "") - http.Handle("/RPC2", RPC) //preserve option to use RPC2 endpoint - http.Handle("/", RPC) //but not make it required when using with Robot Framework - - //TODO: make port and host/IP address binding be configurable via CLI flags and not fixed to localhost:8270 (the default) - log.Println("Robot remote server started on localhost:8270 under / and /RPC2 endpoints. Stop server with Ctrl+C, kill, etc. or XML-RPC method 'run_keyword' with parameter 'stop_remote_server'\n") - log.Fatal(http.ListenAndServe(":8270", nil)) + log.Fatal("not runnable directly") } diff --git a/protocol/protocol.go b/protocol/protocol.go index 1b9814f..ae3693e 100644 --- a/protocol/protocol.go +++ b/protocol/protocol.go @@ -1,6 +1,7 @@ package protocol import ( + "fmt" "log" "net/http" "os" @@ -45,6 +46,12 @@ 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: * @@ -56,6 +63,13 @@ type KeywordNamesReturnValue struct { 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") @@ -76,6 +90,10 @@ func _stopRemoteServer() { os.Exit(1) } +type Response struct { + Content RunKeywordReturnValue +} + type RunKeywordReturnValue struct { Return interface{} `xml:"return"` Status string `xml:"status"` @@ -131,36 +149,45 @@ type KeywordAndArgsInput struct { */ //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 *RunKeywordReturnValue) error { +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() - } - log.Printf("args: %+v\n", args.KeywordAguments) - for _, a := range args.KeywordAguments { - log.Printf("arg: %+v\n", a) - switch reflect.TypeOf(a).Kind() { - case reflect.Slice: - log.Printf("args:\n") - s := reflect.ValueOf(a) - for i := 0; i < s.Len(); i++ { - log.Printf("%v: %+v\n", i, s.Index(i)) + } 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" } - default: - log.Println("Somehow didn't get an array of arguments for keyword.") + } 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" } } - //and return the results in struct below (sample static output for now): - var retval interface{} - retval = 42 //truth of life - reply.Return = retval - reply.Status = "FAIL" - reply.Stdout = "TODO: stdout from keyword execution gets piped into this" - reply.Stderr = "TODO: stderr from keyword execution gets piped into this" - reply.Traceback = "TODO: stack trace info goes here, if any..." + 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 } @@ -179,9 +206,21 @@ type KeywordArgumentsReturnValue struct { //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 - reply.KeywordAguments = make([]interface{}, 0) //if to pass back no arguments - //http://stackoverflow.com/questions/12990338/cannot-convert-string-to-interface - //else something like reply.KeywordAguments = append(reply.KeywordAguments,"two","arguments") + 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 } diff --git a/runner/runner.go b/runner/runner.go new file mode 100644 index 0000000..c4ba68f --- /dev/null +++ b/runner/runner.go @@ -0,0 +1,55 @@ +package runner + +import ( + "log" + "net/http" + + "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) + RPC.RegisterService(protocolType, "") + http.Handle("/RPC2", RPC) //preserve option to use RPC2 endpoint + http.Handle("/", RPC) //but not make it required when using with Robot Framework + + //TODO: make port and host/IP address binding be configurable via CLI flags and not fixed to localhost:8270 (the default) + log.Println("Robot remote server started on localhost:8270 under / and /RPC2 endpoints. Stop server with Ctrl+C, kill, etc. or XML-RPC method 'run_keyword' with parameter 'stop_remote_server'") + log.Fatal(http.ListenAndServe(":8270", nil)) +} From 59dbf38c24745788f933c41e2898724d21c00a03 Mon Sep 17 00:00:00 2001 From: Hi-Fi Date: Mon, 13 Jan 2020 20:33:21 +0200 Subject: [PATCH 6/6] Port from environment variable PORT (App Engine support) --- runner/runner.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/runner/runner.go b/runner/runner.go index c4ba68f..73bea01 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -3,6 +3,7 @@ package runner import ( "log" "net/http" + "os" "github.com/daluu/gorrs/protocol" "github.com/divan/gorilla-xmlrpc/xml" @@ -45,11 +46,19 @@ func RunRemoteServer(library interface{}) { RPC.RegisterCodec(xmlrpcCodec, "text/xml") protocolType := new(protocol.RobotRemoteService) protocolType.InitilizeRemoteLibrary(library) - RPC.RegisterService(protocolType, "") + 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.Println("Robot remote server started on localhost:8270 under / and /RPC2 endpoints. Stop server with Ctrl+C, kill, etc. or XML-RPC method 'run_keyword' with parameter 'stop_remote_server'") - log.Fatal(http.ListenAndServe(":8270", nil)) + 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)) }