Files
waf-platform/EdgeHttpDNS/sdk/ios/AlicloudHttpDNSTests/Network/HttpdnsNWHTTPClient_StateMachineTests.m
2026-02-20 17:56:24 +08:00

592 lines
26 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// HttpdnsNWHTTPClient_StateMachineTests.m
// AlicloudHttpDNSTests
//
// @author Created by Claude Code on 2025-11-01
// Copyright © 2025 alibaba-inc.com. All rights reserved.
//
// 状态机测试 - 包含状态机与异常场景 (Q) 测试组
// 测试总数17 个Q:17
//
#import "HttpdnsNWHTTPClientTestBase.h"
@interface HttpdnsNWHTTPClient_StateMachineTests : HttpdnsNWHTTPClientTestBase
@end
@implementation HttpdnsNWHTTPClient_StateMachineTests
#pragma mark - Q. 状态机与异常场景测试
// Q1.1 池溢出时LRU移除策略验证
- (void)testStateMachine_PoolOverflowLRU_RemovesOldestByLastUsedDate {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 需要并发创建5个连接串行请求会复用
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 并发发起5个请求
for (NSInteger i = 0; i < 5; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
// 使用 /delay/2 确保所有请求同时在飞行中,强制创建多个连接
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/2"
userAgent:[NSString stringWithFormat:@"Request%ld", (long)i]
timeout:15.0
error:&error];
[expectation fulfill];
});
[NSThread sleepForTimeInterval:0.05]; // 小间隔避免完全同时启动
}
[self waitForExpectations:expectations timeout:20.0];
// 等待所有连接归还
[NSThread sleepForTimeInterval:1.0];
// 验证:池大小 ≤ 4LRU移除溢出部分
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolCount, 4,
@"Pool should enforce max 4 connections (LRU)");
// 验证:创建了多个连接
XCTAssertGreaterThanOrEqual(self.client.connectionCreationCount, 3,
@"Should create multiple concurrent connections");
}
// Q2.1 快速连续请求不产生重复连接(间接验证双重归还防护)
- (void)testAbnormal_RapidSequentialRequests_NoDuplicates {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 快速连续发起10个请求测试连接归还的幂等性
for (NSInteger i = 0; i < 10; i++) {
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"RapidTest"
timeout:15.0
error:&error];
XCTAssertNotNil(response);
}
// 等待连接归还
[NSThread sleepForTimeInterval:1.0];
// 验证池中最多1个连接因为串行请求复用同一连接
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolCount, 1,
@"Pool should have at most 1 connection (rapid sequential reuse)");
// 验证创建次数应该是1所有请求复用同一连接
XCTAssertEqual(self.client.connectionCreationCount, 1,
@"Should create only 1 connection for sequential requests");
}
// Q2.2 不同端口请求不互相污染池
- (void)testAbnormal_DifferentPorts_IsolatedPools {
[self.client resetPoolStatistics];
NSString *poolKey11080 = @"127.0.0.1:11080:tcp";
NSString *poolKey11443 = @"127.0.0.1:11443:tls";
// 向端口11080发起请求
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Port11080"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
// 向端口11443发起请求
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"https://127.0.0.1:11443/get"
userAgent:@"Port11443"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
// 等待连接归还
[NSThread sleepForTimeInterval:0.5];
// 验证两个池各自有1个连接
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey11080], 1,
@"Port 11080 pool should have 1 connection");
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey11443], 1,
@"Port 11443 pool should have 1 connection");
// 验证总共2个连接池完全隔离
XCTAssertEqual([self.client totalConnectionCount], 2,
@"Total should be 2 (one per pool)");
}
// Q3.1 池大小不变式:任何时候池大小都不超过限制
- (void)testInvariant_PoolSize_NeverExceedsLimit {
[self.client resetPoolStatistics];
// 快速连续发起20个请求到同一端点
for (NSInteger i = 0; i < 20; i++) {
NSError *error = nil;
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"InvariantTest"
timeout:15.0
error:&error];
}
// 等待所有连接归还
[NSThread sleepForTimeInterval:1.5];
// 验证每个池的大小不超过4
NSArray<NSString *> *allKeys = [self.client allConnectionPoolKeys];
for (NSString *key in allKeys) {
NSUInteger poolCount = [self.client connectionPoolCountForKey:key];
XCTAssertLessThanOrEqual(poolCount, 4,
@"Pool %@ size should never exceed 4 (actual: %lu)",
key, (unsigned long)poolCount);
}
// 验证总连接数也不超过4因为只有一个池
XCTAssertLessThanOrEqual([self.client totalConnectionCount], 4,
@"Total connections should not exceed 4");
}
// Q3.3 无重复连接不变式:并发请求不产生重复
- (void)testInvariant_NoDuplicates_ConcurrentRequests {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 并发发起15个请求可能复用连接
for (NSInteger i = 0; i < 15; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"ConcurrentTest"
timeout:15.0
error:&error];
[expectation fulfill];
});
}
[self waitForExpectations:expectations timeout:30.0];
// 等待连接归还
[NSThread sleepForTimeInterval:1.0];
// 验证:池大小 ≤ 4不变式
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolCount, 4,
@"Pool should not have duplicates (max 4 connections)");
// 验证创建的连接数合理≤15因为可能有复用
XCTAssertLessThanOrEqual(self.client.connectionCreationCount, 15,
@"Should not create excessive connections");
}
// Q4.1 边界条件恰好30秒后连接过期
- (void)testBoundary_Exactly30Seconds_ConnectionExpired {
if (getenv("SKIP_SLOW_TESTS")) {
return;
}
[self.client resetPoolStatistics];
// 第一个请求
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"InitialRequest"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
// 等待恰好30.5秒超过30秒过期时间
[NSThread sleepForTimeInterval:30.5];
// 第二个请求:应该创建新连接(旧连接已过期)
NSError *error2 = nil;
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"AfterExpiry"
timeout:15.0
error:&error2];
XCTAssertNotNil(response2);
// 验证创建了2个连接旧连接过期无法复用
XCTAssertEqual(self.client.connectionCreationCount, 2,
@"Should create 2 connections (first expired after 30s)");
XCTAssertEqual(self.client.connectionReuseCount, 0,
@"Should not reuse expired connection");
}
// Q4.2 边界条件29秒内连接未过期
- (void)testBoundary_Under30Seconds_ConnectionNotExpired {
[self.client resetPoolStatistics];
// 第一个请求
NSError *error1 = nil;
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"InitialRequest"
timeout:15.0
error:&error1];
XCTAssertNotNil(response1);
// 等待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);
// 验证只创建了1个连接复用了
XCTAssertEqual(self.client.connectionCreationCount, 1,
@"Should create only 1 connection (reused within 30s)");
XCTAssertEqual(self.client.connectionReuseCount, 1,
@"Should reuse connection within 30s");
}
// Q4.3 边界条件恰好4个连接全部保留
- (void)testBoundary_ExactlyFourConnections_AllKept {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 并发发起4个请求使用延迟确保同时在飞行中创建4个独立连接
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSInteger i = 0; i < 4; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:
[NSString stringWithFormat:@"Request %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
NSError *error = nil;
// 使用 /delay/2 确保所有请求同时在飞行中
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/2"
userAgent:[NSString stringWithFormat:@"Request%ld", (long)i]
timeout:15.0
error:&error];
[expectation fulfill];
});
[NSThread sleepForTimeInterval:0.05]; // 小间隔避免完全同时启动
}
[self waitForExpectations:expectations timeout:20.0];
// 等待连接归还
[NSThread sleepForTimeInterval:1.0];
// 验证池恰好有4个连接全部保留
NSUInteger poolCount = [self.client connectionPoolCountForKey:poolKey];
XCTAssertEqual(poolCount, 4,
@"Pool should keep all 4 connections (not exceeding limit)");
// 验证恰好创建4个连接
XCTAssertEqual(self.client.connectionCreationCount, 4,
@"Should create exactly 4 connections");
}
// Q1.2 正常状态序列验证
- (void)testStateMachine_NormalSequence_StateTransitionsCorrect {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 第1步创建并使用连接 (CREATING → IN_USE → IDLE)
HttpdnsNWHTTPClientResponse *response1 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"StateTest"
timeout:15.0
error:nil];
XCTAssertNotNil(response1, @"First request should succeed");
[NSThread sleepForTimeInterval:1.0]; // 等待归还
// 验证池中有1个连接
XCTAssertEqual([self.client connectionPoolCountForKey:poolKey], 1,
@"Connection should be in pool");
// 第2步复用连接 (IDLE → IN_USE → IDLE)
HttpdnsNWHTTPClientResponse *response2 = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"StateTest"
timeout:15.0
error:nil];
XCTAssertNotNil(response2, @"Second request should reuse connection");
// 验证:复用计数增加
XCTAssertEqual(self.client.connectionReuseCount, 1,
@"Should have reused connection once");
}
// Q1.3 inUse 标志维护验证
- (void)testStateMachine_InUseFlag_CorrectlyMaintained {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 发起请求并归还
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"InUseTest"
timeout:15.0
error:nil];
[NSThread sleepForTimeInterval:1.0]; // 等待归还
// 获取池中连接
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1, @"Should have 1 connection in pool");
// 验证:池中连接的 inUse 应为 NO
for (HttpdnsNWReusableConnection *conn in connections) {
XCTAssertFalse(conn.inUse, @"Connection in pool should not be marked as inUse");
}
}
// Q2.3 Nil lastUsedDate 处理验证
- (void)testAbnormal_NilLastUsedDate_HandledGracefully {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 发起请求创建连接
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"NilDateTest"
timeout:15.0
error:nil];
[NSThread sleepForTimeInterval:1.0];
// 获取连接并设置 lastUsedDate 为 nil
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1, @"Should have connection");
HttpdnsNWReusableConnection *conn = connections.firstObject;
[conn debugSetLastUsedDate:nil];
// 发起新请求触发 prune内部应使用 distantPast 处理 nil
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"NilDateTest"
timeout:15.0
error:nil];
// 验证:不崩溃,正常工作
XCTAssertNotNil(response, @"Should handle nil lastUsedDate gracefully");
}
// Q3.2 池中无失效连接不变式
- (void)testInvariant_NoInvalidatedInPool {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 发起多个请求(包括成功和超时)
for (NSInteger i = 0; i < 3; i++) {
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"InvariantTest"
timeout:15.0
error:nil];
}
// 发起1个超时请求
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"TimeoutTest"
timeout:0.5
error:nil];
[NSThread sleepForTimeInterval:2.0];
// 获取池中所有连接
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
// 验证:池中无失效连接
for (HttpdnsNWReusableConnection *conn in connections) {
XCTAssertFalse(conn.isInvalidated, @"Pool should not contain invalidated connections");
}
}
// Q3.4 lastUsedDate 单调性验证
- (void)testInvariant_LastUsedDate_Monotonic {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 第1次使用
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"MonotonicTest"
timeout:15.0
error:nil];
[NSThread sleepForTimeInterval:1.0];
NSArray<HttpdnsNWReusableConnection *> *connections1 = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections1.count, 1, @"Should have connection");
NSDate *date1 = connections1.firstObject.lastUsedDate;
XCTAssertNotNil(date1, @"lastUsedDate should be set");
// 等待1秒确保时间推进
[NSThread sleepForTimeInterval:1.0];
// 第2次使用同一连接
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"MonotonicTest"
timeout:15.0
error:nil];
[NSThread sleepForTimeInterval:1.0];
NSArray<HttpdnsNWReusableConnection *> *connections2 = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections2.count, 1, @"Should still have 1 connection");
NSDate *date2 = connections2.firstObject.lastUsedDate;
// 验证lastUsedDate 递增
XCTAssertTrue([date2 timeIntervalSinceDate:date1] > 0,
@"lastUsedDate should increase after reuse");
}
// Q5.1 超时+池溢出复合场景
- (void)testCompound_TimeoutDuringPoolOverflow_Handled {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 先填满池4个成功连接
NSMutableArray<XCTestExpectation *> *expectations = [NSMutableArray array];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (NSInteger i = 0; i < 4; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:
[NSString stringWithFormat:@"Fill pool %ld", (long)i]];
[expectations addObject:expectation];
dispatch_async(queue, ^{
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/2"
userAgent:@"CompoundTest"
timeout:15.0
error:nil];
[expectation fulfill];
});
[NSThread sleepForTimeInterval:0.05];
}
[self waitForExpectations:expectations timeout:20.0];
[NSThread sleepForTimeInterval:1.0];
NSUInteger poolCountBefore = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolCountBefore, 4, @"Pool should have ≤4 connections");
// 第5个请求超时
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:11080/delay/10"
userAgent:@"TimeoutRequest"
timeout:0.5
error:&error];
XCTAssertNil(response, @"Timeout request should return nil");
XCTAssertNotNil(error, @"Should have error");
[NSThread sleepForTimeInterval:1.0];
// 验证:超时连接未加入池
NSUInteger poolCountAfter = [self.client connectionPoolCountForKey:poolKey];
XCTAssertLessThanOrEqual(poolCountAfter, 4, @"Timed-out connection should not be added to pool");
}
// Q2.4 打开失败不加入池
- (void)testAbnormal_OpenFailure_NotAddedToPool {
[self.client resetPoolStatistics];
// 尝试连接无效端口(连接拒绝)
NSError *error = nil;
HttpdnsNWHTTPClientResponse *response = [self.client performRequestWithURLString:@"http://127.0.0.1:99999/get"
userAgent:@"FailureTest"
timeout:2.0
error:&error];
// 验证:请求失败
XCTAssertNil(response, @"Should fail to connect to invalid port");
// 验证:无连接加入池
XCTAssertEqual([self.client totalConnectionCount], 0,
@"Failed connection should not be added to pool");
}
// Q2.5 多次 invalidate 幂等性
- (void)testAbnormal_MultipleInvalidate_Idempotent {
[self.client resetPoolStatistics];
NSString *poolKey = @"127.0.0.1:11080:tcp";
// 创建连接
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"InvalidateTest"
timeout:15.0
error:nil];
[NSThread sleepForTimeInterval:1.0];
NSArray<HttpdnsNWReusableConnection *> *connections = [self.client connectionsInPoolForKey:poolKey];
XCTAssertEqual(connections.count, 1, @"Should have connection");
HttpdnsNWReusableConnection *conn = connections.firstObject;
// 多次 invalidate
[conn debugInvalidate];
[conn debugInvalidate];
[conn debugInvalidate];
// 验证:不崩溃
XCTAssertTrue(conn.isInvalidated, @"Connection should be invalidated");
}
// Q5.2 并发 dequeue 竞态测试
- (void)testCompound_ConcurrentDequeueDuringPrune_Safe {
[self.client resetPoolStatistics];
// 在两个端口创建连接
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"RaceTest"
timeout:15.0
error:nil];
[self.client performRequestWithURLString:@"http://127.0.0.1:11443/get"
userAgent:@"RaceTest"
timeout:15.0
error:nil];
[NSThread sleepForTimeInterval:1.0];
// 等待30秒让连接过期
[NSThread sleepForTimeInterval:30.5];
// 并发触发两个端口的 dequeue会触发 prune
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{
[self.client performRequestWithURLString:@"http://127.0.0.1:11080/get"
userAgent:@"Race1"
timeout:15.0
error:nil];
});
dispatch_group_async(group, queue, ^{
[self.client performRequestWithURLString:@"http://127.0.0.1:11443/get"
userAgent:@"Race2"
timeout:15.0
error:nil];
});
// 等待完成
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_SEC));
// 验证:无崩溃,连接池正常工作
NSUInteger totalCount = [self.client totalConnectionCount];
XCTAssertLessThanOrEqual(totalCount, 4, @"Pool should remain stable after concurrent prune");
}
@end