阿里sdk

This commit is contained in:
Robin
2026-02-20 17:56:24 +08:00
parent 39524692e5
commit f3af234308
524 changed files with 58345 additions and 0 deletions

View File

@@ -0,0 +1,477 @@
//
// DBTest.m
// AlicloudHttpDNSTests
//
// Created by xuyecan on 2025/3/15.
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "../Testbase/TestBase.h"
#import "HttpdnsDB.h"
#import "HttpdnsHostRecord.h"
@interface DBTest : TestBase
@property (nonatomic, strong) HttpdnsDB *db;
@property (nonatomic, assign) NSInteger testAccountId;
@end
@implementation DBTest
- (void)setUp {
[super setUp];
// Use a test-specific account ID to avoid conflicts with real data
self.testAccountId = 999999;
self.db = [[HttpdnsDB alloc] initWithAccountId:self.testAccountId];
// Clean up any existing data
[self.db deleteAll];
}
- (void)tearDown {
// Clean up after tests
[self.db deleteAll];
self.db = nil;
[super tearDown];
}
#pragma mark - Initialization Tests
- (void)testInitialization {
XCTAssertNotNil(self.db, @"Database should be initialized successfully");
// Test with invalid account ID (negative value)
HttpdnsDB *invalidDB = [[HttpdnsDB alloc] initWithAccountId:-1];
XCTAssertNotNil(invalidDB, @"Database should still initialize with negative account ID");
}
#pragma mark - Create Tests
- (void)testCreateRecord {
// Create a test record
HttpdnsHostRecord *record = [self createTestRecordWithHostname:@"test.example.com" cacheKey:@"test_cache_key"];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed");
// Verify the record was created by querying it
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"test_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the created record");
XCTAssertEqualObjects(fetchedRecord.cacheKey, @"test_cache_key", @"Cache key should match");
XCTAssertEqualObjects(fetchedRecord.hostName, @"test.example.com", @"Hostname should match");
XCTAssertEqualObjects(fetchedRecord.clientIp, @"192.168.1.1", @"Client IP should match");
XCTAssertEqual(fetchedRecord.v4ips.count, 2, @"Should have 2 IPv4 addresses");
XCTAssertEqual(fetchedRecord.v6ips.count, 1, @"Should have 1 IPv6 address");
}
- (void)testCreateMultipleRecords {
// Create multiple test records
NSArray<NSString *> *hostnames = @[@"host1.example.com", @"host2.example.com", @"host3.example.com"];
NSArray<NSString *> *cacheKeys = @[@"cache_key_1", @"cache_key_2", @"cache_key_3"];
for (NSInteger i = 0; i < hostnames.count; i++) {
HttpdnsHostRecord *record = [self createTestRecordWithHostname:hostnames[i] cacheKey:cacheKeys[i]];
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed for %@", hostnames[i]);
}
// Verify all records were created
for (NSInteger i = 0; i < cacheKeys.count; i++) {
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKeys[i]];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the created record for %@", cacheKeys[i]);
XCTAssertEqualObjects(fetchedRecord.cacheKey, cacheKeys[i], @"Cache key should match");
XCTAssertEqualObjects(fetchedRecord.hostName, hostnames[i], @"Hostname should match");
}
}
- (void)testCreateRecordWithNilCacheKey {
// Create a record with nil cacheKey
HttpdnsHostRecord *record = [self createTestRecordWithHostname:@"test.example.com" cacheKey:nil];
// Attempt to insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertFalse(result, @"Record creation should fail with nil cacheKey");
}
- (void)testCreateRecordWithEmptyValues {
// Create a record with empty arrays and nil values
HttpdnsHostRecord *record = [[HttpdnsHostRecord alloc] initWithId:1
cacheKey:@"empty_cache_key"
hostName:@"empty.example.com"
createAt:nil
modifyAt:nil
clientIp:nil
v4ips:@[]
v4ttl:0
v4LookupTime:0
v6ips:@[]
v6ttl:0
v6LookupTime:0
extra:@""];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed with empty values");
// Verify the record was created
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"empty_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the created record");
XCTAssertEqualObjects(fetchedRecord.cacheKey, @"empty_cache_key", @"Cache key should match");
XCTAssertEqualObjects(fetchedRecord.hostName, @"empty.example.com", @"Hostname should match");
XCTAssertNil(fetchedRecord.clientIp, @"Client IP should be nil");
XCTAssertEqual(fetchedRecord.v4ips.count, 0, @"Should have 0 IPv4 addresses");
XCTAssertEqual(fetchedRecord.v6ips.count, 0, @"Should have 0 IPv6 addresses");
// Verify createAt and modifyAt were automatically set
XCTAssertNotNil(fetchedRecord.createAt, @"createAt should be automatically set");
XCTAssertNotNil(fetchedRecord.modifyAt, @"modifyAt should be automatically set");
}
- (void)testCreateMultipleRecordsWithSameHostname {
// Create multiple records with the same hostname but different cache keys
NSString *hostname = @"same.example.com";
NSArray<NSString *> *cacheKeys = @[@"same_cache_key_1", @"same_cache_key_2", @"same_cache_key_3"];
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *record = [self createTestRecordWithHostname:hostname cacheKey:cacheKey];
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed for %@", cacheKey);
}
// Verify all records were created
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the created record for %@", cacheKey);
XCTAssertEqualObjects(fetchedRecord.cacheKey, cacheKey, @"Cache key should match");
XCTAssertEqualObjects(fetchedRecord.hostName, hostname, @"Hostname should match");
}
}
#pragma mark - Update Tests
- (void)testUpdateRecord {
// Create a test record
HttpdnsHostRecord *record = [self createTestRecordWithHostname:@"update.example.com" cacheKey:@"update_cache_key"];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed");
// Fetch the record to get its createAt timestamp
HttpdnsHostRecord *originalRecord = [self.db selectByCacheKey:@"update_cache_key"];
NSDate *originalCreateAt = originalRecord.createAt;
NSDate *originalModifyAt = originalRecord.modifyAt;
// Wait a moment to ensure timestamps will be different
[NSThread sleepForTimeInterval:0.1];
// Update the record with new values
HttpdnsHostRecord *updatedRecord = [[HttpdnsHostRecord alloc] initWithId:originalRecord.id
cacheKey:@"update_cache_key"
hostName:@"updated.example.com" // Changed hostname
createAt:[NSDate date] // Try to change createAt
modifyAt:[NSDate date]
clientIp:@"10.0.0.1" // Changed
v4ips:@[@"10.0.0.2", @"10.0.0.3"] // Changed
v4ttl:600 // Changed
v4LookupTime:originalRecord.v4LookupTime + 1000
v6ips:@[@"2001:db8::1", @"2001:db8::2"] // Changed
v6ttl:1200 // Changed
v6LookupTime:originalRecord.v6LookupTime + 1000
extra:@"updated"]; // Changed
// Update the record
result = [self.db createOrUpdate:updatedRecord];
XCTAssertTrue(result, @"Record update should succeed");
// Fetch the updated record
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"update_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the updated record");
// Verify the updated values
XCTAssertEqualObjects(fetchedRecord.hostName, @"updated.example.com", @"Hostname should be updated");
XCTAssertEqualObjects(fetchedRecord.clientIp, @"10.0.0.1", @"Client IP should be updated");
XCTAssertEqual(fetchedRecord.v4ips.count, 2, @"Should have 2 IPv4 addresses");
XCTAssertEqualObjects(fetchedRecord.v4ips[0], @"10.0.0.2", @"IPv4 address should be updated");
XCTAssertEqual(fetchedRecord.v4ttl, 600, @"v4ttl should be updated");
XCTAssertEqual(fetchedRecord.v6ips.count, 2, @"Should have 2 IPv6 addresses");
XCTAssertEqual(fetchedRecord.v6ttl, 1200, @"v6ttl should be updated");
XCTAssertEqualObjects(fetchedRecord.extra, @"updated", @"Extra data should be updated");
// Verify createAt was preserved and modifyAt was updated
XCTAssertEqualWithAccuracy([fetchedRecord.createAt timeIntervalSince1970],
[originalCreateAt timeIntervalSince1970],
0.001,
@"createAt should not change on update");
XCTAssertTrue([fetchedRecord.modifyAt timeIntervalSinceDate:originalModifyAt] > 0,
@"modifyAt should be updated to a later time");
}
- (void)testUpdateNonExistentRecord {
// Create a record that doesn't exist in the database
HttpdnsHostRecord *record = [self createTestRecordWithHostname:@"nonexistent.example.com" cacheKey:@"nonexistent_cache_key"];
// Update (which should actually create) the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"createOrUpdate should succeed for non-existent record");
// Verify the record was created
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"nonexistent_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the created record");
}
#pragma mark - Query Tests
- (void)testSelectByCacheKey {
// Create a test record
HttpdnsHostRecord *record = [self createTestRecordWithHostname:@"query.example.com" cacheKey:@"query_cache_key"];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed");
// Query the record
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"query_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the record");
XCTAssertEqualObjects(fetchedRecord.cacheKey, @"query_cache_key", @"Cache key should match");
XCTAssertEqualObjects(fetchedRecord.hostName, @"query.example.com", @"Hostname should match");
}
- (void)testSelectNonExistentRecord {
// Query a record that doesn't exist
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"nonexistent_cache_key"];
XCTAssertNil(fetchedRecord, @"Should return nil for non-existent record");
}
- (void)testSelectWithNilCacheKey {
// Query with nil cacheKey
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:nil];
XCTAssertNil(fetchedRecord, @"Should return nil for nil cacheKey");
}
#pragma mark - Delete Tests
- (void)testDeleteByCacheKey {
// Create a test record
HttpdnsHostRecord *record = [self createTestRecordWithHostname:@"delete.example.com" cacheKey:@"delete_cache_key"];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed");
// Verify the record exists
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"delete_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Record should exist before deletion");
// Delete the record
result = [self.db deleteByCacheKey:@"delete_cache_key"];
XCTAssertTrue(result, @"Record deletion should succeed");
// Verify the record was deleted
fetchedRecord = [self.db selectByCacheKey:@"delete_cache_key"];
XCTAssertNil(fetchedRecord, @"Record should be deleted");
}
- (void)testDeleteByHostname {
// Create multiple records with the same hostname but different cache keys
NSString *hostname = @"delete_multiple.example.com";
NSArray<NSString *> *cacheKeys = @[@"delete_cache_key_1", @"delete_cache_key_2", @"delete_cache_key_3"];
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *record = [self createTestRecordWithHostname:hostname cacheKey:cacheKey];
[self.db createOrUpdate:record];
}
// Verify records exist
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNotNil(fetchedRecord, @"Record should exist before deletion");
}
// Delete all records with the same hostname
BOOL result = [self.db deleteByHostNameArr:@[hostname]];
XCTAssertTrue(result, @"Deleting records by hostname should succeed");
// Verify all records were deleted
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNil(fetchedRecord, @"Record should be deleted");
}
}
- (void)testDeleteNonExistentRecord {
// Delete a record that doesn't exist
BOOL result = [self.db deleteByCacheKey:@"nonexistent_cache_key"];
XCTAssertTrue(result, @"Deleting non-existent record should still return success");
}
- (void)testDeleteWithNilCacheKey {
// Delete with nil cacheKey
BOOL result = [self.db deleteByCacheKey:nil];
XCTAssertFalse(result, @"Deleting with nil cacheKey should fail");
}
- (void)testDeleteAll {
// Create multiple test records
NSArray<NSString *> *hostnames = @[@"host1.example.com", @"host2.example.com", @"host3.example.com"];
NSArray<NSString *> *cacheKeys = @[@"cache_key_1", @"cache_key_2", @"cache_key_3"];
for (NSInteger i = 0; i < hostnames.count; i++) {
HttpdnsHostRecord *record = [self createTestRecordWithHostname:hostnames[i] cacheKey:cacheKeys[i]];
[self.db createOrUpdate:record];
}
// Verify records exist
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNotNil(fetchedRecord, @"Record should exist before deletion");
}
// Delete all records
BOOL result = [self.db deleteAll];
XCTAssertTrue(result, @"Deleting all records should succeed");
// Verify all records were deleted
for (NSString *cacheKey in cacheKeys) {
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNil(fetchedRecord, @"Record should be deleted");
}
}
#pragma mark - Timestamp Tests
- (void)testCreateAtPreservation {
// Create a test record with a specific createAt time
NSDate *pastDate = [NSDate dateWithTimeIntervalSinceNow:-3600]; // 1 hour ago
HttpdnsHostRecord *record = [[HttpdnsHostRecord alloc] initWithId:1
cacheKey:@"timestamp_cache_key"
hostName:@"timestamp.example.com"
createAt:pastDate
modifyAt:[NSDate date]
clientIp:@"192.168.1.1"
v4ips:@[@"192.168.1.2"]
v4ttl:300
v4LookupTime:1000
v6ips:@[]
v6ttl:0
v6LookupTime:0
extra:@""];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed");
// Fetch the record
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:@"timestamp_cache_key"];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the record");
// Verify createAt was preserved
// 使
XCTAssertEqualWithAccuracy([fetchedRecord.createAt timeIntervalSince1970],
[[NSDate date] timeIntervalSince1970],
0.001,
@"createAt should be preserved");
// Update the record
HttpdnsHostRecord *updatedRecord = [[HttpdnsHostRecord alloc] initWithId:fetchedRecord.id
cacheKey:@"timestamp_cache_key"
hostName:@"timestamp.example.com"
createAt:[NSDate date] // Try to change createAt
modifyAt:[NSDate date]
clientIp:@"10.0.0.1"
v4ips:@[@"10.0.0.2"]
v4ttl:600
v4LookupTime:2000
v6ips:@[]
v6ttl:0
v6LookupTime:0
extra:@""];
// Update the record
result = [self.db createOrUpdate:updatedRecord];
XCTAssertTrue(result, @"Record update should succeed");
// Fetch the updated record
HttpdnsHostRecord *updatedFetchedRecord = [self.db selectByCacheKey:@"timestamp_cache_key"];
XCTAssertNotNil(updatedFetchedRecord, @"Should be able to fetch the updated record");
// Verify createAt was still preserved after update
XCTAssertEqualWithAccuracy([updatedFetchedRecord.createAt timeIntervalSince1970],
[fetchedRecord.createAt timeIntervalSince1970],
0.001,
@"createAt should still be preserved after update");
// Verify modifyAt was updated
XCTAssertTrue([updatedFetchedRecord.modifyAt timeIntervalSinceDate:fetchedRecord.modifyAt] > 0,
@"modifyAt should be updated to a later time");
}
#pragma mark - Concurrency Tests
- (void)testConcurrentAccess {
// Create a dispatch group for synchronization
dispatch_group_t group = dispatch_group_create();
// Create a concurrent queue
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
// Number of concurrent operations
NSInteger operationCount = 10;
// Perform concurrent operations
for (NSInteger i = 0; i < operationCount; i++) {
dispatch_group_enter(group);
dispatch_async(concurrentQueue, ^{
NSString *hostname = [NSString stringWithFormat:@"concurrent%ld.example.com", (long)i];
NSString *cacheKey = [NSString stringWithFormat:@"concurrent_cache_key_%ld", (long)i];
HttpdnsHostRecord *record = [self createTestRecordWithHostname:hostname cacheKey:cacheKey];
// Insert the record
BOOL result = [self.db createOrUpdate:record];
XCTAssertTrue(result, @"Record creation should succeed in concurrent operation");
// Query the record
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch the record in concurrent operation");
dispatch_group_leave(group);
});
}
// Wait for all operations to complete with a timeout
XCTAssertEqual(dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)), 0,
@"All concurrent operations should complete within timeout");
// Verify all records were created
for (NSInteger i = 0; i < operationCount; i++) {
NSString *cacheKey = [NSString stringWithFormat:@"concurrent_cache_key_%ld", (long)i];
HttpdnsHostRecord *fetchedRecord = [self.db selectByCacheKey:cacheKey];
XCTAssertNotNil(fetchedRecord, @"Should be able to fetch all records after concurrent operations");
}
}
#pragma mark - Helper Methods
- (HttpdnsHostRecord *)createTestRecordWithHostname:(NSString *)hostname cacheKey:(NSString *)cacheKey {
return [[HttpdnsHostRecord alloc] initWithId:1
cacheKey:cacheKey
hostName:hostname
createAt:[NSDate date]
modifyAt:[NSDate date]
clientIp:@"192.168.1.1"
v4ips:@[@"192.168.1.2", @"192.168.1.3"]
v4ttl:300
v4LookupTime:1000
v6ips:@[@"2001:db8::1"]
v6ttl:600
v6LookupTime:2000
extra:@"value"];
}
@end

View File

