#[cfg(feature = "rhino")]
use cadmpeg_test_support::wire;
#[cfg(any(feature = "iges", feature = "step"))]
use std::path::Path;
use std::path::PathBuf;
#[cfg(any(feature = "iges", feature = "rhino", feature = "step"))]
use cadmpeg_ir::codec::write::target::TargetRequest;
use cadmpeg_ir::codec::write::{EncodeInput, Encoder};
use cadmpeg_ir::CadIr;
use cadmpeg_registry::Format;
use crate::application::artifact_store::FileDestination;
use crate::application::document::LoadedDocument;
#[cfg(any(feature = "iges", feature = "rhino", feature = "step"))]
use crate::application::refusal::ConversionRefusal;
#[cfg(any(feature = "iges", feature = "rhino", feature = "step"))]
use super::export_target;
use super::{
DestinationPolicy, LossPolicy, PreparedConversion, ResolvedDestination, TargetSelection,
};
#[test]
fn loss_policy_assigns_each_refusal_phase() {
assert_eq!(
[
(LossPolicy::Allow, false, false),
(LossPolicy::RejectDecode, true, false),
(LossPolicy::RejectExport, false, true),
(LossPolicy::RejectAny, true, true),
],
[
(
LossPolicy::Allow,
LossPolicy::Allow.rejects_decode(),
LossPolicy::Allow.rejects_export(),
),
(
LossPolicy::RejectDecode,
LossPolicy::RejectDecode.rejects_decode(),
LossPolicy::RejectDecode.rejects_export(),
),
(
LossPolicy::RejectExport,
LossPolicy::RejectExport.rejects_decode(),
LossPolicy::RejectExport.rejects_export(),
),
(
LossPolicy::RejectAny,
LossPolicy::RejectAny.rejects_decode(),
LossPolicy::RejectAny.rejects_export(),
),
]
);
}
#[test]
fn convert_file_destination_discards_binary_stdout_at_clap_admission() {
use clap::Parser;
let command = crate::Cli::try_parse_from([
"cadmpeg",
"convert",
"input.step",
"-o",
"out.step",
"--binary-stdout",
])
.unwrap()
.command;
let crate::Command::Convert { destinations, .. } = command else {
panic!("expected convert command");
};
assert_eq!(
destinations.destination,
DestinationPolicy::File(FileDestination {
path: PathBuf::from("out.step"),
overwrite: false,
})
);
}
fn prepared(
ir: CadIr,
format: Format,
encoder: Box<dyn Encoder>,
loss_policy: LossPolicy,
) -> PreparedConversion {
let validation =
cadmpeg_ir::validate_neutral(&ir, Vec::new()).expect("resource allocation did not fail");
PreparedConversion {
document: LoadedDocument::neutral(ir, 0),
validation,
encoder,
selection: TargetSelection::new(format, None),
destination: ResolvedDestination::Stdout,
loss_policy,
}
}
#[cfg(feature = "iges")]
#[test]
fn encoder_planning_owns_unknown_explicit_target_admission() {
let mut conversion = prepared(
CadIr::empty(),
Format::Iges,
Box::new(cadmpeg_codec_iges::IgesCodec),
LossPolicy::Allow,
);
conversion.selection.request = Some("nonesuch".into());
let Err(error) = conversion.plan() else {
panic!("the encoder must reject an unknown target");
};
assert!(matches!(
error.refusal(),
Some(ConversionRefusal::UnsupportedTarget { .. })
));
}
#[cfg(feature = "step")]
fn step_ir_with_unrepresentable_native_content() -> CadIr {
let mut ir = CadIr::empty();
ir.native.namespace_mut("f3d").arenas_mut().insert(
"asm_histories".into(),
vec![cadmpeg_ir::NativeRecord::new(
cadmpeg_ir::ids::Identity::new("f3d:test:asm-history#0").expect("valid identity"),
serde_json::Map::default(),
)
.expect("valid native identity")],
);
ir.finalize(&cadmpeg_test_support::service_decode_context())
.expect("fixture ordering is admitted");
ir
}
#[cfg(feature = "step")]
#[test]
fn step_export_losses_are_rejected_by_the_shared_plan_gate() {
let conversion = prepared(
step_ir_with_unrepresentable_native_content(),
Format::Step,
Box::new(cadmpeg_codec_step::StepCodec::default()),
LossPolicy::RejectExport,
);
let Err(error) = conversion.plan() else {
panic!("the plan loss must refuse");
};
let refusal = error
.refusal()
.expect("the shared gate returns a typed refusal");
assert!(
matches!(refusal, ConversionRefusal::ExportLossRejected { .. }),
"{refusal:?}"
);
let message = refusal.evidence().message;
assert!(message.contains("source-native record(s)"), "{message}");
let report = refusal
.evidence()
.reports
.export
.expect("the refusal retains the completed export plan report");
assert!(
report
.losses
.iter()
.any(|loss| loss.message.contains("source-native record(s)")),
"{:?}",
report.losses
);
}
#[cfg(feature = "step")]
#[test]
fn step_export_losses_remain_on_the_plan_without_rejection() {
let conversion = prepared(
step_ir_with_unrepresentable_native_content(),
Format::Step,
Box::new(cadmpeg_codec_step::StepCodec::default()),
LossPolicy::Allow,
);
let planned = conversion.plan().expect("loss reporting is not refusal");
assert!(
planned
.plan
.report()
.losses
.iter()
.any(|loss| loss.message.contains("source-native record(s)")),
"{:?}",
planned.plan.report().losses
);
}
#[cfg(feature = "rhino")]
#[test]
fn rhino_omitted_product_structure_is_rejected_by_the_shared_plan_gate() {
let mut ir = CadIr::empty();
ir.model.points.push(cadmpeg_ir::topology::Point::new(
cadmpeg_ir::ids::PointId::mint("cadir:model:point#pin").expect("identity grammar"),
cadmpeg_ir::features::FinitePoint3::new(cadmpeg_ir::math::Point3::new(0.0, 0.0, 0.0))
.expect("finite position"),
None,
));
ir.model
.product_definitions
.push(cadmpeg_ir::products::ProductDefinition {
id: "cadir:model:product#group"
.try_into()
.expect("valid identity"),
kind: cadmpeg_ir::products::ProductDefinitionKind::Group,
source_name: Some("Group".into()),
label: None,
description: None,
part_number: None,
bom_properties: std::collections::BTreeMap::new(),
bodies: Vec::new(),
native_ref: None,
});
let conversion = prepared(
ir,
Format::Rhino,
Box::new(cadmpeg_codec_rhino::RhinoCodec),
LossPolicy::RejectExport,
);
let Err(error) = conversion.plan() else {
panic!("the omitted product definition must refuse");
};
let refusal = error
.refusal()
.expect("the shared gate returns a typed refusal");
let ConversionRefusal::ExportLossRejected { export_report, .. } = refusal else {
panic!("{refusal:?}");
};
assert!(
export_report
.losses
.iter()
.any(|loss| loss.message.contains("model arena `product_definitions`")),
"{:?}",
export_report.losses
);
}
struct NotImplementedEncoder;
impl cadmpeg_ir::codec::write::EncoderBackend for NotImplementedEncoder {
const FORMAT: cadmpeg_ir::codec::FormatId =
cadmpeg_ir::codec::FormatId::new("not-implemented-test");
type Target = cadmpeg_ir::codec::write::target::DialectFree;
const TARGET: Self::Target = cadmpeg_ir::codec::write::target::DialectFree;
fn plan_resolved(
&self,
_input: EncodeInput<'_>,
_target: (),
) -> Result<cadmpeg_ir::codec::write::ExportBody, cadmpeg_core::CodecError> {
Err(cadmpeg_core::CodecError::NotImplemented(
"writer path is not implemented".into(),
))
}
}
#[test]
fn not_implemented_plan_failure_is_not_reclassified_as_export_loss() {
let conversion = prepared(
CadIr::empty(),
Format::Cadir,
Box::new(NotImplementedEncoder),
LossPolicy::RejectExport,
);
let Err(error) = conversion.plan() else {
panic!("the synthetic writer is not implemented");
};
assert_eq!(
error.to_string(),
"not implemented yet: writer path is not implemented"
);
assert!(error.refusal().is_none());
}
#[cfg(feature = "iges")]
#[test]
fn flag_absence_is_always_an_inherit_request() {
assert_eq!(
TargetSelection::new(Format::Iges, None).request(),
TargetRequest::Inherit
);
let named = TargetSelection::new(Format::Iges, Some("iges:5.1-fixed-ascii".to_owned()));
assert_eq!(
named.request(),
TargetRequest::Explicit("iges:5.1-fixed-ascii")
);
}
#[cfg(feature = "iges")]
#[test]
fn target_selection_owns_format_and_dialect_grammar() {
let format_only = TargetSelection::resolve(Some("iges"), None).expect("format resolves");
assert_eq!(format_only.format, Format::Iges);
assert_eq!(format_only.request(), TargetRequest::Inherit);
let qualified = TargetSelection::resolve(Some("iges:5.1"), None).expect("alias resolves");
assert_eq!(qualified.format, Format::Iges);
assert_eq!(qualified.request(), TargetRequest::Explicit("5.1"));
let inferred = TargetSelection::resolve(Some("5.1"), Some(Path::new("part.iges")))
.expect("output extension supplies the format");
assert_eq!(inferred.format, Format::Iges);
assert_eq!(inferred.request(), TargetRequest::Explicit("5.1"));
}
#[cfg(feature = "step")]
#[test]
fn a_format_alias_does_not_become_a_dialect_request() {
let selection = TargetSelection::resolve(Some("stp"), Some(Path::new("part.stp")))
.expect("the identity registry owns the STEP alias");
assert_eq!(selection.format, Format::Step);
assert_eq!(selection.request(), TargetRequest::Inherit);
}
#[test]
fn target_selection_rejects_an_empty_qualified_dialect() {
let error = TargetSelection::resolve(Some("cadir:"), None).unwrap_err();
assert!(error.to_string().contains("nothing after the colon"));
assert!(error.refusal().is_none());
}
#[cfg(feature = "step")]
#[test]
fn rejecting_export_losses_does_not_name_a_target() {
let target = export_target(TargetSelection::new(Format::Step, None));
assert!(target.selection.request.is_none(), "{:?}", target.selection);
assert_eq!(target.selection.request(), TargetRequest::Inherit);
let named = export_target(TargetSelection::new(
Format::Step,
Some("step:ap242-e3".to_owned()),
));
assert_eq!(
named.selection.request(),
TargetRequest::Explicit("step:ap242-e3")
);
}
#[cfg(feature = "rhino")]
#[test]
fn an_alias_and_an_id_reach_the_encoder_unresolved_and_both_resolve() {
use cadmpeg_core::dialect::DialectId;
let ir = CadIr::empty();
for spelling in ["archive-60", "60"] {
let target = export_target(TargetSelection::new(
Format::Rhino,
Some(spelling.to_owned()),
));
assert_eq!(
target.selection.request(),
TargetRequest::Explicit(spelling)
);
let plan = target
.encoder
.plan(
EncodeInput::new(&ir, None),
TargetRequest::Explicit(spelling),
)
.expect("the catalog carries the row under both spellings");
assert_eq!(
wire::field_or_default::<Option<cadmpeg_core::dialect::DialectId>>(
plan.report(),
"identity/target"
)
.as_ref()
.map(DialectId::as_str),
Some("rhino:archive-60")
);
}
}
#[cfg(feature = "iges")]
#[test]
fn an_unknown_dialect_is_refused_by_plan_with_its_catalog() {
let target = export_target(TargetSelection::new(
Format::Iges,
Some("ap242e3".to_owned()),
));
let error = target
.encoder
.plan(
EncodeInput::new(&CadIr::empty(), None),
target.selection.request(),
)
.expect_err("a STEP alias is not an IGES target");
let message = error.to_string();
assert!(message.contains("ap242e3"), "{message}");
assert!(message.contains("iges:5.3-fixed-ascii"), "{message}");
}
#[test]
fn loss_policy_cli_preserves_omission_and_rejects_allow_spelling() {
use clap::Parser;
for (flag, expected) in [
(None, LossPolicy::Allow),
(Some("--reject-lossy"), LossPolicy::RejectAny),
(Some("--reject-lossy=decode"), LossPolicy::RejectDecode),
(Some("--reject-lossy=export"), LossPolicy::RejectExport),
(Some("--reject-lossy=any"), LossPolicy::RejectAny),
] {
let mut args = vec!["cadmpeg", "convert", "input.step"];
args.extend(flag);
let crate::Command::Convert { reject_lossy, .. } =
crate::Cli::try_parse_from(args).unwrap().command
else {
panic!("expected convert command");
};
assert_eq!(reject_lossy.unwrap_or_default(), expected);
}
let error =
crate::Cli::try_parse_from(["cadmpeg", "convert", "input.step", "--reject-lossy=allow"])
.unwrap_err();
assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue);
}