Add XCTest logger backend to QtTestLib

Will be active when running test apps through Xcode's 'test' action,
and reports QtTestLib test objects and functions to Xcode as XCTest
cases.

This allows running tests on both iOS Simulator and iOS devices from
the command line, through xcodebuild, without relying on any 3rd party
tools. It also integrates Qt test failures and passes into the Xcode
IDE, which may be useful for closer investigation of test failures.

The feature is limited to Xcode 6.x.

Change-Id: I33d39edbabdbaebef48d2d0eb7e08a1ffb72c397
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@theqtcompany.com>
This commit is contained in:
Tor Arne Vestbø 2015-02-11 13:59:46 +01:00 committed by Tor Arne Vestbø
parent 595606fb02
commit 94ea7b7132
10 changed files with 780 additions and 5 deletions

View File

@ -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
}

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -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)