Files
v-hono/tests/test_error_classification_property.v
T
qinqx e7760ea80d feat: integrate multi-cloud storage (S3, OSS, COS, Local)
Add built-in storage support from v-hono-storage module:
- StorageProvider interface with 4 backends
- FileService with HTTP handlers
- Multipart upload support
- 86 property-based tests
- Retry mechanism with exponential backoff

Breaking: None (backward compatible)
2026-01-25 14:07:34 +08:00

815 lines
28 KiB
V

module main
import rand
import time
// ============================================================================
// Property 8: Error Classification
// Feature: v-hono-upload-integration, Property 8: Error Classification
// Validates: Requirements 9.4
//
// *For any* storage operation failure, the error should be correctly classified
// as retryable (network timeout, temporary unavailable) or non-retryable
// (invalid credentials, file not found).
// ============================================================================
const test_iterations = 100
// ============================================================================
// Type definitions (copied from storage_errors.v for standalone testing)
// ============================================================================
// 存储错误类型枚举
enum StorageErrorKind {
// 可重试错误 - 网络和临时性问题
network_timeout
service_unavailable
rate_limited
connection_reset
temporary_failure
// 不可重试错误 - 配置和权限问题
invalid_credentials
access_denied
bucket_not_found
object_not_found
invalid_object_key
quota_exceeded
invalid_config
bucket_not_empty
bucket_already_exists
object_already_exists
invalid_range
checksum_mismatch
// 其他错误
unknown
}
// 重试尝试记录
struct RetryAttempt {
pub:
attempt_number int
timestamp i64
error_message string
delay_ms int
}
// 存储错误结构
struct StorageError {
pub:
kind StorageErrorKind
message string
provider string
operation string
http_status int
retry_count int
details map[string]string
pub mut:
retry_history []RetryAttempt
}
// 判断错误是否可重试
fn (e StorageError) is_retryable() bool {
return e.kind in [
.network_timeout,
.service_unavailable,
.rate_limited,
.connection_reset,
.temporary_failure,
]
}
// 获取建议的重试延迟(毫秒)
fn (e StorageError) suggested_retry_delay() int {
base_delay := match e.kind {
.rate_limited { 5000 }
.service_unavailable { 2000 }
.network_timeout { 1000 }
.connection_reset { 500 }
.temporary_failure { 1000 }
else { 0 }
}
return base_delay
}
// 获取错误类别描述
fn (e StorageError) category() string {
if e.is_retryable() {
return 'retryable'
}
return 'non_retryable'
}
// 实现 IError 接口
fn (e StorageError) msg() string {
return '${e.provider}/${e.operation}: ${e.message} (kind: ${e.kind}, http_status: ${e.http_status})'
}
// 添加重试记录
fn (mut e StorageError) add_retry_attempt(attempt_number int, error_message string, delay_ms int) {
e.retry_history << RetryAttempt{
attempt_number: attempt_number
timestamp: time.now().unix()
error_message: error_message
delay_ms: delay_ms
}
}
// 获取重试历史摘要
fn (e StorageError) retry_summary() string {
if e.retry_history.len == 0 {
return 'No retry attempts'
}
mut summary := 'Retry attempts: ${e.retry_history.len}\n'
for attempt in e.retry_history {
summary += ' Attempt ${attempt.attempt_number}: ${attempt.error_message} (delay: ${attempt.delay_ms}ms)\n'
}
return summary
}
// 创建存储错误的辅助函数
fn new_storage_error(kind StorageErrorKind, message string, provider string, operation string) StorageError {
return StorageError{
kind: kind
message: message
provider: provider
operation: operation
http_status: 0
retry_count: 0
details: map[string]string{}
retry_history: []RetryAttempt{}
}
}
// ============================================================================
// HTTP 状态码到错误类型的映射
// ============================================================================
fn error_kind_from_http_status(status int) StorageErrorKind {
return match status {
400 { StorageErrorKind.invalid_object_key }
401 { StorageErrorKind.invalid_credentials }
403 { StorageErrorKind.access_denied }
404 { StorageErrorKind.object_not_found }
409 { StorageErrorKind.bucket_already_exists }
413 { StorageErrorKind.quota_exceeded }
416 { StorageErrorKind.invalid_range }
429 { StorageErrorKind.rate_limited }
500 { StorageErrorKind.service_unavailable }
502 { StorageErrorKind.service_unavailable }
503 { StorageErrorKind.service_unavailable }
504 { StorageErrorKind.network_timeout }
else { StorageErrorKind.unknown }
}
}
// ============================================================================
// 提供者特定的错误映射
// ============================================================================
fn map_s3_error_code(error_code string, http_status int) StorageErrorKind {
return match error_code {
'AccessDenied' { StorageErrorKind.access_denied }
'AccountProblem' { StorageErrorKind.access_denied }
'AllAccessDisabled' { StorageErrorKind.access_denied }
'BucketAlreadyExists' { StorageErrorKind.bucket_already_exists }
'BucketAlreadyOwnedByYou' { StorageErrorKind.bucket_already_exists }
'BucketNotEmpty' { StorageErrorKind.bucket_not_empty }
'CredentialsNotSupported' { StorageErrorKind.invalid_credentials }
'ExpiredToken' { StorageErrorKind.invalid_credentials }
'InvalidAccessKeyId' { StorageErrorKind.invalid_credentials }
'InvalidBucketName' { StorageErrorKind.invalid_object_key }
'InvalidObjectName' { StorageErrorKind.invalid_object_key }
'InvalidRange' { StorageErrorKind.invalid_range }
'InvalidSecurity' { StorageErrorKind.invalid_credentials }
'InvalidToken' { StorageErrorKind.invalid_credentials }
'NoSuchBucket' { StorageErrorKind.bucket_not_found }
'NoSuchKey' { StorageErrorKind.object_not_found }
'NoSuchUpload' { StorageErrorKind.object_not_found }
'RequestTimeout' { StorageErrorKind.network_timeout }
'RequestTimeTooSkewed' { StorageErrorKind.invalid_credentials }
'ServiceUnavailable' { StorageErrorKind.service_unavailable }
'SignatureDoesNotMatch' { StorageErrorKind.invalid_credentials }
'SlowDown' { StorageErrorKind.rate_limited }
'TemporaryRedirect' { StorageErrorKind.temporary_failure }
'TokenRefreshRequired' { StorageErrorKind.invalid_credentials }
else { error_kind_from_http_status(http_status) }
}
}
fn map_aliyun_oss_error_code(error_code string, http_status int) StorageErrorKind {
return match error_code {
'AccessDenied' { StorageErrorKind.access_denied }
'BucketAlreadyExists' { StorageErrorKind.bucket_already_exists }
'BucketNotEmpty' { StorageErrorKind.bucket_not_empty }
'EntityTooLarge' { StorageErrorKind.quota_exceeded }
'EntityTooSmall' { StorageErrorKind.invalid_object_key }
'FileGroupTooLarge' { StorageErrorKind.quota_exceeded }
'InvalidAccessKeyId' { StorageErrorKind.invalid_credentials }
'InvalidArgument' { StorageErrorKind.invalid_object_key }
'InvalidBucketName' { StorageErrorKind.invalid_object_key }
'InvalidDigest' { StorageErrorKind.checksum_mismatch }
'InvalidObjectName' { StorageErrorKind.invalid_object_key }
'InvalidRange' { StorageErrorKind.invalid_range }
'NoSuchBucket' { StorageErrorKind.bucket_not_found }
'NoSuchKey' { StorageErrorKind.object_not_found }
'NoSuchUpload' { StorageErrorKind.object_not_found }
'RequestTimeout' { StorageErrorKind.network_timeout }
'RequestTimeTooSkewed' { StorageErrorKind.invalid_credentials }
'ServiceUnavailable' { StorageErrorKind.service_unavailable }
'SignatureDoesNotMatch' { StorageErrorKind.invalid_credentials }
'TooManyBuckets' { StorageErrorKind.quota_exceeded }
else { error_kind_from_http_status(http_status) }
}
}
fn map_tencent_cos_error_code(error_code string, http_status int) StorageErrorKind {
return match error_code {
'AccessDenied' { StorageErrorKind.access_denied }
'BucketAlreadyExists' { StorageErrorKind.bucket_already_exists }
'BucketAlreadyOwnedByYou' { StorageErrorKind.bucket_already_exists }
'BucketNotEmpty' { StorageErrorKind.bucket_not_empty }
'EntityTooLarge' { StorageErrorKind.quota_exceeded }
'EntityTooSmall' { StorageErrorKind.invalid_object_key }
'InvalidAccessKeyId' { StorageErrorKind.invalid_credentials }
'InvalidArgument' { StorageErrorKind.invalid_object_key }
'InvalidBucketName' { StorageErrorKind.invalid_object_key }
'InvalidDigest' { StorageErrorKind.checksum_mismatch }
'InvalidObjectName' { StorageErrorKind.invalid_object_key }
'InvalidRange' { StorageErrorKind.invalid_range }
'NoSuchBucket' { StorageErrorKind.bucket_not_found }
'NoSuchKey' { StorageErrorKind.object_not_found }
'NoSuchUpload' { StorageErrorKind.object_not_found }
'RequestTimeout' { StorageErrorKind.network_timeout }
'RequestTimeTooSkewed' { StorageErrorKind.invalid_credentials }
'ServiceUnavailable' { StorageErrorKind.service_unavailable }
'SignatureDoesNotMatch' { StorageErrorKind.invalid_credentials }
'SlowDown' { StorageErrorKind.rate_limited }
'TooManyBuckets' { StorageErrorKind.quota_exceeded }
else { error_kind_from_http_status(http_status) }
}
}
// ============================================================================
// XML 错误解析
// ============================================================================
fn extract_error_code_from_xml(body string) string {
if code_start := body.index('<Code>') {
if code_end := body.index('</Code>') {
return body[code_start + 6..code_end]
}
}
return ''
}
fn extract_error_message_from_xml(body string) string {
if message_start := body.index('<Message>') {
if message_end := body.index('</Message>') {
return body[message_start + 9..message_end]
}
}
return ''
}
// ============================================================================
// Test infrastructure
// ============================================================================
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
}
fn (mut stats PropertyTestStats) run_property_test(test_name string, test_func fn () bool) {
stats.total_tests++
print('🔬 ${test_name}... ')
if test_func() {
stats.passed_tests++
println('✅')
} else {
stats.failed_tests++
println('❌')
}
}
fn (stats PropertyTestStats) print_summary() {
println('\n=== Error Classification 属性测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有属性测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个属性测试失败')
}
}
// 生成随机字符串
fn generate_random_string(min_len int, max_len int) string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
len := rand.int_in_range(min_len, max_len) or { min_len }
mut result := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result += chars[idx].ascii_str()
}
return result
}
// 生成随机提供者名称
fn generate_random_provider() string {
providers := ['local', 's3', 'aliyun_oss', 'tencent_cos']
idx := rand.int_in_range(0, providers.len) or { 0 }
return providers[idx]
}
// 生成随机操作名称
fn generate_random_operation() string {
operations := ['upload', 'download', 'delete', 'exists', 'head', 'list', 'copy', 'presign_url']
idx := rand.int_in_range(0, operations.len) or { 0 }
return operations[idx]
}
// ============================================================================
// Property 8.1: Retryable Error Classification
// For any error of type network_timeout, service_unavailable, rate_limited,
// connection_reset, or temporary_failure, is_retryable() should return true
// ============================================================================
fn test_property_8_1_retryable_errors() bool {
retryable_kinds := [
StorageErrorKind.network_timeout,
StorageErrorKind.service_unavailable,
StorageErrorKind.rate_limited,
StorageErrorKind.connection_reset,
StorageErrorKind.temporary_failure,
]
for _ in 0 .. test_iterations {
// Pick a random retryable error kind
idx := rand.int_in_range(0, retryable_kinds.len) or { 0 }
kind := retryable_kinds[idx]
// Create error with random provider and operation
provider := generate_random_provider()
operation := generate_random_operation()
message := generate_random_string(10, 50)
err := new_storage_error(kind, message, provider, operation)
// Verify is_retryable returns true
if !err.is_retryable() {
println(' Failed: Error kind ${kind} should be retryable but is_retryable() returned false')
return false
}
// Verify category is 'retryable'
if err.category() != 'retryable' {
println(' Failed: Error kind ${kind} should have category "retryable" but got "${err.category()}"')
return false
}
}
return true
}
// ============================================================================
// Property 8.2: Non-Retryable Error Classification
// For any error of type invalid_credentials, access_denied, bucket_not_found,
// object_not_found, invalid_object_key, quota_exceeded, invalid_config,
// is_retryable() should return false
// ============================================================================
fn test_property_8_2_non_retryable_errors() bool {
non_retryable_kinds := [
StorageErrorKind.invalid_credentials,
StorageErrorKind.access_denied,
StorageErrorKind.bucket_not_found,
StorageErrorKind.object_not_found,
StorageErrorKind.invalid_object_key,
StorageErrorKind.quota_exceeded,
StorageErrorKind.invalid_config,
StorageErrorKind.bucket_not_empty,
StorageErrorKind.bucket_already_exists,
StorageErrorKind.object_already_exists,
StorageErrorKind.invalid_range,
StorageErrorKind.checksum_mismatch,
StorageErrorKind.unknown,
]
for _ in 0 .. test_iterations {
// Pick a random non-retryable error kind
idx := rand.int_in_range(0, non_retryable_kinds.len) or { 0 }
kind := non_retryable_kinds[idx]
// Create error with random provider and operation
provider := generate_random_provider()
operation := generate_random_operation()
message := generate_random_string(10, 50)
err := new_storage_error(kind, message, provider, operation)
// Verify is_retryable returns false
if err.is_retryable() {
println(' Failed: Error kind ${kind} should NOT be retryable but is_retryable() returned true')
return false
}
// Verify category is 'non_retryable'
if err.category() != 'non_retryable' {
println(' Failed: Error kind ${kind} should have category "non_retryable" but got "${err.category()}"')
return false
}
}
return true
}
// ============================================================================
// Property 8.3: HTTP Status Code Mapping
// For any HTTP status code, error_kind_from_http_status should return
// the correct error kind
// ============================================================================
fn test_property_8_3_http_status_mapping() bool {
// Define expected mappings
status_mappings := {
400: StorageErrorKind.invalid_object_key
401: StorageErrorKind.invalid_credentials
403: StorageErrorKind.access_denied
404: StorageErrorKind.object_not_found
409: StorageErrorKind.bucket_already_exists
413: StorageErrorKind.quota_exceeded
416: StorageErrorKind.invalid_range
429: StorageErrorKind.rate_limited
500: StorageErrorKind.service_unavailable
502: StorageErrorKind.service_unavailable
503: StorageErrorKind.service_unavailable
504: StorageErrorKind.network_timeout
}
for status, expected_kind in status_mappings {
actual_kind := error_kind_from_http_status(status)
if actual_kind != expected_kind {
println(' Failed: HTTP status ${status} should map to ${expected_kind} but got ${actual_kind}')
return false
}
}
// Test unknown status codes
unknown_statuses := [200, 201, 204, 301, 302, 418, 451]
for status in unknown_statuses {
kind := error_kind_from_http_status(status)
if kind != StorageErrorKind.unknown {
println(' Failed: HTTP status ${status} should map to unknown but got ${kind}')
return false
}
}
return true
}
// ============================================================================
// Property 8.4: S3 Error Code Mapping
// For any S3 error code, map_s3_error_code should return the correct error kind
// ============================================================================
fn test_property_8_4_s3_error_code_mapping() bool {
// Define expected mappings for S3 error codes
s3_mappings := {
'AccessDenied': StorageErrorKind.access_denied
'InvalidAccessKeyId': StorageErrorKind.invalid_credentials
'NoSuchBucket': StorageErrorKind.bucket_not_found
'NoSuchKey': StorageErrorKind.object_not_found
'BucketAlreadyExists': StorageErrorKind.bucket_already_exists
'BucketNotEmpty': StorageErrorKind.bucket_not_empty
'RequestTimeout': StorageErrorKind.network_timeout
'ServiceUnavailable': StorageErrorKind.service_unavailable
'SlowDown': StorageErrorKind.rate_limited
'InvalidRange': StorageErrorKind.invalid_range
'SignatureDoesNotMatch': StorageErrorKind.invalid_credentials
}
for error_code, expected_kind in s3_mappings {
actual_kind := map_s3_error_code(error_code, 0)
if actual_kind != expected_kind {
println(' Failed: S3 error code "${error_code}" should map to ${expected_kind} but got ${actual_kind}')
return false
}
}
return true
}
// ============================================================================
// Property 8.5: Aliyun OSS Error Code Mapping
// For any Aliyun OSS error code, map_aliyun_oss_error_code should return
// the correct error kind
// ============================================================================
fn test_property_8_5_aliyun_oss_error_code_mapping() bool {
// Define expected mappings for Aliyun OSS error codes
oss_mappings := {
'AccessDenied': StorageErrorKind.access_denied
'InvalidAccessKeyId': StorageErrorKind.invalid_credentials
'NoSuchBucket': StorageErrorKind.bucket_not_found
'NoSuchKey': StorageErrorKind.object_not_found
'BucketAlreadyExists': StorageErrorKind.bucket_already_exists
'BucketNotEmpty': StorageErrorKind.bucket_not_empty
'RequestTimeout': StorageErrorKind.network_timeout
'ServiceUnavailable': StorageErrorKind.service_unavailable
'InvalidRange': StorageErrorKind.invalid_range
'InvalidDigest': StorageErrorKind.checksum_mismatch
'SignatureDoesNotMatch': StorageErrorKind.invalid_credentials
'EntityTooLarge': StorageErrorKind.quota_exceeded
}
for error_code, expected_kind in oss_mappings {
actual_kind := map_aliyun_oss_error_code(error_code, 0)
if actual_kind != expected_kind {
println(' Failed: Aliyun OSS error code "${error_code}" should map to ${expected_kind} but got ${actual_kind}')
return false
}
}
return true
}
// ============================================================================
// Property 8.6: Tencent COS Error Code Mapping
// For any Tencent COS error code, map_tencent_cos_error_code should return
// the correct error kind
// ============================================================================
fn test_property_8_6_tencent_cos_error_code_mapping() bool {
// Define expected mappings for Tencent COS error codes
cos_mappings := {
'AccessDenied': StorageErrorKind.access_denied
'InvalidAccessKeyId': StorageErrorKind.invalid_credentials
'NoSuchBucket': StorageErrorKind.bucket_not_found
'NoSuchKey': StorageErrorKind.object_not_found
'BucketAlreadyExists': StorageErrorKind.bucket_already_exists
'BucketNotEmpty': StorageErrorKind.bucket_not_empty
'RequestTimeout': StorageErrorKind.network_timeout
'ServiceUnavailable': StorageErrorKind.service_unavailable
'SlowDown': StorageErrorKind.rate_limited
'InvalidRange': StorageErrorKind.invalid_range
'InvalidDigest': StorageErrorKind.checksum_mismatch
'SignatureDoesNotMatch': StorageErrorKind.invalid_credentials
'EntityTooLarge': StorageErrorKind.quota_exceeded
}
for error_code, expected_kind in cos_mappings {
actual_kind := map_tencent_cos_error_code(error_code, 0)
if actual_kind != expected_kind {
println(' Failed: Tencent COS error code "${error_code}" should map to ${expected_kind} but got ${actual_kind}')
return false
}
}
return true
}
// ============================================================================
// Property 8.7: Error Message Contains Required Information
// For any storage error, the error message should contain provider, operation,
// and error kind information
// ============================================================================
fn test_property_8_7_error_message_format() bool {
all_kinds := [
StorageErrorKind.network_timeout,
StorageErrorKind.service_unavailable,
StorageErrorKind.rate_limited,
StorageErrorKind.connection_reset,
StorageErrorKind.temporary_failure,
StorageErrorKind.invalid_credentials,
StorageErrorKind.access_denied,
StorageErrorKind.bucket_not_found,
StorageErrorKind.object_not_found,
StorageErrorKind.invalid_object_key,
StorageErrorKind.quota_exceeded,
StorageErrorKind.invalid_config,
]
for _ in 0 .. test_iterations {
// Pick a random error kind
idx := rand.int_in_range(0, all_kinds.len) or { 0 }
kind := all_kinds[idx]
provider := generate_random_provider()
operation := generate_random_operation()
message := generate_random_string(10, 50)
err := new_storage_error(kind, message, provider, operation)
error_msg := err.msg()
// Verify error message contains provider
if !error_msg.contains(provider) {
println(' Failed: Error message should contain provider "${provider}" but got: ${error_msg}')
return false
}
// Verify error message contains operation
if !error_msg.contains(operation) {
println(' Failed: Error message should contain operation "${operation}" but got: ${error_msg}')
return false
}
// Verify error message contains kind
if !error_msg.contains('kind:') {
println(' Failed: Error message should contain "kind:" but got: ${error_msg}')
return false
}
}
return true
}
// ============================================================================
// Property 8.8: Suggested Retry Delay for Retryable Errors
// For any retryable error, suggested_retry_delay should return a positive value
// ============================================================================
fn test_property_8_8_suggested_retry_delay() bool {
retryable_kinds := [
StorageErrorKind.network_timeout,
StorageErrorKind.service_unavailable,
StorageErrorKind.rate_limited,
StorageErrorKind.connection_reset,
StorageErrorKind.temporary_failure,
]
for _ in 0 .. test_iterations {
// Pick a random retryable error kind
idx := rand.int_in_range(0, retryable_kinds.len) or { 0 }
kind := retryable_kinds[idx]
provider := generate_random_provider()
operation := generate_random_operation()
message := generate_random_string(10, 50)
err := new_storage_error(kind, message, provider, operation)
delay := err.suggested_retry_delay()
// Verify delay is positive for retryable errors
if delay <= 0 {
println(' Failed: Retryable error kind ${kind} should have positive suggested_retry_delay but got ${delay}')
return false
}
}
// Verify non-retryable errors have zero delay
non_retryable_kinds := [
StorageErrorKind.invalid_credentials,
StorageErrorKind.access_denied,
StorageErrorKind.object_not_found,
]
for kind in non_retryable_kinds {
err := new_storage_error(kind, 'test', 'test', 'test')
delay := err.suggested_retry_delay()
if delay != 0 {
println(' Failed: Non-retryable error kind ${kind} should have zero suggested_retry_delay but got ${delay}')
return false
}
}
return true
}
// ============================================================================
// Property 8.9: XML Error Code Extraction
// For any XML error response, extract_error_code_from_xml should correctly
// extract the error code
// ============================================================================
fn test_property_8_9_xml_error_code_extraction() bool {
for _ in 0 .. test_iterations {
// Generate random error code
error_code := generate_random_string(5, 20)
error_message := generate_random_string(10, 50)
// Create XML response
xml_response := '<?xml version="1.0" encoding="UTF-8"?><Error><Code>${error_code}</Code><Message>${error_message}</Message></Error>'
// Extract error code
extracted_code := extract_error_code_from_xml(xml_response)
if extracted_code != error_code {
println(' Failed: Expected error code "${error_code}" but got "${extracted_code}"')
return false
}
// Extract error message
extracted_message := extract_error_message_from_xml(xml_response)
if extracted_message != error_message {
println(' Failed: Expected error message "${error_message}" but got "${extracted_message}"')
return false
}
}
// Test with empty/invalid XML
empty_code := extract_error_code_from_xml('')
if empty_code != '' {
println(' Failed: Empty XML should return empty error code but got "${empty_code}"')
return false
}
invalid_code := extract_error_code_from_xml('<Invalid>XML</Invalid>')
if invalid_code != '' {
println(' Failed: Invalid XML should return empty error code but got "${invalid_code}"')
return false
}
return true
}
// ============================================================================
// Property 8.10: Retry History Tracking
// For any storage error, retry history should be correctly tracked
// ============================================================================
fn test_property_8_10_retry_history_tracking() bool {
for _ in 0 .. test_iterations {
provider := generate_random_provider()
operation := generate_random_operation()
message := generate_random_string(10, 50)
mut err := new_storage_error(
StorageErrorKind.network_timeout,
message,
provider,
operation
)
// Initially, retry history should be empty
if err.retry_history.len != 0 {
println(' Failed: New error should have empty retry history')
return false
}
// Add some retry attempts
num_retries := rand.int_in_range(1, 5) or { 1 }
for i in 0 .. num_retries {
delay := rand.int_in_range(100, 5000) or { 1000 }
err.add_retry_attempt(i + 1, 'Retry attempt ${i + 1}', delay)
}
// Verify retry history length
if err.retry_history.len != num_retries {
println(' Failed: Expected ${num_retries} retry attempts but got ${err.retry_history.len}')
return false
}
// Verify retry summary is not empty
summary := err.retry_summary()
if summary == '' || summary == 'No retry attempts' {
println(' Failed: Retry summary should not be empty after adding attempts')
return false
}
// Verify each attempt is recorded correctly
for i, attempt in err.retry_history {
if attempt.attempt_number != i + 1 {
println(' Failed: Attempt number mismatch at index ${i}')
return false
}
}
}
return true
}
fn main() {
println('🚀 开始 Error Classification 属性测试...')
println('Feature: v-hono-upload-integration, Property 8: Error Classification')
println('Validates: Requirements 9.4')
println('每个属性测试运行 ${test_iterations} 次迭代\n')
rand.seed([u32(time.now().unix()), u32(54321)])
mut stats := PropertyTestStats{}
// Run property tests
stats.run_property_test('Property 8.1: Retryable Error Classification', test_property_8_1_retryable_errors)
stats.run_property_test('Property 8.2: Non-Retryable Error Classification', test_property_8_2_non_retryable_errors)
stats.run_property_test('Property 8.3: HTTP Status Code Mapping', test_property_8_3_http_status_mapping)
stats.run_property_test('Property 8.4: S3 Error Code Mapping', test_property_8_4_s3_error_code_mapping)
stats.run_property_test('Property 8.5: Aliyun OSS Error Code Mapping', test_property_8_5_aliyun_oss_error_code_mapping)
stats.run_property_test('Property 8.6: Tencent COS Error Code Mapping', test_property_8_6_tencent_cos_error_code_mapping)
stats.run_property_test('Property 8.7: Error Message Contains Required Information', test_property_8_7_error_message_format)
stats.run_property_test('Property 8.8: Suggested Retry Delay for Retryable Errors', test_property_8_8_suggested_retry_delay)
stats.run_property_test('Property 8.9: XML Error Code Extraction', test_property_8_9_xml_error_code_extraction)
stats.run_property_test('Property 8.10: Retry History Tracking', test_property_8_10_retry_history_tracking)
// Print summary
stats.print_summary()
// Exit with error code if any tests failed
if stats.failed_tests > 0 {
exit(1)
}
}