diff --git a/GCDWebServer/Core/GCDWebServer.h b/GCDWebServer/Core/GCDWebServer.h index eca3b5e..9601b82 100644 --- a/GCDWebServer/Core/GCDWebServer.h +++ b/GCDWebServer/Core/GCDWebServer.h @@ -65,12 +65,8 @@ typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* r @property(nonatomic, readonly) NSURL* serverURL; // Only non-nil if server is running @property(nonatomic, readonly) NSURL* bonjourServerURL; // Only non-nil if server is running and Bonjour registration is active #if !TARGET_OS_IPHONE -@property(nonatomic, getter=isRecordingEnabled) BOOL recordingEnabled; // Creates files in the current directory containing the raw data for all requests and responses (directory most NOT contain prior recordings) - (BOOL)runWithPort:(NSUInteger)port; // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only) #endif -#ifdef __GCDWEBSERVER_ENABLE_TESTING__ -- (NSInteger)runTestsInDirectory:(NSString*)path withPort:(NSUInteger)port; // Returns number of failed tests or -1 if server failed to start -#endif @end @interface GCDWebServer (Handlers) @@ -94,3 +90,12 @@ typedef GCDWebServerResponse* (^GCDWebServerProcessBlock)(GCDWebServerRequest* r - (void)logWarning:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2); - (void)logError:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2); @end + +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ + +@interface GCDWebServer (Testing) +@property(nonatomic, getter=isRecordingEnabled) BOOL recordingEnabled; // Creates files in the current directory containing the raw data for all requests and responses (directory most NOT contain prior recordings) +- (NSInteger)runTestsInDirectory:(NSString*)path withPort:(NSUInteger)port; // Returns number of failed tests or -1 if server failed to start +@end + +#endif diff --git a/GCDWebServer/Core/GCDWebServer.m b/GCDWebServer/Core/GCDWebServer.m index e08066e..baa69dc 100644 --- a/GCDWebServer/Core/GCDWebServer.m +++ b/GCDWebServer/Core/GCDWebServer.m @@ -49,7 +49,7 @@ NSUInteger _port; dispatch_source_t _source; CFNetServiceRef _service; -#if !TARGET_OS_IPHONE +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ BOOL _recording; #endif } @@ -334,18 +334,6 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er @implementation GCDWebServer (Extensions) -#if !TARGET_OS_IPHONE - -- (void)setRecordingEnabled:(BOOL)flag { - _recording = flag; -} - -- (BOOL)isRecordingEnabled { - return _recording; -} - -#endif - - (NSURL*)serverURL { if (_source) { NSString* ipAddress = GCDWebServerGetPrimaryIPv4Address(); @@ -395,177 +383,6 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er #endif -#ifdef __GCDWEBSERVER_ENABLE_TESTING__ - -static CFHTTPMessageRef _CreateHTTPMessageFromData(NSData* data, BOOL isRequest) { - CFHTTPMessageRef message = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, isRequest); - if (CFHTTPMessageAppendBytes(message, data.bytes, data.length)) { - return message; - } - CFRelease(message); - return NULL; -} - -static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData, NSUInteger port) { - CFHTTPMessageRef response = NULL; - int httpSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); - if (httpSocket > 0) { - struct sockaddr_in addr4; - bzero(&addr4, sizeof(addr4)); - addr4.sin_len = sizeof(port); - addr4.sin_family = AF_INET; - addr4.sin_port = htons(8080); - addr4.sin_addr.s_addr = htonl(INADDR_ANY); - if (connect(httpSocket, (void*)&addr4, sizeof(addr4)) == 0) { - if (write(httpSocket, inData.bytes, inData.length) == (ssize_t)inData.length) { - NSMutableData* outData = [[NSMutableData alloc] initWithLength:(256 * 1024)]; - NSUInteger length = 0; - while (1) { - ssize_t result = read(httpSocket, (char*)outData.mutableBytes + length, outData.length - length); - if (result < 0) { - length = NSNotFound; - break; - } else if (result == 0) { - break; - } - length += result; - if (length >= outData.length) { - outData.length = 2 * outData.length; - } - } - if (length != NSNotFound) { - outData.length = length; - response = _CreateHTTPMessageFromData(outData, NO); - } else { - DNOT_REACHED(); - } - ARC_RELEASE(outData); - } - } - close(httpSocket); - } - return response; -} - -static void _LogResult(NSString* format, ...) { - va_list arguments; - va_start(arguments, format); - NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments]; - va_end(arguments); - fprintf(stdout, "%s\n", [message UTF8String]); - ARC_RELEASE(message); -} - -- (NSInteger)runTestsInDirectory:(NSString*)path withPort:(NSUInteger)port { - NSArray* ignoredHeaders = @[@"Date", @"Etag"]; // Dates are always different by definition and ETags depend on file system node IDs - NSInteger result = -1; - if ([self startWithPort:port bonjourName:nil]) { - - result = 0; - NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL]; - for (NSString* requestFile in files) { - if (![requestFile hasSuffix:@".request"]) { - continue; - } - @autoreleasepool { - NSString* index = [[requestFile componentsSeparatedByString:@"-"] firstObject]; - BOOL success = NO; - NSData* requestData = [NSData dataWithContentsOfFile:[path stringByAppendingPathComponent:requestFile]]; - if (requestData) { - CFHTTPMessageRef request = _CreateHTTPMessageFromData(requestData, YES); - if (request) { - NSString* requestMethod = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestMethod(request)); - NSURL* requestURL = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(request)); - _LogResult(@"[%i] %@ %@", (int)[index integerValue], requestMethod, requestURL.path); - NSString* prefix = [index stringByAppendingString:@"-"]; - for (NSString* responseFile in files) { - if ([responseFile hasPrefix:prefix] && [responseFile hasSuffix:@".response"]) { - NSData* responseData = [NSData dataWithContentsOfFile:[path stringByAppendingPathComponent:responseFile]]; - if (responseData) { - CFHTTPMessageRef expectedResponse = _CreateHTTPMessageFromData(responseData, NO); - if (expectedResponse) { - CFHTTPMessageRef actualResponse = _CreateHTTPMessageFromPerformingRequest(requestData, port); - if (actualResponse) { - success = YES; - - CFIndex expectedStatusCode = CFHTTPMessageGetResponseStatusCode(expectedResponse); - CFIndex actualStatusCode = CFHTTPMessageGetResponseStatusCode(actualResponse); - if (actualStatusCode != expectedStatusCode) { - _LogResult(@" Status code not matching:\n Expected: %i\n Actual: %i", (int)expectedStatusCode, (int)actualStatusCode); - success = NO; - } - - NSDictionary* expectedHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(expectedResponse)); - NSDictionary* actualHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(actualResponse)); - for (NSString* expectedHeader in expectedHeaders) { - if ([ignoredHeaders containsObject:expectedHeader]) { - continue; - } - NSString* expectedValue = [expectedHeaders objectForKey:expectedHeader]; - NSString* actualValue = [actualHeaders objectForKey:expectedHeader]; - if (![actualValue isEqualToString:expectedValue]) { - _LogResult(@" Header '%@' not matching:\n Expected: \"%@\"\n Actual: \"%@\"", expectedHeader, expectedValue, actualValue); - success = NO; - } - } - for (NSString* actualHeader in actualHeaders) { - if (![expectedHeaders objectForKey:actualHeader]) { - _LogResult(@" Header '%@' not matching:\n Expected: \"%@\"\n Actual: \"%@\"", actualHeader, nil, [actualHeaders objectForKey:actualHeader]); - success = NO; - } - } - - NSData* expectedBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(expectedResponse)); - NSData* actualBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(actualResponse)); - if (![actualBody isEqualToData:expectedBody]) { - _LogResult(@" Bodies not matching:\n Expected: %lu bytes\n Actual: %lu bytes", (unsigned long)expectedBody.length, (unsigned long)actualBody.length); - success = NO; -#if !TARGET_OS_IPHONE -#ifndef NDEBUG - if (GCDWebServerIsTextContentType([expectedHeaders objectForKey:@"Content-Type"])) { - NSString* expectedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; - NSString* actualPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; - if ([expectedBody writeToFile:expectedPath atomically:YES] && [actualBody writeToFile:actualPath atomically:YES]) { - NSTask* task = [[NSTask alloc] init]; - [task setLaunchPath:@"/usr/bin/opendiff"]; - [task setArguments:@[expectedPath, actualPath]]; - [task launch]; - ARC_RELEASE(task); - } - } -#endif -#endif - } - - CFRelease(actualResponse); - } - CFRelease(expectedResponse); - } - } else { - DNOT_REACHED(); - } - break; - } - } - CFRelease(request); - } - } else { - DNOT_REACHED(); - } - _LogResult(@""); - if (!success) { - ++result; - } - } - } - - [self stop]; - } - return result; -} - -#endif - @end @implementation GCDWebServer (Handlers) @@ -777,3 +594,186 @@ static void _LogResult(NSString* format, ...) { } @end + +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ + +@implementation GCDWebServer (Testing) + +- (void)setRecordingEnabled:(BOOL)flag { + _recording = flag; +} + +- (BOOL)isRecordingEnabled { + return _recording; +} + +static CFHTTPMessageRef _CreateHTTPMessageFromData(NSData* data, BOOL isRequest) { + CFHTTPMessageRef message = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, isRequest); + if (CFHTTPMessageAppendBytes(message, data.bytes, data.length)) { + return message; + } + CFRelease(message); + return NULL; +} + +static CFHTTPMessageRef _CreateHTTPMessageFromPerformingRequest(NSData* inData, NSUInteger port) { + CFHTTPMessageRef response = NULL; + int httpSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); + if (httpSocket > 0) { + struct sockaddr_in addr4; + bzero(&addr4, sizeof(addr4)); + addr4.sin_len = sizeof(port); + addr4.sin_family = AF_INET; + addr4.sin_port = htons(8080); + addr4.sin_addr.s_addr = htonl(INADDR_ANY); + if (connect(httpSocket, (void*)&addr4, sizeof(addr4)) == 0) { + if (write(httpSocket, inData.bytes, inData.length) == (ssize_t)inData.length) { + NSMutableData* outData = [[NSMutableData alloc] initWithLength:(256 * 1024)]; + NSUInteger length = 0; + while (1) { + ssize_t result = read(httpSocket, (char*)outData.mutableBytes + length, outData.length - length); + if (result < 0) { + length = NSNotFound; + break; + } else if (result == 0) { + break; + } + length += result; + if (length >= outData.length) { + outData.length = 2 * outData.length; + } + } + if (length != NSNotFound) { + outData.length = length; + response = _CreateHTTPMessageFromData(outData, NO); + } else { + DNOT_REACHED(); + } + ARC_RELEASE(outData); + } + } + close(httpSocket); + } + return response; +} + +static void _LogResult(NSString* format, ...) { + va_list arguments; + va_start(arguments, format); + NSString* message = [[NSString alloc] initWithFormat:format arguments:arguments]; + va_end(arguments); + fprintf(stdout, "%s\n", [message UTF8String]); + ARC_RELEASE(message); +} + +- (NSInteger)runTestsInDirectory:(NSString*)path withPort:(NSUInteger)port { + NSArray* ignoredHeaders = @[@"Date", @"Etag"]; // Dates are always different by definition and ETags depend on file system node IDs + NSInteger result = -1; + if ([self startWithPort:port bonjourName:nil]) { + + result = 0; + NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL]; + for (NSString* requestFile in files) { + if (![requestFile hasSuffix:@".request"]) { + continue; + } + @autoreleasepool { + NSString* index = [[requestFile componentsSeparatedByString:@"-"] firstObject]; + BOOL success = NO; + NSData* requestData = [NSData dataWithContentsOfFile:[path stringByAppendingPathComponent:requestFile]]; + if (requestData) { + CFHTTPMessageRef request = _CreateHTTPMessageFromData(requestData, YES); + if (request) { + NSString* requestMethod = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestMethod(request)); + NSURL* requestURL = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(request)); + _LogResult(@"[%i] %@ %@", (int)[index integerValue], requestMethod, requestURL.path); + NSString* prefix = [index stringByAppendingString:@"-"]; + for (NSString* responseFile in files) { + if ([responseFile hasPrefix:prefix] && [responseFile hasSuffix:@".response"]) { + NSData* responseData = [NSData dataWithContentsOfFile:[path stringByAppendingPathComponent:responseFile]]; + if (responseData) { + CFHTTPMessageRef expectedResponse = _CreateHTTPMessageFromData(responseData, NO); + if (expectedResponse) { + CFHTTPMessageRef actualResponse = _CreateHTTPMessageFromPerformingRequest(requestData, port); + if (actualResponse) { + success = YES; + + CFIndex expectedStatusCode = CFHTTPMessageGetResponseStatusCode(expectedResponse); + CFIndex actualStatusCode = CFHTTPMessageGetResponseStatusCode(actualResponse); + if (actualStatusCode != expectedStatusCode) { + _LogResult(@" Status code not matching:\n Expected: %i\n Actual: %i", (int)expectedStatusCode, (int)actualStatusCode); + success = NO; + } + + NSDictionary* expectedHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(expectedResponse)); + NSDictionary* actualHeaders = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyAllHeaderFields(actualResponse)); + for (NSString* expectedHeader in expectedHeaders) { + if ([ignoredHeaders containsObject:expectedHeader]) { + continue; + } + NSString* expectedValue = [expectedHeaders objectForKey:expectedHeader]; + NSString* actualValue = [actualHeaders objectForKey:expectedHeader]; + if (![actualValue isEqualToString:expectedValue]) { + _LogResult(@" Header '%@' not matching:\n Expected: \"%@\"\n Actual: \"%@\"", expectedHeader, expectedValue, actualValue); + success = NO; + } + } + for (NSString* actualHeader in actualHeaders) { + if (![expectedHeaders objectForKey:actualHeader]) { + _LogResult(@" Header '%@' not matching:\n Expected: \"%@\"\n Actual: \"%@\"", actualHeader, nil, [actualHeaders objectForKey:actualHeader]); + success = NO; + } + } + + NSData* expectedBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(expectedResponse)); + NSData* actualBody = ARC_BRIDGE_RELEASE(CFHTTPMessageCopyBody(actualResponse)); + if (![actualBody isEqualToData:expectedBody]) { + _LogResult(@" Bodies not matching:\n Expected: %lu bytes\n Actual: %lu bytes", (unsigned long)expectedBody.length, (unsigned long)actualBody.length); + success = NO; +#if !TARGET_OS_IPHONE +#ifndef NDEBUG + if (GCDWebServerIsTextContentType([expectedHeaders objectForKey:@"Content-Type"])) { + NSString* expectedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; + NSString* actualPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingPathExtension:@"txt"]]; + if ([expectedBody writeToFile:expectedPath atomically:YES] && [actualBody writeToFile:actualPath atomically:YES]) { + NSTask* task = [[NSTask alloc] init]; + [task setLaunchPath:@"/usr/bin/opendiff"]; + [task setArguments:@[expectedPath, actualPath]]; + [task launch]; + ARC_RELEASE(task); + } + } +#endif +#endif + } + + CFRelease(actualResponse); + } + CFRelease(expectedResponse); + } + } else { + DNOT_REACHED(); + } + break; + } + } + CFRelease(request); + } + } else { + DNOT_REACHED(); + } + _LogResult(@""); + if (!success) { + ++result; + } + } + } + + [self stop]; + } + return result; +} + +@end + +#endif diff --git a/GCDWebServer/Core/GCDWebServerConnection.m b/GCDWebServer/Core/GCDWebServerConnection.m index 05416b4..d498b51 100644 --- a/GCDWebServer/Core/GCDWebServerConnection.m +++ b/GCDWebServer/Core/GCDWebServerConnection.m @@ -27,7 +27,7 @@ #import #import -#if !TARGET_OS_IPHONE +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ #import #endif @@ -49,7 +49,7 @@ static NSData* _CRLFData = nil; static NSData* _CRLFCRLFData = nil; static NSData* _continueData = nil; static NSData* _lastChunkData = nil; -#if !TARGET_OS_IPHONE +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ static int32_t _connectionCounter = 0; #endif @@ -71,7 +71,7 @@ static int32_t _connectionCounter = 0; NSInteger _statusCode; BOOL _opened; -#if !TARGET_OS_IPHONE +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ NSUInteger _connectionIndex; NSString* _requestPath; int _requestFD; @@ -93,7 +93,7 @@ static int32_t _connectionCounter = 0; LOG_DEBUG(@"Connection received %zu bytes on socket %i", size, _socket); _bytesRead += size; [self didUpdateBytesRead]; -#if !TARGET_OS_IPHONE +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ if (_requestFD > 0) { bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) { return (write(_requestFD, chunkBytes, chunkSize) == (ssize_t)chunkSize); @@ -291,7 +291,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { - (void)_writeBuffer:(dispatch_data_t)buffer withCompletionBlock:(WriteBufferCompletionBlock)block { size_t size = dispatch_data_get_size(buffer); -#if !TARGET_OS_IPHONE +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ ARC_DISPATCH_RETAIN(buffer); #endif dispatch_write(_socket, buffer, kGCDWebServerGCDQueue, ^(dispatch_data_t data, int error) { @@ -302,7 +302,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { LOG_DEBUG(@"Connection sent %zu bytes on socket %i", size, _socket); _bytesWritten += size; [self didUpdateBytesWritten]; -#if !TARGET_OS_IPHONE +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ if (_responseFD > 0) { bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) { return (write(_responseFD, chunkBytes, chunkSize) == (ssize_t)chunkSize); @@ -320,7 +320,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) { block(NO); } } -#if !TARGET_OS_IPHONE +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ ARC_DISPATCH_RELEASE(buffer); #endif @@ -715,7 +715,7 @@ static NSString* _StringFromAddressData(NSData* data) { } ARC_RELEASE(_response); -#if !TARGET_OS_IPHONE +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ ARC_RELEASE(_requestPath); ARC_RELEASE(_responsePath); #endif @@ -728,7 +728,7 @@ static NSString* _StringFromAddressData(NSData* data) { @implementation GCDWebServerConnection (Subclassing) - (BOOL)open { -#if !TARGET_OS_IPHONE +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ if (_server.recordingEnabled) { _connectionIndex = OSAtomicIncrement32(&_connectionCounter); @@ -804,7 +804,7 @@ static inline BOOL _CompareResources(NSString* responseETag, NSString* requestET } - (void)close { -#if !TARGET_OS_IPHONE +#ifdef __GCDWEBSERVER_ENABLE_TESTING__ if (_requestPath) { BOOL success = NO; NSError* error = nil; diff --git a/Mac/main.m b/Mac/main.m index 74e0e86..8557121 100644 --- a/Mac/main.m +++ b/Mac/main.m @@ -39,6 +39,10 @@ #import "GCDWebUploader.h" +#ifndef __GCDWEBSERVER_ENABLE_TESTING__ +#error __GCDWEBSERVER_ENABLE_TESTING__ must be defined +#endif + typedef enum { kMode_WebServer = 0, kMode_HTMLPage,