浏览代码

#35 First pass at unit tests

Pierre-Olivier Latour 11 年之前
父节点
当前提交
1b6e4f6491

+ 2 - 0
.gitignore

@@ -1,3 +1,5 @@
 .DS_Store
 .DS_Store
 xcuserdata
 xcuserdata
 project.xcworkspace
 project.xcworkspace
+
+Tests/Payload

+ 2 - 0
CGDWebServer/GCDWebServer.h

@@ -79,7 +79,9 @@ NSString* GCDWebServerGetPrimaryIPv4Address();  // Returns IPv4 address of prima
 @property(nonatomic, readonly) NSURL* serverURL;  // Only non-nil if server is running
 @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
 @property(nonatomic, readonly) NSURL* bonjourServerURL;  // Only non-nil if server is running and Bonjour registration is active
 #if !TARGET_OS_IPHONE
 #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)
 - (BOOL)runWithPort:(NSUInteger)port;  // Starts then automatically stops on SIGINT i.e. Ctrl-C (use on main thread only)
+- (NSInteger)runTestsInDirectory:(NSString*)path withPort:(NSUInteger)port;  // Returns number of failed tests or -1 if server failed to start
 #endif
 #endif
 @end
 @end
 
 

+ 166 - 3
CGDWebServer/GCDWebServer.m

@@ -53,6 +53,9 @@
   NSUInteger _port;
   NSUInteger _port;
   dispatch_source_t _source;
   dispatch_source_t _source;
   CFNetServiceRef _service;
   CFNetServiceRef _service;
+#if !TARGET_OS_IPHONE
+  BOOL _recording;
+#endif
 }
 }
 @end
 @end
 
 
@@ -152,9 +155,13 @@ NSDate* GCDWebServerParseHTTPDate(NSString* string) {
   return date;
   return date;
 }
 }
 
 
