1 /* 2 * Copyright (C) 2015 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 17 package android.hardware.multiprocess.camera.cts; 18 19 import android.app.Activity; 20 import android.app.ActivityManager; 21 import android.app.UiAutomation; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.hardware.Camera; 25 import android.hardware.camera2.CameraAccessException; 26 import android.hardware.camera2.CameraDevice; 27 import android.hardware.camera2.CameraManager; 28 import android.hardware.camera2.cts.CameraTestUtils.HandlerExecutor; 29 import android.hardware.cts.CameraCtsActivity; 30 import android.os.Handler; 31 import android.test.ActivityInstrumentationTestCase2; 32 import android.util.Log; 33 34 import android.Manifest; 35 36 import androidx.test.InstrumentationRegistry; 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.List; 40 import java.util.Objects; 41 import java.util.concurrent.Executor; 42 import java.util.concurrent.TimeoutException; 43 44 import static org.mockito.Mockito.*; 45 46 /** 47 * Tests for multi-process camera usage behavior. 48 */ 49 public class CameraEvictionTest extends ActivityInstrumentationTestCase2<CameraCtsActivity> { 50 51 public static final String TAG = "CameraEvictionTest"; 52 53 private static final int OPEN_TIMEOUT = 2000; // Timeout for camera to open (ms). 54 private static final int SETUP_TIMEOUT = 5000; // Remote camera setup timeout (ms). 55 private static final int EVICTION_TIMEOUT = 1000; // Remote camera eviction timeout (ms). 56 private static final int WAIT_TIME = 2000; // Time to wait for process to launch (ms). 57 private static final int UI_TIMEOUT = 10000; // Time to wait for UI event before timeout (ms). 58 // CACHED_APP_MAX_ADJ - FG oom score 59 private static final int CACHED_APP_VS_FG_OOM_DELTA = 999; 60 ErrorLoggingService.ErrorServiceConnection mErrorServiceConnection; 61 62 private ActivityManager mActivityManager; 63 private Context mContext; 64 private Camera mCamera; 65 private CameraDevice mCameraDevice; 66 private UiAutomation mUiAutomation; 67 private final Object mLock = new Object(); 68 private boolean mCompleted = false; 69 private int mProcessPid = -1; 70 71 /** Load jni on initialization */ 72 static { 73 System.loadLibrary("ctscamera2_jni"); 74 } 75 initializeAvailabilityCallbacksNative()76 private static native long initializeAvailabilityCallbacksNative(); getAccessCallbacksCountAndResetNative(long context)77 private static native int getAccessCallbacksCountAndResetNative(long context); releaseAvailabilityCallbacksNative(long context)78 private static native long releaseAvailabilityCallbacksNative(long context); 79 CameraEvictionTest()80 public CameraEvictionTest() { 81 super(CameraCtsActivity.class); 82 } 83 84 public static class StateCallbackImpl extends CameraDevice.StateCallback { 85 CameraDevice mCameraDevice; 86 StateCallbackImpl()87 public StateCallbackImpl() { 88 super(); 89 } 90 91 @Override onOpened(CameraDevice cameraDevice)92 public void onOpened(CameraDevice cameraDevice) { 93 synchronized(this) { 94 mCameraDevice = cameraDevice; 95 } 96 Log.i(TAG, "CameraDevice onOpened called for main CTS test process."); 97 } 98 99 @Override onClosed(CameraDevice camera)100 public void onClosed(CameraDevice camera) { 101 super.onClosed(camera); 102 synchronized(this) { 103 mCameraDevice = null; 104 } 105 Log.i(TAG, "CameraDevice onClosed called for main CTS test process."); 106 } 107 108 @Override onDisconnected(CameraDevice cameraDevice)109 public void onDisconnected(CameraDevice cameraDevice) { 110 synchronized(this) { 111 mCameraDevice = null; 112 } 113 Log.i(TAG, "CameraDevice onDisconnected called for main CTS test process."); 114 115 } 116 117 @Override onError(CameraDevice cameraDevice, int i)118 public void onError(CameraDevice cameraDevice, int i) { 119 Log.i(TAG, "CameraDevice onError called for main CTS test process with error " + 120 "code: " + i); 121 } 122 getCameraDevice()123 public synchronized CameraDevice getCameraDevice() { 124 return mCameraDevice; 125 } 126 } 127 128 @Override setUp()129 protected void setUp() throws Exception { 130 super.setUp(); 131 132 mCompleted = false; 133 getActivity(); 134 mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 135 mContext = InstrumentationRegistry.getTargetContext(); 136 System.setProperty("dexmaker.dexcache", mContext.getCacheDir().toString()); 137 mActivityManager = mContext.getSystemService(ActivityManager.class); 138 mErrorServiceConnection = new ErrorLoggingService.ErrorServiceConnection(mContext); 139 mErrorServiceConnection.start(); 140 } 141 142 @Override tearDown()143 protected void tearDown() throws Exception { 144 if (mProcessPid != -1) { 145 android.os.Process.killProcess(mProcessPid); 146 mProcessPid = -1; 147 } 148 if (mErrorServiceConnection != null) { 149 mErrorServiceConnection.stop(); 150 mErrorServiceConnection = null; 151 } 152 if (mCamera != null) { 153 mCamera.release(); 154 mCamera = null; 155 } 156 if (mCameraDevice != null) { 157 mCameraDevice.close(); 158 mCameraDevice = null; 159 } 160 mContext = null; 161 mActivityManager = null; 162 super.tearDown(); 163 } 164 165 /** 166 * Test basic eviction scenarios for the Camera1 API. 167 */ testCamera1ActivityEviction()168 public void testCamera1ActivityEviction() throws Throwable { 169 testAPI1ActivityEviction(Camera1Activity.class, "camera1ActivityProcess"); 170 } 171 testBasicCamera2ActivityEviction()172 public void testBasicCamera2ActivityEviction() throws Throwable { 173 testBasicCamera2ActivityEvictionInternal(/*lowerPriority*/ false); 174 } 175 testBasicCamera2ActivityEvictionOomScoreOffset()176 public void testBasicCamera2ActivityEvictionOomScoreOffset() throws Throwable { 177 testBasicCamera2ActivityEvictionInternal(/*lowerPriority*/ true); 178 } 179 /** 180 * Test basic eviction scenarios for the Camera2 API. 181 */ testBasicCamera2ActivityEvictionInternal(boolean lowerPriority)182 private void testBasicCamera2ActivityEvictionInternal(boolean lowerPriority) throws Throwable { 183 UiAutomation uiAutomation = null; 184 if (lowerPriority && mUiAutomation != null) { 185 mUiAutomation.adoptShellPermissionIdentity(); 186 } 187 CameraManager manager = mContext.getSystemService(CameraManager.class); 188 assertNotNull(manager); 189 String[] cameraIds = manager.getCameraIdListNoLazy(); 190 191 if (cameraIds.length == 0) { 192 Log.i(TAG, "Skipping testBasicCamera2ActivityEviction, device has no cameras."); 193 return; 194 } 195 196 assertTrue(mContext.getMainLooper() != null); 197 198 // Setup camera manager 199 String chosenCamera = cameraIds[0]; 200 Handler cameraHandler = new Handler(mContext.getMainLooper()); 201 final CameraManager.AvailabilityCallback mockAvailCb = 202 mock(CameraManager.AvailabilityCallback.class); 203 204 manager.registerAvailabilityCallback(mockAvailCb, cameraHandler); 205 206 Thread.sleep(WAIT_TIME); 207 208 verify(mockAvailCb, times(1)).onCameraAvailable(chosenCamera); 209 verify(mockAvailCb, never()).onCameraUnavailable(chosenCamera); 210 211 // Setup camera device 212 final CameraDevice.StateCallback spyStateCb = spy(new StateCallbackImpl()); 213 manager.openCamera(chosenCamera, spyStateCb, cameraHandler); 214 215 verify(spyStateCb, timeout(OPEN_TIMEOUT).times(1)).onOpened(any(CameraDevice.class)); 216 verify(spyStateCb, never()).onClosed(any(CameraDevice.class)); 217 verify(spyStateCb, never()).onDisconnected(any(CameraDevice.class)); 218 verify(spyStateCb, never()).onError(any(CameraDevice.class), anyInt()); 219 220 // Open camera from remote process 221 startRemoteProcess(Camera2Activity.class, "camera2ActivityProcess"); 222 223 // Verify that the remote camera was opened correctly 224 List<ErrorLoggingService.LogEvent> allEvents = mErrorServiceConnection.getLog(SETUP_TIMEOUT, 225 TestConstants.EVENT_CAMERA_CONNECT); 226 assertNotNull("Camera device not setup in remote process!", allEvents); 227 228 // Filter out relevant events for other camera devices 229 ArrayList<ErrorLoggingService.LogEvent> events = new ArrayList<>(); 230 for (ErrorLoggingService.LogEvent e : allEvents) { 231 int eventTag = e.getEvent(); 232 if (eventTag == TestConstants.EVENT_CAMERA_UNAVAILABLE || 233 eventTag == TestConstants.EVENT_CAMERA_CONNECT || 234 eventTag == TestConstants.EVENT_CAMERA_AVAILABLE) { 235 if (!Objects.equals(e.getLogText(), chosenCamera)) { 236 continue; 237 } 238 } 239 events.add(e); 240 } 241 int[] eventList = new int[events.size()]; 242 int eventIdx = 0; 243 for (ErrorLoggingService.LogEvent e : events) { 244 eventList[eventIdx++] = e.getEvent(); 245 } 246 String[] actualEvents = TestConstants.convertToStringArray(eventList); 247 String[] expectedEvents = new String[] {TestConstants.EVENT_CAMERA_UNAVAILABLE_STR, 248 TestConstants.EVENT_CAMERA_CONNECT_STR}; 249 String[] ignoredEvents = new String[] { TestConstants.EVENT_CAMERA_AVAILABLE_STR, 250 TestConstants.EVENT_CAMERA_UNAVAILABLE_STR }; 251 assertOrderedEvents(actualEvents, expectedEvents, ignoredEvents); 252 253 // Verify that the local camera was evicted properly 254 verify(spyStateCb, times(1)).onDisconnected(any(CameraDevice.class)); 255 verify(spyStateCb, never()).onClosed(any(CameraDevice.class)); 256 verify(spyStateCb, never()).onError(any(CameraDevice.class), anyInt()); 257 verify(spyStateCb, times(1)).onOpened(any(CameraDevice.class)); 258 259 // Verify that we can no longer open the camera, as it is held by a higher priority process 260 try { 261 if (!lowerPriority) { 262 manager.openCamera(chosenCamera, spyStateCb, cameraHandler); 263 } else { 264 // Go to top again, try getting hold of camera with priority lowered, we should get 265 // an exception 266 Executor cameraExecutor = new HandlerExecutor(cameraHandler); 267 forceCtsActivityToTop(); 268 manager.openCamera(chosenCamera, CACHED_APP_VS_FG_OOM_DELTA, cameraExecutor, 269 spyStateCb); 270 } 271 fail("Didn't receive exception when trying to open camera held by higher priority " + 272 "process."); 273 } catch(CameraAccessException e) { 274 assertTrue("Received incorrect camera exception when opening camera: " + e, 275 e.getReason() == CameraAccessException.CAMERA_IN_USE); 276 } 277 278 // Verify that attempting to open the camera didn't cause anything weird to happen in the 279 // other process. 280 List<ErrorLoggingService.LogEvent> eventList2 = null; 281 boolean timeoutExceptionHit = false; 282 try { 283 eventList2 = mErrorServiceConnection.getLog(EVICTION_TIMEOUT); 284 } catch (TimeoutException e) { 285 timeoutExceptionHit = true; 286 } 287 288 assertNone("Remote camera service received invalid events: ", eventList2); 289 assertTrue("Remote camera service exited early", timeoutExceptionHit); 290 android.os.Process.killProcess(mProcessPid); 291 mProcessPid = -1; 292 forceCtsActivityToTop(); 293 if (lowerPriority && mUiAutomation != null) { 294 mUiAutomation.dropShellPermissionIdentity(); 295 } 296 } 297 298 /** 299 * Tests that a client without SYSTEM_CAMERA permissions receives a security exception when 300 * trying to modify the oom score for camera framework. 301 */ testCamera2OomScoreOffsetPermissions()302 public void testCamera2OomScoreOffsetPermissions() throws Throwable { 303 CameraManager manager = mContext.getSystemService(CameraManager.class); 304 assertNotNull(manager); 305 String[] cameraIds = manager.getCameraIdListNoLazy(); 306 307 if (cameraIds.length == 0) { 308 Log.i(TAG, "Skipping testBasicCamera2OomScoreOffsetPermissions, no cameras present."); 309 return; 310 } 311 312 assertTrue(mContext.getMainLooper() != null); 313 for (String cameraId : cameraIds) { 314 // Setup camera manager 315 Handler cameraHandler = new Handler(mContext.getMainLooper()); 316 final CameraManager.AvailabilityCallback mockAvailCb = 317 mock(CameraManager.AvailabilityCallback.class); 318 319 final CameraDevice.StateCallback spyStateCb = spy(new StateCallbackImpl()); 320 manager.registerAvailabilityCallback(mockAvailCb, cameraHandler); 321 322 Thread.sleep(WAIT_TIME); 323 324 verify(mockAvailCb, times(1)).onCameraAvailable(cameraId); 325 verify(mockAvailCb, never()).onCameraUnavailable(cameraId); 326 327 try { 328 // Go to top again, try getting hold of camera with priority lowered, we should get 329 // an exception 330 Executor cameraExecutor = new HandlerExecutor(cameraHandler); 331 manager.openCamera(cameraId, CACHED_APP_VS_FG_OOM_DELTA, cameraExecutor, 332 spyStateCb); 333 fail("Didn't receive security exception when trying to open camera with modifed" + 334 "oom score without SYSTEM_CAMERA permissions"); 335 } catch(SecurityException e) { 336 // fine 337 } 338 } 339 } 340 /** 341 * Test camera availability access callback. 342 */ testCamera2AccessCallback()343 public void testCamera2AccessCallback() throws Throwable { 344 int PERMISSION_CALLBACK_TIMEOUT_MS = 2000; 345 CameraManager manager = mContext.getSystemService(CameraManager.class); 346 assertNotNull(manager); 347 String[] cameraIds = manager.getCameraIdListNoLazy(); 348 349 if (cameraIds.length == 0) { 350 Log.i(TAG, "Skipping testCamera2AccessCallback, device has no cameras."); 351 return; 352 } 353 354 assertTrue(mContext.getMainLooper() != null); 355 356 // Setup camera manager 357 Handler cameraHandler = new Handler(mContext.getMainLooper()); 358 359 final CameraManager.AvailabilityCallback mockAvailCb = 360 mock(CameraManager.AvailabilityCallback.class); 361 manager.registerAvailabilityCallback(mockAvailCb, cameraHandler); 362 363 // Remove current task from top of stack. This will impact the camera access 364 // pririorties. 365 getActivity().moveTaskToBack(/*nonRoot*/true); 366 367 verify(mockAvailCb, timeout( 368 PERMISSION_CALLBACK_TIMEOUT_MS).atLeastOnce()).onCameraAccessPrioritiesChanged(); 369 370 forceCtsActivityToTop(); 371 372 verify(mockAvailCb, timeout( 373 PERMISSION_CALLBACK_TIMEOUT_MS).atLeastOnce()).onCameraAccessPrioritiesChanged(); 374 } 375 376 /** 377 * Test native camera availability access callback. 378 */ testCamera2NativeAccessCallback()379 public void testCamera2NativeAccessCallback() throws Throwable { 380 int PERMISSION_CALLBACK_TIMEOUT_MS = 2000; 381 CameraManager manager = mContext.getSystemService(CameraManager.class); 382 assertNotNull(manager); 383 String[] cameraIds = manager.getCameraIdListNoLazy(); 384 385 if (cameraIds.length == 0) { 386 Log.i(TAG, "Skipping testBasicCamera2AccessCallback, device has no cameras."); 387 return; 388 } 389 390 // Setup camera manager 391 long context = 0; 392 try { 393 context = initializeAvailabilityCallbacksNative(); 394 assertTrue("Failed to initialize native availability callbacks", (context != 0)); 395 396 // Remove current task from top of stack. This will impact the camera access 397 // pririorties. 398 getActivity().moveTaskToBack(/*nonRoot*/true); 399 400 Thread.sleep(PERMISSION_CALLBACK_TIMEOUT_MS); 401 assertTrue("No camera permission access changed callback received", 402 (getAccessCallbacksCountAndResetNative(context) > 0)); 403 404 forceCtsActivityToTop(); 405 406 assertTrue("No camera permission access changed callback received", 407 (getAccessCallbacksCountAndResetNative(context) > 0)); 408 } finally { 409 if (context != 0) { 410 releaseAvailabilityCallbacksNative(context); 411 } 412 } 413 } 414 415 /** 416 * Test basic eviction scenarios for camera used in MediaRecoder 417 */ testMediaRecorderCameraActivityEviction()418 public void testMediaRecorderCameraActivityEviction() throws Throwable { 419 testAPI1ActivityEviction(MediaRecorderCameraActivity.class, 420 "mediaRecorderCameraActivityProcess"); 421 } 422 423 /** 424 * Test basic eviction scenarios for Camera1 API. 425 * 426 * This test will open camera, create a higher priority process to run the specified activity, 427 * open camera again, and verify the right clients are evicted. 428 * 429 * @param activityKlass An activity to run in a higher priority process. 430 * @param processName The process name. 431 */ testAPI1ActivityEviction(java.lang.Class<?> activityKlass, String processName)432 private void testAPI1ActivityEviction (java.lang.Class<?> activityKlass, String processName) 433 throws Throwable { 434 // Open a camera1 client in the main CTS process's activity 435 final Camera.ErrorCallback mockErrorCb1 = mock(Camera.ErrorCallback.class); 436 final boolean[] skip = {false}; 437 runTestOnUiThread(new Runnable() { 438 @Override 439 public void run() { 440 // Open camera 441 mCamera = Camera.open(); 442 if (mCamera == null) { 443 skip[0] = true; 444 } else { 445 mCamera.setErrorCallback(mockErrorCb1); 446 } 447 notifyFromUI(); 448 } 449 }); 450 waitForUI(); 451 452 if (skip[0]) { 453 Log.i(TAG, "Skipping testCamera1ActivityEviction, device has no cameras."); 454 return; 455 } 456 457 verifyZeroInteractions(mockErrorCb1); 458 459 startRemoteProcess(activityKlass, processName); 460 461 // Make sure camera was setup correctly in remote activity 462 List<ErrorLoggingService.LogEvent> events = null; 463 try { 464 events = mErrorServiceConnection.getLog(SETUP_TIMEOUT, 465 TestConstants.EVENT_CAMERA_CONNECT); 466 } finally { 467 if (events != null) assertOnly(TestConstants.EVENT_CAMERA_CONNECT, events); 468 } 469 470 Thread.sleep(WAIT_TIME); 471 472 // Ensure UI thread has a chance to process callbacks. 473 runTestOnUiThread(new Runnable() { 474 @Override 475 public void run() { 476 Log.i("CTS", "Did something on UI thread."); 477 notifyFromUI(); 478 } 479 }); 480 waitForUI(); 481 482 // Make sure we received correct callback in error listener, and nothing else 483 verify(mockErrorCb1, only()).onError(eq(Camera.CAMERA_ERROR_EVICTED), isA(Camera.class)); 484 mCamera = null; 485 486 // Try to open the camera again (even though other TOP process holds the camera). 487 final boolean[] pass = {false}; 488 runTestOnUiThread(new Runnable() { 489 @Override 490 public void run() { 491 // Open camera 492 try { 493 mCamera = Camera.open(); 494 } catch (RuntimeException e) { 495 pass[0] = true; 496 } 497 notifyFromUI(); 498 } 499 }); 500 waitForUI(); 501 502 assertTrue("Did not receive exception when opening camera while camera is held by a" + 503 " higher priority client process.", pass[0]); 504 505 // Verify that attempting to open the camera didn't cause anything weird to happen in the 506 // other process. 507 List<ErrorLoggingService.LogEvent> eventList2 = null; 508 boolean timeoutExceptionHit = false; 509 try { 510 eventList2 = mErrorServiceConnection.getLog(EVICTION_TIMEOUT); 511 } catch (TimeoutException e) { 512 timeoutExceptionHit = true; 513 } 514 515 assertNone("Remote camera service received invalid events: ", eventList2); 516 assertTrue("Remote camera service exited early", timeoutExceptionHit); 517 android.os.Process.killProcess(mProcessPid); 518 mProcessPid = -1; 519 forceCtsActivityToTop(); 520 } 521 522 /** 523 * Ensure the CTS activity becomes foreground again instead of launcher. 524 */ forceCtsActivityToTop()525 private void forceCtsActivityToTop() throws InterruptedException { 526 Thread.sleep(WAIT_TIME); 527 Activity a = getActivity(); 528 Intent activityIntent = new Intent(a, CameraCtsActivity.class); 529 activityIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 530 a.startActivity(activityIntent); 531 Thread.sleep(WAIT_TIME); 532 } 533 534 /** 535 * Block until UI thread calls {@link #notifyFromUI()}. 536 * @throws InterruptedException 537 */ waitForUI()538 private void waitForUI() throws InterruptedException { 539 synchronized(mLock) { 540 if (mCompleted) return; 541 while (!mCompleted) { 542 mLock.wait(); 543 } 544 mCompleted = false; 545 } 546 } 547 548 /** 549 * Wake up any threads waiting in calls to {@link #waitForUI()}. 550 */ notifyFromUI()551 private void notifyFromUI() { 552 synchronized (mLock) { 553 mCompleted = true; 554 mLock.notifyAll(); 555 } 556 } 557 558 /** 559 * Return the PID for the process with the given name in the given list of process info. 560 * 561 * @param processName the name of the process who's PID to return. 562 * @param list a list of {@link ActivityManager.RunningAppProcessInfo} to check. 563 * @return the PID of the given process, or -1 if it was not included in the list. 564 */ getPid(String processName, List<ActivityManager.RunningAppProcessInfo> list)565 private static int getPid(String processName, 566 List<ActivityManager.RunningAppProcessInfo> list) { 567 for (ActivityManager.RunningAppProcessInfo rai : list) { 568 if (processName.equals(rai.processName)) 569 return rai.pid; 570 } 571 return -1; 572 } 573 574 /** 575 * Start an activity of the given class running in a remote process with the given name. 576 * 577 * @param klass the class of the {@link android.app.Activity} to start. 578 * @param processName the remote activity name. 579 * @throws InterruptedException 580 */ startRemoteProcess(java.lang.Class<?> klass, String processName)581 public void startRemoteProcess(java.lang.Class<?> klass, String processName) 582 throws InterruptedException { 583 // Ensure no running activity process with same name 584 Activity a = getActivity(); 585 String cameraActivityName = a.getPackageName() + ":" + processName; 586 List<ActivityManager.RunningAppProcessInfo> list = 587 mActivityManager.getRunningAppProcesses(); 588 assertEquals(-1, getPid(cameraActivityName, list)); 589 590 // Start activity in a new top foreground process 591 Intent activityIntent = new Intent(a, klass); 592 a.startActivity(activityIntent); 593 Thread.sleep(WAIT_TIME); 594 595 // Fail if activity isn't running 596 list = mActivityManager.getRunningAppProcesses(); 597 mProcessPid = getPid(cameraActivityName, list); 598 assertTrue(-1 != mProcessPid); 599 } 600 601 /** 602 * Assert that there is only one event of the given type in the event list. 603 * 604 * @param event event type to check for. 605 * @param events {@link List} of events. 606 */ assertOnly(int event, List<ErrorLoggingService.LogEvent> events)607 public static void assertOnly(int event, List<ErrorLoggingService.LogEvent> events) { 608 assertTrue("Remote camera activity never received event: " + event, events != null); 609 for (ErrorLoggingService.LogEvent e : events) { 610 assertFalse("Remote camera activity received invalid event (" + e + 611 ") while waiting for event: " + event, 612 e.getEvent() < 0 || e.getEvent() != event); 613 } 614 assertTrue("Remote camera activity never received event: " + event, events.size() >= 1); 615 assertTrue("Remote camera activity received too many " + event + " events, received: " + 616 events.size(), events.size() == 1); 617 } 618 619 /** 620 * Assert there were no logEvents in the given list. 621 * 622 * @param msg message to show on assertion failure. 623 * @param events {@link List} of events. 624 */ assertNone(String msg, List<ErrorLoggingService.LogEvent> events)625 public static void assertNone(String msg, List<ErrorLoggingService.LogEvent> events) { 626 if (events == null) return; 627 StringBuilder builder = new StringBuilder(msg + "\n"); 628 for (ErrorLoggingService.LogEvent e : events) { 629 builder.append(e).append("\n"); 630 } 631 assertTrue(builder.toString(), events.isEmpty()); 632 } 633 634 /** 635 * Assert array is null or empty. 636 * 637 * @param array array to check. 638 */ assertNotEmpty(T[] array)639 public static <T> void assertNotEmpty(T[] array) { 640 assertNotNull(array); 641 assertFalse("Array is empty: " + Arrays.toString(array), array.length == 0); 642 } 643 644 /** 645 * Given an 'actual' array of objects, check that the objects given in the 'expected' 646 * array are also present in the 'actual' array in the same order. Objects in the 'actual' 647 * array that are not in the 'expected' array are skipped and ignored if they are given 648 * in the 'ignored' array, otherwise this assertion will fail. 649 * 650 * @param actual the ordered array of objects to check. 651 * @param expected the ordered array of expected objects. 652 * @param ignored the array of objects that will be ignored if present in actual, 653 * but not in expected (or are out of order). 654 * @param <T> 655 */ assertOrderedEvents(T[] actual, T[] expected, T[] ignored)656 public static <T> void assertOrderedEvents(T[] actual, T[] expected, T[] ignored) { 657 assertNotNull(actual); 658 assertNotNull(expected); 659 assertNotNull(ignored); 660 661 int expIndex = 0; 662 int index = 0; 663 for (T i : actual) { 664 // If explicitly expected, move to next 665 if (expIndex < expected.length && Objects.equals(i, expected[expIndex])) { 666 expIndex++; 667 continue; 668 } 669 670 // Fail if not ignored 671 boolean canIgnore = false; 672 for (T j : ignored) { 673 if (Objects.equals(i, j)) { 674 canIgnore = true; 675 break; 676 } 677 678 } 679 680 // Fail if not ignored. 681 assertTrue("Event at index " + index + " in actual array " + 682 Arrays.toString(actual) + " was unexpected: expected array was " + 683 Arrays.toString(expected) + ", ignored array was: " + 684 Arrays.toString(ignored), canIgnore); 685 index++; 686 } 687 assertTrue("Only had " + expIndex + " of " + expected.length + 688 " expected objects in array " + Arrays.toString(actual) + ", expected was " + 689 Arrays.toString(expected), expIndex == expected.length); 690 } 691 } 692