1/*
2 * Copyright (c) 2011 Apple Inc. All rights reserved.
3 *
4 * @APPLE_APACHE_LICENSE_HEADER_START@
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 *     http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 * @APPLE_APACHE_LICENSE_HEADER_END@
19 */
20//
21//  TestCase.m
22//  Copyright (c) 2009-2011 Apple Inc. All rights reserved.
23//
24
25#import "TestCase.h"
26#import "Environment.h"
27#import <objc/objc-auto.h>
28#import <objc/objc-class.h>
29#import <unistd.h>
30
31#define assertTestQueue() do { \
32if (dispatch_get_current_queue() != _testQueue) { \
33    NSLog(@"%@ invoked on incorrect dispatch queue", NSStringFromSelector(_cmd)); abort(); \
34}} while (0)
35
36@implementation TestFinalizer : NSObject
37
38- (id)init
39{
40    self = [super init];
41    if (self) {
42        _allocatingTest = [TestCase currentTestCase];
43    }
44    return self;
45}
46
47- (void)finalize
48{
49    TestCase *tc = [TestCase currentTestCase];
50    // Here is a convenient place to test objc_is_finalized()
51    if (!objc_is_finalized(self)) {
52        [tc fail:[NSString stringWithFormat:@"objc_is_finalized(self) returns false during -finalize. self = %@, allocating = %@, running = %@", [self className], [_allocatingTest className], [tc className]]];
53    }
54    if (tc == _allocatingTest) {
55        [tc didFinalize:self];
56    }
57    [super finalize];
58}
59@end
60
61@implementation TestCase
62
63static TestCase *_currentTestCase;
64
65+ (id)currentTestCase
66{
67    return _currentTestCase;
68}
69
70+ (BOOL)isConcreteTestCase
71{
72    Class tc = [TestCase class];
73    IMP startImp = [tc instanceMethodForSelector:@selector(performTest)];
74    if ([self instanceMethodForSelector:@selector(performTest)] != startImp) {
75        return YES;
76    }
77    return NO;
78}
79
80static NSInteger sortClassesByName(id cls1, id cls2, void* context) {
81    return strcmp(class_getName(cls1), class_getName(cls2));
82}
83
84+ (NSArray *)testClasses
85{
86    static NSArray *_testClasses = nil;
87    // Dynamically find all the subclasses of AutoTestScript
88    if (_testClasses == nil) {
89        NSMutableArray *testClasses = [[NSMutableArray alloc] init];
90        int numClasses;
91        Class * classes = NULL;
92        Class tc = [TestCase class];
93        IMP startImp = [tc instanceMethodForSelector:@selector(performTest)];
94
95        classes = NULL;
96        numClasses = objc_getClassList(NULL, 0);
97
98        if (numClasses > 0 )
99        {
100            classes = (Class *)malloc(sizeof(Class) * numClasses);
101            numClasses = objc_getClassList(classes, numClasses);
102            int i;
103            for (i=0; i<numClasses; i++) {
104                // Some fumkiness to weed out weird clases like NSZombie and Object without actually messaging them.
105                Class sc = class_getSuperclass(classes[i]);
106                if (sc) {
107                    Class ssc = class_getSuperclass(sc);
108                    if (ssc) {
109                        if ([classes[i] isSubclassOfClass:tc]) {
110                            if ([classes[i] isConcreteTestCase])
111                                [testClasses addObject:classes[i]];
112                        }
113                    }
114                }
115            }
116            free(classes);
117        }
118        _testClasses = [testClasses sortedArrayUsingFunction:sortClassesByName context:NULL];
119    }
120    return _testClasses;
121}
122
123+ (void)initialize
124{
125    if (self == [TestCase class]) {
126        Auto::Environment::initialize();
127    }
128}
129
130+ (NSString *)resultStringForResult:(TestResult)resultCode
131{
132    NSString *result;
133    switch (resultCode) {
134        case PASSED:
135            result = @"PASSED";
136            break;
137        case FAILED:
138            result = @"FAILED";
139            break;
140        case SKIPPED:
141            result = @"SKIPPED";
142            break;
143        default:
144            result = @"UNKNOWN";
145            break;
146    }
147    return result;
148}
149
150
151- (id)init
152{
153    self = [super init];
154    if (self) {
155        NSString *label = [NSString stringWithFormat:@"Test Case: %@", [self className]];
156        _testQueue = dispatch_queue_create([label UTF8String], NULL);
157        _result = PENDING;
158        pthread_mutex_init(&_mutex, NULL);
159        pthread_cond_init(&_cond, NULL);
160        _orig_fd = -1;
161    }
162    return self;
163}
164
165- (void)finalize
166{
167    dispatch_release(_testQueue);
168    [super finalize];
169}
170
171
172- (void)setCompletionCallback:(void (^)(void))completionCallback
173{
174    _completionCallback = Block_copy(completionCallback);
175    _completionCallbackQueue = dispatch_get_current_queue();
176    dispatch_retain(_completionCallbackQueue);
177}
178
179- (void)invokeCompletionCallback
180{
181    assertTestQueue();
182    // safe to call this multiple times
183    // do not really do it until we are really done
184    if (_completionCallback && !_testThread && _outputCompleteCalled && _result != IN_PROGRESS) {
185        if (_completionCallback) {
186            _currentTestCase = nil;
187            dispatch_async(_completionCallbackQueue, _completionCallback);
188            Block_release(_completionCallback);
189            dispatch_release(_completionCallbackQueue);
190            _completionCallback = nil;
191        }
192    }
193}
194
195- (TestResult)result
196{
197    return _result;
198}
199
200- (NSString *)resultString
201{
202    TestResult r = [self result];
203    NSString *result;
204    if (_resultMessage)
205        result = [NSString stringWithFormat:@"%@: %@", [TestCase resultStringForResult:r], _resultMessage];
206    else
207        result = [TestCase resultStringForResult:r];
208    return result;
209}
210
211- (NSString *)testOutput
212{
213    return _testOutput;
214}
215
216- (void)setTestResult:(TestResult)result message:(NSString *)msg
217{
218    BOOL badTransition = NO;
219    // sanity check that the result only changes in expected ways
220    switch (_result) {
221        case PENDING:
222            if (result != IN_PROGRESS && result != SKIPPED)
223                badTransition = YES;
224            break;
225        case IN_PROGRESS:
226            if (result != PASSED && result != FAILED)
227                badTransition = YES;
228            break;
229        case PASSED:
230            // allow the result to change from passed->failed to simplify post-test output handling
231            if (result != FAILED)
232                badTransition = YES;
233            break;
234        case SKIPPED:
235            badTransition = YES;
236            break;
237        case FAILED:
238            if (result != FAILED)
239                badTransition = YES;
240            break;
241        default:
242            badTransition = YES;
243    }
244    if (badTransition) {
245        // we might not be able to allocate here
246        // peg the result to failed to get attention
247        result = FAILED;
248        msg = @"Bogus test result transition";
249    }
250    _result = result;
251    if (_resultMessage == nil)
252        _resultMessage = msg;
253}
254
255- (BOOL)resurrectionIsFatal
256{
257    return Auto::Environment::resurrection_is_fatal ? YES : NO;
258}
259
260- (void)monitorOutput:(id)unused
261{
262    dispatch_async(_testQueue, ^{[self performTest];});
263    char buf[512];
264    NSMutableString *stringBuf = [NSMutableString string];
265    int bytes;
266    do {
267        bytes = read(_fds[0], buf, sizeof(buf)-1);
268        if (bytes > 0 && ![self failed]) {
269            buf[bytes] = 0;
270            [stringBuf appendFormat:@"%s", buf];
271            if (!_testOutput)
272                _testOutput = [NSMutableString string];
273            [_testOutput appendFormat:@"%s", buf];
274            NSRange newline;
275            do {
276                newline = [stringBuf rangeOfString:@"\n"];
277                if (newline.location != NSNotFound) {
278                    NSString *line = [stringBuf substringWithRange:NSMakeRange(0, newline.location)];
279                    [stringBuf deleteCharactersInRange:NSMakeRange(0, newline.location+1)];
280                    if ([line length] > 0) {
281                        dispatch_async(_testQueue, ^{if (![self failed]) [self processOutputLine:line];});
282                    }
283                }
284            } while (newline.location != NSNotFound);
285        }
286    } while (bytes > 0);
287    if ([stringBuf length] > 0 && ![self failed]) {
288        dispatch_async(_testQueue, ^{[self processOutputLine:stringBuf];});
289    }
290    dispatch_async(_testQueue, ^{
291        [self outputComplete];
292    });
293}
294
295- (BOOL)shouldMonitorOutput
296{
297    return YES;
298}
299
300- (void)beginMonitoringOutput
301{
302    assertTestQueue();
303
304    if ([self shouldMonitorOutput]) {
305        // take over stderr
306        if (pipe(_fds) == -1) {
307            abort();
308        }
309
310        _orig_fd = dup(2);
311        if (_orig_fd == -1) {
312            abort();
313        }
314        if (dup2(_fds[1], 2) == -1) {
315            abort();
316        }
317
318        [NSThread detachNewThreadSelector:@selector(monitorOutput:) toTarget:self withObject:nil];
319    } else {
320        dispatch_async(_testQueue, ^{[self performTest];});
321    }
322}
323
324- (void)endMonitoringOutput
325{
326    assertTestQueue();
327    // restore file descriptors
328    if (_orig_fd != -1) {
329        close(_fds[0]);
330        close(_fds[1]);
331        if (dup2(_orig_fd, 2)==-1)
332            abort();
333        _orig_fd = -1;
334        _fds[0] = -1;
335        _fds[1] = -1;
336    } else {
337        dispatch_async(_testQueue, ^{
338            [self outputComplete];
339        });
340    }
341}
342
343- (void)flushStderr
344{
345    assertTestQueue();
346    [self endMonitoringOutput];
347}
348
349- (void)outputComplete;
350{
351    assertTestQueue();
352    _outputCompleteCalled = YES;
353    [self invokeCompletionCallback];
354}
355
356
357- (void)startTest
358{
359    NSLog(@"Running: %@", [self className]);
360    _currentTestCase = self;
361    [self setTestResult:IN_PROGRESS message:nil];
362    [self runThreadLocalCollection];
363    dispatch_async(_testQueue, ^{[self beginMonitoringOutput];});
364}
365
366- (void)performTest
367{
368    [self fail:[NSString stringWithFormat:@"%@ not overridden in test class: %@", NSStringFromSelector(_cmd), [self className]]];
369}
370
371- (NSString *)shouldSkip
372{
373    return nil;
374}
375
376- (void)processOutputLine:(NSString *)line
377{
378    NSString *collectionLog = @"[scan + freeze + finalize + reclaim]";
379    NSRange r = [line rangeOfString:collectionLog];
380    if (r.location != NSNotFound) {
381        NSFileHandle *err;
382        if (_orig_fd != -1)
383            err = [[NSFileHandle alloc] initWithFileDescriptor:_orig_fd closeOnDealloc:NO];
384        else
385            err = [NSFileHandle fileHandleWithStandardError];
386        [err writeData:[[line stringByAppendingString:@"\n"] dataUsingEncoding:NSMacOSRomanStringEncoding]];
387    } else {
388        [self fail:[NSString stringWithFormat:@"Unexpected console output: %@", line]];
389    }
390}
391
392- (void)testFinished
393{
394    assertTestQueue();
395    [self stopTestThread];
396    [self endMonitoringOutput];
397    [self invokeCompletionCallback];
398}
399
400- (void)passed
401{
402    if (![self failed]) {
403        // resumptively set result to passed. may still change to failed based on output processing
404        [self setTestResult:PASSED message:nil];
405    }
406}
407
408- (void)fail:(NSString *)msg
409{
410    [self setTestResult:FAILED message:msg];
411}
412
413
414/* Misc convenience methods */
415
416- (BOOL)failed
417{
418    return [self result] == FAILED;
419}
420
421- (void)collectWithOptions:(auto_zone_options_t)options
422{
423    auto_zone_collect([self auto_zone], options);
424}
425
426- (void)collectWithOptions:(auto_zone_options_t)options completionCallback:(void (^)(void))callback
427{
428    auto_zone_collect_and_notify([self auto_zone], options, _testQueue, callback);
429}
430
431- (void)runThreadLocalCollection
432{
433    [self collectWithOptions:AUTO_ZONE_COLLECT_LOCAL_COLLECTION];
434}
435
436- (void)requestAdvisoryCollectionWithCompletionCallback:(void (^)(void))callback;
437{
438    [self collectWithOptions:AUTO_ZONE_COLLECT_NO_OPTIONS completionCallback:callback];
439}
440
441- (void)requestGenerationalCollectionWithCompletionCallback:(void (^)(void))callback;
442{
443    [self collectWithOptions:AUTO_ZONE_COLLECT_GENERATIONAL_COLLECTION completionCallback:callback];
444}
445
446- (void)requestFullCollectionWithCompletionCallback:(void (^)(void))callback;
447{
448    [self collectWithOptions:AUTO_ZONE_COLLECT_FULL_COLLECTION completionCallback:callback];
449}
450
451- (void)requestExhaustiveCollectionWithCompletionCallback:(void (^)(void))callback;
452{
453    [self collectWithOptions:AUTO_ZONE_COLLECT_EXHAUSTIVE_COLLECTION completionCallback:callback];
454}
455
456- (void)requestCompactionWithCompletionCallback:(void (^)(void))callback {
457    auto_zone_compact([self auto_zone], AUTO_ZONE_COMPACT_NO_OPTIONS, _testQueue, callback);
458}
459
460- (void)requestCompactionAnalysisWithCompletionCallback:(void (^)(void))callback {
461    auto_zone_compact([self auto_zone], AUTO_ZONE_COMPACT_ANALYZE, _testQueue, callback);
462}
463
464- (void)clearStack
465{
466    objc_clear_stack(0);
467}
468
469- (void)didFinalize:(TestFinalizer *)finalizer
470{
471}
472
473- (vm_address_t)disguise:(void *)pointer;
474{
475    return (vm_address_t)((intptr_t)pointer + 1);
476}
477
478
479- (void *)undisguise:(vm_address_t)disguisedPointer;
480{
481    return (void *)((intptr_t)disguisedPointer - 1);
482}
483
484- (BOOL)block:(void *)block isInList:(void **)blockList count:(uint32_t)count
485{
486    BOOL found = NO;
487    uint32_t i;
488    for (i = 0; i < count && !found; i++)
489        found = (blockList[i] == block);
490    return found;
491}
492
493- (auto_zone_t *)auto_zone
494{
495    return objc_collectableZone();
496}
497
498
499
500
501
502
503
504/* Test thread support */
505
506
507#define STACK_BUFFER_SIZE 16
508- (void)testThread
509{
510    pthread_mutex_lock(&_mutex);
511    _testThread = pthread_self();
512    SEL s;
513    id stackBuffer[STACK_BUFFER_SIZE];
514    bzero(stackBuffer, sizeof(stackBuffer));
515    _disguisedStackBuffer = [self disguise:&stackBuffer[0]];
516    do {
517        while (_selector == nil)
518            pthread_cond_wait(&_cond, &_mutex);
519        s = _selector;
520        if (s != (SEL)-1) {
521            [self performSelector:s];
522            [self clearStack];
523        }
524        _selector = nil;
525        pthread_cond_signal(&_cond);
526    } while (s != (SEL)-1);
527    _disguisedStackBuffer = 0;
528    _testThread = NULL;
529    pthread_mutex_unlock(&_mutex);
530}
531
532void *test_thread_loop(void *arg) {
533    objc_registerThreadWithCollector();
534    TestCase *testCase = (TestCase *)arg;
535    CFRelease(testCase);
536    [testCase testThread];
537    return NULL;
538}
539
540- (void)startTestThread
541{
542    if (_testThread != NULL) {
543        NSLog(@"%@: attempted to start test thread twice", [self className]);
544        exit(-1);
545    }
546    CFRetain(self);
547    pthread_mutex_lock(&_mutex);
548    if (pthread_create(&_testThread, NULL, test_thread_loop, self)) {
549        NSLog(@"pthread_create() failed");
550        exit(-1);
551    }
552    _selector = @selector(self);
553    while (_selector != nil)
554        pthread_cond_wait(&_cond, &_mutex);
555    pthread_mutex_unlock(&_mutex);
556}
557
558- (void)stopTestThread
559{
560    pthread_mutex_lock(&_mutex);
561    if (_testThread != NULL) {
562        while (_selector != nil)
563            pthread_cond_wait(&_cond, &_mutex);
564        _selector = (SEL)-1;
565        pthread_cond_signal(&_cond);
566        while (_selector != nil)
567            pthread_cond_wait(&_cond, &_mutex);
568    }
569    pthread_mutex_unlock(&_mutex);
570}
571
572- (void)performSelectorOnTestThread:(SEL)cmd
573{
574    if (!_testThread)
575        [self startTestThread];
576    pthread_mutex_lock(&_mutex);
577    while (_selector != nil)
578        pthread_cond_wait(&_cond, &_mutex);
579    _selector = cmd;
580    pthread_cond_signal(&_cond);
581    while (_selector != nil)
582        pthread_cond_wait(&_cond, &_mutex);
583    pthread_mutex_unlock(&_mutex);
584}
585
586- (id *)testThreadStackBuffer
587{
588    if (pthread_self() != _testThread) {
589        NSLog(@"%@ can only be called on the test thread", self);
590        __builtin_trap();
591    }
592    return (id *)[self undisguise:_disguisedStackBuffer];
593}
594
595- (uintptr_t)testThreadStackBufferSize
596{
597    return STACK_BUFFER_SIZE;
598}
599
600@end
601