mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
This commit is contained in:
@@ -0,0 +1,423 @@
|
||||
// Package backendrpc is the Go client for the local Rust Resource Backend.
|
||||
//
|
||||
// The package talks directly to bat.sock over newline-delimited JSON-RPC 2.0.
|
||||
// It is the default Go integration path for bat-api; do not shell out to the
|
||||
// bat binary for normal service calls.
|
||||
package backendrpc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const jsonRPCVersion = "2.0"
|
||||
|
||||
// Client calls the local Rust daemon through a Unix domain socket.
|
||||
type Client struct {
|
||||
SocketPath string
|
||||
Timeout time.Duration
|
||||
|
||||
// DialContext exists for tests and alternative local transports.
|
||||
DialContext func(ctx context.Context, network string, address string) (net.Conn, error)
|
||||
|
||||
seq atomic.Uint64
|
||||
}
|
||||
|
||||
// New returns a client for the given bat.sock path.
|
||||
func New(socketPath string) *Client {
|
||||
return &Client{SocketPath: socketPath, Timeout: 30 * time.Second}
|
||||
}
|
||||
|
||||
// APIError mirrors the Rust ApiError envelope payload.
|
||||
type APIError struct {
|
||||
Code string `json:"code"`
|
||||
Kind string `json:"kind"`
|
||||
Domain string `json:"domain"`
|
||||
Location string `json:"location"`
|
||||
Message string `json:"message"`
|
||||
Retryable bool `json:"retryable"`
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil api error>"
|
||||
}
|
||||
if e.Code == "" {
|
||||
return e.Message
|
||||
}
|
||||
return fmt.Sprintf("%s %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// JSONRPCError is a transport-level JSON-RPC error.
|
||||
type JSONRPCError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *JSONRPCError) Error() string {
|
||||
if e == nil {
|
||||
return "<nil json-rpc error>"
|
||||
}
|
||||
return fmt.Sprintf("json-rpc error %d: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// Envelope is the application-level payload carried in JSON-RPC result.
|
||||
type Envelope struct {
|
||||
OK bool `json:"ok"`
|
||||
Status string `json:"status"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
Error *APIError `json:"error,omitempty"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
type rpcRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID uint64 `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params any `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type rpcResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID uint64 `json:"id"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *JSONRPCError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Call invokes method and decodes the envelope data into out when out is non-nil.
|
||||
func (c *Client) Call(ctx context.Context, method string, params any, out any) (*Envelope, error) {
|
||||
envelope, err := c.callEnvelope(ctx, method, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !envelope.OK {
|
||||
if envelope.Error != nil {
|
||||
return envelope, envelope.Error
|
||||
}
|
||||
return envelope, errors.New("backend rpc returned ok=false without error payload")
|
||||
}
|
||||
if out != nil && len(envelope.Data) > 0 && string(envelope.Data) != "null" {
|
||||
if err := json.Unmarshal(envelope.Data, out); err != nil {
|
||||
return envelope, fmt.Errorf("decode %s response data: %w", method, err)
|
||||
}
|
||||
}
|
||||
return envelope, nil
|
||||
}
|
||||
|
||||
func (c *Client) callEnvelope(ctx context.Context, method string, params any) (*Envelope, error) {
|
||||
if c.SocketPath == "" {
|
||||
return nil, errors.New("backend rpc socket path is empty")
|
||||
}
|
||||
conn, err := c.dial(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if deadline, ok := c.deadline(ctx); ok {
|
||||
_ = conn.SetDeadline(deadline)
|
||||
}
|
||||
|
||||
req := rpcRequest{
|
||||
JSONRPC: jsonRPCVersion,
|
||||
ID: c.seq.Add(1),
|
||||
Method: method,
|
||||
Params: params,
|
||||
}
|
||||
if err := json.NewEncoder(conn).Encode(&req); err != nil {
|
||||
return nil, fmt.Errorf("write backend rpc request %s: %w", method, err)
|
||||
}
|
||||
|
||||
line, err := bufio.NewReader(conn).ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read backend rpc response %s: %w", method, err)
|
||||
}
|
||||
var resp rpcResponse
|
||||
if err := json.Unmarshal(line, &resp); err != nil {
|
||||
return nil, fmt.Errorf("decode backend rpc response %s: %w", method, err)
|
||||
}
|
||||
if resp.Error != nil {
|
||||
return nil, resp.Error
|
||||
}
|
||||
if len(resp.Result) == 0 {
|
||||
return nil, fmt.Errorf("backend rpc %s response missing result", method)
|
||||
}
|
||||
var envelope Envelope
|
||||
if err := json.Unmarshal(resp.Result, &envelope); err != nil {
|
||||
return nil, fmt.Errorf("decode backend rpc envelope %s: %w", method, err)
|
||||
}
|
||||
return &envelope, nil
|
||||
}
|
||||
|
||||
func (c *Client) dial(ctx context.Context) (net.Conn, error) {
|
||||
if c.DialContext != nil {
|
||||
return c.DialContext(ctx, "unix", c.SocketPath)
|
||||
}
|
||||
var dialer net.Dialer
|
||||
return dialer.DialContext(ctx, "unix", c.SocketPath)
|
||||
}
|
||||
|
||||
func (c *Client) deadline(ctx context.Context) (time.Time, bool) {
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
return deadline, true
|
||||
}
|
||||
if c.Timeout > 0 {
|
||||
return time.Now().Add(c.Timeout), true
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
type boolParam struct {
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
type pageParam struct {
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type tailParam struct {
|
||||
Tail int `json:"tail"`
|
||||
}
|
||||
|
||||
type taskIDParam struct {
|
||||
TaskID string `json:"task_id"`
|
||||
}
|
||||
|
||||
// Ack is returned by accepted daemon control methods.
|
||||
type Ack struct {
|
||||
Command string `json:"command"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
StateDir string `json:"state_dir"`
|
||||
SocketPath string `json:"socket_path"`
|
||||
Force *bool `json:"force,omitempty"`
|
||||
}
|
||||
|
||||
// TaskAccepted is returned when an async backend task is queued.
|
||||
type TaskAccepted struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Kind string `json:"kind"`
|
||||
}
|
||||
|
||||
// TaskRecord is the pollable task state.
|
||||
type TaskRecord struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Status string `json:"status"`
|
||||
Stage *string `json:"stage,omitempty"`
|
||||
Message *string `json:"message,omitempty"`
|
||||
CreatedAt uint64 `json:"created_at"`
|
||||
UpdatedAt uint64 `json:"updated_at"`
|
||||
StartedAt *uint64 `json:"started_at,omitempty"`
|
||||
FinishedAt *uint64 `json:"finished_at,omitempty"`
|
||||
Error *APIError `json:"error,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
type TaskList struct {
|
||||
Tasks []TaskRecord `json:"tasks"`
|
||||
}
|
||||
|
||||
type TaskCancelResult struct {
|
||||
TaskID string `json:"task_id"`
|
||||
CancelRequested bool `json:"cancel_requested"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
type TaskLogs struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Lines []string `json:"lines"`
|
||||
}
|
||||
|
||||
type DoctorCheck struct {
|
||||
Name string `json:"name"`
|
||||
OK bool `json:"ok"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type DoctorReport struct {
|
||||
Command string `json:"command"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Checks []DoctorCheck `json:"checks"`
|
||||
}
|
||||
|
||||
type LogsReport struct {
|
||||
Command string `json:"command"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
LogPath string `json:"log_path"`
|
||||
Exists bool `json:"exists"`
|
||||
Empty bool `json:"empty"`
|
||||
Bytes int `json:"bytes"`
|
||||
TotalLines int `json:"total_lines"`
|
||||
ReturnedLines int `json:"returned_lines"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type DaemonStatusReport struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Running bool `json:"running"`
|
||||
PID *int `json:"pid,omitempty"`
|
||||
ResourceOutputRoot *string `json:"resource_output_root,omitempty"`
|
||||
StateDir string `json:"state_dir"`
|
||||
SocketPath string `json:"socket_path"`
|
||||
RPCAvailable bool `json:"rpc_available"`
|
||||
CurrentStage *string `json:"current_stage,omitempty"`
|
||||
CurrentMessage *string `json:"current_message,omitempty"`
|
||||
}
|
||||
|
||||
type ResourceState struct {
|
||||
ResourceOutputRoot *string `json:"resource_output_root,omitempty"`
|
||||
VersionState json.RawMessage `json:"version_state,omitempty"`
|
||||
LastUpdateStatus *string `json:"last_update_status,omitempty"`
|
||||
LastSuccessUnixSeconds *uint64 `json:"last_success_unix_seconds,omitempty"`
|
||||
}
|
||||
|
||||
type ResourceManifestEntry struct {
|
||||
URL string `json:"url"`
|
||||
Destination string `json:"destination"`
|
||||
Bytes *uint64 `json:"bytes,omitempty"`
|
||||
BLAKE3 string `json:"blake3,omitempty"`
|
||||
}
|
||||
|
||||
type ResourceManifestPage struct {
|
||||
Available bool `json:"available"`
|
||||
ResourceRoot string `json:"resource_root,omitempty"`
|
||||
ManifestVersion int `json:"manifest_version,omitempty"`
|
||||
TotalEntries int `json:"total_entries,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Entries []ResourceManifestEntry `json:"entries,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) DaemonStatus(ctx context.Context) (*DaemonStatusReport, error) {
|
||||
var out DaemonStatusReport
|
||||
_, err := c.Call(ctx, "daemon.status", nil, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) DaemonLogs(ctx context.Context, tail int) (*LogsReport, error) {
|
||||
var out LogsReport
|
||||
_, err := c.Call(ctx, "daemon.logs", tailParam{Tail: tail}, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) DaemonStop(ctx context.Context) (*Ack, error) {
|
||||
var out Ack
|
||||
_, err := c.Call(ctx, "daemon.stop", nil, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) DaemonReload(ctx context.Context) (*Ack, error) {
|
||||
var out Ack
|
||||
_, err := c.Call(ctx, "daemon.reload", nil, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) DaemonRefresh(ctx context.Context, force bool) (*Ack, error) {
|
||||
var out Ack
|
||||
_, err := c.Call(ctx, "daemon.refresh", boolParam{Force: force}, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) DaemonDoctor(ctx context.Context) (*DoctorReport, error) {
|
||||
var out DoctorReport
|
||||
_, err := c.Call(ctx, "daemon.doctor", nil, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) ResourceState(ctx context.Context) (*ResourceState, error) {
|
||||
var out ResourceState
|
||||
_, err := c.Call(ctx, "resource.state", nil, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) ResourceSync(ctx context.Context, force bool) (*TaskAccepted, error) {
|
||||
var out TaskAccepted
|
||||
_, err := c.Call(ctx, "resource.sync", boolParam{Force: force}, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) ResourceVerify(ctx context.Context) (*TaskAccepted, error) {
|
||||
var out TaskAccepted
|
||||
_, err := c.Call(ctx, "resource.verify", nil, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) ResourceRepair(ctx context.Context) (*TaskAccepted, error) {
|
||||
var out TaskAccepted
|
||||
_, err := c.Call(ctx, "resource.repair", nil, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) ResourceManifest(ctx context.Context, offset int, limit int) (*ResourceManifestPage, error) {
|
||||
var out ResourceManifestPage
|
||||
_, err := c.Call(ctx, "resource.manifest", pageParam{Offset: offset, Limit: limit}, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) ResourceList(ctx context.Context, offset int, limit int) (*ResourceManifestPage, error) {
|
||||
var out ResourceManifestPage
|
||||
_, err := c.Call(ctx, "resource.list", pageParam{Offset: offset, Limit: limit}, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) CatalogStatus(ctx context.Context) (json.RawMessage, error) {
|
||||
return c.rawData(ctx, "catalog.status", nil)
|
||||
}
|
||||
|
||||
func (c *Client) CatalogVersions(ctx context.Context) (json.RawMessage, error) {
|
||||
return c.rawData(ctx, "catalog.versions", nil)
|
||||
}
|
||||
|
||||
func (c *Client) CatalogDiff(ctx context.Context) (json.RawMessage, error) {
|
||||
return c.rawData(ctx, "catalog.diff", nil)
|
||||
}
|
||||
|
||||
func (c *Client) CatalogRefresh(ctx context.Context, force bool) (*TaskAccepted, error) {
|
||||
var out TaskAccepted
|
||||
_, err := c.Call(ctx, "catalog.refresh", boolParam{Force: force}, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) TaskStatus(ctx context.Context, taskID string) (*TaskRecord, error) {
|
||||
var out TaskRecord
|
||||
_, err := c.Call(ctx, "task.status", taskIDParam{TaskID: taskID}, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) TaskList(ctx context.Context) (*TaskList, error) {
|
||||
var out TaskList
|
||||
_, err := c.Call(ctx, "task.list", nil, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) TaskCancel(ctx context.Context, taskID string) (*TaskCancelResult, error) {
|
||||
var out TaskCancelResult
|
||||
_, err := c.Call(ctx, "task.cancel", taskIDParam{TaskID: taskID}, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) TaskLogs(ctx context.Context, taskID string) (*TaskLogs, error) {
|
||||
var out TaskLogs
|
||||
_, err := c.Call(ctx, "task.logs", taskIDParam{TaskID: taskID}, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) rawData(ctx context.Context, method string, params any) (json.RawMessage, error) {
|
||||
envelope, err := c.Call(ctx, method, params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return envelope.Data, nil
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package backendrpc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.RawMessage `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type testEnvelope struct {
|
||||
OK bool `json:"ok"`
|
||||
Status string `json:"status"`
|
||||
Data any `json:"data,omitempty"`
|
||||
Error any `json:"error,omitempty"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
type testResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.RawMessage `json:"id"`
|
||||
Result any `json:"result,omitempty"`
|
||||
Error *JSONRPCError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func newTestClient(t *testing.T, handler func(t *testing.T, req testRequest) testResponse) *Client {
|
||||
t.Helper()
|
||||
client := New("bat.sock")
|
||||
client.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
|
||||
clientConn, serverConn := net.Pipe()
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
line, err := bufio.NewReader(conn).ReadBytes('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var req testRequest
|
||||
if err := json.Unmarshal(line, &req); err != nil {
|
||||
return
|
||||
}
|
||||
resp := handler(t, req)
|
||||
if len(resp.ID) == 0 {
|
||||
resp.ID = req.ID
|
||||
}
|
||||
if resp.JSONRPC == "" {
|
||||
resp.JSONRPC = jsonRPCVersion
|
||||
}
|
||||
_ = json.NewEncoder(conn).Encode(resp)
|
||||
}(serverConn)
|
||||
return clientConn, nil
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func TestResourceRepairQueuesTask(t *testing.T) {
|
||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||
if req.Method != "resource.repair" {
|
||||
t.Fatalf("method = %s", req.Method)
|
||||
}
|
||||
return testResponse{
|
||||
Result: testEnvelope{
|
||||
OK: true,
|
||||
Status: "accepted",
|
||||
RequestID: "req-test-1",
|
||||
Data: map[string]any{
|
||||
"task_id": "task-1",
|
||||
"kind": "resource.repair",
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
task, err := client.ResourceRepair(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("ResourceRepair error: %v", err)
|
||||
}
|
||||
if task.TaskID != "task-1" || task.Kind != "resource.repair" {
|
||||
t.Fatalf("unexpected task: %#v", task)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceListSendsPagination(t *testing.T) {
|
||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||
if req.Method != "resource.list" {
|
||||
t.Fatalf("method = %s", req.Method)
|
||||
}
|
||||
var params pageParam
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
t.Fatalf("decode params: %v", err)
|
||||
}
|
||||
if params.Offset != 2 || params.Limit != 1 {
|
||||
t.Fatalf("params = %#v", params)
|
||||
}
|
||||
bytes := uint64(42)
|
||||
return testResponse{
|
||||
Result: testEnvelope{
|
||||
OK: true,
|
||||
Status: "ok",
|
||||
RequestID: "req-test-2",
|
||||
Data: map[string]any{
|
||||
"available": true,
|
||||
"resource_root": "/tmp/resources/current",
|
||||
"manifest_version": 1,
|
||||
"total_entries": 3,
|
||||
"offset": 2,
|
||||
"limit": 1,
|
||||
"entries": []map[string]any{
|
||||
{
|
||||
"url": "https://example.invalid/a.zip",
|
||||
"destination": "a.zip",
|
||||
"bytes": bytes,
|
||||
"blake3": "abc",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
page, err := client.ResourceList(context.Background(), 2, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("ResourceList error: %v", err)
|
||||
}
|
||||
if !page.Available || page.TotalEntries != 3 || len(page.Entries) != 1 {
|
||||
t.Fatalf("unexpected page: %#v", page)
|
||||
}
|
||||
if page.Entries[0].Bytes == nil || *page.Entries[0].Bytes != 42 {
|
||||
t.Fatalf("unexpected entry bytes: %#v", page.Entries[0].Bytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationErrorReturnsAPIError(t *testing.T) {
|
||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||
if req.Method != "task.status" {
|
||||
t.Fatalf("method = %s", req.Method)
|
||||
}
|
||||
return testResponse{
|
||||
Result: testEnvelope{
|
||||
OK: false,
|
||||
Status: "error",
|
||||
Error: APIError{
|
||||
Code: "BAT-ERR-700004",
|
||||
Kind: "task_not_found",
|
||||
Domain: "rpc",
|
||||
Location: "task.status",
|
||||
Message: "任务不存在:missing",
|
||||
Retryable: false,
|
||||
},
|
||||
RequestID: "req-test-3",
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
_, err := client.TaskStatus(context.Background(), "missing")
|
||||
var apiErr *APIError
|
||||
if !errors.As(err, &apiErr) {
|
||||
t.Fatalf("expected APIError, got %T %v", err, err)
|
||||
}
|
||||
if apiErr.Code != "BAT-ERR-700004" {
|
||||
t.Fatalf("code = %s", apiErr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransportErrorReturnsJSONRPCError(t *testing.T) {
|
||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||
return testResponse{
|
||||
Error: &JSONRPCError{
|
||||
Code: -32700,
|
||||
Message: "parse error",
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
_, err := client.DaemonStatus(context.Background())
|
||||
var rpcErr *JSONRPCError
|
||||
if !errors.As(err, &rpcErr) {
|
||||
t.Fatalf("expected JSONRPCError, got %T %v", err, err)
|
||||
}
|
||||
if rpcErr.Code != -32700 {
|
||||
t.Fatalf("code = %d", rpcErr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextDeadlineIsApplied(t *testing.T) {
|
||||
client := New("bat.sock")
|
||||
client.Timeout = time.Millisecond
|
||||
client.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.DaemonStatus(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected dial error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user