-NSString* GCDWebServerDescribeData(NSData* data, NSString* contentType) {
-  if ([contentType hasPrefix:@"text/"] || [contentType isEqualToString:@"application/json"] || [contentType isEqualToString:@"application/xml"]) {
-    NSString* charset = GCDWebServerExtractHeaderValueParameter(contentType, @"charset");
+static inline BOOL _IsTextContentType(NSString* type) {
+  return ([type hasPrefix:@"text/"] || [type hasPrefix:@"application/json"] || [type hasPrefix:@"application/xml"]);
+}
+
+NSString* GCDWebServerDescribeData(NSData* data, NSString* type) {
+  if (_IsTextContentType(type)) {
+    NSString* charset = GCDWebServerExtractHeaderValueParameter(type, @"charset");
     NSString* string = [[NSString alloc] initWithData:data encoding:GCDWebServerStringEncodingFromCharset(charset)];
     NSString* string = [[NSString alloc] initWithData:data encoding:GCDWebServerStringEncodingFromCharset(charset)];
     if (string) {
     if (string) {
       return ARC_AUTORELEASE(string);
       return ARC_AUTORELEASE(string);
@@ -529,6 +536,18 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
 
 
 @implementation GCDWebServer (Extensions)
 @implementation GCDWebServer (Extensions)
 
 
+#if !TARGET_OS_IPHONE
+
+- (void)setRecordingEnabled:(BOOL)flag {
+  _recording = flag;
+}
+
+- (BOOL)isRecordingEnabled {
+  return _recording;
+}
+
+#endif
+
 - (NSURL*)serverURL {
 - (NSURL*)serverURL {
   if (_source) {
   if (_source) {
     NSString* ipAddress = GCDWebServerGetPrimaryIPv4Address();
     NSString* ipAddress = GCDWebServerGetPrimaryIPv4Address();
@@ -576,6 +595,150 @@ static void _NetServiceClientCallBack(CFNetServiceRef service, CFStreamError* er
   return success;
   return success;
 }
 }
 
 
+static CFHTTPMessageRef _CreateHTTPMessageFromFileDump(NSString* path, BOOL isRequest) {
+  NSData* data = [NSData dataWithContentsOfFile:path];
+  if (data) {
+    CFHTTPMessageRef message = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, isRequest);
+    if (CFHTTPMessageAppendBytes(message, data.bytes, data.length)) {
+      return message;
+    }
+    CFRelease(message);
+  }
+  return NULL;
+}
+
+static CFHTTPMessageRef _CreateHTTPMessageFromHTTPRequestResponse(CFHTTPMessageRef request) {
+  CFHTTPMessageRef response = NULL;
+  CFReadStreamRef stream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, request);
+  if (CFReadStreamOpen(stream)) {
+    CFMutableDataRef data = CFDataCreateMutable(kCFAllocatorDefault, 0);
+    CFDataSetLength(data, 256 * 1024);
+    CFIndex length = 0;
+    while (1) {
+      CFIndex result = CFReadStreamRead(stream, CFDataGetMutableBytePtr(data) + length, CFDataGetLength(data) - length);
+      if (result <= 0) {
+        break;
+      }
+      length += result;
+      if (length >= CFDataGetLength(data)) {
+        CFDataSetLength(data, 2 * CFDataGetLength(data));
+      }
+    }
+    if (CFReadStreamGetStatus(stream) == kCFStreamStatusAtEnd) {
+      response = (CFHTTPMessageRef)CFReadStreamCopyProperty(stream, kCFStreamPropertyHTTPResponseHeader);
+      if (response) {
+        CFDataSetLength(data, length);
+        CFHTTPMessageSetBody(response, data);
+      }
+    }
+    CFRelease(data);
+    CFReadStreamClose(stream);
+    CFRelease(stream);
+  }
+  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 {
+  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;
+        CFHTTPMessageRef request = _CreateHTTPMessageFromFileDump([path stringByAppendingPathComponent:requestFile], YES);
+        if (request) {
+          _LogResult(@"[%i] %@ %@", (int)[index integerValue], ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestMethod(request)), [ARC_BRIDGE_RELEASE(CFHTTPMessageCopyRequestURL(request)) path]);
+          NSString* prefix = [index stringByAppendingString:@"-"];
+          for (NSString* responseFile in files) {
+            if ([responseFile hasPrefix:prefix] && [responseFile hasSuffix:@".response"]) {
+              CFHTTPMessageRef expectedResponse = _CreateHTTPMessageFromFileDump([path stringByAppendingPathComponent:responseFile], NO);
+              if (expectedResponse) {
+                CFHTTPMessageRef actualResponse = _CreateHTTPMessageFromHTTPRequestResponse(request);
+                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 ([expectedHeader isEqualToString:@"Date"]) {
+                      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 (_IsTextContentType([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);
+                      }
+                    }
+                  }
+                  
+                  CFRelease(actualResponse);
+                }
+                CFRelease(expectedResponse);
+              }
+              break;
+            }
+          }
+          CFRelease(request);
+        }
+        _LogResult(@"");
+        if (!success) {
+          ++result;
+        }
+      }
+    }
+    
+    [self stop];
+  }
+  return result;
+}
+
 #endif
 #endif
 
 
 @end
 @end

+ 104 - 7
CGDWebServer/GCDWebServerConnection.m

@@ -25,7 +25,11 @@
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
  */
 
 
+#import <TargetConditionals.h>
 #import <netdb.h>
 #import <netdb.h>
+#if !TARGET_OS_IPHONE
+#import <libkern/OSAtomic.h>
+#endif
 
 
 #import "GCDWebServerPrivate.h"
 #import "GCDWebServerPrivate.h"
 
 
@@ -45,6 +49,9 @@ static NSData* _CRLFData = nil;
 static NSData* _CRLFCRLFData = nil;
 static NSData* _CRLFCRLFData = nil;
 static NSData* _continueData = nil;
 static NSData* _continueData = nil;
 static NSData* _lastChunkData = nil;
 static NSData* _lastChunkData = nil;
+#if !TARGET_OS_IPHONE
+static int32_t _connectionCounter = 0;
+#endif
 
 
 @interface GCDWebServerConnection () {
 @interface GCDWebServerConnection () {
 @private
 @private
@@ -62,6 +69,14 @@ static NSData* _lastChunkData = nil;
   CFHTTPMessageRef _responseMessage;
   CFHTTPMessageRef _responseMessage;
   GCDWebServerResponse* _response;
   GCDWebServerResponse* _response;
   NSInteger _statusCode;
   NSInteger _statusCode;
+  
+#if !TARGET_OS_IPHONE
+  NSUInteger _connectionIndex;
+  NSString* _requestPath;
+  int _requestFD;
+  NSString* _responsePath;
+  int _responseFD;
+#endif
 }
 }
 @end
 @end
 
 
@@ -77,6 +92,18 @@ static NSData* _lastChunkData = nil;
           LOG_DEBUG(@"Connection received %zu bytes on socket %i", size, _socket);
           LOG_DEBUG(@"Connection received %zu bytes on socket %i", size, _socket);
           _bytesRead += size;
           _bytesRead += size;
           [self didUpdateBytesRead];
           [self didUpdateBytesRead];
+#if !TARGET_OS_IPHONE
+          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);
+            });
+            if (!success) {
+              LOG_ERROR(@"Failed recording request data: %s (%i)", strerror(errno), errno);
+              close(_requestFD);
+              _requestFD = 0;
+            }
+          }
+#endif
           block(buffer);
           block(buffer);
         } else {
         } else {
           if (_bytesRead > 0) {
           if (_bytesRead > 0) {
@@ -100,8 +127,8 @@ static NSData* _lastChunkData = nil;
     
     
     if (buffer) {
     if (buffer) {
       NSMutableData* data = [[NSMutableData alloc] initWithCapacity:dispatch_data_get_size(buffer)];
       NSMutableData* data = [[NSMutableData alloc] initWithCapacity:dispatch_data_get_size(buffer)];
-      dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* bufferChunk, size_t size) {
-        [data appendBytes:bufferChunk length:size];
+      dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) {
+        [data appendBytes:chunkBytes length:chunkSize];
         return true;
         return true;
       });
       });
       block(data);
       block(data);
@@ -119,8 +146,8 @@ static NSData* _lastChunkData = nil;
     
     
     if (buffer) {
     if (buffer) {
       NSMutableData* data = [NSMutableData dataWithCapacity:kHeadersReadBuffer];
       NSMutableData* data = [NSMutableData dataWithCapacity:kHeadersReadBuffer];
-      dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* bufferChunk, size_t size) {
-        [data appendBytes:bufferChunk length:size];
+      dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) {
+        [data appendBytes:chunkBytes length:chunkSize];
         return true;
         return true;
       });
       });
       NSRange range = [data rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(0, data.length)];
       NSRange range = [data rangeOfData:_CRLFCRLFData options:0 range:NSMakeRange(0, data.length)];
