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