feat: sync httpdns sdk/platform updates without large binaries

This commit is contained in:
robin
2026-03-04 17:59:14 +08:00
parent 853897a6f8
commit 532891fad0
700 changed files with 6096 additions and 2712 deletions

View File

@@ -0,0 +1,24 @@
//
// HttpdnsNWHTTPClientTestBase.h
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
// 测试基类 - 为所<E4B8BA><E68980>?HttpdnsNWHTTPClient 测试提供共享<E585B1><E4BAAB>?setup/teardown
//
#import <XCTest/XCTest.h>
#import "HttpdnsNWHTTPClient.h"
#import "HttpdnsNWHTTPClient_Internal.h"
#import "HttpdnsNWReusableConnection.h"
NS_ASSUME_NONNULL_BEGIN
@interface HttpdnsNWHTTPClientTestBase : XCTestCase
@property (nonatomic, strong) HttpdnsNWHTTPClient *client;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,42 @@
//
// HttpdnsNWHTTPClientTestBase.m
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
// -
//
// mock server
// cd TrustHttpDNSTests/Network && python3 mock_server.py
// <EFBFBD><EFBFBD>?
// - HTTP: 11080
// - HTTPS: 11443, 11444, 11445, 11446
//
#import "HttpdnsNWHTTPClientTestBase.h"
@implementation HttpdnsNWHTTPClientTestBase
- (void)setUp {
[super setUp];
// <EFBFBD><EFBFBD>?TLS <EFBFBD><EFBFBD>?mock server <EFBFBD><EFBFBD>?
// <EFBFBD><EFBFBD>?
// 1.
// 2. <EFBFBD><EFBFBD>?loopback (127.0.0.1)
// 3. <EFBFBD><EFBFBD>?
setenv("HTTPDNS_SKIP_TLS_VERIFY", "1", 1);
self.client = [[HttpdnsNWHTTPClient alloc] init];
}
- (void)tearDown {
// <EFBFBD><EFBFBD>?
unsetenv("HTTPDNS_SKIP_TLS_VERIFY");
self.client = nil;
[super tearDown];
}
@end

View File

@@ -0,0 +1,58 @@
//
// HttpdnsNWHTTPClientTestHelper.h
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface HttpdnsNWHTTPClientTestHelper : NSObject
#pragma mark - HTTP 响应数据构<E68DAE><E69E84>?
// 构造标<E980A0><E6A087>?HTTP 响应数据
+ (NSData *)createHTTPResponseWithStatus:(NSInteger)statusCode
statusText:(NSString *)statusText
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
body:(nullable NSData *)body;
// 构<><E69E84>?chunked 编码<E7BC96><E7A081>?HTTP 响应
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
chunks:(NSArray<NSData *> *)chunks;
// 构<><E69E84>?chunked 编码<E7BC96><E7A081>?HTTP 响应(带 trailers<72><73>?
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
chunks:(NSArray<NSData *> *)chunks
trailers:(nullable NSDictionary<NSString *, NSString *> *)trailers;
#pragma mark - Chunked 编码工具
// 编码单个 chunk
+ (NSData *)encodeChunk:(NSData *)data;
// 编码单个 chunk带 extension<6F><6E>?
+ (NSData *)encodeChunk:(NSData *)data extension:(nullable NSString *)extension;
// 编码终止 chunksize=0<><30>?
+ (NSData *)encodeLastChunk;
// 编码终止 chunk带 trailers<72><73>?
+ (NSData *)encodeLastChunkWithTrailers:(NSDictionary<NSString *, NSString *> *)trailers;
#pragma mark - 测试数据生成
// 生成指定大小的随机数<E69CBA><E695B0>?
+ (NSData *)randomDataWithSize:(NSUInteger)size;
// 生成 JSON 格式的响应体
+ (NSData *)jsonBodyWithDictionary:(NSDictionary *)dictionary;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,168 @@
//
// HttpdnsNWHTTPClientTestHelper.m
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
#import "HttpdnsNWHTTPClientTestHelper.h"
@implementation HttpdnsNWHTTPClientTestHelper
#pragma mark - HTTP <EFBFBD><EFBFBD>?
+ (NSData *)createHTTPResponseWithStatus:(NSInteger)statusCode
statusText:(NSString *)statusText
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
body:(nullable NSData *)body {
NSMutableString *response = [NSMutableString string];
//
[response appendFormat:@"HTTP/1.1 %ld %@\r\n", (long)statusCode, statusText ?: @"OK"];
//
if (headers) {
for (NSString *key in headers) {
[response appendFormat:@"%@: %@\r\n", key, headers[key]];
}
}
// <EFBFBD><EFBFBD>?body <EFBFBD><EFBFBD>?Content-Length<EFBFBD><EFBFBD>?
if (body && body.length > 0 && !headers[@"Content-Length"]) {
[response appendFormat:@"Content-Length: %lu\r\n", (unsigned long)body.length];
}
// <EFBFBD><EFBFBD>?body
[response appendString:@"\r\n"];
NSMutableData *responseData = [[response dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// body
if (body) {
[responseData appendData:body];
}
return [responseData copy];
}
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
chunks:(NSArray<NSData *> *)chunks {
return [self createChunkedHTTPResponseWithStatus:statusCode
headers:headers
chunks:chunks
trailers:nil];
}
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
chunks:(NSArray<NSData *> *)chunks
trailers:(nullable NSDictionary<NSString *, NSString *> *)trailers {
NSMutableString *response = [NSMutableString string];
//
[response appendFormat:@"HTTP/1.1 %ld OK\r\n", (long)statusCode];
//
if (headers) {
for (NSString *key in headers) {
[response appendFormat:@"%@: %@\r\n", key, headers[key]];
}
}
// Transfer-Encoding
[response appendString:@"Transfer-Encoding: chunked\r\n"];
//
[response appendString:@"\r\n"];
NSMutableData *responseData = [[response dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// chunk
for (NSData *chunk in chunks) {
[responseData appendData:[self encodeChunk:chunk]];
}
// chunk
if (trailers) {
[responseData appendData:[self encodeLastChunkWithTrailers:trailers]];
} else {
[responseData appendData:[self encodeLastChunk]];
}
return [responseData copy];
}
#pragma mark - Chunked
+ (NSData *)encodeChunk:(NSData *)data {
return [self encodeChunk:data extension:nil];
}
+ (NSData *)encodeChunk:(NSData *)data extension:(nullable NSString *)extension {
NSMutableString *chunkString = [NSMutableString string];
// Chunk size
if (extension) {
[chunkString appendFormat:@"%lx;%@\r\n", (unsigned long)data.length, extension];
} else {
[chunkString appendFormat:@"%lx\r\n", (unsigned long)data.length];
}
NSMutableData *chunkData = [[chunkString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// Chunk data
[chunkData appendData:data];
// CRLF
[chunkData appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
return [chunkData copy];
}
+ (NSData *)encodeLastChunk {
return [@"0\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
}
+ (NSData *)encodeLastChunkWithTrailers:(NSDictionary<NSString *, NSString *> *)trailers {
NSMutableString *lastChunkString = [NSMutableString stringWithString:@"0\r\n"];
// trailer
for (NSString *key in trailers) {
[lastChunkString appendFormat:@"%@: %@\r\n", key, trailers[key]];
}
//
[lastChunkString appendString:@"\r\n"];
return [lastChunkString dataUsingEncoding:NSUTF8StringEncoding];
}
#pragma mark -
+ (NSData *)randomDataWithSize:(NSUInteger)size {
NSMutableData *data = [NSMutableData dataWithLength:size];
if (SecRandomCopyBytes(kSecRandomDefault, size, data.mutableBytes) != 0) {
// SecRandom 使<EFBFBD><EFBFBD>?
uint8_t *bytes = data.mutableBytes;
for (NSUInteger i = 0; i < size; i++) {
bytes[i] = arc4random_uniform(256);
}
}
return [data copy];
}
+ (NSData *)jsonBodyWithDictionary:(NSDictionary *)dictionary {
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
options:0
error:&error];
if (error) {
NSLog(@"JSON serialization error: %@", error);
return nil;
}
return jsonData;
}
@end

View File

@@ -0,0 +1,742 @@
//
// HttpdnsNWHTTPClientTests.m
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import "HttpdnsNWHTTPClient.h"
#import "HttpdnsNWHTTPClient_Internal.h"
#import "HttpdnsNWReusableConnection.h"
#import "HttpdnsNWHTTPClientTestHelper.h"
@interface HttpdnsNWHTTPClientTests : XCTestCase
@property (nonatomic, strong) HttpdnsNWHTTPClient *client;
@end
@implementation HttpdnsNWHTTPClientTests
- (void)setUp {
[super setUp];
self.client = [[HttpdnsNWHTTPClient alloc] init];
}
- (void)tearDown {
self.client = nil;
[super tearDown];
}
#pragma mark - A. HTTP
#pragma mark - A1. Header (9<EFBFBD><EFBFBD>?
// A1.1
- (void)testParseHTTPHeaders_ValidResponse_Success {
NSData *data = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:@{@"Content-Type": @"application/json"}
body:nil];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
XCTAssertEqual(statusCode, 200);
XCTAssertNotNil(headers);
XCTAssertEqualObjects(headers[@"content-type"], @"application/json"); // key
XCTAssertNil(error);
}
// A1.2
- (void)testParseHTTPHeaders_MultipleHeaders_AllParsed {
NSDictionary *testHeaders = @{
@"Content-Type": @"application/json",
@"Content-Length": @"123",
@"Connection": @"keep-alive",
@"X-Custom-Header": @"custom-value",
@"Cache-Control": @"no-cache"
};
NSData *data = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:testHeaders
body:nil];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
XCTAssertEqual(headers.count, testHeaders.count);
// <EFBFBD><EFBFBD>?key
XCTAssertEqualObjects(headers[@"content-type"], @"application/json");
XCTAssertEqualObjects(headers[@"content-length"], @"123");
XCTAssertEqualObjects(headers[@"connection"], @"keep-alive");
XCTAssertEqualObjects(headers[@"x-custom-header"], @"custom-value");
XCTAssertEqualObjects(headers[@"cache-control"], @"no-cache");
}
// A1.3 <EFBFBD><EFBFBD>?
- (void)testParseHTTPHeaders_IncompleteData_ReturnsIncomplete {
NSString *incompleteResponse = @"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n";
NSData *data = [incompleteResponse dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultIncomplete);
}
// A1.4
- (void)testParseHTTPHeaders_InvalidStatusLine_ReturnsError {
NSString *invalidResponse = @"INVALID\r\n\r\n";
NSData *data = [invalidResponse dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultError);
XCTAssertNotNil(error);
}
// A1.5
- (void)testParseHTTPHeaders_HeadersWithWhitespace_Trimmed {
NSString *responseWithSpaces = @"HTTP/1.1 200 OK\r\nContent-Type: application/json \r\n\r\n";
NSData *data = [responseWithSpaces dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
XCTAssertEqualObjects(headers[@"content-type"], @"application/json"); // <EFBFBD><EFBFBD>?trim
}
// A1.6 <EFBFBD><EFBFBD>?
- (void)testParseHTTPHeaders_EmptyHeaderValue_HandledGracefully {
NSString *responseWithEmptyValue = @"HTTP/1.1 200 OK\r\nX-Empty-Header:\r\n\r\n";
NSData *data = [responseWithEmptyValue dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
XCTAssertEqualObjects(headers[@"x-empty-header"], @"");
}
// A1.7 <EFBFBD><EFBFBD>?
- (void)testParseHTTPHeaders_NonNumericStatusCode_ReturnsError {
NSString *invalidStatusCode = @"HTTP/1.1 ABC OK\r\n\r\n";
NSData *data = [invalidStatusCode dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultError);
}
// A1.8
- (void)testParseHTTPHeaders_StatusCodeZero_ReturnsError {
NSString *zeroStatusCode = @"HTTP/1.1 0 OK\r\n\r\n";
NSData *data = [zeroStatusCode dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultError);
}
// A1.9 <EFBFBD><EFBFBD>?
- (void)testParseHTTPHeaders_HeaderWithoutColon_Skipped {
NSString *responseWithInvalidHeader = @"HTTP/1.1 200 OK\r\nInvalidHeader\r\nContent-Type: application/json\r\n\r\n";
NSData *data = [responseWithInvalidHeader dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex;
NSInteger statusCode;
NSDictionary *headers;
NSError *error;
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
headerEndIndex:&headerEndIndex
statusCode:&statusCode
headers:&headers
error:&error];
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
XCTAssertEqualObjects(headers[@"content-type"], @"application/json"); //
}
#pragma mark - A2. Chunked <EFBFBD><EFBFBD>?(8<EFBFBD><EFBFBD>?
// A2.1 chunk
- (void)testCheckChunkedBody_SingleChunk_DetectsComplete {
NSString *singleChunkBody = @"5\r\nhello\r\n0\r\n\r\n";
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", singleChunkBody];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
XCTAssertNil(error);
}
// A2.2 chunks
- (void)testCheckChunkedBody_MultipleChunks_DetectsComplete {
NSString *multiChunkBody = @"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", multiChunkBody];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
XCTAssertNil(error);
}
// A2.3 <EFBFBD><EFBFBD>?chunk
- (void)testCheckChunkedBody_IncompleteChunk_ReturnsIncomplete {
NSString *incompleteChunkBody = @"5\r\nhel"; // <EFBFBD><EFBFBD>?
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", incompleteChunkBody];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultIncomplete);
}
// A2.4 <EFBFBD><EFBFBD>?chunk extension
- (void)testCheckChunkedBody_WithChunkExtension_Ignored {
NSString *chunkWithExtension = @"5;name=value\r\nhello\r\n0\r\n\r\n";
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", chunkWithExtension];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
// headerEndIndex <EFBFBD><EFBFBD>?\r\n\r\n <EFBFBD><EFBFBD>?\r
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
XCTAssertNil(error);
}
// A2.5 size
- (void)testCheckChunkedBody_InvalidHexSize_ReturnsError {
NSString *invalidChunkSize = @"ZZZ\r\nhello\r\n0\r\n\r\n";
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", invalidChunkSize];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultError);
XCTAssertNotNil(error);
}
// A2.6 Chunk size
- (void)testCheckChunkedBody_ChunkSizeOverflow_ReturnsError {
NSString *overflowChunkSize = @"FFFFFFFFFFFFFFFF\r\n";
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", overflowChunkSize];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultError);
XCTAssertNotNil(error);
}
// A2.7 CRLF <EFBFBD><EFBFBD>?
- (void)testCheckChunkedBody_MissingCRLFTerminator_ReturnsError {
NSString *missingTerminator = @"5\r\nhelloXX0\r\n\r\n"; // <EFBFBD><EFBFBD>?hello\r\n
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", missingTerminator];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultError);
XCTAssertNotNil(error);
}
// A2.8 <EFBFBD><EFBFBD>?trailers
- (void)testCheckChunkedBody_WithTrailers_DetectsComplete {
NSString *chunkWithTrailers = @"5\r\nhello\r\n0\r\nX-Trailer: value\r\nX-Custom: test\r\n\r\n";
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", chunkWithTrailers];
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
NSError *error;
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
headerEndIndex:headerEndIndex
error:&error];
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
XCTAssertNil(error);
}
#pragma mark - A3. Chunked (2<EFBFBD><EFBFBD>?
// A3.1 chunks
- (void)testDecodeChunkedBody_MultipleChunks_DecodesCorrectly {
NSArray *chunks = @[
[@"hello" dataUsingEncoding:NSUTF8StringEncoding],
[@" world" dataUsingEncoding:NSUTF8StringEncoding]
];
NSData *chunkedData = [HttpdnsNWHTTPClientTestHelper createChunkedHTTPResponseWithStatus:200
headers:nil
chunks:chunks];
// chunked body <EFBFBD><EFBFBD>?headers<EFBFBD><EFBFBD>?
NSData *headerData = [@"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
NSData *bodyData = [chunkedData subdataWithRange:NSMakeRange(headerData.length, chunkedData.length - headerData.length)];
NSError *error;
NSData *decoded = [self.client decodeChunkedBody:bodyData error:&error];
XCTAssertNotNil(decoded);
XCTAssertNil(error);
NSString *decodedString = [[NSString alloc] initWithData:decoded encoding:NSUTF8StringEncoding];
XCTAssertEqualObjects(decodedString, @"hello world");
}
// A3.2 nil
- (void)testDecodeChunkedBody_InvalidFormat_ReturnsNil {
NSString *invalidChunked = @"ZZZ\r\nbad data\r\n";
NSData *bodyData = [invalidChunked dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSData *decoded = [self.client decodeChunkedBody:bodyData error:&error];
XCTAssertNil(decoded);
XCTAssertNotNil(error);
}
#pragma mark - A4. (6<EFBFBD><EFBFBD>?
// A4.1 Content-Length
- (void)testParseResponse_WithContentLength_ParsesCorrectly {
NSString *bodyString = @"{\"ips\":[]}";
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:@{@"Content-Type": @"application/json"}
body:bodyData];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success);
XCTAssertEqual(statusCode, 200);
XCTAssertNotNil(headers);
XCTAssertEqualObjects(headers[@"content-type"], @"application/json");
XCTAssertEqualObjects(body, bodyData);
XCTAssertNil(error);
}
// A4.2 Chunked
- (void)testParseResponse_WithChunkedEncoding_DecodesBody {
NSArray *chunks = @[
[@"{\"ips\"" dataUsingEncoding:NSUTF8StringEncoding],
[@":[]}" dataUsingEncoding:NSUTF8StringEncoding]
];
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createChunkedHTTPResponseWithStatus:200
headers:@{@"Content-Type": @"application/json"}
chunks:chunks];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success);
XCTAssertEqual(statusCode, 200);
NSString *bodyString = [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding];
XCTAssertEqualObjects(bodyString, @"{\"ips\":[]}");
}
// A4.3 <EFBFBD><EFBFBD>?body
- (void)testParseResponse_EmptyBody_Success {
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:204
statusText:@"No Content"
headers:nil
body:nil];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success);
XCTAssertEqual(statusCode, 204);
XCTAssertEqual(body.length, 0);
}
// A4.4 Content-Length <EFBFBD><EFBFBD>?
- (void)testParseResponse_ContentLengthMismatch_LogsButSucceeds {
NSData *bodyData = [@"short" dataUsingEncoding:NSUTF8StringEncoding];
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:@{@"Content-Length": @"100"} // <EFBFBD><EFBFBD>?
body:bodyData];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success); // <EFBFBD><EFBFBD>?
XCTAssertEqualObjects(body, bodyData);
}
// A4.5 <EFBFBD><EFBFBD>?
- (void)testParseResponse_EmptyData_ReturnsError {
NSData *emptyData = [NSData data];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:emptyData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertFalse(success);
XCTAssertNotNil(error);
}
// A4.6 headers <EFBFBD><EFBFBD>?body
- (void)testParseResponse_OnlyHeaders_EmptyBody {
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:@{@"Content-Type": @"text/plain"}
body:nil];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success);
XCTAssertEqual(statusCode, 200);
XCTAssertNotNil(headers);
XCTAssertEqual(body.length, 0);
}
#pragma mark - C. (7<EFBFBD><EFBFBD>?
// C.1 GET
- (void)testBuildHTTPRequest_BasicGET_CorrectFormat {
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:@"TestAgent"];
XCTAssertTrue([request containsString:@"GET / HTTP/1.1\r\n"]);
XCTAssertTrue([request containsString:@"Host: example.com\r\n"]);
XCTAssertTrue([request containsString:@"User-Agent: TestAgent\r\n"]);
XCTAssertTrue([request containsString:@"Accept: application/json\r\n"]);
XCTAssertTrue([request containsString:@"Accept-Encoding: identity\r\n"]);
XCTAssertTrue([request containsString:@"Connection: keep-alive\r\n"]);
XCTAssertTrue([request hasSuffix:@"\r\n\r\n"]);
}
// C.2 <EFBFBD><EFBFBD>?
- (void)testBuildHTTPRequest_WithQueryString_Included {
NSURL *url = [NSURL URLWithString:@"http://example.com/path?foo=bar&baz=qux"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertTrue([request containsString:@"GET /path?foo=bar&baz=qux HTTP/1.1\r\n"]);
}
// C.3 User-Agent
- (void)testBuildHTTPRequest_WithUserAgent_Included {
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:@"CustomAgent/1.0"];
XCTAssertTrue([request containsString:@"User-Agent: CustomAgent/1.0\r\n"]);
}
// C.4 HTTP <EFBFBD><EFBFBD>?
- (void)testBuildHTTPRequest_HTTPDefaultPort_NotInHost {
NSURL *url = [NSURL URLWithString:@"http://example.com:80/"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertTrue([request containsString:@"Host: example.com\r\n"]);
XCTAssertFalse([request containsString:@"Host: example.com:80\r\n"]);
}
// C.5 HTTPS <EFBFBD><EFBFBD>?
- (void)testBuildHTTPRequest_HTTPSDefaultPort_NotInHost {
NSURL *url = [NSURL URLWithString:@"https://example.com:443/"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertTrue([request containsString:@"Host: example.com\r\n"]);
XCTAssertFalse([request containsString:@"Host: example.com:443\r\n"]);
}
// C.6 <EFBFBD><EFBFBD>?
- (void)testBuildHTTPRequest_NonDefaultPort_InHost {
NSURL *url = [NSURL URLWithString:@"http://example.com:8080/"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertTrue([request containsString:@"Host: example.com:8080\r\n"]);
}
// C.7
- (void)testBuildHTTPRequest_FixedHeaders_Present {
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertTrue([request containsString:@"Accept: application/json\r\n"]);
XCTAssertTrue([request containsString:@"Accept-Encoding: identity\r\n"]);
XCTAssertTrue([request containsString:@"Connection: keep-alive\r\n"]);
}
#pragma mark - E. TLS (4)
// TLS SecTrustRef mock
// <EFBFBD><EFBFBD>?
// E.1 YES
- (void)testEvaluateServerTrust_ValidCertificate_ReturnsYES {
// SecTrustRef
//
}
// E.2 Proceed YES
- (void)testEvaluateServerTrust_ProceedResult_ReturnsYES {
// Mock SecTrustEvaluate kSecTrustResultProceed
}
// E.3 NO
- (void)testEvaluateServerTrust_InvalidCertificate_ReturnsNO {
// Mock SecTrustEvaluate kSecTrustResultDeny
}
// E.4 使 SSL Policy
- (void)testEvaluateServerTrust_WithDomain_UsesSSLPolicy {
// 使<EFBFBD><EFBFBD>?SecPolicyCreateSSL(true, domain)
}
#pragma mark - F. (5<EFBFBD><EFBFBD>?
// F.1 URL
- (void)testPerformRequest_VeryLongURL_HandlesCorrectly {
NSMutableString *longPath = [NSMutableString stringWithString:@"http://example.com/"];
for (int i = 0; i < 1000; i++) {
[longPath appendString:@"long/"];
}
NSURL *url = [NSURL URLWithString:longPath];
XCTAssertNotNil(url);
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertTrue(request.length > 5000);
}
// F.2 <EFBFBD><EFBFBD>?User-Agent
- (void)testBuildRequest_EmptyUserAgent_NoUserAgentHeader {
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
NSString *requestWithNil = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
XCTAssertFalse([requestWithNil containsString:@"User-Agent:"]);
}
// F.3 <EFBFBD><EFBFBD>?
- (void)testParseResponse_VeryLargeBody_HandlesCorrectly {
NSData *largeBody = [HttpdnsNWHTTPClientTestHelper randomDataWithSize:5 * 1024 * 1024];
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:nil
body:largeBody];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success);
XCTAssertEqual(body.length, largeBody.length);
}
// F.4 Chunked 退<EFBFBD><EFBFBD>?
- (void)testParseResponse_ChunkedDecodeFails_FallsBackToRaw {
NSString *badChunked = @"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nBAD_CHUNK_DATA";
NSData *responseData = [badChunked dataUsingEncoding:NSUTF8StringEncoding];
NSInteger statusCode;
NSDictionary *headers;
NSData *body;
NSError *error;
BOOL success = [self.client parseHTTPResponseData:responseData
statusCode:&statusCode
headers:&headers
body:&body
error:&error];
XCTAssertTrue(success);
XCTAssertNotNil(body);
}
// F.5 <EFBFBD><EFBFBD>?key
- (void)testConnectionPoolKey_DifferentHosts_SeparateKeys {
NSString *key1 = [self.client connectionPoolKeyForHost:@"host1.com" port:@"80" useTLS:NO];
NSString *key2 = [self.client connectionPoolKeyForHost:@"host2.com" port:@"80" useTLS:NO];
XCTAssertNotEqualObjects(key1, key2);
}
- (void)testConnectionPoolKey_DifferentPorts_SeparateKeys {
NSString *key1 = [self.client connectionPoolKeyForHost:@"example.com" port:@"80" useTLS:NO];
NSString *key2 = [self.client connectionPoolKeyForHost:@"example.com" port:@"8080" useTLS:NO];
XCTAssertNotEqualObjects(key1, key2);
}
- (void)testConnectionPoolKey_HTTPvsHTTPS_SeparateKeys {
NSString *keyHTTP = [self.client connectionPoolKeyForHost:@"example.com" port:@"80" useTLS:NO];
NSString *keyHTTPS = [self.client connectionPoolKeyForHost:@"example.com" port:@"443" useTLS:YES];
XCTAssertNotEqualObjects(keyHTTP, keyHTTPS);
}
@end

View File

@@ -0,0 +1,406 @@
//
// HttpdnsNWHTTPClient_BasicIntegrationTests.m
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
// - (G) <EFBFBD><EFBFBD>?(J) <EFBFBD><EFBFBD>?
// <EFBFBD><EFBFBD>?2 G:7 + J:5<EFBFBD><EFBFBD>?
//
#import "HttpdnsNWHTTPClientTestBase.h"
@interface HttpdnsNWHTTPClient_BasicIntegrationTests : HttpdnsNWHTTPClientTestBase
@end
@implementation HttpdnsNWHTTPClient_BasicIntegrationTests
#pragma mark - G.
// G.1 HTTP GET
- (void)testIntegration_HTTPGetRequest_RealNetwork {
XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP GET request"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"Response should not be nil");
XCTAssertNil(error, @"Error should be nil, got: %@", error);
XCTAssertEqual(response.statusCode, 200, @"Status code should be 200");
XCTAssertNotNil(response.body, @"Body should not be nil");
XCTAssertGreaterThan(response.body.length, 0, @"Body should not be empty");
// JSON
NSError *jsonError = nil;
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:response.body
options:0
error:&jsonError];
XCTAssertNotNil(jsonDict, @"Response should be valid JSON");
XCTAssertNil(jsonError, @"JSON parsing should succeed");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:20.0];
}
// G.2 HTTPS GET
- (void)testIntegration_HTTPSGetRequest_RealNetwork {
XCTestExpectation *expectation = [self expectationWithDescription:@"HTTPS GET request"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"Response should not be nil");
XCTAssertNil(error, @"Error should be nil, got: %@", error);
XCTAssertEqual(response.statusCode, 200, @"Status code should be 200");
XCTAssertNotNil(response.body, @"Body should not be nil");
// TLS
XCTAssertGreaterThan(response.body.length, 0, @"HTTPS body should not be empty");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:20.0];
}
// G.3 HTTP 404
- (void)testIntegration_NotFound_Returns404 {
XCTestExpectation *expectation = [self expectationWithDescription:@"404 response"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/status/404"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"Response should not be nil even for 404");
XCTAssertNil(error, @"Error should be nil for valid HTTP response");
XCTAssertEqual(response.statusCode, 404, @"Status code should be 404");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:20.0];
}
// G.4
- (void)testIntegration_ConnectionReuse_MultipleRequests {
XCTestExpectation *expectation = [self expectationWithDescription:@"Connection reuse"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1, @"First response should not be nil");
XCTAssertNil(error1, @"First request should succeed");
XCTAssertEqual(response1.statusCode, 200);
//
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2, @"Second response should not be nil");
XCTAssertNil(error2, @"Second request should succeed");
XCTAssertEqual(response2.statusCode, 200);
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:30.0];
}
// G.5 Chunked
- (void)testIntegration_ChunkedResponse_RealNetwork {
XCTestExpectation *expectation = [self expectationWithDescription:@"Chunked response"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
// httpbin.org/stream-bytes chunked <EFBFBD><EFBFBD>?
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/stream-bytes/1024"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"Response should not be nil");
XCTAssertNil(error, @"Error should be nil, got: %@", error);
XCTAssertEqual(response.statusCode, 200);
XCTAssertEqual(response.body.length, 1024, @"Should receive exactly 1024 bytes");
// Transfer-Encoding <EFBFBD><EFBFBD>?
NSString *transferEncoding = response.headers[@"transfer-encoding"];
if (transferEncoding) {
XCTAssertTrue([transferEncoding containsString:@"chunked"], @"Should use chunked encoding");
}
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:20.0];
}
#pragma mark - <EFBFBD><EFBFBD>?
// G.6
- (void)testIntegration_RequestTimeout_ReturnsError {
XCTestExpectation *expectation = [self expectationWithDescription:@"Request timeout"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
// httpbin.org/delay/10 <EFBFBD><EFBFBD>?10 2 <EFBFBD><EFBFBD>?
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:2.0
error:&error];
XCTAssertNil(response, @"Response should be nil on timeout");
XCTAssertNotNil(error, @"Error should be set on timeout");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:5.0];
}
// G.7 <EFBFBD><EFBFBD>?
- (void)testIntegration_CustomHeaders_Reflected {
XCTestExpectation *expectation = [self expectationWithDescription:@"Custom headers"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/headers"
userAgent:@"TestUserAgent/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
XCTAssertEqual(response.statusCode, 200);
// JSON User-Agent <EFBFBD><EFBFBD>?
NSError *jsonError = nil;
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:response.body
options:0
error:&jsonError];
XCTAssertNotNil(jsonDict);
NSDictionary *headers = jsonDict[@"headers"];
XCTAssertTrue([headers[@"User-Agent"] containsString:@"TestUserAgent"], @"User-Agent should be sent");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:20.0];
}
#pragma mark - J.
// J.1 <EFBFBD><EFBFBD>?1
- (void)testConnectionReuse_Expiry31Seconds_NewConnectionCreated {
if (getenv("SKIP_SLOW_TESTS")) {
return;
}
XCTestExpectation *expectation = [self expectationWithDescription:@"Connection expiry"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CFAbsoluteTime time1 = CFAbsoluteTimeGetCurrent();
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"First"
timeout:15.0
error:&error1];
CFAbsoluteTime elapsed1 = CFAbsoluteTimeGetCurrent() - time1;
XCTAssertTrue(response1 != nil || error1 != nil);
// 31
[NSThread sleepForTimeInterval:31.0];
//
CFAbsoluteTime time2 = CFAbsoluteTimeGetCurrent();
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Second"
timeout:15.0
error:&error2];
CFAbsoluteTime elapsed2 = CFAbsoluteTimeGetCurrent() - time2;
XCTAssertTrue(response2 != nil || error2 != nil);
//
//
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:70.0];
}
// J.2 <EFBFBD><EFBFBD>?
- (void)testConnectionReuse_TenRequests_OnlyFourConnectionsKept {
XCTestExpectation *expectation = [self expectationWithDescription:@"Pool size limit"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 10<EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 10; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"PoolSizeTest"
timeout:15.0
error:&error];
XCTAssertTrue(response != nil || error != nil);
}
// <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:1.0];
//
//
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Verification"
timeout:15.0
error:&error];
XCTAssertTrue(response != nil || error != nil);
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:120.0];
}
// J.3
- (void)testConnectionReuse_DifferentPaths_SameConnection {
XCTestExpectation *expectation = [self expectationWithDescription:@"Different paths"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray<NSString *> *paths = @[@"/get", @"/headers", @"/user-agent", @"/uuid"];
NSMutableArray<NSNumber *> *times = [NSMutableArray array];
for (NSString *path in paths) {
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
NSString *urlString = [NSString stringWithFormat:@"http://127.0.0.1:11080%@", path];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"PathTest"
timeout:15.0
error:&error];
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - start;
XCTAssertTrue(response != nil || error != nil);
[times addObject:@(elapsed)];
}
//
//
XCTAssertEqual(times.count, paths.count);
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:60.0];
}
// J.4 HTTP vs HTTPS 使
- (void)testConnectionReuse_HTTPvsHTTPS_DifferentPoolKeys {
XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP vs HTTPS"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// HTTP
NSError *httpError = nil;
HttpdnsNWHTTPClientResponse *httpResponse = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"HTTP"
timeout:15.0
error:&httpError];
XCTAssertTrue(httpResponse != nil || httpError != nil);
// HTTPS 使<EFBFBD><EFBFBD>?key<EFBFBD><EFBFBD>?
NSError *httpsError = nil;
HttpdnsNWHTTPClientResponse *httpsResponse = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"HTTPS"
timeout:15.0
error:&httpsError];
XCTAssertTrue(httpsResponse != nil || httpsError != nil);
//
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:35.0];
}
// J.5 <EFBFBD><EFBFBD>?
- (void)testConnectionReuse_TwentyRequestsOneSecondApart_ConnectionKeptAlive {
if (getenv("SKIP_SLOW_TESTS")) {
return;
}
XCTestExpectation *expectation = [self expectationWithDescription:@"Keep-alive"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSInteger successCount = 0;
NSMutableArray<NSNumber *> *requestTimes = [NSMutableArray array];
// 201
for (NSInteger i = 0; i < 20; i++) {
// 1<EFBFBD><EFBFBD>?
if (i > 0) {
[NSThread sleepForTimeInterval:1.0];
}
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"KeepAlive"
timeout:10.0
error:&error];
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - startTime;
[requestTimes addObject:@(elapsed)];
if (response && (response.statusCode == 200 || response.statusCode == 503)) {
successCount++;
} else {
// 退
break;
}
}
// <EFBFBD><EFBFBD>?
XCTAssertGreaterThan(successCount, 15, @"Most requests should succeed with connection reuse");
// 使keep-alive<EFBFBD><EFBFBD>?
if (requestTimes.count >= 10) {
double firstRequestTime = [requestTimes[0] doubleValue];
double laterAvgTime = 0;
for (NSInteger i = 5; i < MIN(10, requestTimes.count); i++) {
laterAvgTime += [requestTimes[i] doubleValue];
}
laterAvgTime /= MIN(5, requestTimes.count - 5);
//
XCTAssertLessThanOrEqual(laterAvgTime, firstRequestTime * 2.0, @"Connection reuse should keep latency reasonable");
}
[expectation fulfill];
});
// : 19sleep + 20×~2<EFBFBD><EFBFBD>?= 5950退
[self waitForExpectations:@[expectation] timeout:50.0];
}
@end

