test_helpers_test.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. package device
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "sync"
  7. "testing"
  8. "time"
  9. "vocat/internal/modem"
  10. )
  11. type clientStep struct {
  12. command string
  13. response modem.Response
  14. err error
  15. }
  16. type promptClientStep struct {
  17. command string
  18. payload string
  19. validateBody func(string) error
  20. response modem.Response
  21. err error
  22. }
  23. type transcriptClient struct {
  24. mu sync.Mutex
  25. steps []clientStep
  26. promptSteps []promptClientStep
  27. urcs []string
  28. unexpected error
  29. closeCount int
  30. }
  31. func (client *transcriptClient) ExecutePrompt(
  32. ctx context.Context,
  33. command string,
  34. payload []byte,
  35. ) (modem.Response, error) {
  36. if err := ctx.Err(); err != nil {
  37. return modem.Response{}, err
  38. }
  39. client.mu.Lock()
  40. defer client.mu.Unlock()
  41. if len(client.promptSteps) == 0 {
  42. client.unexpected = fmt.Errorf(
  43. "unexpected prompt command %q with payload %q",
  44. command,
  45. payload,
  46. )
  47. return modem.Response{}, client.unexpected
  48. }
  49. step := client.promptSteps[0]
  50. client.promptSteps = client.promptSteps[1:]
  51. if command != step.command {
  52. client.unexpected = fmt.Errorf(
  53. "prompt command %q, want %q",
  54. command,
  55. step.command,
  56. )
  57. return modem.Response{}, client.unexpected
  58. }
  59. if step.validateBody != nil {
  60. if err := step.validateBody(string(payload)); err != nil {
  61. client.unexpected = err
  62. return modem.Response{}, err
  63. }
  64. } else if string(payload) != step.payload {
  65. client.unexpected = fmt.Errorf(
  66. "prompt payload %q, want %q",
  67. payload,
  68. step.payload,
  69. )
  70. return modem.Response{}, client.unexpected
  71. }
  72. response := step.response
  73. if response.Command == "" {
  74. response.Command = command
  75. }
  76. return response, step.err
  77. }
  78. func (client *transcriptClient) Execute(
  79. ctx context.Context,
  80. command string,
  81. ) (modem.Response, error) {
  82. if err := ctx.Err(); err != nil {
  83. return modem.Response{}, err
  84. }
  85. client.mu.Lock()
  86. defer client.mu.Unlock()
  87. if len(client.steps) == 0 {
  88. client.unexpected = fmt.Errorf("unexpected command %q", command)
  89. return modem.Response{}, client.unexpected
  90. }
  91. step := client.steps[0]
  92. client.steps = client.steps[1:]
  93. if command != step.command {
  94. client.unexpected = fmt.Errorf("command %q, want %q", command, step.command)
  95. return modem.Response{}, client.unexpected
  96. }
  97. response := step.response
  98. if response.Command == "" {
  99. response.Command = command
  100. }
  101. return response, step.err
  102. }
  103. func (client *transcriptClient) WaitURC(
  104. ctx context.Context,
  105. predicate func(string) bool,
  106. ) (string, error) {
  107. if err := ctx.Err(); err != nil {
  108. return "", err
  109. }
  110. client.mu.Lock()
  111. defer client.mu.Unlock()
  112. for index, line := range client.urcs {
  113. if predicate(line) {
  114. client.urcs = append(client.urcs[:index], client.urcs[index+1:]...)
  115. return line, nil
  116. }
  117. }
  118. client.unexpected = errors.New("no matching URC in transcript")
  119. return "", client.unexpected
  120. }
  121. func (client *transcriptClient) Close() error {
  122. client.mu.Lock()
  123. client.closeCount++
  124. client.mu.Unlock()
  125. return nil
  126. }
  127. func (client *transcriptClient) assertDone(t *testing.T) {
  128. t.Helper()
  129. client.mu.Lock()
  130. defer client.mu.Unlock()
  131. if client.unexpected != nil {
  132. t.Fatalf("transcript error: %v", client.unexpected)
  133. }
  134. if len(client.steps) != 0 {
  135. t.Fatalf("%d command transcript steps remain; next is %q", len(client.steps), client.steps[0].command)
  136. }
  137. if len(client.promptSteps) != 0 {
  138. t.Fatalf(
  139. "%d prompt transcript steps remain; next is %q",
  140. len(client.promptSteps),
  141. client.promptSteps[0].command,
  142. )
  143. }
  144. }
  145. type staticDiscoverer struct {
  146. candidates []modem.Candidate
  147. err error
  148. }
  149. func (discoverer staticDiscoverer) Discover(
  150. ctx context.Context,
  151. ) ([]modem.Candidate, error) {
  152. if err := ctx.Err(); err != nil {
  153. return nil, err
  154. }
  155. result := append([]modem.Candidate(nil), discoverer.candidates...)
  156. return result, discoverer.err
  157. }
  158. type staticOpener struct {
  159. mu sync.Mutex
  160. client modem.Client
  161. err error
  162. openCount int
  163. ports []modem.Port
  164. }
  165. func (opener *staticOpener) Open(
  166. ctx context.Context,
  167. port modem.Port,
  168. ) (modem.Client, error) {
  169. if err := ctx.Err(); err != nil {
  170. return nil, err
  171. }
  172. opener.mu.Lock()
  173. defer opener.mu.Unlock()
  174. opener.openCount++
  175. opener.ports = append(opener.ports, port)
  176. return opener.client, opener.err
  177. }
  178. func newStartedTestManager(
  179. t *testing.T,
  180. client modem.Client,
  181. ) (*Manager, string) {
  182. t.Helper()
  183. const id = "quectel-test-ec20"
  184. opener := &staticOpener{client: client}
  185. manager, err := NewManager(Options{
  186. Discoverer: staticDiscoverer{candidates: []modem.Candidate{{
  187. ID: id,
  188. VendorID: "2c7c",
  189. ProductID: "0125",
  190. Manufacturer: "Quectel",
  191. Product: "EC20",
  192. ATPort: modem.Port{
  193. Path: "/dev/ttyUSB2",
  194. Name: "ttyUSB2",
  195. InterfaceNumber: 0x04,
  196. Role: modem.PortRoleAT,
  197. },
  198. }}},
  199. Opener: opener,
  200. CommandTimeout: time.Second,
  201. LongTimeout: time.Second,
  202. })
  203. if err != nil {
  204. t.Fatalf("NewManager: %v", err)
  205. }
  206. if err := manager.Start(context.Background()); err != nil {
  207. t.Fatalf("Start: %v", err)
  208. }
  209. t.Cleanup(func() {
  210. _ = manager.Stop(context.Background())
  211. })
  212. return manager, id
  213. }
  214. func okResponse(lines ...string) modem.Response {
  215. return modem.Response{Lines: lines, Final: "OK"}
  216. }