diff --git a/rust/kcl-lib/e2e/executor/main.rs b/rust/kcl-lib/e2e/executor/main.rs index 48b2a00da..3d332ab8e 100644 --- a/rust/kcl-lib/e2e/executor/main.rs +++ b/rust/kcl-lib/e2e/executor/main.rs @@ -1,7 +1,7 @@ mod cache; use kcl_lib::{ - test_server::{execute_and_snapshot, execute_and_snapshot_no_auth}, + test_server::{execute_and_export_step, execute_and_snapshot, execute_and_snapshot_no_auth}, ExecError, UnitLength, }; @@ -2081,3 +2081,17 @@ async fn kcl_test_ensure_nothing_left_in_batch_multi_file() { assert!(ctx.engine.batch().read().await.is_empty()); assert!(ctx.engine.batch_end().read().await.is_empty()); } + +#[tokio::test(flavor = "multi_thread")] +async fn kcl_test_exporting_step_file() { + // This tests export like how we do it in cli and kcl.py. + let code = kcl_input!("helix_defaults_negative_extrude"); + + let (_, _, files) = execute_and_export_step(code, UnitLength::Mm, None).await.unwrap(); + for file in files { + expectorate::assert_contents( + format!("e2e/executor/outputs/helix_defaults_negative_extrude_{}", file.name), + std::str::from_utf8(&file.contents).unwrap(), + ); + } +} diff --git a/rust/kcl-lib/e2e/executor/outputs/helix_defaults_negative_extrude_output.step b/rust/kcl-lib/e2e/executor/outputs/helix_defaults_negative_extrude_output.step new file mode 100644 index 000000000..9a3148321 --- /dev/null +++ b/rust/kcl-lib/e2e/executor/outputs/helix_defaults_negative_extrude_output.step @@ -0,0 +1,80 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION((('zoo.dev export')), '2;1'); +FILE_NAME('test.step', '2021-01-01T00:00:00Z', ('Test'), ('Zoo'), 'zoo.dev beta', 'zoo.dev', 'Test'); +FILE_SCHEMA(('AP203_CONFIGURATION_CONTROLLED_3D_DESIGN_OF_MECHANICAL_PARTS_AND_ASSEMBLIES_MIM_LF')); +ENDSEC; +DATA; +#1 = ( + LENGTH_UNIT() + NAMED_UNIT(*) + SI_UNIT($, .METRE.) +); +#2 = UNCERTAINTY_MEASURE_WITH_UNIT(0.00001, #1, 'DISTANCE_ACCURACY_VALUE', $); +#3 = ( + GEOMETRIC_REPRESENTATION_CONTEXT(3) + GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2)) + GLOBAL_UNIT_ASSIGNED_CONTEXT((#1)) + REPRESENTATION_CONTEXT('', '3D') +); +#4 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005)); +#5 = VERTEX_POINT('NONE', #4); +#6 = CARTESIAN_POINT('NONE', (0.015, 0, -0.005)); +#7 = VERTEX_POINT('NONE', #6); +#8 = DIRECTION('NONE', (1, 0, -0)); +#9 = DIRECTION('NONE', (0, 1, 0)); +#10 = CARTESIAN_POINT('NONE', (0.005, -0.01, -0.005)); +#11 = AXIS2_PLACEMENT_3D('NONE', #10, #9, #8); +#12 = CIRCLE('NONE', #11, 0.01); +#13 = DIRECTION('NONE', (0, 1, 0)); +#14 = VECTOR('NONE', #13, 1); +#15 = CARTESIAN_POINT('NONE', (0.015, -0.01, -0.005)); +#16 = LINE('NONE', #15, #14); +#17 = DIRECTION('NONE', (1, 0, -0)); +#18 = DIRECTION('NONE', (0, 1, 0)); +#19 = CARTESIAN_POINT('NONE', (0.005, 0, -0.005)); +#20 = AXIS2_PLACEMENT_3D('NONE', #19, #18, #17); +#21 = CIRCLE('NONE', #20, 0.01); +#22 = EDGE_CURVE('NONE', #5, #5, #12, .T.); +#23 = EDGE_CURVE('NONE', #5, #7, #16, .T.); +#24 = EDGE_CURVE('NONE', #7, #7, #21, .T.); +#25 = CARTESIAN_POINT('NONE', (0.005, -0.005, -0.005)); +#26 = DIRECTION('NONE', (0, 1, 0)); +#27 = DIRECTION('NONE', (1, 0, -0)); +#28 = AXIS2_PLACEMENT_3D('NONE', #25, #26, #27); +#29 = CYLINDRICAL_SURFACE('NONE', #28, 0.01); +#30 = CARTESIAN_POINT('NONE', (0, -0.01, -0)); +#31 = DIRECTION('NONE', (0, 1, 0)); +#32 = AXIS2_PLACEMENT_3D('NONE', #30, #31, $); +#33 = PLANE('NONE', #32); +#34 = CARTESIAN_POINT('NONE', (0, 0, -0)); +#35 = DIRECTION('NONE', (0, 1, 0)); +#36 = AXIS2_PLACEMENT_3D('NONE', #34, #35, $); +#37 = PLANE('NONE', #36); +#38 = ORIENTED_EDGE('NONE', *, *, #22, .T.); +#39 = ORIENTED_EDGE('NONE', *, *, #24, .F.); +#40 = EDGE_LOOP('NONE', (#38)); +#41 = FACE_BOUND('NONE', #40, .T.); +#42 = EDGE_LOOP('NONE', (#39)); +#43 = FACE_BOUND('NONE', #42, .T.); +#44 = ADVANCED_FACE('NONE', (#41, #43), #29, .T.); +#45 = ORIENTED_EDGE('NONE', *, *, #22, .F.); +#46 = EDGE_LOOP('NONE', (#45)); +#47 = FACE_BOUND('NONE', #46, .T.); +#48 = ADVANCED_FACE('NONE', (#47), #33, .F.); +#49 = ORIENTED_EDGE('NONE', *, *, #24, .T.); +#50 = EDGE_LOOP('NONE', (#49)); +#51 = FACE_BOUND('NONE', #50, .T.); +#52 = ADVANCED_FACE('NONE', (#51), #37, .T.); +#53 = CLOSED_SHELL('NONE', (#44, #48, #52)); +#54 = MANIFOLD_SOLID_BREP('NONE', #53); +#55 = APPLICATION_CONTEXT('configuration controlled 3D design of mechanical parts and assemblies'); +#56 = PRODUCT_DEFINITION_CONTEXT('part definition', #55, 'design'); +#57 = PRODUCT('UNIDENTIFIED_PRODUCT', 'NONE', $, ()); +#58 = PRODUCT_DEFINITION_FORMATION('', $, #57); +#59 = PRODUCT_DEFINITION('design', $, #58, #56); +#60 = PRODUCT_DEFINITION_SHAPE('NONE', $, #59); +#61 = ADVANCED_BREP_SHAPE_REPRESENTATION('NONE', (#54), #3); +#62 = SHAPE_DEFINITION_REPRESENTATION(#60, #61); +ENDSEC; +END-ISO-10303-21; diff --git a/rust/kcl-lib/src/errors.rs b/rust/kcl-lib/src/errors.rs index 226125a82..384accd1a 100644 --- a/rust/kcl-lib/src/errors.rs +++ b/rust/kcl-lib/src/errors.rs @@ -20,6 +20,8 @@ pub enum ExecError { Connection(#[from] ConnectionError), #[error("PNG snapshot could not be decoded: {0}")] BadPng(String), + #[error("Bad export: {0}")] + BadExport(String), } impl From for ExecError { diff --git a/rust/kcl-lib/src/test_server.rs b/rust/kcl-lib/src/test_server.rs index e1f4997fc..22996f6f4 100644 --- a/rust/kcl-lib/src/test_server.rs +++ b/rust/kcl-lib/src/test_server.rs @@ -129,3 +129,88 @@ pub async fn new_context( .map_err(ConnectionError::Establishing)?; Ok(ctx) } + +pub async fn execute_and_export_step( + code: &str, + units: UnitLength, + current_file: Option, +) -> Result< + ( + ExecState, + EnvironmentRef, + Vec, + ), + ExecErrorWithState, +> { + let ctx = new_context(units, true, current_file).await?; + let mut exec_state = ExecState::new(&ctx.settings); + let program = Program::parse_no_errs(code) + .map_err(|err| ExecErrorWithState::new(KclErrorWithOutputs::no_outputs(err).into(), exec_state.clone()))?; + let result = ctx + .run_with_ui_outputs(&program, &mut exec_state) + .await + .map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone()))?; + for e in exec_state.errors() { + if e.severity.is_err() { + return Err(ExecErrorWithState::new( + KclErrorWithOutputs::no_outputs(KclError::Semantic(e.clone().into())).into(), + exec_state.clone(), + )); + } + } + + let resp = ctx + .engine + .send_modeling_cmd( + uuid::Uuid::new_v4(), + crate::SourceRange::default(), + &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export { + entity_ids: vec![], + format: kittycad_modeling_cmds::format::OutputFormat3d::Step( + kittycad_modeling_cmds::format::step::export::Options { + coords: *kittycad_modeling_cmds::coord::KITTYCAD, + // We want all to have the same timestamp. + created: Some( + chrono::DateTime::parse_from_rfc3339("2021-01-01T00:00:00Z") + .unwrap() + .into(), + ), + }, + ), + }), + ) + .await + .map_err(|err| ExecErrorWithState::new(KclErrorWithOutputs::no_outputs(err).into(), exec_state.clone()))?; + + let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { mut files } = resp else { + return Err(ExecErrorWithState::new( + ExecError::BadExport(format!("Expected export response, got: {:?}", resp)), + exec_state.clone(), + )); + }; + + for kittycad_modeling_cmds::websocket::RawFile { contents, .. } in &mut files { + use std::fmt::Write; + let utf8 = std::str::from_utf8(contents).unwrap(); + let mut postprocessed = String::new(); + for line in utf8.lines() { + if line.starts_with("FILE_NAME") { + let name = "test.step"; + let time = "2021-01-01T00:00:00Z"; + let author = "Test"; + let org = "Zoo"; + let version = "zoo.dev beta"; + let system = "zoo.dev"; + let authorization = "Test"; + writeln!(&mut postprocessed, "FILE_NAME('{name}', '{time}', ('{author}'), ('{org}'), '{version}', '{system}', '{authorization}');").unwrap(); + } else { + writeln!(&mut postprocessed, "{line}").unwrap(); + } + } + *contents = postprocessed.into_bytes(); + } + + ctx.close().await; + + Ok((exec_state, result.0, files)) +}