View File

@@ -0,0 +1,534 @@
//
// HttpdnsNWHTTPClient_ConcurrencyTests.m
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
// - (H)<EFBFBD><EFBFBD>?(I) (N) <EFBFBD><EFBFBD>?
// <EFBFBD><EFBFBD>?3 H:5 + I:5 + N:3<EFBFBD><EFBFBD>?
//
#import "HttpdnsNWHTTPClientTestBase.h"
@interface HttpdnsNWHTTPClient_ConcurrencyTests : HttpdnsNWHTTPClientTestBase
@end
@implementation HttpdnsNWHTTPClient_ConcurrencyTests
#pragma mark - H.
// H.1
- (void)testConcurrency_ParallelRequestsSameHost_AllSucceed {
NSInteger concurrentCount = 10;
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray<NSNumber *> *responseTimes = [NSMutableArray array];
NSLock *lock = [[NSLock alloc] init];
for (NSInteger i = 0; i < concurrentCount; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_group_enter(group);
dispatch_async(queue, ^{
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent();
XCTAssertNotNil(response, @"Response %ld should not be nil", (long)i);
XCTAssertTrue(response.statusCode == 200 || response.statusCode == 503,
@"Request %ld got statusCode=%ld, expected 200 or 503", (long)i, (long)response.statusCode);
[lock lock];
[responseTimes addObject:@(endTime - startTime)];
[lock unlock];
[expectation fulfill];
dispatch_group_leave(group);
});
}
[self waitForExpectations:expectations timeout:30.0];
//
XCTAssertEqual(responseTimes.count, concurrentCount);
}
// H.2
- (void)testConcurrency_ParallelRequestsDifferentPaths_AllSucceed {
NSArray<NSString *> *paths = @[@"/get", @"/status/200", @"/headers", @"/user-agent", @"/uuid"];
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSString *path in paths) {
XCTestExpectation *expectation = [self expectationWithDescription:path];
[expectations addObject:expectation];
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSString *urlString = [NSString stringWithFormat:@"http://127.0.0.1:11080%@", path];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"Response for %@ should not be nil", path);
XCTAssertTrue(response.statusCode == 200 || response.statusCode == 503, @"Request %@ should get valid status", path);
[expectation fulfill];
dispatch_group_leave(group);
});
}
[self waitForExpectations:expectations timeout:30.0];
}
// H.3 HTTP + HTTPS
- (void)testConcurrency_MixedHTTPAndHTTPS_BothSucceed {
NSInteger httpCount = 5;
NSInteger httpsCount = 5;
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// HTTP
for (NSInteger i = 0; i < httpCount; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"HTTP %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
[expectation fulfill];
});
}
// HTTPS
for (NSInteger i = 0; i < httpsCount; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"HTTPS %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:40.0];
}
// H.4 <EFBFBD><EFBFBD>?
- (void)testConcurrency_HighLoad50Concurrent_NoDeadlock {
NSInteger concurrentCount = 50;
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLock *successCountLock = [[NSLock alloc] init];
__block NSInteger successCount = 0;
for (NSInteger i = 0; i < concurrentCount; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"HttpdnsNWHTTPClient/1.0"
timeout:15.0
error:&error];
if (response && (response.statusCode == 200 || response.statusCode == 503)) {
[successCountLock lock];
successCount++;
[successCountLock unlock];
}
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:60.0];
// <EFBFBD><EFBFBD>?
XCTAssertGreaterThan(successCount, concurrentCount * 0.8, @"At least 80%% should succeed");
}
// H.5 +
- (void)testConcurrency_MixedSerialAndParallel_NoInterference {
XCTestExpectation *serialExpectation = [self expectationWithDescription:@"Serial requests"];
XCTestExpectation *parallel1 = [self expectationWithDescription:@"Parallel 1"];
XCTestExpectation *parallel2 = [self expectationWithDescription:@"Parallel 2"];
XCTestExpectation *parallel3 = [self expectationWithDescription:@"Parallel 3"];
dispatch_queue_t serialQueue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t parallelQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 线
dispatch_async(serialQueue, ^{
for (NSInteger i = 0; i < 5; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Serial"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
}
[serialExpectation fulfill];
});
// 线
dispatch_async(parallelQueue, ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/uuid"
userAgent:@"Parallel1"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
[parallel1 fulfill];
});
dispatch_async(parallelQueue, ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/headers"
userAgent:@"Parallel2"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
[parallel2 fulfill];
});
dispatch_async(parallelQueue, ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/user-agent"
userAgent:@"Parallel3"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
[parallel3 fulfill];
});
[self waitForExpectations:@[serialExpectation, parallel1, parallel2, parallel3] timeout:60.0];
}
#pragma mark - I. <EFBFBD><EFBFBD>?
// I.1 <EFBFBD><EFBFBD>?
- (void)testRaceCondition_ExceedPoolCapacity_MaxFourConnections {
XCTestExpectation *expectation = [self expectationWithDescription:@"Pool capacity test"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// <EFBFBD><EFBFBD>?10 <EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 10; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"PoolTest"
timeout:15.0
error:&error];
XCTAssertTrue(response != nil || error != nil);
}
//
[NSThread sleepForTimeInterval:1.0];
//
// <EFBFBD><EFBFBD>?4 <EFBFBD><EFBFBD>?
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:120.0];
}
// I.2
- (void)testRaceCondition_SimultaneousConnectionReturn_NoDataRace {
NSInteger concurrentCount = 5;
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSInteger i = 0; i < concurrentCount; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Return %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"ReturnTest"
timeout:15.0
error:&error];
XCTAssertTrue(response != nil || error != nil);
// <EFBFBD><EFBFBD>?
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:30.0];
// <EFBFBD><EFBFBD>?
}
// I.3 --<EFBFBD><EFBFBD>?
- (void)testRaceCondition_AcquireReturnReacquire_CorrectState {
XCTestExpectation *expectation = [self expectationWithDescription:@"Acquire-Return-Reacquire"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// <EFBFBD><EFBFBD>?
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"First"
timeout:15.0
error:&error1];
XCTAssertTrue(response1 != nil || error1 != nil);
// <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:0.1];
//
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Second"
timeout:15.0
error:&error2];
XCTAssertTrue(response2 != nil || error2 != nil);
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:35.0];
}
// I.4 <EFBFBD><EFBFBD>?1<EFBFBD><EFBFBD>?
- (void)testRaceCondition_ExpiredConnectionPruning_CreatesNewConnection {
// SKIP_SLOW_TESTS
if (getenv("SKIP_SLOW_TESTS")) {
return;
}
XCTestExpectation *expectation = [self expectationWithDescription:@"Connection expiry"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Initial"
timeout:15.0
error:&error1];
XCTAssertTrue(response1 != nil || error1 != nil);
// 30<EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:31.0];
//
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"AfterExpiry"
timeout:15.0
error:&error2];
XCTAssertTrue(response2 != nil || error2 != nil);
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:70.0];
}
// I.5 <EFBFBD><EFBFBD>?
- (void)testRaceCondition_ErrorRecovery_PoolRemainsHealthy {
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// <EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 3; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Error %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
// 使<EFBFBD><EFBFBD>?
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/5"
userAgent:@"ErrorTest"
timeout:1.0
error:&error];
//
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:15.0];
//
XCTestExpectation *recoveryExpectation = [self expectationWithDescription:@"Recovery"];
dispatch_async(queue, ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Recovery"
timeout:15.0
error:&error];
XCTAssertTrue(response != nil || error != nil);
[recoveryExpectation fulfill];
});
[self waitForExpectations:@[recoveryExpectation] timeout:20.0];
}
#pragma mark - N. <EFBFBD><EFBFBD>?
// N.1 <EFBFBD><EFBFBD>?
- (void)testConcurrentMultiPort_ParallelKeepAlive_IndependentConnections {
if (getenv("SKIP_SLOW_TESTS")) {
return;
}
XCTestExpectation *expectation11443 = [self expectationWithDescription:@"Port 11443 keep-alive"];
XCTestExpectation *expectation11444 = [self expectationWithDescription:@"Port 11444 keep-alive"];
// 线 1 11443 10 1 <EFBFBD><EFBFBD>?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSInteger i = 0; i < 10; i++) {
if (i > 0) {
[NSThread sleepForTimeInterval:1.0];
}
NSError *error = nil;
[self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"KeepAlive11443"
timeout:15.0
error:&error];
}
[expectation11443 fulfill];
});
// 线 2 11444 10 1 <EFBFBD><EFBFBD>?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (NSInteger i = 0; i < 10; i++) {
if (i > 0) {
[NSThread sleepForTimeInterval:1.0];
}
NSError *error = nil;
[self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"KeepAlive11444"
timeout:15.0
error:&error];
}
[expectation11444 fulfill];
});
[self waitForExpectations:@[expectation11443, expectation11444] timeout:40.0];
}
// N.2
- (void)testConcurrentMultiPort_RoundRobinDistribution_EvenLoad {
XCTestExpectation *expectation = [self expectationWithDescription:@"Round-robin distribution"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445, @11446];
NSInteger totalRequests = 100;
NSMutableDictionary<NSNumber *, NSNumber *> *portRequestCounts = [NSMutableDictionary dictionary];
//
for (NSNumber *port in ports) {
portRequestCounts[port] = @0;
}
// 4 <EFBFBD><EFBFBD>?100 <EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < totalRequests; i++) {
NSNumber *port = ports[i % ports.count];
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"RoundRobin"
timeout:15.0
error:&error];
if (response && response.statusCode == 200) {
NSInteger count = [portRequestCounts[port] integerValue];
portRequestCounts[port] = @(count + 1);
}
}
// 25 <EFBFBD><EFBFBD>?
for (NSNumber *port in ports) {
NSInteger count = [portRequestCounts[port] integerValue];
XCTAssertEqual(count, 25, @"Port %@ should receive 25 requests", port);
}
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:180.0];
}
// N.3 <EFBFBD><EFBFBD>?
- (void)testConcurrentMultiPort_MixedLoadPattern_RobustHandling {
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 11443<EFBFBD><EFBFBD>?0
for (NSInteger i = 0; i < 20; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Heavy11443 %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
[self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"HeavyLoad"
timeout:15.0
error:&error];
[expectation fulfill];
});
}
// 11444<EFBFBD><EFBFBD>?0
for (NSInteger i = 0; i < 10; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Medium11444 %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
[self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"MediumLoad"
timeout:15.0
error:&error];
[expectation fulfill];
});
}
// 11445<EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 5; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Light11445 %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
[self.client performRequestWithURLString:@"https://127.0.0.1:11445/get"
userAgent:@"LightLoad"
timeout:15.0
error:&error];
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:80.0];
}
@end

