blob: 68978ecea27cf4791cfb0369d8b9587dff8bcfcc [file] [log] [blame] [edit]
/*
* Copyright (c) 2013 Matthew Stevens
* Copyright (C) 2024 Apple Inc. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import "TestBundleLoader.h"
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import <gtest/gtest.h>
#import <objc/runtime.h>
using testing::TestCase;
using testing::TestInfo;
using testing::TestPartResult;
using testing::UnitTest;
static NSString * const GoogleTestDisabledPrefix = @"DISABLED_";
/**
* Class prefix used for generated Objective-C class names.
*
* If a class name generated for a Google Test case conflicts with an existing
* class the value of this variable can be changed to add a class prefix.
*/
static NSString * const GeneratedClassPrefix = @"GTEST_";
/**
* Map of test keys to Google Test filter strings.
*
* Some names allowed by Google Test would result in illegal Objective-C
* identifiers and in such cases the generated class and method names are
* adjusted to handle this. This map is used to obtain the original Google Test
* filter string associated with a generated Objective-C test method.
*/
static NSDictionary *GoogleTestFilterMap;
/**
* A Google Test listener that reports failures to XCTest.
*/
class XCTestListener : public testing::EmptyTestEventListener {
public:
XCTestListener(XCTestCase *testCase)
: _testCase(testCase)
{
}
void OnTestPartResult(const TestPartResult& test_part_result)
{
if (test_part_result.passed())
return;
int lineNumber = test_part_result.line_number();
const char *fileName = test_part_result.file_name();
NSString *path = fileName ? [@(fileName) stringByStandardizingPath] : nil;
NSString *description = @(test_part_result.message());
[_testCase recordFailureWithDescription:description inFile:path atLine:(lineNumber >= 0 ? (NSUInteger)lineNumber : 0) expected:YES];
}
private:
XCTestCase *_testCase;
};
/**
* Registers an XCTestCase subclass for each Google Test case.
*
* Generating these classes allows Google Test cases to be represented as peers
* of standard XCTest suites and supports filtering of test runs to specific
* Google Test cases or individual tests via Xcode.
*/
@interface GoogleTestLoader : NSObject <TestBundleLoader>
@end
/**
* Base class for the generated classes for Google Test cases.
*/
@interface GoogleTestCase : XCTestCase
@end
@implementation GoogleTestCase
/**
* Associates generated Google Test classes with the test bundle.
*
* This affects how the generated test cases are represented in reports. By
* associating the generated classes with a test bundle the Google Test cases
* appear to be part of the same test bundle that this source file is compiled
* into. Without this association they appear to be part of a bundle
* representing the directory of an internal Xcode tool that runs the tests.
*/
+ (NSBundle *)bundleForClass
{
return [NSBundle bundleForClass:[GoogleTestLoader class]];
}
/**
* Implementation of +[XCTestCase testInvocations] that returns an array of test
* invocations for each test method in the class.
*
* This differs from the standard implementation of testInvocations, which only
* adds methods with a prefix of "test".
*/
+ (NSArray *)testInvocations
{
NSMutableArray *invocations = [NSMutableArray array];
unsigned methodCount = 0;
Method *methods = class_copyMethodList([self class], &methodCount);
for (unsigned i = 0; i < methodCount; i++) {
SEL sel = method_getName(methods[i]);
NSMethodSignature *sig = [self instanceMethodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setSelector:sel];
[invocations addObject:invocation];
}
free(methods);
return invocations;
}
@end
/**
* Runs a single test.
*/
static void RunTest(id self, SEL _cmd)
{
XCTestListener *listener = new XCTestListener(self);
UnitTest *googleTest = UnitTest::GetInstance();
googleTest->listeners().Append(listener);
NSString *testKey = [NSString stringWithFormat:@"%@.%@", [self class], NSStringFromSelector(_cmd)];
NSString *testFilter = GoogleTestFilterMap[testKey];
XCTAssertNotNil(testFilter, @"No test filter found for test %@", testKey);
testing::GTEST_FLAG(filter) = [testFilter UTF8String];
(void)RUN_ALL_TESTS();
delete googleTest->listeners().Release(listener);
int totalTestsRun = googleTest->successful_test_count() + googleTest->failed_test_count();
XCTAssertEqual(totalTestsRun, 1, @"Expected to run a single test for filter \"%@\"", testFilter);
}
@implementation GoogleTestLoader
+ (void)registerTestClasses
{
// Pass the command-line arguments to Google Test to support the --gtest options
NSArray *arguments = [[NSProcessInfo processInfo] arguments];
int i = 0;
int argc = (int)[arguments count];
const char **argv = (const char **)calloc((unsigned)argc + 1, sizeof(const char *));
for (NSString *arg in arguments)
argv[i++] = [arg UTF8String];
testing::InitGoogleTest(&argc, (char **)argv);
UnitTest *googleTest = UnitTest::GetInstance();
testing::TestEventListeners& listeners = googleTest->listeners();
delete listeners.Release(listeners.default_result_printer());
free(argv);
bool shouldListTestsOnly = testing::GTEST_FLAG(list_tests);
// Calling RUN_ALL_TESTS() has the side effect of applying --gtest_filter to all test cases,
// properly setting the value of TestCase::should_run() and TestInfo::should_run(). Since
// GTEST_FLAG(list_tests) is set, no tests will actually be run.
testing::GTEST_FLAG(list_tests) = true;
(void)RUN_ALL_TESTS();
// If the user specified --gtest_list_tests, skip registering XCTestCases. Otherwise, set the
// list_tests flag back to false and continue.
if (shouldListTestsOnly)
return;
testing::GTEST_FLAG(list_tests) = false;
BOOL runDisabledTests = testing::GTEST_FLAG(also_run_disabled_tests);
NSMutableDictionary *testFilterMap = [NSMutableDictionary dictionary];
NSCharacterSet *decimalDigitCharacterSet = [NSCharacterSet decimalDigitCharacterSet];
for (int testCaseIndex = 0; testCaseIndex < googleTest->total_test_case_count(); testCaseIndex++) {
const TestCase *testCase = googleTest->GetTestCase(testCaseIndex);
if (!testCase->should_run())
continue;
NSString *testCaseName = @(testCase->name());
// For typed tests '/' is used to separate the parts of the test case name.
NSArray *testCaseNameComponents = [testCaseName componentsSeparatedByString:@"/"];
if (runDisabledTests == NO) {
BOOL testCaseDisabled = NO;
for (NSString *component in testCaseNameComponents) {
if ([component hasPrefix:GoogleTestDisabledPrefix]) {
testCaseDisabled = YES;
break;
}
}
if (testCaseDisabled)
continue;
}
// Join the test case name components with '_' rather than '/' to create
// a valid class name.
NSString *className = [GeneratedClassPrefix stringByAppendingString:[testCaseNameComponents componentsJoinedByString:@"_"]];
Class testClass = objc_allocateClassPair([GoogleTestCase class], [className UTF8String], 0);
NSAssert1(testClass, @"Failed to register Google Test class \"%@\", this class may already exist. The value of GeneratedClassPrefix can be changed to avoid this.", className);
BOOL hasMethods = NO;
for (int testIndex = 0; testIndex < testCase->total_test_count(); testIndex++) {
const TestInfo *testInfo = testCase->GetTestInfo(testIndex);
if (!testInfo->should_run())
continue;
NSString *testName = @(testInfo->name());
if (runDisabledTests == NO && [testName hasPrefix:GoogleTestDisabledPrefix])
continue;
// Google Test allows test names starting with a digit, prefix these with an
// underscore to create a valid method name.
NSString *methodName = testName;
if ([methodName length] > 0 && [decimalDigitCharacterSet characterIsMember:[methodName characterAtIndex:0]])
methodName = [@"_" stringByAppendingString:methodName];
NSString *testKey = [NSString stringWithFormat:@"%@.%@", className, methodName];
NSString *testFilter = [NSString stringWithFormat:@"%@.%@", testCaseName, testName];
testFilterMap[testKey] = testFilter;
SEL selector = sel_registerName([methodName UTF8String]);
BOOL added = class_addMethod(testClass, selector, (IMP)RunTest, "v@:");
// FIXME: (283036) PrintWithJSExecutionOptionTests API tests are duplicated in the testing system
NSSet<NSString *> *testsExcludedFromAssertion = [NSSet setWithArray:@[
@"UnifiedPDF/PrintWithJSExecutionOptionTests",
@"Printing/PrintWithJSExecutionOptionTests",
]];
NSAssert([testsExcludedFromAssertion containsObject:testCaseName] || added, @"Failed to add Google Test method \"%@\", this method may already exist in the class.", methodName);
hasMethods = YES;
}
if (hasMethods)
objc_registerClassPair(testClass);
else
objc_disposeClassPair(testClass);
}
GoogleTestFilterMap = testFilterMap;
}
@end