Add mock QLab server and tests, fix OSC typetag parsing off-by-one
This commit is contained in:
232
lib/qlab/mock.go
Normal file
232
lib/qlab/mock.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
package qlab
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockServer struct {
|
||||||
|
listener net.Listener
|
||||||
|
mu sync.Mutex
|
||||||
|
conns []net.Conn
|
||||||
|
|
||||||
|
Version string
|
||||||
|
Workspaces []Workspace
|
||||||
|
CueLists map[string][]Cue
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockServer() (*MockServer, error) {
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m := &MockServer{
|
||||||
|
listener: ln,
|
||||||
|
Version: "5.0.0",
|
||||||
|
Workspaces: []Workspace{},
|
||||||
|
CueLists: make(map[string][]Cue),
|
||||||
|
}
|
||||||
|
go m.serve()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) Port() int {
|
||||||
|
return m.listener.Addr().(*net.TCPAddr).Port
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) Close() error {
|
||||||
|
err := m.listener.Close()
|
||||||
|
m.mu.Lock()
|
||||||
|
for _, conn := range m.conns {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) SendUpdate(addr string) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
msg := buildOSC(addr)
|
||||||
|
encoded := slipEncode(msg)
|
||||||
|
for _, conn := range m.conns {
|
||||||
|
conn.Write(encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) serve() {
|
||||||
|
for {
|
||||||
|
conn, err := m.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
m.conns = append(m.conns, conn)
|
||||||
|
m.mu.Unlock()
|
||||||
|
go m.handleConn(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) handleConn(conn net.Conn) {
|
||||||
|
buf := make([]byte, 0, 65536)
|
||||||
|
tmp := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
n, err := conn.Read(tmp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf = append(buf, tmp[:n]...)
|
||||||
|
for {
|
||||||
|
frame, rest, ok := extractSLIPFrame(buf)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf = rest
|
||||||
|
addr, args, err := parseOSC(frame)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.handleRequest(conn, addr, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) sendReply(conn net.Conn, addr string, wsID string, status string, data any) {
|
||||||
|
jsonData, _ := json.Marshal(data)
|
||||||
|
r := Reply{
|
||||||
|
WorkspaceID: wsID,
|
||||||
|
Address: addr,
|
||||||
|
Status: status,
|
||||||
|
Data: json.RawMessage(jsonData),
|
||||||
|
}
|
||||||
|
replyJSON, _ := json.Marshal(r)
|
||||||
|
msg := buildOSC("/reply"+addr, string(replyJSON))
|
||||||
|
conn.Write(slipEncode(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) handleRequest(conn net.Conn, addr string, args []any) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case addr == "/version":
|
||||||
|
m.sendReply(conn, addr, "", "ok", m.Version)
|
||||||
|
return
|
||||||
|
case addr == "/workspaces":
|
||||||
|
m.sendReply(conn, addr, "", "ok", m.Workspaces)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(addr, "/", 5)
|
||||||
|
if len(parts) < 4 || parts[1] != "workspace" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wsID := parts[2]
|
||||||
|
rest := strings.Join(parts[3:], "/")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case rest == "connect":
|
||||||
|
m.sendReply(conn, addr, wsID, "ok", "ok")
|
||||||
|
case rest == "cueLists":
|
||||||
|
cues := m.CueLists[wsID]
|
||||||
|
if cues == nil {
|
||||||
|
cues = []Cue{}
|
||||||
|
}
|
||||||
|
m.sendReply(conn, addr, wsID, "ok", cues)
|
||||||
|
case rest == "selectedCues":
|
||||||
|
m.sendReply(conn, addr, wsID, "ok", []Cue{})
|
||||||
|
case rest == "runningCues":
|
||||||
|
m.sendReply(conn, addr, wsID, "ok", []Cue{})
|
||||||
|
case strings.HasPrefix(rest, "cue_id/"):
|
||||||
|
sub := strings.SplitN(rest, "/", 3)
|
||||||
|
if len(sub) < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cue := m.findCueByID(wsID, sub[1])
|
||||||
|
if cue == nil {
|
||||||
|
m.sendReply(conn, addr, wsID, "not found", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(args) > 0 {
|
||||||
|
m.setCueProperty(cue, sub[2], args[0])
|
||||||
|
} else {
|
||||||
|
m.sendReply(conn, addr, wsID, "ok", m.getCueProperty(cue, sub[2]))
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(rest, "cue/"):
|
||||||
|
sub := strings.SplitN(rest, "/", 3)
|
||||||
|
if len(sub) < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cue := m.findCueByNumber(wsID, sub[1])
|
||||||
|
if cue == nil {
|
||||||
|
m.sendReply(conn, addr, wsID, "not found", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(args) > 0 {
|
||||||
|
m.setCueProperty(cue, sub[2], args[0])
|
||||||
|
} else {
|
||||||
|
m.sendReply(conn, addr, wsID, "ok", m.getCueProperty(cue, sub[2]))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
m.sendReply(conn, addr, wsID, "ok", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) findCueByID(wsID, cueID string) *Cue {
|
||||||
|
return findCueInList(m.CueLists[wsID], cueID, func(c *Cue) string { return c.UniqueID })
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) findCueByNumber(wsID, num string) *Cue {
|
||||||
|
return findCueInList(m.CueLists[wsID], num, func(c *Cue) string { return c.Number })
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCueInList(cues []Cue, val string, key func(*Cue) string) *Cue {
|
||||||
|
for i := range cues {
|
||||||
|
if key(&cues[i]) == val {
|
||||||
|
return &cues[i]
|
||||||
|
}
|
||||||
|
if found := findCueInList(cues[i].Cues, val, key); found != nil {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) getCueProperty(cue *Cue, prop string) any {
|
||||||
|
switch prop {
|
||||||
|
case "uniqueID":
|
||||||
|
return cue.UniqueID
|
||||||
|
case "number":
|
||||||
|
return cue.Number
|
||||||
|
case "name":
|
||||||
|
return cue.Name
|
||||||
|
case "type":
|
||||||
|
return cue.Type
|
||||||
|
case "colorName":
|
||||||
|
return cue.ColorName
|
||||||
|
case "flagged":
|
||||||
|
return cue.Flagged
|
||||||
|
case "armed":
|
||||||
|
return cue.Armed
|
||||||
|
case "listName":
|
||||||
|
return cue.ListName
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockServer) setCueProperty(cue *Cue, prop string, val any) {
|
||||||
|
str, _ := val.(string)
|
||||||
|
switch prop {
|
||||||
|
case "name":
|
||||||
|
cue.Name = str
|
||||||
|
case "number":
|
||||||
|
cue.Number = str
|
||||||
|
case "colorName":
|
||||||
|
cue.ColorName = str
|
||||||
|
case "listName":
|
||||||
|
cue.ListName = str
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,7 +91,7 @@ func parseOSC(data []byte) (addr string, args []any, err error) {
|
|||||||
ttEnd++
|
ttEnd++
|
||||||
}
|
}
|
||||||
typetag := string(data[pos+1 : ttEnd])
|
typetag := string(data[pos+1 : ttEnd])
|
||||||
pos = ttEnd + 1 + oscPad(ttEnd-pos)
|
pos = ttEnd + 1 + oscPad(ttEnd-pos+1)
|
||||||
|
|
||||||
for _, t := range typetag {
|
for _, t := range typetag {
|
||||||
switch t {
|
switch t {
|
||||||
|
|||||||
318
lib/qlab/qlab_test.go
Normal file
318
lib/qlab/qlab_test.go
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
package qlab
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTest(t *testing.T) (*MockServer, *Client) {
|
||||||
|
t.Helper()
|
||||||
|
mock, err := NewMockServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { mock.Close() })
|
||||||
|
|
||||||
|
client, err := Dial("127.0.0.1", mock.Port())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { client.Close() })
|
||||||
|
|
||||||
|
return mock, client
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersion(t *testing.T) {
|
||||||
|
mock, client := setupTest(t)
|
||||||
|
mock.Version = "5.2.3"
|
||||||
|
|
||||||
|
v, err := client.Version()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if v != "5.2.3" {
|
||||||
|
t.Errorf("got %q, want %q", v, "5.2.3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkspaces(t *testing.T) {
|
||||||
|
mock, client := setupTest(t)
|
||||||
|
mock.Workspaces = []Workspace{
|
||||||
|
{DisplayName: "Show 1", UniqueID: "ws-1"},
|
||||||
|
{DisplayName: "Show 2", UniqueID: "ws-2", HasPasscode: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
ws, err := client.Workspaces()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(ws) != 2 {
|
||||||
|
t.Fatalf("got %d workspaces, want 2", len(ws))
|
||||||
|
}
|
||||||
|
if ws[0].DisplayName != "Show 1" {
|
||||||
|
t.Errorf("got %q, want %q", ws[0].DisplayName, "Show 1")
|
||||||
|
}
|
||||||
|
if ws[0].UniqueID != "ws-1" {
|
||||||
|
t.Errorf("got %q, want %q", ws[0].UniqueID, "ws-1")
|
||||||
|
}
|
||||||
|
if !ws[1].HasPasscode {
|
||||||
|
t.Error("expected HasPasscode to be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnect(t *testing.T) {
|
||||||
|
_, client := setupTest(t)
|
||||||
|
|
||||||
|
if err := client.Connect("ws-1", ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectWithPasscode(t *testing.T) {
|
||||||
|
_, client := setupTest(t)
|
||||||
|
|
||||||
|
if err := client.Connect("ws-1", "secret"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCueLists(t *testing.T) {
|
||||||
|
mock, client := setupTest(t)
|
||||||
|
mock.CueLists["ws-1"] = []Cue{
|
||||||
|
{
|
||||||
|
UniqueID: "list-1",
|
||||||
|
Name: "Main Cue List",
|
||||||
|
Type: "Cue List",
|
||||||
|
Cues: []Cue{
|
||||||
|
{UniqueID: "cue-1", Number: "1", Name: "Lights Up", Type: "Light"},
|
||||||
|
{UniqueID: "cue-2", Number: "2", Name: "Sound", Type: "Audio"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
lists, err := client.CueLists("ws-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(lists) != 1 {
|
||||||
|
t.Fatalf("got %d lists, want 1", len(lists))
|
||||||
|
}
|
||||||
|
if lists[0].Name != "Main Cue List" {
|
||||||
|
t.Errorf("got %q, want %q", lists[0].Name, "Main Cue List")
|
||||||
|
}
|
||||||
|
if len(lists[0].Cues) != 2 {
|
||||||
|
t.Fatalf("got %d cues, want 2", len(lists[0].Cues))
|
||||||
|
}
|
||||||
|
if lists[0].Cues[0].Name != "Lights Up" {
|
||||||
|
t.Errorf("got %q, want %q", lists[0].Cues[0].Name, "Lights Up")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCueGet(t *testing.T) {
|
||||||
|
mock, client := setupTest(t)
|
||||||
|
mock.CueLists["ws-1"] = []Cue{
|
||||||
|
{
|
||||||
|
UniqueID: "list-1",
|
||||||
|
Type: "Cue List",
|
||||||
|
Cues: []Cue{
|
||||||
|
{UniqueID: "cue-1", Number: "1", Name: "Lights Up"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := client.CueGet("ws-1", "cue-1", "name")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var name string
|
||||||
|
if err := json.Unmarshal(reply.Data, &name); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if name != "Lights Up" {
|
||||||
|
t.Errorf("got %q, want %q", name, "Lights Up")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCueGetByNumber(t *testing.T) {
|
||||||
|
mock, client := setupTest(t)
|
||||||
|
mock.CueLists["ws-1"] = []Cue{
|
||||||
|
{
|
||||||
|
UniqueID: "list-1",
|
||||||
|
Type: "Cue List",
|
||||||
|
Cues: []Cue{
|
||||||
|
{UniqueID: "cue-1", Number: "1", Name: "Lights Up"},
|
||||||
|
{UniqueID: "cue-2", Number: "2", Name: "Sound"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := client.CueGetByNumber("ws-1", "2", "name")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var name string
|
||||||
|
if err := json.Unmarshal(reply.Data, &name); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if name != "Sound" {
|
||||||
|
t.Errorf("got %q, want %q", name, "Sound")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCueGetNotFound(t *testing.T) {
|
||||||
|
mock, client := setupTest(t)
|
||||||
|
mock.CueLists["ws-1"] = []Cue{}
|
||||||
|
|
||||||
|
_, err := client.CueGet("ws-1", "nonexistent", "name")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nonexistent cue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCueSet(t *testing.T) {
|
||||||
|
mock, client := setupTest(t)
|
||||||
|
mock.CueLists["ws-1"] = []Cue{
|
||||||
|
{
|
||||||
|
UniqueID: "list-1",
|
||||||
|
Type: "Cue List",
|
||||||
|
Cues: []Cue{
|
||||||
|
{UniqueID: "cue-1", Number: "1", Name: "Lights Up"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.CueSet("ws-1", "cue-1", "name", "Blackout"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := client.CueGet("ws-1", "cue-1", "name")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var name string
|
||||||
|
json.Unmarshal(reply.Data, &name)
|
||||||
|
if name != "Blackout" {
|
||||||
|
t.Errorf("got %q, want %q", name, "Blackout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCueSetByNumber(t *testing.T) {
|
||||||
|
mock, client := setupTest(t)
|
||||||
|
mock.CueLists["ws-1"] = []Cue{
|
||||||
|
{
|
||||||
|
UniqueID: "list-1",
|
||||||
|
Type: "Cue List",
|
||||||
|
Cues: []Cue{
|
||||||
|
{UniqueID: "cue-1", Number: "1", Name: "Lights Up"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.CueSetByNumber("ws-1", "1", "name", "Blackout"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := client.CueGetByNumber("ws-1", "1", "name")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var name string
|
||||||
|
json.Unmarshal(reply.Data, &name)
|
||||||
|
if name != "Blackout" {
|
||||||
|
t.Errorf("got %q, want %q", name, "Blackout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdates(t *testing.T) {
|
||||||
|
mock, client := setupTest(t)
|
||||||
|
|
||||||
|
// Ensure connection is fully established
|
||||||
|
_, err := client.Version()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.SendUpdate("/update/workspace/ws-1/cue_id/cue-1")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case u := <-client.Updates():
|
||||||
|
if u.Address != "/update/workspace/ws-1/cue_id/cue-1" {
|
||||||
|
t.Errorf("got %q, want %q", u.Address, "/update/workspace/ws-1/cue_id/cue-1")
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("timeout waiting for update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransport(t *testing.T) {
|
||||||
|
_, client := setupTest(t)
|
||||||
|
|
||||||
|
for _, fn := range []func(string) error{
|
||||||
|
client.Go,
|
||||||
|
client.Stop,
|
||||||
|
client.Pause,
|
||||||
|
client.Resume,
|
||||||
|
client.Panic,
|
||||||
|
client.Reset,
|
||||||
|
} {
|
||||||
|
if err := fn("ws-1"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectedCues(t *testing.T) {
|
||||||
|
_, client := setupTest(t)
|
||||||
|
|
||||||
|
cues, err := client.SelectedCues("ws-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(cues) != 0 {
|
||||||
|
t.Errorf("got %d cues, want 0", len(cues))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunningCues(t *testing.T) {
|
||||||
|
_, client := setupTest(t)
|
||||||
|
|
||||||
|
cues, err := client.RunningCues("ws-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(cues) != 0 {
|
||||||
|
t.Errorf("got %d cues, want 0", len(cues))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNestedCueGet(t *testing.T) {
|
||||||
|
mock, client := setupTest(t)
|
||||||
|
mock.CueLists["ws-1"] = []Cue{
|
||||||
|
{
|
||||||
|
UniqueID: "list-1",
|
||||||
|
Type: "Cue List",
|
||||||
|
Cues: []Cue{
|
||||||
|
{
|
||||||
|
UniqueID: "group-1",
|
||||||
|
Number: "10",
|
||||||
|
Name: "Group",
|
||||||
|
Type: "Group",
|
||||||
|
Cues: []Cue{
|
||||||
|
{UniqueID: "nested-1", Number: "10.1", Name: "Nested Cue"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := client.CueGet("ws-1", "nested-1", "name")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var name string
|
||||||
|
json.Unmarshal(reply.Data, &name)
|
||||||
|
if name != "Nested Cue" {
|
||||||
|
t.Errorf("got %q, want %q", name, "Nested Cue")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user