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