Files
printer-driver/emulator/internal/usbip/usbip_test.go
T
2026-09-30 10:22:42 +02:00

186 lines
4.8 KiB
Go

package usbip
import (
"bytes"
"encoding/binary"
"io"
"net"
"testing"
"time"
"escpos-emulator/internal/escpos"
)
func startServer(t *testing.T) (*Server, *escpos.Printer) {
t.Helper()
prof, _ := escpos.FindProfile("80mm")
p, err := escpos.NewPrinter(prof)
if err != nil {
t.Fatal(err)
}
s, err := Listen("127.0.0.1:0", DeviceFromProfile(prof), p)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { s.Close() })
return s, p
}
func dial(t *testing.T, s *Server) net.Conn {
t.Helper()
c, err := net.Dial("tcp", s.Addr().String())
if err != nil {
t.Fatal(err)
}
_ = c.SetDeadline(time.Now().Add(5 * time.Second))
t.Cleanup(func() { c.Close() })
return c
}
func readN(t *testing.T, c net.Conn, n int) []byte {
t.Helper()
b := make([]byte, n)
if _, err := io.ReadFull(c, b); err != nil {
t.Fatalf("leyendo %d bytes: %v", n, err)
}
return b
}
func TestDevlist(t *testing.T) {
s, _ := startServer(t)
c := dial(t, s)
c.Write([]byte{0x01, 0x11, 0x80, 0x05, 0, 0, 0, 0})
h := readN(t, c, 12)
if binary.BigEndian.Uint16(h[2:]) != opRepDevlist || binary.BigEndian.Uint32(h[8:]) != 1 {
t.Fatalf("cabecera DEVLIST inesperada: % X", h)
}
dev := readN(t, c, 312+4)
if got := string(bytes.TrimRight(dev[256:288], "\x00")); got != BusID {
t.Fatalf("busid = %q", got)
}
if !bytes.Equal(dev[312:], []byte{7, 1, 2, 0}) {
t.Fatalf("interfaz = % X, want clase impresora 07 01 02", dev[312:])
}
}
type client struct {
t *testing.T
c net.Conn
seq uint32
isIn map[uint32]bool
}
func attach(t *testing.T, s *Server) *client {
c := dial(t, s)
req := []byte{0x01, 0x11, 0x80, 0x03, 0, 0, 0, 0}
bus := make([]byte, 32)
copy(bus, BusID)
c.Write(append(req, bus...))
h := readN(t, c, 8)
if binary.BigEndian.Uint32(h[4:]) != 0 {
t.Fatalf("IMPORT rechazado: % X", h)
}
readN(t, c, 312)
return &client{t: t, c: c, isIn: map[uint32]bool{}}
}
func (cl *client) submit(dir, ep uint32, length int, setup []byte, data []byte) uint32 {
cl.seq++
cl.isIn[cl.seq] = dir == dirIn
h := make([]byte, 48)
binary.BigEndian.PutUint32(h[0:], cmdSubmit)
binary.BigEndian.PutUint32(h[4:], cl.seq)
binary.BigEndian.PutUint32(h[8:], 0x00010002)
binary.BigEndian.PutUint32(h[12:], dir)
binary.BigEndian.PutUint32(h[16:], ep)
binary.BigEndian.PutUint32(h[24:], uint32(length))
binary.BigEndian.PutUint32(h[32:], 0xFFFFFFFF)
copy(h[40:], setup)
cl.c.Write(append(h, data...))
return cl.seq
}
func (cl *client) ret() (seq uint32, status int32, data []byte) {
h := readN(cl.t, cl.c, 48)
seq = binary.BigEndian.Uint32(h[4:])
status = int32(binary.BigEndian.Uint32(h[20:]))
if n := int(binary.BigEndian.Uint32(h[24:])); cl.isIn[seq] && n > 0 {
data = readN(cl.t, cl.c, n)
}
return
}
func TestControlAndBulk(t *testing.T) {
s, p := startServer(t)
cl := attach(t, s)
// GET_DESCRIPTOR(DEVICE)
cl.submit(dirIn, 0, 18, []byte{0x80, 6, 0, 1, 0, 0, 18, 0}, nil)
_, st, d := cl.ret()
if st != 0 || len(d) != 18 || d[1] != 1 {
t.Fatalf("descriptor de dispositivo: status %d, % X", st, d)
}
if vid := binary.LittleEndian.Uint16(d[8:]); vid != 0x1209 {
t.Fatalf("VID = %04X", vid)
}
// GET_DESCRIPTOR(CONFIGURATION) con wLength 9: solo la cabecera
cl.submit(dirIn, 0, 9, []byte{0x80, 6, 0, 2, 0, 0, 9, 0}, nil)
_, _, d = cl.ret()
if len(d) != 9 || d[2] != 32 {
t.Fatalf("configuración: % X", d)
}
// GET_DEVICE_ID de la clase impresora
cl.submit(dirIn, 0, 1024, []byte{0xA1, 0, 0, 0, 0, 0, 0, 4}, nil)
_, _, d = cl.ret()
if !bytes.Contains(d, []byte("CMD:ESC/POS")) {
t.Fatalf("GET_DEVICE_ID = %q", d)
}
// DEVICE_QUALIFIER debe dar STALL
cl.submit(dirIn, 0, 10, []byte{0x80, 6, 0, 6, 0, 0, 10, 0}, nil)
if _, st, _ = cl.ret(); st != errPipe {
t.Fatalf("DEVICE_QUALIFIER status = %d, want %d", st, errPipe)
}
// Lectura bulk IN pendiente antes de que haya datos
inSeq := cl.submit(dirIn, epBulkIn, 64, nil, nil)
// Bulk OUT: texto + DLE EOT 1
payload := []byte("\x1b@Hola USB\n\x10\x04\x01")
outSeq := cl.submit(dirOut, epBulkOut, len(payload), nil, payload)
got := map[uint32][]byte{}
for i := 0; i < 2; i++ {
seq, st, data := cl.ret()
if st != 0 {
t.Fatalf("seq %d status %d", seq, st)
}
got[seq] = data
}
if !bytes.Equal(got[inSeq], []byte{0x12}) {
t.Fatalf("bulk IN = % X, want 12", got[inSeq])
}
if _, ok := got[outSeq]; !ok {
t.Fatal("falta la respuesta del bulk OUT")
}
// UNLINK de una lectura pendiente
pending := cl.submit(dirIn, epBulkIn, 64, nil, nil)
cl.seq++
u := make([]byte, 48)
binary.BigEndian.PutUint32(u[0:], cmdUnlink)
binary.BigEndian.PutUint32(u[4:], cl.seq)
binary.BigEndian.PutUint32(u[20:], pending)
cl.c.Write(u)
h := readN(t, cl.c, 48)
if binary.BigEndian.Uint32(h[0:]) != retUnlink || int32(binary.BigEndian.Uint32(h[20:])) != errConnReset {
t.Fatalf("RET_UNLINK inesperado: % X", h)
}
if len(p.Snapshot(0).Connections) != 1 {
t.Fatal("debería haber una conexión USB activa")
}
}