xref: /aosp_15_r20/external/pigweed/ts/transport/serial_mock.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1// Copyright 2022 The Pigweed Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14
15/* eslint-env browser */
16import { Subject } from 'rxjs';
17import type {
18  SerialConnectionEvent,
19  SerialPort,
20  Serial,
21  SerialPortRequestOptions,
22  SerialOptions,
23} from 'pigweedjs/types/serial';
24/**
25 * AsyncQueue is a queue that allows values to be dequeued
26 * before they are enqueued, returning a promise that resolves
27 * once the value is available.
28 */
29class AsyncQueue<T> {
30  private queue: T[] = [];
31  private requestQueue: Array<(val: T) => unknown> = [];
32
33  /**
34   * Enqueue val into the queue.
35   * @param {T} val
36   */
37  enqueue(val: T) {
38    const callback = this.requestQueue.shift();
39    if (callback) {
40      callback(val);
41    } else {
42      this.queue.push(val);
43    }
44  }
45
46  /**
47   * Dequeue a value from the queue, returning a promise
48   * if the queue is empty.
49   */
50  async dequeue(): Promise<T> {
51    const val = this.queue.shift();
52    if (val !== undefined) {
53      return val;
54    } else {
55      const queuePromise = new Promise<T>((resolve) => {
56        this.requestQueue.push(resolve);
57      });
58      return queuePromise;
59    }
60  }
61}
62
63/**
64 * SerialPortMock is a mock for Chrome's upcoming SerialPort interface.
65 * Since pw_web only depends on a subset of the interface, this mock
66 * only implements that subset.
67 */
68class SerialPortMock implements SerialPort {
69  private deviceData = new AsyncQueue<{
70    data?: Uint8Array;
71    done?: boolean;
72    error?: Error;
73  }>();
74
75  /**
76   * Simulate the device sending data to the browser.
77   * @param {Uint8Array} data
78   */
79  dataFromDevice(data: Uint8Array) {
80    this.deviceData.enqueue({ data });
81  }
82
83  /**
84   * Simulate the device closing the connection with the browser.
85   */
86  closeFromDevice() {
87    this.deviceData.enqueue({ done: true });
88  }
89
90  /**
91   * Simulate an error in the device's read stream.
92   * @param {Error} error
93   */
94  errorFromDevice(error: Error) {
95    this.deviceData.enqueue({ error });
96  }
97
98  /**
99   * An rxjs subject tracking data sent to the (fake) device.
100   */
101  dataToDevice = new Subject<Uint8Array>();
102
103  /**
104   * The ReadableStream of bytes from the device.
105   */
106  readable = new ReadableStream<Uint8Array>({
107    pull: async (controller) => {
108      const { data, done, error } = await this.deviceData.dequeue();
109      if (done) {
110        controller.close();
111        return;
112      }
113      if (error) {
114        throw error;
115      }
116      if (data) {
117        controller.enqueue(data);
118      }
119    },
120  });
121
122  /**
123   * The WritableStream of bytes to the device.
124   */
125  writable = new WritableStream<Uint8Array>({
126    write: (chunk) => {
127      this.dataToDevice.next(chunk);
128    },
129  });
130
131  /**
132   * A spy for opening the serial port.
133   */
134  open = jest.fn(async (options?: SerialOptions) => {
135    // Do nothing.
136  });
137
138  /**
139   * A spy for closing the serial port.
140   */
141  close = jest.fn(() => {
142    // Do nothing.
143  });
144}
145
146export class SerialMock implements Serial {
147  serialPort = new SerialPortMock();
148  dataToDevice = this.serialPort.dataToDevice;
149  dataFromDevice = (data: Uint8Array) => {
150    this.serialPort.dataFromDevice(data);
151  };
152  closeFromDevice = () => {
153    this.serialPort.closeFromDevice();
154  };
155  errorFromDevice = (error: Error) => {
156    this.serialPort.errorFromDevice(error);
157  };
158
159  /**
160   * Request the port from the browser.
161   */
162  async requestPort(options?: SerialPortRequestOptions) {
163    return this.serialPort;
164  }
165
166  // The rest of the methods are unimplemented
167  // and only exist to ensure SerialMock implements Serial
168
169  onconnect(): ((this: this, ev: SerialConnectionEvent) => any) | null {
170    throw new Error('Method not implemented.');
171  }
172
173  ondisconnect(): ((this: this, ev: SerialConnectionEvent) => any) | null {
174    throw new Error('Method not implemented.');
175  }
176
177  getPorts(): Promise<SerialPort[]> {
178    throw new Error('Method not implemented.');
179  }
180
181  addEventListener(
182    type: 'connect' | 'disconnect',
183    listener: (this: this, ev: SerialConnectionEvent) => any,
184    useCapture?: boolean,
185  ): void;
186
187  addEventListener(
188    type: string,
189    listener: EventListener | EventListenerObject | null,
190    options?: boolean | AddEventListenerOptions,
191  ): void;
192
193  addEventListener(type: any, listener: any, options?: any) {
194    console.info('Silently skipping event listeners in mock mode.');
195  }
196
197  removeEventListener(
198    type: 'connect' | 'disconnect',
199    callback: (this: this, ev: SerialConnectionEvent) => any,
200    useCapture?: boolean,
201  ): void;
202
203  removeEventListener(
204    type: string,
205    callback: EventListener | EventListenerObject | null,
206    options?: boolean | EventListenerOptions,
207  ): void;
208
209  removeEventListener(type: any, callback: any, options?: any) {
210    throw new Error('Method not implemented.');
211  }
212
213  dispatchEvent(event: Event): boolean {
214    throw new Error('Method not implemented.');
215  }
216}
217