补 3D 生成选项校验的表驱动单测
- 覆盖最简选项全模型放行、fast 必须配 v3.5 贴图版本 - 覆盖 generate_parts 的 texture/pbr/quad/smart_low_poly 互斥组合 - 覆盖 quad 家族限制与各模式 face_limit 的 min-1/min/max/max+1 边界 - 覆盖空白 task id 按 OutputSchema 失败与 trim 行为
This commit is contained in:
@@ -311,3 +311,249 @@ pub(crate) fn submitted_task_handle(task_id: String) -> Result<TripoTaskHandle,
|
||||
}
|
||||
Ok(TripoTaskHandle { task_id })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 只给必填项的选项:不应触发任何组合规则,任何模型版本都必须放行。
|
||||
fn minimal_options(model: Model3dModelVersion) -> TripoGenerationOptions {
|
||||
TripoGenerationOptions {
|
||||
model,
|
||||
texture: None,
|
||||
pbr: None,
|
||||
texture_quality: None,
|
||||
texture_version: None,
|
||||
geometry_quality: None,
|
||||
face_limit: None,
|
||||
auto_size: None,
|
||||
quad: None,
|
||||
smart_low_poly: None,
|
||||
generate_parts: None,
|
||||
compress: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 取出「被拒字段 + 拒绝原因」,用于断言错误确实来自预期的规则。
|
||||
fn rejection(error: TripoError) -> Option<(Option<TripoField>, TripoValidationReason)> {
|
||||
match error {
|
||||
TripoError::InvalidParameters { field, reason, .. } => Some((field, reason)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimal_options_pass_for_every_model_version() {
|
||||
for model in Model3dModelVersion::ALL {
|
||||
let options = minimal_options(*model);
|
||||
if let Err(error) = validate_generation_options(&options) {
|
||||
panic!("{model:?} 的最简选项应放行,实际被拒:{error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `texture_quality=fast` 必须显式配 `texture_version=v3.5-20260815`。
|
||||
#[test]
|
||||
fn fast_texture_quality_requires_v35_texture_version() {
|
||||
for texture_version in [
|
||||
None,
|
||||
Some(Model3dTextureVersion::V30),
|
||||
Some(Model3dTextureVersion::V25),
|
||||
] {
|
||||
let mut options = minimal_options(Model3dModelVersion::H31);
|
||||
options.texture = Some(true);
|
||||
options.texture_quality = Some(Model3dTextureQuality::Fast);
|
||||
options.texture_version = texture_version;
|
||||
|
||||
let error =
|
||||
validate_generation_options(&options).expect_err("fast 未配 v3.5 贴图版本应被拒");
|
||||
assert_eq!(
|
||||
rejection(error),
|
||||
Some((
|
||||
Some(TripoField::TextureQuality),
|
||||
TripoValidationReason::InvalidCombination
|
||||
)),
|
||||
"texture_version={texture_version:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let mut options = minimal_options(Model3dModelVersion::H31);
|
||||
options.texture = Some(true);
|
||||
options.texture_quality = Some(Model3dTextureQuality::Fast);
|
||||
options.texture_version = Some(Model3dTextureVersion::V35);
|
||||
validate_generation_options(&options).expect("fast + v3.5 应放行");
|
||||
}
|
||||
|
||||
/// `generate_parts=true` 只在「白模 + 不叠几何模式」下成立。
|
||||
#[test]
|
||||
fn generate_parts_requires_plain_geometry_options() {
|
||||
let cases = [
|
||||
(
|
||||
"白模且无几何模式",
|
||||
Some(false),
|
||||
Some(false),
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
("未显式 texture=false", None, Some(false), None, None, false),
|
||||
("texture=true", Some(true), Some(false), None, None, false),
|
||||
("未显式 pbr=false", Some(false), None, None, None, false),
|
||||
("pbr=true", Some(false), Some(true), None, None, false),
|
||||
("叠 quad", Some(false), Some(false), Some(true), None, false),
|
||||
(
|
||||
"叠 smart_low_poly",
|
||||
Some(false),
|
||||
Some(false),
|
||||
None,
|
||||
Some(true),
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
for (label, texture, pbr, quad, smart_low_poly, should_pass) in cases {
|
||||
let mut options = minimal_options(Model3dModelVersion::H31);
|
||||
options.texture = texture;
|
||||
options.pbr = pbr;
|
||||
options.quad = quad;
|
||||
options.smart_low_poly = smart_low_poly;
|
||||
options.generate_parts = Some(true);
|
||||
|
||||
let result = validate_generation_options(&options);
|
||||
assert_eq!(
|
||||
result.is_ok(),
|
||||
should_pass,
|
||||
"{label} 的期望结果不符:{result:?}"
|
||||
);
|
||||
if let Err(error) = result {
|
||||
assert_eq!(
|
||||
rejection(error),
|
||||
Some((
|
||||
Some(TripoField::GenerateParts),
|
||||
TripoValidationReason::InvalidCombination
|
||||
)),
|
||||
"{label}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `quad=true` 只对 v3.x 家族与 P2 放行,其余家族按 Quad 字段拒绝。
|
||||
#[test]
|
||||
fn quad_is_limited_to_v3_families_and_p2() {
|
||||
for model in Model3dModelVersion::ALL {
|
||||
let allowed = matches!(
|
||||
model,
|
||||
Model3dModelVersion::H31 | Model3dModelVersion::H30 | Model3dModelVersion::P2
|
||||
);
|
||||
let mut options = minimal_options(*model);
|
||||
options.quad = Some(true);
|
||||
|
||||
match validate_generation_options(&options) {
|
||||
Ok(()) => assert!(allowed, "{model:?} 不应允许 quad=true"),
|
||||
Err(error) => {
|
||||
assert!(!allowed, "{model:?} 应允许 quad=true,实际被拒:{error}");
|
||||
assert_eq!(
|
||||
rejection(error),
|
||||
Some((
|
||||
Some(TripoField::Quad),
|
||||
TripoValidationReason::InvalidCombination
|
||||
)),
|
||||
"{model:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `face_limit` 边界:min-1 拒绝、min / max 放行、max+1 拒绝。
|
||||
#[test]
|
||||
fn face_limit_uses_the_bounds_of_the_selected_mode() {
|
||||
let cases = [
|
||||
(
|
||||
"H31 默认",
|
||||
Model3dModelVersion::H31,
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
1_500_000,
|
||||
),
|
||||
("H25 默认", Model3dModelVersion::H25, None, None, 1, 500_000),
|
||||
("P1 默认", Model3dModelVersion::P1, None, None, 50, 20_000),
|
||||
(
|
||||
"P2 quad",
|
||||
Model3dModelVersion::P2,
|
||||
Some(true),
|
||||
None,
|
||||
48,
|
||||
25_000,
|
||||
),
|
||||
(
|
||||
"H31 智能低模",
|
||||
Model3dModelVersion::H31,
|
||||
None,
|
||||
Some(true),
|
||||
500,
|
||||
20_000,
|
||||
),
|
||||
(
|
||||
"H31 智能低模 + quad",
|
||||
Model3dModelVersion::H31,
|
||||
Some(true),
|
||||
Some(true),
|
||||
500,
|
||||
10_000,
|
||||
),
|
||||
];
|
||||
|
||||
for (label, model, quad, smart_low_poly, minimum, maximum) in cases {
|
||||
for (face_limit, should_pass) in [
|
||||
(minimum - 1, false),
|
||||
(minimum, true),
|
||||
(maximum, true),
|
||||
(maximum + 1, false),
|
||||
] {
|
||||
let mut options = minimal_options(model);
|
||||
options.quad = quad;
|
||||
options.smart_low_poly = smart_low_poly;
|
||||
options.face_limit = Some(face_limit);
|
||||
|
||||
let result = validate_generation_options(&options);
|
||||
assert_eq!(
|
||||
result.is_ok(),
|
||||
should_pass,
|
||||
"{label} face_limit={face_limit} 期望通过={should_pass},实际 {result:?}"
|
||||
);
|
||||
if let Err(error) = result {
|
||||
assert_eq!(
|
||||
rejection(error),
|
||||
Some((
|
||||
Some(TripoField::FaceLimit),
|
||||
TripoValidationReason::OutOfRange
|
||||
)),
|
||||
"{label} face_limit={face_limit}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 空白 task id 是 provider 违约,按 OutputSchema 失败而不是参数错误。
|
||||
#[test]
|
||||
fn blank_submitted_task_id_is_an_output_schema_error() {
|
||||
for blank in ["", " ", "\t\n"] {
|
||||
let error = submitted_task_handle(blank.to_string()).expect_err("空白 task id 应被拒");
|
||||
assert!(
|
||||
matches!(error, TripoError::OutputSchema { .. }),
|
||||
"空白 task id 应按 OutputSchema 失败,实际:{error}"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
submitted_task_handle(" task-1 ".to_string())
|
||||
.expect("非空 task id 应放行")
|
||||
.task_id,
|
||||
"task-1"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user