feat: sync httpdns sdk/platform updates without large binaries
This commit is contained in:
477
HttpDNSSDK/sdk/ios/NewHttpDNSTests/DB/DBTest.m
Normal file
477
HttpDNSSDK/sdk/ios/NewHttpDNSTests/DB/DBTest.m
Normal file
@@ -0,0 +1,477 @@
|
||||
//
|
||||
// DBTest.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// Created by xuyecan on 2025/3/15.
|
||||
// Copyright © 2025 trustapp.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
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// CacheKeyFunctionTest.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// Created by xuyecan on 2024/6/12.
|
||||
// Copyright © 2024 trustapp.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);
|
||||
// 同步接口,不复用过期ip的情况下,解析出的ip一定是未过期的
|
||||
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再加载到缓存<EFBFBD><EFBFBD>?
|
||||
[self.httpdns.requestManager syncLoadCacheFromDbToMemory];
|
||||
|
||||
HttpdnsResult *result = [self.httpdns resolveHostSyncNonBlocking:testHost byIpType:HttpdnsQueryIPTypeIpv4];
|
||||
// 没有使用cacheKey,所以这里应该是nil
|
||||
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
|
||||
108
HttpDNSSDK/sdk/ios/NewHttpDNSTests/HighLevelTest/CustomTTLTest.m
Normal file
108
HttpDNSSDK/sdk/ios/NewHttpDNSTests/HighLevelTest/CustomTTLTest.m
Normal file
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// CustomTTLAndCleanCacheTest.m
|
||||
// TrustHttpDNS
|
||||
//
|
||||
// Created by xuyecan on 2024/6/17.
|
||||
// Copyright © 2024 trustapp.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:(TrustHttpDNS_IPType)ipType ttl:(int64_t)ttl {
|
||||
// 为了在并发测试中域名快速过期,将ttl设置<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
|
||||
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
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// EnableReuseExpiredIpTest.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// Created by xuyecan on 2024/5/28.
|
||||
// Copyright © 2024 trustapp.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:(TrustHttpDNS_IPType)ipType ttl:(int64_t)ttl {
|
||||
// 在测试中域名快速过<EFBFBD><EFBFBD>?
|
||||
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);
|
||||
// 因为运行复用过期解析结果,因此这里获得的一定是已经过期<EFBFBD><EFBFBD>?
|
||||
XCTAssertGreaterThan([[NSDate date] timeIntervalSince1970], result2.lastUpdatedTimeInterval + result2.ttl);
|
||||
NSString *firstIp2 = [result2 firstIpv4Address];
|
||||
XCTAssertTrue([firstIp2 hasPrefix:ipPrefix]);
|
||||
|
||||
// 等待第二次解析触发的请求完成
|
||||
[NSThread sleepForTimeInterval:1];
|
||||
|
||||
// 再次使用nonblocking方法解析,此时应该已经拿到有效结<EFBFBD><EFBFBD>?
|
||||
HttpdnsResult *result3 = [self.httpdns resolveHostSyncNonBlocking:host byIpType:HttpdnsQueryIPTypeIpv4];
|
||||
XCTAssertNotNil(result3);
|
||||
XCTAssertTrue([result3.host isEqualToString:host]);
|
||||
XCTAssertGreaterThan(result3.ttl, 0);
|
||||
XCTAssertLessThanOrEqual(result3.ttl, ttlForTest);
|
||||
// 有效结果必定未过<EFBFBD><EFBFBD>?
|
||||
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
|
||||
@@ -0,0 +1,165 @@
|
||||
//
|
||||
// HttpdnsHostObjectTest.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// Created by xuyecan on 2025/3/14.
|
||||
// Copyright © 2025 trustapp.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 - 基本属性测<EFBFBD><EFBFBD>?
|
||||
|
||||
- (void)testHostObjectProperties {
|
||||
// 创建一个HttpdnsHostObject实例
|
||||
HttpdnsHostObject *hostObject = [[HttpdnsHostObject alloc] init];
|
||||
|
||||
// 设置基本属<EFBFBD><EFBFBD>?
|
||||
hostObject.host = @"example.com";
|
||||
hostObject.ttl = 60;
|
||||
hostObject.queryTimes = 1;
|
||||
hostObject.clientIP = @"192.168.1.1";
|
||||
|
||||
// 验证属性<EFBFBD><EFBFBD>?
|
||||
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];
|
||||
|
||||
// 设置基本属<EFBFBD><EFBFBD>?
|
||||
ipObject.ip = @"1.2.3.4";
|
||||
ipObject.ttl = 300;
|
||||
ipObject.priority = 10;
|
||||
ipObject.detectRT = 50; // 测试新添加的detectRT属<EFBFBD><EFBFBD>?
|
||||
|
||||
// 验证属性<EFBFBD><EFBFBD>?
|
||||
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];
|
||||
|
||||
// 测试默认<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(ipObject.detectRT, -1, @"detectRT的默认值应该是-1");
|
||||
|
||||
// 测试设置检测时<EFBFBD><EFBFBD>?
|
||||
[ipObject setDetectRT:100];
|
||||
XCTAssertEqual(ipObject.detectRT, 100, @"detectRT应该被正确设置为100");
|
||||
|
||||
// 测试设置为负<EFBFBD><EFBFBD>?
|
||||
[ipObject setDetectRT:-5];
|
||||
XCTAssertEqual(ipObject.detectRT, -1, @"设置负值时detectRT应该被设置为-1");
|
||||
|
||||
// 测试设置<EFBFBD><EFBFBD>?
|
||||
[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对象到主机对<EFBFBD><EFBFBD>?
|
||||
[hostObject addIpv4:ipv4Object];
|
||||
[hostObject addIpv6:ipv6Object];
|
||||
|
||||
// 验证IP对象是否被正确添<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(hostObject.ipv4List.count, 1, @"应该<E5BA94><E8AFA5>?个IPv4对象");
|
||||
XCTAssertEqual(hostObject.ipv6List.count, 1, @"应该<E5BA94><E8AFA5>?个IPv6对象");
|
||||
|
||||
// 验证IP对象的属<EFBFBD><EFBFBD>?
|
||||
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对象,具有不同的检测时<EFBFBD><EFBFBD>?
|
||||
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; // 未检<EFBFBD><EFBFBD>?
|
||||
|
||||
// 添加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, @"应该<E5BA94><E8AFA5>?个IP对象");
|
||||
XCTAssertEqualObjects(sortedIps[0].ip, @"2.2.2.2", @"检测时间最短的IP应该排在第一<EFBFBD><EFBFBD>?);
|
||||
XCTAssertEqualObjects(sortedIps[1].ip, @"1.1.1.1", @"检测时间第二短的IP应该排在第二<EFBFBD><EFBFBD>?);
|
||||
XCTAssertEqualObjects(sortedIps[2].ip, @"3.3.3.3", @"检测时间第三短的IP应该排在第三<EFBFBD><EFBFBD>?);
|
||||
XCTAssertEqualObjects(sortedIps[3].ip, @"4.4.4.4", @"未检测的IP应该排在最<EFBFBD><EFBFBD>?);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,156 @@
|
||||
//
|
||||
// ManuallyCleanCacheTest.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// Created by xuyecan on 2024/6/17.
|
||||
// Copyright © 2024 trustapp.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
|
||||
@@ -0,0 +1,363 @@
|
||||
//
|
||||
// MultithreadCorrectnessTest.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// Created by xuyecan on 2024/5/26.
|
||||
// Copyright © 2024 trustapp.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];
|
||||
}
|
||||
|
||||
// 非阻塞接口不能阻塞调用线<EFBFBD><EFBFBD>?
|
||||
- (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<EFBFBD><EFBFBD>?
|
||||
[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];
|
||||
});
|
||||
|
||||
// 确保第一个请求已经开<EFBFBD><EFBFBD>?
|
||||
[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), ^{
|
||||
// 第二次请求,由于是同一个域名,所以它应该等待第一个请求的返回
|
||||
// 第一个请求返回后,第二个请求不应该再次请求,而是直接从缓存中读取到结果,返回
|
||||
// 所以它的等待时间接<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?
|
||||
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 这里暂时无法跑过,因为现在锁的机制,会导致第二个请求也要去请<EFBFBD><EFBFBD>?
|
||||
// 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 {
|
||||
// 第二次调<EFBFBD><EFBFBD>?
|
||||
[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];
|
||||
});
|
||||
|
||||
// 确保第一个请求已经开<EFBFBD><EFBFBD>?
|
||||
[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), ^{
|
||||
// 第二次请求,由于是同一个域名,所以它应该等待第一个请求的返回
|
||||
// 第一个请求失败后,第二个请求从缓存拿不到结果,应该再次请<EFBFBD><EFBFBD>?
|
||||
// 所以它等待的时间将是约5<EFBFBD><EFBFBD>?
|
||||
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);
|
||||
}
|
||||
|
||||
// 同步接口设置最大等待时<EFBFBD><EFBFBD>?
|
||||
- (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);
|
||||
}
|
||||
|
||||
// 限制设置的等待时间在一个合理范围,目前<EFBFBD><EFBFBD>?.5 - 5<EFBFBD><EFBFBD>?
|
||||
- (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);
|
||||
}
|
||||
|
||||
// 设置异步回调接口的最大回调等待时<EFBFBD><EFBFBD>?
|
||||
- (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);
|
||||
}
|
||||
|
||||
// 多线程状态下每个线程的等待时<EFBFBD><EFBFBD>?
|
||||
- (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
|
||||
@@ -0,0 +1,301 @@
|
||||
//
|
||||
// PresetCacheAndRetrieveTest.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// Created by xuyecan on 2024/5/26.
|
||||
// Copyright © 2024 trustapp.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"
|
||||
|
||||
|
||||
/**
|
||||
* 由于使用OCMock在连续的测试用例中重复Mock对象(即使每次都已经stopMocking)会有内存错乱的问题,
|
||||
* 目前还解决不了,所以这个类中的测试case,需要手动单独执<EFBFBD><EFBFBD>?
|
||||
*/
|
||||
@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];
|
||||
|
||||
// 请求类型为ipv4,拿到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,拿到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]);
|
||||
|
||||
// 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]);
|
||||
|
||||
// 请求类型为auto,只拿到ipv4结果
|
||||
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];
|
||||
|
||||
// 请求类型为ipv4,拿到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,拿到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]);
|
||||
|
||||
// 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]);
|
||||
|
||||
// 请求类型为auto,注意,我们认为ipv6only只存在理论上,比如实验室环境
|
||||
// 因此,ipv4的地址是一定会去解析的,auto的作用在于,如果发现网络还支持ipv6,那就多获取ipv6的结<EFBFBD><EFBFBD>?
|
||||
// 因此,这里得到的也是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]);
|
||||
}
|
||||
|
||||
// 网络情况为ipv4和ipv6下的缓存测试
|
||||
- (void)testSimplyRetrieveCachedResultUnderDualStack {
|
||||
[self presetNetworkEnvAsIpv4AndIpv6];
|
||||
[self.httpdns cleanAllHostCache];
|
||||
|
||||
// 存入ipv4和ipv6的地址
|
||||
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]);
|
||||
|
||||
// 请求ipv4和ipv6
|
||||
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]);
|
||||
}
|
||||
|
||||
// ttl、lastLookupTime,ipv4和ipv6是分开处理<EFBFBD><EFBFBD>?
|
||||
- (void)testTTLAndLastLookUpTime {
|
||||
[self presetNetworkEnvAsIpv4AndIpv6];
|
||||
[self.httpdns cleanAllHostCache];
|
||||
|
||||
// 存入ipv4和ipv6的地址
|
||||
HttpdnsHostObject *hostObject1 = [self constructSimpleIpv4AndIpv6HostObject];
|
||||
hostObject1.v4ttl = 200;
|
||||
hostObject1.v6ttl = 300;
|
||||
|
||||
int64_t currentTimestamp = [[NSDate new] timeIntervalSince1970];
|
||||
|
||||
hostObject1.lastIPv4LookupTime = currentTimestamp - 1;
|
||||
hostObject1.lastIPv6LookupTime = currentTimestamp - 2;
|
||||
|
||||
// 第一次设置缓<EFBFBD><EFBFBD>?
|
||||
[self.httpdns.requestManager mergeLookupResultToManager:hostObject1 host:ipv4AndIpv6Host cacheKey:ipv4AndIpv6Host underQueryIpType:HttpdnsQueryIPTypeBoth];
|
||||
|
||||
// auto在当前环境下即请求ipv4和ipv6
|
||||
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地址的相关信<EFBFBD><EFBFBD>?
|
||||
[self.httpdns.requestManager mergeLookupResultToManager:hostObject2 host:ipv4AndIpv6Host cacheKey:ipv4AndIpv6Host underQueryIpType:HttpdnsQueryIPTypeIpv4];
|
||||
|
||||
// v4的信息发生变化,v6的信息保持不<EFBFBD><EFBFBD>?
|
||||
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);
|
||||
}
|
||||
|
||||
// 只缓存ipv4单栈的地址,按请求双栈类型存入,此时会标记该域名没有ipv6地址
|
||||
// 按预期,会判断该域名没有ipv6地址,因此不会返回ipv6地址,也不会发请<EFBFBD><EFBFBD>?
|
||||
- (void)testMergeNoIpv6ResultAndGetBoth {
|
||||
[self presetNetworkEnvAsIpv4AndIpv6];
|
||||
|
||||
HttpdnsHostObject *hostObject = [self constructSimpleIpv4HostObject];
|
||||
|
||||
// 双栈下解析结果仅有ipv4,合并时会标记该host无ipv6
|
||||
[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);
|
||||
}];
|
||||
}
|
||||
|
||||
// 缓存ipv4单栈的地址,但是请求ipv4类型存入,此时不会打标记没有ipv6
|
||||
// 于是,读取时,会尝试发请求获取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);
|
||||
}
|
||||
|
||||
// 缓存ipv6单栈的地址,但是请求ipv6类型存入,此时不会打标记没有ipv4
|
||||
// 于是,读取时,会尝试发请求获取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
|
||||
@@ -0,0 +1,265 @@
|
||||
//
|
||||
// ResolvingEffectiveHostTest.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// Created by xuyecan on 2024/5/28.
|
||||
// Copyright © 2024 trustapp.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:(TrustHttpDNS_IPType)ipType ttl:(int64_t)ttl {
|
||||
// 为了在并发测试中域名快速过期,将ttl设置为随<EFBFBD><EFBFBD>?-4<EFBFBD><EFBFBD>?
|
||||
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);
|
||||
// 同步接口,不复用过期ip的情况下,解析出的ip一定是未过期的
|
||||
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;
|
||||
// 非阻塞接口任何情况下不应该阻塞超<EFBFBD><EFBFBD>?0ms
|
||||
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);
|
||||
}
|
||||
|
||||
// 指定查询both,但域名都只配置了ipv4
|
||||
// 这种情况下,会自动打标该域名无ipv6,后续的结果只会包含ipv4地址
|
||||
- (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
|
||||
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// ScheduleCenterV4Test.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// Created by xuyecan on 2024/6/16.
|
||||
// Copyright © 2024 trustapp.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"
|
||||
|
||||
|
||||
/**
|
||||
* 由于使用OCMock在连续的测试用例中重复Mock对象(即使每次都已经stopMocking)会有内存错乱的问题,
|
||||
* 目前还解决不了,所以这个类中的测试case,需要手动单独执<EFBFBD><EFBFBD>?
|
||||
*/
|
||||
@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
|
||||
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// ScheduleCenterV6Test.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// Created by xuyecan on 2024/6/17.
|
||||
// Copyright © 2024 trustapp.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"
|
||||
|
||||
|
||||
/**
|
||||
* 由于使用OCMock在连续的测试用例中重复Mock对象(即使每次都已经stopMocking)会有内存错乱的问题,
|
||||
* 目前还解决不了,所以这个类中的测试case,需要手动单独执<EFBFBD><EFBFBD>?
|
||||
*/
|
||||
@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
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// SdnsScenarioTest.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// Created by xuyecan on 2024/5/29.
|
||||
// Copyright © 2024 trustapp.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:(TrustHttpDNS_IPType)ipType ttl:(int64_t)ttl {
|
||||
// 在测试中域名快速过<EFBFBD><EFBFBD>?
|
||||
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);
|
||||
|
||||
// 按预期,上面的结果是按cacheKey来缓存的,这里不指定cacheKey,应当拿到nil
|
||||
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
|
||||
402
HttpDNSSDK/sdk/ios/NewHttpDNSTests/IPDetector/IpDetectorTest.m
Normal file
402
HttpDNSSDK/sdk/ios/NewHttpDNSTests/IPDetector/IpDetectorTest.m
Normal file
@@ -0,0 +1,402 @@
|
||||
//
|
||||
// IpDetectorTest.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// Created by xuyecan on 2025/3/14.
|
||||
// Copyright © 2025 trustapp.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 - 单例和基本属性测<EFBFBD><EFBFBD>?
|
||||
|
||||
- (void)testSharedInstance {
|
||||
// 测试单例模式
|
||||
HttpdnsIPQualityDetector *detector1 = [HttpdnsIPQualityDetector sharedInstance];
|
||||
HttpdnsIPQualityDetector *detector2 = [HttpdnsIPQualityDetector sharedInstance];
|
||||
|
||||
XCTAssertEqual(detector1, detector2, @"单例模式应该返回相同的实<EFBFBD><EFBFBD>?);
|
||||
XCTAssertNotNil(detector1, @"单例实例不应为nil");
|
||||
}
|
||||
|
||||
#pragma mark - TCP连接测试
|
||||
|
||||
- (void)testTcpConnectToValidIP {
|
||||
// 测试连接到有效IP
|
||||
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
|
||||
|
||||
// 使用公共DNS服务器作为测试目<EFBFBD><EFBFBD>?
|
||||
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];
|
||||
|
||||
// 验证连接失败并返<EFBFBD><EFBFBD>?1
|
||||
XCTAssertEqual(costTime, -1, @"连接到无效IP应返<E5BA94><E8BF94>?1");
|
||||
}
|
||||
|
||||
- (void)testTcpConnectWithInvalidParameters {
|
||||
// 测试使用无效参数进行连接
|
||||
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
|
||||
|
||||
// 测试空IP
|
||||
NSInteger costTime = [detector tcpConnectToIP:nil port:80];
|
||||
XCTAssertEqual(costTime, -1, @"使用nil IP应返<E5BA94><E8BF94>?1");
|
||||
|
||||
// 测试无效格式的IP
|
||||
costTime = [detector tcpConnectToIP:@"not-an-ip" port:80];
|
||||
XCTAssertEqual(costTime, -1, @"使用无效格式IP应返<E5BA94><E8BF94>?1");
|
||||
|
||||
// 测试无效端口
|
||||
costTime = [detector tcpConnectToIP:@"8.8.8.8" port:-1];
|
||||
XCTAssertEqual(costTime, -1, @"使用无效端口应返<E5BA94><E8BF94>?1");
|
||||
}
|
||||
|
||||
#pragma mark - 任务调度测试
|
||||
|
||||
- (void)testScheduleIPQualityDetection {
|
||||
// 测试调度IP质量检测任<EFBFBD><EFBFBD>?
|
||||
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方法不应被调<EFBFBD><EFBFBD>?
|
||||
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,我们采用另一种方式测试并发限<EFBFBD><EFBFBD>?
|
||||
// 模拟scheduleIPQualityDetection内部实现,当调用时直接执行addPendingTask
|
||||
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,模拟并发限制已达到的情<EFBFBD><EFBFBD>?
|
||||
[detector addPendingTask:cacheKey ip:ip port:port callback:callback];
|
||||
});
|
||||
|
||||
// 设置期望:验证addPendingTask被调用,而executeDetection不被调用
|
||||
// 使用同一个mock对象,避免创建多个mock
|
||||
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 {
|
||||
// 测试添加待处理任<EFBFBD><EFBFBD>?
|
||||
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
|
||||
id detectorMock = OCMPartialMock(detector);
|
||||
|
||||
// 模拟processPendingTasksIfNeeded方法,避免实际处理任<EFBFBD><EFBFBD>?
|
||||
OCMStub([detectorMock processPendingTasksIfNeeded]);
|
||||
|
||||
// 记录初始待处理任务数<EFBFBD><EFBFBD>?
|
||||
NSUInteger initialCount = [detector pendingTasksCount];
|
||||
|
||||
// 添加一个待处理任务
|
||||
[detectorMock addPendingTask:@"example.com"
|
||||
ip:@"1.2.3.4"
|
||||
port:[NSNumber numberWithInt:80]
|
||||
callback:^(NSString *cacheKey, NSString *ip, NSInteger costTime) {}];
|
||||
|
||||
// 验证待处理任务数量增<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([detector pendingTasksCount], initialCount + 1, @"添加任务后待处理任务数量应增<E5BA94><E5A29E>?");
|
||||
|
||||
// 停止模拟
|
||||
[detectorMock stopMocking];
|
||||
}
|
||||
|
||||
- (void)testPendingTasksProcessing {
|
||||
// 测试待处理任务的处理
|
||||
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
|
||||
id detectorMock = OCMPartialMock(detector);
|
||||
|
||||
// 模拟executeDetection方法,避免实际执行检<EFBFBD><EFBFBD>?
|
||||
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) {}];
|
||||
|
||||
// 手动触发处理待处理任<EFBFBD><EFBFBD>?
|
||||
[detectorMock processPendingTasksIfNeeded];
|
||||
|
||||
// 给处理任务一些时<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:0.1];
|
||||
|
||||
// 验证待处理任务已被处<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([detector pendingTasksCount], 0, @"处理后待处理任务数量应为0");
|
||||
|
||||
// 停止模拟
|
||||
[detectorMock stopMocking];
|
||||
}
|
||||
|
||||
#pragma mark - 异步回调测试
|
||||
|
||||
- (void)testExecuteDetection {
|
||||
// 测试执行检测并回调
|
||||
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
|
||||
id detectorMock = OCMPartialMock(detector);
|
||||
|
||||
// 模拟tcpConnectToIP方法返回固定<EFBFBD><EFBFBD>?
|
||||
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应正<EFBFBD><EFBFBD>?);
|
||||
XCTAssertEqualObjects(ip, @"1.2.3.4", @"回调中的IP应正<EFBFBD><EFBFBD>?);
|
||||
XCTAssertEqual(costTime, 100, @"回调中的耗时应正<EFBFBD><EFBFBD>?);
|
||||
|
||||
[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应正<EFBFBD><EFBFBD>?);
|
||||
XCTAssertEqualObjects(ip, @"1.2.3.4", @"回调中的IP应正<EFBFBD><EFBFBD>?);
|
||||
XCTAssertEqual(costTime, -1, @"连接失败时回调中的耗时应为-1");
|
||||
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
// 等待异步操作完成
|
||||
[self waitForExpectationsWithTimeout:5.0 handler:nil];
|
||||
|
||||
// 停止模拟
|
||||
[detectorMock stopMocking];
|
||||
}
|
||||
|
||||
- (void)testExecuteDetectionWithNilPort {
|
||||
// 测试执行检测时端口为nil的情<EFBFBD><EFBFBD>?
|
||||
HttpdnsIPQualityDetector *detector = [HttpdnsIPQualityDetector sharedInstance];
|
||||
id detectorMock = OCMPartialMock(detector);
|
||||
|
||||
// 模拟tcpConnectToIP方法,验证使用默认端<EFBFBD><EFBFBD>?0
|
||||
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一个机<EFBFBD><EFBFBD>?
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// 设置返回<EFBFBD><EFBFBD>?
|
||||
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应正<EFBFBD><EFBFBD>?);
|
||||
XCTAssertEqualObjects(ip, @"1.2.3.4", @"回调中的IP应正<EFBFBD><EFBFBD>?);
|
||||
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
// 清除局部变量的强引<EFBFBD><EFBFBD>?
|
||||
tempCacheKey = nil;
|
||||
tempIP = nil;
|
||||
|
||||
// 强制GC(注意:在ARC下这不一定会立即触发<EFBFBD><EFBFBD>?
|
||||
@autoreleasepool {
|
||||
// 触发自动释放<EFBFBD><EFBFBD>?
|
||||
}
|
||||
|
||||
// 验证对象没有被释放(应该被executeDetection方法内部强引用)
|
||||
XCTAssertNotNil(weakCacheKey, @"cacheKey不应被释<EFBFBD><EFBFBD>?);
|
||||
XCTAssertNotNil(weakIP, @"IP不应被释<EFBFBD><EFBFBD>?);
|
||||
|
||||
// 等待异步操作完成
|
||||
[self waitForExpectationsWithTimeout:5.0 handler:nil];
|
||||
|
||||
// 停止模拟
|
||||
[detectorMock stopMocking];
|
||||
}
|
||||
|
||||
@end
|
||||
24
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Info.plist
Normal file
24
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Info.plist
Normal 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>
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// HttpdnsNWHTTPClientTestBase.h
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// @author Created by Claude Code on 2025-11-01
|
||||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||||
//
|
||||
// 测试基类 - 为所<E4B8BA><E68980>?HttpdnsNWHTTPClient 测试提供共享<E585B1><E4BAAB>?setup/teardown
|
||||
//
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
#import "HttpdnsNWHTTPClient.h"
|
||||
#import "HttpdnsNWHTTPClient_Internal.h"
|
||||
#import "HttpdnsNWReusableConnection.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HttpdnsNWHTTPClientTestBase : XCTestCase
|
||||
|
||||
@property (nonatomic, strong) HttpdnsNWHTTPClient *client;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// HttpdnsNWHTTPClientTestBase.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// @author Created by Claude Code on 2025-11-01
|
||||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||||
//
|
||||
// 测试基类实现 - 共享的环境配置与清理逻辑
|
||||
//
|
||||
// 注意:所有测试需要先启动本地 mock server
|
||||
// 启动命令:cd TrustHttpDNSTests/Network && python3 mock_server.py
|
||||
// 服务端口<EFBFBD><EFBFBD>?
|
||||
// - HTTP: 11080
|
||||
// - HTTPS: 11443, 11444, 11445, 11446
|
||||
//
|
||||
|
||||
#import "HttpdnsNWHTTPClientTestBase.h"
|
||||
|
||||
@implementation HttpdnsNWHTTPClientTestBase
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
|
||||
// 设置环境变量以跳<EFBFBD><EFBFBD>?TLS 验证(用于本<EFBFBD><EFBFBD>?mock server 的自签名证书<EFBFBD><EFBFBD>?
|
||||
// 这是安全的,因为<EFBFBD><EFBFBD>?
|
||||
// 1. 仅在测试环境生效
|
||||
// 2. 连接限制为本<EFBFBD><EFBFBD>?loopback (127.0.0.1)
|
||||
// 3. 不影响生产代<EFBFBD><EFBFBD>?
|
||||
setenv("HTTPDNS_SKIP_TLS_VERIFY", "1", 1);
|
||||
|
||||
self.client = [[HttpdnsNWHTTPClient alloc] init];
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
// 清除环境变量,避免影响其他测<EFBFBD><EFBFBD>?
|
||||
unsetenv("HTTPDNS_SKIP_TLS_VERIFY");
|
||||
|
||||
self.client = nil;
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// HttpdnsNWHTTPClientTestHelper.h
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// @author Created by Claude Code on 2025-11-01
|
||||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HttpdnsNWHTTPClientTestHelper : NSObject
|
||||
|
||||
#pragma mark - HTTP 响应数据构<E68DAE><E69E84>?
|
||||
|
||||
// 构造标<E980A0><E6A087>?HTTP 响应数据
|
||||
+ (NSData *)createHTTPResponseWithStatus:(NSInteger)statusCode
|
||||
statusText:(NSString *)statusText
|
||||
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||
body:(nullable NSData *)body;
|
||||
|
||||
// 构<><E69E84>?chunked 编码<E7BC96><E7A081>?HTTP 响应
|
||||
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
|
||||
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||
chunks:(NSArray<NSData *> *)chunks;
|
||||
|
||||
// 构<><E69E84>?chunked 编码<E7BC96><E7A081>?HTTP 响应(带 trailers<72><73>?
|
||||
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
|
||||
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||
chunks:(NSArray<NSData *> *)chunks
|
||||
trailers:(nullable NSDictionary<NSString *, NSString *> *)trailers;
|
||||
|
||||
#pragma mark - Chunked 编码工具
|
||||
|
||||
// 编码单个 chunk
|
||||
+ (NSData *)encodeChunk:(NSData *)data;
|
||||
|
||||
// 编码单个 chunk(带 extension<6F><6E>?
|
||||
+ (NSData *)encodeChunk:(NSData *)data extension:(nullable NSString *)extension;
|
||||
|
||||
// 编码终止 chunk(size=0<><30>?
|
||||
+ (NSData *)encodeLastChunk;
|
||||
|
||||
// 编码终止 chunk(带 trailers<72><73>?
|
||||
+ (NSData *)encodeLastChunkWithTrailers:(NSDictionary<NSString *, NSString *> *)trailers;
|
||||
|
||||
#pragma mark - 测试数据生成
|
||||
|
||||
// 生成指定大小的随机数<E69CBA><E695B0>?
|
||||
+ (NSData *)randomDataWithSize:(NSUInteger)size;
|
||||
|
||||
// 生成 JSON 格式的响应体
|
||||
+ (NSData *)jsonBodyWithDictionary:(NSDictionary *)dictionary;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,168 @@
|
||||
//
|
||||
// HttpdnsNWHTTPClientTestHelper.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// @author Created by Claude Code on 2025-11-01
|
||||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpdnsNWHTTPClientTestHelper.h"
|
||||
|
||||
@implementation HttpdnsNWHTTPClientTestHelper
|
||||
|
||||
#pragma mark - HTTP 响应数据构<EFBFBD><EFBFBD>?
|
||||
|
||||
+ (NSData *)createHTTPResponseWithStatus:(NSInteger)statusCode
|
||||
statusText:(NSString *)statusText
|
||||
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||
body:(nullable NSData *)body {
|
||||
NSMutableString *response = [NSMutableString string];
|
||||
|
||||
// 状态行
|
||||
[response appendFormat:@"HTTP/1.1 %ld %@\r\n", (long)statusCode, statusText ?: @"OK"];
|
||||
|
||||
// 头部
|
||||
if (headers) {
|
||||
for (NSString *key in headers) {
|
||||
[response appendFormat:@"%@: %@\r\n", key, headers[key]];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果<EFBFBD><EFBFBD>?body 但没<EFBFBD><EFBFBD>?Content-Length,自动添<EFBFBD><EFBFBD>?
|
||||
if (body && body.length > 0 && !headers[@"Content-Length"]) {
|
||||
[response appendFormat:@"Content-Length: %lu\r\n", (unsigned long)body.length];
|
||||
}
|
||||
|
||||
// 空行分隔头部<EFBFBD><EFBFBD>?body
|
||||
[response appendString:@"\r\n"];
|
||||
|
||||
NSMutableData *responseData = [[response dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
|
||||
|
||||
// 添加 body
|
||||
if (body) {
|
||||
[responseData appendData:body];
|
||||
}
|
||||
|
||||
return [responseData copy];
|
||||
}
|
||||
|
||||
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
|
||||
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||
chunks:(NSArray<NSData *> *)chunks {
|
||||
return [self createChunkedHTTPResponseWithStatus:statusCode
|
||||
headers:headers
|
||||
chunks:chunks
|
||||
trailers:nil];
|
||||
}
|
||||
|
||||
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
|
||||
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
|
||||
chunks:(NSArray<NSData *> *)chunks
|
||||
trailers:(nullable NSDictionary<NSString *, NSString *> *)trailers {
|
||||
NSMutableString *response = [NSMutableString string];
|
||||
|
||||
// 状态行
|
||||
[response appendFormat:@"HTTP/1.1 %ld OK\r\n", (long)statusCode];
|
||||
|
||||
// 头部
|
||||
if (headers) {
|
||||
for (NSString *key in headers) {
|
||||
[response appendFormat:@"%@: %@\r\n", key, headers[key]];
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer-Encoding 头部
|
||||
[response appendString:@"Transfer-Encoding: chunked\r\n"];
|
||||
|
||||
// 空行
|
||||
[response appendString:@"\r\n"];
|
||||
|
||||
NSMutableData *responseData = [[response dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
|
||||
|
||||
// 添加每个 chunk
|
||||
for (NSData *chunk in chunks) {
|
||||
[responseData appendData:[self encodeChunk:chunk]];
|
||||
}
|
||||
|
||||
// 添加终止 chunk
|
||||
if (trailers) {
|
||||
[responseData appendData:[self encodeLastChunkWithTrailers:trailers]];
|
||||
} else {
|
||||
[responseData appendData:[self encodeLastChunk]];
|
||||
}
|
||||
|
||||
return [responseData copy];
|
||||
}
|
||||
|
||||
#pragma mark - Chunked 编码工具
|
||||
|
||||
+ (NSData *)encodeChunk:(NSData *)data {
|
||||
return [self encodeChunk:data extension:nil];
|
||||
}
|
||||
|
||||
+ (NSData *)encodeChunk:(NSData *)data extension:(nullable NSString *)extension {
|
||||
NSMutableString *chunkString = [NSMutableString string];
|
||||
|
||||
// Chunk size(十六进制)
|
||||
if (extension) {
|
||||
[chunkString appendFormat:@"%lx;%@\r\n", (unsigned long)data.length, extension];
|
||||
} else {
|
||||
[chunkString appendFormat:@"%lx\r\n", (unsigned long)data.length];
|
||||
}
|
||||
|
||||
NSMutableData *chunkData = [[chunkString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
|
||||
|
||||
// Chunk data
|
||||
[chunkData appendData:data];
|
||||
|
||||
// CRLF
|
||||
[chunkData appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
|
||||
return [chunkData copy];
|
||||
}
|
||||
|
||||
+ (NSData *)encodeLastChunk {
|
||||
return [@"0\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
+ (NSData *)encodeLastChunkWithTrailers:(NSDictionary<NSString *, NSString *> *)trailers {
|
||||
NSMutableString *lastChunkString = [NSMutableString stringWithString:@"0\r\n"];
|
||||
|
||||
// 添加 trailer 头部
|
||||
for (NSString *key in trailers) {
|
||||
[lastChunkString appendFormat:@"%@: %@\r\n", key, trailers[key]];
|
||||
}
|
||||
|
||||
// 空行终止
|
||||
[lastChunkString appendString:@"\r\n"];
|
||||
|
||||
return [lastChunkString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
#pragma mark - 测试数据生成
|
||||
|
||||
+ (NSData *)randomDataWithSize:(NSUInteger)size {
|
||||
NSMutableData *data = [NSMutableData dataWithLength:size];
|
||||
if (SecRandomCopyBytes(kSecRandomDefault, size, data.mutableBytes) != 0) {
|
||||
// 如果 SecRandom 失败,使用简单的随机<EFBFBD><EFBFBD>?
|
||||
uint8_t *bytes = data.mutableBytes;
|
||||
for (NSUInteger i = 0; i < size; i++) {
|
||||
bytes[i] = arc4random_uniform(256);
|
||||
}
|
||||
}
|
||||
return [data copy];
|
||||
}
|
||||
|
||||
+ (NSData *)jsonBodyWithDictionary:(NSDictionary *)dictionary {
|
||||
NSError *error = nil;
|
||||
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
|
||||
options:0
|
||||
error:&error];
|
||||
if (error) {
|
||||
NSLog(@"JSON serialization error: %@", error);
|
||||
return nil;
|
||||
}
|
||||
return jsonData;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,742 @@
|
||||
//
|
||||
// HttpdnsNWHTTPClientTests.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// @author Created by Claude Code on 2025-11-01
|
||||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import "HttpdnsNWHTTPClient.h"
|
||||
#import "HttpdnsNWHTTPClient_Internal.h"
|
||||
#import "HttpdnsNWReusableConnection.h"
|
||||
#import "HttpdnsNWHTTPClientTestHelper.h"
|
||||
|
||||
@interface HttpdnsNWHTTPClientTests : XCTestCase
|
||||
|
||||
@property (nonatomic, strong) HttpdnsNWHTTPClient *client;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsNWHTTPClientTests
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
self.client = [[HttpdnsNWHTTPClient alloc] init];
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
self.client = nil;
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
#pragma mark - A. HTTP 解析逻辑测试
|
||||
|
||||
#pragma mark - A1. Header 解析 (9<EFBFBD><EFBFBD>?
|
||||
|
||||
// A1.1 正常响应
|
||||
- (void)testParseHTTPHeaders_ValidResponse_Success {
|
||||
NSData *data = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
|
||||
statusText:@"OK"
|
||||
headers:@{@"Content-Type": @"application/json"}
|
||||
body:nil];
|
||||
|
||||
NSUInteger headerEndIndex;
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||||
headerEndIndex:&headerEndIndex
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
|
||||
XCTAssertEqual(statusCode, 200);
|
||||
XCTAssertNotNil(headers);
|
||||
XCTAssertEqualObjects(headers[@"content-type"], @"application/json"); // key 应该转为小写
|
||||
XCTAssertNil(error);
|
||||
}
|
||||
|
||||
// A1.2 多个头部
|
||||
- (void)testParseHTTPHeaders_MultipleHeaders_AllParsed {
|
||||
NSDictionary *testHeaders = @{
|
||||
@"Content-Type": @"application/json",
|
||||
@"Content-Length": @"123",
|
||||
@"Connection": @"keep-alive",
|
||||
@"X-Custom-Header": @"custom-value",
|
||||
@"Cache-Control": @"no-cache"
|
||||
};
|
||||
|
||||
NSData *data = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
|
||||
statusText:@"OK"
|
||||
headers:testHeaders
|
||||
body:nil];
|
||||
|
||||
NSUInteger headerEndIndex;
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||||
headerEndIndex:&headerEndIndex
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
|
||||
XCTAssertEqual(headers.count, testHeaders.count);
|
||||
// 验证所有头部都被解析,<EFBFBD><EFBFBD>?key 转为小写
|
||||
XCTAssertEqualObjects(headers[@"content-type"], @"application/json");
|
||||
XCTAssertEqualObjects(headers[@"content-length"], @"123");
|
||||
XCTAssertEqualObjects(headers[@"connection"], @"keep-alive");
|
||||
XCTAssertEqualObjects(headers[@"x-custom-header"], @"custom-value");
|
||||
XCTAssertEqualObjects(headers[@"cache-control"], @"no-cache");
|
||||
}
|
||||
|
||||
// A1.3 不完整响<EFBFBD><EFBFBD>?
|
||||
- (void)testParseHTTPHeaders_IncompleteData_ReturnsIncomplete {
|
||||
NSString *incompleteResponse = @"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n";
|
||||
NSData *data = [incompleteResponse dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex;
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||||
headerEndIndex:&headerEndIndex
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultIncomplete);
|
||||
}
|
||||
|
||||
// A1.4 无效状态行
|
||||
- (void)testParseHTTPHeaders_InvalidStatusLine_ReturnsError {
|
||||
NSString *invalidResponse = @"INVALID\r\n\r\n";
|
||||
NSData *data = [invalidResponse dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex;
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||||
headerEndIndex:&headerEndIndex
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultError);
|
||||
XCTAssertNotNil(error);
|
||||
}
|
||||
|
||||
// A1.5 头部包含空格
|
||||
- (void)testParseHTTPHeaders_HeadersWithWhitespace_Trimmed {
|
||||
NSString *responseWithSpaces = @"HTTP/1.1 200 OK\r\nContent-Type: application/json \r\n\r\n";
|
||||
NSData *data = [responseWithSpaces dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex;
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||||
headerEndIndex:&headerEndIndex
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
|
||||
XCTAssertEqualObjects(headers[@"content-type"], @"application/json"); // 应该<EFBFBD><EFBFBD>?trim
|
||||
}
|
||||
|
||||
// A1.6 头部没有<EFBFBD><EFBFBD>?
|
||||
- (void)testParseHTTPHeaders_EmptyHeaderValue_HandledGracefully {
|
||||
NSString *responseWithEmptyValue = @"HTTP/1.1 200 OK\r\nX-Empty-Header:\r\n\r\n";
|
||||
NSData *data = [responseWithEmptyValue dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex;
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||||
headerEndIndex:&headerEndIndex
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
|
||||
XCTAssertEqualObjects(headers[@"x-empty-header"], @"");
|
||||
}
|
||||
|
||||
// A1.7 状态码非数<EFBFBD><EFBFBD>?
|
||||
- (void)testParseHTTPHeaders_NonNumericStatusCode_ReturnsError {
|
||||
NSString *invalidStatusCode = @"HTTP/1.1 ABC OK\r\n\r\n";
|
||||
NSData *data = [invalidStatusCode dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex;
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||||
headerEndIndex:&headerEndIndex
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultError);
|
||||
}
|
||||
|
||||
// A1.8 状态码为零
|
||||
- (void)testParseHTTPHeaders_StatusCodeZero_ReturnsError {
|
||||
NSString *zeroStatusCode = @"HTTP/1.1 0 OK\r\n\r\n";
|
||||
NSData *data = [zeroStatusCode dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex;
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||||
headerEndIndex:&headerEndIndex
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultError);
|
||||
}
|
||||
|
||||
// A1.9 头部没有冒号被跳<EFBFBD><EFBFBD>?
|
||||
- (void)testParseHTTPHeaders_HeaderWithoutColon_Skipped {
|
||||
NSString *responseWithInvalidHeader = @"HTTP/1.1 200 OK\r\nInvalidHeader\r\nContent-Type: application/json\r\n\r\n";
|
||||
NSData *data = [responseWithInvalidHeader dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex;
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPHeaderParseResult result = [self.client tryParseHTTPHeadersInData:data
|
||||
headerEndIndex:&headerEndIndex
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPHeaderParseResultSuccess);
|
||||
XCTAssertEqualObjects(headers[@"content-type"], @"application/json"); // 有效头部正常解析
|
||||
}
|
||||
|
||||
#pragma mark - A2. Chunked 编码检<EFBFBD><EFBFBD>?(8<EFBFBD><EFBFBD>?
|
||||
|
||||
// A2.1 单个 chunk
|
||||
- (void)testCheckChunkedBody_SingleChunk_DetectsComplete {
|
||||
NSString *singleChunkBody = @"5\r\nhello\r\n0\r\n\r\n";
|
||||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", singleChunkBody];
|
||||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||||
headerEndIndex:headerEndIndex
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
|
||||
XCTAssertNil(error);
|
||||
}
|
||||
|
||||
// A2.2 多个 chunks
|
||||
- (void)testCheckChunkedBody_MultipleChunks_DetectsComplete {
|
||||
NSString *multiChunkBody = @"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
|
||||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", multiChunkBody];
|
||||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||||
headerEndIndex:headerEndIndex
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
|
||||
XCTAssertNil(error);
|
||||
}
|
||||
|
||||
// A2.3 不完<EFBFBD><EFBFBD>?chunk
|
||||
- (void)testCheckChunkedBody_IncompleteChunk_ReturnsIncomplete {
|
||||
NSString *incompleteChunkBody = @"5\r\nhel"; // 数据不完<EFBFBD><EFBFBD>?
|
||||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", incompleteChunkBody];
|
||||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||||
headerEndIndex:headerEndIndex
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultIncomplete);
|
||||
}
|
||||
|
||||
// A2.4 <EFBFBD><EFBFBD>?chunk extension
|
||||
- (void)testCheckChunkedBody_WithChunkExtension_Ignored {
|
||||
NSString *chunkWithExtension = @"5;name=value\r\nhello\r\n0\r\n\r\n";
|
||||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", chunkWithExtension];
|
||||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
// headerEndIndex 指向第一<EFBFBD><EFBFBD>?\r\n\r\n 中的第一<EFBFBD><EFBFBD>?\r
|
||||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||||
headerEndIndex:headerEndIndex
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
|
||||
XCTAssertNil(error);
|
||||
}
|
||||
|
||||
// A2.5 无效十六进制 size
|
||||
- (void)testCheckChunkedBody_InvalidHexSize_ReturnsError {
|
||||
NSString *invalidChunkSize = @"ZZZ\r\nhello\r\n0\r\n\r\n";
|
||||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", invalidChunkSize];
|
||||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||||
headerEndIndex:headerEndIndex
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultError);
|
||||
XCTAssertNotNil(error);
|
||||
}
|
||||
|
||||
// A2.6 Chunk size 溢出
|
||||
- (void)testCheckChunkedBody_ChunkSizeOverflow_ReturnsError {
|
||||
NSString *overflowChunkSize = @"FFFFFFFFFFFFFFFF\r\n";
|
||||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", overflowChunkSize];
|
||||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||||
headerEndIndex:headerEndIndex
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultError);
|
||||
XCTAssertNotNil(error);
|
||||
}
|
||||
|
||||
// A2.7 缺少 CRLF 终止<EFBFBD><EFBFBD>?
|
||||
- (void)testCheckChunkedBody_MissingCRLFTerminator_ReturnsError {
|
||||
NSString *missingTerminator = @"5\r\nhelloXX0\r\n\r\n"; // 应该<EFBFBD><EFBFBD>?hello\r\n
|
||||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", missingTerminator];
|
||||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||||
headerEndIndex:headerEndIndex
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultError);
|
||||
XCTAssertNotNil(error);
|
||||
}
|
||||
|
||||
// A2.8 <EFBFBD><EFBFBD>?trailers
|
||||
- (void)testCheckChunkedBody_WithTrailers_DetectsComplete {
|
||||
NSString *chunkWithTrailers = @"5\r\nhello\r\n0\r\nX-Trailer: value\r\nX-Custom: test\r\n\r\n";
|
||||
NSString *response = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\n\r\n%@", chunkWithTrailers];
|
||||
NSData *data = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSUInteger headerEndIndex = [@"HTTP/1.1 200 OK" length];
|
||||
NSError *error;
|
||||
|
||||
HttpdnsHTTPChunkParseResult result = [self.client checkChunkedBodyCompletionInData:data
|
||||
headerEndIndex:headerEndIndex
|
||||
error:&error];
|
||||
|
||||
XCTAssertEqual(result, HttpdnsHTTPChunkParseResultSuccess);
|
||||
XCTAssertNil(error);
|
||||
}
|
||||
|
||||
#pragma mark - A3. Chunked 解码 (2<EFBFBD><EFBFBD>?
|
||||
|
||||
// A3.1 多个 chunks 解码
|
||||
- (void)testDecodeChunkedBody_MultipleChunks_DecodesCorrectly {
|
||||
NSArray *chunks = @[
|
||||
[@"hello" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
[@" world" dataUsingEncoding:NSUTF8StringEncoding]
|
||||
];
|
||||
|
||||
NSData *chunkedData = [HttpdnsNWHTTPClientTestHelper createChunkedHTTPResponseWithStatus:200
|
||||
headers:nil
|
||||
chunks:chunks];
|
||||
|
||||
// 提取 chunked body 部分(跳<EFBFBD><EFBFBD>?headers<EFBFBD><EFBFBD>?
|
||||
NSData *headerData = [@"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *bodyData = [chunkedData subdataWithRange:NSMakeRange(headerData.length, chunkedData.length - headerData.length)];
|
||||
|
||||
NSError *error;
|
||||
NSData *decoded = [self.client decodeChunkedBody:bodyData error:&error];
|
||||
|
||||
XCTAssertNotNil(decoded);
|
||||
XCTAssertNil(error);
|
||||
NSString *decodedString = [[NSString alloc] initWithData:decoded encoding:NSUTF8StringEncoding];
|
||||
XCTAssertEqualObjects(decodedString, @"hello world");
|
||||
}
|
||||
|
||||
// A3.2 无效格式返回 nil
|
||||
- (void)testDecodeChunkedBody_InvalidFormat_ReturnsNil {
|
||||
NSString *invalidChunked = @"ZZZ\r\nbad data\r\n";
|
||||
NSData *bodyData = [invalidChunked dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSError *error;
|
||||
NSData *decoded = [self.client decodeChunkedBody:bodyData error:&error];
|
||||
|
||||
XCTAssertNil(decoded);
|
||||
XCTAssertNotNil(error);
|
||||
}
|
||||
|
||||
#pragma mark - A4. 完整响应解析 (6<EFBFBD><EFBFBD>?
|
||||
|
||||
// A4.1 Content-Length 响应
|
||||
- (void)testParseResponse_WithContentLength_ParsesCorrectly {
|
||||
NSString *bodyString = @"{\"ips\":[]}";
|
||||
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
|
||||
statusText:@"OK"
|
||||
headers:@{@"Content-Type": @"application/json"}
|
||||
body:bodyData];
|
||||
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSData *body;
|
||||
NSError *error;
|
||||
|
||||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
body:&body
|
||||
error:&error];
|
||||
|
||||
XCTAssertTrue(success);
|
||||
XCTAssertEqual(statusCode, 200);
|
||||
XCTAssertNotNil(headers);
|
||||
XCTAssertEqualObjects(headers[@"content-type"], @"application/json");
|
||||
XCTAssertEqualObjects(body, bodyData);
|
||||
XCTAssertNil(error);
|
||||
}
|
||||
|
||||
// A4.2 Chunked 响应
|
||||
- (void)testParseResponse_WithChunkedEncoding_DecodesBody {
|
||||
NSArray *chunks = @[
|
||||
[@"{\"ips\"" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
[@":[]}" dataUsingEncoding:NSUTF8StringEncoding]
|
||||
];
|
||||
|
||||
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createChunkedHTTPResponseWithStatus:200
|
||||
headers:@{@"Content-Type": @"application/json"}
|
||||
chunks:chunks];
|
||||
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSData *body;
|
||||
NSError *error;
|
||||
|
||||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
body:&body
|
||||
error:&error];
|
||||
|
||||
XCTAssertTrue(success);
|
||||
XCTAssertEqual(statusCode, 200);
|
||||
NSString *bodyString = [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding];
|
||||
XCTAssertEqualObjects(bodyString, @"{\"ips\":[]}");
|
||||
}
|
||||
|
||||
// A4.3 <EFBFBD><EFBFBD>?body
|
||||
- (void)testParseResponse_EmptyBody_Success {
|
||||
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:204
|
||||
statusText:@"No Content"
|
||||
headers:nil
|
||||
body:nil];
|
||||
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSData *body;
|
||||
NSError *error;
|
||||
|
||||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
body:&body
|
||||
error:&error];
|
||||
|
||||
XCTAssertTrue(success);
|
||||
XCTAssertEqual(statusCode, 204);
|
||||
XCTAssertEqual(body.length, 0);
|
||||
}
|
||||
|
||||
// A4.4 Content-Length 不匹配仍然成<EFBFBD><EFBFBD>?
|
||||
- (void)testParseResponse_ContentLengthMismatch_LogsButSucceeds {
|
||||
NSData *bodyData = [@"short" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
|
||||
statusText:@"OK"
|
||||
headers:@{@"Content-Length": @"100"} // 不匹<EFBFBD><EFBFBD>?
|
||||
body:bodyData];
|
||||
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSData *body;
|
||||
NSError *error;
|
||||
|
||||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
body:&body
|
||||
error:&error];
|
||||
|
||||
XCTAssertTrue(success); // 仍然成功,只是日志警<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqualObjects(body, bodyData);
|
||||
}
|
||||
|
||||
// A4.5 空数据返回错<EFBFBD><EFBFBD>?
|
||||
- (void)testParseResponse_EmptyData_ReturnsError {
|
||||
NSData *emptyData = [NSData data];
|
||||
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSData *body;
|
||||
NSError *error;
|
||||
|
||||
BOOL success = [self.client parseHTTPResponseData:emptyData
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
body:&body
|
||||
error:&error];
|
||||
|
||||
XCTAssertFalse(success);
|
||||
XCTAssertNotNil(error);
|
||||
}
|
||||
|
||||
// A4.6 只有 headers <EFBFBD><EFBFBD>?body
|
||||
- (void)testParseResponse_OnlyHeaders_EmptyBody {
|
||||
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
|
||||
statusText:@"OK"
|
||||
headers:@{@"Content-Type": @"text/plain"}
|
||||
body:nil];
|
||||
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSData *body;
|
||||
NSError *error;
|
||||
|
||||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
body:&body
|
||||
error:&error];
|
||||
|
||||
XCTAssertTrue(success);
|
||||
XCTAssertEqual(statusCode, 200);
|
||||
XCTAssertNotNil(headers);
|
||||
XCTAssertEqual(body.length, 0);
|
||||
}
|
||||
|
||||
#pragma mark - C. 请求构建测试 (7<EFBFBD><EFBFBD>?
|
||||
|
||||
// C.1 基本 GET 请求
|
||||
- (void)testBuildHTTPRequest_BasicGET_CorrectFormat {
|
||||
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
|
||||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:@"TestAgent"];
|
||||
|
||||
XCTAssertTrue([request containsString:@"GET / HTTP/1.1\r\n"]);
|
||||
XCTAssertTrue([request containsString:@"Host: example.com\r\n"]);
|
||||
XCTAssertTrue([request containsString:@"User-Agent: TestAgent\r\n"]);
|
||||
XCTAssertTrue([request containsString:@"Accept: application/json\r\n"]);
|
||||
XCTAssertTrue([request containsString:@"Accept-Encoding: identity\r\n"]);
|
||||
XCTAssertTrue([request containsString:@"Connection: keep-alive\r\n"]);
|
||||
XCTAssertTrue([request hasSuffix:@"\r\n\r\n"]);
|
||||
}
|
||||
|
||||
// C.2 带查询参<EFBFBD><EFBFBD>?
|
||||
- (void)testBuildHTTPRequest_WithQueryString_Included {
|
||||
NSURL *url = [NSURL URLWithString:@"http://example.com/path?foo=bar&baz=qux"];
|
||||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||||
|
||||
XCTAssertTrue([request containsString:@"GET /path?foo=bar&baz=qux HTTP/1.1\r\n"]);
|
||||
}
|
||||
|
||||
// C.3 包含 User-Agent
|
||||
- (void)testBuildHTTPRequest_WithUserAgent_Included {
|
||||
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
|
||||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:@"CustomAgent/1.0"];
|
||||
|
||||
XCTAssertTrue([request containsString:@"User-Agent: CustomAgent/1.0\r\n"]);
|
||||
}
|
||||
|
||||
// C.4 HTTP 默认端口不显<EFBFBD><EFBFBD>?
|
||||
- (void)testBuildHTTPRequest_HTTPDefaultPort_NotInHost {
|
||||
NSURL *url = [NSURL URLWithString:@"http://example.com:80/"];
|
||||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||||
|
||||
XCTAssertTrue([request containsString:@"Host: example.com\r\n"]);
|
||||
XCTAssertFalse([request containsString:@"Host: example.com:80\r\n"]);
|
||||
}
|
||||
|
||||
// C.5 HTTPS 默认端口不显<EFBFBD><EFBFBD>?
|
||||
- (void)testBuildHTTPRequest_HTTPSDefaultPort_NotInHost {
|
||||
NSURL *url = [NSURL URLWithString:@"https://example.com:443/"];
|
||||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||||
|
||||
XCTAssertTrue([request containsString:@"Host: example.com\r\n"]);
|
||||
XCTAssertFalse([request containsString:@"Host: example.com:443\r\n"]);
|
||||
}
|
||||
|
||||
// C.6 非默认端口显<EFBFBD><EFBFBD>?
|
||||
- (void)testBuildHTTPRequest_NonDefaultPort_InHost {
|
||||
NSURL *url = [NSURL URLWithString:@"http://example.com:8080/"];
|
||||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||||
|
||||
XCTAssertTrue([request containsString:@"Host: example.com:8080\r\n"]);
|
||||
}
|
||||
|
||||
// C.7 固定头部存在
|
||||
- (void)testBuildHTTPRequest_FixedHeaders_Present {
|
||||
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
|
||||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||||
|
||||
XCTAssertTrue([request containsString:@"Accept: application/json\r\n"]);
|
||||
XCTAssertTrue([request containsString:@"Accept-Encoding: identity\r\n"]);
|
||||
XCTAssertTrue([request containsString:@"Connection: keep-alive\r\n"]);
|
||||
}
|
||||
|
||||
#pragma mark - E. TLS 验证测试 (4个占位符)
|
||||
|
||||
// 注意:TLS 验证测试需要真实的 SecTrustRef 或复杂的 mock
|
||||
// 这些测试在实际环境中需要根据测试框架调<EFBFBD><EFBFBD>?
|
||||
|
||||
// E.1 有效证书返回 YES
|
||||
- (void)testEvaluateServerTrust_ValidCertificate_ReturnsYES {
|
||||
// 需要创建有效的 SecTrustRef 进行测试
|
||||
// 跳过或标记为手动测试
|
||||
}
|
||||
|
||||
// E.2 Proceed 结果返回 YES
|
||||
- (void)testEvaluateServerTrust_ProceedResult_ReturnsYES {
|
||||
// Mock SecTrustEvaluate 返回 kSecTrustResultProceed
|
||||
}
|
||||
|
||||
// E.3 无效证书返回 NO
|
||||
- (void)testEvaluateServerTrust_InvalidCertificate_ReturnsNO {
|
||||
// Mock SecTrustEvaluate 返回 kSecTrustResultDeny
|
||||
}
|
||||
|
||||
// E.4 指定域名使用 SSL Policy
|
||||
- (void)testEvaluateServerTrust_WithDomain_UsesSSLPolicy {
|
||||
// 验证使用<EFBFBD><EFBFBD>?SecPolicyCreateSSL(true, domain)
|
||||
}
|
||||
|
||||
#pragma mark - F. 边缘情况测试 (5<EFBFBD><EFBFBD>?
|
||||
|
||||
// F.1 超长 URL
|
||||
- (void)testPerformRequest_VeryLongURL_HandlesCorrectly {
|
||||
NSMutableString *longPath = [NSMutableString stringWithString:@"http://example.com/"];
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
[longPath appendString:@"long/"];
|
||||
}
|
||||
|
||||
NSURL *url = [NSURL URLWithString:longPath];
|
||||
XCTAssertNotNil(url);
|
||||
|
||||
NSString *request = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||||
XCTAssertTrue(request.length > 5000);
|
||||
}
|
||||
|
||||
// F.2 <EFBFBD><EFBFBD>?User-Agent
|
||||
- (void)testBuildRequest_EmptyUserAgent_NoUserAgentHeader {
|
||||
NSURL *url = [NSURL URLWithString:@"http://example.com/"];
|
||||
NSString *requestWithNil = [self.client buildHTTPRequestStringWithURL:url userAgent:nil];
|
||||
|
||||
XCTAssertFalse([requestWithNil containsString:@"User-Agent:"]);
|
||||
}
|
||||
|
||||
// F.3 超大响应<EFBFBD><EFBFBD>?
|
||||
- (void)testParseResponse_VeryLargeBody_HandlesCorrectly {
|
||||
NSData *largeBody = [HttpdnsNWHTTPClientTestHelper randomDataWithSize:5 * 1024 * 1024];
|
||||
NSData *responseData = [HttpdnsNWHTTPClientTestHelper createHTTPResponseWithStatus:200
|
||||
statusText:@"OK"
|
||||
headers:nil
|
||||
body:largeBody];
|
||||
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSData *body;
|
||||
NSError *error;
|
||||
|
||||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
body:&body
|
||||
error:&error];
|
||||
|
||||
XCTAssertTrue(success);
|
||||
XCTAssertEqual(body.length, largeBody.length);
|
||||
}
|
||||
|
||||
// F.4 Chunked 解码失败回退到原始数<EFBFBD><EFBFBD>?
|
||||
- (void)testParseResponse_ChunkedDecodeFails_FallsBackToRaw {
|
||||
NSString *badChunked = @"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nBAD_CHUNK_DATA";
|
||||
NSData *responseData = [badChunked dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSInteger statusCode;
|
||||
NSDictionary *headers;
|
||||
NSData *body;
|
||||
NSError *error;
|
||||
|
||||
BOOL success = [self.client parseHTTPResponseData:responseData
|
||||
statusCode:&statusCode
|
||||
headers:&headers
|
||||
body:&body
|
||||
error:&error];
|
||||
|
||||
XCTAssertTrue(success);
|
||||
XCTAssertNotNil(body);
|
||||
}
|
||||
|
||||
// F.5 连接<EFBFBD><EFBFBD>?key 生成测试
|
||||
- (void)testConnectionPoolKey_DifferentHosts_SeparateKeys {
|
||||
NSString *key1 = [self.client connectionPoolKeyForHost:@"host1.com" port:@"80" useTLS:NO];
|
||||
NSString *key2 = [self.client connectionPoolKeyForHost:@"host2.com" port:@"80" useTLS:NO];
|
||||
|
||||
XCTAssertNotEqualObjects(key1, key2);
|
||||
}
|
||||
|
||||
- (void)testConnectionPoolKey_DifferentPorts_SeparateKeys {
|
||||
NSString *key1 = [self.client connectionPoolKeyForHost:@"example.com" port:@"80" useTLS:NO];
|
||||
NSString *key2 = [self.client connectionPoolKeyForHost:@"example.com" port:@"8080" useTLS:NO];
|
||||
|
||||
XCTAssertNotEqualObjects(key1, key2);
|
||||
}
|
||||
|
||||
- (void)testConnectionPoolKey_HTTPvsHTTPS_SeparateKeys {
|
||||
NSString *keyHTTP = [self.client connectionPoolKeyForHost:@"example.com" port:@"80" useTLS:NO];
|
||||
NSString *keyHTTPS = [self.client connectionPoolKeyForHost:@"example.com" port:@"443" useTLS:YES];
|
||||
|
||||
XCTAssertNotEqualObjects(keyHTTP, keyHTTPS);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,406 @@
|
||||
//
|
||||
// HttpdnsNWHTTPClient_BasicIntegrationTests.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// @author Created by Claude Code on 2025-11-01
|
||||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||||
//
|
||||
// 基础集成测试 - 包含基础功能 (G) 和连接复<EFBFBD><EFBFBD>?(J) 测试<EFBFBD><EFBFBD>?
|
||||
// 测试总数<EFBFBD><EFBFBD>?2 个(G:7 + J:5<EFBFBD><EFBFBD>?
|
||||
//
|
||||
|
||||
#import "HttpdnsNWHTTPClientTestBase.h"
|
||||
|
||||
@interface HttpdnsNWHTTPClient_BasicIntegrationTests : HttpdnsNWHTTPClientTestBase
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsNWHTTPClient_BasicIntegrationTests
|
||||
|
||||
#pragma mark - G. 集成测试(真实网络)
|
||||
|
||||
// G.1 HTTP GET 请求
|
||||
- (void)testIntegration_HTTPGetRequest_RealNetwork {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP GET request"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"HttpdnsNWHTTPClient/1.0"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
XCTAssertNotNil(response, @"Response should not be nil");
|
||||
XCTAssertNil(error, @"Error should be nil, got: %@", error);
|
||||
XCTAssertEqual(response.statusCode, 200, @"Status code should be 200");
|
||||
XCTAssertNotNil(response.body, @"Body should not be nil");
|
||||
XCTAssertGreaterThan(response.body.length, 0, @"Body should not be empty");
|
||||
|
||||
// 验证响应包含 JSON
|
||||
NSError *jsonError = nil;
|
||||
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:response.body
|
||||
options:0
|
||||
error:&jsonError];
|
||||
XCTAssertNotNil(jsonDict, @"Response should be valid JSON");
|
||||
XCTAssertNil(jsonError, @"JSON parsing should succeed");
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:20.0];
|
||||
}
|
||||
|
||||
// G.2 HTTPS GET 请求
|
||||
- (void)testIntegration_HTTPSGetRequest_RealNetwork {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"HTTPS GET request"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"HttpdnsNWHTTPClient/1.0"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
XCTAssertNotNil(response, @"Response should not be nil");
|
||||
XCTAssertNil(error, @"Error should be nil, got: %@", error);
|
||||
XCTAssertEqual(response.statusCode, 200, @"Status code should be 200");
|
||||
XCTAssertNotNil(response.body, @"Body should not be nil");
|
||||
|
||||
// 验证 TLS 成功建立
|
||||
XCTAssertGreaterThan(response.body.length, 0, @"HTTPS body should not be empty");
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:20.0];
|
||||
}
|
||||
|
||||
// G.3 HTTP 404 响应
|
||||
- (void)testIntegration_NotFound_Returns404 {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"404 response"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/status/404"
|
||||
userAgent:@"HttpdnsNWHTTPClient/1.0"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
XCTAssertNotNil(response, @"Response should not be nil even for 404");
|
||||
XCTAssertNil(error, @"Error should be nil for valid HTTP response");
|
||||
XCTAssertEqual(response.statusCode, 404, @"Status code should be 404");
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:20.0];
|
||||
}
|
||||
|
||||
// G.4 连接复用测试
|
||||
- (void)testIntegration_ConnectionReuse_MultipleRequests {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Connection reuse"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"HttpdnsNWHTTPClient/1.0"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
|
||||
XCTAssertNotNil(response1, @"First response should not be nil");
|
||||
XCTAssertNil(error1, @"First request should succeed");
|
||||
XCTAssertEqual(response1.statusCode, 200);
|
||||
|
||||
// 立即发起第二个请求,应该复用连接
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"HttpdnsNWHTTPClient/1.0"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
|
||||
XCTAssertNotNil(response2, @"Second response should not be nil");
|
||||
XCTAssertNil(error2, @"Second request should succeed");
|
||||
XCTAssertEqual(response2.statusCode, 200);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:30.0];
|
||||
}
|
||||
|
||||
// G.5 Chunked 响应处理
|
||||
- (void)testIntegration_ChunkedResponse_RealNetwork {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Chunked response"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSError *error = nil;
|
||||
// httpbin.org/stream-bytes 返回 chunked 编码的响<EFBFBD><EFBFBD>?
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/stream-bytes/1024"
|
||||
userAgent:@"HttpdnsNWHTTPClient/1.0"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
XCTAssertNotNil(response, @"Response should not be nil");
|
||||
XCTAssertNil(error, @"Error should be nil, got: %@", error);
|
||||
XCTAssertEqual(response.statusCode, 200);
|
||||
XCTAssertEqual(response.body.length, 1024, @"Should receive exactly 1024 bytes");
|
||||
|
||||
// 验证 Transfer-Encoding <EFBFBD><EFBFBD>?
|
||||
NSString *transferEncoding = response.headers[@"transfer-encoding"];
|
||||
if (transferEncoding) {
|
||||
XCTAssertTrue([transferEncoding containsString:@"chunked"], @"Should use chunked encoding");
|
||||
}
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:20.0];
|
||||
}
|
||||
|
||||
#pragma mark - 额外的集成测<EFBFBD><EFBFBD>?
|
||||
|
||||
// G.6 超时测试(可选)
|
||||
- (void)testIntegration_RequestTimeout_ReturnsError {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Request timeout"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSError *error = nil;
|
||||
// httpbin.org/delay/10 会延<EFBFBD><EFBFBD>?10 秒响应,我们设置 2 秒超<EFBFBD><EFBFBD>?
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
|
||||
userAgent:@"HttpdnsNWHTTPClient/1.0"
|
||||
timeout:2.0
|
||||
error:&error];
|
||||
|
||||
XCTAssertNil(response, @"Response should be nil on timeout");
|
||||
XCTAssertNotNil(error, @"Error should be set on timeout");
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:5.0];
|
||||
}
|
||||
|
||||
// G.7 多个不同头部的请<EFBFBD><EFBFBD>?
|
||||
- (void)testIntegration_CustomHeaders_Reflected {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Custom headers"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/headers"
|
||||
userAgent:@"TestUserAgent/1.0"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
XCTAssertNotNil(response);
|
||||
XCTAssertEqual(response.statusCode, 200);
|
||||
|
||||
// 解析 JSON 响应,验证我们的 User-Agent 被发<EFBFBD><EFBFBD>?
|
||||
NSError *jsonError = nil;
|
||||
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:response.body
|
||||
options:0
|
||||
error:&jsonError];
|
||||
XCTAssertNotNil(jsonDict);
|
||||
|
||||
NSDictionary *headers = jsonDict[@"headers"];
|
||||
XCTAssertTrue([headers[@"User-Agent"] containsString:@"TestUserAgent"], @"User-Agent should be sent");
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:20.0];
|
||||
}
|
||||
|
||||
#pragma mark - J. 连接复用详细测试
|
||||
|
||||
// J.1 连接过期测试<EFBFBD><EFBFBD>?1秒后创建新连接)
|
||||
- (void)testConnectionReuse_Expiry31Seconds_NewConnectionCreated {
|
||||
if (getenv("SKIP_SLOW_TESTS")) {
|
||||
return;
|
||||
}
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Connection expiry"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
CFAbsoluteTime time1 = CFAbsoluteTimeGetCurrent();
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"First"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
CFAbsoluteTime elapsed1 = CFAbsoluteTimeGetCurrent() - time1;
|
||||
XCTAssertTrue(response1 != nil || error1 != nil);
|
||||
|
||||
// 等待31秒让连接过期
|
||||
[NSThread sleepForTimeInterval:31.0];
|
||||
|
||||
// 第二个请求应该创建新连接(可能稍慢,因为需要建立连接)
|
||||
CFAbsoluteTime time2 = CFAbsoluteTimeGetCurrent();
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"Second"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
CFAbsoluteTime elapsed2 = CFAbsoluteTimeGetCurrent() - time2;
|
||||
XCTAssertTrue(response2 != nil || error2 != nil);
|
||||
|
||||
// 注意:由于网络波动,不能严格比较时间
|
||||
// 只验证请求都成功即可
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:70.0];
|
||||
}
|
||||
|
||||
// J.2 连接池容量限制验<EFBFBD><EFBFBD>?
|
||||
- (void)testConnectionReuse_TenRequests_OnlyFourConnectionsKept {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Pool size limit"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// 连续10个请<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 10; i++) {
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"PoolSizeTest"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertTrue(response != nil || error != nil);
|
||||
}
|
||||
|
||||
// 等待所有连接归<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 无法直接验证池大小,但如果实现正确,池应自动限制
|
||||
// 后续请求应该仍能正常工作
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"Verification"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertTrue(response != nil || error != nil);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:120.0];
|
||||
}
|
||||
|
||||
// J.3 不同路径复用连接
|
||||
- (void)testConnectionReuse_DifferentPaths_SameConnection {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Different paths"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray<NSString *> *paths = @[@"/get", @"/headers", @"/user-agent", @"/uuid"];
|
||||
NSMutableArray<NSNumber *> *times = [NSMutableArray array];
|
||||
|
||||
for (NSString *path in paths) {
|
||||
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
|
||||
NSString *urlString = [NSString stringWithFormat:@"http://127.0.0.1:11080%@", path];
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"PathTest"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - start;
|
||||
|
||||
XCTAssertTrue(response != nil || error != nil);
|
||||
[times addObject:@(elapsed)];
|
||||
}
|
||||
|
||||
// 如果连接复用工作正常,后续请求应该更快(但网络波动可能影响)
|
||||
// 至少验证所有请求都成功
|
||||
XCTAssertEqual(times.count, paths.count);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:60.0];
|
||||
}
|
||||
|
||||
// J.4 HTTP vs HTTPS 使用不同连接
|
||||
- (void)testConnectionReuse_HTTPvsHTTPS_DifferentPoolKeys {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP vs HTTPS"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// HTTP 请求
|
||||
NSError *httpError = nil;
|
||||
HttpdnsNWHTTPClientResponse *httpResponse = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"HTTP"
|
||||
timeout:15.0
|
||||
error:&httpError];
|
||||
XCTAssertTrue(httpResponse != nil || httpError != nil);
|
||||
|
||||
// HTTPS 请求(应该使用不同的连接<EFBFBD><EFBFBD>?key<EFBFBD><EFBFBD>?
|
||||
NSError *httpsError = nil;
|
||||
HttpdnsNWHTTPClientResponse *httpsResponse = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"HTTPS"
|
||||
timeout:15.0
|
||||
error:&httpsError];
|
||||
XCTAssertTrue(httpsResponse != nil || httpsError != nil);
|
||||
|
||||
// 两者都应该成功,且不会相互干扰
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:35.0];
|
||||
}
|
||||
|
||||
// J.5 长连接保持测<EFBFBD><EFBFBD>?
|
||||
- (void)testConnectionReuse_TwentyRequestsOneSecondApart_ConnectionKeptAlive {
|
||||
if (getenv("SKIP_SLOW_TESTS")) {
|
||||
return;
|
||||
}
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Keep-alive"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSInteger successCount = 0;
|
||||
NSMutableArray<NSNumber *> *requestTimes = [NSMutableArray array];
|
||||
|
||||
// 20个请求,间隔1秒(第一个请求立即执行)
|
||||
for (NSInteger i = 0; i < 20; i++) {
|
||||
// 除第一个请求外,每次请求前等待1<EFBFBD><EFBFBD>?
|
||||
if (i > 0) {
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
}
|
||||
|
||||
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"KeepAlive"
|
||||
timeout:10.0
|
||||
error:&error];
|
||||
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - startTime;
|
||||
[requestTimes addObject:@(elapsed)];
|
||||
|
||||
if (response && (response.statusCode == 200 || response.statusCode == 503)) {
|
||||
successCount++;
|
||||
} else {
|
||||
// 如果请求失败,提前退出以避免超时
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 至少大部分请求应该成<EFBFBD><EFBFBD>?
|
||||
XCTAssertGreaterThan(successCount, 15, @"Most requests should succeed with connection reuse");
|
||||
|
||||
// 验证连接复用:后续请求应该更快(如果使用了keep-alive<EFBFBD><EFBFBD>?
|
||||
if (requestTimes.count >= 10) {
|
||||
double firstRequestTime = [requestTimes[0] doubleValue];
|
||||
double laterAvgTime = 0;
|
||||
for (NSInteger i = 5; i < MIN(10, requestTimes.count); i++) {
|
||||
laterAvgTime += [requestTimes[i] doubleValue];
|
||||
}
|
||||
laterAvgTime /= MIN(5, requestTimes.count - 5);
|
||||
// 后续请求应该不会明显更慢(说明连接复用工作正常)
|
||||
XCTAssertLessThanOrEqual(laterAvgTime, firstRequestTime * 2.0, @"Connection reuse should keep latency reasonable");
|
||||
}
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
// 超时计算: 19秒sleep + 20个请求×~2<EFBFBD><EFBFBD>?= 59秒,设置50秒(提前退出机制保证效率)
|
||||
[self waitForExpectations:@[expectation] timeout:50.0];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,534 @@
|
||||
//
|
||||
// HttpdnsNWHTTPClient_ConcurrencyTests.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// @author Created by Claude Code on 2025-11-01
|
||||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||||
//
|
||||
// 并发测试 - 包含并发请求 (H)、竞态条<EFBFBD><EFBFBD>?(I)、并发多端口 (N) 测试<EFBFBD><EFBFBD>?
|
||||
// 测试总数<EFBFBD><EFBFBD>?3 个(H:5 + I:5 + N:3<EFBFBD><EFBFBD>?
|
||||
//
|
||||
|
||||
#import "HttpdnsNWHTTPClientTestBase.h"
|
||||
|
||||
@interface HttpdnsNWHTTPClient_ConcurrencyTests : HttpdnsNWHTTPClientTestBase
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsNWHTTPClient_ConcurrencyTests
|
||||
|
||||
#pragma mark - H. 并发测试
|
||||
|
||||
// H.1 并发请求同一主机
|
||||
- (void)testConcurrency_ParallelRequestsSameHost_AllSucceed {
|
||||
NSInteger concurrentCount = 10;
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
NSMutableArray<NSNumber *> *responseTimes = [NSMutableArray array];
|
||||
NSLock *lock = [[NSLock alloc] init];
|
||||
|
||||
for (NSInteger i = 0; i < concurrentCount; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
dispatch_async(queue, ^{
|
||||
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"HttpdnsNWHTTPClient/1.0"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent();
|
||||
|
||||
XCTAssertNotNil(response, @"Response %ld should not be nil", (long)i);
|
||||
XCTAssertTrue(response.statusCode == 200 || response.statusCode == 503,
|
||||
@"Request %ld got statusCode=%ld, expected 200 or 503", (long)i, (long)response.statusCode);
|
||||
|
||||
[lock lock];
|
||||
[responseTimes addObject:@(endTime - startTime)];
|
||||
[lock unlock];
|
||||
|
||||
[expectation fulfill];
|
||||
dispatch_group_leave(group);
|
||||
});
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:30.0];
|
||||
|
||||
// 验证至少部分请求复用了连接(响应时间有差异)
|
||||
XCTAssertEqual(responseTimes.count, concurrentCount);
|
||||
}
|
||||
|
||||
// H.2 并发请求不同路径
|
||||
- (void)testConcurrency_ParallelRequestsDifferentPaths_AllSucceed {
|
||||
NSArray<NSString *> *paths = @[@"/get", @"/status/200", @"/headers", @"/user-agent", @"/uuid"];
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
for (NSString *path in paths) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:path];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
dispatch_async(queue, ^{
|
||||
NSString *urlString = [NSString stringWithFormat:@"http://127.0.0.1:11080%@", path];
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"HttpdnsNWHTTPClient/1.0"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
XCTAssertNotNil(response, @"Response for %@ should not be nil", path);
|
||||
XCTAssertTrue(response.statusCode == 200 || response.statusCode == 503, @"Request %@ should get valid status", path);
|
||||
|
||||
[expectation fulfill];
|
||||
dispatch_group_leave(group);
|
||||
});
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:30.0];
|
||||
}
|
||||
|
||||
// H.3 并发 HTTP + HTTPS
|
||||
- (void)testConcurrency_MixedHTTPAndHTTPS_BothSucceed {
|
||||
NSInteger httpCount = 5;
|
||||
NSInteger httpsCount = 5;
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
// HTTP 请求
|
||||
for (NSInteger i = 0; i < httpCount; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"HTTP %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"HttpdnsNWHTTPClient/1.0"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
XCTAssertNotNil(response);
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
// HTTPS 请求
|
||||
for (NSInteger i = 0; i < httpsCount; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"HTTPS %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"HttpdnsNWHTTPClient/1.0"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
XCTAssertNotNil(response);
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:40.0];
|
||||
}
|
||||
|
||||
// H.4 高负载压力测<EFBFBD><EFBFBD>?
|
||||
- (void)testConcurrency_HighLoad50Concurrent_NoDeadlock {
|
||||
NSInteger concurrentCount = 50;
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
NSLock *successCountLock = [[NSLock alloc] init];
|
||||
__block NSInteger successCount = 0;
|
||||
|
||||
for (NSInteger i = 0; i < concurrentCount; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"HttpdnsNWHTTPClient/1.0"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
if (response && (response.statusCode == 200 || response.statusCode == 503)) {
|
||||
[successCountLock lock];
|
||||
successCount++;
|
||||
[successCountLock unlock];
|
||||
}
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:60.0];
|
||||
|
||||
// 至少大部分请求应该成功(允许部分失败,因为高负载<EFBFBD><EFBFBD>?
|
||||
XCTAssertGreaterThan(successCount, concurrentCount * 0.8, @"At least 80%% should succeed");
|
||||
}
|
||||
|
||||
// H.5 混合串行+并发
|
||||
- (void)testConcurrency_MixedSerialAndParallel_NoInterference {
|
||||
XCTestExpectation *serialExpectation = [self expectationWithDescription:@"Serial requests"];
|
||||
XCTestExpectation *parallel1 = [self expectationWithDescription:@"Parallel 1"];
|
||||
XCTestExpectation *parallel2 = [self expectationWithDescription:@"Parallel 2"];
|
||||
XCTestExpectation *parallel3 = [self expectationWithDescription:@"Parallel 3"];
|
||||
|
||||
dispatch_queue_t serialQueue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
|
||||
dispatch_queue_t parallelQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
// 串行线程
|
||||
dispatch_async(serialQueue, ^{
|
||||
for (NSInteger i = 0; i < 5; i++) {
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"Serial"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertNotNil(response);
|
||||
}
|
||||
[serialExpectation fulfill];
|
||||
});
|
||||
|
||||
// 并发线程
|
||||
dispatch_async(parallelQueue, ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/uuid"
|
||||
userAgent:@"Parallel1"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertNotNil(response);
|
||||
[parallel1 fulfill];
|
||||
});
|
||||
|
||||
dispatch_async(parallelQueue, ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/headers"
|
||||
userAgent:@"Parallel2"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertNotNil(response);
|
||||
[parallel2 fulfill];
|
||||
});
|
||||
|
||||
dispatch_async(parallelQueue, ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/user-agent"
|
||||
userAgent:@"Parallel3"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertNotNil(response);
|
||||
[parallel3 fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[serialExpectation, parallel1, parallel2, parallel3] timeout:60.0];
|
||||
}
|
||||
|
||||
#pragma mark - I. 竞态条件测<EFBFBD><EFBFBD>?
|
||||
|
||||
// I.1 连接池容量测<EFBFBD><EFBFBD>?
|
||||
- (void)testRaceCondition_ExceedPoolCapacity_MaxFourConnections {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Pool capacity test"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// 快速连续发<EFBFBD><EFBFBD>?10 个请<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 10; i++) {
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"PoolTest"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertTrue(response != nil || error != nil);
|
||||
}
|
||||
|
||||
// 等待连接归还
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 注意:无法直接检查池大小(内部实现),只能通过行为验证
|
||||
// 如果实现正确,池应自动限制为最<EFBFBD><EFBFBD>?4 个空闲连<EFBFBD><EFBFBD>?
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:120.0];
|
||||
}
|
||||
|
||||
// I.2 同时归还连接
|
||||
- (void)testRaceCondition_SimultaneousConnectionReturn_NoDataRace {
|
||||
NSInteger concurrentCount = 5;
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
for (NSInteger i = 0; i < concurrentCount; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Return %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"ReturnTest"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertTrue(response != nil || error != nil);
|
||||
// 连接在这里自动归<EFBFBD><EFBFBD>?
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:30.0];
|
||||
|
||||
// 如果没有崩溃或断言失败,说明并发归还处理正<EFBFBD><EFBFBD>?
|
||||
}
|
||||
|
||||
// I.3 获取-归还-再获取竞<EFBFBD><EFBFBD>?
|
||||
- (void)testRaceCondition_AcquireReturnReacquire_CorrectState {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Acquire-Return-Reacquire"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// 第一个请<EFBFBD><EFBFBD>?
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"First"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertTrue(response1 != nil || error1 != nil);
|
||||
|
||||
// 极短暂等待确保连接归<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:0.1];
|
||||
|
||||
// 第二个请求应该能复用连接
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"Second"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertTrue(response2 != nil || error2 != nil);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:35.0];
|
||||
}
|
||||
|
||||
// I.4 超时与活跃连接冲突(需<EFBFBD><EFBFBD>?1秒,标记为慢测试<EFBFBD><EFBFBD>?
|
||||
- (void)testRaceCondition_ExpiredConnectionPruning_CreatesNewConnection {
|
||||
// 跳过此测试如果环境变量设置了 SKIP_SLOW_TESTS
|
||||
if (getenv("SKIP_SLOW_TESTS")) {
|
||||
return;
|
||||
}
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Connection expiry"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// 创建连接
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"Initial"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertTrue(response1 != nil || error1 != nil);
|
||||
|
||||
// 等待超过30秒超<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:31.0];
|
||||
|
||||
// 新请求应该创建新连接(旧连接已过期)
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"AfterExpiry"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertTrue(response2 != nil || error2 != nil);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:70.0];
|
||||
}
|
||||
|
||||
// I.5 错误恢复竞<EFBFBD><EFBFBD>?
|
||||
- (void)testRaceCondition_ErrorRecovery_PoolRemainsHealthy {
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
// 发起一些会失败的请<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 3; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Error %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
// 使用短超时导致失<EFBFBD><EFBFBD>?
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/5"
|
||||
userAgent:@"ErrorTest"
|
||||
timeout:1.0
|
||||
error:&error];
|
||||
// 预期失败
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:15.0];
|
||||
|
||||
// 验证后续正常请求仍能成功
|
||||
XCTestExpectation *recoveryExpectation = [self expectationWithDescription:@"Recovery"];
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"Recovery"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertTrue(response != nil || error != nil);
|
||||
[recoveryExpectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[recoveryExpectation] timeout:20.0];
|
||||
}
|
||||
|
||||
#pragma mark - N. 并发多端口测<EFBFBD><EFBFBD>?
|
||||
|
||||
// N.1 并发保持连接(慢测试<EFBFBD><EFBFBD>?
|
||||
- (void)testConcurrentMultiPort_ParallelKeepAlive_IndependentConnections {
|
||||
if (getenv("SKIP_SLOW_TESTS")) {
|
||||
return;
|
||||
}
|
||||
|
||||
XCTestExpectation *expectation11443 = [self expectationWithDescription:@"Port 11443 keep-alive"];
|
||||
XCTestExpectation *expectation11444 = [self expectationWithDescription:@"Port 11444 keep-alive"];
|
||||
|
||||
// 线程 1:向端口 11443 发起 10 个请求,间隔 1 <EFBFBD><EFBFBD>?
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
for (NSInteger i = 0; i < 10; i++) {
|
||||
if (i > 0) {
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
}
|
||||
NSError *error = nil;
|
||||
[self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"KeepAlive11443"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
}
|
||||
[expectation11443 fulfill];
|
||||
});
|
||||
|
||||
// 线程 2:同时向端口 11444 发起 10 个请求,间隔 1 <EFBFBD><EFBFBD>?
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
for (NSInteger i = 0; i < 10; i++) {
|
||||
if (i > 0) {
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
}
|
||||
NSError *error = nil;
|
||||
[self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"KeepAlive11444"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
}
|
||||
[expectation11444 fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation11443, expectation11444] timeout:40.0];
|
||||
}
|
||||
|
||||
// N.2 轮询端口分配模式
|
||||
- (void)testConcurrentMultiPort_RoundRobinDistribution_EvenLoad {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Round-robin distribution"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445, @11446];
|
||||
NSInteger totalRequests = 100;
|
||||
NSMutableDictionary<NSNumber *, NSNumber *> *portRequestCounts = [NSMutableDictionary dictionary];
|
||||
|
||||
// 初始化计数器
|
||||
for (NSNumber *port in ports) {
|
||||
portRequestCounts[port] = @0;
|
||||
}
|
||||
|
||||
// 以轮询方式向 4 个端口分<EFBFBD><EFBFBD>?100 个请<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < totalRequests; i++) {
|
||||
NSNumber *port = ports[i % ports.count];
|
||||
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
|
||||
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"RoundRobin"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
if (response && response.statusCode == 200) {
|
||||
NSInteger count = [portRequestCounts[port] integerValue];
|
||||
portRequestCounts[port] = @(count + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证每个端口大约获得 25 个请<EFBFBD><EFBFBD>?
|
||||
for (NSNumber *port in ports) {
|
||||
NSInteger count = [portRequestCounts[port] integerValue];
|
||||
XCTAssertEqual(count, 25, @"Port %@ should receive 25 requests", port);
|
||||
}
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:180.0];
|
||||
}
|
||||
|
||||
// N.3 混合负载多端口场<EFBFBD><EFBFBD>?
|
||||
- (void)testConcurrentMultiPort_MixedLoadPattern_RobustHandling {
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
// 端口 11443:高负载<EFBFBD><EFBFBD>?0 个请求)
|
||||
for (NSInteger i = 0; i < 20; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Heavy11443 %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
[self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"HeavyLoad"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
// 端口 11444:中负载<EFBFBD><EFBFBD>?0 个请求)
|
||||
for (NSInteger i = 0; i < 10; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Medium11444 %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
[self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"MediumLoad"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
// 端口 11445:低负载<EFBFBD><EFBFBD>? 个请求)
|
||||
for (NSInteger i = 0; i < 5; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Light11445 %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
[self.client performRequestWithURLString:@"https://127.0.0.1:11445/get"
|
||||
userAgent:@"LightLoad"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:80.0];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,740 @@
|
||||
//
|
||||
// HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// @author Created by Claude Code on 2025-11-01
|
||||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||||
//
|
||||
// 边界条件与超时测<EFBFBD><EFBFBD>?- 包含边界条件 (M)、超时交<EFBFBD><EFBFBD>?(P) <EFBFBD><EFBFBD>?Connection 头部 (R) 测试<EFBFBD><EFBFBD>?
|
||||
// 测试总数<EFBFBD><EFBFBD>?5 个(M:4 + P:6 + R:5<EFBFBD><EFBFBD>?
|
||||
//
|
||||
|
||||
#import "HttpdnsNWHTTPClientTestBase.h"
|
||||
|
||||
@interface HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests : HttpdnsNWHTTPClientTestBase
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsNWHTTPClient_EdgeCasesAndTimeoutTests
|
||||
|
||||
#pragma mark - M. 边界条件与验证测<EFBFBD><EFBFBD>?
|
||||
|
||||
// M.1 连接复用边界:端口内复用,端口间隔离
|
||||
- (void)testEdgeCase_ConnectionReuseWithinPortOnly_NotAcross {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Reuse boundaries"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// 请求 A 到端<EFBFBD><EFBFBD>?11443
|
||||
CFAbsoluteTime timeA = CFAbsoluteTimeGetCurrent();
|
||||
NSError *errorA = nil;
|
||||
HttpdnsNWHTTPClientResponse *responseA = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"RequestA"
|
||||
timeout:15.0
|
||||
error:&errorA];
|
||||
CFAbsoluteTime elapsedA = CFAbsoluteTimeGetCurrent() - timeA;
|
||||
XCTAssertNotNil(responseA);
|
||||
|
||||
// 请求 B 到端<EFBFBD><EFBFBD>?11443(应该复用连接,可能更快<EFBFBD><EFBFBD>?
|
||||
CFAbsoluteTime timeB = CFAbsoluteTimeGetCurrent();
|
||||
NSError *errorB = nil;
|
||||
HttpdnsNWHTTPClientResponse *responseB = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"RequestB"
|
||||
timeout:15.0
|
||||
error:&errorB];
|
||||
CFAbsoluteTime elapsedB = CFAbsoluteTimeGetCurrent() - timeB;
|
||||
XCTAssertNotNil(responseB);
|
||||
|
||||
// 请求 C 到端<EFBFBD><EFBFBD>?11444(应该创建新连接<EFBFBD><EFBFBD>?
|
||||
CFAbsoluteTime timeC = CFAbsoluteTimeGetCurrent();
|
||||
NSError *errorC = nil;
|
||||
HttpdnsNWHTTPClientResponse *responseC = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"RequestC"
|
||||
timeout:15.0
|
||||
error:&errorC];
|
||||
CFAbsoluteTime elapsedC = CFAbsoluteTimeGetCurrent() - timeC;
|
||||
XCTAssertNotNil(responseC);
|
||||
|
||||
// 请求 D 到端<EFBFBD><EFBFBD>?11444(应该复用端<EFBFBD><EFBFBD>?11444 的连接)
|
||||
CFAbsoluteTime timeD = CFAbsoluteTimeGetCurrent();
|
||||
NSError *errorD = nil;
|
||||
HttpdnsNWHTTPClientResponse *responseD = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"RequestD"
|
||||
timeout:15.0
|
||||
error:&errorD];
|
||||
CFAbsoluteTime elapsedD = CFAbsoluteTimeGetCurrent() - timeD;
|
||||
XCTAssertNotNil(responseD);
|
||||
|
||||
// 验证所有请求都成功
|
||||
XCTAssertEqual(responseA.statusCode, 200);
|
||||
XCTAssertEqual(responseB.statusCode, 200);
|
||||
XCTAssertEqual(responseC.statusCode, 200);
|
||||
XCTAssertEqual(responseD.statusCode, 200);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:70.0];
|
||||
}
|
||||
|
||||
// M.2 高端口数量压力测<EFBFBD><EFBFBD>?
|
||||
- (void)testEdgeCase_HighPortCount_AllPortsManaged {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"High port count"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445, @11446];
|
||||
|
||||
// 第一轮:向所有端口各发起一个请<EFBFBD><EFBFBD>?
|
||||
for (NSNumber *port in ports) {
|
||||
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"Round1"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertNotNil(response, @"First round request to port %@ should succeed", port);
|
||||
}
|
||||
|
||||
// 第二轮:再次向所有端口发起请求(应该复用连接<EFBFBD><EFBFBD>?
|
||||
for (NSNumber *port in ports) {
|
||||
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"Round2"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertNotNil(response, @"Second round request to port %@ should reuse connection", port);
|
||||
}
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:120.0];
|
||||
}
|
||||
|
||||
// M.3 并发池访问安全<EFBFBD><EFBFBD>?
|
||||
- (void)testEdgeCase_ConcurrentPoolAccess_NoDataRace {
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445];
|
||||
NSInteger requestsPerPort = 5;
|
||||
|
||||
// 向三个端口并发发起请<EFBFBD><EFBFBD>?
|
||||
for (NSNumber *port in ports) {
|
||||
for (NSInteger i = 0; i < requestsPerPort; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Port %@ Req %ld", port, (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"ConcurrentAccess"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
// 如果没有崩溃或断言失败,说明并发访问安<EFBFBD><EFBFBD>?
|
||||
XCTAssertTrue(response != nil || error != nil);
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:50.0];
|
||||
}
|
||||
|
||||
// M.4 端口迁移模式
|
||||
- (void)testEdgeCase_PortMigration_OldConnectionsEventuallyExpire {
|
||||
if (getenv("SKIP_SLOW_TESTS")) {
|
||||
return;
|
||||
}
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Port migration"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// 阶段 1:向端口 11443 发起多个请求
|
||||
for (NSInteger i = 0; i < 5; i++) {
|
||||
NSError *error = nil;
|
||||
[self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"Port11443"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
}
|
||||
|
||||
// 阶段 2:切换到端口 11444,发起多个请<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 5; i++) {
|
||||
NSError *error = nil;
|
||||
[self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"Port11444"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
}
|
||||
|
||||
// 等待超过 30 秒,让端<EFBFBD><EFBFBD>?11443 的连接过<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:31.0];
|
||||
|
||||
// 阶段 3:验证端<EFBFBD><EFBFBD>?11444 仍然可用
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"Port11444After"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1, @"Port 11444 should still work after 11443 expired");
|
||||
|
||||
// 阶段 4:端<EFBFBD><EFBFBD>?11443 应该创建新连接(旧连接已过期<EFBFBD><EFBFBD>?
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"Port11443New"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2, @"Port 11443 should work with new connection after expiry");
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:120.0];
|
||||
}
|
||||
|
||||
#pragma mark - P. 超时与连接池交互测试
|
||||
|
||||
// P.1 单次超时后连接被正确移除
|
||||
- (void)testTimeout_SingleRequest_ConnectionRemovedFromPool {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 验证初始状<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||||
@"Pool should be empty initially");
|
||||
|
||||
// 发起超时请求(delay 10s, timeout 1s<EFBFBD><EFBFBD>?
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
|
||||
userAgent:@"TimeoutTest"
|
||||
timeout:1.0
|
||||
error:&error];
|
||||
|
||||
// 验证请求失败
|
||||
XCTAssertNil(response, @"Response should be nil on timeout");
|
||||
XCTAssertNotNil(error, @"Error should be set on timeout");
|
||||
|
||||
// 等待异步 returnConnection 完成
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证池状态:超时连接应该被移<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||||
@"Timed-out connection should be removed from pool");
|
||||
XCTAssertEqual([self.client totalConnectionCount], 0,
|
||||
@"No connections should remain in pool");
|
||||
|
||||
// 验证统计
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 1,
|
||||
@"Should have created 1 connection");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 0,
|
||||
@"No reuse for timed-out connection");
|
||||
}
|
||||
|
||||
// P.2 超时后连接池恢复能力
|
||||
- (void)testTimeout_PoolRecovery_SubsequentRequestSucceeds {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 第一个请求:超时
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
|
||||
userAgent:@"TimeoutTest"
|
||||
timeout:1.0
|
||||
error:&error1];
|
||||
XCTAssertNil(response1, @"First request should timeout");
|
||||
XCTAssertNotNil(error1);
|
||||
|
||||
// 等待清理完成
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 第二个请求:正常(验证池已恢复)
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"RecoveryTest"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2, @"Second request should succeed after timeout");
|
||||
XCTAssertEqual(response2.statusCode, 200);
|
||||
|
||||
// 等待 returnConnection
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证池恢复:现在应该<EFBFBD><EFBFBD>?1 个连接(来自第二个请求)
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
|
||||
@"Pool should have 1 connection from second request");
|
||||
|
||||
// 第三个请求:应该复用第二个请求的连接
|
||||
NSError *error3 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response3 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"ReuseTest"
|
||||
timeout:15.0
|
||||
error:&error3];
|
||||
XCTAssertNotNil(response3);
|
||||
XCTAssertEqual(response3.statusCode, 200);
|
||||
|
||||
// 验证统计<EFBFBD><EFBFBD>? 个超时(创建后移除)+ 1 个成功(创建<EFBFBD><EFBFBD>? 1 个复<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 2,
|
||||
@"Should have created 2 connections (1 timed out, 1 succeeded)");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 1,
|
||||
@"Third request should reuse second's connection");
|
||||
}
|
||||
|
||||
// P.3 并发场景:部分超时不影响成功请求的连接复<EFBFBD><EFBFBD>?
|
||||
- (void)testTimeout_ConcurrentPartialTimeout_SuccessfulRequestsReuse {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
NSLock *successCountLock = [[NSLock alloc] init];
|
||||
NSLock *timeoutCountLock = [[NSLock alloc] init];
|
||||
__block NSInteger successCount = 0;
|
||||
__block NSInteger timeoutCount = 0;
|
||||
|
||||
// 发起 10 个请求:5 个正常,5 个超<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 10; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
NSString *urlString;
|
||||
NSTimeInterval timeout;
|
||||
|
||||
if (i % 2 == 0) {
|
||||
// 偶数:正常请<EFBFBD><EFBFBD>?
|
||||
urlString = @"http://127.0.0.1:11080/get";
|
||||
timeout = 15.0;
|
||||
} else {
|
||||
// 奇数:超时请<EFBFBD><EFBFBD>?
|
||||
urlString = @"http://127.0.0.1:11080/delay/10";
|
||||
timeout = 0.5;
|
||||
}
|
||||
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"ConcurrentTest"
|
||||
timeout:timeout
|
||||
error:&error];
|
||||
|
||||
if (response && response.statusCode == 200) {
|
||||
[successCountLock lock];
|
||||
successCount++;
|
||||
[successCountLock unlock];
|
||||
} else {
|
||||
[timeoutCountLock lock];
|
||||
timeoutCount++;
|
||||
[timeoutCountLock unlock];
|
||||
}
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:20.0];
|
||||
|
||||
// 验证结果
|
||||
XCTAssertEqual(successCount, 5, @"5 requests should succeed");
|
||||
XCTAssertEqual(timeoutCount, 5, @"5 requests should timeout");
|
||||
|
||||
// 验证连接创建数合理(5个成<EFBFBD><EFBFBD>?+ 5个超<EFBFBD><EFBFBD>?= 最<EFBFBD><EFBFBD>?0个,可能有复用)
|
||||
XCTAssertGreaterThan(self.client.connectionCreationCount, 0,
|
||||
@"Should have created connections for concurrent requests");
|
||||
XCTAssertLessThanOrEqual(self.client.connectionCreationCount, 10,
|
||||
@"Should not create more than 10 connections");
|
||||
|
||||
// 等待所有连接归还(异步操作需要更长时间)
|
||||
[NSThread sleepForTimeInterval:2.0];
|
||||
|
||||
// 验证总连接数合理(无泄漏<EFBFBD><EFBFBD>? 关键验证<EFBFBD><EFBFBD>?
|
||||
// 在并发场景下,成功的连接可能已经被关闭(remote close),池可能为<EFBFBD><EFBFBD>?
|
||||
XCTAssertLessThanOrEqual([self.client totalConnectionCount], 4,
|
||||
@"Total connections should not exceed pool limit (no leak)");
|
||||
|
||||
// 发起新请求验证池仍然健康(能创建新连接)
|
||||
NSError *recoveryError = nil;
|
||||
HttpdnsNWHTTPClientResponse *recoveryResponse = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"RecoveryTest"
|
||||
timeout:15.0
|
||||
error:&recoveryError];
|
||||
XCTAssertNotNil(recoveryResponse, @"Pool should recover and handle new requests after mixed timeout/success");
|
||||
XCTAssertEqual(recoveryResponse.statusCode, 200, @"Recovery request should succeed");
|
||||
}
|
||||
|
||||
// P.4 连续超时不导致连接泄<EFBFBD><EFBFBD>?
|
||||
- (void)testTimeout_ConsecutiveTimeouts_NoConnectionLeak {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 连续发起 10 个超时请<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 10; i++) {
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
|
||||
userAgent:@"LeakTest"
|
||||
timeout:0.5
|
||||
error:&error];
|
||||
XCTAssertNil(response, @"Request %ld should timeout", (long)i);
|
||||
|
||||
// 等待清理
|
||||
[NSThread sleepForTimeInterval:0.2];
|
||||
}
|
||||
|
||||
// 验证池状态:无连接泄<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||||
@"Pool should be empty after consecutive timeouts");
|
||||
XCTAssertEqual([self.client totalConnectionCount], 0,
|
||||
@"No connections should leak");
|
||||
|
||||
// 验证统计:每次都创建新连接(因为超时的被移除<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 10,
|
||||
@"Should have created 10 connections (all timed out and removed)");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 0,
|
||||
@"No reuse for timed-out connections");
|
||||
}
|
||||
|
||||
// P.5 超时不阻塞连接池(并发正常请求不受影响)
|
||||
- (void)testTimeout_NonBlocking_ConcurrentNormalRequestSucceeds {
|
||||
[self.client resetPoolStatistics];
|
||||
|
||||
XCTestExpectation *timeoutExpectation = [self expectationWithDescription:@"Timeout request"];
|
||||
XCTestExpectation *successExpectation = [self expectationWithDescription:@"Success request"];
|
||||
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
// 请求 A:超时(delay 10s, timeout 2s<EFBFBD><EFBFBD>?
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
|
||||
userAgent:@"TimeoutRequest"
|
||||
timeout:2.0
|
||||
error:&error];
|
||||
XCTAssertNil(response, @"Request A should timeout");
|
||||
[timeoutExpectation fulfill];
|
||||
});
|
||||
|
||||
// 请求 B:正常(应该不受 A 阻塞<EFBFBD><EFBFBD>?
|
||||
dispatch_async(queue, ^{
|
||||
// 稍微延迟,确<EFBFBD><EFBFBD>?A 先开<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:0.1];
|
||||
|
||||
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"NormalRequest"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - startTime;
|
||||
|
||||
XCTAssertNotNil(response, @"Request B should succeed despite A timing out");
|
||||
XCTAssertEqual(response.statusCode, 200);
|
||||
|
||||
// 验证请求 B 没有被请<EFBFBD><EFBFBD>?A 阻塞(应该很快完成)
|
||||
XCTAssertLessThan(elapsed, 5.0,
|
||||
@"Request B should complete quickly, not blocked by A's timeout");
|
||||
|
||||
[successExpectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[timeoutExpectation, successExpectation] timeout:20.0];
|
||||
|
||||
// 等待连接归还
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证池状态:只有请求 B 的连<EFBFBD><EFBFBD>?
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
|
||||
@"Pool should have 1 connection from successful request B");
|
||||
}
|
||||
|
||||
// P.6 多端口场景下的超时隔<EFBFBD><EFBFBD>?
|
||||
- (void)testTimeout_MultiPort_IsolatedPoolCleaning {
|
||||
[self.client resetPoolStatistics];
|
||||
|
||||
NSString *poolKey11443 = @"127.0.0.1:11443:tls";
|
||||
NSString *poolKey11444 = @"127.0.0.1:11444:tls";
|
||||
|
||||
// 端口 11443:超时请<EFBFBD><EFBFBD>?
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/delay/10"
|
||||
userAgent:@"Port11443Timeout"
|
||||
timeout:1.0
|
||||
error:&error1];
|
||||
XCTAssertNil(response1, @"Port 11443 request should timeout");
|
||||
|
||||
// 端口 11444:正常请<EFBFBD><EFBFBD>?
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"Port11444Success"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2, @"Port 11444 request should succeed");
|
||||
XCTAssertEqual(response2.statusCode, 200);
|
||||
|
||||
// 等待连接归还
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证端口隔离:端<EFBFBD><EFBFBD>?11443 无连接,端口 11444 <EFBFBD><EFBFBD>?1 个连<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey11443], 0,
|
||||
@"Port 11443 pool should be empty (timed out)");
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey11444], 1,
|
||||
@"Port 11444 pool should have 1 connection");
|
||||
|
||||
// 验证总连接数
|
||||
XCTAssertEqual([self.client totalConnectionCount], 1,
|
||||
@"Total should be 1 (only from port 11444)");
|
||||
|
||||
// 再次请求端口 11444:应该复用连<EFBFBD><EFBFBD>?
|
||||
NSError *error3 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response3 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"Port11444Reuse"
|
||||
timeout:15.0
|
||||
error:&error3];
|
||||
XCTAssertNotNil(response3);
|
||||
XCTAssertEqual(response3.statusCode, 200);
|
||||
|
||||
// 验证复用发生
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 1,
|
||||
@"Second request to port 11444 should reuse connection");
|
||||
}
|
||||
|
||||
#pragma mark - R. Connection 头部处理测试
|
||||
|
||||
// R.1 Connection: close 导致连接被立即失<EFBFBD><EFBFBD>?
|
||||
- (void)testConnectionHeader_Close_ConnectionInvalidated {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 请求 1:服务器返回 Connection: close
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=close"
|
||||
userAgent:@"CloseTest"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1, @"Request with Connection: close should succeed");
|
||||
XCTAssertEqual(response1.statusCode, 200);
|
||||
|
||||
// 等待异步 returnConnection 完成
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证:连接不在池中(已失效)
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||||
@"Connection with 'Connection: close' header should be invalidated and not returned to pool");
|
||||
|
||||
// 请求 2:应该创建新连接(第一个连接已失效<EFBFBD><EFBFBD>?
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=keep-alive"
|
||||
userAgent:@"KeepAliveTest"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2);
|
||||
XCTAssertEqual(response2.statusCode, 200);
|
||||
|
||||
// 验证统计:创建了 2 个连接(第一个被 close,第二个正常<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 2,
|
||||
@"Should have created 2 connections (first closed by server)");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 0,
|
||||
@"No reuse after Connection: close");
|
||||
}
|
||||
|
||||
// R.2 Connection: keep-alive 允许连接复用
|
||||
- (void)testConnectionHeader_KeepAlive_ConnectionReused {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 请求 1:服务器返回 Connection: keep-alive
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=keep-alive"
|
||||
userAgent:@"KeepAlive1"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1);
|
||||
XCTAssertEqual(response1.statusCode, 200);
|
||||
|
||||
// 等待连接归还
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证:连接在池中
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
|
||||
@"Connection with 'Connection: keep-alive' should be returned to pool");
|
||||
|
||||
// 请求 2:应该复用第一个连<EFBFBD><EFBFBD>?
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=keep-alive"
|
||||
userAgent:@"KeepAlive2"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2);
|
||||
XCTAssertEqual(response2.statusCode, 200);
|
||||
|
||||
// 验证统计:只创建<EFBFBD><EFBFBD>?1 个连接,第二个请求复用了<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 1,
|
||||
@"Should have created only 1 connection");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 1,
|
||||
@"Second request should reuse the first connection");
|
||||
}
|
||||
|
||||
// R.3 Proxy-Connection: close 也会导致连接失效
|
||||
- (void)testConnectionHeader_ProxyClose_ConnectionInvalidated {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 请求 1:服务器返回 Proxy-Connection: close (+ Connection: keep-alive)
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=proxy-close"
|
||||
userAgent:@"ProxyCloseTest"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1, @"Request with Proxy-Connection: close should succeed");
|
||||
XCTAssertEqual(response1.statusCode, 200);
|
||||
|
||||
// 等待异步 returnConnection 完成
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证:连接不在池中(Proxy-Connection: close 应该失效连接<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||||
@"Connection with 'Proxy-Connection: close' header should be invalidated");
|
||||
|
||||
// 请求 2:应该创建新连接
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=keep-alive"
|
||||
userAgent:@"RecoveryTest"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2);
|
||||
XCTAssertEqual(response2.statusCode, 200);
|
||||
|
||||
// 验证统计:创建了 2 个连<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 2,
|
||||
@"Should have created 2 connections (first closed by proxy header)");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 0,
|
||||
@"No reuse after Proxy-Connection: close");
|
||||
}
|
||||
|
||||
// R.4 Connection 头部大小写不敏感
|
||||
- (void)testConnectionHeader_CaseInsensitive_AllVariantsWork {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 测试 1:CONNECTION: CLOSE (全大<EFBFBD><EFBFBD>?
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=close-uppercase"
|
||||
userAgent:@"UppercaseTest"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1);
|
||||
XCTAssertEqual(response1.statusCode, 200);
|
||||
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证:连接不在池中(大写 CLOSE 也应生效<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||||
@"'CONNECTION: CLOSE' (uppercase) should also close connection");
|
||||
|
||||
// 测试 2:Connection: Close (混合大小<EFBFBD><EFBFBD>?
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/connection-test?mode=close-mixed"
|
||||
userAgent:@"MixedCaseTest"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2);
|
||||
XCTAssertEqual(response2.statusCode, 200);
|
||||
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证:连接不在池中(混合大小写也应生效)
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||||
@"'Connection: Close' (mixed case) should also close connection");
|
||||
|
||||
// 验证统计:创建了 2 个连接,都被 close
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 2,
|
||||
@"Should have created 2 connections (both closed due to case-insensitive matching)");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 0,
|
||||
@"No reuse for any closed connections");
|
||||
}
|
||||
|
||||
// R.5 并发场景:混<EFBFBD><EFBFBD>?close <EFBFBD><EFBFBD>?keep-alive
|
||||
- (void)testConnectionHeader_ConcurrentMixed_CloseAndKeepAliveIsolated {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
NSLock *closeCountLock = [[NSLock alloc] init];
|
||||
NSLock *keepAliveCountLock = [[NSLock alloc] init];
|
||||
__block NSInteger closeCount = 0;
|
||||
__block NSInteger keepAliveCount = 0;
|
||||
|
||||
// 发起 10 个请求:5 <EFBFBD><EFBFBD>?close<EFBFBD><EFBFBD>? <EFBFBD><EFBFBD>?keep-alive
|
||||
for (NSInteger i = 0; i < 10; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
NSString *urlString;
|
||||
|
||||
if (i % 2 == 0) {
|
||||
// 偶数:close
|
||||
urlString = @"http://127.0.0.1:11080/connection-test?mode=close";
|
||||
} else {
|
||||
// 奇数:keep-alive
|
||||
urlString = @"http://127.0.0.1:11080/connection-test?mode=keep-alive";
|
||||
}
|
||||
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"ConcurrentMixed"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
if (response && response.statusCode == 200) {
|
||||
if (i % 2 == 0) {
|
||||
[closeCountLock lock];
|
||||
closeCount++;
|
||||
[closeCountLock unlock];
|
||||
} else {
|
||||
[keepAliveCountLock lock];
|
||||
keepAliveCount++;
|
||||
[keepAliveCountLock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:30.0];
|
||||
|
||||
// 验证结果
|
||||
XCTAssertEqual(closeCount, 5, @"5 close requests should succeed");
|
||||
XCTAssertEqual(keepAliveCount, 5, @"5 keep-alive requests should succeed");
|
||||
|
||||
// 等待所有连接归<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 验证池状态:
|
||||
// - close 连接全部被失效(不在池中<EFBFBD><EFBFBD>?
|
||||
// - keep-alive 连接可能在池中(取决于并发时序和 remote close<EFBFBD><EFBFBD>?
|
||||
// - 关键是:总数不超<EFBFBD><EFBFBD>?4(池限制),无泄<EFBFBD><EFBFBD>?
|
||||
NSInteger poolSize = [self.client connectionPoolCountForKey:poolKey];
|
||||
XCTAssertLessThanOrEqual(poolSize, 4,
|
||||
@"Pool size should not exceed limit (no leak from close connections)");
|
||||
|
||||
// 验证统计:close 连接不应该被复用
|
||||
// 创建数应<EFBFBD><EFBFBD>?>= 6 (至少 5 <EFBFBD><EFBFBD>?close + 1 <EFBFBD><EFBFBD>?keep-alive,因<EFBFBD><EFBFBD>?close 不能复用)
|
||||
XCTAssertGreaterThanOrEqual(self.client.connectionCreationCount, 6,
|
||||
@"Should have created at least 6 connections (5 close + at least 1 keep-alive)");
|
||||
|
||||
// 后续请求验证池仍然健<EFBFBD><EFBFBD>?
|
||||
NSError *recoveryError = nil;
|
||||
HttpdnsNWHTTPClientResponse *recoveryResponse = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"RecoveryTest"
|
||||
timeout:15.0
|
||||
error:&recoveryError];
|
||||
XCTAssertNotNil(recoveryResponse, @"Pool should still be healthy after mixed close/keep-alive scenario");
|
||||
XCTAssertEqual(recoveryResponse.statusCode, 200);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,774 @@
|
||||
//
|
||||
// HttpdnsNWHTTPClient_PoolManagementTests.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// @author Created by Claude Code on 2025-11-01
|
||||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||||
//
|
||||
// 连接池管理测<EFBFBD><EFBFBD>?- 包含多端口隔<EFBFBD><EFBFBD>?(K)、端口池耗尽 (L)、池验证 (O)、空闲超<EFBFBD><EFBFBD>?(S) 测试<EFBFBD><EFBFBD>?
|
||||
// 测试总数<EFBFBD><EFBFBD>?6 个(K:5 + L:3 + O:3 + S:5<EFBFBD><EFBFBD>?
|
||||
//
|
||||
|
||||
#import "HttpdnsNWHTTPClientTestBase.h"
|
||||
|
||||
@interface HttpdnsNWHTTPClient_PoolManagementTests : HttpdnsNWHTTPClientTestBase
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsNWHTTPClient_PoolManagementTests
|
||||
|
||||
#pragma mark - K. 多端口连接隔离测<EFBFBD><EFBFBD>?
|
||||
|
||||
// K.1 不同 HTTPS 端口使用不同连接<EFBFBD><EFBFBD>?
|
||||
- (void)testMultiPort_DifferentHTTPSPorts_SeparatePoolKeys {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Different ports use different pools"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// 请求端口 11443
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"Port11443"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1, @"First request to port 11443 should succeed");
|
||||
XCTAssertEqual(response1.statusCode, 200);
|
||||
|
||||
// 请求端口 11444(应该创建新连接,不复用 11443 的)
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"Port11444"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2, @"First request to port 11444 should succeed");
|
||||
XCTAssertEqual(response2.statusCode, 200);
|
||||
|
||||
// 再次请求端口 11443(应该复用之前的连接<EFBFBD><EFBFBD>?
|
||||
NSError *error3 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response3 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"Port11443Again"
|
||||
timeout:15.0
|
||||
error:&error3];
|
||||
XCTAssertNotNil(response3, @"Second request to port 11443 should reuse connection");
|
||||
XCTAssertEqual(response3.statusCode, 200);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:50.0];
|
||||
}
|
||||
|
||||
// K.2 三个不同 HTTPS 端口的并发请<EFBFBD><EFBFBD>?
|
||||
- (void)testMultiPort_ConcurrentThreePorts_AllSucceed {
|
||||
NSInteger requestsPerPort = 10;
|
||||
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445];
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
NSLock *successCountLock = [[NSLock alloc] init];
|
||||
__block NSInteger successCount = 0;
|
||||
|
||||
for (NSNumber *port in ports) {
|
||||
for (NSInteger i = 0; i < requestsPerPort; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Port %@ Request %ld", port, (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"ConcurrentMultiPort"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
if (response && response.statusCode == 200) {
|
||||
[successCountLock lock];
|
||||
successCount++;
|
||||
[successCountLock unlock];
|
||||
}
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:60.0];
|
||||
|
||||
// 验证所有请求都成功
|
||||
XCTAssertEqual(successCount, ports.count * requestsPerPort, @"All 30 requests should succeed");
|
||||
}
|
||||
|
||||
// K.3 快速切换端口模<EFBFBD><EFBFBD>?
|
||||
- (void)testMultiPort_SequentialPortSwitching_ConnectionReusePerPort {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Sequential port switching"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray<NSString *> *urls = @[
|
||||
@"https://127.0.0.1:11443/get", // 第一次访<EFBFBD><EFBFBD>?11443
|
||||
@"https://127.0.0.1:11444/get", // 第一次访<EFBFBD><EFBFBD>?11444
|
||||
@"https://127.0.0.1:11445/get", // 第一次访<EFBFBD><EFBFBD>?11445
|
||||
@"https://127.0.0.1:11443/get", // 第二次访<EFBFBD><EFBFBD>?11443(应复用<EFBFBD><EFBFBD>?
|
||||
@"https://127.0.0.1:11444/get", // 第二次访<EFBFBD><EFBFBD>?11444(应复用<EFBFBD><EFBFBD>?
|
||||
];
|
||||
|
||||
NSMutableArray<NSNumber *> *responseTimes = [NSMutableArray array];
|
||||
|
||||
for (NSString *url in urls) {
|
||||
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:url
|
||||
userAgent:@"SwitchingPorts"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - startTime;
|
||||
|
||||
XCTAssertNotNil(response, @"Request to %@ should succeed", url);
|
||||
XCTAssertEqual(response.statusCode, 200);
|
||||
[responseTimes addObject:@(elapsed)];
|
||||
}
|
||||
|
||||
// 验证所有请求都完成
|
||||
XCTAssertEqual(responseTimes.count, urls.count);
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:80.0];
|
||||
}
|
||||
|
||||
// K.4 每个端口独立的连接池限制
|
||||
- (void)testMultiPort_PerPortPoolLimit_IndependentPools {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Per-port pool limits"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// 向端<EFBFBD><EFBFBD>?11443 发<EFBFBD><EFBFBD>?10 个请<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 10; i++) {
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"Pool11443"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertTrue(response != nil || error != nil);
|
||||
}
|
||||
|
||||
// 向端<EFBFBD><EFBFBD>?11444 发<EFBFBD><EFBFBD>?10 个请求(应该有独立的池)
|
||||
for (NSInteger i = 0; i < 10; i++) {
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"Pool11444"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertTrue(response != nil || error != nil);
|
||||
}
|
||||
|
||||
// 等待连接归还
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 验证两个端口都仍然可<EFBFBD><EFBFBD>?
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"Verify11443"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1, @"Port 11443 should still work after heavy usage");
|
||||
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"Verify11444"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2, @"Port 11444 should still work after heavy usage");
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:150.0];
|
||||
}
|
||||
|
||||
// K.5 交错访问多个端口
|
||||
- (void)testMultiPort_InterleavedRequests_AllPortsAccessible {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Interleaved multi-port requests"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445, @11446];
|
||||
NSInteger totalRequests = 20;
|
||||
NSInteger successCount = 0;
|
||||
|
||||
// 交错请求:依次循环访问所有端<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < totalRequests; i++) {
|
||||
NSNumber *port = ports[i % ports.count];
|
||||
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
|
||||
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"Interleaved"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
if (response && response.statusCode == 200) {
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(successCount, totalRequests, @"All interleaved requests should succeed");
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:120.0];
|
||||
}
|
||||
|
||||
#pragma mark - L. 基于端口的池耗尽测试
|
||||
|
||||
// L.1 四个端口同时承载高负<EFBFBD><EFBFBD>?
|
||||
- (void)testPoolExhaustion_FourPortsSimultaneous_AllSucceed {
|
||||
NSInteger requestsPerPort = 10;
|
||||
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445, @11446];
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
NSLock *successCountLock = [[NSLock alloc] init];
|
||||
__block NSInteger successCount = 0;
|
||||
|
||||
// <EFBFBD><EFBFBD>?4 个端口各发起 10 个并发请求(<EFBFBD><EFBFBD>?40 个)
|
||||
for (NSNumber *port in ports) {
|
||||
for (NSInteger i = 0; i < requestsPerPort; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Port %@ Request %ld", port, (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"FourPortLoad"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
|
||||
if (response && response.statusCode == 200) {
|
||||
[successCountLock lock];
|
||||
successCount++;
|
||||
[successCountLock unlock];
|
||||
}
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:80.0];
|
||||
|
||||
// 验证成功<EFBFBD><EFBFBD>?> 95%(允许少量因并发导致的失败)
|
||||
XCTAssertGreaterThan(successCount, 38, @"At least 95%% of 40 requests should succeed");
|
||||
}
|
||||
|
||||
// L.2 单个端口耗尽时其他端口不受影<EFBFBD><EFBFBD>?
|
||||
- (void)testPoolExhaustion_SinglePortExhausted_OthersUnaffected {
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
__block NSInteger port11444SuccessCount = 0;
|
||||
NSLock *countLock = [[NSLock alloc] init];
|
||||
|
||||
// 向端<EFBFBD><EFBFBD>?11443 发起 20 个并发请求(可能导致池耗尽<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 20; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Exhaust11443 %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
[self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"Exhaust11443"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
// 同时向端<EFBFBD><EFBFBD>?11444 发起 5 个请求(应该不受 11443 影响<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 5; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Port11444 %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"Independent11444"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - startTime;
|
||||
|
||||
if (response && response.statusCode == 200) {
|
||||
[countLock lock];
|
||||
port11444SuccessCount++;
|
||||
[countLock unlock];
|
||||
}
|
||||
|
||||
// 验证响应时间合理(不应因 11443 负载而显著延迟)
|
||||
XCTAssertLessThan(elapsed, 10.0, @"Port 11444 should not be delayed by port 11443 load");
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:60.0];
|
||||
|
||||
// 验证端口 11444 的请求都成功
|
||||
XCTAssertEqual(port11444SuccessCount, 5, @"All port 11444 requests should succeed despite 11443 load");
|
||||
}
|
||||
|
||||
// L.3 多端口使用后的连接清<EFBFBD><EFBFBD>?
|
||||
- (void)testPoolExhaustion_MultiPortCleanup_ExpiredConnectionsPruned {
|
||||
if (getenv("SKIP_SLOW_TESTS")) {
|
||||
return;
|
||||
}
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Multi-port connection cleanup"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray<NSNumber *> *ports = @[@11443, @11444, @11445];
|
||||
|
||||
// 向三个端口各发起一个请<EFBFBD><EFBFBD>?
|
||||
for (NSNumber *port in ports) {
|
||||
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"Initial"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertTrue(response != nil || error != nil);
|
||||
}
|
||||
|
||||
// 等待超过 30 秒,让所有连接过<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:31.0];
|
||||
|
||||
// 再次向三个端口发起请求(应该创建新连接)
|
||||
for (NSNumber *port in ports) {
|
||||
NSString *urlString = [NSString stringWithFormat:@"https://127.0.0.1:%@/get", port];
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:urlString
|
||||
userAgent:@"AfterExpiry"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertTrue(response != nil || error != nil, @"Requests should succeed after expiry");
|
||||
}
|
||||
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[self waitForExpectations:@[expectation] timeout:80.0];
|
||||
}
|
||||
|
||||
#pragma mark - O. 连接池验证测试(使用新增的检<EFBFBD><EFBFBD>?API<EFBFBD><EFBFBD>?
|
||||
|
||||
// O.1 综合连接池验<EFBFBD><EFBFBD>?- 演示所有检查能<EFBFBD><EFBFBD>?
|
||||
- (void)testPoolVerification_ComprehensiveCheck_AllAspectsVerified {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 初始状态:无连<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 0,
|
||||
@"Pool should be empty initially");
|
||||
XCTAssertEqual([self.client totalConnectionCount], 0,
|
||||
@"Total connections should be 0 initially");
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 0,
|
||||
@"Creation count should be 0 initially");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 0,
|
||||
@"Reuse count should be 0 initially");
|
||||
|
||||
// 发<EFBFBD><EFBFBD>?5 个请求到同一端点
|
||||
for (NSInteger i = 0; i < 5; i++) {
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"PoolVerificationTest"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertNotNil(response, @"Request %ld should succeed", (long)i);
|
||||
XCTAssertEqual(response.statusCode, 200, @"Request %ld should return 200", (long)i);
|
||||
}
|
||||
|
||||
// 验证连接池状<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
|
||||
@"Should have exactly 1 connection in pool for key: %@", poolKey);
|
||||
XCTAssertEqual([self.client totalConnectionCount], 1,
|
||||
@"Total connection count should be 1");
|
||||
|
||||
// 验证统计计数
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 1,
|
||||
@"Should create only 1 connection");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 4,
|
||||
@"Should reuse connection 4 times");
|
||||
|
||||
// 验证连接复用<EFBFBD><EFBFBD>?
|
||||
CGFloat reuseRate = (CGFloat)self.client.connectionReuseCount /
|
||||
(self.client.connectionCreationCount + self.client.connectionReuseCount);
|
||||
XCTAssertGreaterThanOrEqual(reuseRate, 0.8,
|
||||
@"Reuse rate should be at least 80%% (actual: %.1f%%)", reuseRate * 100);
|
||||
|
||||
// 验证 pool keys
|
||||
NSArray<NSString *> *allKeys = [self.client allConnectionPoolKeys];
|
||||
XCTAssertEqual(allKeys.count, 1, @"Should have exactly 1 pool key");
|
||||
XCTAssertTrue([allKeys containsObject:poolKey], @"Should contain the expected pool key");
|
||||
}
|
||||
|
||||
// O.2 多端口连接池隔离验证
|
||||
- (void)testPoolVerification_MultiPort_IndependentPools {
|
||||
[self.client resetPoolStatistics];
|
||||
|
||||
NSString *key11443 = @"127.0.0.1:11443:tls";
|
||||
NSString *key11444 = @"127.0.0.1:11444:tls";
|
||||
|
||||
// 初始:两个池都为<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:key11443], 0);
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:key11444], 0);
|
||||
|
||||
// 向端<EFBFBD><EFBFBD>?11443 发<EFBFBD><EFBFBD>?3 个请<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 3; i++) {
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"Port11443"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertNotNil(response);
|
||||
}
|
||||
|
||||
// 验证端口 11443 的池状<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:key11443], 1,
|
||||
@"Port 11443 should have 1 connection");
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:key11444], 0,
|
||||
@"Port 11444 should still be empty");
|
||||
|
||||
// 向端<EFBFBD><EFBFBD>?11444 发<EFBFBD><EFBFBD>?3 个请<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 3; i++) {
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"https://127.0.0.1:11444/get"
|
||||
userAgent:@"Port11444"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertNotNil(response);
|
||||
}
|
||||
|
||||
// 验证两个端口的池都存在且独立
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:key11443], 1,
|
||||
@"Port 11443 should still have 1 connection");
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:key11444], 1,
|
||||
@"Port 11444 should now have 1 connection");
|
||||
XCTAssertEqual([self.client totalConnectionCount], 2,
|
||||
@"Total should be 2 connections (one per port)");
|
||||
|
||||
// 验证统计:应该创建了 2 个连接,复用<EFBFBD><EFBFBD>?4 <EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 2,
|
||||
@"Should create 2 connections (one per port)");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 4,
|
||||
@"Should reuse connections 4 times total");
|
||||
|
||||
// 验证 pool keys
|
||||
NSArray<NSString *> *allKeys = [self.client allConnectionPoolKeys];
|
||||
XCTAssertEqual(allKeys.count, 2, @"Should have 2 pool keys");
|
||||
XCTAssertTrue([allKeys containsObject:key11443], @"Should contain key for port 11443");
|
||||
XCTAssertTrue([allKeys containsObject:key11444], @"Should contain key for port 11444");
|
||||
}
|
||||
|
||||
// O.3 连接池容量限制验<EFBFBD><EFBFBD>?
|
||||
- (void)testPoolVerification_PoolCapacity_MaxFourConnections {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 发<EFBFBD><EFBFBD>?10 个连续请求(每个请求都会归还连接到池<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 10; i++) {
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"CapacityTest"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertNotNil(response);
|
||||
}
|
||||
|
||||
// 等待连接归还
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 验证池大小不超过 4(kHttpdnsNWHTTPClientMaxIdleConnectionsPerKey<EFBFBD><EFBFBD>?
|
||||
NSUInteger poolSize = [self.client connectionPoolCountForKey:poolKey];
|
||||
XCTAssertLessThanOrEqual(poolSize, 4,
|
||||
@"Pool size should not exceed 4 (actual: %lu)", (unsigned long)poolSize);
|
||||
|
||||
// 验证统计:应该只创建<EFBFBD><EFBFBD>?1 个连接(因为串行请求,每次都复用<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 1,
|
||||
@"Should create only 1 connection for sequential requests");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 9,
|
||||
@"Should reuse connection 9 times");
|
||||
}
|
||||
|
||||
#pragma mark - S. 空闲超时详细测试
|
||||
|
||||
// S.1 混合过期和有效连<EFBFBD><EFBFBD>?- 选择性清<EFBFBD><EFBFBD>?
|
||||
- (void)testIdleTimeout_MixedExpiredValid_SelectivePruning {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 创建第一个连<EFBFBD><EFBFBD>?
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"ConnectionA"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1);
|
||||
XCTAssertEqual(response1.statusCode, 200);
|
||||
|
||||
// 等待连接归还
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 使用 DEBUG API 获取连接 A 并设置为过期<EFBFBD><EFBFBD>?5 秒前<EFBFBD><EFBFBD>?
|
||||
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
|
||||
XCTAssertEqual(connections.count, 1, @"Should have 1 connection in pool");
|
||||
|
||||
HttpdnsNWReusableConnection *connectionA = connections.firstObject;
|
||||
NSDate *expiredDate = [NSDate dateWithTimeIntervalSinceNow:-35.0];
|
||||
[connectionA debugSetLastUsedDate:expiredDate];
|
||||
|
||||
// 创建第二个连接(通过并发请求<EFBFBD><EFBFBD>?
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"ConnectionB"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2);
|
||||
|
||||
// 等待归还
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证:应该有 1 个连接(connectionA 过期被移除,connectionB 留下<EFBFBD><EFBFBD>?
|
||||
connections = [self.client connectionsInPoolForKey:poolKey];
|
||||
XCTAssertEqual(connections.count, 1,
|
||||
@"Should have only 1 connection (expired A removed, valid B kept)");
|
||||
|
||||
// 第三个请求应该复<EFBFBD><EFBFBD>?connectionB
|
||||
[self.client resetPoolStatistics];
|
||||
NSError *error3 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response3 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"ReuseB"
|
||||
timeout:15.0
|
||||
error:&error3];
|
||||
XCTAssertNotNil(response3);
|
||||
|
||||
// 验证:复用了 connectionB(没有创建新连接<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 0,
|
||||
@"Should not create new connection (reuse existing valid connection)");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 1,
|
||||
@"Should reuse the valid connection B");
|
||||
}
|
||||
|
||||
// S.2 In-Use 保护 - 使用中的连接不会过期
|
||||
- (void)testIdleTimeout_InUseProtection_ActiveConnectionNotPruned {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 创建第一个连<EFBFBD><EFBFBD>?
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"Initial"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1);
|
||||
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 借出连接并保<EFBFBD><EFBFBD>?inUse=YES
|
||||
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
|
||||
XCTAssertEqual(connections.count, 1);
|
||||
|
||||
HttpdnsNWReusableConnection *conn = connections.firstObject;
|
||||
|
||||
// 手动设置<EFBFBD><EFBFBD>?60 秒前(远<EFBFBD><EFBFBD>?30 秒超时)
|
||||
NSDate *veryOldDate = [NSDate dateWithTimeIntervalSinceNow:-60.0];
|
||||
[conn debugSetLastUsedDate:veryOldDate];
|
||||
|
||||
// 将连接标记为使用<EFBFBD><EFBFBD>?
|
||||
[conn debugSetInUse:YES];
|
||||
|
||||
// 触发清理(通过发起另一个并发请求)
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
__block NSInteger connectionsBefore = 0;
|
||||
__block NSInteger connectionsAfter = 0;
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
connectionsBefore = [self.client totalConnectionCount];
|
||||
|
||||
// 发起请求(会触发 pruneConnectionPool<EFBFBD><EFBFBD>?
|
||||
NSError *error2 = nil;
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"TriggerPrune"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
connectionsAfter = [self.client totalConnectionCount];
|
||||
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
});
|
||||
|
||||
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_SEC));
|
||||
|
||||
// 清理:重<EFBFBD><EFBFBD>?inUse 状<EFBFBD><EFBFBD>?
|
||||
[conn debugSetInUse:NO];
|
||||
|
||||
// 验证:inUse=YES 的连接不应该被清<EFBFBD><EFBFBD>?
|
||||
// connectionsBefore = 1 (旧连<EFBFBD><EFBFBD>?, connectionsAfter = 2 (旧连<EFBFBD><EFBFBD>?+ 新连<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(connectionsBefore, 1,
|
||||
@"Should have 1 connection before (in-use protected)");
|
||||
XCTAssertEqual(connectionsAfter, 2,
|
||||
@"Should have 2 connections after (in-use connection NOT pruned, new connection added)");
|
||||
}
|
||||
|
||||
// S.3 所有连接过<EFBFBD><EFBFBD>?- 批量清理
|
||||
- (void)testIdleTimeout_AllExpired_BulkPruning {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 创建 4 个连接(填满池)
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
for (NSInteger i = 0; i < 4; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"FillPool"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertNotNil(response);
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:30.0];
|
||||
|
||||
// 等待所有连接归<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 验证池已<EFBFBD><EFBFBD>?
|
||||
NSUInteger poolSizeBefore = [self.client connectionPoolCountForKey:poolKey];
|
||||
XCTAssertGreaterThan(poolSizeBefore, 0, @"Pool should have connections");
|
||||
|
||||
// 将所有连接设置为过期<EFBFBD><EFBFBD>?1 秒前<EFBFBD><EFBFBD>?
|
||||
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
|
||||
NSDate *expiredDate = [NSDate dateWithTimeIntervalSinceNow:-31.0];
|
||||
for (HttpdnsNWReusableConnection *conn in connections) {
|
||||
[conn debugSetLastUsedDate:expiredDate];
|
||||
}
|
||||
|
||||
// 发起新请求(触发批量清理<EFBFBD><EFBFBD>?
|
||||
NSError *errorNew = nil;
|
||||
HttpdnsNWHTTPClientResponse *responseNew = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"AfterBulkExpiry"
|
||||
timeout:15.0
|
||||
error:&errorNew];
|
||||
XCTAssertNotNil(responseNew, @"Request should succeed after bulk pruning");
|
||||
XCTAssertEqual(responseNew.statusCode, 200);
|
||||
|
||||
// 等待归还
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证:池中只有新连接(所有旧连接被清理)
|
||||
NSUInteger poolSizeAfter = [self.client connectionPoolCountForKey:poolKey];
|
||||
XCTAssertEqual(poolSizeAfter, 1,
|
||||
@"Pool should have only 1 connection (new one after bulk pruning)");
|
||||
}
|
||||
|
||||
// S.4 过期后池状态验<EFBFBD><EFBFBD>?
|
||||
- (void)testIdleTimeout_PoolStateAfterExpiry_DirectVerification {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 创建连接
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"CreateConnection"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1);
|
||||
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证连接在池<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
|
||||
@"Pool should have 1 connection");
|
||||
|
||||
// 设置连接为过<EFBFBD><EFBFBD>?
|
||||
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
|
||||
HttpdnsNWReusableConnection *conn = connections.firstObject;
|
||||
[conn debugSetLastUsedDate:[NSDate dateWithTimeIntervalSinceNow:-31.0]];
|
||||
|
||||
// 发起请求(触发清理)
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"TriggerPrune"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2);
|
||||
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 直接验证池状态:过期连接已被移除,新连接已加<EFBFBD><EFBFBD>?
|
||||
NSUInteger poolSizeAfter = [self.client connectionPoolCountForKey:poolKey];
|
||||
XCTAssertEqual(poolSizeAfter, 1,
|
||||
@"Pool should have 1 connection (expired removed, new added)");
|
||||
|
||||
// 验证统计:创建了新连接(旧连接过期不可复用)
|
||||
XCTAssertGreaterThanOrEqual(self.client.connectionCreationCount, 1,
|
||||
@"Should have created at least 1 new connection");
|
||||
}
|
||||
|
||||
// S.5 快速过期测试(无需等待<EFBFBD><EFBFBD>? 演示最佳实<EFBFBD><EFBFBD>?
|
||||
- (void)testIdleTimeout_FastExpiry_NoWaiting {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
|
||||
|
||||
// 第一个请求:创建连接
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"FastTest1"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1);
|
||||
XCTAssertEqual(response1.statusCode, 200);
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 1, @"Should create 1 connection");
|
||||
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 使用 DEBUG 辅助函数模拟 31 秒过期(无需实际等待<EFBFBD><EFBFBD>?
|
||||
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
|
||||
XCTAssertEqual(connections.count, 1);
|
||||
|
||||
HttpdnsNWReusableConnection *conn = connections.firstObject;
|
||||
NSDate *expiredDate = [NSDate dateWithTimeIntervalSinceNow:-31.0];
|
||||
[conn debugSetLastUsedDate:expiredDate];
|
||||
|
||||
// 第二个请求:应该检测到过期并创建新连接
|
||||
[self.client resetPoolStatistics];
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"FastTest2"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2);
|
||||
XCTAssertEqual(response2.statusCode, 200);
|
||||
|
||||
// 验证:创建了新连接(而非复用过期的)
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 1,
|
||||
@"Should create new connection (expired connection not reused)");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 0,
|
||||
@"Should not reuse expired connection");
|
||||
|
||||
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - startTime;
|
||||
|
||||
// 关键验证:测试应该很快完成(< 5 秒),而非等待 30+ <EFBFBD><EFBFBD>?
|
||||
XCTAssertLessThan(elapsed, 5.0,
|
||||
@"Fast expiry test should complete quickly (%.1fs) without 30s wait", elapsed);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,591 @@
|
||||
//
|
||||
// HttpdnsNWHTTPClient_StateMachineTests.m
|
||||
// TrustHttpDNSTests
|
||||
//
|
||||
// @author Created by Claude Code on 2025-11-01
|
||||
// Copyright © 2025 trustapp.com. All rights reserved.
|
||||
//
|
||||
// 状态机测试 - 包含状态机与异常场<EFBFBD><EFBFBD>?(Q) 测试<EFBFBD><EFBFBD>?
|
||||
// 测试总数<EFBFBD><EFBFBD>?7 个(Q:17<EFBFBD><EFBFBD>?
|
||||
//
|
||||
|
||||
#import "HttpdnsNWHTTPClientTestBase.h"
|
||||
|
||||
@interface HttpdnsNWHTTPClient_StateMachineTests : HttpdnsNWHTTPClientTestBase
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpdnsNWHTTPClient_StateMachineTests
|
||||
|
||||
#pragma mark - Q. 状态机与异常场景测<EFBFBD><EFBFBD>?
|
||||
|
||||
// Q1.1 池溢出时LRU移除策略验证
|
||||
- (void)testStateMachine_PoolOverflowLRU_RemovesOldestByLastUsedDate {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 需要并发创<EFBFBD><EFBFBD>?个连接(串行请求会复用)
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
// 并发发起5个请<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 5; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
// 使用 /delay/2 确保所有请求同时在飞行中,强制创建多个连接
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/2"
|
||||
userAgent:[NSString stringWithFormat:@"Request%ld", (long)i]
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
[expectation fulfill];
|
||||
});
|
||||
[NSThread sleepForTimeInterval:0.05]; // 小间隔避免完全同时启<EFBFBD><EFBFBD>?
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:20.0];
|
||||
|
||||
// 等待所有连接归<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 验证:池大小 <EFBFBD><EFBFBD>?4(LRU移除溢出部分<EFBFBD><EFBFBD>?
|
||||
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
|
||||
XCTAssertLessThanOrEqual(poolCount, 4,
|
||||
@"Pool should enforce max 4 connections (LRU)");
|
||||
|
||||
// 验证:创建了多个连接
|
||||
XCTAssertGreaterThanOrEqual(self.client.connectionCreationCount, 3,
|
||||
@"Should create multiple concurrent connections");
|
||||
}
|
||||
|
||||
// Q2.1 快速连续请求不产生重复连接(间接验证双重归还防护)
|
||||
- (void)testAbnormal_RapidSequentialRequests_NoDuplicates {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 快速连续发<EFBFBD><EFBFBD>?0个请求(测试连接归还的幂等性)
|
||||
for (NSInteger i = 0; i < 10; i++) {
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"RapidTest"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
XCTAssertNotNil(response);
|
||||
}
|
||||
|
||||
// 等待连接归还
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 验证:池中最<EFBFBD><EFBFBD>?个连接(因为串行请求复用同一连接<EFBFBD><EFBFBD>?
|
||||
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
|
||||
XCTAssertLessThanOrEqual(poolCount, 1,
|
||||
@"Pool should have at most 1 connection (rapid sequential reuse)");
|
||||
|
||||
// 验证:创建次数应该是1(所有请求复用同一连接<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 1,
|
||||
@"Should create only 1 connection for sequential requests");
|
||||
}
|
||||
|
||||
// Q2.2 不同端口请求不互相污染池
|
||||
- (void)testAbnormal_DifferentPorts_IsolatedPools {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey11080 = @"127.0.0.1:11080:tcp";
|
||||
NSString *poolKey11443 = @"127.0.0.1:11443:tls";
|
||||
|
||||
// 向端<EFBFBD><EFBFBD>?1080发起请求
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"Port11080"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1);
|
||||
|
||||
// 向端<EFBFBD><EFBFBD>?1443发起请求
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
|
||||
userAgent:@"Port11443"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2);
|
||||
|
||||
// 等待连接归还
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
|
||||
// 验证:两个池各自<EFBFBD><EFBFBD>?个连<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey11080], 1,
|
||||
@"Port 11080 pool should have 1 connection");
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey11443], 1,
|
||||
@"Port 11443 pool should have 1 connection");
|
||||
|
||||
// 验证:总共2个连接(池完全隔离)
|
||||
XCTAssertEqual([self.client totalConnectionCount], 2,
|
||||
@"Total should be 2 (one per pool)");
|
||||
}
|
||||
|
||||
// Q3.1 池大小不变式:任何时候池大小都不超过限制
|
||||
- (void)testInvariant_PoolSize_NeverExceedsLimit {
|
||||
[self.client resetPoolStatistics];
|
||||
|
||||
// 快速连续发<EFBFBD><EFBFBD>?0个请求到同一端点
|
||||
for (NSInteger i = 0; i < 20; i++) {
|
||||
NSError *error = nil;
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"InvariantTest"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
}
|
||||
|
||||
// 等待所有连接归<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:1.5];
|
||||
|
||||
// 验证:每个池的大小不超过4
|
||||
NSArray<NSString *> *allKeys = [self.client allConnectionPoolKeys];
|
||||
for (NSString *key in allKeys) {
|
||||
NSUInteger poolCount = [self.client connectionPoolCountForKey:key];
|
||||
XCTAssertLessThanOrEqual(poolCount, 4,
|
||||
@"Pool %@ size should never exceed 4 (actual: %lu)",
|
||||
key, (unsigned long)poolCount);
|
||||
}
|
||||
|
||||
// 验证:总连接数也不超过4(因为只有一个池<EFBFBD><EFBFBD>?
|
||||
XCTAssertLessThanOrEqual([self.client totalConnectionCount], 4,
|
||||
@"Total connections should not exceed 4");
|
||||
}
|
||||
|
||||
// Q3.3 无重复连接不变式:并发请求不产生重复
|
||||
- (void)testInvariant_NoDuplicates_ConcurrentRequests {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
// 并发发起15个请求(可能复用连接<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 15; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"ConcurrentTest"
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
[expectation fulfill];
|
||||
});
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:30.0];
|
||||
|
||||
// 等待连接归还
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 验证:池大小 <EFBFBD><EFBFBD>?4(不变式<EFBFBD><EFBFBD>?
|
||||
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
|
||||
XCTAssertLessThanOrEqual(poolCount, 4,
|
||||
@"Pool should not have duplicates (max 4 connections)");
|
||||
|
||||
// 验证:创建的连接数合理(<EFBFBD><EFBFBD>?5,因为可能有复用<EFBFBD><EFBFBD>?
|
||||
XCTAssertLessThanOrEqual(self.client.connectionCreationCount, 15,
|
||||
@"Should not create excessive connections");
|
||||
}
|
||||
|
||||
// Q4.1 边界条件:恰<EFBFBD><EFBFBD>?0秒后连接过期
|
||||
- (void)testBoundary_Exactly30Seconds_ConnectionExpired {
|
||||
if (getenv("SKIP_SLOW_TESTS")) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self.client resetPoolStatistics];
|
||||
|
||||
// 第一个请<EFBFBD><EFBFBD>?
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"InitialRequest"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1);
|
||||
|
||||
// 等待恰好30.5秒(超过30秒过期时间)
|
||||
[NSThread sleepForTimeInterval:30.5];
|
||||
|
||||
// 第二个请求:应该创建新连接(旧连接已过期<EFBFBD><EFBFBD>?
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"AfterExpiry"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2);
|
||||
|
||||
// 验证:创建了2个连接(旧连接过期,无法复用<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 2,
|
||||
@"Should create 2 connections (first expired after 30s)");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 0,
|
||||
@"Should not reuse expired connection");
|
||||
}
|
||||
|
||||
// Q4.2 边界条件<EFBFBD><EFBFBD>?9秒内连接未过<EFBFBD><EFBFBD>?
|
||||
- (void)testBoundary_Under30Seconds_ConnectionNotExpired {
|
||||
[self.client resetPoolStatistics];
|
||||
|
||||
// 第一个请<EFBFBD><EFBFBD>?
|
||||
NSError *error1 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"InitialRequest"
|
||||
timeout:15.0
|
||||
error:&error1];
|
||||
XCTAssertNotNil(response1);
|
||||
|
||||
// 等待29秒(未到30秒过期时间)
|
||||
[NSThread sleepForTimeInterval:29.0];
|
||||
|
||||
// 第二个请求:应该复用连接
|
||||
NSError *error2 = nil;
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"BeforeExpiry"
|
||||
timeout:15.0
|
||||
error:&error2];
|
||||
XCTAssertNotNil(response2);
|
||||
|
||||
// 验证:只创建<EFBFBD><EFBFBD>?个连接(复用了)
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 1,
|
||||
@"Should create only 1 connection (reused within 30s)");
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 1,
|
||||
@"Should reuse connection within 30s");
|
||||
}
|
||||
|
||||
// Q4.3 边界条件:恰<EFBFBD><EFBFBD>?个连接全部保<EFBFBD><EFBFBD>?
|
||||
- (void)testBoundary_ExactlyFourConnections_AllKept {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 并发发起4个请求(使用延迟确保同时在飞行中,创<EFBFBD><EFBFBD>?个独立连接)
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
for (NSInteger i = 0; i < 4; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:
|
||||
[NSString stringWithFormat:@"Request %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
NSError *error = nil;
|
||||
// 使用 /delay/2 确保所有请求同时在飞行<EFBFBD><EFBFBD>?
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/2"
|
||||
userAgent:[NSString stringWithFormat:@"Request%ld", (long)i]
|
||||
timeout:15.0
|
||||
error:&error];
|
||||
[expectation fulfill];
|
||||
});
|
||||
|
||||
[NSThread sleepForTimeInterval:0.05]; // 小间隔避免完全同时启<EFBFBD><EFBFBD>?
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:20.0];
|
||||
|
||||
// 等待连接归还
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 验证:池恰好<EFBFBD><EFBFBD>?个连接(全部保留<EFBFBD><EFBFBD>?
|
||||
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
|
||||
XCTAssertEqual(poolCount, 4,
|
||||
@"Pool should keep all 4 connections (not exceeding limit)");
|
||||
|
||||
// 验证:恰好创<EFBFBD><EFBFBD>?个连<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(self.client.connectionCreationCount, 4,
|
||||
@"Should create exactly 4 connections");
|
||||
}
|
||||
|
||||
// Q1.2 正常状态序列验<EFBFBD><EFBFBD>?
|
||||
- (void)testStateMachine_NormalSequence_StateTransitionsCorrect {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// <EFBFBD><EFBFBD>?步:创建并使用连<EFBFBD><EFBFBD>?(CREATING <EFBFBD><EFBFBD>?IN_USE <EFBFBD><EFBFBD>?IDLE)
|
||||
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"StateTest"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
XCTAssertNotNil(response1, @"First request should succeed");
|
||||
|
||||
[NSThread sleepForTimeInterval:1.0]; // 等待归还
|
||||
|
||||
// 验证:池中有1个连<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
|
||||
@"Connection should be in pool");
|
||||
|
||||
// <EFBFBD><EFBFBD>?步:复用连接 (IDLE <EFBFBD><EFBFBD>?IN_USE <EFBFBD><EFBFBD>?IDLE)
|
||||
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"StateTest"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
XCTAssertNotNil(response2, @"Second request should reuse connection");
|
||||
|
||||
// 验证:复用计数增<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual(self.client.connectionReuseCount, 1,
|
||||
@"Should have reused connection once");
|
||||
}
|
||||
|
||||
// Q1.3 inUse 标志维护验证
|
||||
- (void)testStateMachine_InUseFlag_CorrectlyMaintained {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 发起请求并归<EFBFBD><EFBFBD>?
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"InUseTest"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
|
||||
[NSThread sleepForTimeInterval:1.0]; // 等待归还
|
||||
|
||||
// 获取池中连接
|
||||
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
|
||||
XCTAssertEqual(connections.count, 1, @"Should have 1 connection in pool");
|
||||
|
||||
// 验证:池中连接的 inUse 应为 NO
|
||||
for (HttpdnsNWReusableConnection *conn in connections) {
|
||||
XCTAssertFalse(conn.inUse, @"Connection in pool should not be marked as inUse");
|
||||
}
|
||||
}
|
||||
|
||||
// Q2.3 Nil lastUsedDate 处理验证
|
||||
- (void)testAbnormal_NilLastUsedDate_HandledGracefully {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 发起请求创建连接
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"NilDateTest"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 获取连接并设<EFBFBD><EFBFBD>?lastUsedDate <EFBFBD><EFBFBD>?nil
|
||||
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
|
||||
XCTAssertEqual(connections.count, 1, @"Should have connection");
|
||||
|
||||
HttpdnsNWReusableConnection *conn = connections.firstObject;
|
||||
[conn debugSetLastUsedDate:nil];
|
||||
|
||||
// 发起新请求触<EFBFBD><EFBFBD>?prune(内部应使用 distantPast 处理 nil<EFBFBD><EFBFBD>?
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"NilDateTest"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
|
||||
// 验证:不崩溃,正常工<EFBFBD><EFBFBD>?
|
||||
XCTAssertNotNil(response, @"Should handle nil lastUsedDate gracefully");
|
||||
}
|
||||
|
||||
// Q3.2 池中无失效连接不变式
|
||||
- (void)testInvariant_NoInvalidatedInPool {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 发起多个请求(包括成功和超时<EFBFBD><EFBFBD>?
|
||||
for (NSInteger i = 0; i < 3; i++) {
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"InvariantTest"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
}
|
||||
|
||||
// 发起1个超时请<EFBFBD><EFBFBD>?
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
|
||||
userAgent:@"TimeoutTest"
|
||||
timeout:0.5
|
||||
error:nil];
|
||||
|
||||
[NSThread sleepForTimeInterval:2.0];
|
||||
|
||||
// 获取池中所有连<EFBFBD><EFBFBD>?
|
||||
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
|
||||
|
||||
// 验证:池中无失效连接
|
||||
for (HttpdnsNWReusableConnection *conn in connections) {
|
||||
XCTAssertFalse(conn.isInvalidated, @"Pool should not contain invalidated connections");
|
||||
}
|
||||
}
|
||||
|
||||
// Q3.4 lastUsedDate 单调性验<EFBFBD><EFBFBD>?
|
||||
- (void)testInvariant_LastUsedDate_Monotonic {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// <EFBFBD><EFBFBD>?次使<EFBFBD><EFBFBD>?
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"MonotonicTest"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
NSArray<HttpdnsNWReusableConnection *> *connections1 = [self.client connectionsInPoolForKey:poolKey];
|
||||
XCTAssertEqual(connections1.count, 1, @"Should have connection");
|
||||
NSDate *date1 = connections1.firstObject.lastUsedDate;
|
||||
XCTAssertNotNil(date1, @"lastUsedDate should be set");
|
||||
|
||||
// 等待1秒确保时间推<EFBFBD><EFBFBD>?
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// <EFBFBD><EFBFBD>?次使用同一连接
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"MonotonicTest"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
NSArray<HttpdnsNWReusableConnection *> *connections2 = [self.client connectionsInPoolForKey:poolKey];
|
||||
XCTAssertEqual(connections2.count, 1, @"Should still have 1 connection");
|
||||
NSDate *date2 = connections2.firstObject.lastUsedDate;
|
||||
|
||||
// 验证:lastUsedDate 递增
|
||||
XCTAssertTrue([date2 timeIntervalSinceDate:date1] > 0,
|
||||
@"lastUsedDate should increase after reuse");
|
||||
}
|
||||
|
||||
// Q5.1 超时+池溢出复合场<EFBFBD><EFBFBD>?
|
||||
- (void)testCompound_TimeoutDuringPoolOverflow_Handled {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 先填满池<EFBFBD><EFBFBD>?个成功连接)
|
||||
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
for (NSInteger i = 0; i < 4; i++) {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:
|
||||
[NSString stringWithFormat:@"Fill pool %ld", (long)i]];
|
||||
[expectations addObject:expectation];
|
||||
|
||||
dispatch_async(queue, ^{
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/2"
|
||||
userAgent:@"CompoundTest"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
[expectation fulfill];
|
||||
});
|
||||
[NSThread sleepForTimeInterval:0.05];
|
||||
}
|
||||
|
||||
[self waitForExpectations:expectations timeout:20.0];
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
NSUInteger poolCountBefore = [self.client connectionPoolCountForKey:poolKey];
|
||||
XCTAssertLessThanOrEqual(poolCountBefore, 4, @"Pool should have <20><>? connections");
|
||||
|
||||
// <EFBFBD><EFBFBD>?个请求超<EFBFBD><EFBFBD>?
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
|
||||
userAgent:@"TimeoutRequest"
|
||||
timeout:0.5
|
||||
error:&error];
|
||||
|
||||
XCTAssertNil(response, @"Timeout request should return nil");
|
||||
XCTAssertNotNil(error, @"Should have error");
|
||||
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 验证:超时连接未加入<EFBFBD><EFBFBD>?
|
||||
NSUInteger poolCountAfter = [self.client connectionPoolCountForKey:poolKey];
|
||||
XCTAssertLessThanOrEqual(poolCountAfter, 4, @"Timed-out connection should not be added to pool");
|
||||
}
|
||||
|
||||
// Q2.4 打开失败不加入池
|
||||
- (void)testAbnormal_OpenFailure_NotAddedToPool {
|
||||
[self.client resetPoolStatistics];
|
||||
|
||||
// 尝试连接无效端口(连接拒绝)
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:99999/get"
|
||||
userAgent:@"FailureTest"
|
||||
timeout:2.0
|
||||
error:&error];
|
||||
|
||||
// 验证:请求失<EFBFBD><EFBFBD>?
|
||||
XCTAssertNil(response, @"Should fail to connect to invalid port");
|
||||
|
||||
// 验证:无连接加入<EFBFBD><EFBFBD>?
|
||||
XCTAssertEqual([self.client totalConnectionCount], 0,
|
||||
@"Failed connection should not be added to pool");
|
||||
}
|
||||
|
||||
// Q2.5 多次 invalidate 幂等<EFBFBD><EFBFBD>?
|
||||
- (void)testAbnormal_MultipleInvalidate_Idempotent {
|
||||
[self.client resetPoolStatistics];
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
|
||||
// 创建连接
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"InvalidateTest"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
|
||||
XCTAssertEqual(connections.count, 1, @"Should have connection");
|
||||
|
||||
HttpdnsNWReusableConnection *conn = connections.firstObject;
|
||||
|
||||
// 多次 invalidate
|
||||
[conn debugInvalidate];
|
||||
[conn debugInvalidate];
|
||||
[conn debugInvalidate];
|
||||
|
||||
// 验证:不崩溃
|
||||
XCTAssertTrue(conn.isInvalidated, @"Connection should be invalidated");
|
||||
}
|
||||
|
||||
// Q5.2 并发 dequeue 竞态测<EFBFBD><EFBFBD>?
|
||||
- (void)testCompound_ConcurrentDequeueDuringPrune_Safe {
|
||||
[self.client resetPoolStatistics];
|
||||
|
||||
// 在两个端口创建连<EFBFBD><EFBFBD>?
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"RaceTest"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11443/get"
|
||||
userAgent:@"RaceTest"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
|
||||
[NSThread sleepForTimeInterval:1.0];
|
||||
|
||||
// 等待30秒让连接过期
|
||||
[NSThread sleepForTimeInterval:30.5];
|
||||
|
||||
// 并发触发两个端口<EFBFBD><EFBFBD>?dequeue(会触发 prune<EFBFBD><EFBFBD>?
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
|
||||
dispatch_group_async(group, queue, ^{
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
|
||||
userAgent:@"Race1"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
});
|
||||
|
||||
dispatch_group_async(group, queue, ^{
|
||||
[self.client performRequestWithURLString:@"http://127.0.0.1:11443/get"
|
||||
userAgent:@"Race2"
|
||||
timeout:15.0
|
||||
error:nil];
|
||||
});
|
||||
|
||||
// 等待完成
|
||||
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_SEC));
|
||||
|
||||
// 验证:无崩溃,连接池正常工作
|
||||
NSUInteger totalCount = [self.client totalConnectionCount];
|
||||
XCTAssertLessThanOrEqual(totalCount, 4, @"Pool should remain stable after concurrent prune");
|
||||
}
|
||||
|
||||
@end
|
||||
253
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Network/MOCK_SERVER.md
Normal file
253
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Network/MOCK_SERVER.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# HTTP Mock Server for Integration Tests
|
||||
|
||||
本目录包含用<EFBFBD><EFBFBD>?`HttpdnsNWHTTPClient` 集成测试<E6B58B><E8AF95>?HTTP/HTTPS mock server,用于替<E4BA8E><E69BBF>?httpbin.org<72><67>?
|
||||
|
||||
---
|
||||
|
||||
## 为什么需<E4B988><E99C80>?Mock Server<65><72>?
|
||||
|
||||
1. **可靠<E58FAF><E99DA0>?*: httpbin.org 在高并发测试下表现不稳定,经常返回非预期<E9A284><E69C9F>?HTTP 状态码(如 429 Too Many Requests<74><73>?
|
||||
2. **速度**: 本地服务器响应更快,缩短测试执行时间
|
||||
3. **离线测试**: 无需网络连接即可运行集成测试
|
||||
4. **可控<E58FAF><E68EA7>?*: 完全掌控测试环境,便于调试和复现问题
|
||||
|
||||
---
|
||||
|
||||
## 快速开<E9809F><E5BC80>?
|
||||
|
||||
### 1. 启动 Mock Server
|
||||
|
||||
```bash
|
||||
# 进入测试目录
|
||||
cd TrustHttpDNSTests/Network
|
||||
|
||||
# 启动服务器(无需 sudo 权限,使用非特权端口<E7ABAF><E58FA3>?
|
||||
python3 mock_server.py
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- **无需 root 权限**(使用非特权端口 11080/11443-11446<34><36>?
|
||||
- 首次运行会自动生成自签名证书 (`server.pem`)
|
||||
- <20><>?`Ctrl+C` 停止服务<E69C8D><E58AA1>?
|
||||
|
||||
### 2. 运行集成测试
|
||||
|
||||
在另一个终端窗<EFBFBD><EFBFBD>?
|
||||
|
||||
```bash
|
||||
cd ~/Project/iOS/Trust-ios-sdk-httpdns
|
||||
|
||||
# 运行所有集成测<E68890><E6B58B>?
|
||||
xcodebuild test \
|
||||
-workspace TrustHttpDNS.xcworkspace \
|
||||
-scheme TrustHttpDNSTests \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 15' \
|
||||
-only-testing:TrustHttpDNSTests/HttpdnsNWHTTPClientIntegrationTests
|
||||
|
||||
# 运行单个测试
|
||||
xcodebuild test \
|
||||
-workspace TrustHttpDNS.xcworkspace \
|
||||
-scheme TrustHttpDNSTests \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 15' \
|
||||
-only-testing:TrustHttpDNSTests/HttpdnsNWHTTPClientIntegrationTests/testConcurrency_ParallelRequestsSameHost_AllSucceed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 支持<E694AF><E68C81>?Endpoints
|
||||
|
||||
Mock server 实现了以<E4BA86><E4BBA5>?httpbin.org 兼容<E585BC><E5AEB9>?endpoints:
|
||||
|
||||
| Endpoint | 功能 | 示例 |
|
||||
|----------|------|------|
|
||||
| `GET /get` | 返回请求信息(headers, args, origin<69><6E>?| `http://127.0.0.1:11080/get` |
|
||||
| `GET /status/{code}` | 返回指定状态码<E68081><E7A081>?00-599<39><39>?| `http://127.0.0.1:11080/status/404` |
|
||||
| `GET /stream-bytes/{n}` | 返回 chunked 编码<E7BC96><E7A081>?N 字节数据 | `http://127.0.0.1:11080/stream-bytes/1024` |
|
||||
| `GET /delay/{seconds}` | 延迟指定秒数后返回(最<EFBC88><E69C80>?0秒) | `http://127.0.0.1:11080/delay/5` |
|
||||
| `GET /headers` | 返回所有请求头<E6B182><E5A4B4>?| `http://127.0.0.1:11080/headers` |
|
||||
| `GET /uuid` | 返回随机 UUID | `http://127.0.0.1:11080/uuid` |
|
||||
| `GET /user-agent` | 返回 User-Agent 头部 | `http://127.0.0.1:11080/user-agent` |
|
||||
|
||||
**端口配置**:
|
||||
- **HTTP**: `127.0.0.1:11080`
|
||||
- **HTTPS**: `127.0.0.1:11443`, `11444`, `11445`, `11446`<EFBFBD><EFBFBD>?个端口用于测试连接池隔离<E99A94><E7A6BB>?
|
||||
|
||||
所<EFBFBD><EFBFBD>?endpoints <20><>?HTTP <20><>?HTTPS 端口上均可访问<E8AEBF><E997AE>?
|
||||
|
||||
---
|
||||
|
||||
## 实现细节
|
||||
|
||||
### 架构
|
||||
|
||||
- **HTTP 服务<E69C8D><E58AA1>?*: 监听 `127.0.0.1:11080`
|
||||
- **HTTPS 服务<E69C8D><E58AA1>?*: 监听 `127.0.0.1:11443`, `11444`, `11445`, `11446`<EFBFBD><EFBFBD>?个端口,使用自签名证书)
|
||||
- **多端口目<E58FA3><E79BAE>?*: 测试连接池的端口隔离机制,确保不同端口使用独立的连接<E8BF9E><E68EA5>?
|
||||
- **并发模型**: 多线程(`ThreadingMixIn`),支持高并发请<EFBFBD><EFBFBD>?
|
||||
|
||||
### TLS 证书
|
||||
|
||||
- 自动生成自签名证书(RSA 2048位,有效<E69C89><E69588>?365 天)
|
||||
- CN (Common Name): `localhost`
|
||||
- 证书文件: `server.pem`(同时包含密钥和证书<EFBFBD><EFBFBD>?
|
||||
|
||||
**重要**: 集成测试通过环境变量 `HTTPDNS_SKIP_TLS_VERIFY=1` 跳过 TLS 验证,这是安全的,因为:
|
||||
1. 仅在测试环境生效
|
||||
2. 不影响生产代<E4BAA7><E4BBA3>?
|
||||
3. 连接限制为本<E4B8BA><E69CAC>?loopback (127.0.0.1)
|
||||
|
||||
### 响应格式
|
||||
|
||||
所<EFBFBD><EFBFBD>?JSON 响应遵循 httpbin.org 格式,例<EFBC8C><E4BE8B>?
|
||||
|
||||
```json
|
||||
{
|
||||
"args": {},
|
||||
"headers": {
|
||||
"Host": "127.0.0.1",
|
||||
"User-Agent": "HttpdnsNWHTTPClient/1.0"
|
||||
},
|
||||
"origin": "127.0.0.1",
|
||||
"url": "GET /get"
|
||||
}
|
||||
```
|
||||
|
||||
Chunked 编码响应示例 (`/stream-bytes/10`):
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
a
|
||||
XXXXXXXXXX
|
||||
0
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 端口已被占用
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
<EFBFBD><EFBFBD>?端口 80 已被占用,请关闭占用端口的进程或使用其他端口
|
||||
```
|
||||
|
||||
**解决方法**:
|
||||
|
||||
1. 查找占用进程:
|
||||
```bash
|
||||
sudo lsof -i :80
|
||||
sudo lsof -i :443
|
||||
```
|
||||
|
||||
2. 终止占用进程:
|
||||
```bash
|
||||
sudo kill -9 <PID>
|
||||
```
|
||||
|
||||
3. 或修<E68896><E4BFAE>?mock_server.py 使用其他端口:
|
||||
```python
|
||||
# 修改端口号(同时需要更新测试代码中<E7A081><E4B8AD>?URL<52><4C>?
|
||||
run_http_server(port=8080)
|
||||
run_https_server(port=8443)
|
||||
```
|
||||
|
||||
### 缺少 OpenSSL
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
<EFBFBD><EFBFBD>?未找<E69CAA><E689BE>?openssl 命令,请安装 OpenSSL
|
||||
```
|
||||
|
||||
**解决方法**:
|
||||
|
||||
```bash
|
||||
# macOS (通常已预<E5B7B2><E9A284>?
|
||||
brew install openssl
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install openssl
|
||||
|
||||
# CentOS/RHEL
|
||||
sudo yum install openssl
|
||||
```
|
||||
|
||||
### 权限被拒<E8A2AB><E68B92>?
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
<EFBFBD><EFBFBD>?错误: 需<><E99C80>?root 权限以绑<E4BBA5><E7BB91>?80/443 端口
|
||||
```
|
||||
|
||||
**解决方法**:
|
||||
|
||||
必须使用 `sudo` 运行:
|
||||
```bash
|
||||
sudo python3 mock_server.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 切换<E58887><E68DA2>?httpbin.org
|
||||
|
||||
如需使用真实<EFBFBD><EFBFBD>?httpbin.org 进行测试(例如验证兼容性):
|
||||
|
||||
1. 编辑 `HttpdnsNWHTTPClientIntegrationTests.m`
|
||||
2. 将所<E5B086><E68980>?`127.0.0.1` 替换<E69BBF><E68DA2>?`httpbin.org`
|
||||
3. 注释<E6B3A8><E9878A>?setUp/tearDown 中的环境变量设置
|
||||
|
||||
---
|
||||
|
||||
## 开发与扩展
|
||||
|
||||
### 添加<E6B7BB><E58AA0>?Endpoint
|
||||
|
||||
<EFBFBD><EFBFBD>?`mock_server.py` <20><>?`MockHTTPHandler.do_GET()` 方法中添<E4B8AD><E6B7BB>?
|
||||
|
||||
```python
|
||||
def do_GET(self):
|
||||
path = urlparse(self.path).path
|
||||
|
||||
if path == '/your-new-endpoint':
|
||||
self._handle_your_endpoint()
|
||||
# ... 其他 endpoints
|
||||
|
||||
def _handle_your_endpoint(self):
|
||||
"""处理自定<E887AA><E5AE9A>?endpoint"""
|
||||
data = {'custom': 'data'}
|
||||
self._send_json(200, data)
|
||||
```
|
||||
|
||||
### 调试模式
|
||||
|
||||
取消注释 `log_message` 方法以启用详细日<E7BB86><E697A5>?
|
||||
|
||||
```python
|
||||
def log_message(self, format, *args):
|
||||
print(f"[{self.address_string()}] {format % args}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Python 3.7+** (标准库,无需额外依赖)
|
||||
- **http.server**: HTTP 服务器实<E599A8><E5AE9E>?
|
||||
- **ssl**: TLS/SSL 支持
|
||||
- **socketserver.ThreadingMixIn**: 多线程并<E7A88B><E5B9B6>?
|
||||
|
||||
---
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
1. **仅用于测<E4BA8E><E6B58B>?*: 此服务器设计用于本地测试,不适合生产环境
|
||||
2. **自签名证<E5908D><E8AF81>?*: HTTPS 使用不受信任的自签名证书
|
||||
3. **无身份验<E4BBBD><E9AA8C>?*: 不实现任何身份验证机<E8AF81><E69CBA>?
|
||||
4. **本地绑定**: 服务器仅绑定<E7BB91><E5AE9A>?`127.0.0.1`,不接受外部连接
|
||||
|
||||
---
|
||||
|
||||
**最后更<E5908E><E69BB4>?*: 2025-11-01
|
||||
**维护<E7BBB4><E68AA4>?*: Claude Code
|
||||
282
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Network/README.md
Normal file
282
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Network/README.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# HttpdnsNWHTTPClient 测试套件
|
||||
|
||||
本目录包<EFBFBD><EFBFBD>?`HttpdnsNWHTTPClient` <20><>?`HttpdnsNWReusableConnection` 的完整测试套件<E5A597><E4BBB6>?
|
||||
|
||||
## 测试文件结构
|
||||
|
||||
```
|
||||
TrustHttpDNSTests/Network/
|
||||
├── HttpdnsNWHTTPClientTests.m # 主单元测试(44个)
|
||||
├── HttpdnsNWHTTPClientIntegrationTests.m # 集成测试<E6B58B><E8AF95>?个)
|
||||
├── HttpdnsNWHTTPClientTestHelper.h/m # 测试辅助工具<E5B7A5><E585B7>?
|
||||
└── README.md # 本文<E69CAC><E69687>?
|
||||
```
|
||||
|
||||
## 测试覆盖范围
|
||||
|
||||
### 单元测试 (HttpdnsNWHTTPClientTests.m)
|
||||
|
||||
#### A. HTTP 解析逻辑测试 (25<32><35>?
|
||||
- **A1. Header 解析 (9<><39>?**
|
||||
- 正常响应解析
|
||||
- 多个头部字段
|
||||
- 不完整数据处<E68DAE><E5A484>?
|
||||
- 无效状态行
|
||||
- 空格处理<E5A484><E79086>?trim
|
||||
- 空值头<E580BC><E5A4B4>?
|
||||
- 非数字状态码
|
||||
- 状态码为零
|
||||
- 无效头部<E5A4B4><E983A8>?
|
||||
|
||||
- **A2. Chunked 编码检<E7A081><E6A380>?(8<><38>?**
|
||||
- 单个 chunk
|
||||
- 多个 chunks
|
||||
- 不完<E4B88D><E5AE8C>?chunk
|
||||
- Chunk extension 支持
|
||||
- 无效十六进制 size
|
||||
- Chunk size 溢出
|
||||
- 缺少 CRLF 终止<E7BB88><E6ADA2>?
|
||||
- <20><>?trailers <20><>?chunked
|
||||
|
||||
- **A3. Chunked 解码 (2<><32>?**
|
||||
- 多个 chunks 正确解码
|
||||
- 无效格式返回 nil
|
||||
|
||||
- **A4. 完整响应解析 (6<><36>?**
|
||||
- Content-Length 响应
|
||||
- Chunked 编码响应
|
||||
- <20><>?body
|
||||
- Content-Length 不匹<E4B88D><E58CB9>?
|
||||
- 空数据错<E68DAE><E99499>?
|
||||
- 只有 headers <20><>?body
|
||||
|
||||
#### C. 请求构建测试 (7<><37>?
|
||||
- 基本 GET 请求格式
|
||||
- 查询参数处理
|
||||
- User-Agent 头部
|
||||
- HTTP 默认端口处理
|
||||
- HTTPS 默认端口处理
|
||||
- 非默认端口显<E58FA3><E698BE>?
|
||||
- 固定头部验证
|
||||
|
||||
#### E. TLS 验证测试 (4个占位符)
|
||||
- 有效证书返回 YES
|
||||
- Proceed 结果返回 YES
|
||||
- 无效证书返回 NO
|
||||
- 指定域名使用 SSL Policy
|
||||
|
||||
*注:TLS 测试需要真实的 SecTrustRef 或复<E68896><E5A48D>?mock,当前为占位<E58DA0><E4BD8D>?
|
||||
|
||||
#### F. 边缘情况测试 (8<><38>?
|
||||
- 超长 URL 处理
|
||||
- <20><>?User-Agent
|
||||
- 超大响应体(5MB<4D><42>?
|
||||
- Chunked 解码失败回退
|
||||
- 连接<E8BF9E><E68EA5>?key - 不同 hosts
|
||||
- 连接<E8BF9E><E68EA5>?key - 不同 ports
|
||||
- 连接<E8BF9E><E68EA5>?key - HTTP vs HTTPS
|
||||
|
||||
### 集成测试 (HttpdnsNWHTTPClientIntegrationTests.m)
|
||||
|
||||
使用 httpbin.org 进行真实网络测试 (22<32><32>?<3F><>?
|
||||
|
||||
**G. 基础集成测试 (7<><37>?**
|
||||
- HTTP GET 请求
|
||||
- HTTPS GET 请求
|
||||
- HTTP 404 响应
|
||||
- 连接复用(两次请求)
|
||||
- Chunked 响应处理
|
||||
- 请求超时测试
|
||||
- 自定义头部验<E983A8><E9AA8C>?
|
||||
|
||||
**H. 并发测试 (5<><35>?**
|
||||
- 并发请求同一主机<E4B8BB><E69CBA>?0个线程)
|
||||
- 并发请求不同路径<E8B7AF><E5BE84>?个不同endpoint<6E><74>?
|
||||
- 混合 HTTP + HTTPS 并发(各5个线程)
|
||||
- 高负载压力测试(50个并发请求)
|
||||
- 混合串行+并发模式
|
||||
|
||||
**I. 竞态条件测<E4BBB6><E6B58B>?(5<><35>?**
|
||||
- 连接池容量测试(超过4个连接上限)
|
||||
- 同时归还连接<E8BF9E><E68EA5>?个并发)
|
||||
- 获取-归还-再获取竞<E58F96><E7AB9E>?
|
||||
- 超时与活跃连接冲突(需31秒,可跳过)
|
||||
- 错误恢复后连接池健康状<E5BAB7><E78AB6>?
|
||||
|
||||
**J. 高级连接复用测试 (5<><35>?**
|
||||
- 连接过期与清理(31秒,可跳过)
|
||||
- 连接池容量限制验证(10个连续请求)
|
||||
- 不同路径复用连接<E8BF9E><E68EA5>?个不同路径)
|
||||
- HTTP vs HTTPS 使用不同连接池key
|
||||
- 长连接保持测试(20个请求间<E6B182><E997B4>?秒,可跳过)
|
||||
|
||||
## 运行测试
|
||||
|
||||
### 运行所有单元测<E58583><E6B58B>?
|
||||
```bash
|
||||
xcodebuild test \
|
||||
-workspace TrustHttpDNS.xcworkspace \
|
||||
-scheme TrustHttpDNSTests \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 15' \
|
||||
-only-testing:TrustHttpDNSTests/HttpdnsNWHTTPClientTests
|
||||
```
|
||||
|
||||
### 运行集成测试(需要网络)
|
||||
```bash
|
||||
xcodebuild test \
|
||||
-workspace TrustHttpDNS.xcworkspace \
|
||||
-scheme TrustHttpDNSTests \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 15' \
|
||||
-only-testing:TrustHttpDNSTests/HttpdnsNWHTTPClientIntegrationTests
|
||||
```
|
||||
|
||||
### 运行单个测试
|
||||
```bash
|
||||
xcodebuild test \
|
||||
-workspace TrustHttpDNS.xcworkspace \
|
||||
-scheme TrustHttpDNSTests \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 15' \
|
||||
-only-testing:TrustHttpDNSTests/HttpdnsNWHTTPClientTests/testParseHTTPHeaders_ValidResponse_Success
|
||||
```
|
||||
|
||||
## 测试辅助工具
|
||||
|
||||
### HttpdnsNWHTTPClientTestHelper
|
||||
|
||||
提供以下工具方法<EFBFBD><EFBFBD>?
|
||||
|
||||
#### HTTP 响应构<E5BA94><E69E84>?
|
||||
```objc
|
||||
// 构造标<E980A0><E6A087>?HTTP 响应
|
||||
+ (NSData *)createHTTPResponseWithStatus:(NSInteger)statusCode
|
||||
statusText:(NSString *)statusText
|
||||
headers:(NSDictionary *)headers
|
||||
body:(NSData *)body;
|
||||
|
||||
// 构<><E69E84>?chunked 响应
|
||||
+ (NSData *)createChunkedHTTPResponseWithStatus:(NSInteger)statusCode
|
||||
headers:(NSDictionary *)headers
|
||||
chunks:(NSArray<NSData *> *)chunks;
|
||||
```
|
||||
|
||||
#### Chunked 编码工具
|
||||
```objc
|
||||
+ (NSData *)encodeChunk:(NSData *)data;
|
||||
+ (NSData *)encodeLastChunk;
|
||||
```
|
||||
|
||||
#### 数据生成
|
||||
```objc
|
||||
+ (NSData *)randomDataWithSize:(NSUInteger)size;
|
||||
+ (NSData *)jsonBodyWithDictionary:(NSDictionary *)dictionary;
|
||||
```
|
||||
|
||||
## 测试统计
|
||||
|
||||
| 测试类别 | 测试数量 | 覆盖范围 |
|
||||
|---------|---------|---------|
|
||||
| HTTP 解析 | 25 | HTTP 头部、Chunked 编码、完整响<E695B4><E5938D>?|
|
||||
| 请求构建 | 7 | URL 处理、头部生<E983A8><E7949F>?|
|
||||
| TLS 验证 | 4 (占位<E58DA0><E4BD8D>? | 证书验证 |
|
||||
| 边缘情况 | 8 | 异常输入、连接池 key |
|
||||
| **单元测试合计** | **43** | - |
|
||||
| 基础集成测试 (G) | 7 | 真实网络请求、基本场<E69CAC><E59CBA>?|
|
||||
| 并发测试 (H) | 5 | 多线程并发、高负载 |
|
||||
| 竞态条件测<E4BBB6><E6B58B>?(I) | 5 | 连接池竞态、错误恢<E8AFAF><E681A2>?|
|
||||
| 连接复用测试 (J) | 5 | 连接过期、长连接、协议隔<E8AEAE><E99A94>?|
|
||||
| 多端口连接隔<E68EA5><E99A94>?(K) | 5 | 不同端口独立连接<E8BF9E><E68EA5>?|
|
||||
| 端口池耗尽测试 (L) | 3 | 多端口高负载、池隔离 |
|
||||
| 边界条件验证 (M) | 4 | 端口迁移、高端口数量 |
|
||||
| 并发多端口场<E58FA3><E59CBA>?(N) | 3 | 并行 keep-alive、轮询分<E8AFA2><E58886>?|
|
||||
| **集成测试合计** | **37** | - |
|
||||
| **总计** | **80** | - |
|
||||
|
||||
## 待实现测试(可选)
|
||||
|
||||
以下测试组涉及复杂的 Mock 场景,可根据需要添加:
|
||||
|
||||
### B. 连接池管理测<E79086><E6B58B>?(18<31><38>?
|
||||
需<EFBFBD><EFBFBD>?Mock `HttpdnsNWReusableConnection` 的完整生命周<E591BD><E591A8>?
|
||||
|
||||
### D. 完整流程测试 (13<31><33>?
|
||||
需<EFBFBD><EFBFBD>?Mock 连接池和网络层的集成场景
|
||||
|
||||
## Mock Server 使用
|
||||
|
||||
集成测试使用本地 mock server (127.0.0.1) 替代 httpbin.org,提供稳定可靠的测试环境<E78EAF><E5A283>?
|
||||
|
||||
### 启动 Mock Server
|
||||
|
||||
```bash
|
||||
cd TrustHttpDNSTests/Network
|
||||
python3 mock_server.py
|
||||
```
|
||||
|
||||
**注意**:使用非特权端口<EFBFBD><EFBFBD>?1080/11443-11446),无需 `sudo` 权限<E69D83><E99990>?
|
||||
|
||||
### 运行集成测试
|
||||
|
||||
在另一个终端窗口:
|
||||
|
||||
```bash
|
||||
xcodebuild test \
|
||||
-workspace TrustHttpDNS.xcworkspace \
|
||||
-scheme TrustHttpDNSTests \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 15' \
|
||||
-only-testing:TrustHttpDNSTests/HttpdnsNWHTTPClientIntegrationTests
|
||||
```
|
||||
|
||||
### Mock Server 特<><E789B9>?
|
||||
|
||||
- **HTTP**: 监听 `127.0.0.1:11080`
|
||||
- **HTTPS**: 监听 `127.0.0.1:11443`, `11444`, `11445`, `11446` (自签名证<E5908D><E8AF81>?
|
||||
- **多端口目<E58FA3><E79BAE>?*: 测试连接池的端口隔离机制
|
||||
- **并发支持**: 多线程处理,适合并发测试
|
||||
- **零延<E99BB6><E5BBB6>?*: 本地响应,测试速度<E9809F><E5BAA6>?
|
||||
|
||||
详见 [MOCK_SERVER.md](./MOCK_SERVER.md)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **集成测试依赖 Mock Server**:`HttpdnsNWHTTPClientIntegrationTests` 使用本地 mock server (127.0.0.1)。测试前需先启<E58588><E590AF>?`mock_server.py`<EFBFBD><EFBFBD>?
|
||||
|
||||
2. **慢测试跳<E8AF95><E8B7B3>?*:部分测试需要等<E8A681><E7AD89>?1秒(测试连接过期),可设置环境变<E5A283><E58F98>?`SKIP_SLOW_TESTS=1` 跳过这些测试<E6B58B><E8AF95>?
|
||||
- `testRaceCondition_ExpiredConnectionPruning_CreatesNewConnection`
|
||||
- `testConnectionReuse_Expiry31Seconds_NewConnectionCreated`
|
||||
- `testConnectionReuse_TwentyRequestsOneSecondApart_ConnectionKeptAlive`
|
||||
|
||||
3. **并发测试容错**:并发和压力测试允许部分失败(例<EFBFBD><EFBFBD>?H.4 要求80%成功率),因为高负载下仍可能出现网络波动<E6B3A2><E58AA8>?
|
||||
|
||||
4. **TLS 测试占位<E58DA0><E4BD8D>?*:E <20><>?TLS 测试需要真实的 `SecTrustRef` 或高<E68896><E9AB98>?mock 框架,当前仅为占位符<E4BD8D><E7ACA6>?
|
||||
|
||||
5. **新文件添加到 Xcode**:创建的测试文件需要手动添加到 `TrustHttpDNSTests` target<65><74>?
|
||||
|
||||
6. **测试数据**:使<EFBFBD><EFBFBD>?`HttpdnsNWHTTPClientTestHelper` 生成测试数据,确保测试的可重复性<E5A48D><E680A7>?
|
||||
|
||||
## 文件依赖
|
||||
|
||||
测试文件依赖以下源文件:
|
||||
- `HttpdnsNWHTTPClient.h/m` - 主要被测试类
|
||||
- `HttpdnsNWHTTPClient_Internal.h` - 内部方法暴露(测试专用)
|
||||
- `HttpdnsNWReusableConnection.h/m` - 连接管理
|
||||
- `HttpdnsNWHTTPClientResponse` - 响应模型
|
||||
|
||||
## 贡献指南
|
||||
|
||||
添加新测试时,请遵循<EFBFBD><EFBFBD>?
|
||||
1. 命名规范:`test<Component>_<Scenario>_<ExpectedResult>`
|
||||
2. 使用 `#pragma mark` 组织测试分组
|
||||
3. 添加清晰的注释说明测试目<E8AF95><E79BAE>?
|
||||
4. 验证测试覆盖率并更新本文<E69CAC><E69687>?
|
||||
|
||||
---
|
||||
|
||||
**最后更<E5908E><E69BB4>?*: 2025-11-01
|
||||
**测试框架**: XCTest + OCMock
|
||||
**维护<E7BBB4><E68AA4>?*: Claude Code
|
||||
|
||||
**更新日志**:
|
||||
- 2025-11-01: 新增 15 个多端口连接复用测试(K、L、M、N组),测试总数增至 37 <20><>?
|
||||
- 2025-11-01: Mock server 新增 3 <20><>?HTTPS 端口<E7ABAF><E58FA3>?1444-11446)用于测试连接池隔离
|
||||
- 2025-11-01: 新增本地 mock server,替<EFBC8C><E69BBF>?httpbin.org,提供稳定测试环<E8AF95><E78EAF>?
|
||||
- 2025-11-01: 新增 15 个并发、竞态和连接复用集成测试(H、I、J组)
|
||||
@@ -0,0 +1,514 @@
|
||||
# 连接池状态机验证分析
|
||||
|
||||
## 用户核心问题
|
||||
|
||||
**"have we verified that the state machine of connection in the pool has been correctly maintained? what abnormal situation have we designed? ultrathink"**
|
||||
|
||||
---
|
||||
|
||||
## 连接状态机定义
|
||||
|
||||
### 状态属<E68081><E5B19E>?
|
||||
|
||||
**HttpdnsNWReusableConnection.h:9-11**
|
||||
```objc
|
||||
@property (nonatomic, strong) NSDate *lastUsedDate; // 最后使用时<E794A8><E697B6>?
|
||||
@property (nonatomic, assign) BOOL inUse; // 是否正在被使<E8A2AB><E4BDBF>?
|
||||
@property (nonatomic, assign, getter=isInvalidated, readonly) BOOL invalidated; // 是否已失<E5B7B2><E5A4B1>?
|
||||
```
|
||||
|
||||
### 状态枚<E68081><E69E9A>?
|
||||
|
||||
虽然没有显式枚举,但连接实际存在以下逻辑状态:
|
||||
|
||||
| 状<><E78AB6>?| `inUse` | `invalidated` | `pool` | 描述 |
|
||||
|------|---------|---------------|--------|------|
|
||||
| **CREATING** | - | NO | <20><>?| 新创建,尚未打开 |
|
||||
| **IN_USE** | YES | NO | <20><>?| 已借出,正在使<E59CA8><E4BDBF>?|
|
||||
| **IDLE** | NO | NO | <20><>?| 空闲,可复用 |
|
||||
| **EXPIRED** | NO | NO | <20><>?| 空闲<E7A9BA><E997B2>?0秒,待清<E5BE85><E6B885>?|
|
||||
| **INVALIDATED** | - | YES | <20><>?| 已失效,已移<E5B7B2><E7A7BB>?|
|
||||
|
||||
---
|
||||
|
||||
## 状态转换图
|
||||
|
||||
```
|
||||
┌─────────<E29480><E29480>?
|
||||
│CREATING <20><>?(new connection)
|
||||
└────┬────<E29480><E29480>?
|
||||
<20><>?openWithTimeout success
|
||||
<20><>?
|
||||
┌─────────<E29480><E29480>?
|
||||
<20><>?IN_USE <20><>?(inUse=YES, in pool)
|
||||
└────┬────<E29480><E29480>?
|
||||
<20><>?
|
||||
├──success──<E29480><E29480>?returnConnection(shouldClose=NO)
|
||||
<20><>? <20><>?
|
||||
<20><>? <20><>?
|
||||
<20><>? ┌─────────<E29480><E29480>?
|
||||
<20><>? <20><>? IDLE <20><>?(inUse=NO, in pool)
|
||||
<20><>? └────┬────<E29480><E29480>?
|
||||
<20><>? <20><>?
|
||||
<20><>? ├──dequeue──<E29480><E29480>?IN_USE (reuse)
|
||||
<20><>? <20><>?
|
||||
<20><>? ├──idle 30s──<E29480><E29480>?EXPIRED
|
||||
<20><>? <20><>? <20><>?
|
||||
<20><>? <20><>? └──prune──<E29480><E29480>?INVALIDATED
|
||||
<20><>? <20><>?
|
||||
<20><>? └──!isViable──<E29480><E29480>?INVALIDATED (skip in dequeue)
|
||||
<20><>?
|
||||
├──error/timeout──<E29480><E29480>?returnConnection(shouldClose=YES)
|
||||
<20><>? <20><>?
|
||||
<20><>? <20><>?
|
||||
└──────────<E29480><E29480>?┌──────────────<E29480><E29480>?
|
||||
<20><>?INVALIDATED <20><>?(removed from pool)
|
||||
└──────────────<E29480><E29480>?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码中的状态转<E68081><E8BDAC>?
|
||||
|
||||
### 1. CREATING <20><>?IN_USE (新连<E696B0><E8BF9E>?
|
||||
|
||||
**HttpdnsNWHTTPClient.m:248-249**
|
||||
```objc
|
||||
newConnection.inUse = YES;
|
||||
newConnection.lastUsedDate = now;
|
||||
[pool addObject:newConnection]; // 加入<E58AA0><E585A5>?
|
||||
```
|
||||
|
||||
**何时触发:**
|
||||
- `dequeueConnectionForHost` 找不到可复用连接
|
||||
- 创建新连接并成功打开
|
||||
|
||||
### 2. IDLE <20><>?IN_USE (复用)
|
||||
|
||||
**HttpdnsNWHTTPClient.m:210-214**
|
||||
```objc
|
||||
for (HttpdnsNWReusableConnection *candidate in pool) {
|
||||
if (!candidate.inUse && [candidate isViable]) {
|
||||
candidate.inUse = YES;
|
||||
candidate.lastUsedDate = now;
|
||||
connection = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键检<E994AE><E6A380>?**
|
||||
- `!candidate.inUse` - 必须是空闲状<E997B2><E78AB6>?
|
||||
- `[candidate isViable]` - 连接必须仍然有效
|
||||
|
||||
### 3. IN_USE <20><>?IDLE (正常归还)
|
||||
|
||||
**HttpdnsNWHTTPClient.m:283-288**
|
||||
```objc
|
||||
if (shouldClose || connection.isInvalidated) {
|
||||
// <20><>?INVALIDATED (<28><>?4)
|
||||
} else {
|
||||
connection.inUse = NO;
|
||||
connection.lastUsedDate = now;
|
||||
if (![pool containsObject:connection]) {
|
||||
[pool addObject:connection]; // 防止双重添加
|
||||
}
|
||||
[self pruneConnectionPool:pool referenceDate:now];
|
||||
}
|
||||
```
|
||||
|
||||
**防护措施:**
|
||||
- Line 285: `if (![pool containsObject:connection])` - 防止重复添加
|
||||
|
||||
### 4. IN_USE/IDLE <20><>?INVALIDATED (失效)
|
||||
|
||||
**HttpdnsNWHTTPClient.m:279-281**
|
||||
```objc
|
||||
if (shouldClose || connection.isInvalidated) {
|
||||
[connection invalidate];
|
||||
[pool removeObject:connection];
|
||||
}
|
||||
```
|
||||
|
||||
**触发条件:**
|
||||
- `shouldClose=YES` (timeout, error, parse failure, remote close)
|
||||
- `connection.isInvalidated=YES` (连接已失<E5B7B2><E5A4B1>?
|
||||
|
||||
### 5. EXPIRED <20><>?INVALIDATED (过期清理)
|
||||
|
||||
**HttpdnsNWHTTPClient.m:297-312**
|
||||
```objc
|
||||
- (void)pruneConnectionPool:(NSMutableArray<HttpdnsNWReusableConnection *> *)pool referenceDate:(NSDate *)referenceDate {
|
||||
// ...
|
||||
NSMutableArray<HttpdnsNWReusableConnection *> *toRemove = [NSMutableArray array];
|
||||
for (HttpdnsNWReusableConnection *conn in pool) {
|
||||
if (conn.inUse) continue; // 跳过使用中的
|
||||
|
||||
NSTimeInterval idle = [referenceDate timeIntervalSinceDate:conn.lastUsedDate];
|
||||
if (idle > kHttpdnsNWHTTPClientConnectionIdleTimeout) { // 30<33><30>?
|
||||
[toRemove addObject:conn];
|
||||
}
|
||||
}
|
||||
|
||||
for (HttpdnsNWReusableConnection *conn in toRemove) {
|
||||
[conn invalidate];
|
||||
[pool removeObject:conn];
|
||||
}
|
||||
|
||||
// 限制池大<E6B1A0><E5A4A7>?<3F><>?4
|
||||
while (pool.count > kHttpdnsNWHTTPClientMaxIdleConnectionsPerHost) {
|
||||
HttpdnsNWReusableConnection *oldest = pool.firstObject;
|
||||
[oldest invalidate];
|
||||
[pool removeObject:oldest];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 当前测试覆盖情况
|
||||
|
||||
### <20><>?已测试的正常流程
|
||||
|
||||
| 状态转<E68081><E8BDAC>?| 测试 | 覆盖 |
|
||||
|----------|------|------|
|
||||
| CREATING <20><>?IN_USE <20><>?IDLE | G.1-G.7, O.1 | <20><>?|
|
||||
| IDLE <20><>?IN_USE (复用) | G.2, O.1-O.3, J.1-J.5 | <20><>?|
|
||||
| IN_USE <20><>?INVALIDATED (timeout) | P.1-P.6 | <20><>?|
|
||||
| EXPIRED <20><>?INVALIDATED (30s) | J.2, M.4, I.4 | <20><>?|
|
||||
| 池容量限<E9878F><E99990>?(max 4) | O.3, J.3 | <20><>?|
|
||||
| 并发状态访<E68081><E8AEBF>?| I.1-I.5, M.3 | <20><>?|
|
||||
|
||||
### <20><>?未测试的异常场景
|
||||
|
||||
#### 1. **连接在池中失效(Stale Connection<6F><6E>?*
|
||||
|
||||
**场景:**
|
||||
- 连接空闲 29 秒(未到 30 秒过期)
|
||||
- 服务器主动关闭连<E997AD><E8BF9E>?
|
||||
- `dequeue` <20><>?`isViable` 返回 NO
|
||||
|
||||
**当前代码行为:**
|
||||
```objc
|
||||
for (HttpdnsNWReusableConnection *candidate in pool) {
|
||||
if (!candidate.inUse && [candidate isViable]) { // <20><>?isViable 检<><E6A380>?
|
||||
// 只复用有效连<E69588><E8BF9E>?
|
||||
}
|
||||
}
|
||||
// 如果所有连接都 !isViable,会创建新连<E696B0><E8BF9E>?
|
||||
```
|
||||
|
||||
**风险:** 未验<E69CAA><E9AA8C>?`isViable` 检查是否真的工<E79A84><E5B7A5>?
|
||||
|
||||
**测试需<E8AF95><E99C80>?** Q.1
|
||||
```objc
|
||||
testStateTransition_StaleConnectionInPool_SkipsAndCreatesNew
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2. **双重归还(Double Return<72><6E>?*
|
||||
|
||||
**场景:**
|
||||
- 连接被归<E8A2AB><E5BD92>?
|
||||
- 代码错误,再次归还同一连接
|
||||
|
||||
**当前代码防护:**
|
||||
```objc
|
||||
if (![pool containsObject:connection]) {
|
||||
[pool addObject:connection]; // <20><>?防止重复添加
|
||||
}
|
||||
```
|
||||
|
||||
**风险:** 未验证防护是否有<E590A6><E69C89>?
|
||||
|
||||
**测试需<E8AF95><E99C80>?** Q.2
|
||||
```objc
|
||||
testStateTransition_DoubleReturn_Idempotent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3. **归还错误的池键(Wrong Pool Key<65><79>?*
|
||||
|
||||
**场景:**
|
||||
- 从池A借出连接
|
||||
- 归还到池B(错误的key<65><79>?
|
||||
|
||||
**当前代码行为:**
|
||||
```objc
|
||||
- (void)returnConnection:(HttpdnsNWReusableConnection *)connection
|
||||
forKey:(NSString *)key
|
||||
shouldClose:(BOOL)shouldClose {
|
||||
// ...
|
||||
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
|
||||
// 会添加到错误的池!
|
||||
}
|
||||
```
|
||||
|
||||
**风险:** 可能导致池污<E6B1A0><E6B1A1>?
|
||||
|
||||
**测试需<E8AF95><E99C80>?** Q.3
|
||||
```objc
|
||||
testStateTransition_ReturnToWrongPool_Isolated
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 4. **连接在使用中变为失效**
|
||||
|
||||
**场景:**
|
||||
- 连接被借出 (inUse=YES)
|
||||
- `sendRequestData` 过程中网络错<E7BB9C><E99499>?
|
||||
- 连接被标<E8A2AB><E6A087>?invalidated
|
||||
|
||||
**当前代码行为:**
|
||||
```objc
|
||||
NSData *rawResponse = [connection sendRequestData:requestData ...];
|
||||
if (!rawResponse) {
|
||||
[self returnConnection:connection forKey:poolKey shouldClose:YES]; // <20><>?invalidated
|
||||
}
|
||||
```
|
||||
|
||||
**测试需<E8AF95><E99C80>?** Q.4
|
||||
```objc
|
||||
testStateTransition_ErrorDuringUse_Invalidated
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 5. **池容量超限时的移除策<E999A4><E7AD96>?*
|
||||
|
||||
**场景:**
|
||||
- 池已<E6B1A0><E5B7B2>?4 个连<E4B8AA><E8BF9E>?
|
||||
- <20><>?5 个连接被归还
|
||||
|
||||
**当前代码行为:**
|
||||
```objc
|
||||
while (pool.count > kHttpdnsNWHTTPClientMaxIdleConnectionsPerHost) {
|
||||
HttpdnsNWReusableConnection *oldest = pool.firstObject; // <20><>?移除最老的
|
||||
[oldest invalidate];
|
||||
[pool removeObject:oldest];
|
||||
}
|
||||
```
|
||||
|
||||
**问题:**
|
||||
- 移除 `pool.firstObject` - 是按添加顺序还是使用顺序<E9A1BA><E5BA8F>?
|
||||
- NSMutableArray 顺序是否能保证?
|
||||
|
||||
**测试需<E8AF95><E99C80>?** Q.5
|
||||
```objc
|
||||
testStateTransition_PoolOverflow_RemovesOldest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 6. **并发状态竞<E68081><E7AB9E>?*
|
||||
|
||||
**场景:**
|
||||
- Thread A: dequeue 连接,设<EFBC8C><E8AEBE>?`inUse=YES`
|
||||
- Thread B: 同时 prune 过期连接
|
||||
- 竞态:连接同时被标<E8A2AB><E6A087>?inUse 和被移除
|
||||
|
||||
**当前代码防护:**
|
||||
```objc
|
||||
- (void)pruneConnectionPool:... {
|
||||
for (HttpdnsNWReusableConnection *conn in pool) {
|
||||
if (conn.inUse) continue; // <20><>?跳过使用中的
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**测试需<E8AF95><E99C80>?** Q.6 (可能已被 I 组部分覆<E58886><E8A686>?
|
||||
```objc
|
||||
testStateTransition_ConcurrentDequeueAndPrune_NoCorruption
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 7. **连接打开失败**
|
||||
|
||||
**场景:**
|
||||
- 创建连接
|
||||
- `openWithTimeout` 失败
|
||||
|
||||
**当前代码行为:**
|
||||
```objc
|
||||
if (![newConnection openWithTimeout:timeout error:error]) {
|
||||
[newConnection invalidate]; // <20><>?立即失效
|
||||
return nil; // <20><>?不加入池
|
||||
}
|
||||
```
|
||||
|
||||
**测试需<E8AF95><E99C80>?** Q.7
|
||||
```objc
|
||||
testStateTransition_OpenFails_NotAddedToPool
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 状态不变式(State Invariants<74><73>?
|
||||
|
||||
### 应该始终成立的约<E79A84><E7BAA6>?
|
||||
|
||||
1. **互斥<E4BA92><E696A5>?**
|
||||
```
|
||||
∀ connection: (inUse=YES) <20><>?(dequeue count <20><>?1)
|
||||
```
|
||||
同一连接不能被多次借出
|
||||
|
||||
2. **池完整<E5AE8C><E695B4>?**
|
||||
```
|
||||
∀ pool: <20><>?connections) <20><>?maxPoolSize (4)
|
||||
```
|
||||
每个池最<E6B1A0><E69C80>?4 个连<E4B8AA><E8BF9E>?
|
||||
|
||||
3. **状态一致<E4B880><E887B4>?**
|
||||
```
|
||||
∀ connection in pool: !invalidated
|
||||
```
|
||||
池中不应有失效连<E69588><E8BF9E>?
|
||||
|
||||
4. **时间单调<E58D95><E8B083>?**
|
||||
```
|
||||
∀ connection: lastUsedDate 随每次使用递增
|
||||
```
|
||||
|
||||
5. **失效不可<E4B88D><E58FAF>?**
|
||||
```
|
||||
invalidated=YES <20><>?connection removed from pool
|
||||
```
|
||||
失效连接必须从池中移<E4B8AD><E7A7BB>?
|
||||
|
||||
---
|
||||
|
||||
## 测试设计建议
|
||||
|
||||
### Q 组:状态机异常转换测试<E6B58B><E8AF95>?个新测试<E6B58B><E8AF95>?
|
||||
|
||||
| 测试 | 验证内容 | 难度 |
|
||||
|------|---------|------|
|
||||
| **Q.1** | Stale connection <20><>?`isViable` 检测并跳过 | 🔴 高(需要模拟服务器关闭<E585B3><E997AD>?|
|
||||
| **Q.2** | 双重归还是幂等的 | 🟢 <20><>?|
|
||||
| **Q.3** | 归还到错误池键不污染其他<E585B6><E4BB96>?| 🟡 <20><>?|
|
||||
| **Q.4** | 使用中错误导致连接失<E68EA5><E5A4B1>?| 🟢 低(已有 P 组部分覆盖) |
|
||||
| **Q.5** | 池溢出时移除最旧连<E697A7><E8BF9E>?| 🟡 <20><>?|
|
||||
| **Q.6** | 并发 dequeue/prune 竞<><E7AB9E>?| 🔴 高(需要精确时序) |
|
||||
| **Q.7** | 打开失败的连接不加入<E58AA0><E585A5>?| 🟢 <20><>?|
|
||||
|
||||
---
|
||||
|
||||
## 状态机验证策略
|
||||
|
||||
### 方法1: 直接状态检<E68081><E6A380>?
|
||||
|
||||
```objc
|
||||
// 验证状态属<E68081><E5B19E>?
|
||||
XCTAssertTrue(connection.inUse);
|
||||
XCTAssertFalse(connection.isInvalidated);
|
||||
XCTAssertEqual([poolCount], expectedCount);
|
||||
```
|
||||
|
||||
### 方法2: 状态转换序<E68DA2><E5BA8F>?
|
||||
|
||||
```objc
|
||||
// 验证转换序列
|
||||
[client resetPoolStatistics];
|
||||
|
||||
// CREATING <20><>?IN_USE
|
||||
response1 = [client performRequest...];
|
||||
XCTAssertEqual(creationCount, 1);
|
||||
|
||||
// IN_USE <20><>?IDLE
|
||||
[NSThread sleepForTimeInterval:0.5];
|
||||
XCTAssertEqual(poolCount, 1);
|
||||
|
||||
// IDLE <20><>?IN_USE (reuse)
|
||||
response2 = [client performRequest...];
|
||||
XCTAssertEqual(reuseCount, 1);
|
||||
```
|
||||
|
||||
### 方法3: 不变式验<E5BC8F><E9AA8C>?
|
||||
|
||||
```objc
|
||||
// 验证池不变式
|
||||
NSArray *keys = [client allConnectionPoolKeys];
|
||||
for (NSString *key in keys) {
|
||||
NSUInteger count = [client connectionPoolCountForKey:key];
|
||||
XCTAssertLessThanOrEqual(count, 4, @"Pool invariant: max 4 connections");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 当前覆盖率评<E78E87><E8AF84>?
|
||||
|
||||
### 状态转换覆盖矩<E79B96><E79FA9>?
|
||||
|
||||
| From <20><>?/ To <20><>?| CREATING | IN_USE | IDLE | EXPIRED | INVALIDATED |
|
||||
|---------------|----------|--------|------|---------|-------------|
|
||||
| **CREATING** | - | <20><>?| <20><>?| <20><>?| <20><>?(Q.7 needed) |
|
||||
| **IN_USE** | <20><>?| - | <20><>?| <20><>?| <20><>?|
|
||||
| **IDLE** | <20><>?| <20><>?| - | <20><>?| <20><>?(Q.1 needed) |
|
||||
| **EXPIRED** | <20><>?| <20><>?| <20><>?| - | <20><>?|
|
||||
| **INVALIDATED** | <20><>?| <20><>?| <20><>?| <20><>?| - |
|
||||
|
||||
**覆盖<E8A686><E79B96>?** 6/25 transitions = 24%
|
||||
**有效覆盖<E8A686><E79B96>?** 6/10 valid transitions = 60%
|
||||
|
||||
### 异常场景覆盖
|
||||
|
||||
| 异常场景 | 当前测试 | 覆盖 |
|
||||
|----------|---------|------|
|
||||
| Stale connection | <20><>?| 0% |
|
||||
| Double return | <20><>?| 0% |
|
||||
| Wrong pool key | <20><>?| 0% |
|
||||
| Error during use | P.1-P.6 | 100% |
|
||||
| Pool overflow | O.3, J.3 | 50% (未验证移除策<E999A4><E7AD96>? |
|
||||
| Concurrent race | I.1-I.5 | 80% |
|
||||
| Open failure | <20><>?| 0% |
|
||||
|
||||
**总体异常覆盖:** ~40%
|
||||
|
||||
---
|
||||
|
||||
## 风险评估
|
||||
|
||||
### 高风险未测试场景
|
||||
|
||||
**风险等级 🔴 <20><>?**
|
||||
1. **Stale Connection (Q.1)** - 可能导致请求失败
|
||||
2. **Concurrent Dequeue/Prune (Q.6)** - 可能导致状态不一<E4B88D><E4B880>?
|
||||
|
||||
**风险等级 🟡 <20><>?**
|
||||
3. **Wrong Pool Key (Q.3)** - 可能导致池污<E6B1A0><E6B1A1>?
|
||||
4. **Pool Overflow Strategy (Q.5)** - LRU vs FIFO 影响性能
|
||||
|
||||
**风险等级 🟢 <20><>?**
|
||||
5. **Double Return (Q.2)** - 已有代码防护
|
||||
6. **Open Failure (Q.7)** - 已有错误处理
|
||||
|
||||
---
|
||||
|
||||
## 建议
|
||||
|
||||
### 短期(关键)
|
||||
|
||||
1. <20><>?**添加 Q.2 测试** - 验证双重归还防护
|
||||
2. <20><>?**添加 Q.5 测试** - 验证池溢出移除策<E999A4><E7AD96>?
|
||||
3. <20><>?**添加 Q.7 测试** - 验证打开失败处理
|
||||
|
||||
### 中期(增强)
|
||||
|
||||
4. ⚠️ **添加 Q.3 测试** - 验证池隔<E6B1A0><E99A94>?
|
||||
5. ⚠️ **添加 Q.1 测试** - 验证 stale connection(需<EFBC88><E99C80>?mock<63><6B>?
|
||||
|
||||
### 长期(完整)
|
||||
|
||||
6. 🔬 **添加 Q.6 测试** - 验证并发竞态(复杂<E5A48D><E69D82>?
|
||||
|
||||
---
|
||||
|
||||
**创建时间**: 2025-11-01
|
||||
**作<><E4BD9C>?*: Claude Code
|
||||
**状<><E78AB6>?*: 分析完成,待实现 Q 组测<E7BB84><E6B58B>?
|
||||
226
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Network/TIMEOUT_ANALYSIS.md
Normal file
226
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Network/TIMEOUT_ANALYSIS.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# 超时对连接复用的影响分析
|
||||
|
||||
## 问题描述
|
||||
|
||||
当前测试套件没有充分验证**超时与连接池交互**<2A><>?无形结果"(intangible outcomes),可能存在以下风险<E9A38E><E999A9>?
|
||||
- 超时后的连接泄漏
|
||||
- 连接池被超时连接污染
|
||||
- 连接池无法从超时中恢<E4B8AD><E681A2>?
|
||||
- 并发场景下部分超时影响整体池健康
|
||||
|
||||
---
|
||||
|
||||
## 代码行为分析
|
||||
|
||||
### 超时处理流程
|
||||
|
||||
**HttpdnsNWHTTPClient.m:144-145**
|
||||
```objc
|
||||
if (!rawResponse) {
|
||||
[self returnConnection:connection forKey:poolKey shouldClose:YES];
|
||||
// 返回 nil,error 设置
|
||||
}
|
||||
```
|
||||
|
||||
**returnConnection:forKey:shouldClose: (line 279-281)**
|
||||
```objc
|
||||
if (shouldClose || connection.isInvalidated) {
|
||||
[connection invalidate]; // 取消底层 nw_connection
|
||||
[pool removeObject:connection]; // 从池中移<E4B8AD><E7A7BB>?
|
||||
}
|
||||
```
|
||||
|
||||
**结论**:代码逻辑正确,超时连<EFBFBD><EFBFBD>?*会被移除**而非留在池中<E6B1A0><E4B8AD>?
|
||||
|
||||
---
|
||||
|
||||
## 当前测试覆盖情况
|
||||
|
||||
### 已有测试:`testIntegration_RequestTimeout_ReturnsError`
|
||||
|
||||
**验证内容<E58685><E5AEB9>?*
|
||||
- <20><>?超时返回 `nil` response
|
||||
- <20><>?超时设置 `error`
|
||||
|
||||
**未验证内容(缺失):**
|
||||
- <20><>?连接是否从池中移<E4B8AD><E7A7BB>?
|
||||
- <20><>?池计数是否正<E590A6><E6ADA3>?
|
||||
- <20><>?后续请求是否正常工作
|
||||
- <20><>?是否存在连接泄漏
|
||||
- <20><>?并发场景下部分超时的影响
|
||||
|
||||
---
|
||||
|
||||
## 需要验证的"无形结果"
|
||||
|
||||
### 1. 单次超时后的池清<E6B1A0><E6B885>?
|
||||
|
||||
**场景**<EFBFBD><EFBFBD>?
|
||||
1. 请求 A 超时(timeout=1s, endpoint=/delay/10<31><30>?
|
||||
2. 验证池状<E6B1A0><E78AB6>?
|
||||
|
||||
**应验证:**
|
||||
- Pool count = 0(连接已移除<E7A7BB><E999A4>?
|
||||
- Total connection count 没有异常增长
|
||||
- 无连接泄<E68EA5><E6B384>?
|
||||
|
||||
**测试方法**<EFBFBD><EFBFBD>?
|
||||
```objc
|
||||
[client resetPoolStatistics];
|
||||
|
||||
// 发起超时请求
|
||||
NSError *error = nil;
|
||||
HttpdnsNWHTTPClientResponse *response = [client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
|
||||
userAgent:@"TimeoutTest"
|
||||
timeout:1.0
|
||||
error:&error];
|
||||
|
||||
XCTAssertNil(response);
|
||||
XCTAssertNotNil(error);
|
||||
|
||||
// 验证池状<E6B1A0><E78AB6>?
|
||||
NSString *poolKey = @"127.0.0.1:11080:tcp";
|
||||
XCTAssertEqual([client connectionPoolCountForKey:poolKey], 0, @"Timed-out connection should be removed");
|
||||
XCTAssertEqual([client totalConnectionCount], 0, @"No connections should remain");
|
||||
XCTAssertEqual(client.connectionCreationCount, 1, @"Should have created 1 connection");
|
||||
XCTAssertEqual(client.connectionReuseCount, 0, @"No reuse for timed-out connection");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 超时后的池恢复能<E5A48D><E883BD>?
|
||||
|
||||
**场景**<EFBFBD><EFBFBD>?
|
||||
1. 请求 A 超时
|
||||
2. 请求 B 正常(验证池恢复<E681A2><E5A48D>?
|
||||
3. 请求 C 复用 B 的连<E79A84><E8BF9E>?
|
||||
|
||||
**应验证:**
|
||||
- 请求 B 成功(池已恢复)
|
||||
- 请求 C 复用连接(connectionReuseCount = 1<><31>?
|
||||
- Pool count = 1(只<EFBC88><E58FAA>?B/C 的连接)
|
||||
|
||||
---
|
||||
|
||||
### 3. 并发场景:部分超时不影响成功请求
|
||||
|
||||
**场景**<EFBFBD><EFBFBD>?
|
||||
1. 并发发起 10 个请<E4B8AA><E8AFB7>?
|
||||
2. 5 个正常(timeout=15s<35><73>?
|
||||
3. 5 个超时(timeout=0.5s, endpoint=/delay/10<31><30>?
|
||||
|
||||
**应验证:**
|
||||
- 5 个正常请求成<E6B182><E68890>?
|
||||
- 5 个超时请求失<E6B182><E5A4B1>?
|
||||
- Pool count <20><>?5(只保留成功的连接)
|
||||
- Total connection count <20><>?5(无泄漏<E6B384><E6BC8F>?
|
||||
- connectionCreationCount <20><>?10(合理范围)
|
||||
- 成功的请求可以复用连<E794A8><E8BF9E>?
|
||||
|
||||
---
|
||||
|
||||
### 4. 连续超时不导致资源泄<E6BA90><E6B384>?
|
||||
|
||||
**场景**<EFBFBD><EFBFBD>?
|
||||
1. 连续 20 次超时请<E697B6><E8AFB7>?
|
||||
2. 验证连接池没有累<E69C89><E7B4AF>?僵尸连接"
|
||||
|
||||
**应验证:**
|
||||
- Pool count = 0
|
||||
- Total connection count = 0
|
||||
- connectionCreationCount = 20(每次都创建新连接,因为超时的被移除<E7A7BB><E999A4>?
|
||||
- connectionReuseCount = 0(超时连接不可复用)
|
||||
- 无内存泄漏(虽然代码层面无法直接测试<E6B58B><E8AF95>?
|
||||
|
||||
---
|
||||
|
||||
### 5. 超时不阻塞连接池
|
||||
|
||||
**场景**<EFBFBD><EFBFBD>?
|
||||
1. 请求 A 超时(endpoint=/delay/10, timeout=1s<31><73>?
|
||||
2. 同时请求 B 正常(endpoint=/get, timeout=15s<35><73>?
|
||||
|
||||
**应验证:**
|
||||
- 请求 A <20><>?B 并发执行(不互相阻塞<E998BB><E5A19E>?
|
||||
- 请求 B 成功(不<EFBC88><E4B88D>?A 超时影响<E5BDB1><E5938D>?
|
||||
- 请求 A 的超时连接被正确移除
|
||||
- Pool 中只有请<E69C89><E8AFB7>?B 的连<E79A84><E8BF9E>?
|
||||
|
||||
---
|
||||
|
||||
### 6. 多端口场景下的超时隔<E697B6><E99A94>?
|
||||
|
||||
**场景**<EFBFBD><EFBFBD>?
|
||||
1. 端口 11443 请求超时
|
||||
2. 端口 11444 请求正常
|
||||
3. 验证端口间隔<E997B4><E99A94>?
|
||||
|
||||
**应验证:**
|
||||
- 端口 11443 pool count = 0
|
||||
- 端口 11444 pool count = 1
|
||||
- 两个端口的连接池互不影响
|
||||
|
||||
---
|
||||
|
||||
## 测试实现建议
|
||||
|
||||
### P 组:超时与连接池交互测试
|
||||
|
||||
**P.1 单次超时清理验证**
|
||||
- `testTimeout_SingleRequest_ConnectionRemovedFromPool`
|
||||
|
||||
**P.2 超时后池恢复**
|
||||
- `testTimeout_PoolRecovery_SubsequentRequestSucceeds`
|
||||
|
||||
**P.3 并发部分超时**
|
||||
- `testTimeout_ConcurrentPartialTimeout_SuccessfulRequestsReuse`
|
||||
|
||||
**P.4 连续超时无泄<E697A0><E6B384>?*
|
||||
- `testTimeout_ConsecutiveTimeouts_NoConnectionLeak`
|
||||
|
||||
**P.5 超时不阻塞池**
|
||||
- `testTimeout_NonBlocking_ConcurrentNormalRequestSucceeds`
|
||||
|
||||
**P.6 多端口超时隔<E697B6><E99A94>?*
|
||||
- `testTimeout_MultiPort_IsolatedPoolCleaning`
|
||||
|
||||
---
|
||||
|
||||
## Mock Server 支持
|
||||
|
||||
需要添加可配置延迟<EFBFBD><EFBFBD>?endpoint<6E><74>?
|
||||
- `/delay/10` - 延迟 10 秒(已有<E5B7B2><E69C89>?
|
||||
- 测试时设置短 timeout(如 0.5s-2s)触发超<E58F91><E8B685>?
|
||||
|
||||
---
|
||||
|
||||
## 预期测试结果
|
||||
|
||||
| 验证<E9AA8C><E8AF81>?| 当前状<E5898D><E78AB6>?| 目标状<E6A087><E78AB6>?|
|
||||
|--------|---------|---------|
|
||||
| 超时连接移除 | 未验<E69CAA><E9AA8C>?| <20><>?验证池计<E6B1A0><E8AEA1>?0 |
|
||||
| 池恢复能<E5A48D><E883BD>?| 未验<E69CAA><E9AA8C>?| <20><>?后续请求成功 |
|
||||
| 并发超时隔离 | 未验<E69CAA><E9AA8C>?| <20><>?成功请求不受影响 |
|
||||
| 无连接泄<E68EA5><E6B384>?| 未验<E69CAA><E9AA8C>?| <20><>?总连接数稳定 |
|
||||
| 超时不阻<E4B88D><E998BB>?| 未验<E69CAA><E9AA8C>?| <20><>?并发执行不阻<E4B88D><E998BB>?|
|
||||
| 多端口隔<E58FA3><E99A94>?| 未验<E69CAA><E9AA8C>?| <20><>?端口间独立清<E7AB8B><E6B885>?|
|
||||
|
||||
---
|
||||
|
||||
## 风险评估
|
||||
|
||||
**如果不测试这些场景的风险<E9A38E><E999A9>?*
|
||||
1. **连接泄漏**:超时连接可能未正确清理,导致内存泄<EFBFBD><EFBFBD>?
|
||||
2. **池污<E6B1A0><E6B1A1>?*:超时连接留在池中,被后续请求复用导致失<E887B4><E5A4B1>?
|
||||
3. **级联故障**:部分超时影响整体连接池健康
|
||||
4. **资源耗尽**:连续超时累积连接,最终耗尽系统资源
|
||||
|
||||
**当前代码逻辑正确性:** <20><>?高(代码分析显示正确处理<E5A484><E79086>?
|
||||
**测试验证覆盖率:** <20><>?低(缺少池交互验证)
|
||||
|
||||
**建议<E5BBBA><E8AEAE>?* 添加 P 组测试以提供**可观测的证据**证明超时处理正确<E6ADA3><E7A1AE>?
|
||||
|
||||
---
|
||||
|
||||
**创建时间**: 2025-11-01
|
||||
**维护<E7BBB4><E68AA4>?*: Claude Code
|
||||
347
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Network/mock_server.py
Normal file
347
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Network/mock_server.py
Normal 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()
|
||||
@@ -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
|
||||
194
HttpDNSSDK/sdk/ios/NewHttpDNSTests/OutdatedTest/NetworkManager.m
Normal file
194
HttpDNSSDK/sdk/ios/NewHttpDNSTests/OutdatedTest/NetworkManager.m
Normal 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.Trust.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
|
||||
59
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Testbase/TestBase.h
Normal file
59
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Testbase/TestBase.h
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// TestBase.h
|
||||
// TrustHttpDNS
|
||||
//
|
||||
// Created by ElonChan(地风) on 2017/4/14.
|
||||
// Copyright © 2017<31><37>?trustapp.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
|
||||
133
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Testbase/TestBase.m
Normal file
133
HttpDNSSDK/sdk/ios/NewHttpDNSTests/Testbase/TestBase.m
Normal file
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// TestBase.m
|
||||
// TrustHttpDNS
|
||||
//
|
||||
// Created by ElonChan(地风) on 2017/4/14.
|
||||
// Copyright © 2017<EFBFBD><EFBFBD>?trustapp.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
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// TestBase.h
|
||||
// TrustHttpDNS
|
||||
//
|
||||
// Created by ElonChan(地风) on 2017/4/14.
|
||||
// Copyright © 2017<31><37>?trustapp.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
|
||||
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// TestBase.h
|
||||
// TrustHttpDNS
|
||||
//
|
||||
// Created by ElonChan(地风) on 2017/4/14.
|
||||
// Copyright © 2017<EFBFBD><EFBFBD>?trustapp.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
|
||||
Reference in New Issue
Block a user