aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/core/test/firebase/firestore/FSTGoogleTestTests.mm
blob: bb2f836490919022ff2b4a7d3bfe3dca05c0a568 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
/*
 * Copyright 2017 Google
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#import <XCTest/XCTest.h>
#import <objc/runtime.h>

#include "gtest/gtest.h"

/**
 * An XCTest test case that finds C++ test cases written in the GoogleTest
 * framework, runs them, and reports the results back to Xcode. This allows
 * tests written in C++ that don't rely on XCTest to coexist in this project.
 *
 * As an extra feature, you can run all C++ tests by focusing on the GoogleTests
 * class.
 *
 * Each GoogleTest TestCase is mapped to a dynamically generated XCTestCase
 * class. Each GoogleTest TEST() is mapped to a test method on that XCTestCase.
 */
@interface GoogleTests : XCTestCase
@end

namespace {

// A testing::TestCase named "Foo" corresponds to an XCTestCase named
// "FooTests".
NSString *const kTestCaseSuffix = @"Tests";

// A testing::TestInfo named "Foo" corresponds to test method named "testFoo".
NSString *const kTestMethodPrefix = @"test";

// A map of keys created by TestInfoKey to the corresponding testing::TestInfo
// (wrapped in an NSValue). The generated XCTestCase classes are discovered and
// instantiated by XCTest so this is the only means of plumbing per-test-method
// state into these methods.
NSDictionary<NSString *, NSValue *> *testInfosByKey;

// If the user focuses on GoogleTests itself, this means force all C++ tests to
// run.
BOOL forceAllTests = NO;

/**
 * Loads this XCTest runner's configuration file and figures out which tests to
 * run based on the contents of that configuration file.
 *
 * @return the set of tests to run, or nil if the user asked for all tests or if
 * there's any problem loading or parsing the configuration.
 */
NSSet<NSString *> *_Nullable LoadXCTestConfigurationTestsToRun() {
  // Xcode invokes the test runner with an XCTestConfigurationFilePath
  // environment variable set to the path of a configuration file containing,
  // among other things, the set of tests to run. The configuration file
  // deserializes to a non-public XCTestConfiguration class.
  //
  // This loads that file and then reflectively pulls out the testsToRun set.
  // Just in case any of these private details should change in the future and
  // something should fail here, the mechanism complains but fails open. This
  // way the worst that can happen is that users end up running more tests than
  // they intend, but we never accidentally show a green run that wasn't.
  static NSString *const configEnvVar = @"XCTestConfigurationFilePath";

  NSDictionary<NSString *, NSString *> *env =
      [[NSProcessInfo processInfo] environment];
  NSString *filePath = [env objectForKey:configEnvVar];
  if (!filePath) {
    NSLog(@"Missing %@ environment variable; assuming all tests", configEnvVar);
    return nil;
  }

  id config = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
  if (!config) {
    NSLog(@"Failed to load any configuaration from %@=%@", configEnvVar,
          filePath);
    return nil;
  }

  SEL testsToRunSelector = NSSelectorFromString(@"testsToRun");
  if (![config respondsToSelector:testsToRunSelector]) {
    NSLog(@"Invalid configuaration from %@=%@: missing testsToRun",
          configEnvVar, filePath);
    return nil;
  }

  // Invoke the testsToRun selector safely. This indirection is required because
  // just calling -performSelector: fails to properly retain the NSSet under
  // ARC.
  typedef NSSet<NSString *> *(*TestsToRunFunction)(id, SEL);
  IMP testsToRunMethod = [config methodForSelector:testsToRunSelector];
  auto testsToRunFunction =
      reinterpret_cast<TestsToRunFunction>(testsToRunMethod);
  return testsToRunFunction(config, testsToRunSelector);
}

/**
 * Creates a GoogleTest filter specification, suitable for passing to the
 * --gtest_filter flag, that limits GoogleTest to running the same set of tests
 * that Xcode requested.
 *
 * Each member of the testsToRun set is mapped as follows:
 *
 *   * Bare class: "ClassTests" => "Class.*"
 *   * Class and method: "ClassTests/testMethod" => "Class.Method"
 *
 * These members are then joined with a ":" as googletest requires.
 *
 * @see
 * https://github.com/google/googletest/blob/master/googletest/docs/AdvancedGuide.md
 */
NSString *CreateTestFiltersFromTestsToRun(NSSet<NSString *> *testsToRun) {
  NSMutableString *result = [[NSMutableString alloc] init];
  for (NSString *spec in testsToRun) {
    NSArray<NSString *> *parts = [spec componentsSeparatedByString:@"/"];

    NSString *gtestCaseName = nil;
    if (parts.count > 0) {
      NSString *className = parts[0];
      if ([className hasSuffix:kTestCaseSuffix]) {
        gtestCaseName = [className
            substringToIndex:className.length - kTestCaseSuffix.length];
      }
    }

    NSString *gtestMethodName = nil;
    if (parts.count > 1) {
      NSString *methodName = parts[1];
      if ([methodName hasPrefix:kTestMethodPrefix]) {
        gtestMethodName =
            [methodName substringFromIndex:kTestMethodPrefix.length];
      }
    }

    if (gtestCaseName) {
      if (result.length > 0) {
        [result appendString:@":"];
      }
      [result appendString:gtestCaseName];
      [result appendString:@"."];
      [result appendString:(gtestMethodName ? gtestMethodName : @"*")];
    }
  }

  return result;
}

/** Returns the name of the selector for the test method representing this
 * specific test. */
NSString *SelectorNameForTestInfo(const testing::TestInfo *testInfo) {
  return
      [NSString stringWithFormat:@"%@%s", kTestMethodPrefix, testInfo->name()];
}

/** Returns the name of the class representing the given testing::TestCase. */
NSString *ClassNameForTestCase(const testing::TestCase *testCase) {
  return [NSString stringWithFormat:@"%s%@", testCase->name(), kTestCaseSuffix];
}

/**
 * Returns a key name for the testInfosByKey dictionary. Each (class, selector)
 * pair corresponds to a unique GoogleTest result.
 */
NSString *TestInfoKey(Class testClass, SEL testSelector) {
  return [NSString stringWithFormat:@"%@.%@", NSStringFromClass(testClass),
                                    NSStringFromSelector(testSelector)];
}

/**
 * Looks up the testing::TestInfo for this test method and reports on the
 * outcome to XCTest, as if the test actually ran in this method.
 *
 * Note: this function is the implementation for each generated test method. It
 * shouldn't be used directly. The parameter names of self and _cmd match up
 * with the implicit parameters passed to any Objective-C method. Naming them
 * this way here allows XCTAssert and friends to work.
 */
void ReportTestResult(XCTestCase *self, SEL _cmd) {
  NSString *testInfoKey = TestInfoKey([self class], _cmd);
  NSValue *holder = testInfosByKey[testInfoKey];
  auto testInfo = static_cast<const testing::TestInfo *>(holder.pointerValue);
  if (!testInfo) {
    return;
  }

  if (!testInfo->should_run()) {
    // Test was filtered out by gunit; nothing to report.
    return;
  }

  const testing::TestResult *result = testInfo->result();
  if (result->Passed()) {
    // Let XCode know that the test ran and succeeded.
    XCTAssertTrue(true);
    return;
  }

  // Test failed :-(. Record the failure such that XCode will navigate directly
  // to the file:line.
  int parts = result->total_part_count();
  for (int i = 0; i < parts; i++) {
    const testing::TestPartResult &part = result->GetTestPartResult(i);
    [self
        recordFailureWithDescription:@(part.message())
                              inFile:@(part.file_name() ? part.file_name() : "")
                              atLine:(part.line_number() > 0
                                          ? part.line_number()
                                          : 0)
                            expected:YES];
  }
}

/**
 * Generates a new subclass of XCTestCase for the given GoogleTest TestCase.
 * Each TestInfo (which represents an indivudal test method execution) is
 * translated into a method on the test case.
 *
 * @param The testing::TestCase of interest to translate.
 * @param A map of TestInfoKeys to testing::TestInfos, populated by this method.
 *
 * @return A new Class that's a subclass of XCTestCase, that's been registered
 * with the Objective-C runtime.
 */
Class CreateXCTestCaseClass(
    const testing::TestCase *testCase,
    NSMutableDictionary<NSString *, NSValue *> *infoMap) {
  NSString *testCaseName = ClassNameForTestCase(testCase);
  Class testClass =
      objc_allocateClassPair([XCTestCase class], [testCaseName UTF8String], 0);

  // Create a method for each TestInfo.
  int testInfos = testCase->total_test_count();
  for (int j = 0; j < testInfos; j++) {
    const testing::TestInfo *testInfo = testCase->GetTestInfo(j);

    NSString *selectorName = SelectorNameForTestInfo(testInfo);
    SEL selector = sel_registerName([selectorName UTF8String]);

    // Use the ReportTestResult function as the method implementation. The v@:
    // indicates it is a void objective-C method; this must continue to match
    // the signature of ReportTestResult.
    IMP method = reinterpret_cast<IMP>(ReportTestResult);
    class_addMethod(testClass, selector, method, "v@:");

    NSString *infoKey = TestInfoKey(testClass, selector);
    NSValue *holder = [NSValue valueWithPointer:testInfo];
    infoMap[infoKey] = holder;
  }
  objc_registerClassPair(testClass);

  return testClass;
}

/**
 * Creates a test suite containing all C++ tests, used when the user starts the
 * GoogleTests class.
 *
 * Note: normally XCTest finds all the XCTestCase classes that are registered
 * with the run time and asks them to create suites for themselves. When a user
 * focuses on the GoogleTests class, XCTest no longer does this so we have to
 * force XCTest to see more tests than it would normally look at so that the
 * indicators in the test navigator update properly.
 */
XCTestSuite *CreateAllTestsTestSuite() {
  XCTestSuite *allTestsSuite =
      [[XCTestSuite alloc] initWithName:@"All GoogleTest Tests"];
  [allTestsSuite
      addTest:[XCTestSuite testSuiteForTestCaseClass:[GoogleTests class]]];

  const testing::UnitTest *master = testing::UnitTest::GetInstance();

  int testCases = master->total_test_case_count();
  for (int i = 0; i < testCases; i++) {
    const testing::TestCase *testCase = master->GetTestCase(i);
    NSString *testCaseName = ClassNameForTestCase(testCase);
    Class testClass = objc_getClass([testCaseName UTF8String]);
    [allTestsSuite addTest:[XCTestSuite testSuiteForTestCaseClass:testClass]];
  }

  return allTestsSuite;
}

/**
 * Finds and runs googletest-based tests based on the XCTestConfiguration of the
 * current test invocation.
 */
void RunGoogleTestTests() {
  NSString *masterTestCaseName = NSStringFromClass([GoogleTests class]);

  // Initialize GoogleTest but don't run the tests yet.
  int argc = 1;
  const char *argv[] = {[masterTestCaseName UTF8String]};
  testing::InitGoogleTest(&argc, const_cast<char **>(argv));

  // Convert XCTest's testToRun set to the equivalent --gtest_filter flag.
  //
  // Note that we only set forceAllTests to YES if the user specifically focused
  // on GoogleTests. This prevents XCTest double-counting test cases (and
  // failures) when a user asks for all tests.
  NSSet<NSString *> *allTests = [NSSet setWithObject:masterTestCaseName];
  NSSet<NSString *> *testsToRun = LoadXCTestConfigurationTestsToRun();
  if (testsToRun) {
    if ([allTests isEqual:testsToRun]) {
      NSLog(@"Forcing all tests to run");
      forceAllTests = YES;
    } else {
      NSString *filters = CreateTestFiltersFromTestsToRun(testsToRun);
      NSLog(@"Using --gtest_filter=%@", filters);
      if (filters) {
        testing::GTEST_FLAG(filter) = [filters UTF8String];
      }
    }
  }

  // Create XCTestCases and populate the testInfosByKey map
  const testing::UnitTest *master = testing::UnitTest::GetInstance();
  NSMutableDictionary<NSString *, NSValue *> *infoMap =
      [NSMutableDictionary dictionaryWithCapacity:master->total_test_count()];

  int testCases = master->total_test_case_count();
  for (int i = 0; i < testCases; i++) {
    const testing::TestCase *testCase = master->GetTestCase(i);
    CreateXCTestCaseClass(testCase, infoMap);
  }
  testInfosByKey = infoMap;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-result"
  // RUN_ALL_TESTS by default doesn't want you to ignore its result, but it's
  // safe here. Test failures are already logged by GoogleTest itself (and then
  // again by XCTest). Test failures are reported via
  // -recordFailureWithDescription:inFile:atLine:expected: which then causes
  // XCTest itself to fail the run.
  RUN_ALL_TESTS();
#pragma clang diagnostic pop
}

}  // namespace

@implementation GoogleTests

+ (XCTestSuite *)defaultTestSuite {
  // Only return all tests beyond GoogleTests if the user is focusing on
  // GoogleTests.
  if (forceAllTests) {
    return CreateAllTestsTestSuite();
  } else {
    // just run the tests that are a part of this class
    return [XCTestSuite testSuiteForTestCaseClass:[self class]];
  }
}

- (void)testGoogleTestsActuallyRun {
  // This whole mechanism is sufficiently tricky that we should verify that the
  // build actually plumbed this together correctly.
  const testing::UnitTest *master = testing::UnitTest::GetInstance();
  XCTAssertGreaterThan(master->total_test_case_count(), 0);
}

@end

/**
 * This class is registered as the NSPrincipalClass in the Firestore_Tests
 * bundle's Info.plist. XCTest instantiates this class to perform one-time setup
 * for the test bundle, as documented here:
 *
 *   https://developer.apple.com/documentation/xctest/xctestobservationcenter
 */
@interface FSTGoogleTestsPrincipal : NSObject
@end

@implementation FSTGoogleTestsPrincipal

- (instancetype)init {
  self = [super init];
  RunGoogleTestTests();
  return self;
}

@end