Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qxctestlogger.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include <QtCore/qstring.h>
7
8#include <QtTest/private/qtestlog_p.h>
9#include <QtTest/private/qtestresult_p.h>
10
11#import <XCTest/XCTest.h>
12
13// This XCode logging integration has probably bit-rotted since it was written.
14// It is not even compiled as part of normal builds.
15
16// ---------------------------------------------------------
17
18@interface XCTestProbe (Private)
19+ (BOOL)isTesting;
20+ (void)runTests:(id)unusedArgument;
21+ (NSString*)testScope;
22+ (BOOL)isInverseTestScope;
23@end
24
25@interface XCTestDriver : NSObject
26+ (XCTestDriver*)sharedTestDriver;
27@property (readonly, assign) NSObject *IDEConnection;
28@end
29
30@interface XCTest (Private)
31- (NSString *)nameForLegacyLogging;
32@end
33
34QT_WARNING_PUSH
35// Ignore XCTestProbe deprecation
36QT_WARNING_DISABLE_DEPRECATED
37
38// ---------------------------------------------------------
39
40@interface QtTestLibWrapper : XCTestCase
41@end
42
43@interface QtTestLibTests : XCTestSuite
44+ (XCTestSuiteRun*)testRun;
45@end
46
47@interface QtTestLibTest : XCTestCase
48@property (nonatomic, retain) NSString* testObjectName;
49@property (nonatomic, retain) NSString* testFunctionName;
50@end
51
52// ---------------------------------------------------------
53
54class ThreadBarriers
55{
56public:
57 enum Barrier {
58 XCTestCanStartTesting,
59 XCTestHaveStarted,
60 QtTestsCanStartTesting,
61 QtTestsHaveCompleted,
62 XCTestsHaveCompleted,
63 BarrierCount
64 };
65
66 static ThreadBarriers *get()
67 {
68 static ThreadBarriers instance;
69 return &instance;
70 }
71
72 static void initialize() { get(); }
73
74 void wait(Barrier barrier) { dispatch_semaphore_wait(barriers[barrier], DISPATCH_TIME_FOREVER); }
75 void signal(Barrier barrier) { dispatch_semaphore_signal(barriers[barrier]); }
76
77private:
78 #define FOREACH_BARRIER(cmd) for (int i = 0; i < BarrierCount; ++i) { cmd }
79
80 ThreadBarriers() { FOREACH_BARRIER(barriers[i] = dispatch_semaphore_create(0);) }
81 ~ThreadBarriers() { FOREACH_BARRIER(dispatch_release(barriers[i]);) }
82
83 dispatch_semaphore_t barriers[BarrierCount];
84};
85
86#define WAIT_FOR_BARRIER(b) ThreadBarriers::get()->wait(ThreadBarriers::b);
87#define SIGNAL_BARRIER(b) ThreadBarriers::get()->signal(ThreadBarriers::b);
88
89// ---------------------------------------------------------
90
91@implementation QtTestLibWrapper
92
93+ (void)load
94{
95 NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init];
96
97 if (![XCTestProbe isTesting])
98 return;
99
100 if (Q_UNLIKELY(!([NSDate timeIntervalSinceReferenceDate] > 0)))
101 qFatal("error: Device date '%s' is bad, likely set to update automatically. Please correct.",
102 [[NSDate date] description].UTF8String);
103
104 XCTestDriver *testDriver = nil;
105 if ([QtTestLibWrapper usingTestManager])
106 testDriver = [XCTestDriver sharedTestDriver];
107
108 // Spawn off task to run test infrastructure on separate thread so that we can
109 // let main() execute like normal on the main thread. The queue will never be
110 // destroyed, so there's no point in trying to keep a proper retain count.
111 dispatch_async(dispatch_queue_create("io.qt.QTestLib.xctest-wrapper", DISPATCH_QUEUE_SERIAL), ^{
112 Q_ASSERT(![NSThread isMainThread]);
113 [XCTestProbe runTests:nil];
114 Q_UNREACHABLE();
115 });
116
117 // Initialize barriers before registering exit handler so that the
118 // semaphores stay alive until after the exit handler completes.
119 ThreadBarriers::initialize();
120
121 // We register an exit handler so that we can intercept when main() completes
122 // and let the XCTest thread finish up. For main() functions that never started
123 // testing using QtTestLib we also need to signal that xcTestsCanStart.
124 atexit_b(^{
125 Q_ASSERT([NSThread isMainThread]);
126
127 // In case not started by startLogging
128 SIGNAL_BARRIER(XCTestCanStartTesting);
129
130 // [XCTestProbe runTests:] ends up calling [XCTestProbe exitTests:] after
131 // all test suites have completed, which calls exit(). We use that to signal
132 // to the main thread that it's free to continue its exit handler.
133 atexit_b(^{
134 Q_ASSERT(![NSThread isMainThread]);
135 SIGNAL_BARRIER(XCTestsHaveCompleted);
136
137 // Block forever so that the main thread does all the cleanup
138 dispatch_semaphore_wait(dispatch_semaphore_create(0), DISPATCH_TIME_FOREVER);
139 });
140
141 SIGNAL_BARRIER(QtTestsHaveCompleted);
142
143 // Ensure XCTest complets the top level tests suite
144 WAIT_FOR_BARRIER(XCTestsHaveCompleted);
145 });
146
147 // Let test driver (Xcode) connection setup complete before continuing
148 if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--use-testmanagerd"]) {
149 while (!testDriver.IDEConnection)
150 [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
151 }
152
153 // Wait for our QtTestLib test suite to run before running main
154 WAIT_FOR_BARRIER(QtTestsCanStartTesting);
155
156 // Prevent XCTestProbe from re-launching runTests on application startup
157 [[NSNotificationCenter defaultCenter] removeObserver:[XCTestProbe class]
158 name:[NSString stringWithFormat:@"%@DidFinishLaunchingNotification",
159 #if defined(Q_OS_MACOS)
160 @"NSApplication"
161 #else
162 @"UIApplication"
163 #endif
164 ]
165 object:nil];
166
167 [autoreleasepool release];
168}
169
170+ (QTestLibTests *)defaultTestSuite
171{
172 return [[QtTestLibTests alloc] initWithName:@"QtTestLib"];
173}
174
175+ (BOOL)usingTestManager
176{
177 return [[[NSProcessInfo processInfo] arguments] containsObject:@"--use-testmanagerd"];
178}
179
180@end
181
182// ---------------------------------------------------------
183
184static XCTestSuiteRun *s_qtTestSuiteRun = nullptr;
185
186@implementation QtTestLibTests
187
188- (void)performTest:(XCTestSuiteRun *)testSuiteRun
189{
190 Q_ASSERT(![NSThread isMainThread]);
191
192 Q_ASSERT(!s_qtTestSuiteRun);
193 s_qtTestSuiteRun = testSuiteRun;
194
195 SIGNAL_BARRIER(QtTestsCanStartTesting);
196
197 // Wait for main() to complete, or a QtTestLib test to start, so we
198 // know if we should start the QtTestLib test suite.
199 WAIT_FOR_BARRIER(XCTestCanStartTesting);
200
201 if (QXcodeTestLogger::isActive())
202 [testSuiteRun start];
203
204 SIGNAL_BARRIER(XCTestHaveStarted);
205
206 // All test reporting happens on main thread from now on. Wait until
207 // main() completes before allowing the XCTest thread to continue.
208 WAIT_FOR_BARRIER(QtTestsHaveCompleted);
209
210 if ([testSuiteRun startDate])
211 [testSuiteRun stop];
212}
213
214+ (XCTestSuiteRun*)testRun
215{
216 return s_qtTestSuiteRun;
217}
218
219@end
220
221// ---------------------------------------------------------
222
223@implementation QtTestLibTest
224
225- (instancetype)initWithInvocation:(NSInvocation *)invocation
226{
227 if (self = [super initWithInvocation:invocation]) {
228 // The test object name and function name are used by XCTest after QtTestLib has
229 // reset them, so we need to store them up front for each XCTestCase.
230 self.testObjectName = [NSString stringWithUTF8String:QTestResult::currentTestObjectName()];
231 self.testFunctionName = [NSString stringWithUTF8String:QTestResult::currentTestFunction()];
232 }
233
234 return self;
235}
236
237- (NSString *)testClassName
238{
239 return self.testObjectName;
240}
241
242- (NSString *)testMethodName
243{
244 return self.testFunctionName;
245}
246
247- (NSString *)nameForLegacyLogging
248{
249 NSString *name = [NSString stringWithFormat:@"%@::%@", [self testClassName], [self testMethodName]];
250 if (QTestResult::currentDataTag() || QTestResult::currentGlobalDataTag()) {
251 const char *currentDataTag = QTestResult::currentDataTag() ? QTestResult::currentDataTag() : "";
252 const char *globalDataTag = QTestResult::currentGlobalDataTag() ? QTestResult::currentGlobalDataTag() : "";
253 const char *filler = (currentDataTag[0] && globalDataTag[0]) ? ":" : "";
254 name = [name stringByAppendingString:[NSString stringWithFormat:@"(%s%s%s)",
255 globalDataTag, filler, currentDataTag]];
256 }
257
258 return name;
259}
260
261@end
262
263// ---------------------------------------------------------
264
265bool QXcodeTestLogger::canLogTestProgress()
266{
267 return [XCTestProbe isTesting]; // FIXME: Exclude xctool
268}
269
270int QXcodeTestLogger::parseCommandLineArgument(const char *argument)
271{
272 if (strncmp(argument, "-NS", 3) == 0 || strncmp(argument, "-Apple", 6) == 0)
273 return 2; // -NSTreatUnknownArgumentsAsOpen, -ApplePersistenceIgnoreState, etc, skip argument
274 else if (strcmp(argument, "--use-testmanagerd") == 0)
275 return 2; // Skip UID argument
276 else if (strncmp(argument, "-XCTest", 7) == 0)
277 return 2; // -XCTestInvertScope, -XCTest scope, etc, skip argument
278 else if (strcmp(argument + (strlen(argument) - 7), ".xctest") == 0)
279 return 1; // Skip test bundle
280 else
281 return 0;
282}
283
284// ---------------------------------------------------------
285
286QXcodeTestLogger *QXcodeTestLogger::s_currentTestLogger = 0;
287
288// ---------------------------------------------------------
289
290QXcodeTestLogger::QXcodeTestLogger()
291 : QAbstractTestLogger(0)
292 , m_testRuns([[NSMutableArray<XCTestRun *> arrayWithCapacity:2] retain])
293
294{
295 Q_ASSERT(!s_currentTestLogger);
296 s_currentTestLogger = this;
297}
298
299QXcodeTestLogger::~QXcodeTestLogger()
300{
301 s_currentTestLogger = 0;
302 [m_testRuns release];
303}
304
305void QXcodeTestLogger::startLogging()
306{
307 SIGNAL_BARRIER(XCTestCanStartTesting);
308
309 static dispatch_once_t onceToken;
310 dispatch_once (&onceToken, ^{
311 WAIT_FOR_BARRIER(XCTestHaveStarted);
312 });
313
314 // Scope test object suite under top level QtTestLib test run
315 [m_testRuns addObject:[QtTestLibTests testRun]];
316
317 NSString *suiteName = [NSString stringWithUTF8String:QTestResult::currentTestObjectName()];
318 pushTestRunForTest([XCTestSuite testSuiteWithName:suiteName], true);
319}
320
321void QXcodeTestLogger::stopLogging()
322{
323 popTestRun();
324}
325
326static bool isTestFunctionInActiveScope(const char *function)
327{
328 static NSString *testScope = [XCTestProbe testScope];
329
330 enum TestScope { Unknown, All, None, Self, Selected };
331 static TestScope activeScope = Unknown;
332
333 if (activeScope == Unknown) {
334 if ([testScope isEqualToString:@"All"])
335 activeScope = All;
336 else if ([testScope isEqualToString:@"None"])
337 activeScope = None;
338 else if ([testScope isEqualToString:@"Self"])
339 activeScope = Self;
340 else
341 activeScope = Selected;
342 }
343
344 if (activeScope == All)
345 return true;
346 else if (activeScope == None)
347 return false;
348 else if (activeScope == Self)
349 return true; // Investigate
350
351 Q_ASSERT(activeScope == Selected);
352
353 static NSArray<NSString *> *forcedTests = [@[ @"initTestCase", @"initTestCase_data", @"cleanupTestCase" ] retain];
354 if ([forcedTests containsObject:[NSString stringWithUTF8String:function]])
355 return true;
356
357 static NSArray<NSString *> *testsInScope = [[testScope componentsSeparatedByString:@","] retain];
358 bool inScope = [testsInScope containsObject:[NSString stringWithFormat:@"%s/%s",
359 QTestResult::currentTestObjectName(), function]];
360
361 if ([XCTestProbe isInverseTestScope])
362 inScope = !inScope;
363
364 return inScope;
365}
366
367void QXcodeTestLogger::enterTestFunction(const char *function)
368{
369 if (!isTestFunctionInActiveScope(function))
370 QTestResult::setSkipCurrentTest(true);
371
372 XCTest *test = [QtTestLibTest testCaseWithInvocation:nil];
373 pushTestRunForTest(test, !QTestResult::skipCurrentTest());
374}
375
376void QXcodeTestLogger::leaveTestFunction()
377{
378 popTestRun();
379}
380
381void QXcodeTestLogger::addIncident(IncidentTypes type, const char *description,
382 const char *file, int line)
383{
384 XCTestRun *testRun = [m_testRuns lastObject];
385
386 // The 'expected' argument to recordFailureWithDescription refers to whether
387 // the failure was a regular failed assertion, or an unexpected exception,
388 // so in our case it's always 'YES', and we need to explicitly ignore XFail.
389 if (type == QAbstractTestLogger::XFail) {
390 QTestCharBuffer buf;
391 NSString *testCaseName = [[testRun test] nameForLegacyLogging];
392 QTest::qt_asprintf(&buf, "Test Case '%s' failed expectedly (%s).\n",
393 [testCaseName UTF8String], description);
394 outputString(buf.constData());
395 return;
396 }
397
398 if (type == QAbstractTestLogger::Pass) {
399 // We ignore non-data passes, as we're already reporting that as part of the
400 // normal test case start/stop cycle.
401 if (!(QTestResult::currentDataTag() || QTestResult::currentGlobalDataTag()))
402 return;
403
404 QTestCharBuffer buf;
405 NSString *testCaseName = [[testRun test] nameForLegacyLogging];
406 QTest::qt_asprintf(&buf, "Test Case '%s' passed.\n", [testCaseName UTF8String]);
407 outputString(buf.constData());
408 return;
409 }
410
411 // FIXME: Handle blacklisted tests
412
413 if (!file || !description)
414 return; // Or report?
415
416 [testRun recordFailureWithDescription:[NSString stringWithUTF8String:description]
417 inFile:[NSString stringWithUTF8String:file] atLine:line expected:YES];
418}
419
420void QXcodeTestLogger::addMessage(MessageTypes type, const QString &message,
421 const char *file, int line)
422{
423 QTestCharBuffer buf;
424
425 if (QTestLog::verboseLevel() > 0 && (file && line)) {
426 QTest::qt_asprintf(&buf, "%s:%d: ", file, line);
427 outputString(buf.constData());
428 }
429
430 if (type == QAbstractTestLogger::Skip) {
431 XCTestRun *testRun = [m_testRuns lastObject];
432 NSString *testCaseName = [[testRun test] nameForLegacyLogging];
433 QTest::qt_asprintf(&buf, "Test Case '%s' skipped (%s).\n",
434 [testCaseName UTF8String], message.toUtf8().constData());
435 } else {
436 QTest::qt_asprintf(&buf, "%s\n", message.toUtf8().constData());
437 }
438
439 outputString(buf.constData());
440}
441
442void QXcodeTestLogger::addBenchmarkResult(const QBenchmarkResult &result)
443{
444 Q_UNUSED(result);
445}
446
447void QXcodeTestLogger::pushTestRunForTest(XCTest *test, bool start)
448{
449 XCTestRun *testRun = [[test testRunClass] testRunWithTest:test];
450 [m_testRuns addObject:testRun];
451
452 if (start)
453 [testRun start];
454}
455
456XCTestRun *QXcodeTestLogger::popTestRun()
457{
458 XCTestRun *testRun = [[m_testRuns lastObject] retain];
459 [m_testRuns removeLastObject];
460
461 if ([testRun startDate])
462 [testRun stop];
463
464 [[m_testRuns lastObject] addTestRun:testRun];
465 [testRun release];
466
467 return testRun;
468}
469
470bool QXcodeTestLogger::isActive()
471{
472 return s_currentTestLogger;
473}
474
475QT_WARNING_POP
Q_FORWARD_DECLARE_OBJC_CLASS(NSString)
#define WAIT_FOR_BARRIER(b)
#define SIGNAL_BARRIER(b)
#define FOREACH_BARRIER(cmd)