• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2025 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {
18  ArrayBufferBuilder,
19  byteArrayToString,
20  stringToByteArray,
21} from 'common/buffer_utils';
22import {UnitTestUtils} from 'test/unit/utils';
23import {SyncStream} from './sync_stream';
24
25describe('SyncStream', () => {
26  const serialNumber = '123';
27  const errorListener = jasmine.createSpy();
28  const testFileDataString = 'test file data';
29  const testFileData = stringToByteArray(testFileDataString);
30  const testFilepath = 'test_filepath';
31  const expectedSendBuffer = new Uint8Array(
32    new ArrayBufferBuilder()
33      .append(['RECV', testFilepath.length, testFilepath])
34      .build(),
35  );
36  const emptyByte = Uint8Array.from([0, 0, 0, 0]);
37  let stream: SyncStream;
38  let webSocket: jasmine.SpyObj<WebSocket>;
39
40  beforeEach(async () => {
41    webSocket = UnitTestUtils.makeFakeWebSocket();
42    errorListener.calls.reset();
43    stream = new SyncStream(webSocket, serialNumber, errorListener);
44    await stream.connect();
45  });
46
47  afterEach(() => {
48    expect(errorListener).not.toHaveBeenCalled();
49  });
50
51  it('connects to sync service', async () => {
52    expect(webSocket.send).toHaveBeenCalledOnceWith(
53      JSON.stringify({
54        header: {
55          serialNumber,
56          command: 'sync:',
57        },
58      }),
59    );
60  });
61
62  it('calls error listener if unexpected message type received - AdbResponse json', async () => {
63    setMessageResponses([
64      JSON.stringify({error: {type: '', message: 'failed'}}),
65    ]);
66    const receivedData = await stream.pullFile(testFilepath);
67    expect(errorListener).toHaveBeenCalledOnceWith(
68      `Could not parse data:\nReceived: {"error":{"type":"","message":"failed"}}` +
69        `\nError: Expected message data to be ArrayBuffer or Blob.` +
70        `\nADB Error: failed`,
71    );
72    expect(receivedData).toEqual(Uint8Array.from([]));
73    errorListener.calls.reset();
74  });
75
76  it('calls error listener if unexpected message type received - unknown string', async () => {
77    setMessageResponses(['unknown error']);
78    const receivedData = await stream.pullFile(testFilepath);
79    expect(errorListener).toHaveBeenCalledOnceWith(
80      `Could not parse data:\nReceived: unknown error` +
81        `\nError: Expected message data to be ArrayBuffer or Blob.`,
82    );
83    expect(receivedData).toEqual(Uint8Array.from([]));
84    errorListener.calls.reset();
85  });
86
87  it('calls error listener if unexpected message type received - unknown code', async () => {
88    setMessageResponses([200]);
89    const receivedData = await stream.pullFile(testFilepath);
90    expect(errorListener).toHaveBeenCalledOnceWith(
91      `Could not parse data:\nReceived: 200` +
92        `\nError: Expected message data to be ArrayBuffer or Blob.`,
93    );
94    expect(receivedData).toEqual(Uint8Array.from([]));
95    errorListener.calls.reset();
96  });
97
98  it('pulls file data from one chunk in one message', async () => {
99    const messageData = new ArrayBufferBuilder()
100      .append(['DATA', testFileData.length, testFileData, 'DONE', emptyByte])
101      .build();
102    setMessageResponses([messageData]);
103    const receivedData = await stream.pullFile(testFilepath);
104    expect(byteArrayToString(receivedData)).toEqual(testFileDataString);
105  });
106
107  it('pulls file data from one chunk across two messages', async () => {
108    const fileData1 = testFileData.slice(0, 3);
109    const fileData2 = testFileData.slice(3);
110    const messageData1 = new ArrayBufferBuilder()
111      .append(['DATA', testFileData.length, fileData1])
112      .build();
113    const messageData2 = new ArrayBufferBuilder()
114      .append([fileData2, 'DONE', emptyByte])
115      .build();
116    setMessageResponses([messageData1, messageData2]);
117    const receivedData = await stream.pullFile(testFilepath);
118    expect(byteArrayToString(receivedData)).toEqual(testFileDataString);
119  });
120
121  it('pulls file data from one chunk across three messages', async () => {
122    const fileData1 = testFileData.slice(0, 3);
123    const fileData2 = testFileData.slice(3, 5);
124    const fileData3 = testFileData.slice(5);
125    const messageData1 = new ArrayBufferBuilder()
126      .append(['DATA', testFileData.length, fileData1])
127      .build();
128    const messageData2 = new ArrayBufferBuilder().append([fileData2]).build();
129    const messageData3 = new ArrayBufferBuilder()
130      .append([fileData3, 'DONE', emptyByte])
131      .build();
132    setMessageResponses([messageData1, messageData2, messageData3]);
133    const receivedData = await stream.pullFile(testFilepath);
134    expect(byteArrayToString(receivedData)).toEqual(testFileDataString);
135  });
136
137  it('pulls file data from multiple chunks in one message', async () => {
138    const fileData1 = testFileData.slice(0, 3);
139    const fileData2 = testFileData.slice(3);
140    const messageData = new ArrayBufferBuilder()
141      .append(['DATA', fileData1.length, fileData1])
142      .append(['DATA', fileData2.length, fileData2, 'DONE', emptyByte])
143      .build();
144    setMessageResponses([messageData]);
145    const receivedData = await stream.pullFile(testFilepath);
146    expect(byteArrayToString(receivedData)).toEqual(testFileDataString);
147  });
148
149  it('pulls file data from multiple chunks, one chunk per message', async () => {
150    const fileData1 = testFileData.slice(0, 3);
151    const fileData2 = testFileData.slice(3);
152    const messageData1 = new ArrayBufferBuilder()
153      .append(['DATA', fileData1.length, fileData1])
154      .build();
155    const messageData2 = new ArrayBufferBuilder()
156      .append(['DATA', fileData2.length, fileData2, 'DONE', emptyByte])
157      .build();
158    setMessageResponses([messageData1, messageData2]);
159    const receivedData = await stream.pullFile(testFilepath);
160    expect(byteArrayToString(receivedData)).toEqual(testFileDataString);
161  });
162
163  it('pulls file data from multiple chunks across multiple messages', async () => {
164    const fileData1 = testFileData.slice(0, 3);
165    const fileData2 = testFileData.slice(3, 5);
166    const fileData3 = testFileData.slice(5);
167    const messageData1 = new ArrayBufferBuilder()
168      .append(['DATA', fileData1.length + fileData2.length, fileData1])
169      .build();
170    const messageData2 = new ArrayBufferBuilder()
171      .append([fileData2])
172      .append(['DATA', fileData3.length, fileData3, 'DONE', emptyByte])
173      .build();
174    setMessageResponses([messageData1, messageData2]);
175    const receivedData = await stream.pullFile(testFilepath);
176    expect(byteArrayToString(receivedData)).toEqual(testFileDataString);
177  });
178
179  it('pulls file data where DATA id is in separate message', async () => {
180    const messageData1 = new ArrayBufferBuilder()
181      .append(['DATA', testFileData.length])
182      .build();
183    const messageData2 = new ArrayBufferBuilder()
184      .append([testFileData, 'DONE', emptyByte])
185      .build();
186    setMessageResponses([messageData1, messageData2]);
187    const receivedData = await stream.pullFile(testFilepath);
188    expect(byteArrayToString(receivedData)).toEqual(testFileDataString);
189  });
190
191  it('pulls file data where DONE id is in separate message', async () => {
192    const messageData1 = new ArrayBufferBuilder()
193      .append(['DATA', testFileData.length, testFileData])
194      .build();
195    const messageData2 = new ArrayBufferBuilder()
196      .append(['DONE', emptyByte])
197      .build();
198    setMessageResponses([messageData1, messageData2]);
199    const receivedData = await stream.pullFile(testFilepath);
200    expect(byteArrayToString(receivedData)).toEqual(testFileDataString);
201  });
202
203  it('pulls file data where DATA and DONE ids in separate messages', async () => {
204    const messageData1 = new ArrayBufferBuilder()
205      .append(['DATA', testFileData.length])
206      .build();
207    const messageData2 = new ArrayBufferBuilder()
208      .append([testFileData])
209      .build();
210    const messageData3 = new ArrayBufferBuilder()
211      .append(['DONE', emptyByte])
212      .build();
213    setMessageResponses([messageData1, messageData2, messageData3]);
214    const receivedData = await stream.pullFile(testFilepath);
215    expect(byteArrayToString(receivedData)).toEqual(testFileDataString);
216  });
217
218  it('robust to file data where length is too small', async () => {
219    const messageData = new ArrayBufferBuilder()
220      .append(['DATA', testFileData.length, testFileData, 'DONE'])
221      .build();
222
223    webSocket.send.withArgs(expectedSendBuffer).and.callFake(() => {
224      const message = jasmine.createSpyObj<MessageEvent<ArrayBuffer>>([], {
225        'data': messageData,
226      });
227      webSocket.onmessage!(message);
228    });
229    const receivedData = await stream.pullFile(testFilepath);
230    expect(byteArrayToString(receivedData)).toEqual(testFileDataString);
231  });
232
233  it('robust to unexpected id at start of chunk', async () => {
234    const fileData1 = testFileData.slice(0, 3);
235    const fileData2 = testFileData.slice(3);
236    const messageData1 = new ArrayBufferBuilder()
237      .append(['DATA', fileData1.length, fileData1])
238      .build();
239
240    const messageData2 = new ArrayBufferBuilder()
241      .append(['NEXT', fileData2.length, fileData2, 'DONE', emptyByte])
242      .build();
243    setMessageResponses([messageData1, messageData2]);
244    const receivedData = await stream.pullFile(testFilepath);
245    expect(byteArrayToString(receivedData)).toEqual('tes');
246  });
247
248  it('pulls file data from blob', async () => {
249    const messageData = new ArrayBufferBuilder()
250      .append(['DATA', testFileData.length, testFileData, 'DONE', emptyByte])
251      .build();
252    setMessageResponses([new Blob([messageData])]);
253    const receivedData = await stream.pullFile(testFilepath);
254    expect(byteArrayToString(receivedData)).toEqual(testFileDataString);
255  });
256
257  function setMessageResponses(
258    messageData: Array<Blob | ArrayBuffer | number | string>,
259  ) {
260    webSocket.send.withArgs(expectedSendBuffer).and.callFake(() => {
261      messageData.forEach((data) => {
262        const message = UnitTestUtils.makeFakeWebSocketMessage(data);
263        webSocket.onmessage!(message);
264      });
265    });
266    errorListener.and.callFake(() => {
267      webSocket.close();
268    });
269  }
270});
271