View File

@@ -0,0 +1,740 @@
//
// HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests.m
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
// <EFBFBD><EFBFBD>?- (M)<EFBFBD><EFBFBD>?(P) <EFBFBD><EFBFBD>?Connection (R) <EFBFBD><EFBFBD>?
// <EFBFBD><EFBFBD>?5 M:4 + P:6 + R:5<EFBFBD><EFBFBD>?
//
#import "HttpdnsNWHTTPClientTestBase.h"
@interface HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests : HttpdnsNWHTTPClientTestBase
@end
@implementation HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests
#pragma mark - M. <EFBFBD><EFBFBD>?
// M.1
- (void)testEdgeCase_ConnectionReuseWithinPortOnly_NotAcross {
XCTestExpectation *expectation = [self expectationWithDescription:@"Reuse boundaries"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// A <EFBFBD><EFBFBD>?11443
CFAbsoluteTime timeA = CFAbsoluteTimeGetCurrent();
NSError *errorA = nil;
HttpdnsNWHTTPClientResponse *responseA = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"RequestA"
timeout:15.0
error:&errorA];
CFAbsoluteTime elapsedA = CFAbsoluteTimeGetCurrent() - timeA;
XCTAssertNotNil(responseA);
// B <EFBFBD><EFBFBD>?11443<EFBFBD><EFBFBD>?
CFAbsoluteTime timeB = CFAbsoluteTimeGetCurrent();
NSError *errorB = nil;
HttpdnsNWHTTPClientResponse *responseB = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"RequestB"
timeout:15.0
error:&errorB];
CFAbsoluteTime elapsedB = CFAbsoluteTimeGetCurrent() - timeB;
XCTAssertNotNil(responseB);
// C <EFBFBD><EFBFBD>?11444<EFBFBD><EFBFBD>?
CFAbsoluteTime timeC = CFAbsoluteTimeGetCurrent();
NSError *errorC = nil;
HttpdnsNWHTTPClientResponse *responseC = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"RequestC"
timeout:15.0
error:&errorC];
CFAbsoluteTime elapsedC = CFAbsoluteTimeGetCurrent() - timeC;
XCTAssertNotNil(responseC);
// D <EFBFBD><EFBFBD>?11444<EFBFBD><EFBFBD>?11444
CFAbsoluteTime timeD = CFAbsoluteTimeGetCurrent();
NSError *errorD = nil;
HttpdnsNWHTTPClientResponse *responseD = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"RequestD"
timeout:15.0
error:&errorD];
CFAbsoluteTime elapsedD = CFAbsoluteTimeGetCurrent() - timeD;
XCTAssertNotNil(responseD);
//
XCTAssertEqual(responseA.statusCode, 200);
XCTAssertEqual(responseB.statusCode, 200);
XCTAssertEqual(responseC.statusCode, 200);
XCTAssertEqual(responseD.statusCode, 200);
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:70.0];
}
// M.2 <EFBFBD><EFBFBD>?
- (void)testEdgeCase_HighPortCount_AllPortsManaged {
XCTestExpectation *expectation = [self expectationWithDescription:@"High port count"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445, @11446];
// <EFBFBD><EFBFBD>?
for (NSNumber *port in ports) {
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"Round1"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"First round request to port %@ should succeed", port);
}
// <EFBFBD><EFBFBD>?
for (NSNumber *port in ports) {
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"Round2"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"Second round request to port %@ should reuse connection", port);
}
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:120.0];
}
// M.3 访<EFBFBD><EFBFBD>?
- (void)testEdgeCase_ConcurrentPoolAccess_NoDataRace {
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445];
NSInteger requestsPerPort = 5;
// <EFBFBD><EFBFBD>?
for (NSNumber *port in ports) {
for (NSInteger i = 0; i < requestsPerPort; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Port %@ Req %ld", port, (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"ConcurrentAccess"
timeout:15.0
error:&error];
// 访<EFBFBD><EFBFBD>?
XCTAssertTrue(response != nil || error != nil);
[expectation fulfill];
});
}
}
[self waitForExpectations:expectations timeout:50.0];
}
// M.4
- (void)testEdgeCase_PortMigration_OldConnectionsEventuallyExpire {
if (getenv("SKIP_SLOW_TESTS")) {
return;
}
XCTestExpectation *expectation = [self expectationWithDescription:@"Port migration"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 1 11443
for (NSInteger i = 0; i < 5; i++) {
NSError *error = nil;
[self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"Port11443"
timeout:15.0
error:&error];
}
// 2 11444<EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 5; i++) {
NSError *error = nil;
[self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Port11444"
timeout:15.0
error:&error];
}
// 30 <EFBFBD><EFBFBD>?11443 <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:31.0];
// 3<EFBFBD><EFBFBD>?11444
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Port11444After"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1, @"Port 11444 should still work after 11443 expired");
// 4<EFBFBD><EFBFBD>?11443 <EFBFBD><EFBFBD>?
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"Port11443New"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2, @"Port 11443 should work with new connection after expiry");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:120.0];
}
#pragma mark - P.
// P.1
- (void)testTimeout_SingleRequest_ConnectionRemovedFromPool {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// <EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Pool should be empty initially");
// delay 10s, timeout 1s<EFBFBD><EFBFBD>?
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"TimeoutTest"
timeout:1.0
error:&error];
//
XCTAssertNil(response, @"Response should be nil on timeout");
XCTAssertNotNil(error, @"Error should be set on timeout");
// returnConnection
[NSThread sleepForTimeInterval:0.5];
// <EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Timed-out connection should be removed from pool");
XCTAssertEqual([self.client totalConnectionCount], 0,
@"No connections should remain in pool");
//
XCTAssertEqual(self.client.connectionCreationCount, 1,
@"Should have created 1 connection");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"No reuse for timed-out connection");
}
// P.2
- (void)testTimeout_PoolRecovery_SubsequentRequestSucceeds {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
//
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"TimeoutTest"
timeout:1.0
error:&error1];
XCTAssertNil(response1, @"First request should timeout");
XCTAssertNotNil(error1);
//
[NSThread sleepForTimeInterval:0.5];
//
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"RecoveryTest"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2, @"Second request should succeed after timeout");
XCTAssertEqual(response2.statusCode, 200);
// returnConnection
[NSThread sleepForTimeInterval:0.5];
// <EFBFBD><EFBFBD>?1
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
@"Pool should have 1 connection from second request");
//
NSError *error3 = nil;
HttpdnsNWHTTPClientResponse *response3 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"ReuseTest"
timeout:15.0
error:&error3];
XCTAssertNotNil(response3);
XCTAssertEqual(response3.statusCode, 200);
// <EFBFBD><EFBFBD>? + 1 <EFBFBD><EFBFBD>? 1 <EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionCreationCount, 2,
@"Should have created 2 connections (1 timed out, 1 succeeded)");
XCTAssertEqual(self.client.connectionReuseCount, 1,
@"Third request should reuse second's connection");
}
// P.3 <EFBFBD><EFBFBD>?
- (void)testTimeout_ConcurrentPartialTimeout_SuccessfulRequestsReuse {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLock *successCountLock = [[NSLock alloc] init];
NSLock *timeoutCountLock = [[NSLock alloc] init];
__block NSInteger successCount = 0;
__block NSInteger timeoutCount = 0;
// 10 5 5 <EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 10; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
NSString *urlString;
NSTimeInterval timeout;
if (i % 2 == 0) {
// <EFBFBD><EFBFBD>?
urlString = @"http://127.0.0.1:11080/get";
timeout = 15.0;
} else {
// <EFBFBD><EFBFBD>?
urlString = @"http://127.0.0.1:11080/delay/10";
timeout = 0.5;
}
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"ConcurrentTest"
timeout:timeout
error:&error];
if (response && response.statusCode == 200) {
[successCountLock lock];
successCount++;
[successCountLock unlock];
} else {
[timeoutCountLock lock];
timeoutCount++;
[timeoutCountLock unlock];
}
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:20.0];
//
XCTAssertEqual(successCount, 5, @"5 requests should succeed");
XCTAssertEqual(timeoutCount, 5, @"5 requests should timeout");
// 5<EFBFBD><EFBFBD>?+ 5<EFBFBD><EFBFBD>?= <EFBFBD><EFBFBD>?0
XCTAssertGreaterThan(self.client.connectionCreationCount, 0,
@"Should have created connections for concurrent requests");
XCTAssertLessThanOrEqual(self.client.connectionCreationCount, 10,
@"Should not create more than 10 connections");
//
[NSThread sleepForTimeInterval:2.0];
// <EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?
// remote close<EFBFBD><EFBFBD>?
XCTAssertLessThanOrEqual([self.client totalConnectionCount], 4,
@"Total connections should not exceed pool limit (no leak)");
//
NSError *recoveryError = nil;
HttpdnsNWHTTPClientResponse *recoveryResponse = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"RecoveryTest"
timeout:15.0
error:&recoveryError];
XCTAssertNotNil(recoveryResponse, @"Pool should recover and handle new requests after mixed timeout/success");
XCTAssertEqual(recoveryResponse.statusCode, 200, @"Recovery request should succeed");
}
// P.4 <EFBFBD><EFBFBD>?
- (void)testTimeout_ConsecutiveTimeouts_NoConnectionLeak {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 10 <EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 10; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"LeakTest"
timeout:0.5
error:&error];
XCTAssertNil(response, @"Request %ld should timeout", (long)i);
//
[NSThread sleepForTimeInterval:0.2];
}
// <EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Pool should be empty after consecutive timeouts");
XCTAssertEqual([self.client totalConnectionCount], 0,
@"No connections should leak");
// <EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionCreationCount, 10,
@"Should have created 10 connections (all timed out and removed)");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"No reuse for timed-out connections");
}
// P.5
- (void)testTimeout_NonBlocking_ConcurrentNormalRequestSucceeds {
[self.client resetPoolStatistics];
XCTestExpectation *timeoutExpectation = [self expectationWithDescription:@"Timeout request"];
XCTestExpectation *successExpectation = [self expectationWithDescription:@"Success request"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// Adelay 10s, timeout 2s<EFBFBD><EFBFBD>?
dispatch_async(queue, ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"TimeoutRequest"
timeout:2.0
error:&error];
XCTAssertNil(response, @"Request A should timeout");
[timeoutExpectation fulfill];
});
// B A <EFBFBD><EFBFBD>?
dispatch_async(queue, ^{
// <EFBFBD><EFBFBD>?A <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:0.1];
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"NormalRequest"
timeout:15.0
error:&error];
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - startTime;
XCTAssertNotNil(response, @"Request B should succeed despite A timing out");
XCTAssertEqual(response.statusCode, 200);
// B <EFBFBD><EFBFBD>?A
XCTAssertLessThan(elapsed, 5.0,
@"Request B should complete quickly, not blocked by A's timeout");
[successExpectation fulfill];
});
[self waitForExpectations:@[timeoutExpectation, successExpectation] timeout:20.0];
//
[NSThread sleepForTimeInterval:0.5];
// B <EFBFBD><EFBFBD>?
NSString *poolKey = @"127.0.0.1:11080:tcp";
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
@"Pool should have 1 connection from successful request B");
}
// P.6 <EFBFBD><EFBFBD>?
- (void)testTimeout_MultiPort_IsolatedPoolCleaning {
[self.client resetPoolStatistics];
NSString *poolKey11443 = @"127.0.0.1:11443:tls";
NSString *poolKey11444 = @"127.0.0.1:11444:tls";
// 11443<EFBFBD><EFBFBD>?
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/delay/10"
userAgent:@"Port11443Timeout"
timeout:1.0
error:&error1];
XCTAssertNil(response1, @"Port 11443 request should timeout");
// 11444<EFBFBD><EFBFBD>?
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Port11444Success"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2, @"Port 11444 request should succeed");
XCTAssertEqual(response2.statusCode, 200);
//
[NSThread sleepForTimeInterval:0.5];
// <EFBFBD><EFBFBD>?11443 11444 <EFBFBD><EFBFBD>?1 <EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey11443], 0,
@"Port 11443 pool should be empty (timed out)");
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey11444], 1,
@"Port 11444 pool should have 1 connection");
//
XCTAssertEqual([self.client totalConnectionCount], 1,
@"Total should be 1 (only from port 11444)");
// 11444<EFBFBD><EFBFBD>?
NSError *error3 = nil;
HttpdnsNWHTTPClientResponse *response3 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Port11444Reuse"
timeout:15.0
error:&error3];
XCTAssertNotNil(response3);
XCTAssertEqual(response3.statusCode, 200);
//
XCTAssertEqual(self.client.connectionReuseCount, 1,
@"Second request to port 11444 should reuse connection");
}
#pragma mark - R. Connection
// R.1 Connection: close <EFBFBD><EFBFBD>?
- (void)testConnectionHeader_Close_ConnectionInvalidated {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 1 Connection: close
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=close"
userAgent:@"CloseTest"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1, @"Request with Connection: close should succeed");
XCTAssertEqual(response1.statusCode, 200);
// returnConnection
[NSThread sleepForTimeInterval:0.5];
//
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Connection with 'Connection: close' header should be invalidated and not returned to pool");
// 2<EFBFBD><EFBFBD>?
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=keep-alive"
userAgent:@"KeepAliveTest"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
XCTAssertEqual(response2.statusCode, 200);
// 2 close<EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionCreationCount, 2,
@"Should have created 2 connections (first closed by server)");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"No reuse after Connection: close");
}
// R.2 Connection: keep-alive
- (void)testConnectionHeader_KeepAlive_ConnectionReused {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 1 Connection: keep-alive
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=keep-alive"
userAgent:@"KeepAlive1"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
XCTAssertEqual(response1.statusCode, 200);
//
[NSThread sleepForTimeInterval:0.5];
//
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
@"Connection with 'Connection: keep-alive' should be returned to pool");
// 2<EFBFBD><EFBFBD>?
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=keep-alive"
userAgent:@"KeepAlive2"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
XCTAssertEqual(response2.statusCode, 200);
// <EFBFBD><EFBFBD>?1 <EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionCreationCount, 1,
@"Should have created only 1 connection");
XCTAssertEqual(self.client.connectionReuseCount, 1,
@"Second request should reuse the first connection");
}
// R.3 Proxy-Connection: close
- (void)testConnectionHeader_ProxyClose_ConnectionInvalidated {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 1 Proxy-Connection: close (+ Connection: keep-alive)
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=proxy-close"
userAgent:@"ProxyCloseTest"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1, @"Request with Proxy-Connection: close should succeed");
XCTAssertEqual(response1.statusCode, 200);
// returnConnection
[NSThread sleepForTimeInterval:0.5];
// Proxy-Connection: close <EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Connection with 'Proxy-Connection: close' header should be invalidated");
// 2
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=keep-alive"
userAgent:@"RecoveryTest"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
XCTAssertEqual(response2.statusCode, 200);
// 2 <EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionCreationCount, 2,
@"Should have created 2 connections (first closed by proxy header)");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"No reuse after Proxy-Connection: close");
}
// R.4 Connection
- (void)testConnectionHeader_CaseInsensitive_AllVariantsWork {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 1CONNECTION: CLOSE (<EFBFBD><EFBFBD>?
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=close-uppercase"
userAgent:@"UppercaseTest"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
XCTAssertEqual(response1.statusCode, 200);
[NSThread sleepForTimeInterval:0.5];
// CLOSE <EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"'CONNECTION: CLOSE' (uppercase) should also close connection");
// 2Connection: Close (<EFBFBD><EFBFBD>?
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=close-mixed"
userAgent:@"MixedCaseTest"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
XCTAssertEqual(response2.statusCode, 200);
[NSThread sleepForTimeInterval:0.5];
//
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"'Connection: Close' (mixed case) should also close connection");
// 2 close
XCTAssertEqual(self.client.connectionCreationCount, 2,
@"Should have created 2 connections (both closed due to case-insensitive matching)");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"No reuse for any closed connections");
}
// R.5 <EFBFBD><EFBFBD>?close <EFBFBD><EFBFBD>?keep-alive
- (void)testConnectionHeader_ConcurrentMixed_CloseAndKeepAliveIsolated {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLock *closeCountLock = [[NSLock alloc] init];
NSLock *keepAliveCountLock = [[NSLock alloc] init];
__block NSInteger closeCount = 0;
__block NSInteger keepAliveCount = 0;
// 10 5 <EFBFBD><EFBFBD>?close<EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?keep-alive
for (NSInteger i = 0; i < 10; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
NSString *urlString;
if (i % 2 == 0) {
// close
urlString = @"http://127.0.0.1:11080/connection-test?mode=close";
} else {
// keep-alive
urlString = @"http://127.0.0.1:11080/connection-test?mode=keep-alive";
}
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"ConcurrentMixed"
timeout:15.0
error:&error];
if (response && response.statusCode == 200) {
if (i % 2 == 0) {
[closeCountLock lock];
closeCount++;
[closeCountLock unlock];
} else {
[keepAliveCountLock lock];
keepAliveCount++;
[keepAliveCountLock unlock];
}
}
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:30.0];
//
XCTAssertEqual(closeCount, 5, @"5 close requests should succeed");
XCTAssertEqual(keepAliveCount, 5, @"5 keep-alive requests should succeed");
// <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:1.0];
//
// - close <EFBFBD><EFBFBD>?
// - keep-alive remote close<EFBFBD><EFBFBD>?
// - <EFBFBD><EFBFBD>?4<EFBFBD><EFBFBD>?
NSInteger poolSize = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolSize, 4,
@"Pool size should not exceed limit (no leak from close connections)");
// close
// <EFBFBD><EFBFBD>?>= 6 ( 5 <EFBFBD><EFBFBD>?close + 1 <EFBFBD><EFBFBD>?keep-alive<EFBFBD><EFBFBD>?close )
XCTAssertGreaterThanOrEqual(self.client.connectionCreationCount, 6,
@"Should have created at least 6 connections (5 close + at least 1 keep-alive)");
// <EFBFBD><EFBFBD>?
NSError *recoveryError = nil;
HttpdnsNWHTTPClientResponse *recoveryResponse = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"RecoveryTest"
timeout:15.0
error:&recoveryError];
XCTAssertNotNil(recoveryResponse, @"Pool should still be healthy after mixed close/keep-alive scenario");
XCTAssertEqual(recoveryResponse.statusCode, 200);
}
@end