@@ -0,0 +1,87 @@
//
// CacheKeyFunctionTest.m
// AlicloudHttpDNSTests
//
// Created by xuyecan on 2024/6/12.
// Copyright © 2024 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TestBase.h"
@interface CacheKeyFunctionTest : TestBase
@end
static NSString *sdnsHost = @"sdns1.onlyforhttpdnstest.run.place";
@implementation CacheKeyFunctionTest
+ (void)setUp {
[super setUp];
}
- (void)setUp {
[super setUp];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
});
[self.httpdns setLogEnabled:YES];
[self.httpdns setPersistentCacheIPEnabled:YES];
[self.httpdns setReuseExpiredIPEnabled:NO];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)testSimpleSpecifyingCacheKeySituation {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSString *testHost = hostNameIpPrefixMap.allKeys.firstObject;
NSString *cacheKey = [NSString stringWithFormat:@"cacheKey-%@", testHost];
__block NSString *ipPrefix = hostNameIpPrefixMap[testHost];
// 使ttl
[self.httpdns setTtlDelegate:nil];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeIpv4 withSdnsParams:nil sdnsCacheKey:cacheKey];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:testHost]);
XCTAssertGreaterThan(result.ttl, 0);
// ipip
XCTAssertLessThan([[NSDate date] timeIntervalSince1970], result.lastUpdatedTimeInterval + result.ttl);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [testHost UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[NSThread sleepForTimeInterval:3];
//
[self.httpdns.requestManager cleanAllHostMemoryCache];
// db
[self.httpdns.requestManager syncLoadCacheFromDbToMemory];
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:testHost byIpType:HttpdnsQueryIPTypeIpv4];
// 使cacheKeynil
XCTAssertNil(result);
result = [self.httpdns resolveHostSyncNonBlocking:testHost byIpType:HttpdnsQueryIPTypeIpv4 withSdnsParams:nil sdnsCacheKey:cacheKey];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:testHost]);
NSString *firstIp = [result firstIpv4Address];
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
}
@end

View File

@@ -0,0 +1,108 @@
//
// CustomTTLAndCleanCacheTest.m
// AlicloudHttpDNS
//
// Created by xuyecan on 2024/6/17.
// Copyright © 2024 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <stdatomic.h>
#import <mach/mach.h>
#import "HttpdnsService.h"
#import "HttpdnsRemoteResolver.h"
#import "TestBase.h"
static int TEST_CUSTOM_TTL_SECOND = 3;
@interface CustomTTLAndCleanCacheTest : TestBase<HttpdnsTTLDelegate>
@end
@implementation CustomTTLAndCleanCacheTest
- (void)setUp {
[super setUp];
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
[self.httpdns setLogEnabled:YES];
[self.httpdns setTtlDelegate:self];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
- (int64_t)httpdnsHost:(NSString *)host ipType:(AlicloudHttpDNS_IPType)ipType ttl:(int64_t)ttl {
// ttl3
NSString *testHost = hostNameIpPrefixMap.allKeys.firstObject;
if ([host isEqual:testHost]) {
return TEST_CUSTOM_TTL_SECOND;
}
return ttl;
}
- (void)testCustomTTL {
[self presetNetworkEnvAsIpv4];
[self.httpdns cleanAllHostCache];
NSString *testHost = hostNameIpPrefixMap.allKeys.firstObject;
NSString *expectedIpPrefix = hostNameIpPrefixMap[testHost];
HttpdnsRemoteResolver *resolver = [HttpdnsRemoteResolver new];
id mockResolver = OCMPartialMock(resolver);
__block int invokeCount = 0;
OCMStub([mockResolver resolve:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(^(NSInvocation *invocation) {
invokeCount++;
})
.andForwardToRealObject();
id mockResolverClass = OCMClassMock([HttpdnsRemoteResolver class]);
OCMStub([mockResolverClass new]).andReturn(mockResolver);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, TEST_CUSTOM_TTL_SECOND);
XCTAssertTrue([result.firstIpv4Address hasPrefix:expectedIpPrefix]);
XCTAssertEqual(invokeCount, 1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, TEST_CUSTOM_TTL_SECOND);
XCTAssertTrue([result.firstIpv4Address hasPrefix:expectedIpPrefix]);
XCTAssertEqual(invokeCount, 1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[NSThread sleepForTimeInterval:TEST_CUSTOM_TTL_SECOND + 1];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, TEST_CUSTOM_TTL_SECOND);
XCTAssertTrue([result.firstIpv4Address hasPrefix:expectedIpPrefix]);
XCTAssertEqual(invokeCount, 2);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
@end

View File

@@ -0,0 +1,101 @@
//
// EnableReuseExpiredIpTest.m
// AlicloudHttpDNSTests
//
// Created by xuyecan on 2024/5/28.
// Copyright © 2024 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TestBase.h"
@interface EnableReuseExpiredIpTest : TestBase <HttpdnsTTLDelegate>
@end
static int ttlForTest = 3;
@implementation EnableReuseExpiredIpTest
+ (void)setUp {
[super setUp];
}
- (void)setUp {
[super setUp];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
});
[self.httpdns setLogEnabled:YES];
[self.httpdns setReuseExpiredIPEnabled:YES];
[self.httpdns setTtlDelegate:self];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (int64_t)httpdnsHost:(NSString *)host ipType:(AlicloudHttpDNS_IPType)ipType ttl:(int64_t)ttl {
//
return ttlForTest;
}
- (void)testReuseExpiredIp {
NSString *host = hostNameIpPrefixMap.allKeys.firstObject;
NSString *ipPrefix = hostNameIpPrefixMap[host];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//
[self.httpdns.requestManager cleanAllHostMemoryCache];
//
HttpdnsResult *result = [self.httpdns resolveHostSync:host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:host]);
XCTAssertGreaterThan(result.ttl, 0);
XCTAssertLessThanOrEqual(result.ttl, ttlForTest);
XCTAssertLessThan([[NSDate date] timeIntervalSince1970], result.lastUpdatedTimeInterval + result.ttl);
NSString *firstIp = [result firstIpv4Address];
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
//
[NSThread sleepForTimeInterval:ttlForTest + 1];
//
HttpdnsResult *result2 = [self.httpdns resolveHostSync:host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result2);
XCTAssertTrue([result2.host isEqualToString:host]);
XCTAssertGreaterThan(result2.ttl, 0);
XCTAssertLessThanOrEqual(result2.ttl, ttlForTest);
//
XCTAssertGreaterThan([[NSDate date] timeIntervalSince1970], result2.lastUpdatedTimeInterval + result2.ttl);
NSString *firstIp2 = [result2 firstIpv4Address];
XCTAssertTrue([firstIp2 hasPrefix:ipPrefix]);
//
[NSThread sleepForTimeInterval:1];
// 使nonblocking
HttpdnsResult *result3 = [self.httpdns resolveHostSyncNonBlocking:host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result3);
XCTAssertTrue([result3.host isEqualToString:host]);
XCTAssertGreaterThan(result3.ttl, 0);
XCTAssertLessThanOrEqual(result3.ttl, ttlForTest);
//
XCTAssertLessThan([[NSDate date] timeIntervalSince1970], result3.lastUpdatedTimeInterval + result3.ttl);
NSString *firstIp3 = [result3 firstIpv4Address];
XCTAssertTrue([firstIp3 hasPrefix:ipPrefix]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
@end

View File

@@ -0,0 +1,165 @@
//
// HttpdnsHostObjectTest.m
// AlicloudHttpDNSTests
//
// Created by xuyecan on 2025/3/14.
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "../Testbase/TestBase.h"
#import "HttpdnsHostObject.h"
#import "HttpdnsIpObject.h"
#import <OCMock/OCMock.h>
@interface HttpdnsHostObjectTest : TestBase
@end
@implementation HttpdnsHostObjectTest
- (void)setUp {
[super setUp];
}
- (void)tearDown {
[super tearDown];
}
#pragma mark -
- (void)testHostObjectProperties {
// HttpdnsHostObject
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
//
hostObject.host = @"example.com";
hostObject.ttl = 60;
hostObject.queryTimes = 1;
hostObject.clientIP = @"192.168.1.1";
//
XCTAssertEqualObjects(hostObject.host, @"example.com", @"host属性应该被正确设置");
XCTAssertEqual(hostObject.ttl, 60, @"ttl属性应该被正确设置");
XCTAssertEqual(hostObject.queryTimes, 1, @"queryTimes属性应该被正确设置");
XCTAssertEqualObjects(hostObject.clientIP, @"192.168.1.1", @"clientIP属性应该被正确设置");
}
#pragma mark - IP
- (void)testIpObjectProperties {
// HttpdnsIpObject
HttpdnsIpObject *ipObject = [[HttpdnsIpObject alloc] init];
//
ipObject.ip = @"1.2.3.4";
ipObject.ttl = 300;
ipObject.priority = 10;
ipObject.detectRT = 50; // detectRT
//
XCTAssertEqualObjects(ipObject.ip, @"1.2.3.4", @"ip属性应该被正确设置");
XCTAssertEqual(ipObject.ttl, 300, @"ttl属性应该被正确设置");
XCTAssertEqual(ipObject.priority, 10, @"priority属性应该被正确设置");
XCTAssertEqual(ipObject.detectRT, 50, @"detectRT属性应该被正确设置");
}
- (void)testIpObjectDetectRTMethods {
// HttpdnsIpObject
HttpdnsIpObject *ipObject = [[HttpdnsIpObject alloc] init];
//
XCTAssertEqual(ipObject.detectRT, -1, @"detectRT的默认值应该是-1");
//
[ipObject setDetectRT:100];
XCTAssertEqual(ipObject.detectRT, 100, @"detectRT应该被正确设置为100");
//
[ipObject setDetectRT:-5];
XCTAssertEqual(ipObject.detectRT, -1, @"设置负值时detectRT应该被设置为-1");
// 0
[ipObject setDetectRT:0];
XCTAssertEqual(ipObject.detectRT, 0, @"detectRT应该被正确设置为0");
}
#pragma mark - IP
- (void)testHostObjectIpManagement {
// HttpdnsHostObject
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
hostObject.host = @"example.com";
// IP
HttpdnsIpObject *ipv4Object = [[HttpdnsIpObject alloc] init];
ipv4Object.ip = @"1.2.3.4";
ipv4Object.ttl = 300;
ipv4Object.detectRT = 50;
HttpdnsIpObject *ipv6Object = [[HttpdnsIpObject alloc] init];
ipv6Object.ip = @"2001:db8::1";
ipv6Object.ttl = 600;
ipv6Object.detectRT = 80;
// IP
[hostObject addIpv4:ipv4Object];
[hostObject addIpv6:ipv6Object];
// IP
XCTAssertEqual(hostObject.ipv4List.count, 1, @"应该有1个IPv4对象");
XCTAssertEqual(hostObject.ipv6List.count, 1, @"应该有1个IPv6对象");
// IP
HttpdnsIpObject *retrievedIpv4 = hostObject.ipv4List.firstObject;
XCTAssertEqualObjects(retrievedIpv4.ip, @"1.2.3.4", @"IPv4地址应该正确");
XCTAssertEqual(retrievedIpv4.detectRT, 50, @"IPv4的detectRT应该正确");
HttpdnsIpObject *retrievedIpv6 = hostObject.ipv6List.firstObject;
XCTAssertEqualObjects(retrievedIpv6.ip, @"2001:db8::1", @"IPv6地址应该正确");
XCTAssertEqual(retrievedIpv6.detectRT, 80, @"IPv6的detectRT应该正确");
}
#pragma mark - IP
- (void)testIpSortingByDetectRT {
// HttpdnsHostObject
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
hostObject.host = @"example.com";
// IP
HttpdnsIpObject *ip1 = [[HttpdnsIpObject alloc] init];
ip1.ip = @"1.1.1.1";
ip1.detectRT = 100;
HttpdnsIpObject *ip2 = [[HttpdnsIpObject alloc] init];
ip2.ip = @"2.2.2.2";
ip2.detectRT = 50;
HttpdnsIpObject *ip3 = [[HttpdnsIpObject alloc] init];
ip3.ip = @"3.3.3.3";
ip3.detectRT = 200;
HttpdnsIpObject *ip4 = [[HttpdnsIpObject alloc] init];
ip4.ip = @"4.4.4.4";
ip4.detectRT = -1; //
// IP
[hostObject addIpv4:ip1];
[hostObject addIpv4:ip2];
[hostObject addIpv4:ip3];
[hostObject addIpv4:ip4];
// IP
NSArray<HttpdnsIpObject *> *sortedIps = [hostObject sortedIpv4List];
//
// ip2(50ms) -> ip1(100ms) -> ip3(200ms) -> ip4(-1ms)
XCTAssertEqual(sortedIps.count, 4, @"应该有4个IP对象");
XCTAssertEqualObjects(sortedIps[0].ip, @"2.2.2.2", @"检测时间最短的IP应该排在第一位");
XCTAssertEqualObjects(sortedIps[1].ip, @"1.1.1.1", @"检测时间第二短的IP应该排在第二位");
XCTAssertEqualObjects(sortedIps[2].ip, @"3.3.3.3", @"检测时间第三短的IP应该排在第三位");
XCTAssertEqualObjects(sortedIps[3].ip, @"4.4.4.4", @"未检测的IP应该排在最后");
}
@end

View File

@@ -0,0 +1,156 @@
//
// ManuallyCleanCacheTest.m
// AlicloudHttpDNSTests
//
// Created by xuyecan on 2024/6/17.
// Copyright © 2024 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <stdatomic.h>
#import <mach/mach.h>
#import "HttpdnsService.h"
#import "HttpdnsRemoteResolver.h"
#import "TestBase.h"
static int TEST_CUSTOM_TTL_SECOND = 3;
@interface ManuallyCleanCacheTest : TestBase
@end
@implementation ManuallyCleanCacheTest
- (void)setUp {
[super setUp];
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
[self.httpdns setLogEnabled:YES];
[self.httpdns setIPv6Enabled:YES];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
- (void)testCleanSingleHost {
[self presetNetworkEnvAsIpv4];
[self.httpdns cleanAllHostCache];
NSString *testHost = ipv4OnlyHost;
HttpdnsHostObject *hostObject = [self constructSimpleIpv4HostObject];
[hostObject setV4TTL:60];
__block NSArray *mockResolverHostObjects = @[hostObject];
HttpdnsRemoteResolver *resolver = [HttpdnsRemoteResolver new];
id mockResolver = OCMPartialMock(resolver);
__block int invokeCount = 0;
OCMStub([mockResolver resolve:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(^(NSInvocation *invocation) {
invokeCount++;
})
.andReturn(mockResolverHostObjects);
id mockResolverClass = OCMClassMock([HttpdnsRemoteResolver class]);
OCMStub([mockResolverClass new]).andReturn(mockResolver);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, 60);
XCTAssertEqual(invokeCount, 1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self.httpdns cleanHostCache:@[@"invalidhostofcourse"]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, 60);
XCTAssertEqual(invokeCount, 1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self.httpdns cleanHostCache:@[testHost]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, 60);
XCTAssertEqual(invokeCount, 2);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
- (void)testCleanAllHost {
[self presetNetworkEnvAsIpv4AndIpv6];
[self.httpdns cleanAllHostCache];
NSString *testHost = ipv4OnlyHost;
HttpdnsHostObject *hostObject = [self constructSimpleIpv4HostObject];
[hostObject setV4TTL:60];
HttpdnsRemoteResolver *resolver = [HttpdnsRemoteResolver new];
id mockResolver = OCMPartialMock(resolver);
__block int invokeCount = 0;
__block NSArray *mockResolverHostObjects = @[hostObject];
OCMStub([mockResolver resolve:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(^(NSInvocation *invocation) {
invokeCount++;
})
.andReturn(mockResolverHostObjects);
id mockResolverClass = OCMClassMock([HttpdnsRemoteResolver class]);
OCMStub([mockResolverClass new]).andReturn(mockResolver);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, 60);
XCTAssertEqual(invokeCount, 1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self.httpdns cleanHostCache:@[@"invalidhostofcourse"]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, 60);
XCTAssertEqual(invokeCount, 1);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self.httpdns cleanAllHostCache];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:testHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertEqual(result.ttl, 60);
XCTAssertEqual(invokeCount, 2);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
@end

View File

@@ -0,0 +1,363 @@
//
// MultithreadCorrectnessTest.m
// AlicloudHttpDNSTests
//
// Created by xuyecan on 2024/5/26.
// Copyright © 2024 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <stdatomic.h>
#import <mach/mach.h>
#import "HttpdnsService.h"
#import "HttpdnsRemoteResolver.h"
#import "HttpdnsRequest_Internal.h"
#import "TestBase.h"
@interface MultithreadCorrectnessTest : TestBase
@end
@implementation MultithreadCorrectnessTest
- (void)setUp {
[super setUp];
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
[self.httpdns setLogEnabled:YES];
[self.httpdns setLogHandler:self];
[self.httpdns setTimeoutInterval:2];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
// 线
- (void)testNoneBlockingMethodShouldNotBlock {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:3];
});
[mockedScheduler cleanAllHostMemoryCache];
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
[self.httpdns resolveHostSyncNonBlocking:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime < 1, @"elapsedTime should be less than 1s, but is %f", elapsedTime);
}
// 线线
- (void)testBlockingMethodShouldNotBlockIfInMainThread {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:3];
});
[mockedScheduler cleanAllHostMemoryCache];
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
[self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime < 1, @"elapsedTime should be less than 1s, but is %f", elapsedTime);
}
// 线
- (void)testBlockingMethodShouldBlockIfInBackgroundThread {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:2];
});
[mockedScheduler cleanAllHostMemoryCache];
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime >= 2, @"elapsedTime should be more than 2s, but is %f", elapsedTime);
}
// 线
- (void)testBlockingMethodShouldBlockIfInBackgroundThreadWithSpecifiedMaxWaitTime {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:3];
});
[mockedScheduler cleanAllHostMemoryCache];
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsRequest *request = [HttpdnsRequest new];
request.host = ipv4OnlyHost;
request.queryIpType = HttpdnsQueryIPTypeIpv4;
request.resolveTimeoutInSecond = 3;
[self.httpdns resolveHostSync:request];
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime >= 3, @"elapsedTime should be more than 3s, but is %f", elapsedTime);
}
- (void)testResolveSameHostShouldWaitForTheFirstOne {
__block HttpdnsHostObject *ipv4HostObject = [self constructSimpleIpv4HostObject];
HttpdnsRemoteResolver *realResolver = [HttpdnsRemoteResolver new];
id mockResolver = OCMPartialMock(realResolver);
__block NSArray *mockResolverHostObjects = @[ipv4HostObject];
OCMStub([mockResolver resolve:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
// 1.5
[NSThread sleepForTimeInterval:1.5];
[invocation setReturnValue:&mockResolverHostObjects];
});
id mockResolverClass = OCMClassMock([HttpdnsRemoteResolver class]);
OCMStub([mockResolverClass new]).andReturn(mockResolver);
[self.httpdns.requestManager cleanAllHostMemoryCache];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
});
//
[NSThread sleepForTimeInterval:0.5];
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//
//
// 1
HttpdnsResult *result = [self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4OnlyHost]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime >= 1, @"elapsedTime should be more than 1s, but is %f", elapsedTime);
XCTAssert(elapsedTime <= 1.5, @"elapsedTime should not be more than 1.5s, but is %f", elapsedTime);
// TODO
// XCTAssert(elapsedTime < 4.1, @"elapsedTime should be less than 4.1s, but is %f", elapsedTime);
}
- (void)testResolveSameHostShouldRequestAgainAfterFirstFailed {
__block HttpdnsHostObject *ipv4HostObject = [self constructSimpleIpv4HostObject];
HttpdnsRemoteResolver *realResolver = [HttpdnsRemoteResolver new];
id mockResolver = OCMPartialMock(realResolver);
__block atomic_int count = 0;
__block NSArray *mockResolverHostObjects = @[ipv4HostObject];
OCMStub([mockResolver resolve:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
int localCount = atomic_fetch_add(&count, 1) + 1;
if (localCount == 1) {
[NSThread sleepForTimeInterval:0.4];
//
@throw [NSException exceptionWithName:@"TestException" reason:@"TestException" userInfo:nil];
} else {
//
[NSThread sleepForTimeInterval:0.4];
[invocation setReturnValue:&mockResolverHostObjects];
}
});
id mockResolverClass = OCMClassMock([HttpdnsRemoteResolver class]);
OCMStub([mockResolverClass new]).andReturn(mockResolver);
[self.httpdns.requestManager cleanAllHostMemoryCache];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
});
//
[NSThread sleepForTimeInterval:0.2];
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//
//
// 5
HttpdnsResult *result = [self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4OnlyHost]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime >= 0.6, @"elapsedTime should be more than 0.6s, but is %f", elapsedTime);
XCTAssert(elapsedTime < 0.8, @"elapsedTime should be less than 0.8s, but is %f", elapsedTime);
}
//
- (void)testSyncMethodSetBlockTimeout {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
[self.httpdns cleanAllHostCache];
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:5];
})
.andReturn([self constructSimpleIpv4HostObject]);
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
HttpdnsRequest *request = [HttpdnsRequest new];
request.host = ipv4OnlyHost;
request.queryIpType = HttpdnsQueryIPTypeIpv4;
request.resolveTimeoutInSecond = 2.5;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:request];
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime < 2.6, @"elapsedTime should be less than 2.6s, but is %f", elapsedTime);
XCTAssert(elapsedTime >= 2.5, @"elapsedTime should be greater than or equal to 2.5s, but is %f", elapsedTime);
XCTAssertNil(result);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
// 0.5 - 5
- (void)testLimitResolveTimeoutRange {
HttpdnsRequest *request = [HttpdnsRequest new];
request.host = ipv4OnlyHost;
request.queryIpType = HttpdnsQueryIPTypeAuto;
request.resolveTimeoutInSecond = 0.2;
[request ensureResolveTimeoutInReasonableRange];
XCTAssertGreaterThanOrEqual(request.resolveTimeoutInSecond, 0.5);
request.resolveTimeoutInSecond = 5.1;
[request ensureResolveTimeoutInReasonableRange];
XCTAssertLessThanOrEqual(request.resolveTimeoutInSecond, 5);
request.resolveTimeoutInSecond = 3.5;
[request ensureResolveTimeoutInReasonableRange];
XCTAssertEqual(request.resolveTimeoutInSecond, 3.5);
}
//
- (void)testAsyncMethodSetBlockTimeout {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
[self.httpdns cleanAllHostCache];
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:5];
})
.andReturn([self constructSimpleIpv4HostObject]);
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
HttpdnsRequest *request = [HttpdnsRequest new];
request.host = ipv4OnlyHost;
request.queryIpType = HttpdnsQueryIPTypeIpv4;
request.resolveTimeoutInSecond = 2.5;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.httpdns resolveHostAsync:request completionHandler:^(HttpdnsResult *result) {
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime < 2.6, @"elapsedTime should be less than 2.6s, but is %f", elapsedTime);
XCTAssert(elapsedTime >= 2.5, @"elapsedTime should be greater than or equal to 2.5s, but is %f", elapsedTime);
XCTAssertNil(result);
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
// 线线
- (void)testMultiThreadSyncMethodMaxBlockingTime {
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
[self.httpdns cleanAllHostCache];
HttpdnsRequestManager *mockedScheduler = OCMPartialMock(requestManager);
OCMStub([mockedScheduler executeRequest:[OCMArg any] retryCount:0])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *invocation) {
[NSThread sleepForTimeInterval:5];
})
.andReturn([self constructSimpleIpv4HostObject]);
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
HttpdnsRequest *request = [HttpdnsRequest new];
request.host = ipv4OnlyHost;
request.queryIpType = HttpdnsQueryIPTypeIpv4;
request.resolveTimeoutInSecond = 4.5;
const int threadCount = 10;
NSMutableArray *semaArray = [NSMutableArray new];
for (int i = 0; i < threadCount; i++) {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[semaArray addObject:semaphore];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:request];
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSince1970] - startTime;
XCTAssert(elapsedTime < 4.5 + 0.01, @"elapsedTime should be less than 2.6s, but is %f", elapsedTime);
XCTAssert(elapsedTime >= 4.5, @"elapsedTime should be greater than or equal to 2.5s, but is %f", elapsedTime);
XCTAssertNil(result);
dispatch_semaphore_signal(semaphore);
});
}
for (int i = 0; i < threadCount; i++) {
dispatch_semaphore_wait(semaArray[i], DISPATCH_TIME_FOREVER);
}
}
@end

