summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--mkspecs/features/xctest.prf6
-rw-r--r--mkspecs/macx-xcode/QtTest.plist24
-rw-r--r--mkspecs/macx-xcode/default.xcscheme6
-rw-r--r--qmake/generators/mac/pbuilder_pbx.cpp104
-rw-r--r--src/testlib/qtestcase.cpp14
-rw-r--r--src/testlib/qtestlog.cpp9
-rw-r--r--src/testlib/qtestlog_p.h7
-rw-r--r--src/testlib/qxctestlogger.mm501
-rw-r--r--src/testlib/qxctestlogger_p.h95
-rw-r--r--src/testlib/testlib.pro19
10 files changed, 780 insertions, 5 deletions
diff --git a/mkspecs/features/xctest.prf b/mkspecs/features/xctest.prf
new file mode 100644
index 0000000000..540c7590ad
--- /dev/null
+++ b/mkspecs/features/xctest.prf
@@ -0,0 +1,6 @@
+equals(TEMPLATE, app):macx-xcode {
+ load(sdk)
+ # Make the XCTest framework available. This is normally handled automatically
+ # by Xcode based on heuristics, but we need to explicitly link to XCTest.
+ QMAKE_LFLAGS += -F$${QMAKE_MAC_SDK_PLATFORM_PATH}/Developer/Library/Frameworks -weak_framework XCTest
+}
diff --git a/mkspecs/macx-xcode/QtTest.plist b/mkspecs/macx-xcode/QtTest.plist
new file mode 100644
index 0000000000..41dddb1a53
--- /dev/null
+++ b/mkspecs/macx-xcode/QtTest.plist
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>io.qt.$(PRODUCT_NAME:rfc1034identifier)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/mkspecs/macx-xcode/default.xcscheme b/mkspecs/macx-xcode/default.xcscheme
index ac21df17f3..4a16fefca0 100644
--- a/mkspecs/macx-xcode/default.xcscheme
+++ b/mkspecs/macx-xcode/default.xcscheme
@@ -32,9 +32,9 @@
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
- BlueprintIdentifier = "@TARGET_PBX_KEY@"
- BuildableName = "@QMAKE_ORIG_TARGET@"
- BlueprintName = "@QMAKE_ORIG_TARGET@"
+ BlueprintIdentifier = "@TEST_BUNDLE_PBX_KEY@"
+ BuildableName = "Qt Test"
+ BlueprintName = "Qt Test"
ReferencedContainer = "container:@QMAKE_ORIG_TARGET@.xcodeproj">
</BuildableReference>
</TestableReference>
diff --git a/qmake/generators/mac/pbuilder_pbx.cpp b/qmake/generators/mac/pbuilder_pbx.cpp
index 7798130392..3383e66ea6 100644
--- a/qmake/generators/mac/pbuilder_pbx.cpp
+++ b/qmake/generators/mac/pbuilder_pbx.cpp
@@ -1318,6 +1318,96 @@ ProjectBuilderMakefileGenerator::writeMakeParts(QTextStream &t)
if(!project->isEmpty("DESTDIR"))
t << "\t\t\t" << writeSettings("productInstallPath", project->first("DESTDIR")) << ";\n";
t << "\t\t};\n";
+
+ // Test target for running Qt unit tests under XCTest
+ if (project->isActiveConfig("testcase") && project->isActiveConfig("app_bundle")) {
+ QString devNullFileReferenceKey = keyFor(pbx_dir + "QMAKE_PBX_DEV_NULL_FILE_REFERENCE");
+ t << "\t\t" << devNullFileReferenceKey << " = {\n"
+ << "\t\t\t" << writeSettings("isa", "PBXFileReference", SettingsNoQuote) << ";\n"
+ << "\t\t\t" << writeSettings("name", "/dev/null") << ";\n"
+ << "\t\t\t" << writeSettings("path", "/dev/null") << ";\n"
+ << "\t\t\t" << writeSettings("lastKnownFileType", "sourcecode.c.c") << ";\n"
+ << "\t\t\t" << writeSettings("sourceTree", "<absolute>") << ";\n"
+ << "\t\t};\n";
+
+ QString devNullBuildFileKey = keyFor(pbx_dir + "QMAKE_PBX_DEV_NULL_BUILD_FILE");
+ t << "\t\t" << devNullBuildFileKey << " = {\n"
+ << "\t\t\t" << writeSettings("fileRef", devNullFileReferenceKey) << ";\n"
+ << "\t\t\t" << writeSettings("isa", "PBXBuildFile", SettingsNoQuote) << ";\n"
+ << "\t\t};\n";
+
+ QString dummySourceBuildPhaseKey = keyFor(pbx_dir + "QMAKE_PBX_DUMMY_SOURCE_BUILD_PHASE");
+ t << "\t\t" << dummySourceBuildPhaseKey << " = {\n"
+ << "\t\t\t" << writeSettings("buildActionMask", "2147483647", SettingsNoQuote) << ";\n"
+ << "\t\t\t" << writeSettings("files", devNullBuildFileKey, SettingsAsList, 4) << ";\n"
+ << "\t\t\t" << writeSettings("isa", "PBXSourcesBuildPhase", SettingsNoQuote) << ";\n"
+ << "\t\t\t" << writeSettings("runOnlyForDeploymentPostprocessing", "0", SettingsNoQuote) << ";\n"
+ << "\t\t};\n";
+
+ ProStringList testBundleBuildConfigs;
+
+ ProString targetName = project->first("QMAKE_ORIG_TARGET");
+ ProString testHost = "$(BUILT_PRODUCTS_DIR)/" + targetName + ".app/";
+ if (!project->isActiveConfig("ios"))
+ testHost.append("Contents/MacOS/");
+ testHost.append(targetName);
+
+ static const char * const configs[] = { "Debug", "Release", 0 };
+ for (int i = 0; configs[i]; i++) {
+ QString testBundleBuildConfig = keyFor(pbx_dir + "QMAKE_PBX_TEST_BUNDLE_BUILDCONFIG_" + configs[i]);
+ t << "\t\t" << testBundleBuildConfig << " = {\n"
+ << "\t\t\t" << writeSettings("isa", "XCBuildConfiguration", SettingsNoQuote) << ";\n"
+ << "\t\t\tbuildSettings = {\n"
+ << "\t\t\t\t" << writeSettings("INFOPLIST_FILE", project->first("QMAKE_XCODE_SPECDIR") + "/QtTest.plist") << ";\n"
+ << "\t\t\t\t" << writeSettings("OTHER_LDFLAGS", "") << ";\n"
+ << "\t\t\t\t" << writeSettings("TEST_HOST", testHost) << ";\n"
+ << "\t\t\t\t" << writeSettings("DEBUG_INFORMATION_FORMAT", "dwarf-with-dsym") << ";\n"
+ << "\t\t\t};\n"
+ << "\t\t\t" << writeSettings("name", configs[i], SettingsNoQuote) << ";\n"
+ << "\t\t};\n";
+
+ testBundleBuildConfigs.append(testBundleBuildConfig);
+ }
+
+ QString testBundleBuildConfigurationListKey = keyFor(pbx_dir + "QMAKE_PBX_TEST_BUNDLE_BUILDCONFIG_LIST");
+ t << "\t\t" << testBundleBuildConfigurationListKey << " = {\n"
+ << "\t\t\t" << writeSettings("isa", "XCConfigurationList", SettingsNoQuote) << ";\n"
+ << "\t\t\t" << writeSettings("buildConfigurations", testBundleBuildConfigs, SettingsAsList, 4) << ";\n"
+ << "\t\t\t" << writeSettings("defaultConfigurationIsVisible", "0", SettingsNoQuote) << ";\n"
+ << "\t\t\t" << writeSettings("defaultConfigurationName", "Debug", SettingsNoQuote) << ";\n"
+ << "\t\t};\n";
+
+ QString primaryTargetDependencyKey = keyFor(pbx_dir + "QMAKE_PBX_PRIMARY_TARGET_DEP");
+ t << "\t\t" << primaryTargetDependencyKey << " = {\n"
+ << "\t\t\t" << writeSettings("isa", "PBXTargetDependency", SettingsNoQuote) << ";\n"
+ << "\t\t\t" << writeSettings("target", keyFor(pbx_dir + "QMAKE_PBX_TARGET")) << ";\n"
+ << "\t\t};\n";
+
+ QString testBundleReferenceKey = keyFor("QMAKE_TEST_BUNDLE_REFERENCE");
+ t << "\t\t" << testBundleReferenceKey << " = {\n"
+ << "\t\t\t" << writeSettings("isa", "PBXFileReference", SettingsNoQuote) << ";\n"
+ << "\t\t\t" << writeSettings("explicitFileType", "wrapper.cfbundle") << ";\n"
+ << "\t\t\t" << writeSettings("includeInIndex", "0", SettingsNoQuote) << ";\n"
+ << "\t\t\t" << writeSettings("sourceTree", "BUILT_PRODUCTS_DIR", SettingsNoQuote) << ";\n"
+ << "\t\t};\n";
+
+ QString testTargetKey = keyFor(pbx_dir + "QMAKE_PBX_TEST_TARGET");
+ project->values("QMAKE_PBX_TARGETS").append(testTargetKey);
+ t << "\t\t" << testTargetKey << " = {\n"
+ << "\t\t\t" << writeSettings("buildPhases", dummySourceBuildPhaseKey, SettingsAsList, 4) << ";\n"
+ << "\t\t\t" << writeSettings("dependencies", primaryTargetDependencyKey, SettingsAsList, 4) << ";\n"
+ << "\t\t\t" << writeSettings("buildConfigurationList", testBundleBuildConfigurationListKey) << ";\n"
+ << "\t\t\t" << writeSettings("productType", "com.apple.product-type.bundle.unit-test") << ";\n"
+ << "\t\t\t" << writeSettings("isa", "PBXNativeTarget", SettingsNoQuote) << ";\n"
+ << "\t\t\t" << writeSettings("productReference", testBundleReferenceKey) << ";\n"
+ << "\t\t\t" << writeSettings("name", "Qt Test") << ";\n"
+ << "\t\t};\n";
+
+ QLatin1Literal testTargetID("TestTargetID");
+ project->values(ProKey("QMAKE_PBX_TARGET_ATTRIBUTES_" + testTargetKey + "_" + testTargetID)).append(keyFor(pbx_dir + "QMAKE_PBX_TARGET"));
+ project->values(ProKey("QMAKE_PBX_TARGET_ATTRIBUTES_" + testTargetKey)).append(ProKey(testTargetID));
+ }
+
//DEBUG/RELEASE
QString defaultConfig;
for(int as_release = 0; as_release < 2; as_release++)
@@ -1543,6 +1633,19 @@ ProjectBuilderMakefileGenerator::writeMakeParts(QTextStream &t)
t << "\t\t\t" << writeSettings("projectDirPath", ProStringList()) << ";\n"
<< "\t\t\t" << writeSettings("projectRoot", "") << ";\n"
<< "\t\t\t" << writeSettings("targets", project->values("QMAKE_PBX_TARGETS"), SettingsAsList, 4) << ";\n"
+ << "\t\t\t" << "attributes = {\n"
+ << "\t\t\t\tTargetAttributes = {\n";
+ foreach (const ProString &target, project->values("QMAKE_PBX_TARGETS")) {
+ const ProStringList &attributes = project->values(ProKey("QMAKE_PBX_TARGET_ATTRIBUTES_" + target));
+ if (attributes.isEmpty())
+ continue;
+ t << "\t\t\t\t\t" << target << " = {\n";
+ foreach (const ProString &attribute, attributes)
+ t << "\t\t\t\t\t\t" << writeSettings(attribute.toQString(), project->first(ProKey("QMAKE_PBX_TARGET_ATTRIBUTES_" + target + "_" + attribute))) << ";\n";
+ t << "\t\t\t\t\t};\n";
+ }
+ t << "\t\t\t\t};\n"
+ << "\t\t\t};\n"
<< "\t\t};\n";
// FIXME: Deal with developmentRegion and knownRegions for QMAKE_PBX_ROOT
@@ -1600,6 +1703,7 @@ ProjectBuilderMakefileGenerator::writeMakeParts(QTextStream &t)
schemeData.replace("@QMAKE_ORIG_TARGET@", target);
schemeData.replace("@TARGET_PBX_KEY@", keyFor(pbx_dir + "QMAKE_PBX_TARGET"));
+ schemeData.replace("@TEST_BUNDLE_PBX_KEY@", keyFor("QMAKE_TEST_BUNDLE_REFERENCE"));
QTextStream outputSchemeStream(&outputSchemeFile);
outputSchemeStream << schemeData;
diff --git a/src/testlib/qtestcase.cpp b/src/testlib/qtestcase.cpp
index 222bdd3f39..e2f98c2f04 100644
--- a/src/testlib/qtestcase.cpp
+++ b/src/testlib/qtestcase.cpp
@@ -59,6 +59,9 @@
#include <QtTest/private/qbenchmark_p.h>
#include <QtTest/private/cycle_p.h>
#include <QtTest/private/qtestblacklist_p.h>
+#if defined(HAVE_XCTEST)
+#include <QtTest/private/qxctestlogger_p.h>
+#endif
#include <numeric>
#include <algorithm>
@@ -1530,6 +1533,11 @@ Q_TESTLIB_EXPORT void qtest_qParseArgs(int argc, char *argv[], bool qml)
QTestLog::LogMode logFormat = QTestLog::Plain;
const char *logFilename = 0;
+#if defined(Q_OS_MAC) && defined(HAVE_XCTEST)
+ if (QXcodeTestLogger::canLogTestProgress())
+ logFormat = QTestLog::XCTest;
+#endif
+
const char *testOptions =
" New-style logging options:\n"
" -o filename,format : Output results to file in the specified format\n"
@@ -1782,10 +1790,14 @@ Q_TESTLIB_EXPORT void qtest_qParseArgs(int argc, char *argv[], bool qml)
} else if (strcmp(argv[i], "-vb") == 0) {
QBenchmarkGlobalData::current->verboseOutput = true;
-#ifdef Q_OS_WINRT
+#if defined(Q_OS_WINRT)
} else if (strncmp(argv[i], "-ServerName:", 12) == 0 ||
strncmp(argv[i], "-qdevel", 7) == 0) {
continue;
+#elif defined(Q_OS_MAC) && defined(HAVE_XCTEST)
+ } else if (int skip = QXcodeTestLogger::parseCommandLineArgument(argv[i])) {
+ i += (skip - 1); // Eating argv[i] with a continue counts towards skips
+ continue;
#endif
} else if (argv[i][0] == '-') {
fprintf(stderr, "Unknown option: '%s'\n\n%s", argv[i], testOptions);
diff --git a/src/testlib/qtestlog.cpp b/src/testlib/qtestlog.cpp
index 8a4afae447..59aeb27ffe 100644
--- a/src/testlib/qtestlog.cpp
+++ b/src/testlib/qtestlog.cpp
@@ -40,6 +40,10 @@
#include <QtTest/private/qcsvbenchmarklogger_p.h>
#include <QtTest/private/qxunittestlogger_p.h>
#include <QtTest/private/qxmltestlogger_p.h>
+#if defined(HAVE_XCTEST)
+#include <QtTest/private/qxctestlogger_p.h>
+#endif
+
#include <QtCore/qatomic.h>
#include <QtCore/qbytearray.h>
#include <QtCore/QVariant>
@@ -483,6 +487,11 @@ void QTestLog::addLogger(LogMode mode, const char *filename)
case QTestLog::XunitXML:
logger = new QXunitTestLogger(filename);
break;
+#if defined(HAVE_XCTEST)
+ case QTestLog::XCTest:
+ logger = new QXcodeTestLogger;
+ break;
+#endif
}
QTEST_ASSERT(logger);
QTest::TestLoggers::addLogger(logger);
diff --git a/src/testlib/qtestlog_p.h b/src/testlib/qtestlog_p.h
index 75e39e8f3d..b4786b4904 100644
--- a/src/testlib/qtestlog_p.h
+++ b/src/testlib/qtestlog_p.h
@@ -55,7 +55,12 @@ class QRegularExpression;
class Q_TESTLIB_EXPORT QTestLog
{
public:
- enum LogMode { Plain = 0, XML, LightXML, XunitXML, CSV };
+ enum LogMode {
+ Plain = 0, XML, LightXML, XunitXML, CSV,
+#if defined(HAVE_XCTEST)
+ XCTest
+#endif
+ };
static void enterTestFunction(const char* function);
static void leaveTestFunction();
diff --git a/src/testlib/qxctestlogger.mm b/src/testlib/qxctestlogger.mm
new file mode 100644
index 0000000000..576f46acea
--- /dev/null
+++ b/src/testlib/qxctestlogger.mm
@@ -0,0 +1,501 @@
+/****************************************************************************
+**
+** Copyright (C) 2015 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the QtTest module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL21$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** As a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "qxctestlogger_p.h"
+
+#include <QtCore/qstring.h>
+
+#include <QtTest/private/qtestlog_p.h>
+#include <QtTest/private/qtestresult_p.h>
+
+#import <XCTest/XCTest.h>
+
+// ---------------------------------------------------------
+
+@interface XCTestProbe (Private)
++ (BOOL)isTesting;
++ (void)runTests:(id)unusedArgument;
++ (NSString*)testScope;
++ (BOOL)isInverseTestScope;
+@end
+
+@interface XCTestDriver : NSObject
++ (XCTestDriver*)sharedTestDriver;
+@property (readonly, assign) NSObject *IDEConnection;
+@end
+
+@interface XCTest (Private)
+- (NSString *)nameForLegacyLogging;
+@end
+
+#pragma GCC diagnostic push // Ignore XCTestProbe deprecation
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+
+// ---------------------------------------------------------
+
+@interface QtTestLibWrapper : XCTestCase
+@end
+
+@interface QtTestLibTests : XCTestSuite
++ (XCTestSuiteRun*)testRun;
+@end
+
+@interface QtTestLibTest : XCTestCase
+@property (nonatomic, retain) NSString* testObjectName;
+@property (nonatomic, retain) NSString* testFunctionName;
+@end
+
+// ---------------------------------------------------------
+
+class ThreadBarriers
+{
+public:
+ enum Barrier {
+ XCTestCanStartTesting,
+ XCTestHaveStarted,
+ QtTestsCanStartTesting,
+ QtTestsHaveCompleted,
+ XCTestsHaveCompleted,
+ BarrierCount
+ };
+
+ static ThreadBarriers *get()
+ {
+ static ThreadBarriers instance;
+ return &instance;
+ }
+
+ static void initialize() { get(); }
+
+ void wait(Barrier barrier) { dispatch_semaphore_wait(barriers[barrier], DISPATCH_TIME_FOREVER); }
+ void signal(Barrier barrier) { dispatch_semaphore_signal(barriers[barrier]); }
+
+private:
+ #define FOREACH_BARRIER(cmd) for (int i = 0; i < BarrierCount; ++i) { cmd }
+
+ ThreadBarriers() { FOREACH_BARRIER(barriers[i] = dispatch_semaphore_create(0);) }
+ ~ThreadBarriers() { FOREACH_BARRIER(dispatch_release(barriers[i]);) }
+
+ dispatch_semaphore_t barriers[BarrierCount];
+};
+
+#define WAIT_FOR_BARRIER(b) ThreadBarriers::get()->wait(ThreadBarriers::b);
+#define SIGNAL_BARRIER(b) ThreadBarriers::get()->signal(ThreadBarriers::b);
+
+// ---------------------------------------------------------
+
+@implementation QtTestLibWrapper
+
++ (void)load
+{
+ NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init];
+
+ if (![XCTestProbe isTesting])
+ return;
+
+ if (!([NSDate timeIntervalSinceReferenceDate] > 0))
+ qFatal("error: Device date '%s' is bad, likely set to update automatically. Please correct.",
+ [NSDate date].description.UTF8String);
+
+ XCTestDriver *testDriver = nil;
+ if ([QtTestLibWrapper usingTestManager])
+ testDriver = [XCTestDriver sharedTestDriver];
+
+ // Spawn off task to run test infrastructure on separate thread so that we can
+ // let main() execute like normal on the main thread. The queue will never be
+ // destroyed, so there's no point in trying to keep a proper retain count.
+ dispatch_async(dispatch_queue_create("io.qt.QTestLib.xctest-wrapper", DISPATCH_QUEUE_SERIAL), ^{
+ Q_ASSERT(![NSThread isMainThread]);
+ [XCTestProbe runTests:nil];
+ Q_UNREACHABLE();
+ });
+
+ // Initialize barriers before registering exit handler so that the
+ // semaphores stay alive until after the exit handler completes.
+ ThreadBarriers::initialize();
+
+ // We register an exit handler so that we can intercept when main() completes
+ // and let the XCTest thread finish up. For main() functions that never started
+ // testing using QtTestLib we also need to signal that xcTestsCanStart.
+ atexit_b(^{
+ Q_ASSERT([NSThread isMainThread]);
+
+ // In case not started by startLogging
+ SIGNAL_BARRIER(XCTestCanStartTesting);
+
+ // [XCTestProbe runTests:] ends up calling [XCTestProbe exitTests:] after
+ // all test suites have completed, which calls exit(). We use that to signal
+ // to the main thread that it's free to continue its exit handler.
+ atexit_b(^{
+ Q_ASSERT(![NSThread isMainThread]);
+ SIGNAL_BARRIER(XCTestsHaveCompleted);
+
+ // Block forever so that the main thread does all the cleanup
+ dispatch_semaphore_wait(dispatch_semaphore_create(0), DISPATCH_TIME_FOREVER);
+ });
+
+ SIGNAL_BARRIER(QtTestsHaveCompleted);
+
+ // Ensure XCTest complets the top level tests suite
+ WAIT_FOR_BARRIER(XCTestsHaveCompleted);
+ });
+
+ // Let test driver (Xcode) connection setup complete before continuing
+ if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--use-testmanagerd"]) {
+ while (!testDriver.IDEConnection)
+ [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
+ }
+
+ // Wait for our QtTestLib test suite to run before running main
+ WAIT_FOR_BARRIER(QtTestsCanStartTesting);
+
+ // Prevent XCTestProbe from re-launching runTests on application startup
+ [[NSNotificationCenter defaultCenter] removeObserver:[XCTestProbe class]
+ name:[NSString stringWithFormat:@"%@DidFinishLaunchingNotification",
+ #if defined(Q_OS_OSX)
+ @"NSApplication"
+ #elif defined(Q_OS_IOS)
+ @"UIApplication"
+ #endif
+ ]
+ object:nil];
+
+ [autoreleasepool release];
+}
+
++ (id)defaultTestSuite
+{
+ return [[QtTestLibTests alloc] initWithName:@"QtTestLib"];
+}
+
++ (BOOL)usingTestManager
+{
+ return [[[NSProcessInfo processInfo] arguments] containsObject:@"--use-testmanagerd"];
+}
+
+@end
+
+// ---------------------------------------------------------
+
+static XCTestSuiteRun *s_qtTestSuiteRun = 0;
+
+@implementation QtTestLibTests
+
+- (void)performTest:(XCTestSuiteRun *)testSuiteRun
+{
+ Q_ASSERT(![NSThread isMainThread]);
+
+ Q_ASSERT(!s_qtTestSuiteRun);
+ s_qtTestSuiteRun = testSuiteRun;
+
+ SIGNAL_BARRIER(QtTestsCanStartTesting);
+
+ // Wait for main() to complete, or a QtTestLib test to start, so we
+ // know if we should start the QtTestLib test suite.
+ WAIT_FOR_BARRIER(XCTestCanStartTesting);
+
+ if (QXcodeTestLogger::isActive())
+ [testSuiteRun start];
+
+ SIGNAL_BARRIER(XCTestHaveStarted);
+
+ // All test reporting happens on main thread from now on. Wait until
+ // main() completes before allowing the XCTest thread to continue.
+ WAIT_FOR_BARRIER(QtTestsHaveCompleted);
+
+ if ([testSuiteRun startDate])
+ [testSuiteRun stop];
+}
+
++ (XCTestSuiteRun*)testRun
+{
+ return s_qtTestSuiteRun;
+}
+
+@end
+
+// ---------------------------------------------------------
+
+@implementation QtTestLibTest
+
+- (id)initWithInvocation:(NSInvocation *)invocation
+{
+ if (self = [super initWithInvocation:invocation]) {
+ // The test object name and function name are used by XCTest after QtTestLib has
+ // reset them, so we need to store them up front for each XCTestCase.
+ self.testObjectName = [NSString stringWithUTF8String:QTestResult::currentTestObjectName()];
+ self.testFunctionName = [NSString stringWithUTF8String:QTestResult::currentTestFunction()];
+ }
+
+ return self;
+}
+
+- (NSString *)testClassName
+{
+ return self.testObjectName;
+}
+
+- (NSString *)testMethodName
+{
+ return self.testFunctionName;
+}
+
+- (NSString *)nameForLegacyLogging
+{
+ NSString *name = [NSString stringWithFormat:@"%@::%@", [self testClassName], [self testMethodName]];
+ if (QTestResult::currentDataTag() || QTestResult::currentGlobalDataTag()) {
+ const char *currentDataTag = QTestResult::currentDataTag() ? QTestResult::currentDataTag() : "";
+ const char *globalDataTag = QTestResult::currentGlobalDataTag() ? QTestResult::currentGlobalDataTag() : "";
+ const char *filler = (currentDataTag[0] && globalDataTag[0]) ? ":" : "";
+ name = [name stringByAppendingString:[NSString stringWithFormat:@"(%s%s%s)",
+ globalDataTag, filler, currentDataTag]];
+ }
+
+ return name;
+}
+
+@end
+
+// ---------------------------------------------------------
+
+bool QXcodeTestLogger::canLogTestProgress()
+{
+ return [XCTestProbe isTesting]; // FIXME: Exclude xctool
+}
+
+int QXcodeTestLogger::parseCommandLineArgument(const char *argument)
+{
+ if (strncmp(argument, "-NS", 3) == 0 || strncmp(argument, "-Apple", 6) == 0)
+ return 2; // -NSTreatUnknownArgumentsAsOpen, -ApplePersistenceIgnoreState, etc, skip argument
+ else if (strcmp(argument, "--use-testmanagerd") == 0)
+ return 2; // Skip UID argument
+ else if (strncmp(argument, "-XCTest", 7) == 0)
+ return 2; // -XCTestInvertScope, -XCTest scope, etc, skip argument
+ else if (strcmp(argument + (strlen(argument) - 7), ".xctest") == 0)
+ return 1; // Skip test bundle
+ else
+ return 0;
+}
+
+// ---------------------------------------------------------
+
+QXcodeTestLogger *QXcodeTestLogger::s_currentTestLogger = 0;
+
+// ---------------------------------------------------------
+
+QXcodeTestLogger::QXcodeTestLogger()
+ : QAbstractTestLogger(0)
+ , m_testRuns([[NSMutableArray arrayWithCapacity:2] retain])
+
+{
+ Q_ASSERT(!s_currentTestLogger);
+ s_currentTestLogger = this;
+}
+
+QXcodeTestLogger::~QXcodeTestLogger()
+{
+ s_currentTestLogger = 0;
+ [m_testRuns release];
+}
+
+void QXcodeTestLogger::startLogging()
+{
+ SIGNAL_BARRIER(XCTestCanStartTesting);
+
+ static dispatch_once_t onceToken;
+ dispatch_once (&onceToken, ^{
+ WAIT_FOR_BARRIER(XCTestHaveStarted);
+ });
+
+ // Scope test object suite under top level QtTestLib test run
+ [m_testRuns addObject:[QtTestLibTests testRun]];
+
+ NSString *suiteName = [NSString stringWithUTF8String:QTestResult::currentTestObjectName()];
+ pushTestRunForTest([XCTestSuite testSuiteWithName:suiteName], true);
+}
+
+void QXcodeTestLogger::stopLogging()
+{
+ popTestRun();
+}
+
+static bool isTestFunctionInActiveScope(const char *function)
+{
+ static NSString *testScope = [XCTestProbe testScope];
+
+ enum TestScope { Unknown, All, None, Self, Selected };
+ static TestScope activeScope = Unknown;
+
+ if (activeScope == Unknown) {
+ if ([testScope isEqualToString:@"All"])
+ activeScope = All;
+ else if ([testScope isEqualToString:@"None"])
+ activeScope = None;
+ else if ([testScope isEqualToString:@"Self"])
+ activeScope = Self;
+ else
+ activeScope = Selected;
+ }
+
+ if (activeScope == All)
+ return true;
+ else if (activeScope == None)
+ return false;
+ else if (activeScope == Self)
+ return true; // Investigate
+
+ Q_ASSERT(activeScope == Selected);
+
+ static NSArray *forcedTests = [@[ @"initTestCase", @"initTestCase_data", @"cleanupTestCase" ] retain];
+ if ([forcedTests containsObject:[NSString stringWithUTF8String:function]])
+ return true;
+
+ static NSArray *testsInScope = [[testScope componentsSeparatedByString:@","] retain];
+ bool inScope = [testsInScope containsObject:[NSString stringWithFormat:@"%s/%s",
+ QTestResult::currentTestObjectName(), function]];
+
+ if ([XCTestProbe isInverseTestScope])
+ inScope = !inScope;
+
+ return inScope;
+}
+
+void QXcodeTestLogger::enterTestFunction(const char *function)
+{
+ if (!isTestFunctionInActiveScope(function))
+ QTestResult::setSkipCurrentTest(true);
+
+ XCTest *test = [QtTestLibTest testCaseWithInvocation:nil];
+ pushTestRunForTest(test, !QTestResult::skipCurrentTest());
+}
+
+void QXcodeTestLogger::leaveTestFunction()
+{
+ popTestRun();
+}
+
+void QXcodeTestLogger::addIncident(IncidentTypes type, const char *description,
+ const char *file, int line)
+{
+ XCTestRun *testRun = [m_testRuns lastObject];
+
+ // The 'expected' argument to recordFailureWithDescription refers to whether
+ // the failure was a regular failed assertion, or an unexpected exception,
+ // so in our case it's always 'YES', and we need to explicitly ignore XFail.
+ if (type == QAbstractTestLogger::XFail) {
+ QTestCharBuffer buf;
+ NSString *testCaseName = [[testRun test] nameForLegacyLogging];
+ QTest::qt_asprintf(&buf, "Test Case '%s' failed expectedly (%s).\n",
+ [testCaseName UTF8String], description);
+ outputString(buf.constData());
+ return;
+ }
+
+ if (type == QAbstractTestLogger::Pass) {
+ // We ignore non-data passes, as we're already reporting that as part of the
+ // normal test case start/stop cycle.
+ if (!(QTestResult::currentDataTag() || QTestResult::currentGlobalDataTag()))
+ return;
+
+ QTestCharBuffer buf;
+ NSString *testCaseName = [[testRun test] nameForLegacyLogging];
+ QTest::qt_asprintf(&buf, "Test Case '%s' passed.\n", [testCaseName UTF8String]);
+ outputString(buf.constData());
+ return;
+ }
+
+ // FIXME: Handle blacklisted tests
+
+ if (!file || !description)
+ return; // Or report?
+
+ [testRun recordFailureWithDescription:[NSString stringWithUTF8String:description]
+ inFile:[NSString stringWithUTF8String:file] atLine:line expected:YES];
+}
+
+void QXcodeTestLogger::addMessage(MessageTypes type, const QString &message,
+ const char *file, int line)
+{
+ QTestCharBuffer buf;
+
+ if (QTestLog::verboseLevel() > 0 && (file && line)) {
+ QTest::qt_asprintf(&buf, "%s:%d: ", file, line);
+ outputString(buf.constData());
+ }
+
+ if (type == QAbstractTestLogger::Skip) {
+ XCTestRun *testRun = [m_testRuns lastObject];
+ NSString *testCaseName = [[testRun test] nameForLegacyLogging];
+ QTest::qt_asprintf(&buf, "Test Case '%s' skipped (%s).\n",
+ [testCaseName UTF8String], message.toUtf8().constData());
+ } else {
+ QTest::qt_asprintf(&buf, "%s\n", message.toUtf8().constData());
+ }
+
+ outputString(buf.constData());
+}
+
+void QXcodeTestLogger::addBenchmarkResult(const QBenchmarkResult &result)
+{
+ Q_UNUSED(result);
+}
+
+void QXcodeTestLogger::pushTestRunForTest(XCTest *test, bool start)
+{
+ XCTestRun *testRun = [[test testRunClass] testRunWithTest:test];
+ [m_testRuns addObject:testRun];
+
+ if (start)
+ [testRun start];
+}
+
+XCTestRun *QXcodeTestLogger::popTestRun()
+{
+ XCTestRun *testRun = [[m_testRuns lastObject] retain];
+ [m_testRuns removeLastObject];
+
+ if ([testRun startDate])
+ [testRun stop];
+
+ [[m_testRuns lastObject] addTestRun:testRun];
+ [testRun release];
+
+ return testRun;
+}
+
+bool QXcodeTestLogger::isActive()
+{
+ return s_currentTestLogger;
+}
+
+#pragma GCC diagnostic pop
diff --git a/src/testlib/qxctestlogger_p.h b/src/testlib/qxctestlogger_p.h
new file mode 100644
index 0000000000..95ad1374bc
--- /dev/null
+++ b/src/testlib/qxctestlogger_p.h
@@ -0,0 +1,95 @@
+/****************************************************************************
+**
+** Copyright (C) 2015 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the QtTest module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL21$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** As a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QXCTESTLOGGER_P_H
+#define QXCTESTLOGGER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtTest/private/qabstracttestlogger_p.h>
+
+#include <dispatch/dispatch.h>
+
+Q_FORWARD_DECLARE_OBJC_CLASS(XCTest);
+Q_FORWARD_DECLARE_OBJC_CLASS(XCTestRun);
+Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableArray);
+
+QT_BEGIN_NAMESPACE
+
+class QXcodeTestLogger : public QAbstractTestLogger
+{
+public:
+ QXcodeTestLogger();
+ ~QXcodeTestLogger() Q_DECL_OVERRIDE;
+
+ void startLogging() Q_DECL_OVERRIDE;
+ void stopLogging() Q_DECL_OVERRIDE;
+
+ void enterTestFunction(const char *function) Q_DECL_OVERRIDE;
+ void leaveTestFunction() Q_DECL_OVERRIDE;
+
+ void addIncident(IncidentTypes type, const char *description,
+ const char *file = 0, int line = 0) Q_DECL_OVERRIDE;
+
+ void addMessage(MessageTypes type, const QString &message,
+ const char *file = 0, int line = 0) Q_DECL_OVERRIDE;
+
+ void addBenchmarkResult(const QBenchmarkResult &result) Q_DECL_OVERRIDE;
+
+ static bool canLogTestProgress();
+ static int parseCommandLineArgument(const char *argument);
+
+ static bool isActive();
+
+private:
+ void pushTestRunForTest(XCTest *test, bool start);
+ XCTestRun *popTestRun();
+
+ NSMutableArray *m_testRuns;
+
+ static QXcodeTestLogger *s_currentTestLogger;
+};
+
+
+QT_END_NAMESPACE
+
+#endif
diff --git a/src/testlib/testlib.pro b/src/testlib/testlib.pro
index dbcc588d10..841d913105 100644
--- a/src/testlib/testlib.pro
+++ b/src/testlib/testlib.pro
@@ -76,6 +76,25 @@ wince*::LIBS += libcmt.lib \
mac {
LIBS += -framework Security
osx: LIBS += -framework ApplicationServices -framework IOKit
+
+ # XCTest support
+ !lessThan(QMAKE_XCODE_VERSION, "6.0") {
+ OBJECTIVE_SOURCES += qxctestlogger.mm
+ HEADERS += qxctestlogger_p.h
+
+ DEFINES += HAVE_XCTEST
+ LIBS += -framework Foundation
+
+ load(sdk)
+ platform_dev_frameworks_path = $${QMAKE_MAC_SDK_PLATFORM_PATH}/Developer/Library/Frameworks
+
+ # We can't put this path into LIBS (so that it propagates to the prl file), as we
+ # don't know yet if the target that links to testlib will build under Xcode or not.
+ # The corresponding flags for the target lives in xctest.prf, where we do know.
+ QMAKE_LFLAGS += -F$${platform_dev_frameworks_path} -weak_framework XCTest
+ QMAKE_OBJECTIVE_CFLAGS += -F$${platform_dev_frameworks_path}
+ MODULE_CONFIG += xctest
+ }
}
load(qt_module)