View File

@@ -0,0 +1,774 @@
//
// HttpdnsNWHTTPClient_PoolManagementTests.m
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
// <EFBFBD><EFBFBD>?- <EFBFBD><EFBFBD>?(K) (L) (O)<EFBFBD><EFBFBD>?(S) <EFBFBD><EFBFBD>?
// <EFBFBD><EFBFBD>?6 K:5 + L:3 + O:3 + S:5<EFBFBD><EFBFBD>?
//
#import "HttpdnsNWHTTPClientTestBase.h"
@interface HttpdnsNWHTTPClient_PoolManagementTests : HttpdnsNWHTTPClientTestBase
@end
@implementation HttpdnsNWHTTPClient_PoolManagementTests
#pragma mark - K. <EFBFBD><EFBFBD>?
// K.1 HTTPS 使<EFBFBD><EFBFBD>?
- (void)testMultiPort_DifferentHTTPSPorts_SeparatePoolKeys {
XCTestExpectation *expectation = [self expectationWithDescription:@"Different ports use different pools"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 11443
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"Port11443"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1, @"First request to port 11443 should succeed");
XCTAssertEqual(response1.statusCode, 200);
// 11444 11443
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Port11444"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2, @"First request to port 11444 should succeed");
XCTAssertEqual(response2.statusCode, 200);
// 11443<EFBFBD><EFBFBD>?
NSError *error3 = nil;
HttpdnsNWHTTPClientResponse *response3 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"Port11443Again"
timeout:15.0
error:&error3];
XCTAssertNotNil(response3, @"Second request to port 11443 should reuse connection");
XCTAssertEqual(response3.statusCode, 200);
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:50.0];
}
// K.2 HTTPS <EFBFBD><EFBFBD>?
- (void)testMultiPort_ConcurrentThreePorts_AllSucceed {
NSInteger requestsPerPort = 10;
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445];
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLock *successCountLock = [[NSLock alloc] init];
__block NSInteger successCount = 0;
for (NSNumber *port in ports) {
for (NSInteger i = 0; i < requestsPerPort; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Port %@ Request %ld", port, (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"ConcurrentMultiPort"
timeout:15.0
error:&error];
if (response && response.statusCode == 200) {
[successCountLock lock];
successCount++;
[successCountLock unlock];
}
[expectation fulfill];
});
}
}
[self waitForExpectations:expectations timeout:60.0];
//
XCTAssertEqual(successCount, ports.count * requestsPerPort, @"All 30 requests should succeed");
}
// K.3 <EFBFBD><EFBFBD>?
- (void)testMultiPort_SequentialPortSwitching_ConnectionReusePerPort {
XCTestExpectation *expectation = [self expectationWithDescription:@"Sequential port switching"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray<NSString *> *urls = @[
@"https://127.0.0.1:11443/get", // 访<EFBFBD><EFBFBD>?11443
@"https://127.0.0.1:11444/get", // 访<EFBFBD><EFBFBD>?11444
@"https://127.0.0.1:11445/get", // 访<EFBFBD><EFBFBD>?11445
@"https://127.0.0.1:11443/get", // 访<EFBFBD><EFBFBD>?11443<EFBFBD><EFBFBD>?
@"https://127.0.0.1:11444/get", // 访<EFBFBD><EFBFBD>?11444<EFBFBD><EFBFBD>?
];
NSMutableArray<NSNumber *> *responseTimes = [NSMutableArray array];
for (NSString *url in urls) {
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:url
userAgent:@"SwitchingPorts"
timeout:15.0
error:&error];
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - startTime;
XCTAssertNotNil(response, @"Request to %@ should succeed", url);
XCTAssertEqual(response.statusCode, 200);
[responseTimes addObject:@(elapsed)];
}
//
XCTAssertEqual(responseTimes.count, urls.count);
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:80.0];
}
// K.4
- (void)testMultiPort_PerPortPoolLimit_IndependentPools {
XCTestExpectation *expectation = [self expectationWithDescription:@"Per-port pool limits"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// <EFBFBD><EFBFBD>?11443 <EFBFBD><EFBFBD>?10 <EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 10; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"Pool11443"
timeout:15.0
error:&error];
XCTAssertTrue(response != nil || error != nil);
}
// <EFBFBD><EFBFBD>?11444 <EFBFBD><EFBFBD>?10
for (NSInteger i = 0; i < 10; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Pool11444"
timeout:15.0
error:&error];
XCTAssertTrue(response != nil || error != nil);
}
//
[NSThread sleepForTimeInterval:1.0];
// <EFBFBD><EFBFBD>?
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"Verify11443"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1, @"Port 11443 should still work after heavy usage");
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Verify11444"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2, @"Port 11444 should still work after heavy usage");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:150.0];
}
// K.5 访
- (void)testMultiPort_InterleavedRequests_AllPortsAccessible {
XCTestExpectation *expectation = [self expectationWithDescription:@"Interleaved multi-port requests"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445, @11446];
NSInteger totalRequests = 20;
NSInteger successCount = 0;
// 访<EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < totalRequests; i++) {
NSNumber *port = ports[i % ports.count];
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"Interleaved"
timeout:15.0
error:&error];
if (response && response.statusCode == 200) {
successCount++;
}
}
XCTAssertEqual(successCount, totalRequests, @"All interleaved requests should succeed");
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:120.0];
}
#pragma mark - L.
// L.1 <EFBFBD><EFBFBD>?
- (void)testPoolExhaustion_FourPortsSimultaneous_AllSucceed {
NSInteger requestsPerPort = 10;
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445, @11446];
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLock *successCountLock = [[NSLock alloc] init];
__block NSInteger successCount = 0;
// <EFBFBD><EFBFBD>?4 10 <EFBFBD><EFBFBD>?40
for (NSNumber *port in ports) {
for (NSInteger i = 0; i < requestsPerPort; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Port %@ Request %ld", port, (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"FourPortLoad"
timeout:15.0
error:&error];
if (response && response.statusCode == 200) {
[successCountLock lock];
successCount++;
[successCountLock unlock];
}
[expectation fulfill];
});
}
}
[self waitForExpectations:expectations timeout:80.0];
// <EFBFBD><EFBFBD>?> 95%
XCTAssertGreaterThan(successCount, 38, @"At least 95%% of 40 requests should succeed");
}
// L.2 <EFBFBD><EFBFBD>?
- (void)testPoolExhaustion_SinglePortExhausted_OthersUnaffected {
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
__block NSInteger port11444SuccessCount = 0;
NSLock *countLock = [[NSLock alloc] init];
// <EFBFBD><EFBFBD>?11443 20 <EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 20; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Exhaust11443 %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
[self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"Exhaust11443"
timeout:15.0
error:&error];
[expectation fulfill];
});
}
// <EFBFBD><EFBFBD>?11444 5 11443 <EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 5; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Port11444 %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Independent11444"
timeout:15.0
error:&error];
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - startTime;
if (response && response.statusCode == 200) {
[countLock lock];
port11444SuccessCount++;
[countLock unlock];
}
// 11443
XCTAssertLessThan(elapsed, 10.0, @"Port 11444 should not be delayed by port 11443 load");
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:60.0];
// 11444
XCTAssertEqual(port11444SuccessCount, 5, @"All port 11444 requests should succeed despite 11443 load");
}
// L.3 使<EFBFBD><EFBFBD>?
- (void)testPoolExhaustion_MultiPortCleanup_ExpiredConnectionsPruned {
if (getenv("SKIP_SLOW_TESTS")) {
return;
}
XCTestExpectation *expectation = [self expectationWithDescription:@"Multi-port connection cleanup"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445];
// <EFBFBD><EFBFBD>?
for (NSNumber *port in ports) {
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"Initial"
timeout:15.0
error:&error];
XCTAssertTrue(response != nil || error != nil);
}
// 30 <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:31.0];
//
for (NSNumber *port in ports) {
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
userAgent:@"AfterExpiry"
timeout:15.0
error:&error];
XCTAssertTrue(response != nil || error != nil, @"Requests should succeed after expiry");
}
[expectation fulfill];
});
[self waitForExpectations:@[expectation] timeout:80.0];
}
#pragma mark - O. 使<EFBFBD><EFBFBD>?API<EFBFBD><EFBFBD>?
// O.1 <EFBFBD><EFBFBD>?- <EFBFBD><EFBFBD>?
- (void)testPoolVerification_ComprehensiveCheck_AllAspectsVerified {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// <EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Pool should be empty initially");
XCTAssertEqual([self.client totalConnectionCount], 0,
@"Total connections should be 0 initially");
XCTAssertEqual(self.client.connectionCreationCount, 0,
@"Creation count should be 0 initially");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"Reuse count should be 0 initially");
// <EFBFBD><EFBFBD>?5
for (NSInteger i = 0; i < 5; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"PoolVerificationTest"
timeout:15.0
error:&error];
XCTAssertNotNil(response, @"Request %ld should succeed", (long)i);
XCTAssertEqual(response.statusCode, 200, @"Request %ld should return 200", (long)i);
}
// <EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
@"Should have exactly 1 connection in pool for key: %@", poolKey);
XCTAssertEqual([self.client totalConnectionCount], 1,
@"Total connection count should be 1");
//
XCTAssertEqual(self.client.connectionCreationCount, 1,
@"Should create only 1 connection");
XCTAssertEqual(self.client.connectionReuseCount, 4,
@"Should reuse connection 4 times");
// <EFBFBD><EFBFBD>?
CGFloat reuseRate = (CGFloat)self.client.connectionReuseCount /
(self.client.connectionCreationCount + self.client.connectionReuseCount);
XCTAssertGreaterThanOrEqual(reuseRate, 0.8,
@"Reuse rate should be at least 80%% (actual: %.1f%%)", reuseRate * 100);
// pool keys
NSArray<NSString *> *allKeys = [self.client allConnectionPoolKeys];
XCTAssertEqual(allKeys.count, 1, @"Should have exactly 1 pool key");
XCTAssertTrue([allKeys containsObject:poolKey], @"Should contain the expected pool key");
}
// O.2
- (void)testPoolVerification_MultiPort_IndependentPools {
[self.client resetPoolStatistics];
NSString *key11443 = @"127.0.0.1:11443:tls";
NSString *key11444 = @"127.0.0.1:11444:tls";
// <EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:key11443], 0);
XCTAssertEqual([self.client connectionPoolCountForKey:key11444], 0);
// <EFBFBD><EFBFBD>?11443 <EFBFBD><EFBFBD>?3 <EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 3; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"Port11443"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
}
// 11443 <EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:key11443], 1,
@"Port 11443 should have 1 connection");
XCTAssertEqual([self.client connectionPoolCountForKey:key11444], 0,
@"Port 11444 should still be empty");
// <EFBFBD><EFBFBD>?11444 <EFBFBD><EFBFBD>?3 <EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 3; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
userAgent:@"Port11444"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
}
//
XCTAssertEqual([self.client connectionPoolCountForKey:key11443], 1,
@"Port 11443 should still have 1 connection");
XCTAssertEqual([self.client connectionPoolCountForKey:key11444], 1,
@"Port 11444 should now have 1 connection");
XCTAssertEqual([self.client totalConnectionCount], 2,
@"Total should be 2 connections (one per port)");
// 2 <EFBFBD><EFBFBD>?4 <EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionCreationCount, 2,
@"Should create 2 connections (one per port)");
XCTAssertEqual(self.client.connectionReuseCount, 4,
@"Should reuse connections 4 times total");
// pool keys
NSArray<NSString *> *allKeys = [self.client allConnectionPoolKeys];
XCTAssertEqual(allKeys.count, 2, @"Should have 2 pool keys");
XCTAssertTrue([allKeys containsObject:key11443], @"Should contain key for port 11443");
XCTAssertTrue([allKeys containsObject:key11444], @"Should contain key for port 11444");
}
// O.3 <EFBFBD><EFBFBD>?
- (void)testPoolVerification_PoolCapacity_MaxFourConnections {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// <EFBFBD><EFBFBD>?10 <EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 10; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"CapacityTest"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
}
//
[NSThread sleepForTimeInterval:1.0];
// 4kHttpdnsNWHTTPClientMaxIdleConnectionsPerKey<EFBFBD><EFBFBD>?
NSUInteger poolSize = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolSize, 4,
@"Pool size should not exceed 4 (actual: %lu)", (unsigned long)poolSize);
// <EFBFBD><EFBFBD>?1 <EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionCreationCount, 1,
@"Should create only 1 connection for sequential requests");
XCTAssertEqual(self.client.connectionReuseCount, 9,
@"Should reuse connection 9 times");
}
#pragma mark - S.
// S.1 <EFBFBD><EFBFBD>?- <EFBFBD><EFBFBD>?
- (void)testIdleTimeout_MixedExpiredValid_SelectivePruning {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// <EFBFBD><EFBFBD>?
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"ConnectionA"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
XCTAssertEqual(response1.statusCode, 200);
//
[NSThread sleepForTimeInterval:0.5];
// 使 DEBUG API A <EFBFBD><EFBFBD>?5 <EFBFBD><EFBFBD>?
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1, @"Should have 1 connection in pool");
HttpdnsNWReusableConnection *connectionA = connections.firstObject;
NSDate *expiredDate = [NSDate dateWithTimeIntervalSinceNow:-35.0];
[connectionA debugSetLastUsedDate:expiredDate];
// <EFBFBD><EFBFBD>?
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"ConnectionB"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
//
[NSThread sleepForTimeInterval:0.5];
// 1 connectionA connectionB <EFBFBD><EFBFBD>?
connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1,
@"Should have only 1 connection (expired A removed, valid B kept)");
// <EFBFBD><EFBFBD>?connectionB
[self.client resetPoolStatistics];
NSError *error3 = nil;
HttpdnsNWHTTPClientResponse *response3 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"ReuseB"
timeout:15.0
error:&error3];
XCTAssertNotNil(response3);
// connectionB<EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionCreationCount, 0,
@"Should not create new connection (reuse existing valid connection)");
XCTAssertEqual(self.client.connectionReuseCount, 1,
@"Should reuse the valid connection B");
}
// S.2 In-Use - 使
- (void)testIdleTimeout_InUseProtection_ActiveConnectionNotPruned {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// <EFBFBD><EFBFBD>?
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Initial"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
[NSThread sleepForTimeInterval:0.5];
// <EFBFBD><EFBFBD>?inUse=YES
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1);
HttpdnsNWReusableConnection *conn = connections.firstObject;
// <EFBFBD><EFBFBD>?60 <EFBFBD><EFBFBD>?30
NSDate *veryOldDate = [NSDate dateWithTimeIntervalSinceNow:-60.0];
[conn debugSetLastUsedDate:veryOldDate];
// 使<EFBFBD><EFBFBD>?
[conn debugSetInUse:YES];
//
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSInteger connectionsBefore = 0;
__block NSInteger connectionsAfter = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
connectionsBefore = [self.client totalConnectionCount];
// pruneConnectionPool<EFBFBD><EFBFBD>?
NSError *error2 = nil;
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"TriggerPrune"
timeout:15.0
error:&error2];
[NSThread sleepForTimeInterval:0.5];
connectionsAfter = [self.client totalConnectionCount];
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_SEC));
// <EFBFBD><EFBFBD>?inUse <EFBFBD><EFBFBD>?
[conn debugSetInUse:NO];
// inUse=YES <EFBFBD><EFBFBD>?
// connectionsBefore = 1 (<EFBFBD><EFBFBD>?, connectionsAfter = 2 (<EFBFBD><EFBFBD>?+ <EFBFBD><EFBFBD>?
XCTAssertEqual(connectionsBefore, 1,
@"Should have 1 connection before (in-use protected)");
XCTAssertEqual(connectionsAfter, 2,
@"Should have 2 connections after (in-use connection NOT pruned, new connection added)");
}
// S.3 <EFBFBD><EFBFBD>?-
- (void)testIdleTimeout_AllExpired_BulkPruning {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 4
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSInteger i = 0; i < 4; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"FillPool"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:30.0];
// <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:1.0];
// <EFBFBD><EFBFBD>?
NSUInteger poolSizeBefore = [self.client connectionPoolCountForKey:poolKey];
XCTAssertGreaterThan(poolSizeBefore, 0, @"Pool should have connections");
// <EFBFBD><EFBFBD>?1 <EFBFBD><EFBFBD>?
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
NSDate *expiredDate = [NSDate dateWithTimeIntervalSinceNow:-31.0];
for (HttpdnsNWReusableConnection *conn in connections) {
[conn debugSetLastUsedDate:expiredDate];
}
// <EFBFBD><EFBFBD>?
NSError *errorNew = nil;
HttpdnsNWHTTPClientResponse *responseNew = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"AfterBulkExpiry"
timeout:15.0
error:&errorNew];
XCTAssertNotNil(responseNew, @"Request should succeed after bulk pruning");
XCTAssertEqual(responseNew.statusCode, 200);
//
[NSThread sleepForTimeInterval:0.5];
//
NSUInteger poolSizeAfter = [self.client connectionPoolCountForKey:poolKey];
XCTAssertEqual(poolSizeAfter, 1,
@"Pool should have only 1 connection (new one after bulk pruning)");
}
// S.4 <EFBFBD><EFBFBD>?
- (void)testIdleTimeout_PoolStateAfterExpiry_DirectVerification {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
//
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"CreateConnection"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
[NSThread sleepForTimeInterval:0.5];
// <EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
@"Pool should have 1 connection");
// <EFBFBD><EFBFBD>?
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
HttpdnsNWReusableConnection *conn = connections.firstObject;
[conn debugSetLastUsedDate:[NSDate dateWithTimeIntervalSinceNow:-31.0]];
//
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"TriggerPrune"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
[NSThread sleepForTimeInterval:0.5];
// <EFBFBD><EFBFBD>?
NSUInteger poolSizeAfter = [self.client connectionPoolCountForKey:poolKey];
XCTAssertEqual(poolSizeAfter, 1,
@"Pool should have 1 connection (expired removed, new added)");
//
XCTAssertGreaterThanOrEqual(self.client.connectionCreationCount, 1,
@"Should have created at least 1 new connection");
}
// S.5 <EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?
- (void)testIdleTimeout_FastExpiry_NoWaiting {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
//
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"FastTest1"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
XCTAssertEqual(response1.statusCode, 200);
XCTAssertEqual(self.client.connectionCreationCount, 1, @"Should create 1 connection");
[NSThread sleepForTimeInterval:0.5];
// 使 DEBUG 31 <EFBFBD><EFBFBD>?
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1);
HttpdnsNWReusableConnection *conn = connections.firstObject;
NSDate *expiredDate = [NSDate dateWithTimeIntervalSinceNow:-31.0];
[conn debugSetLastUsedDate:expiredDate];
//
[self.client resetPoolStatistics];
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"FastTest2"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
XCTAssertEqual(response2.statusCode, 200);
//
XCTAssertEqual(self.client.connectionCreationCount, 1,
@"Should create new connection (expired connection not reused)");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"Should not reuse expired connection");
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - startTime;
// < 5 30+ <EFBFBD><EFBFBD>?
XCTAssertLessThan(elapsed, 5.0,
@"Fast expiry test should complete quickly (%.1fs) without 30s wait", elapsed);
}
@end

