mod inc;
#[ cfg( feature = "inference-retry" ) ]
mod retry_tests
{
use api_huggingface::
{
Client,
ExplicitRetryConfig,
error::{ HuggingFaceError, ApiErrorWrap },
};
#[ test ]
fn test_explicit_retry_config_conservative()
{
let config = ExplicitRetryConfig::conservative();
assert_eq!( config.max_retries, 3 );
assert_eq!( config.initial_delay_ms, 1000 );
assert_eq!( config.max_delay_ms, 30_000 );
assert!( ( config.multiplier - 2.0 ).abs() < f64::EPSILON );
assert_eq!( config.jitter_ms, 100 );
}
#[ test ]
fn test_explicit_retry_config_aggressive()
{
let config = ExplicitRetryConfig::aggressive();
assert_eq!( config.max_retries, 5 );
assert_eq!( config.initial_delay_ms, 500 );
assert_eq!( config.max_delay_ms, 10_000 );
assert!( ( config.multiplier - 1.5 ).abs() < f64::EPSILON );
assert_eq!( config.jitter_ms, 50 );
}
#[ test ]
fn test_retryable_error_classification()
{
let http_error = HuggingFaceError::Http( "Connection timeout".to_string() );
assert!( is_error_retryable_test( &http_error ) );
let rate_limit_error = HuggingFaceError::RateLimit( "Too many requests".to_string() );
assert!( is_error_retryable_test( &rate_limit_error ) );
let model_error = HuggingFaceError::ModelUnavailable( "Model loading".to_string() );
assert!( is_error_retryable_test( &model_error ) );
let server_error = HuggingFaceError::Api(
ApiErrorWrap::new( "Internal server error" ).with_status_code( 500 )
);
assert!( is_error_retryable_test( &server_error ) );
let client_error = HuggingFaceError::Api(
ApiErrorWrap::new( "Bad request" ).with_status_code( 400 )
);
assert!( !is_error_retryable_test( &client_error ) );
let auth_error = HuggingFaceError::Authentication( "Invalid API key".to_string() );
assert!( !is_error_retryable_test( &auth_error ) );
let validation_error = HuggingFaceError::Validation( "Invalid input".to_string() );
assert!( !is_error_retryable_test( &validation_error ) );
}
#[ test ]
fn test_api_error_message_patterns()
{
let timeout_error = HuggingFaceError::Api(
ApiErrorWrap::new( "Request timeout occurred" )
);
assert!( is_error_retryable_test( &timeout_error ) );
let unavailable_error = HuggingFaceError::Api(
ApiErrorWrap::new( "Service temporarily unavailable" )
);
assert!( is_error_retryable_test( &unavailable_error ) );
let overloaded_error = HuggingFaceError::Api(
ApiErrorWrap::new( "Server overloaded, try again later" )
);
assert!( is_error_retryable_test( &overloaded_error ) );
let rate_limit_msg_error = HuggingFaceError::Api(
ApiErrorWrap::new( "Rate limit exceeded" )
);
assert!( is_error_retryable_test( &rate_limit_msg_error ) );
let invalid_error = HuggingFaceError::Api(
ApiErrorWrap::new( "Invalid model specified" )
);
assert!( !is_error_retryable_test( &invalid_error ) );
}
fn is_error_retryable_test( error : &HuggingFaceError ) -> bool
{
match error
{
HuggingFaceError::Http( _ ) |
HuggingFaceError::RateLimit( _ ) |
HuggingFaceError::ModelUnavailable( _ ) |
HuggingFaceError::Stream( _ ) => true,
HuggingFaceError::Api( api_error ) =>
{
if let Some( status_code ) = api_error.status_code
{
( 500..600 ).contains( &status_code )
}
else
{
let msg = api_error.message.to_lowercase();
msg.contains( "timeout" ) ||
msg.contains( "unavailable" ) ||
msg.contains( "overloaded" ) ||
msg.contains( "rate limit" ) ||
msg.contains( "service" )
}
},
HuggingFaceError::Authentication( _ ) |
HuggingFaceError::Validation( _ ) |
HuggingFaceError::Serialization( _ ) |
HuggingFaceError::InvalidArgument( _ ) |
HuggingFaceError::Generic( _ ) => false,
}
}
#[ cfg( feature = "env-config" ) ]
#[ tokio::test ]
async fn test_client_with_custom_retry_policy()
{
use api_huggingface::{ environment::HuggingFaceEnvironmentImpl, Secret };
let secret = Secret::new( "test_key".to_string() );
let env = HuggingFaceEnvironmentImpl::build( secret, None )
.expect( "Failed to create test environment" );
let _client = Client::build( env )
.expect( "Failed to build client" );
let explicit_retry_config = ExplicitRetryConfig {
max_retries : 2,
initial_delay_ms : 50,
multiplier : 1.8,
max_delay_ms : 5000,
jitter_ms : 25,
};
assert_eq!( explicit_retry_config.max_retries, 2 );
assert_eq!( explicit_retry_config.initial_delay_ms, 50 );
assert!( ( explicit_retry_config.multiplier - 1.8 ).abs() < f64::EPSILON );
}
#[ cfg( feature = "env-config" ) ]
#[ tokio::test ]
async fn test_client_explicit_retry_pattern()
{
use api_huggingface::{ environment::HuggingFaceEnvironmentImpl, Secret };
let secret = Secret::new( "test_key".to_string() );
let env = HuggingFaceEnvironmentImpl::build( secret, None )
.expect( "Failed to create test environment" );
let _client = Client::build( env )
.expect( "Failed to build client" );
let retry_config = ExplicitRetryConfig::conservative();
assert_eq!( retry_config.max_retries, 3 );
assert_eq!( retry_config.initial_delay_ms, 1000 );
}
#[ test ]
fn test_explicit_retry_config_custom()
{
let config = ExplicitRetryConfig {
max_retries : 3,
initial_delay_ms : 100,
multiplier : 3.0,
max_delay_ms : 10_000,
jitter_ms : 200,
};
assert_eq!( config.max_retries, 3 );
assert!( ( config.multiplier - 3.0 ).abs() < f64::EPSILON );
assert_eq!( config.jitter_ms, 200 );
}
#[ cfg( feature = "integration" ) ]
use api_huggingface::
{
environment::HuggingFaceEnvironmentImpl,
secret::Secret,
};
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn integration_explicit_retry_with_real_api_failures()
{
let invalid_key = Secret::new( "invalid-key-12345".to_string() );
let env = HuggingFaceEnvironmentImpl::build( invalid_key, None )
.expect( "Environment build should succeed" );
let client = Client::build( env )
.expect( "Client build should succeed" );
let retry_config = ExplicitRetryConfig {
max_retries : 2,
initial_delay_ms : 50,
multiplier : 2.0,
max_delay_ms : 1000,
jitter_ms : 10,
};
let start_time = std::time::Instant::now();
let result : Result< serde_json::Value, _ > = client.post_with_explicit_retry(
"https://api-inference.huggingface.co/models/microsoft/DialoGPT-medium",
&serde_json::json!({
"inputs": "test"
}),
&retry_config
).await;
let elapsed = start_time.elapsed();
assert!( result.is_err(), "Invalid key should cause failure" );
assert!( elapsed.as_millis() > 40, "Should have spent time on explicit retries : {elapsed:?}" );
assert_eq!( retry_config.max_retries, 2 );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn integration_retry_with_rate_limiting()
{
let api_key_string = crate::inc::get_api_key_for_integration();
let api_key = Secret::new( api_key_string );
let env = HuggingFaceEnvironmentImpl::build( api_key, None )
.expect( "Environment build should succeed" );
let client = Client::build( env )
.expect( "Client build should succeed" );
let retry_config = ExplicitRetryConfig {
max_retries : 3,
initial_delay_ms : 100,
multiplier : 1.5,
max_delay_ms : 2000,
jitter_ms : 50,
};
let mut results = Vec::new();
for i in 0..3
{
let result : Result< serde_json::Value, _ > = client.post_with_explicit_retry(
"https://api-inference.huggingface.co/models/microsoft/DialoGPT-medium",
&serde_json::json!({
"inputs": format!( "test retry {i}" ),
"parameters": {
"max_new_tokens": 5
}
}),
&retry_config
).await;
results.push( result );
}
let success_count = results.iter().filter( |r| r.is_ok() ).count();
let error_count = results.iter().filter( |r| r.is_err() ).count();
assert!(
success_count > 0 || error_count == 3,
"Should have either successes or all errors with retry attempts"
);
for result in &results
{
if let Err( error ) = result
{
let error_msg = format!( "{error}" );
assert!( !error_msg.is_empty(), "Error message should not be empty" );
}
}
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn integration_retry_disabled_vs_enabled()
{
let api_key_string = crate::inc::get_api_key_for_integration();
let api_key = Secret::new( api_key_string.clone() );
let env = HuggingFaceEnvironmentImpl::build( api_key, None )
.expect( "Environment build should succeed" );
let client_no_retry = Client::build( env )
.expect( "Client build should succeed" );
let api_key2 = Secret::new( api_key_string );
let env2 = HuggingFaceEnvironmentImpl::build( api_key2, None )
.expect( "Environment build should succeed" );
let client_with_retry = Client::build( env2 )
.expect( "Client build should succeed" );
let explicit_retry_config = ExplicitRetryConfig::conservative();
assert_eq!( explicit_retry_config.max_retries, 3 );
let result1 : Result< serde_json::Value, _ > = client_no_retry.post(
"https://api-inference.huggingface.co/models/microsoft/DialoGPT-medium",
&serde_json::json!({
"inputs": "test no retry",
"parameters": { "max_new_tokens": 5 }
})
).await;
let result2 : Result< serde_json::Value, _ > = client_with_retry.post_with_explicit_retry(
"https://api-inference.huggingface.co/models/microsoft/DialoGPT-medium",
&serde_json::json!({
"inputs": "test with retry",
"parameters": { "max_new_tokens": 5 }
}),
&explicit_retry_config
).await;
match ( &result1, &result2 )
{
( Ok( _ ), Ok( _ ) ) | ( Err( _ ), Err( _ ) ) => {}, ( Ok( _ ), Err( _ ) ) => panic!( "Retry client should not fail when no-retry succeeds" ),
( Err( e1 ), Ok( _ ) ) =>
{
println!( "No-retry failed : {e1}, retry client succeeded - this is expected behavior" );
}
}
}
}