mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-14 20:48:17 +08:00
232 lines
7.2 KiB
Go
232 lines
7.2 KiB
Go
//go:build unix
|
|
|
|
package control
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"net"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/matthewlu070111/smart-srun/core/internal/domain"
|
|
)
|
|
|
|
// answering starts a socket that runs answer for one connection.
|
|
//
|
|
// A hand-rolled server rather than the real one, because these are the cases
|
|
// the real one cannot produce: an id that does not match, a version from the
|
|
// future, a response longer than the limit. A client that only ever talks to a
|
|
// correct server is a client whose checks have never run.
|
|
func answering(t *testing.T, answer func(request Request) []byte) Client {
|
|
t.Helper()
|
|
|
|
path := filepath.Join(t.TempDir(), "control.sock")
|
|
listener, err := net.Listen("unix", path)
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
|
|
var served sync.WaitGroup
|
|
served.Go(func() {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
reader := bufio.NewReader(conn)
|
|
line, err := readLine(reader, MaxRequestBytes)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var request Request
|
|
_ = json.Unmarshal(line, &request)
|
|
_, _ = conn.Write(append(answer(request), '\n'))
|
|
})
|
|
t.Cleanup(func() {
|
|
listener.Close()
|
|
served.Wait()
|
|
})
|
|
return Client{Path: path}
|
|
}
|
|
|
|
func mustEncode(t *testing.T, response Response) []byte {
|
|
t.Helper()
|
|
encoded, err := json.Marshal(response)
|
|
if err != nil {
|
|
t.Fatalf("encode: %v", err)
|
|
}
|
|
return encoded
|
|
}
|
|
|
|
// Nothing listening is the ordinary "it is not running" case, and it has its
|
|
// own code so the CLI can exit 3 rather than report a transport fault the user
|
|
// cannot act on.
|
|
func TestCallingAServiceThatIsNotThereIsServiceStopped(t *testing.T) {
|
|
client := Client{Path: filepath.Join(t.TempDir(), "absent.sock")}
|
|
|
|
_, err := client.Call(t.Context(), "version.get", nil)
|
|
if err == nil {
|
|
t.Fatal("a call to nothing succeeded")
|
|
}
|
|
if code, _ := domain.CodeOf(err); code != domain.CodeServiceStopped {
|
|
t.Errorf("code = %s, want ServiceStopped", code)
|
|
}
|
|
}
|
|
|
|
// A result credited to a call that did not ask for it is the one thing the id
|
|
// check exists to prevent.
|
|
func TestAResultForSomebodyElsesRequestIsRefused(t *testing.T) {
|
|
client := answering(t, func(Request) []byte {
|
|
return mustEncode(t, Response{RPCVersion: Version,
|
|
RequestID: "req-somebody-else", OK: true,
|
|
Result: json.RawMessage(`{"version":"9.9.9"}`)})
|
|
})
|
|
|
|
_, err := client.Call(t.Context(), "version.get", nil)
|
|
if err == nil {
|
|
t.Fatal("a result with the wrong id was accepted")
|
|
}
|
|
if code, _ := domain.CodeOf(err); code != domain.CodeProtocolInvalid {
|
|
t.Errorf("code = %s, want ProtocolInvalid", code)
|
|
}
|
|
}
|
|
|
|
// A refusal issued before the request was read has no id to echo -- the daemon
|
|
// is at its connection cap, or the caller is not entitled to the socket. It is
|
|
// still an answer, and dropping it would turn "too busy" into "the service died
|
|
// mid-request".
|
|
func TestARefusalWithNoRequestIDIsStillDelivered(t *testing.T) {
|
|
client := answering(t, func(Request) []byte {
|
|
return mustEncode(t, NewFailure("", domain.Errorf(domain.CodeBusy,
|
|
"同时处理的本地请求过多")))
|
|
})
|
|
|
|
_, err := client.Call(t.Context(), "version.get", nil)
|
|
if err == nil {
|
|
t.Fatal("a refusal was read as success")
|
|
}
|
|
if code, _ := domain.CodeOf(err); code != domain.CodeBusy {
|
|
t.Errorf("code = %s, want Busy", code)
|
|
}
|
|
}
|
|
|
|
func TestAResponseFromAnotherProtocolVersionIsRefused(t *testing.T) {
|
|
client := answering(t, func(request Request) []byte {
|
|
return mustEncode(t, Response{RPCVersion: Version + 1,
|
|
RequestID: request.RequestID, OK: true,
|
|
Result: json.RawMessage(`{}`)})
|
|
})
|
|
|
|
_, err := client.Call(t.Context(), "version.get", nil)
|
|
if err == nil {
|
|
t.Fatal("a response from another protocol version was accepted")
|
|
}
|
|
if code, _ := domain.CodeOf(err); code != domain.CodeProtocolInvalid {
|
|
t.Errorf("code = %s, want ProtocolInvalid", code)
|
|
}
|
|
}
|
|
|
|
// The response limit is enforced while reading, so an oversized answer costs
|
|
// one byte past the limit rather than all of it.
|
|
func TestAnOversizedResponseIsRefused(t *testing.T) {
|
|
client := answering(t, func(Request) []byte {
|
|
return []byte(strings.Repeat("x", MaxResponseBytes+1))
|
|
})
|
|
|
|
_, err := client.Call(t.Context(), "version.get", nil)
|
|
if err == nil {
|
|
t.Fatal("an oversized response was read")
|
|
}
|
|
if code, _ := domain.CodeOf(err); code != domain.CodeServiceStopped {
|
|
t.Errorf("code = %s, want the read to fail", code)
|
|
}
|
|
}
|
|
|
|
// Nonsense on the wire is refused rather than half-parsed.
|
|
func TestAMalformedResponseIsRefused(t *testing.T) {
|
|
client := answering(t, func(Request) []byte { return []byte("not json") })
|
|
|
|
_, err := client.Call(t.Context(), "version.get", nil)
|
|
if err == nil {
|
|
t.Fatal("a malformed response was accepted")
|
|
}
|
|
if code, _ := domain.CodeOf(err); code != domain.CodeProtocolInvalid {
|
|
t.Errorf("code = %s, want ProtocolInvalid", code)
|
|
}
|
|
}
|
|
|
|
// A failure envelope with nothing in it is still a failure. Reporting success
|
|
// because the server forgot to explain itself would be the worst of the three
|
|
// possible answers.
|
|
func TestAnEmptyFailureEnvelopeIsStillAFailure(t *testing.T) {
|
|
client := answering(t, func(request Request) []byte {
|
|
return mustEncode(t, Response{RPCVersion: Version,
|
|
RequestID: request.RequestID, OK: false})
|
|
})
|
|
|
|
_, err := client.Call(t.Context(), "version.get", nil)
|
|
if err == nil {
|
|
t.Fatal("ok=false with no error payload was read as success")
|
|
}
|
|
if code, _ := domain.CodeOf(err); code != domain.CodeInternal {
|
|
t.Errorf("code = %s, want Internal", code)
|
|
}
|
|
}
|
|
|
|
// The request carries what the protocol requires, and the ids do not repeat:
|
|
// they are what correlates a log line with a call.
|
|
func TestTheRequestIsWellFormedAndItsIDIsFresh(t *testing.T) {
|
|
seen := make(chan Request, 2)
|
|
answer := func(request Request) []byte {
|
|
seen <- request
|
|
return mustEncode(t, Response{RPCVersion: Version,
|
|
RequestID: request.RequestID, OK: true, Result: json.RawMessage(`1`)})
|
|
}
|
|
|
|
first := answering(t, answer)
|
|
if _, err := first.Call(t.Context(), "version.get",
|
|
map[string]string{"hello": "world"}); err != nil {
|
|
t.Fatalf("call: %v", err)
|
|
}
|
|
second := answering(t, answer)
|
|
if _, err := second.Call(t.Context(), "version.get", nil); err != nil {
|
|
t.Fatalf("call: %v", err)
|
|
}
|
|
|
|
one, two := <-seen, <-seen
|
|
if one.RPCVersion != Version || one.Method != "version.get" {
|
|
t.Errorf("request = %+v", one)
|
|
}
|
|
if string(one.Params) != `{"hello":"world"}` {
|
|
t.Errorf("params = %s", one.Params)
|
|
}
|
|
if len(two.Params) != 0 {
|
|
t.Errorf("a call with no parameters sent %s", two.Params)
|
|
}
|
|
if one.RequestID == "" || one.RequestID == two.RequestID {
|
|
t.Errorf("request ids repeat: %q and %q", one.RequestID, two.RequestID)
|
|
}
|
|
}
|
|
|
|
// Parameters that cannot be encoded are a programming mistake caught before
|
|
// anything is sent, not a socket error to puzzle over afterwards.
|
|
func TestUnencodableParametersFailBeforeDialing(t *testing.T) {
|
|
client := Client{Path: filepath.Join(t.TempDir(), "never-dialled.sock")}
|
|
|
|
_, err := client.Call(t.Context(), "version.get", make(chan int))
|
|
if err == nil {
|
|
t.Fatal("unencodable parameters were sent")
|
|
}
|
|
if code, _ := domain.CodeOf(err); code != domain.CodeInvalidArgument {
|
|
t.Errorf("code = %s, want InvalidArgument", code)
|
|
}
|
|
var unsupported *json.UnsupportedTypeError
|
|
if !errors.As(err, &unsupported) {
|
|
t.Error("the original encoding failure was not kept as the cause")
|
|
}
|
|
}
|