1 /* 2 * Copyright (C) 2020 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 com.android.media.audiotestharness.server.service; 18 19 import static org.mockito.ArgumentMatchers.any; 20 import static org.mockito.ArgumentMatchers.eq; 21 import static org.mockito.Mockito.doAnswer; 22 import static org.mockito.Mockito.doThrow; 23 import static org.mockito.Mockito.reset; 24 import static org.mockito.Mockito.verify; 25 import static org.mockito.Mockito.when; 26 27 import com.android.media.audiotestharness.proto.AudioTestHarnessGrpc; 28 import com.android.media.audiotestharness.proto.AudioTestHarnessService; 29 import com.android.media.audiotestharness.server.config.SharedHostConfiguration; 30 import com.android.media.audiotestharness.server.core.AudioCapturer; 31 import com.android.media.audiotestharness.server.core.AudioSystemService; 32 33 import com.google.protobuf.ByteString; 34 35 import io.grpc.Context; 36 import io.grpc.ManagedChannel; 37 import io.grpc.Status; 38 import io.grpc.StatusRuntimeException; 39 import io.grpc.inprocess.InProcessChannelBuilder; 40 import io.grpc.inprocess.InProcessServerBuilder; 41 import io.grpc.stub.ServerCallStreamObserver; 42 import io.grpc.stub.StreamObserver; 43 import io.grpc.testing.GrpcCleanupRule; 44 45 import org.hamcrest.CustomMatcher; 46 import org.hamcrest.Matcher; 47 import org.junit.Before; 48 import org.junit.Rule; 49 import org.junit.Test; 50 import org.junit.rules.ExpectedException; 51 import org.junit.runner.RunWith; 52 import org.junit.runners.JUnit4; 53 import org.mockito.Mock; 54 import org.mockito.junit.MockitoJUnit; 55 import org.mockito.junit.MockitoRule; 56 57 import java.io.IOException; 58 import java.util.concurrent.atomic.AtomicReference; 59 60 /** Tests for the {@link AudioTestHarnessImpl} class. */ 61 @RunWith(JUnit4.class) 62 public class AudioTestHarnessImplTests { 63 64 public static final byte[] TEST_PAYLOAD = {0x1, 0x2, 0x3, 0x4}; 65 66 @Rule public GrpcCleanupRule mGrpcCleanupRule = new GrpcCleanupRule(); 67 68 @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); 69 70 @Rule public ExpectedException mExceptionRule = ExpectedException.none(); 71 72 @Mock AudioSystemService mAudioSystemService; 73 74 @Mock AudioCapturer mAudioCapturer; 75 76 @Mock AudioCaptureSession mAudioCaptureSession; 77 78 @Mock AudioCaptureSessionFactory mAudioCaptureSessionFactory; 79 80 /** Stubs used to communicate with the service-under-test. */ 81 private AudioTestHarnessGrpc.AudioTestHarnessBlockingStub mBlockingStub; 82 83 private AudioTestHarnessGrpc.AudioTestHarnessStub mStub; 84 85 @Before setUp()86 public void setUp() throws Exception { 87 String serverName = InProcessServerBuilder.generateName(); 88 89 // Create and Start In-Process Server 90 mGrpcCleanupRule.register( 91 InProcessServerBuilder.forName(serverName) 92 .directExecutor() 93 .addService( 94 new AudioTestHarnessImpl( 95 mAudioSystemService, 96 mAudioCaptureSessionFactory, 97 SharedHostConfiguration.getDefault())) 98 .build() 99 .start()); 100 101 // Create and Start Stubs for interacting with the Service 102 ManagedChannel channel = 103 mGrpcCleanupRule.register( 104 InProcessChannelBuilder.forName(serverName).directExecutor().build()); 105 mBlockingStub = AudioTestHarnessGrpc.newBlockingStub(channel); 106 mStub = AudioTestHarnessGrpc.newStub(channel); 107 108 // Ensure the mocks output is valid. 109 when(mAudioSystemService.createWithDefaultAudioFormat(any())).thenReturn(mAudioCapturer); 110 when(mAudioCaptureSessionFactory.createCaptureSession(any(), any())) 111 .then( 112 (inv) -> { 113 114 // Ensure that the stream observer is closed properly so it can be 115 // cleaned up. 116 ServerCallStreamObserver<AudioTestHarnessService.CaptureChunk> 117 streamObserver = inv.getArgument(0); 118 streamObserver.onCompleted(); 119 120 return mAudioCaptureSession; 121 }); 122 } 123 124 @Test capture_properlyAllocatesDefaultCapturer()125 public void capture_properlyAllocatesDefaultCapturer() throws Exception { 126 mBlockingStub.capture(AudioTestHarnessService.CaptureRequest.getDefaultInstance()); 127 verify(mAudioSystemService) 128 .createWithDefaultAudioFormat( 129 SharedHostConfiguration.getDefault().captureDevices().get(0)); 130 } 131 132 @Test capture_properlyCreatesCaptureSession()133 public void capture_properlyCreatesCaptureSession() throws Exception { 134 mBlockingStub.capture(AudioTestHarnessService.CaptureRequest.getDefaultInstance()); 135 verify(mAudioCaptureSessionFactory).createCaptureSession(any(), eq(mAudioCapturer)); 136 } 137 138 @Test capture_callsStopOnSessionWhenCanceled()139 public void capture_callsStopOnSessionWhenCanceled() throws Exception { 140 AtomicReference<StreamObserver<AudioTestHarnessService.CaptureChunk>> 141 streamObserverReference = new AtomicReference<>(); 142 reset(mAudioCaptureSessionFactory); 143 when(mAudioCaptureSessionFactory.createCaptureSession(any(), any())) 144 .thenAnswer( 145 (invocation -> { 146 // Grab a reference to the stream observer, then return the mock. 147 streamObserverReference.set(invocation.getArgument(0)); 148 return mAudioCaptureSession; 149 })); 150 151 doAnswer( 152 (invocation) -> { 153 if (streamObserverReference.get() != null) { 154 streamObserverReference 155 .get() 156 .onNext( 157 AudioTestHarnessService.CaptureChunk.newBuilder() 158 .setData(ByteString.copyFrom(TEST_PAYLOAD)) 159 .build()); 160 } 161 return null; 162 }) 163 .when(mAudioCaptureSession) 164 .start(); 165 166 Context.CancellableContext cancellableContext = Context.current().withCancellation(); 167 cancellableContext.run( 168 () -> 169 mStub.capture( 170 AudioTestHarnessService.CaptureRequest.getDefaultInstance(), 171 new StreamObserver<AudioTestHarnessService.CaptureChunk>() { 172 @Override 173 public void onNext(AudioTestHarnessService.CaptureChunk value) { 174 cancellableContext.cancel(Status.CANCELLED.asException()); 175 } 176 177 @Override 178 public void onError(Throwable t) {} 179 180 @Override 181 public void onCompleted() {} 182 })); 183 184 verify(mAudioCaptureSession).stop(); 185 } 186 187 @Test capture_properlyStartsCaptureSession()188 public void capture_properlyStartsCaptureSession() throws Exception { 189 mBlockingStub.capture(AudioTestHarnessService.CaptureRequest.getDefaultInstance()); 190 verify(mAudioCaptureSession).start(); 191 } 192 193 @Test capture_throwsProperStatusException_failureToOpenCapturer()194 public void capture_throwsProperStatusException_failureToOpenCapturer() throws Exception { 195 when(mAudioSystemService.createWithDefaultAudioFormat(any())) 196 .thenThrow(new IOException("Some exception occurred.")); 197 198 mExceptionRule.expect( 199 generateCustomMatcherForExpected( 200 /* expectedDescription= */ String.format( 201 "Failed to allocate AudioCapturer %s", 202 SharedHostConfiguration.getDefault().captureDevices().get(0)), 203 Status.UNAVAILABLE)); 204 mBlockingStub 205 .capture(AudioTestHarnessService.CaptureRequest.getDefaultInstance()) 206 .forEachRemaining(chunk -> {}); 207 } 208 209 @Test capture_throwsProperStatusException_failureToStartCapturer()210 public void capture_throwsProperStatusException_failureToStartCapturer() throws Exception { 211 reset(mAudioCaptureSessionFactory); 212 when(mAudioCaptureSessionFactory.createCaptureSession(any(), any())) 213 .thenReturn(mAudioCaptureSession); 214 doThrow(new IOException("Capturer Start Failure!")).when(mAudioCaptureSession).start(); 215 216 mExceptionRule.expect( 217 generateCustomMatcherForExpected( 218 /* expectedDescription= */ "Capturer Start Failure!", Status.INTERNAL)); 219 220 mBlockingStub 221 .capture(AudioTestHarnessService.CaptureRequest.getDefaultInstance()) 222 .forEachRemaining(chunk -> {}); 223 } 224 225 /** 226 * Generates a {@link org.hamcrest.Matcher} that matches a given {@link StatusRuntimeException} 227 * if the description and status code parameters are an exact match. 228 */ generateCustomMatcherForExpected( String expectedDescription, Status expectedStatus)229 public Matcher<StatusRuntimeException> generateCustomMatcherForExpected( 230 String expectedDescription, Status expectedStatus) { 231 return new CustomMatcher<StatusRuntimeException>( 232 String.format( 233 "StatusRuntimeException with Message (%s) and Status (%s)", 234 expectedDescription, expectedStatus)) { 235 @Override 236 public boolean matches(Object item) { 237 if (item instanceof StatusRuntimeException) { 238 StatusRuntimeException exception = (StatusRuntimeException) item; 239 return exception.getStatus().getCode().equals(expectedStatus.getCode()) 240 && exception.getStatus().getDescription() != null 241 && exception.getStatus().getDescription().equals(expectedDescription); 242 } 243 return false; 244 } 245 }; 246 } 247 } 248