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 {ProgressListener} from 'messaging/progress_listener'; 18import {ProxyTracingWarnings} from 'messaging/user_warnings'; 19import {UserNotifierChecker} from 'test/unit/user_notifier_checker'; 20import {AdbDeviceState} from 'trace_collection/adb/adb_device_connection'; 21import {AdbConnectionType} from 'trace_collection/adb_connection_type'; 22import {ConnectionState} from 'trace_collection/connection_state'; 23import {ConnectionStateListener} from 'trace_collection/connection_state_listener'; 24import {MockAdbDeviceConnection} from 'trace_collection/mock/mock_adb_device_connection'; 25import {MockAdbHostConnection} from 'trace_collection/mock/mock_adb_host_connection'; 26import {AdbFileIdentifier, TraceTarget} from 'trace_collection/trace_target'; 27import {UiTraceTarget} from 'trace_collection/ui/ui_trace_target'; 28import {UserRequest} from 'trace_collection/user_request'; 29import {PerfettoSessionModerator} from './perfetto_session_moderator'; 30import {TraceCollectionController} from './trace_collection_controller'; 31import {TracingSession} from './tracing_session'; 32import {WINSCOPE_BACKUP_DIR} from './winscope_backup_dir'; 33 34describe('TraceCollectionController', () => { 35 const listener = jasmine.createSpyObj< 36 ConnectionStateListener & ProgressListener 37 >('ConnectionStateListener', [ 38 'onAvailableTracesChange', 39 'onDevicesChange', 40 'onError', 41 'onConnectionStateChange', 42 'onProgressUpdate', 43 'onOperationFinished', 44 ]); 45 46 const mockDevice = new MockAdbDeviceConnection( 47 '35562', 48 'Pixel 6', 49 AdbDeviceState.AVAILABLE, 50 listener, 51 ); 52 const mockUserRequest: UserRequest = { 53 target: UiTraceTarget.WINDOW_MANAGER_TRACE, 54 config: [], 55 }; 56 57 let controller: TraceCollectionController; 58 let restartSpy: jasmine.Spy; 59 let moveSpy: jasmine.Spy; 60 let runShellCmdSpy: jasmine.Spy; 61 62 beforeEach(() => { 63 restartSpy = spyOn( 64 MockAdbHostConnection.prototype, 65 'restart', 66 ).and.callThrough(); 67 moveSpy = spyOn(TracingSession.prototype, 'moveFiles'); 68 runShellCmdSpy = spyOn(mockDevice, 'runShellCommand'); 69 controller = new TraceCollectionController( 70 AdbConnectionType.MOCK, 71 listener, 72 ); 73 resetListener(); 74 }); 75 76 describe('initialization and destruction:', () => { 77 let hostDestroySpy: jasmine.Spy; 78 let securityTokenSpy: jasmine.Spy; 79 80 beforeEach(() => { 81 hostDestroySpy = spyOn(MockAdbHostConnection.prototype, 'onDestroy'); 82 securityTokenSpy = spyOn( 83 MockAdbHostConnection.prototype, 84 'setSecurityToken', 85 ); 86 }); 87 88 it('exposes connection type', () => { 89 expect(controller.getConnectionType()).toEqual(AdbConnectionType.MOCK); 90 }); 91 92 it('restarts host connection', async () => { 93 await controller.restartConnection(); 94 expect(restartSpy).toHaveBeenCalledTimes(1); 95 expect(listener.onConnectionStateChange.calls.mostRecent().args).toEqual([ 96 ConnectionState.CONNECTING, 97 ]); 98 }); 99 100 it('sets security token', () => { 101 controller.setSecurityToken('12345'); 102 expect(securityTokenSpy).toHaveBeenCalledOnceWith('12345'); 103 }); 104 105 it('requests devices', async () => { 106 await controller.requestDevices(); 107 expect(listener.onDevicesChange).toHaveBeenCalledTimes(1); 108 expect(listener.onConnectionStateChange.calls.mostRecent().args).toEqual([ 109 ConnectionState.IDLE, 110 ]); 111 }); 112 113 it('cancels device requests', async () => { 114 const spy = spyOn( 115 MockAdbHostConnection.prototype, 116 'cancelDeviceRequests', 117 ); 118 controller.cancelDeviceRequests(); 119 expect(spy).toHaveBeenCalledTimes(1); 120 }); 121 122 it('destroys adb session and host on destroy', async () => { 123 runShellCmdSpy.and.returnValue(''); 124 await controller.startTrace(mockDevice, [mockUserRequest]); 125 const spies = [ 126 spyOn(TracingSession.prototype, 'onDestroy'), 127 hostDestroySpy, 128 ]; 129 await controller.onDestroy(mockDevice); 130 spies.forEach((spy) => expect(spy).toHaveBeenCalledTimes(1)); 131 }); 132 }); 133 134 describe('starts traces:', () => { 135 let startSpy: jasmine.Spy<(target: TraceTarget) => Promise<void>>; 136 let userNotifierChecker: UserNotifierChecker; 137 138 beforeEach(async () => { 139 startSpy = spyOn(mockDevice, 'startTrace'); 140 runShellCmdSpy.and.returnValue(''); 141 userNotifierChecker = new UserNotifierChecker(); 142 }); 143 144 it('restarts connection if no traces requested', async () => { 145 await controller.startTrace(mockDevice, []); 146 expect(restartSpy).toHaveBeenCalledTimes(1); 147 expect(startSpy).not.toHaveBeenCalled(); 148 userNotifierChecker.expectNotified([ 149 new ProxyTracingWarnings([ 150 'None of the requested targets are available on this device.', 151 ]), 152 ]); 153 }); 154 155 it('starts legacy traces', async () => { 156 const target = new TraceTarget('WmLegacyTrace', [], '', '', [ 157 new AdbFileIdentifier( 158 '/data/misc/wmtrace/', 159 ['wm_trace.winscope', 'wm_trace.pb'], 160 'window_trace', 161 ), 162 ]); 163 await checkTracingSessionsStarted( 164 [mockUserRequest, mockUserRequest], 165 [target, target], 166 ); 167 }); 168 169 it('starts perfetto traces', async () => { 170 runShellCmdSpy 171 .withArgs('perfetto --query') 172 .and.returnValue('android.windowmanager'); 173 await checkTracingSessionsStarted( 174 [mockUserRequest], 175 [ 176 new TraceTarget('PerfettoTrace', [], '', '', [ 177 new AdbFileIdentifier( 178 '/data/misc/perfetto-traces/winscope-proxy-trace.perfetto-trace', 179 [], 180 'trace.perfetto-trace', 181 ), 182 ]), 183 ], 184 ); 185 }); 186 187 async function checkTracingSessionsStarted( 188 requests: UserRequest[], 189 targets: TraceTarget[], 190 ) { 191 startSpy.calls.reset(); 192 const stopCurrentSession = spyOn( 193 PerfettoSessionModerator.prototype, 194 'tryStopCurrentPerfettoSession', 195 ); 196 const clearPreviousConfigFiles = spyOn( 197 PerfettoSessionModerator.prototype, 198 'clearPreviousConfigFiles', 199 ); 200 201 await controller.startTrace(mockDevice, requests); 202 203 expect(stopCurrentSession).toHaveBeenCalledTimes(1); 204 expect(clearPreviousConfigFiles).toHaveBeenCalledTimes(1); 205 expect(runShellCmdSpy.calls.allArgs().slice(1, 3).flat()).toEqual([ 206 `su root rm -rf ${WINSCOPE_BACKUP_DIR}`, 207 `su root mkdir ${WINSCOPE_BACKUP_DIR}`, 208 ]); 209 startSpy.calls.allArgs().forEach((args, index) => { 210 expect(args[0].traceName).toEqual(targets[index].traceName); 211 expect(args[0].fileIdentifiers).toEqual(targets[index].fileIdentifiers); 212 }); 213 userNotifierChecker.expectNone(); 214 } 215 }); 216 217 describe('ends traces:', () => { 218 it('ends tracing controller', async () => { 219 const endSpy = spyOn(TracingSession.prototype, 'stop'); 220 runShellCmdSpy.and.returnValue(''); 221 await controller.startTrace(mockDevice, [ 222 mockUserRequest, 223 mockUserRequest, 224 ]); 225 await controller.endTrace(mockDevice); 226 expect(endSpy).toHaveBeenCalledTimes(2); 227 expect(moveSpy).toHaveBeenCalledTimes(2); 228 expect(listener.onProgressUpdate).toHaveBeenCalledTimes(4); 229 expect(listener.onOperationFinished).toHaveBeenCalledTimes(1); 230 }); 231 }); 232 233 describe('dumps state:', () => { 234 let userNotifierChecker: UserNotifierChecker; 235 236 beforeEach(async () => { 237 runShellCmdSpy.and.returnValue(''); 238 userNotifierChecker = new UserNotifierChecker(); 239 }); 240 241 it('restarts connection if no dumps requested', async () => { 242 await controller.dumpState(mockDevice, []); 243 expect(restartSpy).toHaveBeenCalledTimes(1); 244 expect(runShellCmdSpy).not.toHaveBeenCalled(); 245 userNotifierChecker.expectNotified([ 246 new ProxyTracingWarnings([ 247 'None of the requested targets are available on this device.', 248 ]), 249 ]); 250 }); 251 252 it('dumps legacy states', async () => { 253 runShellCmdSpy.calls.reset(); 254 255 const expectedCommands = [ 256 'su root cmd window tracing frame', 257 'su root cmd window tracing level debug', 258 'su root cmd window tracing size 16000', 259 'su root cmd window tracing start\necho "WM trace (legacy) started."', 260 'su root cmd window tracing frame', 261 'su root cmd window tracing level debug', 262 'su root cmd window tracing size 16000', 263 'su root cmd window tracing start\necho "WM trace (legacy) started."', 264 ]; 265 266 await checkDump([mockUserRequest, mockUserRequest], expectedCommands); 267 }); 268 269 it('dumps perfetto states', async () => { 270 runShellCmdSpy 271 .withArgs('perfetto --query') 272 .and.returnValue('android.surfaceflinger.layers'); 273 274 const req = [ 275 { 276 target: UiTraceTarget.SURFACE_FLINGER_DUMP, 277 config: [], 278 }, 279 ]; 280 const expectedCommands = [ 281 `cat << EOF >> /data/misc/perfetto-configs/winscope-proxy-dump.conf 282data_sources: { 283 config { 284 name: "android.surfaceflinger.layers" 285 surfaceflinger_layers_config: { 286 mode: MODE_DUMP 287 trace_flags: TRACE_FLAG_INPUT 288 trace_flags: TRACE_FLAG_COMPOSITION 289 trace_flags: TRACE_FLAG_HWC 290 trace_flags: TRACE_FLAG_BUFFERS 291 trace_flags: TRACE_FLAG_VIRTUAL_DISPLAYS 292 } 293 } 294} 295EOF`, 296 `cat << EOF >> /data/misc/perfetto-configs/winscope-proxy-dump.conf 297buffers: { 298 size_kb: 500000 299 fill_policy: RING_BUFFER 300} 301duration_ms: 1 302EOF 303rm -f /data/misc/perfetto-traces/winscope-proxy-dump.perfetto-trace 304perfetto --out /data/misc/perfetto-traces/winscope-proxy-dump.perfetto-trace --txt --config /data/misc/perfetto-configs/winscope-proxy-dump.conf 305echo 'Dumped perfetto'`, 306 ]; 307 await checkDump(req, expectedCommands); 308 }); 309 310 async function checkDump(req: UserRequest[], commands: string[]) { 311 const stopCurrentSession = spyOn( 312 PerfettoSessionModerator.prototype, 313 'tryStopCurrentPerfettoSession', 314 ); 315 const clearPreviousConfigFiles = spyOn( 316 PerfettoSessionModerator.prototype, 317 'clearPreviousConfigFiles', 318 ); 319 320 await controller.dumpState(mockDevice, req); 321 322 expect(stopCurrentSession).toHaveBeenCalledTimes(1); 323 expect(clearPreviousConfigFiles).toHaveBeenCalledTimes(1); 324 325 const expectedCommands = [ 326 'perfetto --query', 327 `su root rm -rf ${WINSCOPE_BACKUP_DIR}`, 328 `su root mkdir ${WINSCOPE_BACKUP_DIR}`, 329 ].concat(commands); 330 runShellCmdSpy.calls.allArgs().forEach((args, index) => { 331 expect(args[0]).toEqual(expectedCommands[index]); 332 }); 333 userNotifierChecker.expectNone(); 334 } 335 }); 336 337 describe('fetches data:', () => { 338 const data = Uint8Array.from([]); 339 const devicePath = 'archive/test_path'; 340 const fetchedPath = 'test_path'; 341 let findSpy: jasmine.Spy; 342 let pullSpy: jasmine.Spy; 343 344 beforeEach(async () => { 345 findSpy = spyOn( 346 MockAdbDeviceConnection.prototype, 347 'findFiles', 348 ).and.returnValue(Promise.resolve([devicePath, devicePath])); 349 pullSpy = spyOn( 350 MockAdbDeviceConnection.prototype, 351 'pullFile', 352 ).and.returnValue(Promise.resolve(data)); 353 }); 354 355 it('fetches last tracing session data', async () => { 356 expect(await controller.fetchLastSessionData(mockDevice)).toEqual([ 357 new File([data], fetchedPath), 358 new File([data], fetchedPath), 359 ]); 360 expect(listener.onProgressUpdate).toHaveBeenCalledTimes(2); 361 expect(listener.onOperationFinished).toHaveBeenCalledTimes(1); 362 }); 363 364 it('does not keep data from last fetch', async () => { 365 await controller.fetchLastSessionData(mockDevice); 366 expect(await controller.fetchLastSessionData(mockDevice)).toEqual([ 367 new File([data], fetchedPath), 368 new File([data], fetchedPath), 369 ]); 370 }); 371 }); 372 373 function resetListener() { 374 listener.onAvailableTracesChange.calls.reset(); 375 listener.onDevicesChange.calls.reset(); 376 listener.onError.calls.reset(); 377 listener.onConnectionStateChange.calls.reset(); 378 listener.onProgressUpdate.calls.reset(); 379 listener.onOperationFinished.calls.reset(); 380 } 381}); 382