恢复 image_provider 单元测试模块
image_provider/mod.rs 重新声明 #[cfg(test)] mod tests,tests.rs 去掉失效的模块包裹并补齐 helper 导入 提取器 extract_image_urls / extract_b64_images 放宽到 pub(crate),parse_reference_image_data_url 调用补 provider 参数 补回 5 个此前被静默跳过的单元测试
This commit is contained in:
@@ -3,6 +3,9 @@ pub(crate) mod protocol;
|
||||
pub(crate) mod runtime;
|
||||
pub(crate) mod transport;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(crate) use protocol::{payload, request, response};
|
||||
pub(crate) use runtime::{audit, budget, error, image_source, types, util};
|
||||
pub(crate) use transport::curl as curl_transport;
|
||||
|
||||
@@ -72,7 +72,7 @@ pub(super) fn extract_generation_id(payload: &Value) -> Option<String> {
|
||||
.or_else(|| find_first_string_by_key(payload, "request_id"))
|
||||
}
|
||||
|
||||
pub(super) fn extract_image_urls(payload: &Value) -> Vec<String> {
|
||||
pub(crate) fn extract_image_urls(payload: &Value) -> Vec<String> {
|
||||
let mut urls = Vec::new();
|
||||
collect_strings_by_key(payload, "url", &mut urls);
|
||||
collect_strings_by_key(payload, "image", &mut urls);
|
||||
@@ -86,7 +86,7 @@ pub(super) fn extract_image_urls(payload: &Value) -> Vec<String> {
|
||||
deduped
|
||||
}
|
||||
|
||||
pub(super) fn extract_b64_images(payload: &Value) -> Vec<String> {
|
||||
pub(crate) fn extract_b64_images(payload: &Value) -> Vec<String> {
|
||||
let mut values = Vec::new();
|
||||
collect_strings_by_key(payload, "b64_json", &mut values);
|
||||
collect_inline_image_data(payload, &mut values);
|
||||
|
||||
@@ -1,223 +1,223 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use serde_json::json;
|
||||
use super::*;
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn request_body_normalizes_size_prompt_and_candidate_count() {
|
||||
let body = build_image_request_body(
|
||||
" 风雨夜里的街道 ",
|
||||
Some(" 低清,水印 "),
|
||||
" 1:1 ",
|
||||
10,
|
||||
&["data:image/png;base64,AAAA".to_string()],
|
||||
);
|
||||
use super::image_source::{decode_generated_image_base64, parse_reference_image_data_url};
|
||||
use super::payload::{extract_b64_images, extract_image_urls};
|
||||
|
||||
assert_eq!(body["model"], GPT_IMAGE_2_5_GENERATION_MODEL);
|
||||
assert_eq!(body["size"], "1024x1024");
|
||||
assert_eq!(body["n"], 4);
|
||||
assert_eq!(body["prompt"], "风雨夜里的街道\n避免:低清,水印");
|
||||
assert!(body.get("image").is_none());
|
||||
}
|
||||
#[test]
|
||||
fn request_body_normalizes_size_prompt_and_candidate_count() {
|
||||
let body = build_image_request_body(
|
||||
" 风雨夜里的街道 ",
|
||||
Some(" 低清,水印 "),
|
||||
" 1:1 ",
|
||||
10,
|
||||
&["data:image/png;base64,AAAA".to_string()],
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn provider_urls_normalize_root_and_v1_base_urls() {
|
||||
let root_settings = ImageProviderSettings {
|
||||
provider: ImageProvider::VectorEngine,
|
||||
base_url: "https://vector.example".to_string(),
|
||||
api_key: "test-key".to_string(),
|
||||
request_timeout_ms: 1_000,
|
||||
request_deadline: None,
|
||||
};
|
||||
let v1_settings = ImageProviderSettings {
|
||||
provider: ImageProvider::VectorEngine,
|
||||
base_url: "https://vector.example/v1".to_string(),
|
||||
api_key: "test-key".to_string(),
|
||||
request_timeout_ms: 1_000,
|
||||
request_deadline: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
images_generation_url(&root_settings),
|
||||
"https://vector.example/v1/images/generations"
|
||||
);
|
||||
assert_eq!(
|
||||
images_generation_url(&v1_settings),
|
||||
"https://vector.example/v1/images/generations"
|
||||
);
|
||||
assert_eq!(
|
||||
images_edit_url(&root_settings),
|
||||
"https://vector.example/v1/images/edits"
|
||||
);
|
||||
assert_eq!(
|
||||
images_edit_url(&v1_settings),
|
||||
"https://vector.example/v1/images/edits"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_url_and_base64_image_decoding_preserves_image_metadata() {
|
||||
let data_url = format!(
|
||||
"data:image/png;base64,{}",
|
||||
BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest")
|
||||
);
|
||||
|
||||
let reference = parse_reference_image_data_url(&data_url, 2)
|
||||
.expect("data url should parse")
|
||||
.expect("image data url should be accepted");
|
||||
assert_eq!(reference.file_name, "reference-2.png");
|
||||
assert_eq!(reference.mime_type, "image/png");
|
||||
assert_eq!(reference.bytes, b"\x89PNG\r\n\x1A\nrest");
|
||||
|
||||
let image = decode_generated_image_base64(
|
||||
BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest").as_str(),
|
||||
)
|
||||
.expect("base64 image should decode");
|
||||
assert_eq!(image.extension, "png");
|
||||
assert_eq!(image.mime_type, "image/png");
|
||||
assert_eq!(image.bytes, b"\x89PNG\r\n\x1A\nrest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_status_hints_and_audit_fields_are_structured() {
|
||||
let audit = PlatformImageFailureAudit {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
endpoint: "https://vector.example/v1/images/generations".to_string(),
|
||||
operation: "图片生成失败".to_string(),
|
||||
failure_stage: "upstream_status",
|
||||
status_code: Some(504),
|
||||
status_class: Some("5xx"),
|
||||
timeout: true,
|
||||
retryable: true,
|
||||
error_message: "上游超时".to_string(),
|
||||
error_source: Some("read timeout".to_string()),
|
||||
raw_excerpt: Some("{\"error\":\"timeout\"}".to_string()),
|
||||
latency_ms: Some(987),
|
||||
prompt_chars: Some(64),
|
||||
reference_image_count: Some(2),
|
||||
image_model: Some(GPT_IMAGE_2_MODEL),
|
||||
};
|
||||
|
||||
let request_error = PlatformImageError::Request {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
message: "请求发送失败".to_string(),
|
||||
endpoint: Some("https://vector.example/v1/images/generations".to_string()),
|
||||
timeout: true,
|
||||
connect: false,
|
||||
request: true,
|
||||
body: false,
|
||||
status_code: None,
|
||||
source: None,
|
||||
audit: None,
|
||||
};
|
||||
let invalid_config = PlatformImageError::InvalidConfig {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
message: "缺少配置".to_string(),
|
||||
};
|
||||
let invalid_request = PlatformImageError::InvalidRequest {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
message: "请求不合法".to_string(),
|
||||
};
|
||||
let upstream_timeout = PlatformImageError::Upstream {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
message: "upstream timeout".to_string(),
|
||||
upstream_status: 502,
|
||||
raw_excerpt: "deadline has elapsed".to_string(),
|
||||
audit: Some(audit.clone()),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
invalid_config.status_hint(),
|
||||
PlatformImageStatusHint::ServiceUnavailable
|
||||
);
|
||||
assert_eq!(
|
||||
invalid_request.status_hint(),
|
||||
PlatformImageStatusHint::BadRequest
|
||||
);
|
||||
assert_eq!(
|
||||
request_error.status_hint(),
|
||||
PlatformImageStatusHint::GatewayTimeout
|
||||
);
|
||||
assert_eq!(
|
||||
upstream_timeout.status_hint(),
|
||||
PlatformImageStatusHint::GatewayTimeout
|
||||
);
|
||||
assert_eq!(
|
||||
PlatformImageError::MissingImage {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
message: "缺图".to_string(),
|
||||
audit: Some(audit.clone()),
|
||||
}
|
||||
.status_hint(),
|
||||
PlatformImageStatusHint::BadGateway
|
||||
);
|
||||
|
||||
let audit_ref = upstream_timeout.audit().expect("audit should be preserved");
|
||||
assert_eq!(audit_ref.provider, VECTOR_ENGINE_PROVIDER);
|
||||
assert_eq!(
|
||||
audit_ref.endpoint,
|
||||
"https://vector.example/v1/images/generations"
|
||||
);
|
||||
assert_eq!(audit_ref.status_code, Some(504));
|
||||
assert_eq!(audit_ref.status_class, Some("5xx"));
|
||||
assert!(audit_ref.timeout);
|
||||
assert!(audit_ref.retryable);
|
||||
assert_eq!(audit_ref.reference_image_count, Some(2));
|
||||
assert_eq!(audit_ref.image_model, Some(GPT_IMAGE_2_MODEL));
|
||||
assert!(invalid_config.audit().is_none());
|
||||
assert!(invalid_request.audit().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_image_urls_and_b64_values_are_deduped() {
|
||||
let payload = json!({
|
||||
"data": [
|
||||
{"image": "https://example.com/a.png"},
|
||||
{"url": "https://example.com/a.png"},
|
||||
{"image_url": "ftp://example.com/b.png"},
|
||||
{"url": "https://example.com/b.png"}
|
||||
],
|
||||
"nested": {
|
||||
"b64_json": ["YWJj", "ZGVm"],
|
||||
"parts": [
|
||||
{
|
||||
"inlineData": {
|
||||
"mimeType": "image/png",
|
||||
"data": "aW1hZ2UtMQ=="
|
||||
}
|
||||
},
|
||||
{
|
||||
"inline_data": {
|
||||
"mime_type": "image/jpeg",
|
||||
"data": "aW1hZ2UtMg=="
|
||||
}
|
||||
},
|
||||
{
|
||||
"inlineData": {
|
||||
"mimeType": "text/plain",
|
||||
"data": "bm90LWltYWdl"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
extract_image_urls(&payload),
|
||||
vec![
|
||||
"https://example.com/a.png".to_string(),
|
||||
"https://example.com/b.png".to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
extract_b64_images(&payload),
|
||||
vec![
|
||||
"YWJj".to_string(),
|
||||
"ZGVm".to_string(),
|
||||
"aW1hZ2UtMQ==".to_string(),
|
||||
"aW1hZ2UtMg==".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
assert_eq!(body["model"], GPT_IMAGE_2_5_GENERATION_MODEL);
|
||||
assert_eq!(body["size"], "1024x1024");
|
||||
assert_eq!(body["n"], 4);
|
||||
assert_eq!(body["prompt"], "风雨夜里的街道\n避免:低清,水印");
|
||||
assert!(body.get("image").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_urls_normalize_root_and_v1_base_urls() {
|
||||
let root_settings = ImageProviderSettings {
|
||||
provider: ImageProvider::VectorEngine,
|
||||
base_url: "https://vector.example".to_string(),
|
||||
api_key: "test-key".to_string(),
|
||||
request_timeout_ms: 1_000,
|
||||
request_deadline: None,
|
||||
};
|
||||
let v1_settings = ImageProviderSettings {
|
||||
provider: ImageProvider::VectorEngine,
|
||||
base_url: "https://vector.example/v1".to_string(),
|
||||
api_key: "test-key".to_string(),
|
||||
request_timeout_ms: 1_000,
|
||||
request_deadline: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
images_generation_url(&root_settings),
|
||||
"https://vector.example/v1/images/generations"
|
||||
);
|
||||
assert_eq!(
|
||||
images_generation_url(&v1_settings),
|
||||
"https://vector.example/v1/images/generations"
|
||||
);
|
||||
assert_eq!(
|
||||
images_edit_url(&root_settings),
|
||||
"https://vector.example/v1/images/edits"
|
||||
);
|
||||
assert_eq!(
|
||||
images_edit_url(&v1_settings),
|
||||
"https://vector.example/v1/images/edits"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_url_and_base64_image_decoding_preserves_image_metadata() {
|
||||
let data_url = format!(
|
||||
"data:image/png;base64,{}",
|
||||
BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest")
|
||||
);
|
||||
|
||||
let reference = parse_reference_image_data_url(&data_url, 2, ImageProvider::VectorEngine)
|
||||
.expect("data url should parse")
|
||||
.expect("image data url should be accepted");
|
||||
assert_eq!(reference.file_name, "reference-2.png");
|
||||
assert_eq!(reference.mime_type, "image/png");
|
||||
assert_eq!(reference.bytes, b"\x89PNG\r\n\x1A\nrest");
|
||||
|
||||
let image =
|
||||
decode_generated_image_base64(BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest").as_str())
|
||||
.expect("base64 image should decode");
|
||||
assert_eq!(image.extension, "png");
|
||||
assert_eq!(image.mime_type, "image/png");
|
||||
assert_eq!(image.bytes, b"\x89PNG\r\n\x1A\nrest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_status_hints_and_audit_fields_are_structured() {
|
||||
let audit = PlatformImageFailureAudit {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
endpoint: "https://vector.example/v1/images/generations".to_string(),
|
||||
operation: "图片生成失败".to_string(),
|
||||
failure_stage: "upstream_status",
|
||||
status_code: Some(504),
|
||||
status_class: Some("5xx"),
|
||||
timeout: true,
|
||||
retryable: true,
|
||||
error_message: "上游超时".to_string(),
|
||||
error_source: Some("read timeout".to_string()),
|
||||
raw_excerpt: Some("{\"error\":\"timeout\"}".to_string()),
|
||||
latency_ms: Some(987),
|
||||
prompt_chars: Some(64),
|
||||
reference_image_count: Some(2),
|
||||
image_model: Some(GPT_IMAGE_2_MODEL),
|
||||
};
|
||||
|
||||
let request_error = PlatformImageError::Request {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
message: "请求发送失败".to_string(),
|
||||
endpoint: Some("https://vector.example/v1/images/generations".to_string()),
|
||||
timeout: true,
|
||||
connect: false,
|
||||
request: true,
|
||||
body: false,
|
||||
status_code: None,
|
||||
source: None,
|
||||
audit: None,
|
||||
};
|
||||
let invalid_config = PlatformImageError::InvalidConfig {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
message: "缺少配置".to_string(),
|
||||
};
|
||||
let invalid_request = PlatformImageError::InvalidRequest {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
message: "请求不合法".to_string(),
|
||||
};
|
||||
let upstream_timeout = PlatformImageError::Upstream {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
message: "upstream timeout".to_string(),
|
||||
upstream_status: 502,
|
||||
raw_excerpt: "deadline has elapsed".to_string(),
|
||||
audit: Some(audit.clone()),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
invalid_config.status_hint(),
|
||||
PlatformImageStatusHint::ServiceUnavailable
|
||||
);
|
||||
assert_eq!(
|
||||
invalid_request.status_hint(),
|
||||
PlatformImageStatusHint::BadRequest
|
||||
);
|
||||
assert_eq!(
|
||||
request_error.status_hint(),
|
||||
PlatformImageStatusHint::GatewayTimeout
|
||||
);
|
||||
assert_eq!(
|
||||
upstream_timeout.status_hint(),
|
||||
PlatformImageStatusHint::GatewayTimeout
|
||||
);
|
||||
assert_eq!(
|
||||
PlatformImageError::MissingImage {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
message: "缺图".to_string(),
|
||||
audit: Some(audit.clone()),
|
||||
}
|
||||
.status_hint(),
|
||||
PlatformImageStatusHint::BadGateway
|
||||
);
|
||||
|
||||
let audit_ref = upstream_timeout.audit().expect("audit should be preserved");
|
||||
assert_eq!(audit_ref.provider, VECTOR_ENGINE_PROVIDER);
|
||||
assert_eq!(
|
||||
audit_ref.endpoint,
|
||||
"https://vector.example/v1/images/generations"
|
||||
);
|
||||
assert_eq!(audit_ref.status_code, Some(504));
|
||||
assert_eq!(audit_ref.status_class, Some("5xx"));
|
||||
assert!(audit_ref.timeout);
|
||||
assert!(audit_ref.retryable);
|
||||
assert_eq!(audit_ref.reference_image_count, Some(2));
|
||||
assert_eq!(audit_ref.image_model, Some(GPT_IMAGE_2_MODEL));
|
||||
assert!(invalid_config.audit().is_none());
|
||||
assert!(invalid_request.audit().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_image_urls_and_b64_values_are_deduped() {
|
||||
let payload = json!({
|
||||
"data": [
|
||||
{"image": "https://example.com/a.png"},
|
||||
{"url": "https://example.com/a.png"},
|
||||
{"image_url": "ftp://example.com/b.png"},
|
||||
{"url": "https://example.com/b.png"}
|
||||
],
|
||||
"nested": {
|
||||
"b64_json": ["YWJj", "ZGVm"],
|
||||
"parts": [
|
||||
{
|
||||
"inlineData": {
|
||||
"mimeType": "image/png",
|
||||
"data": "aW1hZ2UtMQ=="
|
||||
}
|
||||
},
|
||||
{
|
||||
"inline_data": {
|
||||
"mime_type": "image/jpeg",
|
||||
"data": "aW1hZ2UtMg=="
|
||||
}
|
||||
},
|
||||
{
|
||||
"inlineData": {
|
||||
"mimeType": "text/plain",
|
||||
"data": "bm90LWltYWdl"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
extract_image_urls(&payload),
|
||||
vec![
|
||||
"https://example.com/a.png".to_string(),
|
||||
"https://example.com/b.png".to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
extract_b64_images(&payload),
|
||||
vec![
|
||||
"YWJj".to_string(),
|
||||
"ZGVm".to_string(),
|
||||
"aW1hZ2UtMQ==".to_string(),
|
||||
"aW1hZ2UtMg==".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user