diff options
-rw-r--r-- | mkspecs/features/xctest.prf | 6 | ||||
-rw-r--r-- | mkspecs/macx-xcode/QtTest.plist | 24 | ||||
-rw-r--r-- | mkspecs/macx-xcode/default.xcscheme | 6 | ||||
-rw-r--r-- | qmake/generators/mac/pbuilder_pbx.cpp | 104 | ||||
-rw-r--r-- | src/testlib/qtestcase.cpp | 14 | ||||
-rw-r--r-- | src/testlib/qtestlog.cpp | 9 | ||||
-rw-r--r-- | src/testlib/qtestlog_p.h | 7 | ||||
-rw-r--r-- | src/testlib/qxctestlogger.mm | 501 | ||||
-rw-r--r-- | src/testlib/qxctestlogger_p.h | 95 | ||||
-rw-r--r-- | src/testlib/testlib.pro | 19 |
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) |