View File

@@ -0,0 +1,301 @@
//
// PresetCacheAndRetrieveTest.m
// AlicloudHttpDNSTests
//
// Created by xuyecan on 2024/5/26.
// Copyright © 2024 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import "TestBase.h"
#import "HttpdnsHostObject.h"
#import "HttpdnsService.h"
#import "HttpdnsService_Internal.h"
/**
* 使OCMockMock(使stopMocking)
* case
*/
@interface PresetCacheAndRetrieveTest : TestBase
@end
@implementation PresetCacheAndRetrieveTest
+ (void)setUp {
[super setUp];
HttpDnsService *httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
[httpdns setLogEnabled:YES];
}
+ (void)tearDown {
[super tearDown];
}
- (void)setUp {
[super setUp];
self.httpdns = [HttpDnsService sharedInstance];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
// ipv4
- (void)testSimplyRetrieveCachedResultUnderIpv4Only {
[self presetNetworkEnvAsIpv4];
[self.httpdns cleanAllHostCache];
HttpdnsHostObject *hostObject = [self constructSimpleIpv4AndIpv6HostObject];
[self.httpdns.requestManager mergeLookupResultToManager:hostObject host:ipv4AndIpv6Host cacheKey:ipv4AndIpv6Host underQueryIpType:HttpdnsQueryIPTypeBoth];
// ipv4ipv4
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
// ipv6ipv6
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeIpv6];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
// both
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
// autoipv4
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
XCTAssertTrue([result.ipv6s count] == 0);
}
// ipv6
- (void)testSimplyRetrieveCachedResultUnderIpv6Only {
[self presetNetworkEnvAsIpv6];
[self.httpdns cleanAllHostCache];
HttpdnsHostObject *hostObject = [self constructSimpleIpv4AndIpv6HostObject];
[self.httpdns.requestManager mergeLookupResultToManager:hostObject host:ipv4AndIpv6Host cacheKey:ipv4AndIpv6Host underQueryIpType:HttpdnsQueryIPTypeBoth];
// ipv4ipv4
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
// ipv6ipv6
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeIpv6];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
// both
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
// autoipv6only
// ipv4autoipv6ipv6
// ipv4+ipv6
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
}
// ipv4ipv6
- (void)testSimplyRetrieveCachedResultUnderDualStack {
[self presetNetworkEnvAsIpv4AndIpv6];
[self.httpdns cleanAllHostCache];
// ipv4ipv6
HttpdnsHostObject *hostObject = [self constructSimpleIpv4AndIpv6HostObject];
[self.httpdns.requestManager mergeLookupResultToManager:hostObject host:ipv4AndIpv6Host cacheKey:ipv4AndIpv6Host underQueryIpType:HttpdnsQueryIPTypeBoth];
// ipv4
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
// ipv6
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeIpv6];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
// ipv4ipv6
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
//
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4AndIpv6Host]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
XCTAssertTrue([result.ipv6s count] == 2);
XCTAssertTrue([result.ipv6s[0] isEqualToString:ipv61]);
}
// ttllastLookupTimeipv4ipv6
- (void)testTTLAndLastLookUpTime {
[self presetNetworkEnvAsIpv4AndIpv6];
[self.httpdns cleanAllHostCache];
// ipv4ipv6
HttpdnsHostObject *hostObject1 = [self constructSimpleIpv4AndIpv6HostObject];
hostObject1.v4ttl = 200;
hostObject1.v6ttl = 300;
int64_t currentTimestamp = [[NSDate new] timeIntervalSince1970];
hostObject1.lastIPv4LookupTime = currentTimestamp - 1;
hostObject1.lastIPv6LookupTime = currentTimestamp - 2;
//
[self.httpdns.requestManager mergeLookupResultToManager:hostObject1 host:ipv4AndIpv6Host cacheKey:ipv4AndIpv6Host underQueryIpType:HttpdnsQueryIPTypeBoth];
// autoipv4ipv6
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertEqual(result.ttl, hostObject1.v4ttl);
XCTAssertEqual(result.lastUpdatedTimeInterval, hostObject1.lastIPv4LookupTime);
XCTAssertEqual(result.v6ttl, hostObject1.v6ttl);
XCTAssertEqual(result.v6LastUpdatedTimeInterval, hostObject1.lastIPv6LookupTime);
HttpdnsHostObject *hostObject2 = [self constructSimpleIpv4HostObject];
hostObject2.hostName = ipv4AndIpv6Host;
hostObject2.v4ttl = 600;
hostObject2.lastIPv4LookupTime = currentTimestamp - 10;
// ipv4
[self.httpdns.requestManager mergeLookupResultToManager:hostObject2 host:ipv4AndIpv6Host cacheKey:ipv4AndIpv6Host underQueryIpType:HttpdnsQueryIPTypeIpv4];
// v4v6
result = [self.httpdns resolveHostSyncNonBlocking:ipv4AndIpv6Host byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertEqual(result.ttl, hostObject2.v4ttl);
XCTAssertEqual(result.lastUpdatedTimeInterval, hostObject2.lastIPv4LookupTime);
XCTAssertEqual(result.v6ttl, hostObject1.v6ttl);
XCTAssertEqual(result.v6LastUpdatedTimeInterval, hostObject1.lastIPv6LookupTime);
}
// ipv4ipv6
// ipv6ipv6
- (void)testMergeNoIpv6ResultAndGetBoth {
[self presetNetworkEnvAsIpv4AndIpv6];
HttpdnsHostObject *hostObject = [self constructSimpleIpv4HostObject];
// ipv4hostipv6
[self.httpdns.requestManager mergeLookupResultToManager:hostObject host:ipv4OnlyHost cacheKey:ipv4OnlyHost underQueryIpType:HttpdnsQueryIPTypeBoth];
[self shouldNotHaveCallNetworkRequestWhenResolving:^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4OnlyHost]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
result = [self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeAuto];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:ipv4OnlyHost]);
XCTAssertTrue([result.ips count] == 2);
XCTAssertTrue([result.ips[0] isEqualToString:ipv41]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}];
}
// ipv4ipv4ipv6
// ipv6
- (void)testMergeOnlyIpv4ResultAndGetBoth {
[self presetNetworkEnvAsIpv4AndIpv6];
HttpdnsHostObject *hostObject = [self constructSimpleIpv4HostObject];
[self.httpdns.requestManager mergeLookupResultToManager:hostObject host:ipv4OnlyHost cacheKey:ipv4OnlyHost underQueryIpType:HttpdnsQueryIPTypeIpv4];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
// 使线
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self shouldHaveCalledRequestWhenResolving:^{
HttpdnsResult *result = [self.httpdns resolveHostSync:ipv4OnlyHost byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNil(result);
dispatch_semaphore_signal(sema);
}];
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
// ipv6ipv6ipv4
// ipv4
- (void)testMergeOnlyIpv6ResultAndGetBoth {
[self presetNetworkEnvAsIpv4AndIpv6];
HttpdnsHostObject *hostObject = [self constructSimpleIpv6HostObject];
[self.httpdns.requestManager mergeLookupResultToManager:hostObject host:ipv6OnlyHost cacheKey:ipv6OnlyHost underQueryIpType:HttpdnsQueryIPTypeIpv6];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
// 使线
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self shouldHaveCalledRequestWhenResolving:^{
HttpdnsResult *result = [self.httpdns resolveHostSync:ipv6OnlyHost byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNil(result);
dispatch_semaphore_signal(sema);
}];
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
@end

View File

@@ -0,0 +1,265 @@
//
// ResolvingEffectiveHostTest.m
// AlicloudHttpDNSTests
//
// Created by xuyecan on 2024/5/28.
// Copyright © 2024 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <stdatomic.h>
#import <mach/mach.h>
#import "HttpdnsService.h"
#import "HttpdnsRemoteResolver.h"
#import "TestBase.h"
@interface ResolvingEffectiveHostTest : TestBase<HttpdnsTTLDelegate>
@end
@implementation ResolvingEffectiveHostTest
+ (void)setUp {
[super setUp];
}
- (void)setUp {
[super setUp];
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
[self.httpdns setLogEnabled:YES];
[self.httpdns setReuseExpiredIPEnabled:NO];
[self.httpdns setTtlDelegate:self];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
- (int64_t)httpdnsHost:(NSString *)host ipType:(AlicloudHttpDNS_IPType)ipType ttl:(int64_t)ttl {
// ttl1-4
return arc4random_uniform(4) + 1;
}
- (void)testNormalMultipleHostsResolve {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// 使ttl
[self.httpdns setTtlDelegate:nil];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[hostNameIpPrefixMap enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull host, NSString * _Nonnull ipPrefix, BOOL * _Nonnull stop) {
HttpdnsResult *result = [self.httpdns resolveHostSync:host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:host]);
XCTAssertGreaterThan(result.ttl, 0);
// ipip
XCTAssertLessThan([[NSDate date] timeIntervalSince1970], result.lastUpdatedTimeInterval + result.ttl);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
}];
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
- (void)testNonblockingMethodShouldNotBlockDuringMultithreadLongRun {
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
NSTimeInterval testDuration = 10;
int threadCountForEachType = 5;
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
long long executeStartTimeInMs = [[NSDate date] timeIntervalSince1970] * 1000;
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:host byIpType:HttpdnsQueryIPTypeIpv4];
long long executeEndTimeInMs = [[NSDate date] timeIntervalSince1970] * 1000;
// 30ms
if (executeEndTimeInMs - executeStartTimeInMs >= 30) {
printf("XCTAssertWillFailed, host: %s, executeTime: %lldms\n", [host UTF8String], executeEndTimeInMs - executeStartTimeInMs);
}
XCTAssertLessThan(executeEndTimeInMs - executeStartTimeInMs, 30);
if (result) {
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:host]);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
}
[NSThread sleepForTimeInterval:0.1];
}
});
}
[NSThread sleepForTimeInterval:testDuration + 1];
}
- (void)testMultithreadAndMultiHostResolvingForALongRun {
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
NSTimeInterval testDuration = 10;
int threadCountForEachType = 4;
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
HttpdnsResult *result = [self.httpdns resolveHostSync:host byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:host]);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
[NSThread sleepForTimeInterval:0.1];
}
});
}
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
[self.httpdns resolveHostAsync:host byIpType:HttpdnsQueryIPTypeIpv4 completionHandler:^(HttpdnsResult *result) {
XCTAssertNotNil(result);
XCTAssertTrue([result.host isEqualToString:host]);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
}];
[NSThread sleepForTimeInterval:0.1];
}
});
}
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:host byIpType:HttpdnsQueryIPTypeIpv4];
if (result) {
XCTAssertTrue([result.host isEqualToString:host]);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
}
[NSThread sleepForTimeInterval:0.1];
}
});
}
sleep(testDuration + 1);
}
// bothipv4
// ipv6ipv4
- (void)testMultithreadAndMultiHostResolvingForALongRunBySpecifyBothIpv4AndIpv6 {
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
NSTimeInterval testDuration = 10;
int threadCountForEachType = 4;
//
__block int syncCount = 0, asyncCount = 0, syncNonBlockingCount = 0;
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
HttpdnsResult *result = [self.httpdns resolveHostSync:host byIpType:HttpdnsQueryIPTypeBoth];
XCTAssertNotNil(result);
XCTAssertTrue(!result.hasIpv6Address);
XCTAssertTrue([result.host isEqualToString:host]);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
syncCount++;
[NSThread sleepForTimeInterval:0.1];
}
});
}
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
[self.httpdns resolveHostAsync:host byIpType:HttpdnsQueryIPTypeBoth completionHandler:^(HttpdnsResult *result) {
XCTAssertNotNil(result);
XCTAssertTrue(!result.hasIpv6Address);
XCTAssertTrue([result.host isEqualToString:host]);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
asyncCount++;
}];
[NSThread sleepForTimeInterval:0.1];
}
});
}
for (int i = 0; i < threadCountForEachType; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while ([[NSDate date] timeIntervalSince1970] - startTime < testDuration) {
NSString *host = [hostNameIpPrefixMap allKeys][arc4random_uniform((uint32_t)[hostNameIpPrefixMap count])];
NSString *ipPrefix = hostNameIpPrefixMap[host];
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:host byIpType:HttpdnsQueryIPTypeBoth];
if (result) {
XCTAssertTrue([result.host isEqualToString:host]);
XCTAssertTrue(!result.hasIpv6Address);
NSString *firstIp = [result firstIpv4Address];
if (![firstIp hasPrefix:ipPrefix]) {
printf("XCTAssertWillFailed, host: %s, firstIp: %s, ipPrefix: %s\n", [host UTF8String], [firstIp UTF8String], [ipPrefix UTF8String]);
}
XCTAssertTrue([firstIp hasPrefix:ipPrefix]);
}
syncNonBlockingCount++;
[NSThread sleepForTimeInterval:0.1];
}
});
}
sleep(testDuration + 1);
int theoreticalCount = threadCountForEachType * (testDuration / 0.1);
// printf all the counts
printf("syncCount: %d, asyncCount: %d, syncNonBlockingCount: %d, theoreticalCount: %d\n", syncCount, asyncCount, syncNonBlockingCount, theoreticalCount);
}
@end

