diff --git a/bazel/bitdrift_swift_test.bzl b/bazel/bitdrift_swift_test.bzl index 4cd6f1d22..6051c5fdf 100644 --- a/bazel/bitdrift_swift_test.bzl +++ b/bazel/bitdrift_swift_test.bzl @@ -58,3 +58,47 @@ def bitdrift_mobile_objc_test(name, srcs, data = [], deps = [], tags = [], visib tags = tags, visibility = visibility, ) + +def bitdrift_mobile_swift_objc_test(name, srcs_swift, srcs_objc, hdrs_objc, data = [], deps = [], tags = [], use_test_host = False, repository = "", visibility = []): + test_swift_lib_name = name + "_swift_lib" + test_objc_lib_name = name + "_objc_lib" + swift_library( + name = test_swift_lib_name, + srcs = srcs_swift, + data = data, + deps = deps, + linkopts = ["-lresolv.9"], + testonly = True, + visibility = ["//visibility:private"], + tags = ["manual"], + ) + + objc_library( + name = test_objc_lib_name, + srcs = srcs_objc, + hdrs = hdrs_objc, + data = data, + deps = deps, + linkopts = ["-lresolv.9"], + testonly = True, + visibility = ["//visibility:private"], + tags = ["manual"], + ) + + test_host = None + if use_test_host: + test_host = "//test/platform/swift/test_host:TestHost" + + ios_unit_test( + name = name, + data = data, + deps = [test_swift_lib_name, test_objc_lib_name], + minimum_os_version = MINIMUM_IOS_VERSION_TESTS, + timeout = "long", + tags = tags + [ + "no-cache", + "no-remote", + ], + test_host = test_host, + visibility = visibility, + ) diff --git a/platform/swift/source/BUILD b/platform/swift/source/BUILD index 8330a99bb..16337de45 100644 --- a/platform/swift/source/BUILD +++ b/platform/swift/source/BUILD @@ -80,12 +80,14 @@ bitdrift_rust_library( objc_library( name = "objc_bridge", srcs = [ + "InternalAPI.m", "ObjCWrapper.mm", "reports/BitdriftKSCrashWrapper.m", "reports/DiagnosticEventReporter.m", ], hdrs = [ "CaptureRustBridge.h", + "InternalAPI.h", "ObjCWrapper.h", "reports/BitdriftKSCrashWrapper.h", "reports/DiagnosticEventReporter.h", diff --git a/platform/swift/source/InternalAPI.h b/platform/swift/source/InternalAPI.h new file mode 100644 index 000000000..b3016d6ff --- /dev/null +++ b/platform/swift/source/InternalAPI.h @@ -0,0 +1,133 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/// Mechanism for exposing internal APIs to other libraries without exposing them publicly. +/// +/// Place your exposable APIs in the `API Implementation` section at the bottom of the .m file. +/// They DON'T need to be declared in this header (although you can if you want to call them locally). +/// +/// Be sure to version all APIs so that their behaviors can be safely modified in future, +/// and also proxy any returned non-standard-library objects so that they are safe to change in future. +/// +/// ## How to use it (from a client) +/// +/// Clients call `exposeAPI` to attach an internal (versioned) API method to the selector of their choice. e.g. +/// ``` +/// NSError *error = [self.instance exposeAPI:@"example_v1" asSelector:@selector(example)]; +/// ``` +/// This code will add a new method with the selector `example` that actually calls the implementation of `example_v1` (if found). +/// See section `Implementing a client` for how to set up a client-library class to do this. +/// +/// Note: In the following situations, calling the exposed method will no-op: +/// * The host API class is not found (e.g. this library hasn't been linked in) +/// * The requested API is not found (maybe it was misspelled, or has been removed) +/// +/// +/// ## Cloning this internal API hosting code for use in another library +/// +/// Copy all code in the .m file except for these sections, which must be tailored to your implementation: +/// * `Library Specific` +/// * `API Implementation` +/// The rest is implementation-agnostic code. +/// +/// +/// ## Implementing a client +/// +/// A client class (in the calling library) would look like this: +/// +/// **ExampleClient.h** +/// +/// ```objc +/// #import +/// +/// NS_ASSUME_NONNULL_BEGIN +/// +/// @interface ExampleClient : NSObject +/// +/// + (instancetype _Nullable)instance; +/// +/// #pragma mark APIs we will expose from the host library +/// +/// - (NSString *)example; +/// +/// @end +/// +/// NS_ASSUME_NONNULL_END +/// ``` +/// +/// **ExampleClient.m:** +/// +/// ```objc +/// #import "ExampleClient.h" +/// +/// #pragma clang diagnostic push +/// #pragma clang diagnostic ignored "-Wincomplete-implementation" +/// @implementation ExampleClient +/// +/// #pragma mark Host library and APIs to expose +/// +/// static NSString *hostLibraryAPIClassName = @"BD_InternalAPI_Capture"; +/// +/// static void exposeAPIs(id self) { +/// // Example of exposing APIs on initialize +/// NSError *error = [self exposeAPI:@"example_v1" asSelector:@selector(example)]; +/// if(error != nil) { +/// // This would only happen if the ObjC runtime is very broken. +/// NSLog(@"API \"%@\" is unsafe to call", NSStringFromSelector(@selector(example))); +/// } +/// } +/// +/// +/// #pragma mark General code - Do not change +/// +/// static id instance = nil; +/// +/// + (void)initialize { +/// Class cls = NSClassFromString(hostLibraryAPIClassName); +/// static dispatch_once_t once; +/// dispatch_once(&once, ^{ +/// instance = [cls instance]; +/// if(instance != nil) { +/// exposeAPIs(self.instance); +/// } else { +/// NSLog(@"WARNING: API class %@ was not found. Calls to this API will no-op.", hostLibraryAPIClassName); +/// instance = [[self alloc] init]; +/// } +/// }); +/// } +/// +/// + (instancetype _Nullable)instance { +/// return instance; +/// } +/// +/// - (void)forwardInvocation:(NSInvocation *)invocation { +/// // If API calls will be made often, probably best to not log here. +/// NSLog(@"WARNING: API class %@ was not found. Called selector '%@' is a no-op.", hostLibraryAPIClassName, /// NSStringFromSelector(invocation.selector)); +/// } +/// +/// -(NSMethodSignature*)methodSignatureForSelector:(SEL)selector { +/// // We must return a real signature or else it will crash, so use NSObject.init because it always exists. +/// return [NSObject instanceMethodSignatureForSelector:@selector(init)]; +/// } +/// +/// @end +/// ``` + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface BD_InternalAPI_Capture : NSObject + ++ (instancetype)instance; + +// If you need to inject data into this internal API, create a `configure` method to do so: +// - (void)configureWithFribblefrabble:(Fribblefrabble *)frib; + +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/swift/source/InternalAPI.m b/platform/swift/source/InternalAPI.m new file mode 100644 index 000000000..6566c59a3 --- /dev/null +++ b/platform/swift/source/InternalAPI.m @@ -0,0 +1,181 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +#import "InternalAPI.h" +#import + + +#pragma mark Library Specific + +// Place library-specific things (declarations, imports, etc) here. +// Library-specific API implementations are at the end of this file. +// Everything else in between should not be touched. + +#define LIBRARY_NAME Capture + + +#pragma mark Base Implementation + +#define CONCAT(a, ...) CONCAT2(a, __VA_ARGS__) +#define CONCAT2(a, ...) a ## __VA_ARGS__ +#define STRINGIFY(s) STRINGIFY2(s) +#define STRINGIFY2(s) #s + +#define API_CLASS CONCAT(BD_InternalAPI_, LIBRARY_NAME) +#define PROXY_CLASS CONCAT(BD_Proxy_, LIBRARY_NAME) +#define LIB_DOMAIN @"io.bitdrift." STRINGIFY(LIBRARY_NAME) + + +/** + * Proxy that no-ops whenever an unimplemented method is called. + */ +@interface PROXY_CLASS : NSProxy + +@property(nonatomic,strong) id proxied; + +@end + +@implementation PROXY_CLASS + ++ (instancetype)proxyTo:(id)objectToProxy { + if (objectToProxy == nil) { + return nil; + } + + PROXY_CLASS *proxy = [PROXY_CLASS alloc]; + proxy.proxied = objectToProxy; + return proxy; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + if ([self.proxied respondsToSelector:invocation.selector]) { + [invocation setTarget:self.proxied]; + [invocation invoke]; + } +} + +-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector { + NSMethodSignature *signature = [self.proxied methodSignatureForSelector:selector]; + if (signature != nil) { + return signature; + } + + // We must return a real signature or else it will crash, so use NSObject.init because it always exists. + return [NSObject instanceMethodSignatureForSelector:@selector(init)]; +} + +- (BOOL)isEqual:(id)object { return [self.proxied isEqual:object]; } +- (NSUInteger)hash { return [self.proxied hash]; } +- (Class)superclass { return [self.proxied superclass]; } +- (Class)class { return [self.proxied class]; } +- (BOOL)isProxy { return YES; } +- (BOOL)isKindOfClass:(Class)aClass { return [self.proxied isKindOfClass:aClass]; } +- (BOOL)isMemberOfClass:(Class)aClass { return [self.proxied isMemberOfClass:aClass]; } +- (BOOL)conformsToProtocol:(Protocol *)aProtocol { return [self.proxied conformsToProtocol:aProtocol]; } +- (BOOL)respondsToSelector:(SEL)aSelector { return [self.proxied respondsToSelector:aSelector]; } +- (NSString *)description { return [self.proxied description]; } +- (NSString *)debugDescription { return [self.proxied debugDescription]; } + +@end + + +@implementation API_CLASS + ++ (instancetype)instance { + static API_CLASS *instance; + static dispatch_once_t once; + dispatch_once(&once, ^{ instance = [[self alloc] init]; }); + return instance; +} + +/** + * "null" method that gets mapped if a client requests an API name that doesn't exist. + */ +- (id)nullMethod { + return nil; +} + +/** + * Expose an API on this class, assigning it to the specified selector so that it can be called normally. + * If the API name is not found, `nullMethod` will be mapped to the selector. + * + * @param apiName A string describing the selector of the internal method to map. + * @param asSelector The selector to map this method to (if found) + * @return `nil` on success, or an error if the objc runtime is seriously broken (almost impossible). + */ +- (NSError *)exposeAPI:(NSString * _Nonnull)apiName asSelector:(SEL)asSelector { + if(class_getInstanceMethod(self.class, asSelector) != nil) { + NSLog(@"WARNING: Class %@ already implements selector '%@'. Keeping existing mapping.", self.class, NSStringFromSelector(asSelector)); + return nil; + } + + SEL selectorToClone = @selector(nullMethod); + + SEL foundSelector = NSSelectorFromString(apiName); + if(class_getInstanceMethod(self.class, foundSelector) != nil) { + selectorToClone = foundSelector; + } else { + NSLog(@"WARNING: Class %@ doesn't have method '%@' to clone. Mapping to a null implementation.", self.class, apiName); + } + +#define NSERROR(FMT, ...) [NSError errorWithDomain:LIB_DOMAIN code:0 userInfo:@{ \ + NSLocalizedDescriptionKey:[NSString stringWithFormat:FMT, __VA_ARGS__] }]; + + // Note: These errors should never happen unless the objective-c runtime is seriously broken. + + Method method = class_getInstanceMethod(self.class, selectorToClone); + if(method == nil) { + return NSERROR(@"class_getInstanceMethod(%@, %@) failed", self.class, NSStringFromSelector(selectorToClone)); + } + + IMP implementation = method_getImplementation(method); + if(implementation == nil) { + return NSERROR(@"method_getImplementation(%@) failed", NSStringFromSelector(selectorToClone)); + } + + const char *encoding = method_getTypeEncoding(method); + if(encoding == nil) { + return NSERROR(@"method_getTypeEncoding(%@) failed", NSStringFromSelector(selectorToClone)); + } + + if(!class_addMethod(self.class, asSelector, implementation, encoding)) { + return NSERROR(@"class_addMethod(%@, %@, ...) failed", self.class, NSStringFromSelector(asSelector)); + } + + return nil; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + // If a particular API call will be made often, probably best to not log here. + NSLog(@"WARNING: Called nonexistent API '%@', which will no-op", NSStringFromSelector(invocation.selector)); +} + +-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector { + // We must return a real signature or else it will crash, so use NSObject.init because it always exists. + return [NSObject instanceMethodSignatureForSelector:@selector(init)]; +} + + +#pragma mark API implementation + + +#ifdef DEBUG +// These are called by the unit tests. Do not remove them! + +- (void)exampleWithVoidReturn_v1 { +} + +- (NSString *)exampleWithIDReturn_v1 { + return @"This is an example API"; +} + +- (NSString *)exampleWithProxyReturn_v1 { + return (NSString *)[PROXY_CLASS proxyTo:@"This is a proxied string"]; +} +#endif + +@end diff --git a/test/platform/swift/unit_integration/core/BUILD b/test/platform/swift/unit_integration/core/BUILD index 26f38f088..9b176b8a5 100644 --- a/test/platform/swift/unit_integration/core/BUILD +++ b/test/platform/swift/unit_integration/core/BUILD @@ -1,13 +1,15 @@ -load("//bazel:bitdrift_swift_test.bzl", "bitdrift_mobile_swift_test") +load("//bazel:bitdrift_swift_test.bzl", "bitdrift_mobile_swift_objc_test") -bitdrift_mobile_swift_test( +bitdrift_mobile_swift_objc_test( name = "test", - srcs = glob(["**/*.swift"]) + ["//proto:report_swift_source"], data = [ "testdata/lastCrash.bjn", "testdata/metrickit-example.json", ], repository = "@capture", + hdrs_objc = glob(["**/*.h"]), + srcs_objc = glob(["**/*.m"]), + srcs_swift = glob(["**/*.swift"]) + ["//proto:report_swift_source"], tags = ["macos_only"], visibility = ["//visibility:public"], deps = [ diff --git a/test/platform/swift/unit_integration/core/InternalAPITests.m b/test/platform/swift/unit_integration/core/InternalAPITests.m new file mode 100644 index 000000000..4796cf1b9 --- /dev/null +++ b/test/platform/swift/unit_integration/core/InternalAPITests.m @@ -0,0 +1,155 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +#import +#import "SampleClient.h" +#import "TargetNotFoundClient.h" + +@interface InternalAPITests : XCTestCase + +@end + +@implementation InternalAPITests + +// NOTE: Methods can only be mapped once, so each test must use a DIFFERENT selector! + +- (void)testIDReturn_MethodFound { + NSError *error = [SampleClient.instance exposeAPI:@"exampleWithIDReturn_v1" asSelector:@selector(idReturnMethod1)]; + XCTAssertNil(error); + NSString *result = [SampleClient.instance idReturnMethod1]; + XCTAssertEqualObjects(result, @"This is an example API"); + + // Calling again should no-op + error = [SampleClient.instance exposeAPI:@"exampleWithIDReturn_v1" asSelector:@selector(idReturnMethod1)]; + XCTAssertNil(error); + result = [SampleClient.instance idReturnMethod1]; + XCTAssertEqualObjects(result, @"This is an example API"); +} + +- (void)testIDReturn_MethodNotFound { + NSError *error = [SampleClient.instance exposeAPI:@"exampleWithIDReturn_v99" asSelector:@selector(idReturnMethod2)]; + XCTAssertNil(error); + NSString *ph = [self placeholder]; + NSString *result = [SampleClient.instance idReturnMethod2]; + XCTAssertNil(result); + // Make sure no stack/return corruption + XCTAssertEqualObjects(ph, @"placeholder"); + + // Calling again should no-op + error = [SampleClient.instance exposeAPI:@"exampleWithIDReturn_v99" asSelector:@selector(idReturnMethod2)]; + XCTAssertNil(error); + ph = [self placeholder]; + result = [SampleClient.instance idReturnMethod2]; + XCTAssertNil(result); + // Make sure no stack/return corruption + XCTAssertEqualObjects(ph, @"placeholder"); +} + +- (NSString *)placeholder { + return @"placeholder"; +} + +- (void)testIDReturn_TargetNotFound { + // This client will not find the host class, so everything should no-op. + NSError *error = [TargetNotFoundClient.instance exposeAPI:@"exampleWithIDReturn_v1" asSelector:@selector(idReturnMethod1)]; + XCTAssertNil(error); + NSString *ph = [self placeholder]; + NSString *result = [TargetNotFoundClient.instance idReturnMethod1]; + XCTAssertNil(result); + // Make sure no stack/return corruption + XCTAssertEqualObjects(ph, @"placeholder"); + + // Calling again should no-op + error = [TargetNotFoundClient.instance exposeAPI:@"exampleWithIDReturn_v1" asSelector:@selector(idReturnMethod1)]; + XCTAssertNil(error); + ph = [self placeholder]; + result = [TargetNotFoundClient.instance idReturnMethod1]; + XCTAssertNil(result); + // Make sure no stack/return corruption + XCTAssertEqualObjects(ph, @"placeholder"); +} + +- (void)testVoidReturn_MethodFound { + NSError *error = [SampleClient.instance exposeAPI:@"exampleWithVoidReturn_v1" asSelector:@selector(voidReturnMethod1)]; + XCTAssertNil(error); + NSString *ph = [self placeholder]; + [SampleClient.instance voidReturnMethod1]; + // Make sure no stack/return corruption + XCTAssertEqualObjects(ph, @"placeholder"); + + // Calling again should no-op + error = [SampleClient.instance exposeAPI:@"exampleWithVoidReturn_v1" asSelector:@selector(voidReturnMethod1)]; + XCTAssertNil(error); + ph = [self placeholder]; + [SampleClient.instance voidReturnMethod1]; + // Make sure no stack/return corruption + XCTAssertEqualObjects(ph, @"placeholder"); +} + +- (void)testVoidReturn_MethodNotFound { + NSError *error = [SampleClient.instance exposeAPI:@"exampleWithVoidReturn_v99" asSelector:@selector(voidReturnMethod2)]; + XCTAssertNil(error); + NSString *ph = [self placeholder]; + [SampleClient.instance voidReturnMethod2]; + // Make sure no stack/return corruption + XCTAssertEqualObjects(ph, @"placeholder"); + + // Calling again should no-op + error = [SampleClient.instance exposeAPI:@"exampleWithVoidReturn_v99" asSelector:@selector(voidReturnMethod2)]; + XCTAssertNil(error); + ph = [self placeholder]; + [SampleClient.instance voidReturnMethod2]; + // Make sure no stack/return corruption + XCTAssertEqualObjects(ph, @"placeholder"); +} + +- (void)testVoidReturn_TargetNotFound { + // This client will not find the host class, so everything should no-op. + NSError *error = [TargetNotFoundClient.instance exposeAPI:@"exampleWithVoidReturn_v1" asSelector:@selector(voidReturnMethod1)]; + XCTAssertNil(error); + NSString *ph = [self placeholder]; + [TargetNotFoundClient.instance voidReturnMethod1]; + // Make sure no stack/return corruption + XCTAssertEqualObjects(ph, @"placeholder"); + + // Calling again should no-op + error = [TargetNotFoundClient.instance exposeAPI:@"exampleWithVoidReturn_v1" asSelector:@selector(voidReturnMethod1)]; + XCTAssertNil(error); + ph = [self placeholder]; + [TargetNotFoundClient.instance voidReturnMethod1]; + // Make sure no stack/return corruption + XCTAssertEqualObjects(ph, @"placeholder"); +} + +- (void)testProxyReturn_MethodFound { + // This tests returning a proxy to an object. + // We're proxying a string, which is unnecessary, but if you're + // returning a custom object you should be wrapping it in a proxy. + NSError *error = [SampleClient.instance exposeAPI:@"exampleWithProxyReturn_v1" asSelector:@selector(proxyReturnMethod1)]; + XCTAssertNil(error); + NSString *result = [SampleClient.instance proxyReturnMethod1]; + XCTAssertEqualObjects(result, @"This is a proxied string"); + XCTAssertEqual(24, result.length); + + // The proxy object should no-op on nonexistent method calls. + NSURLSession *asSession = (NSURLSession *)result; + [asSession setSessionDescription:@"Blah"]; + XCTAssertNil(asSession.configuration); + + // Calling again should no-op + error = [SampleClient.instance exposeAPI:@"exampleWithProxyReturn_v1" asSelector:@selector(proxyReturnMethod1)]; + XCTAssertNil(error); + result = [SampleClient.instance proxyReturnMethod1]; + XCTAssertEqualObjects(result, @"This is a proxied string"); + + // The proxy object should still no-op on nonexistent method calls. + asSession = (NSURLSession *)result; + [asSession setSessionDescription:@"Blah"]; + XCTAssertNil(asSession.configuration); +} + +@end diff --git a/test/platform/swift/unit_integration/core/SampleClient.h b/test/platform/swift/unit_integration/core/SampleClient.h new file mode 100644 index 000000000..423f7d3e5 --- /dev/null +++ b/test/platform/swift/unit_integration/core/SampleClient.h @@ -0,0 +1,38 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/// An example client to `BD_InternalAPI_Capture` (see `InternalAPI.m`). +/// We've exposed the `exposeAPI` method so that the unit tests can attempt to map more methods + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SampleClient : NSObject + ++ (instancetype _Nullable)instance; + +- (NSError *)exposeAPI:(NSString * _Nonnull)apiName asSelector:(SEL)asSelector; + + +#pragma mark APIs we will expose from the host library + +- (NSString *)example; // Example selector. Not used in unit tests. + +// These are used in the unit tests: + +- (NSString *)idReturnMethod1; +- (NSString *)idReturnMethod2; + +- (void)voidReturnMethod1; +- (void)voidReturnMethod2; + +- (NSString *)proxyReturnMethod1; + +@end + +NS_ASSUME_NONNULL_END diff --git a/test/platform/swift/unit_integration/core/SampleClient.m b/test/platform/swift/unit_integration/core/SampleClient.m new file mode 100644 index 000000000..26b54b044 --- /dev/null +++ b/test/platform/swift/unit_integration/core/SampleClient.m @@ -0,0 +1,60 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +#import "SampleClient.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wincomplete-implementation" +@implementation SampleClient + +#pragma mark Host library and APIs to expose + +static NSString *hostLibraryAPIClassName = @"BD_InternalAPI_Capture"; + +static void exposeAPIs(id self) { + // Example of exposing APIs on initialize + NSError *error = [self exposeAPI:@"example_v1" asSelector:@selector(example)]; + if(error != nil) { + // This would only happen if the ObjC runtime is very broken. + NSLog(@"API \"%@\" is unsafe to call", NSStringFromSelector(@selector(example))); + } +} + + +#pragma mark General code - Do not change + +static id instance = nil; + ++ (void)initialize { + Class cls = NSClassFromString(hostLibraryAPIClassName); + static dispatch_once_t once; + dispatch_once(&once, ^{ + instance = [cls instance]; + if(instance != nil) { + exposeAPIs(self.instance); + } else { + NSLog(@"WARNING: API class %@ was not found. Calls to this API will no-op.", hostLibraryAPIClassName); + instance = [[self alloc] init]; + } + }); +} + ++ (instancetype _Nullable)instance { + return instance; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + // If API calls will be made often, probably best to not log here. + NSLog(@"WARNING: API class %@ was not found. Called selector '%@' is a no-op.", hostLibraryAPIClassName, NSStringFromSelector(invocation.selector)); +} + +-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector { + // We must return a real signature or else it will crash, so use NSObject.init because it always exists. + return [NSObject instanceMethodSignatureForSelector:@selector(init)]; +} + +@end diff --git a/test/platform/swift/unit_integration/core/TargetNotFoundClient.h b/test/platform/swift/unit_integration/core/TargetNotFoundClient.h new file mode 100644 index 000000000..f19a78470 --- /dev/null +++ b/test/platform/swift/unit_integration/core/TargetNotFoundClient.h @@ -0,0 +1,37 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +/// This client is exactly the same as SampleClient, except that it searches for the nonexistent +/// library `BD_InternalAPI_SomeNonexistentLibrary`, simulating what happens when +/// the target library hasn't been linked into the project. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TargetNotFoundClient : NSObject + ++ (instancetype _Nullable)instance; + +- (NSError *)exposeAPI:(NSString * _Nonnull)apiName asSelector:(SEL)asSelector; + + +#pragma mark APIs we will expose from the host library + +- (NSString *)example; // Example selector. Not used in unit tests. + +// These are used in the unit tests: + +- (NSString *)idReturnMethod1; +- (NSString *)idReturnMethod2; + +- (void)voidReturnMethod1; +- (void)voidReturnMethod2; + +@end + +NS_ASSUME_NONNULL_END diff --git a/test/platform/swift/unit_integration/core/TargetNotFoundClient.m b/test/platform/swift/unit_integration/core/TargetNotFoundClient.m new file mode 100644 index 000000000..2bfb7713e --- /dev/null +++ b/test/platform/swift/unit_integration/core/TargetNotFoundClient.m @@ -0,0 +1,58 @@ +// capture-sdk - bitdrift's client SDK +// Copyright Bitdrift, Inc. All rights reserved. +// +// Use of this source code is governed by a source available license that can be found in the +// LICENSE file or at: +// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt + +#import "TargetNotFoundClient.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wincomplete-implementation" +@implementation TargetNotFoundClient + +#pragma mark Host library and APIs to expose + +static NSString *hostLibraryAPIClassName = @"BD_InternalAPI_SomeNonexistentLibrary"; + +static void exposeAPIs(id self) { + NSError *error = [self exposeAPI:@"example_v1" asSelector:@selector(example)]; + if(error != nil) { + // This would only happen if the ObjC runtime is very broken. + NSLog(@"API \"%@\" is unsafe to call", NSStringFromSelector(@selector(example))); + } +} + + +#pragma mark General code - Do not change + +static id instance = nil; + ++ (void)initialize { + Class cls = NSClassFromString(hostLibraryAPIClassName); + static dispatch_once_t once; + dispatch_once(&once, ^{ + instance = [cls instance]; + if(instance != nil) { + exposeAPIs(self.instance); + } else { + NSLog(@"Note: API class %@ was not found. Calls to this API will no-op.", hostLibraryAPIClassName); + instance = [[self alloc] init]; + } + }); +} + ++ (instancetype _Nullable)instance { + return instance; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + NSLog(@"WARNING: API class %@ was not found. Called selector '%@' is a no-op.", hostLibraryAPIClassName, NSStringFromSelector(invocation.selector)); +} + +-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector { + // We must return a real signature or else it will crash, so use NSObject.init because it always exists. + return [NSObject instanceMethodSignatureForSelector:@selector(init)]; +} + +@end