summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicoleYarroch <nicole@livio.io>2019-01-31 13:52:01 -0500
committerNicoleYarroch <nicole@livio.io>2019-01-31 13:52:01 -0500
commitca292eadc2df8ef7cf7159c956f121782173f1c2 (patch)
treed09096292e1f60958ee07d831453df06aff5ab14
parent38ff9ec0442c7ce6269a55544d37c8cfa4236f6b (diff)
downloadsdl_ios-bugfix/issue_1079_switching_transports_fails.tar.gz
* Added test cases for iAPTransport classesbugfix/issue_1079_switching_transports_fails
* Added test cases for iAPTransport and iAPSession classes * Fixed a lot of documentation spelling and grammar.
-rw-r--r--SmartDeviceLink-iOS.xcodeproj/project.pbxproj30
-rw-r--r--SmartDeviceLink/SDLIAPSession.m46
-rw-r--r--SmartDeviceLink/SDLIAPTransport.h4
-rw-r--r--SmartDeviceLink/SDLIAPTransport.m35
-rw-r--r--SmartDeviceLinkTests/TransportSpecs/TCP/SDLTCPTransportSpec.m (renamed from SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m)0
-rw-r--r--SmartDeviceLinkTests/TransportSpecs/iAP/EAAccessory+OCMock.m62
-rw-r--r--SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPSessionSpec.m165
-rw-r--r--SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPTransportSpec.m118
8 files changed, 430 insertions, 30 deletions
diff --git a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj
index 7937c15bf..e87b031e9 100644
--- a/SmartDeviceLink-iOS.xcodeproj/project.pbxproj
+++ b/SmartDeviceLink-iOS.xcodeproj/project.pbxproj
@@ -1256,6 +1256,9 @@
88B848C31F45E1A600DED768 /* TestResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 88B848C21F45E1A600DED768 /* TestResponse.m */; };
88B848C91F462E3600DED768 /* TestFileProgressResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 88B848C81F462E3600DED768 /* TestFileProgressResponse.m */; };
88D2AAE41F682BB20078D5B2 /* SDLLogConstantsSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 88D2AAE31F682BB20078D5B2 /* SDLLogConstantsSpec.m */; };
+ 88DF998D22035CC600477AC1 /* EAAccessory+OCMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 88DF998C22035CC600477AC1 /* EAAccessory+OCMock.m */; };
+ 88DF998F22035D1700477AC1 /* SDLIAPSessionSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 88DF998E22035D1700477AC1 /* SDLIAPSessionSpec.m */; };
+ 88DF999122035D5A00477AC1 /* SDLIAPTransportSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 88DF999022035D5A00477AC1 /* SDLIAPTransportSpec.m */; };
88EED8381F33AE1700E6C42E /* SDLHapticRect.h in Headers */ = {isa = PBXBuildFile; fileRef = 88EED8361F33AE1700E6C42E /* SDLHapticRect.h */; settings = {ATTRIBUTES = (Public, ); }; };
88EED8391F33AE1700E6C42E /* SDLHapticRect.m in Sources */ = {isa = PBXBuildFile; fileRef = 88EED8371F33AE1700E6C42E /* SDLHapticRect.m */; };
88EED83B1F33BECB00E6C42E /* SDLHapticRectSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 88EED83A1F33BECB00E6C42E /* SDLHapticRectSpec.m */; };
@@ -2757,6 +2760,9 @@
88B848C71F462E3600DED768 /* TestFileProgressResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestFileProgressResponse.h; sourceTree = "<group>"; };
88B848C81F462E3600DED768 /* TestFileProgressResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestFileProgressResponse.m; sourceTree = "<group>"; };
88D2AAE31F682BB20078D5B2 /* SDLLogConstantsSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLLogConstantsSpec.m; sourceTree = "<group>"; };
+ 88DF998C22035CC600477AC1 /* EAAccessory+OCMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "EAAccessory+OCMock.m"; sourceTree = "<group>"; };
+ 88DF998E22035D1700477AC1 /* SDLIAPSessionSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDLIAPSessionSpec.m; sourceTree = "<group>"; };
+ 88DF999022035D5A00477AC1 /* SDLIAPTransportSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDLIAPTransportSpec.m; sourceTree = "<group>"; };
88EED8361F33AE1700E6C42E /* SDLHapticRect.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDLHapticRect.h; sourceTree = "<group>"; };
88EED8371F33AE1700E6C42E /* SDLHapticRect.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLHapticRect.m; sourceTree = "<group>"; };
88EED83A1F33BECB00E6C42E /* SDLHapticRectSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDLHapticRectSpec.m; sourceTree = "<group>"; };
@@ -5506,6 +5512,24 @@
name = Helpers;
sourceTree = "<group>";
};
+ 88DF998A22035CA400477AC1 /* TCP */ = {
+ isa = PBXGroup;
+ children = (
+ EE5D1B32208EBCA900D17216 /* SDLTCPTransportSpec.m */,
+ );
+ path = TCP;
+ sourceTree = "<group>";
+ };
+ 88DF998B22035CB100477AC1 /* iAP */ = {
+ isa = PBXGroup;
+ children = (
+ 88DF998C22035CC600477AC1 /* EAAccessory+OCMock.m */,
+ 88DF998E22035D1700477AC1 /* SDLIAPSessionSpec.m */,
+ 88DF999022035D5A00477AC1 /* SDLIAPTransportSpec.m */,
+ );
+ path = iAP;
+ sourceTree = "<group>";
+ };
DA1166D71D14601C00438CEA /* Touches */ = {
isa = PBXGroup;
children = (
@@ -5618,7 +5642,8 @@
EE5D1B31208EBC7100D17216 /* TransportSpecs */ = {
isa = PBXGroup;
children = (
- EE5D1B32208EBCA900D17216 /* SDLTCPTransportSpec.m */,
+ 88DF998B22035CB100477AC1 /* iAP */,
+ 88DF998A22035CA400477AC1 /* TCP */,
);
path = TransportSpecs;
sourceTree = "<group>";
@@ -6817,6 +6842,7 @@
5DC09EDA1F2F7FEC00F4AB1D /* SDLControlFramePayloadNakSpec.m in Sources */,
1EB59CCE202DC97900343A61 /* SDLMassageCushionSpec.m in Sources */,
162E83041A9BDE8B00906325 /* SDLUpdateModeSpec.m in Sources */,
+ 88DF998D22035CC600477AC1 /* EAAccessory+OCMock.m in Sources */,
162E83801A9BDE8B00906325 /* SDLHMIPermissionsSpec.m in Sources */,
1EAA476C2036A52F000FE74B /* SDLLightCapabilitiesSpec.m in Sources */,
5D1654561D3E754F00554D93 /* SDLLifecycleManagerSpec.m in Sources */,
@@ -6824,6 +6850,7 @@
162E83021A9BDE8B00906325 /* SDLTouchTypeSpec.m in Sources */,
1EAA47722036AEF5000FE74B /* SDLLightNameSpec.m in Sources */,
5DB92D2F1AC59F0000C15BB0 /* SDLObjectWithPrioritySpec.m in Sources */,
+ 88DF999122035D5A00477AC1 /* SDLIAPTransportSpec.m in Sources */,
162E838A1A9BDE8B00906325 /* SDLSingleTireStatusSpec.m in Sources */,
5D6EB4CC1BF28DC600693731 /* NSMapTable+SubscriptingSpec.m in Sources */,
162E83051A9BDE8B00906325 /* SDLVehicleDataActiveStatusSpec.m in Sources */,
@@ -7005,6 +7032,7 @@
162E83141A9BDE8B00906325 /* SDLOnDriverDistractionSpec.m in Sources */,
162E83371A9BDE8B00906325 /* SDLResetGlobalPropertiesSpec.m in Sources */,
162E82DF1A9BDE8B00906325 /* SDLGlobalProperySpec.m in Sources */,
+ 88DF998F22035D1700477AC1 /* SDLIAPSessionSpec.m in Sources */,
5DD8406520FCE21A0082CE04 /* SDLElectronicParkBrakeStatusSpec.m in Sources */,
162E82F61A9BDE8B00906325 /* SDLRequestTypeSpec.m in Sources */,
5DE35E4520CAFC5D0034BE5A /* SDLChoiceCellSpec.m in Sources */,
diff --git a/SmartDeviceLink/SDLIAPSession.m b/SmartDeviceLink/SDLIAPSession.m
index 599358557..6fc39be9a 100644
--- a/SmartDeviceLink/SDLIAPSession.m
+++ b/SmartDeviceLink/SDLIAPSession.m
@@ -49,15 +49,19 @@ NSTimeInterval const StreamThreadWaitSecs = 10.0;
#pragma mark - Public Stream Lifecycle
- (BOOL)start {
- __weak typeof(self) weakSelf = self;
SDLLogD(@"Opening EASession withAccessory:%@ forProtocol:%@", _accessory.name, _protocol);
+ self.easession = [[EASession alloc] initWithAccessory:self.accessory forProtocol:self.protocol];
+ return [self sdl_startWithSession:self.easession];
+}
- // TODO: This assignment should be broken out of the if and the if / else should be flipped.
- if ((self.easession = [[EASession alloc] initWithAccessory:self.accessory forProtocol:self.protocol])) {
+- (BOOL)sdl_startWithSession:(EASession *)session {
+ __weak typeof(self) weakSelf = self;
+ if (session == nil) {
+ SDLLogE(@"Error creating the session object");
+ return NO;
+ } else {
+ SDLLogD(@"Created the session object successfully");
__strong typeof(self) strongSelf = weakSelf;
-
- SDLLogD(@"Created Session Object");
-
strongSelf.streamDelegate.streamErrorHandler = [self streamErroredHandler];
strongSelf.streamDelegate.streamOpenHandler = [self streamOpenedHandler];
if (self.isDataSession) {
@@ -72,10 +76,6 @@ NSTimeInterval const StreamThreadWaitSecs = 10.0;
[self startStream:self.easession.inputStream];
}
return YES;
-
- } else {
- SDLLogE(@"Error: Could Not Create Session Object");
- return NO;
}
}
@@ -94,14 +94,13 @@ NSTimeInterval const StreamThreadWaitSecs = 10.0;
if (self.isDataSession) {
[self.ioStreamThread cancel];
- long lWait = dispatch_semaphore_wait(self.canceledSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(StreamThreadWaitSecs * NSEC_PER_SEC)));
- if (lWait == 0) {
- SDLLogD(@"Stream thread cancelled successfully");
- } else {
- SDLLogE(@"Failed to cancel stream thread");
- }
- self.ioStreamThread = nil;
- self.isDataSession = NO;
+ [self sdl_isIOThreadCanceled:self.canceledSemaphore completionHandler:^(BOOL success) {
+ if (success == NO) {
+ SDLLogE(@"About to destroy a thread that has not yet closed.");
+ }
+ self.ioStreamThread = nil;
+ self.isDataSession = NO;
+ }];
} else {
// Stop control session
[self stopStream:self.easession.outputStream];
@@ -110,6 +109,17 @@ NSTimeInterval const StreamThreadWaitSecs = 10.0;
self.easession = nil;
}
+- (void)sdl_isIOThreadCanceled:(dispatch_semaphore_t)canceledSemaphore completionHandler:(void (^)(BOOL success))completionHandler {
+ long lWait = dispatch_semaphore_wait(canceledSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(StreamThreadWaitSecs * NSEC_PER_SEC)));
+ if (lWait == 0) {
+ SDLLogD(@"Stream thread canceled successfully");
+ return completionHandler(YES);
+ } else {
+ SDLLogE(@"Failed to cancel stream thread");
+ return completionHandler(NO);
+ }
+}
+
- (BOOL)isStopped {
return !self.isOutputStreamOpen && !self.isInputStreamOpen;
}
diff --git a/SmartDeviceLink/SDLIAPTransport.h b/SmartDeviceLink/SDLIAPTransport.h
index ec32ae1f1..6934ffc27 100644
--- a/SmartDeviceLink/SDLIAPTransport.h
+++ b/SmartDeviceLink/SDLIAPTransport.h
@@ -11,12 +11,12 @@ NS_ASSUME_NONNULL_BEGIN
@interface SDLIAPTransport : NSObject <SDLTransportType, SDLIAPSessionDelegate>
/**
- * Session for transporting data between the app and Core.
+ * Session for establishing a connection with Core. Once the connection has been established, the session is closed and a session is established. A `controlSession` is not needed if the head unit supports the multisession protocol string.
*/
@property (nullable, strong, nonatomic) SDLIAPSession *controlSession;
/**
- * Session for establishing a connection with Core. Once the connection has been established, the session is closed and a control session is established.
+ * Session for transporting data between the app and Core.
*/
@property (nullable, strong, nonatomic) SDLIAPSession *session;
diff --git a/SmartDeviceLink/SDLIAPTransport.m b/SmartDeviceLink/SDLIAPTransport.m
index 9e36754cd..b0fa07177 100644
--- a/SmartDeviceLink/SDLIAPTransport.m
+++ b/SmartDeviceLink/SDLIAPTransport.m
@@ -68,14 +68,16 @@ int const ProtocolIndexTimeoutSeconds = 10;
*/
- (void)sdl_backgroundTaskStart {
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
+ SDLLogV(@"A background task is already running. No need to start a background task. Returning...");
return;
}
- SDLLogD(@"Starting background task");
self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithName:BackgroundTaskName expirationHandler:^{
SDLLogD(@"Background task expired");
[self sdl_backgroundTaskEnd];
}];
+
+ SDLLogD(@"Started a background task with id: %lu", (unsigned long)self.backgroundTaskId);
}
/**
@@ -83,10 +85,11 @@ int const ProtocolIndexTimeoutSeconds = 10;
*/
- (void)sdl_backgroundTaskEnd {
if (self.backgroundTaskId == UIBackgroundTaskInvalid) {
+ SDLLogV(@"No background task running. No need to stop the background task. Returning...");
return;
}
- SDLLogD(@"Ending background task");
+ SDLLogD(@"Ending background task with id: %lu", (unsigned long)self.backgroundTaskId);
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
@@ -142,13 +145,12 @@ int const ProtocolIndexTimeoutSeconds = 10;
- (void)sdl_accessoryConnected:(NSNotification *)notification {
EAAccessory *newAccessory = [notification.userInfo objectForKey:EAAccessoryKey];
- if ((self.session != nil) && (self.session.accessory.connectionID != newAccessory.connectionID)) {
- SDLLogD(@"Switching transports from Bluetooth to USB. Waiting for disconnect notification.");
+ if ([self sdl_isSessionActive:self.session newAccessory:newAccessory]) {
self.accessoryConnectDuringActiveSession = YES;
return;
}
- double retryDelay = self.retryDelay;
+ double retryDelay = self.sdl_retryDelay;
SDLLogD(@"Accessory Connected (%@), Opening in %0.03fs", notification.userInfo[EAAccessoryKey], retryDelay);
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) {
@@ -161,6 +163,22 @@ int const ProtocolIndexTimeoutSeconds = 10;
}
/**
+ * Checks if the newly connected accessory connected while a data session is already opened. This can happen when a session is established over bluetooth and then the user connects to the same head unit with a USB cord.
+ *
+ * @param session The current data session, which may be nil
+ * @param newAccessory The newly connected accessory
+ * @return True if the accessory connected while a data session is already in progress; false if not
+ */
+- (BOOL)sdl_isSessionActive:(SDLIAPSession *)session newAccessory:(EAAccessory *)newAccessory {
+ if ((session != nil) && (session.accessory.connectionID != newAccessory.connectionID)) {
+ SDLLogD(@"Switching transports from Bluetooth to USB. Waiting for disconnect notification.");
+ return YES;
+ }
+
+ return NO;
+}
+
+/**
* Handles a notification sent by the system when an accessory has been disconnected by cleaning up after the disconnected device. Only check for the data session, the control session is handled separately
*
* @param notification Contains information about the connected accessory
@@ -328,14 +346,13 @@ int const ProtocolIndexTimeoutSeconds = 10;
}
/**
- * Attept to establish a session with an accessory, or if nil is passed, to scan for one.
+ * Attempt to establish a session with an accessory, or if nil is passed, to scan for one.
*
* @param accessory The accessory to try to establish a session with, or nil to scan all connected accessories.
*/
- (void)sdl_establishSessionWithAccessory:(nullable EAAccessory *)accessory {
SDLLogD(@"Attempting to connect accessory: %@", accessory.name);
if (self.retryCounter < CreateSessionRetries) {
- // We should be attempting to connect
self.retryCounter++;
EAAccessory *sdlAccessory = accessory;
@@ -366,7 +383,7 @@ int const ProtocolIndexTimeoutSeconds = 10;
self.sessionSetupInProgress = NO;
}
} else {
- // We are beyond the number of retries allowed
+ // We have surpassed the number of retries allowed
SDLLogW(@"Surpassed allowed retry attempts (%d), dismissing setup", CreateSessionRetries);
self.sessionSetupInProgress = NO;
}
@@ -643,7 +660,7 @@ int const ProtocolIndexTimeoutSeconds = 10;
};
}
-- (double)retryDelay {
+- (double)sdl_retryDelay {
const double MinRetrySeconds = 1.5;
const double MaxRetrySeconds = 9.5;
double RetryRangeSeconds = MaxRetrySeconds - MinRetrySeconds;
diff --git a/SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m b/SmartDeviceLinkTests/TransportSpecs/TCP/SDLTCPTransportSpec.m
index e9a25f834..e9a25f834 100644
--- a/SmartDeviceLinkTests/TransportSpecs/SDLTCPTransportSpec.m
+++ b/SmartDeviceLinkTests/TransportSpecs/TCP/SDLTCPTransportSpec.m
diff --git a/SmartDeviceLinkTests/TransportSpecs/iAP/EAAccessory+OCMock.m b/SmartDeviceLinkTests/TransportSpecs/iAP/EAAccessory+OCMock.m
new file mode 100644
index 000000000..1c32c4b73
--- /dev/null
+++ b/SmartDeviceLinkTests/TransportSpecs/iAP/EAAccessory+OCMock.m
@@ -0,0 +1,62 @@
+//
+// EAAccessory+OCMock.m
+// SmartDeviceLinkTests
+//
+// Created by Nicole on 1/24/19.
+// Copyright © 2019 smartdevicelink. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+#import <OCMock/OCMock.h>
+#import <ExternalAccessory/ExternalAccessory.h>
+
+// Based off of the Pebble Accessory OCKMock by Heiko Behrens https://github.com/HBehrens/PebbleKit-ios-sdk-test/blob/master/PebbleVendor/EAAccessoryFramework%2BOCMock.m
+
+@implementation EAAccessory (OCMock)
+static id coreMockDelegate = nil;
++ (EAAccessory *)sdlCoreMock {
+ id mockEAAccessory = OCMClassMock([EAAccessory class]);
+ OCMStub([mockEAAccessory protocolStrings]).andReturn(@[@"com.smartdevicelink.multisession"]);
+ [[[mockEAAccessory stub] andReturnValue:OCMOCK_VALUE((NSString *)@"SDLTestHeadUnit")] name];
+ OCMStub([mockEAAccessory modelNumber]).andReturn(@"0.0.0");
+ OCMStub([mockEAAccessory serialNumber]).andReturn(@"123456");
+ OCMStub([mockEAAccessory firmwareRevision]).andReturn(@"1.2.3");
+ OCMStub([mockEAAccessory hardwareRevision]).andReturn(@"3.2.1");
+ OCMStub([mockEAAccessory isConnected]).andReturn(OCMOCK_VALUE(YES));
+ OCMStub([mockEAAccessory setDelegate:[OCMArg checkWithBlock:^BOOL(id obj) {
+ coreMockDelegate = obj;
+ return YES;
+ }]]);
+ OCMStub([mockEAAccessory delegate]).andCall(self, @selector(coreDelegate));
+ [[[mockEAAccessory stub] andReturnValue:OCMOCK_VALUE((NSUInteger){5})] connectionID];
+
+ return mockEAAccessory;
+}
+- (id)coreDelegate {
+ return coreMockDelegate;
+}
+@end
+
+@implementation EAAccessoryManager (OCMock)
++ (EAAccessoryManager *)mockManager {
+ id mockEAAccessoryManager = OCMClassMock([EAAccessoryManager class]);
+ id mockEAAccessory = [EAAccessory sdlCoreMock];
+ OCMStub([mockEAAccessoryManager connectedAccessories]).andReturn(@[mockEAAccessory]);
+ OCMStub([mockEAAccessory registerForLocalNotifications]);
+ OCMStub([mockEAAccessory unregisterForLocalNotifications]);
+ return mockEAAccessoryManager;
+}
+@end
+
+@implementation EASession (OCMock)
++ (EASession *)mockSessionWithAccessory:(EAAccessory*)mockAccessory protocolString:(NSString*)mockProtocolString inputStream:(NSInputStream*)mockInputStream outputStream:(NSOutputStream*)mockOutputStream {
+ id session = OCMClassMock([EASession class]);
+ OCMStub([session accessory]).andReturn(mockAccessory);
+ OCMStub([session protocolString]).andReturn(mockProtocolString);
+ OCMStub([session inputStream]).andReturn(mockInputStream);
+ OCMStub([session outputStream]).andReturn(mockOutputStream);
+ return session;
+}
+@end
+
diff --git a/SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPSessionSpec.m b/SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPSessionSpec.m
new file mode 100644
index 000000000..672aa76b1
--- /dev/null
+++ b/SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPSessionSpec.m
@@ -0,0 +1,165 @@
+//
+// SDLIAPSessionSpec.m
+// SmartDeviceLinkTests
+//
+// Created by Nicole on 1/23/19.
+// Copyright © 2019 smartdevicelink. All rights reserved.
+//
+
+#import <Quick/Quick.h>
+#import <Nimble/Nimble.h>
+#import <OCMock/OCMock.h>
+
+#import "EAAccessory+OCMock.m"
+#import "SDLIAPSession.h"
+#import "SDLMutableDataQueue.h"
+#import "SDLStreamDelegate.h"
+
+@interface SDLIAPSession ()
+@property (nonatomic, assign) BOOL isInputStreamOpen;
+@property (nonatomic, assign) BOOL isOutputStreamOpen;
+@property (nonatomic, assign) BOOL isDataSession;
+@property (nullable, nonatomic, strong) NSThread *ioStreamThread;
+@property (nonatomic, strong) SDLMutableDataQueue *sendDataQueue;
+@property (nonatomic, strong) dispatch_semaphore_t canceledSemaphore;
+- (BOOL)sdl_startWithSession:(EASession *)session;
+@end
+
+QuickSpecBegin(SDLIAPSessionSpec)
+
+describe(@"SDLIAPSession", ^{
+ __block SDLIAPSession *iapSession = nil;
+ __block EAAccessory *mockAccessory = nil;
+ __block NSString *protocol = nil;
+
+ describe(@"Initialization", ^{
+ beforeEach(^{
+ mockAccessory = [EAAccessory.class sdlCoreMock];
+ });
+
+ it(@"Should init correctly with a control protocol string", ^{
+ protocol = @"com.smartdevicelink.prot0";
+ iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol];
+
+ expect(iapSession.isDataSession).to(beFalse());
+ });
+
+ it(@"Should init correctly with a multisession protocol string", ^{
+ protocol = @"com.smartdevicelink.multisession";
+ iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol];
+
+ expect(iapSession.isDataSession).to(beTrue());
+ });
+
+ it(@"Should init correctly with a legacy protocol string", ^{
+ protocol = @"com.ford.sync.prot0";
+ iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol];
+
+ expect(iapSession.isDataSession).to(beTrue());
+ });
+
+ it(@"Should init correctly with a indexed protocol string", ^{
+ protocol = @"com.smartdevicelink.prot1";
+ iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol];
+
+ expect(iapSession.isDataSession).to(beTrue());
+ });
+
+ afterEach(^{
+ expect(iapSession).toNot(beNil());
+ expect(iapSession.protocol).to(match(protocol));
+ expect(iapSession.accessory).to(equal(mockAccessory));
+ expect(iapSession.canceledSemaphore).toNot(beNil());
+ expect(iapSession.sendDataQueue).toNot(beNil());
+ expect(iapSession.isInputStreamOpen).to(beFalse());
+ expect(iapSession.isOutputStreamOpen).to(beFalse());
+ });
+ });
+
+ describe(@"When starting a session", ^{
+ __block SDLStreamDelegate *streamDelegate = nil;
+ __block NSInputStream *inputStream = nil;
+ __block NSOutputStream *outputStream = nil;
+
+ describe(@"unsuccessfully", ^{
+ beforeEach(^{
+ protocol = @"com.smartdevicelink.multisession";
+ mockAccessory = [EAAccessory.class sdlCoreMock];
+ iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol];
+ streamDelegate = [[SDLStreamDelegate alloc] init];
+ iapSession.streamDelegate = streamDelegate;
+ });
+
+ it(@"the start method should return false if a session cannot be created", ^{
+ BOOL success = [iapSession sdl_startWithSession:nil];
+ expect(success).to(beFalse());
+ expect(iapSession.ioStreamThread).to(beNil());
+ expect(iapSession.isInputStreamOpen).to(beFalse());
+ expect(iapSession.isOutputStreamOpen).to(beFalse());
+ });
+ });
+
+ describe(@"successfully", ^{
+ beforeEach(^{
+ inputStream = OCMClassMock([NSInputStream class]);
+ outputStream = OCMClassMock([NSOutputStream class]);
+ });
+
+ context(@"if creating a control session", ^{
+ beforeEach(^{
+ protocol = @"com.smartdevicelink.prot0";
+ mockAccessory = [EAAccessory.class sdlCoreMock];
+ iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol];
+ streamDelegate = [[SDLStreamDelegate alloc] init];
+ iapSession.streamDelegate = streamDelegate;
+
+ expect(iapSession.isDataSession).to(beFalse());
+ });
+
+ it(@"should establish a control session ", ^{
+ EASession *mockSession = [EASession.class mockSessionWithAccessory:mockAccessory protocolString:protocol inputStream:inputStream outputStream:outputStream];
+ iapSession.easession = mockSession;
+
+ BOOL success = [iapSession sdl_startWithSession:mockSession];
+ expect(success).to(beTrue());
+ expect(iapSession.ioStreamThread).to(beNil());
+ expect(iapSession.easession.inputStream).toNot(beNil());
+ expect(iapSession.easession.outputStream).toNot(beNil());
+ });
+ });
+
+ context(@"if creating a data session", ^{
+ beforeEach(^{
+ protocol = @"com.smartdevicelink.multisession";
+ mockAccessory = [EAAccessory.class sdlCoreMock];
+ iapSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:protocol];
+ streamDelegate = [[SDLStreamDelegate alloc] init];
+ iapSession.streamDelegate = streamDelegate;
+
+ expect(iapSession.isDataSession).to(beTrue());
+ });
+
+ it(@"should establish a data session ", ^{
+ EASession *mockSession = [EASession.class mockSessionWithAccessory:mockAccessory protocolString:protocol inputStream:inputStream outputStream:outputStream];
+ iapSession.easession = mockSession;
+ BOOL success = [iapSession sdl_startWithSession:mockSession];
+ expect(success).to(beTrue());
+ expect(iapSession.ioStreamThread).toNot(beNil());
+ // The streams are opened in the `sdl_streamHasSpaceHandler` method
+ expect(iapSession.easession.inputStream).toNot(beNil());
+ expect(iapSession.easession.outputStream).toNot(beNil());
+ });
+ });
+ });
+
+ afterEach(^{
+ [iapSession stop];
+
+ expect(iapSession.easession).to(beNil());
+ expect(iapSession.ioStreamThread).to(beNil());
+ });
+ });
+});
+
+QuickSpecEnd
+
diff --git a/SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPTransportSpec.m b/SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPTransportSpec.m
new file mode 100644
index 000000000..7223c8d0b
--- /dev/null
+++ b/SmartDeviceLinkTests/TransportSpecs/iAP/SDLIAPTransportSpec.m
@@ -0,0 +1,118 @@
+//
+// SDLIAPTransportSpec.m
+// SmartDeviceLinkTests
+//
+// Created by Nicole on 1/23/19.
+// Copyright © 2019 smartdevicelink. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <Quick/Quick.h>
+#import <Nimble/Nimble.h>
+#import <OCMock/OCMock.h>
+
+#import "EAAccessory+OCMock.m"
+#import "SDLIAPTransport.h"
+#import "SDLIAPSession.h"
+#import "SDLTimer.h"
+
+@interface SDLIAPTransport ()
+@property (assign, nonatomic) int retryCounter;
+@property (assign, nonatomic) BOOL sessionSetupInProgress;
+@property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskId;
+@property (nullable, strong, nonatomic) SDLTimer *protocolIndexTimer;
+@property (assign, nonatomic) BOOL accessoryConnectDuringActiveSession;
+- (BOOL)sdl_isSessionActive:(SDLIAPSession *)session newAccessory:(EAAccessory *)newAccessory;
+@end
+
+QuickSpecBegin(SDLIAPTransportSpec)
+
+describe(@"SDLIAPTransport", ^{
+ __block SDLIAPTransport *transport = nil;
+ __block id mockTransportDelegate = nil;
+ __block EAAccessory *mockAccessory = nil;
+
+ beforeEach(^{
+ transport = [SDLIAPTransport new];
+ mockTransportDelegate = OCMProtocolMock(@protocol(SDLTransportDelegate));
+ transport.delegate = mockTransportDelegate;
+ mockAccessory = [EAAccessory.class sdlCoreMock];
+ });
+
+ describe(@"Initialization", ^{
+ it(@"Should init correctly", ^{
+ expect(transport.delegate).toNot(beNil());
+ expect(transport.controlSession).to(beNil());
+ expect(transport.session).to(beNil());
+ expect(transport.sessionSetupInProgress).to(beFalse());
+ expect(transport.session).to(beNil());
+ expect(transport.controlSession).to(beNil());
+ expect(transport.retryCounter).to(equal(0));
+ expect(transport.protocolIndexTimer).to(beNil());
+ expect(transport.accessoryConnectDuringActiveSession).to(beFalse());
+ });
+ });
+
+ describe(@"When an accessory connects while a session is not open", ^{
+ beforeEach(^{
+ transport.session = nil;
+ });
+
+ it(@"If no session is open, it should create a session when an accessory connects", ^{
+ BOOL sessionInProgress = [transport sdl_isSessionActive:transport.session newAccessory:mockAccessory];
+ expect(sessionInProgress).to(beFalse());
+ });
+ });
+
+ describe(@"When an accessory connects when a session is already open", ^{
+ beforeEach(^{
+ transport.session = OCMClassMock([SDLIAPSession class]);
+ });
+
+ it(@"If a session is in progress", ^{
+ BOOL sessionInProgress = [transport sdl_isSessionActive:transport.session newAccessory:mockAccessory];
+ expect(sessionInProgress).to(beTrue());
+ });
+ });
+
+ describe(@"When an accessory disconnects while a data session is open", ^{
+ beforeEach(^{
+ transport.controlSession = nil;
+ transport.session = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:@"com.smartdevicelink.multisession"];
+ transport.accessoryConnectDuringActiveSession = YES;
+ NSNotification *accessoryDisconnectedNotification = [[NSNotification alloc] initWithName:EAAccessoryDidDisconnectNotification object:nil userInfo:@{EAAccessoryKey: mockAccessory}];
+ [[NSNotificationCenter defaultCenter] postNotification:accessoryDisconnectedNotification];
+ });
+
+ it(@"It should close the open data session", ^{
+ expect(transport.session).to(beNil());
+ expect(transport.controlSession).to(beNil());
+ expect(transport.retryCounter).to(equal(0));
+ expect(transport.accessoryConnectDuringActiveSession).to(beFalse());
+ expect(transport.sessionSetupInProgress).to(beFalse());
+ });
+ });
+
+ describe(@"When an accessory disconnects while a control session is open", ^{
+ beforeEach(^{
+ transport.controlSession = [[SDLIAPSession alloc] initWithAccessory:mockAccessory forProtocol:@"com.smartdevicelink.prot0"];;
+ transport.session = nil;
+ transport.accessoryConnectDuringActiveSession = NO;
+ transport.sessionSetupInProgress = YES;
+ transport.retryCounter = 1;
+ NSNotification *accessoryDisconnectedNotification = [[NSNotification alloc] initWithName:EAAccessoryDidDisconnectNotification object:nil userInfo:@{EAAccessoryKey: mockAccessory}];
+ [[NSNotificationCenter defaultCenter] postNotification:accessoryDisconnectedNotification];
+ });
+
+ it(@"It should close the open control session", ^{
+ expect(transport.session).to(beNil());
+ expect(transport.controlSession).to(beNil());
+ expect(transport.retryCounter).to(equal(0));
+ expect(transport.accessoryConnectDuringActiveSession).to(beFalse());
+ expect(transport.sessionSetupInProgress).to(beFalse());
+ });
+ });
+});
+
+QuickSpecEnd
+