View File

@@ -0,0 +1,133 @@
//
// ScheduleCenterV4Test.m
// AlicloudHttpDNSTests
//
// Created by xuyecan on 2024/6/16.
// Copyright © 2024 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import "TestBase.h"
#import "HttpdnsHostObject.h"
#import "HttpdnsScheduleCenter.h"
#import "HttpdnsService.h"
#import "HttpdnsService_Internal.h"
#import "HttpdnsRequest_Internal.h"
#import "HttpdnsScheduleExecutor.h"
#import "HttpdnsRemoteResolver.h"
/**
* 使OCMockMock(使stopMocking)
* case
*/
@interface ScheduleCenterV4Test : TestBase
@end
@implementation ScheduleCenterV4Test
+ (void)setUp {
[super setUp];
}
+ (void)tearDown {
[super tearDown];
}
- (void)setUp {
[super setUp];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
});
[self.httpdns setLogEnabled:YES];
[self.httpdns setReuseExpiredIPEnabled:NO];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
- (void)testUpdateFailureWillMoveToNextUpdateServer {
[self presetNetworkEnvAsIpv4];
HttpdnsScheduleExecutor *realRequest = [HttpdnsScheduleExecutor new];
id mockRequest = OCMPartialMock(realRequest);
OCMStub([mockRequest fetchRegionConfigFromServer:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andReturn(nil);
id mockRequestClass = OCMClassMock([HttpdnsScheduleExecutor class]);
OCMStub([mockRequestClass new]).andReturn(mockRequest);
HttpdnsScheduleCenter *scheduleCenter = [[HttpdnsScheduleCenter alloc] initWithAccountId:100000];
NSArray<NSString *> *updateServerHostList = [scheduleCenter currentUpdateServerV4HostList];
int updateServerCount = (int)[updateServerHostList count];
XCTAssertGreaterThan(updateServerCount, 0);
int startIndex = [scheduleCenter currentActiveUpdateServerHostIndex];
// 2
[scheduleCenter asyncUpdateRegionScheduleConfigAtRetry:2];
[NSThread sleepForTimeInterval:0.1];
OCMVerify([mockRequest fetchRegionConfigFromServer:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]]);
int currentIndex = [scheduleCenter currentActiveUpdateServerHostIndex];
XCTAssertEqual((startIndex + 1) % updateServerCount, currentIndex);
for (int i = 0; i < updateServerCount; i++) {
[scheduleCenter asyncUpdateRegionScheduleConfigAtRetry:2];
[NSThread sleepForTimeInterval:0.1];
}
int finalIndex = [scheduleCenter currentActiveUpdateServerHostIndex];
XCTAssertEqual(currentIndex, finalIndex % updateServerCount);
[NSThread sleepForTimeInterval:3];
}
- (void)testResolveFailureWillMoveToNextServiceServer {
[self presetNetworkEnvAsIpv4];
id mockResolver = OCMPartialMock([HttpdnsRemoteResolver new]);
OCMStub([mockResolver resolve:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(^(NSInvocation *invocation) {
NSError *mockError = [NSError errorWithDomain:@"com.example.error" code:123 userInfo:@{NSLocalizedDescriptionKey: @"Mock error"}];
NSError *__autoreleasing *errorPtr = nil;
[invocation getArgument:&errorPtr atIndex:3];
if (errorPtr) {
*errorPtr = mockError;
}
});
id mockResolverClass = OCMClassMock([HttpdnsRemoteResolver class]);
OCMStub([mockResolverClass new]).andReturn(mockResolver);
HttpdnsScheduleCenter *scheduleCenter = [[HttpdnsScheduleCenter alloc] initWithAccountId:100000];
int startIndex = [scheduleCenter currentActiveServiceServerHostIndex];
int serviceServerCount = (int)[scheduleCenter currentServiceServerV4HostList].count;
HttpdnsRequest *request = [[HttpdnsRequest alloc] initWithHost:@"mock" queryIpType:HttpdnsQueryIPTypeAuto];
[request becomeBlockingRequest];
HttpdnsRequestManager *requestManager = self.httpdns.requestManager;
[requestManager executeRequest:request retryCount:1];
int secondIndex = [scheduleCenter currentActiveServiceServerHostIndex];
XCTAssertEqual((startIndex + 1) % serviceServerCount, secondIndex);
}
@end

View File

@@ -0,0 +1,90 @@
//
// ScheduleCenterV6Test.m
// AlicloudHttpDNSTests
//
// Created by xuyecan on 2024/6/17.
// Copyright © 2024 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import "TestBase.h"
#import "HttpdnsHostObject.h"
#import "HttpdnsScheduleExecutor.h"
#import "HttpdnsScheduleCenter.h"
#import "HttpdnsService.h"
#import "HttpdnsService_Internal.h"
#import "HttpdnsUtil.h"
/**
* 使OCMockMock(使stopMocking)
* case
*/
@interface ScheduleCenterV6Test : TestBase
@end
@implementation ScheduleCenterV6Test
+ (void)setUp {
[super setUp];
}
+ (void)tearDown {
[super tearDown];
}
- (void)setUp {
[super setUp];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
});
[self.httpdns setLogEnabled:YES];
[self.httpdns setReuseExpiredIPEnabled:NO];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (void)tearDown {
[super tearDown];
}
- (void)testUpdateFailureWillMoveToNextUpdateServer {
[self presetNetworkEnvAsIpv6];
HttpdnsScheduleExecutor *realRequest = [HttpdnsScheduleExecutor new];
id mockRequest = OCMPartialMock(realRequest);
OCMStub([mockRequest fetchRegionConfigFromServer:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andReturn(nil);
id mockRequestClass = OCMClassMock([HttpdnsScheduleExecutor class]);
OCMStub([mockRequestClass new]).andReturn(mockRequest);
HttpdnsScheduleCenter *scheduleCenter = [[HttpdnsScheduleCenter alloc] initWithAccountId:100000];
NSArray<NSString *> *updateServerHostList = [scheduleCenter currentUpdateServerV4HostList];
int updateServerCount = (int)[updateServerHostList count];
XCTAssertGreaterThan(updateServerCount, 0);
// 2
[scheduleCenter asyncUpdateRegionScheduleConfigAtRetry:2];
[NSThread sleepForTimeInterval:0.1];
NSString *activeUpdateHost = [scheduleCenter getActiveUpdateServerHost];
// ipv4
XCTAssertFalse([HttpdnsUtil isIPv4Address:activeUpdateHost]);
OCMVerify([mockRequest fetchRegionConfigFromServer:[OCMArg any] error:(NSError * __autoreleasing *)[OCMArg anyPointer]]);
[NSThread sleepForTimeInterval:3];
}
@end

View File

@@ -0,0 +1,108 @@
//
// SdnsScenarioTest.m
// AlicloudHttpDNSTests
//
// Created by xuyecan on 2024/5/29.
// Copyright © 2024 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TestBase.h"
@interface SdnsScenarioTest : TestBase <HttpdnsTTLDelegate>
@end
static int ttlForTest = 3;
static NSString *sdnsHost = @"sdns1.onlyforhttpdnstest.run.place";
@implementation SdnsScenarioTest
+ (void)setUp {
[super setUp];
}
- (void)setUp {
[super setUp];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.httpdns = [[HttpDnsService alloc] initWithAccountID:100000];
});
[self.httpdns setLogEnabled:YES];
[self.httpdns setReuseExpiredIPEnabled:NO];
[self.httpdns setTtlDelegate:self];
[self.httpdns setLogHandler:self];
self.currentTimeStamp = [[NSDate date] timeIntervalSince1970];
}
- (int64_t)httpdnsHost:(NSString *)host ipType:(AlicloudHttpDNS_IPType)ipType ttl:(int64_t)ttl {
//
return ttlForTest;
}
- (void)testSimpleSdnsScenario {
NSDictionary *extras = @{
@"testKey": @"testValue",
@"key2": @"value2",
@"key3": @"value3"
};
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:sdnsHost byIpType:HttpdnsQueryIPTypeIpv4 withSdnsParams:extras sdnsCacheKey:nil];
XCTAssertNotNil(result);
XCTAssertNotNil(result.ips);
// 0.0.0.0 FC
XCTAssertTrue([result.ips containsObject:@"0.0.0.0"]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
- (void)testSdnsScenarioUsingCustomCacheKey {
[self.httpdns.requestManager cleanAllHostMemoryCache];
NSDictionary *extras = @{
@"testKey": @"testValue",
@"key2": @"value2",
@"key3": @"value3"
};
NSString *cacheKey = [NSString stringWithFormat:@"abcd_%@", sdnsHost];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HttpdnsResult *result = [self.httpdns resolveHostSync:sdnsHost byIpType:HttpdnsQueryIPTypeIpv4 withSdnsParams:extras sdnsCacheKey:cacheKey];
XCTAssertNotNil(result);
XCTAssertNotNil(result.ips);
// 0.0.0.0 FC
XCTAssertTrue([result.ips containsObject:@"0.0.0.0"]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// cacheKeycacheKeynil
HttpdnsResult *result1 = [self.httpdns resolveHostSyncNonBlocking:sdnsHost byIpType:HttpdnsQueryIPTypeIpv4];
XCTAssertNil(result1);
// 使cachekey
HttpdnsResult *result2 = [self.httpdns resolveHostSync:sdnsHost byIpType:HttpdnsQueryIPTypeIpv4 withSdnsParams:@{} sdnsCacheKey:cacheKey];
XCTAssertNotNil(result2);
XCTAssertNotNil(result2.ips);
// 0.0.0.0 FC
XCTAssertTrue([result2.ips containsObject:@"0.0.0.0"]);
}
@end

View File

@@ -0,0 +1,402 @@
//
// IpDetectorTest.m
// AlicloudHttpDNSTests
//
// Created by xuyecan on 2025/3/14.
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "../Testbase/TestBase.h"
#import "HttpdnsIPQualityDetector.h"
@interface IpDetectorTest : TestBase
@end
@implementation IpDetectorTest
- (void)setUp {
[super setUp];
// 使maxConcurrentDetections
}
- (void)tearDown {
[super tearDown];
}
#pragma mark -
- (void)testSharedInstance {
//
HttpdnsIPQualityDetector *detector1 = [HttpdnsIPQualityDetector sharedInstance];
HttpdnsIPQualityDetector *detector2 = [HttpdnsIPQualityDetector sharedInstance];
XCTAssertEqual(detector1, detector2, @"单例模式应该返回相同的实例");
XCTAssertNotNil(detector1, @"单例实例不应为nil");
}
#pragma mark - TCP
- (void)testTcpConnectToValidIP {
// IP
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
// 使DNS
NSInteger costTime = [detector tcpConnectToIP:@"8.8.8.8" port:53];
//
XCTAssertGreaterThan(costTime, 0, @"连接到有效IP应返回正数耗时");
}
- (void)testTcpConnectToInvalidIP {
// IP
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
// 使IP
NSInteger costTime = [detector tcpConnectToIP:@"192.168.255.255" port:12345];
// -1
XCTAssertEqual(costTime, -1, @"连接到无效IP应返回-1");
}
- (void)testTcpConnectWithInvalidParameters {
// 使
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
// IP
NSInteger costTime = [detector tcpConnectToIP:nil port:80];
XCTAssertEqual(costTime, -1, @"使用nil IP应返回-1");
// IP
costTime = [detector tcpConnectToIP:@"not-an-ip" port:80];
XCTAssertEqual(costTime, -1, @"使用无效格式IP应返回-1");
//
costTime = [detector tcpConnectToIP:@"8.8.8.8" port:-1];
XCTAssertEqual(costTime, -1, @"使用无效端口应返回-1");
}
#pragma mark -
- (void)testScheduleIPQualityDetection {
// IP
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// executeDetection
OCMExpect([detectorMock executeDetection:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:[OCMArg any]]);
//
[detectorMock scheduleIPQualityDetection:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {
// executeDetection
}];
//
OCMVerifyAll(detectorMock);
//
[detectorMock stopMocking];
}
- (void)testScheduleWithInvalidParameters {
// 使
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// executeDetection
OCMReject([detectorMock executeDetection:[OCMArg any]
ip:[OCMArg any]
port:[OCMArg any]
callback:[OCMArg any]]);
// nil cacheKey
[detectorMock scheduleIPQualityDetection:nil
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {}];
// nil IP
[detectorMock scheduleIPQualityDetection:@"example.com"
ip:nil
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {}];
// nil callback
[detectorMock scheduleIPQualityDetection:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:nil];
//
OCMVerifyAll(detectorMock);
//
[detectorMock stopMocking];
}
- (void)testConcurrencyLimitReached {
//
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// dispatch_semaphore_wait
// scheduleIPQualityDetectionaddPendingTask
OCMStub([detectorMock scheduleIPQualityDetection:[OCMArg any]
ip:[OCMArg any]
port:[OCMArg any]
callback:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
//
NSString *cacheKey;
NSString *ip;
NSNumber *port;
HttpdnsIPQualityCallback callback;
[invocation getArgument:&cacheKey atIndex:2];
[invocation getArgument:&ip atIndex:3];
[invocation getArgument:&port atIndex:4];
[invocation getArgument:&callback atIndex:5];
// addPendingTask
[detector addPendingTask:cacheKey ip:ip port:port callback:callback];
});
// addPendingTaskexecuteDetection
// 使mockmock
OCMExpect([detectorMock addPendingTask:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:[OCMArg any]]);
OCMReject([detectorMock executeDetection:[OCMArg any]
ip:[OCMArg any]
port:[OCMArg any]
callback:[OCMArg any]]);
//
[detectorMock scheduleIPQualityDetection:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {}];
//
OCMVerifyAll(detectorMock);
//
[detectorMock stopMocking];
}
- (void)testAddPendingTask {
//
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// processPendingTasksIfNeeded
OCMStub([detectorMock processPendingTasksIfNeeded]);
//
NSUInteger initialCount = [detector pendingTasksCount];
//
[detectorMock addPendingTask:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {}];
//
XCTAssertEqual([detector pendingTasksCount], initialCount + 1, @"添加任务后待处理任务数量应增加1");
//
[detectorMock stopMocking];
}
- (void)testPendingTasksProcessing {
//
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// executeDetection
OCMStub([detectorMock executeDetection:[OCMArg any]
ip:[OCMArg any]
port:[OCMArg any]
callback:[OCMArg any]]);
//
[detectorMock addPendingTask:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {}];
//
[detectorMock processPendingTasksIfNeeded];
//
[NSThread sleepForTimeInterval:0.1];
//
XCTAssertEqual([detector pendingTasksCount], 0, @"处理后待处理任务数量应为0");
//
[detectorMock stopMocking];
}
#pragma mark -
- (void)testExecuteDetection {
//
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// tcpConnectToIP
OCMStub([detectorMock tcpConnectToIP:@"1.2.3.4" port:80]).andReturn(100);
//
XCTestExpectation *expectation = [self expectationWithDescription:@"回调应被执行"];
//
[detectorMock executeDetection:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {
//
XCTAssertEqualObjects(cacheKey, @"example.com", @"回调中的cacheKey应正确");
XCTAssertEqualObjects(ip, @"1.2.3.4", @"回调中的IP应正确");
XCTAssertEqual(costTime, 100, @"回调中的耗时应正确");
[expectation fulfill];
}];
//
[self waitForExpectationsWithTimeout:5.0 handler:nil];
//
[detectorMock stopMocking];
}
- (void)testExecuteDetectionWithFailure {
//
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// tcpConnectToIP
OCMStub([detectorMock tcpConnectToIP:@"1.2.3.4" port:80]).andReturn(-1);
//
XCTestExpectation *expectation = [self expectationWithDescription:@"失败回调应被执行"];
//
[detectorMock executeDetection:@"example.com"
ip:@"1.2.3.4"
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {
//
XCTAssertEqualObjects(cacheKey, @"example.com", @"回调中的cacheKey应正确");
XCTAssertEqualObjects(ip, @"1.2.3.4", @"回调中的IP应正确");
XCTAssertEqual(costTime, -1, @"连接失败时回调中的耗时应为-1");
[expectation fulfill];
}];
//
[self waitForExpectationsWithTimeout:5.0 handler:nil];
//
[detectorMock stopMocking];
}
- (void)testExecuteDetectionWithNilPort {
// nil
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
// tcpConnectToIP使80
OCMExpect([detectorMock tcpConnectToIP:@"1.2.3.4" port:80]).andReturn(100);
//
XCTestExpectation *expectation = [self expectationWithDescription:@"默认端口回调应被执行"];
//
[detectorMock executeDetection:@"example.com"
ip:@"1.2.3.4"
port:nil
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {
[expectation fulfill];
}];
//
[self waitForExpectationsWithTimeout:5.0 handler:nil];
//
OCMVerifyAll(detectorMock);
//
[detectorMock stopMocking];
}
#pragma mark -
- (void)testMemoryManagementInAsyncOperations {
//
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
id detectorMock = OCMPartialMock(detector);
//
__block NSString *tempCacheKey = [@"example.com" copy];
__block NSString *tempIP = [@"1.2.3.4" copy];
//
__weak NSString *weakCacheKey = tempCacheKey;
__weak NSString *weakIP = tempIP;
// tcpConnectToIP
OCMStub([detectorMock tcpConnectToIP:[OCMArg any] port:80]).andDo(^(NSInvocation *invocation) {
// GC
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//
NSInteger result = 100;
[invocation setReturnValue:&result];
});
});
//
XCTestExpectation *expectation = [self expectationWithDescription:@"内存管理回调应被执行"];
//
[detectorMock executeDetection:tempCacheKey
ip:tempIP
port:[NSNumber numberWithInt:80]
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {
//
XCTAssertEqualObjects(cacheKey, @"example.com", @"回调中的cacheKey应正确");
XCTAssertEqualObjects(ip, @"1.2.3.4", @"回调中的IP应正确");
[expectation fulfill];
}];
//
tempCacheKey = nil;
tempIP = nil;
// GCARC
@autoreleasepool {
//
}
// executeDetection
XCTAssertNotNil(weakCacheKey, @"cacheKey不应被释放");
XCTAssertNotNil(weakIP, @"IP不应被释放");
//
[self waitForExpectationsWithTimeout:5.0 handler:nil];
//
[detectorMock stopMocking];
}
@end

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
server.pem

View File

@@ -0,0 +1,24 @@
//
// HttpdnsNWHTTPClientTestBase.h
// AlicloudHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
// 测试基类 - 为所有 HttpdnsNWHTTPClient 测试提供共享的 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
// AlicloudHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
// -
//
// mock server
// cd AlicloudHttpDNSTests/Network && python3 mock_server.py
//
// - HTTP: 11080
// - HTTPS: 11443, 11444, 11445, 11446
//
#import "HttpdnsNWHTTPClientTestBase.h"
@implementation HttpdnsNWHTTPClientTestBase
- (void)setUp {
[super setUp];
// TLS mock server
//
// 1.
// 2. loopback (127.0.0.1)
// 3.
setenv("HTTPDNS_SKIP_TLS_VERIFY", "1", 1);
self.client = [[HttpdnsNWHTTPClient alloc] init];
}
- (void)tearDown {
//
unsetenv("HTTPDNS_SKIP_TLS_VERIFY");
self.client = nil;
[super tearDown];
}
@end

View File

@@ -0,0 +1,58 @@
//
// HttpdnsNWHTTPClientTestHelper.h
// AlicloudHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface HttpdnsNWHTTPClientTestHelper : NSObject
#pragma mark - HTTP 响应数据构造
// 构造标准 HTTP 响应数据
+ (NSData *)createHTTPResponseWithStatus:(NSInteger)statusCode
statusText:(NSString *)statusText
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
body:(nullable NSData *)body;
// 构造 chunked 编码的 HTTP 响应
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
chunks:(NSArray<NSData *> *)chunks;
// 构造 chunked 编码的 HTTP 响应(带 trailers
+ (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
+ (NSData *)encodeChunk:(NSData *)data extension:(nullable NSString *)extension;
// 编码终止 chunksize=0
+ (NSData *)encodeLastChunk;
// 编码终止 chunk带 trailers
+ (NSData *)encodeLastChunkWithTrailers:(NSDictionary<NSString *, NSString *> *)trailers;
#pragma mark - 测试数据生成
// 生成指定大小的随机数据
+ (NSData *)randomDataWithSize:(NSUInteger)size;
// 生成 JSON 格式的响应体
+ (NSData *)jsonBodyWithDictionary:(NSDictionary *)dictionary;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,168 @@
//
// HttpdnsNWHTTPClientTestHelper.m
// AlicloudHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
#import "HttpdnsNWHTTPClientTestHelper.h"
@implementation HttpdnsNWHTTPClientTestHelper
#pragma mark - HTTP
+ (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]];
}
}
// body Content-Length
if (body && body.length > 0 && !headers[@"Content-Length"]) {
[response appendFormat:@"Content-Length: %lu\r\n", (unsigned long)body.length];
}
// 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 使
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
// AlicloudHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 alibaba-inc.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)
// 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);
// 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
- (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"); // trim
}
// A1.6
- (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
- (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
- (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 (8)
// 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 chunk
- (void)testCheckChunkedBody_IncompleteChunk_ReturnsIncomplete {
NSString *incompleteChunkBody = @"5\r\nhel"; //
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 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 \r\n\r\n \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
- (void)testCheckChunkedBody_MissingCRLFTerminator_ReturnsError {
NSString *missingTerminator = @"5\r\nhelloXX0\r\n\r\n"; // 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 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)
// 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 headers
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)
// 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 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
- (void)testParseResponse_ContentLengthMismatch_LogsButSucceeds {
NSData *bodyData = [@"short" dataUsingEncoding:NSUTF8StringEncoding];
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
statusText:@"OK"
headers:@{@"Content-Length": @"100"} //
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); //
XCTAssertEqualObjects(body, bodyData);
}
// A4.5
- (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 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)
// 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
- (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
- (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
- (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
- (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
//
// 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 {
// 使 SecPolicyCreateSSL(true, domain)
}
#pragma mark - F. (5)
// 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 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
- (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 退
- (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 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
// AlicloudHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
// - (G) (J)
// 12 G:7 + J:5
//
#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
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
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 -
// 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 10 2
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
- (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
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 31
- (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
- (void)testConnectionReuse_TenRequests_OnlyFourConnectionsKept {
XCTestExpectation *expectation = [self expectationWithDescription:@"Pool size limit"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 10
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);
}
//
[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 使 key
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
- (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
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;
}
}
//
XCTAssertGreaterThan(successCount, 15, @"Most requests should succeed with connection reuse");
// 使keep-alive
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 = 5950退
[self waitForExpectations:@[expectation] timeout:50.0];
}
@end

View File

@@ -0,0 +1,534 @@
//
// HttpdnsNWHTTPClient_ConcurrencyTests.m
// AlicloudHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
// - (H) (I) (N)
// 13 H:5 + I:5 + N:3
//
#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
- (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];
//
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.
// I.1
- (void)testRaceCondition_ExceedPoolCapacity_MaxFourConnections {
XCTestExpectation *expectation = [self expectationWithDescription:@"Pool capacity test"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 10
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];
//
// 4
[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);
//
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:30.0];
//
}
// I.3 --
- (void)testRaceCondition_AcquireReturnReacquire_CorrectState {
XCTestExpectation *expectation = [self expectationWithDescription:@"Acquire-Return-Reacquire"];
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:@"First"
timeout:15.0
error:&error1];
XCTAssertTrue(response1 != nil || error1 != nil);
//
[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 31
- (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
[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
- (void)testRaceCondition_ErrorRecovery_PoolRemainsHealthy {
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//
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;
// 使
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.
// N.1
- (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
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
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 100
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
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
- (void)testConcurrentMultiPort_MixedLoadPattern_RobustHandling {
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 1144320
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];
});
}
// 1144410
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];
});
}
// 114455
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
// AlicloudHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
// - (M) (P) Connection (R)
// 15 M:4 + P:6 + R:5
//
#import "HttpdnsNWHTTPClientTestBase.h"
@interface HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests : HttpdnsNWHTTPClientTestBase
@end
@implementation HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests
#pragma mark - M.
// M.1
- (void)testEdgeCase_ConnectionReuseWithinPortOnly_NotAcross {
XCTestExpectation *expectation = [self expectationWithDescription:@"Reuse boundaries"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// A 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 11443
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 11444
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 11444 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
- (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];
//
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);
}
//
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 访
- (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;
//
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];
// 访
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
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 11443
[NSThread sleepForTimeInterval:31.0];
// 3 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 11443
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";
//
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Pool should be empty initially");
// delay 10s, timeout 1s
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];
//
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];
// 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);
// 1 + 1 + 1
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
- (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
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) {
//
urlString = @"http://127.0.0.1:11080/get";
timeout = 15.0;
} else {
//
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 + 5 = 10
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];
// -
// remote close
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
- (void)testTimeout_ConsecutiveTimeouts_NoConnectionLeak {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 10
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];
}
//
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"Pool should be empty after consecutive timeouts");
XCTAssertEqual([self.client totalConnectionCount], 0,
@"No connections should leak");
//
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
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
dispatch_async(queue, ^{
// A
[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 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
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
- (void)testTimeout_MultiPort_IsolatedPoolCleaning {
[self.client resetPoolStatistics];
NSString *poolKey11443 = @"127.0.0.1:11443:tls";
NSString *poolKey11444 = @"127.0.0.1:11444:tls";
// 11443
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
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];
// 11443 11444 1
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
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
- (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
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
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
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);
// 1
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
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
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 ()
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
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
@"'CONNECTION: CLOSE' (uppercase) should also close connection");
// 2Connection: Close ()
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 close 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 close5 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");
//
[NSThread sleepForTimeInterval:1.0];
//
// - close
// - keep-alive remote close
// - 4
NSInteger poolSize = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolSize, 4,
@"Pool size should not exceed limit (no leak from close connections)");
// close
// >= 6 ( 5 close + 1 keep-alive close )
XCTAssertGreaterThanOrEqual(self.client.connectionCreationCount, 6,
@"Should have created at least 6 connections (5 close + at least 1 keep-alive)");
//
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
// AlicloudHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
// - (K) (L) (O) (S)
// 16 K:5 + L:3 + O:3 + S:5
//
#import "HttpdnsNWHTTPClientTestBase.h"
@interface HttpdnsNWHTTPClient_PoolManagementTests : HttpdnsNWHTTPClientTestBase
@end
@implementation HttpdnsNWHTTPClient_PoolManagementTests
#pragma mark - K.
// K.1 HTTPS 使
- (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
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
- (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
- (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", // 访 11443
@"https://127.0.0.1:11444/get", // 访 11444
@"https://127.0.0.1:11445/get", // 访 11445
@"https://127.0.0.1:11443/get", // 访 11443
@"https://127.0.0.1:11444/get", // 访 11444
];
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), ^{
// 11443 10
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);
}
// 11444 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];
//
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;
// 访
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
- (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;
// 4 10 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];
// > 95%
XCTAssertGreaterThan(successCount, 38, @"At least 95%% of 40 requests should succeed");
}
// L.2
- (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];
// 11443 20
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];
});
}
// 11444 5 11443
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 使
- (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];
//
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
[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. 使 API
// O.1 -
- (void)testPoolVerification_ComprehensiveCheck_AllAspectsVerified {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
//
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");
// 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);
}
//
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");
//
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";
//
XCTAssertEqual([self.client connectionPoolCountForKey:key11443], 0);
XCTAssertEqual([self.client connectionPoolCountForKey:key11444], 0);
// 11443 3
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
XCTAssertEqual([self.client connectionPoolCountForKey:key11443], 1,
@"Port 11443 should have 1 connection");
XCTAssertEqual([self.client connectionPoolCountForKey:key11444], 0,
@"Port 11444 should still be empty");
// 11444 3
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 4
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
- (void)testPoolVerification_PoolCapacity_MaxFourConnections {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 10
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
NSUInteger poolSize = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolSize, 4,
@"Pool size should not exceed 4 (actual: %lu)", (unsigned long)poolSize);
// 1
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 -
- (void)testIdleTimeout_MixedExpiredValid_SelectivePruning {
[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:@"ConnectionA"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
XCTAssertEqual(response1.statusCode, 200);
//
[NSThread sleepForTimeInterval:0.5];
// 使 DEBUG API A 35
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];
//
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
connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1,
@"Should have only 1 connection (expired A removed, valid B kept)");
// 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
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";
//
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];
// inUse=YES
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1);
HttpdnsNWReusableConnection *conn = connections.firstObject;
// 60 30
NSDate *veryOldDate = [NSDate dateWithTimeIntervalSinceNow:-60.0];
[conn debugSetLastUsedDate:veryOldDate];
// 使
[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
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));
// inUse
[conn debugSetInUse:NO];
// inUse=YES
// connectionsBefore = 1 (), connectionsAfter = 2 ( + )
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 -
- (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];
//
[NSThread sleepForTimeInterval:1.0];
//
NSUInteger poolSizeBefore = [self.client connectionPoolCountForKey:poolKey];
XCTAssertGreaterThan(poolSizeBefore, 0, @"Pool should have connections");
// 31
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
NSDate *expiredDate = [NSDate dateWithTimeIntervalSinceNow:-31.0];
for (HttpdnsNWReusableConnection *conn in connections) {
[conn debugSetLastUsedDate:expiredDate];
}
//
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
- (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];
//
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
@"Pool should have 1 connection");
//
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];
//
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 -
- (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
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+
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
// AlicloudHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
// - (Q)
// 17 Q:17
//
#import "HttpdnsNWHTTPClientTestBase.h"
@interface HttpdnsNWHTTPClient_StateMachineTests : HttpdnsNWHTTPClientTestBase
@end
@implementation HttpdnsNWHTTPClient_StateMachineTests
#pragma mark - Q.
// Q1.1 LRU
- (void)testStateMachine_PoolOverflowLRU_RemovesOldestByLastUsedDate {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 5
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 5
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]; //
}
[self waitForExpectations:expectations timeout:20.0];
//
[NSThread sleepForTimeInterval:1.0];
// 4LRU
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";
// 10
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];
// 1
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolCount, 1,
@"Pool should have at most 1 connection (rapid sequential reuse)");
// 1
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";
// 11080
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Port11080"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
// 11443
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];
// 1
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];
// 20
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];
}
//
[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
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
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];
// 4
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolCount, 4,
@"Pool should not have duplicates (max 4 connections)");
// 15
XCTAssertLessThanOrEqual(self.client.connectionCreationCount, 15,
@"Should not create excessive connections");
}
// Q4.1 30
- (void)testBoundary_Exactly30Seconds_ConnectionExpired {
if (getenv("SKIP_SLOW_TESTS")) {
return;
}
[self.client resetPoolStatistics];
//
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];
//
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
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 29
- (void)testBoundary_Under30Seconds_ConnectionNotExpired {
[self.client resetPoolStatistics];
//
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);
// 1
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 4
- (void)testBoundary_ExactlyFourConnections_AllKept {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 4使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;
// 使 /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]; //
}
[self waitForExpectations:expectations timeout:20.0];
//
[NSThread sleepForTimeInterval:1.0];
// 4
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
XCTAssertEqual(poolCount, 4,
@"Pool should keep all 4 connections (not exceeding limit)");
// 4
XCTAssertEqual(self.client.connectionCreationCount, 4,
@"Should create exactly 4 connections");
}
// Q1.2
- (void)testStateMachine_NormalSequence_StateTransitionsCorrect {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 1使 (CREATING IN_USE 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
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
@"Connection should be in pool");
// 2 (IDLE IN_USE 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");
//
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";
//
[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];
// lastUsedDate nil
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1, @"Should have connection");
HttpdnsNWReusableConnection *conn = connections.firstObject;
[conn debugSetLastUsedDate:nil];
// prune使 distantPast nil
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"NilDateTest"
timeout:15.0
error:nil];
//
XCTAssertNotNil(response, @"Should handle nil lastUsedDate gracefully");
}
// Q3.2
- (void)testInvariant_NoInvalidatedInPool {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
//
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
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"TimeoutTest"
timeout:0.5
error:nil];
[NSThread sleepForTimeInterval:2.0];
//
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
//
for (HttpdnsNWReusableConnection *conn in connections) {
XCTAssertFalse(conn.isInvalidated, @"Pool should not contain invalidated connections");
}
}
// Q3.4 lastUsedDate
- (void)testInvariant_LastUsedDate_Monotonic {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 1使
[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
[NSThread sleepForTimeInterval:1.0];
// 2使
[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 +
- (void)testCompound_TimeoutDuringPoolOverflow_Handled {
[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:@"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 ≤4 connections");
// 5
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];
//
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];
//
XCTAssertNil(response, @"Should fail to connect to invalid port");
//
XCTAssertEqual([self.client totalConnectionCount], 0,
@"Failed connection should not be added to pool");
}
// Q2.5 invalidate
- (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
- (void)testCompound_ConcurrentDequeueDuringPrune_Safe {
[self.client resetPoolStatistics];
//
[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];
// dequeue prune
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
本目录包含用于 `HttpdnsNWHTTPClient` 集成测试的 HTTP/HTTPS mock server用于替代 httpbin.org。
---
## 为什么需要 Mock Server
1. **可靠性**: httpbin.org 在高并发测试下表现不稳定,经常返回非预期的 HTTP 状态码(如 429 Too Many Requests
2. **速度**: 本地服务器响应更快,缩短测试执行时间
3. **离线测试**: 无需网络连接即可运行集成测试
4. **可控性**: 完全掌控测试环境,便于调试和复现问题
---
## 快速开始
### 1. 启动 Mock Server
```bash
# 进入测试目录
cd AlicloudHttpDNSTests/Network
# 启动服务器(无需 sudo 权限,使用非特权端口)
python3 mock_server.py
```
**注意**:
- **无需 root 权限**(使用非特权端口 11080/11443-11446
- 首次运行会自动生成自签名证书 (`server.pem`)
-`Ctrl+C` 停止服务器
### 2. 运行集成测试
在另一个终端窗口:
```bash
cd ~/Project/iOS/alicloud-ios-sdk-httpdns
# 运行所有集成测试
xcodebuild test \
-workspace AlicloudHttpDNS.xcworkspace \
-scheme AlicloudHttpDNSTests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:AlicloudHttpDNSTests/HttpdnsNWHTTPClientIntegrationTests
# 运行单个测试
xcodebuild test \
-workspace AlicloudHttpDNS.xcworkspace \
-scheme AlicloudHttpDNSTests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:AlicloudHttpDNSTests/HttpdnsNWHTTPClientIntegrationTests/testConcurrency_ParallelRequestsSameHost_AllSucceed
```
---
## 支持的 Endpoints
Mock server 实现了以下 httpbin.org 兼容的 endpoints:
| Endpoint | 功能 | 示例 |
|----------|------|------|
| `GET /get` | 返回请求信息headers, args, origin | `http://127.0.0.1:11080/get` |
| `GET /status/{code}` | 返回指定状态码100-599 | `http://127.0.0.1:11080/status/404` |
| `GET /stream-bytes/{n}` | 返回 chunked 编码的 N 字节数据 | `http://127.0.0.1:11080/stream-bytes/1024` |
| `GET /delay/{seconds}` | 延迟指定秒数后返回最多10秒 | `http://127.0.0.1:11080/delay/5` |
| `GET /headers` | 返回所有请求头部 | `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`4个端口用于测试连接池隔离
所有 endpoints 在 HTTP 和 HTTPS 端口上均可访问。
---
## 实现细节
### 架构
- **HTTP 服务器**: 监听 `127.0.0.1:11080`
- **HTTPS 服务器**: 监听 `127.0.0.1:11443`, `11444`, `11445`, `11446`4个端口使用自签名证书
- **多端口目的**: 测试连接池的端口隔离机制,确保不同端口使用独立的连接池
- **并发模型**: 多线程(`ThreadingMixIn`),支持高并发请求
### TLS 证书
- 自动生成自签名证书RSA 2048位有效期 365 天)
- CN (Common Name): `localhost`
- 证书文件: `server.pem`(同时包含密钥和证书)
**重要**: 集成测试通过环境变量 `HTTPDNS_SKIP_TLS_VERIFY=1` 跳过 TLS 验证,这是安全的,因为:
1. 仅在测试环境生效
2. 不影响生产代码
3. 连接限制为本地 loopback (127.0.0.1)
### 响应格式
所有 JSON 响应遵循 httpbin.org 格式,例如:
```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
```
---
## 故障排除
### 端口已被占用
**错误信息**:
```
✗ 端口 80 已被占用,请关闭占用端口的进程或使用其他端口
```
**解决方法**:
1. 查找占用进程:
```bash
sudo lsof -i :80
sudo lsof -i :443
```
2. 终止占用进程:
```bash
sudo kill -9 <PID>
```
3. 或修改 mock_server.py 使用其他端口:
```python
# 修改端口号(同时需要更新测试代码中的 URL
run_http_server(port=8080)
run_https_server(port=8443)
```
### 缺少 OpenSSL
**错误信息**:
```
✗ 未找到 openssl 命令,请安装 OpenSSL
```
**解决方法**:
```bash
# macOS (通常已预装)
brew install openssl
# Ubuntu/Debian
sudo apt-get install openssl
# CentOS/RHEL
sudo yum install openssl
```
### 权限被拒绝
**错误信息**:
```
✗ 错误: 需要 root 权限以绑定 80/443 端口
```
**解决方法**:
必须使用 `sudo` 运行:
```bash
sudo python3 mock_server.py
```
---
## 切换回 httpbin.org
如需使用真实的 httpbin.org 进行测试(例如验证兼容性):
1. 编辑 `HttpdnsNWHTTPClientIntegrationTests.m`
2. 将所有 `127.0.0.1` 替换回 `httpbin.org`
3. 注释掉 setUp/tearDown 中的环境变量设置
---
## 开发与扩展
### 添加新 Endpoint
`mock_server.py``MockHTTPHandler.do_GET()` 方法中添加:
```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):
"""处理自定义 endpoint"""
data = {'custom': 'data'}
self._send_json(200, data)
```
### 调试模式
取消注释 `log_message` 方法以启用详细日志:
```python
def log_message(self, format, *args):
print(f"[{self.address_string()}] {format % args}")
```
---
## 技术栈
- **Python 3.7+** (标准库,无需额外依赖)
- **http.server**: HTTP 服务器实现
- **ssl**: TLS/SSL 支持
- **socketserver.ThreadingMixIn**: 多线程并发
---
## 安全注意事项
1. **仅用于测试**: 此服务器设计用于本地测试,不适合生产环境
2. **自签名证书**: HTTPS 使用不受信任的自签名证书
3. **无身份验证**: 不实现任何身份验证机制
4. **本地绑定**: 服务器仅绑定到 `127.0.0.1`,不接受外部连接
---
**最后更新**: 2025-11-01
**维护者**: Claude Code

View File

@@ -0,0 +1,282 @@
# HttpdnsNWHTTPClient 测试套件
本目录包含 `HttpdnsNWHTTPClient``HttpdnsNWReusableConnection` 的完整测试套件。
## 测试文件结构
```
AlicloudHttpDNSTests/Network/
├── HttpdnsNWHTTPClientTests.m # 主单元测试44个
├── HttpdnsNWHTTPClientIntegrationTests.m # 集成测试7个
├── HttpdnsNWHTTPClientTestHelper.h/m # 测试辅助工具类
└── README.md # 本文件
```
## 测试覆盖范围
### 单元测试 (HttpdnsNWHTTPClientTests.m)
#### A. HTTP 解析逻辑测试 (25个)
- **A1. Header 解析 (9个)**
- 正常响应解析
- 多个头部字段
- 不完整数据处理
- 无效状态行
- 空格处理与 trim
- 空值头部
- 非数字状态码
- 状态码为零
- 无效头部行
- **A2. Chunked 编码检查 (8个)**
- 单个 chunk
- 多个 chunks
- 不完整 chunk
- Chunk extension 支持
- 无效十六进制 size
- Chunk size 溢出
- 缺少 CRLF 终止符
- 带 trailers 的 chunked
- **A3. Chunked 解码 (2个)**
- 多个 chunks 正确解码
- 无效格式返回 nil
- **A4. 完整响应解析 (6个)**
- Content-Length 响应
- Chunked 编码响应
- 空 body
- Content-Length 不匹配
- 空数据错误
- 只有 headers 无 body
#### C. 请求构建测试 (7个)
- 基本 GET 请求格式
- 查询参数处理
- User-Agent 头部
- HTTP 默认端口处理
- HTTPS 默认端口处理
- 非默认端口显示
- 固定头部验证
#### E. TLS 验证测试 (4个占位符)
- 有效证书返回 YES
- Proceed 结果返回 YES
- 无效证书返回 NO
- 指定域名使用 SSL Policy
*注TLS 测试需要真实的 SecTrustRef 或复杂 mock当前为占位符*
#### F. 边缘情况测试 (8个)
- 超长 URL 处理
- 空 User-Agent
- 超大响应体5MB
- Chunked 解码失败回退
- 连接池 key - 不同 hosts
- 连接池 key - 不同 ports
- 连接池 key - HTTP vs HTTPS
### 集成测试 (HttpdnsNWHTTPClientIntegrationTests.m)
使用 httpbin.org 进行真实网络测试 (22个)
**G. 基础集成测试 (7个)**
- HTTP GET 请求
- HTTPS GET 请求
- HTTP 404 响应
- 连接复用(两次请求)
- Chunked 响应处理
- 请求超时测试
- 自定义头部验证
**H. 并发测试 (5个)**
- 并发请求同一主机10个线程
- 并发请求不同路径5个不同endpoint
- 混合 HTTP + HTTPS 并发各5个线程
- 高负载压力测试50个并发请求
- 混合串行+并发模式
**I. 竞态条件测试 (5个)**
- 连接池容量测试超过4个连接上限
- 同时归还连接5个并发
- 获取-归还-再获取竞态
- 超时与活跃连接冲突需31秒可跳过
- 错误恢复后连接池健康状态
**J. 高级连接复用测试 (5个)**
- 连接过期与清理31秒可跳过
- 连接池容量限制验证10个连续请求
- 不同路径复用连接4个不同路径
- HTTP vs HTTPS 使用不同连接池key
- 长连接保持测试20个请求间隔1秒可跳过
## 运行测试
### 运行所有单元测试
```bash
xcodebuild test \
-workspace AlicloudHttpDNS.xcworkspace \
-scheme AlicloudHttpDNSTests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:AlicloudHttpDNSTests/HttpdnsNWHTTPClientTests
```
### 运行集成测试(需要网络)
```bash
xcodebuild test \
-workspace AlicloudHttpDNS.xcworkspace \
-scheme AlicloudHttpDNSTests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:AlicloudHttpDNSTests/HttpdnsNWHTTPClientIntegrationTests
```
### 运行单个测试
```bash
xcodebuild test \
-workspace AlicloudHttpDNS.xcworkspace \
-scheme AlicloudHttpDNSTests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:AlicloudHttpDNSTests/HttpdnsNWHTTPClientTests/testParseHTTPHeaders_ValidResponse_Success
```
## 测试辅助工具
### HttpdnsNWHTTPClientTestHelper
提供以下工具方法:
#### HTTP 响应构造
```objc
// 构造标准 HTTP 响应
+ (NSData *)createHTTPResponseWithStatus:(NSInteger)statusCode
statusText:(NSString *)statusText
headers:(NSDictionary *)headers
body:(NSData *)body;
// 构造 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 编码、完整响应 |
| 请求构建 | 7 | URL 处理、头部生成 |
| TLS 验证 | 4 (占位符) | 证书验证 |
| 边缘情况 | 8 | 异常输入、连接池 key |
| **单元测试合计** | **43** | - |
| 基础集成测试 (G) | 7 | 真实网络请求、基本场景 |
| 并发测试 (H) | 5 | 多线程并发、高负载 |
| 竞态条件测试 (I) | 5 | 连接池竞态、错误恢复 |
| 连接复用测试 (J) | 5 | 连接过期、长连接、协议隔离 |
| 多端口连接隔离 (K) | 5 | 不同端口独立连接池 |
| 端口池耗尽测试 (L) | 3 | 多端口高负载、池隔离 |
| 边界条件验证 (M) | 4 | 端口迁移、高端口数量 |
| 并发多端口场景 (N) | 3 | 并行 keep-alive、轮询分配 |
| **集成测试合计** | **37** | - |
| **总计** | **80** | - |
## 待实现测试(可选)
以下测试组涉及复杂的 Mock 场景,可根据需要添加:
### B. 连接池管理测试 (18个)
需要 Mock `HttpdnsNWReusableConnection` 的完整生命周期
### D. 完整流程测试 (13个)
需要 Mock 连接池和网络层的集成场景
## Mock Server 使用
集成测试使用本地 mock server (127.0.0.1) 替代 httpbin.org提供稳定可靠的测试环境。
### 启动 Mock Server
```bash
cd AlicloudHttpDNSTests/Network
python3 mock_server.py
```
**注意**使用非特权端口11080/11443-11446无需 `sudo` 权限。
### 运行集成测试
在另一个终端窗口:
```bash
xcodebuild test \
-workspace AlicloudHttpDNS.xcworkspace \
-scheme AlicloudHttpDNSTests \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:AlicloudHttpDNSTests/HttpdnsNWHTTPClientIntegrationTests
```
### Mock Server 特性
- **HTTP**: 监听 `127.0.0.1:11080`
- **HTTPS**: 监听 `127.0.0.1:11443`, `11444`, `11445`, `11446` (自签名证书)
- **多端口目的**: 测试连接池的端口隔离机制
- **并发支持**: 多线程处理,适合并发测试
- **零延迟**: 本地响应,测试速度快
详见 [MOCK_SERVER.md](./MOCK_SERVER.md)
## 注意事项
1. **集成测试依赖 Mock Server**`HttpdnsNWHTTPClientIntegrationTests` 使用本地 mock server (127.0.0.1)。测试前需先启动 `mock_server.py`
2. **慢测试跳过**部分测试需要等待31秒测试连接过期可设置环境变量 `SKIP_SLOW_TESTS=1` 跳过这些测试:
- `testRaceCondition_ExpiredConnectionPruning_CreatesNewConnection`
- `testConnectionReuse_Expiry31Seconds_NewConnectionCreated`
- `testConnectionReuse_TwentyRequestsOneSecondApart_ConnectionKeptAlive`
3. **并发测试容错**:并发和压力测试允许部分失败(例如 H.4 要求80%成功率),因为高负载下仍可能出现网络波动。
4. **TLS 测试占位符**E 组 TLS 测试需要真实的 `SecTrustRef` 或高级 mock 框架,当前仅为占位符。
5. **新文件添加到 Xcode**:创建的测试文件需要手动添加到 `AlicloudHttpDNSTests` target。
6. **测试数据**:使用 `HttpdnsNWHTTPClientTestHelper` 生成测试数据,确保测试的可重复性。
## 文件依赖
测试文件依赖以下源文件:
- `HttpdnsNWHTTPClient.h/m` - 主要被测试类
- `HttpdnsNWHTTPClient_Internal.h` - 内部方法暴露(测试专用)
- `HttpdnsNWReusableConnection.h/m` - 连接管理
- `HttpdnsNWHTTPClientResponse` - 响应模型
## 贡献指南
添加新测试时,请遵循:
1. 命名规范:`test<Component>_<Scenario>_<ExpectedResult>`
2. 使用 `#pragma mark` 组织测试分组
3. 添加清晰的注释说明测试目的
4. 验证测试覆盖率并更新本文档
---
**最后更新**: 2025-11-01
**测试框架**: XCTest + OCMock
**维护者**: Claude Code
**更新日志**:
- 2025-11-01: 新增 15 个多端口连接复用测试K、L、M、N组测试总数增至 37 个
- 2025-11-01: Mock server 新增 3 个 HTTPS 端口11444-11446用于测试连接池隔离
- 2025-11-01: 新增本地 mock server替代 httpbin.org提供稳定测试环境
- 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"**
---
## 连接状态机定义
### 状态属性
**HttpdnsNWReusableConnection.h:9-11**
```objc
@property (nonatomic, strong) NSDate *lastUsedDate; // 最后使用时间
@property (nonatomic, assign) BOOL inUse; // 是否正在被使用
@property (nonatomic, assign, getter=isInvalidated, readonly) BOOL invalidated; // 是否已失效
```
### 状态枚举
虽然没有显式枚举,但连接实际存在以下逻辑状态:
| 状态 | `inUse` | `invalidated` | `pool` | 描述 |
|------|---------|---------------|--------|------|
| **CREATING** | - | NO | ✗ | 新创建,尚未打开 |
| **IN_USE** | YES | NO | ✓ | 已借出,正在使用 |
| **IDLE** | NO | NO | ✓ | 空闲,可复用 |
| **EXPIRED** | NO | NO | ✓ | 空闲超30秒待清理 |
| **INVALIDATED** | - | YES | ✗ | 已失效,已移除 |
---
## 状态转换图
```
┌─────────┐
│CREATING │ (new connection)
└────┬────┘
│ openWithTimeout success
┌─────────┐
│ IN_USE │ (inUse=YES, in pool)
└────┬────┘
├──success──► returnConnection(shouldClose=NO)
│ │
│ ▼
│ ┌─────────┐
│ │ IDLE │ (inUse=NO, in pool)
│ └────┬────┘
│ │
│ ├──dequeue──► IN_USE (reuse)
│ │
│ ├──idle 30s──► EXPIRED
│ │ │
│ │ └──prune──► INVALIDATED
│ │
│ └──!isViable──► INVALIDATED (skip in dequeue)
├──error/timeout──► returnConnection(shouldClose=YES)
│ │
│ ▼
└──────────► ┌──────────────┐
│ INVALIDATED │ (removed from pool)
└──────────────┘
```
---
## 代码中的状态转换
### 1. CREATING → IN_USE (新连接)
**HttpdnsNWHTTPClient.m:248-249**
```objc
newConnection.inUse = YES;
newConnection.lastUsedDate = now;
[pool addObject:newConnection]; // 加入池
```
**何时触发:**
- `dequeueConnectionForHost` 找不到可复用连接
- 创建新连接并成功打开
### 2. IDLE → 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;
}
}
```
**关键检查:**
- `!candidate.inUse` - 必须是空闲状态
- `[candidate isViable]` - 连接必须仍然有效
### 3. IN_USE → IDLE (正常归还)
**HttpdnsNWHTTPClient.m:283-288**
```objc
if (shouldClose || connection.isInvalidated) {
// → INVALIDATED (见#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 → 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` (连接已失效)
### 5. EXPIRED → 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秒
[toRemove addObject:conn];
}
}
for (HttpdnsNWReusableConnection *conn in toRemove) {
[conn invalidate];
[pool removeObject:conn];
}
// 限制池大小 ≤ 4
while (pool.count > kHttpdnsNWHTTPClientMaxIdleConnectionsPerHost) {
HttpdnsNWReusableConnection *oldest = pool.firstObject;
[oldest invalidate];
[pool removeObject:oldest];
}
}
```
---
## 当前测试覆盖情况
### ✅ 已测试的正常流程
| 状态转换 | 测试 | 覆盖 |
|----------|------|------|
| CREATING → IN_USE → IDLE | G.1-G.7, O.1 | ✅ |
| IDLE → IN_USE (复用) | G.2, O.1-O.3, J.1-J.5 | ✅ |
| IN_USE → INVALIDATED (timeout) | P.1-P.6 | ✅ |
| EXPIRED → INVALIDATED (30s) | J.2, M.4, I.4 | ✅ |
| 池容量限制 (max 4) | O.3, J.3 | ✅ |
| 并发状态访问 | I.1-I.5, M.3 | ✅ |
### ❌ 未测试的异常场景
#### 1. **连接在池中失效Stale Connection**
**场景:**
- 连接空闲 29 秒(未到 30 秒过期)
- 服务器主动关闭连接
- `dequeue``isViable` 返回 NO
**当前代码行为:**
```objc
for (HttpdnsNWReusableConnection *candidate in pool) {
if (!candidate.inUse && [candidate isViable]) { // ← isViable 检查
// 只复用有效连接
}
}
// 如果所有连接都 !isViable会创建新连接
```
**风险:** 未验证 `isViable` 检查是否真的工作
**测试需求:** Q.1
```objc
testStateTransition_StaleConnectionInPool_SkipsAndCreatesNew
```
---
#### 2. **双重归还Double Return**
**场景:**
- 连接被归还
- 代码错误,再次归还同一连接
**当前代码防护:**
```objc
if (![pool containsObject:connection]) {
[pool addObject:connection]; // ← 防止重复添加
}
```
**风险:** 未验证防护是否有效
**测试需求:** Q.2
```objc
testStateTransition_DoubleReturn_Idempotent
```
---
#### 3. **归还错误的池键Wrong Pool Key**
**场景:**
- 从池A借出连接
- 归还到池B错误的key
**当前代码行为:**
```objc
- (void)returnConnection:(HttpdnsNWReusableConnection *)connection
forKey:(NSString *)key
shouldClose:(BOOL)shouldClose {
// ...
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
// 会添加到错误的池!
}
```
**风险:** 可能导致池污染
**测试需求:** Q.3
```objc
testStateTransition_ReturnToWrongPool_Isolated
```
---
#### 4. **连接在使用中变为失效**
**场景:**
- 连接被借出 (inUse=YES)
- `sendRequestData` 过程中网络错误
- 连接被标记 invalidated
**当前代码行为:**
```objc
NSData *rawResponse = [connection sendRequestData:requestData ...];
if (!rawResponse) {
[self returnConnection:connection forKey:poolKey shouldClose:YES]; // ← invalidated
}
```
**测试需求:** Q.4
```objc
testStateTransition_ErrorDuringUse_Invalidated
```
---
#### 5. **池容量超限时的移除策略**
**场景:**
- 池已有 4 个连接
- 第 5 个连接被归还
**当前代码行为:**
```objc
while (pool.count > kHttpdnsNWHTTPClientMaxIdleConnectionsPerHost) {
HttpdnsNWReusableConnection *oldest = pool.firstObject; // ← 移除最老的
[oldest invalidate];
[pool removeObject:oldest];
}
```
**问题:**
- 移除 `pool.firstObject` - 是按添加顺序还是使用顺序?
- NSMutableArray 顺序是否能保证?
**测试需求:** Q.5
```objc
testStateTransition_PoolOverflow_RemovesOldest
```
---
#### 6. **并发状态竞态**
**场景:**
- Thread A: dequeue 连接,设置 `inUse=YES`
- Thread B: 同时 prune 过期连接
- 竞态:连接同时被标记 inUse 和被移除
**当前代码防护:**
```objc
- (void)pruneConnectionPool:... {
for (HttpdnsNWReusableConnection *conn in pool) {
if (conn.inUse) continue; // ← 跳过使用中的
}
}
```
**测试需求:** Q.6 (可能已被 I 组部分覆盖)
```objc
testStateTransition_ConcurrentDequeueAndPrune_NoCorruption
```
---
#### 7. **连接打开失败**
**场景:**
- 创建连接
- `openWithTimeout` 失败
**当前代码行为:**
```objc
if (![newConnection openWithTimeout:timeout error:error]) {
[newConnection invalidate]; // ← 立即失效
return nil; // ← 不加入池
}
```
**测试需求:** Q.7
```objc
testStateTransition_OpenFails_NotAddedToPool
```
---
## 状态不变式State Invariants
### 应该始终成立的约束
1. **互斥性:**
```
∀ connection: (inUse=YES) ⇒ (dequeue count ≤ 1)
```
同一连接不能被多次借出
2. **池完整性:**
```
∀ pool: ∑(connections) ≤ maxPoolSize (4)
```
每个池最多 4 个连接
3. **状态一致性:**
```
∀ connection in pool: !invalidated
```
池中不应有失效连接
4. **时间单调性:**
```
∀ connection: lastUsedDate 随每次使用递增
```
5. **失效不可逆:**
```
invalidated=YES ⇒ connection removed from pool
```
失效连接必须从池中移除
---
## 测试设计建议
### Q 组状态机异常转换测试7个新测试
| 测试 | 验证内容 | 难度 |
|------|---------|------|
| **Q.1** | Stale connection 被 `isViable` 检测并跳过 | 🔴 高(需要模拟服务器关闭) |
| **Q.2** | 双重归还是幂等的 | 🟢 低 |
| **Q.3** | 归还到错误池键不污染其他池 | 🟡 中 |
| **Q.4** | 使用中错误导致连接失效 | 🟢 低(已有 P 组部分覆盖) |
| **Q.5** | 池溢出时移除最旧连接 | 🟡 中 |
| **Q.6** | 并发 dequeue/prune 竞态 | 🔴 高(需要精确时序) |
| **Q.7** | 打开失败的连接不加入池 | 🟢 低 |
---
## 状态机验证策略
### 方法1: 直接状态检查
```objc
// 验证状态属性
XCTAssertTrue(connection.inUse);
XCTAssertFalse(connection.isInvalidated);
XCTAssertEqual([poolCount], expectedCount);
```
### 方法2: 状态转换序列
```objc
// 验证转换序列
[client resetPoolStatistics];
// CREATING → IN_USE
response1 = [client performRequest...];
XCTAssertEqual(creationCount, 1);
// IN_USE → IDLE
[NSThread sleepForTimeInterval:0.5];
XCTAssertEqual(poolCount, 1);
// IDLE → IN_USE (reuse)
response2 = [client performRequest...];
XCTAssertEqual(reuseCount, 1);
```
### 方法3: 不变式验证
```objc
// 验证池不变式
NSArray *keys = [client allConnectionPoolKeys];
for (NSString *key in keys) {
NSUInteger count = [client connectionPoolCountForKey:key];
XCTAssertLessThanOrEqual(count, 4, @"Pool invariant: max 4 connections");
}
```
---
## 当前覆盖率评估
### 状态转换覆盖矩阵
| From ↓ / To → | CREATING | IN_USE | IDLE | EXPIRED | INVALIDATED |
|---------------|----------|--------|------|---------|-------------|
| **CREATING** | - | ✅ | ❌ | ❌ | ✅ (Q.7 needed) |
| **IN_USE** | ❌ | - | ✅ | ❌ | ✅ |
| **IDLE** | ❌ | ✅ | - | ✅ | ❌ (Q.1 needed) |
| **EXPIRED** | ❌ | ❌ | ❌ | - | ✅ |
| **INVALIDATED** | ❌ | ❌ | ❌ | ❌ | - |
**覆盖率:** 6/25 transitions = 24%
**有效覆盖率:** 6/10 valid transitions = 60%
### 异常场景覆盖
| 异常场景 | 当前测试 | 覆盖 |
|----------|---------|------|
| Stale connection | ❌ | 0% |
| Double return | ❌ | 0% |
| Wrong pool key | ❌ | 0% |
| Error during use | P.1-P.6 | 100% |
| Pool overflow | O.3, J.3 | 50% (未验证移除策略) |
| Concurrent race | I.1-I.5 | 80% |
| Open failure | ❌ | 0% |
**总体异常覆盖:** ~40%
---
## 风险评估
### 高风险未测试场景
**风险等级 🔴 高:**
1. **Stale Connection (Q.1)** - 可能导致请求失败
2. **Concurrent Dequeue/Prune (Q.6)** - 可能导致状态不一致
**风险等级 🟡 中:**
3. **Wrong Pool Key (Q.3)** - 可能导致池污染
4. **Pool Overflow Strategy (Q.5)** - LRU vs FIFO 影响性能
**风险等级 🟢 低:**
5. **Double Return (Q.2)** - 已有代码防护
6. **Open Failure (Q.7)** - 已有错误处理
---
## 建议
### 短期(关键)
1.**添加 Q.2 测试** - 验证双重归还防护
2.**添加 Q.5 测试** - 验证池溢出移除策略
3.**添加 Q.7 测试** - 验证打开失败处理
### 中期(增强)
4. ⚠️ **添加 Q.3 测试** - 验证池隔离
5. ⚠️ **添加 Q.1 测试** - 验证 stale connection需要 mock
### 长期(完整)
6. 🔬 **添加 Q.6 测试** - 验证并发竞态(复杂)
---
**创建时间**: 2025-11-01
**作者**: Claude Code
**状态**: 分析完成,待实现 Q 组测试

View File

@@ -0,0 +1,226 @@
# 超时对连接复用的影响分析
## 问题描述
当前测试套件没有充分验证**超时与连接池交互**的"无形结果"intangible outcomes可能存在以下风险
- 超时后的连接泄漏
- 连接池被超时连接污染
- 连接池无法从超时中恢复
- 并发场景下部分超时影响整体池健康
---
## 代码行为分析
### 超时处理流程
**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]; // 从池中移除
}
```
**结论**:代码逻辑正确,超时连接**会被移除**而非留在池中。
---
## 当前测试覆盖情况
### 已有测试:`testIntegration_RequestTimeout_ReturnsError`
**验证内容:**
- ✅ 超时返回 `nil` response
- ✅ 超时设置 `error`
**未验证内容(缺失):**
- ❌ 连接是否从池中移除
- ❌ 池计数是否正确
- ❌ 后续请求是否正常工作
- ❌ 是否存在连接泄漏
- ❌ 并发场景下部分超时的影响
---
## 需要验证的"无形结果"
### 1. 单次超时后的池清理
**场景**
1. 请求 A 超时timeout=1s, endpoint=/delay/10
2. 验证池状态
**应验证:**
- Pool count = 0连接已移除
- Total connection count 没有异常增长
- 无连接泄漏
**测试方法**
```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);
// 验证池状态
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. 超时后的池恢复能力
**场景**
1. 请求 A 超时
2. 请求 B 正常(验证池恢复)
3. 请求 C 复用 B 的连接
**应验证:**
- 请求 B 成功(池已恢复)
- 请求 C 复用连接connectionReuseCount = 1
- Pool count = 1只有 B/C 的连接)
---
### 3. 并发场景:部分超时不影响成功请求
**场景**
1. 并发发起 10 个请求
2. 5 个正常timeout=15s
3. 5 个超时timeout=0.5s, endpoint=/delay/10
**应验证:**
- 5 个正常请求成功
- 5 个超时请求失败
- Pool count ≤ 5只保留成功的连接
- Total connection count ≤ 5无泄漏
- connectionCreationCount ≤ 10合理范围
- 成功的请求可以复用连接
---
### 4. 连续超时不导致资源泄漏
**场景**
1. 连续 20 次超时请求
2. 验证连接池没有累积"僵尸连接"
**应验证:**
- Pool count = 0
- Total connection count = 0
- connectionCreationCount = 20每次都创建新连接因为超时的被移除
- connectionReuseCount = 0超时连接不可复用
- 无内存泄漏(虽然代码层面无法直接测试)
---
### 5. 超时不阻塞连接池
**场景**
1. 请求 A 超时endpoint=/delay/10, timeout=1s
2. 同时请求 B 正常endpoint=/get, timeout=15s
**应验证:**
- 请求 A 和 B 并发执行(不互相阻塞)
- 请求 B 成功(不受 A 超时影响)
- 请求 A 的超时连接被正确移除
- Pool 中只有请求 B 的连接
---
### 6. 多端口场景下的超时隔离
**场景**
1. 端口 11443 请求超时
2. 端口 11444 请求正常
3. 验证端口间隔离
**应验证:**
- 端口 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 连续超时无泄漏**
- `testTimeout_ConsecutiveTimeouts_NoConnectionLeak`
**P.5 超时不阻塞池**
- `testTimeout_NonBlocking_ConcurrentNormalRequestSucceeds`
**P.6 多端口超时隔离**
- `testTimeout_MultiPort_IsolatedPoolCleaning`
---
## Mock Server 支持
需要添加可配置延迟的 endpoint
- `/delay/10` - 延迟 10 秒(已有)
- 测试时设置短 timeout如 0.5s-2s触发超时
---
## 预期测试结果
| 验证项 | 当前状态 | 目标状态 |
|--------|---------|---------|
| 超时连接移除 | 未验证 | ✅ 验证池计数=0 |
| 池恢复能力 | 未验证 | ✅ 后续请求成功 |
| 并发超时隔离 | 未验证 | ✅ 成功请求不受影响 |
| 无连接泄漏 | 未验证 | ✅ 总连接数稳定 |
| 超时不阻塞 | 未验证 | ✅ 并发执行不阻塞 |
| 多端口隔离 | 未验证 | ✅ 端口间独立清理 |
---
## 风险评估
**如果不测试这些场景的风险:**
1. **连接泄漏**:超时连接可能未正确清理,导致内存泄漏
2. **池污染**:超时连接留在池中,被后续请求复用导致失败
3. **级联故障**:部分超时影响整体连接池健康
4. **资源耗尽**:连续超时累积连接,最终耗尽系统资源
**当前代码逻辑正确性:** ✅ 高(代码分析显示正确处理)
**测试验证覆盖率:** ❌ 低(缺少池交互验证)
**建议:** 添加 P 组测试以提供**可观测的证据**证明超时处理正确。
---
**创建时间**: 2025-11-01
**维护者**: 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()

View File

@@ -0,0 +1,32 @@
#import <Foundation/Foundation.h>
typedef enum {
NotReachable = 0,
ReachableViaWiFi,
ReachableVia2G,
ReachableVia3G,
ReachableVia4G
} _NetworkStatus;
@interface NetworkManager : NSObject
+ (NetworkManager *)instance;
/*
* 当前网络状态的String描述
*/
- (NSString*)currentStatusString;
/*
* 如果当前网络是Wifi,
* 获取到当前网络的ssid
*/
- (NSString *)currentWifiSsid;
/*
* 判断当前网络状态下
* 是否处理有Http/Https代理
*/
+ (BOOL) configureProxies;
@end

View File

@@ -0,0 +1,194 @@
#import "NetworkManager.h"
#import <sys/socket.h>
#import <netinet/in.h>
#import <netinet6/in6.h>
#import <arpa/inet.h>
#import <ifaddrs.h>
#import <netdb.h>
#import <CoreTelephony/CTCarrier.h>
#import <CoreTelephony/CTTelephonyNetworkInfo.h>
#import <SystemConfiguration/SystemConfiguration.h>
#import <SystemConfiguration/CaptiveNetwork.h>
#import <UIKit/UIDevice.h>
static char *const networkManagerQueue = "com.alibaba.managerQueue";
static dispatch_queue_t reachabilityQueue;
@implementation NetworkManager {
_NetworkStatus _current;
_NetworkStatus _last;
SCNetworkReachabilityRef _ref;
NSString* _ssid;
CTTelephonyNetworkInfo* _cttInfo;
}
- (id)init {
if (self = [super init]) {
_ref = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, [@"gw.alicdn.com" UTF8String]);
_cttInfo = [[CTTelephonyNetworkInfo alloc] init];
[self update];
[self startNotify];
}
return self;
}
+ (NetworkManager*)instance {
static NetworkManager* _instance = nil;
@synchronized(self) {
_instance = [[NetworkManager alloc] init];
}
return _instance;
}
/*
* String
*/
- (NSString*)currentStatusString {
return [NSString stringWithFormat:@"%u",_current];
}
/*
* Wifi,
* ssid
*/
- (NSString *)currentWifiSsid {
return _ssid;
}
- (_NetworkStatus)reachabilityFlags:(SCNetworkReachabilityFlags)flags {
if ((flags & kSCNetworkReachabilityFlagsReachable) == 0 || ![self internetConnection]) {
// The target host is not reachable.
return NotReachable;
}
_NetworkStatus returnValue = NotReachable;
if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0) {
returnValue = ReachableViaWiFi;
}
if ((((flags & kSCNetworkReachabilityFlagsConnectionOnDemand ) != 0) || (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0)) {
if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0) {
returnValue = ReachableViaWiFi;
}
}
if ((flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN) {
returnValue = ReachableVia4G;
}
if ((flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN) {
if((flags & kSCNetworkReachabilityFlagsReachable) == kSCNetworkReachabilityFlagsReachable) {
if ((flags & kSCNetworkReachabilityFlagsTransientConnection) == kSCNetworkReachabilityFlagsTransientConnection) {
returnValue = ReachableVia3G;
if((flags & kSCNetworkReachabilityFlagsConnectionRequired) == kSCNetworkReachabilityFlagsConnectionRequired) {
returnValue = ReachableVia2G;
}
}
}
}
double version = [[UIDevice currentDevice].systemVersion doubleValue];
if (version >= 7.0f && returnValue != ReachableViaWiFi) {
NSString *nettype = _cttInfo.currentRadioAccessTechnology;
if (nettype) {
if([CTRadioAccessTechnologyGPRS isEqualToString:nettype]) {
return ReachableVia2G;
} else if([CTRadioAccessTechnologyLTE isEqualToString: nettype] || [CTRadioAccessTechnologyeHRPD isEqualToString: nettype]) {
return ReachableVia4G;
}
}
}
return returnValue;
}
- (void)update {
SCNetworkReachabilityFlags flags = 0;
if (SCNetworkReachabilityGetFlags(_ref, &flags)) {
_last = _current;
_current = [self reachabilityFlags:flags];
// change bssid
if (_current == ReachableViaWiFi) {
NSArray *ifs = (id)CFBridgingRelease(CNCopySupportedInterfaces());
for (NSString *ifnam in ifs) {
id info = (id)CFBridgingRelease(CNCopyCurrentNetworkInfo((__bridge CFStringRef)ifnam));
NSString *bssidValue = [info objectForKey:(NSString*)kCNNetworkInfoKeyBSSID];
NSString *ssidValue = [info objectForKey:(NSString*)kCNNetworkInfoKeySSID];
if (bssidValue.length <= 0) {
continue;
}
_ssid = [NSString stringWithFormat:@"%@-%@", ssidValue, bssidValue];
}
}
}
}
//
static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) {
NetworkManager* instance = [NetworkManager instance];
[instance update];
}
- (void)startNotify {
SCNetworkReachabilityContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
if(SCNetworkReachabilitySetCallback(_ref, ReachabilityCallback, &context)) {
reachabilityQueue = dispatch_queue_create(networkManagerQueue, DISPATCH_QUEUE_SERIAL);
SCNetworkReachabilitySetDispatchQueue(_ref, reachabilityQueue);
}
}
- (BOOL)internetConnection {
struct sockaddr_in zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;
SCNetworkReachabilityRef defaultRouteReachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress);
SCNetworkReachabilityFlags flags;
BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags);
CFRelease(defaultRouteReachability);
if (!didRetrieveFlags) {
return NO;
}
BOOL isReachable = flags & kSCNetworkFlagsReachable;
BOOL needsConnection = flags & kSCNetworkFlagsConnectionRequired;
return (isReachable && !needsConnection) ? YES : NO;
}
+ (BOOL) configureProxies {
NSDictionary *proxySettings = CFBridgingRelease(CFNetworkCopySystemProxySettings());
NSLog(@"proxy setting: %@", proxySettings);
NSArray *proxies = nil;
NSURL *url = [[NSURL alloc] initWithString:@"http://api.m.taobao.com"];
proxies = CFBridgingRelease(CFNetworkCopyProxiesForURL((__bridge CFURLRef)url,
(__bridge CFDictionaryRef)proxySettings));
if (proxies > 0) {
NSDictionary *settings = [proxies objectAtIndex:0];
NSString* host = [settings objectForKey:(NSString *)kCFProxyHostNameKey];
NSString* port = [settings objectForKey:(NSString *)kCFProxyPortNumberKey];
if (host || port) {
return YES;
}
}
return NO;
}
@end

View File

@@ -0,0 +1,59 @@
//
// TestBase.h
// AlicloudHttpDNS
//
// Created by ElonChan地风 on 2017/4/14.
// Copyright © 2017年 alibaba-inc.com. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import "XCTestCase+AsyncTesting.h"
#import "HttpdnsRequest.h"
#import "HttpdnsHostObject.h"
#import "HttpdnsService.h"
#import "HttpdnsService_Internal.h"
#define NOTIFY [self notify:XCTAsyncTestCaseStatusSucceeded];
#define WAIT [self waitForStatus:XCTAsyncTestCaseStatusSucceeded timeout:30];
#define WAIT_60 [self waitForStatus:XCTAsyncTestCaseStatusSucceeded timeout:60];
#define WAIT_120 [self waitForStatus:XCTAsyncTestCaseStatusSucceeded timeout:120];
#define WAIT_10 [self waitForStatus:XCTAsyncTestCaseStatusSucceeded timeout:10.0];
#define WAIT_FOREVER [self waitForStatus:XCTAsyncTestCaseStatusSucceeded timeout:DBL_MAX];
static NSString *ipv4OnlyHost = @"ipv4.only.com";
static NSString *ipv6OnlyHost = @"ipv6.only.com";
static NSString *ipv4AndIpv6Host = @"ipv4.and.ipv6.com";
static NSString *ipv41 = @"1.1.1.1";
static NSString *ipv42 = @"2.2.2.2";
static NSString *ipv61 = @"2001:4860:4860::8888";
static NSString *ipv62 = @"2001:4860:4860::8844";
extern NSDictionary<NSString *, NSString *> *hostNameIpPrefixMap;
@interface TestBase : XCTestCase <HttpdnsLoggerProtocol>
@property (nonatomic, strong) HttpDnsService *httpdns;
@property (nonatomic, assign) NSTimeInterval currentTimeStamp;
- (HttpdnsHostObject *)constructSimpleIpv4HostObject;
- (HttpdnsHostObject *)constructSimpleIpv6HostObject;
- (HttpdnsHostObject *)constructSimpleIpv4AndIpv6HostObject;
- (void)presetNetworkEnvAsIpv4;
- (void)presetNetworkEnvAsIpv6;
- (void)presetNetworkEnvAsIpv4AndIpv6;
- (void)shouldNotHaveCallNetworkRequestWhenResolving:(void (^)(void))resolvingBlock;
- (void)shouldHaveCalledRequestWhenResolving:(void (^)(void))resolvingBlock;
@end

View File

@@ -0,0 +1,133 @@
//
// TestBase.m
// AlicloudHttpDNS
//
// Created by ElonChan on 2017/4/14.
// Copyright © 2017 alibaba-inc.com. All rights reserved.
//
#import "TestBase.h"
#import <mach/mach.h>
#import "HttpdnsIpStackDetector.h"
NSDictionary<NSString *, NSString *> *hostNameIpPrefixMap;
@implementation TestBase
+ (void)setUp {
hostNameIpPrefixMap = @{
@"v4host1.onlyforhttpdnstest.run.place": @"0.0.1",
@"v4host2.onlyforhttpdnstest.run.place": @"0.0.2",
@"v4host3.onlyforhttpdnstest.run.place": @"0.0.3",
@"v4host4.onlyforhttpdnstest.run.place": @"0.0.4",
@"v4host5.onlyforhttpdnstest.run.place": @"0.0.5"
};
}
- (void)setUp {
[super setUp];
}
- (void)tearDown {
[super tearDown];
}
- (void)log:(NSString *)logStr {
mach_port_t threadID = mach_thread_self();
NSString *threadIDString = [NSString stringWithFormat:@"%x", threadID];
printf("%ld-%s %s\n", (long)[[NSDate date] timeIntervalSince1970], [threadIDString UTF8String], [logStr UTF8String]);
}
- (HttpdnsHostObject *)constructSimpleIpv4HostObject {
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
hostObject.hostName = ipv4OnlyHost;
hostObject.v4ttl = 60;
HttpdnsIpObject *ip1 = [[HttpdnsIpObject alloc] init];
[ip1 setIp:ipv41];
HttpdnsIpObject *ip2 = [[HttpdnsIpObject alloc] init];
[ip2 setIp:ipv42];
hostObject.v4Ips = @[ip1, ip2];
hostObject.lastIPv4LookupTime = self.currentTimeStamp;
return hostObject;
}
- (HttpdnsHostObject *)constructSimpleIpv6HostObject {
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
hostObject.hostName = ipv4OnlyHost;
hostObject.v6ttl = 60;
HttpdnsIpObject *ip1 = [[HttpdnsIpObject alloc] init];
[ip1 setIp:@"2001:4860:4860::8888"];
HttpdnsIpObject *ip2 = [[HttpdnsIpObject alloc] init];
[ip2 setIp:@"2001:4860:4860::8844"];
hostObject.v6Ips = @[ip1, ip2];
hostObject.lastIPv6LookupTime = self.currentTimeStamp;
return hostObject;
}
- (HttpdnsHostObject *)constructSimpleIpv4AndIpv6HostObject {
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
hostObject.hostName = ipv4AndIpv6Host;
hostObject.v4ttl = 60;
HttpdnsIpObject *ip1 = [[HttpdnsIpObject alloc] init];
[ip1 setIp:ipv41];
HttpdnsIpObject *ip2 = [[HttpdnsIpObject alloc] init];
[ip2 setIp:ipv42];
hostObject.v4Ips = @[ip1, ip2];
hostObject.lastIPv4LookupTime = self.currentTimeStamp;
hostObject.v6ttl = 60;
HttpdnsIpObject *ip3 = [[HttpdnsIpObject alloc] init];
[ip3 setIp:ipv61];
HttpdnsIpObject *ip4 = [[HttpdnsIpObject alloc] init];
[ip4 setIp:ipv62];
hostObject.v6Ips = @[ip3, ip4];
hostObject.lastIPv6LookupTime = self.currentTimeStamp;
return hostObject;
}
- (void)presetNetworkEnvAsIpv4 {
HttpdnsIpStackDetector *mockIpv6Adapter = OCMPartialMock([HttpdnsIpStackDetector sharedInstance]);
OCMStub([mockIpv6Adapter currentIpStack]).andReturn(kHttpdnsIpv4Only);
OCMStub([mockIpv6Adapter isIpv6OnlyNetwork]).andReturn(NO);
id mockAdapterClass = OCMClassMock([HttpdnsIpStackDetector class]);
OCMStub([mockAdapterClass sharedInstance]).andReturn(mockIpv6Adapter);
}
- (void)presetNetworkEnvAsIpv6 {
HttpdnsIpStackDetector *mockIpv6Adapter = OCMPartialMock([HttpdnsIpStackDetector sharedInstance]);
OCMStub([mockIpv6Adapter currentIpStack]).andReturn(kHttpdnsIpv6Only);
OCMStub([mockIpv6Adapter isIpv6OnlyNetwork]).andReturn(YES);
id mockAdapterClass = OCMClassMock([HttpdnsIpStackDetector class]);
OCMStub([mockAdapterClass sharedInstance]).andReturn(mockIpv6Adapter);
}
- (void)presetNetworkEnvAsIpv4AndIpv6 {
HttpdnsIpStackDetector *mockIpv6Adapter = OCMPartialMock([HttpdnsIpStackDetector sharedInstance]);
OCMStub([mockIpv6Adapter currentIpStack]).andReturn(kHttpdnsIpDual);
OCMStub([mockIpv6Adapter isIpv6OnlyNetwork]).andReturn(NO);
id mockAdapterClass = OCMClassMock([HttpdnsIpStackDetector class]);
OCMStub([mockAdapterClass sharedInstance]).andReturn(mockIpv6Adapter);
}
- (void)shouldNotHaveCallNetworkRequestWhenResolving:(void (^)(void))resolvingBlock {
HttpDnsService *httpdns = [HttpDnsService sharedInstance];
HttpdnsRequestManager *requestManager = httpdns.requestManager;
HttpdnsRequestManager *mockScheduler = OCMPartialMock(requestManager);
OCMReject([mockScheduler executeRequest:[OCMArg any] retryCount:0]);
resolvingBlock();
OCMVerifyAll(mockScheduler);
}
- (void)shouldHaveCalledRequestWhenResolving:(void (^)(void))resolvingBlock {
HttpDnsService *httpdns = [HttpDnsService sharedInstance];
HttpdnsRequestManager *requestManager = httpdns.requestManager;
HttpdnsRequestManager *mockScheduler = OCMPartialMock(requestManager);
OCMExpect([mockScheduler executeRequest:[OCMArg any] retryCount:0]).andReturn(nil);
resolvingBlock();
OCMVerifyAll(mockScheduler);
}
@end

View File

@@ -0,0 +1,28 @@
//
// TestBase.h
// AlicloudHttpDNS
//
// Created by ElonChan地风 on 2017/4/14.
// Copyright © 2017年 alibaba-inc.com. All rights reserved.
//
#import <XCTest/XCTest.h>
enum {
XCTAsyncTestCaseStatusUnknown = 0,
XCTAsyncTestCaseStatusWaiting,
XCTAsyncTestCaseStatusSucceeded,
XCTAsyncTestCaseStatusFailed,
XCTAsyncTestCaseStatusCancelled,
};
typedef NSUInteger XCTAsyncTestCaseStatus;
@interface XCTestCase (AsyncTesting)
- (void)waitForStatus:(XCTAsyncTestCaseStatus)status timeout:(NSTimeInterval)timeout;
- (void)waitForTimeout:(NSTimeInterval)timeout;
- (void)notify:(XCTAsyncTestCaseStatus)status;
@end

View File

@@ -0,0 +1,113 @@
//
// TestBase.h
// AlicloudHttpDNS
//
// Created by ElonChan on 2017/4/14.
// Copyright © 2017 alibaba-inc.com. All rights reserved.
//
#import "XCTestCase+AsyncTesting.h"
#import "objc/runtime.h"
static void *kLoopUntil_Key = "LoopUntil_Key";
static void *kNotified_Key = "kNotified_Key";
static void *kNotifiedStatus_Key = "kNotifiedStatus_Key";
static void *kExpectedStatus_Key = "kExpectedStatus_Key";
@implementation XCTestCase (AsyncTesting)
#pragma mark - Public
- (void)waitForStatus:(XCTAsyncTestCaseStatus)status timeout:(NSTimeInterval)timeout {
self.notified = NO;
self.expectedStatus = status;
self.loopUntil = [NSDate dateWithTimeIntervalSinceNow:timeout];
NSDate *dt = [NSDate dateWithTimeIntervalSinceNow:0.1];
while (!self.notified && [self.loopUntil timeIntervalSinceNow] > 0) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:dt];
dt = [NSDate dateWithTimeIntervalSinceNow:0.1];
}
// Only assert when notified. Do not assert when timed out
// Fail if not notified
if (self.notified) {
XCTAssertEqual(self.notifiedStatus, self.expectedStatus, @"Notified status does not match the expected status.");
} else {
XCTFail(@"Async test timed out.");
}
}
- (void)waitForTimeout:(NSTimeInterval)timeout {
self.notified = NO;
self.expectedStatus = XCTAsyncTestCaseStatusUnknown;
self.loopUntil = [NSDate dateWithTimeIntervalSinceNow:timeout];
NSDate *dt = [NSDate dateWithTimeIntervalSinceNow:0.1];
while (!self.notified && [self.loopUntil timeIntervalSinceNow] > 0) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:dt];
dt = [NSDate dateWithTimeIntervalSinceNow:0.1];
}
}
- (void)notify:(XCTAsyncTestCaseStatus)status {
self.notifiedStatus = status;
// self.notified must be set at the last of this method
self.notified = YES;
}
#pragma nark - Object Association Helpers -
- (void)setAssociatedObject:(id)anObject key:(void*)key {
objc_setAssociatedObject(self, key, anObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)getAssociatedObject:(void*)key {
id anObject = objc_getAssociatedObject(self, key);
return anObject;
}
#pragma mark - Property Implementations -
- (NSDate*)loopUntil {
return [self getAssociatedObject:kLoopUntil_Key];
}
- (void)setLoopUntil:(NSDate*)value {
[self setAssociatedObject:value key:kLoopUntil_Key];
}
- (BOOL)notified {
NSNumber *valueNumber = [self getAssociatedObject:kNotified_Key];
return [valueNumber boolValue];
}
- (void)setNotified:(BOOL)value {
NSNumber *valueNumber = [NSNumber numberWithBool:value];
[self setAssociatedObject:valueNumber key:kNotified_Key];
}
- (XCTAsyncTestCaseStatus)notifiedStatus {
NSNumber *valueNumber = [self getAssociatedObject:kNotifiedStatus_Key];
return [valueNumber integerValue];
}
- (void)setNotifiedStatus:(XCTAsyncTestCaseStatus)value {
NSNumber *valueNumber = [NSNumber numberWithInt:(int)value];
[self setAssociatedObject:valueNumber key:kNotifiedStatus_Key];
}
- (XCTAsyncTestCaseStatus)expectedStatus {
NSNumber *valueNumber = [self getAssociatedObject:kExpectedStatus_Key];
return [valueNumber integerValue];
}
- (void)setExpectedStatus:(XCTAsyncTestCaseStatus)value {
NSNumber *valueNumber = [NSNumber numberWithInt:(int)value];
[self setAssociatedObject:valueNumber key:kExpectedStatus_Key];
}
@end