View File

@@ -0,0 +1,591 @@
//
// HttpdnsNWHTTPClient_StateMachineTests.m
// TrustHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 trustapp.com. All rights reserved.
//
// - <EFBFBD><EFBFBD>?(Q) <EFBFBD><EFBFBD>?
// <EFBFBD><EFBFBD>?7 Q:17<EFBFBD><EFBFBD>?
//
#import "HttpdnsNWHTTPClientTestBase.h"
@interface HttpdnsNWHTTPClient_StateMachineTests : HttpdnsNWHTTPClientTestBase
@end
@implementation HttpdnsNWHTTPClient_StateMachineTests
#pragma mark - Q. <EFBFBD><EFBFBD>?
// Q1.1 LRU
- (void)testStateMachine_PoolOverflowLRU_RemovesOldestByLastUsedDate {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// <EFBFBD><EFBFBD>?
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 5<EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 5; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
// 使 /delay/2
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/2"
userAgent:[NSString stringWithFormat:@"Request%ld", (long)i]
timeout:15.0
error:&error];
[expectation fulfill];
});
[NSThread sleepForTimeInterval:0.05]; // <EFBFBD><EFBFBD>?
}
[self waitForExpectations:expectations timeout:20.0];
// <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:1.0];
// <EFBFBD><EFBFBD>?4LRU<EFBFBD><EFBFBD>?
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolCount, 4,
@"Pool should enforce max 4 connections (LRU)");
//
XCTAssertGreaterThanOrEqual(self.client.connectionCreationCount, 3,
@"Should create multiple concurrent connections");
}
// Q2.1
- (void)testAbnormal_RapidSequentialRequests_NoDuplicates {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// <EFBFBD><EFBFBD>?0
for (NSInteger i = 0; i < 10; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"RapidTest"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
}
//
[NSThread sleepForTimeInterval:1.0];
// <EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolCount, 1,
@"Pool should have at most 1 connection (rapid sequential reuse)");
// 1<EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionCreationCount, 1,
@"Should create only 1 connection for sequential requests");
}
// Q2.2
- (void)testAbnormal_DifferentPorts_IsolatedPools {
[self.client resetPoolStatistics];
NSString *poolKey11080 = @"127.0.0.1:11080:tcp";
NSString *poolKey11443 = @"127.0.0.1:11443:tls";
// <EFBFBD><EFBFBD>?1080
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Port11080"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
// <EFBFBD><EFBFBD>?1443
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"Port11443"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
//
[NSThread sleepForTimeInterval:0.5];
// <EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey11080], 1,
@"Port 11080 pool should have 1 connection");
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey11443], 1,
@"Port 11443 pool should have 1 connection");
// 2
XCTAssertEqual([self.client totalConnectionCount], 2,
@"Total should be 2 (one per pool)");
}
// Q3.1
- (void)testInvariant_PoolSize_NeverExceedsLimit {
[self.client resetPoolStatistics];
// <EFBFBD><EFBFBD>?0
for (NSInteger i = 0; i < 20; i++) {
NSError *error = nil;
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"InvariantTest"
timeout:15.0
error:&error];
}
// <EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:1.5];
// 4
NSArray<NSString *> *allKeys = [self.client allConnectionPoolKeys];
for (NSString *key in allKeys) {
NSUInteger poolCount = [self.client connectionPoolCountForKey:key];
XCTAssertLessThanOrEqual(poolCount, 4,
@"Pool %@ size should never exceed 4 (actual: %lu)",
key, (unsigned long)poolCount);
}
// 4<EFBFBD><EFBFBD>?
XCTAssertLessThanOrEqual([self.client totalConnectionCount], 4,
@"Total connections should not exceed 4");
}
// Q3.3
- (void)testInvariant_NoDuplicates_ConcurrentRequests {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 15<EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 15; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"ConcurrentTest"
timeout:15.0
error:&error];
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:30.0];
//
[NSThread sleepForTimeInterval:1.0];
// <EFBFBD><EFBFBD>?4<EFBFBD><EFBFBD>?
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolCount, 4,
@"Pool should not have duplicates (max 4 connections)");
// <EFBFBD><EFBFBD>?5<EFBFBD><EFBFBD>?
XCTAssertLessThanOrEqual(self.client.connectionCreationCount, 15,
@"Should not create excessive connections");
}
// Q4.1 <EFBFBD><EFBFBD>?0
- (void)testBoundary_Exactly30Seconds_ConnectionExpired {
if (getenv("SKIP_SLOW_TESTS")) {
return;
}
[self.client resetPoolStatistics];
// <EFBFBD><EFBFBD>?
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"InitialRequest"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
// 30.530
[NSThread sleepForTimeInterval:30.5];
// <EFBFBD><EFBFBD>?
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"AfterExpiry"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
// 2<EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionCreationCount, 2,
@"Should create 2 connections (first expired after 30s)");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"Should not reuse expired connection");
}
// Q4.2 <EFBFBD><EFBFBD>?9<EFBFBD><EFBFBD>?
- (void)testBoundary_Under30Seconds_ConnectionNotExpired {
[self.client resetPoolStatistics];
// <EFBFBD><EFBFBD>?
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"InitialRequest"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
// 2930
[NSThread sleepForTimeInterval:29.0];
//
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"BeforeExpiry"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
// <EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionCreationCount, 1,
@"Should create only 1 connection (reused within 30s)");
XCTAssertEqual(self.client.connectionReuseCount, 1,
@"Should reuse connection within 30s");
}
// Q4.3 <EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
- (void)testBoundary_ExactlyFourConnections_AllKept {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 4使<EFBFBD><EFBFBD>?
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSInteger i = 0; i < 4; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:
[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
// 使 /delay/2 <EFBFBD><EFBFBD>?
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/2"
userAgent:[NSString stringWithFormat:@"Request%ld", (long)i]
timeout:15.0
error:&error];
[expectation fulfill];
});
[NSThread sleepForTimeInterval:0.05]; // <EFBFBD><EFBFBD>?
}
[self waitForExpectations:expectations timeout:20.0];
//
[NSThread sleepForTimeInterval:1.0];
// <EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
XCTAssertEqual(poolCount, 4,
@"Pool should keep all 4 connections (not exceeding limit)");
// <EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionCreationCount, 4,
@"Should create exactly 4 connections");
}
// Q1.2 <EFBFBD><EFBFBD>?
- (void)testStateMachine_NormalSequence_StateTransitionsCorrect {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// <EFBFBD><EFBFBD>?使<EFBFBD><EFBFBD>?(CREATING <EFBFBD><EFBFBD>?IN_USE <EFBFBD><EFBFBD>?IDLE)
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"StateTest"
timeout:15.0
error:nil];
XCTAssertNotNil(response1, @"First request should succeed");
[NSThread sleepForTimeInterval:1.0]; //
// 1<EFBFBD><EFBFBD>?
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
@"Connection should be in pool");
// <EFBFBD><EFBFBD>? (IDLE <EFBFBD><EFBFBD>?IN_USE <EFBFBD><EFBFBD>?IDLE)
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"StateTest"
timeout:15.0
error:nil];
XCTAssertNotNil(response2, @"Second request should reuse connection");
// <EFBFBD><EFBFBD>?
XCTAssertEqual(self.client.connectionReuseCount, 1,
@"Should have reused connection once");
}
// Q1.3 inUse
- (void)testStateMachine_InUseFlag_CorrectlyMaintained {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// <EFBFBD><EFBFBD>?
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"InUseTest"
timeout:15.0
error:nil];
[NSThread sleepForTimeInterval:1.0]; //
//
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1, @"Should have 1 connection in pool");
// inUse NO
for (HttpdnsNWReusableConnection *conn in connections) {
XCTAssertFalse(conn.inUse, @"Connection in pool should not be marked as inUse");
}
}
// Q2.3 Nil lastUsedDate
- (void)testAbnormal_NilLastUsedDate_HandledGracefully {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
//
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"NilDateTest"
timeout:15.0
error:nil];
[NSThread sleepForTimeInterval:1.0];
// <EFBFBD><EFBFBD>?lastUsedDate <EFBFBD><EFBFBD>?nil
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1, @"Should have connection");
HttpdnsNWReusableConnection *conn = connections.firstObject;
[conn debugSetLastUsedDate:nil];
// <EFBFBD><EFBFBD>?prune使 distantPast nil<EFBFBD><EFBFBD>?
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"NilDateTest"
timeout:15.0
error:nil];
// <EFBFBD><EFBFBD>?
XCTAssertNotNil(response, @"Should handle nil lastUsedDate gracefully");
}
// Q3.2
- (void)testInvariant_NoInvalidatedInPool {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// <EFBFBD><EFBFBD>?
for (NSInteger i = 0; i < 3; i++) {
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"InvariantTest"
timeout:15.0
error:nil];
}
// 1<EFBFBD><EFBFBD>?
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"TimeoutTest"
timeout:0.5
error:nil];
[NSThread sleepForTimeInterval:2.0];
// <EFBFBD><EFBFBD>?
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
//
for (HttpdnsNWReusableConnection *conn in connections) {
XCTAssertFalse(conn.isInvalidated, @"Pool should not contain invalidated connections");
}
}
// Q3.4 lastUsedDate <EFBFBD><EFBFBD>?
- (void)testInvariant_LastUsedDate_Monotonic {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// <EFBFBD><EFBFBD>?使<EFBFBD><EFBFBD>?
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"MonotonicTest"
timeout:15.0
error:nil];
[NSThread sleepForTimeInterval:1.0];
NSArray<HttpdnsNWReusableConnection *> *connections1 = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections1.count, 1, @"Should have connection");
NSDate *date1 = connections1.firstObject.lastUsedDate;
XCTAssertNotNil(date1, @"lastUsedDate should be set");
// 1<EFBFBD><EFBFBD>?
[NSThread sleepForTimeInterval:1.0];
// <EFBFBD><EFBFBD>?使
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"MonotonicTest"
timeout:15.0
error:nil];
[NSThread sleepForTimeInterval:1.0];
NSArray<HttpdnsNWReusableConnection *> *connections2 = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections2.count, 1, @"Should still have 1 connection");
NSDate *date2 = connections2.firstObject.lastUsedDate;
// lastUsedDate
XCTAssertTrue([date2 timeIntervalSinceDate:date1] > 0,
@"lastUsedDate should increase after reuse");
}
// Q5.1 +<EFBFBD><EFBFBD>?
- (void)testCompound_TimeoutDuringPoolOverflow_Handled {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// <EFBFBD><EFBFBD>?
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSInteger i = 0; i < 4; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:
[NSString stringWithFormat:@"Fill pool %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/2"
userAgent:@"CompoundTest"
timeout:15.0
error:nil];
[expectation fulfill];
});
[NSThread sleepForTimeInterval:0.05];
}
[self waitForExpectations:expectations timeout:20.0];
[NSThread sleepForTimeInterval:1.0];
NSUInteger poolCountBefore = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolCountBefore, 4, @"Pool should have <20><>? connections");
// <EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"TimeoutRequest"
timeout:0.5
error:&error];
XCTAssertNil(response, @"Timeout request should return nil");
XCTAssertNotNil(error, @"Should have error");
[NSThread sleepForTimeInterval:1.0];
// <EFBFBD><EFBFBD>?
NSUInteger poolCountAfter = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolCountAfter, 4, @"Timed-out connection should not be added to pool");
}
// Q2.4
- (void)testAbnormal_OpenFailure_NotAddedToPool {
[self.client resetPoolStatistics];
//
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:99999/get"
userAgent:@"FailureTest"
timeout:2.0
error:&error];
// <EFBFBD><EFBFBD>?
XCTAssertNil(response, @"Should fail to connect to invalid port");
// <EFBFBD><EFBFBD>?
XCTAssertEqual([self.client totalConnectionCount], 0,
@"Failed connection should not be added to pool");
}
// Q2.5 invalidate <EFBFBD><EFBFBD>?
- (void)testAbnormal_MultipleInvalidate_Idempotent {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
//
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"InvalidateTest"
timeout:15.0
error:nil];
[NSThread sleepForTimeInterval:1.0];
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1, @"Should have connection");
HttpdnsNWReusableConnection *conn = connections.firstObject;
// invalidate
[conn debugInvalidate];
[conn debugInvalidate];
[conn debugInvalidate];
//
XCTAssertTrue(conn.isInvalidated, @"Connection should be invalidated");
}
// Q5.2 dequeue <EFBFBD><EFBFBD>?
- (void)testCompound_ConcurrentDequeueDuringPrune_Safe {
[self.client resetPoolStatistics];
// <EFBFBD><EFBFBD>?
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"RaceTest"
timeout:15.0
error:nil];
[self.client performRequestWithURLString:@"http://127.0.0.1:11443/get"
userAgent:@"RaceTest"
timeout:15.0
error:nil];
[NSThread sleepForTimeInterval:1.0];
// 30
[NSThread sleepForTimeInterval:30.5];
// <EFBFBD><EFBFBD>?dequeue prune<EFBFBD><EFBFBD>?
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Race1"
timeout:15.0
error:nil];
});
dispatch_group_async(group, queue, ^{
[self.client performRequestWithURLString:@"http://127.0.0.1:11443/get"
userAgent:@"Race2"
timeout:15.0
error:nil];
});
//
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_SEC));
//
NSUInteger totalCount = [self.client totalConnectionCount];
XCTAssertLessThanOrEqual(totalCount, 4, @"Pool should remain stable after concurrent prune");
}
@end

View File

