Shims/Shimulator/TestLoadingShim/FBXCTestMain.m (124 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "FBXCTestMain.h" #import <dlfcn.h> #import <objc/runtime.h> #import "FBDebugLog.h" #import "FBRuntimeTools.h" #import "FBXCTestConstants.h" #import "XCTestPrivate.h" #include "TargetConditionals.h" #if TARGET_OS_IPHONE #import <UIKit/UIKit.h> #elif TARGET_OS_MAC #import <AppKit/AppKit.h> #endif __attribute__((constructor)) static void XCTestMainEntryPoint() { FBDebugLog(@"[XCTestMainEntryPoint] Running inside: %@", [[NSBundle mainBundle] bundleIdentifier]); if (!NSProcessInfo.processInfo.environment[kEnv_ShimStartXCTest]) { FBDebugLog(@"[XCTestMainEntryPoint] %@ not present. Bye", kEnv_ShimStartXCTest); return; } if ([[[NSBundle mainBundle] bundleIdentifier] hasPrefix:@"com.apple.test"]) { FBDebugLog(@"[XCTestMainEntryPoint] Looks like I am running inside Apple's test runner app. Bye"); return; } FBDebugLog(@"[XCTestMainEntryPoint] Hold back, trying to load test bundle"); if (!FBXCTestMain()) { NSLog(@"[XCTestMainEntryPoint] Loading XCTest bundle failed, bye"); exit(1); } FBDebugLog(@"[XCTestMainEntryPoint] End of XCTestMainEntryPoint"); } BOOL FBLoadXCTestIfNeeded() { FBDebugLog(@"Env: %@", [NSProcessInfo processInfo].environment); if (objc_lookUpClass("XCTest")) { FBDebugLog(@"[XCTestMainEntryPoint] XCTest already loaded"); return YES; } FBDebugLog(@"[XCTestMainEntryPoint] Loading XCTest framework"); if (dlopen("XCTest.framework/XCTest", RTLD_LAZY)) { FBDebugLog(@"[XCTestMainEntryPoint] XCTest loaded"); return YES; } FBDebugLog(@"[XCTestMainEntryPoint] Failed to load XCTest.framework. %@", [NSString stringWithUTF8String:dlerror()]); // Even though XCTest.framework actually is located in one of the `DYLD_FALLBACK_FRAMEWORK_PATH` directories, starting // on Xcode13.0/iOS15.0, dlopen does not look into those directories, failing to load XCTest. // As a last attempt, idb tries itself to find XCTest.framework and passes the absolute path to `dlopen` NSArray<NSString *> *fallbackFrameworkDirs = [[[NSProcessInfo processInfo].environment objectForKey:@"DYLD_FALLBACK_FRAMEWORK_PATH"] componentsSeparatedByString:@":"]; FBDebugLog(@"[XCTestMainEntryPoint] Explictly looking for XCTest.framework in DYLD_FALLBACK_FRAMEWORK_PATH: %@", fallbackFrameworkDirs); for(NSString *frameworkDir in fallbackFrameworkDirs) { NSString *possibleLocation = [frameworkDir stringByAppendingPathComponent:@"XCTest.framework/XCTest"]; if ([NSFileManager.defaultManager fileExistsAtPath:possibleLocation isDirectory:nil]) { if (dlopen([possibleLocation cStringUsingEncoding:NSUTF8StringEncoding], RTLD_LAZY)) { FBDebugLog(@"[XCTestMainEntryPoint] Found and loaded XCTest from %@", possibleLocation); return YES; } else { FBDebugLog(@"[XCTestMainEntryPoint] Failed to load XCTest.framework. %@", [NSString stringWithUTF8String:dlerror()]); } } else { FBDebugLog(@"[XCTestMainEntryPoint] XCTest not found at %@", possibleLocation); } } FBDebugLog(@"[XCTestMainEntryPoint] Could not load XCTest.framework"); return NO; } void FBDeployBlockWhenAppLoads(void(^mainBlock)()) { #if TARGET_OS_IPHONE NSString *notification = UIApplicationDidFinishLaunchingNotification; #elif TARGET_OS_MAC NSString *notification = NSApplicationDidFinishLaunchingNotification; #endif [[NSNotificationCenter defaultCenter] addObserverForName:notification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { mainBlock(); }]; } BOOL FBXCTestMain() { if (!FBLoadXCTestIfNeeded()) { exit(TestShimExitCodeXCTestFailedLoading); } NSString *configurationPath = NSProcessInfo.processInfo.environment[@"XCTestConfigurationFilePath"]; if (!configurationPath) { NSLog(@"Failed to load XCTest as XCTestConfigurationFilePath environment variable is empty"); return NO; } NSError *error; NSData *data = [NSData dataWithContentsOfFile:configurationPath options:0 error:&error]; if (!data) { NSLog(@"Failed to load data of %@ due to %@", configurationPath, error); return NO; } XCTestConfiguration *configuration = nil; if([NSKeyedUnarchiver respondsToSelector:@selector(xct_unarchivedObjectOfClass:fromData:)]){ configuration = (XCTestConfiguration *)[NSKeyedUnarchiver xct_unarchivedObjectOfClass:NSClassFromString(@"XCTestConfiguration") fromData:data]; } else { configuration = [NSKeyedUnarchiver unarchiveObjectWithData:data]; } if (!configuration) { NSLog(@"Loaded XCTestConfiguration is nil"); return NO; } NSURL *testBundleURL = configuration.testBundleURL; if (!testBundleURL) { NSLog(@"XCTestConfiguration has no test bundle URL value"); return NO; } NSBundle *testBundle = [NSBundle bundleWithURL:testBundleURL]; if (!testBundle) { NSLog(@"Failed to open test bundle from %@", testBundleURL); return NO; } if (![testBundle loadAndReturnError:&error]) { NSLog(@"Failed load test bundle with error: %@", error); return NO; } void (*XCTestMain)(XCTestConfiguration *) = (void (*)(XCTestConfiguration *))FBRetrieveXCTestSymbol("_XCTestMain"); FBDeployBlockWhenAppLoads(^{ CFRunLoopPerformBlock([NSRunLoop mainRunLoop].getCFRunLoop, kCFRunLoopCommonModes, ^{ XCTestMain(configuration); }); }); return YES; }