补 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:
2026-09-22 16:50:06 +08:00
parent 62bf602fb1
commit ed2eb43fe6
@@ -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"
);
}
}