/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
//
// TestCase.m
// Copyright (c) 2009-2011 Apple Inc. All rights reserved.
//
#import "TestCase.h"
#import "Environment.h"
#import <objc/objc-auto.h>
#import <objc/objc-class.h>
#import <unistd.h>
#define assertTestQueue() do { \
if (dispatch_get_current_queue() != _testQueue) { \
NSLog(@"}} while (0)
@implementation TestFinalizer : NSObject
- (id)init
{
self = [super init];
if (self) {
_allocatingTest = [TestCase currentTestCase];
}
return self;
}
- (void)finalize
{
TestCase *tc = [TestCase currentTestCase];
// Here is a convenient place to test objc_is_finalized()
if (!objc_is_finalized(self)) {
[tc fail:[NSString stringWithFormat:@"objc_is_finalized(self) returns false during -finalize. self = }
if (tc == _allocatingTest) {
[tc didFinalize:self];
}
[super finalize];
}
@end
@implementation TestCase
static TestCase *_currentTestCase;
+ (id)currentTestCase
{
return _currentTestCase;
}
+ (BOOL)isConcreteTestCase
{
Class tc = [TestCase class];
IMP startImp = [tc instanceMethodForSelector:@selector(performTest)];
if ([self instanceMethodForSelector:@selector(performTest)] != startImp) {
return YES;
}
return NO;
}
static NSInteger sortClassesByName(id cls1, id cls2, void* context) {
return strcmp(class_getName(cls1), class_getName(cls2));
}
+ (NSArray *)testClasses
{
static NSArray *_testClasses = nil;
// Dynamically find all the subclasses of AutoTestScript
if (_testClasses == nil) {
NSMutableArray *testClasses = [[NSMutableArray alloc] init];
int numClasses;
Class * classes = NULL;
Class tc = [TestCase class];
IMP startImp = [tc instanceMethodForSelector:@selector(performTest)];
classes = NULL;
numClasses = objc_getClassList(NULL, 0);
if (numClasses > 0 )
{
classes = (Class *)malloc(sizeof(Class) * numClasses);
numClasses = objc_getClassList(classes, numClasses);
int i;
for (i=0; i<numClasses; i++) {
// Some fumkiness to weed out weird clases like NSZombie and Object without actually messaging them.
Class sc = class_getSuperclass(classes[i]);
if (sc) {
Class ssc = class_getSuperclass(sc);
if (ssc) {
if ([classes[i] isSubclassOfClass:tc]) {
if ([classes[i] isConcreteTestCase])
[testClasses addObject:classes[i]];
}
}
}
}
free(classes);
}
_testClasses = [testClasses sortedArrayUsingFunction:sortClassesByName context:NULL];
}
return _testClasses;
}
+ (void)initialize
{
if (self == [TestCase class]) {
Auto::Environment::initialize();
}
}
+ (NSString *)resultStringForResult:(TestResult)resultCode
{
NSString *result;
switch (resultCode) {
case PASSED:
result = @"PASSED";
break;
case FAILED:
result = @"FAILED";
break;
case SKIPPED:
result = @"SKIPPED";
break;
default:
result = @"UNKNOWN";
break;
}
return result;
}
- (id)init
{
self = [super init];
if (self) {
NSString *label = [NSString stringWithFormat:@"Test Case: _testQueue = dispatch_queue_create([label UTF8String], NULL);
_result = PENDING;
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_cond, NULL);
_orig_fd = -1;
}
return self;
}
- (void)finalize
{
dispatch_release(_testQueue);
[super finalize];
}
- (void)setCompletionCallback:(void (^)(void))completionCallback
{
_completionCallback = Block_copy(completionCallback);
_completionCallbackQueue = dispatch_get_current_queue();
dispatch_retain(_completionCallbackQueue);
}
- (void)invokeCompletionCallback
{
assertTestQueue();
// safe to call this multiple times
// do not really do it until we are really done
if (_completionCallback && !_testThread && _outputCompleteCalled && _result != IN_PROGRESS) {
if (_completionCallback) {
_currentTestCase = nil;
dispatch_async(_completionCallbackQueue, _completionCallback);
Block_release(_completionCallback);
dispatch_release(_completionCallbackQueue);
_completionCallback = nil;
}
}
}
- (TestResult)result
{
return _result;
}
- (NSString *)resultString
{
TestResult r = [self result];
NSString *result;
if (_resultMessage)
result = [NSString stringWithFormat:@" else
result = [TestCase resultStringForResult:r];
return result;
}
- (NSString *)testOutput
{
return _testOutput;
}
- (void)setTestResult:(TestResult)result message:(NSString *)msg
{
BOOL badTransition = NO;
// sanity check that the result only changes in expected ways
switch (_result) {
case PENDING:
if (result != IN_PROGRESS && result != SKIPPED)
badTransition = YES;
break;
case IN_PROGRESS:
if (result != PASSED && result != FAILED)
badTransition = YES;
break;
case PASSED:
// allow the result to change from passed->failed to simplify post-test output handling
if (result != FAILED)
badTransition = YES;
break;
case SKIPPED:
badTransition = YES;
break;
case FAILED:
if (result != FAILED)
badTransition = YES;
break;
default:
badTransition = YES;
}
if (badTransition) {
// we might not be able to allocate here
// peg the result to failed to get attention
result = FAILED;
msg = @"Bogus test result transition";
}
_result = result;
if (_resultMessage == nil)
_resultMessage = msg;
}
- (BOOL)resurrectionIsFatal
{
return Auto::Environment::resurrection_is_fatal ? YES : NO;
}
- (void)monitorOutput:(id)unused
{
dispatch_async(_testQueue, ^{[self performTest];});
char buf[512];
NSMutableString *stringBuf = [NSMutableString string];
int bytes;
do {
bytes = read(_fds[0], buf, sizeof(buf)-1);
if (bytes > 0 && ![self failed]) {
buf[bytes] = 0;
[stringBuf appendFormat:@" if (!_testOutput)
_testOutput = [NSMutableString string];
[_testOutput appendFormat:@" NSRange newline;
do {
newline = [stringBuf rangeOfString:@"\n"];
if (newline.location != NSNotFound) {
NSString *line = [stringBuf substringWithRange:NSMakeRange(0, newline.location)];
[stringBuf deleteCharactersInRange:NSMakeRange(0, newline.location+1)];
if ([line length] > 0) {
dispatch_async(_testQueue, ^{if (![self failed]) [self processOutputLine:line];});
}
}
} while (newline.location != NSNotFound);
}
} while (bytes > 0);
if ([stringBuf length] > 0 && ![self failed]) {
dispatch_async(_testQueue, ^{[self processOutputLine:stringBuf];});
}
dispatch_async(_testQueue, ^{
[self outputComplete];
});
}
- (BOOL)shouldMonitorOutput
{
return YES;
}
- (void)beginMonitoringOutput
{
assertTestQueue();
if ([self shouldMonitorOutput]) {
// take over stderr
if (pipe(_fds) == -1) {
abort();
}
_orig_fd = dup(2);
if (_orig_fd == -1) {
abort();
}
if (dup2(_fds[1], 2) == -1) {
abort();
}
[NSThread detachNewThreadSelector:@selector(monitorOutput:) toTarget:self withObject:nil];
} else {
dispatch_async(_testQueue, ^{[self performTest];});
}
}
- (void)endMonitoringOutput
{
assertTestQueue();
// restore file descriptors
if (_orig_fd != -1) {
close(_fds[0]);
close(_fds[1]);
if (dup2(_orig_fd, 2)==-1)
abort();
_orig_fd = -1;
_fds[0] = -1;
_fds[1] = -1;
} else {
dispatch_async(_testQueue, ^{
[self outputComplete];
});
}
}
- (void)flushStderr
{
assertTestQueue();
[self endMonitoringOutput];
}
- (void)outputComplete;
{
assertTestQueue();
_outputCompleteCalled = YES;
[self invokeCompletionCallback];
}
- (void)startTest
{
NSLog(@"Running: _currentTestCase = self;
[self setTestResult:IN_PROGRESS message:nil];
[self runThreadLocalCollection];
dispatch_async(_testQueue, ^{[self beginMonitoringOutput];});
}
- (void)performTest
{
[self fail:[NSString stringWithFormat:@"}
- (NSString *)shouldSkip
{
return nil;
}
- (void)processOutputLine:(NSString *)line
{
NSString *collectionLog = @"[scan + freeze + finalize + reclaim]";
NSRange r = [line rangeOfString:collectionLog];
if (r.location != NSNotFound) {
NSFileHandle *err;
if (_orig_fd != -1)
err = [[NSFileHandle alloc] initWithFileDescriptor:_orig_fd closeOnDealloc:NO];
else
err = [NSFileHandle fileHandleWithStandardError];
[err writeData:[[line stringByAppendingString:@"\n"] dataUsingEncoding:NSMacOSRomanStringEncoding]];
} else {
[self fail:[NSString stringWithFormat:@"Unexpected console output: }
}
- (void)testFinished
{
assertTestQueue();
[self stopTestThread];
[self endMonitoringOutput];
[self invokeCompletionCallback];
}
- (void)passed
{
if (![self failed]) {
// resumptively set result to passed. may still change to failed based on output processing
[self setTestResult:PASSED message:nil];
}
}
- (void)fail:(NSString *)msg
{
[self setTestResult:FAILED message:msg];
}
/* Misc convenience methods */
- (BOOL)failed
{
return [self result] == FAILED;
}
- (void)collectWithOptions:(auto_zone_options_t)options
{
auto_zone_collect([self auto_zone], options);
}
- (void)collectWithOptions:(auto_zone_options_t)options completionCallback:(void (^)(void))callback
{
auto_zone_collect_and_notify([self auto_zone], options, _testQueue, callback);
}
- (void)runThreadLocalCollection
{
[self collectWithOptions:AUTO_ZONE_COLLECT_LOCAL_COLLECTION];
}
- (void)requestAdvisoryCollectionWithCompletionCallback:(void (^)(void))callback;
{
[self collectWithOptions:AUTO_ZONE_COLLECT_NO_OPTIONS completionCallback:callback];
}
- (void)requestGenerationalCollectionWithCompletionCallback:(void (^)(void))callback;
{
[self collectWithOptions:AUTO_ZONE_COLLECT_GENERATIONAL_COLLECTION completionCallback:callback];
}
- (void)requestFullCollectionWithCompletionCallback:(void (^)(void))callback;
{
[self collectWithOptions:AUTO_ZONE_COLLECT_FULL_COLLECTION completionCallback:callback];
}
- (void)requestExhaustiveCollectionWithCompletionCallback:(void (^)(void))callback;
{
[self collectWithOptions:AUTO_ZONE_COLLECT_EXHAUSTIVE_COLLECTION completionCallback:callback];
}
- (void)requestCompactionWithCompletionCallback:(void (^)(void))callback {
auto_zone_compact([self auto_zone], AUTO_ZONE_COMPACT_NO_OPTIONS, _testQueue, callback);
}
- (void)requestCompactionAnalysisWithCompletionCallback:(void (^)(void))callback {
auto_zone_compact([self auto_zone], AUTO_ZONE_COMPACT_ANALYZE, _testQueue, callback);
}
- (void)clearStack
{
objc_clear_stack(0);
}
- (void)didFinalize:(TestFinalizer *)finalizer
{
}
- (vm_address_t)disguise:(void *)pointer;
{
return (vm_address_t)((intptr_t)pointer + 1);
}
- (void *)undisguise:(vm_address_t)disguisedPointer;
{
return (void *)((intptr_t)disguisedPointer - 1);
}
- (BOOL)block:(void *)block isInList:(void **)blockList count:(uint32_t)count
{
BOOL found = NO;
uint32_t i;
for (i = 0; i < count && !found; i++)
found = (blockList[i] == block);
return found;
}
- (auto_zone_t *)auto_zone
{
return objc_collectableZone();
}
/* Test thread support */
#define STACK_BUFFER_SIZE 16
- (void)testThread
{
pthread_mutex_lock(&_mutex);
_testThread = pthread_self();
SEL s;
id stackBuffer[STACK_BUFFER_SIZE];
bzero(stackBuffer, sizeof(stackBuffer));
_disguisedStackBuffer = [self disguise:&stackBuffer[0]];
do {
while (_selector == nil)
pthread_cond_wait(&_cond, &_mutex);
s = _selector;
if (s != (SEL)-1) {
[self performSelector:s];
[self clearStack];
}
_selector = nil;
pthread_cond_signal(&_cond);
} while (s != (SEL)-1);
_disguisedStackBuffer = 0;
_testThread = NULL;
pthread_mutex_unlock(&_mutex);
}
void *test_thread_loop(void *arg) {
objc_registerThreadWithCollector();
TestCase *testCase = (TestCase *)arg;
CFRelease(testCase);
[testCase testThread];
return NULL;
}
- (void)startTestThread
{
if (_testThread != NULL) {
NSLog(@" exit(-1);
}
CFRetain(self);
pthread_mutex_lock(&_mutex);
if (pthread_create(&_testThread, NULL, test_thread_loop, self)) {
NSLog(@"pthread_create() failed");
exit(-1);
}
_selector = @selector(self);
while (_selector != nil)
pthread_cond_wait(&_cond, &_mutex);
pthread_mutex_unlock(&_mutex);
}
- (void)stopTestThread
{
pthread_mutex_lock(&_mutex);
if (_testThread != NULL) {
while (_selector != nil)
pthread_cond_wait(&_cond, &_mutex);
_selector = (SEL)-1;
pthread_cond_signal(&_cond);
while (_selector != nil)
pthread_cond_wait(&_cond, &_mutex);
}
pthread_mutex_unlock(&_mutex);
}
- (void)performSelectorOnTestThread:(SEL)cmd
{
if (!_testThread)
[self startTestThread];
pthread_mutex_lock(&_mutex);
while (_selector != nil)
pthread_cond_wait(&_cond, &_mutex);
_selector = cmd;
pthread_cond_signal(&_cond);
while (_selector != nil)
pthread_cond_wait(&_cond, &_mutex);
pthread_mutex_unlock(&_mutex);
}
- (id *)testThreadStackBuffer
{
if (pthread_self() != _testThread) {
NSLog(@" __builtin_trap();
}
return (id *)[self undisguise:_disguisedStackBuffer];
}
- (uintptr_t)testThreadStackBufferSize
{
return STACK_BUFFER_SIZE;
}
@end