1/* 2 * Copyright 2016 The WebRTC project authors. All Rights Reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11#import <Foundation/Foundation.h> 12#import <OCMock/OCMock.h> 13 14#include <vector> 15 16#include "rtc_base/gunit.h" 17 18#import "components/audio/RTCAudioSession+Private.h" 19 20#import "components/audio/RTCAudioSession.h" 21#import "components/audio/RTCAudioSessionConfiguration.h" 22 23@interface RTCAudioSession (UnitTesting) 24 25@property(nonatomic, readonly) std::vector<__weak id<RTCAudioSessionDelegate> > delegates; 26 27- (instancetype)initWithAudioSession:(id)audioSession; 28 29@end 30 31@interface MockAVAudioSession : NSObject 32 33@property (nonatomic, readwrite, assign) float outputVolume; 34 35@end 36 37@implementation MockAVAudioSession 38@synthesize outputVolume = _outputVolume; 39@end 40 41@interface RTCAudioSessionTestDelegate : NSObject <RTCAudioSessionDelegate> 42 43@property (nonatomic, readonly) float outputVolume; 44 45@end 46 47@implementation RTCAudioSessionTestDelegate 48 49@synthesize outputVolume = _outputVolume; 50 51- (instancetype)init { 52 if (self = [super init]) { 53 _outputVolume = -1; 54 } 55 return self; 56} 57 58- (void)audioSessionDidBeginInterruption:(RTCAudioSession *)session { 59} 60 61- (void)audioSessionDidEndInterruption:(RTCAudioSession *)session 62 shouldResumeSession:(BOOL)shouldResumeSession { 63} 64 65- (void)audioSessionDidChangeRoute:(RTCAudioSession *)session 66 reason:(AVAudioSessionRouteChangeReason)reason 67 previousRoute:(AVAudioSessionRouteDescription *)previousRoute { 68} 69 70- (void)audioSessionMediaServerTerminated:(RTCAudioSession *)session { 71} 72 73- (void)audioSessionMediaServerReset:(RTCAudioSession *)session { 74} 75 76- (void)audioSessionShouldConfigure:(RTCAudioSession *)session { 77} 78 79- (void)audioSessionShouldUnconfigure:(RTCAudioSession *)session { 80} 81 82- (void)audioSession:(RTCAudioSession *)audioSession 83 didChangeOutputVolume:(float)outputVolume { 84 _outputVolume = outputVolume; 85} 86 87@end 88 89// A delegate that adds itself to the audio session on init and removes itself 90// in its dealloc. 91@interface RTCTestRemoveOnDeallocDelegate : RTCAudioSessionTestDelegate 92@end 93 94@implementation RTCTestRemoveOnDeallocDelegate 95 96- (instancetype)init { 97 if (self = [super init]) { 98 RTCAudioSession *session = [RTCAudioSession sharedInstance]; 99 [session addDelegate:self]; 100 } 101 return self; 102} 103 104- (void)dealloc { 105 RTCAudioSession *session = [RTCAudioSession sharedInstance]; 106 [session removeDelegate:self]; 107} 108 109@end 110 111 112@interface RTCAudioSessionTest : NSObject 113 114- (void)testLockForConfiguration; 115 116@end 117 118@implementation RTCAudioSessionTest 119 120- (void)testLockForConfiguration { 121 RTCAudioSession *session = [RTCAudioSession sharedInstance]; 122 123 for (size_t i = 0; i < 2; i++) { 124 [session lockForConfiguration]; 125 EXPECT_TRUE(session.isLocked); 126 } 127 for (size_t i = 0; i < 2; i++) { 128 EXPECT_TRUE(session.isLocked); 129 [session unlockForConfiguration]; 130 } 131 EXPECT_FALSE(session.isLocked); 132} 133 134- (void)testAddAndRemoveDelegates { 135 RTCAudioSession *session = [RTCAudioSession sharedInstance]; 136 NSMutableArray *delegates = [NSMutableArray array]; 137 const size_t count = 5; 138 for (size_t i = 0; i < count; ++i) { 139 RTCAudioSessionTestDelegate *delegate = 140 [[RTCAudioSessionTestDelegate alloc] init]; 141 [session addDelegate:delegate]; 142 [delegates addObject:delegate]; 143 EXPECT_EQ(i + 1, session.delegates.size()); 144 } 145 [delegates enumerateObjectsUsingBlock:^(RTCAudioSessionTestDelegate *obj, 146 NSUInteger idx, 147 BOOL *stop) { 148 [session removeDelegate:obj]; 149 }]; 150 EXPECT_EQ(0u, session.delegates.size()); 151} 152 153- (void)testPushDelegate { 154 RTCAudioSession *session = [RTCAudioSession sharedInstance]; 155 NSMutableArray *delegates = [NSMutableArray array]; 156 const size_t count = 2; 157 for (size_t i = 0; i < count; ++i) { 158 RTCAudioSessionTestDelegate *delegate = 159 [[RTCAudioSessionTestDelegate alloc] init]; 160 [session addDelegate:delegate]; 161 [delegates addObject:delegate]; 162 } 163 // Test that it gets added to the front of the list. 164 RTCAudioSessionTestDelegate *pushedDelegate = 165 [[RTCAudioSessionTestDelegate alloc] init]; 166 [session pushDelegate:pushedDelegate]; 167 EXPECT_TRUE(pushedDelegate == session.delegates[0]); 168 169 // Test that it stays at the front of the list. 170 for (size_t i = 0; i < count; ++i) { 171 RTCAudioSessionTestDelegate *delegate = 172 [[RTCAudioSessionTestDelegate alloc] init]; 173 [session addDelegate:delegate]; 174 [delegates addObject:delegate]; 175 } 176 EXPECT_TRUE(pushedDelegate == session.delegates[0]); 177 178 // Test that the next one goes to the front too. 179 pushedDelegate = [[RTCAudioSessionTestDelegate alloc] init]; 180 [session pushDelegate:pushedDelegate]; 181 EXPECT_TRUE(pushedDelegate == session.delegates[0]); 182} 183 184// Tests that delegates added to the audio session properly zero out. This is 185// checking an implementation detail (that vectors of __weak work as expected). 186- (void)testZeroingWeakDelegate { 187 RTCAudioSession *session = [RTCAudioSession sharedInstance]; 188 @autoreleasepool { 189 // Add a delegate to the session. There should be one delegate at this 190 // point. 191 RTCAudioSessionTestDelegate *delegate = 192 [[RTCAudioSessionTestDelegate alloc] init]; 193 [session addDelegate:delegate]; 194 EXPECT_EQ(1u, session.delegates.size()); 195 EXPECT_TRUE(session.delegates[0]); 196 } 197 // The previously created delegate should've de-alloced, leaving a nil ptr. 198 EXPECT_FALSE(session.delegates[0]); 199 RTCAudioSessionTestDelegate *delegate = 200 [[RTCAudioSessionTestDelegate alloc] init]; 201 [session addDelegate:delegate]; 202 // On adding a new delegate, nil ptrs should've been cleared. 203 EXPECT_EQ(1u, session.delegates.size()); 204 EXPECT_TRUE(session.delegates[0]); 205} 206 207// Tests that we don't crash when removing delegates in dealloc. 208// Added as a regression test. 209- (void)testRemoveDelegateOnDealloc { 210 @autoreleasepool { 211 RTCTestRemoveOnDeallocDelegate *delegate = 212 [[RTCTestRemoveOnDeallocDelegate alloc] init]; 213 EXPECT_TRUE(delegate); 214 } 215 RTCAudioSession *session = [RTCAudioSession sharedInstance]; 216 EXPECT_EQ(0u, session.delegates.size()); 217} 218 219- (void)testAudioSessionActivation { 220 RTCAudioSession *audioSession = [RTCAudioSession sharedInstance]; 221 EXPECT_EQ(0, audioSession.activationCount); 222 [audioSession audioSessionDidActivate:[AVAudioSession sharedInstance]]; 223 EXPECT_EQ(1, audioSession.activationCount); 224 [audioSession audioSessionDidDeactivate:[AVAudioSession sharedInstance]]; 225 EXPECT_EQ(0, audioSession.activationCount); 226} 227 228// Hack - fixes OCMVerify link error 229// Link error is: Undefined symbols for architecture i386: 230// "OCMMakeLocation(objc_object*, char const*, int)", referenced from: 231// -[RTCAudioSessionTest testConfigureWebRTCSession] in RTCAudioSessionTest.o 232// ld: symbol(s) not found for architecture i386 233// REASON: https://github.com/erikdoe/ocmock/issues/238 234OCMLocation *OCMMakeLocation(id testCase, const char *fileCString, int line){ 235 return [OCMLocation locationWithTestCase:testCase 236 file:[NSString stringWithUTF8String:fileCString] 237 line:line]; 238} 239 240- (void)testConfigureWebRTCSession { 241 NSError *error = nil; 242 243 void (^setActiveBlock)(NSInvocation *invocation) = ^(NSInvocation *invocation) { 244 __autoreleasing NSError **retError; 245 [invocation getArgument:&retError atIndex:4]; 246 *retError = [NSError errorWithDomain:@"AVAudioSession" 247 code:AVAudioSessionErrorInsufficientPriority 248 userInfo:nil]; 249 BOOL failure = NO; 250 [invocation setReturnValue:&failure]; 251 }; 252 253 id mockAVAudioSession = OCMPartialMock([AVAudioSession sharedInstance]); 254 OCMStub([[mockAVAudioSession ignoringNonObjectArgs] 255 setActive:YES withOptions:0 error:((NSError __autoreleasing **)[OCMArg anyPointer])]). 256 andDo(setActiveBlock); 257 258 id mockAudioSession = OCMPartialMock([RTCAudioSession sharedInstance]); 259 OCMStub([mockAudioSession session]).andReturn(mockAVAudioSession); 260 261 RTCAudioSession *audioSession = mockAudioSession; 262 EXPECT_EQ(0, audioSession.activationCount); 263 [audioSession lockForConfiguration]; 264 EXPECT_TRUE([audioSession checkLock:nil]); 265 // configureWebRTCSession is forced to fail in the above mock interface, 266 // so activationCount should remain 0 267 OCMExpect([[mockAVAudioSession ignoringNonObjectArgs] 268 setActive:YES withOptions:0 error:((NSError __autoreleasing **)[OCMArg anyPointer])]). 269 andDo(setActiveBlock); 270 OCMExpect([mockAudioSession session]).andReturn(mockAVAudioSession); 271 EXPECT_FALSE([audioSession configureWebRTCSession:&error]); 272 EXPECT_EQ(0, audioSession.activationCount); 273 274 id session = audioSession.session; 275 EXPECT_EQ(session, mockAVAudioSession); 276 EXPECT_EQ(NO, [mockAVAudioSession setActive:YES withOptions:0 error:&error]); 277 [audioSession unlockForConfiguration]; 278 279 OCMVerify([mockAudioSession session]); 280 OCMVerify([[mockAVAudioSession ignoringNonObjectArgs] setActive:YES withOptions:0 error:&error]); 281 OCMVerify([[mockAVAudioSession ignoringNonObjectArgs] setActive:NO withOptions:0 error:&error]); 282 283 [mockAVAudioSession stopMocking]; 284 [mockAudioSession stopMocking]; 285} 286 287- (void)testAudioVolumeDidNotify { 288 MockAVAudioSession *mockAVAudioSession = [[MockAVAudioSession alloc] init]; 289 RTCAudioSession *session = [[RTCAudioSession alloc] initWithAudioSession:mockAVAudioSession]; 290 RTCAudioSessionTestDelegate *delegate = 291 [[RTCAudioSessionTestDelegate alloc] init]; 292 [session addDelegate:delegate]; 293 294 float expectedVolume = 0.75; 295 mockAVAudioSession.outputVolume = expectedVolume; 296 297 EXPECT_EQ(expectedVolume, delegate.outputVolume); 298} 299 300@end 301 302namespace webrtc { 303 304class AudioSessionTest : public ::testing::Test { 305 protected: 306 void TearDown() override { 307 RTCAudioSession *session = [RTCAudioSession sharedInstance]; 308 for (id<RTCAudioSessionDelegate> delegate : session.delegates) { 309 [session removeDelegate:delegate]; 310 } 311 } 312}; 313 314TEST_F(AudioSessionTest, LockForConfiguration) { 315 RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init]; 316 [test testLockForConfiguration]; 317} 318 319TEST_F(AudioSessionTest, AddAndRemoveDelegates) { 320 RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init]; 321 [test testAddAndRemoveDelegates]; 322} 323 324TEST_F(AudioSessionTest, PushDelegate) { 325 RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init]; 326 [test testPushDelegate]; 327} 328 329TEST_F(AudioSessionTest, ZeroingWeakDelegate) { 330 RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init]; 331 [test testZeroingWeakDelegate]; 332} 333 334TEST_F(AudioSessionTest, RemoveDelegateOnDealloc) { 335 RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init]; 336 [test testRemoveDelegateOnDealloc]; 337} 338 339TEST_F(AudioSessionTest, AudioSessionActivation) { 340 RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init]; 341 [test testAudioSessionActivation]; 342} 343 344TEST_F(AudioSessionTest, ConfigureWebRTCSession) { 345 RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init]; 346 [test testConfigureWebRTCSession]; 347} 348 349TEST_F(AudioSessionTest, AudioVolumeDidNotify) { 350 RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init]; 351 [test testAudioVolumeDidNotify]; 352} 353 354} // namespace webrtc 355