feat(go): add ping CLI subcommand (§4.3)
Add PingRaw to Session (session.go), runPing to the CLI dispatch (main.go), and three tests covering plain-text echo, JSON output, and missing-session-id validation (main_test.go). Default message is "ping"; gateway echo is read from DiagnosticMessage, falling back to the kind string if absent.
This commit is contained in:
@@ -121,6 +121,8 @@ func runWithIO(ctx context.Context, args []string, stdout, stderr io.Writer) err
|
|||||||
return runGalaxyDiscover(ctx, args[1:], stdout, stderr)
|
return runGalaxyDiscover(ctx, args[1:], stdout, stderr)
|
||||||
case "galaxy-watch":
|
case "galaxy-watch":
|
||||||
return runGalaxyWatch(ctx, args[1:], stdout, stderr)
|
return runGalaxyWatch(ctx, args[1:], stdout, stderr)
|
||||||
|
case "ping":
|
||||||
|
return runPing(ctx, args[1:], stdout, stderr)
|
||||||
case "batch":
|
case "batch":
|
||||||
return runBatch(ctx, os.Stdin, stdout, stderr)
|
return runBatch(ctx, os.Stdin, stdout, stderr)
|
||||||
default:
|
default:
|
||||||
@@ -228,6 +230,47 @@ func runCloseSession(ctx context.Context, args []string, stdout, stderr io.Write
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runPing(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
|
flags := flag.NewFlagSet("ping", flag.ContinueOnError)
|
||||||
|
flags.SetOutput(stderr)
|
||||||
|
common := bindCommonFlags(flags)
|
||||||
|
jsonOutput := flags.Bool("json", false, "write JSON output")
|
||||||
|
sessionID := flags.String("session-id", "", "gateway session id")
|
||||||
|
message := flags.String("message", "ping", "ping payload message")
|
||||||
|
|
||||||
|
if err := flags.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if *sessionID == "" {
|
||||||
|
return errors.New("session-id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, options, err := dialForCommand(ctx, common)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
session := mxgateway.NewSessionForID(client, *sessionID)
|
||||||
|
reply, err := session.PingRaw(ctx, *message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if *jsonOutput {
|
||||||
|
return writeJSON(stdout, commandReplyOutput{
|
||||||
|
Command: "ping",
|
||||||
|
Options: options,
|
||||||
|
Reply: mustMarshalProto(reply),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
echo := reply.GetDiagnosticMessage()
|
||||||
|
if echo == "" {
|
||||||
|
echo = reply.GetKind().String()
|
||||||
|
}
|
||||||
|
fmt.Fprintln(stdout, echo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func runRegister(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
func runRegister(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
flags := flag.NewFlagSet("register", flag.ContinueOnError)
|
flags := flag.NewFlagSet("register", flag.ContinueOnError)
|
||||||
flags.SetOutput(stderr)
|
flags.SetOutput(stderr)
|
||||||
@@ -1196,7 +1239,7 @@ type protojsonMessage interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeUsage(writer io.Writer) {
|
func writeUsage(writer io.Writer) {
|
||||||
fmt.Fprintln(writer, "usage: mxgw-go <version|open-session|close-session|register|add-item|advise|subscribe-bulk|unsubscribe-bulk|read-bulk|write-bulk|write2-bulk|write-secured-bulk|write-secured2-bulk|bench-read-bulk|write|stream-events|stream-alarms|acknowledge-alarm|smoke|galaxy-test-connection|galaxy-last-deploy|galaxy-discover|galaxy-watch|batch>")
|
fmt.Fprintln(writer, "usage: mxgw-go <version|open-session|close-session|ping|register|add-item|advise|subscribe-bulk|unsubscribe-bulk|read-bulk|write-bulk|write2-bulk|write-secured-bulk|write-secured2-bulk|bench-read-bulk|write|stream-events|stream-alarms|acknowledge-alarm|smoke|galaxy-test-connection|galaxy-last-deploy|galaxy-discover|galaxy-watch|batch>")
|
||||||
}
|
}
|
||||||
|
|
||||||
// batchEOR is the end-of-result sentinel emitted to stdout after every command
|
// batchEOR is the end-of-result sentinel emitted to stdout after every command
|
||||||
|
|||||||
@@ -190,6 +190,102 @@ func TestRunBenchReadBulkRespectsContextCancellation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRunPingPlainText verifies the ping subcommand round-trips through the
|
||||||
|
// fake gateway and prints the echo (diagnostic_message) in plain-text mode.
|
||||||
|
func TestRunPingPlainText(t *testing.T) {
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listen: %v", err)
|
||||||
|
}
|
||||||
|
server := grpc.NewServer()
|
||||||
|
fake := &pingFakeGateway{}
|
||||||
|
pb.RegisterMxAccessGatewayServer(server, fake)
|
||||||
|
go func() { _ = server.Serve(listener) }()
|
||||||
|
defer server.Stop()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
args := []string{
|
||||||
|
"ping",
|
||||||
|
"-endpoint", listener.Addr().String(),
|
||||||
|
"-plaintext",
|
||||||
|
"-api-key", "test",
|
||||||
|
"-session-id", "test-session",
|
||||||
|
"-message", "hello",
|
||||||
|
}
|
||||||
|
if err := runWithIO(t.Context(), args, &stdout, &stderr); err != nil {
|
||||||
|
t.Fatalf("runWithIO() error = %v; stderr = %s", err, stderr.String())
|
||||||
|
}
|
||||||
|
got := strings.TrimSpace(stdout.String())
|
||||||
|
if got != "pong:hello" {
|
||||||
|
t.Fatalf("ping plain-text output = %q, want %q", got, "pong:hello")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunPingJSON verifies the ping subcommand emits valid JSON in --json mode.
|
||||||
|
func TestRunPingJSON(t *testing.T) {
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listen: %v", err)
|
||||||
|
}
|
||||||
|
server := grpc.NewServer()
|
||||||
|
fake := &pingFakeGateway{}
|
||||||
|
pb.RegisterMxAccessGatewayServer(server, fake)
|
||||||
|
go func() { _ = server.Serve(listener) }()
|
||||||
|
defer server.Stop()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
args := []string{
|
||||||
|
"ping",
|
||||||
|
"-endpoint", listener.Addr().String(),
|
||||||
|
"-plaintext",
|
||||||
|
"-api-key", "test",
|
||||||
|
"-session-id", "test-session",
|
||||||
|
"-message", "hello",
|
||||||
|
"-json",
|
||||||
|
}
|
||||||
|
if err := runWithIO(t.Context(), args, &stdout, &stderr); err != nil {
|
||||||
|
t.Fatalf("runWithIO() error = %v; stderr = %s", err, stderr.String())
|
||||||
|
}
|
||||||
|
var out commandReplyOutput
|
||||||
|
if err := json.Unmarshal(stdout.Bytes(), &out); err != nil {
|
||||||
|
t.Fatalf("parse JSON: %v\noutput: %s", err, stdout.String())
|
||||||
|
}
|
||||||
|
if out.Command != "ping" {
|
||||||
|
t.Fatalf("command = %q, want %q", out.Command, "ping")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunPingRequiresSessionID verifies the ping subcommand rejects missing session-id.
|
||||||
|
func TestRunPingRequiresSessionID(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
err := runWithIO(t.Context(), []string{"ping", "-plaintext", "-api-key", "test"}, &stdout, &stderr)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("runWithIO(ping without --session-id) returned no error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "session-id is required") {
|
||||||
|
t.Fatalf("error = %v; want 'session-id is required'", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pingFakeGateway handles Invoke for MX_COMMAND_KIND_PING by echoing the
|
||||||
|
// message back in the diagnostic_message field so the CLI plain-text path
|
||||||
|
// has a deterministic, non-empty string to assert on.
|
||||||
|
type pingFakeGateway struct {
|
||||||
|
pb.UnimplementedMxAccessGatewayServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *pingFakeGateway) Invoke(_ context.Context, req *pb.MxCommandRequest) (*pb.MxCommandReply, error) {
|
||||||
|
echo := "pong:" + req.GetCommand().GetPing().GetMessage()
|
||||||
|
return &pb.MxCommandReply{
|
||||||
|
SessionId: req.GetSessionId(),
|
||||||
|
Kind: pb.MxCommandKind_MX_COMMAND_KIND_PING,
|
||||||
|
DiagnosticMessage: echo,
|
||||||
|
ProtocolStatus: &pb.ProtocolStatus{Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// benchFakeGateway is a minimal MxAccessGatewayServer that satisfies the
|
// benchFakeGateway is a minimal MxAccessGatewayServer that satisfies the
|
||||||
// bench-read-bulk session-setup sequence (OpenSession + Invoke for Register
|
// bench-read-bulk session-setup sequence (OpenSession + Invoke for Register
|
||||||
// / SubscribeBulk / ReadBulk / UnsubscribeBulk / CloseSession).
|
// / SubscribeBulk / ReadBulk / UnsubscribeBulk / CloseSession).
|
||||||
|
|||||||
@@ -580,6 +580,17 @@ func (s *Session) WriteRaw(ctx context.Context, serverHandle, itemHandle int32,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PingRaw sends a diagnostic PING command and returns the raw reply.
|
||||||
|
// The message is echoed back by the gateway in the reply's DiagnosticMessage field.
|
||||||
|
func (s *Session) PingRaw(ctx context.Context, message string) (*MxCommandReply, error) {
|
||||||
|
return s.invokeCommand(ctx, &pb.MxCommand{
|
||||||
|
Kind: pb.MxCommandKind_MX_COMMAND_KIND_PING,
|
||||||
|
Payload: &pb.MxCommand_Ping{
|
||||||
|
Ping: &pb.PingCommand{Message: message},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Write2 invokes MXAccess Write2 (timestamped single-item write).
|
// Write2 invokes MXAccess Write2 (timestamped single-item write).
|
||||||
func (s *Session) Write2(ctx context.Context, serverHandle, itemHandle int32, value, timestampValue *MxValue, userID int32) error {
|
func (s *Session) Write2(ctx context.Context, serverHandle, itemHandle int32, value, timestampValue *MxValue, userID int32) error {
|
||||||
_, err := s.Write2Raw(ctx, serverHandle, itemHandle, value, timestampValue, userID)
|
_, err := s.Write2Raw(ctx, serverHandle, itemHandle, value, timestampValue, userID)
|
||||||
|
|||||||
Reference in New Issue
Block a user