mirror of
https://github.com/omniscale/magnacarto.git
synced 2025-09-29 14:03:33 +02:00
594 lines
14 KiB
Go
594 lines
14 KiB
Go
package pie
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/rpc"
|
|
"net/rpc/jsonrpc"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var _ io.ReadWriteCloser = rwCloser{}
|
|
var _ io.ReadWriteCloser = ioPipe{}
|
|
|
|
func TestRWCloser(t *testing.T) {
|
|
rc := &closeRW{}
|
|
wc := &closeRW{}
|
|
rwc := rwCloser{rc, wc}
|
|
if err := rwc.Close(); err != nil {
|
|
t.Errorf("unexpected error from rwCloser.Close: %#v", err)
|
|
}
|
|
if !rc.closed {
|
|
t.Error("Close not called on ReadCloser.")
|
|
}
|
|
if !wc.closed {
|
|
t.Error("Close not called on WriteCloser.")
|
|
}
|
|
}
|
|
|
|
func TestRWCloserReadCloserError(t *testing.T) {
|
|
readCloserErr := errors.New("read")
|
|
rc := &closeRW{err: readCloserErr}
|
|
wc := &closeRW{}
|
|
rwc := rwCloser{rc, wc}
|
|
err := rwc.Close()
|
|
if !rc.closed {
|
|
t.Error("Close not called on ReadCloser.")
|
|
}
|
|
if !wc.closed {
|
|
t.Error("Close not called on WriteCloser.")
|
|
}
|
|
if err == nil {
|
|
t.Error("ReadCloser error not passed through from rwCloser.Close")
|
|
}
|
|
if err != readCloserErr {
|
|
t.Errorf("Different error returned from rwCloser than expected: %#v", err)
|
|
}
|
|
}
|
|
|
|
func TestRWCloserWriteCloserError(t *testing.T) {
|
|
writeCloserErr := errors.New("write")
|
|
rc := &closeRW{}
|
|
wc := &closeRW{err: writeCloserErr}
|
|
rwc := rwCloser{rc, wc}
|
|
err := rwc.Close()
|
|
if !rc.closed {
|
|
t.Error("Close not called on ReadCloser.")
|
|
}
|
|
if !wc.closed {
|
|
t.Error("Close not called on WriteCloser.")
|
|
}
|
|
if err == nil {
|
|
t.Error("ReadCloser error not passed through from rwCloser.Close")
|
|
}
|
|
if err != writeCloserErr {
|
|
t.Errorf("Different error returned from rwCloser than expected: %#v", err)
|
|
}
|
|
}
|
|
|
|
func TestRWCloserBothCloserError(t *testing.T) {
|
|
writeCloserErr := errors.New("write")
|
|
readCloserErr := errors.New("read")
|
|
rc := &closeRW{err: readCloserErr}
|
|
wc := &closeRW{err: writeCloserErr}
|
|
rwc := rwCloser{rc, wc}
|
|
err := rwc.Close()
|
|
if !rc.closed {
|
|
t.Error("Close not called on ReadCloser.")
|
|
}
|
|
if !wc.closed {
|
|
t.Error("Close not called on WriteCloser.")
|
|
}
|
|
if err == nil {
|
|
t.Error("Error not passed through from rwCloser.Close")
|
|
}
|
|
|
|
// I don't think we actually care which of these errors gets returned, as
|
|
// long as one of them does.
|
|
if err != writeCloserErr && err != readCloserErr {
|
|
t.Errorf("Different error returned from rwCloser than expected: %#v", err)
|
|
}
|
|
}
|
|
|
|
func TestIOPipeClose(t *testing.T) {
|
|
rc := &closeRW{}
|
|
wc := &closeRW{}
|
|
p := &proc{}
|
|
iop := ioPipe{rc, wc, p}
|
|
if err := iop.Close(); err != nil {
|
|
t.Errorf("Unexpected error from ioPipe.Close: %#v", err)
|
|
}
|
|
if !rc.closed {
|
|
t.Error("Close not called on ReadCloser.")
|
|
}
|
|
if !wc.closed {
|
|
t.Error("Close not called on WriteCloser.")
|
|
}
|
|
if p.sig == nil {
|
|
t.Errorf("No signal sent to process")
|
|
}
|
|
if p.sig != os.Interrupt {
|
|
t.Errorf("Unexpected signal sent to process, expected os.Interrupt, got %#v", p.sig)
|
|
}
|
|
if p.killed {
|
|
t.Errorf("Kill() called unexpectedly on process.")
|
|
}
|
|
}
|
|
|
|
func TestIOPipeSlowProc(t *testing.T) {
|
|
defer func(d time.Duration) {
|
|
procTimeout = d
|
|
}(procTimeout)
|
|
procTimeout = 5 * time.Millisecond
|
|
rc := &closeRW{}
|
|
wc := &closeRW{}
|
|
p := &proc{delay: procTimeout * 2}
|
|
iop := ioPipe{rc, wc, p}
|
|
if err := iop.Close(); err != errProcStopTimeout {
|
|
t.Errorf("Unexpected error from ioPipe.Close, expected %#v, got: %#v", errProcStopTimeout, err)
|
|
}
|
|
if !rc.closed {
|
|
t.Error("Close not called on ReadCloser.")
|
|
}
|
|
if !wc.closed {
|
|
t.Error("Close not called on WriteCloser.")
|
|
}
|
|
if p.sig == nil {
|
|
t.Errorf("no signal sent to process")
|
|
}
|
|
if p.sig != os.Interrupt {
|
|
t.Errorf("Unexpected signal sent to process, expected os.Interrupt, got %#v", p.sig)
|
|
}
|
|
if !p.killed {
|
|
t.Errorf("Kill() unexpectedly not called on process.")
|
|
}
|
|
}
|
|
|
|
func TestNewProvider(t *testing.T) {
|
|
p := NewProvider()
|
|
if p.server == nil {
|
|
t.Error("Unexpected nil rpc Server")
|
|
}
|
|
if p.rwc == nil {
|
|
t.Error("Unexpected nil ReadWriteCloser")
|
|
}
|
|
rwc, ok := p.rwc.(rwCloser)
|
|
if !ok {
|
|
t.Errorf("Expected ReadWriteCloser to be rwCloser, but is %#v", p.rwc)
|
|
}
|
|
if rwc.ReadCloser != os.Stdin {
|
|
t.Errorf("Expected rwc.ReadCloser to be os.Stdin but is %#v", rwc.ReadCloser)
|
|
}
|
|
if rwc.WriteCloser != os.Stdout {
|
|
t.Errorf("Expected rwc.WriteCloser to be os.Stdout but is %#v", rwc.ReadCloser)
|
|
}
|
|
}
|
|
|
|
func TestNewConsumer(t *testing.T) {
|
|
c := NewConsumer()
|
|
if c == nil {
|
|
t.Fatal("Unexpected nil pointer from NewConsumer")
|
|
}
|
|
}
|
|
|
|
func TestNewConsumerCodec(t *testing.T) {
|
|
tcc := &testClientCodec{}
|
|
c := NewConsumerCodec(tcc.NewClientCodec)
|
|
if c == nil {
|
|
t.Fatal("Unexpected nil pointer from NewConsumerCodec")
|
|
}
|
|
if !tcc.called {
|
|
t.Fatal("NewClientCodec function never called.")
|
|
}
|
|
}
|
|
|
|
func TestServeAndStart(t *testing.T) {
|
|
testServeAndStart(nil, nil, t)
|
|
}
|
|
|
|
func TestServeAndStartCodec(t *testing.T) {
|
|
testServeAndStart(jsonrpc.NewServerCodec, jsonrpc.NewClientCodec, t)
|
|
}
|
|
|
|
func testServeAndStart(
|
|
servercodec func(io.ReadWriteCloser) rpc.ServerCodec,
|
|
clientcodec func(io.ReadWriteCloser) rpc.ClientCodec,
|
|
t *testing.T,
|
|
) {
|
|
// set up some pipes for reading/writing that we can pretend are
|
|
// stdin and stdout for a plugin application.
|
|
stdinR, stdinW := io.Pipe()
|
|
stdoutR, stdoutW := io.Pipe()
|
|
process := &proc{}
|
|
|
|
rwc := rwCloser{
|
|
ReadCloser: stdinR,
|
|
WriteCloser: stdoutW,
|
|
}
|
|
|
|
// now start a plugin provider using these pipes
|
|
s := Server{server: rpc.NewServer(), rwc: rwc}
|
|
|
|
api := api{}
|
|
s.RegisterName("api", api)
|
|
api2 := API2{}
|
|
s.Register(api2)
|
|
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
if servercodec == nil {
|
|
s.Serve()
|
|
} else {
|
|
s.ServeCodec(servercodec)
|
|
}
|
|
close(done)
|
|
}()
|
|
|
|
// now we mock out the makeCommand that'll get called by the host.
|
|
f := &fakeCmdData{
|
|
stdout: stdoutR,
|
|
stdin: stdinW,
|
|
p: process,
|
|
}
|
|
old := makeCommand
|
|
makeCommand = f.makeCommand
|
|
defer func() { makeCommand = old }()
|
|
|
|
output := &bytes.Buffer{}
|
|
path := "foo"
|
|
args := []string{"bar", "baz"}
|
|
var client *rpc.Client
|
|
var err error
|
|
if clientcodec == nil {
|
|
client, err = StartProvider(output, path, args...)
|
|
if err != nil {
|
|
t.Errorf("Unexpected non-nil error from Start: %#v", err)
|
|
}
|
|
} else {
|
|
client, err = StartProviderCodec(clientcodec, output, path, args...)
|
|
if err != nil {
|
|
t.Errorf("Unexpected non-nil error from StartWithCodec: %#v", err)
|
|
}
|
|
}
|
|
|
|
if f.w != output {
|
|
t.Error("Output writer not passed to makeCommand")
|
|
}
|
|
if f.path != path {
|
|
t.Error("Path not passed to makeCommand")
|
|
}
|
|
if !reflect.DeepEqual(f.args, args) {
|
|
t.Error("Args not passed to makeCommand")
|
|
}
|
|
|
|
name := "bob"
|
|
var response string
|
|
if err := client.Call("api.SayHi", name, &response); err != nil {
|
|
t.Fatalf("Unexpected non-nil error from client.Call: %#v", err)
|
|
}
|
|
var expected string
|
|
api.SayHi(name, &expected)
|
|
if response != expected {
|
|
t.Fatalf("Wrong Response from api call, expected %q, got %q", expected, response)
|
|
}
|
|
if err := client.Call("API2.SayBye", name, &response); err != nil {
|
|
t.Fatalf("Unexpected non-nil error from client.Call: %#v", err)
|
|
}
|
|
api2.SayBye(name, &expected)
|
|
if response != expected {
|
|
t.Fatalf("Wrong Response from API2 call, expected %q, got %q", expected, response)
|
|
}
|
|
if err := client.Close(); err != nil {
|
|
t.Fatalf("Unexpected non-nil error from client.Call: %#v", err)
|
|
}
|
|
select {
|
|
case <-done:
|
|
// pass
|
|
case <-time.After(time.Millisecond * 10):
|
|
t.Fatal("Server failed to stop after close in 10ms")
|
|
}
|
|
}
|
|
|
|
func TestConsumerClientClose(t *testing.T) {
|
|
testConsumer(false, nil, nil, t)
|
|
}
|
|
|
|
func TestConsumerServerClose(t *testing.T) {
|
|
testConsumer(true, nil, nil, t)
|
|
}
|
|
|
|
func TestConsumerCodecClientCLose(t *testing.T) {
|
|
testConsumer(false, jsonrpc.NewServerCodec, jsonrpc.NewClientCodec, t)
|
|
}
|
|
|
|
func TestConsumerCodecServerClose(t *testing.T) {
|
|
testConsumer(true, jsonrpc.NewServerCodec, jsonrpc.NewClientCodec, t)
|
|
}
|
|
|
|
func testConsumer(
|
|
closeServer bool,
|
|
servercodec func(io.ReadWriteCloser) rpc.ServerCodec,
|
|
clientcodec func(io.ReadWriteCloser) rpc.ClientCodec,
|
|
t *testing.T,
|
|
) {
|
|
// set up some pipes for reading/writing that we can pretend are
|
|
// stdin and stdout for a plugin application.
|
|
stdinR, stdinW := io.Pipe()
|
|
stdoutR, stdoutW := io.Pipe()
|
|
process := &proc{}
|
|
|
|
// mock out the makeCommand that'll get called by the host.
|
|
f := &fakeCmdData{
|
|
stdout: stdoutR,
|
|
stdin: stdinW,
|
|
p: process,
|
|
}
|
|
old := makeCommand
|
|
makeCommand = f.makeCommand
|
|
defer func() { makeCommand = old }()
|
|
output := &bytes.Buffer{}
|
|
|
|
path := "foo"
|
|
args := []string{"bar", "baz"}
|
|
server, err := StartConsumer(output, "foo", args...)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from StartConsumer: %#v", err)
|
|
}
|
|
|
|
if f.w != output {
|
|
t.Error("Output writer not passed to makeCommand")
|
|
}
|
|
if f.path != path {
|
|
t.Error("Path not passed to makeCommand")
|
|
}
|
|
if !reflect.DeepEqual(f.args, args) {
|
|
t.Error("Args not passed to makeCommand")
|
|
}
|
|
|
|
api := api{}
|
|
server.RegisterName("api", api)
|
|
api2 := API2{}
|
|
server.Register(api2)
|
|
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
if servercodec == nil {
|
|
server.Serve()
|
|
} else {
|
|
server.ServeCodec(servercodec)
|
|
}
|
|
close(done)
|
|
}()
|
|
|
|
var client *rpc.Client
|
|
if clientcodec == nil {
|
|
client = rpc.NewClient(rwCloser{stdinR, stdoutW})
|
|
} else {
|
|
client = rpc.NewClientWithCodec(clientcodec(rwCloser{stdinR, stdoutW}))
|
|
}
|
|
defer client.Close()
|
|
|
|
name := "bob"
|
|
var response string
|
|
if err := client.Call("api.SayHi", name, &response); err != nil {
|
|
t.Fatalf("Unexpected non-nil error from client.Call: %#v", err)
|
|
}
|
|
var expected string
|
|
api.SayHi(name, &expected)
|
|
if response != expected {
|
|
t.Fatalf("Wrong Response from api call, expected %q, got %q", expected, response)
|
|
}
|
|
|
|
if err := client.Call("API2.SayBye", name, &response); err != nil {
|
|
t.Fatalf("Unexpected non-nil error from client.Call: %#v", err)
|
|
}
|
|
api2.SayBye(name, &expected)
|
|
if response != expected {
|
|
t.Fatalf("Wrong Response from api2 call, expected %q, got %q", expected, response)
|
|
}
|
|
|
|
if closeServer {
|
|
if err := server.Close(); err != nil {
|
|
t.Fatalf("Unexpected non-nil error from server.Close: %#v", err)
|
|
}
|
|
} else {
|
|
if err := client.Close(); err != nil {
|
|
t.Fatalf("Unexpected non-nil error from client.Call: %#v", err)
|
|
}
|
|
}
|
|
select {
|
|
case <-done:
|
|
// pass
|
|
case <-time.After(time.Millisecond * 10):
|
|
t.Fatal("Server failed to stop after close in 10ms")
|
|
}
|
|
|
|
if closeServer && !process.waited {
|
|
t.Fatal("Server was closed, but process was not stopped")
|
|
}
|
|
}
|
|
|
|
func TestMakeCommandAndStart(t *testing.T) {
|
|
path := "echo"
|
|
args := []string{"something"}
|
|
c := makeCommand(nil, path, args)
|
|
_, ok := c.(execCmd)
|
|
if !ok {
|
|
t.Fatalf("Expected commander to be type execCmd, but was %#v", c)
|
|
}
|
|
pipe, err := start(c)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected non-nil error: %#v", err)
|
|
}
|
|
defer pipe.Close()
|
|
if pipe.proc == nil {
|
|
t.Fatal("Unexpected nil proc in ioPipe")
|
|
}
|
|
p, ok := pipe.proc.(*os.Process)
|
|
if !ok {
|
|
t.Fatalf("Expected proc to be os.Process but was %#v", pipe.proc)
|
|
}
|
|
var out []byte
|
|
var readerr error
|
|
readFinished := make(chan struct{})
|
|
go func() {
|
|
out, readerr = ioutil.ReadAll(pipe.ReadCloser)
|
|
close(readFinished)
|
|
}()
|
|
go func() {
|
|
// When the process finishes, the above ioutil.ReadAll will complete as
|
|
// well.
|
|
p.Wait()
|
|
}()
|
|
select {
|
|
case <-time.After(time.Millisecond * 100):
|
|
p.Kill()
|
|
t.Fatalf("Timed out waiting for process to run")
|
|
case <-readFinished:
|
|
if readerr != nil {
|
|
t.Fatalf("Unexpected error reading from the process' stdout: %#v", readerr)
|
|
}
|
|
actual := strings.TrimSpace(string(out))
|
|
if actual != args[0] {
|
|
t.Fatalf("Wrong output, expected %q, got %q", args[0], actual)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
type testClientCodec struct {
|
|
called bool
|
|
}
|
|
|
|
func (t *testClientCodec) NewClientCodec(r io.ReadWriteCloser) rpc.ClientCodec {
|
|
t.called = true
|
|
return jsonrpc.NewClientCodec(r)
|
|
}
|
|
|
|
func fakeServerCodec(conn io.ReadWriteCloser) rpc.ServerCodec {
|
|
return nil
|
|
}
|
|
|
|
type fakeCmdData struct {
|
|
stdout io.ReadCloser
|
|
stdin io.WriteCloser
|
|
p *proc
|
|
w io.Writer
|
|
path string
|
|
args []string
|
|
}
|
|
|
|
func (f *fakeCmdData) makeCommand(w io.Writer, path string, args []string) commander {
|
|
f.w = w
|
|
f.path = path
|
|
f.args = args
|
|
return fakeCommand{f.stdin, f.stdout, f.p}
|
|
}
|
|
|
|
type nopWCloser struct {
|
|
io.Writer
|
|
}
|
|
|
|
func (nopWCloser) Close() error { return nil }
|
|
|
|
type fakeCommand struct {
|
|
stdin io.WriteCloser
|
|
stdout io.ReadCloser
|
|
p *proc
|
|
}
|
|
|
|
func (f fakeCommand) Start() (osProcess, error) {
|
|
return f.p, nil
|
|
}
|
|
|
|
func (f fakeCommand) StdinPipe() (io.WriteCloser, error) {
|
|
return f.stdin, nil
|
|
}
|
|
|
|
func (f fakeCommand) StdoutPipe() (io.ReadCloser, error) {
|
|
return f.stdout, nil
|
|
}
|
|
|
|
type api struct{}
|
|
|
|
func (api) SayHi(name string, response *string) error {
|
|
*response = "Hi " + name
|
|
return nil
|
|
}
|
|
|
|
type API2 struct{}
|
|
|
|
func (API2) SayBye(name string, response *string) error {
|
|
*response = "Bye " + name
|
|
return nil
|
|
}
|
|
|
|
// proc is a helper that fullfills the osProcess interface for testing purposes.
|
|
type proc struct {
|
|
mu sync.Mutex
|
|
delay time.Duration
|
|
waitErr error
|
|
killErr error
|
|
signalErr error
|
|
sig os.Signal
|
|
killed bool
|
|
waited bool
|
|
}
|
|
|
|
// Wait will wait for delay time and then return waitErr.
|
|
func (p *proc) Wait() (*os.ProcessState, error) {
|
|
p.mu.Lock()
|
|
p.waited = true
|
|
p.mu.Unlock()
|
|
<-time.After(p.delay)
|
|
return nil, p.waitErr
|
|
}
|
|
|
|
// Kill returns killErr.
|
|
func (p *proc) Kill() error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.killed = true
|
|
return p.killErr
|
|
}
|
|
|
|
// Signal ignores the signal and returns signalErr.
|
|
func (p *proc) Signal(sig os.Signal) error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.sig = sig
|
|
return p.signalErr
|
|
}
|
|
|
|
// closeRW is a helper that fulfills io.Reader, io.Writer, and io.Closer for
|
|
// testing purposes.
|
|
type closeRW struct {
|
|
closed bool
|
|
err error
|
|
}
|
|
|
|
// Close fulfills io.Closer and will record that it was called, and return this
|
|
// value's error, if any.
|
|
func (c *closeRW) Close() error {
|
|
c.closed = true
|
|
return c.err
|
|
}
|
|
|
|
// Read fulfills io.Reader and does nothing.
|
|
func (*closeRW) Read(_ []byte) (int, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
// Write fulfills io.Writer and does nothing.
|
|
func (*closeRW) Write(_ []byte) (int, error) {
|
|
return 0, nil
|
|
}
|