mod inc;
use api_huggingface::
{
Client,
environment::HuggingFaceEnvironmentImpl,
secret::Secret,
components::
{
embeddings::
{
EmbeddingRequest, EmbeddingResponse, EmbeddingOptions, EmbeddingInput, PoolingStrategy,
},
},
error::{ HuggingFaceError, Result },
};
fn create_test_client() -> Result< Client< HuggingFaceEnvironmentImpl > >
{
let api_key = Secret::new( "test-api-key".to_string() );
let env = HuggingFaceEnvironmentImpl::build( api_key, None )?;
Client::build( env )
}
#[ tokio::test ]
async fn test_embeddings_api_creation()
{
let client = create_test_client().expect( "[test_embeddings_api_creation] Failed to create test client with test-api-key - check HuggingFaceEnvironmentImpl::build() and Client::build() implementations" );
let embeddings = client.embeddings();
assert!( core::mem::size_of_val( &embeddings ) > 0, "Embeddings API group should be created" );
}
#[ test ]
fn test_embedding_request_single_construction()
{
let input_text = "Hello, world!";
let request = EmbeddingRequest::new( input_text );
assert!( matches!( request.inputs, EmbeddingInput::Single( ref text ) if text == input_text ) );
assert!( request.options.is_none() );
}
#[ test ]
fn test_embedding_request_batch_construction()
{
let input_texts = vec![ "Hello".to_string(), "World".to_string() ];
let request = EmbeddingRequest::new_batch( input_texts.clone() );
assert!( matches!( request.inputs, EmbeddingInput::Batch( ref texts ) if *texts == input_texts ) );
assert!( request.options.is_none() );
}
#[ test ]
fn test_embedding_request_with_options()
{
let input_text = "Generate embeddings with custom options";
let options = EmbeddingOptions
{
use_cache : Some( false ),
wait_for_model : Some( true ),
normalize : Some( true ),
pooling : Some( PoolingStrategy::Max ),
};
let request = EmbeddingRequest::new( input_text ).with_options( options.clone() );
assert!( matches!( request.inputs, EmbeddingInput::Single( ref text ) if text == input_text ) );
assert!( request.options.is_some() );
let req_options = request.options.as_ref().expect( "[test_embedding_request_with_options] EmbeddingRequest options should be Some after with_options() call - check EmbeddingRequest::with_options() implementation" );
assert_eq!( req_options.use_cache, Some( false ) );
assert_eq!( req_options.wait_for_model, Some( true ) );
assert_eq!( req_options.normalize, Some( true ) );
assert_eq!( req_options.pooling, Some( PoolingStrategy::Max ) );
}
#[ test ]
fn test_embedding_options_defaults()
{
let default_options = EmbeddingOptions::default();
assert_eq!( default_options.use_cache, Some( true ) );
assert_eq!( default_options.wait_for_model, Some( true ) );
assert_eq!( default_options.normalize, Some( true ) );
assert_eq!( default_options.pooling, Some( PoolingStrategy::Mean ) );
}
#[ test ]
fn test_pooling_strategy_variants()
{
let mean = PoolingStrategy::Mean;
let max = PoolingStrategy::Max;
let cls = PoolingStrategy::Cls;
assert_eq!( mean, PoolingStrategy::Mean );
assert_eq!( max, PoolingStrategy::Max );
assert_eq!( cls, PoolingStrategy::Cls );
assert_ne!( mean, max );
assert_ne!( max, cls );
assert_ne!( cls, mean );
}
#[ test ]
fn test_embedding_input_serialization()
{
let single_input = EmbeddingInput::Single( "Test text".to_string() );
let batch_input = EmbeddingInput::Batch( vec![ "Text 1".to_string(), "Text 2".to_string() ] );
let single_json = serde_json::to_string( &single_input );
let batch_json = serde_json::to_string( &batch_input );
assert!( single_json.is_ok(), "Single input should serialize" );
assert!( batch_json.is_ok(), "Batch input should serialize" );
let single_str = single_json.expect( "[test_embedding_input_serialization] Single EmbeddingInput serialization failed after is_ok() check - check serde_json::to_string() implementation" );
let batch_str = batch_json.expect( "[test_embedding_input_serialization] Batch EmbeddingInput serialization failed after is_ok() check - check serde_json::to_string() implementation" );
assert!( single_str.contains( "Test text" ), "Serialized single should contain text" );
assert!( batch_str.contains( "Text 1" ), "Serialized batch should contain first text" );
assert!( batch_str.contains( "Text 2" ), "Serialized batch should contain second text" );
}
#[ test ]
fn test_embedding_request_serialization()
{
let input_text = "Serialize this embedding request";
let options = EmbeddingOptions
{
use_cache : Some( false ),
wait_for_model : Some( true ),
normalize : Some( false ),
pooling : Some( PoolingStrategy::Cls ),
};
let request = EmbeddingRequest::new( input_text ).with_options( options );
let serialized = serde_json::to_string( &request );
assert!( serialized.is_ok(), "Request serialization should succeed" );
let json_str = serialized.expect( "[test_embedding_request_serialization] EmbeddingRequest serialization failed after is_ok() check - check serde_json::to_string() implementation" );
assert!( json_str.contains( "Serialize this embedding request" ), "JSON should contain input text" );
assert!( json_str.contains( "false" ), "JSON should contain use_cache false" );
assert!( json_str.contains( "cls" ), "JSON should contain pooling strategy" );
}
#[ test ]
fn test_embedding_input_text_validation()
{
use api_huggingface::validation::validate_input_text;
let valid_inputs = vec![
"Hello, world!",
"This is a sentence for embedding generation.",
"Mixed Unicode : Hello 🌍! 你好世界 مرحبا بالعالم",
];
for input in valid_inputs
{
let result = validate_input_text( input );
assert!( result.is_ok(), "Input should be valid : '{input}'" );
}
let result = validate_input_text( "" );
assert!( result.is_err(), "Empty input should be invalid" );
let long_input = "a".repeat( 60_000 );
let result = validate_input_text( &long_input );
assert!( result.is_err(), "Excessively long input should be invalid" );
}
#[ test ]
fn test_embedding_model_identifier_validation()
{
use api_huggingface::validation::validate_model_identifier;
let valid_models = vec![
"sentence-transformers/all-MiniLM-L6-v2",
"sentence-transformers/all-mpnet-base-v2",
"microsoft/DialoGPT-medium",
"distilbert-base-uncased",
];
for model in valid_models
{
let result = validate_model_identifier( model );
assert!( result.is_ok(), "Model '{model}' should be valid" );
}
let invalid_models = vec![
"", " ", "model with spaces", "/leading-slash", "trailing-slash/", "double//slash", ];
for model in invalid_models
{
let result = validate_model_identifier( model );
assert!( result.is_err(), "Model '{model}' should be invalid" );
}
}
#[ test ]
fn test_embedding_batch_validation()
{
use api_huggingface::validation::validate_batch_inputs;
let valid_batches = vec![
vec![ "Single item".to_string() ],
vec![ "First".to_string(), "Second".to_string() ],
vec![ "One".to_string(), "Two".to_string(), "Three".to_string() ],
];
for batch in valid_batches
{
let result = validate_batch_inputs( &batch );
assert!( result.is_ok(), "Batch with {} items should be valid", batch.len() );
}
let empty_batch : Vec< String > = vec![];
let result = validate_batch_inputs( &empty_batch );
assert!( result.is_err(), "Empty batch should be invalid" );
let large_batch : Vec< String > = ( 0..1001 ).map( | i | format!( "input_{i}" ) ).collect();
let result = validate_batch_inputs( &large_batch );
assert!( result.is_err(), "Batch with 1001 items should be invalid" );
let invalid_batch = vec![ "Valid input".to_string(), String::new() ];
let result = validate_batch_inputs( &invalid_batch );
assert!( result.is_err(), "Batch with empty string should be invalid" );
}
#[ test ]
fn test_cosine_similarity_basic()
{
let vec_a = vec![ 1.0, 0.0, 0.0 ];
let vec_b = vec![ 1.0, 0.0, 0.0 ]; let vec_c = vec![ 0.0, 1.0, 0.0 ]; let vec_d = vec![ -1.0, 0.0, 0.0 ];
fn test_cosine_similarity( a : &[ f32 ], b : &[ f32 ] ) -> Result< f32 >
{
if a.len() != b.len()
{
return Err( HuggingFaceError::InvalidArgument(
"Vectors must have the same dimension".to_string()
) );
}
let dot_product : f32 = a.iter().zip( b.iter() ).map( | ( x, y ) | x * y ).sum();
let magnitude_a : f32 = a.iter().map( | x | x * x ).sum::< f32 >().sqrt();
let magnitude_b : f32 = b.iter().map( | x | x * x ).sum::< f32 >().sqrt();
if magnitude_a == 0.0 || magnitude_b == 0.0
{
return Err( HuggingFaceError::Generic(
"Cannot compute similarity with zero magnitude vector".to_string()
) );
}
let similarity = dot_product / ( magnitude_a * magnitude_b );
Ok( similarity.clamp( -1.0, 1.0 ) )
}
let sim_identical = test_cosine_similarity( &vec_a, &vec_b ).expect( "[test_cosine_similarity_basic] Cosine similarity calculation failed for identical vectors [1,0,0] and [1,0,0] - check test_cosine_similarity() implementation" );
assert!( ( sim_identical - 1.0 ).abs() < 1e-6, "Identical vectors should have similarity ~1.0, got {sim_identical}" );
let sim_orthogonal = test_cosine_similarity( &vec_a, &vec_c ).expect( "[test_cosine_similarity_basic] Cosine similarity calculation failed for orthogonal vectors [1,0,0] and [0,1,0] - check test_cosine_similarity() implementation" );
assert!( sim_orthogonal.abs() < 1e-6, "Orthogonal vectors should have similarity ~0.0, got {sim_orthogonal}" );
let sim_opposite = test_cosine_similarity( &vec_a, &vec_d ).expect( "[test_cosine_similarity_basic] Cosine similarity calculation failed for opposite vectors [1,0,0] and [-1,0,0] - check test_cosine_similarity() implementation" );
assert!( ( sim_opposite + 1.0 ).abs() < 1e-6, "Opposite vectors should have similarity ~-1.0, got {sim_opposite}" );
}
#[ test ]
fn test_cosine_similarity_error_cases()
{
fn test_cosine_similarity( a : &[ f32 ], b : &[ f32 ] ) -> Result< f32 >
{
if a.len() != b.len()
{
return Err( HuggingFaceError::InvalidArgument(
"Vectors must have the same dimension".to_string()
) );
}
let dot_product : f32 = a.iter().zip( b.iter() ).map( | ( x, y ) | x * y ).sum();
let magnitude_a : f32 = a.iter().map( | x | x * x ).sum::< f32 >().sqrt();
let magnitude_b : f32 = b.iter().map( | x | x * x ).sum::< f32 >().sqrt();
if magnitude_a == 0.0 || magnitude_b == 0.0
{
return Err( HuggingFaceError::Generic(
"Cannot compute similarity with zero magnitude vector".to_string()
) );
}
let similarity = dot_product / ( magnitude_a * magnitude_b );
Ok( similarity.clamp( -1.0, 1.0 ) )
}
let vec_normal = vec![ 1.0, 2.0, 3.0 ];
let vec_different_size = vec![ 1.0, 2.0 ];
let vec_zero = vec![ 0.0, 0.0, 0.0 ];
let result = test_cosine_similarity( &vec_normal, &vec_different_size );
assert!( result.is_err(), "Different sized vectors should error" );
if let Err( HuggingFaceError::InvalidArgument( msg ) ) = result
{
assert!( msg.contains( "same dimension" ), "Error should mention dimension mismatch" );
}
else
{
panic!( "Expected InvalidArgument error for dimension mismatch" );
}
let result = test_cosine_similarity( &vec_normal, &vec_zero );
assert!( result.is_err(), "Zero magnitude vector should error" );
if let Err( HuggingFaceError::Generic( msg ) ) = result
{
assert!( msg.to_lowercase().contains( "zero magnitude" ), "Error should mention zero magnitude" );
}
else
{
panic!( "Expected Generic error for zero magnitude vector" );
}
}
#[ test ]
fn test_cosine_similarity_vector_types()
{
fn test_cosine_similarity( a : &[ f32 ], b : &[ f32 ] ) -> Result< f32 >
{
if a.len() != b.len()
{
return Err( HuggingFaceError::InvalidArgument(
"Vectors must have the same dimension".to_string()
) );
}
let dot_product : f32 = a.iter().zip( b.iter() ).map( | ( x, y ) | x * y ).sum();
let magnitude_a : f32 = a.iter().map( | x | x * x ).sum::< f32 >().sqrt();
let magnitude_b : f32 = b.iter().map( | x | x * x ).sum::< f32 >().sqrt();
if magnitude_a == 0.0 || magnitude_b == 0.0
{
return Err( HuggingFaceError::Generic(
"Cannot compute similarity with zero magnitude vector".to_string()
) );
}
let similarity = dot_product / ( magnitude_a * magnitude_b );
Ok( similarity.clamp( -1.0, 1.0 ) )
}
let vec_positive = vec![ 1.0, 2.0, 3.0 ];
let vec_negative = vec![ -1.0, -2.0, -3.0 ];
let vec_mixed = vec![ 1.0, -2.0, 3.0 ];
let vec_small = vec![ 0.001, 0.002, 0.003 ];
let vec_large = vec![ 1000.0, 2000.0, 3000.0 ];
let sim = test_cosine_similarity( &vec_positive, &vec_negative ).expect( "[test_cosine_similarity_vector_types] Cosine similarity calculation failed for opposite-signed vectors - check test_cosine_similarity() implementation" );
assert!( ( sim + 1.0 ).abs() < 1e-6, "Opposite signed vectors should have similarity ~-1.0" );
let sim = test_cosine_similarity( &vec_positive, &vec_mixed ).expect( "[test_cosine_similarity_vector_types] Cosine similarity calculation failed for mixed-sign vectors - check test_cosine_similarity() implementation" );
assert!( sim > -1.0 && sim < 1.0, "Mixed similarity should be between -1 and 1, got {sim}" );
let sim = test_cosine_similarity( &vec_small, &vec_large ).expect( "[test_cosine_similarity_vector_types] Cosine similarity calculation failed for scale-invariant vectors - check test_cosine_similarity() implementation" );
assert!( ( sim - 1.0 ).abs() < 1e-5, "Scale invariant vectors should have similarity ~1.0, got {sim}" );
}
#[ test ]
fn test_embedding_error_message_formatting()
{
use api_huggingface::validation::validate_batch_inputs;
let empty_batch : Vec< String > = vec![];
let result = validate_batch_inputs( &empty_batch );
assert!( result.is_err(), "Should produce validation error" );
if let Err( error ) = result
{
let error_string = error.to_string();
assert!( error_string.to_lowercase().contains( "empty" ), "Error should mention empty batch" );
}
else
{
panic!( "Expected error result" );
}
}
#[ test ]
fn test_embedding_response_variants()
{
let single_response = EmbeddingResponse::Single( vec![ vec![ 1.0, 2.0, 3.0 ] ] );
let batch_response = EmbeddingResponse::Batch( vec![
vec![ vec![ 1.0, 2.0 ], vec![ 3.0, 4.0 ] ],
vec![ vec![ 5.0, 6.0 ], vec![ 7.0, 8.0 ] ],
] );
match single_response
{
EmbeddingResponse::Single( ref embeddings ) =>
{
assert_eq!( embeddings.len(), 1, "Single response should have one embedding" );
assert_eq!( embeddings[ 0 ].len(), 3, "Embedding should have 3 dimensions" );
},
EmbeddingResponse::Batch( _ ) => panic!( "Expected Single variant" ),
}
match batch_response
{
EmbeddingResponse::Batch( ref embeddings ) =>
{
assert_eq!( embeddings.len(), 2, "Batch response should have 2 embeddings" );
assert_eq!( embeddings[ 0 ].len(), 2, "First embedding should have 2 vectors" );
assert_eq!( embeddings[ 1 ].len(), 2, "Second embedding should have 2 vectors" );
},
EmbeddingResponse::Single( _ ) => panic!( "Expected Batch variant" ),
}
}
#[ test ]
fn test_embedding_vector_performance()
{
use std::time::Instant;
fn test_cosine_similarity( a : &[ f32 ], b : &[ f32 ] ) -> Result< f32 >
{
if a.len() != b.len()
{
return Err( HuggingFaceError::InvalidArgument(
"Vectors must have the same dimension".to_string()
) );
}
let dot_product : f32 = a.iter().zip( b.iter() ).map( | ( x, y ) | x * y ).sum();
let magnitude_a : f32 = a.iter().map( | x | x * x ).sum::< f32 >().sqrt();
let magnitude_b : f32 = b.iter().map( | x | x * x ).sum::< f32 >().sqrt();
if magnitude_a == 0.0 || magnitude_b == 0.0
{
return Err( HuggingFaceError::Generic(
"Cannot compute similarity with zero magnitude vector".to_string()
) );
}
let similarity = dot_product / ( magnitude_a * magnitude_b );
Ok( similarity.clamp( -1.0, 1.0 ) )
}
let large_vec_a : Vec< f32 > = ( 0..1000 ).map( | i | i as f32 ).collect();
let large_vec_b : Vec< f32 > = ( 0..1000 ).map( | i | ( i * 2 ) as f32 ).collect();
let start = Instant::now();
let result = test_cosine_similarity( &large_vec_a, &large_vec_b );
let duration = start.elapsed();
assert!( result.is_ok(), "Large vector similarity should succeed" );
assert!( duration.as_millis() < 100, "Similarity calculation should be fast, took {}ms", duration.as_millis() );
let similarity = result.expect( "[test_embedding_vector_performance] Cosine similarity calculation failed for 1000-element vectors after is_ok() check - check test_cosine_similarity() implementation" );
const EPSILON : f32 = 1e-6;
assert!( ( -1.0 - EPSILON..=1.0 + EPSILON ).contains( &similarity ), "Similarity should be in valid range [-1, 1] (±ε), got {similarity}" );
}
#[ test ]
fn test_three_phase_pattern_example()
{
let test_input = "Test input for embeddings";
let expected_input = test_input;
let request = EmbeddingRequest::new( test_input );
assert!( matches!( request.inputs, EmbeddingInput::Single( ref text ) if text == expected_input ) );
assert!( request.options.is_none() );
}
#[ test ]
fn test_comprehensive_embedding_request()
{
let input_texts = vec![ "First text".to_string(), "Second text".to_string() ];
let options = EmbeddingOptions
{
use_cache : Some( true ),
wait_for_model : Some( false ),
normalize : Some( true ),
pooling : Some( PoolingStrategy::Mean ),
};
let request = EmbeddingRequest::new_batch( input_texts.clone() )
.with_options( options );
assert!( matches!( request.inputs, EmbeddingInput::Batch( ref texts ) if *texts == input_texts ) );
assert!( request.options.is_some() );
let opts = request.options.as_ref().expect( "[test_comprehensive_embedding_request] EmbeddingRequest options should be Some after with_options() call - check EmbeddingRequest::with_options() implementation" );
assert_eq!( opts.use_cache, Some( true ) );
assert_eq!( opts.wait_for_model, Some( false ) );
assert_eq!( opts.normalize, Some( true ) );
assert_eq!( opts.pooling, Some( PoolingStrategy::Mean ) );
}
#[ cfg( feature = "integration" ) ]
fn create_integration_environment() -> HuggingFaceEnvironmentImpl
{
let api_key_string = crate::inc::get_api_key_for_integration();
let api_key = Secret::new( api_key_string );
HuggingFaceEnvironmentImpl::build( api_key, None )
.expect( "[create_integration_environment] Failed to create HuggingFace environment with workspace API key - check HUGGINGFACE_API_KEY validity and HuggingFaceEnvironmentImpl::build() implementation" )
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn integration_embedding_create()
{
let env = create_integration_environment();
let client = Client::build( env )
.expect( "[integration_embedding_create] Failed to create Client from integration environment - check Client::build() implementation and API key configuration" );
let embeddings = client.embeddings();
let result = embeddings.create(
"Hello, this is a test sentence for embedding generation.",
"BAAI/bge-large-en-v1.5"
).await;
match result
{
Ok( _response ) =>
{
println!( "Integration test successful - received embedding response" );
},
Err( e ) => panic!( "Integration test failed: {e}" ),
}
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn integration_embedding_create_batch()
{
let env = create_integration_environment();
let client = Client::build( env )
.expect( "[integration_embedding_create_batch] Failed to create Client from integration environment - check Client::build() implementation and API key configuration" );
let embeddings = client.embeddings();
let input_texts = vec![
"First sentence for embedding.".to_string(),
"Second sentence for embedding.".to_string()
];
let result = embeddings.create_batch(
input_texts,
"BAAI/bge-large-en-v1.5"
).await;
match result
{
Ok( _response ) =>
{
println!( "Integration batch test successful" );
},
Err( e ) => panic!( "Integration batch test failed: {e}" ),
}
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn integration_similarity_calculation()
{
let env = create_integration_environment();
let client = Client::build( env )
.expect( "[integration_similarity_calculation] Failed to create Client from integration environment - check Client::build() implementation and API key configuration" );
let embeddings = client.embeddings();
let result = embeddings.similarity(
"The cat sat on the mat.",
"A feline rested on the rug.",
"BAAI/bge-large-en-v1.5"
).await;
match result
{
Ok( similarity ) =>
{
println!( "Integration similarity test successful - similarity : {similarity}" );
assert!( ( -1.0..=1.0 ).contains( &similarity ), "Similarity should be in valid range" );
},
Err( e ) => panic!( "Integration similarity test failed: {e}" ),
}
}
#[ test ]
fn test_cosine_similarity_clamping()
{
fn clamped_cosine( a : &[ f32 ], b : &[ f32 ] ) -> f32
{
let dot : f32 = a.iter().zip( b.iter() ).map( | ( x, y ) | x * y ).sum();
let mag_a : f32 = a.iter().map( | x | x * x ).sum::< f32 >().sqrt();
let mag_b : f32 = b.iter().map( | x | x * x ).sum::< f32 >().sqrt();
( dot / ( mag_a * mag_b ) ).clamp( -1.0, 1.0 )
}
let big_vec : Vec< f32 > = ( 0..1024 ).map( | i | ( i as f32 ).sin() ).collect();
let sim = clamped_cosine( &big_vec, &big_vec );
assert!(
( -1.0..=1.0 ).contains( &sim ),
"Similarity must be in [-1.0, 1.0], got {sim}"
);
assert!( sim <= 1.0, "Similarity must not exceed 1.0, got {sim}" );
let neg_vec : Vec< f32 > = big_vec.iter().map( | x | -x ).collect();
let sim_opp = clamped_cosine( &big_vec, &neg_vec );
assert!(
( -1.0..=1.0 ).contains( &sim_opp ),
"Opposite similarity must be in [-1.0, 1.0], got {sim_opp}"
);
assert!( sim_opp >= -1.0, "Similarity must not go below -1.0, got {sim_opp}" );
}
#[ test ]
fn test_embedding_authentication_error_handling()
{
let empty_key = Secret::new( String::new() );
let env_result = HuggingFaceEnvironmentImpl::build( empty_key, None );
match env_result
{
Ok( _env ) =>
{
},
Err( e ) =>
{
match e
{
HuggingFaceError::Authentication( _ ) => {}, other => panic!( "Expected Authentication error, got : {other:?}" ),
}
}
}
}