/*
* Copyright (c) 2008-2024 Jonathan Schleifer <js@nil.im>
*
* All rights reserved.
*
* This file is part of ObjFW. It may be distributed under the terms of the
* Q Public License 1.0, which can be found in the file LICENSE.QPL included in
* the packaging of this file.
*
* Alternatively, it may be distributed under the terms of the GNU General
* Public License, either version 2 or 3, which can be found in the file
* LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this
* file.
*/
#include "config.h"
#import "OFApplication.h"
#import "OFColor.h"
#import "OFDictionary.h"
#import "OFMethodSignature.h"
#import "OFSet.h"
#import "OFStdIOStream.h"
#import "OFValue.h"
#import "OTTestCase.h"
#import "OTAssertionFailedException.h"
#import "OTTestSkippedException.h"
@interface OTAppDelegate: OFObject <OFApplicationDelegate>
@end
enum Status {
StatusRunning,
StatusOk,
StatusFailed,
StatusSkipped
};
OF_APPLICATION_DELEGATE(OTAppDelegate)
static bool
isSubclassOfClass(Class class, Class superclass)
{
for (Class iter = class; iter != Nil; iter = class_getSuperclass(iter))
if (iter == superclass)
return true;
return false;
}
@implementation OTAppDelegate
- (OFSet OF_GENERIC(Class) *)testClasses
{
Class *classes = objc_copyClassList(NULL);
OFMutableSet *testClasses;
if (classes == NULL)
return nil;
@try {
testClasses = [OFMutableSet set];
for (Class *iter = classes; *iter != Nil; iter++) {
/*
* Make sure the class is initialized.
* Required for the ObjFW runtime, as otherwise
* class_getSuperclass() crashes.
*/
[*iter class];
/*
* Don't use +[isSubclassOfClass:], as the Apple runtime
* can return (presumably internal?) classes that don't
* implement it, resulting in a crash.
*/
if (isSubclassOfClass(*iter, [OTTestCase class]))
[testClasses addObject: *iter];
}
} @finally {
OFFreeMemory(classes);
}
[testClasses removeObject: [OTTestCase class]];
[testClasses makeImmutable];
return testClasses;
}
- (OFSet OF_GENERIC(OFValue *) *)testsInClass: (Class)class
{
Method *methods = class_copyMethodList(class, NULL);
OFMutableSet *tests;
if (methods == NULL)
return nil;
@try {
tests = [OFMutableSet set];
for (Method *iter = methods; *iter != NULL; iter++) {
SEL selector = method_getName(*iter);
void *pool;
OFMethodSignature *sig;
if (selector == NULL)
continue;
if (strncmp(sel_getName(selector), "test", 4) != 0)
continue;
pool = objc_autoreleasePoolPush();
sig = [OFMethodSignature signatureWithObjCTypes:
method_getTypeEncoding(*iter)];
if (strcmp(sig.methodReturnType, "v") == 0 &&
sig.numberOfArguments == 2 &&
strcmp([sig argumentTypeAtIndex: 0], "@") == 0 &&
strcmp([sig argumentTypeAtIndex: 1], ":") == 0)
[tests addObject:
[OFValue valueWithPointer: selector]];
objc_autoreleasePoolPop(pool);
}
} @finally {
OFFreeMemory(methods);
}
if (class_getSuperclass(class) != Nil)
[tests unionSet:
[self testsInClass: class_getSuperclass(class)]];
[tests makeImmutable];
return tests;
}
- (void)printStatusForTest: (SEL)test
inClass: (Class)class
status: (enum Status)status
description: (OFString *)description
{
switch (status) {
case StatusRunning:
[OFStdOut setForegroundColor: [OFColor olive]];
[OFStdOut writeFormat: @"-[%@ ", class];
[OFStdOut setForegroundColor: [OFColor yellow]];
[OFStdOut writeFormat: @"%s", sel_getName(test)];
[OFStdOut setForegroundColor: [OFColor olive]];
[OFStdOut writeString: @"]: "];
break;
case StatusOk:
[OFStdOut setForegroundColor: [OFColor green]];
[OFStdOut writeFormat: @"\r-[%@ ", class];
[OFStdOut setForegroundColor: [OFColor lime]];
[OFStdOut writeFormat: @"%s", sel_getName(test)];
[OFStdOut setForegroundColor: [OFColor green]];
[OFStdOut writeLine: @"]: ok"];
break;
case StatusFailed:
[OFStdOut setForegroundColor: [OFColor maroon]];
[OFStdOut writeFormat: @"\r-[%@ ", class];
[OFStdOut setForegroundColor: [OFColor red]];
[OFStdOut writeFormat: @"%s", sel_getName(test)];
[OFStdOut setForegroundColor: [OFColor maroon]];
[OFStdOut writeLine: @"]: failed"];
[OFStdOut writeLine: description];
break;
case StatusSkipped:
[OFStdOut setForegroundColor: [OFColor gray]];
[OFStdOut writeFormat: @"\r-[%@ ", class];
[OFStdOut setForegroundColor: [OFColor silver]];
[OFStdOut writeFormat: @"%s", sel_getName(test)];
[OFStdOut setForegroundColor: [OFColor gray]];
[OFStdOut writeLine: @"]: skipped"];
if (description != nil)
[OFStdOut writeLine: description];
break;
}
}
- (void)applicationDidFinishLaunching: (OFNotification *)notification
{
OFSet OF_GENERIC(Class) *testClasses = [self testClasses];
size_t numSucceeded = 0, numFailed = 0, numSkipped = 0;
OFMutableDictionary *summaries = [OFMutableDictionary dictionary];
[OFStdOut setForegroundColor: [OFColor purple]];
[OFStdOut writeString: @"Found "];
[OFStdOut setForegroundColor: [OFColor fuchsia]];
[OFStdOut writeFormat: @"%zu", testClasses.count];
[OFStdOut setForegroundColor: [OFColor purple]];
[OFStdOut writeFormat: @" test case%s\n",
(testClasses.count != 1 ? "s" : "")];
for (Class class in testClasses) {
OFArray *summary;
[OFStdOut setForegroundColor: [OFColor teal]];
[OFStdOut writeFormat: @"Running ", class];
[OFStdOut setForegroundColor: [OFColor aqua]];
[OFStdOut writeFormat: @"%@\n", class];
for (OFValue *test in [self testsInClass: class]) {
void *pool = objc_autoreleasePoolPush();
bool failed = false, skipped = false;
OTTestCase *instance;
[self printStatusForTest: test.pointerValue
inClass: class
status: StatusRunning
description: nil];
instance = [[[class alloc] init] autorelease];
@try {
[instance setUp];
[instance performSelector: test.pointerValue];
} @catch (OTAssertionFailedException *e) {
/*
* If an assertion fails during -[setUp], don't
* run the test.
* If an assertion fails during a test, abort
* the test.
*/
[self printStatusForTest: test.pointerValue
inClass: class
status: StatusFailed
description: e.description];
failed = true;
} @catch (OTTestSkippedException *e) {
[self printStatusForTest: test.pointerValue
inClass: class
status: StatusSkipped
description: e.description];
skipped = true;
}
@try {
[instance tearDown];
} @catch (OTAssertionFailedException *e) {
/*
* If an assertion fails during -[tearDown],
* abort the tear down.
*/
if (!failed) {
SEL selector = test.pointerValue;
OFString *description = e.description;
[self printStatusForTest: selector
inClass: class
status: StatusFailed
description: description];
failed = true;
}
}
if (!failed && !skipped) {
[self printStatusForTest: test.pointerValue
inClass: class
status: StatusOk
description: nil];
numSucceeded++;
} else if (failed)
numFailed++;
else if (skipped)
numSkipped++;
objc_autoreleasePoolPop(pool);
}
summary = [class summary];
if (summary != nil)
[summaries setObject: summary forKey: class];
}
for (Class class in summaries) {
OFArray *summary = [summaries objectForKey: class];
[OFStdOut setForegroundColor: [OFColor teal]];
[OFStdOut writeString: @"Summary for "];
[OFStdOut setForegroundColor: [OFColor aqua]];
[OFStdOut writeFormat: @"%@\n", class];
for (OFPair *line in summary) {
[OFStdOut setForegroundColor: [OFColor navy]];
[OFStdOut writeFormat: @"%@: ", line.firstObject];
[OFStdOut setForegroundColor: [OFColor blue]];
[OFStdOut writeFormat: @"%@\n", line.secondObject];
}
}
[OFStdOut setForegroundColor: [OFColor fuchsia]];
[OFStdOut writeFormat: @"%zu", numSucceeded];
[OFStdOut setForegroundColor: [OFColor purple]];
[OFStdOut writeFormat: @" test%s succeeded, ",
(numSucceeded != 1 ? "s" : "")];
[OFStdOut setForegroundColor: [OFColor fuchsia]];
[OFStdOut writeFormat: @"%zu", numFailed];
[OFStdOut setForegroundColor: [OFColor purple]];
[OFStdOut writeFormat: @" test%s failed, ",
(numFailed != 1 ? "s" : "")];
[OFStdOut setForegroundColor: [OFColor fuchsia]];
[OFStdOut writeFormat: @"%zu", numSkipped];
[OFStdOut setForegroundColor: [OFColor purple]];
[OFStdOut writeFormat: @" test%s skipped\n",
(numSkipped != 1 ? "s" : "")];
[OFStdOut reset];
[OFApplication terminateWithStatus: (int)numFailed];
}
@end