use std::collections::HashMap;
use kcmc::ModelingCmd;
use kcmc::each_cmd as mcmd;
use kcmc::ok_response::OkModelingCmdResponse;
use kcmc::shared::BodyType;
use kcmc::websocket::OkWebSocketResponseData;
use kittycad_modeling_cmds::{self as kcmc};
use super::extrude::do_post_extrude;
use crate::errors::KclError;
use crate::errors::KclErrorDetails;
use crate::execution::ExecState;
use crate::execution::ExtrudeSurface;
use crate::execution::GeometryWithImportedGeometry;
use crate::execution::KclValue;
use crate::execution::ModelingCmdMeta;
use crate::execution::Sketch;
use crate::execution::Solid;
use crate::execution::types::ArrayLen;
use crate::execution::types::PrimitiveType;
use crate::execution::types::RuntimeType;
use crate::parsing::ast::types::TagNode;
use crate::std::Args;
use crate::std::extrude::BeingExtruded;
use crate::std::extrude::NamedCapTags;
type Result<T> = std::result::Result<T, KclError>;
pub async fn clone(exec_state: &mut ExecState, args: Args) -> Result<KclValue> {
let geometries = args.get_unlabeled_kw_arg(
"geometry",
&RuntimeType::Array(
Box::new(RuntimeType::Union(vec![
RuntimeType::Primitive(PrimitiveType::Sketch),
RuntimeType::Primitive(PrimitiveType::Solid),
RuntimeType::imported(),
])),
ArrayLen::Minimum(1),
),
exec_state,
)?;
let cloned = inner_clone(geometries, exec_state, args).await?;
Ok(cloned.into())
}
async fn inner_clone(
geometries: Vec<GeometryWithImportedGeometry>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<GeometryWithImportedGeometry>> {
let mut res = vec![];
for g in geometries {
let new_id = exec_state.next_uuid();
let mut geometry = g.clone();
let old_id = geometry.id(&args.ctx).await?;
let mut new_geometry = match &geometry {
GeometryWithImportedGeometry::ImportedGeometry(imported) => {
let mut new_imported = imported.clone();
new_imported.id = new_id;
GeometryWithImportedGeometry::ImportedGeometry(new_imported)
}
GeometryWithImportedGeometry::Sketch(sketch) => {
let mut new_sketch = sketch.clone();
new_sketch.id = new_id;
new_sketch.original_id = new_id;
new_sketch.artifact_id = new_id.into();
GeometryWithImportedGeometry::Sketch(new_sketch)
}
GeometryWithImportedGeometry::Solid(solid) => {
exec_state
.flush_batch_for_solids(
ModelingCmdMeta::from_args(exec_state, &args),
std::slice::from_ref(solid),
)
.await?;
let mut new_solid = solid.clone();
new_solid.id = new_id;
if let Some(sketch) = new_solid.sketch_mut() {
sketch.original_id = new_id;
}
new_solid.artifact_id = new_id.into();
GeometryWithImportedGeometry::Solid(new_solid)
}
};
if args.ctx.no_engine_commands().await {
res.push(new_geometry);
} else {
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::from_args_id(exec_state, &args, new_id),
ModelingCmd::from(mcmd::EntityClone::builder().entity_id(old_id).build()),
)
.await?;
fix_tags_and_references(&mut new_geometry, old_id, exec_state, &args)
.await
.map_err(|e| {
KclError::new_internal(KclErrorDetails::new(
format!("failed to fix tags and references: {e:?}"),
vec![args.source_range],
))
})?;
res.push(new_geometry)
}
}
Ok(res)
}
async fn fix_tags_and_references(
new_geometry: &mut GeometryWithImportedGeometry,
old_geometry_id: uuid::Uuid,
exec_state: &mut ExecState,
args: &Args,
) -> Result<()> {
let new_geometry_id = new_geometry.id(&args.ctx).await?;
let entity_id_map = get_old_new_child_map(new_geometry_id, old_geometry_id, exec_state, args).await?;
match new_geometry {
GeometryWithImportedGeometry::ImportedGeometry(_) => {}
GeometryWithImportedGeometry::Sketch(sketch) => {
sketch.clone = Some(old_geometry_id);
fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, args, None).await?;
}
GeometryWithImportedGeometry::Solid(solid) => {
let (start_tag, end_tag) = get_named_cap_tags(solid);
let solid_value = solid.value.clone();
let sketch = solid.sketch_mut().ok_or_else(|| {
KclError::new_type(KclErrorDetails::new(
"Cloning solids created without a sketch is not yet supported.".to_owned(),
vec![args.source_range],
))
})?;
sketch.id = new_geometry_id;
sketch.original_id = new_geometry_id;
sketch.artifact_id = new_geometry_id.into();
sketch.clone = Some(old_geometry_id);
fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, args, Some(solid_value)).await?;
let sketch_for_post = sketch.clone();
for edge_cut in solid.edge_cuts.iter_mut() {
if let Some(id) = entity_id_map.get(&edge_cut.id()) {
edge_cut.set_id(*id);
} else {
crate::log::logln!(
"Failed to find new edge cut id for old edge cut id: {:?}",
edge_cut.id()
);
}
if let Some(new_edge_id) = entity_id_map.get(&edge_cut.edge_id()) {
edge_cut.set_edge_id(*new_edge_id);
} else {
crate::log::logln!("Failed to find new edge id for old edge id: {:?}", edge_cut.edge_id());
}
}
let new_solid = do_post_extrude(
&sketch_for_post,
new_geometry_id.into(),
solid.sectional,
&NamedCapTags {
start: start_tag.as_ref(),
end: end_tag.as_ref(),
},
kittycad_modeling_cmds::shared::ExtrudeMethod::New,
exec_state,
args,
None,
Some(&entity_id_map.clone()),
BodyType::Solid, BeingExtruded::Sketch,
)
.await?;
*solid = new_solid;
}
}
Ok(())
}
async fn get_old_new_child_map(
new_geometry_id: uuid::Uuid,
old_geometry_id: uuid::Uuid,
exec_state: &mut ExecState,
args: &Args,
) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
let response = exec_state
.send_modeling_cmd(
ModelingCmdMeta::from_args(exec_state, args),
ModelingCmd::from(
mcmd::EntityGetAllChildUuids::builder()
.entity_id(old_geometry_id)
.build(),
),
)
.await?;
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::EntityGetAllChildUuids(old_resp),
} = response
else {
return Err(KclError::new_engine(KclErrorDetails::new(
format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
vec![args.source_range],
)));
};
let old_entity_ids = old_resp.entity_ids;
let response = exec_state
.send_modeling_cmd(
ModelingCmdMeta::from_args(exec_state, args),
ModelingCmd::from(
mcmd::EntityGetAllChildUuids::builder()
.entity_id(new_geometry_id)
.build(),
),
)
.await?;
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::EntityGetAllChildUuids(new_resp),
} = response
else {
return Err(KclError::new_engine(KclErrorDetails::new(
format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
vec![args.source_range],
)));
};
let new_entity_ids = new_resp.entity_ids;
Ok(HashMap::from_iter(
old_entity_ids
.iter()
.zip(new_entity_ids.iter())
.map(|(old_id, new_id)| (*old_id, *new_id)),
))
}
async fn fix_sketch_tags_and_references(
new_sketch: &mut Sketch,
entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
exec_state: &mut ExecState,
args: &Args,
surfaces: Option<Vec<ExtrudeSurface>>,
) -> Result<()> {
for path in new_sketch.paths.as_mut_slice() {
if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
path.set_id(*new_path_id);
} else {
crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
}
}
let mut surface_id_map: HashMap<String, &ExtrudeSurface> = HashMap::new();
let surfaces = surfaces.unwrap_or_default();
for surface in surfaces.iter() {
if let Some(tag) = surface.get_tag() {
surface_id_map.insert(tag.name.clone(), surface);
}
}
for path in new_sketch.paths.clone() {
if let Some(tag) = path.get_tag() {
let mut surface = None;
if let Some(found_surface) = surface_id_map.get(&tag.name) {
let mut new_surface = (*found_surface).clone();
let Some(new_face_id) = entity_id_map.get(&new_surface.face_id()).copied() else {
return Err(KclError::new_engine(KclErrorDetails::new(
format!(
"Failed to find new face id for old face id: {:?}",
new_surface.face_id()
),
vec![args.source_range],
)));
};
new_surface.set_face_id(new_face_id);
surface = Some(new_surface);
}
new_sketch.add_tag(&tag, &path, exec_state, surface.as_ref());
}
}
if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
new_sketch.start.geo_meta.id = *new_base_path;
} else {
crate::log::logln!(
"Failed to find new base path id for old base path id: {:?}",
new_sketch.start.geo_meta.id
);
}
Ok(())
}
fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
let mut start_tag = None;
let mut end_tag = None;
if let Some(start_cap_id) = solid.start_cap_id {
for value in &solid.value {
if value.get_id() == start_cap_id {
start_tag = value.get_tag();
break;
}
}
}
if let Some(end_cap_id) = solid.end_cap_id {
for value in &solid.value {
if value.get_id() == end_cap_id {
end_tag = value.get_tag();
break;
}
}
}
(start_tag, end_tag)
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use pretty_assertions::assert_ne;
use crate::exec::KclValue;
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_clone_sketch() {
let code = r#"cube = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, 10])
|> line(end = [10, 0])
|> line(end = [0, -10])
|> close()
clonedCube = clone(cube)
"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
let result = ctx.run_with_caching(program.clone()).await.unwrap();
let cube = result.variables.get("cube").unwrap();
let cloned_cube = result.variables.get("clonedCube").unwrap();
assert_ne!(cube, cloned_cube);
let KclValue::Sketch { value: cube } = cube else {
panic!("Expected a sketch, got: {cube:?}");
};
let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
panic!("Expected a sketch, got: {cloned_cube:?}");
};
assert_ne!(cube.id, cloned_cube.id);
assert_ne!(cube.original_id, cloned_cube.original_id);
assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
assert_eq!(cloned_cube.original_id, cloned_cube.id);
for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
assert_ne!(path.get_id(), cloned_path.get_id());
assert_eq!(path.get_tag(), cloned_path.get_tag());
}
assert_eq!(cube.tags.len(), 0);
assert_eq!(cloned_cube.tags.len(), 0);
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_clone_solid() {
let code = r#"cube = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, 10])
|> line(end = [10, 0])
|> line(end = [0, -10])
|> close()
|> extrude(length = 5)
clonedCube = clone(cube)
"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
let result = ctx.run_with_caching(program.clone()).await.unwrap();
let cube = result.variables.get("cube").unwrap();
let cloned_cube = result.variables.get("clonedCube").unwrap();
assert_ne!(cube, cloned_cube);
let KclValue::Solid { value: cube } = cube else {
panic!("Expected a solid, got: {cube:?}");
};
let KclValue::Solid { value: cloned_cube } = cloned_cube else {
panic!("Expected a solid, got: {cloned_cube:?}");
};
let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
assert_ne!(cube.id, cloned_cube.id);
assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
assert_ne!(path.get_id(), cloned_path.get_id());
assert_eq!(path.get_tag(), cloned_path.get_tag());
}
for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
assert_ne!(value.get_id(), cloned_value.get_id());
assert_eq!(value.get_tag(), cloned_value.get_tag());
}
assert_eq!(cube_sketch.tags.len(), 0);
assert_eq!(cloned_cube_sketch.tags.len(), 0);
assert_eq!(cube.edge_cuts.len(), 0);
assert_eq!(cloned_cube.edge_cuts.len(), 0);
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_clone_sketch_with_tags() {
let code = r#"cube = startSketchOn(XY)
|> startProfile(at = [0,0]) // tag this one
|> line(end = [0, 10], tag = $tag02)
|> line(end = [10, 0], tag = $tag03)
|> line(end = [0, -10], tag = $tag04)
|> close(tag = $tag05)
clonedCube = clone(cube)
"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
let result = ctx.run_with_caching(program.clone()).await.unwrap();
let cube = result.variables.get("cube").unwrap();
let cloned_cube = result.variables.get("clonedCube").unwrap();
assert_ne!(cube, cloned_cube);
let KclValue::Sketch { value: cube } = cube else {
panic!("Expected a sketch, got: {cube:?}");
};
let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
panic!("Expected a sketch, got: {cloned_cube:?}");
};
assert_ne!(cube.id, cloned_cube.id);
assert_ne!(cube.original_id, cloned_cube.original_id);
for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
assert_ne!(path.get_id(), cloned_path.get_id());
assert_eq!(path.get_tag(), cloned_path.get_tag());
}
for (tag_name, tag) in &cube.tags {
let cloned_tag = cloned_cube.tags.get(tag_name).unwrap();
let tag_info = tag.get_cur_info().unwrap();
let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
assert_ne!(tag_info.id, cloned_tag_info.id);
assert_ne!(tag_info.geometry.id(), cloned_tag_info.geometry.id());
assert_ne!(tag_info.path, cloned_tag_info.path);
assert_eq!(tag_info.surface, None);
assert_eq!(cloned_tag_info.surface, None);
}
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_clone_solid_with_tags() {
let code = r#"cube = startSketchOn(XY)
|> startProfile(at = [0,0]) // tag this one
|> line(end = [0, 10], tag = $tag02)
|> line(end = [10, 0], tag = $tag03)
|> line(end = [0, -10], tag = $tag04)
|> close(tag = $tag05)
|> extrude(length = 5) // TODO: Tag these
clonedCube = clone(cube)
"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
let result = ctx.run_with_caching(program.clone()).await.unwrap();
let cube = result.variables.get("cube").unwrap();
let cloned_cube = result.variables.get("clonedCube").unwrap();
assert_ne!(cube, cloned_cube);
let KclValue::Solid { value: cube } = cube else {
panic!("Expected a solid, got: {cube:?}");
};
let KclValue::Solid { value: cloned_cube } = cloned_cube else {
panic!("Expected a solid, got: {cloned_cube:?}");
};
let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
assert_ne!(cube.id, cloned_cube.id);
assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
assert_ne!(path.get_id(), cloned_path.get_id());
assert_eq!(path.get_tag(), cloned_path.get_tag());
}
for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
assert_ne!(value.get_id(), cloned_value.get_id());
assert_eq!(value.get_tag(), cloned_value.get_tag());
}
for (tag_name, tag) in &cube_sketch.tags {
let cloned_tag = cloned_cube_sketch.tags.get(tag_name).unwrap();
let tag_info = tag.get_cur_info().unwrap();
let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
assert_ne!(tag_info.id, cloned_tag_info.id);
assert_ne!(tag_info.geometry.id(), cloned_tag_info.geometry.id());
assert_ne!(tag_info.path, cloned_tag_info.path);
assert_ne!(tag_info.surface, cloned_tag_info.surface);
}
assert_eq!(cube.edge_cuts.len(), 0);
assert_eq!(cloned_cube.edge_cuts.len(), 0);
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
async fn kcl_test_clone_cube_already_closed_sketch() {
let code = r#"// Clone a basic solid and move it.
exampleSketch = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(end = [10, 0])
|> line(end = [0, 10])
|> line(end = [-10, 0])
|> line(end = [0, -10])
|> close()
cube = extrude(exampleSketch, length = 5)
clonedCube = clone(cube)
|> translate(
x = 25.0,
)"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
let result = ctx.run_with_caching(program.clone()).await.unwrap();
let cube = result.variables.get("cube").unwrap();
let cloned_cube = result.variables.get("clonedCube").unwrap();
assert_ne!(cube, cloned_cube);
let KclValue::Solid { value: cube } = cube else {
panic!("Expected a solid, got: {cube:?}");
};
let KclValue::Solid { value: cloned_cube } = cloned_cube else {
panic!("Expected a solid, got: {cloned_cube:?}");
};
let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
assert_ne!(cube.id, cloned_cube.id);
assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
assert_ne!(path.get_id(), cloned_path.get_id());
assert_eq!(path.get_tag(), cloned_path.get_tag());
}
for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
assert_ne!(value.get_id(), cloned_value.get_id());
assert_eq!(value.get_tag(), cloned_value.get_tag());
}
for (tag_name, tag) in &cube_sketch.tags {
let cloned_tag = cloned_cube_sketch.tags.get(tag_name).unwrap();
let tag_info = tag.get_cur_info().unwrap();
let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
assert_ne!(tag_info.id, cloned_tag_info.id);
assert_ne!(tag_info.geometry.id(), cloned_tag_info.geometry.id());
assert_ne!(tag_info.path, cloned_tag_info.path);
assert_ne!(tag_info.surface, cloned_tag_info.surface);
}
for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
assert_ne!(edge_cut.id(), cloned_edge_cut.id());
assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
}
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
#[ignore] async fn kcl_test_clone_solid_with_edge_cuts() {
let code = r#"cube = startSketchOn(XY)
|> startProfile(at = [0,0]) // tag this one
|> line(end = [0, 10], tag = $tag02)
|> line(end = [10, 0], tag = $tag03)
|> line(end = [0, -10], tag = $tag04)
|> close(tag = $tag05)
|> extrude(length = 5) // TODO: Tag these
|> fillet(
radius = 2,
tags = [
getNextAdjacentEdge(tag02),
],
tag = $fillet01,
)
|> fillet(
radius = 2,
tags = [
getNextAdjacentEdge(tag04),
],
tag = $fillet02,
)
|> chamfer(
length = 2,
tags = [
getNextAdjacentEdge(tag03),
],
tag = $chamfer01,
)
|> chamfer(
length = 2,
tags = [
getNextAdjacentEdge(tag05),
],
tag = $chamfer02,
)
clonedCube = clone(cube)
"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
let result = ctx.run_with_caching(program.clone()).await.unwrap();
let cube = result.variables.get("cube").unwrap();
let cloned_cube = result.variables.get("clonedCube").unwrap();
assert_ne!(cube, cloned_cube);
let KclValue::Solid { value: cube } = cube else {
panic!("Expected a solid, got: {cube:?}");
};
let KclValue::Solid { value: cloned_cube } = cloned_cube else {
panic!("Expected a solid, got: {cloned_cube:?}");
};
let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
assert_ne!(cube.id, cloned_cube.id);
assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
assert_ne!(value.get_id(), cloned_value.get_id());
assert_eq!(value.get_tag(), cloned_value.get_tag());
}
for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
assert_ne!(edge_cut.id(), cloned_edge_cut.id());
assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
}
ctx.close().await;
}
}