@@ -158,7 +185,7 @@ static NSData* _lastChunkData = nil;
     
     
     if (buffer) {
     if (buffer) {
       if (dispatch_data_get_size(buffer) <= length) {
       if (dispatch_data_get_size(buffer) <= length) {
-        bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* chunkBytes, size_t chunkSize) {
+        bool success = dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) {
           NSData* data = [NSData dataWithBytesNoCopy:(void*)chunkBytes length:chunkSize freeWhenDone:NO];
           NSData* data = [NSData dataWithBytesNoCopy:(void*)chunkBytes length:chunkSize freeWhenDone:NO];
           NSError* error = nil;
           NSError* error = nil;
           if (![_request performWriteData:data error:&error]) {
           if (![_request performWriteData:data error:&error]) {
@@ -245,7 +272,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
   [self _readBufferWithLength:SIZE_T_MAX completionBlock:^(dispatch_data_t buffer) {
   [self _readBufferWithLength:SIZE_T_MAX completionBlock:^(dispatch_data_t buffer) {
     
     
     if (buffer) {
     if (buffer) {
-      dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t offset, const void* chunkBytes, size_t chunkSize) {
+      dispatch_data_apply(buffer, ^bool(dispatch_data_t region, size_t chunkOffset, const void* chunkBytes, size_t chunkSize) {
         [chunkData appendBytes:chunkBytes length:chunkSize];
         [chunkData appendBytes:chunkBytes length:chunkSize];
         return true;
         return true;
       });
       });
@@ -263,6 +290,9 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
 
 
 - (void)_writeBuffer:(dispatch_data_t)buffer withCompletionBlock:(WriteBufferCompletionBlock)block {
 - (void)_writeBuffer:(dispatch_data_t)buffer withCompletionBlock:(WriteBufferCompletionBlock)block {
   size_t size = dispatch_data_get_size(buffer);
   size_t size = dispatch_data_get_size(buffer);
+#if !TARGET_OS_IPHONE
+  ARC_DISPATCH_RETAIN(buffer);
+#endif
   dispatch_write(_socket, buffer, kGCDWebServerGCDQueue, ^(dispatch_data_t data, int error) {
   dispatch_write(_socket, buffer, kGCDWebServerGCDQueue, ^(dispatch_data_t data, int error) {
     
     
     @autoreleasepool {
     @autoreleasepool {
@@ -271,12 +301,27 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
         LOG_DEBUG(@"Connection sent %zu bytes on socket %i", size, _socket);
         LOG_DEBUG(@"Connection sent %zu bytes on socket %i", size, _socket);
         _bytesWritten += size;
         _bytesWritten += size;
         [self didUpdateBytesWritten];
         [self didUpdateBytesWritten];
+#if !TARGET_OS_IPHONE
+        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);
+          });
+          if (!success) {
+            LOG_ERROR(@"Failed recording response data: %s (%i)", strerror(errno), errno);
+            close(_responseFD);
+            _responseFD = 0;
+          }
+        }
+#endif
         block(YES);
         block(YES);
       } else {
       } else {
         LOG_ERROR(@"Error while writing to socket %i: %s (%i)", _socket, strerror(error), error);
         LOG_ERROR(@"Error while writing to socket %i: %s (%i)", _socket, strerror(error), error);
         block(NO);
         block(NO);
       }
       }
     }
     }
+#if !TARGET_OS_IPHONE
+    ARC_DISPATCH_RELEASE(buffer);
+#endif
     
     
   });
   });
 }
 }
@@ -285,7 +330,7 @@ static inline NSUInteger _ScanHexNumber(const void* bytes, NSUInteger size) {
 #if !__has_feature(objc_arc)
 #if !__has_feature(objc_arc)
   [data retain];
   [data retain];
 #endif
 #endif
-  dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, dispatch_get_main_queue(), ^{
+  dispatch_data_t buffer = dispatch_data_create(data.bytes, data.length, kGCDWebServerGCDQueue, ^{
 #if __has_feature(objc_arc)
 #if __has_feature(objc_arc)
     [data self];  // Keeps ARC from releasing data too early
     [data self];  // Keeps ARC from releasing data too early
 #else
 #else
@@ -655,6 +700,11 @@ static NSString* _StringFromAddressData(NSData* data) {
   }
   }
   ARC_RELEASE(_response);
   ARC_RELEASE(_response);
   
   
+#if !TARGET_OS_IPHONE
+  ARC_RELEASE(_requestPath);
+  ARC_RELEASE(_responsePath);
+#endif
+  
   ARC_DEALLOC(super);
   ARC_DEALLOC(super);
 }
 }
 
 
@@ -664,6 +714,21 @@ static NSString* _StringFromAddressData(NSData* data) {
 
 
 - (void)open {
 - (void)open {
   LOG_DEBUG(@"Did open connection on socket %i", _socket);
   LOG_DEBUG(@"Did open connection on socket %i", _socket);
+  
+#if !TARGET_OS_IPHONE
+  if (_server.recordingEnabled) {
+    _connectionIndex = OSAtomicIncrement32(&_connectionCounter);
+    
+    _requestPath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]);
+    _requestFD = open([_requestPath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY);
+    DCHECK(_requestFD > 0);
+    
+    _responsePath = ARC_RETAIN([NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]);
+    _responseFD = open([_responsePath fileSystemRepresentation], O_CREAT | O_TRUNC | O_WRONLY);
+    DCHECK(_responseFD > 0);
+  }
+#endif
+  
   [self _readRequestHeaders];
   [self _readRequestHeaders];
 }
 }
 
 
@@ -732,6 +797,38 @@ static inline BOOL _CompareResources(NSString* responseETag, NSString* requestET
   } else {
   } else {
     LOG_DEBUG(@"Did close connection on socket %i", _socket);
     LOG_DEBUG(@"Did close connection on socket %i", _socket);
   }
   }
+  
+#if !TARGET_OS_IPHONE
+  if (_requestPath) {
+    BOOL success = NO;
+    NSError* error = nil;
+    if (_requestFD > 0) {
+      close(_requestFD);
+      NSString* name = [NSString stringWithFormat:@"%03lu-%@.request", (unsigned long)_connectionIndex, _virtualHEAD ? @"HEAD" : _request.method];
+      success = [[NSFileManager defaultManager] moveItemAtPath:_requestPath toPath:[[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:name] error:&error];
+    }
+    if (!success) {
+      LOG_ERROR(@"Failed saving recorded request: %@", error);
+      DNOT_REACHED();
+    }
+    unlink([_requestPath fileSystemRepresentation]);
+  }
+  
+  if (_responsePath) {
+    BOOL success = NO;
+    NSError* error = nil;
+    if (_responseFD > 0) {
+      close(_responseFD);
+      NSString* name = [NSString stringWithFormat:@"%03lu-%i.response", (unsigned long)_connectionIndex, (int)_statusCode];
+      success = [[NSFileManager defaultManager] moveItemAtPath:_responsePath toPath:[[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:name] error:&error];
+    }
+    if (!success) {
+      LOG_ERROR(@"Failed saving recorded response: %@", error);
+      DNOT_REACHED();
+    }
+    unlink([_responsePath fileSystemRepresentation]);
+  }
+#endif
   if (_request) {
   if (_request) {
     LOG_VERBOSE(@"[%@] %@ %i \"%@ %@\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead, (unsigned long)_bytesWritten);
     LOG_VERBOSE(@"[%@] %@ %i \"%@ %@\" (%lu | %lu)", self.localAddressString, self.remoteAddressString, (int)_statusCode, _virtualHEAD ? @"HEAD" : _request.method, _request.path, (unsigned long)_bytesRead, (unsigned long)_bytesWritten);
   } else {
   } else {

+ 3 - 0
CGDWebServer/GCDWebServerPrivate.h

@@ -36,8 +36,10 @@
 #define ARC_AUTORELEASE(__OBJECT__) __OBJECT__
 #define ARC_AUTORELEASE(__OBJECT__) __OBJECT__
 #define ARC_DEALLOC(__OBJECT__)
 #define ARC_DEALLOC(__OBJECT__)
 #if (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_6_0)) || (!TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_8))
 #if (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_6_0)) || (!TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_8))
+#define ARC_DISPATCH_RETAIN(__OBJECT__)
 #define ARC_DISPATCH_RELEASE(__OBJECT__)
 #define ARC_DISPATCH_RELEASE(__OBJECT__)
 #else
 #else
+#define ARC_DISPATCH_RETAIN(__OBJECT__) dispatch_retain(__OBJECT__)
 #define ARC_DISPATCH_RELEASE(__OBJECT__) dispatch_release(__OBJECT__)
 #define ARC_DISPATCH_RELEASE(__OBJECT__) dispatch_release(__OBJECT__)
 #endif
 #endif
 #else
 #else
@@ -47,6 +49,7 @@
 #define ARC_RELEASE(__OBJECT__) [__OBJECT__ release]
 #define ARC_RELEASE(__OBJECT__) [__OBJECT__ release]
 #define ARC_AUTORELEASE(__OBJECT__) [__OBJECT__ autorelease]
 #define ARC_AUTORELEASE(__OBJECT__) [__OBJECT__ autorelease]
 #define ARC_DEALLOC(__OBJECT__) [__OBJECT__ dealloc]
 #define ARC_DEALLOC(__OBJECT__) [__OBJECT__ dealloc]
+#define ARC_DISPATCH_RETAIN(__OBJECT__) dispatch_retain(__OBJECT__)
 #define ARC_DISPATCH_RELEASE(__OBJECT__) dispatch_release(__OBJECT__)
 #define ARC_DISPATCH_RELEASE(__OBJECT__) dispatch_release(__OBJECT__)
 #endif
 #endif
 
 

+ 92 - 14
Mac/main.m

@@ -25,6 +25,8 @@
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
  */
 
 
+#import <libgen.h>
+
 #import "GCDWebServer.h"
 #import "GCDWebServer.h"
 
 
 #import "GCDWebServerDataRequest.h"
 #import "GCDWebServerDataRequest.h"
@@ -37,22 +39,71 @@
 
 
 #import "GCDWebUploader.h"
 #import "GCDWebUploader.h"
 
 
+typedef enum {
+  kMode_WebServer = 0,
+  kMode_HTMLPage,
+  kMode_HTMLForm,
+  kMode_WebDAV,
+  kMode_WebUploader,
+  kMode_StreamingResponse
+} Mode;
+
 int main(int argc, const char* argv[]) {
 int main(int argc, const char* argv[]) {
-  BOOL success = NO;
-  int mode = (argc == 2 ? MIN(MAX(atoi(argv[1]), 0), 5) : 0);
+  int result = -1;
   @autoreleasepool {
   @autoreleasepool {
+    Mode mode = kMode_WebServer;
+    BOOL recording = NO;
+    NSString* rootDirectory = NSHomeDirectory();
+    NSString* testDirectory = nil;
+    
+    if (argc == 1) {
+      fprintf(stdout, "Usage: %s [-mode webServer | htmlPage | htmlForm | webDAV | webUploader | streamingResponse] [-record] [-root directory] [-tests directory]\n\n", basename((char*)argv[0]));
+    } else {
+      for (int i = 1; i < argc; ++i) {
+        if (argv[i][0] != '-') {
+          continue;
+        }
+        if (!strcmp(argv[i], "-mode") && (i + 1 < argc)) {
+          ++i;
+          if (!strcmp(argv[i], "webServer")) {
+            mode = kMode_WebServer;
+          } else if (!strcmp(argv[i], "htmlPage")) {
+            mode = kMode_HTMLPage;
+          } else if (!strcmp(argv[i], "htmlForm")) {
+            mode = kMode_HTMLForm;
+          } else if (!strcmp(argv[i], "webDAV")) {
+            mode = kMode_WebDAV;
+          } else if (!strcmp(argv[i], "webUploader")) {
+            mode = kMode_WebUploader;
+          } else if (!strcmp(argv[i], "streamingResponse")) {
+            mode = kMode_StreamingResponse;
+          }
+        } else if (!strcmp(argv[i], "-record")) {
+          recording = YES;
+        } else if (!strcmp(argv[i], "-root") && (i + 1 < argc)) {
+          ++i;
+          rootDirectory = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[i] length:strlen(argv[i])] stringByStandardizingPath];
+        } else if (!strcmp(argv[i], "-tests") && (i + 1 < argc)) {
+          ++i;
+          testDirectory = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:argv[i] length:strlen(argv[i])] stringByStandardizingPath];
+        }
+      }
+    }
+    
     GCDWebServer* webServer = nil;
     GCDWebServer* webServer = nil;
     switch (mode) {
     switch (mode) {
       
       
       // Simply serve contents of home directory
       // Simply serve contents of home directory
-      case 0: {
+      case kMode_WebServer: {
+        fprintf(stdout, "Running in Web Server mode from \"%s\"", [rootDirectory UTF8String]);
         webServer = [[GCDWebServer alloc] init];
         webServer = [[GCDWebServer alloc] init];
-        [webServer addGETHandlerForBasePath:@"/" directoryPath:NSHomeDirectory() indexFilename:nil cacheAge:0 allowRangeRequests:YES];
+        [webServer addGETHandlerForBasePath:@"/" directoryPath:rootDirectory indexFilename:nil cacheAge:0 allowRangeRequests:YES];
         break;
         break;
       }
       }
       
       
       // Renders a HTML page
       // Renders a HTML page
-      case 1: {
+      case kMode_HTMLPage: {
+        fprintf(stdout, "Running in HTML Page mode");
         webServer = [[GCDWebServer alloc] init];
         webServer = [[GCDWebServer alloc] init];
         [webServer addDefaultHandlerForMethod:@"GET"
         [webServer addDefaultHandlerForMethod:@"GET"
                                  requestClass:[GCDWebServerRequest class]
                                  requestClass:[GCDWebServerRequest class]
@@ -65,7 +116,8 @@ int main(int argc, const char* argv[]) {
       }
       }
       
       
       // Implements an HTML form
       // Implements an HTML form
-      case 2: {
+      case kMode_HTMLForm: {
+        fprintf(stdout, "Running in HTML Form mode");
         webServer = [[GCDWebServer alloc] init];
         webServer = [[GCDWebServer alloc] init];
         [webServer addHandlerForMethod:@"GET"
         [webServer addHandlerForMethod:@"GET"
                                   path:@"/"
                                   path:@"/"
@@ -96,17 +148,23 @@ int main(int argc, const char* argv[]) {
         break;
         break;
       }
       }
       
       
-      case 3: {
-        webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath]];
+      // Serve home directory through WebDAV
+      case kMode_WebDAV: {
+        fprintf(stdout, "Running in WebDAV mode from \"%s\"", [rootDirectory UTF8String]);
+        webServer = [[GCDWebDAVServer alloc] initWithUploadDirectory:rootDirectory];
         break;
         break;
       }
       }
       
       
-      case 4: {
-        webServer = [[GCDWebUploader alloc] initWithUploadDirectory:[[NSFileManager defaultManager] currentDirectoryPath]];
+      // Serve home directory through web uploader
+      case kMode_WebUploader: {
+        fprintf(stdout, "Running in Web Uploader mode from \"%s\"", [rootDirectory UTF8String]);
+        webServer = [[GCDWebUploader alloc] initWithUploadDirectory:rootDirectory];
         break;
         break;
       }
       }
       
       
-      case 5: {
+      // Test streaming responses
+      case kMode_StreamingResponse: {
+        fprintf(stdout, "Running in Streaming Response mode");
         webServer = [[GCDWebServer alloc] init];
         webServer = [[GCDWebServer alloc] init];
         [webServer addHandlerForMethod:@"GET"
         [webServer addHandlerForMethod:@"GET"
                                   path:@"/"
                                   path:@"/"
@@ -130,10 +188,30 @@ int main(int argc, const char* argv[]) {
       }
       }
       
       
     }
     }
-    success = [webServer runWithPort:8080];
+#if __has_feature(objc_arc)
+    fprintf(stdout, " (ARC is ON)\n");
+#else
+    fprintf(stdout, " (ARC is OFF)\n");
+#endif
+    
+    if (webServer) {
+      if (testDirectory) {
+        fprintf(stdout, "<RUNNING TESTS FROM \"%s\">\n\n", [testDirectory UTF8String]);
+        result = (int)[webServer runTestsInDirectory:testDirectory withPort:8080];
+      } else {
+        if (recording) {
+          fprintf(stdout, "<RECORDING ENABLED>\n");
+          webServer.recordingEnabled = YES;
+        }
+        fprintf(stdout, "\n");
+        if ([webServer runWithPort:8080]) {
+          result = 0;
+        }
+      }
 #if !__has_feature(objc_arc)
 #if !__has_feature(objc_arc)
-    [webServer release];
+      [webServer release];
 #endif
 #endif
+    }
   }
   }
-  return success ? 0 : -1;
+  return result;
 }
 }

+ 32 - 0
Run-Tests.sh

@@ -0,0 +1,32 @@
+#!/bin/sh -ex
+
+TARGET="GCDWebServer (Mac)"
+CONFIGURATION="Release"
+PAYLOAD_ZIP="Tests/Payload.zip"
+PAYLOAD_DIR="/tmp/payload"
+
+MRC_BUILD_DIR="/tmp/GCDWebServer-MRC"
+MRC_PRODUCT="$MRC_BUILD_DIR/$CONFIGURATION/GCDWebServer"
+ARC_BUILD_DIR="/tmp/GCDWebServer-ARC"
+ARC_PRODUCT="$ARC_BUILD_DIR/$CONFIGURATION/GCDWebServer"
+
+function runTests {
+  rm -rf "$PAYLOAD_DIR"
+  ditto -x -k "$PAYLOAD_ZIP" "$PAYLOAD_DIR"
+  logLevel=2 $1 -root "$PAYLOAD_DIR" -tests "$2"
+}
+
+# Build in manual memory management mode
+rm -rf "MRC_BUILD_DIR"
+xcodebuild -target "$TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$MRC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=NO" > /dev/null
+
+# Build in ARC mode
+rm -rf "ARC_BUILD_DIR"
+xcodebuild -target "$TARGET" -configuration "$CONFIGURATION" build "SYMROOT=$ARC_BUILD_DIR" "CLANG_ENABLE_OBJC_ARC=YES" > /dev/null
+
+# Run tests
+runTests $MRC_PRODUCT "WebServer"
+runTests $ARC_PRODUCT "WebServer"
+
+# Done
+echo "\nAll tests completed successfully!"

二进制
Tests/Payload.zip


+ 16 - 0
Tests/WebServer/001-200.response

@@ -0,0 +1,16 @@
+HTTP/1.1 200 OK
+Cache-Control: no-cache
+Content-Length: 221
+Content-Type: text/html; charset=utf-8
+Connection: Close
+Server: GCDWebServer
+Date: Fri, 11 Apr 2014 02:42:22 GMT
+
+<!DOCTYPE html>
+<html><head><meta charset="utf-8"></head><body>
+<ul>
+<li><a href="Copy.txt">Copy.txt</a></li>
+<li><a href="images/">images/</a></li>
+<li><a href="PDF%20Reports/">PDF Reports/</a></li>
+</ul>
+</body></html>

+ 9 - 0
Tests/WebServer/001-GET.request

@@ -0,0 +1,9 @@
+GET / HTTP/1.1
+Host: localhost:8080
+Connection: keep-alive
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
+DNT: 1
+Accept-Encoding: gzip,deflate,sdch
+Accept-Language: en-US,en;q=0.8,fr;q=0.6
+

+ 14 - 0
Tests/WebServer/002-200.response

@@ -0,0 +1,14 @@
+HTTP/1.1 200 OK
+Connection: Close
+Server: GCDWebServer
+Content-Type: text/plain
+Last-Modified: Thu, 10 Apr 2014 11:10:14 GMT
+Date: Fri, 11 Apr 2014 02:42:24 GMT
+Accept-Ranges: bytes
+Content-Length: 271
+Cache-Control: no-cache
+Etag: 73212403/1397128214/0
+
+For the colorful.
+
+Color is more than just a hue. It expresses a feeling. Makes a statement. Declares an allegiance. Color reveals your personality. iPhone 5c, in five anything-but-shy colors, does just that. It’s not just for lovers of color. It’s for the colorful.

+ 10 - 0
Tests/WebServer/002-GET.request

@@ -0,0 +1,10 @@
+GET /Copy.txt HTTP/1.1
+Host: localhost:8080
+Connection: keep-alive
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
+DNT: 1
+Referer: http://localhost:8080/
+Accept-Encoding: gzip,deflate,sdch
+Accept-Language: en-US,en;q=0.8,fr;q=0.6
+

+ 15 - 0
Tests/WebServer/003-200.response

@@ -0,0 +1,15 @@
+HTTP/1.1 200 OK
+Cache-Control: no-cache
+Content-Length: 218
+Content-Type: text/html; charset=utf-8
+Connection: Close
+Server: GCDWebServer
+Date: Fri, 11 Apr 2014 02:42:27 GMT
+
+<!DOCTYPE html>
+<html><head><meta charset="utf-8"></head><body>
+<ul>
+<li><a href="capable_green_ipad_l.png">capable_green_ipad_l.png</a></li>
+<li><a href="hero_mba_11.jpg">hero_mba_11.jpg</a></li>
+</ul>
+</body></html>

+ 10 - 0
Tests/WebServer/003-GET.request

@@ -0,0 +1,10 @@
+GET /images/ HTTP/1.1
+Host: localhost:8080
+Connection: keep-alive
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
+DNT: 1
+Referer: http://localhost:8080/
+Accept-Encoding: gzip,deflate,sdch
+Accept-Language: en-US,en;q=0.8,fr;q=0.6
+

二进制
Tests/WebServer/004-200.response


+ 10 - 0
Tests/WebServer/004-GET.request

@@ -0,0 +1,10 @@
+GET /images/capable_green_ipad_l.png HTTP/1.1
+Host: localhost:8080
+Connection: keep-alive
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
+DNT: 1
+Referer: http://localhost:8080/images/
+Accept-Encoding: gzip,deflate,sdch
+Accept-Language: en-US,en;q=0.8,fr;q=0.6
+

二进制
Tests/WebServer/005-200.response


+ 10 - 0
Tests/WebServer/005-GET.request

@@ -0,0 +1,10 @@
+GET /images/hero_mba_11.jpg HTTP/1.1
+Host: localhost:8080
+Connection: keep-alive
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
+DNT: 1
+Referer: http://localhost:8080/images/
+Accept-Encoding: gzip,deflate,sdch
+Accept-Language: en-US,en;q=0.8,fr;q=0.6
+

+ 7 - 0
Tests/WebServer/006-304.response

@@ -0,0 +1,7 @@
+HTTP/1.1 304 Not Modified
+Last-Modified: Thu, 10 Apr 2014 21:46:56 GMT
+Etag: 73209474/1397166416/0
+Connection: Close
+Server: GCDWebServer
+Date: Fri, 11 Apr 2014 02:42:34 GMT
+

+ 12 - 0
Tests/WebServer/006-GET.request

@@ -0,0 +1,12 @@
+GET /images/capable_green_ipad_l.png HTTP/1.1
+Host: localhost:8080
+Connection: keep-alive
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
+DNT: 1
+Referer: http://localhost:8080/images/
+Accept-Encoding: gzip,deflate,sdch
+Accept-Language: en-US,en;q=0.8,fr;q=0.6
+If-None-Match: 73209474/1397166416/0
+If-Modified-Since: Thu, 10 Apr 2014 21:46:56 GMT
+

+ 7 - 0
Tests/WebServer/007-304.response

@@ -0,0 +1,7 @@
+HTTP/1.1 304 Not Modified
+Last-Modified: Thu, 10 Apr 2014 21:51:14 GMT
+Etag: 73212154/1397166674/0
+Connection: Close
+Server: GCDWebServer
+Date: Fri, 11 Apr 2014 02:42:37 GMT
+

+ 12 - 0
Tests/WebServer/007-GET.request

@@ -0,0 +1,12 @@
+GET /images/hero_mba_11.jpg HTTP/1.1
+Host: localhost:8080
+Connection: keep-alive
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
+DNT: 1
+Referer: http://localhost:8080/images/
+Accept-Encoding: gzip,deflate,sdch
+Accept-Language: en-US,en;q=0.8,fr;q=0.6
+If-None-Match: 73212154/1397166674/0
+If-Modified-Since: Thu, 10 Apr 2014 21:51:14 GMT
+

+ 14 - 0
Tests/WebServer/008-200.response

@@ -0,0 +1,14 @@
+HTTP/1.1 200 OK
+Cache-Control: no-cache
+Content-Length: 199
+Content-Type: text/html; charset=utf-8
+Connection: Close
+Server: GCDWebServer
+Date: Fri, 11 Apr 2014 02:42:40 GMT
+
+<!DOCTYPE html>
+<html><head><meta charset="utf-8"></head><body>
+<ul>
+<li><a href="Apple%20Economic%20Impact%20on%20Cupertino.pdf">Apple Economic Impact on Cupertino.pdf</a></li>
+</ul>
+</body></html>

+ 10 - 0
Tests/WebServer/008-GET.request

@@ -0,0 +1,10 @@
+GET /PDF%20Reports/ HTTP/1.1
+Host: localhost:8080
+Connection: keep-alive
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
+DNT: 1
+Referer: http://localhost:8080/
+Accept-Encoding: gzip,deflate,sdch
+Accept-Language: en-US,en;q=0.8,fr;q=0.6
+

二进制
Tests/WebServer/009-200.response


+ 10 - 0
Tests/WebServer/009-GET.request

@@ -0,0 +1,10 @@
+GET /PDF%20Reports/Apple%20Economic%20Impact%20on%20Cupertino.pdf HTTP/1.1
+Host: localhost:8080
+Connection: keep-alive
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
+DNT: 1
+Referer: http://localhost:8080/PDF%20Reports/
+Accept-Encoding: gzip,deflate,sdch
+Accept-Language: en-US,en;q=0.8,fr;q=0.6
+

+ 7 - 0
Tests/WebServer/010-304.response

@@ -0,0 +1,7 @@
+HTTP/1.1 304 Not Modified
+Last-Modified: Wed, 01 May 2013 12:01:13 GMT
+Etag: 73212107/1367409673/0
+Connection: Close
+Server: GCDWebServer
+Date: Fri, 11 Apr 2014 02:42:42 GMT
+

+ 13 - 0
Tests/WebServer/010-GET.request

@@ -0,0 +1,13 @@
+GET /PDF%20Reports/Apple%20Economic%20Impact%20on%20Cupertino.pdf HTTP/1.1
+Host: localhost:8080
+Connection: keep-alive
+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
+Accept: */*
+DNT: 1
+Referer: http://localhost:8080/PDF%20Reports/Apple%20Economic%20Impact%20on%20Cupertino.pdf
+Accept-Encoding: gzip,deflate,sdch
+Accept-Language: en-US,en;q=0.8,fr;q=0.6
+Range: bytes=0-32767
+If-None-Match: 73212107/1367409673/0
+If-Modified-Since: Wed, 01 May 2013 12:01:13 GMT
+

+ 7 - 0
Tests/WebServer/011-304.response

@@ -0,0 +1,7 @@
+HTTP/1.1 304 Not Modified
+Last-Modified: Wed, 01 May 2013 12:01:13 GMT
+Etag: 73212107/1367409673/0
+Connection: Close
+Server: GCDWebServer
+Date: Fri, 11 Apr 2014 02:42:42 GMT
+

+ 13 - 0
Tests/WebServer/011-GET.request

@@ -0,0 +1,13 @@
+GET /PDF%20Reports/Apple%20Economic%20Impact%20on%20Cupertino.pdf HTTP/1.1
+Host: localhost:8080
+Connection: keep-alive
+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
+Accept: */*
+DNT: 1
+Referer: http://localhost:8080/PDF%20Reports/Apple%20Economic%20Impact%20on%20Cupertino.pdf
+Accept-Encoding: gzip,deflate,sdch
+Accept-Language: en-US,en;q=0.8,fr;q=0.6
+Range: bytes=32768-181951
+If-None-Match: 73212107/1367409673/0
+If-Modified-Since: Wed, 01 May 2013 12:01:13 GMT
+

+ 8 - 0
Tests/WebServer/012-200.response

@@ -0,0 +1,8 @@
+HTTP/1.1 200 OK
+Cache-Control: no-cache
+Content-Length: 221
+Content-Type: text/html; charset=utf-8
+Connection: Close
+Server: GCDWebServer
+Date: Fri, 11 Apr 2014 02:43:00 GMT
+

+ 5 - 0
Tests/WebServer/012-HEAD.request

@@ -0,0 +1,5 @@
+HEAD / HTTP/1.1
+User-Agent: curl/7.30.0
+Host: localhost:8080
+Accept: */*
+