Shims/Shimulator/TestReporterShim/XCTestReporterShim.m (656 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 <Foundation/Foundation.h> #import <dlfcn.h> #import <objc/message.h> #import <objc/runtime.h> #import "FBXCTestConstants.h" #import "XCTestPrivate.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wundeclared-selector" static NSString *const XCTestFilterArg = @"XCTest"; static NSString *const XCTestFrameworkName = @"XCTest"; static NSString *const XCTestProbeClassName = @"XCTestProbe"; static NSString *const XCTestSuiteClassName = @"XCTestSuite"; static FILE *__stdout; static FILE *__stderr; static NSMutableArray<NSDictionary<NSString *, id> *> *__testExceptions = nil; static int __testSuiteDepth = 0; static void parseXCTestCase(XCTestCase *testCase, NSString **classNameOut, NSString **methodNameOut, NSString **testKeyOut) { NSString *className = NSStringFromClass(testCase.class); NSString *methodName; if ([testCase respondsToSelector:@selector(languageAgnosticTestMethodName)]) { methodName = [testCase languageAgnosticTestMethodName]; } else { methodName = NSStringFromSelector([testCase.invocation selector]); } NSString *testKey = [NSString stringWithFormat:@"-[%@ %@]", className, methodName]; if (classNameOut) { *classNameOut = className; } if (methodNameOut) { *methodNameOut = methodName; } if (testKeyOut) { *testKeyOut = testKey; } } static NSString *parseXCTestSuiteKey(XCTestSuite *suite) { NSString *testKey = nil; for (id test in suite.tests) { if (![test isKindOfClass:NSClassFromString(@"XCTestCase")]) { return [suite name]; } XCTestCase *testCase = test; NSString *innerTestKey = nil; parseXCTestCase(testCase, &innerTestKey, nil, nil); if (!testKey) { testKey = innerTestKey; continue; } if (![innerTestKey isEqualToString:testKey]) { return [suite name]; } } return testKey ?: [suite name]; } NSDictionary<NSString *, id> *EventDictionaryWithNameAndContent(NSString *name, NSDictionary *content) { NSMutableDictionary<NSString *, id> *eventJSON = [NSMutableDictionary dictionaryWithDictionary:@{ kReporter_Event_Key: name, kReporter_TimestampKey: @([[NSDate date] timeIntervalSince1970]) }]; [eventJSON addEntriesFromDictionary:content]; return eventJSON; } void XTSwizzleClassSelectorForFunction(Class cls, SEL sel, IMP newImp) __attribute__((no_sanitize("nullability-arg"))) { Class clscls = object_getClass((id)cls); Method originalMethod = class_getClassMethod(cls, sel); NSString *selectorName = [[NSString alloc] initWithFormat: @"__%s_%s", class_getName(cls), sel_getName(sel)]; SEL newSelector = sel_registerName([selectorName UTF8String]); class_addMethod(clscls, newSelector, newImp, method_getTypeEncoding(originalMethod)); Method replacedMethod = class_getClassMethod(cls, newSelector); method_exchangeImplementations(originalMethod, replacedMethod); } void XTSwizzleSelectorForFunction(Class cls, SEL sel, IMP newImp) { Method originalMethod = class_getInstanceMethod(cls, sel); const char *typeEncoding = method_getTypeEncoding(originalMethod); NSString *selectorName = [[NSString alloc] initWithFormat: @"__%s_%s", class_getName(cls), sel_getName(sel)]; SEL newSelector = sel_registerName([selectorName UTF8String]); class_addMethod(cls, newSelector, newImp, typeEncoding); Method newMethod = class_getInstanceMethod(cls, newSelector); // @lint-ignore FBOBJCDISCOURAGEDFUNCTION if (class_addMethod(cls, sel, newImp, typeEncoding)) { class_replaceMethod(cls, newSelector, method_getImplementation(originalMethod), typeEncoding); } else { method_exchangeImplementations(originalMethod, newMethod); } } NSArray<XCTestCase *> *TestsFromSuite(id testSuite) { NSMutableArray<XCTestCase *> *tests = [NSMutableArray array]; NSMutableArray<id> *queue = [NSMutableArray array]; [queue addObject:testSuite]; while ([queue count] > 0) { id test = [queue objectAtIndex:0]; [queue removeObjectAtIndex:0]; if ([test isKindOfClass:[testSuite class]] || [test respondsToSelector:@selector(tests)]) { // XCTestSuite keep a list of tests in an ivar called 'tests'. id testsInSuite = [test valueForKey:@"tests"]; NSCAssert(testsInSuite != nil, @"Can't get tests for suite: %@", testSuite); [queue addObjectsFromArray:testsInSuite]; } else { [tests addObject:test]; } } return tests; } // Key used by objc_setAssociatedObject static int TestDescriptionKey; static NSString *TestCase_nameOrDescription(id self, SEL cmd) { id description = objc_getAssociatedObject(self, &TestDescriptionKey); NSCAssert(description != nil, @"Value for `TestNameKey` wasn't set."); return description; } static char *const kEventQueueLabel = "xctool.events"; @interface XCToolAssertionHandler : NSAssertionHandler @end @implementation XCToolAssertionHandler - (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ... { // Format message va_list vl; va_start(vl, format); NSString *msg = [[NSString alloc] initWithFormat:format arguments:vl]; va_end(vl); // Raise exception [NSException raise:NSInternalInconsistencyException format:@"*** Assertion failure in %@, %@:%lld: %@", functionName, fileName, (long long)line, msg]; } @end static dispatch_queue_t EventQueue() { static dispatch_queue_t eventQueue = {0}; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // We'll serialize all events through this queue. eventQueue = dispatch_queue_create(kEventQueueLabel, DISPATCH_QUEUE_SERIAL); }); return eventQueue; } static void PrintJSON(id JSONObject) { NSError *error = nil; NSData *data = [NSJSONSerialization dataWithJSONObject:JSONObject options:0 error:&error]; if (error) { fprintf(__stderr, "ERROR: Error generating JSON for object: %s: %s\n", [[JSONObject description] UTF8String], [[error localizedFailureReason] UTF8String]); exit(1); } fwrite([data bytes], 1, [data length], __stdout); fputs("\n", __stdout); fflush(__stdout); } #pragma mark - testSuiteDidStart static void XCToolLog_testSuiteDidStart(NSString *name) { if (__testSuiteDepth > 0) { dispatch_sync(EventQueue(), ^{ PrintJSON(EventDictionaryWithNameAndContent( kReporter_Events_BeginTestSuite, @{kReporter_BeginTestSuite_SuiteKey : name} )); }); } __testSuiteDepth++; } static void XCTestLog_testSuiteDidStart(id self, SEL sel, XCTestSuiteRun *run) { XCToolLog_testSuiteDidStart(kReporter_TestSuite_TopLevelSuiteName); } static void XCTestLog_testSuiteWillStart(id self, SEL sel, XCTestSuite *suite) { XCToolLog_testSuiteDidStart(parseXCTestSuiteKey(suite)); } #pragma mark - testSuiteDidStop static void XCToolLog_testSuiteDidStop(NSString *testSuiteName, XCTestSuiteRun *run) { __testSuiteDepth--; if (__testSuiteDepth > 0) { NSDictionary<NSString *, id> *content = @{ kReporter_EndTestSuite_SuiteKey : testSuiteName, kReporter_EndTestSuite_TestCaseCountKey : @([run testCaseCount]), kReporter_EndTestSuite_TotalFailureCountKey : @([run totalFailureCount]), kReporter_EndTestSuite_UnexpectedExceptionCountKey : @([run unexpectedExceptionCount]), kReporter_EndTestSuite_TestDurationKey: @([run testDuration]), kReporter_EndTestSuite_TotalDurationKey : @([run totalDuration]), }; NSDictionary<NSString *, id> *json = EventDictionaryWithNameAndContent(kReporter_Events_EndTestSuite, content); dispatch_sync(EventQueue(), ^{ PrintJSON(json); }); } } static void XCTestLog_testSuiteDidStop(id self, SEL sel, XCTestSuiteRun *run) { XCToolLog_testSuiteDidStop(kReporter_TestSuite_TopLevelSuiteName, run); } static void XCTestLog_testSuiteDidFinish(id self, SEL sel, XCTestSuite *suite) { XCToolLog_testSuiteDidStop(parseXCTestSuiteKey(suite), (id)suite.testRun); } #pragma mark - testCaseDidStart static void XCToolLog_testCaseDidStart(XCTestCase *testCase) { dispatch_sync(EventQueue(), ^{ NSString *testKey; NSString *className; NSString *methodName; parseXCTestCase(testCase, &className, &methodName, &testKey); PrintJSON(EventDictionaryWithNameAndContent( kReporter_Events_BeginTest, @{ kReporter_BeginTest_TestKey : testKey, kReporter_BeginTest_ClassNameKey : className, kReporter_BeginTest_MethodNameKey : methodName, } )); __testExceptions = [[NSMutableArray alloc] init]; }); } static void XCTestLog_testCaseDidStart(id self, SEL sel, XCTestCaseRun *run) { XCToolLog_testCaseDidStart([run test]); } static void XCTestLog_testCaseWillStart(id self, SEL sel, XCTestCase *testCase) { id (*msgsend)(id, SEL) = (void *) objc_msgSend; XCTestLog_testCaseDidStart(self, sel, msgsend(testCase, @selector(testRun))); } #pragma mark - testCaseDidStop static void XCToolLog_testCaseDidStop(XCTestCase *testCase, NSNumber *unexpectedExceptionCount, NSNumber *failureCount, NSNumber *totalDuration) { dispatch_sync(EventQueue(), ^{ NSString *className = nil; NSString *methodName = nil; NSString *testKey = nil; parseXCTestCase(testCase, &className, &methodName, &testKey); BOOL errored = [unexpectedExceptionCount integerValue] > 0; BOOL failed = [failureCount integerValue] > 0; BOOL succeeded = NO; NSString *result; if (errored) { result = kReporter_EndTest_ResultValueError; } else if (failed) { result = kReporter_EndTest_ResultValueFailure; } else { result = kReporter_EndTest_ResultValueSuccess; succeeded = YES; } // report test results NSArray<NSDictionary<NSString *, id> *> *retExceptions = [__testExceptions copy]; NSDictionary<NSString *, id> *json = EventDictionaryWithNameAndContent( kReporter_Events_EndTest, @{ kReporter_EndTest_TestKey : testKey, kReporter_EndTest_ClassNameKey : className, kReporter_EndTest_MethodNameKey : methodName, kReporter_EndTest_SucceededKey: @(succeeded), kReporter_EndTest_ResultKey : result, kReporter_EndTest_TotalDurationKey : totalDuration, kReporter_EndTest_ExceptionsKey : retExceptions, }); PrintJSON(json); }); } static void XCTestLog_testCaseDidStop(id self, SEL sel, XCTestCaseRun *run) { XCToolLog_testCaseDidStop([run test], @([run unexpectedExceptionCount]), @([run failureCount]), @([run totalDuration])); } static void XCTestLog_testCaseDidFinish(id self, SEL sel, XCTestCase *testCase) { id (*msgsend)(id, SEL) = (void *) objc_msgSend; XCTestLog_testCaseDidStop(self, sel, msgsend(testCase, @selector(testRun))); } #pragma mark - testCaseDidFail static void XCToolLog_testCaseDidFail(NSDictionary *exceptionInfo) { dispatch_sync(EventQueue(), ^{ [__testExceptions addObject:exceptionInfo]; }); } static void XCTestLog_testCaseDidFail(id self, SEL sel, XCTestCaseRun *run, NSString *description, NSString *file, NSUInteger line) { XCToolLog_testCaseDidFail(@{ kReporter_EndTest_Exception_FilePathInProjectKey : file ?: @"Unknown File", kReporter_EndTest_Exception_LineNumberKey : @(line), kReporter_EndTest_Exception_ReasonKey : description, }); } static void XCTestLog_testCaseDidFailWithDescription(id self, SEL sel, XCTestCase *testCase, NSString *description, NSString *file, NSUInteger line) { id (*msgsend)(id, SEL) = (void *) objc_msgSend; XCTestLog_testCaseDidFail(self, sel, msgsend(testCase, @selector(testRun)), description, file, line); } #pragma mark - performTest static void XCPerformTestWithSuppressedExpectedAssertionFailures(id self, SEL origSel, id arg1) { void (*msgsend)(id, SEL, id) = (void *) objc_msgSend; int timeout = [@(getenv("TEST_SHIM_TEST_TIMEOUT") ?: "0") intValue]; NSAssertionHandler *handler = [[XCToolAssertionHandler alloc] init]; NSThread *currentThread = [NSThread currentThread]; NSMutableDictionary *currentThreadDict = [currentThread threadDictionary]; [currentThreadDict setObject:handler forKey:NSAssertionHandlerKey]; if (timeout > 0) { BOOL isSuite = [self isKindOfClass:objc_getClass("XCTestCaseSuite")]; // If running in a suite, time out if we run longer than the combined timeouts of all tests + a fudge factor. int64_t testCount = isSuite ? [[self tests] count] : 1; // When in a suite, add a second per test to help account for the time required to switch tests in a suite. int64_t fudgeFactor = isSuite ? MAX(testCount, 1) : 0; int64_t interval = (timeout * testCount + fudgeFactor) * NSEC_PER_SEC ; NSString *queueName = [NSString stringWithFormat:@"test.timer.%p", self]; dispatch_queue_t queue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)); dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_source_set_timer(source, dispatch_time(DISPATCH_TIME_NOW, interval), 0, 0); dispatch_source_set_event_handler(source, ^{ if (isSuite) { NSString *additionalInformation = @""; if ([self respondsToSelector:@selector(testRun)]) { XCTestRun *run = [self testRun]; NSUInteger executedTests = [run executionCount]; if (executedTests == 0) { additionalInformation = [NSString stringWithFormat:@"(No tests ran, likely stalled in +[%@ setUp])", [self name]]; } else if (executedTests == testCount) { additionalInformation = [NSString stringWithFormat:@"(All tests ran, likely stalled in +[%@ tearDown])", [self name]]; } } [NSException raise:NSInternalInconsistencyException format:@"*** Suite %@ ran longer than combined test time limit: %lld second(s) %@", [self name], testCount * timeout, additionalInformation]; } else { [NSException raise:NSInternalInconsistencyException format:@"*** Test %@ ran longer than specified test time limit: %d second(s)", self, timeout]; } }); dispatch_resume(source); // Call through original implementation msgsend(self, origSel, arg1); dispatch_source_cancel(source); } else { // Call through original implementation msgsend(self, origSel, arg1); } // The assertion handler hasn't been touched for our test, so we can safely remove it. [currentThreadDict removeObjectForKey:NSAssertionHandlerKey]; } static void XCWaitForDebuggerIfNeeded() { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSDictionary<NSString *, NSString *> *env = [[NSProcessInfo processInfo] environment]; BOOL waitForDebugger = [env[kEnv_WaitForDebugger] isEqualToString:@"YES"]; if (waitForDebugger) { int pid = [[NSProcessInfo processInfo] processIdentifier]; NSString *beginMessage = [NSString stringWithFormat:@"Waiting for debugger to be attached to pid '%d' ...", pid]; dispatch_sync(EventQueue(), ^{ PrintJSON(EventDictionaryWithNameAndContent( kReporter_Events_BeginStatus, @{ kReporter_BeginStatus_MessageKey : beginMessage, kReporter_BeginStatus_LevelKey : @"Info" } )); }); // Halt process execution until a debugger is attached raise(SIGSTOP); NSString *endMessage = [NSString stringWithFormat:@"Debugger was successfully attached to pid '%d'.", pid]; dispatch_sync(EventQueue(), ^{ PrintJSON(EventDictionaryWithNameAndContent( kReporter_Events_EndStatus, @{ kReporter_BeginStatus_MessageKey : endMessage, kReporter_BeginStatus_LevelKey : @"Info" } )); }); } }); } static void XCTestCase_performTest(id self, SEL sel, id arg1) { SEL originalSelector = @selector(__XCTestCase_performTest:); XCWaitForDebuggerIfNeeded(); XCPerformTestWithSuppressedExpectedAssertionFailures(self, originalSelector, arg1); } static void XCTestCaseSuite_performTest(id self, SEL sel, id arg1) { SEL originalSelector = @selector(__XCTestCaseSuite_performTest:); XCWaitForDebuggerIfNeeded(); XCPerformTestWithSuppressedExpectedAssertionFailures(self, originalSelector, arg1); } static id XCTRunnerDaemonSession_sharedSession(Class cls, SEL cmd) { return nil; } #pragma mark - _enableSymbolication static BOOL XCTestCase__enableSymbolication(id self, SEL sel) { return NO; } #pragma mark - Interposes /* * We need to close opened fds so all pipe readers are notified and unblocked. * The not obvious and weird part is that we need to print "\n" before closing. * For some reason `select()`, `poll()` and `dispatch_io_read()` will be stuck * if a test calls `exit()` or `abort()`. The found workaround was to print * anithing to a pipe before closing it. Simply closing a pipe doesn't send EOF * to the pipe reader. Printing "\n" should be safe because reader is skipping * empty lines. */ static void PrintNewlineAndCloseFDs() { if (__stdout == NULL) { return; } fprintf(__stdout, "\n"); fclose(__stdout); __stdout = NULL; } #pragma mark - Entry static void SwizzleXCTestMethodsIfAvailable() { if ([[[NSBundle mainBundle] bundleIdentifier] hasPrefix:@"com.apple.dt.xctest"]) { // Start from Xcode 11.1, XCTest will try to connect to testmanagerd service // when reporting test failures (for capture screenshots automatically), and crash // if it cannot make a connection. // We don't really boot the simulator for running logic tests, so just force // it to return nil. static dispatch_once_t token; dispatch_once(&token, ^{ XTSwizzleClassSelectorForFunction( NSClassFromString(@"XCTRunnerDaemonSession"), @selector(sharedSession), (IMP)XCTRunnerDaemonSession_sharedSession ); }); } Class testLogClass = objc_getClass("XCTestLog"); if (testLogClass == nil) { // Looks like the XCTest framework has not been loaded yet. return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ if ([testLogClass instancesRespondToSelector:@selector(testSuiteWillStart:)]) { // Swizzle methods for Xcode 8. XTSwizzleSelectorForFunction( testLogClass, @selector(testSuiteWillStart:), (IMP)XCTestLog_testSuiteWillStart ); XTSwizzleSelectorForFunction( testLogClass, @selector(testSuiteDidFinish:), (IMP)XCTestLog_testSuiteDidFinish ); XTSwizzleSelectorForFunction( testLogClass, @selector(testCaseWillStart:), (IMP)XCTestLog_testCaseWillStart ); XTSwizzleSelectorForFunction( testLogClass, @selector(testCaseDidFinish:), (IMP)XCTestLog_testCaseDidFinish ); XTSwizzleSelectorForFunction( testLogClass, @selector(testCase:didFailWithDescription:inFile:atLine:), (IMP)XCTestLog_testCaseDidFailWithDescription ); } else { // Swizzle methods for Xcode 7 and earlier. XTSwizzleSelectorForFunction( testLogClass, @selector(testSuiteDidStart:), (IMP)XCTestLog_testSuiteDidStart ); XTSwizzleSelectorForFunction( testLogClass, @selector(testSuiteDidStop:), (IMP)XCTestLog_testSuiteDidStop ); XTSwizzleSelectorForFunction( testLogClass, @selector(testCaseDidStart:), (IMP)XCTestLog_testCaseDidStart ); XTSwizzleSelectorForFunction( testLogClass, @selector(testCaseDidStop:), (IMP)XCTestLog_testCaseDidStop ); XTSwizzleSelectorForFunction( testLogClass, @selector(testCaseDidFail:withDescription:inFile:atLine:), (IMP)XCTestLog_testCaseDidFail ); XTSwizzleSelectorForFunction( objc_getClass("XCTestCaseSuite"), @selector(performTest:), (IMP)XCTestCaseSuite_performTest ); } XTSwizzleSelectorForFunction( objc_getClass("XCTestCase"), @selector(performTest:), (IMP)XCTestCase_performTest ); if ([objc_getClass("XCTestCase") respondsToSelector:@selector(_enableSymbolication)]) { // Disable symbolication thing on xctest 7 because it sometimes takes forever. XTSwizzleClassSelectorForFunction( objc_getClass("XCTestCase"), @selector(_enableSymbolication), (IMP)XCTestCase__enableSymbolication ); } }); } static void listBundle(NSString *testBundlePath, NSString *outputFile) { NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:outputFile]; NSBundle *bundle = [NSBundle bundleWithPath:testBundlePath]; if (!bundle) { fprintf( stderr, "Bundle '%s' does not identify an accessible bundle directory.\n", testBundlePath.UTF8String ); exit(TestShimExitCodeBundleOpenError); } if (![bundle executablePath]) { fprintf(stderr, "The bundle at %s does not contain an executable.\n", [testBundlePath UTF8String]); exit(TestShimExitCodeMissingExecutable); } // Make sure the 'XCTest' preference is cleared before we load the // test bundle - otherwise we may accidentally start running tests. // // Instead of seeing the JSON list of test methods, you'll see output like ... // // Test Suite 'All tests' started at 2013-11-07 23:47:46 +0000 // Test Suite 'All tests' finished at 2013-11-07 23:47:46 +0000. // Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.001) seconds // // Here's what happens -- As soon as we dlopen() the test bundle, it will also // trigger the linker to load XCTest.framework since those are linked by the test bundle. // And, as soon as the testing framework loads, the class initializer // '+[XCTestSuite initialize]' is triggered. // // By clearing the preference, we can prevent tests from running. [NSUserDefaults.standardUserDefaults removeObjectForKey:XCTestFrameworkName]; [NSUserDefaults.standardUserDefaults synchronize]; // We use dlopen() instead of -[NSBundle loadAndReturnError] because, if // something goes wrong, dlerror() gives us a much more helpful error message. if (dlopen([[bundle executablePath] UTF8String], RTLD_LAZY) == NULL) { fprintf(stderr, "%s\n", dlerror()); exit(TestShimExitCodeDLOpenError); } // Load the Test Bundle's 'Principal Class' and initialize it. // This is necessary for some Testing Frameworks that dynamically add XCTests // inside the basic `-init` method. Class principalClass = [bundle principalClass]; if (principalClass && [principalClass instancesRespondToSelector:@selector(init)]) { NSLog(@"Calling Principal Class initializer -[%@ init]", NSStringFromClass(principalClass)); id principalObject = [[principalClass alloc] init]; NSLog(@"Principal Class %@ initialized", principalObject); } // Ensure that the principal class exists. Class testSuiteClass = NSClassFromString(XCTestSuiteClassName); NSCAssert(testSuiteClass, @"Should have %@ class", XCTestFrameworkName); // By setting `-XCTest None`, we'll make `-[XCTestSuite allTests]` // return all tests. [NSUserDefaults.standardUserDefaults setObject:@"None" forKey:XCTestFilterArg]; XCTestSuite *allTestsSuite = [testSuiteClass performSelector:@selector(allTests)]; NSCAssert(allTestsSuite, @"Should have gotten a test suite from allTests"); // Enumerate the test cases, constructing the reported name for them. NSArray<XCTestCase *> *allTestCases = TestsFromSuite(allTestsSuite); NSMutableArray<NSDictionary<NSString *, NSString *> *> *testsToReport = NSMutableArray.array; for (XCTestCase *testCase in allTestCases) { NSString *className = nil; NSString *methodName = nil; NSString *testKey = nil; parseXCTestCase(testCase, &className, &methodName, &testKey); NSString *legacyTestName = [NSString stringWithFormat:@"%@/%@", className, methodName]; [testsToReport addObject:@{ kReporter_ListTest_LegacyTestNameKey: legacyTestName, kReporter_ListTest_ClassNameKey: className, kReporter_ListTest_MethodNameKey: methodName, kReporter_ListTest_TestKey: testKey, }]; } // Now write them out after sorting [testsToReport sortUsingComparator:^ NSComparisonResult (NSDictionary<NSString *, NSString *> *left, NSDictionary<NSString *, NSString *> *right) { return [left[kReporter_ListTest_LegacyTestNameKey] compare:right[kReporter_ListTest_LegacyTestNameKey]]; }]; NSError *error = nil; NSData *output = [NSJSONSerialization dataWithJSONObject:testsToReport options:0 error:&error]; NSCAssert(output, @"Failed to generate list test JSON", error); [fileHandle writeData:output]; // Close the file so the other end knows this is the end of the input. [fileHandle closeFile]; exit(TestShimExitCodeSuccess); } static BOOL NSBundle_loadAndReturnError(id self, SEL sel, NSError **error) { BOOL (*msgsend)(id, SEL, NSError **) = (void *) objc_msgSend; SEL originalSelector = @selector(__NSBundle_loadAndReturnError:); BOOL result = msgsend(self, originalSelector, error); SwizzleXCTestMethodsIfAvailable(); return result; } static void assignOutputFiles(void) { static const char *stdoutFileKey = "TEST_SHIM_STDOUT_PATH"; FILE *shimStdoutFile = fopen(getenv(stdoutFileKey), "w"); if (shimStdoutFile) { __stdout = shimStdoutFile; } else { int stdoutHandle = dup(STDOUT_FILENO); __stdout = fdopen(stdoutHandle, "w"); } setvbuf(__stdout, NULL, _IONBF, 0); static const char *stderrFileKey = "TEST_SHIM_STDERR_PATH"; FILE *shimStderrFile = fopen(getenv(stderrFileKey), "w"); if (shimStderrFile) { __stderr = shimStderrFile; } else { int stderrHandle = dup(STDERR_FILENO); __stderr = fdopen(stderrHandle, "w"); } } void handle_signal(int signal) { PrintNewlineAndCloseFDs(); } static id SimServiceContext_deviceSetWithPath_error(id cls, SEL sel, NSString *path, NSError **error) { id (*msgsend)(id, SEL, NSString *, NSError **) = (void *) objc_msgSend; SEL originalSelector = @selector(__SimServiceContext_deviceSetWithPath:error:); NSString *simDeviceSetPath = NSProcessInfo.processInfo.environment[@"SIM_DEVICE_SET_PATH"]; NSLog(@"Calling original -[SimServiceContext deviceSetWithPath:error:] with a custom path: %@", simDeviceSetPath); return msgsend(cls, originalSelector, simDeviceSetPath, error); } static void SwizzleXcodebuildMethods() { static dispatch_once_t token; dispatch_once(&token, ^{ NSLog(@"Swizzling -[SimServiceContext deviceSetWithPath:error:]"); NSBundle *bundle = [[NSBundle alloc] initWithPath:@"/Library/Developer/PrivateFrameworks/CoreSimulator.framework"]; NSError *error = nil; [bundle loadAndReturnError:&error]; if (error) { NSLog(@"ERROR: failed to load CoreSimulator.framework: %@", [error localizedFailureReason]); exit(1); } XTSwizzleSelectorForFunction( // @lint-ignore FBOBJCDISCOURAGEDFUNCTION objc_getClass("SimServiceContext"), @selector(deviceSetWithPath:error:), (IMP)SimServiceContext_deviceSetWithPath_error ); }); } __attribute__((constructor)) static void EntryPoint() { // Unset so we don't cascade into any other process that might be spawned. unsetenv("DYLD_INSERT_LIBRARIES"); NSString *bundlePath = NSProcessInfo.processInfo.environment[@"TEST_SHIM_BUNDLE_PATH"]; if (bundlePath) { assignOutputFiles(); // Listing takes a different path, if the 'TEST_SHIM_OUTPUT_PATH' is set. NSString *listPath = NSProcessInfo.processInfo.environment[@"TEST_SHIM_OUTPUT_PATH"]; if (listPath) { NSLog(@"Querying Bundle %@ to Path %@", bundlePath, listPath); listBundle(bundlePath, listPath); return; } // Install a signal handler to deal with tests crashing. struct sigaction sa_abort; sa_abort.sa_handler = &handle_signal; sigaction(SIGABRT, &sa_abort, NULL); // Let's register to get notified when libraries are initialized XTSwizzleSelectorForFunction([NSBundle class], @selector(loadAndReturnError:), (IMP)NSBundle_loadAndReturnError); // Then Swizzle SwizzleXCTestMethodsIfAvailable(); return; } NSString *simDeviceSetPath = NSProcessInfo.processInfo.environment[@"SIM_DEVICE_SET_PATH"]; if (simDeviceSetPath) { BOOL isDir = NO; if (![[NSFileManager defaultManager] fileExistsAtPath:simDeviceSetPath isDirectory:&isDir]) { NSLog(@"ERROR: SIM_DEVICE_SET_PATH (%@) does not exist", simDeviceSetPath); exit(1); } if (!isDir) { NSLog(@"ERROR: SIM_DEVICE_SET_PATH (%@) is not a directory", simDeviceSetPath); exit(1); } SwizzleXcodebuildMethods(); return; } } __attribute__((destructor)) static void ExitPoint() { PrintNewlineAndCloseFDs(); } #pragma clang diagnostic pop