@@ -0,0 +1,253 @@
# HTTP Mock Server for Integration Tests
本目录包含用<EFBFBD><EFBFBD>?`HttpdnsNWHTTPClient` 集成测试<E6B58B><E8AF95>?HTTP/HTTPS mock server用于替<E4BA8E><E69BBF>?httpbin.org<72><67>?
---
## 为什么需<E4B988><E99C80>?Mock Server<65><72>?
1. **可靠<E58FAF><E99DA0>?*: httpbin.org 在高并发测试下表现不稳定经常返回非预期<E9A284><E69C9F>?HTTP 状态码(如 429 Too Many Requests<74><73>?
2. **速度**: 本地服务器响应更快,缩短测试执行时间
3. **离线测试**: 无需网络连接即可运行集成测试
4. **可控<E58FAF><E68EA7>?*: 完全掌控测试环境,便于调试和复现问题
---
## 快速开<E9809F><E5BC80>?
### 1. 启动 Mock Server
```bash
# 进入测试目录
cd TrustHttpDNSTests/Network
# 启动服务器(无需 sudo 权限使用非特权端口<E7ABAF><E58FA3>?
python3 mock_server.py
```
**注意**:
- **无需 root 权限**(使用非特权端口 11080/11443-11446<34><36>?
- 首次运行会自动生成自签名证书 (`server.pem`)
- <20><>?`Ctrl+C` 停止服务<E69C8D><E58AA1>?
### 2. 运行集成测试
在另一个终端窗<EFBFBD><EFBFBD>?
```bash
cd ~/Project/iOS/Trust-ios-sdk-httpdns
# 运行所有集成测<E68890><E6B58B>?
xcodebuild test \
-workspace TrustHttpDNS.xcworkspace \
-scheme TrustHttpDNSTests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:TrustHttpDNSTests/HttpdnsNWHTTPClientIntegrationTests
# 运行单个测试
xcodebuild test \
-workspace TrustHttpDNS.xcworkspace \
-scheme TrustHttpDNSTests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:TrustHttpDNSTests/HttpdnsNWHTTPClientIntegrationTests/testConcurrency_ParallelRequestsSameHost_AllSucceed
```
---
## 支持<E694AF><E68C81>?Endpoints
Mock server 实现了以<E4BA86><E4BBA5>?httpbin.org 兼容<E585BC><E5AEB9>?endpoints:
| Endpoint | 功能 | 示例 |
|----------|------|------|
| `GET /get` | 返回请求信息headers, args, origin<69><6E>?| `http://127.0.0.1:11080/get` |
| `GET /status/{code}` | 返回指定状态码<E68081><E7A081>?00-599<39><39>?| `http://127.0.0.1:11080/status/404` |
| `GET /stream-bytes/{n}` | 返回 chunked 编码<E7BC96><E7A081>?N 字节数据 | `http://127.0.0.1:11080/stream-bytes/1024` |
| `GET /delay/{seconds}` | 延迟指定秒数后返回<EFBC88><E69C80>?0秒 | `http://127.0.0.1:11080/delay/5` |
| `GET /headers` | 返回所有请求头<E6B182><E5A4B4>?| `http://127.0.0.1:11080/headers` |
| `GET /uuid` | 返回随机 UUID | `http://127.0.0.1:11080/uuid` |
| `GET /user-agent` | 返回 User-Agent 头部 | `http://127.0.0.1:11080/user-agent` |
**端口配置**:
- **HTTP**: `127.0.0.1:11080`
- **HTTPS**: `127.0.0.1:11443`, `11444`, `11445`, `11446`<EFBFBD><EFBFBD>?个端口用于测试连接池隔离<E99A94><E7A6BB>?
<EFBFBD><EFBFBD>?endpoints <20><>?HTTP <20><>?HTTPS 端口上均可访问<E8AEBF><E997AE>?
---
## 实现细节
### 架构
- **HTTP 服务<E69C8D><E58AA1>?*: 监听 `127.0.0.1:11080`
- **HTTPS 服务<E69C8D><E58AA1>?*: 监听 `127.0.0.1:11443`, `11444`, `11445`, `11446`<EFBFBD><EFBFBD>?个端口,使用自签名证书)
- **多端口目<E58FA3><E79BAE>?*: 测试连接池的端口隔离机制确保不同端口使用独立的连接<E8BF9E><E68EA5>?
- **并发模型**: 多线程(`ThreadingMixIn`支持高并发请<EFBFBD><EFBFBD>?
### TLS 证书
- 自动生成自签名证书RSA 2048位有效<E69C89><E69588>?365 天)
- CN (Common Name): `localhost`
- 证书文件: `server.pem`同时包含密钥和证书<EFBFBD><EFBFBD>?
**重要**: 集成测试通过环境变量 `HTTPDNS_SKIP_TLS_VERIFY=1` 跳过 TLS 验证,这是安全的,因为:
1. 仅在测试环境生效
2. 不影响生产代<E4BAA7><E4BBA3>?
3. 连接限制为本<E4B8BA><E69CAC>?loopback (127.0.0.1)
### 响应格式
<EFBFBD><EFBFBD>?JSON 响应遵循 httpbin.org 格式<EFBC8C><E4BE8B>?
```json
{
"args": {},
"headers": {
"Host": "127.0.0.1",
"User-Agent": "HttpdnsNWHTTPClient/1.0"
},
"origin": "127.0.0.1",
"url": "GET /get"
}
```
Chunked 编码响应示例 (`/stream-bytes/10`):
```
HTTP/1.1 200 OK
Transfer-Encoding: chunked
a
XXXXXXXXXX
0
```
---
## 故障排除
### 端口已被占用
**错误信息**:
```
<EFBFBD><EFBFBD>?端口 80 已被占用,请关闭占用端口的进程或使用其他端口
```
**解决方法**:
1. 查找占用进程:
```bash
sudo lsof -i :80
sudo lsof -i :443
```
2. 终止占用进程:
```bash
sudo kill -9 <PID>
```
3. 或修<E68896><E4BFAE>?mock_server.py 使用其他端口:
```python
# 修改端口号同时需要更新测试代码中<E7A081><E4B8AD>?URL<52><4C>?
run_http_server(port=8080)
run_https_server(port=8443)
```
### 缺少 OpenSSL
**错误信息**:
```
<EFBFBD><EFBFBD>?未找<E69CAA><E689BE>?openssl 命令,请安装 OpenSSL
```
**解决方法**:
```bash
# macOS (通常已预<E5B7B2><E9A284>?
brew install openssl
# Ubuntu/Debian
sudo apt-get install openssl
# CentOS/RHEL
sudo yum install openssl
```
### 权限被拒<E8A2AB><E68B92>?
**错误信息**:
```
<EFBFBD><EFBFBD>?错误: 需<><E99C80>?root 权限以绑<E4BBA5><E7BB91>?80/443 端口
```
**解决方法**:
必须使用 `sudo` 运行:
```bash
sudo python3 mock_server.py
```
---
## 切换<E58887><E68DA2>?httpbin.org
如需使用真实<EFBFBD><EFBFBD>?httpbin.org 进行测试(例如验证兼容性):
1. 编辑 `HttpdnsNWHTTPClientIntegrationTests.m`
2. 将所<E5B086><E68980>?`127.0.0.1` 替换<E69BBF><E68DA2>?`httpbin.org`
3. 注释<E6B3A8><E9878A>?setUp/tearDown 中的环境变量设置
---
## 开发与扩展
### 添加<E6B7BB><E58AA0>?Endpoint
<EFBFBD><EFBFBD>?`mock_server.py` <20><>?`MockHTTPHandler.do_GET()` 方法中添<E4B8AD><E6B7BB>?
```python
def do_GET(self):
path = urlparse(self.path).path
if path == '/your-new-endpoint':
self._handle_your_endpoint()
# ... 其他 endpoints
def _handle_your_endpoint(self):
"""处理自定<E887AA><E5AE9A>?endpoint"""
data = {'custom': 'data'}
self._send_json(200, data)
```
### 调试模式
取消注释 `log_message` 方法以启用详细日<E7BB86><E697A5>?
```python
def log_message(self, format, *args):
print(f"[{self.address_string()}] {format % args}")
```
---
## 技术栈
- **Python 3.7+** (标准库,无需额外依赖)
- **http.server**: HTTP 服务器实<E599A8><E5AE9E>?
- **ssl**: TLS/SSL 支持
- **socketserver.ThreadingMixIn**: 多线程并<E7A88B><E5B9B6>?
---
## 安全注意事项
1. **仅用于测<E4BA8E><E6B58B>?*: 此服务器设计用于本地测试,不适合生产环境
2. **自签名证<E5908D><E8AF81>?*: HTTPS 使用不受信任的自签名证书
3. **无身份验<E4BBBD><E9AA8C>?*: 不实现任何身份验证机<E8AF81><E69CBA>?
4. **本地绑定**: 服务器仅绑定<E7BB91><E5AE9A>?`127.0.0.1`,不接受外部连接
---
**最后更<E5908E><E69BB4>?*: 2025-11-01
**维护<E7BBB4><E68AA4>?*: Claude Code

View File

@@ -0,0 +1,282 @@
# HttpdnsNWHTTPClient 测试套件
本目录包<EFBFBD><EFBFBD>?`HttpdnsNWHTTPClient` <20><>?`HttpdnsNWReusableConnection` 的完整测试套件<E5A597><E4BBB6>?
## 测试文件结构
```
TrustHttpDNSTests/Network/
├── HttpdnsNWHTTPClientTests.m # 主单元测试44个
├── HttpdnsNWHTTPClientIntegrationTests.m # 集成测试<E6B58B><E8AF95>?个)
├── HttpdnsNWHTTPClientTestHelper.h/m # 测试辅助工具<E5B7A5><E585B7>?
└── README.md # 本文<E69CAC><E69687>?
```
## 测试覆盖范围
### 单元测试 (HttpdnsNWHTTPClientTests.m)
#### A. HTTP 解析逻辑测试 (25<32><35>?
- **A1. Header 解析 (9<><39>?**
- 正常响应解析
- 多个头部字段
- 不完整数据处<E68DAE><E5A484>?
- 无效状态行
- 空格处理<E5A484><E79086>?trim
- 空值头<E580BC><E5A4B4>?
- 非数字状态码
- 状态码为零
- 无效头部<E5A4B4><E983A8>?
- **A2. Chunked 编码检<E7A081><E6A380>?(8<><38>?**
- 单个 chunk
- 多个 chunks
- 不完<E4B88D><E5AE8C>?chunk
- Chunk extension 支持
- 无效十六进制 size
- Chunk size 溢出
- 缺少 CRLF 终止<E7BB88><E6ADA2>?
- <20><>?trailers <20><>?chunked
- **A3. Chunked 解码 (2<><32>?**
- 多个 chunks 正确解码
- 无效格式返回 nil
- **A4. 完整响应解析 (6<><36>?**
- Content-Length 响应
- Chunked 编码响应
- <20><>?body
- Content-Length 不匹<E4B88D><E58CB9>?
- 空数据错<E68DAE><E99499>?
- 只有 headers <20><>?body
#### C. 请求构建测试 (7<><37>?
- 基本 GET 请求格式
- 查询参数处理
- User-Agent 头部
- HTTP 默认端口处理
- HTTPS 默认端口处理
- 非默认端口显<E58FA3><E698BE>?
- 固定头部验证
#### E. TLS 验证测试 (4个占位符)
- 有效证书返回 YES
- Proceed 结果返回 YES
- 无效证书返回 NO
- 指定域名使用 SSL Policy
*注TLS 测试需要真实的 SecTrustRef 或复<E68896><E5A48D>?mock当前为占位<E58DA0><E4BD8D>?
#### F. 边缘情况测试 (8<><38>?
- 超长 URL 处理
- <20><>?User-Agent
- 超大响应体5MB<4D><42>?
- Chunked 解码失败回退
- 连接<E8BF9E><E68EA5>?key - 不同 hosts
- 连接<E8BF9E><E68EA5>?key - 不同 ports
- 连接<E8BF9E><E68EA5>?key - HTTP vs HTTPS
### 集成测试 (HttpdnsNWHTTPClientIntegrationTests.m)
使用 httpbin.org 进行真实网络测试 (22<32><32>?<3F><>?
**G. 基础集成测试 (7<><37>?**
- HTTP GET 请求
- HTTPS GET 请求
- HTTP 404 响应
- 连接复用(两次请求)
- Chunked 响应处理
- 请求超时测试
- 自定义头部验<E983A8><E9AA8C>?
**H. 并发测试 (5<><35>?**
- 并发请求同一主机<E4B8BB><E69CBA>?0个线程
- 并发请求不同路径<E8B7AF><E5BE84>?个不同endpoint<6E><74>?
- 混合 HTTP + HTTPS 并发各5个线程
- 高负载压力测试50个并发请求
- 混合串行+并发模式
**I. 竞态条件测<E4BBB6><E6B58B>?(5<><35>?**
- 连接池容量测试超过4个连接上限
- 同时归还连接<E8BF9E><E68EA5>?个并发)
- 获取-归还-再获取竞<E58F96><E7AB9E>?
- 超时与活跃连接冲突需31秒可跳过
- 错误恢复后连接池健康状<E5BAB7><E78AB6>?
**J. 高级连接复用测试 (5<><35>?**
- 连接过期与清理31秒可跳过
- 连接池容量限制验证10个连续请求
- 不同路径复用连接<E8BF9E><E68EA5>?个不同路径)
- HTTP vs HTTPS 使用不同连接池key
- 长连接保持测试20个请求间<E6B182><E997B4>?秒,可跳过)
## 运行测试
### 运行所有单元测<E58583><E6B58B>?
```bash
xcodebuild test \
-workspace TrustHttpDNS.xcworkspace \
-scheme TrustHttpDNSTests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:TrustHttpDNSTests/HttpdnsNWHTTPClientTests
```
### 运行集成测试(需要网络)
```bash
xcodebuild test \
-workspace TrustHttpDNS.xcworkspace \
-scheme TrustHttpDNSTests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:TrustHttpDNSTests/HttpdnsNWHTTPClientIntegrationTests
```
### 运行单个测试
```bash
xcodebuild test \
-workspace TrustHttpDNS.xcworkspace \
-scheme TrustHttpDNSTests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:TrustHttpDNSTests/HttpdnsNWHTTPClientTests/testParseHTTPHeaders_ValidResponse_Success
```
## 测试辅助工具
### HttpdnsNWHTTPClientTestHelper
提供以下工具方法<EFBFBD><EFBFBD>?
#### HTTP 响应构<E5BA94><E69E84>?
```objc
// 构造标<E980A0><E6A087>?HTTP 响应
+ (NSData *)createHTTPResponseWithStatus:(NSInteger)statusCode
statusText:(NSString *)statusText
headers:(NSDictionary *)headers
body:(NSData *)body;
// 构<><E69E84>?chunked 响应
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
headers:(NSDictionary *)headers
chunks:(NSArray<NSData *> *)chunks;
```
#### Chunked 编码工具
```objc
+ (NSData *)encodeChunk:(NSData *)data;
+ (NSData *)encodeLastChunk;
```
#### 数据生成
```objc
+ (NSData *)randomDataWithSize:(NSUInteger)size;
+ (NSData *)jsonBodyWithDictionary:(NSDictionary *)dictionary;
```
## 测试统计
| 测试类别 | 测试数量 | 覆盖范围 |
|---------|---------|---------|
| HTTP 解析 | 25 | HTTP 头部、Chunked 编码、完整响<E695B4><E5938D>?|
| 请求构建 | 7 | URL 处理、头部生<E983A8><E7949F>?|
| TLS 验证 | 4 (占位<E58DA0><E4BD8D>? | 证书验证 |
| 边缘情况 | 8 | 异常输入、连接池 key |
| **单元测试合计** | **43** | - |
| 基础集成测试 (G) | 7 | 真实网络请求、基本场<E69CAC><E59CBA>?|
| 并发测试 (H) | 5 | 多线程并发、高负载 |
| 竞态条件测<E4BBB6><E6B58B>?(I) | 5 | 连接池竞态、错误恢<E8AFAF><E681A2>?|
| 连接复用测试 (J) | 5 | 连接过期、长连接、协议隔<E8AEAE><E99A94>?|
| 多端口连接隔<E68EA5><E99A94>?(K) | 5 | 不同端口独立连接<E8BF9E><E68EA5>?|
| 端口池耗尽测试 (L) | 3 | 多端口高负载、池隔离 |
| 边界条件验证 (M) | 4 | 端口迁移、高端口数量 |
| 并发多端口场<E58FA3><E59CBA>?(N) | 3 | 并行 keep-alive、轮询分<E8AFA2><E58886>?|
| **集成测试合计** | **37** | - |
| **总计** | **80** | - |
## 待实现测试(可选)
以下测试组涉及复杂的 Mock 场景,可根据需要添加:
### B. 连接池管理测<E79086><E6B58B>?(18<31><38>?
<EFBFBD><EFBFBD>?Mock `HttpdnsNWReusableConnection` 的完整生命周<E591BD><E591A8>?
### D. 完整流程测试 (13<31><33>?
<EFBFBD><EFBFBD>?Mock 连接池和网络层的集成场景
## Mock Server 使用
集成测试使用本地 mock server (127.0.0.1) 替代 httpbin.org提供稳定可靠的测试环境<E78EAF><E5A283>?
### 启动 Mock Server
```bash
cd TrustHttpDNSTests/Network
python3 mock_server.py
```
**注意**使用非特权端口<EFBFBD><EFBFBD>?1080/11443-11446无需 `sudo` 权限<E69D83><E99990>?
### 运行集成测试
在另一个终端窗口:
```bash
xcodebuild test \
-workspace TrustHttpDNS.xcworkspace \
-scheme TrustHttpDNSTests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:TrustHttpDNSTests/HttpdnsNWHTTPClientIntegrationTests
```
### Mock Server 特<><E789B9>?
- **HTTP**: 监听 `127.0.0.1:11080`
- **HTTPS**: 监听 `127.0.0.1:11443`, `11444`, `11445`, `11446` (自签名证<E5908D><E8AF81>?
- **多端口目<E58FA3><E79BAE>?*: 测试连接池的端口隔离机制
- **并发支持**: 多线程处理,适合并发测试
- **零延<E99BB6><E5BBB6>?*: 本地响应测试速度<E9809F><E5BAA6>?
详见 [MOCK_SERVER.md](./MOCK_SERVER.md)
## 注意事项
1. **集成测试依赖 Mock Server**`HttpdnsNWHTTPClientIntegrationTests` 使用本地 mock server (127.0.0.1)。测试前需先启<E58588><E590AF>?`mock_server.py`<EFBFBD><EFBFBD>?
2. **慢测试跳<E8AF95><E8B7B3>?*部分测试需要等<E8A681><E7AD89>?1秒测试连接过期可设置环境变<E5A283><E58F98>?`SKIP_SLOW_TESTS=1` 跳过这些测试<E6B58B><E8AF95>?
- `testRaceCondition_ExpiredConnectionPruning_CreatesNewConnection`
- `testConnectionReuse_Expiry31Seconds_NewConnectionCreated`
- `testConnectionReuse_TwentyRequestsOneSecondApart_ConnectionKeptAlive`
3. **并发测试容错**并发和压力测试允许部分失败<EFBFBD><EFBFBD>?H.4 要求80%成功率因为高负载下仍可能出现网络波动<E6B3A2><E58AA8>?
4. **TLS 测试占位<E58DA0><E4BD8D>?*E <20><>?TLS 测试需要真实的 `SecTrustRef` 或高<E68896><E9AB98>?mock 框架当前仅为占位符<E4BD8D><E7ACA6>?
5. **新文件添加到 Xcode**:创建的测试文件需要手动添加到 `TrustHttpDNSTests` target<65><74>?
6. **测试数据**使<EFBFBD><EFBFBD>?`HttpdnsNWHTTPClientTestHelper` 生成测试数据确保测试的可重复性<E5A48D><E680A7>?
## 文件依赖
测试文件依赖以下源文件:
- `HttpdnsNWHTTPClient.h/m` - 主要被测试类
- `HttpdnsNWHTTPClient_Internal.h` - 内部方法暴露(测试专用)
- `HttpdnsNWReusableConnection.h/m` - 连接管理
- `HttpdnsNWHTTPClientResponse` - 响应模型
## 贡献指南
添加新测试时请遵循<EFBFBD><EFBFBD>?
1. 命名规范:`test<Component>_<Scenario>_<ExpectedResult>`
2. 使用 `#pragma mark` 组织测试分组
3. 添加清晰的注释说明测试目<E8AF95><E79BAE>?
4. 验证测试覆盖率并更新本文<E69CAC><E69687>?
---
**最后更<E5908E><E69BB4>?*: 2025-11-01
**测试框架**: XCTest + OCMock
**维护<E7BBB4><E68AA4>?*: Claude Code
**更新日志**:
- 2025-11-01: 新增 15 个多端口连接复用测试K、L、M、N组测试总数增至 37 <20><>?
- 2025-11-01: Mock server 新增 3 <20><>?HTTPS 端口<E7ABAF><E58FA3>?1444-11446用于测试连接池隔离
- 2025-11-01: 新增本地 mock server<EFBC8C><E69BBF>?httpbin.org提供稳定测试环<E8AF95><E78EAF>?
- 2025-11-01: 新增 15 个并发、竞态和连接复用集成测试H、I、J组

View File

@@ -0,0 +1,514 @@
# 连接池状态机验证分析
## 用户核心问题
**"have we verified that the state machine of connection in the pool has been correctly maintained? what abnormal situation have we designed? ultrathink"**
---
## 连接状态机定义
### 状态属<E68081><E5B19E>?
**HttpdnsNWReusableConnection.h:9-11**
```objc
@property (nonatomic, strong) NSDate *lastUsedDate; // 最后使用时<E794A8><E697B6>?
@property (nonatomic, assign) BOOL inUse; // 是否正在被使<E8A2AB><E4BDBF>?
@property (nonatomic, assign, getter=isInvalidated, readonly) BOOL invalidated; // 是否已失<E5B7B2><E5A4B1>?
```
### 状态枚<E68081><E69E9A>?
虽然没有显式枚举,但连接实际存在以下逻辑状态:
| 状<><E78AB6>?| `inUse` | `invalidated` | `pool` | 描述 |
|------|---------|---------------|--------|------|
| **CREATING** | - | NO | <20><>?| 新创建,尚未打开 |
| **IN_USE** | YES | NO | <20><>?| 已借出正在使<E59CA8><E4BDBF>?|
| **IDLE** | NO | NO | <20><>?| 空闲,可复用 |
| **EXPIRED** | NO | NO | <20><>?| 空闲<E7A9BA><E997B2>?0秒待清<E5BE85><E6B885>?|
| **INVALIDATED** | - | YES | <20><>?| 已失效已移<E5B7B2><E7A7BB>?|
---
## 状态转换图
```
┌─────────<E29480><E29480>?
│CREATING <20><>?(new connection)
└────┬────<E29480><E29480>?
<20><>?openWithTimeout success
<20><>?
┌─────────<E29480><E29480>?
<20><>?IN_USE <20><>?(inUse=YES, in pool)
└────┬────<E29480><E29480>?
<20><>?
├──success──<E29480><E29480>?returnConnection(shouldClose=NO)
<20><>? <20><>?
<20><>? <20><>?
<20><>? ┌─────────<E29480><E29480>?
<20><>? <20><>? IDLE <20><>?(inUse=NO, in pool)
<20><>? └────┬────<E29480><E29480>?
<20><>? <20><>?
<20><>? ├──dequeue──<E29480><E29480>?IN_USE (reuse)
<20><>? <20><>?
<20><>? ├──idle 30s──<E29480><E29480>?EXPIRED
<20><>? <20><>? <20><>?
<20><>? <20><>? └──prune──<E29480><E29480>?INVALIDATED
<20><>? <20><>?
<20><>? └──!isViable──<E29480><E29480>?INVALIDATED (skip in dequeue)
<20><>?
├──error/timeout──<E29480><E29480>?returnConnection(shouldClose=YES)
<20><>? <20><>?
<20><>? <20><>?
└──────────<E29480><E29480>?┌──────────────<E29480><E29480>?
<20><>?INVALIDATED <20><>?(removed from pool)
└──────────────<E29480><E29480>?
```
---
## 代码中的状态转<E68081><E8BDAC>?
### 1. CREATING <20><>?IN_USE (新连<E696B0><E8BF9E>?
**HttpdnsNWHTTPClient.m:248-249**
```objc
newConnection.inUse = YES;
newConnection.lastUsedDate = now;
[pool addObject:newConnection]; // 加入<E58AA0><E585A5>?
```
**何时触发:**
- `dequeueConnectionForHost` 找不到可复用连接
- 创建新连接并成功打开
### 2. IDLE <20><>?IN_USE (复用)
**HttpdnsNWHTTPClient.m:210-214**
```objc
for (HttpdnsNWReusableConnection *candidate in pool) {
if (!candidate.inUse && [candidate isViable]) {
candidate.inUse = YES;
candidate.lastUsedDate = now;
connection = candidate;
break;
}
}
```
**关键检<E994AE><E6A380>?**
- `!candidate.inUse` - 必须是空闲状<E997B2><E78AB6>?
- `[candidate isViable]` - 连接必须仍然有效
### 3. IN_USE <20><>?IDLE (正常归还)
**HttpdnsNWHTTPClient.m:283-288**
```objc
if (shouldClose || connection.isInvalidated) {
// <20><>?INVALIDATED (<28><>?4)
} else {
connection.inUse = NO;
connection.lastUsedDate = now;
if (![pool containsObject:connection]) {
[pool addObject:connection]; // 防止双重添加
}
[self pruneConnectionPool:pool referenceDate:now];
}
```
**防护措施:**
- Line 285: `if (![pool containsObject:connection])` - 防止重复添加
### 4. IN_USE/IDLE <20><>?INVALIDATED (失效)
**HttpdnsNWHTTPClient.m:279-281**
```objc
if (shouldClose || connection.isInvalidated) {
[connection invalidate];
[pool removeObject:connection];
}
```
**触发条件:**
- `shouldClose=YES` (timeout, error, parse failure, remote close)
- `connection.isInvalidated=YES` (连接已失<E5B7B2><E5A4B1>?
### 5. EXPIRED <20><>?INVALIDATED (过期清理)
**HttpdnsNWHTTPClient.m:297-312**
```objc
- (void)pruneConnectionPool:(NSMutableArray<HttpdnsNWReusableConnection *> *)pool referenceDate:(NSDate *)referenceDate {
// ...
NSMutableArray<HttpdnsNWReusableConnection *> *toRemove = [NSMutableArray array];
for (HttpdnsNWReusableConnection *conn in pool) {
if (conn.inUse) continue; // 跳过使用中的
NSTimeInterval idle = [referenceDate timeIntervalSinceDate:conn.lastUsedDate];
if (idle > kHttpdnsNWHTTPClientConnectionIdleTimeout) { // 30<33><30>?
[toRemove addObject:conn];
}
}
for (HttpdnsNWReusableConnection *conn in toRemove) {
[conn invalidate];
[pool removeObject:conn];
}
// 限制池大<E6B1A0><E5A4A7>?<3F><>?4
while (pool.count > kHttpdnsNWHTTPClientMaxIdleConnectionsPerHost) {
HttpdnsNWReusableConnection *oldest = pool.firstObject;
[oldest invalidate];
[pool removeObject:oldest];
}
}
```
---
## 当前测试覆盖情况
### <20><>?已测试的正常流程
| 状态转<E68081><E8BDAC>?| 测试 | 覆盖 |
|----------|------|------|
| CREATING <20><>?IN_USE <20><>?IDLE | G.1-G.7, O.1 | <20><>?|
| IDLE <20><>?IN_USE (复用) | G.2, O.1-O.3, J.1-J.5 | <20><>?|
| IN_USE <20><>?INVALIDATED (timeout) | P.1-P.6 | <20><>?|
| EXPIRED <20><>?INVALIDATED (30s) | J.2, M.4, I.4 | <20><>?|
| 池容量限<E9878F><E99990>?(max 4) | O.3, J.3 | <20><>?|
| 并发状态访<E68081><E8AEBF>?| I.1-I.5, M.3 | <20><>?|
### <20><>?未测试的异常场景
#### 1. **连接在池中失效Stale Connection<6F><6E>?*
**场景:**
- 连接空闲 29 秒(未到 30 秒过期)
- 服务器主动关闭连<E997AD><E8BF9E>?
- `dequeue` <20><>?`isViable` 返回 NO
**当前代码行为:**
```objc
for (HttpdnsNWReusableConnection *candidate in pool) {
if (!candidate.inUse && [candidate isViable]) { // <20><>?isViable 检<><E6A380>?
// 只复用有效连<E69588><E8BF9E>?
}
}
// 如果所有连接都 !isViable会创建新连<E696B0><E8BF9E>?
```
**风险:** 未验<E69CAA><E9AA8C>?`isViable` 检查是否真的工<E79A84><E5B7A5>?
**测试需<E8AF95><E99C80>?** Q.1
```objc
testStateTransition_StaleConnectionInPool_SkipsAndCreatesNew
```
---
#### 2. **双重归还Double Return<72><6E>?*
**场景:**
- 连接被归<E8A2AB><E5BD92>?
- 代码错误,再次归还同一连接
**当前代码防护:**
```objc
if (![pool containsObject:connection]) {
[pool addObject:connection]; // <20><>?防止重复添加
}
```
**风险:** 未验证防护是否有<E590A6><E69C89>?
**测试需<E8AF95><E99C80>?** Q.2
```objc
testStateTransition_DoubleReturn_Idempotent
```
---
#### 3. **归还错误的池键Wrong Pool Key<65><79>?*
**场景:**
- 从池A借出连接
- 归还到池B错误的key<65><79>?
**当前代码行为:**
```objc
- (void)returnConnection:(HttpdnsNWReusableConnection *)connection
forKey:(NSString *)key
shouldClose:(BOOL)shouldClose {
// ...
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
// 会添加到错误的池!
}
```
**风险:** 可能导致池污<E6B1A0><E6B1A1>?
**测试需<E8AF95><E99C80>?** Q.3
```objc
testStateTransition_ReturnToWrongPool_Isolated
```
---
#### 4. **连接在使用中变为失效**
**场景:**
- 连接被借出 (inUse=YES)
- `sendRequestData` 过程中网络错<E7BB9C><E99499>?
- 连接被标<E8A2AB><E6A087>?invalidated
**当前代码行为:**
```objc
NSData *rawResponse = [connection sendRequestData:requestData ...];
if (!rawResponse) {
[self returnConnection:connection forKey:poolKey shouldClose:YES]; // <20><>?invalidated
}
```
**测试需<E8AF95><E99C80>?** Q.4
```objc
testStateTransition_ErrorDuringUse_Invalidated
```
---
#### 5. **池容量超限时的移除策<E999A4><E7AD96>?*
**场景:**
- 池已<E6B1A0><E5B7B2>?4 个连<E4B8AA><E8BF9E>?
- <20><>?5 个连接被归还
**当前代码行为:**
```objc
while (pool.count > kHttpdnsNWHTTPClientMaxIdleConnectionsPerHost) {
HttpdnsNWReusableConnection *oldest = pool.firstObject; // <20><>?移除最老的
[oldest invalidate];
[pool removeObject:oldest];
}
```
**问题:**
- 移除 `pool.firstObject` - 是按添加顺序还是使用顺序<E9A1BA><E5BA8F>?
- NSMutableArray 顺序是否能保证?
**测试需<E8AF95><E99C80>?** Q.5
```objc
testStateTransition_PoolOverflow_RemovesOldest
```
---
#### 6. **并发状态竞<E68081><E7AB9E>?*
**场景:**
- Thread A: dequeue 连接<EFBC8C><E8AEBE>?`inUse=YES`
- Thread B: 同时 prune 过期连接
- 竞态连接同时被标<E8A2AB><E6A087>?inUse 和被移除
**当前代码防护:**
```objc
- (void)pruneConnectionPool:... {
for (HttpdnsNWReusableConnection *conn in pool) {
if (conn.inUse) continue; // <20><>?跳过使用中的
}
}
```
**测试需<E8AF95><E99C80>?** Q.6 (可能已被 I 组部分覆<E58886><E8A686>?
```objc
testStateTransition_ConcurrentDequeueAndPrune_NoCorruption
```
---
#### 7. **连接打开失败**
**场景:**
- 创建连接
- `openWithTimeout` 失败
**当前代码行为:**
```objc
if (![newConnection openWithTimeout:timeout error:error]) {
[newConnection invalidate]; // <20><>?立即失效
return nil; // <20><>?不加入池
}
```
**测试需<E8AF95><E99C80>?** Q.7
```objc
testStateTransition_OpenFails_NotAddedToPool
```
---
## 状态不变式State Invariants<74><73>?
### 应该始终成立的约<E79A84><E7BAA6>?
1. **互斥<E4BA92><E696A5>?**
```
∀ connection: (inUse=YES) <20><>?(dequeue count <20><>?1)
```
同一连接不能被多次借出
2. **池完整<E5AE8C><E695B4>?**
```
∀ pool: <20><>?connections) <20><>?maxPoolSize (4)
```
每个池最<E6B1A0><E69C80>?4 个连<E4B8AA><E8BF9E>?
3. **状态一致<E4B880><E887B4>?**
```
∀ connection in pool: !invalidated
```
池中不应有失效连<E69588><E8BF9E>?
4. **时间单调<E58D95><E8B083>?**
```
∀ connection: lastUsedDate 随每次使用递增
```
5. **失效不可<E4B88D><E58FAF>?**
```
invalidated=YES <20><>?connection removed from pool
```
失效连接必须从池中移<E4B8AD><E7A7BB>?
---
## 测试设计建议
### Q 组状态机异常转换测试<E6B58B><E8AF95>?个新测试<E6B58B><E8AF95>?
| 测试 | 验证内容 | 难度 |
|------|---------|------|
| **Q.1** | Stale connection <20><>?`isViable` 检测并跳过 | 🔴 高需要模拟服务器关闭<E585B3><E997AD>?|
| **Q.2** | 双重归还是幂等的 | 🟢 <20><>?|
| **Q.3** | 归还到错误池键不污染其他<E585B6><E4BB96>?| 🟡 <20><>?|
| **Q.4** | 使用中错误导致连接失<E68EA5><E5A4B1>?| 🟢 低(已有 P 组部分覆盖) |
| **Q.5** | 池溢出时移除最旧连<E697A7><E8BF9E>?| 🟡 <20><>?|
| **Q.6** | 并发 dequeue/prune 竞<><E7AB9E>?| 🔴 高(需要精确时序) |
| **Q.7** | 打开失败的连接不加入<E58AA0><E585A5>?| 🟢 <20><>?|
---
## 状态机验证策略
### 方法1: 直接状态检<E68081><E6A380>?
```objc
// 验证状态属<E68081><E5B19E>?
XCTAssertTrue(connection.inUse);
XCTAssertFalse(connection.isInvalidated);
XCTAssertEqual([poolCount], expectedCount);
```
### 方法2: 状态转换序<E68DA2><E5BA8F>?
```objc
// 验证转换序列
[client resetPoolStatistics];
// CREATING <20><>?IN_USE
response1 = [client performRequest...];
XCTAssertEqual(creationCount, 1);
// IN_USE <20><>?IDLE
[NSThread sleepForTimeInterval:0.5];
XCTAssertEqual(poolCount, 1);
// IDLE <20><>?IN_USE (reuse)
response2 = [client performRequest...];
XCTAssertEqual(reuseCount, 1);
```
### 方法3: 不变式验<E5BC8F><E9AA8C>?
```objc
// 验证池不变式
NSArray *keys = [client allConnectionPoolKeys];
for (NSString *key in keys) {
NSUInteger count = [client connectionPoolCountForKey:key];
XCTAssertLessThanOrEqual(count, 4, @"Pool invariant: max 4 connections");
}
```
---
## 当前覆盖率评<E78E87><E8AF84>?
### 状态转换覆盖矩<E79B96><E79FA9>?
| From <20><>?/ To <20><>?| CREATING | IN_USE | IDLE | EXPIRED | INVALIDATED |
|---------------|----------|--------|------|---------|-------------|
| **CREATING** | - | <20><>?| <20><>?| <20><>?| <20><>?(Q.7 needed) |
| **IN_USE** | <20><>?| - | <20><>?| <20><>?| <20><>?|
| **IDLE** | <20><>?| <20><>?| - | <20><>?| <20><>?(Q.1 needed) |
| **EXPIRED** | <20><>?| <20><>?| <20><>?| - | <20><>?|
| **INVALIDATED** | <20><>?| <20><>?| <20><>?| <20><>?| - |
**覆盖<E8A686><E79B96>?** 6/25 transitions = 24%
**有效覆盖<E8A686><E79B96>?** 6/10 valid transitions = 60%
### 异常场景覆盖
| 异常场景 | 当前测试 | 覆盖 |
|----------|---------|------|
| Stale connection | <20><>?| 0% |
| Double return | <20><>?| 0% |
| Wrong pool key | <20><>?| 0% |
| Error during use | P.1-P.6 | 100% |
| Pool overflow | O.3, J.3 | 50% (未验证移除策<E999A4><E7AD96>? |
| Concurrent race | I.1-I.5 | 80% |
| Open failure | <20><>?| 0% |
**总体异常覆盖:** ~40%
---
## 风险评估
### 高风险未测试场景
**风险等级 🔴 <20><>?**
1. **Stale Connection (Q.1)** - 可能导致请求失败
2. **Concurrent Dequeue/Prune (Q.6)** - 可能导致状态不一<E4B88D><E4B880>?
**风险等级 🟡 <20><>?**
3. **Wrong Pool Key (Q.3)** - 可能导致池污<E6B1A0><E6B1A1>?
4. **Pool Overflow Strategy (Q.5)** - LRU vs FIFO 影响性能
**风险等级 🟢 <20><>?**
5. **Double Return (Q.2)** - 已有代码防护
6. **Open Failure (Q.7)** - 已有错误处理
---
## 建议
### 短期(关键)
1. <20><>?**添加 Q.2 测试** - 验证双重归还防护
2. <20><>?**添加 Q.5 测试** - 验证池溢出移除策<E999A4><E7AD96>?
3. <20><>?**添加 Q.7 测试** - 验证打开失败处理
### 中期(增强)
4. ⚠️ **添加 Q.3 测试** - 验证池隔<E6B1A0><E99A94>?
5. ⚠️ **添加 Q.1 测试** - 验证 stale connection<EFBC88><E99C80>?mock<63><6B>?
### 长期(完整)
6. 🔬 **添加 Q.6 测试** - 验证并发竞态复杂<E5A48D><E69D82>?
---
**创建时间**: 2025-11-01
**作<><E4BD9C>?*: Claude Code
**状<><E78AB6>?*: 分析完成,待实现 Q 组测<E7BB84><E6B58B>?

View File

@@ -0,0 +1,226 @@
# 超时对连接复用的影响分析
## 问题描述
当前测试套件没有充分验证**超时与连接池交互**<2A><>?无形结果"intangible outcomes可能存在以下风险<E9A38E><E999A9>?
- 超时后的连接泄漏
- 连接池被超时连接污染
- 连接池无法从超时中恢<E4B8AD><E681A2>?
- 并发场景下部分超时影响整体池健康
---
## 代码行为分析
### 超时处理流程
**HttpdnsNWHTTPClient.m:144-145**
```objc
if (!rawResponse) {
[self returnConnection:connection forKey:poolKey shouldClose:YES];
// 返回 nilerror 设置
}
```
**returnConnection:forKey:shouldClose: (line 279-281)**
```objc
if (shouldClose || connection.isInvalidated) {
[connection invalidate]; // 取消底层 nw_connection
[pool removeObject:connection]; // 从池中移<E4B8AD><E7A7BB>?
}
```
**结论**代码逻辑正确超时连<EFBFBD><EFBFBD>?*会被移除**而非留在池中<E6B1A0><E4B8AD>?
---
## 当前测试覆盖情况
### 已有测试:`testIntegration_RequestTimeout_ReturnsError`
**验证内容<E58685><E5AEB9>?*
- <20><>?超时返回 `nil` response
- <20><>?超时设置 `error`
**未验证内容(缺失):**
- <20><>?连接是否从池中移<E4B8AD><E7A7BB>?
- <20><>?池计数是否正<E590A6><E6ADA3>?
- <20><>?后续请求是否正常工作
- <20><>?是否存在连接泄漏
- <20><>?并发场景下部分超时的影响
---
## 需要验证的"无形结果"
### 1. 单次超时后的池清<E6B1A0><E6B885>?
**场景**<EFBFBD><EFBFBD>?
1. 请求 A 超时timeout=1s, endpoint=/delay/10<31><30>?
2. 验证池状<E6B1A0><E78AB6>?
**应验证:**
- Pool count = 0连接已移除<E7A7BB><E999A4>?
- Total connection count 没有异常增长
- 无连接泄<E68EA5><E6B384>?
**测试方法**<EFBFBD><EFBFBD>?
```objc
[client resetPoolStatistics];
// 发起超时请求
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"TimeoutTest"
timeout:1.0
error:&error];
XCTAssertNil(response);
XCTAssertNotNil(error);
// 验证池状<E6B1A0><E78AB6>?
NSString *poolKey = @"127.0.0.1:11080:tcp";
XCTAssertEqual([client connectionPoolCountForKey:poolKey], 0, @"Timed-out connection should be removed");
XCTAssertEqual([client totalConnectionCount], 0, @"No connections should remain");
XCTAssertEqual(client.connectionCreationCount, 1, @"Should have created 1 connection");
XCTAssertEqual(client.connectionReuseCount, 0, @"No reuse for timed-out connection");
```
---
### 2. 超时后的池恢复能<E5A48D><E883BD>?
**场景**<EFBFBD><EFBFBD>?
1. 请求 A 超时
2. 请求 B 正常验证池恢复<E681A2><E5A48D>?
3. 请求 C 复用 B 的连<E79A84><E8BF9E>?
**应验证:**
- 请求 B 成功(池已恢复)
- 请求 C 复用连接connectionReuseCount = 1<><31>?
- Pool count = 1<EFBC88><E58FAA>?B/C 的连接)
---
### 3. 并发场景:部分超时不影响成功请求
**场景**<EFBFBD><EFBFBD>?
1. 并发发起 10 个请<E4B8AA><E8AFB7>?
2. 5 个正常timeout=15s<35><73>?
3. 5 个超时timeout=0.5s, endpoint=/delay/10<31><30>?
**应验证:**
- 5 个正常请求成<E6B182><E68890>?
- 5 个超时请求失<E6B182><E5A4B1>?
- Pool count <20><>?5只保留成功的连接
- Total connection count <20><>?5无泄漏<E6B384><E6BC8F>?
- connectionCreationCount <20><>?10合理范围
- 成功的请求可以复用连<E794A8><E8BF9E>?
---
### 4. 连续超时不导致资源泄<E6BA90><E6B384>?
**场景**<EFBFBD><EFBFBD>?
1. 连续 20 次超时请<E697B6><E8AFB7>?
2. 验证连接池没有累<E69C89><E7B4AF>?僵尸连接"
**应验证:**
- Pool count = 0
- Total connection count = 0
- connectionCreationCount = 20每次都创建新连接因为超时的被移除<E7A7BB><E999A4>?
- connectionReuseCount = 0超时连接不可复用
- 无内存泄漏虽然代码层面无法直接测试<E6B58B><E8AF95>?
---
### 5. 超时不阻塞连接池
**场景**<EFBFBD><EFBFBD>?
1. 请求 A 超时endpoint=/delay/10, timeout=1s<31><73>?
2. 同时请求 B 正常endpoint=/get, timeout=15s<35><73>?
**应验证:**
- 请求 A <20><>?B 并发执行不互相阻塞<E998BB><E5A19E>?
- 请求 B 成功<EFBC88><E4B88D>?A 超时影响<E5BDB1><E5938D>?
- 请求 A 的超时连接被正确移除
- Pool 中只有请<E69C89><E8AFB7>?B 的连<E79A84><E8BF9E>?
---
### 6. 多端口场景下的超时隔<E697B6><E99A94>?
**场景**<EFBFBD><EFBFBD>?
1. 端口 11443 请求超时
2. 端口 11444 请求正常
3. 验证端口间隔<E997B4><E99A94>?
**应验证:**
- 端口 11443 pool count = 0
- 端口 11444 pool count = 1
- 两个端口的连接池互不影响
---
## 测试实现建议
### P 组:超时与连接池交互测试
**P.1 单次超时清理验证**
- `testTimeout_SingleRequest_ConnectionRemovedFromPool`
**P.2 超时后池恢复**
- `testTimeout_PoolRecovery_SubsequentRequestSucceeds`
**P.3 并发部分超时**
- `testTimeout_ConcurrentPartialTimeout_SuccessfulRequestsReuse`
**P.4 连续超时无泄<E697A0><E6B384>?*
- `testTimeout_ConsecutiveTimeouts_NoConnectionLeak`
**P.5 超时不阻塞池**
- `testTimeout_NonBlocking_ConcurrentNormalRequestSucceeds`
**P.6 多端口超时隔<E697B6><E99A94>?*
- `testTimeout_MultiPort_IsolatedPoolCleaning`
---
## Mock Server 支持
需要添加可配置延迟<EFBFBD><EFBFBD>?endpoint<6E><74>?
- `/delay/10` - 延迟 10 秒已有<E5B7B2><E69C89>?
- 测试时设置短 timeout如 0.5s-2s触发超<E58F91><E8B685>?
---
## 预期测试结果
| 验证<E9AA8C><E8AF81>?| 当前状<E5898D><E78AB6>?| 目标状<E6A087><E78AB6>?|
|--------|---------|---------|
| 超时连接移除 | 未验<E69CAA><E9AA8C>?| <20><>?验证池计<E6B1A0><E8AEA1>?0 |
| 池恢复能<E5A48D><E883BD>?| 未验<E69CAA><E9AA8C>?| <20><>?后续请求成功 |
| 并发超时隔离 | 未验<E69CAA><E9AA8C>?| <20><>?成功请求不受影响 |
| 无连接泄<E68EA5><E6B384>?| 未验<E69CAA><E9AA8C>?| <20><>?总连接数稳定 |
| 超时不阻<E4B88D><E998BB>?| 未验<E69CAA><E9AA8C>?| <20><>?并发执行不阻<E4B88D><E998BB>?|
| 多端口隔<E58FA3><E99A94>?| 未验<E69CAA><E9AA8C>?| <20><>?端口间独立清<E7AB8B><E6B885>?|
---
## 风险评估
**如果不测试这些场景的风险<E9A38E><E999A9>?*
1. **连接泄漏**超时连接可能未正确清理导致内存泄<EFBFBD><EFBFBD>?
2. **池污<E6B1A0><E6B1A1>?*超时连接留在池中被后续请求复用导致失<E887B4><E5A4B1>?
3. **级联故障**:部分超时影响整体连接池健康
4. **资源耗尽**:连续超时累积连接,最终耗尽系统资源
**当前代码逻辑正确性:** <20><>?高代码分析显示正确处理<E5A484><E79086>?
**测试验证覆盖率:** <20><>?低(缺少池交互验证)
**建议<E5BBBA><E8AEAE>?* 添加 P 组测试以提供**可观测的证据**证明超时处理正确<E6ADA3><E7A1AE>?
---
**创建时间**: 2025-11-01
**维护<E7BBB4><E68AA4>?*: Claude Code

View File

@@ -0,0 +1,347 @@
#!/usr/bin/env python3
"""
HTTP/HTTPS Mock Server for HttpdnsNWHTTPClient Integration Tests
模拟 httpbin.org 的核心功能,用于替代不稳定的外部依赖。
支持 HTTP (端口 11080) 和多个 HTTPS 端口 (11443-11446自签名证书)。
使用方法:
python3 mock_server.py
端口配置:
- HTTP: 127.0.0.1:11080
- HTTPS: 127.0.0.1:11443, 11444, 11445, 11446
注意:
- 使用非特权端口,无需 root 权限
- HTTPS 使用自签名证书,测试时需禁用 TLS 验证
- 多个 HTTPS 端口用于测试连接池隔离
- 按 Ctrl+C 停止服务器
"""
import json
import time
import uuid
import ssl
import os
import subprocess
import signal
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse
from threading import Thread
from socketserver import ThreadingMixIn
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
"""多线程 HTTP 服务器,支持并发请求"""
daemon_threads = True
allow_reuse_address = True
class MockHTTPHandler(BaseHTTPRequestHandler):
"""模拟 httpbin.org 的请求处理器"""
# 使用 HTTP/1.1 协议(支持 keep-alive
protocol_version = 'HTTP/1.1'
# 禁用日志输出(可选,便于查看测试输出)
def log_message(self, format, *args):
# 取消注释以启用详细日志
# print(f"[{self.address_string()}] {format % args}")
pass
def do_GET(self):
"""处理 GET 请求"""
path = urlparse(self.path).path
if path == '/get':
self._handle_get()
elif path.startswith('/status/'):
self._handle_status(path)
elif path.startswith('/stream-bytes/'):
self._handle_stream_bytes(path)
elif path.startswith('/delay/'):
self._handle_delay(path)
elif path == '/headers':
self._handle_headers()
elif path == '/uuid':
self._handle_uuid()
elif path == '/user-agent':
self._handle_user_agent()
elif path == '/connection-test':
self._handle_connection_test()
else:
self._handle_not_found()
def _handle_get(self):
"""模拟 /get - 返回请求信息"""
data = {
'args': {},
'headers': dict(self.headers),
'origin': self.client_address[0],
'url': f'{self.command} {self.path}'
}
self._send_json(200, data)
def _handle_status(self, path):
"""模拟 /status/{code} - 返回指定状态码"""
try:
status_code = int(path.split('/')[-1])
# 限制状态码范围在 100-599
if 100 <= status_code < 600:
self._send_json(status_code, {'status': status_code})
else:
self._send_json(400, {'error': 'Invalid status code'})
except (ValueError, IndexError):
self._send_json(400, {'error': 'Invalid status code format'})
def _handle_stream_bytes(self, path):
"""模拟 /stream-bytes/{n} - 返回 chunked 编码的 n 字节数据"""
try:
n = int(path.split('/')[-1])
except (ValueError, IndexError):
self._send_json(400, {'error': 'Invalid byte count'})
return
# 发送 chunked 响应
self.send_response(200)
self.send_header('Content-Type', 'application/octet-stream')
self.send_header('Transfer-Encoding', 'chunked')
self.send_header('Connection', 'keep-alive')
self.end_headers()
# 发送 chunk
chunk_data = b'X' * n
chunk_size_hex = f'{n:x}\r\n'.encode('utf-8')
self.wfile.write(chunk_size_hex)
self.wfile.write(chunk_data)
self.wfile.write(b'\r\n')
# 发送最后一个 chunk (size=0)
self.wfile.write(b'0\r\n\r\n')
self.wfile.flush() # 确保数据发送
def _handle_delay(self, path):
"""模拟 /delay/{seconds} - 延迟指定秒数后返回"""
try:
seconds = int(path.split('/')[-1])
except (ValueError, IndexError):
self._send_json(400, {'error': 'Invalid delay value'})
return
# 最多延迟 10 秒(防止意外)
seconds = min(seconds, 10)
time.sleep(seconds)
data = {
'args': {},
'headers': dict(self.headers),
'origin': self.client_address[0],
'url': f'{self.command} {self.path}',
'delayed': seconds
}
self._send_json(200, data)
def _handle_headers(self):
"""模拟 /headers - 返回所有请求头部"""
data = {
'headers': dict(self.headers)
}
self._send_json(200, data)
def _handle_uuid(self):
"""模拟 /uuid - 返回随机 UUID"""
data = {
'uuid': str(uuid.uuid4())
}
self._send_json(200, data)
def _handle_user_agent(self):
"""模拟 /user-agent - 返回 User-Agent 头部"""
data = {
'user-agent': self.headers.get('User-Agent', '')
}
self._send_json(200, data)
def _handle_connection_test(self):
"""处理 /connection-test - 返回指定的 Connection 头部用于测试"""
from urllib.parse import parse_qs
# 解析查询参数
query_string = urlparse(self.path).query
params = parse_qs(query_string)
mode = params.get('mode', ['keep-alive'])[0]
data = {
'mode': mode,
'message': f'Connection header test with mode: {mode}'
}
body = json.dumps(data).encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', len(body))
# 根据 mode 参数设置不同的 Connection 头部
if mode == 'close':
self.send_header('Connection', 'close')
elif mode == 'proxy-close':
self.send_header('Proxy-Connection', 'close')
self.send_header('Connection', 'keep-alive')
elif mode == 'close-uppercase':
self.send_header('CONNECTION', 'CLOSE')
elif mode == 'close-mixed':
self.send_header('Connection', 'Close')
else: # keep-alive (default)
self.send_header('Connection', 'keep-alive')
self.end_headers()
self.wfile.write(body)
self.wfile.flush()
def _handle_not_found(self):
"""处理未知路径"""
self._send_json(404, {'error': 'Not Found', 'path': self.path})
def _send_json(self, status_code, data):
"""发送 JSON 响应"""
try:
body = json.dumps(data).encode('utf-8')
self.send_response(status_code)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', len(body))
# 支持 HTTP/1.1 keep-alive
self.send_header('Connection', 'keep-alive')
self.end_headers()
self.wfile.write(body)
self.wfile.flush() # 确保数据发送
except Exception as e:
print(f"Error sending response: {e}", file=sys.stderr)
def create_self_signed_cert(cert_file='server.pem'):
"""生成自签名证书(如果不存在)"""
if os.path.exists(cert_file):
print(f"✓ 使用现有证书: {cert_file}")
return cert_file
print(f"正在生成自签名证书: {cert_file} ...")
try:
subprocess.run([
'openssl', 'req', '-x509', '-newkey', 'rsa:2048',
'-keyout', cert_file, '-out', cert_file,
'-days', '365', '-nodes',
'-subj', '/CN=localhost'
], check=True, capture_output=True)
print(f"✓ 证书生成成功")
return cert_file
except subprocess.CalledProcessError as e:
print(f"✗ 证书生成失败: {e.stderr.decode()}", file=sys.stderr)
sys.exit(1)
except FileNotFoundError:
print("✗ 未找到 openssl 命令,请安装 OpenSSL", file=sys.stderr)
sys.exit(1)
def run_http_server(port=11080):
"""运行 HTTP 服务器"""
try:
server = ThreadedHTTPServer(('127.0.0.1', port), MockHTTPHandler)
print(f"✓ HTTP 服务器运行在 http://127.0.0.1:{port}")
server.serve_forever()
except OSError as e:
if e.errno == 48: # Address already in use
print(f"✗ 端口 {port} 已被占用,请关闭占用端口的进程或使用其他端口", file=sys.stderr)
else:
print(f"✗ HTTP 服务器启动失败: {e}", file=sys.stderr)
sys.exit(1)
def run_https_server(port=11443, cert_file='server.pem'):
"""运行 HTTPS 服务器(使用自签名证书)"""
try:
server = ThreadedHTTPServer(('127.0.0.1', port), MockHTTPHandler)
# 配置 SSL 上下文
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(cert_file)
# 包装 socket
server.socket = context.wrap_socket(server.socket, server_side=True)
print(f"✓ HTTPS 服务器运行在 https://127.0.0.1:{port} (自签名证书)")
server.serve_forever()
except OSError as e:
if e.errno == 48: # Address already in use
print(f"✗ 端口 {port} 已被占用,请关闭占用端口的进程或使用其他端口", file=sys.stderr)
else:
print(f"✗ HTTPS 服务器启动失败: {e}", file=sys.stderr)
sys.exit(1)
except ssl.SSLError as e:
print(f"✗ SSL 配置失败: {e}", file=sys.stderr)
sys.exit(1)
def signal_handler(sig, frame):
"""处理 Ctrl+C 信号"""
print("\n\n✓ 服务器已停止")
sys.exit(0)
def main():
"""主函数"""
# 注册信号处理器
signal.signal(signal.SIGINT, signal_handler)
# 生成自签名证书
script_dir = os.path.dirname(os.path.abspath(__file__))
cert_file = os.path.join(script_dir, 'server.pem')
create_self_signed_cert(cert_file)
print("\n" + "="*60)
print(" HttpdnsNWHTTPClient Mock Server")
print("="*60)
print("\n支持的 endpoints:")
print(" GET /get - 返回请求信息")
print(" GET /status/{code} - 返回指定状态码")
print(" GET /stream-bytes/N - 返回 chunked 编码的 N 字节数据")
print(" GET /delay/N - 延迟 N 秒后返回")
print(" GET /headers - 返回所有请求头部")
print(" GET /uuid - 返回随机 UUID")
print(" GET /user-agent - 返回 User-Agent 头部")
print(" GET /connection-test?mode={mode}")
print(" - 返回指定 Connection 头部")
print(" mode: close, keep-alive, proxy-close,")
print(" close-uppercase, close-mixed")
print("\n按 Ctrl+C 停止服务器\n")
print("="*60 + "\n")
# 启动 HTTP 和 HTTPS 服务器(使用线程)
http_thread = Thread(target=run_http_server, args=(11080,), daemon=True)
# 启动多个 HTTPS 端口用于测试连接复用隔离
https_ports = [11443, 11444, 11445, 11446]
https_threads = []
http_thread.start()
time.sleep(0.5) # 等待 HTTP 服务器启动
# 启动所有 HTTPS 服务器
for port in https_ports:
https_thread = Thread(target=run_https_server, args=(port, cert_file), daemon=True)
https_threads.append(https_thread)
https_thread.start()
time.sleep(0.1) # 错峰启动避免端口冲突
# 主线程等待(保持服务器运行)
try:
http_thread.join()
for thread in https_threads:
thread.join()
except KeyboardInterrupt:
signal_handler(signal.SIGINT, None)
if __name__ == '__main__':
main()